Overall Statistics
Total Trades
1902
Average Win
0.15%
Average Loss
-0.12%
Compounding Annual Return
8.367%
Drawdown
8.800%
Expectancy
0.115
Net Profit
13.719%
Sharpe Ratio
0.95
Probabilistic Sharpe Ratio
45.218%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.27
Alpha
0.076
Beta
-0.019
Annual Standard Deviation
0.075
Annual Variance
0.006
Information Ratio
-0.595
Tracking Error
0.267
Treynor Ratio
-3.758
Total Fees
$2636.58
Estimated Strategy Capacity
$260000.00
Lowest Capacity Asset
FXA TJSL8DEZVWBP
dev_mode = False # for Shile's use, keep False
if dev_mode:
    from AlgorithmImports import *

# ---indicies---
market = 'SPY'
silver = 'SLV'
gold = 'GLD'
utility = 'XLU'
industrial = 'XLI'
safe = 'FXF'  # safe currency 
risk = 'FXA'  # risk currency
debt_short = 'SHY'
debt_inflation = 'TIP'
metal = 'DBB'
inp = 'IGE' # input
cash = 'UUP'

# ---equities
equities = ['AAPL', 'AMGN', 'AXP', 'BA', 'CAT', 'CRM', 'CSCO', 'CVX', 'DIS', 'DOW', 'GS', 'HD', 'IBM', 'INTC', 'JNJ', 'JPM', 'KO', 'MCD', 'MMM', 'MRK', 'MSFT', 'NKE', 'PG', 'TRV', 'UNH', 'V', 'VZ', 'WBA', 'WMT']
#equities = ['ATVI', 'ADBE', 'AMD', 'ALGN', 'ALXN', 'AMZN', 'AMGN', 'AAL', 'ADI', 'AAPL', 'AMAT', 'ASML', 'ADSK', 'ADP', 'AVGO', 'BIDU', 'BIIB', 'BMRN', 'CDNS', 'CERN', 'CHKP', 'CHTR', 'TCOM', 'CTAS', 'CSCO', 'CTXS', 'CMCSA', 'COST', 'CSX', 'CTSH', 'DLTR', 'EA', 'EBAY', 'EXC', 'EXPE', 'FAST', 'FB', 'FISV', 'GILD', 'GOOG', 'GOOGL', 'HAS', 'HSIC', 'ILMN', 'INCY', 'INTC', 'INTU', 'ISRG', 'IDXX', 'JBHT', 'JD', 'KLAC', 'KHC', 'LRCX', 'LBTYA', 'LBTYK', 'LULU', 'MELI', 'MAR', 'MCHP', 'MDLZ', 'MNST', 'MSFT', 'MU', 'MXIM', 'MYL', 'NTAP', 'NFLX', 'NTES', 'NVDA', 'NXPI', 'ORLY', 'PAYX', 'PCAR', 'BKNG', 'PYPL', 'PEP', 'QCOM', 'REGN', 'ROST', 'SIRI', 'SWKS', 'SBUX', 'NLOK', 'SNPS', 'TTWO', 'TSLA', 'TXN', 'TMUS', 'ULTA', 'UAL', 'VRSN', 'VRSK', 'VRTX', 'WBA', 'WDC', 'WDAY', 'WYNN', 'XEL', 'XLNX']

    
    #'A','AAP','AAPL','ABC','ABMD','ABT','ADBE','ADI','ADM','ADP','ADSK','AGN','AGR','AKAM','ALB','ALGN','ALK','ALLE','ALV','ALXN','AMAT','AMD','AME','AMGN','AMT','ANET','ANSS','AOS','APD','APTV','ARW','ASH','ATO','ATVI','AVB','AVY','AYI','AZO','BAX','BBY','BG','BIIB','BIO','BKNG','BKR','BMRN','BSX','BURL','BWA','CAH','CCEP','CDNS','CDW','CERN','CF','CGNX','CHD','CHRW','CL','CLX','CMI','COG','COO','COP','CPRI','CPRT','CRM','CSCO','CSX','CTAS','CTSH','CTVA','CTXS','CVS','CVX','CXO','DAL','DD','DGX','DHI','DHR','DLTR','DOV','DOW','DOX','DRE','DVA','DVN','EBAY','ECL','EIX','EL','ELAN','EMR','EOG','EQIX','ETN','EW','EXC','EXPD','EXPE','FANG','FAST','FB','FCX','FDX','FFIV','FISV','FL','FLEX','FLS','FLT','FMC','FTI','FTNT','FTV','GDDY','GILD','GLW','GNTX','GOOG','GOOGL','GPC','GPS','GRMN','GRUB','GWW','HAL','HAS','HD','HEI','HEIA','HES','HFC','HOLX','HP','HPE','HPQ','HSIC','HSY','IBM','IDXX','IEX','IFF','ILMN','INCY','INFO','INTC','INTU','IP','IPG','IPGP','IR','ISRG','IT','ITW','JAZZ','JBHT','JBL','JCI','JNJ','JNPR','JWN','KDP','KEYS','KHC','KLAC','KMB','KNX','KO','KSS','KSU','LDOS','LEA','LEN','LIN','LKQ','LLY','LOW','LRCX','LULU','LUV','LW','LYB','M','MAS','MCD','MCK','MDLZ','MDT','MDU','MHK','MKC','MLM','MMM','MNST','MOS','MPC','MRK','MRO','MRVL','MSFT','MSI','MTD','MU','MXIM','NBL','NEM','NKE','NLOK','NOV','NOW','NSC','NTAP','NUE','NVDA','NVR','NWS','NWSA','NXPI','ODFL','OGE','OMC','ORCL','ORLY','OXY','PAYC','PAYX','PCG','PEG','PEP','PFE','PG','PHM','PKG','PKI','PLD','PNR','PNW','PPG','PRGO','PSA','PSX','PTC','PVH','PXD','QCOM','QRVO','REGN','RHI','RL','RMD','ROK','ROL','ROP','ROST','SBUX','SCCO','SHW','SLB','SNA','SNPS','ST','STE','STLD','STX','SWK','SWKS','SYK','TEL','TFX','TGT','TIF','TJX','TMO','TOL','TPR','TRMB','TSCO','TSLA','TT','TWTR','TXN','TYL','UA','UAA','UAL','ULTA','UNP','UPS','VAR','VFC','VLO','VMC','VMW','VRSN','VRTX','WAB','WAT','WBA','WBC','WDAY','WDC','WHR','WLK','WY','XEC','XOM','XRAY','XRX','XYL','YNDX','ZBRA']
    


