Overall Statistics |
Total Orders 2270 Average Win 0.04% Average Loss -0.02% Compounding Annual Return 12.676% Drawdown 19.100% Expectancy 0.297 Start Equity 100000 End Equity 104068.45 Net Profit 4.068% Sharpe Ratio 0.273 Sortino Ratio 0.348 Probabilistic Sharpe Ratio 34.484% Loss Rate 57% Win Rate 43% Profit-Loss Ratio 2.01 Alpha -0.042 Beta 1.802 Annual Standard Deviation 0.311 Annual Variance 0.097 Information Ratio 0.055 Tracking Error 0.269 Treynor Ratio 0.047 Total Fees $1849.59 Estimated Strategy Capacity $15000000.00 Lowest Capacity Asset ARE R735QTJ8XC9X Portfolio Turnover 8.45% |
from AlgorithmImports import * class OLMARAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2024, 1, 1) # Set Start Date self.SetEndDate(2024, 5, 1) # Set End Date self.SetCash(100000) # Set Strategy Cash self.epsilon = 10.0 # Control parameter for mean reversion strength self.windowSize = 5 # Window size for moving average self.rebalanceTime = datetime.min self.symbolData = {} self.AddEquity("SPY", Resolution.Daily) # Use SPY to ensure valid market times for scheduling self.AddUniverse(self.CoarseSelectionFunction) self.Schedule.On(self.DateRules.WeekStart(), self.TimeRules.AfterMarketOpen("SPY", 30), self.Rebalance) def CoarseSelectionFunction(self, coarse): # Select the top 500 stocks by dollar volume selected = sorted([x for x in coarse if x.HasFundamentalData], key=lambda x: x.DollarVolume, reverse=True)[:500] return [x.Symbol for x in selected] def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol self.symbolData[symbol] = SymbolData(symbol, self.windowSize) history = self.History(symbol, self.windowSize, Resolution.Daily) if not history.empty: for time, row in history.loc[symbol].iterrows(): self.symbolData[symbol].PriceWindow.Add(row['close']) for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbolData: del self.symbolData[symbol] def OnData(self, data): if self.Time < self.rebalanceTime: return for symbol, symbolData in self.symbolData.items(): if data.ContainsKey(symbol) and data[symbol] is not None and data[symbol].Price is not None: price = data[symbol].Price symbolData.PriceWindow.Add(price) def Rebalance(self): if not all([symbolData.PriceWindow.IsReady for symbolData in self.symbolData.values()]): return weights = {} totalWeight = 0 for symbol, symbolData in self.symbolData.items(): averagePrice = sum(symbolData.PriceWindow) / self.windowSize predictedPrice = self.PredictNextPrice(averagePrice) weight = self.CalculateWeights(predictedPrice, symbolData.PriceWindow[0]) weights[symbol] = weight totalWeight += weight for symbol in weights: weights[symbol] /= totalWeight for symbol, weight in weights.items(): self.SetHoldings(symbol, weight) self.rebalanceTime = self.Time + timedelta(days=1) def PredictNextPrice(self, averagePrice): # Use the simple moving average as the predicted price for the next period return averagePrice def CalculateWeights(self, predictedPrice, currentPrice): reversionRatio = predictedPrice / currentPrice # OLMAR formula: target weight = epsilon * (predictedPrice / currentPrice - 1) weight = self.epsilon * (reversionRatio - 1) # Ensure weight is within bounds [0, 1] return max(0, min(1, weight)) class SymbolData: def __init__(self, symbol, windowSize): self.Symbol = symbol self.PriceWindow = RollingWindow[float](windowSize)
#region imports from AlgorithmImports import * from statsmodels.tsa.stattools import adfuller #endregion class StationarySelectionModel(ETFConstituentsUniverseSelectionModel): def __init__(self, algorithm, etf, lookback = 10, universe_settings = None): self.algorithm = algorithm self.lookback = lookback self.symbolData = {} symbol = Symbol.Create(etf, SecurityType.Equity, Market.USA) super().__init__(symbol, universe_settings, self.ETFConstituentsFilter) def ETFConstituentsFilter(self, constituents): stationarity = {} self.algorithm.Debug(f"{self.algorithm.Time}::{len(list(constituents))} tickers in SPY") for c in constituents: symbol = c.Symbol if symbol not in self.symbolData: self.symbolData[symbol] = SymbolData(self.algorithm, symbol, self.lookback) data = self.symbolData[symbol] # Update with the last price self.algorithm.Debug(f"{self.algorithm.Time}::{symbol}::{c.MarketValue}::{c.SharesHeld}") if c.MarketValue and c.SharesHeld: price = c.MarketValue / c.SharesHeld data.Update(price) # Cache the stationarity test statistics in the dict if data.TestStatistics: stationarity[symbol] = data.TestStatistics # Return the top 10 lowest test statistics stocks (more negative stat means higher prob to have no unit root) selected = sorted(stationarity.items(), key=lambda x: x[1]) return [x[0] for x in selected[:10]] class SymbolData: def __init__(self, algorithm, symbol, lookback): # RollingWindow to hold log price series for stationary testing self.window = RollingWindow[float](lookback) self.model = None # Warm up RollingWindow history = algorithm.History[TradeBar](symbol, lookback, Resolution.Daily) for bar in list(history)[:-1]: self.window.Add(np.log(bar.Close)) def Update(self, value): if value == 0: return # Update RollingWindow with log price self.window.Add(np.log(value)) if self.window.IsReady: # Test stationarity for log price series by augmented dickey-fuller test price = np.array(list(self.window))[::-1] self.model = adfuller(price, regression='n', autolag='BIC') @property def TestStatistics(self): return self.model[0] if self.model else None