| Overall Statistics |
|
Total Orders 1450 Average Win 0.59% Average Loss -0.39% Compounding Annual Return -46.389% Drawdown 20.000% Expectancy -0.124 Start Equity 100000 End Equity 85751.4 Net Profit -14.249% Sharpe Ratio -1.908 Sortino Ratio -1.704 Probabilistic Sharpe Ratio 2.661% Loss Rate 65% Win Rate 35% Profit-Loss Ratio 1.50 Alpha 0 Beta 0 Annual Standard Deviation 0.206 Annual Variance 0.043 Information Ratio -1.642 Tracking Error 0.206 Treynor Ratio 0 Total Fees $1298.60 Estimated Strategy Capacity $160000000.00 Lowest Capacity Asset ES YYFADOG4CO3L Portfolio Turnover 2378.80% Drawdown Recovery 0 |
from AlgorithmImports import *
from datetime import timedelta
from collections import defaultdict
"""
MULTI-TIMEFRAME FUTURES TRADING STRATEGY
==========================================
QuantConnect Algorithm with Interactive Brokers Integration
DATE: October 2025
VERSION: 2.0
DESCRIPTION:
This algorithm trades futures (ES and others) using multi-timeframe confirmation.
It monitors user-selected higher timeframes and enters positions based on alignment requirements.
Features instant bracket orders with trailing stops and optional re-entry logic.
NEW IN v2.0:
- Instant stop-loss on entry using bracket orders
- Trading hours configuration
- Instant stop-loss on re-entries with configurable offsets
- Improved order management and tracking
- Reversal re-entry option
IMPORTANT: Before running live, ensure your Interactive Brokers account:
1. Has futures trading permissions enabled
2. Has sufficient margin for futures positions
3. Is connected to QuantConnect via your IB account number
"""
class MultiTimeframeFuturesStrategy(QCAlgorithm):
def initialize(self) -> None:
# Core settings
self.set_start_date(2025, 8, 1)
self.set_cash(100000)
self.set_time_zone(TimeZones.NEW_YORK)
# Brokerage model & security initializer for IB-like fills/fees and seeding prices
self.set_brokerage_model(InteractiveBrokersBrokerageModel())
self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
# Default order properties (explicit TimeInForce for auditability)
self.DefaultOrderProperties = OrderProperties()
self.DefaultOrderProperties.time_in_force = TimeInForce.DAY
# ============================================
# USER CONFIGURABLE PARAMETERS - EDIT HERE
# ============================================
# Direction Mode: "BOTH", "LONG_ONLY", "SHORT_ONLY"
self.direction_mode = "BOTH"
# Primary Trading Timeframe (for entry/exit bar calculations)
self.primary_timeframe_minutes = 15 # Options: 5, 15, 30, 60
# ===== TRADING HOURS CONFIGURATION =====
# Set to None to trade all hours, or specify time ranges
# Times are in the timezone set above (NEW_YORK)
self.trading_start_time = time(9, 30) # Example: time(9, 30) for 9:30 AM
self.trading_end_time = time(16, 0) # Example: time(16, 0) for 4:00 PM
# ===== TIMEFRAME SELECTION =====
# Choose which higher timeframes to monitor
self.monitored_timeframes = [
Resolution.HOUR, # 1 Hour
(Resolution.HOUR, 4), # 4 Hours
Resolution.DAILY, # 1 Day
(Resolution.DAILY, 7), # 1 Week
(Resolution.DAILY, 30), # 1 Month
(Resolution.DAILY, 90), # 1 Quarter
(Resolution.DAILY, 365), # 1 Year
]
# Multi-Timeframe Confirmation Settings
self.required_confirmations = 4 # Need 4 out of monitored timeframes aligned
# Entry Offsets (in points - ES = $50 per point)
self.long_entry_offset = 1.0 # Points above high for long entry
self.short_entry_offset = 1.0 # Points below low for short entry
# Exit Offsets (initial stop-loss on entry)
self.long_exit_offset = 1.0 # Points below low for long stop
self.short_exit_offset = 1.0 # Points above high for short stop
# Position Sizing
self.contracts_per_trade = 1 # Number of contracts per trade
# Re-entry controls
self.reentry_enabled = True
self.reentry_mode = "REVERSAL" # Options: "FIXED" or "REVERSAL"
# FIXED mode: Re-entry at fixed distance from exit price
self.reentry_distance_points = 1.0 # Points from exit price for re-entry
# REVERSAL mode: Re-entry after price reverses from extreme
self.reversal_distance_points = 5.0 # Points from extreme for re-entry trigger
# Common re-entry settings
self.reentry_time_window_minutes = 30 # Time window to allow re-entry
self.max_reentries = 1 # Maximum re-entries per original signal
# Re-entry stop-loss offsets (from current candle)
self.reentry_long_stop_offset = 0.25 # Points below low for long re-entry stop
self.reentry_short_stop_offset = 0.25 # Points above high for short re-entry stop
# Subscribe to ES front month with minute data
ticker = Futures.Indices.SP_500_E_MINI
future = self.add_future(ticker, Resolution.MINUTE)
future.set_filter(0, 90)
self.future_symbol = future.symbol # canonical future symbol
# ============================================
# END CONFIGURATION EDIT
# ============================================
# Validate configuration
if self.required_confirmations > len(self.monitored_timeframes):
self.debug(f"WARNING: required_confirmations ({self.required_confirmations}) exceeds monitored timeframes ({len(self.monitored_timeframes)})")
self.debug(f"Setting required_confirmations to {len(self.monitored_timeframes)}")
self.required_confirmations = len(self.monitored_timeframes)
# Generate timeframe labels for logging
self.timeframe_labels = []
for tf in self.monitored_timeframes:
if isinstance(tf, tuple):
resolution, mult = tf
if resolution == Resolution.DAILY:
if mult == 7:
self.timeframe_labels.append("1W")
elif mult == 30:
self.timeframe_labels.append("1M")
elif mult == 90:
self.timeframe_labels.append("1Q")
elif mult == 365:
self.timeframe_labels.append("1Y")
elif mult == 14:
self.timeframe_labels.append("2W")
else:
self.timeframe_labels.append(f"{mult}D")
elif resolution == Resolution.HOUR:
self.timeframe_labels.append(f"{mult}H")
else:
self.timeframe_labels.append(f"{mult}m")
else:
if tf == Resolution.DAILY:
self.timeframe_labels.append("1D")
elif tf == Resolution.HOUR:
self.timeframe_labels.append("1H")
elif tf == Resolution.MINUTE:
self.timeframe_labels.append("1m")
else:
self.timeframe_labels.append(str(tf))
# Calculate longest timeframe for warmup
max_days = 0
for tf in self.monitored_timeframes:
if isinstance(tf, tuple):
resolution, mult = tf
if resolution == Resolution.DAILY:
max_days = max(max_days, mult)
elif resolution == Resolution.HOUR:
max_days = max(max_days, mult / 24)
else:
if tf == Resolution.DAILY:
max_days = max(max_days, 1)
elif tf == Resolution.HOUR:
max_days = max(max_days, 1/24)
warmup_days = int(max_days * 5) # 5x longest timeframe for safety
self.set_warmup(timedelta(days=warmup_days))
# Data buffers and state
self.timeframe_data = {} # index -> {"bars": list[TradeBar], "current_bar": TradeBar}
self.active_contract = None
self.current_consolidated_symbol = None
self.consolidators_by_symbol = {} # symbol -> list[consolidators]
self.current_position = 0 # -1, 0, +1
self.entry_stop_order = None
self.exit_stop_order = None
self.last_completed_bar = None
# Order tracking for bracket orders
self.entry_ticket = None
self.stop_ticket = None
self.entry_filled = False
self.reentry_ticket = None
self.reentry_stop_ticket = None
self.reentry_expiry = None
self.reentry_count = 0
self.current_signal_direction = 0 # -1 short, +1 long, 0 none
# Reversal tracking for REVERSAL mode
self.tracking_reversal = False
self.reversal_extreme_price = None # Track highest/lowest since exit
self.reversal_exit_price = None
# Scheduled events
self.schedule.on(
self.date_rules.every_day(self.future_symbol),
self.time_rules.every(timedelta(minutes=self.primary_timeframe_minutes)),
self.check_and_update_orders,
)
self.schedule.on(
self.date_rules.every_day(self.future_symbol),
self.time_rules.before_market_close(self.future_symbol, 5),
self.close_all_positions_eod,
)
self.debug("=" * 60)
self.debug("MULTI-TIMEFRAME FUTURES STRATEGY v2.0 INITIALIZED")
self.debug("=" * 60)
self.debug(f"Direction Mode: {self.direction_mode}")
self.debug(f"Primary Timeframe: {self.primary_timeframe_minutes} minutes")
if self.trading_start_time and self.trading_end_time:
self.debug(f"Trading Hours: {self.trading_start_time.strftime('%H:%M')} - {self.trading_end_time.strftime('%H:%M')}")
else:
self.debug("Trading Hours: 24/7 (No restrictions)")
self.debug(f"Monitored Timeframes ({len(self.monitored_timeframes)}): {', '.join(self.timeframe_labels)}")
self.debug(f"Required Confirmations: {self.required_confirmations}/{len(self.monitored_timeframes)}")
self.debug(f"Entry Offsets - Long: {self.long_entry_offset}, Short: {self.short_entry_offset}")
self.debug(f"Exit Offsets - Long: {self.long_exit_offset}, Short: {self.short_exit_offset}")
self.debug(f"Contracts Per Trade: {self.contracts_per_trade}")
self.debug(f"Re-entry Enabled: {self.reentry_enabled}")
if self.reentry_enabled:
self.debug(f" - Mode: {self.reentry_mode}")
if self.reentry_mode == "FIXED":
self.debug(f" - Fixed Distance: {self.reentry_distance_points} points")
elif self.reentry_mode == "REVERSAL":
self.debug(f" - Reversal Distance: {self.reversal_distance_points} points")
self.debug(f" - Time Window: {self.reentry_time_window_minutes} minutes")
self.debug(f" - Max Re-entries: {self.max_reentries}")
self.debug(f" - Re-entry Stop Offsets - Long: {self.reentry_long_stop_offset}, Short: {self.reentry_short_stop_offset}")
self.debug(f"Warmup Period: {warmup_days} days")
self.debug("=" * 60)
def is_within_trading_hours(self) -> bool:
"""Check if current time is within configured trading hours"""
if self.trading_start_time is None or self.trading_end_time is None:
return True
current_time = self.time.time()
# Handle cases where trading hours span midnight
if self.trading_start_time <= self.trading_end_time:
return self.trading_start_time <= current_time <= self.trading_end_time
else:
return current_time >= self.trading_start_time or current_time <= self.trading_end_time
def on_data(self, data: Slice) -> None:
# Track reversal extremes if in reversal mode
if self.tracking_reversal and self.active_contract is not None:
current_price = self.securities[self.active_contract.symbol].price
if self.current_signal_direction == 1: # Looking for long re-entry
# Track lowest price since exit
if self.reversal_extreme_price is None or current_price < self.reversal_extreme_price:
self.reversal_extreme_price = current_price
# Check if price has reversed enough from the low
if current_price >= self.reversal_extreme_price + self.reversal_distance_points:
self.place_reversal_reentry()
elif self.current_signal_direction == -1: # Looking for short re-entry
# Track highest price since exit
if self.reversal_extreme_price is None or current_price > self.reversal_extreme_price:
self.reversal_extreme_price = current_price
# Check if price has reversed enough from the high
if current_price <= self.reversal_extreme_price - self.reversal_distance_points:
self.place_reversal_reentry()
# Identify and roll to the front month contract if needed
for chain_kvp in data.future_chains:
chain = chain_kvp.value
contracts = [c for c in chain]
if not contracts:
continue
# Select nearest expiry (front month)
front = sorted(contracts, key=lambda c: c.expiry)[0]
new_symbol = front.symbol
if self.active_contract is None or self.active_contract.symbol != new_symbol:
# Roll: liquidate and cancel old orders, remove old consolidators
if self.active_contract is not None:
old_symbol = self.active_contract.symbol
self.debug(f"ROLLING CONTRACT: {old_symbol} -> {new_symbol}")
if self.portfolio[old_symbol].invested:
self.liquidate(old_symbol)
self.debug(f" Liquidated position in {old_symbol}")
self.cancel_all_tracked_orders()
self.remove_consolidators_for_symbol(old_symbol)
else:
self.debug(f"INITIAL CONTRACT: {new_symbol}")
# Activate new contract and set up consolidators
self.active_contract = front
self.debug(f" Contract Expiry: {front.expiry.strftime('%Y-%m-%d')}")
self.debug(f" Setting up consolidators for {new_symbol}")
self.setup_consolidators(new_symbol)
# ---------------- Consolidator Management ----------------
def setup_consolidators(self, symbol: Symbol) -> None:
if self.current_consolidated_symbol is not None and self.current_consolidated_symbol != symbol:
self.remove_consolidators_for_symbol(self.current_consolidated_symbol)
#self.timeframe_data.clear()
self.last_completed_bar = None
cons = []
# Primary timeframe consolidator
primary_con = TradeBarConsolidator(timedelta(minutes=self.primary_timeframe_minutes))
primary_con.data_consolidated += self.on_primary_bar_consolidated
self.subscription_manager.add_consolidator(symbol, primary_con)
cons.append(primary_con)
# Higher timeframes
for i, tf in enumerate(self.monitored_timeframes):
if isinstance(tf, tuple):
resolution, mult = tf
if resolution == Resolution.DAILY:
period = timedelta(days=int(mult))
elif resolution == Resolution.HOUR:
period = timedelta(hours=int(mult))
else:
period = timedelta(minutes=int(mult))
else:
if tf == Resolution.DAILY:
period = timedelta(days=1)
elif tf == Resolution.HOUR:
period = timedelta(hours=1)
elif tf == Resolution.MINUTE:
period = timedelta(minutes=1)
else:
period = timedelta(minutes=1)
con = TradeBarConsolidator(period)
con.data_consolidated += self._make_higher_tf_handler(i)
self.subscription_manager.add_consolidator(symbol, con)
cons.append(con)
self.consolidators_by_symbol[symbol] = cons
self.current_consolidated_symbol = symbol
self.debug(f" Consolidators setup complete ({len(cons)} total)")
def remove_consolidators_for_symbol(self, symbol: Symbol) -> None:
if symbol in self.consolidators_by_symbol:
for con in self.consolidators_by_symbol[symbol]:
self.subscription_manager.remove_consolidator(symbol, con)
self.consolidators_by_symbol[symbol].clear()
del self.consolidators_by_symbol[symbol]
def _make_higher_tf_handler(self, index: int):
def _handler(sender, bar):
self.on_higher_timeframe_bar(sender, bar, index)
return _handler
# ---------------- Data Handlers ----------------
def on_primary_bar_consolidated(self, sender, bar: TradeBar) -> None:
self.last_completed_bar = bar
def on_higher_timeframe_bar(self, sender, bar: TradeBar, timeframe_index: int) -> None:
tf_data = self._ensure_tf_slot(timeframe_index)
if tf_data["current_bar"] is not None:
tf_data["bars"].append(tf_data["current_bar"])
if len(tf_data["bars"]) > 10:
tf_data["bars"].pop(0)
tf_data["current_bar"] = bar
def _ensure_tf_slot(self, index: int) -> dict:
if index not in self.timeframe_data:
self.timeframe_data[index] = {"bars": [], "current_bar": None}
return self.timeframe_data[index]
# ---------------- Core Logic ----------------
def check_and_update_orders(self) -> None:
if self.active_contract is None or self.last_completed_bar is None:
return
# Handle re-entry ticket expiration
if self.reentry_expiry is not None and self.time >= self.reentry_expiry:
if self.reentry_ticket is not None:
self.reentry_ticket.cancel()
self.reentry_ticket = None
if self.reentry_stop_ticket is not None:
self.reentry_stop_ticket.cancel()
self.reentry_stop_ticket = None
self.tracking_reversal = False
self.reversal_extreme_price = None
self.reversal_exit_price = None
self.debug("Re-entry window EXPIRED - resuming normal entry signals")
# Update current position state
self.current_position = 0
pos = self.portfolio[self.active_contract.symbol]
if pos is not None and pos.invested:
self.current_position = 1 if pos.is_long else -1
if self.current_position != 0:
self.update_trailing_stop()
else:
# Only check for new entry signals if not in re-entry mode
if not self.tracking_reversal and self.reentry_ticket is None and self.entry_ticket is None:
self.check_entry_signal()
def check_entry_signal(self) -> None:
if self.is_warming_up or self.last_completed_bar is None:
return
# Check trading hours
if not self.is_within_trading_hours():
return
bullish_count = 0
bearish_count = 0
ready_timeframes = 0
timeframe_status = []
log_message = f"--- Signal Check ({self.time.strftime('%H:%M')}) ---"
for i in range(len(self.monitored_timeframes)):
tf_data = self.timeframe_data.get(i)
if tf_data is None:
timeframe_status.append(f"{self.timeframe_labels[i]}:N/A")
continue
current_forming = tf_data.get("current_bar")
bars = tf_data.get("bars")
if current_forming is None or bars is None or len(bars) == 0:
timeframe_status.append(f"{self.timeframe_labels[i]}:N/A")
continue
ready_timeframes += 1
previous_completed = bars[-1]
if current_forming.close > previous_completed.close:
bullish_count += 1
timeframe_status.append(f"{self.timeframe_labels[i]}:BULL")
elif current_forming.close < previous_completed.close:
bearish_count += 1
timeframe_status.append(f"{self.timeframe_labels[i]}:BEAR")
else:
timeframe_status.append(f"{self.timeframe_labels[i]}:FLAT")
log_message += f"\nCounts: B:{bullish_count} | R:{bearish_count} | Required:{self.required_confirmations}"
log_message += f"\nStatus: {' | '.join(timeframe_status)}"
self.debug(log_message)
if ready_timeframes < int(self.required_confirmations):
return
long_signal = bullish_count >= int(self.required_confirmations)
short_signal = bearish_count >= int(self.required_confirmations)
if self.direction_mode == "LONG_ONLY":
short_signal = False
elif self.direction_mode == "SHORT_ONLY":
long_signal = False
if self.entry_ticket is not None:
self.entry_ticket.cancel()
self.entry_ticket = None
if self.stop_ticket is not None:
self.stop_ticket.cancel()
self.stop_ticket = None
symbol = self.active_contract.symbol
if long_signal:
entry_price = float(self.last_completed_bar.high + self.long_entry_offset)
stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
# Place bracket order (entry with attached stop)
self.entry_ticket = self.stop_market_order(symbol, int(self.contracts_per_trade), entry_price)
self.entry_filled = False
self.current_signal_direction = 1
self.reentry_count = 0
self.debug("=" * 60)
self.debug(f"LONG SIGNAL DETECTED ({bullish_count}/{len(self.monitored_timeframes)} bullish)")
self.debug(f" Timeframes: {' | '.join(timeframe_status)}")
self.debug(f" Last Bar High: {self.last_completed_bar.high:.2f}")
self.debug(f" BUY STOP @ {entry_price:.2f}")
self.debug(f" Initial STOP @ {stop_price:.2f} (will activate on fill)")
self.debug("=" * 60)
elif short_signal:
entry_price = float(self.last_completed_bar.low - self.short_entry_offset)
stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
# Place bracket order (entry with attached stop)
self.entry_ticket = self.stop_market_order(symbol, -int(self.contracts_per_trade), entry_price)
self.entry_filled = False
self.current_signal_direction = -1
self.reentry_count = 0
self.debug("=" * 60)
self.debug(f"SHORT SIGNAL DETECTED ({bearish_count}/{len(self.monitored_timeframes)} bearish)")
self.debug(f" Timeframes: {' | '.join(timeframe_status)}")
self.debug(f" Last Bar Low: {self.last_completed_bar.low:.2f}")
self.debug(f" SELL STOP @ {entry_price:.2f}")
self.debug(f" Initial STOP @ {stop_price:.2f} (will activate on fill)")
self.debug("=" * 60)
def update_trailing_stop(self) -> None:
if self.last_completed_bar is None or self.active_contract is None:
return
if self.exit_stop_order is not None:
self.exit_stop_order.cancel()
symbol = self.active_contract.symbol
if self.current_position == 1:
stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
self.debug(f"Long trailing stop updated -> {stop_price:.2f}")
elif self.current_position == -1:
stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
self.debug(f"Short trailing stop updated -> {stop_price:.2f}")
def on_order_event(self, orderEvent: OrderEvent) -> None:
if orderEvent is None:
return
order_id = orderEvent.order_id
# Entry order filled - immediately place stop loss
if self.entry_ticket is not None and order_id == self.entry_ticket.order_id:
if orderEvent.status == OrderStatus.FILLED and not self.entry_filled:
self.entry_filled = True
position_type = "LONG" if orderEvent.fill_quantity > 0 else "SHORT"
symbol = self.active_contract.symbol
# Place immediate stop loss
if orderEvent.fill_quantity > 0: # Long position
stop_price = float(self.last_completed_bar.low - self.long_exit_offset)
self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
else: # Short position
stop_price = float(self.last_completed_bar.high + self.short_exit_offset)
self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
self.debug("=" * 60)
self.debug(f"ENTRY FILLED - {position_type} position")
self.debug(f" Fill Price: {orderEvent.fill_price:.2f}")
self.debug(f" Quantity: {orderEvent.fill_quantity}")
self.debug(f" INSTANT STOP placed @ {stop_price:.2f}")
self.debug("=" * 60)
self.entry_ticket = None
return
# Exit stop triggered (stopped out)
if self.exit_stop_order is not None and order_id == self.exit_stop_order.order_id:
if orderEvent.status == OrderStatus.FILLED:
exit_price = float(orderEvent.fill_price)
position_type = "LONG" if self.current_position == 1 else "SHORT"
self.debug("=" * 60)
self.debug(f"STOPPED OUT - {position_type} position")
self.debug(f" Exit Price: {exit_price:.2f}")
self.debug(f" Quantity: {orderEvent.fill_quantity}")
self.exit_stop_order = None
if self.reentry_enabled and self.reentry_count < int(self.max_reentries) and self.current_signal_direction != 0:
if self.is_within_trading_hours():
self.debug(f" Re-entry enabled (attempt {self.reentry_count + 1}/{self.max_reentries})")
if self.reentry_mode == "FIXED":
self.place_fixed_reentry(exit_price)
elif self.reentry_mode == "REVERSAL":
self.start_reversal_tracking(exit_price)
else:
self.debug(f" Unknown re-entry mode: {self.reentry_mode}")
else:
self.debug(" Re-entry skipped: Outside trading hours")
else:
if not self.reentry_enabled:
self.debug(" Re-entry disabled")
elif self.reentry_count >= int(self.max_reentries):
self.debug(f" Max re-entries reached ({self.max_reentries})")
self.debug("=" * 60)
return
# Re-entry order filled - immediately place stop loss
if self.reentry_ticket is not None and order_id == self.reentry_ticket.order_id:
if orderEvent.status == OrderStatus.FILLED:
position_type = "LONG" if orderEvent.fill_quantity > 0 else "SHORT"
symbol = self.active_contract.symbol
# Place immediate stop loss for re-entry
if orderEvent.fill_quantity > 0: # Long re-entry
stop_price = float(self.last_completed_bar.low - self.reentry_long_stop_offset)
self.exit_stop_order = self.stop_market_order(symbol, -int(self.contracts_per_trade), stop_price)
else: # Short re-entry
stop_price = float(self.last_completed_bar.high + self.reentry_short_stop_offset)
self.exit_stop_order = self.stop_market_order(symbol, int(self.contracts_per_trade), stop_price)
self.debug("=" * 60)
self.debug(f"RE-ENTRY FILLED - {position_type} position")
self.debug(f" Fill Price: {orderEvent.fill_price:.2f}")
self.debug(f" Quantity: {orderEvent.fill_quantity}")
self.debug(f" Re-entry count: {self.reentry_count + 1}")
self.debug(f" INSTANT STOP placed @ {stop_price:.2f}")
self.debug("=" * 60)
self.reentry_ticket = None
if self.reentry_stop_ticket is not None:
self.reentry_stop_ticket.cancel()
self.reentry_stop_ticket = None
self.reentry_count += 1
self.tracking_reversal = False
self.reversal_extreme_price = None
self.reversal_exit_price = None
def place_fixed_reentry(self, exit_price: float) -> None:
"""Place re-entry order at fixed distance from exit price (FIXED mode)"""
if self.active_contract is None or self.last_completed_bar is None:
return
symbol = self.active_contract.symbol
if self.current_signal_direction == 1:
reentry_price = float(exit_price + self.reentry_distance_points)
self.reentry_ticket = self.stop_market_order(symbol, int(self.contracts_per_trade), reentry_price)
self.debug(f" [FIXED] RE-ENTRY BUY STOP placed @ {reentry_price:.2f}")
elif self.current_signal_direction == -1:
reentry_price = float(exit_price - self.reentry_distance_points)
self.reentry_ticket = self.stop_market_order(symbol, -int(self.contracts_per_trade), reentry_price)
self.debug(f" [FIXED] RE-ENTRY SELL STOP placed @ {reentry_price:.2f}")
else:
return
self.reentry_expiry = self.time + timedelta(minutes=int(self.reentry_time_window_minutes))
self.debug(f" Expires at: {self.reentry_expiry.strftime('%H:%M:%S')}")
def start_reversal_tracking(self, exit_price: float) -> None:
"""Start tracking price extremes for reversal-based re-entry (REVERSAL mode)"""
self.tracking_reversal = True
self.reversal_exit_price = exit_price
self.reversal_extreme_price = exit_price # Initialize with exit price
self.reentry_expiry = self.time + timedelta(minutes=int(self.reentry_time_window_minutes))
direction = "LONG" if self.current_signal_direction == 1 else "SHORT"
self.debug(f" [REVERSAL] Tracking started for {direction} re-entry")
self.debug(f" Will trigger on {self.reversal_distance_points:.2f} point reversal")
self.debug(f" Expires at: {self.reentry_expiry.strftime('%H:%M:%S')}")
def place_reversal_reentry(self) -> None:
"""Place re-entry order when reversal condition is met"""
if self.active_contract is None or self.last_completed_bar is None:
return
if not self.tracking_reversal:
return
symbol = self.active_contract.symbol
current_price = self.securities[symbol].price
if self.current_signal_direction == 1:
# Long re-entry: price reversed up from the low
self.reentry_ticket = self.market_order(symbol, int(self.contracts_per_trade))
self.debug("=" * 60)
self.debug(f"[REVERSAL] LONG RE-ENTRY TRIGGERED")
self.debug(f" Extreme Low: {self.reversal_extreme_price:.2f}")
self.debug(f" Current Price: {current_price:.2f}")
self.debug(f" Reversal: {current_price - self.reversal_extreme_price:.2f} points")
self.debug(f" MARKET ORDER placed")
self.debug("=" * 60)
elif self.current_signal_direction == -1:
# Short re-entry: price reversed down from the high
self.reentry_ticket = self.market_order(symbol, -int(self.contracts_per_trade))
self.debug("=" * 60)
self.debug(f"[REVERSAL] SHORT RE-ENTRY TRIGGERED")
self.debug(f" Extreme High: {self.reversal_extreme_price:.2f}")
self.debug(f" Current Price: {current_price:.2f}")
self.debug(f" Reversal: {self.reversal_extreme_price - current_price:.2f} points")
self.debug(f" MARKET ORDER placed")
self.debug("=" * 60)
# Stop tracking reversal once order is placed
self.tracking_reversal = False
def place_reentry_order(self, exit_price: float) -> None:
"""Legacy method - redirects to appropriate re-entry mode"""
if self.reentry_mode == "FIXED":
self.place_fixed_reentry(exit_price)
elif self.reentry_mode == "REVERSAL":
self.start_reversal_tracking(exit_price)
def cancel_all_tracked_orders(self) -> None:
"""Cancel all tracked order tickets"""
if self.entry_ticket is not None:
self.entry_ticket.cancel()
self.entry_ticket = None
if self.stop_ticket is not None:
self.stop_ticket.cancel()
self.stop_ticket = None
if self.exit_stop_order is not None:
self.exit_stop_order.cancel()
self.exit_stop_order = None
if self.reentry_ticket is not None:
self.reentry_ticket.cancel()
self.reentry_ticket = None
if self.reentry_stop_ticket is not None:
self.reentry_stop_ticket.cancel()
self.reentry_stop_ticket = None
def close_all_positions_eod(self) -> None:
if self.is_warming_up:
return
self.debug("=" * 60)
self.debug("END OF DAY - Closing all positions and canceling orders")
# Cancel all tracked orders
self.cancel_all_tracked_orders()
self.debug(" Canceled all tracked orders")
# Cancel any other open orders by id
self.cancel_open_orders()
# Liquidate all positions
if self.portfolio.invested:
self.liquidate(tag="EOD liquidation")
self.debug(" Liquidated all positions")
else:
self.debug(" No positions to close")
# Reset state
self.current_position = 0
self.current_signal_direction = 0
self.reentry_count = 0
self.entry_filled = False
self.tracking_reversal = False
self.reversal_extreme_price = None
self.reversal_exit_price = None
self.debug(" State reset complete")
self.debug("=" * 60)
def cancel_open_orders(self) -> None:
"""Cancel remaining open orders by id"""
open_orders = self.transactions.get_open_orders()
if open_orders is None:
return
for order in open_orders:
if order is not None:
self.transactions.cancel_order(order.id)
# ---------------- Brokerage Lifecycle & Margin Safety ----------------
def on_brokerage_disconnect(self) -> None:
self._ib_pause_new_entries = True
self._ib_disconnect_time = self.time
self.debug("Brokerage disconnected. Pausing new entries.")
# Cancel risk-increasing entries while disconnected
if self.entry_ticket is not None:
self.entry_ticket.cancel()
self.entry_ticket = None
if self.stop_ticket is not None:
self.stop_ticket.cancel()
self.stop_ticket = None
if self.reentry_ticket is not None:
self.reentry_ticket.cancel()
self.reentry_ticket = None
if self.reentry_stop_ticket is not None:
self.reentry_stop_ticket.cancel()
self.reentry_stop_ticket = None
def on_brokerage_message(self, message_event: BrokerageMessageEvent) -> None:
self.debug(f"Brokerage message: {message_event.message}")
if message_event.type == BrokerageMessageType.RECONNECT:
self._ib_pause_new_entries = False
self._ib_disconnect_time = None
self.debug("Brokerage reconnected. Resuming entries.")
def on_margin_call_warning(self) -> None:
self.debug("Margin call warning received: canceling entries and reducing exposure.")
self.cancel_all_tracked_orders()
if self.portfolio.invested:
self.liquidate(tag="Margin Warning Flatten")
def on_margin_call(self, requests: list) -> None:
self.debug("Margin call received: liquidating positions to restore compliance.")
if self.portfolio.invested:
self.liquidate(tag="Margin Call Flatten")
self.cancel_all_tracked_orders()
self.cancel_open_orders()
def on_end_of_algorithm(self) -> None:
self.debug("=" * 60)
self.debug("STRATEGY COMPLETE")
self.debug("=" * 60)
self.debug(f"Final Portfolio Value: ${self.portfolio.total_portfolio_value:,.2f}")
if self.portfolio.total_portfolio_value > self.portfolio.cash:
profit = self.portfolio.total_portfolio_value - self.portfolio.cash
pct = (profit / self.portfolio.cash) * 100
self.debug(f"Total Profit: ${profit:,.2f} ({pct:+.2f}%)")
else:
loss = self.portfolio.cash - self.portfolio.total_portfolio_value
pct = (loss / self.portfolio.cash) * 100
self.debug(f"Total Loss: ${loss:,.2f} ({pct:.2f}%)")
self.debug("=" * 60)