| Overall Statistics |
|
Total Orders 29 Average Win 94.62% Average Loss -9.69% Compounding Annual Return 43.903% Drawdown 78.800% Expectancy 6.692 Start Equity 100000 End Equity 33951419.57 Net Profit 33851.420% Sharpe Ratio 0.934 Sortino Ratio 1.093 Probabilistic Sharpe Ratio 20.411% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 9.77 Alpha 0.26 Beta 1.317 Annual Standard Deviation 0.401 Annual Variance 0.161 Information Ratio 0.806 Tracking Error 0.357 Treynor Ratio 0.284 Total Fees $6133.46 Estimated Strategy Capacity $25000000.00 Lowest Capacity Asset PLTR XIAKBH8EIMHX Portfolio Turnover 0.49% Drawdown Recovery 2130 Avg. Lost% Per Losser -9.54% Avg. Win% Per Winner 94.71% Max Win% 419.04% Max Loss% -30.45% *Profit Ratio 179.87 |
# ================================================================================
# DISCLAIMER
# ================================================================================
# This code is provided free of charge for EDUCATIONAL PURPOSES ONLY. Users are
# granted permission to study, modify, and redistribute this script for non-
# commercial learning and research.
#
# The author and developers of this code assume NO LIABILITY for any financial
# losses, trading damages, or missed opportunities resulting from the use or
# misuse of this algorithm. Quantitative trading involves significant risk,
# and past performance is not indicative of future results
#
# This code is provided "AS IS" without any warranties. The author does not
# provide technical support, bug fixes, or maintenance for this script.
# Users are responsible for their own due diligence and backtesting before
# considering any live deployment
from AlgorithmImports import *
from datetime import timedelta, datetime
from security_initializer import CustomSecurityInitializer
from utils import Utils
class ALLIN02(QCAlgorithm):
def initialize(self):
# Set backtest range and initial capital
self.set_start_date(2010, 1, 1)
self.set_end_date(2025, 12, 31)
self.init_cash = 100000
self.set_cash(self.init_cash) # Set Strategy Cash
# Universe settings: Resolution.HOUR is used for trading, though selection is annual
self.universe_settings.resolution = Resolution.HOUR
# Define the benchmark and the base ETF for constituent selection (SPY)
# Change The Equity e.g. QQQ, DIA, IWM
self._symbol = self.add_equity("SPY", Resolution.HOUR).Symbol
# Add a universe that tracks the constituents of the SPY ETF
self.add_universe_selection(ETFConstituentsUniverseSelectionModel(self._symbol, self.universe_settings, self.etf_constituents_filter))
# Alpha Model: Emits a daily "UP" insight for all symbols currently in the universe
self.add_alpha(NonBenchmarkConstantAlphaModel(self, InsightType.PRICE, InsightDirection.UP, timedelta(days=1)))
# Security Initializer: Seeds new securities with historical price data to prevent trade delays
self.set_security_initializer(CustomSecurityInitializer(InteractiveBrokersBrokerageModel(AccountType.CASH), FuncSecuritySeeder(self.get_last_known_prices)))
# Use Interactive Brokers brokerage model with a Cash account (no margin)
self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.CASH)
# Portfolio settings: Fully invest, rebalance when insights change, and set margin buffer
self.settings.free_portfolio_value_percentage = 0.00
self.settings.rebalance_portfolio_on_insight_changes = True
self.settings.minimum_order_margin_portfolio_percentage = 0.5
# Construction Model: Distributes portfolio value equally among all symbols with active insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Dictionary to cache the selected symbols per year to avoid redundant heavy calculations
self.picked = {}
self.utils = Utils(self, self._symbol)
self.schedule.on(self.date_rules.every_day(), self.time_rules.before_market_close(self._symbol, 0), self.utils.plot)
def etf_constituents_filter(self, constituents: List[ETFConstituentData]) -> List[Symbol]:
"""
Filters the ETF constituents to find the single best performing stock from the previous calendar year.
This runs every time the universe updates, but logic is gated to execute once per year.
"""
if self.time.year not in self.picked:
# Filter out constituents without weight and sort by weight (optional step for cleanliness)
selected = sorted([c for c in constituents if c.weight], key=lambda c: c.weight, reverse=True)
# Request daily historical data for the entire previous year for all ~500 SPY constituents
history = self.history([s.symbol for s in selected],
datetime(self.time.year-1, 1, 1),
datetime(self.time.year-1, 12, 31),
Resolution.DAILY)
# Reset index to make 'symbol' and 'time' accessible columns in the DataFrame
df = history.reset_index()
# Extract the year from the timestamp for grouping
df['year'] = df['time'].dt.year
# Group by symbol and year to find the 'first' open price and 'last' close price of the previous year
yearly_df = df.groupby(['symbol', 'year']).agg({
'open': 'first',
'close': 'last'
}).reset_index()
# Calculate the percentage price change: (Close - Open) / Open
yearly_df['change'] = (yearly_df['close'] - yearly_df['open']) / yearly_df['open']
# Select the ticker with the highest percentage change and store it in the 'picked' cache
# Returns a list containing the Symbol of the #1 top performer
self.picked[self.time.year] = [c for c in yearly_df.sort_values('change', ascending=False).head(1)['symbol'].values]
# Return the cached list of symbols for the current year to the universe selection model
return self.picked[self.time.year]
def on_end_of_algorithm(self):
self.utils.stats()
class NonBenchmarkConstantAlphaModel(AlphaModel):
''' Provides an implementation of IAlphaModel that always returns the same insight for each security'''
def __init__(self, algo, type, direction, period, magnitude = None, confidence = None, weight = None):
'''Initializes a new instance of the ConstantAlphaModel class
Args:
type: The type of insight
direction: The direction of the insight
period: The period over which the insight with come to fruition
magnitude: The predicted change in magnitude as a +- percentage
confidence: The confidence in the insight
weight: The portfolio weight of the insights'''
self.algo = algo
self.type = type
self.direction = direction
self.period = period
self.magnitude = magnitude
self.confidence = confidence
self.weight = weight
self.securities = []
self.insights_time_by_symbol = {}
self.Name = '{}({},{},{}'.format(self.__class__.__name__, type, direction, strfdelta(period))
if magnitude is not None:
self.Name += ',{}'.format(magnitude)
if confidence is not None:
self.Name += ',{}'.format(confidence)
self.Name += ')'
def update(self, algorithm, data):
''' Creates a constant insight for each security as specified via the constructor
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for security in self.securities:
# security price could be zero until we get the first data point. e.g. this could happen
# when adding both forex and equities, we will first get a forex data point
if security.price != 0 and self.should_emit_insight(algorithm.utc_time, security.symbol) and security.symbol != self.algo._symbol:
insights.append(Insight(security.symbol, self.period, self.type, self.direction, self.magnitude, self.confidence, weight = self.weight))
return insights
def on_securities_changed(self, algorithm, changes):
''' Event fired each time the we add/remove securities from the data feed
Args:
algorithm: The algorithm instance that experienced the change in securities
changes: The security additions and removals from the algorithm'''
for added in changes.added_securities:
self.securities.append(added)
# this will allow the insight to be re-sent when the security re-joins the universe
for removed in changes.removed_securities:
if removed in self.securities:
self.securities.remove(removed)
if removed.symbol in self.insights_time_by_symbol:
self.insights_time_by_symbol.pop(removed.symbol)
def should_emit_insight(self, utc_time, symbol):
if symbol.is_canonical():
# canonical futures & options are none tradable
return False
generated_time_utc = self.insights_time_by_symbol.get(symbol)
if generated_time_utc is not None:
# we previously emitted a insight for this symbol, check it's period to see
# if we should emit another insight
if utc_time - generated_time_utc < self.period:
return False
# we either haven't emitted a insight for this symbol or the previous
# insight's period has expired, so emit a new insight now for this symbol
self.insights_time_by_symbol[symbol] = utc_time
return True
def strfdelta(tdelta):
d = tdelta.days
h, rem = divmod(tdelta.seconds, 3600)
m, s = divmod(rem, 60)
return "{}.{:02d}:{:02d}:{:02d}".format(d,h,m,s)# region imports
from AlgorithmImports import *
# endregion
class CustomSecurityInitializer(BrokerageModelSecurityInitializer):
def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None:
super().__init__(brokerage_model, security_seeder)
def initialize(self, security: Security) -> None:
super().initialize(security)
security.set_slippage_model(VolumeShareSlippageModel())
security.set_settlement_model(ImmediateSettlementModel())
security.set_leverage(1.0)
security.set_buying_power_model(CashBuyingPowerModel())
security.set_fee_model(InteractiveBrokersFeeModel())
security.set_margin_model(SecurityMarginModel.NULL)
from AlgorithmImports import *
from Newtonsoft.Json import JsonConvert
import System
import psutil
class Utils():
def __init__(self, algo, ticker):
self.algo = algo
self.ticker = ticker
self.mkt = []
self.insights_key = f"{self.algo.project_id}/Live_{self.algo.live_mode}_insights"
self.algo.set_benchmark(ticker)
self._initial_portfolio_value = self.algo.init_cash
self._initial_benchmark_price = 0
self._portfolio_high_watermark = 0
self.init_chart()
def init_chart(self):
chart_name = "Strategy Performance"
chart = Chart(chart_name)
strategy_series = Series("Strategy", SeriesType.LINE, 0, "$")
strategy_series.color = Color.ORANGE
chart.add_series(strategy_series)
benchmark_series = Series("Benchmark", SeriesType.LINE, 0, "$")
benchmark_series.color = Color.LIGHT_GRAY
chart.add_series(benchmark_series)
drawdown_series = Series("Drawdown", SeriesType.LINE, 1, "%")
drawdown_series.color = Color.INDIAN_RED
chart.add_series(drawdown_series)
allocation_series = Series("Allocation", SeriesType.LINE, 2, "%")
allocation_series.color = Color.CORNFLOWER_BLUE
chart.add_series(allocation_series)
holding_series = Series("Holdings", SeriesType.LINE, 3, "")
holding_series.color = Color.YELLOW_GREEN
chart.add_series(holding_series)
self.algo.add_chart(chart)
def plot(self):
if self.algo.live_mode or self.algo.is_warming_up:
return
# Capture initial reference values
if self._initial_portfolio_value == 0.0:
self._initial_portfolio_value = float(self.algo.portfolio.total_portfolio_value)
benchmark_price = float(self.algo.securities[self.algo._symbol].price)
if self._initial_benchmark_price == 0.0 and benchmark_price > 0.0:
self._initial_benchmark_price = benchmark_price
# Ensure both initial values are set
if self._initial_portfolio_value == 0.0 or self._initial_benchmark_price == 0.0:
return
# Current values
current_portfolio_value = float(self.algo.portfolio.total_portfolio_value)
# Defensive check (avoid division by zero)
if self._initial_portfolio_value == 0.0 or self._initial_benchmark_price == 0.0:
return
# Normalize (start at 1.0)
normalized_portfolio = current_portfolio_value / self._initial_portfolio_value
normalized_benchmark = benchmark_price / self._initial_benchmark_price
current_value = self.algo.portfolio.total_portfolio_value
if current_value > self._portfolio_high_watermark:
self._portfolio_high_watermark = current_value
drawdown = 0.0
if self._portfolio_high_watermark != 0.0:
drawdown = (current_value - self._portfolio_high_watermark) / self._portfolio_high_watermark * 100.0
holding_count = 0
for symbol in list(self.algo.securities.keys()):
if symbol is None:
continue
holding = self.algo.portfolio[symbol]
if holding is None or not holding.invested:
continue
holding_count += 1
chart_name = "Strategy Performance"
self.algo.plot(chart_name, "Drawdown", drawdown)
self.algo.plot(chart_name, "Strategy", normalized_portfolio*self.algo.init_cash)
self.algo.plot(chart_name, "Benchmark", normalized_benchmark*self.algo.init_cash)
self.algo.plot(chart_name, "Allocation", round(self.algo.portfolio.total_holdings_value / self.algo.portfolio.total_portfolio_value,2)*100)
self.algo.plot(chart_name, "Holdings", holding_count)
self.algo.plot('Strategy Equity', self.ticker, normalized_benchmark*self.algo.init_cash)
def pctc(no1, no2):
return((float(str(no2))-float(str(no1)))/float(str(no1)))
def stats(self):
df = None
trades = self.algo.trade_builder.closed_trades
for trade in trades:
data = {
'symbol': trade.symbol,
'time': trade.entry_time,
'entry_price': trade.entry_price,
'exit_price': trade.exit_price,
'pnl': trade.profit_loss,
'pnl_pct': (trade.exit_price - trade.entry_price)/trade.entry_price,
}
df = pd.concat([pd.DataFrame(data=data, index=[0]), df])
if df is not None:
profit = df.query('pnl >= 0')['pnl'].sum()
loss = df.query('pnl < 0')['pnl'].sum()
avgWinPercentPerWin = "{0:.2%}".format(df.query('pnl >= 0')['pnl_pct'].mean())
avgLostPercentPerLost = "{0:.2%}".format(df.query('pnl < 0')['pnl_pct'].mean())
maxLost = "{0:.2%}".format(df.query('pnl < 0')['pnl_pct'].min())
maxWin = "{0:.2%}".format(df.query('pnl > 0')['pnl_pct'].max())
self.algo.set_summary_statistic("*Profit Ratio", round(profit / abs(loss),2))
self.algo.set_summary_statistic("Avg. Win% Per Winner", avgWinPercentPerWin)
self.algo.set_summary_statistic("Avg. Lost% Per Losser", avgLostPercentPerLost)
self.algo.set_summary_statistic("Max Loss%", maxLost)
self.algo.set_summary_statistic("Max Win%", maxWin)
def read_insight(self):
if self.algo.object_store.contains_key(self.insights_key) and self.algo.live_mode:
insights = self.algo.object_store.read_json[System.Collections.Generic.List[Insight]](self.insights_key)
self.algo.log.debug(f"Read {len(insights)} insight(s) from the Object Store")
self.algo.insights.add_range(insights)
#self.algo.object_store.delete(self.insights_key)
def store_insight(self):
if self.algo.live_mode:
insights = self.algo.insights.get_insights(lambda x: x.is_active(self.algo.utc_time))
# If we want to save all insights (expired and active), we can use
# insights = self.insights.get_insights(lambda x: True)
self.algo.log.debug(f"Save {len(insights)} insight(s) to the Object Store.")
content = ','.join([JsonConvert.SerializeObject(x) for x in insights])
self.algo.object_store.save(self.insights_key, f'[{content}]')
def trace_memory(self, name):
self.algo.log.debug(f"[{name}] RAM memory % used: {psutil.virtual_memory()[2]} / RAM Used (GB): {round(psutil.virtual_memory()[3]/1000000000,2)}")