| Overall Statistics |
|
Total Orders 1699 Average Win 0.10% Average Loss -0.04% Compounding Annual Return 11.655% Drawdown 19.100% Expectancy 1.743 Start Equity 80000 End Equity 154031.64 Net Profit 92.540% Sharpe Ratio 0.41 Sortino Ratio 0.289 Probabilistic Sharpe Ratio 12.680% Loss Rate 16% Win Rate 84% Profit-Loss Ratio 2.28 Alpha 0 Beta 0 Annual Standard Deviation 0.145 Annual Variance 0.021 Information Ratio 0.626 Tracking Error 0.145 Treynor Ratio 0 Total Fees $1699.00 Estimated Strategy Capacity $2200000.00 Lowest Capacity Asset VIXY UT076X30D0MD Portfolio Turnover 0.36% Drawdown Recovery 115 |
# region imports
from AlgorithmImports import *
from QuantConnect.DataSource import *
from trade_manager import TradeManager
from dataclasses import dataclass
# endregion
# VIXY Short Model:
# 1. Strategy 1:
# Hold VIXY ETF for 60 days if VIX value is above 20 for 4 days straight.
# There are 60 EW brackets that are filled with new positions and each position is held for 60 days.
#
# 2. Strategy 2:
# Hold VIXY ETF for 60 days if VIX value is above 18 at least for 1 day.
# There are 60 EW brackets that are filled with new positions and each position is held for 60 days.
#
# 50% of notional values is allocated for each strategy. Signal is checked daily. Most recent daily VIX is used as a signal value.
# Trades are executed 15 minutes before market close.
class MetatronVIXYShortModel(QCAlgorithm):
_notional_value: float = 80_000
_trade_exec_minute_offset: int = 15
_holding_period: int = 60
_strategy_weight: float = 0.5
# Strategy 1
_volatility_threshold_1: float = 20.
_lookback_1: int = 4
# Strategy 2
_volatility_threshold_2: float = 18.
_lookback_2: int = 1
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
self._traded_asset: Symbol = self.add_equity('VIXY', Resolution.MINUTE).symbol
self._volatility: Symbol = self.add_data(CBOE, 'VIX', Resolution.Daily).symbol
self._volatility_values: RollingWindow = RollingWindow[float](max(self._lookback_1, self._lookback_2))
self._traded_strategies: List[TradedStrategy] = [
TradedStrategy(
TradeManager(self, 0, self._holding_period, self._holding_period, self._strategy_weight),
self._volatility_threshold_1,
self._lookback_1
),
TradedStrategy(
TradeManager(self, 0, self._holding_period, self._holding_period, self._strategy_weight),
self._volatility_threshold_2,
self._lookback_2
)
]
# schedule function
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
)
def on_data(self, slice: Slice) -> None:
if self._day_close_flag:
self.log(f'New day logic initiated')
self._day_close_flag = False
if self.securities[self._volatility].get_last_data() and self.securities[self._volatility].price != 0:
self.log(f'Symbol {self._volatility} data present in the algorithm')
self._volatility_values.add(self.securities[self._volatility].price)
# liquidate opened symbols after holding period
for ts in self._traded_strategies:
ts.trade_manager.try_liquidate()
if not self._volatility_values.is_ready:
self.log(f'{self._volatility} history is not ready yet')
return
if slice.contains_key(self._traded_asset) and slice[self._traded_asset]:
self.log(f'Symbol {self._traded_asset} data present in the algorithm')
self.log(f'{self._volatility} values: {list(self._volatility_values)}')
for ts in self._traded_strategies:
related_vix_values: List[float] = list(self._volatility_values)[:ts.lookback]
if all([x > ts.volatility_threshold for x in related_vix_values]):
self.log(f'Strategy {ts}: all VIX values are above threshold: {related_vix_values}. Trade initiated')
ts.trade_manager.add(self._traded_asset, False, self.securities[self._traded_asset].ask_price)
def _before_eod(self) -> None:
self._day_close_flag = True
@dataclass
class TradedStrategy:
trade_manager: TradeManager
volatility_threshold: float
lookback: int# region imports
from AlgorithmImports import *
from dataclasses import dataclass
from typing import Optional
# endregion
# NOTE: Manager for new trades. It's represented by certain count of equally weighted brackets for long and short positions.
# If there's a place for new trade, it will be managed for time of holding period.
class TradeManager():
def __init__(
self,
algorithm: QCAlgorithm,
long_size: int,
short_size: int,
holding_period: int,
weight: float = 0.5
) -> None:
self._algorithm: QCAlgorithm = algorithm
self._long_size: int = long_size
self._short_size: int = short_size
self._long_len: int = 0
self._short_len: int = 0
# array of ManagedSymbols
self._symbols: List[ManagedSymbol] = []
self._holding_period: int = holding_period
self._weight: float = weight
def add(
self,
symbol: Symbol,
long_flag: bool,
price: float,
MOC_flag: bool = False
) -> None:
quantity: Optional[int] = None
# open new long trade
if long_flag:
if self._long_len < self._long_size:
# there's a place for new trade
quantity: int = int(self._algorithm._notional_value * self._weight / self._long_size / price)
if quantity != 0:
if MOC_flag:
self._algorithm.market_on_close_order(symbol, quantity)
else:
self._algorithm.market_order(symbol, quantity)
self._long_len += 1
else:
self._algorithm.log(f"Final position quantity is 0 for symbol: {symbol}")
else:
self._algorithm.log(f"There's no place for additional long trade {symbol}")
# open new short trade
else:
if self._short_len < self._short_size:
# there's a place for new trade
quantity: int = int((self._algorithm._notional_value * self._weight / self._short_size / price))
if quantity != 0:
if MOC_flag:
self._algorithm.market_on_close_order(symbol, -quantity)
else:
self._algorithm.market_order(symbol, -quantity)
self._short_len += 1
else:
self._algorithm.log(f"Final position quantity is 0 for symbol: {symbol}")
else:
self._algorithm.log(f"There's no place for additional short trade {symbol}")
if quantity:
self._symbols.append(
ManagedSymbol(symbol, self._holding_period, long_flag, quantity)
)
def try_liquidate(self) -> None:
# decrement holding period and liquidate symbols
symbols_to_delete: List[ManagedSymbol] = []
for managed_symbol in self._symbols:
managed_symbol.days_to_liquidate -= 1
# liquidate
if managed_symbol.days_to_liquidate == 0:
self._algorithm.log(f'Liquidating {managed_symbol.symbol} with quantity {managed_symbol.quantity}')
symbols_to_delete.append(managed_symbol)
if managed_symbol.long_flag:
self._algorithm.market_order(managed_symbol.symbol, -managed_symbol.quantity)
self._long_len -= 1
else:
self._short_len -= 1
self._algorithm.market_order(managed_symbol.symbol, managed_symbol.quantity)
# remove symbols from management
for managed_symbol in symbols_to_delete:
self._symbols.remove(managed_symbol)
@dataclass
class ManagedSymbol():
symbol: Symbol
days_to_liquidate: int
long_flag: bool
quantity: int