Created with Highcharts 12.1.2Equity200620082010201220142016201820202022202420260250k500k-50-25000.050.10120100M200M010M242526
Overall Statistics
Total Orders
149
Average Win
3.21%
Average Loss
-1.34%
Compounding Annual Return
7.571%
Drawdown
29.200%
Expectancy
1.145
Start Equity
100000
End Equity
437798.05
Net Profit
337.798%
Sharpe Ratio
0.307
Sortino Ratio
0.302
Probabilistic Sharpe Ratio
0.276%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
2.39
Alpha
0.036
Beta
0.002
Annual Standard Deviation
0.118
Annual Variance
0.014
Information Ratio
-0.111
Tracking Error
0.195
Treynor Ratio
16.232
Total Fees
$1260.70
Estimated Strategy Capacity
$46000000.00
Lowest Capacity Asset
GLD T3SKPOF94JFP
Portfolio Turnover
1.26%
# region imports
from AlgorithmImports import *
# endregion

class SymbolData():
    def __init__(self, period: int, mom_period) -> None:
        self._prices: RollingWindow = RollingWindow[float](mom_period) 
        self._cpi_mom_change: RollingWindow = RollingWindow[float](period)
        self._last_inflation_change: int = 0
    
    def price_is_ready(self) -> bool:
        return self._prices.is_ready

    def is_ready(self) -> bool:
        return self._cpi_mom_change.is_ready
    
    def update_price(self, price: float) -> None:
        self._prices.add(price)

    def update(self, cpi_value: float) -> None:
        self._cpi_mom_change.add(cpi_value)

    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_inflation_change
            )
        )
        self._last_inflation_change = inflation_change
        return True if inflation_change == 1 else False

    def positive_momentum(self) -> bool:
        momentum: float = self._prices[0] / self._prices[self._prices.count - 1] - 1
        return True if momentum > 0 else False

    # def get_inflation_trend(self) -> List[int]:
    #     return [self.get_inflation_change(), self.get_momentum()]

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 data.Time.date() > BLS_CPI._last_update_date:
            BLS_CPI._last_update_date = data.Time.date()
        
        return data
# https://quantpedia.com/strategies/using-inflation-data-for-systematic-gold-and-treasury-investment-strategies/
# 
# The investment universe for this strategy includes gold, treasury bonds, and cash equivalents, explicitly using the GLD, IEF, and SHY ETFs.
# (Asset Selection Explanation: These instruments are selected based on their sensitivity to inflation dynamics as discussed in the research paper. Gold is chosen 
# for its traditional role as an inflation hedge, while treasury bonds are included due to their complex relationship with inflation and interest rates. The cash 
# equivalent (SHY) is used for capital preservation when no other assets show favorable performance.)
# (Data set includes the Consumer Price Index for All Urban Consumers: All Items in U.S. City Average (CPIAUCSL) from FRED (Federal Reserve Bank of St. Louis) and 
# the U.S. Bureau of Labor Statistics inflation data. Price data from Yahoo Finance.)
# Final Model Trading Strategy Variant: The strategy employs inflation data and momentum indicators to guide trading decisions. Inflation regimes are identified 
# using the rate of change in inflation metrics like the CPI. If the inflation rate increases, the regime is classified as Inflation UP; otherwise, it’s Inflation 
# DOWN. A 12-month momentum indicator is calculated for each asset to assess its performance trend.
# Buy and sell rules are based on these signals:
# In an Inflation UP and Momentum UP regime, buy GLD (if it has positive momentum),
# in an Inflation DOWN and Momentum UP regime, buy the treasury bond ETF IEF (it has positive momentum),
# if neither condition is fulfilled, allocate capital to SHY.
# Rebalancing & Weighting: The strategy involves monthly rebalancing to adjust positions based on the latest inflation and momentum signals. Position size assumes 
# full allocation to one asset only at the time.

# region imports
from AlgorithmImports import *
import data_tools
# endregion

class UsingInflationDataForSystematicGoldAndTreasuryInvestmentStrategies(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2005, 1, 1)
        self.set_cash(100_000)

        leverage: int = 4
        period: int = 2
        mom_period: int = 12

        tickers: List[str] = ['GLD', 'IEF', 'SHY']

        self._gld, self._ief, self._shy = [
            self.add_equity(ticker, Resolution.DAILY, leverage=leverage).symbol for ticker in tickers
        ]
        self._CPI: Symbol = self.add_data(data_tools.BLS_CPI, 'BLSCPI', Resolution.DAILY).symbol

        self._data: Dict[Symbol, data_tools.SymbolData] = {}

        for symbol in [self._gld, self._ief, self._CPI]:
            self._data[symbol] = data_tools.SymbolData(period, mom_period)

        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.

        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.at(0, 0),
            self._rebalance
        )

    def on_data(self, slice: Slice):
        # Monthly rebalance.
        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        # Check if data is still coming.
        if self.securities[self._CPI].get_last_data() and self.time.date() > data_tools.BLS_CPI.get_last_update_date():
            self.log('Data for CPI stopped coming.')
            return

        if not all(self.securities[symbol].get_last_data() for symbol in [self._gld, self._ief, self._shy, self._CPI]):
            return

        # Update data.
        for symbol, symbol_data in self._data.items():
            if symbol == self._CPI:
                symbol_data.update(self.securities[symbol].get_last_data().price)
            else:
                symbol_data.update_price(self.securities[symbol].get_last_data().price)

        if not all(self._data[symbol].price_is_ready() for symbol in [self._gld, self._ief]) and not self._data[self._CPI].is_ready():
            return

        # Trade asset decision.
        traded_asset: Symbol = (
            self._gld 
            if self._data[self._CPI].inflation_increase() 
            and self._data[self._gld].positive_momentum()
            else (
                self._ief 
                if not self._data[self._CPI].inflation_increase()
                and self._data[self._ief].positive_momentum()
                else self._shy
            )
        )

        # Trade execution.
        self.set_holdings(traded_asset, 1, True)

    def _rebalance(self) -> None:
        self._rebalance_flag = True