Overall Statistics
Total Orders
3
Average Win
0.40%
Average Loss
0%
Compounding Annual Return
12.893%
Drawdown
0%
Expectancy
0
Start Equity
1000000
End Equity
1003994.97
Net Profit
0.399%
Sharpe Ratio
2.135
Sortino Ratio
0
Probabilistic Sharpe Ratio
94.319%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
0.037
Beta
-0.125
Annual Standard Deviation
0.019
Annual Variance
0
Information Ratio
0.953
Tracking Error
0.075
Treynor Ratio
-0.326
Total Fees
$2.63
Estimated Strategy Capacity
$19000.00
Lowest Capacity Asset
KROS XDI9347V4LET
Portfolio Turnover
0.13%
###############################################################################
# Standard library imports
# from dateutil.relativedelta import *
import datetime as DT
# from dateutil.parser import parse
# import decimal
# import numpy as np
# import pandas as pd
# import pickle
# import pytz
# from System.Drawing import Color

# QuantConnect specific imports
# import QuantConnect as qc
from AlgorithmImports import *

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

###############################################################################
class CustomTradingStrategy(QCAlgorithm):
    def initialize(self):
        """Initialize algorithm."""
        # Set backtest details
        self.set_backtest_details()
        # Add strategy variables
        self.add_strategy_variables()
        # Add instrument data to the algo
        self.add_instrument_data()
        # Schedule functions
        self.schedule_functions()
        # Warmup
        # self.set_warm_up(DT.timedelta(days=5))

