Overall Statistics
Total Trades
69
Average Win
0.05%
Average Loss
-0.06%
Compounding Annual Return
-1.201%
Drawdown
0.800%
Expectancy
-0.138
Net Profit
-0.304%
Sharpe Ratio
-0.765
Probabilistic Sharpe Ratio
14.423%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
0.83
Alpha
-0.008
Beta
0.01
Annual Standard Deviation
0.011
Annual Variance
0
Information Ratio
-0.143
Tracking Error
0.092
Treynor Ratio
-0.818
Total Fees
$69.00
Estimated Strategy Capacity
$1400000.00
Lowest Capacity Asset
SGEN S2TCB9V1OIG5
"""
Moving Average Cross Universe Strategy
Version 1.0.0
Platform: QuantConnect
By: Aaron Eller
www.excelintrading.com
aaron@excelintrading.com

Revision Notes:
    1.0.0 (01/17/2020) - Initial. Started from "Universe Strategy_v103". Also 
                          copied SymbolData logic from 
                          "Moving Average Crossover_v107".
                          
References:
-QC (Lean) Class List
  https://lean-api-docs.netlify.app/annotated.html
-OrderTicket properties
  https://lean-api-docs.netlify.app/classQuantConnect_1_1Orders_1_1OrderTicket.html
-QC Universe
  https://www.quantconnect.com/docs/algorithm-reference/universes
-QC Universe Settings
  https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Universe-Settings
-QC Universe Fundamentals
  https://www.quantconnect.com/docs/data-library/fundamentals
-Speeding up QC Universe
  https://www.quantconnect.com/forum/discussion/7875/speeding-up-universe-selection/p1
"""
# Standard library imports
import datetime as DT

###############################################################################
# Backtest inputs
START_DATE = "07-01-2021" # must be in "MM-DD-YYYY" format
END_DATE   = "09-30-2021" #None # must be in "MM-DD-YYYY" format or None
CASH = 100000 # starting portfolio value
TIMEZONE = "US/Eastern" # e.g. "US/Eastern", "US/Central", "US/Pacific"

#-------------------------------------------------------------------------------
# DATA INPUTS

# Define the data resolution to be fed to the algorithm
# Must be "SECOND", or "MINUTE"
DATA_RESOLUTION = 'MINUTE'                                                      # NEW

# How often to update the universe?
# Options: 'daily', 'weekly', or 'monthly'
UNIVERSE_FREQUENCY = 'monthly'                                                  # NEW

#-------------------------------------------------------------------------------
# COARSE UNIVERSE SELECTION INPUTS

REQUIRE_FUNDAMENTAL_DATA = True # if True, stocks only / no etfs e.g.
MIN_PRICE = 10.0 # set to 0 to disable
MAX_PRICE = 1e6 # set to 1e6 to disable
MIN_DAILY_VOLUME = 0 # set to 0 to disable
MIN_DAILY_DOLLAR_VOLUME = 10e6 # dollar volume = last price times volume 

#-------------------------------------------------------------------------------
# FINE UNIVERSE SELECTION INPUTS

# Market cap filtering / e6=million/e9=billion/e12=trillion
MIN_MARKET_CAP = 10e9
MAX_MARKET_CAP = 10e12

# Turn on/off specific exchanges allowed
ARCX = False # Archipelago Electronic Communications Network
ASE = False # American Stock Exchange
BATS = False # Better Alternative Trading System
NAS = True # Nasdaq Stock Exchange
NYS = True # New York Stock Exchange

# Only allow a stock's primary shares?
PRIMARY_SHARES = True

# Turn on/off specific sectors allowed
BASIC_MATERIALS = True
CONSUMER_CYCLICAL = True
FINANCIAL_SERVICES = True
REAL_ESTATE = True
CONSUMER_DEFENSIVE = True
HEALTHCARE = True
UTILITIES = True
COMMUNICATION_SERVICES = True
ENERGY = True
INDUSTRIALS = True
TECHNOLOGY = True

# Set Morningstar Industry Groups not allowed
# Use Industry Group Code from:
# https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Asset-Classification
# Scroll down to Industry Groups section for codes
GROUPS_NOT_ALLOWED = [
    # 10320, # MorningstarIndustryGroupCode.Banks
    # 10322, # MorningstarIndustryGroupCode.CreditServices
    # 10323, # MorningstarIndustryGroupCode.Insurance
    # 10324, # MorningstarIndustryGroupCode.InsuranceLife
    # 10325, # MorningstarIndustryGroupCode.InsurancePropertyAndCasualty
    # 10326, # MorningstarIndustryGroupCode.InsurancePropertyAndCasualty
    ]
    
# Set Morningstar Industries not allowed
# Use Industry Codes from:
# https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Asset-Classification
# Scroll down to Industries section for codes
INDUSTRIES_NOT_ALLOWED = [
    # 10218038, # MorningstarIndustryCode.Gambling
    # 10320043, # MorningstarIndustryCode.BanksGlobal
    # 10320044, # MorningstarIndustryCode.BanksRegionalAfrica
    # 10320045, # MorningstarIndustryCode.BanksRegionalAsia
    # 10320046, # MorningstarIndustryCode.BanksRegionalAustralia
    # 10320047, # MorningstarIndustryCode.BanksRegionalCanada
    # 10320048, # MorningstarIndustryCode.BanksRegionalEurope
    # 10320049, # MorningstarIndustryCode.BanksRegionalLatinAmerica
    # 10320050, # MorningstarIndustryCode.BanksRegionalUS
    # 10320051, # MorningstarIndustryCode.SavingsAndCooperativeBanks
    # 10320052, # MorningstarIndustryCode.SpecialtyFinance
    # 10321053, # MorningstarIndustryCode.CapitalMarkets
    # 10322056, # MorningstarIndustryCode.CreditServices
    # 10323057, # MorningstarIndustryCode.InsuranceDiversified
    # 10324058, # MorningstarIndustryCode.InsuranceLife
    # 10325059, # MorningstarIndustryCode.InsurancePropertyAndCasualty
    # 10326060, # MorningstarIndustryCode.InsuranceReinsurance
    # 10326061, # MorningstarIndustryCode.InsuranceSpecialty
    # 20636085, # MorningstarIndustryCode.DrugManufacturersMajor
    # 20636086, # MorningstarIndustryCode.DrugManufacturersSpecialtyAndGeneric
    # 20637087, # MorningstarIndustryCode.HealthCarePlans
    # 20640091, # MorningstarIndustryCode.DiagnosticsAndResearch
    ]
  
# Set the minimum number of days to leave a stock in a universe
# This helps with making the universe output more stable
MIN_TIME_IN_UNIVERSE = 65

# Set the minimum number of days with historical data
MIN_TRADING_DAYS = 200

#-------------------------------------------------------------------------------
# ENTRY SIGNAL INPUTS

# Set the moving average periods
EMA_FAST_PERIOD = 50
EMA_SLOW_PERIOD = 200

# How many minutes prior to the open to check for new signals?
# Ideally this is called AFTER the universe filters run!
SIGNAL_CHECK_MINUTES = 30

#-------------------------------------------------------------------------------
# POSITION SIZING INPUTS

# Set the percentage of the portfolio to free up to avoid buying power issues
FREE_PORTFOLIO_VALUE_PCT = 0.025 # decimal percent, e.g. 0.025=2.5% (default)

# Set the max number of positions allowed
MAX_POSITIONS = 20
# Calculate max % of portfolio per position
MAX_PCT_PER_POSITION = 1.0/MAX_POSITIONS

#-------------------------------------------------------------------------------
# EXIT SIGNAL INPUTS

# Turn on/off stop loss
STOP_LOSS = True
# Set stop loss percentage as a decimal percent, e.g. 0.02=2.0%
SL_PCT = 0.02

# Turn on/off trailing stop
TRAILING_STOP = True
# Starts and trails based on SL_PCT

#-------------------------------------------------------------------------------
# Turn on/off end of day exit
EOD_EXIT = False
# When end of exit exit is desired, how many minutes prior to the market close
#  should positions be liquidated?
EOD_EXIT_MINUTES = 15