# ---safeties
safeties = ['FXF', 'FXA', 'UUP']
#safeties = ['FXF', 'FXA']

# ---invest
Invest = 100000
#StartDay = 2020,1,1

# --universe resolution
resolution = Resolution.Daily                       # don't touch unless you are removing all options logic
InOut_resolution = Resolution.Daily
returns_resolution = Resolution.Daily

# disable select indicators
disableSupertrend = False
disableSqueeze = False
disableInAndOut = False  # setting this to True -> go long always except 10% drawdown

# ---in and out parameters
# parameters found from file from you
bull = True # set False for bear

inOutLookbackBull = 30
inOutLookbackBear = 5
waitDaysConstant = 1 # WAITD_CONSTANT from your file
iniWaitDays = 1# INI_WAIT_DAYS from your file
minWaitDays = 1 # 60 from the `min(60, self.WDadjvar)` from your file

# ---supertrend parameters
superTrendPeriod = 1
superTrendMultiple = 3
superTrendUseHA = True  # use Heiken-Ashii for superTrend

# ---squeeze parameters
squeezeTrendPeriod = 1
squeezeBBMultiple = 2 # BollingerBands
squeezeKeltMultiple = 3 # Kelter Channel (originally 1.5, increased to increase bullish trades)
# the lower the BBMultiple and the higher the KeltMultiple
# the more likely squeeze will allow trades

# ---portfolio parameters
# for the returns based portfolio allocation
max_drawdown = 0.5 # max drawdown allowed before liquidation is signaled
drawdown_waitdays = 1 # number of days to stay out of the market after drawdown liquidation 
drawdown_lookback = 1 # lookback for drawdon
max_alloc = .05 # max allocation to any given stock
returns_lookback = 1 # lookback for returns

