| Overall Statistics |
|
Total Orders 331 Average Win 0.60% Average Loss -0.06% Compounding Annual Return 3.671% Drawdown 5.500% Expectancy 8.785 Start Equity 100000 End Equity 249543.89 Net Profit 149.544% Sharpe Ratio 0.095 Sortino Ratio 0.087 Probabilistic Sharpe Ratio 15.825% Loss Rate 7% Win Rate 93% Profit-Loss Ratio 9.52 Alpha 0.001 Beta 0.043 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -0.242 Tracking Error 0.156 Treynor Ratio 0.067 Total Fees $330.00 Estimated Strategy Capacity $13000000.00 Lowest Capacity Asset SHY SGNKIKYGE9NP Portfolio Turnover 0.22% |
# region imports
from AlgorithmImports import *
from pandas.tseries.offsets import BDay
from dateutil.relativedelta import relativedelta
from dataclasses import dataclass
# endregion
class FOMC():
def __init__(self, algorithm: QCAlgorithm, offset_days: int) -> None:
csv_string_file = algorithm.Download(
'data.quantpedia.com/backtesting_data/economic/fed_days.csv'
)
dates = csv_string_file.split('\r\n')
self._dates: List[datetime.date] = [
(datetime.strptime(x, "%Y-%m-%d") - BDay(offset_days)).date() for x in dates
]
@property
def Dates(self) -> List[datetime.date]:
return self._dates
# NOTE: Manager for new trades. It's represented by certain count of equally weighted brackets for long and short positions.
# If there's a place for new trade, it will be managed for time of holding period.
class TradeManager():
def __init__(self,
algorithm: QCAlgorithm,
long_size: int,
short_size: int,
holding_period: int,) -> None:
self.algorithm: QCAlgorithm = algorithm # algorithm to execute orders in.
self.long_size: int = long_size
self.short_size: int = short_size
self.long_len: int = 0
self.short_len: int = 0
# Arrays of ManagedSymbols
self.symbols: List[ManagedSymbol] = []
self.holding_period: int = holding_period # Days of holding.
# Add stock symbol object
def add(
self,
symbol: Symbol,
long_flag: bool,
price: float) -> None:
# Open new long trade.
quantity: Union[None, int] = None
if long_flag:
# If there's a place for it.
if self.long_len < self.long_size:
quantity: int = int(self.algorithm.portfolio.total_portfolio_value / self.long_size / price)
self.algorithm.market_order(symbol, quantity)
self.long_len += 1
# else:
# self.algorithm.Log("There's not place for additional trade.")
# Open new short trade.
else:
# If there's a place for it.
if self.short_len < self.short_size:
quantity: int = int(self.algorithm.portfolio.total_portfolio_value / self.short_size / price)
self.algorithm.market_order(symbol, -quantity)
self.short_len += 1
# else:
# self.algorithm.Log("There's not place for additional trade.")
if quantity:
managed_symbol: ManagedSymbol = ManagedSymbol(symbol, self.algorithm.time + relativedelta(months=self.holding_period), quantity, long_flag)
self.symbols.append(managed_symbol)
# Decrement holding period and liquidate symbols.
def try_liquidate(self) -> None:
symbols_to_delete: List[ManagedSymbol] = []
for managed_symbol in self.symbols:
# Liquidate.
if self.algorithm.time >= managed_symbol.liquidation_date:
symbols_to_delete.append(managed_symbol)
if managed_symbol.long_flag:
self.algorithm.market_order(managed_symbol.symbol, -managed_symbol.quantity)
self.long_len -= 1
else:
self.algorithm.market_order(managed_symbol.symbol, managed_symbol.quantity)
self.short_len -= 1
# Remove symbols from management.
for managed_symbol in symbols_to_delete:
self.symbols.remove(managed_symbol)
def liquidate_ticker(self, ticker: str) -> None:
symbol_to_delete: Union[None, Symbol] = None
for managed_symbol in self.symbols:
if managed_symbol.symbol.Value == ticker:
if managed_symbol.long_flag:
self.algorithm.market_order(managed_symbol.symbol, -managed_symbol.quantity)
self.long_len -= 1
else:
self.short_len -= 1
self.algorithm.market_order(managed_symbol.symbol, managed_symbol.quantity)
symbol_to_delete = managed_symbol
break
if symbol_to_delete: self.symbols.remove(symbol_to_delete)
else: self.algorithm.Debug("Ticker is not held in portfolio!")
@dataclass
class ManagedSymbol():
symbol: Symbol
liquidation_date: datetime
quantity: int
long_flag: bool
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# https://quantpedia.com/strategies/informative-price-pressure-during-pre-fomc-days
#
# The investment universe for this strategy primarily consists of high-beta stocks and broad market ETFs like SPY. (The selection is based on the research paper's
# findings that high-beta stocks exhibit more substantial hedging-induced price pressure and return predictability.)
# (Detailed variable definitions are in Appendix A.)
# Fundamental Recapitulation: The strategy uses historical pre-FOMC-meeting returns as key indicator. The methodology involves calculating the average pre-FOMC-meeting
# return over the past 24 months for each FOMC Day (-1).
# Input Variable Definition: The pre-FOMC-meeting return is the stock market return on the day before the start of a FOMC meeting. To reduce estimation noise,
# incorporate all rescheduled FOMC meetings over the past two years. Specifically, for each month m, the key independent variable, Average Past Day (−1) Return[m−23,m],
# is computed as the simple average of pre-FOMC-meeting returns over the previous 24 months in percentage, covering the period from month m−23 to month m.
# Trading Process: The strategy involves rebalancing positions on each FOMC Day (-1):
# If there is a one standard deviation decrease in Average Past Day (-1) Return[m−23,m], invest in the broad market for up to 2 years,
# else, liquidate the position or hold cash.
# Weighting & Rebalancing: Trade one asset (ETF representing stock index: SPY, QQQ, DIA) or a market-weighted high beta stock basket. Rebalanced is explained in the
# previous paragraph.
#
# QC Implementation changes:
# - Entry mean return threshold is set to -5%.
# region imports
from AlgorithmImports import *
import data_tools
from dateutil.relativedelta import relativedelta
# endregion
class InformativePricePressureDuringPreFOMCDays(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2000, 1, 1)
self.set_cash(100_000)
FOMC_offset_days: int = 1
long_count: int = 16
self._period: int = 24 # months
# historical values
# NOTE these value were obtained from in-sample statistics evaluation
# FOMCd-1 2y averages
hist_ret_mean: float = 0.0011076739323817675
hist_ret_std: float = 0.0028424951741022274
# FOMCd-1
# hist_ret_mean: float = 0.0016810414297294828
# hist_ret_std: float = 0.013461857412966283
# hist_ret_std = -0.00173482124172046
self._threshold: float = hist_ret_mean - hist_ret_std
# self._threshold: float = -0.05
self._FOMC_averages: List[float] = []
self._FOMC_obj = data_tools.FOMC(self, FOMC_offset_days)
self._trade_manager: data_tools.TradeManager = data_tools.TradeManager(self, long_count, 0, self._period)
self._market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol
self._cash: Symbol = self.add_equity('SHY', Resolution.DAILY).symbol
self._rebalance_flag: bool = False
self.schedule.on(
self.date_rules.on(self._FOMC_obj.Dates),
self.time_rules.before_market_close(self._market),
self._trade
)
def on_data(self, slice: Slice) -> None:
self._trade_manager.try_liquidate()
# Rebalance on -1 FOMC days.
if not self._rebalance_flag:
return
self._rebalance_flag = False
# Get history and average performance on -1 FOMC days.
history: DataFrame = self.history(self._market, start=self.time - relativedelta(months=self._period), end=self.time).unstack(level=0)
if history.empty:
return
returns: DataFrame = history.close.pct_change().dropna()
recent_dates: List[datetime.date] = [
d for d in self._FOMC_obj.Dates
if (self.time - relativedelta(months=self._period)).date() <= d <= self.time.date()
]
df_observed: DataFrame = returns.loc[returns.index.normalize().isin(recent_dates)]
traded_asset: Symbol = self._market if df_observed.mean()[0] <= self._threshold else self._cash
# Trade execution.
if slice.contains_key(traded_asset) and slice[traded_asset]:
self._trade_manager.add(traded_asset, True, slice[traded_asset].price)
def _trade(self) -> None:
if all(self.securities[symbol].get_last_data() for symbol in [self._market, self._cash]):
self._rebalance_flag = True