#-------------------------------------------------------------------------------
# Turn on/off exiting on fast EMA crossing under the slow EMA
EMA_CROSSUNDER_EXIT = True # full exit when triggered

#-------------------------------------------------------------------------------
# Set the RSI period
RSI_PERIOD = 14

# Turn on/off RSI exit #1
RSI_EXIT_1 = False
RSI_EXIT_1_VALUE = 60 # long exit when RSI crosses below this value
RSI_EXIT_1_PCT = 0.50 # decimal percent, 0.50=50.0%

# Turn on/off RSI exit #2
RSI_EXIT_2 = False
RSI_EXIT_2_VALUE = 50 # long exit when RSI crosses below this value
RSI_EXIT_2_PCT = 0.50 # decimal percent, 0.50=50.0%

#-------------------------------------------------------------------------------
# Turn on/off days helds exit #1
DAYS_HELD_EXIT_1 = False
DAYS_HELD_EXIT_1_VALUE = 2
DAYS_HELD_EXIT_1_PCT = 0.25 # decimal percent, e.g. 0.25=25.0%

# Turn on/off days helds exit #2
DAYS_HELD_EXIT_2 = False
DAYS_HELD_EXIT_2_VALUE = 4
DAYS_HELD_EXIT_2_PCT = 0.25 # decimal percent, e.g. 0.25=25.0%

# Turn on/off days helds exit #3
DAYS_HELD_EXIT_3 = False
DAYS_HELD_EXIT_3_VALUE = 6
DAYS_HELD_EXIT_3_PCT = 0.25 # decimal percent, e.g. 0.25=25.0%

# Turn on/off days helds exit #4
DAYS_HELD_EXIT_4 = False
DAYS_HELD_EXIT_4_VALUE = 8
DAYS_HELD_EXIT_4_PCT = 0.25 # decimal percent, e.g. 0.25=25.0%

#-------------------------------------------------------------------------------
# Turn on/off profit target 1
PROFIT_TARGET_1 = True
# Set profit target percentage as a decimal percent, e.g. 0.05=5.0%
PT1_PCT = 0.05
# Set profit target order percentage as a decimal percent, e.g. 0.50=50.0%
PT1_ORDER_PCT = 0.50

# Turn on/off profit target 2
PROFIT_TARGET_2 = True
# Set profit target percentage as a decimal percent, e.g. 0.07=7.0%
PT2_PCT = 0.07
# Set profit target order percentage as a decimal percent, e.g. 0.25=25.0%
PT2_ORDER_PCT = 0.25

# Turn on/off profit target 3
PROFIT_TARGET_3 = True
# Set profit target percentage as a decimal percent, e.g. 0.09=9.0%
PT3_PCT = 0.09
# Set profit target order percentage as a decimal percent, e.g. 0.25=25.0%
PT3_ORDER_PCT = 0.25

#-------------------------------------------------------------------------------
# How long to keep rolling window of indicators?
INDICATOR_WINDOW_LENGTH = 5 # change to 30

#-------------------------------------------------------------------------------
# BENCHMARK DETAILS

# Turn on/off using the custom benchmark plot on the strategy equity chart
PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART = True
# Define benchmark equity
# Currently set to not be able to trade the benchmark!
# Also used for scheduling functions, so make sure it has same trading hours
#  as instruments traded.
BENCHMARK = "SPY"

#-------------------------------------------------------------------------------
# LOGGING DETAILS

# What logs to print?
PRINT_COARSE  = True # print summary of coarse universe selection
PRINT_FINE    = True # print summary of fine universe selection
PRINT_ENTRIES = True # print summary of daily entry signals
PRINT_EXITS   = True # print exit signals triggered
PRINT_ORDERS  = True # print new orders

################################################################################ 
############################ END OF ALL USER INPUTS ############################
################################################################################

# VALIDATE USER INPUTS - DO NOT CHANGE BELOW!!!
#-------------------------------------------------------------------------------
# Verify start date
try:
    START_DT = datetime.strptime(START_DATE, '%m-%d-%Y')
except:
    raise ValueError("Invalid START_DATE format ({}). Must be in MM-DD-YYYY "
        "format.".format(START_DATE))
        
# Verify end date
try:
    if END_DATE:
        END_DT = datetime.strptime(END_DATE, '%m-%d-%Y')
except:
    raise ValueError("Invalid END_DATE format ({}). Must be in MM-DD-YYYY "
        "format or set to None to run to date.".format(END_DATE))
        
#------------------------------------------------------------------------------- 
# Get list of the allowed exchanges
ALLOWED_EXCHANGE = []
if ARCX:
    ALLOWED_EXCHANGE.append('ARCX')
if ASE:
    ALLOWED_EXCHANGE.append('ASE')
if BATS:
    ALLOWED_EXCHANGE.append('BATS')
if NAS:
    ALLOWED_EXCHANGE.append('NAS')
if NYS:
    ALLOWED_EXCHANGE.append('NYS')
# Throw error if no exchange is allowed
if len(ALLOWED_EXCHANGE) == 0:
    raise ValueError("At least one exchange must be set True.")
    
#-------------------------------------------------------------------------------  
# Get list of the sectors NOT allowed
SECTORS_NOT_ALLOWED = []

if not BASIC_MATERIALS:
    SECTORS_NOT_ALLOWED.append(101)
if not CONSUMER_CYCLICAL:
    SECTORS_NOT_ALLOWED.append(102)
if not FINANCIAL_SERVICES:
    SECTORS_NOT_ALLOWED.append(103)
if not REAL_ESTATE:
    SECTORS_NOT_ALLOWED.append(104)
if not CONSUMER_DEFENSIVE:
    SECTORS_NOT_ALLOWED.append(205)
if not HEALTHCARE:
    SECTORS_NOT_ALLOWED.append(206)
if not UTILITIES:
    SECTORS_NOT_ALLOWED.append(207)
if not COMMUNICATION_SERVICES:
    SECTORS_NOT_ALLOWED.append(308)
if not ENERGY:
    SECTORS_NOT_ALLOWED.append(309)
if not INDUSTRIALS:
    SECTORS_NOT_ALLOWED.append(310)
if not TECHNOLOGY:
    SECTORS_NOT_ALLOWED.append(311)
    
#-------------------------------------------------------------------------------
# Verify DATA_RESOLUTION input
DATA_RESOLUTION = DATA_RESOLUTION.upper()
resolutions = ['SECOND', 'MINUTE']
if DATA_RESOLUTION not in resolutions:
    raise ValueError(f"Invalid DATA_RESOLUTION ({DATA_RESOLUTION}). "
        f"Must be: {resolutions}")
        
#------------------------------------------------------------------------------- 
# Verify universe update frequency
if UNIVERSE_FREQUENCY not in ['daily', 'weekly', 'monthly']:
    raise ValueError(f"UNIVERSE_FREQUENCY ({UNIVERSE_FREQUENCY}) must be "
        f"'daily', 'weekly', or 'monthly'.")
        
#------------------------------------------------------------------------------- 
# Verify profit target orders
# Make sure order percentages <= 1.0
pt_order_total = 0
if PROFIT_TARGET_1:
    pt_order_total += PT1_ORDER_PCT
if PROFIT_TARGET_2:
    pt_order_total += PT2_ORDER_PCT
if PROFIT_TARGET_3:
    pt_order_total += PT3_ORDER_PCT
if pt_order_total > 1:
    raise ValueError(f"Invalid PT_ORDER_PCTS. Total ({pt_order_total}) > 1")
    
#------------------------------------------------------------------------------- 
# Verify RSI exit percent totals doesn't exceed 1
if RSI_EXIT_1_PCT+RSI_EXIT_2_PCT > 1:
    raise ValueError("Invalid RSI exit percents. Total cannot exceed 1.")
    
#-------------------------------------------------------------------------------
# Calculate the number of days in the backtest
PLOT_LIMIT = 4000
if not END_DATE:
    today = DT.datetime.today()
    BT_DAYS = (today-START_DT).days
