Overall Statistics
Total Orders
1699
Average Win
1.36%
Average Loss
-1.29%
Compounding Annual Return
11.534%
Drawdown
60.300%
Expectancy
0.307
Start Equity
100000
End Equity
1520227.52
Net Profit
1420.228%
Sharpe Ratio
0.381
Sortino Ratio
0.42
Probabilistic Sharpe Ratio
0.064%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
1.06
Alpha
0.035
Beta
1.026
Annual Standard Deviation
0.21
Annual Variance
0.044
Information Ratio
0.272
Tracking Error
0.133
Treynor Ratio
0.078
Total Fees
$6932.76
Estimated Strategy Capacity
$160000.00
Lowest Capacity Asset
MMMB XQ652EL0JWPX
Portfolio Turnover
0.81%
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
import datetime
from datetime import timedelta, date

''' 
GOING PITBULL 
This is a value + momentum strategy. 
we find the best prices in the same way of warren buffet, and then we find the best momentum stocks using the sharpe ratio.

Need to add a check for extra cash to be added to the portfolio.

            symbol_fundamentals = self.fundamentals_values_dict[symbol]
            pe = symbol_fundamentals[0]
            fcfps = symbol_fundamentals[1]
            eps_growth = symbol_fundamentals[2]
            eg = symbol_fundamentals[3]
            analysts_eps = symbol_fundamentals[4]
            price = symbol_fundamentals[5]
            current_fair_price = symbol_fundamentals[6]
            price_percent = symbol_fundamentals[7]
            sharpe_ratio = symbol_fundamentals[8]



'''

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.AddUniverse(self.SelectCoarse, self.SelectFine)

        ###   VARIABLES   ###
        self.fundamentals_rank = []             # list of tuples (symbol, rank)
        self.fundamentals_values_dict = {}
        self.shares_in_portfolio = 12

        ### DATES ###
        self.trading_day = (self.Time).date()                           # every time initialize is called, trading_day is set to the current day
        self.manual_trading_date = date.fromisoformat('2023-07-03')     # use YYYY-MM-DD format


    def SelectCoarse(self, coarse):

        open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested]
        if not open_positions or self.manual_trading_date == (self.Time).date():
            self.trading_day = (self.Time).date()

        if (self.Time).date() != self.trading_day:
            return Universe.Unchanged

        self.fundamentals_rank.clear()
        self.fundamentals_values_dict.clear()
        volume = self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 500_000 else 500_000
        selected = [x for x in coarse if x.HasFundamentalData
                                    and x.Price < self.Portfolio.TotalPortfolioValue / 200
                                    and (self.Time - x.Symbol.ID.Date).days > 365
                                    and x.DollarVolume > volume]

        sortedByDollarVolume = sorted(selected, key=lambda x: x.DollarVolume, reverse=False)
        return [c.Symbol for c in sortedByDollarVolume]

        
    def SelectFine(self, fine):
        
        prefilter = [x for x in fine]
        self.Log(f'there are {len(prefilter)} companies left BEFORE the fine filter')
                
        filtered = [x for x in fine if x.CompanyReference.CountryId in {"USA"}
                                        and x.CompanyReference.PrimaryExchangeID in {"NYS", "NAS"}
                                        and (self.Time - x.SecurityReference.IPODate).days > 365
                                        and x.AssetClassification.MorningstarSectorCode != 103 #filter out financials
                                        and x.AssetClassification.MorningstarSectorCode != 207 #filter out utilities according to joel greenblatt
                                        and x.ValuationRatios.PERatio >= 5  
                                    ]
        
        for fine in filtered:
            symbol = fine.Symbol
            pe = fine.ValuationRatios.NormalizedPERatio
            fcfps = (fine.ValuationRatios.CFOPerShare + (fine.FinancialStatements.IncomeStatement.EBIT.TwelveMonths /  fine.CompanyProfile.SharesOutstanding))/2 if fine.CompanyProfile.SharesOutstanding > 0 else fine.ValuationRatios.CFOPerShare
            eps_growth = (fine.EarningRatios.NormalizedDilutedEPSGrowth.FiveYears)
            eg = (fine.EarningRatios.EquityPerShareGrowth.FiveYears)
            analysts_eps = (fine.ValuationRatios.first_year_estimated_eps_growth) if eps_growth < 0 and eg < 0 else 0
            fundamentals_values = [pe, fcfps, eps_growth, eg, analysts_eps]
            self.fundamentals_values_dict[symbol] = fundamentals_values

        self.fundamentals_rank.clear()
        #list the keys of self.fundamentals_values_dict
        sorted1 = sorted(filtered, key=lambda x: self.find_buyable_price(x.Symbol) , reverse=False)
        sorted2 = sorted(filtered, key=lambda x: self.calc_sharpe_ratio(x.Symbol) , reverse=True)

        stockBySymbol = {}
        max_rank_allowed = 40
        while len(stockBySymbol) < self.shares_in_portfolio:
            stockBySymbol.clear()

            for index, stock in enumerate(sorted1):
                rank1 = index
                rank2 = sorted2.index(stock)
                avgRank = (rank1 + rank2) /2

                if rank1 < max_rank_allowed and rank2 < max_rank_allowed*2:
                    stockBySymbol[stock.Symbol] = avgRank
            max_rank_allowed += 1
        self.Log(f'there are {len(filtered)} companies left AFTER the fine filter and max rank allowed is {max_rank_allowed}')
        
        self.fundamentals_rank = sorted(stockBySymbol.items(), key = lambda x: x[1], reverse = False) #list of tuples (symbol, rank)
        self.fundamentals_rank = self.fundamentals_rank[:self.shares_in_portfolio]
        self.Log(f'there are {len(self.fundamentals_rank)} companies left AFTER the rank filter')
        # for (symbol, rank) in self.fundamentals_rank:
        #     symbol_fundamentals = self.fundamentals_values_dict[symbol]
        #     price = symbol_fundamentals[5] if symbol_fundamentals[5] else 0
        #     self.Log(f'{symbol} has rank {rank}, pe: {symbol_fundamentals[0]}, fcfps: {symbol_fundamentals[1]}, eps_growth: {symbol_fundamentals[2]}, eg: {symbol_fundamentals[3]}, analysts_eps: {symbol_fundamentals[4]}, price: {price}')
        
        symbols = [x[0] for x in self.fundamentals_rank]

        return symbols


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

        if self.Time.date() != self.trading_day:
            self.SelectTradingDay()
            return

        self.liquidations()

        self.PrintIntentions()
        
        self.ExecuteBuyOrders()
        

        #######################################
        ### Strategy Calculations           ###
        #######################################


    def calc_sharpe_ratio(self, symbol):

        '''Calculate the sharpe ratio of a stock for the last year , daily resolution, and exluding the last month'''
        history = self.History(symbol, 365, Resolution.Daily)

        if 'close' not in history or history['close'].empty:
            return -1

        returns = history['close'].pct_change()
        returns = returns.dropna()
        returns = returns[:-21]
        sharpe_ratio = returns.mean() / returns.std()
        self.fundamentals_values_dict[symbol].append(sharpe_ratio)
        return sharpe_ratio


    def find_buyable_price(self, symbol):

        '''Find the buyable price of each stock and returns in what % of the price it is
         -0.5 of the evaluated price is the maximum price we can buy'''
        symbol_fundamentals = self.fundamentals_values_dict[symbol]
        pe = symbol_fundamentals[0]
        fcfps = symbol_fundamentals[1]
        eps_growth = symbol_fundamentals[2]
        eg = symbol_fundamentals[3]
        analysts_eps = symbol_fundamentals[4]

        eps = (fcfps)
        growth_ratio = (eps_growth + eg + analysts_eps )/3
        # growth_ratio =  analysts_eps if eps_growth < 0 and eg < 0 else (eps_growth + eg)

        earnings_dict = {0: eps}
        # calculate earnings for 10 year
        for i in range(1,10):
            j = i - 1
            earnings_dict[i] = round(earnings_dict[j]+(earnings_dict[j] * growth_ratio),2)

        fair_price_dict = {9: earnings_dict[9]*pe}
        # discount 15% for 10 years
        for i in range(8,-1,-1):    
            j = i + 1
            fair_price_dict[i] = fair_price_dict[j]/(1+0.15)

        current_fair_price = round(fair_price_dict[0],2)

        history = self.History(symbol, 3, Resolution.Minute)   

        if 'close' not in history or history['close'].empty:
            price = 999_999_999
            current_fair_price = 1
            price_percent = 100  
            self.fundamentals_values_dict[symbol].append(price)
            self.fundamentals_values_dict[symbol].append(current_fair_price)      
            self.fundamentals_values_dict[symbol].append(price_percent)     
            return 100
        
        price = history['close']
        price = price.dropna()
        price = history['close'].iloc[-1]
        self.fundamentals_values_dict[symbol].append(price) # self.fundamentals_values_dict
        self.fundamentals_values_dict[symbol].append(current_fair_price) # self.fundamentals_values_dict

        #price = self.Securities[symbol].Price
        if current_fair_price > 0 and price > 0:
                price_percent = round(((price / current_fair_price)- 1) , 2)  
                self.fundamentals_values_dict[symbol].append(price_percent) # self.fundamentals_values_dict
                return price_percent
        
        price_percent = 100        
        self.fundamentals_values_dict[symbol].append(price_percent)        
        return price_percent
    
    
        #######################################
        ### Portfolio Operations            ###
        #######################################
    

    def ExecuteBuyOrders(self):

        buy_list = [x[0] for x in self.fundamentals_rank]
        holding = 1/round(len(buy_list)*0.98,3)

        for symbol in buy_list:
            self.SetHoldings(symbol, holding)


    def liquidations(self):

        open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested]
        sell_list = [symbol for symbol in open_positions if symbol not in [x[0] for x in self.fundamentals_rank]]
        if len(sell_list) == 0:
            return

        #if a symbol is in open_positions but not in self.fundamentals_rank, liquidate
        for  symbol in sell_list:
            self.Log(f'SELLING {symbol.Value}')
            self.Liquidate(symbol)


        #######################################
        ### Logistics                       ###
        #######################################    


    
    def PrintIntentions(self):
        
        symbols = [x[0] for x in self.fundamentals_rank]
        stock_allocation = self.Portfolio.Cash / self.shares_in_portfolio
        self.Log(f'free cash in portfolio: {self.Portfolio.Cash}')
        self.Log(f"we're allocating {stock_allocation} for each stock")
        for symbol in symbols:
            symbol_fundamentals = self.fundamentals_values_dict[symbol]
            updated_symbol = symbol.Value
            pe = round(symbol_fundamentals[0],2)
            fcfps = round(symbol_fundamentals[1],2)
            eps_growth = round(symbol_fundamentals[2],2)
            eg = round(symbol_fundamentals[3],2)
            analysts_eps = round(symbol_fundamentals[4],2)
            price = round(symbol_fundamentals[5],2)
            current_fair_price = round(symbol_fundamentals[6],2)
            price_percent = round(symbol_fundamentals[7],2)
            sharpe_ratio = round(symbol_fundamentals[8],2)
            shares_amount = stock_allocation // price
            self.Log(f'For {updated_symbol}, buy {shares_amount} at {price}')
            self.Log(f'{updated_symbol}, has a current fair price of {current_fair_price} so the profit of margin is {price_percent} (Best: -1)')
            self.Log(f'VALUE DATA: PE Ratio: {pe}, fcfps: {fcfps}, eps_growth: {eps_growth}, Equity Growth: {eg}, Analyst Growth: { analysts_eps}')
            self.Log(f'Sharpe Ratio: {sharpe_ratio}') 
            self.Log(f'------')

    
    def SelectTradingDay(self):

        # buys last day of training of the year --> check for connection and set alarm!
        #buying once a year has vastly outperformed shorter periods (6 and 4) both in gains and in consistency over 22 years of backtest 
        quarter_last_month = (self.Time.month - 1) // 6 * 6 + 6  
        quarter_last_day = DateTime(self.Time.year, quarter_last_month, DateTime.DaysInMonth(self.Time.year, quarter_last_month))
        
        # Get the trading days within the current quarter
        trading_days = self.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, self.Time, quarter_last_day)
        
        # Find the last trading day of the quarter
        for x in trading_days:
            day = x.Date.date()
            if day.weekday() == 0 or day.weekday() == 1:
                continue
            self.trading_day = day




'''
This code has been updated on 27-12-2023 to include the following changes:
- the stock size calcultion is now based on the free cash in the portfolio and not on the total portfolio value

'''