/** * The MIT License (MIT) * * Copyright (c) 2014-2017 Marc de Verdelhan & respective authors (see AUTHORS) * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package eu.verdelhan.ta4j; import eu.verdelhan.ta4j.Order.OrderType; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Sequence of {@link Tick ticks} separated by a predefined period (e.g. 15 minutes, 1 day, etc.) * <p> * Notably, a {@link TimeSeries time series} can be: * <ul> * <li>splitted into sub-series * <li>the base of {@link Indicator indicator} calculations * <li>limited to a fixed number of ticks (e.g. for actual trading) * <li>used to run {@link Strategy trading strategies} * </ul> */ public class TimeSeries implements Serializable { private static final long serialVersionUID = -1878027009398790126L; /** The logger */ private final Logger log = LoggerFactory.getLogger(getClass()); /** Name of the series */ private final String name; /** Begin index of the time series */ private int beginIndex = -1; /** End index of the time series */ private int endIndex = -1; /** List of ticks */ private final List<Tick> ticks; /** Maximum number of ticks for the time series */ private int maximumTickCount = Integer.MAX_VALUE; /** Number of removed ticks */ private int removedTicksCount = 0; /** True if the current series is a sub-series, false otherwise */ private boolean subSeries = false; /** * Constructor. * @param name the name of the series * @param ticks the list of ticks of the series */ public TimeSeries(String name, List<Tick> ticks) { this(name, ticks, 0, ticks.size() - 1, false); } /** * Constructor of an unnamed series. * @param ticks the list of ticks of the series */ public TimeSeries(List<Tick> ticks) { this("unnamed", ticks); } /** * Constructor. * @param name the name of the series */ public TimeSeries(String name) { this.name = name; this.ticks = new ArrayList<Tick>(); } /** * Constructor of an unnamed series. */ public TimeSeries() { this("unamed"); } /** * Constructor. * @param name the name of the series * @param ticks the list of ticks of the series * @param beginIndex the begin index (inclusive) of the time series * @param endIndex the end index (inclusive) of the time series * @param subSeries true if the current series is a sub-series, false otherwise */ private TimeSeries(String name, List<Tick> ticks, int beginIndex, int endIndex, boolean subSeries) { // TODO: add null checks and out of bounds checks if (endIndex < beginIndex - 1) { throw new IllegalArgumentException("end cannot be < than begin - 1"); } this.name = name; this.ticks = ticks; this.beginIndex = beginIndex; this.endIndex = endIndex; this.subSeries = subSeries; } /** * @return the name of the series */ public String getName() { return name; } /** * @param i an index * @return the tick at the i-th position */ public Tick getTick(int i) { int innerIndex = i - removedTicksCount; if (innerIndex < 0) { if (i < 0) { // Cannot return the i-th tick if i < 0 throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, i)); } log.trace("Time series `{}` ({} ticks): tick {} already removed, use {}-th instead", name, ticks.size(), i, removedTicksCount); if (ticks.isEmpty()) { throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, removedTicksCount)); } innerIndex = 0; } else if (innerIndex >= ticks.size()) { // Cannot return the n-th tick if n >= ticks.size() throw new IndexOutOfBoundsException(buildOutOfBoundsMessage(this, i)); } return ticks.get(innerIndex); } /** * @return the first tick of the series */ public Tick getFirstTick() { return getTick(beginIndex); } /** * @return the last tick of the series */ public Tick getLastTick() { return getTick(endIndex); } /** * @return the number of ticks in the series */ public int getTickCount() { if (endIndex < 0) { return 0; } final int startIndex = Math.max(removedTicksCount, beginIndex); return endIndex - startIndex + 1; } /** * @return the begin index of the series */ public int getBegin() { return beginIndex; } /** * @return the end index of the series */ public int getEnd() { return endIndex; } /** * @return the description of the series period (e.g. "from 12:00 21/01/2014 to 12:15 21/01/2014") */ public String getSeriesPeriodDescription() { StringBuilder sb = new StringBuilder(); if (!ticks.isEmpty()) { final String timeFormat = "hh:mm dd/MM/yyyy"; Tick firstTick = getFirstTick(); Tick lastTick = getLastTick(); sb.append(firstTick.getEndTime().toString(timeFormat)) .append(" - ") .append(lastTick.getEndTime().toString(timeFormat)); } return sb.toString(); } /** * Sets the maximum number of ticks that will be retained in the series. * <p> * If a new tick is added to the series such that the number of ticks will exceed the maximum tick count, * then the FIRST tick in the series is automatically removed, ensuring that the maximum tick count is not exceeded. * @param maximumTickCount the maximum tick count */ public void setMaximumTickCount(int maximumTickCount) { if (subSeries) { throw new IllegalStateException("Cannot set a maximum tick count on a sub-series"); } if (maximumTickCount <= 0) { throw new IllegalArgumentException("Maximum tick count must be strictly positive"); } this.maximumTickCount = maximumTickCount; removeExceedingTicks(); } /** * @return the maximum number of ticks */ public int getMaximumTickCount() { return maximumTickCount; } /** * @return the number of removed ticks */ public int getRemovedTicksCount() { return removedTicksCount; } /** * Adds a tick at the end of the series. * <p> * Begin index set to 0 if if wasn't initialized.<br> * End index set to 0 if if wasn't initialized, or incremented if it matches the end of the series.<br> * Exceeding ticks are removed. * @param tick the tick to be added * @see TimeSeries#setMaximumTickCount(int) */ public void addTick(Tick tick) { if (tick == null) { throw new IllegalArgumentException("Cannot add null tick"); } final int lastTickIndex = ticks.size() - 1; if (!ticks.isEmpty()) { DateTime seriesEndTime = ticks.get(lastTickIndex).getEndTime(); if (!tick.getEndTime().isAfter(seriesEndTime)) { throw new IllegalArgumentException("Cannot add a tick with end time <= to series end time"); } } ticks.add(tick); if (beginIndex == -1) { // Begin index set to 0 only if if wasn't initialized beginIndex = 0; } endIndex++; removeExceedingTicks(); } /** * Returns a new time series which is a view of a subset of the current series. * <p> * The new series has begin and end indexes which correspond to the bounds of the sub-set into the full series.<br> * The tick of the series are shared between the original time series and the returned one (i.e. no copy). * @param beginIndex the begin index (inclusive) of the time series * @param endIndex the end index (inclusive) of the time series * @return a constrained {@link TimeSeries time series} which is a sub-set of the current series */ public TimeSeries subseries(int beginIndex, int endIndex) { if (maximumTickCount != Integer.MAX_VALUE) { throw new IllegalStateException("Cannot create a sub-series from a time series for which a maximum tick count has been set"); } return new TimeSeries(name, ticks, beginIndex, endIndex, true); } /** * Returns a new time series which is a view of a subset of the current series. * <p> * The new series has begin and end indexes which correspond to the bounds of the sub-set into the full series.<br> * The tick of the series are shared between the original time series and the returned one (i.e. no copy). * @param beginIndex the begin index (inclusive) of the time series * @param duration the duration of the time series * @return a constrained {@link TimeSeries time series} which is a sub-set of the current series */ public TimeSeries subseries(int beginIndex, Period duration) { // Calculating the sub-series interval DateTime beginInterval = getTick(beginIndex).getEndTime(); DateTime endInterval = beginInterval.plus(duration); Interval subseriesInterval = new Interval(beginInterval, endInterval); // Checking ticks belonging to the sub-series (starting at the provided index) int subseriesNbTicks = 0; for (int i = beginIndex; i <= endIndex; i++) { // For each tick... DateTime tickTime = getTick(i).getEndTime(); if (!subseriesInterval.contains(tickTime)) { // Tick out of the interval break; } // Tick in the interval // --> Incrementing the number of ticks in the subseries subseriesNbTicks++; } return subseries(beginIndex, beginIndex + subseriesNbTicks - 1); } /** * Splits the time series into sub-series containing nbTicks ticks each.<br> * The current time series is splitted every nbTicks ticks.<br> * The last sub-series may have less ticks than nbTicks. * @param nbTicks the number of ticks of each sub-series * @return a list of sub-series */ public List<TimeSeries> split(int nbTicks) { ArrayList<TimeSeries> subseries = new ArrayList<TimeSeries>(); for (int i = beginIndex; i <= endIndex; i += nbTicks) { // For each nbTicks ticks int subseriesBegin = i; int subseriesEnd = Math.min(subseriesBegin + nbTicks - 1, endIndex); subseries.add(subseries(subseriesBegin, subseriesEnd)); } return subseries; } /** * Splits the time series into sub-series lasting sliceDuration.<br> * The current time series is splitted every splitDuration.<br> * The last sub-series may last less than sliceDuration. * @param splitDuration the duration between 2 splits * @param sliceDuration the duration of each sub-series * @return a list of sub-series */ public List<TimeSeries> split(Period splitDuration, Period sliceDuration) { ArrayList<TimeSeries> subseries = new ArrayList<TimeSeries>(); if (splitDuration != null && !splitDuration.equals(Period.ZERO) && sliceDuration != null && !sliceDuration.equals(Period.ZERO)) { List<Integer> beginIndexes = getSplitBeginIndexes(splitDuration); for (Integer subseriesBegin : beginIndexes) { subseries.add(subseries(subseriesBegin, sliceDuration)); } } return subseries; } /** * Splits the time series into sub-series lasting duration.<br> * The current time series is splitted every duration.<br> * The last sub-series may last less than duration. * @param duration the duration between 2 splits (and of each sub-series) * @return a list of sub-series */ public List<TimeSeries> split(Period duration) { return split(duration, duration); } /** * Runs the strategy over the series. * <p> * Opens the trades with {@link OrderType.BUY} orders. * @param strategy the trading strategy * @return the trading record coming from the run */ public TradingRecord run(Strategy strategy) { return run(strategy, OrderType.BUY); } /** * Runs the strategy over the series. * <p> * Opens the trades with {@link OrderType.BUY} orders. * @param strategy the trading strategy * @param orderType the {@link OrderType} used to open the trades * @return the trading record coming from the run */ public TradingRecord run(Strategy strategy, OrderType orderType) { return run(strategy, orderType, Decimal.NaN); } /** * Runs the strategy over the series. * <p> * @param strategy the trading strategy * @param orderType the {@link OrderType} used to open the trades * @param amount the amount used to open/close the trades * @return the trading record coming from the run */ public TradingRecord run(Strategy strategy, OrderType orderType, Decimal amount) { log.trace("Running strategy: {} (starting with {})", strategy, orderType); TradingRecord tradingRecord = new TradingRecord(orderType); for (int i = beginIndex; i <= endIndex; i++) { // For each tick in the sub-series... if (strategy.shouldOperate(i, tradingRecord)) { tradingRecord.operate(i, ticks.get(i).getClosePrice(), amount); } } if (!tradingRecord.isClosed()) { // If the last trade is still opened, we search out of the end index. // May works if the current series is a sub-series (but not the last sub-series). for (int i = endIndex + 1; i < ticks.size(); i++) { // For each tick out of sub-series bound... // --> Trying to close the last trade if (strategy.shouldOperate(i, tradingRecord)) { tradingRecord.operate(i, ticks.get(i).getClosePrice(), amount); break; } } } return tradingRecord; } /** * Removes the N first ticks which exceed the maximum tick count. */ private void removeExceedingTicks() { int tickCount = ticks.size(); if (tickCount > maximumTickCount) { // Removing old ticks int nbTicksToRemove = tickCount - maximumTickCount; for (int i = 0; i < nbTicksToRemove; i++) { ticks.remove(0); } // Updating removed ticks count removedTicksCount += nbTicksToRemove; } } /** * Builds a list of split indexes from splitDuration. * @param splitDuration the duration between 2 splits * @return a list of begin indexes after split */ private List<Integer> getSplitBeginIndexes(Period splitDuration) { ArrayList<Integer> beginIndexes = new ArrayList<Integer>(); // Adding the first begin index beginIndexes.add(beginIndex); // Building the first interval before next split DateTime beginInterval = getTick(beginIndex).getEndTime(); DateTime endInterval = beginInterval.plus(splitDuration); Interval splitInterval = new Interval(beginInterval, endInterval); for (int i = beginIndex; i <= endIndex; i++) { // For each tick... DateTime tickTime = getTick(i).getEndTime(); if (!splitInterval.contains(tickTime)) { // Tick out of the interval if (!endInterval.isAfter(tickTime)) { // Tick after the interval // --> Adding a new begin index beginIndexes.add(i); } // Building the new interval before next split beginInterval = endInterval.isBefore(tickTime) ? tickTime : endInterval; endInterval = beginInterval.plus(splitDuration); splitInterval = new Interval(beginInterval, endInterval); } } return beginIndexes; } /** * @param series a time series * @param index an out of bounds tick index * @return a message for an OutOfBoundsException */ private static String buildOutOfBoundsMessage(TimeSeries series, int index) { return "Size of series: " + series.ticks.size() + " ticks, " + series.removedTicksCount + " ticks removed, index = " + index; } }