Overall Statistics
Total Orders
74
Average Win
1.19%
Average Loss
-0.82%
Compounding Annual Return
3.665%
Drawdown
5.000%
Expectancy
0.728
Start Equity
400000
End Equity
495374.89
Net Profit
23.844%
Sharpe Ratio
-0.117
Sortino Ratio
-0.042
Probabilistic Sharpe Ratio
4.374%
Loss Rate
30%
Win Rate
70%
Profit-Loss Ratio
1.46
Alpha
-0.012
Beta
0.078
Annual Standard Deviation
0.044
Annual Variance
0.002
Information Ratio
-0.563
Tracking Error
0.165
Treynor Ratio
-0.066
Total Fees
$414.90
Estimated Strategy Capacity
$5900000.00
Lowest Capacity Asset
VOO UPSZVZA9EQUD
Portfolio Turnover
2.88%
Drawdown Recovery
819
from AlgorithmImports import *
from pandas.tseries.offsets import BDay
from object_store_helper import ObjectStoreHelper
from typing import Any, List, Dict
from traded_strategy import TradedStrategy

# Algorithm notes:
# - Buy VOO 15 min before close on Monday if current price is at 10 day minumim at that time.
# - Liquidate position on Tuesday 15 min before close.

class MetatronTurnaroundTuesday(QCAlgorithm):

    _traded_strategy: TradedStrategy = TradedStrategy.SHORT_TERM_MIN

    _notional_value: float = 400_000
    _trade_exec_minute_offset: int = 15
    _history_period: int = 10
    _consecutive_days_period: int = 2

    _order_liquidation_date: Optional[datetime] = None

    # True -> Looking for trading everyday.
    # False -> Trading only at Monday.
    _every_day_trading_flag: bool = False
    _intraday_performance_filter_flag: bool = False

    if _traded_strategy == TradedStrategy.TRUNAROUND_TUESDAY:
        _every_day_trading_flag = False

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

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

        self.log(f'Strategy {self._traded_strategy} is selected')

        self._trade_flag: bool = False
        if self._every_day_trading_flag:
            self.schedule.on(
                self.date_rules.every_day(),
                self.time_rules.before_market_close(self._traded_asset, self._trade_exec_minute_offset), 
                self._selection
            )
        else:
            self.schedule.on(
                self.date_rules.every(DayOfWeek.MONDAY),
                self.time_rules.before_market_close(self._traded_asset, self._trade_exec_minute_offset), 
                self._selection
            )
        
        self.settings.daily_precise_end_time = False

    def on_data(self, slice: Slice) -> None:
        if self._order_liquidation_date:
            if self.portfolio.invested and self.time >= self._order_liquidation_date:
                self.log(f'Liquidation date: {self._order_liquidation_date}; Current date: {self.time}')
                self.liquidate()
                self._order_liquidation_date = None

        if not self._trade_flag:
            return
        self._trade_flag = False

        if slice.contains_key(self._traded_asset) and slice[self._traded_asset]:
            self.log(f'{self._traded_asset} present in algorithm')

            history: DataFrame = self.history(
                TradeBar, self._traded_asset, start=self.time - BDay(self._history_period), end=self.time
            ).unstack(level=0)

            if not history.empty:
                closes: DataFrame = history.resample('B').last().close[-self._history_period:]
                if len(closes) < self._history_period:
                    self.log(f'{self._traded_asset} history has not enough data points with lenght of {len(closes)} while {self._history_period} is required')
                    return
                
                trade_flag: bool = False
                self.log(f'Intraday performance filter set: {self._intraday_performance_filter_flag}')

                # Trade if there were last two consecutive down days.
                if self._traded_strategy == TradedStrategy.TRUNAROUND_TUESDAY:
                    return_series = closes.diff()[-self._consecutive_days_period:]
                    self.log(f'Recent daily return series: {return_series}')

                    if (return_series < 0).all().bool():
                        trade_flag = True

                elif self._traded_strategy == TradedStrategy.SHORT_TERM_MIN:
                    # Trade close at 10 day minimum.
                    close: float = closes.iloc[-1][0]
                    close_min: float = closes.min()[0]

                    if close == close_min:
                        self.log(f'Trade close is at {self._history_period} day minimum; Close: {close}; Min: {close_min}')

                        if self._intraday_performance_filter_flag:
                            intraday_history = history.loc[history.index.date == self.time.date()]
                            recent_close: float = intraday_history.close.iloc[-1][0]
                            previous_close: float = intraday_history.close.iloc[0][0]
                            if recent_close < previous_close:
                                self.log(f'Recent close is bellow previous close; recent close: {recent_close}; previous close: {previous_close}')
                                trade_flag = True
                            else:
                                self.log(f'Recent close is NOT bellow previous close; recent close: {recent_close}; previous close: {previous_close}')
                        else:
                            trade_flag = True
                    else:
                        self.log(f'Trade close is NOT at {self._history_period} day minimum; Close: {close}; Min: {close_min}')

                self.log(f'Trade flag set: {trade_flag}')

                if trade_flag:
                    self.log(f'Submitting market order')
                    self.market_order(
                        self._traded_asset, 
                        self._notional_value // slice[self._traded_asset].price,
                        tag='MarketOrder'
                    )
            else:
                self.log(f'{self._traded_asset} history is empty')
        else:
            self.log(f'{self._traded_asset} NOT present in algorithm')

    def on_order_event(self, orderEvent: OrderEvent) -> None:
        order_ticket: OrderTicker = self.transactions.get_order_ticket(orderEvent.order_id)
        symbol: Symbol = order_ticket.symbol

        if orderEvent.status == OrderStatus.FILLED:
            if 'MarketOrder' in order_ticket.tag:
                # Set date for liquidation.
                self._order_liquidation_date = self.securities[symbol].exchange.hours.get_next_trading_day(self.time)
                self.log(f'Liquidation date set: {self._order_liquidation_date}')

    def _selection(self) -> None:
        self.log(f'New selection/rebalance signal on {self.time.date()}')
        self._trade_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):
    TRUNAROUND_TUESDAY = 1
    SHORT_TERM_MIN = 2