Overall Statistics
Total Trades
18
Average Win
31.04%
Average Loss
-21.55%
Compounding Annual Return
19.283%
Drawdown
11.300%
Expectancy
0.220
Net Profit
7.600%
Sharpe Ratio
1.226
Probabilistic Sharpe Ratio
55.576%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.44
Alpha
0.13
Beta
-0.037
Annual Standard Deviation
0.112
Annual Variance
0.012
Information Ratio
1.422
Tracking Error
0.236
Treynor Ratio
-3.743
Total Fees
$180.00
Estimated Strategy Capacity
$3000.00
Lowest Capacity Asset
SPY 31Y46M3K287DY|SPY R735QTJ8XC9X
Portfolio Turnover
3.37%
#region imports
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Orders.Fees;
#endregion

// sell to open a credit spread during the market’s close for a net credit, 
// then buy to close the spread the next day for a net debit.

// Expiry should be 3 or 5 DTE

// We place trades based on the price action of daily candles. Check the daily candle at 3:20 p.m. to see the momentum. The candle’s body should be full, with no large wicks. To be considered a momentum candle, the body must be at least 3/4 full. Please avoid this strategy if the score is 2/4. If a doji or spinning top candle forms, avoid.
// Ichimoku cloud to filter out counter-trend trades. 

// Open a 0.14 delta spread. For a bear call spread, we open above the candle’s opening price, and for a bull put spread, we open below it. There is less probability that it will break the opening price if it is a momentum candle.

//     Exit — Try to capture 50% premium atleast.

//     At market open, the volatility will be high due to power hours. If it’s in our favor. Exit it. Else wait atleast 10:30 to 11 for volatility to decrease.
//     If you want to hold longer and get more juice, try exiting with a TTM squeeze fade out. But, as usual, I prefer to be conservative. So my trades are very mechanical, and I like to close with a 50% premium captured.


namespace QuantConnect.Algorithm.CSharp.Options
{
    public partial class SellingCreditSpreads : QCAlgorithm
    {
        string[] tickers = new string[] { "SPY" };

        // TODO not used, remove?
        decimal maxPercentagePerContract = 1m;

        Resolution resolution = Resolution.Minute;
        int minExpirationDays = 1;
        int maxExpirationDays = 3;
        int minStrike = -20;
        int maxStrike = 1;
        decimal minPrice = 0m;
        decimal maxPrice = 0m;

        Dictionary<Symbol, Stock> stocks = new Dictionary<Symbol, Stock>();

        public override void Initialize()
        {
            SetCash(2500);
            SetStartDate(2022, 1, 1);
            SetEndDate(2022, 6, 1);

            //SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage);
            SetBrokerageModel(BrokerageName.QuantConnectBrokerage);

            AddStocks(this.tickers);

            UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
            SetSecurityInitializer(x => x.SetDataNormalizationMode(DataNormalizationMode.Raw));
        }

        void CancelOpenOrders()
        {
            var openOrders = this.Transactions.GetOpenOrders();
            foreach (var order in openOrders)
            {
                this.Transactions.CancelOrder(order.Id);
            }
        }

        void RemoveAllConsolidators(Symbol symbol)
        {
            foreach (var subscription in this.SubscriptionManager.Subscriptions.Where(x => x.Symbol == symbol))
            {
                foreach (var consolidator in subscription.Consolidators)
                {
                    this.SubscriptionManager.RemoveConsolidator(symbol, consolidator);
                }

                subscription.Consolidators.Clear();
            }
        }

        // Subscribe to stocks
        void AddStocks(string[] tickers)
        {
            foreach (string ticker in tickers)
            {
                Stock stock = new Stock(ticker, this.resolution, this, this.maxPercentagePerContract, this.minExpirationDays, this.maxExpirationDays, this.minStrike, this.maxStrike, this.minPrice, this.maxPrice);
                this.stocks.TryAdd(stock.stockSymbol, stock);
            }
        }

        public override void OnData(Slice slice)
        {
            if (Time > new DateTime(Time.Year, Time.Month, Time.Day, 15, 20, 0) && Time < new DateTime(Time.Year, Time.Month, Time.Day, 15, 59, 0))
            {
                this.TryEnter(slice);
            }

            if (Time >= new DateTime(Time.Year, Time.Month, Time.Day, 9, 30, 0))
            {
                this.TryExit(slice);
            }
        }

