Overall Statistics
Total Orders
69734
Average Win
0.23%
Average Loss
-1.41%
Compounding Annual Return
22.019%
Drawdown
48.900%
Expectancy
-0.106
Start Equity
100000
End Equity
1043290.64
Net Profit
943.291%
Sharpe Ratio
0.662
Sortino Ratio
0.645
Probabilistic Sharpe Ratio
8.984%
Loss Rate
23%
Win Rate
77%
Profit-Loss Ratio
0.17
Alpha
0.042
Beta
1.38
Annual Standard Deviation
0.248
Annual Variance
0.061
Information Ratio
0.46
Tracking Error
0.165
Treynor Ratio
0.119
Total Fees
$767.13
Estimated Strategy Capacity
$37000000.00
Lowest Capacity Asset
FI VIXMZJV0W8RP
Portfolio Turnover
0.18%
# region imports
from AlgorithmImports import *
from datetime import datetime, timedelta
from collections import deque
import math
# endregion

class ETFConstituentUniverseROCPAlphaModelAlgorithm(QCAlgorithm):

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

        self.SetAlpha(ConstituentWeightedROCPAlphaModel())
        self.SetPortfolioConstruction(PercentagePortfolioConstructionModel(self, 0.04))  # 4% per trade
        self.SetExecution(ImmediateExecutionModel())

        iwb = self.AddEquity("IWB", Resolution.Daily).Symbol  # IWB is the iShares Russell 1000 ETF
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.01
        self.AddUniverse(self.Universe.ETF(iwb, self.UniverseSettings, self.FilterETFConstituents))

    def FilterETFConstituents(self, constituents):
        return [i.Symbol for i in constituents if i.Weight is not None and i.Weight >= 0.001]

class ConstituentWeightedROCPAlphaModel(AlphaModel):
    def __init__(self):
        self.rocp_symbol_data = {}
        self.entry_signals = {}
        self.trade_start_time = {}

    def Update(self, algorithm: QCAlgorithm, data: Slice):
        insights = []

        for symbol, symbol_data in self.rocp_symbol_data.items():
            if symbol not in data.Bars:
                continue

            bar = data.Bars[symbol]
            symbol_data.Update(bar)

            if not symbol_data.rocp.IsReady or not symbol_data.atr.IsReady:
                continue

            current_roc = symbol_data.rocp.Current.Value
            yesterday_roc = symbol_data.previous_roc

            if symbol not in self.entry_signals:
                if current_roc < -20 and current_roc > yesterday_roc:
                    self.entry_signals[symbol] = True
                    algorithm.Debug(f"Entry signal for {symbol}: ROC = {current_roc:.2f}, Yesterday ROC = {yesterday_roc:.2f}")
            elif self.entry_signals[symbol]:
                entry_price = bar.Open
                profit_target = entry_price + symbol_data.atr.Current.Value
                stop_loss = entry_price - 2.5 * symbol_data.atr.Current.Value

                insights.append(Insight.Price(
                    symbol,
                    timedelta(days=20),
                    InsightDirection.Up,
                    abs(current_roc),
                    None
                ))
                self.entry_signals[symbol] = False
                self.trade_start_time[symbol] = algorithm.Time
                algorithm.Debug(f"Entering trade for {symbol} at {entry_price:.2f}, PT: {profit_target:.2f}, SL: {stop_loss:.2f}")
            elif symbol in self.trade_start_time:
                if (algorithm.Time - self.trade_start_time[symbol]).days >= 20:
                    insights.append(Insight.Price(
                        symbol,
                        timedelta(days=1),
                        InsightDirection.Flat,
                        0,
                        None
                    ))
                    del self.trade_start_time[symbol]
                    algorithm.Debug(f"Exiting trade for {symbol} due to 20-day hold period")

        return insights

    def OnSecuritiesChanged(self, algorithm, changes):
        for added in changes.AddedSecurities:
            if added.Symbol not in self.rocp_symbol_data:
                self.rocp_symbol_data[added.Symbol] = SymbolData(added.Symbol, algorithm, 14)
        
        for removed in changes.RemovedSecurities:
            if removed.Symbol in self.rocp_symbol_data:
                del self.rocp_symbol_data[removed.Symbol]
            if removed.Symbol in self.entry_signals:
                del self.entry_signals[removed.Symbol]
            if removed.Symbol in self.trade_start_time:
                del self.trade_start_time[removed.Symbol]

class SymbolData:
    def __init__(self, symbol, algorithm, period):
        self.symbol = symbol
        self.rocp = algorithm.ROCP(symbol, period)
        self.atr = algorithm.ATR(symbol, period)
        self.previous_values = deque(maxlen=2)
        self.previous_roc = 0

    def Update(self, bar):
        self.rocp.Update(bar.EndTime, bar.Close)
        self.atr.Update(bar)
        self.previous_values.append(self.rocp.Current.Value)
        if len(self.previous_values) == 2:
            self.previous_roc = self.previous_values[0]

class PercentagePortfolioConstructionModel(PortfolioConstructionModel):
    def __init__(self, algorithm, percentage_per_trade):
        self.algorithm = algorithm
        self.percentage_per_trade = percentage_per_trade
        self.targets = {}

    def CreateTargets(self, algorithm, insights):
        targets = []

        for insight in insights:
            if insight.Direction == InsightDirection.Up:
                self.targets[insight.Symbol] = self.percentage_per_trade
            elif insight.Direction == InsightDirection.Flat:
                if insight.Symbol in self.targets:
                    del self.targets[insight.Symbol]

        portfolio_value = algorithm.Portfolio.TotalPortfolioValue
        for symbol, target_percentage in self.targets.items():
            target_value = portfolio_value * target_percentage
            quantity = math.floor(target_value / algorithm.Securities[symbol].Price)
            targets.append(PortfolioTarget(symbol, quantity))

        return targets