# ---general parameters
rebalance = 1  # how often you want to update weights for rebalancing, refresh in&out signal

options_weight = 0 # what % of portfolio for options
equities_weight = 1 - options_weight

# specific options parameters
optionright = OptionRight.Call
price_strike_ratio = .95
'''
price_strike_ratio = (current equity price) / (strike price)
If call option and price_strike_ratio = .9
    that means we want calls whose underlying is 90% of the strike price - which means it is .1 (10%) OTM
Similary, call option and price_strike_ratio = 1.1  -  that means we want calls 10% ITM
Reverse the logic for puts
'''
expiry_liquidation = 7 # how many days before expiry to sell option
min_expiry = 50 # option will expire in at least this many days before buying

debug = False # show debug messages

dev_mode = False
if dev_mode:
    from AlgorithmImports import *
    
from collections import deque
from datetime import datetime 
from typing import Union
import pandas as pd

class MySuperTrend:
    def __init__(self, period, multiple, movingAverageType=MovingAverageType.Simple):
        self.Name = "SuperTrend"
        self.Time = datetime.min
        self.Value = 0
        self.multiplier = multiple
        self.atr = AverageTrueRange(period, movingAverageType)
        self.values = deque(maxlen=period)
        self.previousTrailingLowerBand = 0
        self.previousTrailingUpperBand = 0
        self.previousClose = 0
        self.previousTrend = 0
        
    def __repr__(self):
        return "{0} -> IsReady: {1}. Time: {2}. Value: {3}".format(self.Name, self.IsReady, self.Time, self.Value)

    def Update(self, input:TradeBar):
        self.Time = input.EndTime
        self.atr.Update(input)
        superTrend = 0
        currentClose = input.Close
        currentBasicLowerBand = (input.Low + input.High) / 2 - self.multiplier * self.atr.Current.Value
        currentBasicUpperBand = (input.Low + input.High) / 2 + self.multiplier * self.atr.Current.Value

        if self.previousClose > self.previousTrailingLowerBand:
            currentTrailingLowerBand = max(currentBasicLowerBand, self.previousTrailingLowerBand)
        else:
            currentTrailingLowerBand = currentBasicLowerBand

        if self.previousClose < self.previousTrailingUpperBand:
            currentTrailingUpperBand = min(currentBasicUpperBand, self.previousTrailingUpperBand)
        else:
            currentTrailingUpperBand = currentBasicUpperBand

        if currentClose > currentTrailingUpperBand:
            currentTrend = 1
        elif currentClose < currentTrailingLowerBand:
            currentTrend = -1
        else:
            currentTrend = self.previousTrend

        if currentTrend == 1:
            superTrend = currentTrailingLowerBand
        elif currentTrend == -1:
            superTrend = currentTrailingUpperBand
        
        self.previousTrailingLowerBand = currentTrailingLowerBand
        self.previousTrailingUpperBand = currentTrailingUpperBand
        self.previousClose = currentClose
        self.previousTrend = currentTrend

        if not self.atr.IsReady:
            return 0
        
        self.Value = superTrend
        return self.IsReady
    
    @property
    def IsReady(self):
        return self.atr.IsReady and self.Value != 0

class Squeeze:
    '''
    .Value = 1 iff "squeezed" else .Value = 0
    Tells us if we are in or out of squeeze
    Is Squeeze: lower BB > lower Keltner and upper BB < upper Keltner
    '''
    def __init__(self, period, squeezeBBMultiple, squeezeKeltMultiple, movingAverageType=MovingAverageType.Simple):
        '''
        Value = 1 iff "squeezed" else .Value = 0
        '''
        self.Name = "Squeeze"
        self.Time = datetime.min
        self.Value = 0
        self.bb = BollingerBands(period, squeezeBBMultiple, movingAverageType)
        self.kelt = KeltnerChannels(period, squeezeKeltMultiple, movingAverageType)

    def __repr__(self):
        return "{0} -> IsReady: {1}. Time: {2}. Value: {3}".format(self.Name, self.IsReady, self.Time, self.Value)

    def Update(self, input:TradeBar):
        self.Time = input.EndTime
        self.kelt.Update(input)
        self.bb.Update(input.EndTime, input.Close)
        isSqueeze = self.bb.UpperBand.Current.Value > self.kelt.UpperBand.Current.Value
        self.Value = int(isSqueeze)
        return self.IsReady 

    @property
    def IsReady(self):
        return self.kelt.IsReady and self.bb.IsReady

