| Overall Statistics |
|
Total Orders 207 Average Win 0.76% Average Loss -1.06% Compounding Annual Return 2.491% Drawdown 14.300% Expectancy 0.060 Start Equity 100000 End Equity 103826.17 Net Profit 3.826% Sharpe Ratio -0.307 Sortino Ratio -0.234 Probabilistic Sharpe Ratio 12.390% Loss Rate 38% Win Rate 62% Profit-Loss Ratio 0.72 Alpha -0.086 Beta 0.354 Annual Standard Deviation 0.1 Annual Variance 0.01 Information Ratio -1.624 Tracking Error 0.114 Treynor Ratio -0.087 Total Fees $561.75 Estimated Strategy Capacity $1800000.00 Lowest Capacity Asset HBAN R735QTJ8XC9X Portfolio Turnover 11.10% |
'''
https://www.quantitativo.com/p/robustness-of-the-211-sharpe-mean
focus on sp500 constituents
'''
from AlgorithmImports import *
from collections import deque
from datetime import time, timedelta
import numpy as np
import requests
import pandas as pd
class SymbolData:
def __init__(self, symbol, algo):
self.symbol = symbol
self.close = 0.0
self.open = 0.0
self.high = 0.0
self.low = 0.0
self.close_on_open_trigger = False
self.long_on_open_trigger = False
self.todays_high = 0.0
self.todays_low = 999999.9
self.yesterdays_high = 0.0
self.yesterdays_close = 0.0
# Rolling Mean High Minus Low
self.rolling_mean_length = 25
self.rolling_mean_high_low_list = deque(maxlen=self.rolling_mean_length)
# SMA
self.rolling_sma_list = deque(maxlen=200)
## Lower band
self.lower_band_multiple = 2.5
self.rolling_high_list = deque(maxlen=10)
## ATR
self.atr = None
self.atr_periods = 10
self.atr_values_list = deque(maxlen=self.atr_periods)
## Volume
self.volume_history_days = 60
self.volumes_list = deque(maxlen=self.volume_history_days)
self.todays_total_volume = 0.0
# Initialize technical indicators
self.ibs = 0.0
self.lower_band = 0.0
self.natr = 0
def update(self, bar, algo):
self.high, self.low, self.close, self.open, self.volume = bar.High, bar.Low, bar.Close, bar.Open, bar.Volume
## Daily high/low
self.todays_high = max(self.todays_high, self.high)
self.todays_low = min(self.todays_low, self.low)
self.todays_total_volume = self.todays_total_volume + self.volume
# algo.Debug(f'{algo.Time} {self.symbol} UPDATED {self.high} {self.todays_high}')
def update_eod(self, algo):
# algo.Debug(f'{algo.Time} : update_eod running')
self.rolling_high_list.append(self.todays_high)
self.rolling_mean_high_low_list.append(self.todays_high - self.todays_low)
self.rolling_sma_list.append(self.close)
self.volumes_list.append(self.todays_total_volume)
## ATR
tr = max(self.todays_high - self.todays_low, abs(self.todays_high - self.yesterdays_close), abs(self.todays_low - self.yesterdays_close))
self.atr_values_list.append(tr)
self.atr = np.mean(self.atr_values_list)
# normalized
self.natr = self.atr / self.close
## Rolling Mean High Minus Low
if len(self.rolling_mean_high_low_list) >= self.rolling_mean_length:
high_low_mean = np.mean(self.rolling_mean_high_low_list)
## Lower band
self.lower_band = max(self.rolling_high_list) - self.lower_band_multiple * high_low_mean
## IBS
if self.todays_high != self.todays_low:
self.ibs = (self.close - self.todays_low) / (self.todays_high - self.todays_low)
else:
self.ibs = 0.0
def eod_reset(self):
self.yesterdays_high = self.todays_high
self.todays_high = 0.0
self.todays_low = 999999.9
self.yesterdays_close = self.close
self.todays_total_volume = 0.0
class MyAlgorithm(QCAlgorithm):
def Initialize(self):
'''
This method is the entry point of your algorithm where you define a series of settings.
LEAN only calls this method one time, at the start of your algorithm.
'''
self.Debug(f'--- Initializing Algorithm ----')
self.SetStartDate(2023, 1, 1)
self.SetEndDate(2024, 7, 10)
self.SetCash(100000)
self.set_benchmark("SPY")
# Define universe selection method
self.AddEquity("SPY", Resolution.Minute)
# Use Filter to select universe based on market cap and price
self.universe_settings.asynchronous = True
self.UniverseSettings.Resolution = Resolution.Minute
self.add_universe(self.universe.etf("SPY"))
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.MARGIN)
self.portfolio.set_positions(SecurityPositionGroupModel.NULL)
self.universe_settings.leverage = 10
# self.set_brokerage_model(BrokerageName.ALPACA)
# Initialize market open and close times
self.market_open_time = time(9, 31)
self.market_close_time = time(15, 59)
self.number_of_stocks = 3
self.symbol_data = {}
self.buy_on_open_list = []
self.store_trades = ''
self.store_decisions = ''
self.store_holdings = ''
self.store_longlist = ''
self.store_longlist_entries = ''
self.errors = ''
## IBS
self.ibs_threshold = 0.3
def OnData(self, data):
if self.Time.hour == 0:
return
# Update the indicator values
for symbol in self.symbol_data:
try:
bar = data.Bars[symbol]
symboldata = self.symbol_data[symbol]
symboldata.update(bar, self)
except:
continue
# maybe newly added to universe and no tradebar?
# Check if we need to close
## MARKET OPEN
if self.Time.time() == self.market_open_time:
self.check_close_on_open()
self.check_buy_on_open()
## MARKET CLOSE
elif self.Time.time() == self.market_close_time:
# self.Debug(f'{self.Time} market close time')
self.update_eod_data() # update last data and calculate EOD
self.check_closure_close_sma()
self.check_closure_eod()
self.check_long_eod()
self.eod_reset_data()
def check_closure_close_sma(self):
# self.Debug(f'{self.Time} checking closure hourly')
currInvested = [x.Symbol for x in self.Portfolio.Values if x.Invested]
for symbol in currInvested:
try:
symboldata = self.symbol_data[symbol]
sma_list = symboldata.rolling_sma_list
sma_length = len(sma_list)
sma_value = np.mean(sma_list)
low = symboldata.low
close = symboldata.close
# We will close the trade whenever the price is lower than the 200-day SMA;
if close < sma_value:
# self.Debug(f'{self.Time} !! closing symbol {symbol}, low {low} < sma {sma}')
self.Liquidate(symbol)
## DATASTORE
# Trades
data = {'time': self.Time, 'symbol': symbol.value, 'low': low, 'close': close, 'sma': sma_value, 'direction': 'liquidate'}
self.store_trades += f'{data},'
# Decision
data = {'time': self.Time, 'symbol': symbol.value, 'reason': 'closing_because_low<sma', 'low': low, 'close': close, 'sma': sma_value, 'sma_length': sma_length}
self.store_decisions += f'{data},'
except:
self.Debug(f'{self.Time} {symbol} couldnt close hourly as symbol data missing. Maybe delisted?')
def check_buy_on_open(self):
# self.Debug(f'{self.Time} checking long on open')
for symbol in self.buy_on_open_list:
## DATASTORE
data = {'time': self.Time, 'symbol': symbol.value}
self.store_longlist += f'{data},'
if symbol in self.symbol_data:
holding_percentage = (1 / self.number_of_stocks) * 0.9
# self.Debug(f'{symbol} setting holding for {holding_percentage}%')
self.set_holdings(symbol, holding_percentage)
## DATASTORE
symboldata = self.symbol_data[symbol]
sma_list = symboldata.rolling_sma_list
sma_value = np.mean(sma_list)
low = symboldata.low
close = symboldata.close
num_holdings = sum(x.Invested for x in self.Portfolio.Values)
data = {'time': self.Time, 'symbol': symbol.value, 'low': low, 'close': close, 'sma': sma_value, 'direction': 'long', 'amount': holding_percentage, 'num_holdings': num_holdings}
self.store_trades += f'{data},'
else:
data = {'time': self.Time, 'symbol': symbol.value, 'error': 'symbol in self.buyonopen list but not in symobl_data'}
self.errors += f'{data},'
self.buy_on_open_list = []
def check_close_on_open(self):
# self.Debug(f'{self.Time} checking close on open')
currInvested = [x.Symbol for x in self.Portfolio.Values if x.Invested]
for symbol in currInvested:
if symbol not in self.symbol_data:
# self.Debug(f'{self.Time} !! 350 {symbol} no longer found in symbol_data. CLosing.')
self.Liquidate(symbol)
## DATASTORE
# Trades
data = {'time': self.Time, 'symbol': symbol.value, 'direction': 'liquidate'}
self.store_trades += f'{data},'
# Decision
data = {'time': self.Time, 'symbol': symbol.value, 'reason': 'closing as dropped from symbol_data'}
self.store_decisions += f'{data},'
else:
symboldata = self.symbol_data[symbol]
if symboldata.close_on_open_trigger:
# self.Debug(f'{self.Time} !! {symbol} Closing on market open!')
self.Liquidate(symbol)
## DATASTORE
# Trades
data = {'time': self.Time, 'symbol': symbol.value, 'direction': 'liquidate'}
self.store_trades += f'{data},'
symboldata.close_on_open_trigger = False
def update_eod_data(self):
# self.Debug(f'{self.Time} running update_eod_data')
for symbol in self.symbol_data:
self.symbol_data[symbol].update_eod(self)
def check_closure_eod(self):
holdings_list = [str(symbol) for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested]
self.Debug(f"{self.Time} EOD: Securities Held: {holdings_list}")
## OBJECT SORE
data = {'time': self.Time, 'holdings': holdings_list}
self.store_holdings += f'{data},'
currInvested = [x.Symbol for x in self.Portfolio.Values if x.Invested]
for symbol in currInvested:
symboldata = self.symbol_data[symbol]
close = symboldata.close
yesterdays_high = symboldata.yesterdays_high
# When the stock closes above yesterday's high, we will exit on the next open;
if close > yesterdays_high:
# self.Debug(f'{self.Time} {symbol} will close on tomorrows open as close {close} > yesterday high {yesterdays_high}')
symboldata.close_on_open_trigger = True
sma_list = symboldata.rolling_sma_list
sma_length = len(sma_list)
sma_value = np.mean(sma_list)
# Decision
data = {'time': self.Time, 'symbol': symbol.value, 'reason': 'closing as close>yesterday_hight', 'yesterdays_high':yesterdays_high, 'close': close, 'sma': sma_value, 'sma_length': sma_length}
self.store_decisions += f'{data},'
def check_long_eod(self):
symbols_long_list = {}
num_holdings = sum(x.Invested for x in self.Portfolio.Values)
for symbol in self.symbol_data:
if not self.Portfolio[symbol].Invested:
symboldata = self.symbol_data[symbol]
sma_list = symboldata.rolling_sma_list
sma_length = len(sma_list)
# for i, value in enumerate(sma.Window):
# self.Debug(f"{i}: {value.Time}, {value.Value}")
# self.Debug(f"{self.Time} sma ready {sma.is_ready}")
close = symboldata.close
if sma_length == 200 and close > 10:
# self.Debug(f"{self.Time} {symbol} close: {symboldata.close} high: {symboldata.todays_high} low: {symboldata.todays_low} yesthigh: {symboldata.yesterdays_high} yest close: {symboldata.yesterdays_close} vol: {symboldata.todays_total_volume} sma: {sma.current.value} ")
lower_band = symboldata.lower_band
ibs = symboldata.ibs
natr = symboldata.natr
sma_value = np.mean(sma_list)
# Only trade the stock if the allocated capital for the trade does not exceed 5% of the stock's median ADV of the past 3 months
allocation_per_symbol = self.portfolio.total_portfolio_value / self.number_of_stocks
adv_median = np.median(symboldata.volumes_list)
if adv_median > allocation_per_symbol:
# closes under the lower band, and IBS is lower than 0.3, we go long at the next open
if close < lower_band and ibs < self.ibs_threshold and close > sma_value:
# self.Debug(f'{self.Time} {symbol} meets entry criteria: close {close} lower band {lower_band} ibs {ibs} sma {sma}. adv_median {adv_median} allocation per symbol {allocation_per_symbol}')
symbols_long_list[symbol] = natr
## DATASTORE
data = {'time': self.Time, 'symbol': symbol.value, 'lower_band': lower_band,'ibs':ibs,'sma':sma_value,'natr':natr, 'adv_median':adv_median,'close':close,'sma_length':sma_length, 'num_holdings': num_holdings}
self.store_longlist_entries += f'{data},'
# Sort them by volatility (Normalized Average True Range) and prioritize the most volatile ones;
symbols_long_list_sorted = sorted(symbols_long_list.items(), key=lambda x: x[1], reverse=True)
num_holdings = sum(x.Invested for x in self.Portfolio.Values)
number_of_available_stocks = self.number_of_stocks - num_holdings
top_symbols = [item[0] for item in symbols_long_list_sorted[:number_of_available_stocks]]
self.buy_on_open_list = top_symbols
# self.Debug(f'{self.Time} length of long symbols list {len(self.buy_on_open_list)}')
def eod_reset_data(self):
# self.Debug(f'{self.Time} running eod_reset_data')
for symbol in self.symbol_data:
self.symbol_data[symbol].eod_reset()
def on_securities_changed(self, changes: SecurityChanges) -> None:
for security in changes.added_securities:
if security.symbol == 'SPY':
continue
if security.symbol not in self.symbol_data:
self.symbol_data[security.symbol] = SymbolData(security.symbol, self)
for security in changes.removed_securities:
# self.debug(f"{self.time}: Universe: Removed {security.symbol}")
if security.symbol in self.symbol_data:
self.symbol_data.pop(security.symbol)
# self.Debug(f'{self.Time} - len of entire universe = {len(self.symbol_data)}')
def on_end_of_algorithm(self):
self.object_store.save('store_trades', self.store_trades)
self.object_store.save('store_decisions', self.store_decisions)
self.object_store.save('store_holdings', self.store_holdings)
self.object_store.save('store_longlist_entries', self.store_longlist_entries)
self.object_store.save('store_longlist', self.store_longlist)
self.object_store.save('errors', self.errors)