| Overall Statistics |
|
Total Orders 3682 Average Win 3.29% Average Loss -3.40% Compounding Annual Return 429.766% Drawdown 86.500% Expectancy 0.285 Start Equity 100000 End Equity 419181867.36 Net Profit 419081.867% Sharpe Ratio 4.383 Sortino Ratio 4.807 Probabilistic Sharpe Ratio 94.762% Loss Rate 35% Win Rate 65% Profit-Loss Ratio 0.97 Alpha 5.008 Beta -0.484 Annual Standard Deviation 1.136 Annual Variance 1.291 Information Ratio 4.266 Tracking Error 1.153 Treynor Ratio -10.296 Total Fees $7450023.24 Estimated Strategy Capacity $12000000.00 Lowest Capacity Asset ANTX XX1A9VEDRA05 Portfolio Turnover 27.64% Drawdown Recovery 197 |
# region imports
from AlgorithmImports import *
from datetime import timedelta
import math
import numpy as np
# endregion
class ConnorsCrash(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5*365))
self.set_cash(100_000)
self._rsi_period = 3
self._streak_period = 2
self._pct_rank_period = 100
self._vola_period = 100
self._crsi_entry = 90
self._crsi_exit = 30
self._max_shorts = 20
self.settings.automatic_indicator_warm_up = True
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(
lambda fundamentals: [f.symbol for f in fundamentals if f.price > 5 and f.volume > 1e6] # switch to dollar volume
)
def on_securities_changed(self, changes):
for security in changes.added_securities:
# Attach ConnorsRSI indicator to each security.
security.connors = self.crsi(security, self._rsi_period, self._streak_period, self._pct_rank_period)
# Initialize custom volatility indicator and.
security.volatility = CustomVolatility(self._vola_period)
for bar in self.history[TradeBar](security, self._vola_period + 1):
security.volatility.update(bar)
self.register_indicator(security, security.volatility)
for security in changes.removed_securities:
self.deregister_indicator(security.volatility)
self.liquidate(security)
def on_data(self, data):
# Filter securities with volatility > 100%.
securities = [self.securities[symbol] for symbol in self._universe.selected]
filter_vola = [s for s in securities if s.volatility.is_ready and s.volatility.value > 100]
# Find invested short positions positions.
short_positions = [s for s in securities if self.portfolio[s.symbol].is_short]
# Liquidate short positions when ConnorsRSI falls below exit threshold.
for security in short_positions:
if security.connors.is_ready and security.connors.current.value < self._crsi_exit:
self.liquidate(security)
# Track symbols with open pending orders to avoid duplicate entries.
pending_symbols = {t.symbol for t in self.transactions.get_open_order_tickets()}
# Find short entry candidates (high volatility and high CRSI)
short_candidates = [
s for s in filter_vola
if (s.connors.is_ready and
s.connors.current.value > self._crsi_entry and
not self.portfolio[s.symbol].invested and
s.symbol not in pending_symbols)
]
# Calculate available position slots
available_slots = self._max_shorts - len(short_positions) - len(pending_symbols)
if available_slots <= 0 or not short_candidates:
return
# Place short orders for candidates using available margin for shorts.
n_orders = min(len(short_candidates), available_slots)
for i, security in enumerate(short_candidates[:n_orders]):
if security.price <= 0:
continue
remaining = n_orders - i
# Equal weight the available margin per slot. ### shorts are 50% margin so we can use 2x the margin for position sizing
max_position_value = self.portfolio.margin_remaining * 0.70 / remaining
quantity = int(max_position_value / security.price)
if quantity > 0:
self.limit_order(security, -quantity, round(1.03 * security.price, 2))
class CustomVolatility(PythonIndicator):
def __init__(self, period):
super().__init__()
self.value = 0
self._window = RollingWindow[float](period)
def update(self, input_: BaseData):
# Annualized log-return volatility.
price = input_.value
if price <= 0:
return
self._window.add(price)
if self._window.is_ready:
prices = np.array(list(self._window)[::-1])
log_diffs = np.diff(np.log(prices))
self.value = np.std(log_diffs) * math.sqrt(252) * 100.0
return self.is_ready
@property
def is_ready(self) -> bool:
return self._window.is_ready