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