BreakoutFollowTrend_Strategy.pine
Copy
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © jzd101
//@version=6
strategy("Breakout Follow Trend [Strategy]", overlay=true, initial_capital=10000, currency=currency.USD, commission_type=strategy.commission.cash_per_contract, commission_value=0.0, pyramiding=100, margin_long=0, margin_short=0)
// ==========================================
// Inputs
// ==========================================
// Risk Management
grp_risk = "Risk Management"
inpRiskPct = input.float(1.0, "Risk % per Trade", minval=0.1, step=0.1, group=grp_risk, tooltip="Percentage of base balance to risk based on SL distance")
inpRR = input.float(2.0, "Risk:Reward Ratio", minval=0.1, step=0.1, group=grp_risk)
inpATRMult = input.float(2.0, "ATR Multiplier (SL)", minval=0.1, step=0.1, group=grp_risk)
inpMaxTrades = input.int(1, "Max Concurrent Trades", minval=1, group=grp_risk)
inpCompound = input.bool(true, "Use Compounding Risk", group=grp_risk, tooltip="If true, risk = current equity * risk%. If false, risk = fixed balance * risk%.")
inpFixedBal = input.float(10000.0, "Fixed Balance (when compounding off)", minval=0.0, group=grp_risk)
inpDailyLoss = input.float(1.0, "Daily Loss Limit %", minval=0.0, step=0.1, group=grp_risk, tooltip="0 = disabled. Blocks new entries once realized daily P&L drops below this % of base balance.")
// Partial Take Profit
grp_partial_tp = "Partial Take Profit"
inpUsePartialTP = input.bool(true, "Enable Partial TP", group=grp_partial_tp, tooltip="When price reaches Partial TP at RR, close a percentage of the order.")
inpPartialTPAtRR = input.float(1.6, "Partial TP at RR", minval=0.1, step=0.1, group=grp_partial_tp, tooltip="Trigger partial close when price reaches this RR level.")
inpPartialTPPct = input.float(50.0, "Close % at Partial TP", minval=1.0, maxval=99.0, step=1.0, group=grp_partial_tp, tooltip="Percentage of the position to close.")
// Indicators
grp_ind = "Indicators"
inpUseEMA = input.bool(true, "Use EMA Trend Filter", group=grp_ind)
inpEMAPeriod = input.int(200, "EMA Period", minval=1, group=grp_ind)
inpUseEMABodyFilter= input.bool(false, "Block Entry if Candle Overlaps EMA", group=grp_ind, tooltip="If the signal bar's High/Low range straddles the EMA (EMA is between High and Low), block the entry. Direction is ambiguous when price is crossing the EMA.")
inpBBPeriod = input.int(15, "Bollinger Bands Period", minval=1, group=grp_ind)
inpBBDev = input.float(1.5, "Bollinger Bands Deviation", minval=0.1, step=0.1, group=grp_ind)
inpATRPeriod = input.int(18, "ATR Period", minval=1, group=grp_ind)
// Volume Filter
grp_vol = "Volume Filter"
inpUseVol = input.bool(true, "Use Volume Filter", group=grp_vol)
inpVolPeriod = input.int(15, "Volume MA Period", minval=1, group=grp_vol)
// Cooldown Bars After Close/SL/TP
grp_cooldown = "Cooldown After Close/SL/TP"
inpUseCooldown = input.bool(true, "Enable Cooldown Bars", group=grp_cooldown, tooltip="Block new entries for X bars after any position is closed (SL/TP/Manual close).")
inpCooldownBars = input.int(6, "Cooldown Bars", minval=1, group=grp_cooldown, tooltip="Number of completed bars to wait after a close/SL/TP before allowing new entries. E.g. 1 = skip the next 1 bar.")
// Time & Filters
grp_time = "Time Filters"
inpUseTimeFilter = input.bool(true, "Enable Time Filter", group=grp_time, tooltip="When disabled, the EA trades all day without any hourly restriction.")
inpStartHour = input.int(12, "Start Hour (0-23)", minval=0, maxval=23, group=grp_time)
inpEndHour = input.int(18, "End Hour (0-23)", minval=0, maxval=23, group=grp_time)
inpWeekendCl = input.bool(false, "Weekend Close (Friday)", group=grp_time)
inpFridayTime = input.string("2345", "Friday Close Time (HHMM)", group=grp_time)
// ==========================================
// Calculations
// ==========================================
// Moving Average (EMA)
emaVal = ta.ema(close, inpEMAPeriod)
// Bollinger Bands
[basis, upperBB, lowerBB] = ta.bb(close, inpBBPeriod, inpBBDev)
// ATR (Wilder's / RMA) — ta.atr uses same RMA formula as MQL5 CalculateRMA_ATR
atrVal = ta.atr(inpATRPeriod)
// Volume MA
volMA = ta.sma(volume, inpVolPeriod)
// Time Filter (disabled = trade all day)
isInTimeWindow = not inpUseTimeFilter or (inpStartHour < inpEndHour ? (hour >= inpStartHour and hour < inpEndHour) : (hour >= inpStartHour or hour < inpEndHour))
// Weekend Block (matches MQL5 CheckWeekendClose: Friday past time, full Sat/Sun, Monday before StartHour)
cleanTimeVal = str.tonumber(str.replace_all(inpFridayTime, ":", ""))
fHour = na(cleanTimeVal) ? 23 : int(math.floor(cleanTimeVal / 100))
fMin = na(cleanTimeVal) ? 45 : int(math.floor(cleanTimeVal % 100))
fridayMin = fHour * 60 + fMin
nowMin = hour * 60 + minute
isFridayPast = dayofweek == dayofweek.friday and nowMin >= fridayMin
isWeekend = dayofweek == dayofweek.saturday or dayofweek == dayofweek.sunday
// isMondayBefore only applies when time filter is on (StartHour is meaningful)
isMondayBefore = inpUseTimeFilter and dayofweek == dayofweek.monday and hour < inpStartHour
isWeekendBlock = inpWeekendCl and (isFridayPast or isWeekend or isMondayBefore)
// ==========================================
// Risk Calculations
// ==========================================
// Matches MQL5: compound uses live equity, otherwise fixed balance input
baseBal = inpCompound ? strategy.equity : inpFixedBal
slDist = atrVal * inpATRMult
riskAmount = baseBal * (inpRiskPct / 100)
// Divide by syminfo.pointvalue to scale properly on Futures or CFDs
float pVal = nz(syminfo.pointvalue, 1.0)
bool isValidSize = not na(slDist) and slDist > 0 and pVal > 0
float posSize = isValidSize ? (riskAmount / slDist) / pVal : 0.0
// ==========================================
// Daily Loss Limit & Cooldown Bars tracking (realized P&L)
// ==========================================
var float dailyPnL = 0.0
var int currentDay = 0
var float dailyLossMax = 0.0
var int lastClosedBar = -1000 // safe default
var int prev_closed_trades = 0
int d = time("D")
if not na(d) and d != currentDay
currentDay := d
dailyPnL := 0.0
// Recalculate daily loss max with current equity/balance (matches MQL5 daily reset)
dailyLossMax := baseBal * (inpDailyLoss / 100)
// Sum ALL trades closed this bar (MQL5 OnTradeTransaction fires per-deal; loop covers simultaneous exits)
if strategy.closedtrades > prev_closed_trades
for i = prev_closed_trades to strategy.closedtrades - 1
dailyPnL := dailyPnL + strategy.closedtrades.profit(i) - nz(strategy.closedtrades.commission(i))
lastClosedBar := bar_index
prev_closed_trades := strategy.closedtrades
bool dailyLossHit = inpDailyLoss > 0 and currentDay != 0 and dailyPnL <= -dailyLossMax
bool cooldownActive = inpUseCooldown and (bar_index - lastClosedBar) <= inpCooldownBars
// ==========================================
// Signal Conditions — plain comparison on closed bar (matches MQL5: close1 > upperBB / close1 < lowerBB)
// ==========================================
bool volCond = (not inpUseVol) or na(volMA) or nz(volMA) == 0 or volume > nz(volMA)
// v6: explicit na-guards for all indicator values used in conditions
bool emaReady = not na(emaVal)
bool atrReady = not na(atrVal)
bool bbReady = not na(upperBB) and not na(lowerBB)
// EMA Body Overlap filter: block if EMA is inside the candle's High-Low range (ambiguous direction)
bool emaOverlap = emaReady and (low <= nz(emaVal) and high >= nz(emaVal))
bool emaOverlapBlocked = inpUseEMABodyFilter and inpUseEMA and emaOverlap
bool isAboveEMA = emaReady and close > nz(emaVal)
bool isBelowEMA = emaReady and close < nz(emaVal)
bool isAboveBB = bbReady and close > nz(upperBB)
bool isBelowBB = bbReady and close < nz(lowerBB)
bool canLong = (not inpUseEMA or isAboveEMA) and isAboveBB
bool canShort = (not inpUseEMA or isBelowEMA) and isBelowBB
bool isInTimeWindowVal = (not inpUseTimeFilter) or (inpStartHour < inpEndHour ? (hour >= inpStartHour and hour < inpEndHour) : (hour >= inpStartHour or hour < inpEndHour))
bool longCondition = atrReady and canLong and volCond and isInTimeWindowVal and not isWeekendBlock and not dailyLossHit and not cooldownActive and not emaOverlapBlocked
bool shortCondition = atrReady and canShort and volCond and isInTimeWindowVal and not isWeekendBlock and not dailyLossHit and not cooldownActive and not emaOverlapBlocked
// ==========================================
// Strategy Execution
// ==========================================
// Entry IDs are unique so each position carries its own SL/TP (matches MQL5 hedge-like per-position management)
var int entryCounter = 0
// Per-entry slDist queue: stores (entryCounter, slDist) so visual section can retrieve
// the exact ATR-based distance for the bar that generated each signal,
// even when MaxTrades > 1 and signals fire on consecutive bars.
var int[] sl_entry_ids = array.new_int()
var float[] sl_dist_queue = array.new_float()
// Push signal's slDist into queue BEFORE incrementing entryCounter
// so we store the counter value that will be assigned to this entry.
// Guard: only one direction per bar (matches MQL5 if/else-if structure)
bool didLongEntry = false
if longCondition and strategy.opentrades < inpMaxTrades and posSize > 0
array.push(sl_entry_ids, entryCounter + 1)
array.push(sl_dist_queue, slDist)
if (not didLongEntry) and shortCondition and strategy.opentrades < inpMaxTrades and posSize > 0
array.push(sl_entry_ids, entryCounter + 1)
array.push(sl_dist_queue, slDist)
// Matches MQL5: if(lotSize > 0) guard prevents zero-size entries when ATR is unavailable
if (longCondition and strategy.opentrades < inpMaxTrades and posSize > 0)
entryCounter += 1
string trade_id = "L" + str.tostring(entryCounter)
strategy.entry(trade_id, strategy.long, qty=posSize, comment="Long")
didLongEntry := true
if ((not didLongEntry) and shortCondition and strategy.opentrades < inpMaxTrades and posSize > 0)
entryCounter += 1
string trade_id = "S" + str.tostring(entryCounter)
strategy.entry(trade_id, strategy.short, qty=posSize, comment="Short")
// Weekend Close — close all positions while inside the weekend block
if (isWeekendBlock)
strategy.close_all(comment="Weekend Close")
// ==========================================
// Visuals & Position Tools
// ==========================================
plot(inpUseEMA ? emaVal : na, color=color.new(color.blue, 0), title="EMA Trend")
p1 = plot(upperBB, color=color.new(color.gray, 50), title="Upper BB")
p2 = plot(lowerBB, color=color.new(color.gray, 50), title="Lower BB")
fill(p1, p2, color=color.new(color.gray, 90), title="BB Background")
// ── Trade Box Color Palette (TradingView Style) ──────────────────────────────
color_tp_fill = color.new(#089981, 85)
color_sl_fill = color.new(#f23645, 85)
color_tp_border = color.new(#089981, 0)
color_sl_border = color.new(#f23645, 0)
color_tp_label = color.new(#089981, 0)
color_sl_label = color.new(#f23645, 0)
color_entry_line = color.new(#b2b5be, 0)
color_entry_lbl = color.new(#b2b5be, 0)
// ── Partial TP Label Color ───────────────────────────────────────────────────
color_ptp_label = color.new(#089981, 0)
// ── Persistent Arrays for Active Visual Objects ───────────────────────────────
var float[] active_sl_prices = array.new_float()
var float[] active_tp_prices = array.new_float()
var string[] active_trade_ids = array.new_string()
var bool[] active_trade_dirs = array.new_bool() // true = long
var box[] active_tp_boxes = array.new_box()
var box[] active_sl_boxes = array.new_box()
var line[] active_entry_lines = array.new_line()
var label[] active_tp_labels = array.new_label()
var label[] active_sl_labels = array.new_label()
var label[] active_en_labels = array.new_label()
var float[] active_entry_prices = array.new_float() // Entry price per trade (for SL move calc)
var float[] active_sl_dists = array.new_float() // Original SL distance per trade
var bool[] active_partial_tp_done = array.new_bool() // Whether Partial TP has executed
// ── Helper: format price to 5 sig-figs ───────────────────────────────────────
f_fmt(float p) => str.tostring(p, format.mintick)
// ── 1. Extend all active boxes & lines to the current bar ─────────────────────
if array.size(active_tp_boxes) > 0
for i = 0 to array.size(active_tp_boxes) - 1
box.set_right(array.get(active_tp_boxes, i), bar_index)
box.set_right(array.get(active_sl_boxes, i), bar_index)
line.set_x2(array.get(active_entry_lines, i), bar_index)
// Slide labels to the right edge
label.set_x(array.get(active_tp_labels, i), bar_index)
label.set_x(array.get(active_sl_labels, i), bar_index)
label.set_x(array.get(active_en_labels, i), bar_index)
// ── Helper: look up slDist from queue by entry-ID number ─────────────────────
f_pop_sl_dist(string entry_id) =>
float result = na
int num = int(str.tonumber(str.substring(entry_id, 1, str.length(entry_id))))
if not na(num) and array.size(sl_entry_ids) > 0
for k = 0 to array.size(sl_entry_ids) - 1
if array.get(sl_entry_ids, k) == num
result := array.get(sl_dist_queue, k)
array.remove(sl_entry_ids, k)
array.remove(sl_dist_queue, k)
break
result
// ── 2. New entry detection ────────────────────────────────────────────────────
int total_trades = strategy.closedtrades + strategy.opentrades
var int prev_total_trades = 0
if total_trades > prev_total_trades
int num_opened = total_trades - prev_total_trades
int trades_to_process = math.min(num_opened, strategy.opentrades)
if trades_to_process > 0
for i = 0 to trades_to_process - 1
int trade_idx = strategy.opentrades - trades_to_process + i
string eid = strategy.opentrades.entry_id(trade_idx)
float sd = f_pop_sl_dist(eid)
if na(sd)
sd := slDist
bool is_long = strategy.opentrades.size(trade_idx) > 0
float en = strategy.opentrades.entry_price(trade_idx)
// Round to mintick (MQL5 parity)
float sd_rounded = math.round(sd / syminfo.mintick) * syminfo.mintick
float tp_rounded = math.round((sd * inpRR) / syminfo.mintick) * syminfo.mintick
float tp = is_long ? en + tp_rounded : en - tp_rounded
float sl = is_long ? en - sd_rounded : en + sd_rounded
// ── Box y-coordinates: top > bottom always ────────────────────────
float tp_top = is_long ? tp : en
float tp_bottom = is_long ? en : tp
float sl_top = is_long ? en : sl
float sl_bottom = is_long ? sl : en
// ── Draw boxes ────────────────────────────────────────────────────
box b_tp = box.new(
bar_index - 1, tp_top, bar_index, tp_bottom,
bgcolor=color_tp_fill, border_color=color_tp_border,
border_width=1, border_style=line.style_solid,
text="", text_size=size.tiny)
box b_sl = box.new(
bar_index - 1, sl_top, bar_index, sl_bottom,
bgcolor=color_sl_fill, border_color=color_sl_border,
border_width=1, border_style=line.style_solid,
text="", text_size=size.tiny)
// ── Entry dashed line ─────────────────────────────────────────────
line l_en = line.new(
bar_index - 1, en, bar_index, en,
color=color_entry_line, width=1, style=line.style_dashed)
// ── TP label (positioned at TP price level, right edge) ───────────
string tp_txt = "TP " + f_fmt(tp)
label lbl_tp = label.new(
bar_index, tp,
text=tp_txt,
style=is_long ? label.style_label_left : label.style_label_left,
color=color.new(#089981, 100),
textcolor=color_tp_label,
size=size.small)
// ── SL label ─────────────────────────────────────────────────────
string sl_txt = "SL " + f_fmt(sl)
label lbl_sl = label.new(
bar_index, sl,
text=sl_txt,
style=label.style_label_left,
color=color.new(#f23645, 100),
textcolor=color_sl_label,
size=size.small)
// ── Entry label ───────────────────────────────────────────────────
string dir_txt = is_long ? "▲ LONG" : "▼ SHORT"
string en_txt = dir_txt + " " + f_fmt(en)
label lbl_en = label.new(
bar_index, en,
text=en_txt,
style=label.style_label_left,
color=color.new(#b2b5be, 100),
textcolor=color_entry_lbl,
size=size.small)
array.push(active_tp_boxes, b_tp)
array.push(active_sl_boxes, b_sl)
array.push(active_entry_lines, l_en)
array.push(active_tp_labels, lbl_tp)
array.push(active_sl_labels, lbl_sl)
array.push(active_en_labels, lbl_en)
array.push(active_sl_prices, sl)
array.push(active_tp_prices, tp)
array.push(active_trade_ids, eid)
array.push(active_trade_dirs, is_long)
array.push(active_entry_prices, en)
array.push(active_sl_dists, sd_rounded)
array.push(active_partial_tp_done, false)
prev_total_trades := total_trades
// ── 3. Prune closed trades ────────────────────────────────────────────────────
if array.size(active_trade_ids) > 0
box[] temp_tp_boxes = array.new_box()
box[] temp_sl_boxes = array.new_box()
line[] temp_entry_lines = array.new_line()
label[] temp_tp_labels = array.new_label()
label[] temp_sl_labels = array.new_label()
label[] temp_en_labels = array.new_label()
float[] temp_sl_prices = array.new_float()
float[] temp_tp_prices = array.new_float()
string[] temp_trade_ids = array.new_string()
bool[] temp_trade_dirs = array.new_bool()
float[] temp_entry_prices = array.new_float()
float[] temp_sl_dists = array.new_float()
bool[] temp_partial_tp_done = array.new_bool()
for i = 0 to array.size(active_trade_ids) - 1
string tid = array.get(active_trade_ids, i)
bool is_still_open = false
if strategy.opentrades > 0
for j = 0 to strategy.opentrades - 1
if strategy.opentrades.entry_id(j) == tid
is_still_open := true
break
if is_still_open
// Keep active trades to slide them on next bars
array.push(temp_tp_boxes, array.get(active_tp_boxes, i))
array.push(temp_sl_boxes, array.get(active_sl_boxes, i))
array.push(temp_entry_lines, array.get(active_entry_lines, i))
array.push(temp_tp_labels, array.get(active_tp_labels, i))
array.push(temp_sl_labels, array.get(active_sl_labels, i))
array.push(temp_en_labels, array.get(active_en_labels, i))
array.push(temp_sl_prices, array.get(active_sl_prices, i))
array.push(temp_tp_prices, array.get(active_tp_prices, i))
array.push(temp_trade_ids, tid)
array.push(temp_trade_dirs, array.get(active_trade_dirs, i))
array.push(temp_entry_prices, array.get(active_entry_prices, i))
array.push(temp_sl_dists, array.get(active_sl_dists, i))
array.push(temp_partial_tp_done, array.get(active_partial_tp_done, i))
active_tp_boxes := temp_tp_boxes
active_sl_boxes := temp_sl_boxes
active_entry_lines := temp_entry_lines
active_tp_labels := temp_tp_labels
active_sl_labels := temp_sl_labels
active_en_labels := temp_en_labels
active_sl_prices := temp_sl_prices
active_tp_prices := temp_tp_prices
active_trade_ids := temp_trade_ids
active_trade_dirs := temp_trade_dirs
active_entry_prices := temp_entry_prices
active_sl_dists := temp_sl_dists
active_partial_tp_done := temp_partial_tp_done
// ── 4. Emergency Reset (Omitted to preserve historical boxes) ─────────────────
// ── 5. Partial TP executed natively via strategy.exit ───────────────────────
// ── 6. Re-submit exit orders every bar using stored prices ────────────────────
if array.size(active_trade_ids) > 0
for i = 0 to array.size(active_trade_ids) - 1
string tid = array.get(active_trade_ids, i)
float sl_val = array.get(active_sl_prices, i)
float tp_val = array.get(active_tp_prices, i)
if inpUsePartialTP
float sd_t = array.get(active_sl_dists, i)
float en_t = array.get(active_entry_prices, i)
bool is_long_t = array.get(active_trade_dirs, i)
float partial_tp_raw = is_long_t ? en_t + sd_t * inpPartialTPAtRR : en_t - sd_t * inpPartialTPAtRR
float partial_tp_val = math.round(partial_tp_raw / syminfo.mintick) * syminfo.mintick
bool ptp_done = array.get(active_partial_tp_done, i)
if not ptp_done
strategy.exit("XP" + tid, tid, qty_percent=inpPartialTPPct, stop=sl_val, limit=partial_tp_val, comment_loss="SL", comment_profit="P-TP")
bool hit = is_long_t ? high >= partial_tp_val : low <= partial_tp_val
if hit
array.set(active_partial_tp_done, i, true)
strategy.exit("X" + tid, tid, stop=sl_val, limit=tp_val, comment_loss="SL", comment_profit="TP")
else
strategy.exit("X" + tid, tid, stop=sl_val, limit=tp_val, comment_loss="SL", comment_profit="TP")
Show all 428 lines