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