class Drawdown:
    def __init__(self, period:int):
        '''
        drawdown indicator for past `period` values
        Call Update with floats that represent returns
        '''
        self.values = deque(maxlen=period)
        self.Value = 0

    def Update(self, input:Union[TradeBar, float]):
        if isinstance(input, float):
            self.values.append(input)
        else:
            self.values.append(input.Close)
       
        # https://stackoverflow.com/questions/36750571/calculate-max-draw-down-with-a-vectorized-solution-in-python
        cum_returns = (1 + pd.Series(self.values)).cumprod()
        self.Value = 1 - cum_returns.div(cum_returns.cummax()).iloc[-1]  # drawdown
        return self.IsReady
        
    @property
    def IsReady(self):
        return len(self.values) == self.values.maxlen

from collections import deque
from collections.abc import Iterable
from datetime import datetime
from typing import Deque, Dict, List, Union

import numpy as np
import pandas as pd

#from indicators import Drawdown
dev_mode = False
if dev_mode:
    from AlgorithmImports import *

class InAndOut:
    def __init__(self, algo:QCAlgorithm, InOut_resolution, symbols:List[str], period:int, iniWaitDays, minWaitDays, waitDaysConst, bull=True):
        self.Time = datetime.min
        
        self.period = period
        self.iniWaitDays = iniWaitDays
        self.minWaitDays = minWaitDays
        self.waitDaysConst = waitDaysConst
        self.bull = bull

        (self.market, self.silver, self.gold, self.utility, 
            self.industrial, self.safe, self.risk, self.debt_short, 
            self.debt_inflation, self.metal, self.input, self.cash) = symbols

        self.bull_signal_indices = [self.industrial, self.metal, self.input]
        self.history: dict[Symbol, Deque[float]] = {}
        history_df = algo.History(symbols, period, resolution)

        for symbol in symbols:
            if symbol in history_df and len(history_df[symbol]) > 0:
                self.history[symbol] = deque(history_df[symbol]['close'], maxlen=period)
            else:
                self.history[symbol] = deque(maxlen=period)

        self.wait_days = 0

    def Update(self, input:Slice):
        self.Time = input.Time

        for symbol, history in self.history.items():
            if input.Bars.ContainsKey(symbol):
                history.append(input[symbol].Close)
        
    def is_bullish(self):
        '''
        returns (true iff "bullish", wait_days)
        '''
        history_dict = {symbol: pd.Series(data) for symbol, data in self.history.items()}

        # (100 day returns / 11 day centered sma shifted by 60 days) - 1
        cust_returns_dict: dict[Union[Symbol, str], pd.Series] = {}
        for symbol in history_dict:
            if len(history_dict[symbol]) != self.period:
                return False, 0
        
            hist_series = history_dict[symbol]
            cust_returns_dict[symbol] = (hist_series / hist_series.rolling(5, center=True).mean().shift(10)).dropna() - 1
            history_dict[symbol] = history_dict[symbol][-len(cust_returns_dict[symbol]):]  # make all series the same length

        gold_min_silver = 'gold_min_silver'
        industrial_min_utility = 'industrial_min_utility'
        risk_min_safe = 'risk_min_safe'
        cash_inverse = 'cash_inverse'

        cust_returns_dict[gold_min_silver] = cust_returns_dict[self.gold] - cust_returns_dict[self.silver]
        cust_returns_dict[industrial_min_utility] = cust_returns_dict[self.industrial] - cust_returns_dict[self.utility]
        cust_returns_dict[risk_min_safe] = cust_returns_dict[self.risk] - cust_returns_dict[self.safe]
        cust_returns_dict[cash_inverse] = -1 * cust_returns_dict[self.cash]

        # values are true if last return is < 1 percentile
        is_extreme_returns_dict: dict[Union[Symbol, str], bool] = {}
        for symbol, returns in cust_returns_dict.items():
            is_extreme_returns_dict[symbol] = returns.iloc[-1] < np.percentile(returns, 1)

        inflation = 'inflation'
        history_dict[inflation] = cust_returns_dict[self.debt_short] - cust_returns_dict[self.debt_inflation]
        
        isabovemedian_dict = {
            symbol: (series.iloc[-1] > series.median()) for symbol, series in history_dict.items()
        }

        interest_expected = 'interest_expected'

        if is_extreme_returns_dict[self.debt_short] and isabovemedian_dict[self.metal] and isabovemedian_dict[self.input]:
            is_extreme_returns_dict[interest_expected] = False
        else:
            is_extreme_returns_dict[interest_expected] = is_extreme_returns_dict[self.debt_short]

        gold_min_silver_adj = 'gold_min_silver_adj'
    
        if is_extreme_returns_dict[gold_min_silver] and isabovemedian_dict[inflation]:
            is_extreme_returns_dict[gold_min_silver_adj] = False
        else:
            is_extreme_returns_dict[gold_min_silver_adj] = is_extreme_returns_dict[gold_min_silver]
        
        def wait_days_helper(symbol0, symbol1):
            series0 = cust_returns_dict[symbol0]
            series1 = cust_returns_dict[symbol1]
            if series0.iloc[-1] > 0 and series1.iloc[-1] < 0 and series1.iloc[-2] > 0:
                return self.iniWaitDays
            else:
                return 1

        self.wait_days = int(
            max(
                self.wait_days/2,
                self.iniWaitDays * max (
                    1, 
                    wait_days_helper(self.gold, self.silver), 
                    wait_days_helper(self.utility, self.industrial), 
                    wait_days_helper(self.safe, self.risk)
                )
            )
        )

        signals = self.bull_signal_indices + [gold_min_silver_adj, industrial_min_utility, risk_min_safe, cash_inverse]
        
        bullish = any([is_extreme_returns_dict[signal] for signal in signals])
        return bullish, min(self.minWaitDays, self.wait_days)

    def is_bearish(self):
        '''
        returns (true iff "bearish", wait_days)
        '''
        market_returns = pd.Series(self.history[self.market]).pct_change().dropna()
        volatililty = .6 * np.sqrt(252) * np.log1p(market_returns).std()
        returns_lookback = int(min(
            (1-volatililty)*self.waitDaysConst,
            self.period
        ))
        wait_days = int(volatililty * self.waitDaysConst)
        
        signals = [self.silver, self.gold, self.industrial, self.utility, self.metal, self.cash]

        returns = {}
        for signal in signals:
            data = self.history[signal]
            if len(data) < returns_lookback:
                return False, 0
            returns[signal] = pd.Series(data).pct_change(returns_lookback).iloc[-1]

        def compare(symbol0, symbol1):
            return returns[symbol0] < returns[symbol1]
        
        compares = compare(self.silver, self.gold) and compare(self.industrial, self.utility) and compare(self.metal, self.cash)
        return compares, wait_days            

    def minmax(self, n1, min_val, max_val):
        return max(min(n1, min_val), max_val)

    def get_signal(self):
        '''
        returns (true iff "bullish" and bull==True, wait_days) 
        returns (true iff "bearish" and bull==False, wait_days)
        '''
        if self.bull:
            return self.is_bullish()
        else:
            return self.is_bearish()