else:
    BT_DAYS = (END_DT-START_DT).days
# Convert calendar days to estimated market days
# Round up to the nearest integer
# This uses // for integer division which rounds down
# Take division for negative number to round up, then negate the negative
BT_DAYS = -(-BT_DAYS*252//365)
# Calculate the frequency of days that we can create a new plot
# Use the same approach as above to round up to the nearest integer
PLOT_EVERY_DAYS = -(-BT_DAYS//PLOT_LIMIT)

#-------------------------------------------------------------------------------
# Calculate the period to warm up the data
BARS_PER_DAY = 1
# Get the minimum number of bars required to fill all indicators
MIN_BARS = max([EMA_FAST_PERIOD, EMA_SLOW_PERIOD, RSI_PERIOD]) \
    + INDICATOR_WINDOW_LENGTH
# Calculate the number of market warmup days required
MARKET_WARMUP_DAYS = -(-MIN_BARS//BARS_PER_DAY)
# Add a 10% buffer
MARKET_WARMUP_DAYS = int(1.1*MARKET_WARMUP_DAYS)
# Convert the number of market days to be actual calendar days
# Assume 252 market days per calendar year (or 365 calendar days)
CALENDAR_WARMUP_DAYS = int(MARKET_WARMUP_DAYS*(365/252))
# Standard library imports
import datetime as DT
# from dateutil.parser import parse
# import decimal
# import numpy as np
# import pandas as pd
import pytz
# from System.Drawing import Color

# QuantConnect specific imports
# import QuantConnect as qc

# Import from files
from notes_and_inputs import *

################################################################################
class SymbolData(object):
    """Class to store data for a specific symbol."""
    def __init__(self, algo, symbol_object):
        """Initialize SymbolData object."""
        # Save a reference to the QCAlgorithm class
        self.algo = algo
        # Save the .Symbol object and the symbol's string
        self.symbol_object = symbol_object
        self.symbol = symbol_object.ID.Symbol                                   # NEW
        # Get the symbol's exchange market info
        self.get_exchange_info()
        # Add strategy specific variables
        self.add_strategy_variables()
        # Add the bars and indicators required
        self.add_bars_indicators()
        
#-------------------------------------------------------------------------------
    def get_exchange_info(self):
        """Get the security's exchange info."""
        # Get the SecurityExchangeHours Class object for the symbol
        self.exchange_hours = \
            self.algo.Securities[self.symbol_object].Exchange.Hours
            
        # Create a datetime I know the market was open for the full day
        dt = DT.datetime(2021, 1, 4)
        # Get the next open datetime from the SecurityExchangeHours Class
        mkt_open_dt = self.exchange_hours.GetNextMarketOpen(dt, False)
        # Save the typical market open and close times
        self.mkt_open = mkt_open_dt.time()
        mkt_close_dt = self.exchange_hours.GetNextMarketClose(dt, False)
        self.mkt_close = mkt_close_dt.time()
        
        # Get the exchange timezone
        self.mkt_tz = pytz.timezone(str(self.exchange_hours.TimeZone))
        # Create pytz timezone objects for the exchange tz and local tz
        exchange_tz = self.mkt_tz
        local_tz = pytz.timezone(TIMEZONE)
        # Get the difference in the timezones
        # REF: http://pytz.sourceforge.net/#tzinfo-api 
        #  for pytz timezone.utcoffset() method
        # 3600 seconds/hour
        exchange_utc_offset_hrs = int(exchange_tz.utcoffset(dt).seconds/3600)
        local_utc_offset_hrs = int(local_tz.utcoffset(dt).seconds/3600)
        self.offset_hrs = exchange_utc_offset_hrs-local_utc_offset_hrs
        # NOTE: offset hours are very helpful if you want to schedule functions
        #  around market open/close times
        
        # Get the market close time for the local time zone
        self.mkt_close_local_tz = \
            (mkt_close_dt-DT.timedelta(hours=self.offset_hrs)).time()
            
#-------------------------------------------------------------------------------
    def add_strategy_variables(self):
        """Add other required variables for the strategy."""
        # Initialize order variables
        self.reset_order_variables()
        
#-------------------------------------------------------------------------------
    def reset_order_variables(self):
        """Reset order variables for the strategy."""
        self.cost_basis = None
        self.trade_best_price = None
        self.sl_order = None
        self.sl_price = None
        self.pt_order1 = None
        self.pt_order2 = None
        self.pt_order3 = None
        self.days_held = 0
        
#-------------------------------------------------------------------------------
    def add_bars_indicators(self):
        """Add bars, indicators, and other required variables."""
        # Create the desired daily bar consolidator for the symbol
        consolidator = TradeBarConsolidator(self.daily_US_equity_calendar)
        # Create an event handler to be called on each new consolidated bar
        consolidator.DataConsolidated += self.on_data_consolidated
        # Link the consolidator with our symbol and add it to the algo manager
        self.algo.SubscriptionManager.AddConsolidator(
            self.symbol_object, consolidator)
        # Save consolidator link so we can remove it when necessary
        self.consolidator = consolidator
        
        # Create indicators to be based on the desired consolidated bars
        self.ema_fast = ExponentialMovingAverage(EMA_FAST_PERIOD)
        self.ema_slow = ExponentialMovingAverage(EMA_SLOW_PERIOD)
        self.rsi = RelativeStrengthIndex(RSI_PERIOD)
        
        # Create rolling windows of whether the fast EMA is > or < slow EMA
        # format: RollingWindow[object type](length)
        self.fast_ema_gt_slow_ema = RollingWindow[bool](2)
        self.fast_ema_lt_slow_ema = RollingWindow[bool](2)
        
        # Create a rolling window of the last closing prices
        #  used to make sure there is enough data to start trading
        self.window_closes = RollingWindow[float](MIN_TRADING_DAYS)
        
        # Create rolling windows for desired data
        self.window_ema_fast = RollingWindow[float](INDICATOR_WINDOW_LENGTH)
        self.window_ema_slow = RollingWindow[float](INDICATOR_WINDOW_LENGTH)
        self.window_rsi = RollingWindow[float](INDICATOR_WINDOW_LENGTH)
        self.window_bar = RollingWindow[TradeBar](INDICATOR_WINDOW_LENGTH)
        
        # Keep a list of all indicators - for indicators_ready property
        self.indicators = [self.ema_fast, self.ema_slow, self.rsi, 
            self.fast_ema_gt_slow_ema, self.fast_ema_lt_slow_ema, 
            self.window_closes, self.window_ema_fast, self.window_ema_slow, 
            self.window_rsi, self.window_bar]
            
        # Get min bars required to initialize the indicators
        self.min_bars = MARKET_WARMUP_DAYS
        # Warm up the indicators with historical data
        self.warmup_indicators()
        
#-------------------------------------------------------------------------------
    def daily_US_equity_calendar(self, dt):
        """
        Set up daily consolidator calendar info for the US equity market.
        This should return a start datetime object that is timezone unaware
        with a valid date/time for the desired securities' exchange's time zone.
        
        Useful Refs:
        datetime.replace() method:
        https://docs.python.org/3/library/datetime.html#datetime.datetime.replace
        """
        # Create a datetime.datetime object to represent the market open for the
        # **EXCHANGE** timezone
        start = dt.replace(hour=self.mkt_open.hour, minute=self.mkt_open.minute,
            second=0, microsecond=0)
            
        # Need to handle case where algo initializes and this function is called
        #  for the first time. When that occurs, the current datetime when you
        #  requested a backtest is passed to this function as dt. This is the
        #  only time that dt has a timezone attached to it.
        if dt.tzinfo:
            # We need to make sure that the start datetime that we return from
            # this function is BEFORE dt, otherwise it will throw this error:
            #   FuncPeriodSpecification: Please use a function that computes a 
            #       date/time in the past 
            #       (e.g.: Time.StartOfWeek and Time.StartOfMonth)
            if start > dt:
                # Make the start datetime go back by one day
                start -= timedelta(1)
                
        # Get today's end time from the SecurityExchangeHours Class object
        # exchange_class = self.algo.Securities[self.symbol].Exchange
        # exchange_hrs_class = self.algo.Securities[self.symbol].Exchange.Hours
        #  which is saved as self.exchange_hours
        end = self.exchange_hours.GetNextMarketClose(start, False)
            
        # Return the start datetime and the consolidation period
        return CalendarInfo(start, end-start)
        
#-------------------------------------------------------------------------------
    def warmup_indicators(self):
        """Warm up indicators using historical data.""" 
        # Get historical data in pandas dataframe
        df = self.algo.History(
            [self.symbol_object], 
            self.min_bars,
            Resolution.Daily
            )
            
        # Drops level 0 (Symbols), so only time as index
        df.reset_index(level=0, inplace=True)
        
        # Get only the desired columns
        columns = ['open', 'high', 'low', 'close', 'volume']
        try:
            df = df[columns]
            # Drop all rows with nans
            df.dropna(inplace=True)
        except:
            self.algo.Debug(f"{self.symbol} ERROR warming up indicators")
            return
        
        # Loop through rows of df and update indicators
        for index, row in df.iterrows():
            # Create TradeBar
            bar = TradeBar(index, self.symbol, row['open'], row['high'], 
                row['low'], row['close'], row['volume'])
            self.update_indicators(bar)
            
        # Log message that the indicators have been warmed up
        # self.algo.Debug(f"{self.symbol} indicators warmed up with history")
        
#-------------------------------------------------------------------------------
    def on_data_consolidated(self, sender, bar):
        """Event handler for desired custom bars."""
        # Manually update all of the indicators
        self.update_indicators(bar)
        
#-------------------------------------------------------------------------------
    @property
    # REF: https://www.programiz.com/python-programming/property
    def indicators_ready(self):
        """Check if all of the indicators used are ready (warmed up)."""
        # Return False if any indicator is not ready
        for indicator in self.indicators:
            if not indicator.IsReady:
                return False
        # Otherwise all indicators are ready, so return True
        return True
        
#-------------------------------------------------------------------------------
    @property
    def ema_cross_over(self):
        """Return if there is an EMA cross-over."""
        # Check if the rolling window is ready
        if self.fast_ema_gt_slow_ema.IsReady:
            # Need latest value True (fast > slow ema) 
            # and previous one False (fast <= slow ema)
            return self.fast_ema_gt_slow_ema[0] \
                and not self.fast_ema_gt_slow_ema[1]
        # Otherwise return False
        return False
        
#-------------------------------------------------------------------------------
    @property
    def ema_cross_under(self):
        """Return if there is an EMA cross-under."""
        # Check if the rolling window is ready
        if self.fast_ema_lt_slow_ema.IsReady:
            # Need latest value True (fast < slow ema) 
            # and previous one False (fast >= slow ema)
            return self.fast_ema_lt_slow_ema[0] \
                and not self.fast_ema_lt_slow_ema[1]
        # Otherwise return False
        return False
        
#-------------------------------------------------------------------------------
    @property
    def fast_slow_pct_difference(self):
        """Return the percent difference between the fast and slow EMAs."""
        # Check if both EMAs are ready
        if self.ema_fast.IsReady and self.ema_slow.IsReady:
            return (self.ema_fast.Current.Value-self.ema_slow.Current.Value) \
                /self.ema_slow.Current.Value
        # Otherwise return 0
        return 0
        
#-------------------------------------------------------------------------------
    @property
    def current_qty(self):
        """Return the current quantity held in the portfolio."""
        return self.algo.Portfolio[self.symbol_object].Quantity
        
#-------------------------------------------------------------------------------
    def long_entry_signal(self):
        """Check if there is a valid long entry signal."""
        # Trigger on an EMA cross-over
        if self.ema_cross_over:
            # Log message when desired
            if PRINT_ENTRIES:
                self.algo.Log(f"{self.symbol} LONG ENTRY SIGNAL: EMA CROSSOVER")
            return True
            
#-------------------------------------------------------------------------------
    def long_exit_signal_checks(self):
        """Check if there are any valid long exit signals."""
        # Trigger on an EMA cross-under
        if EMA_CROSSUNDER_EXIT and self.ema_cross_under:
            # Log message when desired
            if PRINT_EXITS:
                self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: EMA CROSSUNDER")
            self.algo.Liquidate(self.symbol_object)
            return
        
        # Check for RSI exits
        exit_pct = 0
        if RSI_EXIT_1:
            # Check for RSI below the RSI_EXIT_1_VALUE
            if self.rsi.Current.Value < RSI_EXIT_1_VALUE:
                exit_pct += RSI_EXIT_1_PCT
                # Log message when desired
                if PRINT_EXITS:
                    self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: RSI "
                        f"({self.rsi.Current.Value}) < {RSI_EXIT_1_VALUE}. Now "
                        f"closing {exit_pct*100.0}% of the position.")
        if RSI_EXIT_2:
            # Check for RSI below the RSI_EXIT_2_VALUE
            if self.rsi.Current.Value < RSI_EXIT_2_VALUE:
                exit_pct += RSI_EXIT_2_PCT
                # Log message when desired
                if PRINT_EXITS:
                    self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: RSI "
                        f"({self.rsi.Current.Value}) < {RSI_EXIT_2_VALUE}. Now "
                        f"closing {exit_pct*100.0}% of the position.")
                        
        # Check for days held exits
        if DAYS_HELD_EXIT_1:
            if self.days_held == DAYS_HELD_EXIT_1_VALUE:
                exit_pct += DAYS_HELD_EXIT_1_PCT
                # Make sure we don't trade more than the full position (100%)
                exit_pct = min(1, exit_pct)
                # Log message when desired
                if PRINT_EXITS:
                    self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: Days "
                        f"held ({self.days_held}) = {DAYS_HELD_EXIT_1_VALUE}. "
                        f"Now closing {exit_pct*100.0}% of the position.")
        if DAYS_HELD_EXIT_2:
            if self.days_held == DAYS_HELD_EXIT_2_VALUE:
                exit_pct += DAYS_HELD_EXIT_2_PCT
                # Make sure we don't trade more than the full position (100%)
                exit_pct = min(1, exit_pct)
                # Log message when desired
                if PRINT_EXITS:
                    self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: Days "
                        f"held ({self.days_held}) = {DAYS_HELD_EXIT_2_VALUE}. "
                        f"Now closing {exit_pct*100.0}% of the position.")
        if DAYS_HELD_EXIT_3:
            if self.days_held == DAYS_HELD_EXIT_3_VALUE:
                exit_pct += DAYS_HELD_EXIT_3_PCT
                # Make sure we don't trade more than the full position (100%)
                exit_pct = min(1, exit_pct)
                # Log message when desired
                if PRINT_EXITS:
                    self.algo.Log(f"{self.symbol} LONG EXIT SIGNAL: Days "
                        f"held ({self.days_held}) = {DAYS_HELD_EXIT_3_VALUE}. "
                        f"Now closing {exit_pct*100.0}% of the position.")
                        
        # Check if any of the position should be closed
        if exit_pct > 0:
            # Get actual quantity to exit
            exit_qty = -self.current_qty*exit_pct
            # Place market order to exit
            self.algo.MarketOrder(self.symbol_object, exit_qty)
            
