Overall Statistics |
Total Trades 4 Average Win 2.10% Average Loss -3.18% Compounding Annual Return -52.441% Drawdown 80.500% Expectancy -0.170 Net Profit -79.968% Sharpe Ratio -0.444 Probabilistic Sharpe Ratio 0.250% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 0.66 Alpha -0.157 Beta -0.602 Annual Standard Deviation 0.593 Annual Variance 0.352 Information Ratio -0.659 Tracking Error 0.666 Treynor Ratio 0.437 Total Fees $5.00 Estimated Strategy Capacity $180000.00 Lowest Capacity Asset NVCR W4DLGUNCVQLH |
using System; using System.Linq; using QuantConnect.Data; using QuantConnect.Data.Market; using System.Collections.Generic; using QuantConnect.Securities.Option.StrategyMatcher; using QuantConnect.Orders; using QuantConnect.Securities.Option; using QuantConnect.Securities.Positions; using QuantConnect.Securities.Equity; namespace QuantConnect.Algorithm.CSharp.S89 { /// <summary> /// Regression algorithm exercising an equity Call Calendar Spread option strategy and asserting it's being detected by Lean and works as expected /// </summary> public class OptionEquityCallCalendarSpreadRegressionAlgorithmS89 : OptionEquityBaseStrategyRegressionAlgorithmS89 { /// <summary> /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. /// </summary> /// <param name="slice">Slice object keyed by symbol containing the stock data</param> public override void OnData(Slice slice) { if (equity != null) { this.RemoveSecurity(equity.Symbol); equity = null; } foreach(var kvp in ActiveSecurities) { if(kvp.Value.Type != SecurityType.Option) this.RemoveSecurity(kvp.Value.Symbol); } if (!Portfolio.Invested) { OptionChain chain; if (IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out chain)) { var contracts = chain .Where(contract => contract.Right == OptionRight.Call) .OrderBy(x => x.Expiry) .ThenBy(x => x.Strike) .ToList(); var shortCall = contracts.FirstOrDefault(); var longCall = contracts.FirstOrDefault(contract => contract.Expiry > shortCall.Expiry && contract.Strike == shortCall.Strike); if (shortCall == null || longCall == null) return; var initialMargin = Portfolio.MarginRemaining; MarketOrder(shortCall.Symbol, -10); AssertDefaultGroup(shortCall.Symbol, -10); MarketOrder(longCall.Symbol, +10); AssertOptionStrategyIsPresent(OptionStrategyDefinitions.CallCalendarSpread.Name, 10); var freeMarginPostTrade = Portfolio.MarginRemaining; var expectedMarginUsage = 0; // if (expectedMarginUsage != Portfolio.TotalMarginUsed) // { // throw new Exception("Unexpect margin used!"); // } // we payed the ask and value using the assets price var priceSpreadDifference = GetPriceSpreadDifference(shortCall.Symbol, longCall.Symbol); // if (initialMargin != (freeMarginPostTrade + expectedMarginUsage + _paidFees - priceSpreadDifference)) // { // throw new Exception("Unexpect margin remaining!"); // } } } } } /// <summary> /// Base class for equity option strategy regression algorithms which holds some basic shared setup logic /// </summary> public abstract class OptionEquityBaseStrategyRegressionAlgorithmS89 : QCAlgorithm { protected Equity equity; protected decimal _paidFees; protected Symbol _optionSymbol; public override void Initialize() { SetStartDate(2019, 02, 24); SetEndDate(2021, 04, 24); SetCash(200000); equity = AddEquity("NVCR", leverage: 4); var option = AddOption(equity.Symbol); _optionSymbol = option.Symbol; // set our strike/expiry filter for this option chain option.SetFilter(u => u.Strikes(-2, +2) // Expiration method accepts TimeSpan objects or integer for days. // The following statements yield the same filtering criteria .Expiration(0, 180)); } protected void AssertOptionStrategyIsPresent(string name, int? quantity = null) { if (Portfolio.PositionGroups.Where(group => group.BuyingPowerModel is OptionStrategyPositionGroupBuyingPowerModel) .Count(group => ((OptionStrategyPositionGroupBuyingPowerModel)@group.BuyingPowerModel).ToString() == name && (!quantity.HasValue || Math.Abs(group.Quantity) == quantity)) != 1) { throw new Exception($"Option strategy: '{name}' was not found!"); } } protected void AssertDefaultGroup(Symbol symbol, decimal quantity) { if (Portfolio.PositionGroups.Where(group => group.BuyingPowerModel is SecurityPositionGroupBuyingPowerModel) .Count(group => group.Positions.Any(position => position.Symbol == symbol && position.Quantity == quantity)) != 1) { throw new Exception($"Default groupd for symbol '{symbol}' and quantity '{quantity}' was not found!"); } } protected decimal GetPriceSpreadDifference(params Symbol[] symbols) { var spreadPaid = 0m; foreach (var symbol in symbols) { var security = Securities[symbol]; var actualQuantity = security.Holdings.AbsoluteQuantity; var spread = 0m; if (security.Holdings.IsLong) { if (security.AskPrice != 0) { spread = security.Price - security.AskPrice; } } else if (security.BidPrice != 0) { spread = security.BidPrice - security.Price; } spreadPaid += spread * actualQuantity * security.SymbolProperties.ContractMultiplier; } return spreadPaid; } /// <summary> /// Order fill event handler. On an order fill update the resulting information is passed to this method. /// </summary> /// <param name="orderEvent">Order event details containing details of the evemts</param> /// <remarks>This method can be called asynchronously and so should only be used by seasoned C# experts. Ensure you use proper locks on thread-unsafe objects</remarks> public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status == OrderStatus.Filled) { _paidFees += orderEvent.OrderFee.Value.Amount; if (orderEvent.Symbol.SecurityType.IsOption()) { var security = Securities[orderEvent.Symbol]; var premiumPaid = orderEvent.Quantity * orderEvent.FillPrice * security.SymbolProperties.ContractMultiplier; Log($"{orderEvent}. Premium paid: {premiumPaid}"); return; } } Log($"{orderEvent}"); } } }