| Overall Statistics |
|
Total Orders 150 Average Win 0.80% Average Loss -0.84% Compounding Annual Return 1.000% Drawdown 15.200% Expectancy 0.315 Start Equity 100000 End Equity 122453.47 Net Profit 22.453% Sharpe Ratio -0.338 Sortino Ratio -0.297 Probabilistic Sharpe Ratio 0.000% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 0.95 Alpha -0.015 Beta 0 Annual Standard Deviation 0.044 Annual Variance 0.002 Information Ratio -0.442 Tracking Error 0.164 Treynor Ratio -31.265 Total Fees $0.00 Estimated Strategy Capacity $95000.00 Lowest Capacity Asset UUP TQBX2PUC67OL Portfolio Turnover 0.50% |
# region imports
from AlgorithmImports import *
# endregion
class SymbolData():
def __init__(self, period: int) -> None:
self._cpi_mom: RollingWindow = RollingWindow[float](period)
self._cpi_mom_change: RollingWindow = RollingWindow[float](period)
self._rates: RollingWindow = RollingWindow[float](period + 1)
self._last_fed_rate: Optional[float] = None
self._last_value: int = 0
def rate_is_ready(self) -> bool:
return self._rates.is_ready
def is_ready(self) -> bool:
return self._cpi_mom_change.is_ready
def update_rate(self, value: float) -> None:
self._rates.add(value)
def rate_increase(self, period) -> bool:
observed_rates: List[float] = list(self._rates)[::-1][-period-1:]
current_trend = (
-1
if all(observed_rates[i] > observed_rates[i+1] for i in range(len(observed_rates) - 1))
else (
1 if all(observed_rates[i] < observed_rates[i+1] for i in range(len(observed_rates) - 1))
else self._last_value
)
)
self._last_value = current_trend
return True if current_trend == 1 else False
def update(self, cpi_value: float) -> None:
self._cpi_mom.add(cpi_value)
if self._cpi_mom.is_ready:
self._cpi_mom_change.add(np.diff(list(self._cpi_mom)[::-1])[0])
def inflation_increase(self) -> bool:
inflation_change: int = (
1
if all(x > 0 for x in list(self._cpi_mom_change))
else (
-1 if all(x < 0 for x in list(self._cpi_mom_change))
else self._last_value
)
)
self._last_value = inflation_change
return True if inflation_change == 1 else False
class TradeManager():
def __init__(self, algo: QCAlgorithm, symbol: Optional[Symbol] = None, quantity: int = 0) -> None:
self._algo: QCAlgorithm = algo
self._symbol: Optional[Symbol] = symbol
self._quantity: int = quantity
def _execute(self, symbol: Symbol, quantity: int) -> None:
if not self._algo.portfolio.invested:
self._algo.market_order(symbol, quantity)
self._quantity = quantity
self._symbol = symbol
else:
if self._symbol == symbol:
self._algo.market_order(self._symbol, quantity - self._quantity)
else:
self._algo.market_order(self._symbol, -self._quantity)
self._algo.market_order(symbol, quantity)
self._symbol = symbol
self._quantity = quantity
def _liquidate(self) -> None:
if self._symbol is not None:
self._algo.market_order(self._symbol, -self._quantity)
self._quantity = 0
class LastDateHandler():
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return LastDateHandler._last_update_date
class DailyCustomData(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return DailyCustomData._last_update_date
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 = DailyCustomData()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
if split[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 BLS_CPI(PythonData):
_last_update_date: datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return BLS_CPI._last_update_date
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/economic/BLS_INFLATION_AS_REPORTED.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = BLS_CPI()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[1], "%Y-%m-%d") #+ timedelta(days=1)
if split[-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 ShortTermRate(PythonData):
_last_update_date: datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return ShortTermRate._last_update_date
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/economic/3M_RATE.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = ShortTermRate()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%Y-%m-%d") #+ timedelta(days=1)
if split[-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# https://quantpedia.com/strategies/the-impact-of-the-inflation-on-the-performance-of-the-us-dollar/
#
# The investment universe for this strategy is focused on currency trading, explicitly involving the US Dollar (USD) and its performance against a basket of major
# global currencies. The individual instruments selected are primarily exchange-traded funds (ETFs) that track the USD, such as the Invesco DB US Dollar Index
# Bullish Fund (UUP) for long positions and the Invesco DB US Dollar Index Bearish Fund (UDN) for short positions. (The selection is based on the research paper’s
# emphasis on the USD’s response to inflation and interest rate dynamics.)
# (Data can be obtained from Yahoo Finance.)
# Introductory Recapitulation: The trading rules are based on several macroeconomic indicators and signals from the research paper. Inflation rates are monitored
# monthly, focusing on consecutive rises or declines to determine USD positioning. Additionally, the strategy incorporates Federal Reserve rate changes, 3-month
# treasury rates, and interest rate differentials. The methodology involves aligning these signals to determine the optimal buy and sell rules, prioritizing trades
# where multiple indicators suggest the same direction. The strategy does not rely on traditional technical indicators but instead uses macroeconomic data as its
# primary tool.
# Presented Variant Version: from 3.3. Finding 3 — Influence of the 3-Month Rate. 2 trading rules as follows:
# Inflation Acceleration Rule: If inflation rises for two straight months, a short position on the USD is taken. Conversely, a long position is initiated if
# inflation declines for two consecutive months.
# Fundamental Basis Translated to Execution Strategy: Going long USD is advantageous if inflation decelerates and the 3M rates decrease. Simultaneously, it’s a
# good idea to short USD if inflation rises and the 3M rates go down (which might again signal an erroneous central bank policy).
# Rebalancing & Weighting: Rebalance each month as new data comes in. Allocate the entire capital (100% weight) to only one of 2 assets.
# region imports
from AlgorithmImports import *
import data_tools
from dataclasses import dataclass
# endregion
class TheImpactOfTheInflationOnThePerformanceOfTheUSDollar(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2005, 1, 1)
self.set_cash(100_000)
leverage: int = 4
period: int = 2
self._traded_percentage: float = .5
self._FED_rate_period: int = 1
self._3m_rate_period: int = 2
self._uup: Symbol = self.add_equity('UUP', Resolution.DAILY).symbol
self._udn: Symbol = self.add_equity('UDN', Resolution.DAILY).symbol
for symbol in [self._uup, self._udn]:
self.securities[symbol].set_fee_model(ConstantFeeModel(0))
self._data: Dict[Symbol, data_tools.SymbolData] = {}
self._trade_manager: Dict[int, HoldingItem] = {}
self._FED_rate: Symbol = self.add_data(data_tools.DailyCustomData, 'DFEDTAR', Resolution.DAILY).symbol # Discontinued on 2008-12-15
fed_rate_tickers: List[str] = ['DFEDTARU', 'DFEDTARL']
self._FED_rate_symbols: List[Symbol] = [
self.add_data(data_tools.DailyCustomData, ticker, Resolution.DAILY).symbol for ticker in fed_rate_tickers
]
self._short_term_rate: Security = self.add_data(data_tools.ShortTermRate, '3M_rate', Resolution.DAILY)
self._CPI: Security = self.add_data(data_tools.BLS_CPI, 'CPI', Resolution.DAILY)
self._short_term_rate._data = data_tools.SymbolData(period)
self._CPI._data = data_tools.SymbolData(period)
self._FED_rate_data: data_tools.SymbolData = data_tools.SymbolData(period)
market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol
self._rebalance_flag: bool = False
self.schedule.on(
self.date_rules.month_start(market),
self.time_rules.after_market_open(market),
self._rebalance
)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
def on_data(self, slice: Slice) -> None:
# Monthly rebalance.
if not self._rebalance_flag:
return
self._rebalance_flag = False
# Use discontinued FED rate first.
current_FED_rate_symbol: List[Symbol] = (
[self._FED_rate]
if self.securities[self._FED_rate].get_last_data()
and self.time.date() <= data_tools.LastDateHandler.get_last_update_date()[self._FED_rate]
else self._FED_rate_symbols
)
current_FED_rate: float = np.mean([self.securities[symbol].get_last_data().value for symbol in current_FED_rate_symbol])
# Check if data is still coming.
if any(self.securities[symbol].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[symbol] for symbol in self._FED_rate_symbols + [self._CPI.symbol, self._short_term_rate.symbol]):
self.log('Data for CPI stopped coming.')
return
if not all(security.get_last_data() for security in [self._short_term_rate, self._CPI]):
return
# Update data.
self._FED_rate_data.update_rate(current_FED_rate)
self._short_term_rate._data.update_rate(self._short_term_rate.get_last_data().price)
self._CPI._data.update(self._CPI.get_last_data().price)
if not self._CPI._data.is_ready() or not self._short_term_rate._data.rate_is_ready() or not self._FED_rate_data.rate_is_ready():
self.log("Data not ready yet.")
return
# Get signal on both models.
first_model: Symbol|None = (
self._uup
if not self._CPI._data.inflation_increase()
and not self._FED_rate_data.rate_increase(self._FED_rate_period)
else (
self._udn
if self._CPI._data.inflation_increase()
and not self._FED_rate_data.rate_increase(self._FED_rate_period)
else None
)
)
second_model: Symbol|None = (
self._uup
if not self._CPI._data.inflation_increase()
and not self._short_term_rate._data.rate_increase(self._3m_rate_period)
else (
self._udn
if self._CPI._data.inflation_increase()
and not self._short_term_rate._data.rate_increase(self._3m_rate_period)
else None
)
)
if not all(self.securities[symbol].get_last_data() for symbol in [self._uup, self._udn]):
return
# Trade execution.
for i, symbol in enumerate([first_model, second_model]):
if i not in self._trade_manager:
self._trade_manager[i] = data_tools.TradeManager(self)
if symbol:
if slice.contains_key(symbol) and slice[symbol]:
quantity: int = self.portfolio.total_portfolio_value * self._traded_percentage // slice[symbol].close
self._trade_manager[i]._execute(symbol, quantity)
else:
self._trade_manager[i]._liquidate()
def _rebalance(self) -> None:
self._rebalance_flag = True