|
| 1 | +//@version=6 |
| 2 | +strategy( |
| 3 | + "Regime Trend v1 +2 ATR BE Ratchet", |
| 4 | + overlay = true, |
| 5 | + pyramiding = 0, |
| 6 | + initial_capital = 10000, |
| 7 | + default_qty_type = strategy.percent_of_equity, |
| 8 | + default_qty_value = 100, |
| 9 | + commission_type = strategy.commission.percent, |
| 10 | + commission_value = 0.10, |
| 11 | + process_orders_on_close = false, |
| 12 | + calc_on_order_fills = true) |
| 13 | + |
| 14 | +// IMPORTANT: Run this script on a 5-minute chart. |
| 15 | +// The 4-hour entry/trend logic is requested from completed 4-hour candles. |
| 16 | +// The custom model below is authoritative for percentage-slippage parity. |
| 17 | +// TradingView's built-in `slippage` setting is tick-based, so visual Strategy Tester |
| 18 | +// fills can differ slightly from the custom normalized model shown in the table. |
| 19 | + |
| 20 | +const string BASE_TF = "240" |
| 21 | +const int FIVE_MINUTES_MS = 5 * 60 * 1000 |
| 22 | +const float COMMISSION_RATE = 0.001 |
| 23 | +const float SLIPPAGE_RATE = 0.0005 |
| 24 | +const float INITIAL_STOP_ATR = 2.5 |
| 25 | +const float TRAILING_STOP_ATR = 3.0 |
| 26 | +const float ACTIVATION_ATR = 2.0 |
| 27 | +const float ATR_FLOOR = 0.005 |
| 28 | + |
| 29 | +bool isFiveMinuteChart = timeframe.isminutes and timeframe.multiplier == 5 |
| 30 | +if barstate.isfirst and not isFiveMinuteChart |
| 31 | + runtime.error("Regime Trend v1 Ratchet must run on a 5-minute chart.") |
| 32 | + |
| 33 | +float htfClose = request.security( |
| 34 | + syminfo.tickerid, |
| 35 | + BASE_TF, |
| 36 | + close, |
| 37 | + gaps = barmerge.gaps_off, |
| 38 | + lookahead = barmerge.lookahead_off) |
| 39 | +float htfEma50 = request.security( |
| 40 | + syminfo.tickerid, |
| 41 | + BASE_TF, |
| 42 | + ta.ema(close, 50), |
| 43 | + gaps = barmerge.gaps_off, |
| 44 | + lookahead = barmerge.lookahead_off) |
| 45 | +float htfEma200 = request.security( |
| 46 | + syminfo.tickerid, |
| 47 | + BASE_TF, |
| 48 | + ta.ema(close, 200), |
| 49 | + gaps = barmerge.gaps_off, |
| 50 | + lookahead = barmerge.lookahead_off) |
| 51 | +float htfAtr14 = request.security( |
| 52 | + syminfo.tickerid, |
| 53 | + BASE_TF, |
| 54 | + ta.atr(14), |
| 55 | + gaps = barmerge.gaps_off, |
| 56 | + lookahead = barmerge.lookahead_off) |
| 57 | +float htfBreakout = request.security( |
| 58 | + syminfo.tickerid, |
| 59 | + BASE_TF, |
| 60 | + ta.highest(high[1], 20), |
| 61 | + gaps = barmerge.gaps_off, |
| 62 | + lookahead = barmerge.lookahead_off) |
| 63 | + |
| 64 | +bool htfBarClosesHere = time_close == time_close(BASE_TF) |
| 65 | +bool htfReady = not na(htfClose) and not na(htfEma50) and not na(htfEma200) and not na(htfAtr14) and not na(htfBreakout) |
| 66 | + |
| 67 | +f_breakEvenRawFloor(float modelEntryFill) => |
| 68 | + float entryFeePerUnitNotional = COMMISSION_RATE |
| 69 | + float exitFillNeeded = modelEntryFill * (1.0 + entryFeePerUnitNotional) / (1.0 - COMMISSION_RATE) |
| 70 | + exitFillNeeded / (1.0 - SLIPPAGE_RATE) |
| 71 | + |
| 72 | +f_exitFill(float rawReference) => |
| 73 | + rawReference * (1.0 - SLIPPAGE_RATE) |
| 74 | + |
| 75 | +var bool modelInTrade = false |
| 76 | +var bool pendingEntry = false |
| 77 | +var float pendingEntryAtr = na |
| 78 | +var bool pendingTrendExit = false |
| 79 | + |
| 80 | +var float modelEntryFill = na |
| 81 | +var float modelEntryAtr = na |
| 82 | +var float modelQuantity = na |
| 83 | +var float modelEntryFee = na |
| 84 | +var float originalStop = na |
| 85 | +var float pendingOriginalStop = na |
| 86 | +var float highestHtfClose = na |
| 87 | +var float activationPrice = na |
| 88 | +var float ratchetFloor = na |
| 89 | +var bool ratchetArmed = false |
| 90 | +var bool ratchetActive = false |
| 91 | +var bool parityGap = false |
| 92 | +var int modelEntryTime = na |
| 93 | + |
| 94 | +var int modelTradeCount = 0 |
| 95 | +var int modelWinnerCount = 0 |
| 96 | +var int modelRatchetExitCount = 0 |
| 97 | +var int modelGapTradeCount = 0 |
| 98 | +var float modelCumulativeNet = 0.0 |
| 99 | +var float lastModelNet = na |
| 100 | +var string lastModelExitReason = "none" |
| 101 | + |
| 102 | +bool dataGapThisBar = not na(time[1]) and time - time[1] != FIVE_MINUTES_MS |
| 103 | +if modelInTrade and dataGapThisBar |
| 104 | + parityGap := true |
| 105 | + |
| 106 | +// State calculated on the previous 5-minute close becomes active now. |
| 107 | +if modelInTrade and not na(pendingOriginalStop) |
| 108 | + originalStop := math.max(originalStop, pendingOriginalStop) |
| 109 | + pendingOriginalStop := na |
| 110 | + |
| 111 | +if modelInTrade and ratchetArmed |
| 112 | + ratchetActive := true |
| 113 | + ratchetArmed := false |
| 114 | + |
| 115 | +bool enteredThisBar = false |
| 116 | +bool exitedThisBar = false |
| 117 | + |
| 118 | +// A signal submitted on the final 5-minute bar of a 4-hour candle fills here, |
| 119 | +// at the next 5-minute open, which is also the next 4-hour open. |
| 120 | +if pendingEntry and not modelInTrade |
| 121 | + modelEntryFill := open * (1.0 + SLIPPAGE_RATE) |
| 122 | + modelEntryAtr := pendingEntryAtr |
| 123 | + modelQuantity := 1.0 / modelEntryFill |
| 124 | + modelEntryFee := modelEntryFill * modelQuantity * COMMISSION_RATE |
| 125 | + originalStop := modelEntryFill - INITIAL_STOP_ATR * modelEntryAtr |
| 126 | + pendingOriginalStop := na |
| 127 | + highestHtfClose := na |
| 128 | + activationPrice := modelEntryFill + ACTIVATION_ATR * modelEntryAtr |
| 129 | + ratchetFloor := f_breakEvenRawFloor(modelEntryFill) |
| 130 | + ratchetArmed := false |
| 131 | + ratchetActive := false |
| 132 | + parityGap := dataGapThisBar |
| 133 | + pendingTrendExit := false |
| 134 | + modelEntryTime := time |
| 135 | + modelInTrade := true |
| 136 | + pendingEntry := false |
| 137 | + pendingEntryAtr := na |
| 138 | + enteredThisBar := true |
| 139 | + |
| 140 | +float activeRatchetFloor = modelInTrade and ratchetActive and not parityGap ? ratchetFloor : na |
| 141 | +float combinedStop = modelInTrade ? math.max(originalStop, nz(activeRatchetFloor, originalStop)) : na |
| 142 | + |
| 143 | +string modelExitReason = "" |
| 144 | +float modelRawExitReference = na |
| 145 | +bool ratchetBindingExit = false |
| 146 | + |
| 147 | +if modelInTrade |
| 148 | + if pendingTrendExit |
| 149 | + if open <= combinedStop |
| 150 | + modelRawExitReference := open |
| 151 | + ratchetBindingExit := ratchetActive and not parityGap and ratchetFloor > originalStop and open > originalStop |
| 152 | + modelExitReason := ratchetBindingExit ? "ratchet_gap" : "baseline_stop_gap" |
| 153 | + else |
| 154 | + modelRawExitReference := open |
| 155 | + modelExitReason := "trend_exit" |
| 156 | + else if open <= combinedStop |
| 157 | + modelRawExitReference := open |
| 158 | + ratchetBindingExit := ratchetActive and not parityGap and ratchetFloor > originalStop and open > originalStop |
| 159 | + modelExitReason := ratchetBindingExit ? "ratchet_gap" : "baseline_stop_gap" |
| 160 | + else if low <= combinedStop |
| 161 | + modelRawExitReference := combinedStop |
| 162 | + ratchetBindingExit := ratchetActive and not parityGap and ratchetFloor > originalStop |
| 163 | + modelExitReason := ratchetBindingExit ? "ratchet_stop" : "baseline_stop" |
| 164 | + |
| 165 | +if modelInTrade and modelExitReason != "" |
| 166 | + float modelExitFill = f_exitFill(modelRawExitReference) |
| 167 | + float modelExitFee = modelExitFill * modelQuantity * COMMISSION_RATE |
| 168 | + float modelGrossPnl = (modelExitFill - modelEntryFill) * modelQuantity |
| 169 | + float modelNetPnl = modelGrossPnl - modelEntryFee - modelExitFee |
| 170 | + |
| 171 | + modelTradeCount += 1 |
| 172 | + modelWinnerCount += modelNetPnl > 0 ? 1 : 0 |
| 173 | + modelRatchetExitCount += ratchetBindingExit ? 1 : 0 |
| 174 | + modelGapTradeCount += parityGap ? 1 : 0 |
| 175 | + modelCumulativeNet += modelNetPnl |
| 176 | + lastModelNet := modelNetPnl |
| 177 | + lastModelExitReason := modelExitReason |
| 178 | + |
| 179 | + modelInTrade := false |
| 180 | + pendingTrendExit := false |
| 181 | + modelEntryFill := na |
| 182 | + modelEntryAtr := na |
| 183 | + modelQuantity := na |
| 184 | + modelEntryFee := na |
| 185 | + originalStop := na |
| 186 | + pendingOriginalStop := na |
| 187 | + highestHtfClose := na |
| 188 | + activationPrice := na |
| 189 | + ratchetFloor := na |
| 190 | + ratchetArmed := false |
| 191 | + ratchetActive := false |
| 192 | + parityGap := false |
| 193 | + modelEntryTime := na |
| 194 | + exitedThisBar := true |
| 195 | + |
| 196 | +// Activation is evaluated only after the current candle's pre-existing stop. |
| 197 | +// Therefore the break-even floor can first act on the next 5-minute candle. |
| 198 | +if modelInTrade and not ratchetActive and not ratchetArmed and not parityGap |
| 199 | + bool activationTouched = open >= activationPrice or high >= activationPrice |
| 200 | + if activationTouched |
| 201 | + ratchetArmed := true |
| 202 | + |
| 203 | +// Completed 4-hour close logic. The resulting stop update and trend-exit order |
| 204 | +// become active at the next 5-minute bar, i.e. the next 4-hour open. |
| 205 | +if modelInTrade and htfBarClosesHere and htfReady and not exitedThisBar |
| 206 | + highestHtfClose := na(highestHtfClose) ? htfClose : math.max(highestHtfClose, htfClose) |
| 207 | + float candidateOriginalStop = highestHtfClose - TRAILING_STOP_ATR * htfAtr14 |
| 208 | + pendingOriginalStop := math.max(originalStop, candidateOriginalStop) |
| 209 | + |
| 210 | + if htfClose < htfEma50 |
| 211 | + pendingTrendExit := true |
| 212 | + strategy.close("Long", comment = "4H trend exit") |
| 213 | + |
| 214 | +// Keep the broker-emulator stop aligned with the stop that will be active on |
| 215 | +// the next 5-minute candle. The custom model remains authoritative for the |
| 216 | +// percentage-slippage arithmetic. |
| 217 | +if modelInTrade |
| 218 | + float nextOriginalStop = not na(pendingOriginalStop) ? math.max(originalStop, pendingOriginalStop) : originalStop |
| 219 | + bool nextRatchetActive = ratchetActive or ratchetArmed |
| 220 | + float nextCombinedStop = nextRatchetActive and not parityGap ? math.max(nextOriginalStop, ratchetFloor) : nextOriginalStop |
| 221 | + strategy.exit("Long Stop", "Long", stop = nextCombinedStop, comment_loss = "Active stop") |
| 222 | + |
| 223 | +bool bullishRegime = htfReady and htfEma50 > htfEma200 and htfClose > htfEma200 |
| 224 | +bool breakout = htfReady and htfClose > htfBreakout |
| 225 | +bool volatilityAllowed = htfReady and htfAtr14 / htfClose >= ATR_FLOOR |
| 226 | +bool longSignal = htfBarClosesHere and bullishRegime and breakout and volatilityAllowed |
| 227 | + |
| 228 | +if longSignal and not modelInTrade and not pendingEntry and not exitedThisBar |
| 229 | + pendingEntry := true |
| 230 | + pendingEntryAtr := htfAtr14 |
| 231 | + strategy.entry("Long", strategy.long, comment = "4H breakout") |
| 232 | + |
| 233 | +plot(htfEma50, "4H EMA 50", color = color.orange) |
| 234 | +plot(htfEma200, "4H EMA 200", color = color.blue) |
| 235 | +plot(modelInTrade ? originalStop : na, "Original 4H Stop", color = color.red, style = plot.style_linebr) |
| 236 | +plot(modelInTrade and ratchetActive and not parityGap ? ratchetFloor : na, "Ratchet Floor", color = color.aqua, style = plot.style_linebr) |
| 237 | +plot(modelInTrade ? activationPrice : na, "+2 ATR Activation", color = color.new(color.green, 40), style = plot.style_linebr) |
| 238 | + |
| 239 | +plotshape(longSignal and not modelInTrade, title = "Long signal", style = shape.triangleup, location = location.belowbar, color = color.green, size = size.tiny, text = "4H") |
| 240 | +plotshape(ratchetArmed, title = "Ratchet armed", style = shape.circle, location = location.abovebar, color = color.aqua, size = size.tiny, text = "BE") |
| 241 | +bgcolor(modelInTrade and parityGap ? color.new(color.red, 88) : na, title = "Parity gap trade") |
| 242 | + |
| 243 | +var table status = table.new(position.top_right, 2, 9, border_width = 1) |
| 244 | +if barstate.islast |
| 245 | + table.cell(status, 0, 0, "Model") |
| 246 | + table.cell(status, 1, 0, "Regime Trend v1 Ratchet") |
| 247 | + table.cell(status, 0, 1, "Chart") |
| 248 | + table.cell(status, 1, 1, timeframe.period) |
| 249 | + table.cell(status, 0, 2, "Trades") |
| 250 | + table.cell(status, 1, 2, str.tostring(modelTradeCount)) |
| 251 | + table.cell(status, 0, 3, "Winners") |
| 252 | + table.cell(status, 1, 3, str.tostring(modelWinnerCount)) |
| 253 | + table.cell(status, 0, 4, "Ratchet exits") |
| 254 | + table.cell(status, 1, 4, str.tostring(modelRatchetExitCount)) |
| 255 | + table.cell(status, 0, 5, "Gap-flagged") |
| 256 | + table.cell(status, 1, 5, str.tostring(modelGapTradeCount)) |
| 257 | + table.cell(status, 0, 6, "Custom net") |
| 258 | + table.cell(status, 1, 6, str.tostring(modelCumulativeNet, "#.######")) |
| 259 | + table.cell(status, 0, 7, "Last net") |
| 260 | + table.cell(status, 1, 7, na(lastModelNet) ? "n/a" : str.tostring(lastModelNet, "#.######")) |
| 261 | + table.cell(status, 0, 8, "Last exit") |
| 262 | + table.cell(status, 1, 8, lastModelExitReason) |
0 commit comments