Overall Statistics |
Total Orders
5309
Average Win
0.36%
Average Loss
-0.40%
Compounding Annual Return
0.833%
Drawdown
74.200%
Expectancy
-0.010
Start Equity
100000
End Equity
112990.87
Net Profit
12.991%
Sharpe Ratio
0.038
Sortino Ratio
0.039
Probabilistic Sharpe Ratio
0.003%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
0.91
Alpha
-0.002
Beta
0.1
Annual Standard Deviation
0.187
Annual Variance
0.035
Information Ratio
-0.355
Tracking Error
0.226
Treynor Ratio
0.071
Total Fees
$377.23
Estimated Strategy Capacity
$54000000.00
Lowest Capacity Asset
MXL UL6LC6GQH8PX
Portfolio Turnover
1.12%
|
# https://quantpedia.com/strategies/momentum-effect-in-stocks-in-small-portfolios/ # # The investment universe consists of all UK listed companies (this is the investment universe used in the source academic study, # and it could be easily changed into any other market – see Ammann, Moellenbeck, Schmid: Feasible Momentum Strategies in the US Stock Market). # Stocks with the lowest market capitalization (25% of the universe) are excluded due to liquidity reasons. Momentum profits are calculated # by ranking companies based on their stock market performance over the previous 12 months (the rank period). The investor goes long # in the ten stocks with the highest performance and goes short in the ten stocks with the lowest performance. The portfolio is equally weighted # and rebalanced yearly. We assume the investor has an account size of 10 000 pounds. # # QC implementation changes: # - Universe consists of 1000 most liquid US stocks. # - Instead of 10 000 pounds we use 100 000 dollars. # - Decile is used instead of 10 stocks. from AlgorithmImports import * from typing import Dict, List import pandas as pd class MomentumEffectinStocksinSmallPortfolios(QCAlgorithm): def Initialize(self) -> None: self.SetStartDate(2010, 1, 1) self.SetCash(100_000) self.UniverseSettings.Leverage = 10 self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.FundamentalSelectionFunction) self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0 self.settings.daily_precise_end_time = False self.fundamental_count: int = 1_000 self.fundamental_sorting_key = lambda x: x.DollarVolume self.rank_period: int = 12 * 21 # days self.quantile: int = 10 self.month_counter: int = 11 self.rebalance_period: int = 12 # months # daily prices self.symbol_data: Dict[Symbol, SymbolData] = {} self.long_symbols: List[Symbol] = [] self.short_symbols: List[Symbol] = [] self.selection_flag: bool = True market = self.AddEquity('SPY', Resolution.Daily).Symbol self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection) def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel()) def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: # update the rolling window every day for f in fundamental: if f.Symbol in self.symbol_data: self.symbol_data[f.Symbol].update(f.Price) # selection once a month if not self.selection_flag: return Universe.Unchanged filtered: List[Fundamental] = [ f for f in fundamental if f.HasFundamentalData and f.MarketCap != 0 ] sorted_filter: List[Fundamental] = sorted(filtered, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count] # warmup price rolling windows for f in sorted_filter: if f.Symbol in self.symbol_data: continue self.symbol_data[f.Symbol] = SymbolData(f.Symbol, self.rank_period) history: pd.DataFrame = self.History(f.Symbol, self.rank_period, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {f.Symbol} yet") continue closes: pd.Series = history.loc[f.Symbol].close for time, close in closes.items(): self.symbol_data[f.Symbol].update(close) ready_symbols: List[Symbol] = [f.Symbol for f in sorted_filter if self.symbol_data[f.Symbol].is_ready()] # performance sorting performance: Dict[Symbol, float] = {symbol: self.symbol_data[symbol].performance() for symbol in ready_symbols} if len(performance) >= self.quantile: quantile: int = int(len(performance) / self.quantile) sorted_by_perf: List[Symbol] = sorted(performance, key=performance.get, reverse=True) self.long_symbols = sorted_by_perf[:quantile] self.short_symbols = sorted_by_perf[-quantile:] return self.long_symbols + self.short_symbols def OnData(self, slice: Slice) -> None: if not self.selection_flag: return self.selection_flag = False # trade execution targets: List[PortfolioTarget] = [] for i, portfolio in enumerate([self.long_symbols, self.short_symbols]): for symbol in portfolio: if slice.ContainsKey(symbol) and slice[symbol] is not None: targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio))) self.SetHoldings(targets, True) self.long_symbols.clear() self.short_symbols.clear() def Selection(self) -> None: # rebalance every 12 months if self.month_counter == self.rebalance_period: self.selection_flag = True self.month_counter += 1 if self.month_counter > self.rebalance_period: self.month_counter = 1 class SymbolData(): def __init__(self, symbol: Symbol, period: int) -> None: self.Symbol: Symbol = symbol self.Price: RollingWindow = RollingWindow[float](period) def update(self, value: float) -> None: self.Price.Add(value) def is_ready(self) -> bool: return self.Price.IsReady def performance(self) -> float: closes: List[float] = [x for x in self.Price] return closes[0] / closes[-1] - 1 class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee: fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))