| Overall Statistics |
|
Total Orders 4 Average Win 0% Average Loss -0.01% Compounding Annual Return -1.106% Drawdown 0.100% Expectancy -1 Start Equity 100000 End Equity 99990.86 Net Profit -0.009% Sharpe Ratio -64.769 Sortino Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.058 Beta 0.014 Annual Standard Deviation 0.001 Annual Variance 0 Information Ratio 7.527 Tracking Error 0.075 Treynor Ratio -4.893 Total Fees $2.00 Estimated Strategy Capacity $23000.00 Lowest Capacity Asset FCUV XRGF2SRY5D9H Portfolio Turnover 0.34% |
###############################################################################
# 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.PC4_PCT_CHG = self.GetParameter('PC4_PCT_CHG', PC4_PCT_CHG)
self.PH_DOLLAR_CHG = self.GetParameter('PH_DOLLAR_CHG', PH_DOLLAR_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 = {}
self.symbols = []
self.symbols_to_add = []
# 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
)
# Add new symbols
for i in range(0,160,10):
# Start at 1am
dt = DT.datetime(2025,1,1,1,0) + DT.timedelta(minutes=i)
if self.live_mode:
self.my_log(f"Will run add_new_symbols @ {dt}")
self.schedule.on(
self.date_rules.every_day(self.bm),
self.time_rules.at(dt.hour, dt.minute),
self.add_new_symbols
)
# 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
)
# Stop entries at the desired STOP_ENTRY_TIME
self.schedule.on(
self.date_rules.every_day(self.bm),
self.time_rules.at(STOP_ENTRY_TIME.hour, STOP_ENTRY_TIME.minute),
self.stop_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 = ['ETHT', 'ETHE', 'ETHA']
# target_tickers = ['ETHT', 'ETHE', 'ETHA']
# fundamental_dict = {x.symbol: x for x in fundamental}
# kros = fundamental_dict.get(
# Symbol.create("ETHT", 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 'ETHT' in tickers:
# self.my_log('KROS in universe')
# if 'ETHA' in tickers:
# self.my_log('SIGA in universe')
# if 'ETHT' in tickers:
# self.my_log('LQDA in universe')
# if 'PLTD' 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."""
if self.live_mode:
self.my_log(f"on_securities_changed event handler called")
# Loop through added securities
for security in changes.added_securities:
# Add the symbol to the list to add at a later time
symbol = security.symbol
if symbol != self.bm:
self.symbols_to_add.append(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 add_new_symbols(self):
"""Built-in event handler for securities added and removed."""
# Log info for live trading
if self.live_mode and len(self.symbols_to_add) > 0:
self.my_log(
f"add_new_symbols() -> need to initialize "
f"{len(self.symbols_to_add)} symbols"
)
# Loop through and initialize the first 75 symbols
for symbol in self.symbols_to_add.copy()[:75]:
# 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
# Remove from list to process
self.symbols_to_add.remove(symbol)
#-------------------------------------------------------------------------------
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
]
if self.live_mode:
self.my_log(
f"{len(self.prefiltered_symbols)} symbols that pass "
"prefiltered criteria."
)
#------------------------------------------------------------------------------
def stop_trading(self):
"""Stop trading (entries)."""
self.trading = False
#------------------------------------------------------------------------------
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
if self.portfolio.total_portfolio_value > 0:
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.9
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.
1.0.7 (12/23/2024) - Added STOP_ENTRY_TIME input and logic
1.0.8 (01/20/2025) - Added additional logs for live debugging.
- Added self.symbols_to_add to help initialize a large
number of symbols for live trading.
1.0.9 (01/28/2025) - Added PC4_PCT_CHG input and logic.
- Added PH_DOLLAR_CHG input and logic.
- Do not update_intraday_indicators() after
STOP_ENTRY_TIME.
- Removed SymbolData.bar_window_intraday.
#** -> 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 = "02-02-2025" # must be in "MM-DD-YYYY" format
END_DATE = "02-05-2025" # "12-13-2024" # must be in "MM-DD-YYYY" format or None
CASH = 100_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 = False
# When True, universe will only include these stocks
TARGET_TICKERS = [
'ETHT', 'ETHE', 'ETHA', 'PLTD'
]
#------------------------------------------------------------------------------
# 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 #0 # set to 0 to disable
MAX_PRICE = 70 #1e9 # set to 1e6 to disable
MAX_PRICE_FOR_ENTRY = 70.0 # set to 1e6 to disable
# Market cap filtering / e6=million/e9=billion/e12=trillion
MIN_MARKET_CAP = 5e6 #30e6 # 0 to disable
MAX_MARKET_CAP = 50e9 #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.20 #**
# Set the minimum pct change required from the previous day close
PC_PCT_CHG = 0.20 #**
# Set the minimum pct change required from the first premarket price
PM_PCT_CHG = 0.05 #**
# Set the minimum pct change required from the close 4 days ago
PC4_PCT_CHG = 0.05 #**
# Set the minimum dollar change required from the previous day high
PH_DOLLAR_CHG = 2.00 #**
# 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 #**
# Stop checking for entries at this time
STOP_ENTRY_TIME = DT.time(9,33)
#------------------------------------------------------------------------------
# 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 = 10
#------------------------------------------------------------------------------
# POSITION SIZING INPUTS
# Set the desired dollar amount to invest per position
FIXED_DOLLAR_SIZE = 500 #**
# 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 = [5]
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
# min_bars_intraday = 330
# self.bar_window_intraday = RollingWindow[TradeBar](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.first_premarket_price = None
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."""
# Return if trading is no longer allowed
if not self.algo.trading:
return
# Manually update the intraday indicators
self.update_intraday_indicators(bar)
# Check for signal when allowed
if 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)
# Update self.first_premarket_price
if self.first_premarket_price is None:
self.first_premarket_price = bar.open
#-------------------------------------------------------------------------------
# @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 the price change from previous high is okay
previous_high = self.bar_window[0].high
price_chg_high = price-previous_high
if price_chg_high > -self.algo.PH_DOLLAR_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 previous close 4 days ago
pct_close4 = self.pct_chg_from_previous_close4()
if pct_close4 > -self.algo.PC4_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"$({price_chg_high:.2f}), "
f"PC={self.previous_close} ({round(100*pct_close,2)}%), "
f"PC4={self.bar_window[3].close} ({round(100*pct_close4,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 pct_chg_from_previous_close4(self):
"""
Return the current percent change from the previous close 4 days ago.
"""
try:
price = self.price
previous_close = self.bar_window[3].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()