//@version=6 // This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ // © ProEA // // Donchian Breakout 20/10 in R [ProEA]: a Donchian channel breakout on the chart's market, each trade measured in R // from its own entry and initial stop, and a running table of the trades in R. // One market at a time, the chart's own. A hypothetical tool: it never places orders, and every trade it draws is one // the rules below would have taken on this chart's closed bars. // // Entry a close above the highest high of the previous 20 bars, taken at that close. The bar itself isn't in the // channel: ta.highest(high, 20)[1]. Long only by default; "Both ways" also takes a close below the lowest low of // the previous 20 bars as a short. // Stop 2 × ATR(20) from the entry, Wilder's ATR (ta.atr), set once at the entry and never moved. // Exit the stop, or a close below the lowest low of the previous 10 bars (above the highest high, for a short), // whichever comes first. The stop is checked before the close: a bar that opens beyond the stop exits at its // open, a bar that trades through it exits at the stop. After an exit, the next entry needs a later bar. // R long (exit − entry) / (entry − initial stop); short (entry − exit) / (initial stop − entry). No fees, slippage // or funding are taken off. // Decisions on closed bars only (barstate.isconfirmed). No other timeframe is read, and nothing decided on a closed bar // moves afterwards. indicator("Donchian Breakout 20/10 in R [ProEA]", "Donchian 20/10 R", overlay = true, max_labels_count = 500) string GR = "Rules" int entryLen = input.int(20, "Entry length", minval = 1, group = GR, tooltip = "A close above the highest high of this many bars before it enters long (below the lowest low, short). The bar itself isn't counted.") int exitLen = input.int(10, "Exit length", minval = 1, group = GR, tooltip = "A close below the lowest low of this many bars before it exits a long (above the highest high, a short). The bar itself isn't counted.") int atrLen = input.int(20, "ATR length", minval = 1, group = GR, tooltip = "Wilder's ATR (ta.atr): the first true ranges averaged, then each new one weighted 1/length.") float atrMult = input.float(2.0, "ATR multiplier", minval = 0.1, step = 0.1, group = GR, tooltip = "The stop sits this many ATRs from the entry, set once at the entry. One R is that distance.") string dirIn = input.string("Long only", "Direction", options = ["Long only", "Both ways"], group = GR, tooltip = "Long only: closes above the upper band enter. Both ways: closes below the lower band enter short too.") string GD = "Display" bool showBands = input.bool(true, "Show the bands", group = GD) bool showTable = input.bool(true, "Show the table", group = GD) string posIn = input.string("Top right", "Table position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = GD) string sizeIn = input.string("Small", "Text size", options = ["Tiny", "Small", "Normal"], group = GD) color bandCol = input.color(#2962ff, "Entry band and entries", group = GD) color exitCol = input.color(#ff9800, "Exit band", group = GD) color stopCol = input.color(#f23645, "Stop", group = GD) color upCol = input.color(#089981, "Trade above 0R", group = GD) color downCol = input.color(#f23645, "Trade below 0R", group = GD) bool bothWays = dirIn == "Both ways" string txtSize = sizeIn == "Tiny" ? size.tiny : sizeIn == "Normal" ? size.normal : size.small string tblPos = posIn == "Bottom right" ? position.bottom_right : posIn == "Top left" ? position.top_left : posIn == "Bottom left" ? position.bottom_left : position.top_right // Wilder's ATR and the four channel lines, on every bar. Each channel is the bars before this one ([1]), so a close can // be above it. float atr = ta.atr(atrLen) float upper = ta.highest(high, entryLen)[1] // long entry: the highest high of the previous entryLen bars float lower = ta.lowest(low, entryLen)[1] // short entry: the lowest low of the previous entryLen bars float exitLo = ta.lowest(low, exitLen)[1] // long exit: the lowest low of the previous exitLen bars float exitHi = ta.highest(high, exitLen)[1] // short exit: the highest high of the previous exitLen bars f_r(float x) => na(x) ? "–" : (x > 0 ? "+" : x < 0 ? "−" : "") + str.tostring(math.abs(x), "0.00") + "R" f_row(table tb, int row, string a, string b, color tc, string sz) => table.cell(tb, 0, row, a, text_color = tc, text_size = sz, text_halign = text.align_left) table.cell(tb, 1, row, b, text_color = tc, text_size = sz, text_halign = text.align_right) row + 1 f_wide(table tb, int row, string a, color tc, string sz) => table.cell(tb, 0, row, a, text_color = tc, text_size = sz, text_halign = text.align_left) table.merge_cells(tb, 0, row, 1, row) row + 1 var int side = 0 // 1 long, -1 short, 0 flat var float entryPx = na var float stopPx = na // the initial stop, never moved var float riskPx = na // one R in price: the entry to the initial stop var int nTrades = 0 var int nWins = 0 var float netR = 0.0 var float worstR = na // the most negative R; na until a trade closes below 0R var float lastClose = na // the trade that stood while this bar formed, set at an earlier close float stopNow = side != 0 ? stopPx : na float entryNow = side != 0 ? entryPx : na bool longIn = false bool shortIn = false if barstate.isconfirmed and chart.is_standard if side != 0 // the stop first, then the close: a bar that opens beyond the stop exits at its open, one that trades // through it at the stop, and a close beyond the exit channel at the close float exitPx = na string why = "" if side == 1 ? open <= stopPx : open >= stopPx exitPx := open why := "opened beyond the stop" else if side == 1 ? low <= stopPx : high >= stopPx exitPx := stopPx why := "traded through the stop" else if bar_index >= exitLen and (side == 1 ? close < exitLo : close > exitHi) exitPx := close why := side == 1 ? "closed below the " + str.tostring(exitLen) + "-bar low" : "closed above the " + str.tostring(exitLen) + "-bar high" if not na(exitPx) float r = side * (exitPx - entryPx) / riskPx nTrades += 1 netR += r if r > 0 nWins += 1 if r < 0 worstR := na(worstR) ? r : math.min(worstR, r) color c = r > 0 ? upCol : r < 0 ? downCol : color.gray string tip = (side == 1 ? "long" : "short") + " · entry " + str.tostring(entryPx, format.mintick) + " · stop " + str.tostring(stopPx, format.mintick) + " · exit " + str.tostring(exitPx, format.mintick) + ", " + why + "\n" + f_r(r) + " before costs" label.new(bar_index, exitPx, f_r(r), yloc = side == 1 ? yloc.abovebar : yloc.belowbar, style = side == 1 ? label.style_label_down : label.style_label_up, color = color.new(c, 15), textcolor = color.white, size = txtSize, tooltip = tip) side := 0 else if bar_index >= entryLen and not na(atr) // an exit bar never enters: one decision per bar bool up = close > upper bool dn = close < lower int d = up and not dn ? 1 : dn and not up and bothWays ? -1 : 0 if d != 0 side := d entryPx := close riskPx := atrMult * atr stopPx := close - d * riskPx stopNow := stopPx entryNow := entryPx longIn := d == 1 shortIn := d == -1 lastClose := close // the bands from standard candles only: Heikin Ashi and other synthetic candles aren't the market's prices bool drawBands = showBands and chart.is_standard plot(drawBands ? upper : na, "Upper band (entry)", bandCol, 1, plot.style_stepline) plot(drawBands and bothWays ? lower : na, "Lower band (short entry)", bandCol, 1, plot.style_stepline) plot(drawBands ? exitLo : na, "Exit band (long)", exitCol, 1, plot.style_stepline) plot(drawBands and bothWays ? exitHi : na, "Exit band (short)", exitCol, 1, plot.style_stepline) plot(entryNow, "Entry price", color.new(chart.fg_color, 40), 1, plot.style_linebr) plot(stopNow, "Stop", stopCol, 2, plot.style_linebr) plotshape(longIn, "Long entry", shape.triangleup, location.belowbar, bandCol, size = size.small) plotshape(shortIn, "Short entry", shape.triangledown, location.abovebar, bandCol, size = size.small) // the table: every closed trade on this chart, in R, from closed bars var table tbl = showTable ? table.new(tblPos, 2, 10, bgcolor = color.new(chart.bg_color, 8), frame_color = color.new(chart.fg_color, 70), frame_width = 1, border_width = 0) : na if barstate.islast and showTable color fg = chart.fg_color color dim = color.new(chart.fg_color, 45) string cfgTxt = str.tostring(entryLen) + "/" + str.tostring(exitLen) + " · " + str.tostring(atrMult, "#.##") + " × ATR(" + str.tostring(atrLen) + ")" int row = 0 row := f_row(tbl, row, "Donchian Breakout in R", cfgTxt, fg, txtSize) if not chart.is_standard row := f_wide(tbl, row, "needs standard candles", downCol, txtSize) else string openTxt = side == 0 ? "none" : (side == 1 ? "long " : "short ") + f_r(side * (lastClose - entryPx) / riskPx) + " at the last close" row := f_row(tbl, row, "Direction", dirIn, fg, txtSize) row := f_row(tbl, row, "Trades", str.tostring(nTrades), fg, txtSize) row := f_row(tbl, row, "Wins", str.tostring(nWins), fg, txtSize) row := f_row(tbl, row, "Net R", nTrades > 0 ? f_r(netR) : "–", fg, txtSize) row := f_row(tbl, row, "Average R", nTrades > 0 ? f_r(netR / nTrades) : "–", fg, txtSize) row := f_row(tbl, row, "Largest loss", na(worstR) ? "none" : f_r(worstR), fg, txtSize) row := f_row(tbl, row, "Open trade", openTxt, side == 0 ? dim : fg, txtSize) row := f_wide(tbl, row, "Closed bars only · before costs · hypothetical, never an order", dim, txtSize)