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