| Overall Statistics |
|
Total Orders 324 Average Win 0.74% Average Loss -0.04% Compounding Annual Return 19.699% Drawdown 26.600% Expectancy 18.953 Start Equity 100000 End Equity 604628.35 Net Profit 504.628% Sharpe Ratio 0.925 Sortino Ratio 1.017 Probabilistic Sharpe Ratio 60.289% Loss Rate 2% Win Rate 98% Profit-Loss Ratio 19.34 Alpha 0.058 Beta 0.654 Annual Standard Deviation 0.123 Annual Variance 0.015 Information Ratio 0.319 Tracking Error 0.091 Treynor Ratio 0.174 Total Fees $334.40 Estimated Strategy Capacity $930000.00 Lowest Capacity Asset IYW RUTTRZ1RC7L1 Portfolio Turnover 0.16% Drawdown Recovery 572 Avg. Lost% Per Losser -3.10% Avg. Win% Per Winner 83.29% Max Win% 286.13% Max Loss% -7.64% *Profit Ratio 1583.24 |
'''
Usage:
def Initialize(self):
self.log = Log(self)
# code xxxxxx
self.log.log("---->1")
'''
from AlgorithmImports import *
import time
class Log():
def __init__(self, algo):
self.timer = round(time.time() * 1000)
self.algo = algo
self.maxLine = 200
self.count = 0
self.debug(f"Live mode={self.algo.live_mode}.....Log Initialized")
def log(self, message):
self.algo.Log(f"[LOG] {message}")
def info(self, message):
now = round(time.time() * 1000)
timer = (now - self.timer) / 1000
self.timer = now
if (self.algo.Time <= self.algo.Time.replace(hour=9, minute=35)):
self.algo.Log(f"[INFO] {message}")
def debug(self, message):
if (self.count < self.maxLine or self.algo.live_mode):
self.algo.Log(f"[DEUBG] {message}")
self.count += 1
def live(self, message):
if self.algo.live_mode:
self.algo.Log(f"[DEUBG] {message}")
# ================================================================================
# REB01 - Rebalancing Strategy
# ================================================================================
# Author: Angus Li
# Date: January 14, 2026
#
# ⚠️ DISCLAIMER - FOR EDUCATIONAL & BACKTESTING PURPOSES ONLY ⚠️
#
# This code is provided for demonstration and educational purposes only.
# It is NOT intended for live trading without proper due diligence, risk
# management, and thorough testing in paper trading environments.
#
# Past performance does not guarantee future results. The author assumes
# NO LIABILITY for any financial losses, damages, or adverse outcomes
# resulting from the use of this code in live trading environments.
#
# By using this code, you acknowledge that:
# - You trade at your own risk
# - You are solely responsible for your trading decisions
# - The author is not a financial advisor
# - This is not financial advice
#
# Always consult with qualified financial professionals before making
# investment decisions.
# ================================================================================
# region imports
from AlgorithmImports import *
from log import Log
from security_initializer import CustomSecurityInitializer
from utils import Utils
# endregion
'''
Scope: Rebalance monthly strategy IYW & GLD - for substack
'''
# lean project-create --language python "REB01"
# lean cloud backtest "REB01" --push --open
class REB01(QCAlgorithm):
def initialize(self):
self.set_start_date(2016, 1, 1)
self.set_end_date(2025, 12, 31)
self.init_cash = 100000
self.set_cash(self.init_cash) # Set Strategy Cash
self._symbol = self.add_equity("SPY", Resolution.HOUR).Symbol
self.logging = Log(self)
self.utils = Utils(self, self._symbol)
self._stock = "IYW"
self._gold = "GLD"
self.assets = [self._stock, self._gold]
# 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.01
# Add Equity ------------------------------------------------
for ticker in self.assets:
self.add_equity(ticker, Resolution.HOUR).symbol
self.schedule.on(self.date_rules.week_start(self._symbol),
self.time_rules.after_market_open(self._symbol, 1),
self.every_day_before_market_close)
self.schedule.on(self.date_rules.every_day(), self.time_rules.before_market_close(self._symbol, 0), self.utils.plot)
def every_day_before_market_close(self):
self.set_holdings([PortfolioTarget(self._gold, 0.5), PortfolioTarget(self._stock, 0.5)])
def on_end_of_algorithm(self):
self.utils.stats()
# 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)}")