
开盘区间突破ATR追踪止损策略是一种结合了开盘区间突破(Opening Range Breakout)与智能市场分析(Smart Money Concepts)的量化交易系统。该策略专注于捕捉美股市场开盘后5分钟(09:30-09:35 EST)形成的价格区间突破机会,并结合多重过滤条件确保交易信号质量。系统支持即时或回调入场,采用风险回报比动态调整机制,并可选择ATR(Average True Range)追踪止损来优化利润管理。该策略还具备”二次机会”交易功能,允许在初始交易失败后捕捉反向突破机会,同时提供全面的可视化功能以帮助交易者更好地理解市场动态。
开盘区间突破ATR追踪止损策略的核心逻辑建立在市场开盘后初始价格区间的重要性基础上。该策略首先在特定时间窗口(09:30-09:35 EST)捕捉并记录价格的最高点和最低点,形成”开盘区间”(Opening Range)。随后,系统监控价格对该区间的突破行为,结合以下关键机制确保交易质量:
开盘区间识别与突破验证:系统在指定时间窗口内记录价格最高点和最低点,随后监控突破情况。突破必须通过两重过滤机制验证:
入场机制:策略支持两种入场方式:
止损设置:系统提供两种止损类型:
风险管理:系统采用风险回报比乘数(Risk:Reward Multiplier)自动计算止盈位置,实现动态风险管理。例如,设置2:1的风险回报比意味着潜在盈利是潜在亏损的两倍。
ATR追踪止损:一旦盈利达到预设的风险回报比率,系统可激活基于ATR的追踪止损,锁定部分利润的同时允许趋势延续。
二次机会交易:当初始交易触发止损或失败时,系统可自动寻找开盘区间反向的突破机会,实现当日双向交易的可能性。
专注高质量交易机会:通过多重验证机制(影线过滤、距离过滤),策略显著减少假突破交易,提高胜率。
灵活的入场机制:支持即时或回调入场,适应不同交易风格和市场条件。即时入场适合强劲趋势,而回调入场则可获得更优入场价格。
自适应风险管理:基于风险回报比乘数的动态止盈设置确保每笔交易都具有一致的风险特征,实现标准化的资金管理。
利润最大化:ATR追踪止损功能在保护已实现利润的同时,允许强势行情持续发展,避免过早离场。
高度可视化:系统提供全面的视觉辅助功能,包括区间标记、突破验证标签、交易状态指示、入场/止损/止盈标记等,提升交易决策的直观性。
无偏见后验设计:策略全面采用barstate.isconfirmed确保所有决策都基于确认后的价格数据,避免前瞻偏差,符合真实交易环境。
二次机会机制:通过启用二次机会交易功能,策略可以在初始方向判断错误时快速适应市场变化,捕捉反向机会,提高资金利用效率。
会话管理优化:内置会话结束自动平仓功能确保不会持仓过夜,降低隔夜风险。
区间形成期波动风险:在开盘区间形成期(09:30-09:35),市场可能出现异常波动,导致区间过宽或过窄。过宽的区间可能导致止损过大,而过窄的区间则可能频繁触发假突破。 解决方法:可考虑增加开盘区间大小的过滤条件,排除异常区间;或调整交易日期过滤器,避开高波动性的特定日子(如重要经济数据公布日)。
突破后剧烈回撤风险:有效突破后市场可能出现剧烈回撤,导致止损被触发后市场又继续原方向运动。 解决方法:考虑使用更宽松的止损设置,如对向区间止损;或调整入场机制为回调入场,以获得更好的入场价格和更小的风险暴露。
信号质量依赖过滤器设置:突破验证的影线过滤和距离过滤参数设置对信号质量影响显著,不当的参数可能过滤掉好的交易机会或接受过多低质量信号。 解决方法:通过历史回测优化过滤器参数,找到特定市场和品种的最佳设置;考虑使用自适应参数,根据市场波动性动态调整过滤标准。
追踪止损参数敏感性:ATR追踪止损的参数设置过紧可能导致在小回调中过早退出,而设置过松则可能导致过多回吐利润。 解决方法:基于目标品种的历史波动特性调整ATR周期和乘数;考虑实施分批平仓策略,部分头寸采用固定止盈,部分头寸使用追踪止损。
交易频率限制:策略每日最多执行两次交易(初始交易和二次机会交易),可能无法充分利用日内所有机会。 解决方法:考虑拓展策略,监控日内其他时间段的重要价格区间;或结合其他技术指标形成复合策略,增加交易信号来源。
自适应开盘区间周期:目前策略使用固定的5分钟开盘区间,可考虑根据市场波动性动态调整区间时长。在低波动市场可缩短区间时间至3分钟,而高波动市场则可延长至10分钟,更好地适应不同市场状态。
结合成交量确认:在突破验证机制中增加成交量过滤条件,要求突破时的成交量显著高于前几个周期的平均成交量,提高突破有效性。这可通过计算突破蜡烛成交量与前N个周期成交量均值的比率来实现。
多时间框架分析:引入更高时间框架的趋势方向过滤,仅在日线或小时线趋势方向与突破方向一致时入场,提高交易胜率。可通过简单移动平均线斜率或更高级的趋势指标来确定更高时间框架趋势。
优化资金管理:实施动态头寸规模调整机制,基于历史波动性、当前账户规模和近期绩效自动调整合约数量,实现更精细的风险控制。例如,在连续盈利后逐步增加头寸,连续亏损后减少头寸。
集成机器学习模型:引入机器学习模型评估突破质量,通过历史数据训练模型识别最有可能成功的突破模式。特征可包括开盘区间大小、市场波动性、前一交易日的价格走势、特定时间模式等。
增强二次机会交易逻辑:优化二次机会交易的触发条件,不仅基于初始交易失败,还考虑市场结构变化和新兴的动量指标,提高二次交易的成功率。
个性化品种参数:为不同交易品种开发优化的参数集,考虑到各品种独特的波动特性和价格行为。例如,波动较大的品种可能需要更宽松的过滤器设置和更保守的风险回报比。
整合市场情绪指标:引入VIX指数或其他市场情绪指标,在极端市场情绪期间调整策略参数或暂时禁用交易,避开高不确定性环境。
开盘区间突破ATR追踪止损策略是一个结构完善的量化交易系统,它巧妙地结合了开盘区间突破、智能过滤机制、灵活入场选项和先进的风险管理功能。该策略特别适合美股和期货市场的日内交易,通过捕捉开盘后的方向性突破实现盈利。
策略的核心价值在于其多层验证机制和风险管理系统,通过影线和距离过滤器显著减少假突破交易,同时使用风险回报比乘数和ATR追踪止损确保一致的风险暴露和利润保护。二次机会交易功能则为策略增添了适应性和额外的盈利机会。
尽管该策略具有多项优势,但使用者仍需注意参数优化的重要性,不同市场和品种可能需要针对性调整以达到最佳效果。同时,建议交易者将该策略作为完整交易系统的一部分,结合更广泛的市场分析和风险管理原则使用。
通过实施建议的优化方向,特别是自适应参数、多时间框架分析和增强的资金管理系统,该策略有潜力进一步提高其稳定性和盈利能力,成为专业交易者工具箱中的有力工具。
/*backtest
start: 2025-07-18 00:00:00
end: 2025-07-30 00:00:00
period: 30m
basePeriod: 30m
exchanges: [{"eid":"Futures_Binance","currency":"ETH_USDT"}]
*/
//@version=5
strategy("Casper SMC 5min ORB - Roboquant AI", overlay=true, default_qty_type=strategy.fixed, default_qty_value=1, max_bars_back=500, calc_on_order_fills=true, calc_on_every_tick=false, initial_capital=50000, currency=currency.USD)
// === STRATEGY SETTINGS ===
// Risk Management
contracts = input.int(1, "Contracts", minval=1, group="Risk Management")
risk_multiplier = input.float(2.0, "Risk:Reward Multiplier", minval=0.5, maxval=10.0, group="Risk Management")
sl_points = input.int(2, "Stop Loss Points Below/Above Breakout Candle", minval=1, group="Risk Management")
// Entry Settings
entry_type = input.string("Instant", "Entry Type", options=["Retracement", "Instant"], group="Entry Settings")
retracement_percent = input.float(50.0, "Retracement % of Breakout Candle Body", minval=10.0, maxval=90.0, group="Entry Settings")
// Stop Loss Settings
sl_type = input.string("Opposite Range", "Stop Loss Type", options=["Breakout Candle", "Opposite Range"], group="Stop Loss Settings")
// Second Chance Trade Settings
enable_second_chance = input.bool(false, "Enable Second Chance Trade", group="Second Chance Trade")
second_chance_info = input.string("If initial SL is hit, allow opposite breakout trade", "Info: Second Chance Logic", group="Second Chance Trade")
// Breakout Filter Settings
use_wick_filter = input.bool(false, "Use Wick Filter", group="Breakout Filter")
max_wick_percent = input.float(50.0, "Max Wick % of Candle Body", minval=10.0, maxval=200.0, group="Breakout Filter")
// Breakout Distance Filters
use_breakout_distance_filter = input.bool(true, "Use Breakout Distance Filter", group="Breakout Distance Filter")
min_breakout_multiplier = input.float(0.1, "Min Breakout Distance (OR Size * X)", minval=0.0, maxval=3.0, group="Breakout Distance Filter")
max_breakout_multiplier = input.float(1.6, "Max Breakout Distance (OR Size * X)", minval=0.5, maxval=5.0, group="Breakout Distance Filter")
// Trailing Stop Loss Settings
use_trailing_sl = input.bool(false, "Use Trailing Stop Loss", group="Trailing Stop Loss")
profit_r_multiplier = input.float(1.0, "Start Trailing After X R Profit", minval=0.5, maxval=5.0, group="Trailing Stop Loss")
atr_length = input.int(14, "ATR Length", minval=1, maxval=50, group="Trailing Stop Loss")
atr_multiplier = input.float(1.0, "ATR Multiplier for Trailing", minval=0.5, maxval=5.0, group="Trailing Stop Loss")
// Session Management
or_start_hour = input.int(9, "Opening Range Start Hour", minval=0, maxval=23, group="Session Management")
or_start_minute = input.int(30, "Opening Range Start Minute", minval=0, maxval=59, group="Session Management")
or_end_minute = input.int(35, "Opening Range End Minute", minval=0, maxval=59, group="Session Management")
session_timezone = input.string("America/New_York", "Session Timezone", group="Session Management")
force_session_close = input.bool(true, "Force Close at Session End", group="Session Management")
session_end_hour = input.int(16, "Session End Hour", minval=0, maxval=23, group="Session Management")
session_end_minute = input.int(0, "Session End Minute", minval=0, maxval=59, group="Session Management")
// Day of Week Trading Filters
trade_monday = input.bool(true, "Trade on Monday", group="Day of Week Filters")
trade_tuesday = input.bool(true, "Trade on Tuesday", group="Day of Week Filters")
trade_wednesday = input.bool(true, "Trade on Wednesday", group="Day of Week Filters")
trade_thursday = input.bool(true, "Trade on Thursday", group="Day of Week Filters")
trade_friday = input.bool(true, "Trade on Friday", group="Day of Week Filters")
// Visual Settings
high_line_color = input.color(color.green, title="Opening Range High Line Color", group="Visual Settings")
low_line_color = input.color(color.red, title="Opening Range Low Line Color", group="Visual Settings")
// Label Control Settings
show_trading_disabled_labels = input.bool(false, "Show Trading Disabled Labels", group="Label Controls")
show_breakout_validation_labels = input.bool(true, "Show Breakout Validation Labels", group="Label Controls")
show_second_chance_labels = input.bool(false, "Show Second Chance Labels", group="Label Controls")
show_trade_status_labels = input.bool(false, "Show Trade Status Labels", group="Label Controls")
show_entry_labels = input.bool(false, "Show Entry Labels", group="Label Controls")
show_sl_tp_labels = input.bool(false, "Show Stop Loss / Take Profit Labels", group="Label Controls")
// === VARIABLES ===
// ATR for trailing stop loss
atr = ta.atr(atr_length)
// === NYSE OPENING RANGE LOGIC ===
// FIXED: Using configurable hour/minute inputs with timezone
current_time = time(timeframe.period, "0000-2400:23456", session_timezone)
current_hour = hour(current_time, session_timezone)
current_minute = minute(current_time, session_timezone)
is_opening_range = current_hour == or_start_hour and current_minute >= or_start_minute and current_minute <= or_end_minute
// Check if we're at the start of a new trading day - FIXED: More reliable detection
is_new_day = ta.change(time("1D"))
// ADDED: Check if trading is allowed on current day of week (using session timezone)
current_day = dayofweek(current_time, session_timezone)
is_trading_day_allowed = (current_day == dayofweek.monday and trade_monday) or (current_day == dayofweek.tuesday and trade_tuesday) or (current_day == dayofweek.wednesday and trade_wednesday) or (current_day == dayofweek.thursday and trade_thursday) or (current_day == dayofweek.friday and trade_friday)
// Variables to store opening range high and low for current day
var float or_high = na
var float or_low = na
var bool lines_drawn = false
var bool breakout_occurred = false
var float breakout_candle_high = na
var float breakout_candle_low = na
var float breakout_price = na
var string breakout_direction = na
var int or_start_bar = na // ADDED: Store the bar index when opening range starts
// ADDED: Second chance trade variables
var bool first_trade_sl_hit = false
var string first_trade_direction = na
var bool second_chance_available = false
var bool second_trade_taken = false
var bool daily_trades_complete = false // ADDED: Prevent more than 2 trades per day
// Reset variables at the start of each trading day
if is_new_day
or_high := na
or_low := na
lines_drawn := false
breakout_occurred := false
breakout_candle_high := na
breakout_candle_low := na
breakout_price := na
breakout_direction := na
or_start_bar := na // ADDED: Reset opening range start bar
// ADDED: Reset second chance variables
first_trade_sl_hit := false
first_trade_direction := na
second_chance_available := false
second_trade_taken := false
daily_trades_complete := false // ADDED: Reset trade limit
// Capture opening range data during 09:30-09:35 EST
if is_opening_range
if na(or_high) or na(or_low)
or_high := high
or_low := low
or_start_bar := bar_index // ADDED: Store the bar index when opening range starts
else
or_high := math.max(or_high, high)
or_low := math.min(or_low, low)
// Draw lines when we're past the opening range and haven't drawn yet
if not is_opening_range and not na(or_high) and not na(or_low) and not na(or_start_bar) and not lines_drawn
// FIXED: Lines start from the actual opening range start time and extend forward
start_x = or_start_bar
end_x = bar_index + 50 // Extend lines forward for visibility
lines_drawn := true
// ADDED: Show visual indicator if trading is disabled for current day
if not is_trading_day_allowed and show_trading_disabled_labels
day_name = current_day == dayofweek.monday ? "Monday" :
current_day == dayofweek.tuesday ? "Tuesday" :
current_day == dayofweek.wednesday ? "Wednesday" :
current_day == dayofweek.thursday ? "Thursday" :
current_day == dayofweek.friday ? "Friday" : "Weekend"
label.new(x=bar_index, y=(or_high + or_low) / 2, text="Trading Disabled\n" + day_name, color=color.gray, textcolor=color.white, style=label.style_label_center, size=size.normal)
// Check for breakouts after opening range is complete (only first breakout of the day)
// FIXED: Added barstate.isconfirmed to avoid lookahead bias
if barstate.isconfirmed and not is_opening_range and not na(or_high) and not na(or_low) and lines_drawn and not breakout_occurred and not daily_trades_complete and is_trading_day_allowed
// Calculate candle body and wick percentages
candle_body = math.abs(close - open)
top_wick = high - math.max(open, close)
bottom_wick = math.min(open, close) - low
top_wick_percent = candle_body > 0 ? (top_wick / candle_body) * 100 : 0
bottom_wick_percent = candle_body > 0 ? (bottom_wick / candle_body) * 100 : 0
// ADDED: Calculate opening range size for distance filters
or_size = or_high - or_low
// Check for first breakout above opening range high
if close > or_high
// FIXED: Mark breakout as occurred FIRST (this is THE breakout candle)
breakout_occurred := true
breakout_candle_high := high
breakout_candle_low := low
breakout_price := close
breakout_direction := "long"
// ADDED: Validate this specific breakout candle against distance filter
breakout_distance_valid = true
if use_breakout_distance_filter
min_breakout_level = or_high + (or_size * min_breakout_multiplier)
max_breakout_level = or_high + (or_size * max_breakout_multiplier)
breakout_distance_valid := close >= min_breakout_level and close <= max_breakout_level
// Apply wick filter for long breakouts
wick_filter_valid = not use_wick_filter or top_wick_percent <= max_wick_percent
// Show appropriate label based on validation results
if show_breakout_validation_labels
if wick_filter_valid and breakout_distance_valid
label.new(x=bar_index, y=high, text="VALID", color=high_line_color, textcolor=color.white, style=label.style_label_down, size=size.tiny)
else
label.new(x=bar_index, y=high, text="INVALID", color=color.gray, textcolor=color.white, style=label.style_label_down, size=size.tiny)
// Mark breakout as invalid so no trade will be placed (regardless of label setting)
if not (wick_filter_valid and breakout_distance_valid)
breakout_direction := "invalid"
// Check for first breakout below opening range low
else if close < or_low
// FIXED: Mark breakout as occurred FIRST (this is THE breakout candle)
breakout_occurred := true
breakout_candle_high := high
breakout_candle_low := low
breakout_price := close
breakout_direction := "short"
// ADDED: Validate this specific breakout candle against distance filter
breakout_distance_valid = true
if use_breakout_distance_filter
min_breakout_level = or_low - (or_size * min_breakout_multiplier)
max_breakout_level = or_low - (or_size * max_breakout_multiplier)
breakout_distance_valid := close <= min_breakout_level and close >= max_breakout_level
// Apply wick filter for short breakouts
wick_filter_valid = not use_wick_filter or bottom_wick_percent <= max_wick_percent
// Show appropriate label based on validation results
if show_breakout_validation_labels
if wick_filter_valid and breakout_distance_valid
label.new(x=bar_index, y=low, text="VALID", color=low_line_color, textcolor=color.white, style=label.style_label_up, size=size.tiny)
else
label.new(x=bar_index, y=low, text="INVALID", color=color.gray, textcolor=color.white, style=label.style_label_up, size=size.tiny)
// Mark breakout as invalid so no trade will be placed (regardless of label setting)
if not (wick_filter_valid and breakout_distance_valid)
breakout_direction := "invalid"
// ADDED: Check for second chance breakout (opposite direction after initial SL hit)
// FIXED: Added barstate.isconfirmed to avoid lookahead bias
if barstate.isconfirmed and not is_opening_range and not na(or_high) and not na(or_low) and lines_drawn and second_chance_available and not second_trade_taken and not daily_trades_complete and is_trading_day_allowed
// Calculate candle body and wick percentages
candle_body = math.abs(close - open)
top_wick = high - math.max(open, close)
bottom_wick = math.min(open, close) - low
top_wick_percent = candle_body > 0 ? (top_wick / candle_body) * 100 : 0
bottom_wick_percent = candle_body > 0 ? (bottom_wick / candle_body) * 100 : 0
// ADDED: Calculate opening range size for distance filters
or_size = or_high - or_low
// If first trade was LONG and failed, look for SHORT breakout
if first_trade_direction == "long" and close < or_low
// FIXED: Mark second chance breakout as taken FIRST
second_trade_taken := true
second_chance_available := false
breakout_candle_high := high
breakout_candle_low := low
breakout_price := close
breakout_direction := "short"
// ADDED: Validate this specific breakout candle against distance filter
breakout_distance_valid = true
if use_breakout_distance_filter
min_breakout_level = or_low - (or_size * min_breakout_multiplier)
max_breakout_level = or_low - (or_size * max_breakout_multiplier)
breakout_distance_valid := close <= min_breakout_level and close >= max_breakout_level
// Apply wick filter for short breakouts
wick_filter_valid = not use_wick_filter or bottom_wick_percent <= max_wick_percent
// Show appropriate label based on validation results
if show_second_chance_labels
if wick_filter_valid and breakout_distance_valid
label.new(x=bar_index, y=low, text="2nd Chance\nOR Low Break\nVALID", color=color.orange, textcolor=color.white, style=label.style_label_up, size=size.tiny)
else
label.new(x=bar_index, y=low, text="2nd Chance\nOR Low Break\nINVALID", color=color.gray, textcolor=color.white, style=label.style_label_up, size=size.tiny)
// Mark breakout as invalid so no trade will be placed (regardless of label setting)
if not (wick_filter_valid and breakout_distance_valid)
breakout_direction := "invalid"
// If first trade was SHORT and failed, look for LONG breakout
else if first_trade_direction == "short" and close > or_high
// FIXED: Mark second chance breakout as taken FIRST
second_trade_taken := true
second_chance_available := false
breakout_candle_high := high
breakout_candle_low := low
breakout_price := close
breakout_direction := "long"
// ADDED: Validate this specific breakout candle against distance filter
breakout_distance_valid = true
if use_breakout_distance_filter
min_breakout_level = or_high + (or_size * min_breakout_multiplier)
max_breakout_level = or_high + (or_size * max_breakout_multiplier)
breakout_distance_valid := close >= min_breakout_level and close <= max_breakout_level
// Apply wick filter for long breakouts
wick_filter_valid = not use_wick_filter or top_wick_percent <= max_wick_percent
// Show appropriate label based on validation results
if show_second_chance_labels
if wick_filter_valid and breakout_distance_valid
label.new(x=bar_index, y=high, text="2nd Chance\nOR High Break\nVALID", color=color.orange, textcolor=color.white, style=label.style_label_down, size=size.tiny)
else
label.new(x=bar_index, y=high, text="2nd Chance\nOR High Break\nINVALID", color=color.gray, textcolor=color.white, style=label.style_label_down, size=size.tiny)
// Mark breakout as invalid so no trade will be placed (regardless of label setting)
if not (wick_filter_valid and breakout_distance_valid)
breakout_direction := "invalid"
// === STRATEGY LOGIC ===
// Check if we have a breakout and place retracement entry orders
var bool entry_placed = false
var bool second_entry_placed = false // ADDED: Track second trade entry separately
var float entry_price = na
var float stop_loss = na
var float take_profit = na
var float trailing_stop = na
var bool trailing_active = false
var float initial_risk = na
var bool trailing_started = false
var string current_entry_id = na // FIXED: Track which entry ID we're using
// Arrays to store historical trade boxes
var array<box> historical_trade_boxes = array.new<box>()
var array<box> historical_sl_boxes = array.new<box>()
var array<box> historical_tp_boxes = array.new<box>()
// Variables to track current active trade boxes for extending to exit
var box current_profit_box = na
var box current_sl_box = na
// ADDED: General position close detection for extending boxes - Handle timing issues
if barstate.isconfirmed and strategy.position_size == 0 and strategy.position_size[1] != 0
// Extend trade visualization boxes to exact exit point when any position closes
if not na(current_profit_box)
// Ensure minimum 8 bars width or extend to current bar, whichever is longer
box_left = box.get_left(current_profit_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_profit_box, final_right)
current_profit_box := na // Clear reference after extending
if not na(current_sl_box)
// Ensure minimum 8 bars width or extend to current bar, whichever is longer
box_left = box.get_left(current_sl_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_sl_box, final_right)
current_sl_box := na // Clear reference after extending
// ADDED: Backup safety check - extend boxes if position is closed but boxes still active
if not na(current_profit_box) and strategy.position_size == 0
box_left = box.get_left(current_profit_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_profit_box, final_right)
current_profit_box := na
if not na(current_sl_box) and strategy.position_size == 0
box_left = box.get_left(current_sl_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_sl_box, final_right)
current_sl_box := na
// Reset entry flag on new day
if is_new_day
entry_placed := false
second_entry_placed := false // ADDED: Reset second entry flag
entry_price := na
stop_loss := na
take_profit := na
trailing_stop := na
trailing_active := false
initial_risk := na
trailing_started := false
current_entry_id := na // FIXED: Reset entry ID
current_profit_box := na // ADDED: Reset current trade boxes
current_sl_box := na
// SIMPLIFIED: Detect when position closes to enable second chance (FIXED for lookahead bias)
if barstate.isconfirmed and strategy.position_size == 0 and strategy.position_size[1] != 0 and entry_placed and not first_trade_sl_hit
// A position just closed and we had an active trade
if enable_second_chance and not second_trade_taken
// Simplified logic - if position closed, enable second chance
first_trade_sl_hit := true
first_trade_direction := breakout_direction
second_chance_available := true
// Reset variables for potential second trade
entry_price := na
trailing_stop := na
trailing_active := false
initial_risk := na
trailing_started := false
current_entry_id := na
// Add visual marker
if show_trade_status_labels
label.new(x=bar_index, y=close, text="Trade Closed\nSecond Chance Available", color=color.yellow, textcolor=color.black, style=label.style_label_down, size=size.tiny)
else
// Second chance not enabled or already taken - mark day complete
daily_trades_complete := true
// ADDED: Handle case where first breakout was invalid (no trade placed)
if breakout_occurred and breakout_direction == "invalid" and enable_second_chance and not first_trade_sl_hit
// First breakout was invalid, enable second chance immediately
first_trade_sl_hit := true
// Determine what direction the invalid breakout was
first_trade_direction := breakout_price > or_high ? "long" : "short"
second_chance_available := true
if show_trade_status_labels
label.new(x=bar_index + 1, y=(or_high + or_low) / 2, text="First Breakout Invalid\nSecond Chance Available", color=color.yellow, textcolor=color.black, style=label.style_label_center, size=size.tiny)
// REMOVED: Complex historical box cleanup to avoid lookahead bias
// Historical boxes will be cleaned up automatically by Pine Script's runtime
// Place entry orders after breakout - FIXED: Add barstate.isconfirmed for consistency
if barstate.isconfirmed and not daily_trades_complete and is_trading_day_allowed and ((breakout_occurred and not entry_placed and not na(breakout_candle_high) and breakout_direction != "invalid") or (second_trade_taken and not second_entry_placed and not na(breakout_candle_high) and breakout_direction != "invalid"))
// For long breakout
if breakout_direction == "long"
// Calculate stop loss based on selected method
if sl_type == "Breakout Candle"
stop_loss := breakout_candle_low - (sl_points * syminfo.mintick)
else
// Use opposite side of opening range (below opening range low)
stop_loss := or_low - (sl_points * syminfo.mintick)
if entry_type == "Retracement"
// Calculate retracement entry price (x% of breakout candle body)
breakout_candle_body = breakout_candle_high - breakout_candle_low
retracement_amount = breakout_candle_body * (retracement_percent / 100)
entry_price := breakout_candle_high - retracement_amount
// FIXED: Store the entry ID we're using (differentiate first vs second chance)
current_entry_id := second_trade_taken ? "Long Retracement 2nd" : "Long Retracement"
// Place buy limit order at retracement level
strategy.entry(current_entry_id, strategy.long, limit=entry_price, qty=contracts)
// Add visual markers
if show_entry_labels
entry_label_text = second_trade_taken ? "BUY LIMIT (2nd)\n" + str.tostring(entry_price, "#.##") : "BUY LIMIT\n" + str.tostring(entry_price, "#.##")
label.new(x=bar_index, y=entry_price, text=entry_label_text, color=color.green, textcolor=color.white, style=label.style_label_up, size=size.tiny)
else
// Immediate entry at breakout candle close
entry_price := breakout_price
// FIXED: Store the entry ID we're using (differentiate first vs second chance)
current_entry_id := second_trade_taken ? "Instant Long 2nd" : "Instant Long"
// Place buy market order
strategy.entry(current_entry_id, strategy.long, qty=contracts)
// Add visual markers
if show_entry_labels
entry_label_text = second_trade_taken ? "BUY MARKET (2nd)\n" + str.tostring(entry_price, "#.##") : "BUY MARKET\n" + str.tostring(entry_price, "#.##")
label.new(x=bar_index, y=entry_price, text=entry_label_text, color=color.green, textcolor=color.white, style=label.style_label_up, size=size.tiny)
// Calculate take profit based on risk:reward
risk_size = entry_price - stop_loss
take_profit := entry_price + (risk_size * risk_multiplier)
// FIXED: Set exit orders with proper entry ID and always include initial stop loss
if use_trailing_sl
// Initialize trailing stop and calculate initial risk
trailing_stop := stop_loss
trailing_active := true
initial_risk := math.abs(entry_price - stop_loss)
trailing_started := false
// FIXED: Always set initial stop loss, even with trailing enabled
exit_id = second_trade_taken ? "Long Exit 2nd" : "Long Exit"
strategy.exit(exit_id, current_entry_id, stop=stop_loss, limit=take_profit)
else
// FIXED: Use stored entry ID
exit_id = second_trade_taken ? "Long Exit 2nd" : "Long Exit"
strategy.exit(exit_id, current_entry_id, stop=stop_loss, limit=take_profit)
// Create trade visualization boxes (TradingView style) - FIXED: Minimum 8 bars width
// Blue profit zone box (from entry to take profit)
// Store trade boxes for historical display - FIXED: Remove time usage
array.push(historical_trade_boxes, current_profit_box)
array.push(historical_sl_boxes, current_sl_box)
array.push(historical_tp_boxes, na) // No TP box for long trades
// Add stop loss and take profit markers
if show_sl_tp_labels
label.new(x=bar_index, y=stop_loss, text="SL\n" + str.tostring(stop_loss, "#.##"), color=color.red, textcolor=color.white, style=label.style_label_down, size=size.tiny)
label.new(x=bar_index, y=take_profit, text="TP\n" + str.tostring(take_profit, "#.##"), color=color.blue, textcolor=color.white, style=label.style_label_down, size=size.tiny)
// ADDED: Set the appropriate entry flag based on which trade this is
if second_trade_taken
second_entry_placed := true
daily_trades_complete := true
else
entry_placed := true
// For short breakout
else if breakout_direction == "short"
// Calculate stop loss based on selected method
if sl_type == "Breakout Candle"
stop_loss := breakout_candle_high + (sl_points * syminfo.mintick)
else
// Use opposite side of opening range (above opening range high)
stop_loss := or_high + (sl_points * syminfo.mintick)
if entry_type == "Retracement"
// Calculate retracement entry price (x% of breakout candle body)
breakout_candle_body = breakout_candle_high - breakout_candle_low
retracement_amount = breakout_candle_body * (retracement_percent / 100)
entry_price := breakout_candle_low + retracement_amount
// FIXED: Store the entry ID we're using (differentiate first vs second chance)
current_entry_id := second_trade_taken ? "Short Retracement 2nd" : "Short Retracement"
// Place sell limit order at retracement level
strategy.entry(current_entry_id, strategy.short, limit=entry_price, qty=contracts)
// Add visual markers
if show_entry_labels
entry_label_text = second_trade_taken ? "SELL LIMIT (2nd)\n" + str.tostring(entry_price, "#.##") : "SELL LIMIT\n" + str.tostring(entry_price, "#.##")
label.new(x=bar_index, y=entry_price, text=entry_label_text, color=color.red, textcolor=color.white, style=label.style_label_down, size=size.tiny)
else
// Immediate entry at breakout candle close
entry_price := breakout_price
// FIXED: Store the entry ID we're using (differentiate first vs second chance)
current_entry_id := second_trade_taken ? "Instant 2nd" : "Instant Short"
// Place sell market order
strategy.entry(current_entry_id, strategy.short, qty=contracts)
// Add visual markers
if show_entry_labels
entry_label_text = second_trade_taken ? "SELL MARKET (2nd)\n" + str.tostring(entry_price, "#.##") : "SELL MARKET\n" + str.tostring(entry_price, "#.##")
label.new(x=bar_index, y=entry_price, text=entry_label_text, color=color.red, textcolor=color.white, style=label.style_label_down, size=size.tiny)
// Calculate take profit based on risk:reward
risk_size = stop_loss - entry_price
take_profit := entry_price - (risk_size * risk_multiplier)
// FIXED: Set exit orders with proper entry ID and always include initial stop loss
if use_trailing_sl
// Initialize trailing stop and calculate initial risk
trailing_stop := stop_loss
trailing_active := true
initial_risk := math.abs(entry_price - stop_loss)
trailing_started := false
// FIXED: Always set initial stop loss, even with trailing enabled
exit_id = second_trade_taken ? "Short Exit 2nd" : "Short Exit"
strategy.exit(exit_id, current_entry_id, stop=stop_loss, limit=take_profit)
else
// FIXED: Use stored entry ID
exit_id = second_trade_taken ? "Short Exit 2nd" : "Short Exit"
strategy.exit(exit_id, current_entry_id, stop=stop_loss, limit=take_profit)
// Create trade visualization boxes (TradingView style) - FIXED: Minimum 8 bars width
// Store trade boxes for historical display - FIXED: Remove time usage
array.push(historical_trade_boxes, current_profit_box)
array.push(historical_sl_boxes, current_sl_box)
array.push(historical_tp_boxes, na) // No TP box for short trades
// Add stop loss and take profit markers
if show_sl_tp_labels
label.new(x=bar_index, y=stop_loss, text="SL\n" + str.tostring(stop_loss, "#.##"), color=color.red, textcolor=color.white, style=label.style_label_up, size=size.tiny)
label.new(x=bar_index, y=take_profit, text="TP\n" + str.tostring(take_profit, "#.##"), color=color.blue, textcolor=color.white, style=label.style_label_up, size=size.tiny)
// ADDED: Set the appropriate entry flag based on which trade this is
if second_trade_taken
second_entry_placed := true
daily_trades_complete := true
else
entry_placed := true
// === TRAILING STOP LOGIC ===
// FIXED: Proper trailing stop loss management
if use_trailing_sl and trailing_active and strategy.position_size != 0 and not na(current_entry_id)
if strategy.position_size > 0 // Long position
// Calculate current unrealized profit in points
current_profit = close - entry_price
profit_r = current_profit / initial_risk
// Check if we should start trailing (after X R profit)
if not trailing_started and profit_r >= profit_r_multiplier
trailing_started := true
// Start trailing from a level that's better than the initial stop
trailing_stop := math.max(trailing_stop, close - (atr * atr_multiplier))
// Update trailing stop if trailing has started
if trailing_started
// Calculate new trailing stop using ATR
potential_new_stop = close - (atr * atr_multiplier)
// Only move stop loss up (never down) and ensure it's better than initial SL
if potential_new_stop > trailing_stop and potential_new_stop > stop_loss
trailing_stop := potential_new_stop
// Update the exit order with new trailing stop
exit_id = second_trade_taken ? "Long Exit 2nd" : "Long Exit"
strategy.exit(exit_id, current_entry_id, stop=trailing_stop, limit=take_profit)
else if strategy.position_size < 0 // Short position
// Calculate current unrealized profit in points
current_profit = entry_price - close
profit_r = current_profit / initial_risk
// Check if we should start trailing (after X R profit)
if not trailing_started and profit_r >= profit_r_multiplier
trailing_started := true
// Start trailing from a level that's better than the initial stop
trailing_stop := math.min(trailing_stop, close + (atr * atr_multiplier))
// Update trailing stop if trailing has started
if trailing_started
// Calculate new trailing stop using ATR
potential_new_stop = close + (atr * atr_multiplier)
// Only move stop loss down (never up) and ensure it's better than initial SL
if potential_new_stop < trailing_stop and potential_new_stop < stop_loss
trailing_stop := potential_new_stop
// Update the exit order with new trailing stop
exit_id = second_trade_taken ? "Short Exit 2nd" : "Short Exit"
strategy.exit(exit_id, current_entry_id, stop=trailing_stop, limit=take_profit)
// === SESSION END CLOSE ===
// Force close all positions at configured session end time (optional)
// FIXED: Using configurable hour/minute with timezone
if force_session_close and current_hour == session_end_hour and current_minute == session_end_minute
// ADDED: Extend boxes immediately before session close to prevent timing issues
if not na(current_profit_box)
// Ensure minimum 8 bars width or extend to current bar, whichever is longer
box_left = box.get_left(current_profit_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_profit_box, final_right)
current_profit_box := na // Clear reference after extending
if not na(current_sl_box)
// Ensure minimum 8 bars width or extend to current bar, whichever is longer
box_left = box.get_left(current_sl_box)
min_right = box_left + 8
final_right = math.max(min_right, bar_index)
box.set_right(current_sl_box, final_right)
current_sl_box := na // Clear reference after extending
strategy.close_all(comment="Session End Close")
// === ALERTS ===
alert_once_long = (strategy.position_size > 0) and (strategy.position_size[1] == 0)
alert_once_short = (strategy.position_size < 0) and (strategy.position_size[1] == 0)
alertcondition(alert_once_long, title="Long Entry (Once)", message="Long Entry Signal")
alertcondition(alert_once_short, title="Short Entry (Once)", message="Short Entry Signal")