Created with Highcharts 12.1.2EquityJan 2019Jan…Jul 2019Jan 2020Jul 2020Jan 2021Jul 2021Jan 2022Jul 2022Jan 2023Jul 2023Jan 2024Jul 2024Jan 2025Jul 2025025M50M-50000.25-101010M025M50M0202550
Overall Statistics
Total Orders
372
Average Win
4.80%
Average Loss
-4.51%
Compounding Annual Return
87.544%
Drawdown
53.700%
Expectancy
0.436
Start Equity
1000000
End Equity
26582134.24
Net Profit
2558.213%
Sharpe Ratio
1.437
Sortino Ratio
0.957
Probabilistic Sharpe Ratio
64.236%
Loss Rate
30%
Win Rate
70%
Profit-Loss Ratio
1.07
Alpha
0.711
Beta
-0.107
Annual Standard Deviation
0.489
Annual Variance
0.239
Information Ratio
1.194
Tracking Error
0.525
Treynor Ratio
-6.564
Total Fees
$215639.94
Estimated Strategy Capacity
$2300000.00
Lowest Capacity Asset
GIG XNAR4L6AIOV9
Portfolio Turnover
6.17%
# region imports
from AlgorithmImports import *

from sklearn.cluster import KMeans
from sklearn.preprocessing import MinMaxScaler
# endregion

class MuscularFluorescentPinkPanda(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2020, 1, 1)
        self.set_cash(1_000_000)
        self.universe_settings.extended_market_hours = True
        self._universe = self.add_universe(self._select_assets)
        self._spy = self.add_equity('SPY')
        self.schedule.on(self.date_rules.every_day(self._spy.symbol), self.time_rules.before_market_open(self._spy.symbol, 30), self._rebalance)

        self._liquidity_filter_size = self.get_parameter('liquidity_filter_size', 100)
        self._roc_threshold = self.get_parameter('roc_threshold', 0.1)
        self._clusters = self.get_parameter('clusters', 5)
        self._training_data_period = timedelta(self.get_parameter('training_data_days', 365))  # Calendar days
        self._hold_duration = self.get_parameter('hold_duration', 3)  # Trading days
        self._max_positions = 3

        self._scaler = MinMaxScaler(feature_range=(0, 1))
        self._kmeans = KMeans(n_clusters=self._clusters, random_state=42)

        self._trades = []
        self._columns = ['end_time', 'symbol', 'price_volatility', 'volume_volatility', 'pct_above_vwap', 'vwap_deviation', 'fraction_of_vol_below_vwap']
        self._factor_history = pd.DataFrame(columns=self._columns).set_index(['end_time', 'symbol'])
        self.set_warm_up(self._training_data_period)

    def _select_assets(self, fundamentals):
        # Select the most liquid assets.
        symbols = [f.symbol for f in sorted(fundamentals, key=lambda f: f.dollar_volume)[-self._liquidity_filter_size:]]

        # Select the subset of symbols that had >=10% growth yesterday from open to close.
        history = self.history(symbols, 1, Resolution.DAILY)
        symbols = [idx[0] for idx in history[(history.close / history.open - 1) >= self._roc_threshold].index]

        # Get the data we'll need to calculate the factors.
        vwap_by_symbol = {s: IntradayVwap('') for s in symbols}
        df_by_symbol = {s: pd.DataFrame(columns=['close', 'vwap', 'volume']) for s in symbols}
        for bars in self.history[TradeBar](symbols, self._spy.exchange.hours.get_previous_market_open(self.time, False), self.time, Resolution.MINUTE, extended_market_hours=False):
            for symbol, bar in bars.items():
                vwap = vwap_by_symbol[symbol]
                if vwap.update(bar):
                    df_by_symbol[symbol].loc[bar.end_time] = [bar.close, vwap.current.value, bar.volume]
        # Calculate factors for the selected assets.
        # The clustering process will try to minimize these.
        factors = pd.DataFrame(columns=self._columns)
        for symbol, df in df_by_symbol.items():
            if df.empty:
                continue
            factors.loc[len(factors)] = [
                self.time, 
                symbol, 
                df.close.std() / df.close.mean(),                      # price_volatility
                df.volume.std() / df.volume.mean(),                    # volume_volatility
                (df.close > df.vwap).astype(int).sum() / len(df),      # pct_above_vwap
                np.mean(np.abs(df.close - df.vwap)) / df.vwap.mean(),  # vwap_deviation
                df[df.close < df.vwap].volume.sum() / df.volume.sum()  # fraction_of_vol_below_vwap
            ]
        if factors.empty:
            return []
        factors.set_index(['end_time', 'symbol'], inplace=True)

        # Define the universe.
        universe = []
        if not self.is_warming_up:
            # Fit the scaler and k-means model to the training data.
            self._kmeans.fit(self._scaler.fit_transform(self._factor_history)) # Drop the `time` column
            # Find the cluster that's closest to the origin.
            closest_cluster_idx = np.argmin(np.linalg.norm(self._kmeans.cluster_centers_, axis=1))
            # Predict the cluster of the current universe.
            cluster_labels = self._kmeans.predict(self._scaler.transform(factors))
            # Select the subset of asset in the universe that are in the cluster closest to the origin.
            universe = [factors.index.levels[1][i] for i, label in enumerate(cluster_labels) if label == closest_cluster_idx]

        # Append the latest factor samples to the history.
        self._factor_history = pd.concat([self._factor_history, factors], axis=0).reset_index()
        # Trim off samples that have fallen out of the lookback window (1 year).
        self._factor_history = self._factor_history[self._factor_history.end_time >= self.time-self._training_data_period].set_index(['end_time', 'symbol'])
        # Return the assets selected for the universe.
        return universe

    def _rebalance(self):
        # Scan for exits.
        closed_trades = []
        for i, trade in enumerate(self._trades):
            trade.scan(self)
            if trade.closed:
                closed_trades.append(i)
        # Delete closed trades
        for i in closed_trades[::-1]:
            del self._trades[i]
        # Scan for entries.
        if self._universe.selected and len(self._trades) < self._max_positions:
            for symbol in self._universe.selected:
                self._trades.append(Trade(self, symbol, self._hold_duration, -1/self._max_positions))
        self.plot('Trades', 'Open', len(self._trades))


class Trade:

    def __init__(self, algorithm, symbol, hold_duration, weight):
        self._symbol = symbol
       
        # Determine position size
        self.closed = True
        price = algorithm.securities[symbol].price
        if not price:
            return
        self._quantity = algorithm.calculate_order_quantity(symbol, weight)
        if self._quantity == 0:
            return
        self.closed = False
       
        # Enter trade
        algorithm.market_order(symbol, self._quantity)
       
        # Set variable for exit logic
        self._hold_duration = hold_duration
       
    def scan(self, algorithm):
        if self.closed:
            return
        self._hold_duration -= 1
        if self._hold_duration <= 0:
           algorithm.market_order(self._symbol, -self._quantity)    
           self.closed = True