| Overall Statistics |
|
Total Orders 255 Average Win 0.49% Average Loss -0.09% Compounding Annual Return 12.782% Drawdown 21.000% Expectancy 1.552 Start Equity 500000.00 End Equity 915998.28 Net Profit 83.200% Sharpe Ratio 0.447 Sortino Ratio 0.474 Probabilistic Sharpe Ratio 17.812% Loss Rate 59% Win Rate 41% Profit-Loss Ratio 5.16 Alpha 0.005 Beta 0.622 Annual Standard Deviation 0.141 Annual Variance 0.02 Information Ratio -0.253 Tracking Error 0.123 Treynor Ratio 0.102 Total Fees $318.50 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 1.58% Drawdown Recovery 448 |
#region imports
from AlgorithmImports import *
import numpy as np
import json
from hmmlearn.hmm import GaussianHMM
from datetime import timedelta
from collections import defaultdict
#endregion
class RegimeAwareMultiStrategyAlgorithm(QCAlgorithm):
"""
Backtesting-Only Multi-Strategy Trading Algorithm
Strategies Implemented:
1. BTC Momentum (Long/Short based on RSI + Momentum)
2. QQQ Wheel (Covered Calls + Cash-Secured Puts)
3. VIX/SPY Gap Option Spreads (3-DTE Vertical Spreads)
4. Forex Basket Momentum (EUR/USD/GBP/CHF Basket vs. SMA)
REMOVED: All alpha models, insights, portfolio construction, and risk management
frameworks that were required for alphaStream licensing.
TRADING METHOD: Direct order tickets and position management.
"""
def Initialize(self):
"""Initialize algorithm: symbols, indicators, and strategy parameters."""
# ===== LIVE TRADING CONFIG =====
self.maxMarginUsage = 0.80 # Do not open new trades if margin usage exceeds 80%
self.maxDrawdownAlertPercent = 0.04 # Alert if equity drops 4% from peak
# ===== CONFIGURATION: Strategy Parameters =====
# BTC Momentum Strategy
self.btcMomentumPeriod = 15 # This is kept for the indicator definition
self.btcRsiPeriod = 15 # This is kept for the indicator definition
# BTC Trade Frequency Control
self.btcLastTradeTime = None
self.btcMinHoldingPeriod = timedelta(days=2)
self.btcMinReentryPeriod = timedelta(hours=24)
self.btcEntryTime = None
self.btcPreviousSignal = None
self.btcSignalConfirmationBars = 0
self.btcRequiredConfirmations = 2 # Need 2 bars of same signal
# Tighter thresholds
self.btcMomentumBullEntry = 1
self.btcMomentumBearEntry = -1
self.btcOverbought = 55
self.btcSold = 45
self.btc_trailing_high = 0
self.btcTrailingStopPercent = 0.07 # 7% trailing stop
# QQQ is used by the Gap strategy for gap calculation
self.qqqSymbolTicker = "QQQ"
# Gap Spread Strategy
self.gapAllocation = 0.15
self.gapVixThreshold = 0.009
self.gapQqqThreshold = 0.001
self.gapDteTarget = 3
self.gapShortDelta = 0.16
self.gapLongDelta = 0.12
self.gapMinSpreadBid = 0.08
self.gapMinOi = 100
self.gapMaxSpreadPct = 0.10
self.gapMonthlyDdLimit = 0.02
# Gap Strategy State Tracking
self.gapYesterdayVixClose = None
self.gapYesterdayQqqClose = None
self.gapTodayProcessed = False
self.gapLastProcessedDate = None
self.gapCurrentMonth = None
self.gapMonthlyStartEquity = None
self.gapStopTradingMonth = False
self.openGapSpreadTickets = []
# Forex Basket Strategy
self.fxPairs = ["EURUSD", "USDCHF", "GBPUSD", "EURGBP"]
self.fxSymbols = []
self.fxSmaPeriods = 15 * 24 # 75 days in hourly bars
self.fxAtrPeriods = 12 * 24 # 25 days in hourly bars
self.fxSmaIndicators = {}
self.fxAtrIndicators = {}
self.fxVolatilityWindow = RollingWindow[float](14)
self.fxPreviousSignal = None
self.fxSignalThreshold = 0.001 # 1%
self.fxRiskRewardRatio = 5.0
self.fxStopLossAtrMultiplier = 5.0
self.fxOpenTradeTickets = []
self.fxOpenEntryTickets = []
self.fxPrevSmaAvg = None
# Regime Detection
self.regimeVixBullThreshold = 23.50
self.regimeVixBearThreshold = 24.50
self.marketRegime = None # -1 = Bear, 0 = Neutral, 1 = Bull
# Drawdown tracking
self.peakEquity = self.Portfolio.TotalPortfolioValue
self.drawdownAlertSent = False
# ===== ALGORITHM SETUP =====
self.SetStartDate(2020, 11, 1)
self.SetEndDate(2025, 11, 11)
self.SetCash(500000)
#self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
# ===== ADD SECURITIES & INDICATORS =====
# BTC Crypto (Daily resolution for momentum/RSI)
self.btcSymbol = self.AddCrypto("BTCUSD", Resolution.Daily, Market.Coinbase).Symbol
self.btcMomentum = self.MOM(self.btcSymbol, self.btcMomentumPeriod, Resolution.Daily)
self.btcRsi = self.RSI(self.btcSymbol, self.btcRsiPeriod, MovingAverageType.Simple, Resolution.Daily)
self.btcAtr = self.ATR(self.btcSymbol, 14, MovingAverageType.Simple, Resolution.Daily)
self.btcStoch = self.STO(self.btcSymbol, 14, 3, 3, Resolution.Daily)
# QQQ Equity & Options (Wheel Strategy)
self.qqqSymbol = self.AddEquity(self.qqqSymbolTicker, Resolution.Hour).Symbol
# SPY Equity & Options (Gap Strategy)
self.spySymbol = self.AddEquity("SPY", Resolution.Hour).Symbol
self.spyOption = self.AddOption("SPY", Resolution.Hour)
self.spyOption.SetFilter(lambda universe: universe.Strikes(-15, 15).Expiration(0, 3))
# VIX for Regime Detection
self.vixIndexSymbol = self.AddIndex("VIX", Resolution.Hour).Symbol
self.spyDailySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.vixDailySymbol = self.AddIndex("VIX", Resolution.Daily).Symbol
# Forex Basket Assets & Indicators
for pair in self.fxPairs:
symbol = self.AddForex(pair, Resolution.Hour, Market.Oanda).Symbol
self.fxSymbols.append(symbol)
self.fxSmaIndicators[symbol] = self.SMA(symbol, self.fxSmaPeriods, Resolution.Hour)
self.fxAtrIndicators[symbol] = self.ATR(symbol, self.fxAtrPeriods, MovingAverageType.Simple, Resolution.Hour)
self.load_state() # Load saved state for restart resilience
warmup_periods = max(25, self.fxSmaPeriods)
self.SetWarmUp(warmup_periods)
# ===== SCHEDULED FUNCTIONS =====
self.Schedule.On(
self.DateRules.MonthStart(),
self.TimeRules.AfterMarketOpen(self.vixDailySymbol, 5),
self.updateregime
)
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.BeforeMarketClose(self.spySymbol, 15),
self.gapliquidateandlog
)
# Schedule BTC once daily instead of hourly
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.At(0, 5),
self.btc_scheduled_check
)
# Schedule for Forex Basket Strategy
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(6, 0), self.fx_signal_check)
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(18, 0), self.fx_signal_check)
# Schedule daily summary log for live trading
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.At(21, 0), # End of day
self.log_daily_summary
)
def OnData(self, slice):
"""Main trading logic - executed on data events."""
if self.IsWarmingUp:
return
# Initialize regime if needed
if self.marketRegime is None:
self.updateregime()
# Execute strategies
self.check_drawdown()
self.btc_trailing_stop_check()
self.gap_option_spread(slice)
# Update runtime statistics for live dashboard
margin_used_percent = self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 0 else 0
self.SetRuntimeStatistic("Margin Usage", f"{margin_used_percent:.1%}")
self.SetRuntimeStatistic("Peak Equity", f"${self.peakEquity:,.2f}")
def btc_scheduled_check(self):
"""Scheduled daily BTC check."""
self.btc_momentum_trading(None)
def OnOrderEvent(self, orderEvent):
"""
Handle order events for all strategies.
Specifically used by the Forex strategy to place SL/TP orders after entry.
"""
if orderEvent.Status != OrderStatus.Filled:
return
order = self.Transactions.GetOrderById(orderEvent.OrderId)
symbol = orderEvent.Symbol
# Handle BTC order events
if symbol == self.btcSymbol:
position = self.Portfolio[self.btcSymbol]
log_message = ""
if position.Invested: # Entry
self.btc_trailing_high = orderEvent.FillPrice # Reset trailing stop high
log_message = f"BTC {order.Direction} ENTRY @ {orderEvent.FillPrice:,.2f}"
else: # Exit
log_message = f"BTC EXIT @ {orderEvent.FillPrice:,.2f}"
self.Log(log_message)
if self.LiveMode:
self.Notify.Email(
"dca.llc.md@gmail.com",
log_message,
f"Time: {self.Time}. Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}"
)
# Check if the filled order is one of our Forex entry orders
is_entry_order = any(t.OrderId == orderEvent.OrderId for t in self.fxOpenEntryTickets)
if is_entry_order:
self.fx_place_bracket_orders(orderEvent.Symbol)
if self.LiveMode:
self.Notify.Email(
"dca.llc.md@gmail.com",
f"FX Basket Trade: {order.Direction} {symbol.Value}",
f"Filled {order.Quantity} of {symbol.Value} @ {orderEvent.FillPrice:.5f}. Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}"
)
def updateregime(self):
"""Update market regime based on VIX thresholds."""
if self.IsWarmingUp:
return
if not self.Securities.ContainsKey(self.vixDailySymbol) or self.Securities[self.vixDailySymbol].Price == 0:
self.Log("Regime update failed - VIX data unavailable.")
return
vixlevel = self.Securities[self.vixDailySymbol].Price
if vixlevel >= self.regimeVixBearThreshold:
self.marketRegime = -1 # BEAR
elif vixlevel < self.regimeVixBullThreshold:
self.marketRegime = 1 # BULL
else:
self.marketRegime = 0 # NEUTRAL
regimename = {1: "BULL", 0: "NEUTRAL", -1: "BEAR"}[self.marketRegime]
self.Log(f"Regime Updated: {regimename} | VIX Level: {vixlevel:.2f}")
self.SetRuntimeStatistic("Market Regime", regimename)
def btc_trailing_stop_check(self):
"""Liquidate BTC if price falls below trailing high."""
position = self.Portfolio[self.btcSymbol]
if not position.Invested:
return
price = self.Securities[self.btcSymbol].Price
# Update high-water mark
if position.IsLong:
self.btc_trailing_high = max(self.btc_trailing_high, price)
stop_price = self.btc_trailing_high * (1 - self.btcTrailingStopPercent)
if price < stop_price:
self.Liquidate(self.btcSymbol, f"Trailing Stop Triggered at {price:.2f}")
self.btcLastTradeTime = self.Time # Apply cooldown
# Trailing stop for shorts can be added here if needed
def check_drawdown(self):
"""Monitors portfolio equity for major drawdowns from its peak."""
current_equity = self.Portfolio.TotalPortfolioValue
# Update peak equity
if current_equity > self.peakEquity:
self.peakEquity = current_equity
self.drawdownAlertSent = False # Reset alert flag when a new peak is made
# Check for drawdown
drawdown = (self.peakEquity - current_equity) / self.peakEquity
if drawdown >= self.maxDrawdownAlertPercent and not self.drawdownAlertSent:
self.drawdownAlertSent = True # Prevent spamming alerts
alert_message = (f"LIFETIME DRAWDOWN ALERT: Portfolio has dropped {drawdown:.2%} from its peak.\n"
f"Peak Equity: ${self.peakEquity:,.2f}\n"
f"Current Equity: ${current_equity:,.2f}")
self.Log(alert_message)
if self.LiveMode:
self.Notify.Email("dca.llc.md@gmail.com", "LIFETIME DRAWDOWN ALERT", alert_message)
def btc_momentum_trading(self, data):
"""Execute BTC momentum strategy with RSI and momentum indicators.
Long/Short based on regime, momentum, and RSI levels with reduced frequency."""
if not self.btcRsi.IsReady or not self.btcMomentum.IsReady:
return
rsi = self.btcRsi.Current.Value
momentum = self.btcMomentum.Current.Value
holdings = self.Portfolio[self.btcSymbol]
# Cooldown checks
if not holdings.Invested and self.btcLastTradeTime:
if self.Time - self.btcLastTradeTime < self.btcMinReentryPeriod:
return
if holdings.Invested and self.btcEntryTime:
if self.Time - self.btcEntryTime < self.btcMinHoldingPeriod:
return
# Signal generation
current_signal = None
if self.marketRegime == -1:
if momentum < self.btcMomentumBearEntry and rsi < self.btcOverbought:
current_signal = "SHORT"
elif rsi < self.btcSold or momentum > 1:
current_signal = "COVER"
else:
if holdings.IsShort:
current_signal = "COVER"
elif momentum > self.btcMomentumBullEntry and rsi > self.btcSold:
current_signal = "BUY"
elif rsi > self.btcOverbought or momentum < -1 :
current_signal = "SELL"
# Signal confirmation
if current_signal == self.btcPreviousSignal:
self.btcSignalConfirmationBars += 1
else:
self.btcSignalConfirmationBars = 1
self.btcPreviousSignal = current_signal
if self.btcSignalConfirmationBars < self.btcRequiredConfirmations:
return
# Execution
if not holdings.Invested and current_signal == "BUY":
if not self.has_sufficient_buying_power(): return
quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
self.MarketOrder(self.btcSymbol, quantity)
self.btcEntryTime = self.Time
elif not holdings.Invested and current_signal == "SHORT":
if not self.has_sufficient_buying_power(): return
quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
self.MarketOrder(self.btcSymbol, -quantity)
self.btcEntryTime = self.Time
elif holdings.IsLong and current_signal == "SELL":
self.Liquidate(self.btcSymbol)
self.btcLastTradeTime = self.Time
self.btcEntryTime = None
elif holdings.IsShort and current_signal == "COVER":
self.Liquidate(self.btcSymbol)
self.btcLastTradeTime = self.Time
self.btcEntryTime = None
def gapliquidateandlog(self):
"""Liquidate expired gap spread positions and cancel open orders."""
if self.IsWarmingUp:
return
# Cancel any open limit orders from the gap strategy
for ticket in self.openGapSpreadTickets:
if ticket.Status != OrderStatus.Filled and ticket.Status != OrderStatus.Canceled:
ticket.Cancel()
self.Log(f"[GAP STRATEGY] Canceled open limit order for {ticket.Symbol}")
self.openGapSpreadTickets.clear()
# Liquidate any remaining option positions from the gap strategy
openpositions = [pos for pos in self.Portfolio.Values
if pos.Invested and pos.Symbol.SecurityType == SecurityType.Option
and pos.Symbol.ID.Symbol == self.spyOption.Symbol.Value] # Only liquidate SPY options
for pos in openpositions:
self.MarketOrder(pos.Symbol, -pos.Quantity)
self.Log(f"GAP: Liquidated position {pos.Symbol} at market close.")
def gap_option_spread(self, data):
"""Sell iron condors on VIX/QQQ gap days."""
currentdate = self.Time.date()
# Use current bar's Open/Close for VIX and QQQ
currentvixopen = self.Securities[self.vixDailySymbol].Open if self.Securities[self.vixDailySymbol].HasData else None
currentvixclose = self.Securities[self.vixDailySymbol].Close if self.Securities[self.vixDailySymbol].HasData else None
currentqqqopen = self.Securities[self.qqqSymbol].Open if self.Securities[self.qqqSymbol].HasData else None
currentqqqclose = self.Securities[self.qqqSymbol].Close if self.Securities[self.qqqSymbol].HasData else None
# Ensure we have valid open/close prices
if currentvixopen is None or currentvixclose is None or currentqqqopen is None or currentqqqclose is None:
return
# Initialize yesterday's closes if not set
if self.gapYesterdayVixClose is None or self.gapYesterdayQqqClose is None:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
self.gapLastProcessedDate = currentdate
return
# Reset daily flag at new day
if currentdate != self.gapLastProcessedDate:
self.gapTodayProcessed = False
self.gapLastProcessedDate = currentdate
if self.gapTodayProcessed:
# Update yesterday's close for the next day's calculation
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
# Calculate gap percentages
vixgappct = (currentvixopen - self.gapYesterdayVixClose) / self.gapYesterdayVixClose
qqqgappct = (currentqqqopen - self.gapYesterdayQqqClose) / self.gapYesterdayQqqClose
# Only trade if NOT in bear regime
if self.marketRegime == -1:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
if not data.OptionChains or self.spyOption.Symbol not in data.OptionChains:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
optionchain = data.OptionChains[self.spyOption.Symbol]
if not optionchain or len(optionchain) == 0:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
# Call spread: VIX gap up, QQQ gap up (risk-on)
# FIX: Corrected logic for call spread.
if vixgappct > self.gapVixThreshold and qqqgappct < -self.gapQqqThreshold:
self.Log(f"GAP TRIGGERED - CALL SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
if self.submitspreadlimitorder(optionchain, call=True):
self.gapTodayProcessed = True
self.Log("HYBRID GAPSPREAD: SUBMITTED CALL SPREAD")
# Put spread: VIX gap up, QQQ gap down (fear/dip)
# FIX: Corrected logic for put spread.
elif vixgappct < -self.gapVixThreshold and qqqgappct > self.gapQqqThreshold:
self.Log(f"GAP TRIGGERED - PUT SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
if self.submitspreadlimitorder(optionchain, call=False):
self.gapTodayProcessed = True
self.Log("HYBRID GAPSPREAD: SUBMITTED PUT SPREAD")
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
def submitspreadlimitorder(self, optionchain, call=True):
"""Submit combo limit order for gap spreads."""
if not self.has_sufficient_buying_power():
return False
qty = 5 # Number of spreads to trade
targetdeltashort = self.gapShortDelta if call else -self.gapShortDelta
targetdeltalong = self.gapLongDelta if call else -self.gapLongDelta
optiontype = OptionRight.Call if call else OptionRight.Put
# Find short leg (selling this leg)
shorts = [x for x in optionchain
if x.Right == optiontype
and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
and ((call and x.Greeks.Delta >= targetdeltashort) or (not call and x.Greeks.Delta <= targetdeltashort)) # Delta logic for short leg
and self.isliquid(x)
and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
if not shorts:
self.Log(f"GAP: No liquid shorts found matching delta {targetdeltashort}")
return False
shortleg = min(shorts, key=lambda x: abs(x.Greeks.Delta - targetdeltashort))
# Find long leg (buying this leg)
longs = [x for x in optionchain
if x.Right == optiontype
and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
and ((call and x.Greeks.Delta <= targetdeltalong) or (not call and x.Greeks.Delta >= targetdeltalong)) # Delta logic for long leg
and self.isliquid(x)
and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
if not longs:
self.Log(f"GAP: No liquid longs found matching delta {targetdeltalong}")
return False
longleg = min(longs, key=lambda x: abs(x.Greeks.Delta - targetdeltalong))
# Verify same expiration and different strikes
if shortleg.Expiry != longleg.Expiry:
self.Log("GAP: Different expiries for short and long legs")
return False
if shortleg.Strike == longleg.Strike:
self.Log("GAP: Short and long leg strikes are the same")
return False
# Create combo order: sell 1 short leg, buy 1 long leg for each spread
legs = [
Leg.Create(shortleg.Symbol, -1), # Sell 1 short leg contract
Leg.Create(longleg.Symbol, 1) # Buy 1 long leg contract
]
# FIX: Simplified limit price calculation. It's always (bid of short - ask of long) for a credit spread.
limitprice = shortleg.BidPrice - longleg.AskPrice
# FIX: Pass 'qty' (number of spreads) to ComboLimitOrder
tickets = self.ComboLimitOrder(legs, qty, limitprice)
self.openGapSpreadTickets.extend(tickets)
return True
def isliquid(self, contract):
"""Check if option contract has sufficient liquidity."""
try:
bid = contract.BidPrice if hasattr(contract, 'BidPrice') and contract.BidPrice else 0
ask = contract.AskPrice if hasattr(contract, 'AskPrice') and contract.AskPrice else 0
if bid < self.gapMinSpreadBid:
return False
# FIX: Calculate spread as a percentage of the bid price
if bid <= 0: return False # Avoid division by zero
spread_pct = (ask - bid) / bid
oi = contract.OpenInterest if hasattr(contract, 'OpenInterest') else 0
if oi < self.gapMinOi:
return False
# FIX: Compare percentage spread to percentage threshold
if spread_pct > self.gapMaxSpreadPct:
return False
return True
except Exception as e:
self.Log(f"isliquid ERROR: {str(e)}")
return False
def has_sufficient_buying_power(self, new_trade_margin_estimate=0):
"""Check if margin usage is within the defined limit."""
margin_used_percent = self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 0 else 0
if margin_used_percent >= self.maxMarginUsage:
self.Log(f"Trade SKIPPED: Margin usage at {margin_used_percent:.1%} exceeds limit of {self.maxMarginUsage:.1%}.")
return False
return True
# ==================================================================================
# ===== FOREX BASKET STRATEGY ======================================================
# ==================================================================================
def fx_signal_check(self):
"""
Runs on a schedule to check for Forex Basket trading signals.
"""
self.Log(f"FX_SIGNAL_CHECK called at {self.Time}, "f"basket_avg={np.mean([self.Securities[p].Price for p in self.fxSymbols if self.Securities[p].Price > 0]):.5f}")
if self.IsWarmingUp or not all(indicator.IsReady for indicator in self.fxSmaIndicators.values()):
return
basket_avg_price = np.mean([self.Securities[p].Price for p in self.fxSymbols if self.Securities[p].Price > 0])
if basket_avg_price == 0: return
sma_avg = np.mean([self.fxSmaIndicators[p].Current.Value for p in self.fxSymbols])
current_signal = None
if basket_avg_price > sma_avg * (1 + self.fxSignalThreshold):
current_signal = "BUY"
elif basket_avg_price < sma_avg * (1 - self.fxSignalThreshold):
current_signal = "SELL"
self.fxPreviousSignal = current_signal
is_invested = any(self.Portfolio[p].Invested for p in self.fxSymbols)
if not is_invested and current_signal:
self.fx_place_basket_trade(current_signal)
def fx_place_basket_trade(self, direction):
"""
Liquidates existing positions and places new limit orders to enter a position.
"""
if not self.has_sufficient_buying_power():
return
# Cancel all open orders (entry, SL, TP) before placing new ones
for ticket in self.fxOpenTradeTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
self.fxOpenTradeTickets.clear()
for ticket in self.fxOpenEntryTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
self.fxOpenEntryTickets.clear()
# Liquidate any existing positions
for pair in self.fxSymbols:
if self.Portfolio[pair].Invested:
self.Liquidate(pair, f"FX Basket: New signal '{direction}' received.")
# --- AGGRESSIVE DYNAMIC POSITION SIZING ---
# WARNING: This sizing is very aggressive and likely to cause margin calls.
portfolio_value_increment = 25000
lots_per_basket_increment = 12500
num_baskets_to_trade = int(self.Portfolio.TotalPortfolioValue / portfolio_value_increment)
self.Log(f"FX Basket Sizing: Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}. Affords {num_baskets_to_trade} baskets.")
if num_baskets_to_trade == 0:
self.Log("FX Basket: Trade SKIPPED. Portfolio value is less than the $25,000 required for one basket.")
return
final_lots_per_pair = lots_per_basket_increment * num_baskets_to_trade
self.Log(f"FX Basket: Placing BASKET {direction} limit orders. Lots per pair: {final_lots_per_pair:,} ({num_baskets_to_trade} baskets)")
for pair in self.fxSymbols:
price = self.Securities[pair].Price
if price == 0:
self.Log(f"FX Basket: Price for {pair} is zero. Skipping order.")
continue
order_quantity = final_lots_per_pair if direction == "BUY" else -final_lots_per_pair
ticket = self.LimitOrder(pair, order_quantity, price)
self.fxOpenEntryTickets.append(ticket)
def fx_place_bracket_orders(self, symbol):
"""
Places ATR-based Stop Loss and Take Profit orders for a given symbol.
"""
position = self.Portfolio[symbol]
if not position.Invested:
return
atr_value = self.fxAtrIndicators[symbol].Current.Value
if atr_value == 0:
self.Log(f"FX Basket: ATR for {symbol} is zero, cannot place bracket orders.")
return
entry_price = position.AveragePrice
stop_distance = atr_value * self.fxStopLossAtrMultiplier
profit_distance = stop_distance * self.fxRiskRewardRatio
if position.IsLong:
stop_price = entry_price - stop_distance
profit_price = entry_price + profit_distance
else: # IsShort
stop_price = entry_price + stop_distance
profit_price = entry_price - profit_distance
# Place Stop Loss Order
stop_loss_order = self.StopLimitOrder(symbol, -position.Quantity, stop_price, stop_price)
self.fxOpenTradeTickets.append(stop_loss_order)
# Place Take Profit Order
take_profit_order = self.LimitOrder(symbol, -position.Quantity, profit_price)
self.fxOpenTradeTickets.append(take_profit_order)
self.Log(f"FX Basket: Placed Bracket for {symbol}: Stop @ {stop_price:.5f}, TP @ {profit_price:.5f}")
# ==================================================================================
# ===== LIVE TRADING & RESILIENCE HELPERS ==========================================
# ==================================================================================
def log_daily_summary(self):
"""Logs a daily summary of portfolio performance."""
if self.LiveMode:
open_positions = [pos.Symbol.Value for pos in self.Portfolio.Values if pos.Invested]
self.Log(f"DAILY SUMMARY - {self.Time:%Y-%m-%d}: Equity: ${self.Portfolio.TotalPortfolioValue:,.2f}, "
f"Unrealized PnL: ${self.Portfolio.TotalUnrealizedProfit:,.2f}, "
f"Open Positions: {len(open_positions)}, "
f"Margin Used: {(self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue):.1%}")
def OnEndOfAlgorithm(self):
"""Save state at the end of the algorithm."""
self.save_state()
def save_state(self):
"""Saves the algorithm's state variables to ObjectStore for persistence."""
state = {
"btcLastTradeTime": self.btcLastTradeTime,
"btcEntryTime": self.btcEntryTime,
"gapYesterdayVixClose": self.gapYesterdayVixClose,
"gapYesterdayQqqClose": self.gapYesterdayQqqClose,
"gapLastProcessedDate": self.gapLastProcessedDate,
"btc_trailing_high": self.btc_trailing_high,
"peakEquity": self.peakEquity,
"drawdownAlertSent": self.drawdownAlertSent
}
self.ObjectStore.Save("algorithm_state", json.dumps(state, default=str))
self.Log("Algorithm state saved.")
def load_state(self):
"""Loads the algorithm's state from ObjectStore on startup."""
if self.ObjectStore.ContainsKey("algorithm_state"):
try:
state_json = self.ObjectStore.Read("algorithm_state")
state = json.loads(state_json)
parse_dt = lambda dt_str: datetime.strptime(dt_str.split('.')[0], '%Y-%m-%d %H:%M:%S') if dt_str and dt_str != 'None' else None
self.btcLastTradeTime = parse_dt(state.get("btcLastTradeTime"))
self.btcEntryTime = parse_dt(state.get("btcEntryTime"))
self.gapYesterdayVixClose = state.get("gapYesterdayVixClose")
self.gapYesterdayQqqClose = state.get("gapYesterdayQqqClose")
self.gapLastProcessedDate = datetime.strptime(state.get("gapLastProcessedDate"), '%Y-%m-%d').date() if state.get("gapLastProcessedDate") else None
self.btc_trailing_high = state.get("btc_trailing_high", 0)
self.peakEquity = state.get("peakEquity", self.Portfolio.TotalPortfolioValue)
self.drawdownAlertSent = state.get("drawdownAlertSent", False)
self.Log("Successfully loaded saved algorithm state.")
except Exception as e:
self.Log(f"Error loading state: {e}. Starting with a fresh state.")