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