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