Overall Statistics |
Total Trades 5 Average Win 0% Average Loss -2.89% Compounding Annual Return -28.067% Drawdown 35.600% Expectancy -1 Net Profit -23.165% Sharpe Ratio -0.852 Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.23 Beta 0.007 Annual Standard Deviation 0.273 Annual Variance 0.075 Information Ratio 0.445 Tracking Error 0.401 Treynor Ratio -33.52 Total Fees $14.21 |
using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Data.Custom; using QuantConnect.Data.Market; using System; using System.Collections; using System.Collections.Generic; using QuantConnect.Indicators; using QuantConnect.Data.Consolidators; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Orders.Fills; using QuantConnect.Securities; namespace QuantConnect { /* * QuantConnect University: Full Basic Template: * * The underlying QCAlgorithm class is full of helper methods which enable you to use QuantConnect. * We have explained some of these here, but the full algorithm can be found at: * https://github.com/QuantConnect/QCAlgorithm/blob/master/QuantConnect.Algorithm/QCAlgorithm.cs */ public class BasicTemplateAlgorithm : QCAlgorithm { //Initializers: int onDataTick = 0; int logTick = 71000; // DateTime logDate = new DateTime(2008, 9, 22); double dayCount = 0; bool entryPoint = false; int timeTick = 0; int timeTickMod; TradeBars prices = new TradeBars(); ArrayList symbols = new ArrayList(); DataDictionary<CreditDefaultSwap> swaps = new DataDictionary<CreditDefaultSwap>(); int sellcount = 0; Chart plotter = new Chart("Strategy Equity"); //Init data and resolution for strategy: public override void Initialize() { //Start and End Date range for the backtest: SetStartDate(2008, 1, 5); timeTickMod = 2000; SetEndDate(2008, 10, 22); //SetEndDate(DateTime.Now.Date.AddDays(-400)); //SetEndDate(DateTime.Now.Date.AddDays(-1)); //Cash allocation SetCash(100000); symbols.Add("XOM"); symbols.Add("CVX"); // XOM/CVX 2yr 65% 24,1,68,32 | 14,77,52,48 | 14,1,78,22 symbols.Add("HAL"); //symbols.Add("GLOP"); //symbols.Add("LNG"); // SMLP //symbols.Add("APC"); // ANADARKO //symbols.Add("DVN"); // devon //symbols.Add("ECA"); // encana //symbols.Add("HAL"); //symbols.Add("BHI"); //symbols.Add("MTRX"); //symbols.Add("SMLP"); foreach (string symbol in symbols) { AddSecurity(SecurityType.Equity, symbol, Resolution.Minute); //Securities[symbol].SlippageModel = new ConstantSlippageModel(0.001m); Securities[symbol].SetLeverage(1m); // we are not using leverage } //plotter.AddSeries(new Series("SOXX", SeriesType.Line, 0)); // SeriesType.Scatter SeriesType.Flag Series trades = new Series("trade", SeriesType.Scatter, 0); trades.Color = System.Drawing.Color.LightCoral; plotter.AddSeries(trades); // , "$", System.Drawing.Color.LightCoral)); AddChart(plotter); } //Data Event Handler: New data arrives here. "TradeBars" type is a dictionary of strings so you can access it by symbol. public void OnData(TradeBars data) { // in OnData, returns outside of 9:30am - 4pm //if (Time.Hour < 9 || (Time.Hour == 9 && Time.Minute < 30)) return; //if ( Time.Hour >= 16) return; // "TradeBars" object holds many "TradeBar" objects: it is a dictionary indexed by the symbol: //Save price data: foreach (string symbol in data.Keys) { if (!prices.ContainsKey(symbol)) { prices.Add(symbol, data[symbol]); plotter.AddSeries(new Series(symbol, SeriesType.Line, 0)); Log("init symbol " + symbol); } else { prices[symbol] = data[symbol]; } } if ( (timeTick % 10000) == 0 ) { LogHoldings("status"); } timeTick++; // initial (dumb) entryPoint... buy equal amounts of each stock if (!entryPoint) { entryPoint = true; int count = data.Keys.Count; foreach (string entrySym in data.Keys) { Order(entrySym, 99000 / (count * data[entrySym].Price)); LogHoldings("entry"); } } decimal sellQuant = 30000; TradeBar targ = data["XOM"]; TradeBar r1 = data["HAL"]; if ( onDataTick > logTick ) { string reason = "SellHi " + targ.Symbol + " p " + String.Format("{0:0.00}", targ.Price) + " q " + sellQuant + " B " + r1.Symbol + "@" + String.Format("{0:0.00}", r1.Price); CreditDefaultSwap(targ, sellQuant, r1, reason); } targ = data["CVX"]; r1 = data["HAL"]; if ( onDataTick > logTick ) { string reason = "SellHi " + targ.Symbol + " p " + String.Format("{0:0.00}", targ.Price) + " q " + sellQuant + " B " + r1.Symbol + "@" + String.Format("{0:0.00}", r1.Price); CreditDefaultSwap(targ, sellQuant, r1, reason); } ArrayList removeKeys = new ArrayList(); foreach (string swapkey in swaps.Keys) { CreditDefaultSwap swap = swaps[swapkey]; swap.buyme = prices[swap.buyme.Symbol]; swap.sellme = prices[swap.sellme.Symbol]; UpdateSell(swap); UpdateBuy(swap); if (swap.buyTicket == null && swap.sellQuant == 0) { LogHoldings("done " + swap.reason); Plot("Strategy Equity", "trade", prices[swap.buyme.Symbol].Close ); removeKeys.Add(swapkey); } } foreach (string swapkey in removeKeys) { swaps.Remove(swapkey); } if ( removeKeys.Count > 0 && swaps.Count > 0 ) { Log( "dead swaps? " + swaps.Count ); foreach (string swapkey in swaps.Keys) { CreditDefaultSwap swap = swaps[swapkey]; UpdateSell(swap); UpdateBuy(swap); Log( "Cancel dead swaps?" ); CancelSwapSynchronously(swap); } } onDataTick++; } // Requirements: // Jeremy Irons: "We are selling to willing buyers at the current fair market price." // To maximize profit, we must achieve these conflicting goals: // - sellme as quickly as possible using a limit order near the current price. // - once sellme limit order is filled, buyme as quickly as possible using a limit order. // - we must limit total slippage to 0.2%, which gives us 0.1% on the sell side and 0.1% on the buy side. // - if slippage > 0.2% this strategy fails, since we are making ~50 swaps per year. // ( 50 swaps per year * 0.2% equates to 10% ) // - this strategy fails when spreads are high, so if this is a small/mid cap stock, // can only trade from 9:30 to 9:35, or 3:55 to 3:58, or during high volume periods when spread is low. // - It is imperative that we are long every day at 3:59, otherwise we miss out on upside news. // - limit orders are executed asynchronously so each phase must be monitored on a tick-by-tick basis void CreditDefaultSwap(TradeBar sellme, decimal sellQuant, TradeBar buyme, string reason) { foreach ( string swapKey in swaps.Keys ) { CreditDefaultSwap inplay = swaps[swapKey]; if ( (sellme.Symbol == inplay.buyme.Symbol) || (buyme.Symbol == inplay.sellme.Symbol)) { Log( "???uhoh inplay inversion! S " + sellme.Symbol + " B " + buyme.Symbol ); return; } if (inplay.buyme.Symbol != buyme.Symbol ) { if ( logTick > 0 && onDataTick >= logTick ) Log("inplay but buying different " + inplay.buyme.Symbol + " " + buyme.Symbol ); return; } if ( inplay.sellme.Symbol != sellme.Symbol ) { if ( logTick > 0 && onDataTick >= logTick ) Log("inplay but selling different " + inplay.sellme.Symbol + " " + sellme.Symbol ); return; } if ( logTick > 0 && onDataTick >= logTick ) Log("inplay " + inplay.sellme.Symbol + " " + inplay.buyme + " " + sellme.Symbol + " " + buyme.Symbol ); } CreditDefaultSwap swap = null; /////////////////// SELL phase of swap //////////////////////////// int remainQ = Portfolio.Securities[sellme.Symbol].Holdings.Quantity; if (remainQ >= 100) // short this dog { if (sellQuant > remainQ) sellQuant = remainQ; bool needNewSell = true; if (swaps.ContainsKey(sellme.Symbol)) swap = swaps[sellme.Symbol]; else swap = new CreditDefaultSwap(); swap.sellme = sellme; // self ref MUST be non-null swap.buyme = buyme; if (swap.sellTicket != null) { if (swap.sellTicket.Status == OrderStatus.Filled) { Log(" SQ continuing, sold " + swap.sellTicket.QuantityFilled + " " + sellme.Symbol + " " + String.Format("{0:0.00}", swap.sellTicket.AverageFillPrice)); swap.sellTicket = null; // tbd needs work // need to continue selling remaining shares } else needNewSell = false; } if (needNewSell) { swap.reason = reason; swap.sellPrice = sellme.Close * 0.999m; swap.sellQuant = sellQuant; swap.sellTicket = LimitOrder(sellme.Symbol, -(int)sellQuant, swap.sellPrice); swap.firstSellTick = 0; if (swap.sellTicket.Status == OrderStatus.Invalid) { swap.sellQuant = 0; swap.sellTicket = null; if (swap.buyTicket == null) swap = null; // abort this swap } else { if (!swaps.ContainsKey(sellme.Symbol)) swaps.Add(sellme.Symbol, swap); } } UpdateSell(swap); /* original moronic market order: Sell(targ.Symbol, sellQuant); sellcount = Time.Hour; */ } ////////////////////// BUY phase of swap ////////////////////////////////// // The buy phase is tricky... need to wait until the asynch sell phase // complete. int quantity = (int)Math.Floor((Portfolio.Cash - 100) / buyme.Price); bool needBuy = (quantity >= 100); // However, even if sell fails, we may have cash available so go long now! if (needBuy) { if (swaps.ContainsKey(sellme.Symbol)) { swap = swaps[sellme.Symbol]; } else { swap = new CreditDefaultSwap(); swap.buyPrice = buyme.Close * 1.001m; //if ( onDataTick > logTick ) Log( " buyPrice " + swap.buyPrice ); swaps.Add(sellme.Symbol, swap); } swap.reason = reason; swap.buyme = buyme; swap.sellme = sellme; } UpdateBuy(swap); if (swap != null && swap.buyQuant == 0 && swap.sellQuant == 0) { Log("done immediate? " + swap.reason); if (swaps.ContainsKey(sellme.Symbol)) swaps.Remove(sellme.Symbol); } /* original moronic market order if (quantity >= 100) Order(buyme.Symbol, quantity); */ } public void UpdateSell(CreditDefaultSwap swap) { if (swap == null) return; if (swap.sellQuant == 0) return; switch (swap.sellTicket.Status) { case OrderStatus.Canceled: case OrderStatus.Invalid: Log(" !!! Sell Cancelled or invalid " + swap.sellTicket.Status + " " + swap.sellQuant + " " + swap.reason); swap.sellTicket = null; swap.sellQuant = 0; // abort sell break; case OrderStatus.Filled: //if ( onDataTick > logTick ) Log(" SQ done " + swap.sellTicket.QuantityFilled + " " + swap.sellme.Symbol + " @ " + String.Format("{0:0.00}", swap.sellTicket.AverageFillPrice)); swap.sellQuant = 0; // "phase 1 complete. now on to phase 2... clean you two." break; default: // like fishing "whoa-oa-oa... you almost had it" // gets complicated... if (swap.tick == onDataTick) return; if (swap.firstSellTick == 0) swap.firstSellTick = onDataTick; swap.tick = onDataTick; int age = swap.tick - swap.firstSellTick; if (age == 0) return; // not done swap.tick = onDataTick; swap.sellPrice *= 0.999m; if (age > 2) { if (age > 4) { Log(" Sell CANCEL... " + swap.sellQuant + " " + swap.sellme.Symbol + " p " + String.Format("{0:0.00}", swap.sellPrice)); Log( "Cancel sell age > 4" ); CancelSwapSynchronously(swap); return; } else { //if ( onDataTick > logTick ) Log(" Sell fishing... " + swap.sellQuant + " " + swap.sellme.Symbol + " p " + String.Format("{0:0.00}", swap.sellPrice) ); } } swap.sellTicket.Update(new UpdateOrderFields { // we could change the quantity, but need to specify it //Quantity = LimitPrice = swap.sellPrice, Tag = "Update #" + (swap.sellTicket.UpdateRequests.Count + 1) }); break; } } public void UpdateBuy(CreditDefaultSwap swap) { if (swap == null) return; int quantity = (int)Math.Floor((Portfolio.Cash - 100) / (swap.buyme.Close * 1.001m)); quantity -= quantity % 100; bool needBuy = (quantity >= 100); if (needBuy) { swap.buyQuant = quantity; if (swap.buyTicket == null) { swap.buyPrice = swap.buyme.Close * 1.001m; // tbd need to update with fresh data swap.buyTicket = LimitOrder(swap.buyme.Symbol, (int)swap.buyQuant, swap.buyPrice); swap.firstBuyTick = 0; Log( "firstBuyTick " + swap.buyme.Symbol + " " + swap.buyPrice + " Q " + swap.buyQuant ); } } if (swap.buyTicket != null) { if ( logTick > 0 && onDataTick >= logTick ) { LogHoldings("B "); } switch (swap.buyTicket.Status) { case OrderStatus.Canceled: case OrderStatus.Invalid: Log(" !!! buy Cancelled or invalid " + swap.buyTicket.Status + " " + swap.buyQuant + " " + swap.reason); swap.buyQuant = 0; swap.buyTicket = null; break; case OrderStatus.Filled: //if ( onDataTick > logTick ) Log(" BQ " + swap.buyme.Symbol + " " + swap.buyQuant + " @ " + String.Format("{0:0.00}", swap.buyTicket.AverageFillPrice) ); swap.buyQuant = 0; swap.buyTicket = null; break; default: // gets complicated if (swap.tick == onDataTick) return; if (swap.firstBuyTick == 0) swap.firstBuyTick = onDataTick; swap.tick = onDataTick; int age = swap.tick - swap.firstBuyTick; if (age == 0) return; // not done swap.buyPrice *= 1.001m; Log( "buyTick " + swap.buyme.Symbol + " " + swap.buyPrice ); if (age > 2) { if (age > 4) { Log(" buy CANCEL... " + swap.buyQuant + " " + swap.buyme.Symbol + " p " + swap.buyPrice); CancelSwapSynchronously(swap); return; } } //if ( onDataTick > logTick ) Log(" buy fishing... " + swap.buyQuant + " " + swap.buyme.Symbol + " p " + swap.buyPrice); int newq = (int)Math.Floor((Portfolio.Cash - 100) / swap.buyPrice); if (newq >= 100) { newq -= newq % 100; } swap.buyTicket.Update(new UpdateOrderFields { // we could change the quantity, but need to specify it Quantity = newq, LimitPrice = swap.buyPrice, Tag = "Update #" + (swap.buyTicket.UpdateRequests.Count + 1) }); break; } } } public void CancelSwapSynchronously(CreditDefaultSwap swap) { while ( swap.buyTicket != null && swap.sellTicket != null ) { if ( swap.buyTicket != null ) { swap.buyTicket.Cancel(); swap.buyTicket.OrderClosed.WaitOne(1); switch ( swap.buyTicket.Status ) { case OrderStatus.Canceled: case OrderStatus.Invalid: Log(" !!! buy Cancelled or invalid " + swap.buyQuant + " " + swap.reason); swap.buyQuant = 0; swap.buyTicket = null; return; case OrderStatus.Filled: //if ( onDataTick > logTick ) Log(" BQ " + swap.buyme.Symbol + " " + swap.buyQuant + " @ " + String.Format("{0:0.00}", swap.buyTicket.AverageFillPrice) ); swap.buyQuant = 0; swap.buyTicket = null; return; default: Log("swap.buyTicket " + swap.buyTicket.Status ); break; } } if ( swap.sellTicket != null ) { swap.sellTicket.Cancel(); swap.sellTicket.OrderClosed.WaitOne(1); switch ( swap.sellTicket.Status ) { case OrderStatus.Canceled: case OrderStatus.Invalid: Log(" !!! sell Cancelled or invalid " + swap.sellQuant + " " + swap.reason); swap.sellQuant = 0; swap.sellTicket = null; return; case OrderStatus.Filled: //if ( onDataTick > logTick ) Log(" BQ " + swap.sellme.Symbol + " " + swap.sellQuant + " @ " + String.Format("{0:0.00}", swap.sellTicket.AverageFillPrice) ); swap.sellQuant = 0; swap.sellTicket = null; return; default: Log("swap.sellTicket " + swap.sellTicket.Status ); break; } } } } public void LogHoldings(string tag) { string logstr = ""; foreach ( string symbol in symbols ) { if ( Portfolio.Securities[symbol].Holdings.Quantity > 0 ) { logstr += symbol + " " + Portfolio.Securities[symbol].Holdings.Quantity + " @" + String.Format("{0:0.00}", prices[symbol].Price); } } Log(tag + " cash " + String.Format("{0:0.00}", Portfolio.Cash) + " margin " + String.Format("{0:0.00}", Portfolio.MarginRemaining) + " " + logstr); } public void OnData(Quandl data) { //Debug("quandl data " + data.Price); } public override void OnEndOfDay() { dayCount++; sellcount = 0; } } // swap one security for another. public class CreditDefaultSwap // mockingly... no relation to a real-world CDS. { public int firstBuyTick; public int firstSellTick; public int tick; public string reason; // for logging public TradeBar sellme; public decimal sellPrice; public decimal sellQuant; public OrderTicket sellTicket; public TradeBar buyme; public decimal buyPrice; public decimal buyQuant; public OrderTicket buyTicket; } }