#-------------------------------------------------------------------------------
    def update_indicators(self, bar):
        """Manually update all of the symbol's indicators."""
        # Update the EMAs and RSI
        self.ema_fast.Update(bar.EndTime, bar.Close)
        self.ema_slow.Update(bar.EndTime, bar.Close)
        self.rsi.Update(bar.EndTime, bar.Close)
        
        # Update rolling windows when ready
        if self.ema_fast.IsReady:
            self.window_ema_fast.Add(self.ema_fast.Current.Value)
        if self.ema_slow.IsReady:
            self.window_ema_slow.Add(self.ema_slow.Current.Value)
        if self.ema_fast.IsReady and self.ema_slow.IsReady:
            self.fast_ema_gt_slow_ema.Add(
                self.ema_fast.Current.Value>self.ema_slow.Current.Value)
            self.fast_ema_lt_slow_ema.Add(
                self.ema_fast.Current.Value<self.ema_slow.Current.Value)
        if self.rsi.IsReady:
            self.window_rsi.Add(self.rsi.Current.Value) 
        self.window_closes.Add(bar.Close)
        self.window_bar.Add(bar)
        
        # Update the trades best price if trailing stop is used
        if TRAILING_STOP:
            self.update_trade_best_price(bar)
        # Increment days held counter if there is a positioin
        if self.current_qty != 0:
            self.days_held += 1
            
