Overall Statistics
Total Orders
49
Average Win
19.17%
Average Loss
-2.18%
Compounding Annual Return
10.936%
Drawdown
19.700%
Expectancy
5.924
Start Equity
100000
End Equity
1331827.89
Net Profit
1231.828%
Sharpe Ratio
0.517
Sortino Ratio
0.536
Probabilistic Sharpe Ratio
3.003%
Loss Rate
29%
Win Rate
71%
Profit-Loss Ratio
8.78
Alpha
0
Beta
0
Annual Standard Deviation
0.114
Annual Variance
0.013
Information Ratio
0.712
Tracking Error
0.114
Treynor Ratio
0
Total Fees
$1017.61
Estimated Strategy Capacity
$840000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
0.54%
# region imports
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
# endregion

class RecessionSignal(QCAlgorithm):

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

        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.daily_precise_end_time = False

        period: int = 10 * 21
        self._market: Symbol = self.add_equity("SPY", Resolution.MINUTE).symbol
        self._sma: SimpleMovingAverage = self.SMA(self._market, period, Resolution.DAILY)

        # safe asset
        self._safe_asset: Symbol = self.add_equity("IEF", Resolution.MINUTE).symbol

        self.set_warm_up(period, Resolution.DAILY)
        
        self._indpro: Symbol = self.add_data(YOYData, 'INDPRO_YOY', Resolution.DAILY).symbol
        self._rrsfs: Symbol = self.add_data(YOYData, 'RRSFS_YOY', Resolution.DAILY).symbol

        self._rebalance_flag: bool = False
        self.schedule.on(self.date_rules.month_end(self._market), self.time_rules.before_market_close(self._market, 1), self._selection)
    
    def on_data(self, slice: Slice) -> None:
        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        if self.is_warming_up: return
        if not self._sma.is_ready: return
        if not slice.contains_key(self._market) or not slice.contains_key(self._safe_asset): return

        traded_asset: Symbol = self._market

        # check end of custom datasets
        last_update_date: datetime.date = YOYData.get_last_update_date()
        if not all(self.securities[symbol].get_last_data() and symbol.value in last_update_date and self.time.date() < last_update_date[symbol.value] \
            for symbol in [self._indpro, self._rrsfs]):
            # signal no recession in case there's an end of custom data
            pass
        else:
            # signal recession
            if any(self.securities[symbol].price < 0. for symbol in [self._indpro, self._rrsfs]):
                if slice[self._market].value <= self._sma.current.value:
                    traded_asset = self._safe_asset
        
        if not self.portfolio[traded_asset].invested:
            self.set_holdings(traded_asset, 1., True)

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

class YOYData(PythonData):
    _last_update_date: Dict[str, datetime.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.REMOTE_FILE, FileFormat.CSV)

    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return YOYData._last_update_date

    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, is_live_mode: bool) -> BaseData:
        data: YOYData = YOYData()
        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") + relativedelta(months=1)
        data.value = float(split[1])

        if config.symbol.value not in YOYData._last_update_date:
            YOYData._last_update_date[config.symbol.value] = datetime(1,1,1).date()

        if data.time.date() > YOYData._last_update_date[config.symbol.value]:
            YOYData._last_update_date[config.symbol.value] = data.time.date()
        
        return data