| Overall Statistics |
|
Total Orders 100 Average Win 6.94% Average Loss -3.82% Compounding Annual Return 8.668% Drawdown 44.500% Expectancy 0.712 Start Equity 100000 End Equity 499879.01 Net Profit 399.879% Sharpe Ratio 0.324 Sortino Ratio 0.341 Probabilistic Sharpe Ratio 0.181% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 1.82 Alpha 0.039 Beta 0.167 Annual Standard Deviation 0.153 Annual Variance 0.023 Information Ratio -0.055 Tracking Error 0.201 Treynor Ratio 0.297 Total Fees $466.07 Estimated Strategy Capacity $820000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 1.27% |
# region imports
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
from pandas.tseries.offsets import BDay
# endregion
class LastDateHandler():
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return LastDateHandler._last_update_date
# Source: https://trends.google.com/trends/explore?date=all&geo=US&q=S%26P%20500&hl=sk
class GoogleSearchVolume(PythonData):
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/google_search/{config.Symbol.Value}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = GoogleSearchVolume()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split: str = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m") + relativedelta(months=1) + BDay(1)
data.Value = float(split[1])
if config.Symbol not in LastDateHandler._last_update_date:
LastDateHandler._last_update_date[config.Symbol] = datetime(1,1,1).date()
if data.Time.date() > LastDateHandler._last_update_date[config.Symbol]:
LastDateHandler._last_update_date[config.Symbol] = data.Time.date()
return data
class UNRATE(PythonData):
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/economic/{config.Symbol.Value}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = UNRATE()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
if split[1] == '.':
return None
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + relativedelta(months=1) + timedelta(days=10)
data.Value = float(split[1])
if config.Symbol not in LastDateHandler._last_update_date:
LastDateHandler._last_update_date[config.Symbol] = datetime(1,1,1).date()
if data.Time.date() > LastDateHandler._last_update_date[config.Symbol]:
LastDateHandler._last_update_date[config.Symbol] = data.Time.date()
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))# https://quantpedia.com/strategies/google-trends-unemployment-market-timing-strategy/
#
# This strategy`s investment universe is centered on one instrument: the S&P 500 index (ETF SPY or CFD).
# (Monthly and seasonally adjusted U.S. civilian unemployment rate data were obtained from the Federal Reserve Bank of St. Louis. Google Search query volumes
# are from Google Trends.)
# General Summary: The strategy is based on macroeconomic signals (like unemployment rates, which we selected as our reported version, which gave the most
# significant increase in out-of-sample forecasting accuracy) derived from Google Trends data. The methodology involves predicting the change in the unemployment
# rate for the upcoming month. So, this investment strategy is based on unemployment predictions using Google Trends data: Consider a variant of the Google
# Trends strategy utilizing the search volume of the term “laid off.” The strategy involves acting on the predicted month-over-month change in U.S. unemployment
# rates (∆UNEMt).
# Model and Variable Selection: the UNEMt (UNEMt is predicted response variable) forecasting models are based on a linear regression formulation (Eq. 2) with
# an additional independent variable Xt-1, the contemporaneous search volumes. So it includes a lagged autoregressive component and one lag of the exogenous
# variables (Xt-1 i ), which in this study will be the Google Search volumes for a particular search term or category i = laid off.
# Strategy Execution: Investment decisions are made 15 trading days before the government data release. They are based on prediction models that include one
# autoregressive lag (UNEMt-1) and one lag of monthly Google search volumes (Xt-1 laid off) as explanatory variables for UNEMt. The buy and sell rules are
# as follows:
# If the expected change in the unemployment rate is negative, indicating a decrease, the strategy buys the S&P 500 on close.
# If the expected change is positive, a second-order criterion is applied:
# buy if the rate of increase is slowing compared to the previous period, and
# short sell if the growth rate is accelerating
# Weighting & Rebalancing: The strategy involves rebalancing positions monthly, aligned with the timing of unemployment data releases. Only one asset is traded,
# so the whole position size is taken from the allocated portfolio.
# region imports
from AlgorithmImports import *
import data_tools
from dateutil.relativedelta import relativedelta
import statsmodels.api as sm
from typing import List, Dict
# endregion
class GoogleTrendsUnemploymentMarketTimingStrategy(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2006, 1, 1)
self.set_cash(100_000)
period: int = 36
month_period: int = 21
# Source: https://trends.google.com/trends/explore?date=all&geo=US&q=laid%20off&hl=en-GB
self._GSV: Symbol = self.add_data(data_tools.GoogleSearchVolume, 'LAID_OFF', Resolution.Daily).symbol
# Source: https://fred.stlouisfed.org/series/UNRATE
self._UNRATE: Symbol = self.AddData(data_tools.UNRATE, 'UNRATE', Resolution.Daily).Symbol
self._data: Dict[Symbol, RollingWindow] = {symbol: RollingWindow[float](period) for symbol in [self._GSV, self._UNRATE]}
self._traded_asset: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol
self.set_warm_up(timedelta(days=period * month_period), Resolution.DAILY)
self.settings.minimum_order_margin_portfolio_percentage = 0
self.settings.daily_precise_end_time = False
def on_data(self, slice: Slice) -> None:
# Check if data is still coming.
if any(self.securities[x].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[x] for x in [self._GSV, self._UNRATE]):
self.log('Data stopped comming for custom data.')
self.liquidate()
return
# Save data for regression.
for symbol, regression_data in self._data.items():
if slice.contains_key(symbol) and slice[symbol]:
regression_data.add(slice[symbol].price)
if self.is_warming_up:
return
if not all(regression_data.is_ready for regression_data in list(self._data.values())):
return
# Rebalance when google search volume data arrives.
if slice.contains_key(self._GSV) and slice[self._GSV]:
y: np.ndarray = np.array(list(self._data[self._UNRATE])[::-1])[1:]
x: np.ndarray = np.array([pd.Series(list(self._data[self._UNRATE])[::-1]).shift(1).dropna().values, np.array(list(self._data[self._GSV])[::-1])[:-1]])
model = self.multiple_linear_regression(x, y)
predict_y: float = model.predict(sm.add_constant(np.append(y[-1], x[1][-1]).reshape(1,2), has_constant='add'))[0]
trade_direction: bool = 1 if predict_y < y[-1] else False
if not trade_direction:
lastperiod_diff: np.ndarray = np.diff(np.append(y[-2:], predict_y))
trade_direction: int = 1 if lastperiod_diff[-1] - lastperiod_diff[0] < 0 else -1
self.set_holdings(self._traded_asset, trade_direction, True)
def multiple_linear_regression(self, x: np.ndarray, y: np.ndarray):
x: np.ndarray = np.array(x).T
x = sm.add_constant(x)
result = sm.OLS(endog=y, exog=x).fit()
return result