        private void TryEnter(Slice slice)
        {
            //            CancelOpenOrders();

            foreach (KeyValuePair<Symbol, Stock> entry in this.stocks)
            {
                Stock stock = entry.Value;
                stock.TryEnter(slice);
            }
        }

        private void TryExit(Slice slice)
        {
            //            CancelOpenOrders();

            foreach (KeyValuePair<Symbol, Stock> entry in this.stocks)
            {
                Stock stock = entry.Value;
                stock.TryExit(slice);
            }
        }
    }

    class Stock
    {
        decimal minPrice;
        decimal maxPrice;

        public Symbol stockSymbol;
        public Symbol optionSymbol;

        SellingCreditSpreads algorithm;

        Indicators indicators;
        decimal percentagePerStock;

        decimal maxPercentagePerContract;

        int minExpirationDays;
        int maxExpirationDays;
        int minStrike;
        int maxStrike;
        bool investedPuts = false;
        bool investedCalls = false;

        public int contracts;

        public DateTime expirationDate;

        public DateTime createdDate;

        public OptionContract shortLeg;
        public OptionContract longLeg;

        decimal distanceToInsurance = 0.01m;

        // percent of strike price
        public readonly decimal minDistanceBetweenPutsCalls = 0.02M;

        // percent of strike price
        public readonly decimal maxDistanceBetweenPutsCalls = 0.07M;

        // percent of strike price
        private readonly decimal safeDistanceToMaxBet = 0.001M;

        public bool DecideIfClosing(Slice slice)
        {
            bool closing = false;
            if(this.CalculatePnlPercentagePerShare(slice) >= 0.5m)
            // Use closeInDays
            //            if ((this.algorithm.Time - this.createdDate).TotalDays >= this.closeInDays)
            {
                closing = true;
                this.Log("Capturing 50% pnl.");
            }

            // close before expiration
            if ((this.expirationDate.Date - this.algorithm.Time.Date).TotalDays <= 0)
            {
                // just before market close
                if (this.algorithm.Time.TimeOfDay >= new TimeSpan(15, 0, 0))
                {
                    closing = true;
                }
            }

            return closing;
        }

        public TradeBar GetTodayPrices()
        {
            IEnumerable<TradeBar> history = this.algorithm.History("SPY", 1, Resolution.Daily);
            return history.ElementAt(0);
        }

        public bool DecideIfOpening(Slice slice)
        {
            bool opening = false;
            TradeBar today = GetTodayPrices();
//            opening = today.Close > today.Open && today.High <= today.Close && today.Low >= today.Open;
            opening = ((today.Close - today.Open)/(today.High - today.Low) > 0.7m) 
            && ((today.High - today.Close)/(today.Close - today.Open) < 0.2m) 
            && ((today.Open - today.Low)/(today.Close - today.Open) < 0.2m);

            // do not open if yesturday was not a business day
//            if (!this.IsDayBeforeBusinessDay())
//            {
//                this.Log("Not entering: yesturday was not a business day.");
//                return false;
//            }

            return opening;
        }

        public void Log(string message)
        {
            this.algorithm.Debug(message);
        }

        public decimal CalculateCurrentPremiumPerShare(Slice slice)
        {
            decimal shortLegLastPrice = this.algorithm.Securities[this.shortLeg.Symbol].AskPrice;
            decimal longLegLastPrice = this.algorithm.Securities[this.longLeg.Symbol].BidPrice;

            return shortLegLastPrice - longLegLastPrice;
        }

        public decimal CalculateInitialPremiumPerShare()
        {
            return this.averageFillPriceShort - this.averageFillPriceLong;
        }

        public decimal CalculatePnlPercentagePerShare(Slice slice)
        {
            decimal initialPremium = CalculateInitialPremiumPerShare();
            decimal currentPremium = CalculateCurrentPremiumPerShare(slice);

            decimal result = 0;
            if(initialPremium != 0)
            {
                result = (initialPremium - currentPremium)/initialPremium; 
            }

            return result;
        }

        public decimal CalculateMaxLoss()
        {
            return Math.Abs(this.shortLeg.Strike - this.longLeg.Strike);
        }

        public void SetInvestedPuts(bool investedPuts)
        {
            this.investedPuts = investedPuts;
        }

        public void SetInvestedCalls(bool investedCalls)
        {
            this.investedCalls = investedCalls;
        }

