/*******************************************************************************
* Copyright (c) 2013 Luigi Sgro. All rights reserved. This
* program and the accompanying materials are made available under the terms of
* the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Luigi Sgro - initial API and implementation
******************************************************************************/
package com.quantcomponents.algo.ta;
import java.util.Date;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.quantcomponents.algo.IOrderReceiver;
import com.quantcomponents.algo.IPosition;
import com.quantcomponents.algo.ITradingAgent;
import com.quantcomponents.algo.OrderBean;
import com.quantcomponents.core.calendar.CalendarTradingSchedule;
import com.quantcomponents.core.calendar.ITradingCalendar;
import com.quantcomponents.core.calendar.ITradingSchedule;
import com.quantcomponents.core.model.IContract;
import com.quantcomponents.core.model.ISeries;
import com.quantcomponents.core.model.ISeriesAugmentable;
import com.quantcomponents.core.model.ISeriesListener;
import com.quantcomponents.core.model.ISeriesPoint;
import com.quantcomponents.core.model.OrderSide;
import com.quantcomponents.core.model.OrderType;
import com.quantcomponents.marketdata.IOHLCPoint;
import com.quantcomponents.marketdata.IOHLCTimeSeries;
import com.quantcomponents.marketdata.TimeSeriesTail;
/**
* A trivial example of a trend following trading strategy.
* It starts folling the trend based on the crossing of a short moving average above or below a long moving average of the prices.
* When the short moving average crosses above the long one, it takes a long position, and vice-versa.
* Number of periods for short and long moving averages, trading calendar to be used, position size (short or long) are configurable.
* Also among the configuration parameters there is: <emph>ignoreLastPeriod</emph>: this parameter should be set when trading in real
* time, and not set when backtesting. In fact when trading in real time, only the completed periods must enter into the algorithm.
* When backtesting, all the periods must be taken into account, since otherwise the last of the series will not enter the test.
* <br>
* <b>NOTE</b>This is only an example to show how to write trading algorithms with this framework: it has been proved consistenly
* loss-making!
*
*/
public class AverageCrossingTradingAgent implements ITradingAgent, ISeriesListener<Date, Double> {
private static final Logger logger = Logger.getLogger(AverageCrossingTradingAgent.class.getName());
public static final String TRADING_CALENDAR_NAME = "tradingCalendarName";
public static final String SHORT_AVERAGE_PERIODS = "shortAveragePeriods";
public static final String LONG_AVERAGE_PERIODS = "longAveragePeriods";
public static final String POSITION_SIZE = "positionSize";
public static final String IGNORE_LAST_PERIOD = "ignoreLastPeriod";
// configuration values
private final int positionSize;
private final boolean ignoreLastPeriod;
// tail operators
private final TimeSeriesTail<IOHLCPoint> shortAveragingTail;
private final TimeSeriesTail<IOHLCPoint> longAveragingTail;
// input/output
private volatile IOrderReceiver orderReceiver;
private volatile IOHLCTimeSeries stockTimeSeries;
private volatile ISeriesAugmentable<Date, Double, ISeriesPoint<Date, Double>> outputSeries;
// status
private volatile RunningStatus runningStatus = RunningStatus.NEW;
private volatile int position;
public AverageCrossingTradingAgent(ITradingCalendar tradingCalendar, int shortAveragePeriods, int longAveragePeriods, int positionSize,
boolean ignoreLastPeriod) {
this.positionSize = positionSize;
this.ignoreLastPeriod = ignoreLastPeriod;
if (ignoreLastPeriod) {
shortAveragePeriods++;
longAveragePeriods++;
}
ITradingSchedule tradingSchedule = new CalendarTradingSchedule(tradingCalendar);
shortAveragingTail = new TimeSeriesTail<IOHLCPoint>(tradingSchedule, shortAveragePeriods);
longAveragingTail = new TimeSeriesTail<IOHLCPoint>(tradingSchedule, longAveragePeriods);
}
@Override
public void wire(Map<String, ? extends ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>> inputSeries, ISeriesAugmentable<Date, Double, ISeriesPoint<Date, Double>> outputSeries) {
this.outputSeries = outputSeries;
ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>> series = inputSeries.get(AverageCrossingTradingAgentFactory.INPUT_SERIES_NAMES[0]);
if (!(series instanceof IOHLCTimeSeries)) {
throw new IllegalArgumentException("Only '" + IOHLCTimeSeries.class.getName() + "' instances can be passed as input series");
}
stockTimeSeries = (IOHLCTimeSeries) series;
}
@Override
public void unwire() {
kill();
}
@Override
public void setOrderReceiver(IOrderReceiver orderReceiver) {
this.orderReceiver = orderReceiver;
}
@Override
public void pause() {
synchronized (runningStatus) {
if (runningStatus == RunningStatus.RUNNING) {
runningStatus = RunningStatus.PAUSED;
}
}
}
@Override
public void resume() {
synchronized (runningStatus) {
if (runningStatus == RunningStatus.PAUSED) {
runningStatus = RunningStatus.RUNNING;
}
}
}
@Override
public RunningStatus getRunningStatus() {
return runningStatus;
}
@Override
public void inputComplete() {
kill();
}
@Override
public void run() {
synchronized (runningStatus) {
if (runningStatus != RunningStatus.NEW) {
throw new IllegalStateException("Could not run from running status: " + runningStatus.name());
}
runningStatus = RunningStatus.RUNNING;
}
stockTimeSeries.addSeriesListener(this);
try {
while (!Thread.interrupted() && runningStatus != RunningStatus.TERMINATED) {
updatePosition();
synchronized(this) {
wait();
}
}
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Interrupted. Exit", e);
} finally {
synchronized (runningStatus) {
runningStatus = RunningStatus.TERMINATED;
}
}
stockTimeSeries.removeSeriesListener(this);
}
@Override
public synchronized void kill() {
synchronized (runningStatus) {
runningStatus = RunningStatus.TERMINATED;
}
notify();
}
@Override
public void onItemUpdated(ISeriesPoint<Date, Double> existingItem, ISeriesPoint<Date, Double> updatedItem) {
if (runningStatus == RunningStatus.RUNNING) {
updatePosition();
}
}
@Override
public void onItemAdded(ISeriesPoint<Date, Double> newItem) {
if (runningStatus == RunningStatus.RUNNING) {
if (outputSeries != null) {
outputSeries.insertFromTail(newItem);
}
updatePosition();
}
}
@Override
public void onOrderSubmitted(String orderId, boolean active) { }
@Override
public void onOrderFilled(String orderId, int filled, boolean full, double averagePrice) { }
@Override
public void onOrderCancelled(String orderId) { }
@Override
public void onOrderStatus(String orderId, String status) { }
@Override
public void onPositionUpdate(IContract contract, IPosition position) { }
private double calculateMovingAverage(ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>> timeSeries, boolean ignoreLastPeriod) {
double result;
int actualNumberOfPeriods = timeSeries.size();
if (ignoreLastPeriod) {
actualNumberOfPeriods--;
}
double totalSum = 0.0;
int currentPeriod = 0;
for (ISeriesPoint<Date, Double> point : timeSeries) {
if (currentPeriod >= actualNumberOfPeriods) {
break;
}
IOHLCPoint bar = (IOHLCPoint) point;
totalSum += bar.getClose();
currentPeriod++;
}
result = totalSum / currentPeriod;
return result; // it must never be called with 0 periods
}
private void updatePosition() {
if (stockTimeSeries != null && orderReceiver != null) {
if (stockTimeSeries.size() < 2) {
if (stockTimeSeries.size() < 1 || ignoreLastPeriod) {
return;
}
}
ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>> shortTail = (ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>) shortAveragingTail.transform(stockTimeSeries);
ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>> longTail = (ISeries<Date, Double, ? extends ISeriesPoint<Date, Double>>) longAveragingTail.transform(stockTimeSeries);
double shortAverageResult = calculateMovingAverage(shortTail, ignoreLastPeriod);
double longAverageResult = calculateMovingAverage(longTail, ignoreLastPeriod);
int deltaPosition = 0;
if (shortAverageResult < longAverageResult) {
deltaPosition = -positionSize - position;
} else if (shortAverageResult > longAverageResult) {
deltaPosition = positionSize - position;
}
if (deltaPosition != 0) {
try {
OrderBean order = new OrderBean(stockTimeSeries.getContract(), deltaPosition > 0 ? OrderSide.BUY : OrderSide.SELL, OrderType.MARKET, Math.abs(deltaPosition), 0.0, 0.0);
String orderId = orderReceiver.sendOrder(order);
order.setId(orderId);
position += deltaPosition;
} catch (Exception e) {
logger.log(Level.SEVERE, "Error while adding delta position: " + deltaPosition, e);
}
}
}
}
}