Overall Statistics
Total Orders
5213
Average Win
0.40%
Average Loss
-0.38%
Compounding Annual Return
10.639%
Drawdown
61.800%
Expectancy
0.263
Start Equity
100000
End Equity
1350182.95
Net Profit
1250.183%
Sharpe Ratio
0.385
Sortino Ratio
0.363
Probabilistic Sharpe Ratio
0.153%
Loss Rate
39%
Win Rate
61%
Profit-Loss Ratio
1.05
Alpha
0.03
Beta
0.774
Annual Standard Deviation
0.167
Annual Variance
0.028
Information Ratio
0.167
Tracking Error
0.118
Treynor Ratio
0.083
Total Fees
$56838.54
Estimated Strategy Capacity
$0
Lowest Capacity Asset
TECTP X4V3TKMIY6P1
Portfolio Turnover
1.09%
Drawdown Recovery
1431
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
import datetime
from datetime import timedelta, date
import random



class BuildingMagic(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)    # Set Start Date
        #self.SetEndDate(2011, 12, 31)      # Set End Date
        self.SetCash(100_000)             # Set Cash
        self.spy = self.AddEquity("SPY", Resolution.Hour).Symbol
        self.SetBenchmark(self.spy)

        self.UniverseSettings.Resolution = Resolution.Hour
        self.UniverseSettings.Leverage = 1
        self._universe = self.add_universe(self._fundamental_function)

        ###   VARIABLES   ###
        self.shares_in_portfolio = 50
        self.max_perc = 100
        self.min_perc = self.max_perc -10

        self.buy_list = []
        self.you_need_to_trade = False
        
        self.traded_days = []
        self.decile_upper_limits = []

        ### DATES ###
        self.trading_dates()
                     


    def _fundamental_function(self, fundamental: List[Fundamental]) -> List[Symbol]:

        if not self.portfolio.invested:
            self.you_need_to_trade = True

        today = self.Time.date()
        for index, trading_day in enumerate(self.trading_days):
            if trading_day <= today:
                self.you_need_to_trade = True
                #delete the item from the list
                del self.trading_days[index]

        open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested]
        if len(open_positions) < self.shares_in_portfolio * 0.8:
            self.you_need_to_trade = True

        if not self.you_need_to_trade:
            return Universe.Unchanged

        self.liquidate()
        self.buy_list.clear()
        
        filtered = [x for x in fundamental 
                        #if x.CompanyReference.country_id == "USA"
                        ]
    

        ### RANDOMIZE ###
        self.buy_list = self.values_between_percentiles(filtered, self.min_perc, self.max_perc) 
         

        self.traded_days.append(self.Time.date())
        
        if self.Time.date() > date(2024, 1, 1):
            blu = 0

        return [x.Symbol for x in self.buy_list]

        

    def OnData(self, slice: Slice) -> None:

        if not self.you_need_to_trade:
            return
        
        if len(self.buy_list) < self.shares_in_portfolio /2:
            return

        if self.Securities[self.spy].Exchange.ExchangeOpen:
            self.sell_and_buy(self.buy_list)   

        if  self.portfolio.invested:
            self.buy_list.clear()
            self.you_need_to_trade = False




    def values_between_percentiles(self, fundamentals_list, min_percentile, max_percentile):
        import numpy as np

        working_list = []
        for x in fundamentals_list:
            ex_value = x.ValuationRatios.trailing_dividend_yield
            earnings = 0
            if ex_value == 0 or np.isnan(ex_value):
                continue
            working_list.append([x, ex_value])
        working_list.sort(key=lambda item: item[1], reverse=True)  # descending order

        if not 0 <= min_percentile <= 100 or not 0 <= max_percentile <= 100:
            raise ValueError("Percentiles must be between 0 and 100")
        if min_percentile > max_percentile:
            raise ValueError("min_percentile should be less than or equal to max_percentile")
        if not working_list:
            return []

        market_caps = [item[1] for item in working_list]  # extract numeric values
        min_val = np.percentile(market_caps, min_percentile)
        max_val = np.percentile(market_caps, max_percentile)

        self.decile_upper_limits.append((min_val,max_val, self.Time.date()))
        if self.Time.date() > date(2025, 1, 1):
            upper_limit_average = sum([x[1] for x in self.decile_upper_limits]) / len(self.decile_upper_limits)
            self.log(f"Average PB for decile {self.max_perc}: {upper_limit_average}")
            

        # Filter fundamentals whose market_cap falls between min_val and max_val inclusive
        result = [item[0] for item in working_list if min_val <= item[1] <= max_val]
        
        if len(result) > self.shares_in_portfolio*2:
            return random.sample(result, self.shares_in_portfolio*2)

        return result


    def get_random_fundamentals(self, fundamentals_list, count):
        # Make sure count doesn't exceed the list length
        count = min(count, len(fundamentals_list))
        return random.sample(fundamentals_list, count)


    def sell_and_buy(self, buy_list):
        
        self.liquidate()
        
        if len(buy_list) == 0:
            return
        holding = round((1/self.shares_in_portfolio),3)
        for symbol in buy_list:
            if  self.securities[symbol.symbol].is_tradable:
                self.SetHoldings(symbol.symbol, holding)
                open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested]
                if len(open_positions) >= self.shares_in_portfolio:
                    break

        open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested]


    def trading_dates(self):

        self.trading_days = [
            date(2000, 1, 1),

            date(2000, 6, 30),  # end-of-June
            date(2000, 12, 29), # end-of-December (31st is Sunday)
            # 2001
            date(2001, 6, 29),  # (June 30 is Saturday)
            date(2001, 12, 31),
            # 2002
            date(2002, 6, 28),  # (June 30 is Sunday)
            date(2002, 12, 31),
            # 2003
            date(2003, 6, 30),
            date(2003, 12, 31),
            # 2004
            date(2004, 6, 30),
            date(2004, 12, 31),
            # 2005
            date(2005, 6, 30),
            date(2005, 12, 30), # (31st is Saturday)
            # 2006
            date(2006, 6, 30),
            date(2006, 12, 29), # (31st is Sunday)
            # 2007
            date(2007, 6, 29),  # (30th is Saturday)
            date(2007, 12, 31),
            # 2008
            date(2008, 6, 30),
            date(2008, 12, 31),
            # 2009
            date(2009, 6, 30),
            date(2009, 12, 31),
            # 2010
            date(2010, 6, 30),
            date(2010, 12, 31),
            # 2011
            date(2011, 6, 30),
            date(2011, 12, 30), # (31st is Saturday)
            # 2012
            date(2012, 6, 29),  # (30th is Saturday)
            date(2012, 12, 31),
            # 2013
            date(2013, 6, 28),  # (30th is Sunday)
            date(2013, 12, 31),
            # 2014
            date(2014, 6, 30),
            date(2014, 12, 31),
            # 2015
            date(2015, 6, 30),
            date(2015, 12, 31),
            # 2016
            date(2016, 6, 30),
            date(2016, 12, 30), # (31st is Saturday)
            # 2017
            date(2017, 6, 30),
            date(2017, 12, 29), # (31st is Sunday)
            # 2018
            date(2018, 6, 29),  # (30th is Saturday)
            date(2018, 12, 31),
            # 2019
            date(2019, 6, 28),  # (30th is Sunday)
            date(2019, 12, 31),
            # 2020
            date(2020, 6, 30),
            date(2020, 12, 31),
            # 2021
            date(2021, 6, 30),
            date(2021, 12, 31),
            # 2022
            date(2022, 6, 30),
            date(2022, 12, 30), # (31st is Saturday)
            # 2023
            date(2023, 6, 30),
            date(2023, 12, 29), # (31st is Sunday)
            # 2024
            date(2024, 6, 28),  # (30th is Sunday)
            date(2024, 12, 31),
            # 2025
            date(2025, 6, 30),
            date(2025, 12, 31),
            # 2026
            date(2026, 6, 30),
            date(2026, 12, 31),
        ]