Overall Statistics
Total Orders
104
Average Win
5.32%
Average Loss
-4.33%
Compounding Annual Return
30.544%
Drawdown
48.500%
Expectancy
0.603
Start Equity
100000
End Equity
405753.16
Net Profit
305.753%
Sharpe Ratio
0.842
Sortino Ratio
0.926
Probabilistic Sharpe Ratio
32.182%
Loss Rate
28%
Win Rate
72%
Profit-Loss Ratio
1.23
Alpha
0.088
Beta
1.244
Annual Standard Deviation
0.263
Annual Variance
0.069
Information Ratio
0.712
Tracking Error
0.16
Treynor Ratio
0.178
Total Fees
$280.24
Estimated Strategy Capacity
$1900000000.00
Lowest Capacity Asset
NVDA RHM8UTD8DT2D
Portfolio Turnover
1.31%
# region imports
from AlgorithmImports import *

import torch
from chronos import ChronosPipeline
from scipy.optimize import minimize
from transformers import set_seed 
# endregion

class HuggingFaceBaseModelDemo(QCAlgorithm):
    """
    This algorithm demonstrates how to use a pre-trained HuggingFace 
    model. It uses the "amazon/chronos-t5-tiny" model to forecast the 
    future equity curves of the 5 most liquid assets in the market,
    then it uses the SciPy package to find the portfolio weights
    that will maximize the future Sharpe ratio of the portfolio. 
    The portfolio is rebalanced every 3 months.
    """

    def initialize(self):
        self.set_start_date(2019, 1, 1)
        self.set_end_date(2024, 4, 1)
        self.set_cash(100_000)

        self.settings.min_absolute_portfolio_target_percentage = 0

        # Enable reproducibility.
        set_seed(1, True)

        # Load the pre-trained model.
        self._pipeline = ChronosPipeline.from_pretrained(
            "amazon/chronos-t5-tiny",
            device_map="cuda" if torch.cuda.is_available() else "cpu",
            torch_dtype=torch.bfloat16,
        )

        # Define the universe.
        spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)
        self.universe_settings.schedule.on(self.date_rules.month_start(spy))
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(
            self.universe.dollar_volume.top(
                self.get_parameter('universe_size', 5)
            )
        )

        # Define some trading parameters.
        self._lookback_period = timedelta(
            365 * self.get_parameter('lookback_years', 1)
        )
        self._prediction_length = 3*21  # Three months of trading days

        # Schedule rebalances.
        self._last_rebalance = datetime.min
        self.schedule.on(
            self.date_rules.month_start(spy, 1), 
            self.time_rules.midnight, 
            self._trade
        )

        # Add warm up so the algorithm trades on deployment.
        self.set_warmup(timedelta(31))

    def on_warmup_finished(self):
        # Trade right after warm up is done.
        self._trade()

    def _sharpe_ratio(
            self, weights, returns, risk_free_rate, trading_days_per_year=252):
        # Define how to calculate the Sharpe ratio so we can use
        # it to optimize the portfolio weights.

        # Calculate the annualized returns and covariance matrix.
        mean_returns = returns.mean() * trading_days_per_year 
        cov_matrix = returns.cov() * trading_days_per_year

        # Calculate the Sharpe ratio.
        portfolio_return = np.sum(mean_returns * weights)
        portfolio_std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
        sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_std
        
        # Return negative Sharpe ratio because we minimize this
        # function in optimization.
        return -sharpe_ratio

    def _optimize_portfolio(self, equity_curves):
        returns = equity_curves.pct_change().dropna()
        num_assets = returns.shape[1]
        initial_guess = num_assets * [1. / num_assets,]
        # Find portfolio weights that mazimize the forward Sharpe
        # ratio.
        result = minimize(
            self._sharpe_ratio, 
            initial_guess, 
            args=(
                returns,
                self.risk_free_interest_rate_model.get_interest_rate(self.time)
            ), 
            method='SLSQP', 
            bounds=tuple((0, 1) for _ in range(num_assets)), 
            constraints=(
                {'type': 'eq', 'fun': lambda weights: np.sum(weights) - 1}
            )
        )    
        return result.x

    def _trade(self):
        # Don't rebalance during warm-up.
        if self.is_warming_up:
            return
        # Only rebalance on a quarterly basis.
        if self.time - self._last_rebalance < timedelta(80):
            return  
        self._last_rebalance = self.time

        symbols = list(self._universe.selected)

        # Get historical equity curves.
        history = self.history(symbols, self._lookback_period)['close'].unstack(0)
        
        # Forecast the future equity curves.
        all_forecasts = self._pipeline.predict(
            [
                torch.tensor(history[symbol].dropna()) 
                for symbol in symbols
            ], 
            self._prediction_length
        )
        
        # Take the median forecast for each asset.
        forecasts_df = pd.DataFrame(
            {
                symbol: np.quantile(
                    all_forecasts[i].numpy(), 0.5, axis=0   # 0.5 = median
                )
                for i, symbol in enumerate(symbols)
            }
        )

        # Find the weights that maximize the forward Sharpe 
        # ratio of the portfolio.
        optimal_weights = self._optimize_portfolio(forecasts_df)

        # Rebalance the portfolio.
        self.set_holdings(
            [
                PortfolioTarget(symbol, optimal_weights[i])
                for i, symbol in enumerate(symbols)
            ], 
            True
        )