| Overall Statistics |
|
Total Orders 63 Average Win 3.54% Average Loss -2.55% Compounding Annual Return -2.855% Drawdown 33.400% Expectancy -0.170 Start Equity 100000 End Equity 81626.8 Net Profit -18.373% Sharpe Ratio -0.369 Sortino Ratio -0.402 Probabilistic Sharpe Ratio 0.016% Loss Rate 65% Win Rate 35% Profit-Loss Ratio 1.39 Alpha -0.037 Beta -0.045 Annual Standard Deviation 0.11 Annual Variance 0.012 Information Ratio -0.601 Tracking Error 0.201 Treynor Ratio 0.9 Total Fees $0.00 Estimated Strategy Capacity $33000.00 Lowest Capacity Asset SPX500USD 8I Portfolio Turnover 2.97% Drawdown Recovery 3 |
# ==================================================================================
# Carver Starter System — Chapter 5 of "Leveraged Trading"
# S&P 500 CFD with MAC 16,64 crossover
# ==================================================================================
#
# EXACT BOOK FORMULAS:
#
# Instrument risk (Appendix C):
# daily_returns[i] = (price[i] - price[i-1]) / price[i-1]
# instrument_risk = stdev(last 25 daily_returns) × 16
#
# Opening rule (Formula 12-13):
# MA16 = average of last 16 closing prices
# MA64 = average of last 64 closing prices
# MA16 > MA64 → Go Long
# MA16 < MA64 → Go Short
#
# Position sizing (Formula 14):
# notional_exposure = (risk_target × capital) / instrument_risk
# risk_target = 12% (Starter System, p.99)
#
# Stop loss (Formulas 22-24):
# price_volatility = instrument_risk × current_price
# stop_gap = price_volatility × 0.5 (fraction for MAC 16,64, Table 12)
# Long: exit if close ≤ highest_close_since_entry - stop_gap
# Short: exit if close ≥ lowest_close_since_entry + stop_gap
#
# Expected: ~5.4 trades/year, SR ≈ 0.24, return ≈ 4.9% at 12% risk
# ==================================================================================
from AlgorithmImports import *
import numpy as np
class CarverStarterSystem(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2018, 1, 1)
self.set_end_date(2024, 12, 31)
self.set_cash(100000)
# S&P 500 CFD
self.cfd = self.add_cfd("SPX500USD", Resolution.DAILY)
self.sym = self.cfd.symbol
# Book parameters
self.RISK_TARGET = 0.12 # 12% annual (Ch. 5 p.99)
self.STOP_FRACTION = 0.5 # for MAC 16,64 (Table 12)
self.MA_FAST = 16
self.MA_SLOW = 64
self.VOL_LOOKBACK = 25 # Appendix C: 25 trading days
# State
self.direction = 0 # +1=long, -1=short, 0=flat
self.high_wm = None # trailing stop: highest close (long)
self.low_wm = None # trailing stop: lowest close (short)
self.trade_count = 0
self.prices = [] # all closing prices
self.ready = False
self.last_date = None
def on_data(self, data: Slice) -> None:
# --- Once per day ---
today = self.time.date()
if self.last_date == today:
return
self.last_date = today
if not data.contains_key(self.sym):
return
bar = data[self.sym]
if bar is None:
return
price = float(bar.close)
if price <= 0:
return
# --- Pre-fill history on first bar ---
if not self.ready:
self._load_history()
self.ready = True
# --- Append today's close ---
self.prices.append(price)
# Need at least 64 prices for slow MA + 25 for vol
if len(self.prices) < self.MA_SLOW + 1:
return
# =============================================
# STEP 1: Instrument risk (Appendix C)
# =============================================
# daily % returns over last 25 days
p = np.array(self.prices)
recent = p[-(self.VOL_LOOKBACK + 1):]
daily_returns = np.diff(recent) / recent[:-1]
instrument_risk = float(np.std(daily_returns, ddof=1) * 16)
if instrument_risk < 0.01:
return
# =============================================
# STEP 2: Check trailing stop loss (F.22-24)
# =============================================
if self._check_stop(price, instrument_risk):
self._close_position("Stop loss hit")
return
# =============================================
# STEP 3: Opening rule — MAC 16,64 (F.12-13)
# =============================================
ma16 = float(np.mean(p[-self.MA_FAST:]))
ma64 = float(np.mean(p[-self.MA_SLOW:]))
if ma16 > ma64:
new_dir = 1 # Long
else:
new_dir = -1 # Short
# =============================================
# STEP 4: Trade only on direction change
# =============================================
if new_dir == self.direction:
# No change — just update watermark
self._update_watermark(price)
return
# =============================================
# STEP 5: Position sizing (Formula 14)
# =============================================
capital = self.portfolio.total_portfolio_value
notional = (self.RISK_TARGET * capital) / instrument_risk
target_pct = new_dir * notional / capital
# Place order
self.set_holdings(self.sym, target_pct)
# Update state
self.direction = new_dir
self.trade_count += 1
if new_dir == 1:
self.high_wm = price
self.low_wm = None
else:
self.low_wm = price
self.high_wm = None
self.log(
f"TRADE #{self.trade_count} | {'LONG' if new_dir == 1 else 'SHORT'} | "
f"Price={price:.2f} | MA16={ma16:.2f} | MA64={ma64:.2f} | "
f"Vol={instrument_risk:.3f} | Alloc={target_pct:.2%}"
)
# ==================================================================
# TRAILING STOP LOSS (Formulas 22-24)
# ==================================================================
def _check_stop(self, price: float, instrument_risk: float) -> bool:
"""
Formula 22: price_vol = instrument_risk × price
Formula 23: stop_gap = price_vol × stop_fraction
Formula 24: stop_level = watermark ∓ stop_gap
"""
if self.direction == 0:
return False
price_vol = instrument_risk * price # Formula 22
stop_gap = price_vol * self.STOP_FRACTION # Formula 23
if self.direction == 1 and self.high_wm is not None:
# Update watermark first
self.high_wm = max(self.high_wm, price)
stop_level = self.high_wm - stop_gap # Formula 24
if price <= stop_level:
self.log(
f"STOP (Long) | Price={price:.2f} | "
f"HWM={self.high_wm:.2f} | Stop={stop_level:.2f}"
)
return True
elif self.direction == -1 and self.low_wm is not None:
self.low_wm = min(self.low_wm, price)
stop_level = self.low_wm + stop_gap # Formula 24
if price >= stop_level:
self.log(
f"STOP (Short) | Price={price:.2f} | "
f"LWM={self.low_wm:.2f} | Stop={stop_level:.2f}"
)
return True
return False
def _close_position(self, reason: str) -> None:
"""Flatten position and reset state."""
self.liquidate(self.sym, reason)
self.direction = 0
self.high_wm = None
self.low_wm = None
self.trade_count += 1
def _update_watermark(self, price: float) -> None:
if self.direction == 1 and self.high_wm is not None:
self.high_wm = max(self.high_wm, price)
elif self.direction == -1 and self.low_wm is not None:
self.low_wm = min(self.low_wm, price)
# ==================================================================
# HISTORY PRE-LOAD
# ==================================================================
def _load_history(self) -> None:
"""
Pre-fill price buffer with 200 days of history.
(Need 64 for MA64 + 25 for vol + buffer)
"""
history = self.history([self.sym], 200, Resolution.DAILY)
if history.empty:
return
closes = history.close.unstack(level=0)
for col in closes.columns:
vals = closes[col].dropna().values
for v in vals:
self.prices.append(float(v))
# ==================================================================
# LIFECYCLE
# ==================================================================
def on_order_event(self, event: OrderEvent) -> None:
if event.status == OrderStatus.FILLED:
self.debug(
f"FILLED | Qty={event.fill_quantity} | "
f"Price={event.fill_price:.2f}"
)
def on_end_of_algorithm(self) -> None:
yrs = (self.end_date - self.start_date).days / 365.25
self.log(f"=== STARTER SYSTEM RESULTS ===")
self.log(f"Period: {self.start_date.date()} to {self.end_date.date()}")
self.log(f"Total trades: {self.trade_count}")
self.log(f"Trades/year: {self.trade_count / yrs:.1f}")
self.log(f"Final portfolio: ${self.portfolio.total_portfolio_value:,.2f}")
self.log(f"Risk target: {self.RISK_TARGET:.0%}")
self.log(f"Stop fraction: {self.STOP_FRACTION}")
self.log(f"Rule: MAC {self.MA_FAST},{self.MA_SLOW}")