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