Overall Statistics |
Total Trades 96 Average Win 0.48% Average Loss -0.09% Compounding Annual Return 9.423% Drawdown 0.900% Expectancy 0.576 Net Profit 2.540% Sharpe Ratio 1.815 Probabilistic Sharpe Ratio 71.167% Loss Rate 75% Win Rate 25% Profit-Loss Ratio 5.31 Alpha 0.028 Beta 0.116 Annual Standard Deviation 0.028 Annual Variance 0.001 Information Ratio -1.79 Tracking Error 0.082 Treynor Ratio 0.438 Total Fees $240.00 Estimated Strategy Capacity $1400000.00 Lowest Capacity Asset SPY XQ55MD94802U|SPY R735QTJ8XC9X |
using System; using QuantConnect.Data; using QuantConnect.Securities; using QuantConnect.Securities.Option; using QuantConnect.Securities.Option.StrategyMatcher; namespace QuantConnect { public class SpyATMSpread: QCAlgorithm { const int INITIAL_CAPITAL = 50000; //baseline capital const int NUM_CONTRACTS = 10; //total number of contracts per position private decimal lastBuyPrice = 0; //price of last transaction buy side private decimal lastSellPrice = 0; //price of last transaction sell side private decimal posPrice = 0; //purchase price of position private decimal PortfolioVal = 0; private decimal OpenPrice = 0; //open price of day, used later private decimal FirstCandleHighPrice = 0; private decimal FirstCandleLowPrice = 0; private decimal SecondCandleClosePrice = 0; private Symbol spySymbol; private List<OptionContract> Contracts; //contracts updated each min public override void Initialize() { SetStartDate(2021,4, 5); //start date SetEndDate(2021, 7, 15); //end date SetCash(INITIAL_CAPITAL); SetWarmUp(TimeSpan.FromDays(30), Resolution.Minute); Option option = (Option) null; option = AddOption("SPY", Resolution.Minute); //add spy option to universe option.SetFilter(universe => from symbol in universe //filter out to 3 days of options .WeeklysOnly() .Strikes(-20,20) .Expiration(TimeSpan.Zero, TimeSpan.FromDays(3)) select symbol); spySymbol = option.Symbol; option.PriceModel = OptionPriceModels.BjerksundStensland(); // var optionMuliplier = option.ContractMultiplier; Schedule.On( DateRules.Every(DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday), TimeRules.At(10,02), () => ExecuteTrade(spySymbol,0, NUM_CONTRACTS, "") ); Schedule.On( DateRules.EveryDay(), TimeRules.At(9,30), () => PortfolioVal = Portfolio.TotalPortfolioValue); //Schedule.On( DateRules.MonthEnd(), TimeRules.At(9,00), ListHoldings ); //Schedule.On( DateRules.EveryDay(), TimeRules.At(9,30), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue)); //Schedule.On( DateRules.EveryDay(), TimeRules.At(13,58), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue)); //Schedule.On( DateRules.EveryDay(), TimeRules.At(14,02), () => Debug("Time: " + Time + "Total Value: " + Portfolio.TotalPortfolioValue)); } public override void OnData(Slice slice) { OptionChain chain; CaptureProfitLoss(); if ( slice.OptionChains.TryGetValue(spySymbol, out chain) ) { if (chain.Count() == 0) return; //available contracts to build spread Contracts = chain .OrderBy(c => c.Expiry) .ToList(); //Debug("PutContractsSize: " + put_Contracts.Count()); if (Contracts.Count == 0) return; } else { return; } //record first 15 min high and low. if (Time.Hour == 9 && Time.Minute == 31) { FirstCandleLowPrice = Securities[spySymbol.Underlying].Price; FirstCandleHighPrice = FirstCandleLowPrice; OpenPrice = FirstCandleLowPrice; //Debug("Open Price: " + OpenPrice + " Time: " + Time); //record second candle high and low } else if (Time.Hour == 9 && Time.Minute > 30 && Time.Minute < 45) { if ( FirstCandleHighPrice < Securities[spySymbol.Underlying].Price) { FirstCandleHighPrice = Securities[spySymbol.Underlying].Price; } else if ( FirstCandleLowPrice > Securities[spySymbol.Underlying].Price) { FirstCandleLowPrice = Securities[spySymbol.Underlying].Price; } //Debug("First Candle: " + FirstCandleClosePrice + " Time: " + Time); //close price of second candle } else if (Time.Hour == 10 && Time.Minute == 0) { SecondCandleClosePrice = Securities[spySymbol.Underlying].Price; //Debug("Second Candle: " + SecondCandleClosePrice + " Time: " + Time); } else if ( Time.Hour == 11 ) { FirstCandleHighPrice = 0; SecondCandleClosePrice = 0; } if(IsMarketOpen(spySymbol)) { if(Time.Hour == 15 && Time.Minute == 30) { Liquidate(null, "End of Day Close"); } } } public override void OnOrderEvent(OrderEvent orderEvent){ var order = Transactions.GetOrderById(orderEvent.OrderId); var quan = orderEvent.FillQuantity; var fillPrice = orderEvent.FillPrice; var sym = orderEvent.Symbol; //detect order purchase pricing if (orderEvent.Status != OrderStatus.Filled){ //Debug("Order failed: " + sym + " Time: " + Time + " Quan: " + orderEvent.Quantity + " Portfolio: " + Portfolio.TotalPortfolioValue); return; } else { if (orderEvent.Direction == OrderDirection.Buy) { lastBuyPrice = fillPrice; } else if (orderEvent.Direction == OrderDirection.Sell) { lastSellPrice = fillPrice; } //ListHoldings(); //Debug("Order failed: " + sym + " Time: " + Time + " Quan: " + orderEvent.Quantity + " UnderlyingPrice: " + Securities[sym.Underlying].Price + " Portfolio: " + Portfolio.TotalPortfolioValue); } } public void ExecuteTrade(Symbol sym, int duration, int quan, string tag) { decimal CurPrice = Securities[ spySymbol.Underlying ].Price; //use linq to create possible order if (Contracts != null && Contracts.Count != 0 ) { var spread = (from short_p in Contracts join short_c in Contracts on short_p.UnderlyingSymbol equals short_c.UnderlyingSymbol join long_p in Contracts on short_p.UnderlyingSymbol equals long_p.UnderlyingSymbol join long_c in Contracts on short_p.UnderlyingSymbol equals long_c.UnderlyingSymbol where short_p.Expiry == short_c.Expiry && short_p.Expiry == long_p.Expiry && long_p.Expiry == long_c.Expiry && (short_p.Expiry.Date-Time.Date).Days == duration && short_p.Strike == Math.Floor(CurPrice) && //just out of the money puts short_c.Strike == Math.Ceiling(CurPrice) && //just out of the money calls long_p.Strike == short_p.Strike - 5 && //5 point wide spread long_c.Strike == short_c.Strike + 5 && //5 point wide spread short_p.Right == OptionRight.Put && long_p.Right == OptionRight.Put && long_c.Right == OptionRight.Call && short_c.Right == OptionRight.Call orderby Math.Abs((short_p.Expiry.Date-Time.Date).Days - duration), Math.Abs(short_p.Strike - CurPrice) select new Tuple<OptionContract, OptionContract, OptionContract, OptionContract>( short_p, long_p, short_c, long_c ) ) .FirstOrDefault(); //place an order if high low criteria met if ( IsMarketOpen(sym) ) { if (spread != null) { if (spread.Item1 != null && spread.Item2 != null && spread.Item3 != null && spread.Item4 != null) { //build order for put spread, bullish if (SecondCandleClosePrice > FirstCandleHighPrice) { Order(spread.Item1.Symbol, -quan, false, ("Price " + CurPrice + " Low: " + FirstCandleLowPrice + " High: " + FirstCandleHighPrice) ); Order(spread.Item2.Symbol, quan ); posPrice = lastSellPrice - lastBuyPrice; } //build order for call spread, bearish else if (SecondCandleClosePrice < FirstCandleLowPrice ) { Order(spread.Item3.Symbol, -quan, false, ("Price " + CurPrice + " Low: " + FirstCandleLowPrice + " High: " + FirstCandleHighPrice) ); Order(spread.Item4.Symbol, quan ); posPrice = lastSellPrice - lastBuyPrice; } } else { Debug("No Contracts in field"); } Debug("Position Price: " + posPrice); } } } } public void CaptureProfitLoss(){ Dictionary<Symbol, SecurityHolding> option_invested = Portfolio .Where(x => x.Value.Invested == true && x.Value.Type == SecurityType.Option ) .OrderBy(x => x.Key.ID.StrikePrice) .Reverse().ToDictionary(); decimal value = 0; decimal profit = 0; decimal credit = 0; decimal cost = 0; decimal pandl = 0; //check if we have open positions if (option_invested.Count > 0) { //iterate through option holdings foreach(var pos in option_invested) { pandl += Portfolio[pos.Key].TotalCloseProfit(); if (Portfolio[pos.Key].IsShort) { value += Portfolio[pos.Key].Price; cost += Portfolio[pos.Key].AveragePrice; } else if (Portfolio[pos.Key].IsLong) { value -= Portfolio[pos.Key].Price; cost -= Portfolio[pos.Key].AveragePrice; } //profit += Portfolio[pos.Key].UnrealizedProfit; //cost += Portfolio[pos.Key].HoldingsCost; //value += Portfolio[pos.Key].TotalCloseProfit(); } //close for profit at 50% if ( cost >= value * 2M ){ //Debug("Take Profit " + Time); Liquidate(null, "Take Profit"); } // close for loss at ~70 loss else if ( pandl <= -70M ) { //Debug("Stop loss: " + Time); Liquidate(null, "Stop Loss" ); } //Debug("Position Price: " + posPrice + " Value: " + value); } } //Are you holding any options of symbol public bool isInvested(Symbol sym) { Dictionary<Symbol, SecurityHolding> option_invested = Portfolio .Where(x => x.Value.Symbol.Underlying == sym.Underlying && x.Value.Type == SecurityType.Option && x.Value.Invested == true) .ToDictionary(); if (option_invested.Count() > 0) return true; return false; } public bool isMarketOpenDay(Symbol sym, int d) { var hours = Securities[sym.Canonical].Exchange.Hours; var todayClose = hours.GetNextMarketClose(Time.Date, false); if (hours.IsDateOpen(Time.Date.AddDays(d))){ var tomorrowOpen = hours.GetNextMarketOpen(Time.Date.AddDays(d), false); var tomorrowClose = hours.GetNextMarketClose(Time.Date.AddDays(d), false); return true; } return false; } public void ListHoldings() { Dictionary<Symbol, SecurityHolding> option_invested = Portfolio .Where(x => x.Value.Invested == true ) .OrderBy(x => x.Key) .Reverse().ToDictionary(); foreach(KeyValuePair<Symbol, SecurityHolding> entry in option_invested) { Log(entry.Value.Symbol + " Quan: " + entry.Value.Quantity + " Margin: " + Portfolio.MarginRemaining + " Time: " + Time); } Log("____________"); } public decimal MidPrice(OptionContract s, OptionContract l) { return ((s.AskPrice - l.AskPrice) + (s.BidPrice - l.BidPrice)) / 2; } } }