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