Created with Highcharts 12.1.2Equity200620082010201220142016201820202022202420260100k200k300k-20-10000.0250.0501025M50M02.5M5M050100
Overall Statistics
Total Orders
259
Average Win
1.03%
Average Loss
-0.27%
Compounding Annual Return
5.169%
Drawdown
18.400%
Expectancy
2.321
Start Equity
75000
End Equity
207990.46
Net Profit
177.321%
Sharpe Ratio
0.222
Sortino Ratio
0.208
Probabilistic Sharpe Ratio
0.706%
Loss Rate
31%
Win Rate
69%
Profit-Loss Ratio
3.82
Alpha
0.015
Beta
-0
Annual Standard Deviation
0.068
Annual Variance
0.005
Information Ratio
-0.252
Tracking Error
0.17
Treynor Ratio
-32.101
Total Fees
$531.53
Estimated Strategy Capacity
$4900000.00
Lowest Capacity Asset
GLD T3SKPOF94JFP
Portfolio Turnover
0.60%
# region imports
from AlgorithmImports import *
# endregion

class SymbolData():
    def __init__(self, period: int, mom_period: int) -> 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

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
# region imports
from AlgorithmImports import *
import data_tools
# endregion

class InflationforGoldandTreasuries(QCAlgorithm):

    _notional_value: int = 75_000
    _trade_exec_minute_offset: int = 15
    
    _period: int = 2
    _mom_period: int = 12

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

        leverage: int = 1

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

        self._gld, self._ief, self._shy = [
            self.add_equity(ticker, Resolution.MINUTE, 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] = {}

        self._price_symbols: List[Symbol] = [self._gld, self._ief]
        for symbol in self._price_symbols:
            self._data[symbol] = data_tools.SymbolData(self._period, self._mom_period)
        self._data[self._CPI] = data_tools.SymbolData(self._period, self._mom_period)

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

        market: Symbol = self.add_equity('SPY', Resolution.MINUTE).symbol

        self.set_warm_up(self._mom_period*30, Resolution.DAILY)

        self._rebalance_flag: bool = False
        self.schedule.on(
            self.date_rules.month_start(market),
            self.time_rules.before_market_close(market, self._trade_exec_minute_offset),
            self._rebalance
        )

    def on_data(self, slice: Slice) -> None:
        # Update data when new CPI data arrives.
        if slice.contains_key(self._CPI) and slice[self._CPI]:
            self.log(f'new CPI data arrived - CPI: {slice[self._CPI].value}')

            # store CPI
            self._data[self._CPI].update(slice[self._CPI].value)

            # store asset prices
            for s in self._price_symbols:
                if self.securities.contains_key(s) and self.securities[s].get_last_data():
                    self.log(f'{s} asset price data updated')
                    self._data[s].update_price(self.securities[s].get_last_data().close)
        
        if self.is_warming_up: return

        # Monthly rebalance.
        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        self.log('new monthly rebalance')

        price_ready: Dict[str, bool] = { s.value : self._data[s].price_is_ready() for s in self._price_symbols }
        if not all(price_ready.values()):
            self.log(f'price data not ready yet - {price_ready}')
            return
        if not self._data[self._CPI].is_ready():
            self.log(f'CPI data not ready yet')
            return

        # Check if data is still coming.
        CPI_last_update_date: datetime.date = data_tools.BLS_CPI.get_last_update_date()
        if self.securities[self._CPI].get_last_data() and self.time.date() > CPI_last_update_date:
            self.log(f'data for CPI stopped coming in; last update date: {CPI_last_update_date}')
            self.liquidate()
            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.
        if not self.portfolio[traded_asset].invested:
            self.log(f'liquidating existing holdings')
            self.liquidate()
        
        # Calculate additional quantity.
        quantity: int = self._notional_value // self.securities[traded_asset].price
        additional_q: float = quantity - self.portfolio[traded_asset].quantity
        self.log(f'additional quantity for {traded_asset.value}: {additional_q}; existing quantity: {self.portfolio[traded_asset].quantity}')
        self.market_order(traded_asset, additional_q)

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