#-------------------------------------------------------------------------------
    def update_trade_best_price(self, bar):
        """Update the trade's best price if there is an open position."""
        # Check if there is an open position
        if self.current_qty > 0: # long
            # Update the trade best price when appropriate
            if not self.trade_best_price:
                # Should be set, so raise error to debug
                raise
            elif bar.High > self.trade_best_price:
                self.trade_best_price = bar.High
                # Get the current stop loss price
                sl_price = self.get_stop_price()
                # Check for increase in stop loss price
                if sl_price > self.sl_price:
                    # Update the stop loss order's price
                    self.update_order_stop_price(self.sl_order, sl_price)
                    self.sl_price = sl_price
                    
#-------------------------------------------------------------------------------
    def get_stop_price(self):
        """Get the current stop loss price.""" 
        # Calculate the stop loss price
        if self.current_qty > 0: # long
            return round(self.trade_best_price*(1-SL_PCT),2)
        elif self.current_qty < 0: # short
            return round(self.trade_best_price*(1+SL_PCT),2)
            
#-------------------------------------------------------------------------------
    def cancel_exit_orders(self):
        """Cancel any open exit orders."""  
        # Cancel open profit target order #1, if one
        if self.pt_order1:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Cancelling {self.symbol} open profit target #1 "
                    f"order.")
            try:
                self.pt_order1.Cancel()
            except:
                self.algo.Log(f"Error trying to cancel {self.symbol} profit "
                    f"target #1 order.")
        # Cancel open profit target order #2, if one
        if self.pt_order2:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Cancelling {self.symbol} open profit target #2 "
                    f"order.")
            try:
                self.pt_order2.Cancel()
            except:
                self.algo.Log(f"Error trying to cancel {self.symbol} profit "
                    f"target #2 order.")
        # Cancel open profit target order #3, if one
        if self.pt_order3:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Cancelling {self.symbol} open profit target #3 "
                    f"order.")
            try:
                self.pt_order3.Cancel()
            except:
                self.algo.Log(f"Error trying to cancel {self.symbol} profit "
                    f"target #3 order.")
                    
        # Cancel open stop order, if one
        if self.sl_order:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Cancelling {self.symbol} open stop order.")
            try:
                self.sl_order.Cancel()
            except:
                self.algo.Log(f"Error trying to cancel {self.symbol} stop "
                    f"loss order.")
        # Reset order variables
        self.reset_order_variables()
        
#-------------------------------------------------------------------------------
    def get_pt_exit_quantities(self, initial=False):
        """Get the profit target exit quantities for orders #1, #2, #3."""
        # Get the exit order qty 
        exit_qty = -self.current_qty
        
        # Initialize each to 0
        pt1_exit_qty = 0
        pt2_exit_qty = 0
        pt3_exit_qty = 0
        
        # Get PT1, PT2, PT3 exit qtys
        pt_exit_qty = 0
        if (initial and PROFIT_TARGET_1) or self.pt_order1:
            pt1_exit_qty = int(exit_qty*PT1_ORDER_PCT)
            pt_exit_qty += pt1_exit_qty
        if (initial and PROFIT_TARGET_1) or self.pt_order2:
            pt2_exit_qty = int(exit_qty*PT2_ORDER_PCT)
            pt_exit_qty += pt2_exit_qty
        if (initial and PROFIT_TARGET_1) or self.pt_order3:
            pt3_exit_qty = int(exit_qty*PT3_ORDER_PCT)
            pt_exit_qty += pt3_exit_qty
            
        # Make sure pt_exit_qty equals exit_qty
        if pt_exit_qty != exit_qty:
            # Get the difference
            diff = exit_qty-pt_exit_qty
            # Add the difference to the last pt order
            if (initial and PROFIT_TARGET_1) or self.pt_order3:
                pt3_exit_qty += diff
            elif (initial and PROFIT_TARGET_1) or self.pt_order2:
                pt2_exit_qty += diff
            elif (initial and PROFIT_TARGET_1) or self.pt_order1:
                pt2_exit_qty += diff
                
        # Return the quantities
        return pt1_exit_qty, pt2_exit_qty, pt3_exit_qty
        
#-------------------------------------------------------------------------------
    def update_exit_orders(self):
        """Update any open exit orders."""
        # Get the exit order qty 
        exit_qty = -self.current_qty
        
        # Get the desired profit target exit order quantities
        pt1_exit_qty, pt2_exit_qty, pt3_exit_qty = self.get_pt_exit_quantities()
        
        # Update open profit target order #1, if one
        if self.pt_order1:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Updating {self.symbol} open profit target #1 "
                    f"order qty to {pt1_exit_qty}.")
            # Get the profit taking order ticket, then update it
            ticket = self.pt_order1
            self.update_order_qty(ticket, pt1_exit_qty)
        # Update open profit target order #2, if one
        if self.pt_order2:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Updating {self.symbol} open profit target #2 "
                    f"order qty to {pt2_exit_qty}.")
            # Get the profit taking order ticket, then update it
            ticket = self.pt_order2
            self.update_order_qty(ticket, pt2_exit_qty)
        # Update open profit target order #3, if one
        if self.pt_order3:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Updating {self.symbol} open profit target #3 "
                    f"order qty to {pt3_exit_qty}.")
            # Get the profit taking order ticket, then update it
            ticket = self.pt_order3
            self.update_order_qty(ticket, pt3_exit_qty)
            
        # Update open stop loss order, if one
        if self.sl_order:
            # Log message whe desired
            if PRINT_ORDERS:
                self.algo.Log(f"Updating {self.symbol} open stop loss "
                    f"order qty to {exit_qty}.")
            # Get the profit taking order ticket, then update it
            ticket = self.sl_order
            self.update_order_qty(ticket, exit_qty)
            
#-------------------------------------------------------------------------------
    def update_order_qty(self, ticket, qty):
        """Update the desired order ticket's qty."""
        ticket.UpdateQuantity(qty, tag=f'updating qty to {qty}')
        
#-------------------------------------------------------------------------------
    def update_order_stop_price(self, ticket, price):
        """Update the desired order ticket's stop price."""
        ticket.UpdateStopPrice(price, tag=f'updating stop price to {price}')
        
#-------------------------------------------------------------------------------
    def update_order_limit_price(self, ticket, price):
        """Update the desired order ticket's limit price."""
        ticket.UpdateLimitPrice(price, tag=f'updating limit price to {price}')
        
#-------------------------------------------------------------------------------
    def place_sl_order(self):
        """Place the desired stop loss order."""
        # Get the current stop loss price
        self.sl_price = self.get_stop_price()
        # Place and save the stop loss order
        self.sl_order = self.algo.StopMarketOrder(
            self.symbol_object, -self.current_qty, self.sl_price)
        # Log message when desired
        if PRINT_ORDERS:
            self.algo.Log(f"{self.symbol} stop order placed at {self.sl_price}")
            
