Overall Statistics
Total Trades
2467
Average Win
0.08%
Average Loss
-0.06%
Compounding Annual Return
8.906%
Drawdown
4.400%
Expectancy
0.371
Net Profit
30.435%
Sharpe Ratio
1.334
Probabilistic Sharpe Ratio
73.680%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.29
Alpha
0.058
Beta
0.039
Annual Standard Deviation
0.046
Annual Variance
0.002
Information Ratio
-0.127
Tracking Error
0.203
Treynor Ratio
1.602
Total Fees
$1298.54
Estimated Strategy Capacity
$16000.00
Lowest Capacity Asset
TRXUSD E3
# https://quantpedia.com/strategies/time-series-momentum-factor-in-cryptocurrencies/
#
# The investment universe consists of 11 cryptocurrencies (the full list can be found in the paper). Momentum factor is the prior week’s return
# for each currency and therefore can be calculated as a sum of returns for the last week (the data are avalaible at coinmetrics.io). After that,
# the momentum factor is standardized with z-scoring the variable longitudinally – de-meaning it and dividing by its standard deviation to create
# a normalized variable with a zero mean and unit standard deviation, separately for each currency. Portfolio is equally weighted, where the 
# absolute weight is 10% divided by n, where 10% is the gross exposure limit (only 10% of portfolio is invested in cryptocurrencies) and n is
# the number of currencies available for investment. The weight is positive when the longitudinally standardized momentum factor is above zero
# and negative when this factor is below zero, this allows portfolios that can be net long or short the market. However, it is not possible to
# short cryptocurrencies and the practical application would require for example the long only strategy. Portfolio is rebalanced weekly. Last 
# but not least, there are two weighting schemes, the second one is risk based and more information about it is in the paper, we have chosen 
# equally-weighted strategy for a representative purposes.

#region imports
from AlgorithmImports import *
import numpy as np
import pandas as pd
from datetime import timedelta
#endregion

class TimeSeriesMomentumCryptocurrencies(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetCash(100000)

        self.symbols = ['BTCUSD', 'ETHUSD',
                       'XRPUSD', 'ADAUSD', 'XMRUSD',
                       'MATICUSD', 'TRXUSD',
                       'ATOMUSD', 'ETCUSD','LINKUSD',
                       'LTCUSD', 'XLMUSD', 'XTZUSD', 'EOSUSD']
        self.data = {}
        self.percentage_traded = 0.1
    
        month_list = []
        for year in ["2020","2021","2022","2023","2024"]:
            for i in range(12):
                if i >= 9:
                    month_list.append(f"{year}{i+1}")
                else:
                    month_list.append(f"{year}0{i+1}")
        df = pd.DataFrame({'Date': month_list})
        df['EndOfMonth'] = pd.to_datetime(df['Date'], format="%Y%m") + pd.tseries.offsets.MonthEnd(0)
        self.date_store = df.EndOfMonth.dt.date.tolist()
        self.end_of_month = False;
        self.momentum_run = True;
        self.eom_profit = 1.0;
        self.sign_eom_profit = np.sign(self.eom_profit);
        
        for symbol in self.symbols:
            data = self.AddCrypto(symbol, Resolution.Daily, Market.Bitfinex)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(10)
            self.data[symbol] = RollingWindow[float](5)
        

    def OnData(self, data):
        for symbol in self.data:
            symbol_obj = self.Symbol(symbol)
            if symbol_obj in data.Bars and data[symbol_obj]:
                self.data[symbol].Add(data[symbol_obj].Value)
        if self.Time.date() + timedelta(days=2) in self.date_store:
            self.end_of_month = True;
            self.eom_profit = self.Portfolio.TotalPortfolioValue;

            for symbol in self.symbols:
                self.SetHoldings(symbol, self.percentage_traded / 10)
            return
        elif self.Time.date() - timedelta(days=1) in self.date_store:
            invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
            for symbol in invested:
                self.Liquidate(symbol)
            self.end_of_month = False;
            self.momentum_run = True;
            self.eom_profit = self.Portfolio.TotalPortfolioValue - self.eom_profit;
            self.sign_eom_profit = int(self.eom_profit > 0);
        elif self.end_of_month:
            return

        if self.momentum_run:
            self.momentum_run = False;
        else:
            if self.Time.date().weekday() != 0:
                return

        perf_vol = {}
        
        for symbol in self.symbols:
            if self.data[symbol].IsReady:
                prices = np.array([x for x in self.data[symbol]])
                perf = prices[0] / prices[-1] - 1
                
                daily_returns = prices[:-1] / prices[1:] - 1
                vol = np.std(daily_returns)
                perf_vol[symbol] = (perf, vol)

        # Volatility weighting
        total_vol = sum([1 / x[1][1] for x in perf_vol.items()])
        if total_vol == 0: return

        weight = {}
        for symbol in perf_vol:
            vol = perf_vol[symbol][1]
            if vol != 0:
                weight[symbol] = (1.0 / vol) / total_vol
            else: 
                weight[symbol] = 0

        # Trade execution.
        long = [x[0] for x in perf_vol.items() if x[1][0] > 0]

        invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long:
                self.Liquidate(symbol)

        for symbol in long:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, self.sign_eom_profit * self.percentage_traded * weight[symbol])

# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.0006
        return OrderFee(CashAmount(fee, "USD"))