| 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
'''