Code: Select all
//@version=5
strategy("Pro MFE Dual-Exit Engine", overlay=true, calc_on_every_tick=true, max_labels_count=50, commission_type=strategy.commission.cash_per_order, commission_value=3.0, slippage=1)
// =========================================================================
// 1. INPUTS & CONFIGURATION
// =========================================================================
grpStats = "Analytics Configuration"
i_rollingWindow = input.int(20, "Rolling Window (Last N Trades)", group=grpStats)
i_showLabels = input.bool(true, "Show Trade Labels", group=grpStats)
// =========================================================================
// 2. USER-DEFINED TYPES (UDT) & STATE MANAGEMENT
// =========================================================================
type TradeMetrics
float realizedProfit
float maxRunupDollars
float maxRunupPoints // Normalized for execution feedback
float captureRatio
bool isWinner
var array<TradeMetrics> recentTrades = array.new<TradeMetrics>()
var float avgWinMfePoints = 0.0 // Global variable fed back to execution
method pushTrade(array<TradeMetrics> arr, TradeMetrics newTrade, int maxSize) =>
array.push(arr, newTrade)
if array.size(arr) > maxSize
array.shift(arr)
var float maxAccountEquity = strategy.initial_capital
maxAccountEquity := math.max(maxAccountEquity, strategy.equity)
// =========================================================================
// 3. EXECUTION BLOCK (Adaptive Scale-Out & Trailing Stop)
// =========================================================================
// Structural 15-minute price action logic
lookbackHigh = ta.highest(high, 15)
lookbackLow = ta.lowest(low, 15)
atr14 = ta.atr(14)
longTrigger = ta.crossover(close, lookbackHigh[1])
shortTrigger = ta.crossunder(close, lookbackLow[1])
// Dynamic target logic: Use average MFE points if available, fallback to 2x ATR
float dynamicTargetPoints = avgWinMfePoints > 0 ? avgWinMfePoints : (atr14 * 2)
// Convert points to integer ticks for Pine's internal trailing stop engine
int trailActTicks = math.round(dynamicTargetPoints / syminfo.mintick) // Trail activates when TP1 is hit
int trailOffTicks = math.round(atr14 / syminfo.mintick) // Trail distance (1 ATR behind price)
if longTrigger and strategy.position_size == 0
strategy.entry("Long", strategy.long) // Uses default qty; scale-out handles the split
float sl = low - atr14
float tp1 = close + dynamicTargetPoints
// Exit 1: Close 50% volume exactly at the dynamic MFE limit
strategy.exit("TP1", "Long", qty_percent=50, limit=tp1, stop=sl)
// Exit 2: Trailing stop on the remainder (activates at TP1 distance, trails by 1 ATR)
strategy.exit("Trail", "Long", stop=sl, trail_points=trailActTicks, trail_offset=trailOffTicks)
if shortTrigger and strategy.position_size == 0
strategy.entry("Short", strategy.short)
float sl = high + atr14
float tp1 = close - dynamicTargetPoints
strategy.exit("TP1", "Short", qty_percent=50, limit=tp1, stop=sl)
strategy.exit("Trail", "Short", stop=sl, trail_points=trailActTicks, trail_offset=trailOffTicks)
// =========================================================================
// 4. MFE EXTRACTION & FEEDBACK LOOP
// =========================================================================
if strategy.closedtrades > nz(strategy.closedtrades[1])
lastIndex = strategy.closedtrades - 1
t_profit = strategy.closedtrades.profit(lastIndex)
t_runup = strategy.closedtrades.max_runup(lastIndex)
t_qty = strategy.closedtrades.size(lastIndex)
t_winner = t_profit > 0
// Normalize MFE to raw price points (ignores position size scaling)
float t_runupPoints = (t_qty > 0 and syminfo.pointvalue > 0) ? (t_runup / t_qty) / syminfo.pointvalue : 0.0
float t_efficiency = (t_winner and t_runup > 0) ? (t_profit / t_runup) * 100 : 0.0
newMetric = TradeMetrics.new(t_profit, t_runup, t_runupPoints, t_efficiency, t_winner)
recentTrades.pushTrade(newMetric, i_rollingWindow)
// Immediately update the global target for the next trade
float sumMfePts = 0.0
int winCount = 0
if array.size(recentTrades) > 0
for trade in recentTrades
if trade.isWinner
sumMfePts += trade.maxRunupPoints
winCount += 1
avgWinMfePoints := winCount > 0 ? (sumMfePts / winCount) : 0.0
if i_showLabels
if t_winner
label.new(bar_index, high, str.tostring(t_efficiency, "#.#") + "% CR", color=color.new(color.teal, 20), style=label.style_label_down, textcolor=color.white, size=size.small)
else
label.new(bar_index, low, "$" + str.tostring(t_runup, "#.#") + " MFE", color=color.new(color.maroon, 20), style=label.style_label_up, textcolor=color.white, size=size.small)
// =========================================================================
// 5. QUANTITATIVE DASHBOARD
// =========================================================================
var table dash = table.new(position.bottom_right, 2, 6, border_width=1, border_color=color.new(color.gray, 80), frame_color=color.new(color.gray, 80), frame_width=1)
if barstate.islast
float sumWinEfficiency = 0.0
int wCount = 0
float rollingPnl = 0.0
float rollingPeak = 0.0
float rollingMaxDD = 0.0
if array.size(recentTrades) > 0
for trade in recentTrades
if trade.isWinner
sumWinEfficiency += trade.captureRatio
wCount += 1
rollingPnl += trade.realizedProfit
rollingPeak := math.max(rollingPeak, rollingPnl)
rollingMaxDD := math.max(rollingMaxDD, rollingPeak - rollingPnl)
avgEfficiency = wCount > 0 ? (sumWinEfficiency / wCount) : 0.0
winRate = array.size(recentTrades) > 0 ? (wCount / array.size(recentTrades)) * 100 : 0.0
currentOpenDD_pct = maxAccountEquity > 0 ? ((maxAccountEquity - strategy.equity) / maxAccountEquity) * 100 : 0.0
cellBg = color.new(#1e222d, 10)
table.cell(dash, 0, 0, "Adaptive Engine (" + str.tostring(array.size(recentTrades)) + ")", text_color=color.white, bgcolor=color.new(color.gray, 80), text_size=size.small)
table.merge_cells(dash, 0, 0, 1, 0)
table.cell(dash, 0, 1, "Win Rate:", text_color=color.gray, bgcolor=cellBg, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, 1, str.tostring(winRate, "#.#") + "%", text_color=color.white, bgcolor=cellBg, text_size=size.small, text_halign=text.align_right)
table.cell(dash, 0, 2, "Capture Ratio:", text_color=color.gray, bgcolor=cellBg, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, 2, str.tostring(avgEfficiency, "#.#") + "%", text_color=color.teal, bgcolor=cellBg, text_size=size.small, text_halign=text.align_right)
table.cell(dash, 0, 3, "Current Target (Pts):", text_color=color.gray, bgcolor=cellBg, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, 3, str.tostring(dynamicTargetPoints, "#.#####"), text_color=color.yellow, bgcolor=cellBg, text_size=size.small, text_halign=text.align_right)
table.cell(dash, 0, 4, "Rolling Max DD:", text_color=color.gray, bgcolor=cellBg, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, 4, "$" + str.tostring(rollingMaxDD, "#.##"), text_color=color.red, bgcolor=cellBg, text_size=size.small, text_halign=text.align_right)
table.cell(dash, 0, 5, "Account Heat (DD):", text_color=color.gray, bgcolor=cellBg, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, 5, str.tostring(currentOpenDD_pct, "#.##") + "%", text_color=color.red, bgcolor=cellBg, text_size=size.small, text_halign=text.align_right)