Overall Statistics
Total Orders
540
Average Win
4.54%
Average Loss
-3.85%
Compounding Annual Return
-1.994%
Drawdown
82.000%
Expectancy
0.055
Start Equity
100000
End Equity
83539.14
Net Profit
-16.461%
Sharpe Ratio
0.065
Sortino Ratio
0.065
Probabilistic Sharpe Ratio
0.119%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.18
Alpha
0.021
Beta
0.022
Annual Standard Deviation
0.347
Annual Variance
0.12
Information Ratio
-0.17
Tracking Error
0.378
Treynor Ratio
1.011
Total Fees
$316.61
Estimated Strategy Capacity
$2000.00
Lowest Capacity Asset
TRXUSD E3
Portfolio Turnover
1.93%
Drawdown Recovery
374
# https://quantpedia.com/strategies/crypto-skewness-strategy/
#
# The investment universe for this strategy consists of the 45 most popular cryptocurrencies based on their trading history. The selection criteria require that each cryptocurrency
# has a trading history from at least January 1, 2018, to the present. The dataset includes daily exchange rates in USD, obtained from Coinmetrics. This ensures that the selected
# cryptocurrencies have sufficient historical data for skewness calculations and performance evaluation. The primary tool used in this strategy is the skewness of daily returns.
# Skewness is calculated over two different periods: 30 days for the Monthly Model and 360 days for the Yearly Model. For the Monthly Model, at the end of each month, cryptocurrencies
# are sorted into quartiles based on their 30-day skewness. Two portfolios are constructed: one containing the top quartile (highest skewness) and the other containing the bottom
# quartile (lowest skewness). The adjusted trading rule for the Monthly Model is to long the top quartile portfolio and short the bottom quartile portfolio. For the Yearly Model, 
# the same process is followed, but skewness is calculated over the past 360 days. The trading rule for the Yearly Model is to long the bottom quartile portfolio and short the top
# quartile portfolio. Portfolios are rebalanced at the end of each month based on the updated skewness calculations. The number of positions in each portfolio depends on the sorting 
# method. For quartiles, each portfolio contains 11 cryptocurrencies. Position sizes are typically equal-weighted to ensure diversification and manage risk. Risk management techniques
# include monitoring performance metrics such as cumulative returns, Sharpe ratio, and maximum drawdown. Additionally, traders should consider implementing stop-loss orders and position
# sizing rules to limit potential losses. Transaction costs and slippage should also be accounted for, as they can impact net performance. Regularly reviewing and adjusting the strategy
# parameters based on changing market conditions can help maintain its effectiveness.

# region imports
from AlgorithmImports import *
from scipy.stats import skew
from scipy.stats import linregress
# endregion

