Momentum Based on Fractional-Difference Filter
Log in to collectAcademic paper
Soros Chitsiripanich; Marc S. Paolella; Paweł Polak; Patrick S. Walker
- Institute of Finance and Banking
- CHUniversity of Zurich
- ?University of Zurich - Department of Banking and Finance
- CHSwiss Finance Institute
- Stony Brook University
- ?Stony Brook University-Department of Applied Mathematics and Statistics
- ?University of Zurich, Department of Banking and Finance
Strategy in a nutshell
Apply a 0.9-order fractional difference filter to daily log prices of mid- and large-cap US stocks. Predict returns, sort into quintiles, and long the top quintile while shorting the bottom. Rebalance daily with weekly holdings.
Economic rationale
The strategy combines momentum and short-term reversal by using fractional differencing to process the full price history. This mixture leverages well-established investment styles while utilizing more data than simple momentum or reversal methods.
Backtest performance
Annualised return20.3%
Volatility22.1%
Beta-0.214
Sharpe ratio0.92
Sortino ratio0.089
Win rate51%
Full Python code
from AlgorithmImports import *
import data_tools
import numpy as np
from dateutil.relativedelta import relativedelta
from typing import List, Dict, Tuple
from math import factorial, prod
from decimal import Decimal
# endregion
class MomentumBasedonFractionalDifferenceFilter(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.data:Dict[Symbol, float] = {}
self.stocks_to_liquidate:List[data_tools.HoldingItem] = []
self.traded_portfolio_portion:Dict[Symbol, float] = {}
self.t:int = 252
self.period:int = self.t * 5
self.quantile:int = 5
self.leverage:int = 25
self.holding_period:int = 5
self.fundamental_count:int = 50
self.d:Decimal = Decimal(0.9)
# pi and weights calculations
self.pi_list:List[Decimal] = [ self.pi(i, self.d) for i in range(self.period) ]
self.weights:List[Decimal] = [ self.w(np.array(list(range(self.period))), self.pi_list[i], i) for i in range(self.period) ]
self.rebalance_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.EveryDay(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# store daily stock prices
for stock in fundamental:
symbol:Symbol = stock.Symbol
if symbol in self.data:
self.data[symbol].update_daily_return(stock.AdjustedPrice)
selected:List[Symbol] = [x.Symbol
for x in sorted([x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 and \
(x.SecurityReference.ExchangeId == 'NYS') or (x.SecurityReference.ExchangeId == 'NAS') or (x.SecurityReference.ExchangeId == 'ASE')],
key = lambda x: x.DollarVolume, reverse = True)[:self.fundamental_count]]
# price warmup and prediction
predict_by_symbol:Dict[Symbol, float] = {}
for symbol in selected:
if symbol not in self.data:
self.data[symbol] = data_tools.SymbolData(self.period)
# price warmup
history:DataFrame = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes:pd.Series = history.loc[symbol].close
for time, close in closes.items():
self.data[symbol].update_daily_return(close)
if self.data[symbol].is_ready():
# prediction
prices:List[float] = np.log(self.data[symbol].get_daily_prices())
predict:float = - (self.weights[len(self.weights) - self.t] / self.t - 1) * Decimal(sum([ prices[i] - prices[len(self.weights) - self.t] for i in range(len(self.weights) - self.t + 1, len(self.weights) - 1) ])) \
+ Decimal(sum([ (self.weights[i] + (self.weights[len(self.weights) - self.t] / (self.t - 1))) * Decimal(prices[i] - prices[len(self.weights) - 1]) for i in range(len(self.weights) - self.t + 1, len(self.weights) - 1) ]))
predict_by_symbol[symbol] = predict
# sort and divide into quantiles
if len(predict_by_symbol) >= self.quantile:
sorted_predicts:List[Symbol] = sorted(predict_by_symbol, key=predict_by_symbol.get, reverse=True)
quantile:int = len(sorted_predicts) // self.quantile
long = sorted_predicts[:quantile]
short = sorted_predicts[-quantile:]
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
self.traded_portfolio_portion[symbol] = ((-1) ** i) / len(portfolio) * (self.Portfolio.TotalPortfolioValue / self.holding_period)
self.rebalance_flag = True
return selected
def OnData(self, data: Slice) -> None:
if not self.rebalance_flag:
return
self.rebalance_flag = False
items_to_remove:List[data_tools.HoldingItem] = []
# execute order and hold for holding period
for item in self.stocks_to_liquidate:
item._holding_period += 1
if item._holding_period >= self.holding_period:
self.MarketOrder(item._symbol, -item._quantity)
items_to_remove.append(item)
# remove from collection
for item in items_to_remove:
self.stocks_to_liquidate.remove(item)
# execute order
for price_symbol, portfolio_portion in self.traded_portfolio_portion.items():
if price_symbol in data and data[price_symbol]:
final_quantity:int = portfolio_portion // data[price_symbol].Price
if portfolio_portion != 0:
self.MarketOrder(price_symbol, final_quantity)
self.stocks_to_liquidate.append(data_tools.HoldingItem(price_symbol, final_quantity))
self.traded_portfolio_portion.clear()
def Selection(self) -> None:
self.selection_flag = True
def w(self, price_dataset:np.ndarray, pi:Decimal, u:int) -> Decimal:
result:Decimal = (np.sum((np.arange((len(price_dataset) - u - self.t + 1), len(price_dataset) - u)) * pi) / self.t) - self.pi((len(price_dataset) - u + 1), self.d) if u < len(price_dataset) - self.t \
else ((np.sum((np.arange(len(price_dataset) - u)) * pi) / self.t) - self.pi((len(price_dataset) - u + 1), self.d) if u in range((len(price_dataset) - self.t + 1) , len(price_dataset) - 1) \
else (self.pi(0, self.d) / self.t) - self.pi(1, self.d) - 1)
return result
def pi(self, s:int, d:Decimal) -> Decimal:
s_factorial:Decimal = Decimal(factorial(s))
result:Decimal = Decimal(((-1) ** s) * (prod([ Decimal(d - i) / s_factorial for i in range(s-1) ])))
return result