        bool IsDayBeforeExpirationBusinessDay(DateTime expirationDate)
        {
            return this.algorithm.TradingCalendar.GetTradingDay(expirationDate.AddDays(-1)).BusinessDay;
        }

        bool IsDayBeforeBusinessDay()
        {
            return this.algorithm.TradingCalendar.GetTradingDay(this.algorithm.Time.AddDays(-1)).BusinessDay;
        }

        bool AreAllDaysBeforeExpirationBusinessDays(DateTime expirationDate)
        {
            int numberOfBusinessDaysUntilExpiration = this.algorithm.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, this.algorithm.Time, expirationDate).Count();
            int numberOfDaysUntilExpiration = (expirationDate - this.algorithm.Time).Days;
            //        	Log("numberOfBusinessDaysUntilExpiration=" + numberOfBusinessDaysUntilExpiration + ", numberOfDaysUntilExpiration" + numberOfDaysUntilExpiration);

            return numberOfBusinessDaysUntilExpiration == numberOfDaysUntilExpiration + 2;
        }

        bool IsNextDayBusinessDay()
        {
            return this.algorithm.TradingCalendar.GetTradingDay(this.algorithm.Time.AddDays(1)).BusinessDay;
        }

        public Stock(string underlyingTicker, Resolution resolution, SellingCreditSpreads algorithm, decimal maxPercentagePerContract, int minExpirationDays, int maxExpirationDays, int minStrike, int maxStrike, decimal minPrice, decimal maxPrice)
        {
            this.algorithm = algorithm;
            this.createdDate = algorithm.Time;
            this.stockSymbol = this.algorithm.AddEquity(underlyingTicker, resolution).Symbol;

            var option = this.algorithm.AddOption(underlyingTicker, Resolution.Minute);
            option.PriceModel = OptionPriceModels.CrankNicolsonFD();
            option.SetFilter(universe => from symbol in universe.Strikes(this.minStrike, this.maxStrike).WeeklysOnly().Expiration(TimeSpan.FromDays(this.minExpirationDays), TimeSpan.FromDays(this.maxExpirationDays)) select symbol);
            this.optionSymbol = option.Symbol;

            this.indicators = new Indicators(algorithm, this.stockSymbol, resolution);
            this.maxPercentagePerContract = maxPercentagePerContract;
            this.algorithm.Securities[this.stockSymbol].SetDataNormalizationMode(DataNormalizationMode.Raw);

            this.minExpirationDays = minExpirationDays;
            this.maxExpirationDays = maxExpirationDays;
            this.minStrike = minStrike;
            this.maxStrike = maxStrike;
            this.minPrice = minPrice;
            this.maxPrice = maxPrice;
        }

        bool IsTradable(Symbol symbol)
        {
            Security security;
            if (this.algorithm.Securities.TryGetValue(symbol, out security))
            {
                return security.IsTradable;
            }

            return false;
        }

        bool IsTradable()
        {
            return IsTradable(this.optionSymbol);
        }

        int CalculateNumberOfContracts()
        {
            var percentagePerContract = this.maxPercentagePerContract;

            int numberOfContracts = (int)((this.algorithm.Portfolio.MarginRemaining) / (100m * (this.shortLeg.Strike - this.longLeg.Strike)));

            return numberOfContracts;
        }

        public OptionContract FindContractWithDelta(Slice slice, OptionRight right)
        {
            TradeBar today = GetTodayPrices();

            OptionContract foundContract = null;
            OptionChain chain;
            if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
            {
                if (right == OptionRight.Put)
                {
                    foundContract = (
                            from contract in chain
                                .OrderByDescending(contract => (Math.Abs(contract.Greeks.Delta)))
                            where Math.Abs(contract.Greeks.Delta) <= 0.14m
                            where contract.Right == right
                            where contract.Strike < contract.UnderlyingLastPrice
                            where contract.Strike < today.Open
                            select contract
                        ).FirstOrDefault();
                }
                else
                {
                    foundContract = (
                            from contract in chain
                                .OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
                            where contract.Right == right
                            where contract.Strike > contract.UnderlyingLastPrice
                            select contract
                        ).FirstOrDefault();
                }
            }

            return foundContract;
        }

