Overall Statistics
Total Trades
1770
Average Win
0.43%
Average Loss
-0.20%
Compounding Annual Return
-4.672%
Drawdown
59.100%
Expectancy
-0.987
Net Profit
-34.947%
Sharpe Ratio
-0.004
Probabilistic Sharpe Ratio
0.046%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
2.09
Alpha
0
Beta
0
Annual Standard Deviation
0.28
Annual Variance
0.078
Information Ratio
-0.004
Tracking Error
0.28
Treynor Ratio
0
Total Fees
$5364.05
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Parameters;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;

namespace QuantConnect.Algorithm.CSharp
{
	public class NNNHedge : QCAlgorithm
	{
		// Parameters
		private const decimal MonthlyNOI = 35000m;
		private const int NumOfPayments = 165;
		private const double DiscountRate = 0.07 / 12;
		private const decimal ResidualUnderlyingValue = 0.50m;
		private const int TradedMaDays = 5;
		private const int StartingCash = 300000;
		private const decimal MaxPercentDailyVolume = 0.25m;
		private const int MinDaysToAccumulate = 5;

		private const int MinExpiryDays = 90;
		private const int MaxExpiryDays = 3*365;
		private const int LiquidateExpiryDays = 60;
		private const int AggLiquidateExpiryDays = 45;

		private const decimal MaxAskPrice = 0.15m;
		private const decimal MaxAbsoluteSpread = 0.05m;
		private const decimal MaxPercentSpread = 0.80m;
		
		private const int MaxOptionOrdersPerPeriod = 20;
		private const int MinContractsPerOrder = 5;
		
		[Parameter("ema-period")]
		private int EmaPeriod = 252;
		
		[Parameter("hdd-cutoff-mult")]
		private decimal HddCutoffMultiple = 3.0m;


		// WBA
		// QuantConnect data starts 6/1/2010. Ticker change from WAG to WBA 1/2015
		private static readonly DateTime Start = new DateTime(2013, 1, 1); //new DateTime(2011, 10, 7); //new DateTime(2010, 6, 1); // Missing data 4/30/11 - 10/5/11
		private static readonly DateTime End = new DateTime(2020, 12, 15);
		// private static readonly DateTime Start = new DateTime(2019, 1, 1);
		// private static readonly DateTime End = Start.AddMonths(1);
		private static readonly string UnderlyingTicker = "WBA";

		// AAPL
		// private static readonly DateTime Start = new DateTime(2014, 6, 6);
		// private static readonly DateTime End = new DateTime(2014, 6, 9);
		// private static readonly string UnderlyingTicker = "AAPL";
		
		// GOOG
		// private static readonly DateTime Start = new DateTime(2015, 12, 24);
		// private static readonly DateTime End = Start;
		// private static readonly string UnderlyingTicker = "GOOG";


		private static readonly Symbol OptionSymbol = QuantConnect.Symbol.Create(UnderlyingTicker, SecurityType.Option, Market.USA);
		private static readonly Symbol UnderlyingSymbol = QuantConnect.Symbol.Create(UnderlyingTicker, SecurityType.Equity, Market.USA);
		
		private static readonly TimeSpan TradeMaTimeSpan = TimeSpan.FromDays(TradedMaDays);
		private static readonly TimeSpan CyclePeriodTimeSpan = TimeSpan.FromMinutes(60);
	    private static readonly decimal NumPeriodsPerTradingDay = ((decimal)TimeSpan.FromHours(6).Ticks) / CyclePeriodTimeSpan.Ticks; // SOD doesn't start till ~9:40 and want to end before close
	    private static readonly decimal MaxHedgePerPeriod = Decimal.One / (NumPeriodsPerTradingDay * MinDaysToAccumulate); //Don't accumulate faster than spread over a month

	    private readonly Dictionary<DateTime, decimal> discountedMonthlyIncome = new Dictionary<DateTime, decimal>();
	    private readonly Dictionary<Symbol, decimal> averageVolume = new Dictionary<Symbol, decimal>();
	    private readonly Dictionary<int, OrderTicket> knownOrders = new Dictionary<int, OrderTicket>();
        private readonly Dictionary<DateTime, decimal> currentHedgeNotionalByExpiry = new Dictionary<DateTime, decimal>();
        private readonly Dictionary<Symbol, decimal> tradedVolume = new Dictionary<Symbol, decimal>();
        
        private decimal targetHedgeNotional = Decimal.Zero;
        private decimal currentHedgeNotional = Decimal.Zero;
        private DateTime noiScheduleDate = DateTime.MinValue;
        private DateTime minUnwindDate;
        private DateTime aggUnwindDate;
        private DateTime minExpiryDate;
        private DateTime maxExpiryDate;
        private bool isActive = false;
        private ExponentialMovingAverage hddMedianEma;
        private ExponentialMovingAverage hddAverageEma;
        

