// Copyright 2015 Ivan Popivanov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.tradelib.core;
import java.io.BufferedReader;
import java.io.FileReader;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BinaryOperator;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
public class Series implements Cloneable {
private List<LocalDateTime> index;
List<List<Double>> data;
// The names of the data columns
private HashMap<String, Integer> columnNames;
static public Series fromDailyCsv(String path, boolean header) throws Exception {
return fromCsv(path, header, DateTimeFormatter.ofPattern("yyyy-MM-dd"), LocalTime.of(17, 0));
}
static public Series fromCsv(String path, boolean header, DateTimeFormatter dtf) throws Exception {
return fromCsv(path, header, dtf, null);
}
static public Series fromCsv(String path, boolean header, DateTimeFormatter dtf, LocalTime lt) throws Exception {
if(dtf == null) {
if(lt == null) dtf = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
else dtf = DateTimeFormatter.ISO_DATE;
}
// Parse and import the csv
CSVFormat csvFmt = CSVFormat
.DEFAULT
.withCommentMarker('#')
.withIgnoreSurroundingSpaces();
if(header) csvFmt = csvFmt.withHeader();
CSVParser csv = csvFmt.parse(new BufferedReader(new FileReader(path)));
int ncols = -1;
Series result = null;
double [] values = null;
for(CSVRecord rec : csv.getRecords()) {
if(result == null) {
ncols = rec.size() - 1;
values = new double[ncols];
result = new Series(ncols);
}
for(int ii = 0; ii < ncols; ++ii) {
values[ii] = Double.parseDouble(rec.get(ii + 1));
}
LocalDateTime ldt;
if(lt != null) {
ldt = LocalDate.parse(rec.get(0), dtf).atTime(lt);
} else {
ldt = LocalDateTime.parse(rec.get(0), dtf);
}
result.append(ldt, values);
}
if(header) {
Map<String,Integer> headerMap = csv.getHeaderMap();
result.clearNames();
for(Map.Entry<String,Integer> me : headerMap.entrySet()) {
if(me.getValue() > 0) result.setName(me.getKey(), me.getValue() - 1);
}
}
return result;
}
public Series(int ncols) {
index = new ArrayList<LocalDateTime>();
data = new ArrayList<List<Double>>(ncols);
for(int ii = 0; ii < ncols; ++ii) {
data.add(new ArrayList<Double>());
}
columnNames = new HashMap<String, Integer>();
}
public double get(int rowId, int colId) {
return data.get(colId).get(rowId);
}
public double get(int rowId) {
return get(rowId, 0);
}
public double get(int rowId, String colName) {
return get(rowId, columnNames.get(colName));
}
public LocalDateTime getTimestamp(int rowId) {
return index.get(rowId);
}
public double get(LocalDateTime ts, int colId) {
int rowId = Collections.binarySearch(index, ts);
if(rowId < 0) {
throw new BadIndexException(ts.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " is not in the index");
}
return data.get(colId).get(rowId);
}
public double get(LocalDateTime ts, String colName) {
return get(ts, columnNames.get(colName));
}
public List<Double> getColumn(int colId) {
return data.get(colId);
}
public List<Double> getColumn() {
return data.get(0);
}
public void set(int row, double value) {
data.get(0).set(row, value);
}
public void set(int row, int col, double value) {
data.get(col).set(row, value);
}
public void set(int row, LocalDateTime ts) {
index.set(row, ts);
}
public void set(int row, LocalDateTime ts, double ...args) {
set(row, ts);
for(int ii = 0; ii < args.length; ++ii) {
set(row, ii, args[ii]);
}
}
public void setNames(String ...names) {
columnNames.clear();
for(int ii = 0; ii < names.length; ++ii) {
columnNames.put(names[ii], ii);
}
}
public void setNames(Map<String,Integer> map) {
columnNames.clear();
columnNames.putAll(map);
}
public void setName(String name, int id) {
columnNames.put(name, id);
}
public void clearNames() {
columnNames.clear();
}
public int size() {
return index.size();
}
public void append(LocalDateTime ts, double ...args) {
index.add(ts);
int endLoop = Math.min(args.length, data.size());
for(int ii = 0; ii < endLoop; ++ii) {
data.get(ii).add(args[ii]);
}
for(int ii = endLoop; ii < data.size(); ++ii) {
data.get(ii).add(args[args.length - 1]);
}
}
public Series head(int rows) {
if(rows < 0) {
if(Math.abs(rows) > size()) return null;
rows = size() + rows;
} else if(rows > size()){
return this;
}
Series result = new Series();
result.index = index.subList(0, rows);
result.data = new ArrayList<List<Double>>(data.size());
for(int ii = 0; ii < data.size(); ++ii) {
result.data.add(data.get(ii).subList(0, rows));
}
if(columnNames != null) {
result.columnNames = new HashMap<String, Integer>(columnNames);
}
return result;
}
public Series tail(int rows) {
if(rows < 0) rows = size() + rows;
Series result = new Series();
result.index = index.subList(size() - rows, size());
result.data = new ArrayList<List<Double>>(data.size());
for(int ii = 0; ii < data.size(); ++ii) {
result.data.add(data.get(ii).subList(size() - rows, size()));
}
if(columnNames != null) {
result.columnNames = new HashMap<String, Integer>(columnNames);
}
return result;
}
private TreeMap<LocalDateTime,Integer> buildDailyIndex() {
TreeMap<LocalDateTime,Integer> result = new TreeMap<LocalDateTime, Integer>();
int ii = 0;
while(ii < size()) {
LocalDate ld = index.get(ii).toLocalDate();
int jj = ii + 1;
for(; jj < size(); ++jj) {
if(!index.get(jj).toLocalDate().equals(ld)) {
break;
}
}
--jj;
result.put(ld.atStartOfDay(), jj - ii + 1);
ii = jj + 1;
}
return result;
}
public Series toDaily(BinaryOperator<Double> accumulator) {
return toDaily(0.0, accumulator);
}
public Series toDaily(double identity, BinaryOperator<Double> accumulator) {
TreeMap<LocalDateTime,Integer> newIndex = buildDailyIndex();
Series result = new Series();
int numUnique = newIndex.size();
result.index = new ArrayList<LocalDateTime>(numUnique);
result.data = new ArrayList<List<Double>>(data.size());
for(int ii = 0; ii < data.size(); ++ii) {
result.data.add(new ArrayList<Double>(newIndex.size()));
}
int currentIndex = 0;
Iterator<Map.Entry<LocalDateTime, Integer>> iter = newIndex.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry<LocalDateTime, Integer> entry = iter.next();
result.append(entry.getKey(), 0.0);
int lastIndex = result.size() - 1;
for(int jj = 0; jj < data.size(); ++jj) {
result.set(lastIndex, data.get(jj).subList(currentIndex, currentIndex + entry.getValue()).stream().reduce(identity, accumulator));
}
currentIndex += entry.getValue();
// result.append(entry.getKey(), 0.0);
// int lastIndex = result.size() - 1;
// for(int ii = 0; ii < entry.getValue(); ++ii, ++currentIndex) {
// for(int jj = 0; jj < data.size(); ++jj) {
// result.set(lastIndex, jj, result.get(lastIndex, jj) + get(currentIndex, jj));
// }
// }
}
if(columnNames != null) {
result.columnNames = new HashMap<String, Integer>(columnNames);
}
return result;
}
public void print(DateTimeFormatter dtf) {
for(int ii = 0; ii < size(); ++ii) {
System.out.print(index.get(ii).format(dtf) + ": ");
System.out.format("%,.2f", data.get(0).get(ii));
for(int jj = 1; jj < data.size(); ++jj) {
System.out.format(", %,.2f", data.get(jj).get(ii));
}
System.out.println();
}
}
public void print() {
print(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public void print(String pattern) {
print(DateTimeFormatter.ofPattern(pattern));
}
private Series() {
}
public int columns() {
return data.size();
}
public boolean isOrdered() {
for(int ii = 1; ii < index.size(); ++ii) {
if(index.get(ii).isBefore(index.get(ii-1))) return false;
}
return true;
}
/**
* @brief Clones (deep copy) of the object.
*
* @return The copy.
*/
public Series clone() {
Series result = new Series();
result.index = new ArrayList<LocalDateTime>(index);
result.data = new ArrayList<List<Double>>(data.size());
for(int ii = 0; ii < data.size(); ++ii) {
ArrayList<Double> column = new ArrayList<Double>();
column.addAll(data.get(ii));
result.data.add(column);
}
if(columnNames != null) {
result.columnNames = new HashMap<String, Integer>(columnNames);
}
return result;
}
/**
* @brief Shifts the series k-periods down.
*
* k can be negative (shift to the left) or negative (shift to the right).
* NAs are appended/prepended to the series.
*
* @return The lagged (shifted) time series.
*/
public Series lag(int k) {
if(k < 0) {
int ii = 0;
int jj = -k;
for(; jj < index.size(); ++ii, ++jj) {
for(int col = 0; col < data.size(); ++col) {
data.get(col).set(ii, data.get(col).get(jj));
}
}
// Set to NA for the rest
for(; ii < index.size(); ++ii) {
for(int col = 0; col < data.size(); ++col) {
data.get(col).set(ii, Double.NaN);
}
}
} else if(k > 0) {
int ii = size() - 1;
int jj = ii - k;
for(; jj >= 0; --ii, --jj) {
for(int col = 0; col < data.size(); ++col) {
data.get(col).set(ii, data.get(col).get(jj));
}
}
// Set to NA for the rest
for(; ii >= 0; --ii) {
for(int col = 0; col < data.size(); ++col) {
data.get(col).set(ii, Double.NaN);
}
}
}
return this;
}
/**
* @brief Shifts the series 1-period down.
*
* NAs are appended to the series.
*
* @return The lagged (shifted) time series.
*/
public Series lag() {
return lag(1);
}
}