        // find put contract with max dollar bet: Open Interest * LastPrice, with strike below/above underlying price
        public OptionContract FindContractWithMaxDollarBet(Slice slice, OptionRight right)
        {
            OptionContract foundContract = null;
            OptionChain chain;
            if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
            {
                if (right == OptionRight.Put)
                {
                    foundContract = (
                            from contract in chain
                                .OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
                            where contract.Right == right
                            where contract.Strike < contract.UnderlyingLastPrice
                            select contract
                        ).FirstOrDefault();
                }
                else
                {
                    foundContract = (
                            from contract in chain
                                .OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
                            where contract.Right == right
                            where contract.Strike > contract.UnderlyingLastPrice
                            select contract
                        ).FirstOrDefault();
                }
            }

            return foundContract;
        }

        /// <summary>
        /// Finds contract that has a distance from mainContract.
        /// Examples: insurance contract, less risky contract.
        /// </summary>
        /// <param name="slice"></param>Option chains to find contract in. 
        /// <param name="mainContract"></param>Main contract distance from which is specified.
        /// <param name="distance"></param>Distance from main contract, percent of strike price.
        /// <returns>Found contract or null.</returns>
        private OptionContract FindContractWithDistance(Slice slice, OptionContract mainContract, decimal distance)
        {
            OptionContract foundContract = null;

            OptionChain chain;
            if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
            {
                if (mainContract.Right == OptionRight.Put)
                {
                    // first contract below strike of main contract
                    foundContract = (
                        from contract in chain
                            .OrderByDescending(contract => contract.Strike)
                        where contract.Right == mainContract.Right
                        where contract.Expiry == mainContract.Expiry
                        where contract.Strike < mainContract.Strike
                        select contract
                    ).FirstOrDefault();
                }
                else
                {
                    // first contract above strike of main contract
                    foundContract = (
                        from contract in chain
                            .OrderByDescending(contract => contract.Strike)
                        where contract.Right == mainContract.Right
                        where contract.Expiry == mainContract.Expiry
                        where contract.Strike > mainContract.Strike
                        select contract
                    ).LastOrDefault();
                }
            }

            return foundContract;
        }

        public OptionRight GetOppositeRight(OptionRight right)
        {
            if (right == OptionRight.Put)
            {
                return OptionRight.Call;
            }
            else
            {
                return OptionRight.Put;
            }
        }

        public decimal CalculateDistanceBetweenPutsCalls(Decimal strike1, Decimal strike2)
        {
            decimal distanceBetweenPutsCalls = Math.Abs(strike1 - strike2);
            //            Log("distanceBetweenPutsCalls=[" + distanceBetweenPutsCalls + "]");

            return distanceBetweenPutsCalls;
        }

        public OptionContract FindContractWithMaxDollarBetAndGoodDistanceFromOppositeContract(Slice slice, OptionRight right)
        {
            OptionContract maxBet = this.FindContractWithMaxDollarBet(slice, right);
            Log("Contract WithMaxDollarBet=[" + maxBet.Strike + "]");

            OptionContract oppositeBet = this.FindContractWithMaxDollarBet(slice, GetOppositeRight(maxBet.Right));
            decimal distanceBetweenPutsCalls = CalculateDistanceBetweenPutsCalls(oppositeBet.Strike, maxBet.Strike);

            if (distanceBetweenPutsCalls < maxBet.UnderlyingLastPrice * this.minDistanceBetweenPutsCalls)
            {
                Log("distanceBetweenPutsCalls is too small=[" + distanceBetweenPutsCalls + "]");

                return null;
            }
            else if (distanceBetweenPutsCalls > maxBet.UnderlyingLastPrice * this.maxDistanceBetweenPutsCalls)
            {
                Log("distanceBetweenPutsCalls is too big=[" + distanceBetweenPutsCalls + "]");

                return null;
            }

            // do not use max bet, use safer bet than safeDistanceToMaxBet less risky
            return this.FindContractWithDistance(slice, maxBet, this.safeDistanceToMaxBet);
        }