        public override void Initialize()
        {
	        Log($"Param, StartDate = {Start.ToShortDateString()}");
	        Log($"Param, EndDate = {End.ToShortDateString()}");
	        Log($"Param, MinExpiryDays = {MinExpiryDays}");
	        Log($"Param, MaxExpiryDays = {MaxExpiryDays}");
	        Log($"Param, MaxPercentDailyVolume = {MaxPercentDailyVolume}");
	        Log($"Param, StartingCash = {StartingCash}");
	        Log($"Param, MinDaysToAccumulate = {MinDaysToAccumulate}");
	        Log($"Param, LiquidateExpiryDays = {LiquidateExpiryDays}");
	        Log($"Param, AggLiquidateExpiryDays = {AggLiquidateExpiryDays}");
	        Log($"Param, MonthlyNOI = {MonthlyNOI}");
	        Log($"Param, NumOfPayments = {NumOfPayments}");
	        Log($"Param, DiscountRate = {DiscountRate * 12}");
	        Log($"Param, ResidualUnderlyingValue = {ResidualUnderlyingValue}");
	        Log($"Param, MaxAskPrice = {MaxAskPrice}");
	        Log($"Param, MaxAbsoluteSpread = {MaxAbsoluteSpread}");
	        Log($"Param, MaxPercentSpread = {MaxPercentSpread}");
	        Log($"Param, MaxOptionOrdersPerPeriod = {MaxOptionOrdersPerPeriod}");
	        Log($"Param, MinContractsPerOrder = {MinContractsPerOrder}");
	        Log($"Param, EmaPeriod = {EmaPeriod}");
	        Log($"Param, HddCutoffMultiple = {HddCutoffMultiple}");

	        Log("EOD, Hedge, Target, Exp Days, Exp Soon, Port Val, Cash, Fees, Hold Cost, Hold Val, Profit, Unrealized");
	        
	        hddMedianEma = new ExponentialMovingAverage("HD/$ Median EMA", EmaPeriod, ExponentialMovingAverage.SmoothingFactorDefault(EmaPeriod));
	        hddAverageEma = new ExponentialMovingAverage("HD/$ Average EMA", EmaPeriod, ExponentialMovingAverage.SmoothingFactorDefault(EmaPeriod));
	        
	        decimal prevIncome = 0.0m;
	        int period = 1;
	        DateTime startDate = new DateTime(Start.Year, Start.Month, 1);
	        DateTime currentDay = startDate.AddMonths(NumOfPayments);
	        while (currentDay >= startDate)
	        {
		        discountedMonthlyIncome[currentDay] = MonthlyNOI / Convert.ToDecimal(Math.Pow((1 + DiscountRate), period)) + prevIncome;
		        prevIncome = discountedMonthlyIncome[currentDay];
		        currentDay = currentDay.AddMonths(-1);
		        period++;
	        }
	        
	        DateTime warmupDatartDate = TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, Start.AddDays(-(EmaPeriod*370/250)), Start)
		        .Reverse().Take(EmaPeriod + 5).Last().Date;
	        SetStartDate(warmupDatartDate);
            SetEndDate(End);
            SetCash(StartingCash);
            SetWarmup(TimeSpan.FromDays(TradedMaDays));
            SetBenchmark(d => StartingCash);
            SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage);

			CustomFillModel fillModel = new CustomFillModel(this);
            SetSecurityInitializer(security => security.SetFillModel(fillModel));
            
            AddEquity(UnderlyingSymbol, Resolution.Daily, Market.USA, false);
            Option option = AddOption(UnderlyingSymbol, Resolution.Minute, Market.USA, false);
	        option.SetFilter(u => 
			        u.PutsOnly()
	            	.Strikes(Int32.MaxValue, 0) //ATM or OTM
            );
	        option.PriceModel = OptionPriceModels.BaroneAdesiWhaley();
	        // optionContract.PriceModel = new QLOptionPriceModel(process => new BaroneAdesiWhaleyApproximationEngine(process),
		       //  _underlyingVolEstimator,
		       //  _riskFreeRateEstimator,
		       //  _dividendYieldEstimator);
	        
	        DefaultOrderProperties.TimeInForce = TimeInForce.Day; //Cancel all orders at end of day
	        
