| Overall Statistics |
|
Total Trades 171 Average Win 7.14% Average Loss -3.60% Compounding Annual Return 18.330% Drawdown 23.000% Expectancy 1.141 Net Profit 2615.708% Sharpe Ratio 0.938 Probabilistic Sharpe Ratio 26.307% Loss Rate 28% Win Rate 72% Profit-Loss Ratio 1.98 Alpha 0.117 Beta 0.231 Annual Standard Deviation 0.144 Annual Variance 0.021 Information Ratio 0.312 Tracking Error 0.184 Treynor Ratio 0.585 Total Fees $791.81 Estimated Strategy Capacity $0 Lowest Capacity Asset QQQ.CustomTicker 2S Portfolio Turnover 2.38% |
# region imports
from AlgorithmImports import *
from pandas.core.frame import DataFrame
# endregion
class TacticalAllocation(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2004, 1, 1) # VOO Inception Date Sep 07, 2010
self.init_cash:float = 10000.
self.SetCash(self.init_cash)
# one drive file hash - not used at the moment
ticker_hash:Dict[str, str] = {
'MDY' : '1KCeGIxxtIOSPx0ygnmouwv6f_XZyCLVvlCQsUvQkVjU',
'EFA' : '1QJsulsdxY4zjxXXCzXpTzrqrIk3rFaq6NCwe_Qo6xkg',
'TLT' : '185hK7chN1e25_phYL47vjuSusnBy4cweUxZhZ3BIDlo',
'QQQ' : '1iqBsgueshFKwYxxeKf5kSmXen-OWlynjyiPFPsoP_dY',
'FEMKX' : '1uLMoCAGxzcvEufsy50xeeKL2vdXksKdBb8MfbVfFgUw',
}
self.traded_symbols:List[Symbol] = []
# self.AddRiskManagement(MaximumDrawdownPercentPortfolio(-0.10))
self.ma_period:int = 8
self.momentum_period:int = 4
self.SetWarmup(max([self.ma_period, self.momentum_period]), Resolution.Daily)
self.traded_count:int = 1 # number of assets to hold
# subscribe data
for ticker, hash_ in ticker_hash.items():
data:Security = self.AddData(CustomTicker, ticker, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
self.traded_symbols.append(data.Symbol)
self.recent_month:int = self.Time.month
# benchmark
self.print_benchmark:bool = False
self.benchmark:Symbol = self.AddEquity("VOO", Resolution.Daily).Symbol
self.benchmark_values:List[float] = []
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
def OnEndOfDay(self) -> None:
if self.print_benchmark:
mkt_price_df:DataFrame = self.History(self.benchmark, 2, Resolution.Daily)
if not mkt_price_df.empty:
mkt_price:float = mkt_price_df['close'].unstack(level= 0).iloc[-1]
if len(self.benchmark_values) == 2:
self.benchmark_values[-1] = mkt_price
mkt_perf:float = self.init_cash * self.benchmark_values[-1] / self.benchmark_values[0]
self.Plot('Strategy Equity', self.benchmark, mkt_perf)
else:
self.benchmark_values.append(mkt_price)
def OnData(self, data: Slice) -> None:
if self.IsWarmingUp: return
# rebalance once a month
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
last_update_date:Dict[str, datetime.date] = CustomTicker.get_last_update_date()
# custom data stopped comming in
if all(x.Value in last_update_date and self.Time.date() < last_update_date[x.Value] for x in self.traded_symbols):
# momentum sort
traded_symbols:List[Symbol] = []
m_period:int = max([self.ma_period, self.momentum_period])
hist_period:int = m_period * 31
price_df:DataFrame = self.History(self.traded_symbols, hist_period, Resolution.Daily).unstack(level=0)['adj close']
price_df = price_df.groupby(pd.Grouper(freq='M')).last()[-m_period:]
if len(price_df) == m_period:
momentum_df:DataFrame = price_df.pct_change(periods=self.momentum_period)
ma_df:DataFrame = price_df.rolling(self.ma_period).mean()
top_by_momentum:str = momentum_df.iloc[-1].sort_values(ascending=0).index[0]
if price_df.iloc[-1][top_by_momentum] >= ma_df.iloc[-1][top_by_momentum]:
traded_symbols.append(self.Symbol(top_by_momentum))
# liquidate
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in traded_symbols:
self.Liquidate(symbol)
# rebalance
for symbol in traded_symbols:
if not self.Portfolio[symbol].Invested:
self.SetHoldings(symbol, 1 / len(traded_symbols))
else:
self.Liquidate()
return
# Custom Ticker data
# NOTE Data order must be ascending (datewise)
class CustomTicker(PythonData):
_last_update_date:Dict[str, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[str, datetime.date]:
return CustomTicker._last_update_date
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource(f"data.quantpedia.com/backtesting_data/equity/{config.Symbol.Value}.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
if not (line.strip() and line[0].isdigit()): return None
data = CustomTicker()
data.Symbol = config.Symbol
split:List[str] = line.split(',')
# Date,Open,High,Low,Close,Adj Close,Volume
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data['Open'] = float(split[1])
data['High'] = float(split[2])
data['Low'] = float(split[3])
data['Close'] = float(split[4])
data['Adj Close'] = float(split[5])
data['Volume'] = float(split[6])
data.Value = float(split[5])
# store last update date
if config.Symbol.Value not in CustomTicker._last_update_date:
CustomTicker._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > CustomTicker._last_update_date[config.Symbol.Value]:
CustomTicker._last_update_date[config.Symbol.Value] = data.Time.date()
return data
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))