| Overall Statistics |
|
Total Trades 423 Average Win 2.11% Average Loss -1.17% Compounding Annual Return 34.669% Drawdown 20.700% Expectancy 0.612 Net Profit 295.988% Sharpe Ratio 1.996 Probabilistic Sharpe Ratio 93.821% Loss Rate 43% Win Rate 57% Profit-Loss Ratio 1.81 Alpha 0.338 Beta 0.203 Annual Standard Deviation 0.185 Annual Variance 0.034 Information Ratio 0.896 Tracking Error 0.237 Treynor Ratio 1.828 Total Fees $709.88 |
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Parameters;
using QuantConnect.Brokerages;
using QuantConnect.Indicators;
using QuantConnect.Securities;
using QuantConnect.Orders;
using MathNet.Numerics.Statistics;
using QuantConnect.Data.Market;
using Newtonsoft.Json.Serialization;
using MathNet.Numerics;
#pragma warning disable CA1305 // Specify IFormatProvider
#pragma warning disable CS0618 // Type or member is obsolete
namespace QuantConnect.Algorithm.CSharp.v5
{
public enum Positions
{
Flat,
Long,
Short,
BuynHold
};
public partial class PairsTradingAlgorithm : QCAlgorithm
{
private readonly string[] _tickers = new string[] { "SPY", "QQQ" };
private readonly string _tickerBnH = "QQQ";
[Parameter("cash")]
private readonly int _cash = 30000;
[Parameter("tradeCash")]
private readonly int _tradeCash = 30000;
[Parameter("windowSlow")]
private readonly int _windowSlow = 50;
[Parameter("takeProfitParameter")]
private readonly int _takeProfitParameter = 1;
[Parameter("stopLossParameter")]
private readonly int _stopLossParameter = 3;
private decimal _takeProfitPercent;
private decimal _stopLossPercent;
private string _url = "http://157.230.29.134:42067";
private bool _stopTrading = false;
public override void Initialize()
{
SetStartDate(2016, 1, 1);
SetEndDate(2021, 1, 1);
SetCash(_cash);
SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage);
InitProperties();
InitSecurities();
InitHoldings();
InitSchedules();
InitCharts();
Warmup();
}
public override void OnData(Slice data)
{
int time = data.Time.ToString("HHmm").ToInt32();
string date = data.Time.ToString("dd.MM.yyyy");
string dt = DT(data.Time);
LastDT = dt;
if (LiveMode)
{
string newPosition = Download(_url + "/set_position");
if (newPosition == "Flat")
{
_stopTrading = true;
Flat(data.Time);
}
string trade = Download(_url + "/trade");
if (trade == "true")
{
_stopTrading = false;
}
else
{
_stopTrading = true;
}
Download(_url + "/position?dt=" + dt + "&direction=" + Position); // report position
}
if (!SliceContainsData(data)) { return; }
var plotSpread = Math.Round(Spread(data), 2);
if (NextPosition == Positions.Long && !HoldStock())
{
LogNewPosition(dt, Position, Positions.Long);
NextPosition = Positions.Flat;
Position = Positions.Long;
LastPosition["Long"] = date;
Plot("Chart", "Long", plotSpread);
if (LiveMode) { Download(_url + "/position?dt=" + dt + "&direction=" + Position); }
ClearOrders();
var closePrice = data[Symbols[1]].Close;
var holdings = Portfolio[Symbols[1]].Quantity;
var quantity = CalculateOrderQuantity(Symbols[1], 1.0m);
var stopOrdersQuantity = holdings == 0 ? Math.Abs(quantity) : Math.Abs(CalculateOrderQuantity(Symbols[1], 0m));
var longOrder = MarketOrder(Symbols[1], quantity);
var takeProfit = LimitOrder(Symbols[1], -stopOrdersQuantity, Math.Round(closePrice * (1 + _takeProfitPercent), 2));
var stopLoss = StopMarketOrder(Symbols[1], -stopOrdersQuantity, Math.Round(closePrice * (1 - _stopLossPercent), 2));
StopOrders.Add(takeProfit.OrderId);
StopOrders.Add(stopLoss.OrderId);
}
if (NextPosition == Positions.Short && !HoldStock())
{
LogNewPosition(dt, Position, Positions.Short);
NextPosition = Positions.Flat;
Position = Positions.Short;
LastPosition["Short"] = date;
Plot("Chart", "Short", plotSpread);
if (LiveMode) { Download(_url + "/position?dt=" + dt + "&direction=" + Position); }
ClearOrders();
var closePrice = data[Symbols[0]].Close;
var holdings = Portfolio[Symbols[0]].Quantity;
var quantity = CalculateOrderQuantity(Symbols[0], -1.0m);
var stopOrdersQuantity = holdings == 0 ? Math.Abs(quantity) : Math.Abs(CalculateOrderQuantity(Symbols[0], 0m));
var shortOrder = MarketOrder(Symbols[0], quantity);
var takeProfit = LimitOrder(Symbols[0], stopOrdersQuantity, Math.Round(closePrice * (1 - _takeProfitPercent), 2));
var stopLoss = StopMarketOrder(Symbols[0], stopOrdersQuantity, Math.Round(closePrice * (1 + _stopLossPercent), 2));
StopOrders.Add(takeProfit.OrderId);
StopOrders.Add(stopLoss.OrderId);
}
// On day end
if (time == 1600)
{
OnDayEnd(data);
}
// Every hour
if (time % 100 == 0)
{
OnHourData(data);
var spread = Math.Round(Spread(data), 2);
var momSpread = Math.Round(MomSpread, 2);
SpreadUT = Math.Round(SpreadMean + SpreadStd, 2);
SpreadLT = Math.Round(SpreadMean - SpreadStd, 2);
var HV = Tradeables.Select(x => Helpers.CalcHV(x.ATR, x.LastDailyClose)).ToArray().Average();
if (LiveMode)
{
// Log chart data
var message = dt + "_|_" + "_Sp_" + spread + "_|_" + "_Sp_UT_" + SpreadUT + "_|_" + "_Sp_LT_" + SpreadLT + "_|_" + "_Sm_" + momSpread + "_|_" + "_Sm_UT_" + MomSpreadUT + "_|_" + "_Sm_LT_" + MomSpreadLT;
Log(message);
Download(_url + "/last_log?s=" + message);
}
Plot("Chart", "Spread", spread);
Plot("Chart", "Spread UT", SpreadUT);
Plot("Chart", "Spread LT", SpreadLT);
Plot("Chart", "Mom Spread", momSpread);
Plot("Chart", "Mom Spread UT", MomSpreadUT);
Plot("Chart", "Mom Spread LT", MomSpreadLT);
Plot("Chart", "Price " + Symbols[0], data[Symbols[0]].Close);
Plot("Chart", "Price " + Symbols[1], data[Symbols[1]].Close);
Plot("Chart", "Momentum " + Symbols[0], Tradeables[0].MOM[Resolution.Hour]);
Plot("Chart", "Momentum " + Symbols[1], Tradeables[1].MOM[Resolution.Hour]);
Plot("Chart", "HV", HV);
Plot("Chart", "PearsonR", PearsonR);
if (_stopTrading || time == 1600) { return; }
//if (Tradeables.Select(x => data[x.Symbol].Close > x.SMA[20]).Contains(true) && Tradeables.Select(x => x.MOM[Resolution.Daily] > 0).Contains(true))
if (HV != 0 && HV < 0.01m)
{
var buynhold = true;
foreach (var order in Orders)
{
if (StopOrders.Contains(order.Id))
{
buynhold = false;
Flat(data.Time);
LastPosition["Long"] = date;
break;
}
}
if (buynhold && Position == Positions.Flat)
{
LogNewPosition(dt, Position, Positions.BuynHold);
Position = Positions.BuynHold;
//var quantity = CalculateOrderQuantity(Symbols[1], TradeableHoldingsPercent());
var quantity = CalculateOrderQuantity(Symbols[1], 1m);
MarketOrder(_tickerBnH, quantity);
}
}
else
{
if (Position == Positions.BuynHold)
{
Flat(data.Time);
return;
}
if (NextPosition != Positions.Flat) { return; }
// LONG
if (
(spread < SpreadLT && momSpread > MomSpreadUT)
||
(spread > SpreadUT && momSpread < MomSpreadUT && momSpread > MomSpreadLT)
||
(spread < SpreadLT && momSpread < MomSpreadLT)
)
{
if (Position == Positions.Flat || Position == Positions.Short && LastPosition["Short"] != date)
{
Flat(data.Time);
NextPosition = Positions.Long;
return;
}
}
// SHORT
if (
(spread > SpreadUT && momSpread > MomSpreadUT)
||
(spread < SpreadLT && momSpread < MomSpreadUT && momSpread > MomSpreadLT)
)
{
if (Position == Positions.Flat || Position == Positions.Long && LastPosition["Long"] != date)
{
Flat(data.Time);
NextPosition = Positions.Short;
return;
}
}
//// FLAT
if (Position != Positions.Flat && spread < SpreadUT && spread > SpreadLT) { Flat(data.Time); }
//if (Position == Positions.Long && momSpread < MomSpreadUT && momSpread > MomSpreadLT)
}
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (LiveMode && orderEvent.Status.IsFill() && orderEvent.Quantity != 0)
{
Download(
_url +
"/notifications/order" +
"?dt=" + LastDT +
"&symbol=" + orderEvent.Symbol +
"&price=" + orderEvent.FillPrice +
"&quantity=" + orderEvent.FillQuantity
);
}
if (!orderEvent.Status.IsClosed()) { return; }
if (StopOrders.Count() == 0) { return; }
var filledOrderId = orderEvent.OrderId;
if (StopOrders.Contains(filledOrderId)) { Flat(orderEvent.UtcTime); }
}
private bool IsLongCondition(decimal spread, decimal momSpread)
{
return (spread < SpreadLT && momSpread > MomSpreadUT) || (spread > SpreadUT && momSpread < MomSpreadUT && momSpread > MomSpreadLT);
}
private bool IsShortCondition(decimal spread, decimal momSpread)
{
return (spread > SpreadUT && momSpread > MomSpreadUT) || (spread < SpreadLT && momSpread < MomSpreadUT && momSpread > MomSpreadLT);
}
private void Flat(DateTime dateTime)
{
var dt = DT(dateTime);
LogNewPosition(dt, Position, Positions.Flat);
Position = Positions.Flat;
if (LiveMode) { Download(_url + "/position?dt=" + dt + "&direction=" + Position); }
ClearOrders();
foreach (var symbol in Symbols) { Liquidate(symbol); }
Liquidate(_tickerBnH);
}
private void ClearOrders()
{
StopOrders.Clear();
foreach (var order in Orders) { Transactions.CancelOrder(order.Id); }
}
private void OnHourData(Slice data)
{
foreach (var tradeable in Tradeables)
{
TradeBar tradeBar = data[tradeable.Symbol];
tradeable.MOM[Resolution.Hour].Update(tradeBar.Time, tradeBar.Close);
}
}
private void OnDayEnd(Slice data)
{
foreach (var tradeable in Tradeables)
{
TradeBar tradeBar = data[tradeable.Symbol];
tradeable.MOM[Resolution.Daily].Update(tradeBar.Time, tradeBar.Close);
tradeable.ATR.Update(tradeBar);
tradeable.LastDailyClose = tradeBar.Close;
foreach (var sma in tradeable.SMA) { sma.Value.Update(tradeBar.Time, tradeBar.Close); }
}
var spread = Spread(data);
SpreadMean.Update(data.Time, spread);
SpreadStd.Update(data.Time, spread);
MomSpreadMean.Update(data.Time, MomSpread);
MomSpreadStd.Update(data.Time, MomSpread);
var history = History(Symbols, _windowSlow, Resolution.Daily);
var prices = Symbols.Select(s => history.Select(d => d.ContainsKey(s) ? (double)d[s].Close : 0));
PearsonR = Correlation.Pearson(prices.First(), prices.Last());
}
private void LogNewPosition(string dt, Positions from, Positions to)
{
if (!LiveMode) { return; }
Download(
_url +
"/notifications/position" +
"?dt=" + dt +
"&from=" + from +
"&to=" + to +
"&symbolOne=" + Symbols[0] +
"&symbolTwo=" + Symbols[1] +
"&symbolOnePrice=" + Securities[Symbols[0]].Price +
"&symbolTwoPrice=" + Securities[Symbols[1]].Price
);
}
private Tradeable[] Tradeables { get; set; } = new Tradeable[2];
private Symbol[] Symbols { get { return Tradeables.Select(x => x.Symbol).ToArray(); } }
private Positions Position { get; set; } = Positions.Flat;
private Positions NextPosition { get; set; } = Positions.Flat;
private IEnumerable<Order> Orders { get { return Transactions.GetOpenOrders().Where(x => Symbols.Contains(x.Symbol)); } }
private List<int> StopOrders { get; set; } = new List<int>();
private decimal Spread(Slice data) { return Helpers.CalcStd(Symbols.Select(x => (double)data[x].Close)); }
private decimal SpreadUT { get; set; }
private decimal SpreadLT { get; set; }
private StandardDeviation SpreadStd { get; set; }
private SimpleMovingAverage SpreadMean { get; set; }
private decimal MomSpread { get { return Helpers.CalcStd(Tradeables.Select(x => (double)x.MOM[Resolution.Hour].Current.Value)); } }
private decimal MomSpreadUT { get { return Math.Round(MomSpreadMean + MomSpreadStd, 2); } }
private decimal MomSpreadLT { get { return Math.Round(MomSpreadMean - MomSpreadStd, 2); } }
private StandardDeviation MomSpreadStd { get; set; }
private SimpleMovingAverage MomSpreadMean { get; set; }
private string DT(DateTime dateTime) { return dateTime.ToString("HH:mm_dd.MM.yyyy"); }
private string LastDT { get; set; }
private Dictionary<string, string> LastPosition { get; set; } = new Dictionary<string, string>() { { "Long", "" }, { "Short", "" } };
private double PearsonR { get; set; }
private decimal TradeableHoldingsPercent() { return Math.Round(Math.Min(Portfolio.TotalPortfolioValue, _tradeCash) / Portfolio.TotalPortfolioValue, 3); }
private decimal EqualWeightedHoldingsPercent() { return Math.Round(TradeableHoldingsPercent() / Securities.Keys.Count(), 3); }
private bool HoldStock() { return Portfolio[Symbols[0]].Quantity != 0 && Portfolio[Symbols[1]].Quantity != 0; }
private void InitSecurities()
{
for (var i = 0; i < _tickers.Count(); i++)
{
var equity = AddEquity(_tickers[i], Resolution.Minute, Market.USA);
equity.SetDataNormalizationMode(DataNormalizationMode.Raw);
Tradeables[i] = new Tradeable(this, equity.Symbol, _windowSlow);
}
var equityBnH = AddEquity(_tickerBnH, Resolution.Minute, Market.USA);
equityBnH.SetDataNormalizationMode(DataNormalizationMode.Raw);
}
private void InitHoldings()
{
foreach (var symbol in Symbols) { Debug(symbol + " " + Portfolio[symbol].Quantity); }
if (Portfolio[Symbols[1]].Quantity > 0) { Position = Positions.Long; }
if (Portfolio[Symbols[0]].Quantity < 0) { Position = Positions.Short; }
if (LiveMode) { Download(_url + "/started?position=" + Position); }
}
private void InitSchedules()
{
}
private void InitProperties()
{
SpreadStd = new StandardDeviation(_windowSlow);
SpreadMean = new SimpleMovingAverage(_windowSlow);
MomSpreadStd = new StandardDeviation(_windowSlow);
MomSpreadMean = new SimpleMovingAverage(_windowSlow);
_takeProfitPercent = _takeProfitParameter / 100m;
_stopLossPercent = _stopLossParameter / 100m;
}
private void InitCharts()
{
int i = 0;
Chart chart = new Chart("Chart");
chart.AddSeries(new Series("Spread", SeriesType.Line, i));
chart.AddSeries(new Series("Spread UT", SeriesType.Line, i));
chart.AddSeries(new Series("Spread LT", SeriesType.Line, i));
chart.AddSeries(new Series("Long", SeriesType.Scatter, i));
chart.AddSeries(new Series("Short", SeriesType.Scatter, i));
chart.AddSeries(new Series("Flat", SeriesType.Scatter, i));
i++;
chart.AddSeries(new Series("Mom Spread", SeriesType.Line, i));
chart.AddSeries(new Series("Mom Spread UT", SeriesType.Line, i));
chart.AddSeries(new Series("Mom Spread LT", SeriesType.Line, i));
i++;
chart.AddSeries(new Series("Price " + Symbols[0], SeriesType.Line, i));
i++;
chart.AddSeries(new Series("Momentum " + Symbols[0], SeriesType.Line, i));
i++;
chart.AddSeries(new Series("Price " + Symbols[1], SeriesType.Line, i));
i++;
chart.AddSeries(new Series("Momentum " + Symbols[1], SeriesType.Line, i));
i++;
chart.AddSeries(new Series("HV", SeriesType.Line, i));
i++;
chart.AddSeries(new Series("PearsonR", SeriesType.Line, i));
AddChart(chart);
}
private void Warmup()
{
IEnumerable<Slice> historyHour = History(Symbols, _windowSlow * 7, Resolution.Hour);
foreach (Slice data in historyHour)
{
int time = data.Time.ToString("HHmm").ToInt32();
OnHourData(data);
if (time == 1600)
{
OnDayEnd(data);
}
}
}
private bool SliceContainsData(Slice data)
{
return data.ContainsKey(Symbols[0]) &&
data.ContainsKey(Symbols[1]) &&
data[Symbols[0]] != null &&
data[Symbols[1]] != null &&
data[Symbols[0]].Close != null &&
data[Symbols[1]].Close != null;
}
}
public class Tradeable
{
private readonly QCAlgorithm _algorithm;
private readonly int _window;
public Tradeable(QCAlgorithm algorithm, Symbol symbol, int window)
{
_algorithm = algorithm;
_window = window;
Symbol = symbol;
LastDailyClose = 0;
MOM = new Dictionary<Resolution, Momentum>()
{
{ Resolution.Hour, new Momentum(Math.Min(10, _window)) },
{ Resolution.Daily, new Momentum(Math.Min(10, _window)) },
};
SMA = new Dictionary<int, SimpleMovingAverage>()
{
{ 20, new SimpleMovingAverage(20) },
{ 50, new SimpleMovingAverage(50) },
{ 200, new SimpleMovingAverage(200) },
};
ATR = new AverageTrueRange(Math.Min(10, _window));
}
public Symbol Symbol { get; private set; }
public decimal LastDailyClose { get; set; }
public Dictionary<Resolution, Momentum> MOM { get; set; }
public Dictionary<int, SimpleMovingAverage> SMA { get; set; }
public AverageTrueRange ATR { get; set; }
}
public static class Helpers
{
public static decimal CalcStd(IEnumerable<double> values)
{
double std = 0;
if (values.Count() > 1)
{
double avg = values.Average();
double sum = values.Sum(d => Math.Pow(d - avg, 2));
std = Math.Sqrt((sum) / (values.Count() - 1));
}
decimal stdDev = Convert.ToDecimal(std);
return stdDev;
}
public static decimal CalcHV(AverageTrueRange atr, decimal close)
{
if (!atr.IsReady || close == 0) { return 0; }
var hv = atr / close;
return hv;
}
}
}
#pragma warning restore CA1305 // Specify IFormatProvider
#pragma warning restore CS0618 // Type or member is obsolete