	        Schedule.On(DateRules.EveryDay(UnderlyingSymbol), TimeRules.AfterMarketOpen(UnderlyingSymbol, -1), AtStartOfDayHandler); // 9:29 AM
	        Schedule.On(DateRules.EveryDay(UnderlyingSymbol), TimeRules.BeforeMarketClose(UnderlyingSymbol, 1), AtEndOfDayHandler); // 3:59 PM
	        Schedule.On(DateRules.EveryDay(UnderlyingSymbol), TimeRules.Every(CyclePeriodTimeSpan), PeriodicCycleHandler);
        }
        
        

        private void PeriodicCycleHandler()
        {
	        if (!isActive)
		        return;

		    // Cancel all previous orders
		    foreach (var ticket in Transactions.GetOpenOrderTickets())
		    {
			    ticket.Cancel();
		    }

	        // Figure out current hedge state
            CalculateHedgeNotional();

            OptionChain chain;
            if (!CurrentSlice.OptionChains.TryGetValue(OptionSymbol, out chain))
            {
	            Error($"No option contract chain for {OptionSymbol}");
	            return;
            }

            decimal hedgeNeeded = targetHedgeNotional - currentHedgeNotional;

            //Check for those needing unwinding and reduce notional by those expiring soon
            decimal minExpiryHedgeNotional = Decimal.Zero;
			Dictionary<Symbol,decimal> unwindLevels = new Dictionary<Symbol, decimal>();
            foreach (Option option in GetUnderMinimumExpiration())
            {
	            //Regardless if we unwind or not, pretend these positions don't count and aim to replace them
	            decimal securityHedgeNotional = CalculateSecurityHedgeNotional(option.Symbol, Portfolio[option.Symbol].Quantity + GetTradedQuantity(option.Symbol));
	            decimal unwindLevel;
	            if (!unwindLevels.TryGetValue(option.Symbol, out unwindLevel))
	            {
		            unwindLevel = Math.Min(1.0m, (minUnwindDate - option.Expiry).Ticks /
			            (decimal) (minUnwindDate - aggUnwindDate).Ticks);
		            unwindLevels.Add(option.Symbol, unwindLevel);
	            }

	            minExpiryHedgeNotional += unwindLevel * securityHedgeNotional;
            }

            // If there is something that qualifies for rolling and we have enough hedge already
            if (minExpiryHedgeNotional > Decimal.Zero && hedgeNeeded < Decimal.Zero)
            {
	             decimal liquidationAttemptedHedgeNotional = Decimal.Zero;
	             foreach (Option option in GetUnderMinimumExpiration().OrderByDescending(opt => Portfolio[opt.Symbol].HoldingsValue))
	             {
		             // Only liquidate if we haven't already attempted to liquidate enough to get us to neutral hedge
		             if (liquidationAttemptedHedgeNotional < hedgeNeeded || option.BidPrice <= 0.01m)
		             {
			             continue;
		             }
		             
		             OptionContract optionContract;
		             if (!chain.Contracts.TryGetValue(option.Symbol, out optionContract))
		             {
			             // Debug($"Unable to unwind {optionContract.Symbol} for min exp, not in chain");
			             continue;
		             }

		             decimal prctOfSpread = Math.Max(unwindLevels[option.Symbol], 0.5m);
		             decimal sentQty = SendLimitOrder(optionContract, -Portfolio[option.Symbol].Quantity,
			             prctOfSpread,
			             "MinExp Unwind", 
			             $"{hedgeNeeded:F1}");
		             if (sentQty < Decimal.Zero)
		             {
			             liquidationAttemptedHedgeNotional += CalculateSecurityHedgeNotional(option.Symbol, sentQty);
		             }
	             }
            }

            //Now check what we need to do
            hedgeNeeded += minExpiryHedgeNotional; //Add what is going to expire or be rolled soon
            decimal prctHedgeNeeded = hedgeNeeded / targetHedgeNotional;
            
            if (prctHedgeNeeded > Decimal.Zero && hddAverageEma.IsReady)
            {
	            decimal periodHedge = Math.Min(hedgeNeeded, targetHedgeNotional * MaxHedgePerPeriod);
	            IEnumerable<Tuple<OptionContract, decimal>> accumulateRank = GetAccumulationRank(chain);
	            // decimal medianEma = hddMedianEma.IsReady ? hddMedianEma.Current.Value : Math.Max(hddMedianEma.Current.Value, 12750m);
	            // decimal averageEma = hddAverageEma.IsReady ? hddAverageEma.Current.Value: Math.Max(hddAverageEma.Current.Value, 22750m);
	            decimal medianEma = hddMedianEma.Current.Value;
	            decimal averageEma = hddAverageEma.Current.Value;
	            decimal minHdd = averageEma * HddCutoffMultiple;
	            IEnumerable<Tuple<OptionContract,decimal>> selected = accumulateRank
		            .Where(t => t.Item2 >= minHdd)
		            .Take(MaxOptionOrdersPerPeriod);

	            decimal[] hedgeDayValues = selected.Select(tuple => tuple.Item2).ToArray();
	            int numOptions = hedgeDayValues.Length;
	            decimal sum = hedgeDayValues.Sum();

	            using (IEnumerator<Tuple<OptionContract, decimal>> enumerator = selected.GetEnumerator())
	            {
		            while (enumerator.MoveNext() && periodHedge > Decimal.Zero && numOptions > 0)
		            {
			            OptionContract optionContract = enumerator.Current.Item1;
			            decimal hedgeDayValue = enumerator.Current.Item2;
			            Option option = Securities[optionContract.Symbol] as Option;
			            if (option != null)
			            {
				            decimal perContractHedgeNotional = option.ContractMultiplier * (option.StrikePrice - ResidualUnderlyingValue);
				            decimal targetQty = Math.Max(MinContractsPerOrder, Math.Ceiling((periodHedge * (hedgeDayValue/sum)) / perContractHedgeNotional));
				            decimal prctOfSpread = 0.0m; //prctHedgeNeeded > 0.5m ? 1.0m : (prctHedgeNeeded * 2m);
				            decimal sentQty = SendLimitOrder(optionContract,
					            targetQty,
					            prctOfSpread,
					            "Accumulate",
					            $"{hedgeNeeded:F1}, {medianEma:F1}, {averageEma:F1}, {minHdd:F1}, {hedgeDayValue:F1}, {sum:F1}"
				            );

				            if (sentQty > Decimal.Zero)
				            {
					            periodHedge -= sentQty * perContractHedgeNotional;
					            numOptions--;
				            }
			            }
			            else
			            { 
				            Error($"No optionContract found for {optionContract.Symbol}");
			            }
		            }
	            }
            }
	        else if (prctHedgeNeeded < -0.05m) //dispose
            {
		         foreach (Option option in GetDisposeRanking())
		         {
			         if (hedgeNeeded >= Decimal.Zero)
				         break;
			         
			         OptionContract optionContract;
			         if (!chain.Contracts.TryGetValue(option.Symbol, out optionContract))
			         {
				         Debug($"Unable to unwind {option.Symbol} for dispose, not in chain");
				         continue;
			         }
			         
			         decimal perContractHedgeNotional = option.ContractMultiplier * (option.StrikePrice - ResidualUnderlyingValue);
			         decimal targetQty = Math.Max(Math.Floor(hedgeNeeded / perContractHedgeNotional), -Portfolio[option.Symbol].Quantity);
			         if (targetQty <= -Decimal.One)
			         {
				         const decimal prctOfSpread = 0.0m; //0.5m;
				         decimal sentQty = SendLimitOrder(optionContract,
					        targetQty,
					        prctOfSpread,
					        "Dispose", 
				         $"{hedgeNeeded:F1}"
				         );
				         hedgeNeeded -= sentQty * perContractHedgeNotional;
			         }
		         }
	        }
        }

        private decimal GetTradedQuantity(Symbol symbol)
        {
	        decimal traded;
	        return tradedVolume.TryGetValue(symbol, out traded) ? traded : Decimal.Zero;
        }

        private IOrderedEnumerable<Tuple<OptionContract, decimal>> GetAccumulationRank(OptionChain chain)
        {
	        IEnumerable<Tuple<OptionContract, decimal>> selected;
	        // decimal maxAskPrice = 0.00375m * underlyingLastPrice;
		        // decimal maxSpread = maxAskPrice / 3;
		        selected = chain
			        .Where(oc => (oc.Expiry >= minExpiryDate && oc.Expiry <= maxExpiryDate))
			        .Where(oc => oc.Strike > ResidualUnderlyingValue /*&& oc.Strike <= 0.9m * oc.UnderlyingLastPrice*/)
			        .Where(oc => oc.BidPrice > Decimal.Zero && oc.AskPrice > Decimal.Zero && oc.AskPrice > oc.BidPrice)
			        .Where(oc => /*oc.AskPrice <= maxAskPrice /*MaxAskPrice#1# &&*/
				        ( /*(oc.AskPrice - oc.BidPrice) < maxSpread /*MaxAbsoluteSpread#1# ||*/
					        ((oc.AskPrice - oc.BidPrice) / oc.AskPrice) <= MaxPercentSpread))
			        .Select(oc => Tuple.Create(oc, HedgeValueRank(oc)));
			        
	        // }
	        
	        return selected.OrderByDescending(t => t.Item2);
        }

        private decimal HedgeValueRank(OptionContract optionContract)
        {
	        return ((optionContract.Strike - ResidualUnderlyingValue) * (decimal) (optionContract.Expiry - CurrentSlice.Time).TotalDays) /
		        optionContract.AskPrice;
        }

        private decimal GetWeightedExpirationDays()
        {
	        return currentHedgeNotionalByExpiry
		        .Sum(kvp => Convert.ToDecimal((kvp.Key - CurrentSlice.Time).TotalDays) * (kvp.Value / currentHedgeNotional));
        }
        
        private IEnumerable<Option> GetUnderMinimumExpiration()
        {
	        // Dispose of closest expiries first and then by ones furthest OTM
	        return Portfolio.Values
		        .Where(sh => sh.Invested)
		        .Select(sh => Securities[sh.Symbol] as Option)
		        .Where(opt => opt != null) //TODO log
		        .Where(option => option.Expiry <= minUnwindDate);
        }

        private IOrderedEnumerable<Option> GetDisposeRanking()
        {
	        // Dispose of closest expiries first and then by ones furthest OTM
	        return Portfolio.Values
		        .Where(sh => sh.Invested)
		        .Select(sh => Securities[sh.Symbol] as Option)
		        .Where(opt => opt != null) //TODO log
		        .Where(option => option.Expiry > minUnwindDate)
		        .OrderBy(o => o.Expiry)
		        .ThenByDescending(o => o.Underlying.Price - o.StrikePrice);
        }

        public override void OnData(Slice slice)
        {
			if (!isActive)
		        return;
	        
	        DateTime cutoff = UtcTime.AddMinutes(-5); // Don't update faster than 5 minutes
	        IOrderedEnumerable<OrderTicket> orderTickets = Transactions.GetOpenOrderTickets().Where(t => t.Time < cutoff).OrderBy(t => t.Time);
	        // OrderTicket ticket = orderTickets.First();
	        foreach (var ticket in orderTickets)
	        {
		        LimitOrder limitOrder = Transactions.GetOrderById(ticket.OrderId) as LimitOrder;
		        if (limitOrder == null)
		        {
			        continue;
		        }
		        
		        decimal prevPrice = limitOrder.LimitPrice;
		        QuoteBar quoteBar;
		        if (!slice.QuoteBars.TryGetValue(ticket.Symbol, out quoteBar) || quoteBar.Bid == null || quoteBar.Ask == null || quoteBar.Bid.Close > quoteBar.Ask.Close)
		        {
			        // Debug($"No or missing quote bars for {ticket.Symbol} @ {slice.Time}");
			        continue;
		        }

		        decimal currentSpread = quoteBar.Ask.Close - quoteBar.Bid.Close;
		        decimal updatedPrice;
		        if (limitOrder.Direction == OrderDirection.Buy)
		        {
			        if (prevPrice >= quoteBar.Ask.Close /*|| prevPrice >= MaxAskPrice*/)
			        {
				        // Debug($"Order ({ticket.OrderId}) to buy {ticket.Symbol} @ {prevPrice} is >= current ask {quoteBar.Ask.Close} but hasn't executed, time = {slice.Time}");
				        continue; //Doesn't need update, not sure why it hasn't executed
			        }

			        prevPrice = Math.Max(prevPrice, quoteBar.Bid.Close);
			        decimal bumpUp = Math.Max(0.01m, Math.Round(0.2m * currentSpread, 2)); // Increase by 10% of spread or 0.01, whichever greater
			        // updatedPrice = Math.Min(MaxAskPrice, prevPrice + bumpUp);
			        updatedPrice = prevPrice + bumpUp;

			        OrderResponse orderResponse = ticket.UpdateLimitPrice(updatedPrice, ticket.Tag + $", {updatedPrice}");
			        if (orderResponse.IsSuccess)
			        {
				        // Debug($"Updated order ({ticket.OrderId}) price to buy {ticket.Symbol} @ {prevPrice} to {updatedPrice}, bid = {quoteBar.Bid.Close}, ask = {quoteBar.Ask.Close}, time = {slice.Time}");
				        // return; //Do one per cycle
			        }
			        else
			        {
				        Error($"Unable to update order ({ticket.OrderId}) price to buy {ticket.Symbol} @ {prevPrice} to {updatedPrice}, response = {orderResponse.ErrorMessage}, bid = {quoteBar.Bid.Close}, ask = {quoteBar.Ask.Close}, time = {slice.Time}");
			        }
		        }
		        else
		        {
			        if (prevPrice <= quoteBar.Bid.Close)
			        {
				        // Debug($"Order ({ticket.OrderId}) to sell {ticket.Symbol} @ {prevPrice} is <= current bid {quoteBar.Bid.Close} but hasn't executed, time = {slice.Time}");
				        continue; //Doesn't need update, not sure why it hasn't executed
			        }

			        prevPrice = Math.Min(prevPrice, quoteBar.Ask.Close);
			        decimal bumpDown = Math.Max(0.01m, Math.Round(0.1m * currentSpread, 2)); // Increase by 10% of spread or 0.01, whichever greater
			        updatedPrice = prevPrice - bumpDown;

			        OrderResponse orderResponse = ticket.UpdateLimitPrice(updatedPrice, ticket.Tag + $", {updatedPrice}");
			        if (orderResponse.IsSuccess)
			        {
				        // Debug($"Updated order ({ticket.OrderId}) price to sell {ticket.Symbol} @ {prevPrice} to {updatedPrice}, bid = {quoteBar.Bid.Close}, ask = {quoteBar.Ask.Close}, time = {slice.Time}");
				        // return; //Do one per cycle
			        }
			        else
			        {
				        Error($"Unable to update order ({ticket.OrderId}) price to sell {ticket.Symbol} @ {prevPrice} to {updatedPrice}, response = {orderResponse.ErrorMessage}, bid = {quoteBar.Bid.Close}, ask = {quoteBar.Ask.Close}, time = {slice.Time}");
			        }
		        }
	        }
        }

        private void AtStartOfDayHandler()
        {
	        isActive = !IsWarmingUp && CurrentSlice.Time.Date >= Start;
	        
	        minExpiryDate = CurrentSlice.Time.AddDays(MinExpiryDays);
	        maxExpiryDate = CurrentSlice.Time.AddDays(MaxExpiryDays);
	        minUnwindDate = CurrentSlice.Time.Date.AddDays(LiquidateExpiryDays);
	        aggUnwindDate = CurrentSlice.Time.Date.AddDays(AggLiquidateExpiryDays);

	        // Entered new month, recalibrate on new target notional
	        if (isActive)
	        {
		        if (CurrentSlice.Time.Year != noiScheduleDate.Year || CurrentSlice.Time.Month != noiScheduleDate.Month)
		        {
			        noiScheduleDate = new DateTime(CurrentSlice.Time.Year, CurrentSlice.Time.Month, 1);
			        if (!discountedMonthlyIncome.TryGetValue(noiScheduleDate, out targetHedgeNotional))
			        {
				        Error($"Failed to lookup NOI schedule for {noiScheduleDate.ToShortDateString()}");
				        Quit();
				        // Debug($"New month {noiScheduleDate.ToShortDateString()} -> {targetHedgeNotional:N}");
			        }
		        }

		        foreach (var symbolGroup in History(TradeMaTimeSpan)
			        .SelectMany(s => s.Bars.Values)
			        .GroupBy(bar => bar.Symbol))
		        {
			        SetAverageVolume(symbolGroup.Key, symbolGroup.Average(bar => bar.Volume) / TradedMaDays);
		        }

		        // Check if any positions are expired but somehow left over
		        IEnumerable<Option> expiredPos = Portfolio.Where(kvp => kvp.Value.Invested)
			        .Select(kvp => Securities[kvp.Key] as Option)
			        .Where(opt => opt != null)
			        .Where(opt => opt.Expiry.Date < CurrentSlice.Time.Date);
		        if (expiredPos.Any())
		        {
			        string msg = string.Join(", ", expiredPos.Select(option =>
					        $"{option.Symbol}, {Portfolio[option.Symbol].Quantity}, {Securities[option.Symbol].Price}"));
			        Error(msg);
			        Quit("There are expired holdings");
		        }

		        // Check if there are any short positions which should not happen
		        IEnumerable<SecurityHolding> shortPos = Portfolio.Values.Where(sh => sh.Invested && sh.IsShort);
		        foreach (SecurityHolding securityHolding in shortPos.Where(sh => sh.Type == SecurityType.Equity))
		        {
			        Log($"Liquidating exercised short stock {securityHolding.Quantity} {securityHolding.NetProfit}");
			        OrderTicket orderTicket = MarketOrder(securityHolding.Symbol, -securityHolding.Quantity, true,
				        "Liquidate Exercised");
			        if (orderTicket.Status != OrderStatus.Invalid)
			        {
				        knownOrders[orderTicket.OrderId] = orderTicket;
			        }
		        }

		        if (shortPos.Any(sh => sh.Type != SecurityType.Equity))
		        {
			        Error(string.Join(", ", shortPos.Select(sh => $"{sh.Symbol}:{sh.Quantity}")));
			        Quit("There are short positions");
		        }

		        // Log(string.Join(", ", Portfolio.Values.Where(sh => sh.Invested).OrderBy(sh => sh.Symbol).Select(sh => $"{sh.Symbol}:{sh.Quantity}")));
	        }
        }

        private decimal SendLimitOrder(OptionContract optionContract, decimal targetQty, decimal prctOfSpread, string type, string addlTags = "")
        {
	        if (targetQty == Decimal.Zero || !IsMarketOpen(optionContract.Symbol))
	        {
		        // Debug($"{optionContract.Symbol} - Market is not open");
		        return Decimal.Zero;
	        }

	        decimal traded = GetTradedQuantity(optionContract.Symbol);
	        decimal qty = targetQty;
	        decimal avgVolume = Decimal.Zero;
	        // Only restrict volume if accumulating, otherwise take what market is offering
	        if (qty > Decimal.Zero) // Going long
	        {
	        	if(traded > Decimal.Zero)
	        	{
			        if (!averageVolume.TryGetValue(optionContract.Symbol, out avgVolume) || avgVolume == Decimal.Zero)
			        {
				        // Debug($"{optionContract.Symbol} - No volume");
				        return Decimal.Zero;
			        }
	
			        // Adjust quantity to be % of avg volume less what has already traded
			        qty = Math.Min(qty, Math.Ceiling(avgVolume * MaxPercentDailyVolume) - traded);
			        if (qty <= Decimal.Zero)
			        {
				        // Debug($"{optionContract.Symbol} - No more quantity {volume}, {traded}, {maxQty}, {targetQty}");
				        return Decimal.Zero;
			        }
	        	}
		        
		        if (qty >= Decimal.One)
		        {
			        Option optSec = Securities[optionContract.Symbol] as Option;
			        if (optSec != null)
			        {
				        GetMaximumOrderQuantityResult buyingPower =
					        optSec.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(Portfolio, optSec, qty);
				        if (buyingPower.Quantity < Decimal.One)
				        {
					        // Debug($"NNN: Not enough buying power to purchase {qty} of {optionContract.Symbol} - {buyingPower.Reason}");
					        return Decimal.Zero;
				        }
				        else if(qty > buyingPower.Quantity)
				        {
					        // Reduce amount based on available margin
					        Debug($"Reducing quantity from {qty} to {buyingPower.Quantity} because of margin requirements");
					        qty = Math.Min(buyingPower.Quantity, qty);
				        }
			        }
		        }
	        }
	        
	        decimal bidPrice = optionContract.BidPrice;
	        decimal askPrice = optionContract.AskPrice;
	        if (askPrice < bidPrice || bidPrice <= Decimal.Zero || askPrice <= Decimal.Zero)
			{
				QuoteBar quoteBar;
				if (!CurrentSlice.QuoteBars.TryGetValue(optionContract.Symbol, out quoteBar) || quoteBar?.Ask == null || 
					quoteBar?.Bid == null || quoteBar.Ask.Close <= Decimal.Zero || quoteBar.Bid.Close <= Decimal.Zero || 
					quoteBar.Ask.Close <= quoteBar.Bid.Close)
				{
					// Debug($"{optionContract.Symbol} - No quote bar or Ask/Bid bar");
					return Decimal.Zero;
				}
				else
				{
					bidPrice = quoteBar.Bid.Close;
					askPrice = quoteBar.Ask.Close;
					Debug($"{optionContract.Symbol} - Bad option contact prices, using quote bars [${quoteBar.Bid.Close}, ${quoteBar.Ask.Close}]");
				}
			}

		    // rawPrice = qty > 0 ? bidPrice : askPrice;
		    decimal spread = optionContract.AskPrice - optionContract.BidPrice;
		    decimal spreadToPay = Math.Max(0.0m, prctOfSpread) * spread;
		    decimal rawPrice = qty > 0 ? optionContract.BidPrice + spreadToPay : optionContract.AskPrice - spreadToPay;

		    decimal price = Math.Round(rawPrice, 2);
		    if(price <= Decimal.Zero)
			    return Decimal.Zero;

		    string sentTag = 
		        $"{type}, {targetQty}, {price}, {bidPrice}, {askPrice}, {prctOfSpread:F1}, {targetHedgeNotional:F1}, " +
		        $"{currentHedgeNotional:F1}, {Portfolio[optionContract.Symbol].Quantity}, {traded}, {avgVolume}, " +
		        $"{optionContract.UnderlyingLastPrice}, {optionContract.ImpliedVolatility:F6}, {optionContract.TheoreticalPrice:F6}, " +
		        $"{optionContract.Greeks.Delta:F6}, {optionContract.Greeks.Vega:F6}, {optionContract.Greeks.Theta:F4}, " +
		        $"{optionContract.Greeks.Gamma:F4}";

	        if(!string.IsNullOrEmpty(addlTags))
		        sentTag += $", {addlTags}";
				    
	        OrderTicket orderTicket = LimitOrder(optionContract.Symbol, qty, price, sentTag);
	        if (orderTicket.Status == OrderStatus.Invalid)
	        {
		        Debug(
			        $"Received invalid order - {orderTicket.OrderId}, {optionContract.Symbol}, {(optionContract.Strike / optionContract.UnderlyingLastPrice):P}, " +
			        $"{qty}, {price}, {bidPrice}, {askPrice}, {prctOfSpread:P}, {bidPrice}, {askPrice}, {orderTicket.GetMostRecentOrderRequest().Response}");
		        return Decimal.Zero;
	        }

	        //  Debug($"Sending order - {orderTicket.OrderId}, {optionContract.Symbol}, {(optionContract.StrikePrice / optionContract.Underlying.Price):P}, " + 
	        // $"{qty}, {price}, {spread}, {prctOfSpread:P}, {quoteBar.Bid?.Close}, {quoteBar.Ask?.Close}, {rawPrice.SmartRounding()}");
	        knownOrders[orderTicket.OrderId] = orderTicket;
	        return qty;
        }

        private void AtEndOfDayHandler()
        {
	        isActive = false;

	        CalculateHedgeNotional();
	        
	        decimal weightedHedgeDays = GetWeightedExpirationDays();
	        if (weightedHedgeDays < Decimal.Zero)
	        {
		        string data = string.Join(",", currentHedgeNotionalByExpiry.Select(kvp => $"{kvp.Key}={kvp.Value.SmartRounding()}"));
		        Error($"{data}, {CurrentSlice.Time}, {currentHedgeNotional.SmartRounding()}");
		        Quit("Negative weighted hedge expiration days");
	        }

	        decimal hedgeNotionalExpiringSoon = GetUnderMinimumExpiration().Select(opt => CalculateSecurityHedgeNotional(opt.Symbol, Portfolio[opt.Symbol].Quantity)).Sum();
	        
	        Log(
		        $"EOD, {currentHedgeNotional:F1}, {targetHedgeNotional:F1}, {weightedHedgeDays:F1}, {hedgeNotionalExpiringSoon:F1}, {Portfolio.TotalPortfolioValue:F1}, {Portfolio.Cash:F1}, " +
		        $"{Portfolio.TotalFees:F1}, {Portfolio.TotalAbsoluteHoldingsCost:F1}, {Portfolio.TotalHoldingsValue:F1}, {Portfolio.TotalProfit:F1}, {Portfolio.TotalUnrealizedProfit:F1}"
	        );
	        
	        Plot("Hedge", "Current", currentHedgeNotional);
	        Plot("Hedge", "Target", targetHedgeNotional);
	        
	        Plot("Expiry", "Weighted Days", weightedHedgeDays);
	        
	        OptionChain chain;
	        if (!CurrentSlice.OptionChains.TryGetValue(OptionSymbol, out chain))
	        {
		        Error($"EOD: No option contract chain for {OptionSymbol}");
	        }
	        else
	        {
		        IEnumerable<decimal> rank = chain
			        .Where(oc => oc.Expiry >= minExpiryDate && oc.Expiry <= maxExpiryDate)
			        .Where(oc => oc.BidPrice > Decimal.Zero && oc.AskPrice > Decimal.Zero && oc.AskPrice > oc.BidPrice)
			        .Select(HedgeValueRank)
			        .OrderByDescending(d => d);
		        decimal[] hedgeDayValues = rank.ToArray();
		        if (hedgeDayValues.Length > 0)
		        {
			        decimal medianEma = hddMedianEma.Current.Value;
			        decimal averageEma = hddAverageEma.Current.Value;
			        decimal max = hedgeDayValues.First();
			        decimal min = hedgeDayValues.Last();
			        decimal median = hedgeDayValues[hedgeDayValues.Length / 2];
			        decimal average = hedgeDayValues.Sum() / hedgeDayValues.Length;
			        hddMedianEma.Update(CurrentSlice.Time, median);
			        hddAverageEma.Update(CurrentSlice.Time, average);
			        Log($"HDD, {hedgeDayValues.Length}, {max:F1}, {min:F1}, {median:F1}, {average:F1}, {medianEma:F1}, {averageEma:F1}, {hddAverageEma.IsReady}");

			        Plot("HD/$", "Median", median);
			        Plot("HD/$", "Average", average);
			        Plot("HD/$", "Median EMA", medianEma);
			        Plot("HD/$", "Average EMA", averageEma);
			        Plot("HD/$", "Max", max);
			        Plot("HD/$", "Min", min);
		        }
		        else
		        {
			        Log($"HDD, {hedgeDayValues.Length}, N/A, N/A, N/A, N/A, N/A, N/A, {hddAverageEma.IsReady}"); 
		        }
	        }
	        
	        currentHedgeNotional = Decimal.Zero;
	        minUnwindDate = DateTime.MaxValue;
	        // aggUnwindDate = DateTime.MaxValue;
	        averageVolume.Clear();
	        tradedVolume.Clear();
	        currentHedgeNotionalByExpiry.Clear();
        }

        // private void SetCurrentExpiries(OptionChain chain)
        // {
	       //  var surroundingExpiries = chain
		      //   .Select(x => x.Expiry)
		      //   .Distinct()
		      //   .ToDictionary(expiry => (expiry-pivotDate).TotalDays)
		      //   .Where(kvp => (kvp.Key > 0 || (kvp.Key + TargetExpiryPivot) > MinExpiryDays))
		      //   //Don't roll to next expiry unless at least a month out to minimize transaction costs
		      //   // .Where(kvp => kvp.Key > 30)
		      //   .OrderBy(kvp => Math.Abs(kvp.Key))
		      //   .ToLookup(kvp => kvp.Key < 0);
	       //  
	       //  int[] daysAway = new int[] {-1, -1};
	       //  
	       //  DateTime earlierExp = surroundingExpiries[true].Select(kvp => kvp.Value).FirstOrDefault();
	       //  if (earlierExp != default(DateTime))
	       //  {
		      //   if (pivotDate < earlierExp)
		      //   {
			     //    Error(
				    //     $"Earlier expiration {earlierExp.ToShortDateString()} is after pivot {pivotDate.ToShortDateString()}"
			     //    );
		      //   }
		      //   else
		      //   {
			     //    expirations[0] = earlierExp;
			     //    daysAway[0] = (pivotDate - earlierExp).Days;
		      //   }
	       //  }
        //
	       //  DateTime laterExp = surroundingExpiries[false].Select(kvp => kvp.Value).FirstOrDefault();
	       //  if (laterExp != default(DateTime))
	       //  {
		      //   if (pivotDate > laterExp)
		      //   {
			     //    Error(
				    //     $"Later expiration {laterExp.ToShortDateString()} is after pivot {pivotDate.ToShortDateString()}"
			     //    );
		      //   }
		      //   else
		      //   {
			     //    expirations[1] = laterExp;
			     //    daysAway[1] = (laterExp - pivotDate).Days;
		      //   }
	       //  }
        //
	       //  if (daysAway[0] < 0 && daysAway[1] < 0)
	       //  {
		      //   Error($"No expiries found with pivot @ {pivotDate.ToShortDateString()}");
	       //  }
	       //  else if (daysAway[0] < 0)
	       //  {
		      //   targetHedgeNotionalByExpiry[0] = Decimal.Zero;
		      //   targetHedgeNotionalByExpiry[1] = 0.75m * targetHedgeNotional;
	       //  }
	       //  else if (daysAway[1] < 0)
	       //  {
		      //   targetHedgeNotionalByExpiry[0] = 0.25m * targetHedgeNotional;
		      //   targetHedgeNotionalByExpiry[1] = Decimal.Zero;
	       //  }
	       //  else
	       //  {
		      //   decimal totalDays = Convert.ToDecimal(daysAway[0] + daysAway[1]);
		      //   targetHedgeNotionalByExpiry[0] = (Convert.ToDecimal(daysAway[1]) / totalDays) * targetHedgeNotional;
		      //   targetHedgeNotionalByExpiry[1] = (Convert.ToDecimal(daysAway[0]) / totalDays) * targetHedgeNotional;
	       //  }
        // }

        public override void OnOrderEvent(OrderEvent orderEvent)
        {
	        switch (orderEvent.Status)
	        {
		        case OrderStatus.Filled:
		        case OrderStatus.PartiallyFilled:
			        if (tradedVolume.ContainsKey(orderEvent.Symbol))
				        tradedVolume[orderEvent.Symbol] += orderEvent.AbsoluteFillQuantity;
			        else
				        tradedVolume.Add(orderEvent.Symbol, orderEvent.AbsoluteFillQuantity);

			        if (orderEvent.Status == OrderStatus.Filled)
				        RemoveKnownOrder(orderEvent);
			        // Debug($"Order filled - {orderEvent.Symbol}, {orderEvent.OrderId}, {orderEvent.Id}, {orderEvent.FillQuantity}, {orderEvent.FillPrice.SmartRounding()}, {orderEvent.OrderFee.Value.Amount}, {orderEvent.Message}, {orderEvent.Status}");
			        break;
		        case OrderStatus.Invalid:
			        //Usually not enough margin
			        RemoveKnownOrder(orderEvent);
			        // Debug($"Invalid order - {orderEvent.Symbol}, {orderEvent.OrderId}, {orderEvent.Id}, {orderEvent.Quantity}, {orderEvent.Message}");
			        break;
		        case OrderStatus.Canceled:
			        RemoveKnownOrder(orderEvent);
			        // Log($"Canceled order - {orderEvent.Symbol}, {orderEvent.OrderId}, {orderEvent.Id}");
			        break;
		        case OrderStatus.Submitted:
		        case OrderStatus.UpdateSubmitted:
		        case OrderStatus.New:
		        case OrderStatus.None:
			        break;
		        case OrderStatus.CancelPending:
			        // Log($"Cancel pending - {orderEvent.Symbol}, {orderEvent.OrderId}, {orderEvent.Id}");
			        break;
		        default:
			        Error($"Unhandled order status on order event - {orderEvent.Status}");
			        break;
	        }
        }

        public override void OnSecuritiesChanged(SecurityChanges changes)
        {
	        foreach (var change in changes.AddedSecurities)
	        {
		        // Only print options price
		        if (change.Symbol == UnderlyingSymbol) continue;
		        IEnumerable<TradeBar> history = History(change.Symbol, TradeMaTimeSpan);
		        if (history.Any())
		        {
			        SetAverageVolume(change.Symbol, history.Sum(bar => bar.Volume) / TradedMaDays);
		        }
	        }
        }

        private void SetAverageVolume(Symbol symbol, decimal avgVolume)
        {
	        if(averageVolume.ContainsKey(symbol))
		        averageVolume[symbol] = avgVolume;
	        else
				averageVolume.Add(symbol, avgVolume);
        }

        private void RemoveKnownOrder(OrderEvent orderEvent)
        {
	        if (knownOrders.ContainsKey(orderEvent.OrderId))
		        knownOrders.Remove(orderEvent.OrderId);
	        else if(orderEvent.IsAssignment)
		        // Shouldn't happen
				Error($"Option assigned - {orderEvent.Symbol}, {orderEvent.FillQuantity}, {orderEvent.FillPrice.SmartRounding()}");
	        else
	        {
		        OrderTicket ticket = Transactions.GetOrderTicket(orderEvent.OrderId);
		        if (ticket.OrderType == OrderType.OptionExercise && orderEvent.FillQuantity != Decimal.Zero)
		        {
			        // decimal underlyingPrice = Securities[UnderlyingSymbol].Price;
			        // if()
			        // {
				       //  Log(
					      //   $"Option exercised - {orderEvent.Symbol}, {orderEvent.FillQuantity}, {orderEvent.FillPrice}, {underlyingPrice.SmartRounding()}");
			        // }
		        }
		        else if(orderEvent.FillQuantity != Decimal.Zero)
		        {
			        Error($"Received order event for unknown order - {ticket.OrderId}, {orderEvent.Symbol}, {ticket?.OrderType}, {ticket?.Quantity}, " + 
			              $"{orderEvent.FillPrice.SmartRounding()}, {orderEvent.Message}, {orderEvent.Status}, {ticket?.OrderType}");
		        }
	        }
        }

        private void UpdateExpiryHedgeNotional(DateTime expiry, decimal hedgeNotional)
        {
	        decimal currentExpiry;
	        currentHedgeNotionalByExpiry.TryGetValue(expiry, out currentExpiry);
	        currentHedgeNotionalByExpiry[expiry] = currentExpiry + hedgeNotional;
        }

        private void CalculateHedgeNotional()
        {
	        currentHedgeNotional = Decimal.Zero;
	        currentHedgeNotionalByExpiry.Clear();
	        
	        foreach (var kvp in Portfolio.Where(kvp => kvp.Value.Invested))
	        {
		        decimal secNotional = CalculateSecurityHedgeNotional(kvp.Key, kvp.Value.Quantity);
		        var security = Securities[kvp.Key] as Option;
		        if (security != null)
			        UpdateExpiryHedgeNotional(security.Expiry, secNotional);
		        // else
			       //  Error($"{kvp.Key} is missing from security registry"); //Will be reported in CalculateSecurityHedgeNotional
		        currentHedgeNotional += secNotional;
	        }
        }

        private decimal CalculateSecurityHedgeNotional(Symbol symbol, decimal quantity)
        {
	        Option optionSecurity = Securities[symbol] as Option;
	        if (optionSecurity != null)
	        {
		        var secNotionalHedge = quantity * optionSecurity.ContractMultiplier * (optionSecurity.StrikePrice - ResidualUnderlyingValue);
		        // Debug($"Notional Hedge - {symbol} {quantity} {optionContract.StrikePrice} {secNotionalHedge}");
		        return secNotionalHedge;
	        }
	        else
	        {
		        Error($"{symbol} is missing from security registry");
		        return Decimal.Zero;
	        }
        }

        internal class CustomFillModel : ImmediateFillModel
        {
	        private readonly QCAlgorithm _algorithm;
	        private readonly Dictionary<int, decimal> _absoluteRemainingByOrderId = new Dictionary<int, decimal>();

	        public CustomFillModel(QCAlgorithm algorithm)
	        {
		        _algorithm = algorithm;
	        }
	        
	        public override OrderEvent LimitFill(Security asset, LimitOrder order)
	        {
		        DateTime utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
		        OrderEvent fill = new OrderEvent(order, utcTime, OrderFee.Zero);
		        
		        try
		        {
			        if (order.Status == OrderStatus.Canceled || !IsExchangeOpen(asset, false))
				        return fill;
			        
			        // Only fill once a time slice
			        if (order.LastFillTime != null && utcTime <= order.LastFillTime)
				        return fill;

			        Prices prices = GetPrices(asset, order.Direction);
			        QuoteBar quoteBar = asset.Cache.GetData<QuoteBar>();
			        DateTime pricesEndTime = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone);
			        // do not fill again on a previous quote
			        if (order.LastFillTime != null && pricesEndTime <= order.LastFillTime) 
				        return fill;
		        
			        decimal absoluteRemaining;
			        if (!_absoluteRemainingByOrderId.TryGetValue(order.Id, out absoluteRemaining))
			        {
				        absoluteRemaining = order.AbsoluteQuantity;
			        }
			        
			        switch (order.Direction)
			        {
				        case OrderDirection.Buy:
					        if (prices.Low <= order.LimitPrice)
					        {
						        fill.FillPrice = Math.Min(prices.High, order.LimitPrice);
						        fill.FillQuantity = Math.Min(absoluteRemaining, quoteBar?.LastAskSize ?? absoluteRemaining);
					        }
					        break;
				        case OrderDirection.Sell:
					        if (prices.High >= order.LimitPrice)
					        {
						        fill.FillPrice = Math.Max(prices.Low, order.LimitPrice);
						        fill.FillQuantity = -Math.Min(absoluteRemaining, quoteBar?.LastBidSize ?? absoluteRemaining);
					        }
					        break;
			        }
			        
			        decimal absoluteFillQuantity = Math.Abs(fill.FillQuantity);
			        if (absoluteFillQuantity > Decimal.Zero)
			        {
				        if (absoluteRemaining == absoluteFillQuantity)
				        {
					        fill.Status = OrderStatus.Filled;
					        _absoluteRemainingByOrderId.Remove(order.Id);
				        }
				        else
				        {
					        fill.Status = OrderStatus.PartiallyFilled;
					        absoluteRemaining -= absoluteFillQuantity;
					        _absoluteRemainingByOrderId[order.Id] = absoluteRemaining;
				        }

				        // _algorithm.Debug(
					       // $"Model Filled - {fill.Symbol}, {fill.FillQuantity}, {fill.FillPrice}, {fill.Quantity}, {orderTicket?.Quantity}, {absoluteRemaining}, {quoteBar.Time.ToShortTimeString()}, {quoteBar}"
				        // );
			        }
		        }
		        catch (Exception e)
		        {
			        _algorithm.Error($"Exception in CustomFillModel for {order.Id}: {e.StackTrace}");
		        }
		        
		        return fill;
	        }
        }
	}
}