#-------------------------------------------------------------------------------
    def place_pt_orders(self):
        """Place the desired profit target orders."""
        # Get the desired profit target exit order quantities
        pt1_exit_qty, pt2_exit_qty, pt3_exit_qty = \
            self.get_pt_exit_quantities(initial=True)    
            
        # Check for placing a profit target order #1
        if PROFIT_TARGET_1:
            # Calculate the profit target price
            if self.current_qty > 0: # long
                pt_price = round(self.cost_basis*(1+PT1_PCT),2)
            elif self.current_qty < 0: # short
                pt_price = round(self.cost_basis*(1-PT1_PCT),2)
            # Place and save the stop loss order
            self.pt_order1 = self.algo.LimitOrder(
                self.symbol_object, pt1_exit_qty, pt_price)
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} profit target #1 order: "
                    f"{pt1_exit_qty} at {pt_price}")
                    
        # Check for placing a profit target order #2
        if PROFIT_TARGET_2:
            # Calculate the profit target price
            if self.current_qty > 0: # long
                pt_price = round(self.cost_basis*(1+PT2_PCT),2)
            elif self.current_qty < 0: # short
                pt_price = round(self.cost_basis*(1-PT2_PCT),2)
            # Place and save the stop loss order
            self.pt_order2 = self.algo.LimitOrder(
                self.symbol_object, pt2_exit_qty, pt_price)
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} profit target #2 order: "
                    f"{pt2_exit_qty} at {pt_price}") 
                    
        # Check for placing a profit target order #3
        if PROFIT_TARGET_3:
            # Calculate the profit target price
            if self.current_qty > 0: # long
                pt_price = round(self.cost_basis*(1+PT3_PCT),2)
            elif self.current_qty < 0: # short
                pt_price = round(self.cost_basis*(1-PT3_PCT),2)
            # Place and save the stop loss order
            self.pt_order3 = self.algo.LimitOrder(
               self.symbol_object, pt2_exit_qty, pt_price)
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} profit target #3 order: "
                    f"{pt2_exit_qty} at {pt_price}")
                    
#-------------------------------------------------------------------------------
    def on_order_event(self, order_event):
        """New order event."""
        # Get the order details
        order = self.algo.Transactions.GetOrderById(order_event.OrderId)
        order_qty = int(order.Quantity)
        avg_fill = order_event.FillPrice
        # Get current qty of symbol
        qty = self.current_qty
        
        # Check for entry order
        if order_qty == qty:
            # Entry order filled
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} entry order filled: {order_qty}"
                    f" at {avg_fill}")
            # Save the cost basis and trade best price
            self.cost_basis = avg_fill
            self.trade_best_price = avg_fill
            
            # Place a stop loss order when desired
            if STOP_LOSS or TRAILING_STOP:
                self.place_sl_order()
            # Place profit target orders when desired
            self.place_pt_orders()
            # Done with event, so return
            return
        
        # Check for stop order
        if self.sl_order: 
            # Check for matching order ids
            if order_event.OrderId == self.sl_order.OrderId:
                # Stop order filled
                # Log message when desired
                if PRINT_ORDERS:
                    self.algo.Log(f"{self.symbol} stop order filled: "
                        f"{order_qty} at {avg_fill}") 
                # Cancel open exit orders
                self.cancel_exit_orders()
                # Done with event, so return
                return
                
        # Check for profit target order #1
        if self.pt_order1: 
            # Check for matching order ids
            if order_event.OrderId == self.pt_order1.OrderId:
                # Profit target order filled
                # Log message when desired
                if PRINT_ORDERS:
                    self.algo.Log(f"{self.symbol} profit target order #1 "
                        f"filled: {order_qty} at {avg_fill}") 
                # Check if the position is still open
                if qty != 0:
                    # Update open exit orders
                    self.update_exit_orders()
                else:
                    # Cancel open exit orders
                    self.cancel_exit_orders()
                # Set order to None
                self.pt_order1 = None
                # Done with event, so return
                return
            
        # Check for profit target order #2
        if self.pt_order2: 
            # Check for matching order ids
            if order_event.OrderId == self.pt_order2.OrderId:
                # Profit target order filled
                # Log message when desired
                if PRINT_ORDERS:
                    self.algo.Log(f"{self.symbol} profit target order #2 "
                        f"filled: {order_qty} at {avg_fill}") 
                # Check if the position is still open
                if qty != 0:
                    # Update open exit orders
                    self.update_exit_orders()
                else:
                    # Cancel open exit orders
                    self.cancel_exit_orders()
                # Set order to None
                self.pt_order2 = None
                # Done with event, so return
                return
            
        # Check for profit target order #3
        if self.pt_order3: 
            # Check for matching order ids
            if order_event.OrderId == self.pt_order3.OrderId:
                # Profit target order filled
                # Log message when desired
                if PRINT_ORDERS:
                    self.algo.Log(f"{self.symbol} profit target order #3 "
                        f"filled: {order_qty} at {avg_fill}") 
                # Check if the position is still open
                if qty != 0:
                    # Update open exit orders
                    self.update_exit_orders()
                else:
                    # Cancel open exit orders
                    self.cancel_exit_orders()
                # Set order to None
                self.pt_order3 = None
                # Done with event, so return
                return
            
        # Check for full exit order
        if qty == 0:
            # Exit order filled
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} exit order filled: "
                    f"{order_qty} at {avg_fill}")
            # Cancel open exit orders
            self.cancel_exit_orders()
            # Done with event, so return
            return
        
        # Check for pyramid entry order (qty and order_qty have the same signs)
        if qty*order_qty > 0:
            # This strategy doesn't have pyramid entries, so raise error
            raise
        # Otherwise a partial exit order
        else:
            # Partial exit order filled
            # Log message when desired
            if PRINT_ORDERS:
                self.algo.Log(f"{self.symbol} partial exit order filled: "
                    f"{order_qty} at {avg_fill}")
            # Update open exit orders
            self.update_exit_orders()
            # Done with event, so return
            return
###############################################################################
# Standard library imports
import datetime as DT
# from dateutil.parser import parse
# import decimal
# import numpy as np
import pandas as pd
# import pytz
# from System.Drawing import Color

# QuantConnect specific imports
# import QuantConnect as qc

# Import from files
from notes_and_inputs import *
from symbol_data import *

###############################################################################
class CustomTradingStrategy(QCAlgorithm):
    def Initialize(self):
        """Initialize algorithm."""
        # Set backtest details
        self.SetBacktestDetails()
        # Add instrument data to the algo
        self.AddInstrumentData()
        # Schedule functions
        self.ScheduleFunctions()
        
        # Warm up the indicators prior to the start date
        # self.SetWarmUp()
        # This doesn't work with universe filters
        # Instead we'll use History() to warm up indicators when SymbolData
        #  class objects get created
        
#-------------------------------------------------------------------------------
    def SetBacktestDetails(self):
        """Set the backtest details."""
        self.SetStartDate(START_DT.year, START_DT.month, START_DT.day)
        if END_DATE:
            self.SetEndDate(END_DT.year, END_DT.month, END_DT.day)
        self.SetCash(CASH)
        self.SetTimeZone(TIMEZONE)
        
        # Setup trading framework
        # Transaction and submit/execution rules will use IB models
        # brokerages: https://github.com/QuantConnect/Lean/blob/master/Common/Brokerages/BrokerageName.cs
        # account types: AccountType.Margin, AccountType.Cash
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, 
            AccountType.Cash)                                                   # CASH ACCOUNT
            
        # Configure all universe securities
        # This sets the data normalization mode
        # You can also set custom fee, slippage, fill, and buying power models
        self.SetSecurityInitializer(self.CustomSecurityInitializer)
        
         # Adjust the cash buffer from the default 2.5% to custom setting
        self.Settings.FreePortfolioValuePercentage = FREE_PORTFOLIO_VALUE_PCT
        
#-------------------------------------------------------------------------------
    def AddInstrumentData(self):
        """Add instrument data to the algo."""
        # Set data resolution based on input
        if DATA_RESOLUTION == 'SECOND':
            resolution = Resolution.Second
        elif DATA_RESOLUTION == 'MINUTE':
            resolution = Resolution.Minute
        
        # Define the desired universe
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        # Set universe data properties desired
        self.UniverseSettings.Resolution = resolution
        self.UniverseSettings.ExtendedMarketHours = False
        self.UniverseSettings.DataNormalizationMode = \
            DataNormalizationMode.Adjusted
        self.UniverseSettings.MinimumTimeInUniverse = MIN_TIME_IN_UNIVERSE
        
        # Add data for the benchmark and set benchmark
        # Always use minute data for the benchmark
        self.bm = self.AddEquity(BENCHMARK, Resolution.Minute).Symbol
        self.SetBenchmark(BENCHMARK)
        
        # Create a dictionary to hold SymbolData class objects
        self.symbol_data = {}
        
        # Create a variable to tell the algo when to update the universe
        self.update_universe = True # update at the beginning of the backtest
        
