| Overall Statistics |
|
Total Trades 1 Average Win 0% Average Loss 0% Compounding Annual Return 99.013% Drawdown 0.400% Expectancy 0 Net Profit 0.947% Sharpe Ratio 14.525 Probabilistic Sharpe Ratio 100% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0.312 Beta 0.192 Annual Standard Deviation 0.056 Annual Variance 0.003 Information Ratio -7.945 Tracking Error 0.226 Treynor Ratio 4.233 Total Fees $0.08 |
namespace QuantConnect {
public class OneCancelsOtherTicketSet
{
public OneCancelsOtherTicketSet(params OrderTicket[] orderTickets)
{
this.OrderTickets = orderTickets;
}
private OrderTicket[] OrderTickets { get; set; }
public void Filled()
{
// Cancel all the outstanding tickets.
foreach (var orderTicket in this.OrderTickets)
{
if (orderTicket.Status == OrderStatus.Submitted)
{
orderTicket.Cancel();
}
}
}
}
}namespace QuantConnect.Algorithm.CSharp
{
public class FX_Gap_Trader : QCAlgorithm
{
/* ****************************
MOST IMPORTANT PARAMS
*****************************/
private readonly decimal TradeSize = 2500;
private readonly List<string> symbolList = new List<string>()
{
"EURUSD"
};
int period = 1;
Resolution resolution = Resolution.Second;
/* ****************************
**************************** */
List<StockDataClass> stockDatas = new List<StockDataClass>();
decimal limitRatio = 0m;
int priceDecimals = 2;
TimeSpan orderExpiryTime = new TimeSpan(0,0,0,59);
// One cancels the other module
private OrderTicket EntryOrder { get; set; }
private Func<QCAlgorithm, string, decimal, OneCancelsOtherTicketSet> OnOrderFilledEvent { get; set; }
private OneCancelsOtherTicketSet ProfitLossOrders { get; set; }
public override void Initialize()
{
SetStartDate(2020, 5, 15); //Set Start Date
SetEndDate(2020, 5, 19);
SetCash(TradeSize * (symbolList.Count)); //Set Strategy Cash
SetBrokerageModel(BrokerageName.FxcmBrokerage);
foreach (string ticker in symbolList) // primary reference: https://www.quantconnect.com/forum/discussion/469/indicators-with-multiple-resolutions-for-multiple-symbols/p1
{
AddForex(ticker, resolution);
StockDataClass stockData = new StockDataClass(ticker);
stockData.rollingwindow = new RollingWindow<QuoteBar>(period);
stockData.OldPL = 0m;
stockData.NewPL= 0m;
// add the symbol's class members to the list of tickers
stockDatas.Add(stockData);
}
if (LiveMode)
{
// print daily P&L figures during live trading
//Schedule.On(DateRules.EveryDay("MLAB"), TimeRules.BeforeMarketClose("MLAB", 0), PrintPL);
}
// Trigger on FX market open at 5pm Sunday's EST
Schedule.On(DateRules.Every(DayOfWeek.Sunday), TimeRules.At(17, 0), () =>
{
Trade();
});
}
public override void OnData(Slice data)
{
for (int i = 0; i < stockDatas.Count; i++)
{
//Debug("stock datas element " + Datas[i].Ticker);
// if data contains key ticker in symbrolList and bar is not empty
if (data.Bars.ContainsKey(stockDatas[i].Ticker) && data.Bars[stockDatas[i].Ticker] != null)
{
// Add TradeBar to rolling window; return if not ready
stockDatas[i].rollingwindow.Add(data[stockDatas[i].Ticker]);
if (!stockDatas[i].rollingwindow.IsReady) return;
// Get Friday's close
if (Time.DayOfWeek == DayOfWeek.Friday && Time.Hour == 16 && Time.Minute == 59 && Time.Second == 59)
{
stockDatas[i].fri_close = stockDatas[i].rollingwindow[0].Close;
}
}
}
}
public void Trade()
{
for (int i = 0; i < stockDatas.Count; i++)
{
var history = History<QuoteBar>(stockDatas[i].Ticker,period,resolution);
foreach (var bar in history)
{
stockDatas[i].rollingwindow.Add(bar);
}
if (!stockDatas[i].rollingwindow.IsReady) return;
// Get Friday's closing price, should be at window index [1].Close
// Get Open price now, , should be at window index [0].Open
// Calculate Gap from Friday
decimal sun_open = stockDatas[i].rollingwindow[0].Open;
decimal fri_close = stockDatas[i].fri_close;
decimal gap = sun_open - fri_close;
// Trading rule
// Long if Gap <0; else short
if (gap < 0)
{
// buy market
TradeLong(stockDatas[i].Ticker,sun_open,fri_close);
}
else if (gap > 0)
{
// buy market
TradeShort(stockDatas[i].Ticker,sun_open,fri_close);
}
}
}
public void TradeLong(string ticker, decimal sun_open, decimal fri_close)
{
int quantity = (int)Math.Floor((TradeSize) / sun_open);
decimal gap = Math.Abs(sun_open - fri_close);
decimal target = fri_close;
//DefaultOrderProperties.TimeInForce = TimeInForce.Day;
// On order event status == filled
this.OnOrderFilledEvent = (algo, symbol, filledPrice) =>
{
// if filled, set stop loss and trailing stops as GTC
//DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled;
return new OneCancelsOtherTicketSet(
// if order placed, Stop loss as a multiple of gap below entry
algo.LimitOrder(symbol, -quantity, target, "Long Profit Target"),
algo.StopMarketOrder(symbol, -quantity, filledPrice - gap, "Long Stop Loss"));
// Set target equal to friday's close
};
if (EntryOpened(ticker, quantity) == false)
{
//DefaultOrderProperties.TimeInForce = TimeInForce.Day;
this.EntryOrder = MarketOrder(ticker, quantity);
}
}
public void TradeShort(string ticker, decimal sun_open, decimal fri_close)
{
decimal quantity = Math.Floor((TradeSize) / sun_open);
decimal gap = Math.Abs(sun_open - fri_close);
decimal target = fri_close;
// On order event status == filled
this.OnOrderFilledEvent = (algo, symbol, filledPrice) =>
{
// if filled, set stop loss and trailing stops as GTC
// DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled;
return new OneCancelsOtherTicketSet(
// if order placed, Stop loss as a multiple of gap below entry
// Set stop limit order 2x size of gap
algo.LimitOrder(symbol, quantity, target , "short Profit Target"),
algo.StopMarketOrder(symbol, quantity, filledPrice + gap, "short Stop Loss"));
// Set target equal to friday's close
};
if (EntryOpened(ticker, -quantity) == false)
{
//DefaultOrderProperties.TimeInForce = TimeInForce.Day;
this.EntryOrder = MarketOrder(ticker, - quantity);
}
}
public bool EntryOpened(string symbol, decimal quantity)
{
List<Order> orders = Transactions.GetOpenOrders(symbol);
foreach (Order order in orders)
{
if (order.Symbol == symbol)
{
if (Math.Sign(quantity) == Math.Sign(order.Quantity)) // fine for now but long short will need further5 filter
{
return true;
//Debug("Order already placed. order.Quantity: " + order.Quantity + " quantity: " + quantity + " price: " + price + "order.Price" + order.Price);
}
}
}
return false;
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (EntryOrder != null)
{
this.EntryOrder = null;
}
if (orderEvent.Status == OrderStatus.Filled || orderEvent.Status == OrderStatus.PartiallyFilled)
{
if (this.OnOrderFilledEvent != null)
{
this.ProfitLossOrders = OnOrderFilledEvent(this, orderEvent.Symbol, orderEvent.FillPrice);
OnOrderFilledEvent = null;
}
else if (this.ProfitLossOrders != null)
{
this.ProfitLossOrders.Filled();
this.ProfitLossOrders = null;
}
// Log round trip profits
Log(orderEvent.Symbol + " Last trade Profit = " + Portfolio[orderEvent.Symbol].LastTradeProfit);
}
}
public bool OrderIsPlaced(string symbol, decimal quantity)
{
List<Order> orders = Transactions.GetOpenOrders(symbol);
foreach (Order order in orders)
{
if (order.Symbol.Value == symbol)
{
if (Math.Sign(quantity) == Math.Sign(order.Quantity))
{
return true;
}
}
}
return false;
}
// Backtesting stats
public override void OnEndOfAlgorithm()
{
// Dicts for total stats
Dictionary<string, decimal> CumPL = new Dictionary<string, decimal>(); // Total P&L per symbol
for (int i = 0; i < stockDatas.Count; i++)
{
// Populate cumulative P&L dict
CumPL.Add(stockDatas[i].Ticker,Math.Round(Portfolio[stockDatas[i].Ticker].NetProfit, priceDecimals));
int wins = 0;
int losers = 0;
decimal profits = 0m;
decimal losses = 0m;
decimal maxDD = 0m;
//Aggregate trades by symbol and figure out stats
foreach (var trade in TradeBuilder.ClosedTrades)
{
// wins & losses
if (trade.Symbol == stockDatas[i].Ticker)
{
if (trade.ProfitLoss > 0)
{
wins += 1;
profits += trade.ProfitLoss;
}
else
{
losers += 1;
losses += trade.ProfitLoss;
// max drawdown
if (trade.ProfitLoss < maxDD)
maxDD = trade.ProfitLoss;
}
}
}
if (wins != 0)
{
Log("Number of round trip trades for " +stockDatas[i].Ticker + ": " + (wins + losers));
// Calc win rate
Log("Win rate for " +stockDatas[i].Ticker + ": " + Math.Round(Decimal.Divide(wins,(wins + losers))*100,priceDecimals) + "%");
// Average win average loss
Log("Average win for " +stockDatas[i].Ticker + ": " + Math.Round(profits/wins,priceDecimals));
Log("Average loss for " +stockDatas[i].Ticker + ": " + Math.Round(losses/wins,priceDecimals));
Log("Max drawdown for " +stockDatas[i].Ticker + ": " + Math.Round(maxDD,priceDecimals));
}
}
string CumPL_output = string.Join("; ", CumPL.Select(x => string.Join(" = $", x.Key, x.Value)));
string cumPL_report = ("Cumulative net profits since launch: " + Environment.NewLine + CumPL_output);
Log(cumPL_report);
}
public class StockDataClass
{
public string Ticker;
public RollingWindow<QuoteBar> rollingwindow;
public StockDataClass(string ticker)
{
Ticker = ticker;
}
public decimal fri_close;
public decimal sun_open;
// Performance metrics
public decimal OldPL;
public decimal NewPL;
}
}
}