class CryptoSkewnessStrategy(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2017, 1, 1)

        # Exclued stablecoins from universe
        stablecoins: List[str] = [
            "USDTUSD",
            "USDCUSD",
            "DAIUSD",
            "TUSDUSD",
            "FDUSDUSD",
            "BUSDUSD",
            "USDPUSD", 
            "GUSDUSD",
            "USDDUSD",
            "EURTUSD",  
            "EURSUSD",   
            "FRAXUSD",
            "LUSDUSD",
            "PYUSDUSD",
        ]

        self.set_account_currency("USD") 
        self._market = Market.BITFINEX
        self._market_pairs = [
            x.key.symbol 
            for x in self.symbol_properties_database.get_symbol_properties_list(self._market) 
            if x.value.quote_currency == self.account_currency
            and x.key.symbol not in stablecoins
        ]

        self._top_coins: int = 20
        self._period: int = 360 + 1
        self._quantile: int = 4
        self._leverage: int = 10
        self._trade_multiplier: float = .5

        self.set_warmup(self._period, Resolution.DAILY)
        self._price_data: Dict[Symbol, RollingWindow] = {}

        # Add a universe of Cryptocurrencies.
        self._universe = self.add_universe(CoinGeckoUniverse, 'CoinGeckoUniverse', Resolution.DAILY, self._select_assets)
        
        self._daily_flag: bool = False
        self.schedule.on(
            self.date_rules.every_day(), 
            self.time_rules.at(0, 0), 
            self._daily_close
        )
        self.schedule.on(
            self.date_rules.month_start(),
            self.time_rules.at(0, 0), 
            self._rebalance
        )
    
    def on_securities_changed(self, changes: SecurityChanges) -> None:
        for security in changes.added_securities:
            security.set_fee_model(CustomFeeModel())
            security.set_leverage(self._leverage)
            self._price_data.setdefault(security.symbol, RollingWindow[float](self._period))

    def _select_assets(self, data: List[CoinGecko]) -> List[Symbol]:
        tradable_coins: List[CoinGecko] = [d for d in data if d.coin + self.account_currency in self._market_pairs]

        # Select the largest coins and create their Symbol objects.
        current_universe: List[Symbol] = [
            c.create_symbol(self._market, self.account_currency) 
            for c in sorted(tradable_coins, key=lambda x: x.market_cap)[-self._top_coins:]
        ]
        
        return current_universe

    def on_data(self, slice: Slice) -> None:
        # Update price daily
        if not self._daily_flag:
            return
        self._daily_flag = False

        for symbol, price_data in self._price_data.items():
            if slice.contains_key(symbol) and slice[symbol]:
                price_data.add(slice[symbol].close)

    def _daily_close(self) -> None:
        self._daily_flag = True

    def _rebalance(self) -> None:
        if self.is_warming_up: return

        if not self._universe.selected:
            return
        
        symbol_skew: Dict[Symbol, float] = {}
        beta: Dict[Symbol, float] = {}
        
        for symbol in self._price_data:
            if symbol.value == 'BTCUSD':
                if self._price_data[symbol].is_ready:
                    benchmark_daily_prices: np.ndarray = np.array(list(self._price_data[symbol]))
                    benchmark_daily_returns: np.ndarray = benchmark_daily_prices[:-1] / benchmark_daily_prices[1:] - 1
                continue

            if symbol not in self._universe.selected or not self._price_data[symbol].is_ready:
                continue

            if self._price_data[symbol].is_ready:
                daily_prices: np.ndarray = np.array(list(self._price_data[symbol]))
                daily_returns: np.ndarray = daily_prices[:-1] / daily_prices[1:] - 1

                # calculate skew
                symbol_skew[symbol] = skew(daily_returns)

                # calculate beta
                # beta_coeff,_,_,_,_ = linregress(daily_returns, benchmark_daily_returns)
                # beta[symbol] = beta_coeff

        long: List[Symbol] = []
        short: List[Symbol] = []
        targets: List[PortfolioTarget] = []

        if len(symbol_skew) >= self._quantile:
            # long the bottom portfolios and short the top portfolios
            sorted_by_skew: List[Symbol] = sorted(symbol_skew, key = symbol_skew.get, reverse = True)
            quantile: int = int(len(sorted_by_skew) / self._quantile)
            long = sorted_by_skew[-quantile:]
            short = sorted_by_skew[:quantile]

            # order execution
            # long_beta: float = sum([beta[s] for s in long]) / len(long)
            # short_beta: float = sum([beta[s] for s in short]) / len(short)
            # coeff: float = long_beta / short_beta
            coeff: float = 1.

            for i, portfolio in enumerate([long, short]):
                for ticker in portfolio:
                    targets.append(PortfolioTarget(ticker, ((((-1) ** i) / len(portfolio)) * (coeff if i == 1 else 1.)) * self._trade_multiplier))
        
        self.set_holdings(targets, True)

class CustomFeeModel(FeeModel):
    ''' Models custom trade fees. '''
    def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
        fee: float = parameters.security.price * parameters.order.absolute_quantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))