class ReturnsManager:
    def __init__(self, algo, period, returns_resolution, max_drawdown, drawdown_lookback, max_alloc):
        '''
        Manages asset weighting. Get weights with the `GetWeights()` method.
        Also tells us if the `max_drawdown` has been reached with `IsSell()` method
        '''
        self.algo = algo
        self.period = period
        self.drawdown_lookback = drawdown_lookback
        #self.resolution = resolution
    
        # one day returns
        self.daily_returns_dict: Dict[Symbol, RateOfChange] = {}
        # `period` day returns
        self.returns_dict: Dict[Symbol, RateOfChange] = {}
        self.dd = Drawdown(self.drawdown_lookback)

        self.weights_dict: Dict[Symbol, float] = {}

        self.max_drawdown = max_drawdown
        self.max_alloc = max_alloc
        
    def Update(self, input:Slice):
        portfolio_returns_cross_section = 0
        
        # update daily returns and period returns
        for symbol in self.returns_dict: 
            if input.Bars.ContainsKey(symbol):
                daily_returns = self.daily_returns_dict[symbol]
                daily_returns.Update(input.Time, input[symbol].Close)
                self.returns_dict[symbol].Update(input.Time, input[symbol].Close)
                
                if symbol in self.weights_dict and daily_returns.IsReady:
                    # weighted returns of one item in the portfolio
                    item_returns = self.weights_dict[symbol] * daily_returns.Current.Value
                    portfolio_returns_cross_section += item_returns

        if portfolio_returns_cross_section:
            self.dd.Update(portfolio_returns_cross_section)

    def UpdateWeights(self):
        # used to divide weights to make them sum to 1
        total_weight = sum(
            returns.Current.Value 
            for returns in self.returns_dict.values() 
            if returns.IsReady
        )

        if not total_weight:
            return {}

        self.weights_dict = {
            symbol: min(returns.Current.Value / total_weight, self.max_alloc)
                for symbol, returns in self.returns_dict.items()
        }
        return self.weights_dict

    def GetWeights(self):
        return self.weights_dict

    def IsSell(self):
        return self.dd.Value > self.max_drawdown

    @property
    def IsReady(self):
        return np.any([returns.IsReady for symbol, returns in self.returns_dict.items()]) and len(self.weights_dict) > 0

    def WarmUp(self, symbols:Union[Symbol, None]):
        '''
        warmups the symbol/symbols indicators
        '''
        if not isinstance(symbols, Iterable):
            symbols = [symbols] 

        hist = self.algo.History(symbols, self.period, self.resolution)
        for symbol in symbols:
            if symbol not in hist.index or not len(hist.loc[symbol]):
                continue
            closes = hist.loc[symbol]['close']
            for dt, close in closes.iteritems():
                self.daily_returns_dict[symbol].Update(dt, close)
                self.returns_dict[symbol].Update(dt, close)

    def AddSecurity(self, symbol:Symbol):
        '''
        registers the new symbol to the ReturnsManager
        '''
        if symbol not in self.returns_dict:
            self.daily_returns_dict[symbol] = RateOfChange(1)
            self.returns_dict[symbol] = RateOfChange(self.period)
            # self.WarmUp(symbol)