#-------------------------------------------------------------------------------
    def ScheduleFunctions(self):
        """Scheduling the functions required by the algo."""
        # For live trading, universe selection occurs approximately 04:00-07:00
        #  EST on Tue-Sat. 
        # For backtesting, universe selection occurs at 00:00 EST
        # We need to update the self.update_universe variable
        #  before both of these scenarios are triggered
        
        # Desired order of events:
        # 1. Update the self.update_universe variable True 
        #    end of week/month, 5 min after market close 
        # 2. Coarse/Fine universe filters run and update universe
        #    run everyday at either 00:00 or 04:00 EST
        
        # Update self.update_universe variable True when desired
        if UNIVERSE_FREQUENCY == 'daily':
            date_rules = self.DateRules.EveryDay(self.bm)
        elif UNIVERSE_FREQUENCY == 'weekly':
            # Want to schedule at end of the week, so actual update on start 
            #  of the next week
            date_rules = self.DateRules.WeekEnd(self.bm)
        elif UNIVERSE_FREQUENCY == 'monthly':
            # Want to schedule at end of the month, so actual update on 
            #  first day of the next month
            date_rules = self.DateRules.MonthEnd(self.bm)
        # Timing is after the market closes
        self.Schedule.On(
            date_rules,
            self.TimeRules.BeforeMarketClose(self.bm, -5),
            self.UpdateUniverse
            )
        # Calling -5 minutes "BeforeMarketClose" schedules the function 5 
        # minutes "after market close"
        # Calling -5 minutes "AfterMarketOpen" schedules the function 5 
        # minutes "before market open"
        
        # Now the coarse/fine universe filters will run automatically either at
        #  00:00 EST for backtesting or
        #  04:00 EST for live trading
        
        # Check for new signals SIGNAL_CHECK_MINUTES before the market open
        self.Schedule.On(
            self.DateRules.EveryDay(self.bm),
            self.TimeRules.AfterMarketOpen(self.bm, -SIGNAL_CHECK_MINUTES),
            self.CheckForSignals
            )
            
        # Check if end of day exit is desired
        if EOD_EXIT:
            # Schedule function to liquidate the portfolio EOD_EXIT_MINUTES
            #  before the market close
            self.Schedule.On(
                self.DateRules.EveryDay(self.bm),
                self.TimeRules.BeforeMarketClose(self.bm, EOD_EXIT_MINUTES),
                self.LiquidatePortfolio
                )
              
        # Check if we want to plot the benchmark on the equity curve
        if PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART:
            # Schedule benchmark end of day event 5 minutes after the close
            self.Schedule.On(
                self.DateRules.EveryDay(self.bm),
                self.TimeRules.BeforeMarketClose(self.bm, -5),
                self.BenchmarkOnEndOfDay
                )
                
#-------------------------------------------------------------------------------
    def UpdateUniverse(self):
        """Event called when rebalancing is desired."""
        # Update variable to trigger the universe to be updated
        self.update_universe = True
        
#-------------------------------------------------------------------------------
    def CoarseSelectionFunction(self, coarse):
        """
        Perform coarse filters on universe.
        Called once per day.
        Returns all stocks meeting the desired criteria.
        
        Attributes available:
         .AdjustedPrice
         .DollarVolume
         .HasFundamentalData
         .Price -> always the raw price!
         .Volume
        """
        # # Testing - catch specific symbol
        # for x in coarse:
        #     # if str(x.Symbol).split(" ")[0] in ['AAPL']:
        #     if x.Symbol.ID.Symbol in ['AAPL']:
        #         # Stop and debug below
        #         print(x)
        
        # Check if the universe doesn't need to be updated
        if not self.update_universe:
            # Return unchanged universe
            return Universe.Unchanged
            
        # Otherwise update the universe based on the desired filters
        # Filter all securities with appropriate price and volume
        filtered_coarse = [x for x in coarse if \
            x.Price >= MIN_PRICE and \
            x.Price <= MAX_PRICE and \
            x.Volume >= MIN_DAILY_VOLUME and \
            x.DollarVolume >= MIN_DAILY_DOLLAR_VOLUME
            ]
            
        # Check if fundamental data is required
        if REQUIRE_FUNDAMENTAL_DATA:
            # Filter all securities with fundamental data
            filtered_coarse = \
                [x for x in filtered_coarse if x.HasFundamentalData]
            
        # Return the symbol objects
        symbols = [x.Symbol for x in filtered_coarse]
            
        # Print universe details when desired
        if PRINT_COARSE:
            self.Log(f"Coarse filter returned {len(symbols)} stocks.")
            
        return symbols
        
#-------------------------------------------------------------------------------
    def FineSelectionFunction(self, fine):
        """
        Perform fine filters on universe.
        Called once per day.
        Returns all stocks meeting the desired criteria.
        
        Attribues available:
         .AssetClassification
         .CompanyProfile
         .CompanyReference
         .EarningRatios
         .EarningReports
         .FinancialStatements
         .MarketCap
         .OperationRatios
         .Price -> always the raw price!
         .ValuationRatios
        """
        # # Testing - catch specific symbol
        # for x in coarse:
        #     # if str(x.Symbol).split(" ")[0] in ['AAPL']:
        #     if x.Symbol.ID.Symbol in ['AAPL']:
        #         # Stop and debug below
        #         print(x)
        
        # Check if the universe doesn't need to be updated
        if not self.update_universe:
            # Return unchanged universe
            return Universe.Unchanged
        # Otherwise update the universe based on the desired filters
        
        # Filter by allowed exchange and market cap
        symbols = [x for x in fine if \
            x.SecurityReference.ExchangeId in ALLOWED_EXCHANGE and \
            x.MarketCap >= MIN_MARKET_CAP and \
            x.MarketCap <= MAX_MARKET_CAP
            ]
        # Filter stocks based on primary share class
        if PRIMARY_SHARES:
            symbols = [x for x in symbols if x.SecurityReference.IsPrimaryShare]
        # Filter stocks based on disallowed sectors
        if len(SECTORS_NOT_ALLOWED) > 0:
            symbols = [x for x in symbols if \
                x.AssetClassification.MorningstarSectorCode \
                not in SECTORS_NOT_ALLOWED
                ]
        # Filter stocks based on disallowed industry groups
        if len(GROUPS_NOT_ALLOWED) > 0:
            symbols = [x for x in symbols if \
                x.AssetClassification.MorningstarIndustryGroupCode \
                not in GROUPS_NOT_ALLOWED
                ]
        # Filter stocks based on disallowed industries
        if len(INDUSTRIES_NOT_ALLOWED) > 0:
            symbols = [x for x in symbols if \
                x.AssetClassification.MorningstarIndustryCode \
                not in INDUSTRIES_NOT_ALLOWED
                ]
                
        # Return the symbol objects
        self.symbols = [x.Symbol for x in symbols]
        
        # Print universe details when desired
        if PRINT_FINE:
            self.Log(f"Fine filter returned {len(self.symbols)} stocks.")
            
        # Set update universe variable back to False
        self.update_universe = False
        
        return self.symbols
        
