Code: Select all
//@version=5
strategy("FSM Trade Management Engine", overlay=true, initial_capital=10000, calc_on_every_tick=true, commission_type=strategy.commission.cash_per_contract, commission_value=3, slippage=1)
// ==============================================================================
// 1. CONFIGURATION & SESSION MACROS
// ==============================================================================
grp_risk = "Risk & Sizing"
riskPerTrade = input.float(1.0, "Risk Per Trade (%)", step=0.1, group=grp_risk)
beBuffer = input.int(2, "BE Cost Buffer (Ticks)", group=grp_risk, tooltip="Offset added to BE to cover commissions/spread")
grp_mgmt = "FSM Management Rules"
partialR = input.float(1.0, "First Partial (R)", step=0.1, group=grp_mgmt)
partialPct = input.int(50, "Partial Size (%)", step=10, group=grp_mgmt)
timeStopBars = input.int(12, "Time Stop (Bars)", group=grp_mgmt)
trailLookback= input.int(5, "Trailing Structure (Bars)", group=grp_mgmt)
grp_time = "Session Killzones"
sessionTime = input.session("0800-1130;1330-1600", "Active Trading Windows (NY Time)", group=grp_time)
tz = input.string("America/New_York", "Timezone", group=grp_time)
// ==============================================================================
// 2. FINITE STATE MACHINE (FSM) ARCHITECTURE
// ==============================================================================
// State Enums
var int STATE_FLAT = 0 // No active trade
var int STATE_RISK = 1 // Trade active, original stop loss in place
var int STATE_RUNNER = 2 // Partial secured, stop at BE + Buffer
type TradeContext
int state
int dir // 1 = Long, -1 = Short
float entryPrice
float stopLoss
float targetPrice // Partial target
int entryBar
string exitReason // Tagging system for journaling
var TradeContext ctx = TradeContext.new(STATE_FLAT, 0, na, na, na, na, "")
// Exit Reason Logging Arrays (For the Dashboard)
var int countFullStop = 0
var int countTimeStop = 0
var int countBE = 0
var int countTrailed = 0
method reset(TradeContext this) =>
this.state := STATE_FLAT
this.dir := 0
this.entryPrice := na
this.stopLoss := na
this.targetPrice := na
this.entryBar := na
this.exitReason := ""
// ==============================================================================
// 3. SESSION & STRUCTURE ENGINE
// ==============================================================================
inSession = not na(time(timeframe.period, sessionTime, tz))
pivotHigh = ta.pivothigh(high, 5, 5)
pivotLow = ta.pivotlow(low, 5, 5)
var float swingHigh = na
var float swingLow = na
if not na(pivotHigh)
swingHigh := pivotHigh
if not na(pivotLow)
swingLow := pivotLow
// High-Probability Sweep (Only valid inside session)
validLongSweep = inSession and low < swingLow and close > swingLow and close > open
validShortSweep = inSession and high > swingHigh and close < swingHigh and close < open
// ==============================================================================
// 4. EXECUTION CONTROLLER
// ==============================================================================
calc_size(stopDist) =>
riskAmt = strategy.equity * (riskPerTrade / 100)
tickVal = syminfo.mintick * syminfo.pointvalue
math.max(0.01, riskAmt / (stopDist / syminfo.mintick * tickVal))
// Entry Router
if (ctx.state == STATE_FLAT and strategy.position_size == 0)
if (validLongSweep)
sl = low - (syminfo.mintick * 2)
dist = close - sl
if dist > 0
ctx.state := STATE_RISK
ctx.dir := 1
ctx.entryPrice := close
ctx.stopLoss := sl
ctx.targetPrice := close + (dist * partialR)
ctx.entryBar := bar_index
strategy.entry("Long", strategy.long, qty=calc_size(dist))
else if (validShortSweep)
sl = high + (syminfo.mintick * 2)
dist = sl - close
if dist > 0
ctx.state := STATE_RISK
ctx.dir := -1
ctx.entryPrice := close
ctx.stopLoss := sl
ctx.targetPrice := close - (dist * partialR)
ctx.entryBar := bar_index
strategy.entry("Short", strategy.short, qty=calc_size(dist))
// ==============================================================================
// 5. THE "NO NEGOTIATION" MANAGEMENT FSM
// ==============================================================================
if (strategy.position_size != 0)
barsHeld = bar_index - ctx.entryBar
buffer = syminfo.mintick * beBuffer
switch ctx.state
// ---------------------------------------------------------
// PHASE 1: INITIAL RISK EXPOSURE
// ---------------------------------------------------------
STATE_RISK =>
// Failsafe 1: Time Stop
if (barsHeld >= timeStopBars)
strategy.close_all(comment="Time Stop")
ctx.exitReason := "Time"
if (ctx.dir == 1)
strategy.exit("Partial", "Long", qty_percent=partialPct, limit=ctx.targetPrice, stop=ctx.stopLoss)
// State Transition Check: Did we hit the partial?
if (high >= ctx.targetPrice)
ctx.state := STATE_RUNNER
ctx.stopLoss := ctx.entryPrice + buffer // BE + Cost Buffer
else
strategy.exit("Partial", "Short", qty_percent=partialPct, limit=ctx.targetPrice, stop=ctx.stopLoss)
if (low <= ctx.targetPrice)
ctx.state := STATE_RUNNER
ctx.stopLoss := ctx.entryPrice - buffer
// ---------------------------------------------------------
// PHASE 2: RISK-FREE RUNNER (STRUCTURAL TRAIL)
// ---------------------------------------------------------
STATE_RUNNER =>
if (ctx.dir == 1)
// Lock into higher lows, never retreating
newTrail = ta.lowest(low, trailLookback) - (syminfo.mintick * 2)
if (newTrail > ctx.stopLoss)
ctx.stopLoss := newTrail
strategy.exit("Runner", "Long", stop=ctx.stopLoss)
else
newTrail = ta.highest(high, trailLookback) + (syminfo.mintick * 2)
if (newTrail < ctx.stopLoss)
ctx.stopLoss := newTrail
strategy.exit("Runner", "Short", stop=ctx.stopLoss)
// ==============================================================================
// 6. EVENT LISTENER: TRADE CLOSURE & LOGGING
// ==============================================================================
// Detect exact moment we go flat to log the result
wentFlat = strategy.position_size == 0 and strategy.position_size[1] != 0
if (wentFlat)
// Determine exit reason based on state at time of closure
if (ctx.exitReason == "Time")
countTimeStop += 1
else if (ctx.state == STATE_RISK)
countFullStop += 1
else if (ctx.state == STATE_RUNNER)
// Check if stopped out at the BE buffer or deep in profit
isBE = (ctx.dir == 1 and close <= ctx.entryPrice + (syminfo.mintick * beBuffer * 2)) or
(ctx.dir == -1 and close >= ctx.entryPrice - (syminfo.mintick * beBuffer * 2))
if isBE
countBE += 1
else
countTrailed += 1
ctx.reset()
// ==============================================================================
// 7. VISUAL & ANALYTICS ENGINE
// ==============================================================================
// Draw session background
bgcolor(inSession ? color.new(color.blue, 96) : na, title="Killzone")
var line slLine = na
var line tpLine = na
if (strategy.position_size != 0)
if (na(slLine))
colorSl = ctx.state == STATE_RISK ? color.new(color.red, 20) : color.new(color.purple, 20)
slLine := line.new(ctx.entryBar, ctx.stopLoss, bar_index, ctx.stopLoss, color=colorSl, width=2, style=line.style_dashed)
if (ctx.state == STATE_RISK)
tpLine := line.new(ctx.entryBar, ctx.targetPrice, bar_index, ctx.targetPrice, color=color.new(color.blue, 20), width=2)
else
line.set_x2(slLine, bar_index)
line.set_y1(slLine, ctx.stopLoss)
line.set_y2(slLine, ctx.stopLoss)
if (ctx.state == STATE_RISK)
line.set_x2(tpLine, bar_index)
else
line.delete(tpLine)
line.set_color(slLine, color.new(color.purple, 20)) // Turn SL purple when at BE/Trailing
else
line.delete(slLine)
line.delete(tpLine)
slLine := na
tpLine := na
// Analytics Dashboard (The Expectancy Leak Tracker)
var table dash = table.new(position.bottom_right, 2, 5, border_width=1, border_color=color.gray)
if (barstate.islast)
total = math.max(1, countFullStop + countTimeStop + countBE + countTrailed)
table.cell(dash, 0, 0, "Exit Analytics", text_color=color.white, bgcolor=color.new(color.blue, 50))
table.cell(dash, 1, 0, str.tostring(total) + " Trades", text_color=color.white, bgcolor=color.new(color.blue, 50))
table.cell(dash, 0, 1, "Full Stops (Risk)", text_color=color.red)
table.cell(dash, 1, 1, str.tostring(math.round(countFullStop/total*100, 1)) + "%")
table.cell(dash, 0, 2, "Time Scratches", text_color=color.orange)
table.cell(dash, 1, 2, str.tostring(math.round(countTimeStop/total*100, 1)) + "%")
table.cell(dash, 0, 3, "Runner BE Exits", text_color=color.gray)
table.cell(dash, 1, 3, str.tostring(math.round(countBE/total*100, 1)) + "%")
table.cell(dash, 0, 4, "Runner Trailed", text_color=color.green)
table.cell(dash, 1, 4, str.tostring(math.round(countTrailed/total*100, 1)) + "%")