#import configs as cfg
dev_mode = False
if dev_mode:
    from AlgorithmImports import *
    from datetime import timedelta

from typing import Dict, List
#from indicators import MySuperTrend, Squeeze
#from aggregate_indicators import InAndOut, ReturnsManager
import operator

class AdaptableRedSnake(QCAlgorithm):
    def load_configs_and_indicators(self):
        # OVERRIDE CONFIGS HERE
        # EXAMPLE: returns_lookback = self.GetParameter('returns_lookback')
        
        self.Cash = Invest
        
        index_tickers = [market, silver, gold, utility, industrial, safe, risk, debt_short, debt_inflation, metal, inp, cash]
        self.indices = [
            self.AddEquity(ticker, resolution).Symbol for ticker in index_tickers
        ]

        inOutLookback = inOutLookbackBull if bull else inOutLookbackBear
        self.inandout = InAndOut(self, resolution, self.indices, inOutLookback, iniWaitDays, minWaitDays, waitDaysConstant, bull)
        self.returnsmanager = ReturnsManager(self, returns_lookback, resolution, max_drawdown, drawdown_lookback, max_alloc)

        self.equities = [
            self.AddEquity(ticker, resolution).Symbol for ticker in equities
        ]
        
        for equity in self.equities:
            self.Securities[equity].SetDataNormalizationMode(DataNormalizationMode.Raw)
    
        self.safeties = [
            self.AddEquity(ticker, resolution).Symbol for ticker in safeties
        ]
        
        for safety in self.safeties:
            self.Securities[safety].SetDataNormalizationMode(DataNormalizationMode.Raw)
        
        self.symbolData = {
            symbol: SymbolData(self, symbol, resolution,
                superTrendPeriod, superTrendMultiple, 
                squeezeTrendPeriod, squeezeBBMultiple, 
                squeezeKeltMultiple, superTrendUseHA)
                for symbol in self.equities + self.safeties
        }

        self.universe = self.equities
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
            
        self.next_reentry = self.Time
        self.was_bull = False
        self.inout_signal = False
        self.inout_waitdays = 0

        for symbol in self.equities + self.safeties:
            self.returnsmanager.AddSecurity(symbol)

        self.SetWarmUp(max(
            inOutLookbackBear, inOutLookbackBull, returns_lookback
        ), InOut_resolution)
        
        self.SetWarmUp(max(
            superTrendPeriod, squeezeTrendPeriod
        ), resolution)

    def Initialize(self):
        # self.SetSecurityInitializer(
        #     lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw) 
        #         if x.Type == SecurityType.Equity else None
        # )
        self.load_configs_and_indicators()
        
        self.market = market
        self.SetBenchmark(self.market)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage)
        #self.SetWarmUp(252, Resolution.Hour) 

        self.SetStartDate(2020, 1, 1)
        #self.SetStartDate(StartDay)
        self.SetCash(Invest) 

        # self.Schedule.On(self.DateRules.WeekStart(), self.TimeRules.Midnight, self.WeekleyFn)  
        self.days_count = 0
        self.curr_day = -1

        # equity Symbol: option Symbol
        self.options: Dict[Symbol, Symbol] = {}

        # flag to make sure "rebalance" logic is called immediately
        self.init_rebalance = True

    def Print(self, msg):
        if debug:
            self.Debug(msg)

    def OnData(self, data:Slice):
        for symbol, symbolData in self.symbolData.items():
            if data.Bars.ContainsKey(symbol):
                symbolData.Update(data[symbol])
        
        if self.was_bull and not self.IsWarmingUp:
            self.rebalance(data)
        
        if self.curr_day == self.Time.day:
            return 
        self.curr_day = self.Time.day

        self.CheckOptions()

        # EQUITIES LOGIC
        if not any([data.Bars.ContainsKey(symbol) for symbol in self.equities]):
            return

        self.days_count += 1
        self.inandout.Update(data)
        self.returnsmanager.Update(data)
        if self.init_rebalance or self.days_count % rebalance == 0:
            self.init_rebalance = False
            self.returnsmanager.UpdateWeights()
            self.inout_signal, self.inout_waitdays = self.inandout.get_signal()
        
        if self.IsWarmingUp:
            return

        if self.returnsmanager.IsSell():
            self.Liquidate()
            self.next_reentry = self.Time + timedelta(days=drawdown_waitdays)
        elif (self.inout_signal or disableInAndOut):
            debug and self.Print('Bull Condition Reached')
            if not self.was_bull and self.Time >= self.next_reentry:
                self.Print('Going Bull')
                self.SetOptions()
                self.go_bull()
                self.was_bull = True
        elif self.was_bull:
            self.Print(f'Going Bear:')
            self.SetOptions()
            self.go_bear()
            self.was_bull = False
            self.next_reentry = self.Time + timedelta(days=self.inout_waitdays)
            self.buy_options = True
        
        self.PlotSeries()
    
    def BuyOptions(self, data:Slice, equities:List[Symbol]):
        # OPTIONS LOGIC
        options: List[Symbol] = []
        for equity, option in   self.options.items():
            if option is None or not data.ContainsKey(option) or equity not in equities:
                continue
            options.append(option)

        invested_options = {
            kvp.Key.Underlying: kvp.Key for kvp in self.Portfolio 
                if kvp.Value.Invested and kvp.Key.SecurityType == SecurityType.Option 
        }

        for option in options:
            if not option.Underlying in invested_options.keys() and not self.Securities[option].Invested:
                if options_weight:
                    self.SetHoldings(option, (int(self.was_bull) * 2 - 1) * options_weight / len(options))


    def CheckOptions(self):
        '''
        Sell options near expiry, set new options for liquidated ones
        '''

        invested_options = {
            kvp.Key.Underlying: kvp.Key for kvp in self.Portfolio 
                if kvp.Value.Invested and kvp.Key.SecurityType == SecurityType.Option 
        }

        for equity, option in invested_options.items():
            if option.ID.Date - self.Time <= timedelta(days=expiry_liquidation):
                self.Liquidate(option)
                if option in self.options.keys():
                    self.SetOptions(equity)

    def SetOptions(self, equity=None):
        '''
        Set new options in the self.options dictionary
        '''
        if self.IsWarmingUp:
            return
        self.Print('Setting Options')
        if equity is None:
            equities = self.equities + self.safeties
        else:
            equities = [equity]
        
        for equity in equities:
            contracts = self.OptionChainProvider.GetOptionContractList(equity, self.Time)
            if not contracts:
                continue
            equity_price = self.Securities[equity].Price

            if optionright == OptionRight.Call:
                compare = operator.lt if price_strike_ratio < 1 else operator.gt
            else:
                compare = operator.lt if price_strike_ratio >= 1 else operator.gt
            
            def filter(id: SecurityIdentifier):
                return (
                    id.OptionRight == optionright 
                    and compare(equity_price / id.StrikePrice, price_strike_ratio)
                    and id.Date - self.Time > timedelta(days=min_expiry)
                )

            # filter
            contracts = [contract for contract in contracts if filter(contract.ID)]
            
            if not contracts:
                continue

            # sort by date
            contracts = sorted(contracts, key=lambda x: x.ID.Date)
            
            # sort by closest strike.
            contracts = sorted(contracts, key=lambda x: abs(equity_price - x.ID.StrikePrice))

            if contracts:
                contract = contracts[0]
                
                self.AddOptionContract(contract, Resolution.Minute)
                self.options[equity] = contract
                
        return self.options

    def PlotSeries(self): 
        self.Plot('BullBear', 'bull=1,bear=0', int(self.was_bull))
 
    def go_bear(self):
        if self.IsWarmingUp:
            return
        self.Liquidate()

        self.universe = self.safeties
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
        
        for symbol in self.safeties:
            self.SetHoldings(symbol, equities_weight/len(self.safeties))
        self.was_bull = False

    def go_bull(self):
        if self.IsWarmingUp:
            return
        self.Liquidate()
        for symbol, symbolData in self.symbolData.items():
            if symbolData.IsBuy(self.Securities[symbol].Price):
                weight = self.returnsmanager.GetWeights().get(symbol, 0)
                if weight:
                    self.SetHoldings(symbol, weight * equities_weight)

    def rebalance(self, data:Slice):
        bought = []
        for symbol, symbolData in self.symbolData.items():
            if symbol not in self.universe:
                continue
            isbuy = symbolData.IsBuy(self.Securities[symbol].Price)
            if not isbuy:
                self.Liquidate(symbol)
            elif not self.Portfolio[symbol].Invested:
                weight = self.returnsmanager.GetWeights().get(symbol, 0)
                self.SetHoldings(symbol, weight * equities_weight)
                bought.append(symbol)

        self.BuyOptions(data, bought)


class SymbolData:
    def __init__(self, algo:QCAlgorithm, symbol, resolution, periodST, multipleST, periodSQ, BBmultipleSQ, KELTmultipleSQ, useHA):
        self.symbol = symbol
        self.supertrend = MySuperTrend(periodST, multipleST)
        self.squeeze = Squeeze(periodSQ, BBmultipleSQ, KELTmultipleSQ)
        
        self.algo = algo

        self.useHA = useHA
        if useHA:
            self.ha = HeikinAshi(symbol)

    def Update(self, input:TradeBar):
        if self.useHA:
            self.ha.Update(input)
            if self.ha.IsReady:
                haBar = TradeBar(self.algo.Time, self.symbol, 
                    self.ha.Open.Current.Value, self.ha.High.Current.Value, 
                    self.ha.Low.Current.Value, self.ha.Close.Current.Value, self.ha.Volume.Current.Value)
                self.supertrend.Update(haBar)
        else:
            self.supertrend.Update(input)

        self.squeeze.Update(input)

    def IsBuy(self, price):
        return (self.squeeze.Value or disableSqueeze) or (price > self.supertrend.Value or disableSupertrend)