Overall Statistics
Total Orders
51
Average Win
1.98%
Average Loss
-0.29%
Compounding Annual Return
-19.713%
Drawdown
25.600%
Expectancy
-0.271
Start Equity
1000000
End Equity
801905.08
Net Profit
-19.809%
Sharpe Ratio
-1.731
Sortino Ratio
-2.256
Probabilistic Sharpe Ratio
0.253%
Loss Rate
91%
Win Rate
9%
Profit-Loss Ratio
6.78
Alpha
0
Beta
0
Annual Standard Deviation
0.108
Annual Variance
0.012
Information Ratio
-1.237
Tracking Error
0.108
Treynor Ratio
0
Total Fees
$136.59
Estimated Strategy Capacity
$570000.00
Lowest Capacity Asset
OEF RZ8CR0XXNOF9
Portfolio Turnover
1.18%
# region imports
from AlgorithmImports import *
# endregion


class VIXAlphaModel(AlphaModel):

    _symbols = []

    def __init__(self, algorithm, lookback_days, long_percentile, short_percentile):
        self._vix = algorithm.add_data(CBOE, "VIX", Resolution.DAILY).symbol
        self._window = RollingWindow[float](lookback_days)
        self._long_percentile = long_percentile
        self._short_percentile = short_percentile

    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        if data.contains_key(self._vix):
            # Get the current VIX value
            price = data[self._vix].price

            # Get the trailing VIX values
            self._window.add(price)
            if not self._window.is_ready:
                return []
            history_close = [i for i in self._window]

            # Check if the current VIX value is in an area of extreme relative to trailing values
            if price > np.percentile(history_close, self._long_percentile):
                direction = InsightDirection.UP
            elif price < np.percentile(history_close, self._short_percentile):
                direction = InsightDirection.DOWN
            else:
                return []
            # Emit insights if the current VIX value is extreme
            return [Insight.price(symbol, timedelta(365), direction) for symbol in self._symbols]
        return []

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        for security in changes.added_securities:
            self._symbols.append(security.symbol)
# region imports
from AlgorithmImports import *

from alpha import VIXAlphaModel
# endregion


class VIXPredictsStockIndexReturns(QCAlgorithm):

    _undesired_symbols_from_previous_deployment = []
    _checked_symbols_from_previous_deployment = False
    _previous_direction_by_symbol = {}
    _week = -1

    def initialize(self):
        self.set_start_date(2023, 3, 1)
        self.set_end_date(2024, 3, 1)
        self.set_cash(1_000_000)

        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)

        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
        self.add_universe_selection(ManualUniverseSelectionModel([Symbol.create("OEF", SecurityType.EQUITY, Market.USA)]))

        lookback_days = self.get_parameter("lookback_days", 504)
        self.add_alpha(VIXAlphaModel(
            self, 
            lookback_days,
            self.get_parameter("long_percentile", 90),
            self.get_parameter("short_percentile", 10)
        ))

        self.settings.rebalance_portfolio_on_insight_changes = False
        self.settings.rebalance_portfolio_on_security_changes = False
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(self._rebalance_func))

        self.add_risk_management(NullRiskManagementModel())
        
        self.set_execution(ImmediateExecutionModel())
        
        self.set_warm_up(timedelta(lookback_days*2))


    def _rebalance_func(self, time):
        # Rebalance when all of the following are true:
        #  - Not warming up
        #  - There is QuoteBar data in the current slice
        #  - It's a new week or the insight direction has changed
        direction_by_symbol = {insight.symbol: insight.direction for insight in self.insights}
        if (not self.is_warming_up and self.current_slice.quote_bars.count > 0 and 
            (direction_by_symbol != self._previous_direction_by_symbol or self.time.date().isocalendar()[1] != self._week)):
            self._week = self.time.date().isocalendar()[1]
            self._previous_direction_by_symbol = direction_by_symbol
            return time
        return None
        

    def on_data(self, data):
        # Exit positions that aren't backed by existing insights.
        # If you don't want this behavior, delete this method definition.
        if not self.is_warming_up and not self._checked_symbols_from_previous_deployment:
            for security_holding in self.portfolio.values():
                if not security_holding.invested:
                    continue
                symbol = security_holding.symbol
                if not self.insights.has_active_insights(symbol, self.utc_time):
                    self._undesired_symbols_from_previous_deployment.append(symbol)
            self._checked_symbols_from_previous_deployment = True
        
        for symbol in self._undesired_symbols_from_previous_deployment:
            if self.is_market_open(symbol):
                self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired")
                self._undesired_symbols_from_previous_deployment.remove(symbol)