| Overall Statistics |
|
Total Trades 540 Average Win 0.01% Average Loss -0.02% Compounding Annual Return -44.726% Drawdown 4.500% Expectancy -0.840 Net Profit -4.550% Sharpe Ratio -9.943 Probabilistic Sharpe Ratio 0% Loss Rate 89% Win Rate 11% Profit-Loss Ratio 0.49 Alpha 0 Beta 0 Annual Standard Deviation 0.034 Annual Variance 0.001 Information Ratio -9.943 Tracking Error 0.034 Treynor Ratio 0 Total Fees $83.73 Estimated Strategy Capacity $0 Lowest Capacity Asset TSLA UNU3P8Y3WFAD |
namespace QuantConnect.Algorithm.CSharp
{
public class InterExchangeArbitrageAlgorithm : QCAlgorithm
{
private Symbol _symbol;
private decimal _fees;
private ExchangeMapper _exchangeMapper = new ExchangeMapper();
private DateTime ptime = DateTime.MinValue;
private AtreyuOrderProperties lowestSellorderProperties = new AtreyuOrderProperties();
private AtreyuOrderProperties highestBuyorderProperties = new AtreyuOrderProperties();
private decimal quantity;
public override void Initialize()
{
SetStartDate(2021, 1, 1);
SetEndDate(2021, 1, 31);
SetCash(100000);
SetBrokerageModel(BrokerageName.Atreyu);
var security = AddEquity("TSLA", Resolution.Tick);
security.SetFillModel(new CustomFillModel(this));
_symbol = security.Symbol;
_fees = 0.0015m; // Atreyu fees are $0.0015/share
}
public override void OnData(Slice data)
{
// Gather quote ticks for each exchange
var quotes = data.Ticks[_symbol].Where(tick => tick.TickType == TickType.Quote && _exchangeMapper.ContainsKey(tick.Exchange));
var sellQuotes = quotes.Where(quote => quote.AskPrice != 0);
var buyQuotes = quotes.Where(quote => quote.BidPrice != 0);
// Check if there are both buy and sell quotes
if (sellQuotes.Count() == 0 || buyQuotes.Count() == 0)
{
return;
}
var time = data.Time;
if (ptime != DateTime.MinValue)
{
TimeSpan diff = time - ptime;
int milliseconds = (int) diff.TotalMilliseconds;
if (milliseconds > 3 && milliseconds < 10)
{
var sSellQuote = sellQuotes.Where(quote => quote.Exchange == lowestSellorderProperties.Exchange);
if (sSellQuote.Count() == 0)
{
return;
}
var sBuyQuote = buyQuotes.Where(quote => quote.Exchange == highestBuyorderProperties.Exchange);
if (sBuyQuote.Count() == 0)
{
return;
}
MarketOrder(_symbol, quantity, true, $"{lowestSellorderProperties.Exchange}", lowestSellorderProperties);
MarketOrder(_symbol, -quantity, true, $"{highestBuyorderProperties.Exchange}", highestBuyorderProperties);
ptime = DateTime.MinValue;
}
if (milliseconds >= 10)
{
ptime = DateTime.MinValue;
}
return;
}
// Find lowest sell quote
var lowestSellQuote = sellQuotes.OrderBy(quote => quote.AskPrice).First();
// Find highest buy quote
var highestBuyQuote = buyQuotes.OrderByDescending(quote => quote.BidPrice).First();
// Check if there is an arbitrage opportunity
if (lowestSellQuote.AskPrice >= highestBuyQuote.BidPrice - _fees)
{
return;
}
// Determine order size
quantity = Math.Min(lowestSellQuote.AskSize, highestBuyQuote.BidSize);
quantity = Math.Min(quantity, CalculateOrderQuantity(_symbol, 1.0m));
// Buy from underpriced exchange
lowestSellorderProperties.Exchange = _exchangeMapper.getExchangeFromName(lowestSellQuote.Exchange);
// Sell on overpriced exchange
highestBuyorderProperties.Exchange = _exchangeMapper.getExchangeFromName(highestBuyQuote.Exchange);
ptime = time;
}
internal class CustomFillModel : FillModel
{
private readonly QCAlgorithm _algorithm;
private ExchangeMapper _exchangeMapper = new ExchangeMapper();
public CustomFillModel(QCAlgorithm algorithm)
: base()
{
_algorithm = algorithm;
}
public override OrderEvent MarketFill(Security asset, MarketOrder order)
{
var fill = new OrderEvent(order, order.Time, OrderFee.Zero);
var quotes = asset.Cache.GetAll<Tick>().Where(tick => tick.TickType == TickType.Quote && _exchangeMapper.ContainsKey(tick.Exchange));
if (order.Quantity > 0) // Buy order
{
// Set fill price to price of lowest sell quote
fill.FillPrice = quotes.Where(quote => quote.AskPrice != 0)
.OrderBy(quote => quote.AskPrice)
.First()
.AskPrice;
}
else // Sell order
{
// Set fill price to price of highest bid quote
fill.FillPrice = quotes.Where(quote => quote.BidPrice != 0)
.OrderByDescending(quote => quote.BidPrice)
.First()
.BidPrice;
}
fill.FillQuantity = order.Quantity;
fill.Status = OrderStatus.Filled;
return fill;
}
}
internal class ExchangeMapper
{
private Dictionary<string, Exchange> _exchangeByName = new Dictionary<string, Exchange>()
{
{"NASDAQ", Exchange.NASDAQ},
{"BATS", Exchange.BATS},
{"ARCA", Exchange.ARCA},
{"NYSE", Exchange.NYSE},
{"NSX", Exchange.NSX},
{"FINRA", Exchange.FINRA},
{"ISE", Exchange.ISE},
{"CSE", Exchange.CSE},
{"CBOE", Exchange.CBOE},
{"NASDAQ_BX", Exchange.NASDAQ_BX},
{"SIAC", Exchange.SIAC},
{"EDGA", Exchange.EDGA},
{"EDGX", Exchange.EDGX},
{"NASDAQ_PSX", Exchange.NASDAQ_PSX},
{"BATS_Y", Exchange.BATS_Y},
{"BOSTON", Exchange.BOSTON},
{"AMEX", Exchange.AMEX},
{"BSE", Exchange.BSE},
{"NSE", Exchange.NSE}
};
public Exchange getExchangeFromName(string name)
{
return _exchangeByName[name];
}
public bool ContainsKey(string key)
{
return _exchangeByName.ContainsKey(key);
}
}
}
}