Overall Statistics
Total Orders
1093
Average Win
0.39%
Average Loss
-0.22%
Compounding Annual Return
16.505%
Drawdown
22.700%
Expectancy
1.171
Start Equity
100000
End Equity
461246.56
Net Profit
361.247%
Sharpe Ratio
0.836
Sortino Ratio
0.899
Probabilistic Sharpe Ratio
53.287%
Loss Rate
21%
Win Rate
79%
Profit-Loss Ratio
1.75
Alpha
0.046
Beta
0.528
Annual Standard Deviation
0.109
Annual Variance
0.012
Information Ratio
0.06
Tracking Error
0.103
Treynor Ratio
0.173
Total Fees
$1172.03
Estimated Strategy Capacity
$1300000.00
Lowest Capacity Asset
VDE T2FCD04TATET
Portfolio Turnover
1.25%
Drawdown Recovery
511
Avg. Lost% Per Losser
-7.69%
Avg. Win% Per Winner
13.01%
Max Win%
78.23%
Max Loss%
-45.72%
*Profit Ratio
5.48
'''
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}")
# ================================================================================
# REB02 - Optimization portfolio - for substack
# ================================================================================
# Author: Angus Li
# 
# ⚠️ 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
from scipy.optimize import *
from Portfolio.MaximumSharpeRatioPortfolioOptimizer import MaximumSharpeRatioPortfolioOptimizer
from Portfolio.MinimumVariancePortfolioOptimizer import MinimumVariancePortfolioOptimizer
from Portfolio.RiskParityPortfolioOptimizer import RiskParityPortfolioOptimizer
# endregion


# lean project-create --language python "REB02"
# lean cloud backtest "REB02" --push --open
class REB02(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.minimum_weight = 0
        self.maximum_weight = 1

        self.logging = Log(self)
        self.utils = Utils(self, self._symbol)

        # 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


        # Define sector ETF symbols
        self._tickers = ['XLK','GLD','VDE']
        self._symbols = [self.add_equity(ticker, Resolution.HOUR).symbol for ticker in self._tickers]
        
        # Schedule weekly rebalancing at market open every Monday
        self.schedule.on(self.date_rules.week_start(),
                        self.time_rules.after_market_open(self._symbols[0], 5),
                        self._rebalance)
        
        # MinimumVariancePortfolioOptimizer - Optimize portfolio to minimize variance
        #self.optimizer = MinimumVariancePortfolioOptimizer(minimum_weight = self.minimum_weight, maximum_weight = self.maximum_weight)

        # MaximumSharpeRatioPortfolioOptimizer - Optimize portfolio to maximize Sharpe Ratio
        #self.optimizer = MaximumSharpeRatioPortfolioOptimizer(minimum_weight = self.minimum_weight, maximum_weight = self.maximum_weight)

        # RiskParityPortfolioOptimizer - Optimize portfolio for risk parity allocation
        self.optimizer = RiskParityPortfolioOptimizer(minimum_weight = self.minimum_weight, maximum_weight = self.maximum_weight)

        # If you want to disable optimization and use equal weights, uncomment the line below
        #self.optimizer = None

        self.schedule.on(self.date_rules.every_day(), self.time_rules.before_market_close(self._symbol, 0), self.utils.plot)

    def _rebalance(self):
        """Rebalance portfolio using risk budgeting optimization"""
        # Get historical prices for optimization (22 days lookback)
        history = self.history(self._symbols, 22, Resolution.DAILY)
        
        if history.empty:
            self.logging.debug("History is empty, skipping rebalance")
            return
        
        # Build returns dataframe with proper alignment
        returns_dict = {}
        for symbol in self._symbols:
            if symbol in history.index.get_level_values(0):
                prices = history.loc[symbol]['close']
                returns_dict[symbol] = prices.pct_change()
        
        if len(returns_dict) != len(self._symbols):
            self.logging.debug(f"Missing data for some symbols, skipping rebalance")
            return
        
        # Create aligned returns dataframe
        returns_df = pd.DataFrame(returns_dict).dropna()
        
        if len(returns_df) < 20:
            self.logging.debug("Insufficient return data, skipping rebalance")
            return
        
        try:
            if self.optimizer is None:
                # Create portfolio targets
                targets = [PortfolioTarget(symbol, 1/len(self._tickers)) for symbol in self._tickers]
            else:
                # Use risk budgeting optimization instead of minimum variance
                weights = self.optimizer.optimize(returns_df)
                
                # Map weights to symbols (using column order from dataframe)
                weight_dict = {symbol: float(weight) for symbol, weight in zip(returns_df.columns, weights)}
                
                # Create portfolio targets
                targets = [PortfolioTarget(symbol, weight) for symbol, weight in weight_dict.items()]
                
            # Rebalance portfolio
            self.set_holdings(targets, liquidate_existing_holdings=True)
            
        except Exception as e:
            self.logging.debug(f"Failed to optimize portfolio: {str(e)}")
            return
        

    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)}")