#-------------------------------------------------------------------------------
    def OnSecuritiesChanged(self, changes):
        """Event handler for changes to our universe."""
        # Loop through securities added to the universe
        for security in changes.AddedSecurities:
            # Get the security symbol string
            symbol = security.Symbol
            # Skip if BENCHMARK - we cannot trade this!
            if symbol.ID.Symbol == BENCHMARK:
                continue
            # Create a new symbol_data object for the security
            self.symbol_data[symbol] = SymbolData(self, symbol)                 # Using QC.Symbol for key
            
        # Loop through securities removed from the universe
        for security in changes.RemovedSecurities:
            # Get the security symbol string
            symbol = security.Symbol
            # Liquidate removed securities
            if security.Invested:
                # Log message when desired
                if PRINT_ORDERS:
                    self.Log(f"{symbol.ID.Symbol} removed from the universe, "
                        f"so closing open position.")
                self.Liquidate(security.Symbol)
            # Remove from symbol_data dictionary
            if symbol in self.symbol_data:
                # Remove desired bar consolidator for security
                consolidator = self.symbol_data[symbol].consolidator
                self.SubscriptionManager.RemoveConsolidator(
                    security.Symbol, consolidator)
                # Remove symbol from symbol data
                self.symbol_data.pop(symbol)
                  
#-------------------------------------------------------------------------------
    def LiquidatePortfolio(self):
        """Liquidate the entire portfolio. Also cancel any pending orders."""
        # Log message when desired
        if self.Portfolio.Invested and PRINT_ORDERS:
            self.Log("Time for end of day exit. Liquidating the portfolio.")
        self.Liquidate()
        
#-------------------------------------------------------------------------------
    def CheckForSignals(self):
        """Event called when signal checks are desired."""
        # self.Log(f"CheckForSignals: {self.Time}")
        # Check for exit signals
        self.CheckForExits()
        # Check for entry signals
        self.CheckForEntries()
        
#-------------------------------------------------------------------------------
    def CheckForExits(self):
        """Check for exit signals."""
        # Get list of current positions
        positions = [symbol for symbol in self.Portfolio.Keys \
            if self.Portfolio[symbol].Invested]
        # Loop through positions
        for sym in positions:
            # Check for long position
            if self.Portfolio[sym].Quantity > 0: # long
                # Check for long exit signal
                self.symbol_data[sym].long_exit_signal_checks()
                    
#-------------------------------------------------------------------------------
    def CheckForEntries(self):
        """Check for entry signals."""
        # Get list of current positions
        positions = [symbol for symbol in self.Portfolio.Keys \
            if self.Portfolio[symbol].Invested]
        # Get number of new entries allowed
        new_entry_num = MAX_POSITIONS-len(positions)
        
        # Return if no new positions are allowed
        if new_entry_num == 0:
            return
        
        # Create a list of long symbol, pct_change tuples
        long_tuples = []
        long_tuples_symbols = []
        # Loop through the SymbolData class objects
        for sym in self.symbol_data.keys():
            # Skip if already invested
            if self.Securities[sym].Invested:
                continue
            # Check if indicators are ready
            if self.symbol_data[sym].indicators_ready:
                # Check for long entry signal
                if self.symbol_data[sym].long_entry_signal():
                    # Add to the long list
                    long_tuples.append(
                        (sym, 
                        self.symbol_data[sym].fast_slow_pct_difference)
                        )
                    long_tuples_symbols.append(sym)
                    
        # Check if the number of new entries exceeds limit
        if len(long_tuples) > new_entry_num:
            # Sort the entry_tuples list of tuples by largest pct_change
            #  pct_change is the second element of the tuple
            #  reverse=True for descending order (highest to lowest)
            long_tuples = sorted(long_tuples, key=lambda x:x[1], reverse=True)
            # Only keep the top new_entry_num
            long_tuples = long_tuples[:new_entry_num]
            
        # Get list of long symbol objects
        long_symbols = []
        for tup in long_tuples:
            if tup[0] in long_tuples_symbols:
                # Verify there is data available
                if self.Securities[tup[0]].HasData:
                    long_symbols.append(tup[0])
                else: # must ignore signal, order will not be handled
                    continue
                
        # Print entry signal summary when desired
        if PRINT_ENTRIES:
            # Log message
            if len(long_symbols) > 0:
                long_symbol_strings = [x.ID.Symbol for x in long_symbols]
                self.Log(f"{len(long_symbols)} LONG entry signal(s): "
                    f"{long_symbol_strings}")
                    
        # Place entry orders
        for symbol_object in long_symbols:
            self.SetHoldings(symbol_object, MAX_PCT_PER_POSITION)
            
#-------------------------------------------------------------------------------  
    def OnOrderEvent(self, orderEvent):
        """Built-in event handler for orders."""
        # Skip if not filled
        if orderEvent.Status != OrderStatus.Filled:
            return
        # Get the order's symbol
        symbol = orderEvent.Symbol
        # Call on_order_event for the symbol's SymbolData class
        self.symbol_data[symbol].on_order_event(orderEvent)
        
#-------------------------------------------------------------------------------
    # def OnData(self, data):
    #     """Event handler for new data pumped into the algo."""
    #     pass
    
#-------------------------------------------------------------------------------
    def BenchmarkOnEndOfDay(self):
        """Event handler for end of trading day for the benchmark."""
        self.PlotBenchmarkOnEquityCurve()
        
#-------------------------------------------------------------------------------
    def OnEndOfAlgorithm(self):
        """Built-in event handler for end of the backtest."""
        # Check if we want to plot the benchmark
        if PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART:
            self.PlotBenchmarkOnEquityCurve(True)
        # self.Log("End of Backtest")
        
#-------------------------------------------------------------------------------
    def PlotBenchmarkOnEquityCurve(self, force_plot=False):
        """Plot the benchmark buy & hold value on the strategy equity chart."""
        # Initially set percent change to zero
        pct_change = 0
        
        # Get today's daily prices 
        # history algo on QC github shows different formats that can be used:
        # https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/HistoryAlgorithm.py
        hist = self.History([self.bm], timedelta(1), Resolution.Daily)
        
        # Make sure hist df is not empty
        if not hist.empty:
            # Get today's closing price
            price = hist.iloc[-1]['close']
            
            try:
                # Calculate the percent change since the first price
                pct_change = (price-self.bm_first_price)/self.bm_first_price
            except:
                # We have not created the first price variable yet
                # Get today's open and save as the first price
                self.bm_first_price = hist.iloc[-1]['open']
                # Log the benchmark's first price for reference
                self.Log(f"Benchmark first price = {self.bm_first_price}")
                # Calculate the percent change since the first price
                pct_change = (price-self.bm_first_price)/self.bm_first_price
                
        # Calculate today's ending value if we have the % change from the start
        if pct_change != 0:
            bm_value = round(CASH*(1+pct_change),2)
            
            # Plot every PLOT_EVERY_DAYS days
            try:
                # We've previously created the counter, so increment it by 1
                self.bm_plot_counter += 1
                # same as: self.bm_plot_counter = self.bm_plot_counter + 1
            except:
                # We've not created the counter, so set it to 1
                self.bm_plot_counter = 1
            
            # Check if it's time to plot the benchmark value
            if self.bm_plot_counter == PLOT_EVERY_DAYS or force_plot:
                # Plot the benchmark's value to the Strategy Equity chart
                # Plot function requires passing the chart name, series name, 
                # then the value to plot
                self.Plot('Strategy Equity', 'Benchmark', bm_value)
                
                # Plot the account leverage
                account_leverage = self.Portfolio.TotalHoldingsValue \
                    / self.Portfolio.TotalPortfolioValue
                self.Plot('Leverage', 'Leverge', account_leverage)
                
                # Reset counter to 0
                self.bm_plot_counter = 0
                # Log benchmark's ending price for reference
                if force_plot:
                    self.Log("Benchmark's first price = {}".format(
                        self.bm_first_price))
                    self.Log(f"Benchmark's final price = {price}")
                    self.Log(f"Benchmark buy & hold value = {bm_value}")
                    
#-------------------------------------------------------------------------------
    def CustomSecurityInitializer(self, security):
        """
        Define models to be used for securities as they are added to the 
        algorithm's universe.
        """
        # Define the data normalization mode
        security.SetDataNormalizationMode(DataNormalizationMode.Adjusted)
        
        # Define the fee model to use for the security
        # security.SetFeeModel()
        # Define the slippage model to use for the security
        # security.SetSlippageModel()
        # Define the fill model to use for the security
        # security.SetFillModel()
        # Define the buying power model to use for the security
        # security.SetBuyingPowerModel()