Overall Statistics
Total Orders
1598
Average Win
0.81%
Average Loss
-0.82%
Compounding Annual Return
-4.478%
Drawdown
44.000%
Expectancy
-0.044
Start Equity
1000000.00
End Equity
682039.63
Net Profit
-31.796%
Sharpe Ratio
-0.53
Sortino Ratio
-0.685
Probabilistic Sharpe Ratio
0.001%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
0.99
Alpha
-0.056
Beta
0.042
Annual Standard Deviation
0.1
Annual Variance
0.01
Information Ratio
-0.725
Tracking Error
0.18
Treynor Ratio
-1.272
Total Fees
$3917.83
Estimated Strategy Capacity
$5000.00
Lowest Capacity Asset
ZECUSD E3
Portfolio Turnover
3.37%
# https://quantpedia.com/strategies/price-to-low-effect-in-cryptocurrencies/
# 
# The investment universe for this strategy consists of a predefined list of 33 cryptocurrencies, focusing on those with larger market values and sufficient liquidity.
# (Data is from the LSEG Datastream.)
# Rationale of Strategy: The strategy utilizes a behavioral approach by incorporating anchoring points, specifically the highest and lowest prices during formation.
# 1. Calculate the primary indicator used, which is the reversal signal (REV) (by eq. (1)), as the natural logarithm of the price ratio at the start and end of the 
# formation period.
# 2. Decompose this signal further into Price-to-Low (PTL) and Low-to-Price (LTP) components (by eq. (2)).
# Ranking: Cryptocurrencies are ranked based on their PTL values.
# Selected Variant Execution: Our included version focuses on the PTL 30 signal/variant. The presented strategies buy (sell) a tertile of cryptocurrencies with the 
# highest (lowest) PTL values in the 30-day ranking period.
# The buy rule involves forming an extended portfolio with cryptocurrencies in the top tercile of PTL values. In contrast,
# the sell rule creates a short portfolio with those in the bottom tercile
# This approach aims to exploit short-term reversal effects. All portfolios are equal-weighted and rebalanced weekly. (Due to cryptos extreme volatility, we universally 
# advise exposing a maximum of 10 % of the conservative capital allocation to any from crypto strategies.)

# region imports
from AlgorithmImports import *
# endregion

class PricetoLowEffectInCryptocurrencies(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2017, 1, 1)
        self.set_cash(1_000_000)

        crypto_pairs: List[str] = {
            "XLMUSD",    # Stellar
            "XMRUSD",    # Monero
            "XRPUSD",    # XRP
            "ADAUSD",    # Cardano
            "DOTUSD",    # Polkadot
            "LINKUSD",   # Chainlink
            "BTCUSD",    # Bitcoin
            "BTGUSD",    # Bitcoin Gold
            "DASHUSD",   # Dash
            "ETCUSD",    # Ethereum Classic
            "ETHUSD",    # Ethereum
            "LTCUSD",    # Litecoin
            "NEOUSD",    # Neo
            "TRXUSD",    # Tron
            "XRPUSD",    # XRP
            "XVGUSD",    # Verge
            "ZECUSD",    # Zcash
            "ZRXUSD",    # Ox
            "BATUSD",    # Basic Attention Token
            "EOSUSD",    # EOS   
            "IOTAUSD",   # IOTA
            "SOLUSD",    # Solana
        }

        leverage: int = 10
        period: int = 30
        self._percentage_traded: float = .1
        self._quantile: int = 10

        self._data: Dict[Fundamental, SymbolData] = {}

        # data subscription
        for ticker in crypto_pairs:
            data = self.add_crypto(ticker, Resolution.DAILY, Market.BITFINEX, leverage=leverage)
            data.SetFeeModel(CustomFeeModel())

            self._data[data.Symbol] = SymbolData(period)

        self._selection_flag: bool = False

        self.schedule.on(
            self.date_rules.every(DayOfWeek.TUESDAY),
            self.time_rules.at(0, 0),
            self.selection
        )

        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.settings.daily_precise_end_time = False

    def on_data(self, slice: Slice) -> None:
        # store daily price
        for symbol, symbol_data in self._data.items():
            if slice.contains_key(symbol) and slice[symbol]:
                self._data[symbol].update(slice[symbol].close)

        # rebalance weekly
        if not self._selection_flag:
            return
        self._selection_flag = False

        PTL: Dict[Symbol, float] = {
            symbol: symbol_data.get_PTL() 
            for symbol, symbol_data in self._data.items() 
            if symbol_data.is_ready()
            and symbol_data.get_PTL() != .0
        }

        if len(PTL) < self._quantile:
            self.log('Not enought data for further calculation.')
            return Universe.UNCHANGED

        # sort and divide
        sorted_PTL: List[Symbol] = sorted(PTL, key=PTL.get, reverse=True)
        quantile: int = len(sorted_PTL) // self._quantile
        long: List[Symbol] = sorted_PTL[:quantile]
        short: List[Symbol] = sorted_PTL[-quantile:]

        # trade execution
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(
                        PortfolioTarget(
                            symbol, ((-1) ** i) * self._percentage_traded / len(portfolio)
                        )
                    )

        self.set_holdings(targets, True)

    def selection(self) -> None:
        self._selection_flag = True

class SymbolData():
    def __init__(self, period: int) -> None:
        self._daily_price: RollingWindow = RollingWindow[float](period)
    
    def update(self, price: float) -> None:
        self._daily_price.add(price)

    def get_PTL(self) -> float:
        prices: List[float] = list(self._daily_price)
        return np.log(prices[-1] / min(prices))

    def is_ready(self) -> bool:
        return self._daily_price.is_ready

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