Overall Statistics
Total Orders
325
Average Win
1.19%
Average Loss
-0.88%
Compounding Annual Return
4.451%
Drawdown
18.600%
Expectancy
0.192
Start Equity
400000
End Equity
518176.61
Net Profit
29.544%
Sharpe Ratio
0.021
Sortino Ratio
0.015
Probabilistic Sharpe Ratio
6.760%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.35
Alpha
0
Beta
0
Annual Standard Deviation
0.065
Annual Variance
0.004
Information Ratio
0.505
Tracking Error
0.065
Treynor Ratio
0
Total Fees
$6428.33
Estimated Strategy Capacity
$5400000.00
Lowest Capacity Asset
TLT SGNKIKYGE9NP
Portfolio Turnover
13.30%
Drawdown Recovery
735
# Aggregate strategy:
#   1. TLT Calendar:
#       TLT is traded 2 days before the EOM. 
#       Signal is set on a third day before the EOM 15 minutes before market close and trade is held until EOM (last traded day of the month, 15 minutes before market close).
#
#   2. TLT Reversal Model:
#       TLT is bought after 3 down days (15 minutes before market close) and is held for 3 days.

from AlgorithmImports import *
from pandas.tseries.offsets import BDay
from pandas.tseries.offsets import BMonthEnd
from object_store_helper import ObjectStoreHelper
from typing import Any, List, Dict
from traded_strategy import TradedStrategy

class MetatronTLTCalendarPlusReversalModel(QCAlgorithm):
    
    _notional_value: float = 400_000
    _trade_calendar: bool = True
    _trade_reversal_model: bool = True
    _trade_exec_minute_offset: int = 15
    
    # Calendar Strategy
    _eom_trigger_day_offset: int = 2

    # Reversal Strategy
    _reversal_days_lookback: int = 3
    _holding_period: int = 3

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

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

        # initialize Object Store Helper
        self.object_store_helper: ObjectStoreHelper = ObjectStoreHelper(
            self,
            "Metatron/1_TLT_Calendar_plus_Reversal_Model/trade_signal.json"
        )

        # load state from Object Store if available
        signal_state: Dict = self.object_store_helper.load_state()

        self._trade_signal: Dict[TradedStrategy, bool] = signal_state['trade_signal']
        self._reversal_model_days_held = signal_state['reversal_model_days_held']

        self._traded_asset: Symbol = self.add_equity('TLT', Resolution.MINUTE).symbol

        # schedule functions
        self._month_close_flag: bool = False
        self._day_close_flag: bool = False

        self.schedule.on(
            self.date_rules.every_day(self._traded_asset), 
            self.time_rules.before_market_close(self._traded_asset, self._trade_exec_minute_offset), 
            self._before_eod
        )
        self.schedule.on(
            self.date_rules.month_end(self._traded_asset), 
            self.time_rules.before_market_close(self._traded_asset, self._trade_exec_minute_offset), 
            self._month_close
        )
    
    def on_data(self, slice: Slice) -> None:
        if not self._day_close_flag:
            return
        
        self.log(f'Day close flag set')
        self._day_close_flag = False

        # reset Calendar Strategy signal
        if self._trade_calendar:
            if self._month_close_flag:
                self.log(f'Month close signal check')
                self._month_close_flag = False

                if self._trade_signal[TradedStrategy.CALENDAR]:
                    self.log(f'Signal reset for {TradedStrategy.CALENDAR}')
                    self._trade_signal[TradedStrategy.CALENDAR] = False
                                
        # reset Reversal Model signal
        if self._trade_reversal_model:
            if self._trade_signal[TradedStrategy.REVERSAL_MODEL]:
                self._reversal_model_days_held += 1
                self.log(f'Days held counter incremented for {TradedStrategy.REVERSAL_MODEL}. Days held: {self._reversal_model_days_held}')

                if self._reversal_model_days_held == self._holding_period:
                    self.log(f'Signal reset for {TradedStrategy.REVERSAL_MODEL}. Days held count reached {self._reversal_model_days_held}/{self._holding_period} days')
                    self._reversal_model_days_held = 0
                    self._trade_signal[TradedStrategy.REVERSAL_MODEL] = False

        # close position
        if all(not signal for signal in self._trade_signal.values()):
            if self.portfolio[self._traded_asset].invested:
                self.log(f'No active signal, liquidating portfolio')
                self.liquidate(self._traded_asset)
                
        if slice.contains_key(self._traded_asset) and slice[self._traded_asset]:
            self.log(f'Symbol {self._traded_asset} data present in the algorithm')
            if self._trade_reversal_model:
                # Reversal Model Strategy
                history: DataFrame = self.history[TradeBar](
                    self._traded_asset, 
                    self._reversal_days_lookback, 
                    Resolution.DAILY
                )
                closes: List[float] = [bar.close for bar in history] + [slice[self._traded_asset].close]
                is_in_consecutive_downtrend: bool = all(closes[i] > closes[i + 1] for i in range(len(closes) - 1))
                if is_in_consecutive_downtrend:
                    if not self._trade_signal[TradedStrategy.REVERSAL_MODEL]:
                        self.log(f'Positive signal for {TradedStrategy.REVERSAL_MODEL}')
                        self._trade_signal[TradedStrategy.REVERSAL_MODEL] = True

            if self._trade_calendar:
                # Calendar Strategy
                offset = BMonthEnd()
                last_day_of_month: datetime = offset.rollforward(self.time)

                # find last trading day of the month
                while not self.securities[self._traded_asset].exchange.hours.is_date_open(last_day_of_month):
                    last_day_of_month = last_day_of_month - timedelta(days=1)

                # add one day to account for before the close execution
                trigger_day: datetime = last_day_of_month - BDay(self._eom_trigger_day_offset)
                if self.time == trigger_day:
                    if not self._trade_signal[TradedStrategy.CALENDAR]:
                        self.log(f'Positive signal for {TradedStrategy.CALENDAR}')
                        self._trade_signal[TradedStrategy.CALENDAR] = True
        else:
            self.log(f'Symbol {self._traded_asset} data NOT present in the algorithm')

        if any(signal for signal in self._trade_signal.values()):
            if not self.portfolio[self._traded_asset].invested:
                self.log(f'Trade executed for {self._traded_asset}')
                quantity: int = self._notional_value // self.securities[self._traded_asset].ask_price
                self.market_order(self._traded_asset, quantity)

        # NOTE do not save portfolio state for now, liquidate live positions every time live restart is needed
        # save state to Object Store
        # signal_state: Dict[str, Any] = {
        #     "trade_signal": {
        #         TradedStrategy.CALENDAR.name: self._trade_signal[TradedStrategy.CALENDAR],
        #         TradedStrategy.REVERSAL_MODEL.name: self._trade_signal[TradedStrategy.REVERSAL_MODEL]
        #     },
        #     "reversal_model_days_held": self._reversal_model_days_held
        # }
        # self.object_store_helper.save_state(signal_state)        

    def _before_eod(self) -> None:
        self._day_close_flag = True
    
    def _month_close(self) -> None:
        self._month_close_flag = True
