//@version=5 // SA_EXPORT|format=sa_tv_exact_v1|semantics=tv_exact_v1|fill=signal_close|intrabar_path=tv_ohlc_path|reentry=cancel_first|export_id=sa-b254eda4366da358 // Strategy Arena Lab export, executionSemantics = tv_exact_v1 (TradingView-exact semantics of the Lab CPU reference). // Fills at the signal bar close (process_orders_on_close = true); quantity = cash x size / (close x (1 + commission)), rounded down to saQtyStep when set (TradingView lot step). // stop_loss / take_profit: one strategy.exit(stop, limit) placed at entry, filled intrabar on high/low, at the open on a gap; levels on the tick grid away from the entry (TradingView rule). // A close decided on a bar close cancels its bracket first, so an entry on the same close fills exactly like the Lab (TradingView probe, 8 Oct 2026). Pine Logs report any gap between the Lab mirror and the TradingView position as SATRADES1|DESYNC. // Stop and target inside the same bar: TradingView open/high/low path, as in the Lab run with intrabarPath = tv_ohlc_path. // Trailing stop, timeout, exit conditions and implicit signal exits: decided on each close in the Lab order, filled at that close. // Indicators: TradingView formulas anchored at the first bar of the Lab window (saFromTime), so chart history before it does not change them. // Warmup: no signal before window bar saFirstTradable (every indicator defined). Commission in percent of the traded value per side; slippage in ticks on market and stop fills (TradingView model). // Optional trade log: each closed trade is written to Pine Logs (SATRADES1 lines); copy the Pine Logs panel into the Lab to compare trade by trade. // Descriptive sandbox configuration, not a sealed proof. Parameters are fixed at the exported values. strategy("V5 EMA 50/200 Long Short", overlay=true, initial_capital=10000.0, default_qty_type=strategy.fixed, default_qty_value=1, commission_type=strategy.commission.percent, commission_value=0.1, slippage=0, process_orders_on_close=true, calc_on_order_fills=false, calc_on_every_tick=false, pyramiding=0, margin_long=0, margin_short=0) // ---- Lab configuration (fixed at the exported values) ---- int saFromTime = 0 int saToTime = 0 string saExportId = "sa-b254eda4366da358" string saScriptName = "V5 EMA 50/200 Long Short" string saIntrabarPath = "tv_ohlc_path" string saTieBreak = "low_first" string saReentryOrder = "cancel_first" float saQtyStep = 0.0 int saQtyDecimals = 0 float saCapital = 10000.0 float saPositionSize = 1.0 float saFee = 0.001 int saSlippageTicks = 0 float saTakeProfit = na float saStopLoss = na int saTimeout = na float saTrailing = na int saCooldown = 0 float saStopAtrMult = na float saTakeAtrMult = na float saShortInitialMargin = na float saShortMaintenance = 0.0 int saFirstTradable = 199 bool saImplicitSignalExits = false bool saLetRun = true bool saHasShortEntry = true bool saLogTrades = input.bool(true, "Write each closed trade to Pine Logs (Strategy Arena trade check)") float saSlip = saSlippageTicks * syminfo.mintick bool saInWindow = time >= saFromTime and (saToTime == 0 or time <= saToTime) var int saN = -1 if saInWindow saN += 1 // ---- Indicators anchored at the first Lab bar (saN = window bar index) ---- saGateA(float v, int first) => saN >= first ? v : na saSmaA(float src, simple int n) => float v = ta.sma(src, n) saN >= n - 1 ? v : na // ta.ema / ta.rma recursion: SMA seed at window bar `first`, then alpha * src + (1 - alpha) * previous. saRecA(float src, simple int n, float alpha, int first) => float seed = ta.sma(src, n) var float r = na if saN >= first r := na(r) ? seed : alpha * src + (1.0 - alpha) * r r saRsiA(simple int n) => float u = saN >= 1 ? math.max(close - close[1], 0.0) : na float d = saN >= 1 ? math.max(close[1] - close, 0.0) : na float up = saRecA(u, n, 1.0 / n, n) float dn = saRecA(d, n, 1.0 / n, n) na(up) or na(dn) ? na : dn == 0 ? 100.0 : up == 0 ? 0.0 : 100.0 - 100.0 / (1.0 + up / dn) saAtrA(simple int n) => float tr = saN < 0 ? na : saN == 0 ? high - low : math.max(math.max(high - low, math.abs(high - close[1])), math.abs(low - close[1])) saRecA(tr, n, 1.0 / n, n - 1) // Comparisons with na are false, as in the Lab (no signal during the warmup). saGt(float a, float b) => not na(a) and not na(b) and a > b saLt(float a, float b) => not na(a) and not na(b) and a < b saXUp(float a, float b) => not na(a) and not na(b) and not na(a[1]) and not na(b[1]) and a > b and a[1] <= b[1] saXDn(float a, float b) => not na(a) and not na(b) and not na(a[1]) and not na(b[1]) and a < b and a[1] >= b[1] // Lab qtyStep: quantity rounded down to the lot step (identity when saQtyStep = 0). saRoundQty(float q) => saQtyStep > 0 ? math.round(math.floor(q / saQtyStep + 0.000000001) * saQtyStep, saQtyDecimals) : q saNum(float v) => str.tostring(v, "#.##########") // Lab 236: stop and limit levels on the tick grid, AWAY from the entry (the TradingView rule): // long stop / short target down, long target / short stop up. saTickDown(float v) => na(v) ? na : math.round_to_mintick(math.floor(v / syminfo.mintick + 0.0000001) * syminfo.mintick) saTickUp(float v) => na(v) ? na : math.round_to_mintick(math.ceil(v / syminfo.mintick - 0.0000001) * syminfo.mintick) // ---- Series (computed on every bar) ---- float saI0 = saRecA(close, 50, 2.0 / (50 + 1), 49) // EMA(50) float saI1 = saRecA(close, 200, 2.0 / (200 + 1), 199) // EMA(200) float saAtrStop = na float saAtrTake = na bool saA0 = saGt(saI0, saI1) bool saA1 = saLt(saI0, saI1) bool saLongEntry = saA0 bool saShortEntry = saA1 bool saLongExit = saA1 bool saShortExit = saA0 bool saLongInverseExit = false // ---- Lab CPU reference state (run_reference_with_policy, tv_exact_v1, signal_close) ---- var float saCash = saCapital var float saQty = 0.0 var float saEntryRef = na var float saEntryFill = na var int saEntryBar = na var float saPeak = 0.0 var float saTrough = 0.0 var int saCooldownLeft = 0 var string saEntryId = "" var bool saWindowClosed = false var int saLogged = 0 var bool saSummaryDone = false var int saDesyncLogged = 0 // ---- Trade log (SATRADES1, read by the Strategy Arena Lab trade comparator) ---- if saLogTrades if barstate.isfirst log.info("SATRADES1|META|export_id=" + saExportId + "|format=1|semantics=tv_exact_v1|fill=signal_close|intrabar_path=" + saIntrabarPath + "|tickerid=" + syminfo.tickerid + "|timeframe=" + timeframe.period + "|chart_first_time=" + str.tostring(time) + "|from_time=" + str.tostring(saFromTime) + "|to_time=" + str.tostring(saToTime) + "|capital=" + saNum(saCapital) + "|fee_bps=" + saNum(saFee * 10000.0) + "|slippage_ticks=" + str.tostring(saSlippageTicks) + "|mintick=" + saNum(syminfo.mintick) + "|intrabar_tie_break=" + saTieBreak + "|qty_step=" + saNum(saQtyStep) + "|reentry=" + saReentryOrder + "|first_tradable=" + str.tostring(saFirstTradable) + "|script=" + saScriptName) while saLogged < strategy.closedtrades int k = saLogged float sz = strategy.closedtrades.size(k) log.info("SATRADES1|TRADE|export_id=" + saExportId + "|n=" + str.tostring(k + 1) + "|side=" + (sz > 0 ? "long" : "short") + "|entry_time=" + str.tostring(strategy.closedtrades.entry_time(k)) + "|entry_price=" + saNum(strategy.closedtrades.entry_price(k)) + "|exit_time=" + str.tostring(strategy.closedtrades.exit_time(k)) + "|exit_price=" + saNum(strategy.closedtrades.exit_price(k)) + "|qty=" + saNum(math.abs(sz)) + "|profit=" + saNum(strategy.closedtrades.profit(k)) + "|commission=" + saNum(strategy.closedtrades.commission(k)) + "|entry_id=" + strategy.closedtrades.entry_id(k) + "|exit_id=" + strategy.closedtrades.exit_id(k)) saLogged += 1 if not saSummaryDone and ((saToTime != 0 and time > saToTime) or barstate.islastconfirmedhistory) if strategy.opentrades > 0 for j = 0 to strategy.opentrades - 1 float osz = strategy.opentrades.size(j) log.info("SATRADES1|OPEN|export_id=" + saExportId + "|n=" + str.tostring(strategy.closedtrades + j + 1) + "|side=" + (osz > 0 ? "long" : "short") + "|entry_time=" + str.tostring(strategy.opentrades.entry_time(j)) + "|entry_price=" + saNum(strategy.opentrades.entry_price(j)) + "|qty=" + saNum(math.abs(osz)) + "|entry_id=" + strategy.opentrades.entry_id(j)) log.info("SATRADES1|SUMMARY|export_id=" + saExportId + "|closed=" + str.tostring(strategy.closedtrades) + "|open=" + str.tostring(strategy.opentrades) + "|net_profit=" + saNum(strategy.netprofit) + "|last_time=" + str.tostring(time)) saSummaryDone := true // After the Lab window: no new order; an open position stays open (the Lab closes it at its last bar). if saToTime != 0 and time > saToTime and not saWindowClosed strategy.cancel_all() saWindowClosed := true if saInWindow bool saCooling = saCooldownLeft > 0 if saCooling saCooldownLeft -= 1 // Lab 236 DESYNC guard: the Lab mirror must hold the position TradingView holds; never a silent gap. if saLogTrades and saDesyncLogged < 20 float saTvQty = strategy.position_size float saQtyTol = saQtyStep > 0 ? saQtyStep * 0.5 : math.abs(saQty) * 0.001 string saDesync = "" if saQty != 0 and saTvQty != 0 and (math.sign(saTvQty) != math.sign(saQty) or math.abs(math.abs(saTvQty) - math.abs(saQty)) > saQtyTol) saDesync := "size_differs" else if saQty != 0 and saTvQty == 0 and (strategy.closedtrades == 0 or strategy.closedtrades.entry_id(strategy.closedtrades - 1) != saEntryId) saDesync := "entry_not_filled" else if saQty == 0 and saTvQty != 0 saDesync := "unexpected_position" if saDesync != "" log.warning("SATRADES1|DESYNC|export_id=" + saExportId + "|time=" + str.tostring(time) + "|reason=" + saDesync + "|lab_qty=" + saNum(saQty) + "|tv_qty=" + saNum(saTvQty) + "|entry_id=" + saEntryId) saDesyncLogged += 1 // Intrabar stop / target / liquidation filled by strategy.exit since the last close. if saQty != 0 and strategy.position_size == 0 and strategy.closedtrades > 0 float saExitPx = strategy.closedtrades.exit_price(strategy.closedtrades - 1) saCash := math.max(saCash + saQty * (saQty > 0 ? saExitPx * (1.0 - saFee) : saExitPx * (1.0 + saFee)), 0.0) saQty := 0.0 saEntryRef := na saEntryFill := na saEntryBar := na saPeak := 0.0 saTrough := 0.0 saCooldownLeft := saCooldown // Exits decided on this close, in the Lab order. if saQty != 0 bool saIsLong = saQty > 0 float saPnlPct = saIsLong ? (close - saEntryFill) / saEntryFill * 100.0 : (saEntryFill - close) / saEntryFill * 100.0 if saIsLong if saPeak <= 0 or close > saPeak saPeak := close else if saTrough <= 0 or close < saTrough saTrough := close bool saTrailHit = not na(saTrailing) and saTrailing > 0 and (saIsLong ? (saPeak > 0 and saPnlPct > 0 and (saPeak - close) / saPeak * 100.0 >= saTrailing) : (saTrough > 0 and saPnlPct > 0 and (close - saTrough) / saTrough * 100.0 >= saTrailing)) string saReason = "" if saTrailHit saReason := "trailing_stop" else if not na(saTimeout) and saTimeout > 0 and bar_index - saEntryBar >= saTimeout saReason := "timeout" else if saIsLong ? saLongExit : saShortExit saReason := "exit_condition" else if saIsLong and saImplicitSignalExits and saLongInverseExit saReason := "signal" else if saIsLong and saImplicitSignalExits and saLetRun and not saLongEntry saReason := "signal" else if not saIsLong and saImplicitSignalExits and saHasShortEntry and not saShortEntry saReason := "signal" if saReason != "" // Lab 236: cancel the bracket BEFORE the close. With an active strategy.exit, TradingView does not make // strategy.close effective when it is placed: a same-bar entry would be refused (same side) or doubled (reversal). strategy.cancel("X" + saEntryId) strategy.close(saEntryId, comment=saReason, immediately=true) saCash := math.max(saCash + saQty * (saIsLong ? (close - saSlip) * (1.0 - saFee) : (close + saSlip) * (1.0 + saFee)), 0.0) saQty := 0.0 saEntryRef := na saEntryFill := na saEntryBar := na saPeak := 0.0 saTrough := 0.0 saCooldownLeft := saCooldown // Entries on this close (long first). bool saMayEnter = saN >= saFirstTradable and saQty == 0 and saCash > 0 and not saCooling if saMayEnter and saLongEntry float saSpend = saCash * saPositionSize saEntryRef := close + saSlip saEntryFill := saEntryRef * (1.0 + saFee) saQty := saRoundQty(saSpend / saEntryFill) saCash := saCash - saQty * saEntryFill saEntryBar := bar_index saPeak := close saTrough := 0.0 saEntryId := "L" + str.tostring(bar_index) strategy.entry(saEntryId, strategy.long, qty=saQty) float saStopPx = na if not na(saStopLoss) saStopPx := saEntryRef * (1.0 - saStopLoss / 100.0) if not na(saStopAtrMult) and saStopAtrMult > 0 and not na(saAtrStop) and saAtrStop > 0.000000000001 float lvl = saEntryRef - saStopAtrMult * saAtrStop saStopPx := na(saStopPx) ? lvl : math.max(saStopPx, lvl) float saTargetPx = na if not na(saTakeProfit) saTargetPx := saEntryRef * (1.0 + saTakeProfit / 100.0) if not na(saTakeAtrMult) and saTakeAtrMult > 0 and not na(saAtrTake) and saAtrTake > 0.000000000001 float lvl = saEntryRef + saTakeAtrMult * saAtrTake saTargetPx := na(saTargetPx) ? lvl : math.min(saTargetPx, lvl) saStopPx := saTickDown(saStopPx) saTargetPx := saTickUp(saTargetPx) if not na(saStopPx) or not na(saTargetPx) strategy.exit("X" + saEntryId, from_entry=saEntryId, stop=saStopPx, limit=saTargetPx) else if saMayEnter and saHasShortEntry and saShortEntry float saNotional = saCash * saPositionSize if not na(saShortInitialMargin) saNotional := math.min(saNotional, saCash / (saShortInitialMargin / 100.0)) saEntryRef := close - saSlip saEntryFill := saEntryRef * (1.0 - saFee) saQty := -saRoundQty(saNotional / saEntryFill) saCash := saCash + (-saQty) * saEntryFill saEntryBar := bar_index saTrough := saEntryFill saPeak := 0.0 saEntryId := "S" + str.tostring(bar_index) strategy.entry(saEntryId, strategy.short, qty=-saQty) float saStopPx = na if not na(saStopLoss) saStopPx := saEntryRef * (1.0 + saStopLoss / 100.0) if not na(saStopAtrMult) and saStopAtrMult > 0 and not na(saAtrStop) and saAtrStop > 0.000000000001 float lvl = saEntryRef + saStopAtrMult * saAtrStop saStopPx := na(saStopPx) ? lvl : math.min(saStopPx, lvl) // Lab equity floor / maintenance margin: liquidation level, used as the stop when tighter. float saLiq = saSlip > 0 ? (saCash / (-saQty) - saSlip * (1.0 + saFee)) / ((1.0 + saFee) + saShortMaintenance / 100.0) : saCash / ((-saQty) * ((1.0 + saFee) + saShortMaintenance / 100.0)) saStopPx := saTickUp(na(saStopPx) ? saLiq : saLiq < saStopPx ? saLiq : saStopPx) float saTargetPx = na if not na(saTakeProfit) saTargetPx := saEntryRef * (1.0 - saTakeProfit / 100.0) if not na(saTakeAtrMult) and saTakeAtrMult > 0 and not na(saAtrTake) and saAtrTake > 0.000000000001 float lvl = saEntryRef - saTakeAtrMult * saAtrTake saTargetPx := na(saTargetPx) ? lvl : math.max(saTargetPx, lvl) saTargetPx := saTickDown(saTargetPx) strategy.exit("X" + saEntryId, from_entry=saEntryId, stop=saStopPx, limit=saTargetPx)