        public void TryEnter(Slice slice)
        {
            if (!this.IsInvested())
            {
                if (this.algorithm.Securities[this.stockSymbol].Exchange.ExchangeOpen)
                {
//                    this.indicators.CalculateFromHistory();
                    if (DecideIfOpening(slice))
                    {
                        this.shortLeg = this.FindContractWithDelta(slice, OptionRight.Put);
                        if (this.shortLeg != null)
                        {
                            // do not open if not all days before expiration are business days
                            if (!this.IsNextDayBusinessDay())
                            {
                                this.Log("Not entering: tomorrow is not business day.");
                                return;
                            }

                            // find insurance contract
                            this.longLeg = this.FindContractWithDistance(slice, this.shortLeg, this.distanceToInsurance);
                            if (this.longLeg != null)
                            {
                                // TODO
                                if (IsTradable(this.shortLeg.Symbol))
                                {
//                                    this.contracts = CalculateNumberOfContracts();
                                    this.contracts = 20;
                                    if (this.contracts > 0)
                                    {
                                        Open();
                                        this.expirationDate = shortLeg.Expiry;
                                    }
                                    else
                                    {
                                        Log("Not enough cash");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        public void TryExit(Slice slice)
        {
            if (this.IsInvested())
            {
                if (DecideIfClosing(slice))
                {
                    if (this.algorithm.Securities[this.stockSymbol].Exchange.ExchangeOpen)
                    {
                        Close();
                    }
                }
            }
        }

        decimal averageFillPriceLong;
        decimal averageFillPriceShort;

        public void Open()
        {
            string tag = "Opening";
            {
                decimal feePerOrder = 0.5m * this.contracts;
                this.algorithm.Securities[this.shortLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
                this.algorithm.Securities[this.longLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
                var optionStrategy = OptionStrategies.BullPutSpread(this.optionSymbol, this.shortLeg.Strike, this.longLeg.Strike, this.longLeg.Expiry);
                var tickets = this.algorithm.Buy(optionStrategy, this.contracts, true, tag);
                foreach (var ticket in tickets)
                {
                    if (ticket.Status != OrderStatus.Filled)
                    {
                        throw new Exception("Failed to fill order=[" + ticket.Symbol + "], [" + ticket.Quantity + "]");
                    }

                    if(ticket.Quantity > 0)
                    {
                        this.averageFillPriceLong = ticket.AverageFillPrice;
                    }
                    else
                    {
                        this.averageFillPriceShort = ticket.AverageFillPrice;
                    }
                }                
            }

            Log("================================= Opened spread=[" + shortLeg.Strike + ", " + longLeg.Strike + "]");

            this.SetInvestedCalls(true);
            this.SetInvestedPuts(true);
        }

        public void Close()
        {
            string tag = "Closing";
            {
                decimal feePerOrder = 0.5m * this.contracts;

                this.algorithm.Securities[this.shortLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
                this.algorithm.Securities[this.longLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
                var optionStrategy = OptionStrategies.BullPutSpread(this.optionSymbol, this.shortLeg.Strike, this.longLeg.Strike, this.longLeg.Expiry);
                var tickets = this.algorithm.Sell(optionStrategy, this.contracts, true, tag);
                foreach (var ticket in tickets)
                {
                    if (ticket.Status != OrderStatus.Filled)
                    {
                        throw new Exception("Failed to fill order=[" + ticket.Symbol + "], [" + ticket.Quantity + "]");
                    }
                }                
            }

            this.SetInvestedCalls(false);
            this.SetInvestedPuts(false);

            Log("================================= Closed spread=[" + shortLeg.Strike + ", " + longLeg.Strike + "]");
        }

        public bool ShouldExit()
        {
            return true;
        }

        public bool ShouldSellPut()
        {
            return this.indicators.ShouldSellPut();
        }

        public bool ShouldSellCall()
        {
            return this.indicators.ShouldSellCall();
        }

        private bool IsInvested()
        {
            return this.investedPuts; // TODO && this.investedCalls;
        }

        public bool ShouldBuy()
        {
            return this.percentagePerStock <= 0 && this.indicators.ShouldBuy() && this.IsTradable();
        }

        public bool ShouldSell()
        {
            return this.percentagePerStock >= 0 && this.indicators.ShouldSell() && this.IsTradable();
        }

        public bool ShouldShort()
        {
            return this.percentagePerStock >= 0 && this.indicators.ShouldShort() && this.IsTradable();
        }

        public bool ShouldCover()
        {
            return this.percentagePerStock <= 0 && this.indicators.ShouldCover() && this.IsTradable();
        }

        public bool AreIndicatorsReady()
        {
            return this.indicators.IsReady();
        }

        public bool UpdateIndicators(TradeBar tradeBar)
        {
            return this.indicators.Update(tradeBar);
        }

        public void ResetIndicators()
        {
            this.indicators.Reset();
        }
    }

    public class Indicators
    {
        QCAlgorithm algorithm;
        Symbol symbol;
        decimal price;
        MovingAverageConvergenceDivergence macd;
        ExponentialMovingAverage ema20;
        ExponentialMovingAverage ema50;
        AroonOscillator aroon;
        // Keep history for 15 days
        RollingWindow<AroonOscillatorState> aroonHistory = new RollingWindow<AroonOscillatorState>(15);
        Resolution resolution;

        public Indicators(QCAlgorithm algorithm, Symbol symbol, Resolution resolution)
        {
            this.algorithm = algorithm;
            this.symbol = symbol;
            this.resolution = resolution;

            this.macd = new MovingAverageConvergenceDivergence(12, 26, 9, MovingAverageType.Wilders);
            this.ema20 = new ExponentialMovingAverage((int)(3 * 6.5 * 60));
            this.ema50 = new ExponentialMovingAverage(50); ;
            this.aroon = new AroonOscillator(20, 20);
        }

        public void Reset()
        {
            this.macd.Reset();
            this.ema20.Reset();
            this.ema50.Reset();
            this.aroon.Reset();
        }

        public bool ShouldBuy()
        {
            if (IsReady())
            {
                if (IsMacdInUptrend() && IsEma20AboveEma50())
                {
                    if (this.price > this.ema20)
                    {
                        if (HasAroonCrossedUp() && IsAroonAbove50())
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        public bool ShouldCover()
        {
            if (IsReady())
            {
                if (IsMacdInUptrend() || IsEma20AboveEma50())
                {
                    if (this.price > this.ema20)
                    {
                        if (HasAroonCrossedUp() && IsAroonAbove50())
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        public bool ShouldSell()
        {
            if (IsReady())
            {
                if (!IsMacdInUptrend() || !IsEma20AboveEma50())
                {
                    if (this.price < this.ema20)
                    {
                        if (HasAroonCrossedDown() && IsAroonAbove50())
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        public bool ShouldShort()
        {
            if (IsReady())
            {
                if (!IsMacdInUptrend() && !IsEma20AboveEma50())
                {
                    //                        if (this.price < this.ema20)
                    {
                        if (HasAroonCrossedDown() && IsAroonAbove50())
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        public void CalculateFromHistory()
        {
            this.Reset();

            IEnumerable<TradeBar> history = this.algorithm.History(this.symbol, 100, this.resolution);
            foreach (TradeBar tradeBar in history)
            {
                this.Update(tradeBar);
            }
        }

        public bool Update(TradeBar tradeBar)
        {
            DateTime time = tradeBar.EndTime;
            this.price = tradeBar.Close;

            bool result = this.macd.Update(time, price) && this.ema20.Update(time, price) && this.ema50.Update(time, price) && this.aroon.Update(tradeBar);
            SaveAroonHistory();

            return result;
        }

        public bool IsReady()
        {
            return (this.macd.IsReady && this.ema20.IsReady && this.ema50.IsReady && this.aroon.IsReady && this.aroonHistory.IsReady);
        }

        public bool IsMacdInUptrend()
        {
            return (this.macd > this.macd.Signal);
        }

        public bool IsEma20AboveEma50()
        {
            return (this.ema20 > this.ema50);
        }

        public bool HasAroonCrossedUp()
        {
            // cross up between 2 days ago and today
            return this.aroon.AroonUp.Current.Value > this.aroon.AroonDown.Current.Value && this.aroonHistory[2].Up < this.aroonHistory[2].Down;
        }

        public bool HasAroonCrossedDown()
        {
            // cross down between 2 days ago and today
            return this.aroon.AroonUp.Current.Value < this.aroon.AroonDown.Current.Value && this.aroonHistory[2].Up > this.aroonHistory[2].Down;
        }

        public bool IsAroonAbove50()
        {
            return this.aroon.AroonUp.Current.Value > 55 && this.aroon.AroonDown.Current.Value > 53;
        }

        public void SaveAroonHistory()
        {
            this.aroonHistory.Add(new AroonOscillatorState(this.aroon));
        }

        internal bool ShouldSellPut()
        {
            return this.IsReady() && IsMacdInUptrend();
        }

        internal bool ShouldSellCall()
        {
            return this.IsReady() && !this.IsEma20AboveEma50();
        }
    }

    // class to hold the current state of a AroonOscillator instance
    public class AroonOscillatorState
    {
        public readonly decimal Up;
        public readonly decimal Down;

        public AroonOscillatorState(AroonOscillator aroon)
        {
            Up = aroon.AroonUp.Current.Value;
            Down = aroon.AroonDown.Current.Value;
        }
    }
}