| Overall Statistics |
|
Total Orders 167 Average Win 2.27% Average Loss -0.97% Compounding Annual Return 5.961% Drawdown 7.600% Expectancy 0.403 Start Equity 100000 End Equity 133612.17 Net Profit 33.612% Sharpe Ratio 0.24 Sortino Ratio 0.155 Probabilistic Sharpe Ratio 16.397% Loss Rate 58% Win Rate 42% Profit-Loss Ratio 2.33 Alpha 0.003 Beta 0.15 Annual Standard Deviation 0.068 Annual Variance 0.005 Information Ratio -0.443 Tracking Error 0.161 Treynor Ratio 0.109 Total Fees $233.91 Estimated Strategy Capacity $1300000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 9.11% |
from AlgorithmImports import *
import numpy as np
class ComplexCrossoverStrategy(QCAlgorithm):
"""
This strategy trades SPY based on a multi-factor confirmation model using
the full polar representation (magnitude and phase) of complex numbers.
- A 200-day SMA is used as a long-term trend filter.
- Magnitudes (r) and phase angles (phi) of z1 and z2 are smoothed.
- Entry requires a bullish crossover in BOTH magnitude and phase.
- Exits are triggered by an ATR trailing stop-loss or a bearish magnitude crossover.
"""
def Initialize(self):
"""Initialise the data and resolution required for the algorithm."""
self.SetStartDate(2020, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
# --- Indicator Configuration ---
self.trend_period = 200
self.smoothing_period = 5
self.atr_period = 14
self.atr_multiplier = 2.5
# --- Trend and Signal Indicators ---
self.trend_filter = self.SMA(self.symbol, self.trend_period)
# --- UPGRADE: Indicators for Magnitude (r) ---
self.smoothed_r1 = SimpleMovingAverage(self.smoothing_period)
# FIX: Corrected the typo from SimpleMovingAverag to SimpleMovingAverage
self.smoothed_r2 = SimpleMovingAverage(self.smoothing_period)
# --- UPGRADE: Indicators for Phase Angle (phi) ---
self.smoothed_phi1 = SimpleMovingAverage(self.smoothing_period)
self.smoothed_phi2 = SimpleMovingAverage(self.smoothing_period)
# --- Risk Management Indicators ---
self.atr = self.ATR(self.symbol, self.atr_period)
self.trailing_stop_price = 0
# --- State variables for crossover detection ---
self.prev_smoothed_r1 = None
self.prev_smoothed_r2 = None
# Set warm-up period for the longest indicator
self.SetWarmUp(self.trend_filter.Period + self.smoothing_period)
def OnData(self, data: Slice):
"""OnData event is the primary entry point for your algorithm."""
if self.IsWarmingUp or not data.ContainsKey(self.symbol) or not self.atr.IsReady:
return
bar = data[self.symbol]
if bar is None:
return
# --- Signal Calculation ---
# 1. Construct complex numbers
if bar.Close >= bar.Open:
z1, z2 = (bar.Open + 1j * bar.Low, bar.Close + 1j * bar.High)
else:
z1, z2 = (bar.Open + 1j * bar.High, bar.Close + 1j * bar.Low)
# 2. Update Magnitude (r) and Phase (phi) indicators
self.smoothed_r1.Update(bar.EndTime, abs(z1))
self.smoothed_r2.Update(bar.EndTime, abs(z2))
self.smoothed_phi1.Update(bar.EndTime, np.angle(z1))
self.smoothed_phi2.Update(bar.EndTime, np.angle(z2))
# 3. Wait for all indicators to be ready
if not all([self.trend_filter.IsReady, self.smoothed_r1.IsReady, self.smoothed_phi1.IsReady]):
return
# Get current smoothed values
r1_smooth = self.smoothed_r1.Current.Value
r2_smooth = self.smoothed_r2.Current.Value
phi1_smooth = self.smoothed_phi1.Current.Value
phi2_smooth = self.smoothed_phi2.Current.Value
if self.prev_smoothed_r1 is None:
self.prev_smoothed_r1 = r1_smooth
self.prev_smoothed_r2 = r2_smooth
return
# --- Trading Logic ---
is_invested = self.Portfolio[self.symbol].Invested
price = self.Securities[self.symbol].Price
# --- Risk Management: Trailing Stop-Loss ---
if is_invested:
new_stop_price = price - self.atr.Current.Value * self.atr_multiplier
self.trailing_stop_price = max(self.trailing_stop_price, new_stop_price)
if price <= self.trailing_stop_price:
self.Debug(f"{self.Time} - ATR Trailing Stop Hit at {price:.2f}. Liquidating.")
self.Liquidate(self.symbol)
self.trailing_stop_price = 0
return
# --- Entry and Exit Signals ---
is_uptrend = price > self.trend_filter.Current.Value
# --- UPGRADE: Dual Confirmation Bullish Entry ---
is_magnitude_bullish = self.prev_smoothed_r2 <= self.prev_smoothed_r1 and r2_smooth > r1_smooth
is_phase_bullish = phi2_smooth > phi1_smooth
if is_magnitude_bullish and is_phase_bullish:
if is_uptrend and not is_invested:
self.Debug(f"{self.Time} - Dual Confirmation: Bullish Crossover in Uptrend. Buying SPY.")
self.SetHoldings(self.symbol, 1.0)
self.trailing_stop_price = price - self.atr.Current.Value * self.atr_multiplier
self.Debug(f"{self.Time} - Initial ATR Stop set at {self.trailing_stop_price:.2f}")
# --- Bearish Magnitude Crossover Exit ---
is_magnitude_bearish = self.prev_smoothed_r1 <= self.prev_smoothed_r2 and r1_smooth > r2_smooth
if is_magnitude_bearish and is_invested:
self.Debug(f"{self.Time} - Bearish Magnitude Crossover. Liquidating SPY.")
self.Liquidate(self.symbol)
self.trailing_stop_price = 0
# Update previous values for the next iteration
self.prev_smoothed_r1 = r1_smooth
self.prev_smoothed_r2 = r2_smooth
def OnEndOfAlgorithm(self):
"""Called at the end of the algorithm execution."""
self.Log(f"Ending backtest. Final portfolio value: {self.Portfolio.TotalPortfolioValue}")