#------------------------------------------------------------------------------
    def set_backtest_details(self):
        """Set the backtest details."""
        self.set_start_date(START_DT.year, START_DT.month, START_DT.day)
        if END_DATE:
            self.set_end_date(END_DT.year, END_DT.month, END_DT.day)
        self.set_cash(CASH)
        self.set_time_zone(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.set_brokerage_model(
            BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, 
            AccountType.MARGIN
        )  
        # Configure all universe securities
        # This sets the data normalization mode to raw
        self.set_security_initializer(self.custom_security_initializer)
        # Adjust the cash buffer from the default 2.5% to custom setting
        self.settings.free_portfolio_value_percentage = FREE_PORTFOLIO_VALUE_PCT
        # Disable margin calls
        self.portfolio.margin_call_model = MarginCallModel.NULL
        # Use precise daily end times
        self.settings.daily_precise_end_time = True

#------------------------------------------------------------------------------
    def custom_security_initializer(self, security):
        """Configure settings for securities added to our universe."""
        # Set data normalization mode
        if DATA_MODE == 'ADJUSTED':
            # Throw error if live
            if self.live_mode:
                raise ValueError(f"Must use 'RAW' DATA_MODE for live trading!")
            security.set_data_normalization_mode(
                DataNormalizationMode.ADJUSTED
            )
        elif DATA_MODE == 'RAW':
            security.set_data_normalization_mode(DataNormalizationMode.RAW)
        else:
            raise ValueError(f"Invalid DATA_MODE: {DATA_MODE}")
        # Check for specific security type
        # if security.type == SecurityType.EQUITY:
        # Set the margin model
        security.margin_model = PatternDayTradingMarginModel()
        # Overwrite the security buying power     
        # security.set_buying_power_model(BuyingPowerModel.NULL)  
        security.set_buying_power_model(SecurityMarginModel(4.0))
        # Overwrite the fee model
        # security.set_fee_model(ConstantFeeModel(0))

#------------------------------------------------------------------------------
    def add_strategy_variables(self):
        """Create required strategy variables."""
        # Read algo parameters
        self.PH_PCT_CHG = self.GetParameter('PH_PCT_CHG', PH_PCT_CHG)
        self.PC_PCT_CHG = self.GetParameter('PC_PCT_CHG', PC_PCT_CHG)
        self.PM_PCT_CHG = self.GetParameter('PM_PCT_CHG', PM_PCT_CHG)
        self.MIN_PREVIOUS_VOL = \
            self.GetParameter('MIN_PREVIOUS_VOL', MIN_PREVIOUS_VOL)
        self.MIN_VOL = self.GetParameter('MIN_VOL', MIN_VOL)
        self.STOP_PCT = self.GetParameter('STOP_PCT', STOP_PCT)
        self.TAKE_PROFIT_PCT = self.GetParameter(
            'TAKE_PROFIT_PCT', TAKE_PROFIT_PCT
        )
        self.FIXED_DOLLAR_SIZE = self.GetParameter(
            'FIXED_DOLLAR_SIZE', FIXED_DOLLAR_SIZE
        )
        self.MAX_TRADES = self.GetParameter('MAX_TRADES', MAX_TRADES)
        # Always update the universe when initializing
        self.update_universe = True
        # Keep track of SymbolData class instances and Symbols
        #  These are symbols that have already been filtered to be in our
        #  desired universe. 
        self.symbol_data = {}
        self.symbol_objects = {}
        # Keep track of the last valid market day (for previous day reference)
        self.previous_day = None
        # Keep track if trading is allowed & symbols that pass previous day check
        self.trading = False
        self.prefiltered_symbols = []
        self.positions = 0

#------------------------------------------------------------------------------
    def add_instrument_data(self):
        """Add instrument data to the algo."""
        # Set data resolution
        if DATA_RESOLUTION == 'SECOND':
            self.resolution = Resolution.SECOND
        elif DATA_RESOLUTION == 'MINUTE':
            self.resolution = Resolution.MINUTE
        else:
            raise ValueError(f"Invalid DATA_RESOLUTION: {DATA_RESOLUTION}")
        # Set the universe data properties desired
        self.universe_settings.resolution = self.resolution
        self.universe_settings.extended_market_hours = True
        self.universe_settings.minimum_time_in_universe = MIN_TIME_IN_UNIVERSE
        # Use custom coarse and filter universe selection
        self.add_universe(self.custom_universe_filter)
        # Add the benchmark
        self.bm = self.add_equity(BENCHMARK, self.resolution).symbol
        self.set_benchmark(self.bm)

#------------------------------------------------------------------------------
    def schedule_functions(self):
        """Scheduling the functions required by the algo."""
        # Update self.update_universe variable True when desired
        if UNIVERSE_FREQUENCY == 'DAILY':
            date_rules = self.date_rules.every_day(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.date_rules.week_end(self.bm)
        else: # 'MONTHLY'
            # Want to schedule at end of the month, so actual update on 
            #  first day of the next month
            date_rules = self.date_rules.month_end(self.bm)
        # Timing is after the market closes
        self.schedule.on(
            date_rules,
            self.time_rules.before_market_close(self.bm, -5),
            self.on_update_universe
        )
        # Start trading at the pre-market open @ 4am ET
        self.schedule.on(
            self.date_rules.every_day(self.bm),
            self.time_rules.at(4, 0),
            self.start_trading
        )
        # Exit open trades the desired number of minutes before the close
        self.schedule.on(
            self.date_rules.every_day(self.bm),
            self.time_rules.before_market_close(self.bm, EOD_EXIT_MINUTES),
            self.end_of_day_exit
        )
        # Schedule benchmark end of day event 5 minutes after the close
        # Used to plot the benchmark on the equity curve
        self.schedule.on(
            self.date_rules.every_day(self.bm),
            self.time_rules.before_market_close(self.bm, -5),
            self.benchmark_on_end_of_day
        )

#-------------------------------------------------------------------------------
    def on_update_universe(self):
        """Event called when rebalancing is desired."""
        # Update the variable to trigger the universe to be updated
        self.update_universe = True

#-------------------------------------------------------------------------------
    def custom_universe_filter(self, fundamental):
        """
        Perform custom filters on universe.
        Called once per day.
        Returns all stocks meeting the desired criteria.
        """
        # # Slow - so only for debugging
        # # if self.time.day == 23:
        # #     print('debug')
        # # Loop through all fundamental objects
        # companies = ['KROS','Keros Therapeutics']
        # target_tickers = ['KROS']

        # fundamental_dict = {x.symbol: x for x in fundamental}
        # kros = fundamental_dict.get(
        #     Symbol.create("KROS", SecurityType.EQUITY, Market.USA)
        # )

        # for f in fundamental: 
        #     # Catch a specific company name
        #     if f.company_reference.standard_name is not None:
        #         for c in companies:
        #             if c in f.company_reference.standard_name:
        #                 print('debug')
        #     # Catch a specific ticker symbol
        #     if f.symbol.value in target_tickers:
        #         # Why is 'ALAR' company_reference all None?
        #         #  REF: https://finance.yahoo.com/quote/ALAR/
        #         if f.symbol.value == 'ALAR':
        #             self.my_log(
        #                 f"ALAR company references all None? "
        #                 f"{f.company_reference.standard_name}"
        #             )
        #         # Why is 'SIGA' not even in fundamental?
        #         #  REF: https://finance.yahoo.com/quote/SIGA/

        # Check if the universe doesn't need to be updated
        if not self.update_universe and not self.is_warming_up:
            # Return unchanged universe
            return Universe.UNCHANGED
            
        if ONLY_TRADE_TARGET_TICKERS:
            filtered = [
                f for f in fundamental if f.symbol.value in TARGET_TICKERS
            ]
        else:
            # First filter based on properties that will never change
            # Filter based on allowed exchange
            if USE_EXCHANGE_FILTER:
                filtered = [
                    f for f in fundamental \
                    if f.security_reference.exchange_id in ALLOWED_EXCHANGE
                ]
            else:
                filtered = [f for f in fundamental]
            # Check if fundamental data is required
            if REQUIRE_FUNDAMENTAL_DATA:
                # Filter all securities with fundamental data
                filtered = [f for f in filtered if f.has_fundamental_data]
            # Filter stocks based on primary share class
            if PRIMARY_SHARES:
                filtered = [
                    f for f in filtered if f.security_reference.is_primary_share
                ]
            # Now filter based on properties that constantly change
            # Filter by price
            filtered = [
                f for f in filtered \
                if f.price >= MIN_PRICE and f.price <= MAX_PRICE
            ]
            # Filter by allowed market cap
            filtered = [
                f for f in filtered \
                if f.market_cap >= MIN_MARKET_CAP \
                and f.market_cap <= MAX_MARKET_CAP
            ]

        # Return a unique list of symbol objects
        self.symbols = [f.symbol for f in filtered]
        # Print universe details when desired
        if PRINT_UNIVERSE or self.live_mode:
            self.my_log(f"Universe filter returned {len(self.symbols)} stocks")
            # tickers = [f.Value for f in self.symbols]
            # tickers.sort()
            # self.my_log(
            #     f"Universe filter returned {len(self.symbols)} stocks: {tickers}"
            # )
        # Set update universe variable back to False and return universe symbols
        if not self.is_warming_up:
            self.update_universe = False

        # tickers = [x.value for x in self.symbols]
        # if 'KROS' in tickers:
        #     self.my_log('KROS in universe')
        # if 'SIGA' in tickers:
        #     self.my_log('SIGA in universe')
        # if 'LQDA' in tickers:
        #     self.my_log('LQDA in universe')
        # if 'ALAR' in tickers:
        #     self.my_log('ALAR in universe')
        # if 'UBXG' in tickers:
        #     self.my_log('UBXG in universe')

        return self.symbols

#-------------------------------------------------------------------------------
    def my_log(self, message):
        """Add algo time to log if live trading. Otherwise just log message."""
        if self.live_mode:
            self.log(f'{self.time}: {message}')
        else:
            self.log(message)

#-------------------------------------------------------------------------------
    def on_securities_changed(self, changes):
        """Built-in event handler for securities added and removed."""
        # Loop through added securities
        for security in changes.added_securities:
            symbol = security.symbol
            # Create a new SymbolData object for the security
            self.symbol_data[symbol] = SymbolData(self, symbol)
            # Save a link to the symbol object
            self.symbol_objects[symbol.Value] = symbol
        # Loop through removed securities
        for security in changes.removed_securities:
            symbol = security.symbol
            # Get the SymbolData class instance
            sd = self.symbol_data.get(symbol)
            if sd:
                # Dispose of the symbol's data handlers and pop off dictionary
                sd.dispose()
                data = self.symbol_data.pop(symbol, None)
            # Remove symbol_object from dictionary
            self.symbol_objects.pop(symbol.Value, None)

#-------------------------------------------------------------------------------
    def on_splits(self, splits):
        """Built-in event handler for split events."""
        # Loop through the splits
        for symbol, split in splits.items():
            # Verify this is not a warning
            if split.Type == 1:
                # Get the split factor
                split = split.SplitFactor
                # If this is for the benchmark, update the benchmark price
                if symbol.value == BENCHMARK:
                    try:
                        # Adjust the first benchmark price for the split
                        self.bm_first_price *= split
                    except:
                        # Benchmark first price not set, so skip
                        self.my_log(
                            "Benchmark's first price not set. Trying to "
                            "adjust it for a split."
                        )
                # Catch the appropriate symbol_data instance
                sd = self.symbol_data.get(symbol)
                if sd:
                    # Log message when desired
                    if PRINT_SPLITS or self.live_mode:
                        self.my_log(
                            f"New {symbol.value} split: split factor={split}. "
                            f"Updating {symbol.value}'s indicators."
                        )
                    # Adjust the previous bars by the split adjustment factor
                    sd.adjust_indicators(split, is_split=True)

#-------------------------------------------------------------------------------
    def on_dividends(self, dividends):
        """Built-in event handler for dividend events."""
        # Loop through the dividends
        for symbol, dividend in dividends.items():
            # Get the dividend distribution amount
            dividend = dividend.Distribution
            # Get last 2 daily prices 
            hist = self.history([symbol], 2, Resolution.DAILY)
            price = hist.iloc[-1]['close'] # [-1] for last
            previous_close = hist.iloc[0]['close'] # [0] for first
            # Calculate the dividend adjustment factor
            af = (previous_close-dividend)/previous_close
            # If this is for the benchmark, then we update the benchmark price
            if symbol.value == BENCHMARK:
                try:
                    # Adjust the first benchmark price based on the af
                    self.bm_first_price *= af
                except:
                    # Benchmark first price not set, so skip
                    self.my_log(
                        "Benchmark's first price not set. Trying to adjust it "
                        "for a dividend payment."
                    )
            # Catch the appropriate symbol_data instance
            sd = self.symbol_data.get(symbol)
            if sd:
                # Log message when desired
                if PRINT_DIVIDENDS or self.live_mode:
                    self.my_log(
                        f"New {symbol.value} dividend={dividend}. Close={price}, "
                        f"previous close={previous_close}, so dividend "
                        f"adjustment factor={af}. Updating {symbol.value}'s "
                        "indicators."
                    )
                # Adjust the previous bars by the dividend adjustment factor
                sd.adjust_indicators(af)

#-------------------------------------------------------------------------------  
    def on_order_event(self, order_event):
        """Built-in event handler for orders."""
        # Catch invalid order
        if order_event.Status == OrderStatus.INVALID:
            order = self.transactions.get_order_by_id(order_event.order_id)
            msg = order_event.get_Message()
            self.my_log(f"on_order_event() invalid order ({order}): {msg}")
        # Check if filled
        elif order_event.Status == OrderStatus.FILLED:
            # Get the order's symbol
            order = self.transactions.get_order_by_id(order_event.order_id)
            symbol = order.symbol
            # Get the SymbolData class instance
            sd = self.symbol_data.get(symbol)
            if sd:
                # Pass to the SymbolData's handler
                sd.on_order_event(order_event)
            else:
                msg = f"on_order_event() order without SymbolData class " + \
                    f"instance: ({order})"
                self.my_log(msg)
                if not self.live_mode:
                    raise ValueError(msg)

#------------------------------------------------------------------------------
    def start_trading(self):
        """At beginning of trading day, reset trading allowed variable."""
        # Return if warming up
        if self.is_warming_up:
            return
        # Reset variables
        self.trading = True
        self.positions = 0
        # Get a list of all symbols that pass the previous day filter
        self.prefiltered_symbols = [
            x for x, sd in self.symbol_data.items() \
            if sd.previous_day_entry_filter
        ]

#------------------------------------------------------------------------------
    def end_of_day_exit(self):
        """At end of trading day, so exit any open positions."""
        positions = [
            x for x in self.portfolio.Keys if self.portfolio[x].invested
        ]
        for symbol in positions:
            self.liquidate(symbol, tag='end of day exit')
        self.trading = False

#------------------------------------------------------------------------------
    def benchmark_on_end_of_day(self):
        """Event handler for end of trading day for the benchmark."""
        self.plot_benchmark_on_equity_curve()
        # Update prevous day reference
        self.previous_day = self.time.date()
        
#------------------------------------------------------------------------------
    def on_end_of_algorithm(self):
        """Built-in event handler for end of the backtest."""
        # Plot the benchmark buy and hold value on the equity curve chart
        self.plot_benchmark_on_equity_curve(force_plot=True)

#------------------------------------------------------------------------------
    def plot_benchmark_on_equity_curve(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.my_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.total_holdings_value \
                    / self.portfolio.total_portfolio_value
                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.my_log(
                        f"Benchmark's first price = {self.bm_first_price}"
                    )
                    self.my_log(f"Benchmark's final price = {price}")
                    self.my_log(f"Benchmark buy & hold value = {bm_value}")
from AlgorithmImports import *
"""
Custom Shorting Day Trading Strategy
Version 1.0.6
Platform: QuantConnect
By: Aaron Eller
r.aaron.eller@gmail.com

Revision Notes:
    1.0.0 (08/28/2024) - Initial.
    1.0.1 (08/29/2024) - Corrected the first_premarket_price.
    1.0.3 (09/02/2024) - Changed to use daily regular session for the 
                          previous day references. 
    1.0.4 (09/04/2024) - Added MAX_PRICE_FOR_ENTRY input and logic.
    1.0.5 (09/05/2024) - Added USE_EXCHANGE_FILTER input and logic.
    1.0.6 (12/19/2024) - Added TAKE_PROFIT_PCT input and logic.
                       - Changed to start trading as soon as the pre-market
                          session opens. Checks for possible entry signals 
                          constantly rather than only at the market open.
                       - Added ONLY_TRADE_TARGET_TICKERS input and logic.

#** -> can be "Parameters" for optimization

References:
-QC (Lean) Class List
  https://lean-api-docs.netlify.app/annotated.html
"""
# Standard library imports
import datetime as DT
###############################################################################
# Backtest inputs

# NOTE1: QC index data composition starts in August 2009
# NOTE2: if not using index data, can start in 1998
START_DATE = "12-02-2024" # must be in "MM-DD-YYYY" format
END_DATE   = "12-13-2024" # must be in "MM-DD-YYYY" format or None
CASH = 1_000_000 # starting portfolio value

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

# Set the data resolution required
# Must be 'SECOND' or 'MINUTE'
DATA_RESOLUTION = 'MINUTE'

# Set the data normalization mode - either 'RAW' or 'ADJUSTED'
#  For live trading, must use 'RAW'
DATA_MODE = 'RAW'

# Set the Benchmark
BENCHMARK = 'SPY'

# Only trade target tickers?
ONLY_TRADE_TARGET_TICKERS = True
# When True, universe will only include these stocks
TARGET_TICKERS = ['KROS']

#------------------------------------------------------------------------------
# CUSTOM UNIVERSE INPUTS

# How often to update the universe?
# Options: 'DAILY', 'WEEKLY', 'MONTHLY'
UNIVERSE_FREQUENCY = 'DAILY'

# Set the minimum number of days to leave a stock in the universe
# This helps with making the universe output more stable
MIN_TIME_IN_UNIVERSE = 21 # approximately 1 month

REQUIRE_FUNDAMENTAL_DATA = False # if True, stocks only / no etfs e.g.
MIN_PRICE = 1.50 # set to 0 to disable
MAX_PRICE = 70.0 # set to 1e6 to disable
MAX_PRICE_FOR_ENTRY = 100.0 # set to 1e6 to disable

# Market cap filtering / e6=million/e9=billion/e12=trillion
MIN_MARKET_CAP = 500e6 #30e6 # 0 to disable
MAX_MARKET_CAP = 4e9 #100e12 # extremely high value like 100e12 to disable

# Turn on/off specific exchanges allowed
USE_EXCHANGE_FILTER = False
# When used, turn on/off specific exchanges
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?
# Example is Alphabet (Google): 
#   A shares = GOOGL (voting rights) -> considered the 'primary share' class.
#   C shares = GOOG (no voting rights)
# REF: 
'''https://www.investopedia.com/ask/answers/052615/whats-difference-between-
googles-goog-and-googl-stock-tickers.asp'''
PRIMARY_SHARES = False

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

# Checks for signals at the market open

# All percents below are decimal percents. E.g. 0.05=5.0%

# Set the minimum pct change required from the previous day high
PH_PCT_CHG = 0.2 #**
# Set the minimum pct change required from the previous day close
PC_PCT_CHG = 0.2 #**
# Set the minimum pct change required from the first premarket price
PM_PCT_CHG = 0.2 #**

# Set the previous day's minimum allowed volume
MIN_PREVIOUS_VOL = 100_000 #**

# Set the current day's minimum trading volume
MIN_VOL = 10_000 #**

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

# Set the percentage stop to use (as a decimal percent, e.g. 0.20=20.0%)
STOP_PCT = 0.20 #**
# Set the target to exit for profit (as a decimal percent, e.g. 0.40=40.0%)
TAKE_PROFIT_PCT = 0.40 #**

# Set the number of minutes prior to the market close to exit
EOD_EXIT_MINUTES = 1

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

# Set the desired dollar amount to invest per position
FIXED_DOLLAR_SIZE = 10_000 #**

# Set the max number of trades allowed per day
MAX_TRADES = 10 #**

#------------------------------------------------------------------------------
# LOGGING INPUTS
PRINT_UNIVERSE  = False # print universe info
PRINT_DIVIDENDS = False # turn on/off logs for new dividends
PRINT_SPLITS    = False # turn on/off logs for new splits 
PRINT_SIGNALS   = True # print new signal information
PRINT_ORDERS    = True # print new orders


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

# VALIDATE USER INPUTS - DO NOT CHANGE BELOW!!!
#-------------------------------------------------------------------------------
# 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 time zone -> do not change! Algo will use hard coded times!
TIMEZONE = "US/Eastern" # e.g. "US/Eastern", "US/Central", "US/Pacific"

#-------------------------------------------------------------------------------
# Verify start date
try:
    START_DT = DT.datetime.strptime(START_DATE, '%m-%d-%Y')
except:
    raise ValueError(
        f"Invalid START_DATE format ({START_DATE}). Must be in MM-DD-YYYY "
        "format."
    )
# Verify end date
try:
    if END_DATE:
        END_DT = DT.datetime.strptime(END_DATE, '%m-%d-%Y')
except:
    raise ValueError(
        f"Invalid END_DATE format ({END_DATE}). Must be in MM-DD-YYYY "
        "format or set to None to run to date."
    )

# Verify universe update frequency
UNIVERSE_FREQUENCY = UNIVERSE_FREQUENCY.upper()
if UNIVERSE_FREQUENCY not in ['DAILY', 'WEEKLY', 'MONTHLY']: #'QUARTERLY'
    raise ValueError(
        f"Invalid UNIVERSE_FREQUENCY ({UNIVERSE_FREQUENCY}). "
        f"Must be ['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY']."
    )

#-------------------------------------------------------------------------------
# Verify the DATA_MODE input
DATA_MODE = DATA_MODE.upper()
if DATA_MODE not in ['RAW', 'ADJUSTED']:
    raise ValueError(
        f"Invalid DATA_MODE ({DATA_MODE}). Must be: 'RAW' or 'ADJUSTED'"
    )

#-------------------------------------------------------------------------------
# Verify DATA_RESOLUTION input
DATA_RESOLUTION = DATA_RESOLUTION.upper()
if DATA_RESOLUTION not in ['SECOND', 'MINUTE']:
    raise ValueError(
        f"Invalid DATA_RESOLUTION ({DATA_RESOLUTION}). "
        "Must be: 'SECOND' or 'MINUTE'"
    )
      
#------------------------------------------------------------------------------- 
# 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.")  

#-------------------------------------------------------------------------------
# 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)
# Standard library imports
import datetime as DT
# from dateutil.relativedelta import relativedelta
# import numpy as np
# import pandas as pd
import pytz
# import statistics

# QuantConnect specific imports
# import QuantConnect as qc
from AlgorithmImports import *

# 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
        self.symbol_object = symbol_object
        self.ticker = symbol_object.Value
        self.symbol = str(self.symbol_object.id)
        # Get the symbol's exchange market info
        self.get_exchange_info()
        # Add strategy variables
        self.set_strategy_variables()
        # Add the bars and indicators required
        self.add_bars()
        self.add_indicators()
        # Schedule functions
        self.schedule_functions()

#-------------------------------------------------------------------------------
    def get_exchange_info(self):
        """Get the securities exchange info."""
        # Get the SecurityExchangeHours Class object for the symbol
        security = self.algo.securities[self.symbol_object]
        self.exchange_hours = security.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.get_next_market_open(dt, False)
        # Save the typical (regualar session) market open and close times
        self.mkt_open = mkt_open_dt.time()
        mkt_close_dt = self.exchange_hours.get_next_market_close(dt, False)
        self.mkt_close = mkt_close_dt.time()
        
        # Get the exchange timezone
        self.mkt_tz = pytz.timezone(str(self.exchange_hours.time_zone))
        # 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()
        self.mkt_open_local_tz = \
            (mkt_open_dt-DT.timedelta(hours=self.offset_hrs)).time()

        # Get the min price
        symbol_properties = security.symbol_properties
        # Get and save the contract specs
        self.min_tick = symbol_properties.minimum_price_variation

#-------------------------------------------------------------------------------
    def set_strategy_variables(self):
        """Set strategy specific variables."""
        self.warming_up = False
        self.reset_trade_variables()
        # Get the min tick size
        self.tick_size = self.algo.securities[
            self.symbol_object
        ].symbol_properties.minimum_price_variation
        self.entry_limit_order = None

#-------------------------------------------------------------------------------
    def reset_trade_variables(self):
        """Reset trade specific variables."""
        self.stop_price = None
        self.stop_loss_order = None
        self.best_price = None
        self.take_profit_order = None

#-------------------------------------------------------------------------------
    def add_bars(self):
        """Add bars required."""
        # Create a daily bar consolidator
        self.calendar_initialized = False
        consolidator = TradeBarConsolidator(self.daily_calendar)
        # Create an event handler to be called on each new consolidated bar
        consolidator.data_consolidated  += self.on_data_consolidated
        # Link the consolidator with our symbol and add it to the algo manager
        self.algo.subscription_manager.add_consolidator(
            self.symbol_object, consolidator
        )
        # Save consolidator link so we can remove it when necessary
        self.consolidator = consolidator

        # Create an intraday minute bar consolidator
        consolidator2 = TradeBarConsolidator(DT.timedelta(minutes=1))
        # Create an event handler to be called on each new consolidated bar
        consolidator2.data_consolidated  += self.on_intraday_data_consolidated
        # Link the consolidator with our symbol and add it to the algo manager
        self.algo.subscription_manager.add_consolidator(
            self.symbol_object, consolidator2
        )
        # Save consolidator link so we can remove it when necessary
        self.consolidator2 = consolidator2

#-------------------------------------------------------------------------------
    def daily_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.
        """
        # Need to handle case where algo initializes and this function is called
        #  for the first time.
        if not self.calendar_initialized:
            # Since this doesn't matter, we'll pass dt as start and one day 
            # as the timedelta until end_dt
            start_dt = dt
            end_dt = start_dt + DT.timedelta(1)
            self.calendar_initialized = True
            return CalendarInfo(start_dt, end_dt-start_dt)
        # 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
        )
        # Get today's end time from the SecurityExchangeHours Class object
        end = self.exchange_hours.get_next_market_close(start, False)
        # Catch when start is after the passed dt
        # QC now throws an error in this case
        if start > dt:
            # To handle the QC error, pass period for no data
            # Set the end to be the next desired start
            end = start
            # And set start to dt to avoid QC throwing error
            start = dt
            # This will result in the next dt being the desired start time
        # Return the start datetime and the consolidation period
        return CalendarInfo(start, end-start)

#-------------------------------------------------------------------------------
    def dispose(self):
        """Stop the data consolidators."""
        # Remove the consolidators from the algo manager
        self.consolidator.data_consolidated -= self.on_data_consolidated
        self.algo.subscription_manager.remove_consolidator(
            self.symbol_object, self.consolidator
        )
        self.consolidator2.data_consolidated -= self.on_intraday_data_consolidated
        self.algo.subscription_manager.remove_consolidator(
            self.symbol_object, self.consolidator2
        )

#-------------------------------------------------------------------------------
    def add_indicators(self):
        """Add indicators and other required variables."""
        # Create an empty list to hold all indicators
        # Will add (indicator, update_method) tuples
        #  where update_method is either 'high', 'low', 'close', 'volume', or 
        #  'bar'
        self.indicators = []
        min_intraday_bars = [2]
        self.todays_volume = 0 # used for the cumulative volume
        # Keep track of bars for the daily indicators
        self.min_bars = 5 # only need last 1 daily bar
        self.bar_window = RollingWindow[TradeBar](self.min_bars)
        # Keep track of bars for the intraday indicators
        # We need 1min bars from 4am to 930am -> 330 minutes
        self.min_bars_intraday = 330
        self.bar_window_intraday = RollingWindow[TradeBar](self.min_bars_intraday)
        # Warm up the indicators with historical data
        self.warmup_indicators()
        self.reset_intraday_indicators()

#-------------------------------------------------------------------------------
    def schedule_functions(self):
        """Schedule functions required by the algo."""
        # Reset the intraday indicators at midnight every day
        self.algo.schedule.on(
            self.algo.date_rules.every_day(self.symbol_object),
            self.algo.time_rules.at(0,0),
            self.reset_intraday_indicators
        )

#-------------------------------------------------------------------------------
    def reset_indicators(self):
        """Reset indicators required."""
        # Loop through list of indicators
        for indicator, _ in self.indicators:
            indicator.reset()
        # Handle custom indicators - if any
        # Reset the rolling windows
        self.bar_window.reset()
        
#-------------------------------------------------------------------------------
    def reset_intraday_indicators(self):
        """Reset indicators required."""
        # Handle custom indicators - if any
        # Reset the rolling windows
        self.bar_window_intraday.reset()
        self.todays_volume = 0
        self.traded_today = False

#-------------------------------------------------------------------------------
    def adjust_indicators(self, adjustment_factor, is_split=False):
        """Adjust all indicators for splits or dividends."""
        self.warming_up = True
        # Get a list of the current bars
        bars = list(self.bar_window)
        # bars_intraday = list(self.bar_window_intraday)
        # Current order is newest to oldest (default for rolling window)
        # Reverse the list to be oldest to newest
        bars.reverse()
        # bars_intraday.reverse()
        # Reset all indicators
        self.reset_indicators()
        self.reset_intraday_indicators()
        # Loop through the daily bars from oldest to newest
        for bar in bars:
            # Adjust the bar by the adjustment factor
            bar.Open *= adjustment_factor
            bar.High *= adjustment_factor
            bar.Low *= adjustment_factor
            bar.Close *= adjustment_factor
            # Update volume on split adjustment
            if is_split:
                vol_adjustment = 1.0/adjustment_factor
                bar.Volume *= vol_adjustment
            # Use the bar to update the indicators
            # This also adds the bar to the rolling window
            self.update_indicators(bar)
        self.warming_up = False

#-------------------------------------------------------------------------------
    def warmup_indicators(self):
        """Warm up indicators using historical data.""" 
        # Update warmup variable
        self.warming_up = True
        # Get historical daily trade bars for the symbol
        min_days = int(10.0*self.min_bars)
        bars = self.algo.history[TradeBar](
            self.symbol_object, 
            DT.timedelta(days=min_days),
            Resolution.DAILY
        )
        # Loop through the bars and update the indicators
        for bar in bars:
            # Pass directly to the event handler (no consolidating necessary)
            self.on_data_consolidated(None, bar)
        # We don't need to warm up intraday indicators
        # Update warmup variable back to False
        self.warming_up = False

#-------------------------------------------------------------------------------
    def on_data_consolidated(self, sender, bar):
        """Event handler for desired daily bar."""
        # Skip if not regular session
        if not self.warming_up:
            if bar.time.time() != self.mkt_open_local_tz:
                return
        # Manually update all of the indicators
        self.update_indicators(bar)

#-------------------------------------------------------------------------------
    def update_indicators(self, bar):
        """Manually update all of the symbol's indicators."""
        # Loop through the indicators
        for indicator, update_method in self.indicators:
            if update_method == 'close':
                indicator.update(bar.end_time, bar.close)
            elif update_method == 'high':
                indicator.update(bar.end_time, bar.high)
            elif update_method == 'low':
                indicator.update(bar.end_time, bar.low)
            elif update_method == 'bar':
                indicator.update(bar) 
            elif update_method == 'volume':
                indicator.update(bar.end_time, bar.volume)
        # Add bar to the rolling window
        self.bar_window.add(bar)

#-------------------------------------------------------------------------------
    def on_intraday_data_consolidated(self, sender, bar):
        """Event handler for desired intraday bar."""
        # Manually update the intraday indicators
        self.update_intraday_indicators(bar)
        # Check for signal if trading allowed
        if self.algo.trading \
        and self.symbol_object in self.algo.prefiltered_symbols \
        and self.algo.positions < self.algo.MAX_TRADES:
            # Go short on an entry signal
            if self.short_entry_signal(bar):
                self.go_short(bar)
                
#-------------------------------------------------------------------------------
    def update_intraday_indicators(self, bar):
        """Manually update all of the symbol's intraday indicators."""
        # Update cummulative volume for today
        self.todays_volume += bar.volume
        # Add bar to the rolling window
        self.bar_window_intraday.add(bar)

#-------------------------------------------------------------------------------
    # @property
    # def indicators_ready(self):
    #     """Return whether the indicators are warmed up or not."""
    #     # Loop through list of indicators
    #     for indicator, _ in self.indicators:
    #         if not indicator.is_ready:
    #             return False     
    #     if not self.bar_window.is_ready:
    #         return False
    #     if not self.bar_window_intraday.is_ready:
    #         return False   
    #     # Otherwise True
    #     return True

#-------------------------------------------------------------------------------
    @property
    def current_qty(self):
        """Return the current traded symbol quantity held in the portfolio."""
        return self.algo.portfolio[self.symbol_object].quantity

#-------------------------------------------------------------------------------
    @property
    def price(self):
        """Return the current traded symbol quantity held in the portfolio."""
        return self.algo.securities[self.symbol_object].price
     
#-------------------------------------------------------------------------------
    @property
    def previous_day_entry_filter(self):
        """
        Return whether the stock meets all of the entry requirements from the 
        previous day.
        """
        # Check if the symbol is in the universe
        if self.symbol_object not in self.algo.symbols:
            return False
        # Check yesterday's volume
        elif self.previous_volume < self.algo.MIN_PREVIOUS_VOL:
            return False
        # Check yesterday's date
        elif self.previous_date != self.algo.previous_day \
        and self.algo.previous_day is not None:
            return False
        return True

#-------------------------------------------------------------------------------
    def short_entry_signal(self, bar):
        """
        Check for a short entry signal.
        """
        # # Debugging
        # if self.ticker == 'KROS':
        #     self.algo.my_log(f"KROS check entry_filter()")
        # elif self.ticker == 'LQDA':
        #     self.algo.my_log(f"LQDA check entry_filter()")
        # elif self.ticker == 'ALAR':
        #     self.algo.my_log(f"ALAR check entry_filter()")
        # elif self.ticker == 'UBXG':
        #    self.algo.my_log(f"UBXG check entry_filter()")

        # # Check if the symbol is in the universe
        # if self.symbol_object not in self.algo.symbols:
        #     return False

        # Not valid if we already have a position
        if self.current_qty < 0:
            return False
        elif self.traded_today:
            return False
        
        # Check for min/max price
        price = bar.close
        if price < MIN_PRICE:
            return False
        elif price > MAX_PRICE_FOR_ENTRY:
            return False

        # # Check yesterday's volume
        # if self.previous_volume < self.algo.MIN_PREVIOUS_VOL:
        #     return False

        # Check today's volume
        if self.todays_volume < self.algo.MIN_VOL:
            return False
        # Check if price is min percentage from previous high
        pct_high = self.pct_chg_from_previous_high(price)
        if pct_high > -self.algo.PH_PCT_CHG:
            return False
        # Check if price is min percentage from previous close
        pct_close = self.pct_chg_from_previous_close()
        if pct_close > -self.algo.PC_PCT_CHG:
            return False
        # Check if price is min percentage from first premarket price
        first_price = self.first_premarket_price
        if first_price == 0:
            return False
        pct_first = (price-first_price)/first_price
        if pct_first > -self.algo.PM_PCT_CHG:
            return False

        # # Check yesterday's date
        # if self.previous_date != self.algo.previous_day \
        # and self.algo.previous_day is not None:
        #     self.algo.my_log(
        #         f"{self.ticker} Previously a signal, but now filtered out. The "
        #         f"previous date is {self.previous_date} vs. "
        #         f"{self.algo.previous_day}"
        #     )
        #     return False

        # Otherwise valid
        # Print info
        if PRINT_SIGNALS or self.algo.live_mode:
            self.algo.my_log(
                f"{self.ticker} SHORT ENTRY SIGNAL: price={price}, "
                f"PH={self.previous_high} ({round(100*pct_high,2)}%), "
                f"PC={self.previous_close} ({round(100*pct_close,2)}%), "
                f"PV={self.previous_volume}, "
                f"PM={self.first_premarket_price} ({round(100*pct_first,2)}%), "
                f"today's vol={self.todays_volume}"
            )
        return True

#-------------------------------------------------------------------------------
    @property
    def previous_date(self):
        """Return the previous day's date."""
        try:
            return self.bar_window[0].end_time.date()
        except:
            return 0

#-------------------------------------------------------------------------------
    @property
    def previous_volume(self):
        """Return the previous day's volume."""
        try:
            return self.bar_window[0].volume
        except:
            return 0

#-------------------------------------------------------------------------------
    @property
    def previous_high(self):
        """Return the previous day's high."""
        try:
            return self.bar_window[0].high
        except:
            return 0

#-------------------------------------------------------------------------------
    @property
    def previous_close(self):
        """Return the previous day's close."""
        try:
            return self.bar_window[0].close
        except:
            return 0

#-------------------------------------------------------------------------------
    @property
    def first_premarket_price(self):
        """Return the first premarket price for the day."""
        try:
            # First bar is the last in the rolling window
            count = self.bar_window_intraday.count
            return self.bar_window_intraday[count-1].open
        except:
            return 0

#-------------------------------------------------------------------------------
    def pct_chg_from_previous_high(self, price):
        """Return the current percent change from the previous high."""
        try:
            previous_high = self.bar_window[0].high
            pct_chg = (price-previous_high)/previous_high
            return pct_chg
        except:
            return 0

#-------------------------------------------------------------------------------
    def pct_chg_from_previous_close(self):
        """Return the current percent change from the previous close."""
        try:
            price = self.price
            previous_close = self.bar_window[0].close
            pct_chg = (price-previous_close)/previous_close
            return pct_chg
        except:
            return 0

#-------------------------------------------------------------------------------
    def update_stop_order(self, price, tag):
        """Update the desired stop order."""
        # Get the stop order ticket
        ticket = self.stop_loss_order
        if ticket is None:
            return
        # Update the price
        # price = self.round_price(price)
        ticket.UpdateStopPrice(price, tag=tag)
        self.stop_price = price
        # Print details when desired
        if PRINT_ORDERS or self.algo.live_mode:
            self.algo.my_log(
                f"{self.ticker} trailing stop price updated to {price}"
            )

#-------------------------------------------------------------------------------
    def round_price(self, price):
        """Round the given price to the nearest tick value."""
        # Get the priced rounded to the nearest tick value
        return round(price, 2)
        # price = round(price/self.min_tick)*self.min_tick
        # # Only return the desired number of decimals
        # try:
        #     num_left_decimal = str(price).index('.')
        # except:
        #     # min tick uses exponential format
        #     # Create a decimal with max of 12 decimals
        #     tmp = round(decimal.Decimal(price),12).normalize()
        #     num_left_decimal = '{:f}'.format(tmp).index('.')
        # length = num_left_decimal + self.min_tick_decimals + 1
        # # return float(str(price)[:length])
        # return float('{:f}'.format(price)[:length])

#-------------------------------------------------------------------------------
    def valid_order(self, order):
        """Return True / False if the order placed is valid."""
        # Check order status
        if order.Status == OrderStatus.INVALID:
            return False
        else:
            return True

#-------------------------------------------------------------------------------
    def update_limit_order(self, ticket, price):
        """Update the limit order's price.""" 
        update_settings = UpdateOrderFields()
        update_settings.limit_price = price
        response = ticket.update(update_settings)
        if not response.is_success:
            self.algo.debug(
                f"{self.symbol} limit order update request not successful!"
            )

#-------------------------------------------------------------------------------
    def go_short(self, bar):
        """Go short the desired amount."""
        # Get the desired order qty
        order_qty = -int(self.algo.FIXED_DOLLAR_SIZE/bar.close)

        # Use a limit order if not after the market open
        new_entry = False
        if self.algo.time.time() < DT.time(9,30):
            # Sell at the bid price
            limit_price = round(
                self.algo.securities[self.symbol_object].bid_price, 2
            )
            # Check if we already have an open limit order
            if self.entry_limit_order:
                # Update the price
                self.update_limit_order(self.entry_limit_order, limit_price)
            else:
                # Place a new order
                order = self.algo.limit_order(
                    self.symbol_object, order_qty, limit_price, 
                    tag='short entry limit'
                )
                self.entry_limit_order = order
                # Check order status
                if not self.valid_order(order):
                    self.algo.my_log(
                        f"Invalid {self.ticker} short entry order!"
                    )
                else:
                    new_entry = True
        else:
            # Cancel entry limit order - if one
            if self.entry_limit_order:
                self.entry_limit_order.cancel()
            # Place a market order
            order = self.algo.market_order(
                self.symbol_object, order_qty, tag='short entry'
            )
            # Check order status
            if not self.valid_order(order):
                self.algo.my_log(
                    f"Invalid {self.ticker} short entry order!"
                )
            else:
                new_entry = True

        # Increment the algo's positions
        if new_entry:
            self.algo.positions += 1
            self.traded_today = True

#-------------------------------------------------------------------------------
    def go_flat(self):
        """Go short the desired amount."""
        # Cancel any open exit orders
        self.cancel_exit_orders()
        # Get the desired order qty
        order_qty = -self.current_qty
        # Place the exit order
        order = self.algo.market_order(
            self.symbol_object, order_qty, tag='exit'
        )
        # Check order status
        if not self.valid_order(order):
            if not self.algo.live_mode:
                raise ValueError(f"Invalid {self.ticker} exit order!")

#-------------------------------------------------------------------------------
    def cancel_exit_orders(self):
        """Cancel any open exit orders."""
        if self.stop_loss_order:
            self.stop_loss_order.cancel()
        if self.take_profit_order:
            self.take_profit_order.cancel()

#-------------------------------------------------------------------------------  
    def on_order_event(self, order_event):
        """Built-in event handler for orders."""
        # Get the order's info
        order = self.algo.transactions.get_order_by_id(order_event.order_id)
        symbol = order.symbol
        tag = order.tag
        qty = int(order.quantity)
        avg_fill = order_event.fill_price
        # Log message when desired
        if PRINT_ORDERS or self.algo.live_mode:
            self.algo.my_log(
                f"{symbol.Value} {tag} order for {qty} shares filled @ "
                f"{avg_fill:.2f}"
            )

        # Catch entry order
        if 'entry' in tag: # don't do tag == 'entry bla bla'
            # Set entry order back to None
            if 'limit':
                self.entry_limit_order = None

            # Calculate the stop loss AND take profit prices
            if qty > 0: # long
                # not valid for strategy!
                if self.algo.live_mode:
                    self.go_flat()
                else:
                    raise ValueError(f"Invalid long {symbol.value} position!")
            else: # short
                stop_price = round((1+self.algo.STOP_PCT)*avg_fill,2)
                tp_price = round((1-self.algo.TAKE_PROFIT_PCT)*avg_fill,2)

            self.stop_price = stop_price
            self.best_price = avg_fill
            
            # Place and save the stop loss order
            order_qty = -qty
            order = self.algo.stop_market_order(
                symbol, order_qty, stop_price, tag='stop loss exit'
            )
            if not self.valid_order(order) and not self.algo.live_mode:
                raise
            self.stop_loss_order = order

            # Place and save the take profit order
            order2 = self.algo.limit_order(
                symbol, order_qty, tp_price, tag='take profit exit'
            )
            if not self.valid_order(order2) and not self.algo.live_mode:
                raise
            self.take_profit_order = order2

        # Catch exit order
        elif 'exit' in tag:
            # Catch stop order
            if 'stop loss' in tag:
                self.stop_loss_order = None
            # Catch take profit
            elif 'take profit' in tag:
                self.take_profit_order = None
            # Reset trade if qty 0 
            if self.current_qty == 0:
                # Cancel any open exit order
                self.cancel_exit_orders()
                self.reset_trade_variables()