package jtrade.timeseries; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import jtrade.util.DoubleBuffer; import jtrade.util.Util; import org.joda.time.DateTime; import org.joda.time.LocalTime; import org.joda.time.Period; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; public class TimeSeriesArray implements TimeSeries { DateTime[] dates; double[] data; public TimeSeriesArray() { dates = new DateTime[0]; data = new double[0]; } public TimeSeriesArray(TimeSeries ts) { dates = ts.dates().clone(); data = ts.data().clone(); } public TimeSeriesArray(Map<DateTime, ? extends Number> map) { dates = new DateTime[map.size()]; data = new double[map.size()]; int i = 0; for (Map.Entry<DateTime, ? extends Number> e : map.entrySet()) { dates[i] = e.getKey(); data[i++] = e.getValue().doubleValue(); } } public TimeSeriesArray(Map<DateTime, ?> map, String property) { dates = new DateTime[map.size()]; data = new double[map.size()]; int i = 0; try { for (Map.Entry<DateTime, ?> e : map.entrySet()) { dates[i] = e.getKey(); data[i++] = Util.getDoubleProperty(e.getValue(), property); } } catch (Exception e) { throw new IllegalArgumentException("Invalid property " + property, e); } } public TimeSeriesArray(DateTime[] dates, double[] data) { if (dates.length != data.length) { throw new IllegalArgumentException("Dates and data lengths must match"); } this.dates = dates; this.data = data; } public TimeSeriesArray(DateTime start, Period period, double[] data) { DateTime[] dates = new DateTime[data.length]; dates[0] = start; for (int i = 1; i < dates.length; i++) { dates[i] = dates[i - 1].plus(period); } this.dates = dates; this.data = data; } public TimeSeriesArray(DateTime start, DateTime end, Period period, double value) { long periodMillis = period.toStandardSeconds().getSeconds() * 1000; int len = (int) Math.ceil((end.getMillis() - start.getMillis()) / periodMillis); DateTime[] dates = new DateTime[len]; double[] data = new double[len]; long s = start.getMillis(); for (int i = 0; i < len; i++) { dates[i] = new DateTime(s + i * periodMillis); data[i] = value; } this.dates = dates; this.data = data; } public TimeSeriesArray(DateTime start, Period period, int size, double value) { DateTime[] dates = new DateTime[size]; double[] data = new double[size]; dates[0] = start; data[0] = value; for (int i = 1; i < dates.length; i++) { dates[i] = dates[i - 1].plus(period); data[i] = value; } this.dates = dates; this.data = data; } @Override public DateTime[] dates() { return dates; } @Override public double[] data() { return data; } @Override public int size() { return dates.length; } @Override public long duration() { return size() == 0 ? 0 : end().getMillis() - start().getMillis(); } @Override public DateTime start() { return dates.length == 0 ? null : dates[0]; } @Override public DateTime end() { return dates.length == 0 ? null : dates[dates.length - 1]; } @Override public double first() { return data.length == 0 ? Double.NaN : data[0]; } @Override public double last() { return data.length == 0 ? Double.NaN : data[data.length - 1]; } private int nearestIndex(DateTime date) { int index = Arrays.binarySearch(dates, date); if (index < 0) { index = (-index) - 2; } return index; } @Override public double get(DateTime date) { int index = Arrays.binarySearch(dates, date); return index >= 0 ? data[index] : Double.NaN; } @Override public void set(DateTime date, double value) { int index = Arrays.binarySearch(dates, date); if (index < 0) { throw new IllegalArgumentException("Date '" + date + "' is not valid for this TimeSeries"); } data[index] = value; } @Override public TimeSeries copy() { return new TimeSeriesArray(dates.clone(), data.clone()); } @Override public TimeSeries truncate(int from, int to) { return new TimeSeriesArray(Arrays.copyOfRange(this.dates, from, to), Arrays.copyOfRange(this.data, from, to)); } @Override public TimeSeries truncate(DateTime from, DateTime to) { int fromIndex = from.isBefore(start()) ? 0 : nearestIndex(from); int toIndex = to.isAfter(end()) ? size() : nearestIndex(to) + 1; return new TimeSeriesArray(Arrays.copyOfRange(this.dates, fromIndex, toIndex), Arrays.copyOfRange(this.data, fromIndex, toIndex)); } @Override public TimeSeries last(int size) { return new TimeSeriesArray(Arrays.copyOfRange(this.dates, dates.length - size, dates.length), Arrays.copyOfRange(this.data, data.length - size, data.length)); } @Override public TimeSeries first(int size) { return new TimeSeriesArray(Arrays.copyOfRange(this.dates, 0, size), Arrays.copyOfRange(this.data, 0, size)); } @Override public TimeSeries shift(int periods) { double[] newData = data.clone(); if (periods > 0) { System.arraycopy(newData, 0, newData, periods, newData.length - periods); Arrays.fill(newData, 0, periods, Double.NaN); } else if (periods < 0) { System.arraycopy(newData, -periods, newData, 0, newData.length + periods); Arrays.fill(newData, newData.length + periods, newData.length, Double.NaN); } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries shift(Period period) { DateTime[] newDates = new DateTime[size()]; for (int i = 0; i < newDates.length; i++) { newDates[i] = dates[i].plus(period); } return new TimeSeriesArray(newDates, data.clone()); } @Override public TimeSeries union(TimeSeries ts) { Map<DateTime, Double> map = toMap(); Map<DateTime, Double> otherMap = ts.toMap(); map.putAll(otherMap); return new TimeSeriesArray(map); } @Override public TimeSeries intersect(TimeSeries ts) { Map<DateTime, Double> map = toMap(); Map<DateTime, Double> otherMap = ts.toMap(); map.keySet().retainAll(otherMap.keySet()); return new TimeSeriesArray(map); } @Override public TimeSeries[] split(Period period) { if (size() == 0) { return new TimeSeries[0]; } List<Integer> partitions = new ArrayList<Integer>(); DateTime d = start().plus(period); for (int i = 1; i < dates.length; i++) { if (d.isBefore(dates[i]) || d.equals(dates[i])) { partitions.add(i); d = dates[i].plus(period); } } TimeSeries[] result = new TimeSeriesArray[partitions.size() + 1]; for (int i = 0, j = 0; i < result.length; i++) { int p = i < partitions.size() ? partitions.get(i) : dates.length; DateTime[] newDates = Arrays.copyOfRange(dates, j, p); double[] newData = Arrays.copyOfRange(data, j, p); result[i] = new TimeSeriesArray(newDates, newData); j = p; } return result; } @Override public TimeSeries valid() { List<DateTime> validDates = new ArrayList<DateTime>(size()); DoubleBuffer validData = new DoubleBuffer(size()); for (int i = 0; i < data.length; i++) { double d = data[i]; if (d == d) { validDates.add(dates[i]); validData.add(d); } } return new TimeSeriesArray(validDates.toArray(new DateTime[validDates.size()]), validData.toArray()); } @Override public TimeSeries trim() { int from = 0; int to = data.length; for (int i = 0; i < data.length; i++) { double d = data[i]; if (d == d) { from = i; break; } } for (int i = data.length - 1; i >= from; i--) { double d = data[i]; if (d == d) { to = i + 1; break; } } return new TimeSeriesArray(Arrays.copyOfRange(this.dates, from, to), Arrays.copyOfRange(this.data, from, to)); } @Override public TimeSeries reindex(DateTime[] newDates, FillMethod method) { NavigableMap<DateTime, Double> map = toMap(); double[] newData = new double[newDates.length]; for (int i = 0; i < newDates.length; i++) { Double d = map.get(newDates[i]); newData[i] = d != null ? d : Double.NaN; } if (FillMethod.BACKFILL.equals(method)) { for (int i = 0; i < newDates.length; i++) { if (Double.isNaN(newData[i])) { DateTime date = newDates[i]; while ((date = map.higherKey(date)) != null) { Double d = map.get(date); if (!d.isNaN()) { newData[i] = d; break; } } } } } else if (FillMethod.FORWARDFILL.equals(method)) { for (int i = 0; i < newDates.length; i++) { if (Double.isNaN(newData[i])) { DateTime date = newDates[i]; while ((date = map.lowerKey(date)) != null) { Double d = map.get(date); if (!d.isNaN()) { newData[i] = d; break; } } } } } else if (FillMethod.FORWARDBACKFILL.equals(method)) { for (int i = 0; i < newDates.length; i++) { if (Double.isNaN(newData[i])) { DateTime date = newDates[i]; while ((date = map.lowerKey(date)) != null) { Double d = map.get(date); if (!d.isNaN()) { newData[i] = d; break; } } } } for (int i = 0; i < newDates.length; i++) { if (Double.isNaN(newData[i])) { DateTime date = newDates[i]; while ((date = map.higherKey(date)) != null) { Double d = map.get(date); if (!d.isNaN()) { newData[i] = d; break; } } } } } else if (FillMethod.INTERPOLATE.equals(method)) { DoubleBuffer datesBuf = new DoubleBuffer(size()); DoubleBuffer dataBuf = new DoubleBuffer(size()); for (TimeSeriesValuePair p : this) { if (!Double.isNaN(p.getValue())) { datesBuf.add(p.getDateTime().getMillis()); dataBuf.add(p.getValue()); } } double[] cleanedDates = datesBuf.toArray(); double[] cleanedData = dataBuf.toArray(); for (int i = 0; i < newDates.length; i++) { if (Double.isNaN(newData[i])) { double x = newDates[i].getMillis(); int i0 = Arrays.binarySearch(cleanedDates, x); if (i0 < 0) { i0 = -(i0 + 2); } if (i0 < 0) { i0 = 0; } int i1 = i0 + 1; if (i1 >= cleanedDates.length) { i0 = cleanedDates.length - 2; i1 = cleanedDates.length - 1; } newData[i] = cleanedData[i0] + (cleanedData[i1] - cleanedData[i0]) * ((x - cleanedDates[i0]) / (cleanedDates[i1] - cleanedDates[i0])); } } } else if (FillMethod.NAN.equals(method)) { // Leave NaNs as is } else { throw new IllegalArgumentException("Invalid method: " + method); } return new TimeSeriesArray(newDates, newData); } @Override public TimeSeries asFreq(FreqMethod method) { if (size() == 0) { return new TimeSeriesArray(); } DateTime[] newDates = null; List<DateTime> ds = new ArrayList<DateTime>(); DateTime d = dates[0]; if (FreqMethod.END_OF_DAY.equals(method)) { int lastDate = d.getDayOfYear(); for (int i = 1; i < dates.length; i++) { int currDate = dates[i].getDayOfYear(); if (lastDate != currDate) { ds.add(d); lastDate = currDate; } d = dates[i]; } } else if (FreqMethod.END_OF_MONTH.equals(method)) { int lastDate = d.getMonthOfYear(); for (int i = 1; i < dates.length; i++) { int currDate = dates[i].getMonthOfYear(); if (lastDate != currDate) { ds.add(d); lastDate = currDate; } d = dates[i]; } } else if (FreqMethod.END_OF_YEAR.equals(method)) { int lastDate = d.getYear(); for (int i = 1; i < dates.length; i++) { int currDate = dates[i].getYear(); if (lastDate != currDate) { ds.add(d); lastDate = currDate; } d = dates[i]; } } else { throw new IllegalArgumentException("Invalid method: " + method); } ds.add(d); newDates = ds.toArray(new DateTime[ds.size()]); return reindex(newDates, FillMethod.FORWARDFILL); } @Override public TimeSeries asFreq(Period period, FillMethod method) { if (size() == 0) { return new TimeSeriesArray(); } long periodMillis = period.toStandardSeconds().getSeconds() * 1000; int newLen = (int) (duration() / periodMillis + 1); DateTime[] dates = new DateTime[newLen]; dates[0] = new DateTime(start().getMillis() - (start().getMillis() % periodMillis)); for (int i = 1; i < dates.length; i++) { dates[i] = dates[i - 1].plus(period); } return reindex(dates, method); } @Override public TimeSeries match(TimeSeries ts, FillMethod method) { if (ts.size() == 0) { return new TimeSeriesArray(); } return reindex(ts.dates(), method).truncate(ts.start(), ts.end()); } @Override public TimeSeries filter(LocalTime from, LocalTime to) { long fromMillis = from.getMillisOfDay(); long toMillis = to.getMillisOfDay(); NavigableMap<DateTime, Double> ts = toMap(); for (int i = 0; i < dates.length; i++) { DateTime d = dates[i]; long m = d.getMillisOfDay(); if (m < fromMillis || m > toMillis) { ts.remove(d); } } return new TimeSeriesArray(ts); } @Override public TimeSeries filter(double min, double max) { NavigableMap<DateTime, Double> ts = toMap(); for (int i = 0; i < data.length; i++) { double v = data[i]; if (v < min || v > max) { ts.remove(dates[i]); } } return new TimeSeriesArray(ts); } @Override public TimeSeries clearTime() { DateTime[] newDates = new DateTime[dates.length]; for (int i = 0; i < dates.length; i++) { newDates[i] = dates[i].withMillisOfDay(0); } return new TimeSeriesArray(newDates, data.clone()); } @Override public TimeSeries fill(double value) { double[] newData = data.clone(); for (int i = 0; i < data.length; i++) { if (data[i] != data[i]) { newData[i] = value; } } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries fill(FillMethod method) { return reindex(dates, method); } @Override public TimeSeries add(double value) { double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] + value; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries add(TimeSeries ts) { double[] tsData = ts.data(); double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] + tsData[i]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries sub(double value) { double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] - value; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries sub(TimeSeries ts) { double[] tsData = ts.data(); double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] - tsData[i]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries mul(double value) { double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] * value; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries mul(TimeSeries ts) { double[] tsData = ts.data(); double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] * tsData[i]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries div(double value) { double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] / value; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries div(TimeSeries ts) { double[] tsData = ts.data(); double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { newData[i] = data[i] / tsData[i]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries clip(double min, double max) { double[] newData = new double[size()]; for (int i = 0; i < newData.length; i++) { double d = data[i]; d = d < min ? min : d; newData[i] = d > max ? max : d; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries diff(int lag) { double[] newData = new double[data.length]; Arrays.fill(newData, 0, lag, 0); for (int i = lag; i < data.length; i++) { newData[i] = data[i] - data[i - lag]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries arithReturn(int lag) { double[] newData = new double[data.length]; Arrays.fill(newData, 0, lag, 0); for (int i = lag; i < data.length; i++) { double d2 = data[i]; double d1 = data[i - lag]; newData[i] = (d2 - d1) / d1; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries logReturn(int lag) { double[] newData = new double[data.length]; Arrays.fill(newData, 0, lag, 0); for (int i = lag; i < data.length; i++) { newData[i] = Math.log(data[i] / data[i - lag]); } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries standardize() { double[] newData = data.clone(); double mean = mean(); double sum = 0; for (int i = 0; i < newData.length; i++) { double v = newData[i] - mean; sum += v * v; } double stdev = Math.sqrt(sum / (newData.length - 1)); for (int i = 0; i < newData.length; i++) { newData[i] = (newData[i] - mean) / stdev; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries normalize(double min, double max) { double[] newData = data.clone(); double tsMin = Double.POSITIVE_INFINITY, tsMax = Double.NEGATIVE_INFINITY; if (Double.isNaN(min) || Double.isNaN(max)) { for (int i = 0; i < newData.length; i++) { double d = newData[i]; tsMin = tsMin < d ? tsMin : d; tsMax = tsMax > d ? tsMax : d; } } else { tsMin = min; tsMax = max; } if (tsMin == tsMax) { Arrays.fill(newData, 0); } else { for (int i = 0; i < newData.length; i++) { double d = newData[i]; if (d == tsMin) { newData[i] = -1.0; } else if (d == tsMax) { newData[i] = 1.0; } else { newData[i] = -1.0 + 2.0 * (d - tsMin) / (tsMax - tsMin); } } } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries cumsum() { double[] newData = new double[size()]; newData[0] = Double.isNaN(data[0]) ? 0 : data[0]; for (int i = 1; i < newData.length; i++) { double d = data[i]; newData[i] = (Double.isNaN(d) ? 0 : d) + newData[i - 1]; } return new TimeSeriesArray(dates.clone(), newData); } @Override public double mean() { if (data.length == 0) { return Double.NaN; } double sum = 0.0; for (int i = 0; i < data.length; i++) { sum += data[i]; } return sum / data.length; } @Override public double median() { if (data.length == 0) { return Double.NaN; } double[] d = data.clone(); Arrays.sort(d); if ((d.length & 1) == 0) { int i = d.length / 2; return (d[i - 1] + d[i]) / 2; } return d[d.length / 2]; } @Override public double mode() { if (data.length == 0) { return Double.NaN; } double[] sorted = data.clone(); Arrays.sort(sorted); int f = 0; int fMax = 0; double mode = Double.NaN; double last = Double.NaN; for (int i = 0; i < data.length; i++) { double v = sorted[i]; if (v == last) { f++; if (f > fMax) { fMax = f; mode = v; } } else { f = 0; } last = v; } return mode; } @Override public double sum() { if (data.length == 0) { return Double.NaN; } double sum = 0.0; for (int i = 0; i < data.length; i++) { sum += data[i]; } return sum; } @Override public double min() { if (data.length == 0) { return Double.NaN; } double min = Double.POSITIVE_INFINITY; for (int i = 0; i < data.length; i++) { min = Math.min(data[i], min); } return min; } @Override public double max() { if (data.length == 0) { return Double.NaN; } double max = Double.NEGATIVE_INFINITY; for (int i = 0; i < data.length; i++) { max = Math.max(data[i], max); } return max; } @Override public double var() { if (data.length == 0) { return Double.NaN; } if (data.length == 1) { return 0.0; } double mean = mean(); double sum = 0; for (int i = 0; i < data.length; i++) { double v = data[i] - mean; sum += v * v; } return sum / (data.length - 1); } @Override public double std() { return Math.sqrt(var()); } @Override public TimeSeries rollingMean(int window) { if (window <= 1) { throw new IllegalArgumentException("Window must be greater than 1"); } double[] newData = new double[data.length]; Arrays.fill(newData, 0, window, Double.NaN); for (int i = window - 1; i < data.length; i++) { double sum = 0.0; for (int j = i - (window - 1); j <= i; j++) { sum += data[j]; } newData[i] = sum / window; } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries rollingMedian(int window) { if (window <= 1) { throw new IllegalArgumentException("Window must be greater than 1"); } double[] newData = new double[data.length]; Arrays.fill(newData, 0, window, Double.NaN); for (int i = window - 1; i < data.length; i++) { double[] d = Arrays.copyOfRange(data, i - (window - 1), i + 1); Arrays.sort(d); if ((d.length & 1) == 0) { int k = d.length / 2; newData[i] = (d[k - 1] + d[k]) / 2; } else { newData[i] = d[d.length / 2]; } } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries rollingVar(int window) { if (window <= 1) { throw new IllegalArgumentException("Window must be greater than 1"); } double[] newData = new double[data.length]; Arrays.fill(newData, 0, window, Double.NaN); for (int i = window - 1; i < data.length; i++) { double sum = 0.0; for (int j = i - (window - 1); j <= i; j++) { sum += data[j]; } double mean = sum / window; sum = 0.0; for (int j = i - (window - 1); j <= i; j++) { double v = data[j] - mean; sum += v * v; } newData[i] = sum / (window - 1); } return new TimeSeriesArray(dates.clone(), newData); } @Override public TimeSeries rollingStd(int window) { if (window <= 1) { throw new IllegalArgumentException("Window must be greater than 1"); } double[] newData = new double[data.length]; Arrays.fill(newData, 0, window - 1, Double.NaN); for (int i = window - 1; i < data.length; i++) { double sum = 0.0; for (int j = i - (window - 1); j <= i; j++) { sum += data[j]; } double mean = sum / window; sum = 0.0; for (int j = i - (window - 1); j <= i; j++) { double v = data[j] - mean; sum += v * v; } newData[i] = Math.sqrt(sum / (window - 1)); } return new TimeSeriesArray(dates.clone(), newData); } @Override public double corr(TimeSeries ts) { double[] data = this.data; double[] otherData = ts.data(); if (data.length != otherData.length) { throw new IllegalArgumentException(String.format("Data length must match: %s != %s", data.length, otherData.length)); } final int len = data.length; double sum = 0; double sumOther = 0; double sumSq = 0; double sumSqOther = 0; double sumProduct = 0; for (int i = 0; i < len; i++) { sum += data[i]; sumOther += otherData[i]; sumSq += data[i] * data[i]; sumSqOther += otherData[i] * otherData[i]; sumProduct += data[i] * otherData[i]; } double numerator = sumProduct - sum * sumOther / len; double denominator = Math.sqrt((sumSq - sum * sum / len) * (sumSqOther - sumOther * sumOther / len)); return numerator / denominator; } @Override public double autoCorr(int lag) { int n = data.length; if (lag >= n) { throw new IllegalArgumentException("Lag is too large"); } double mean = mean(); double sum = 0; for (int i = 0; i < data.length; i++) { double v = data[i] - mean; sum += v * v; } double variance = sum / data.length; // (data.length - 1); double run = 0; for (int i = lag; i < n; ++i) { run += (data[i] - mean) * (data[i - lag] - mean); } return (run / (n - lag)) / variance; } @Override public TimeSeries apply(TimeSeriesOp op) { return op.apply(this); } @Override public NavigableMap<DateTime, Double> toMap() { NavigableMap<DateTime, Double> map = new TreeMap<DateTime, Double>(); for (int i = 0; i < dates.length; i++) { map.put(dates[i], data[i]); } return map; } @Override public Iterator<TimeSeriesValuePair> iterator() { return new Iterator<TimeSeriesValuePair>() { int i = 0; @Override public boolean hasNext() { return i < dates.length; } @Override public TimeSeriesValuePair next() { return new TimeSeriesValuePair() { final int idx = i++; @Override public DateTime getDateTime() { return dates[idx]; } @Override public double getValue() { return data[idx]; } @Override public double setValue(double v) { double old = data[idx]; data[idx] = v; return old; } }; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } @Override public String toString(boolean tabular) { StringBuilder sb = new StringBuilder(); if (tabular) { for (int i = 0; i < dates.length; i++) { sb.append(dates[i]); sb.append(" "); sb.append(data[i]); sb.append('\n'); } } else { sb.append("["); DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); for (int i = 0; i < data.length; i++) { sb.append(dateFormatter.print(dates[i])); sb.append(' '); sb.append(Util.roundSig(data[i], 4)); if (i + 1 < data.length) { sb.append(", "); } } sb.append(']'); return sb.toString(); } return sb.toString(); } @Override public String toString() { return toString(false); } @Override public int hashCode() { int result = 31 + Arrays.hashCode(data); result = 31 * result + Arrays.hashCode(dates); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; TimeSeriesArray other = (TimeSeriesArray) obj; if (size() != other.size()) return false; if (!Arrays.equals(dates, other.dates)) return false; if (!Arrays.equals(data, other.data)) return false; return true; } }