# region imports
from AlgorithmImports import *
import json
from traded_strategy import TradedStrategy
# endregion

class ObjectStoreHelper:
    def __init__(
        self, 
        algorithm: QCAlgorithm, 
        path: str
    ) -> None:
        """
        Initializes ObjectStoreHelper with reference to the algorithm instance.
        """
        self._algorithm: QCAlgorithm = algorithm
        self._path: str = path

    def save_state(self, state: Dict) -> None:
        """
        Saves a dictionary `state` to the Object Store as JSON.
        """
        if not self._algorithm.live_mode:
            return

        json_data = json.dumps(state)
        self._algorithm.object_store.save(self._path, json_data)
        self._algorithm.log(f"Saved state to Object Store: {json_data}")

    def load_state(self) -> Dict:
        """
        Loads a JSON string from the Object Store and returns it as a dictionary.
        """
        if self._algorithm.object_store.contains_key(self._path) and self._algorithm.live_mode:
            json_data = self._algorithm.object_store.read(self._path)
            if json_data:
                self._algorithm.log(f"Loaded state from Object Store: {json_data}")
                result: Dict = json.loads(json_data)
                result['trade_signal'] = {TradedStrategy._member_map_[key]: value for key, value in result['trade_signal'].items() if key in TradedStrategy._member_map_}
                return result
        else:
            return {
                'trade_signal': {
                    TradedStrategy.CALENDAR: False,
                    TradedStrategy.REVERSAL_MODEL: False
                },
                'reversal_model_days_held': 0
            }

        return {}
# region imports
from AlgorithmImports import *
from enum import Enum
# endregion

class TradedStrategy(Enum):
    CALENDAR = 1
    REVERSAL_MODEL = 2