com.quant.TimeSeries.java Source code

Java tutorial

Introduction

Here is the source code for com.quant.TimeSeries.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2016 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 com.quant;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.quant.Order.OrderType;

/**
 * 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 {

    /** The logger */
    private final Logger log = LoggerFactory.getLogger(getClass());
    /** Name of the series */
    private final String name;
    /** The industry the stock belongs to */
    private String industry;
    /** 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;
    /** Time period of the series */
    private Period timePeriod;
    /** 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;

    Map<DateTime, Integer> dateToIndex;

    /**
     * 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);
    }

    /**
     *  Get the close price for given date
     * @param dateTime
     * @return
     */
    public Decimal getClosePrice(DateTime dateTime) {
        int idx = getIndexFromDate(dateTime);
        if (idx == -1) {
            return null;
        }
        return ticks.get(idx).getClosePrice();
    }

    /**
     * Constructor.
     * @param name the name of the series
     * @param timePeriod the time period (between 2 ticks)
     */
    public TimeSeries(String name, Period timePeriod) {
        if (timePeriod == null) {
            throw new IllegalArgumentException("Time period cannot be null");
        }
        this.name = name;
        this.ticks = new ArrayList<Tick>();
        this.timePeriod = timePeriod;
    }

    public int getIndexFromDate(DateTime dateTime) {
        if (dateToIndex.containsKey(dateTime))
            return dateToIndex.get(dateTime);
        else
            return -1;
    }

    /**
     * Constructor of an unnamed series.
     *
     * @param timePeriod the time period (between 2 ticks)
     */
    public TimeSeries(Period timePeriod) {
        this("unamed", timePeriod);
    }

    public void setDateToIndex(Map<DateTime, Integer> dateToIndex) {
        this.dateToIndex = dateToIndex;
    }

    /**
     * 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;
        computeTimePeriod();
    }

    /**
     * @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 mappings between the date time and index
     */
    public Map<DateTime, Integer> getDateToIndex() {
        return dateToIndex;
    }

    /**
     * @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 industry the stock belongs to  
     */
    public String getIndustry() {
        return industry;
    }

    /** 
     * Set the industry for the stock 
     */
    public void setIndustry(String industry) {
        this.industry = industry;
    }

    /**
     * @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();
    }

    /**
     * @return the time period of the series
     */
    public Period getTimePeriod() {
        return timePeriod;
    }

    /**
     * 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);
    }

    public TimeSeries subseriesDeep(int beginIndex, int endIndex) {
        List<Tick> newTicks = new ArrayList<Tick>();
        for (int i = beginIndex; i <= endIndex; i++) {
            newTicks.add(ticks.get(i));
        }

        for (int i = 1; i <= endIndex - beginIndex; i++) {
            newTicks.get(i)
                    .setClosePrice(newTicks.get(i).getClosePrice().dividedBy(newTicks.get(0).getClosePrice()));
        }

        newTicks.get(0).setClosePrice(Decimal.ONE);
        return new TimeSeries(name, newTicks, 0, endIndex - beginIndex, false);
    }

    /**
     * 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;
    }

    /**
     * Computes the time period of the series.
     */
    private void computeTimePeriod() {

        Period minPeriod = null;
        for (int i = beginIndex; i < endIndex; i++) {
            // For each tick interval...
            // Looking for the minimum period.
            long currentPeriodMillis = getTick(i + 1).getEndTime().getMillis()
                    - getTick(i).getEndTime().getMillis();
            if (minPeriod == null) {
                minPeriod = new Period(currentPeriodMillis);
            } else {
                long minPeriodMillis = minPeriod.getMillis();
                if (minPeriodMillis > currentPeriodMillis) {
                    minPeriod = new Period(currentPeriodMillis);
                }
            }
        }
        if (minPeriod == null || Period.ZERO.equals(minPeriod)) {
            // Minimum period not found (or zero ms found)
            // --> Use a one-day period
            minPeriod = Period.days(1);
        }
        timePeriod = minPeriod;
    }

    /**
     * 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;
    }
}