//@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 // // Trade Log in R [ProEA]: draws the trades you type into its settings on the chart, measures each one in R (the // result over the distance from entry to stop) and totals them in a table. // It reads only what you type. It doesn't connect to a broker, to Paper Trading or to your TradingView account, and it // can't see your fills. Paste only the chart symbol's trades: every line is drawn on the chart you open. // // One trade per line, the fields separated by commas (a tab or a semicolon counts as a comma): // entry time, side, entry, stop, exit, tag // 2026-10-01 08:15, long, 1.1700, 1.1680, 1.1740, london break // entry time YYYY-MM-DD HH:MM, seconds optional, a T between date and time allowed, in the timezone you pick; // side long or short (L, S, buy and sell are read too); // prices entry, stop and exit, with a decimal point and no thousands separators; // tag optional, free text; tags are grouped without regard to case. // Blank lines and lines that start with # or // are ignored. Any other line that doesn't read is counted as a line // skipped. The first 500 trade lines are read; lines after them are counted, not read. // // R of one trade: long (exit - entry) / (entry - stop); short (entry - exit) / (stop - entry); then the cost per trade // in R is subtracted (0 by default). A stop on the wrong side of the entry, or at the entry, gives no R: the trade is // labelled on the chart and left out of the totals. // Expectancy = win rate x average win - loss rate x |average loss|, shown beside total R / trades as a check. indicator("Trade Log in R [ProEA]", "Trade Log R", overlay = true, max_labels_count = 500, max_lines_count = 500, max_boxes_count = 500) int CAP = 500 string GT = "Your trades" string logIn = input.text_area("# entry time, side, entry, stop, exit, tag\n# 2026-10-01 08:15, long, 1.1700, 1.1680, 1.1740, london break", "Trades, one per line", tooltip = "One trade per line: entry time, side, entry, stop, exit, tag. Example: 2026-10-01 08:15, long, 1.1700, 1.1680, 1.1740, london break. Time as YYYY-MM-DD HH:MM (seconds optional). Side: long or short (L, S, buy, sell). Prices with a decimal point, no thousands separators. The tag is optional. Tabs and semicolons count as commas. Lines starting with # or // are ignored. Paste only this chart's symbol. The first 500 trade lines are read.", group = GT) string tzIn = input.string("Exchange", "Timezone of your times", options = ["Exchange", "UTC", "America/New_York", "America/Chicago", "America/Los_Angeles", "America/Sao_Paulo", "Europe/London", "Europe/Berlin", "Europe/Athens", "Europe/Moscow", "Asia/Dubai", "Asia/Kolkata", "Asia/Bangkok", "Asia/Singapore", "Asia/Hong_Kong", "Asia/Tokyo", "Australia/Sydney"], tooltip = "The clock your entry times are written in. Exchange is the chart symbol's own timezone.", group = GT) float costIn = input.float(0.0, "Cost per trade, in R", minval = 0.0, step = 0.01, tooltip = "Subtracted from every trade's R: 0.05 takes 5% of the risk off each trade for spread and commission. 0 by default.", group = GT) string GD = "Display" int widthIn = input.int(8, "Box width, in bars", minval = 1, maxval = 200, tooltip = "How far right of the entry time each trade's box and stop line reach. The times you type have no exit time.", 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 upCol = input.color(#089981, "Trade above 0R", group = GD) color downCol = input.color(#f23645, "Trade below 0R", group = GD) color stopCol = input.color(#ff9800, "Stop line", group = GD) 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 string TZ = tzIn == "Exchange" ? syminfo.timezone : tzIn type Trade int t // entry time, ms int side // 1 long, -1 short float en float st float ex string tag float r // net R; na when the stop is on the wrong side of the entry or at it int issue // 0 none, 1 stop on the wrong side, 2 stop at the entry // a whole number between lo and hi, read from text; na when it isn't one f_int(string s, int lo, int hi) => float v = str.tonumber(str.trim(s)) int out = na if not na(v) if v == math.floor(v) and v >= lo and v <= hi out := int(v) out // the entry time in ms from "YYYY-MM-DD HH:MM[:SS]" (or a T between date and time, or / in the date); na when it // doesn't read or names a day the month doesn't have f_time(string s, string tz) => string txt = str.replace_all(str.replace_all(str.trim(s), "T", " "), "/", "-") array p = array.new_string() for x in str.split(txt, " ") if str.length(x) > 0 p.push(x) int out = na if p.size() == 1 or p.size() == 2 array d = str.split(p.get(0), "-") if d.size() == 3 int yy = f_int(d.get(0), 1970, 2100) int mm = f_int(d.get(1), 1, 12) int dd = f_int(d.get(2), 1, 31) int hh = 0 int mi = 0 int ss = 0 bool ok = not na(yy) and not na(mm) and not na(dd) if ok and p.size() == 2 array c = str.split(p.get(1), ":") if c.size() == 2 or c.size() == 3 hh := f_int(c.get(0), 0, 23) mi := f_int(c.get(1), 0, 59) ss := c.size() == 3 ? f_int(c.get(2), 0, 59) : 0 ok := not na(hh) and not na(mi) and not na(ss) else ok := false if ok int ts = timestamp(tz, yy, mm, dd, hh, mi, ss) if dayofmonth(ts, tz) == dd and month(ts, tz) == mm out := ts out // 1 long, -1 short, 0 when the side doesn't read f_side(string s) => string v = str.lower(str.trim(s)) v == "long" or v == "l" or v == "buy" ? 1 : v == "short" or v == "s" or v == "sell" ? -1 : 0 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 firstBarTime = time var table tbl = showTable ? table.new(tblPos, 2, 32, bgcolor = color.new(chart.bg_color, 8), frame_color = color.new(chart.fg_color, 70), frame_width = 1, border_width = 0) : na // Everything is read and drawn once, on the last bar of history: the log is an input, the same on every bar. if barstate.islastconfirmedhistory // ---- read the log array trades = array.new() int skipped = 0 int overCap = 0 for raw in str.split(str.replace_all(str.replace_all(logIn, "\t", ","), ";", ","), "\n") string ln = str.trim(raw) if str.length(ln) == 0 or str.startswith(ln, "#") or str.startswith(ln, "//") continue if trades.size() >= CAP overCap += 1 continue array f = str.split(ln, ",") if f.size() < 5 skipped += 1 continue int t = f_time(f.get(0), TZ) int sd = f_side(f.get(1)) float en = str.tonumber(str.trim(f.get(2))) float st = str.tonumber(str.trim(f.get(3))) float ex = str.tonumber(str.trim(f.get(4))) if na(t) or sd == 0 or na(en) or na(st) or na(ex) skipped += 1 continue string tag = f.size() > 5 ? str.trim(f.slice(5, f.size()).join(",")) : "" // the risk distance: entry to stop, positive when the stop is on the losing side float risk = sd == 1 ? en - st : st - en int issue = risk < 0 ? 1 : risk == 0 ? 2 : 0 float r = issue == 0 ? sd * (ex - en) / risk - costIn : na trades.push(Trade.new(t, sd, en, st, ex, tag, r, issue)) // ---- totals int n = 0 int wins = 0 int losses = 0 int flat = 0 int wrongSide = 0 int atEntry = 0 int offChart = 0 float sumWin = 0.0 float sumLoss = 0.0 float worst = na array tagName = array.new_string() array tagN = array.new_int() array tagR = array.new_float() for tr in trades if tr.t < firstBarTime or tr.t > time_close offChart += 1 if tr.issue == 1 wrongSide += 1 else if tr.issue == 2 atEntry += 1 else n += 1 if tr.r > 0 wins += 1 sumWin += tr.r else if tr.r < 0 losses += 1 sumLoss += tr.r worst := na(worst) ? tr.r : math.min(worst, tr.r) else flat += 1 string key = tr.tag == "" ? "(no tag)" : str.lower(tr.tag) int k = tagName.indexof(key) if k < 0 tagName.push(key) tagN.push(1) tagR.push(tr.r) else tagN.set(k, tagN.get(k) + 1) tagR.set(k, tagR.get(k) + tr.r) float total = sumWin + sumLoss float winRate = n > 0 ? float(wins) / n : na float lossRate = n > 0 ? float(losses) / n : na float avgWin = wins > 0 ? sumWin / wins : na float avgLoss = losses > 0 ? sumLoss / losses : na float expectancy = n > 0 ? winRate * nz(avgWin) - lossRate * math.abs(nz(avgLoss)) : na float perTrade = n > 0 ? total / n : na // ---- drawings: a box from entry to exit, the stop as a short dashed line, the R in a label at the box's right end int barMs = timeframe.in_seconds() * 1000 for tr in trades // a trade dated outside this chart's bars is counted in the table and not drawn if tr.t < firstBarTime or tr.t > time_close continue int right = tr.t + widthIn * barMs string tip = str.format_time(tr.t, "yyyy-MM-dd HH:mm", TZ) + " " + (tr.side == 1 ? "long" : "short") + (tr.tag == "" ? "" : " · " + tr.tag) + "\nentry " + str.tostring(tr.en, format.mintick) + " · stop " + str.tostring(tr.st, format.mintick) + " · exit " + str.tostring(tr.ex, format.mintick) if tr.issue == 0 color c = tr.r > 0 ? upCol : tr.r < 0 ? downCol : color.gray box.new(tr.t, math.max(tr.en, tr.ex), right, math.min(tr.en, tr.ex), border_color = c, xloc = xloc.bar_time, bgcolor = color.new(c, 82)) line.new(tr.t, tr.st, right, tr.st, xloc = xloc.bar_time, color = stopCol, style = line.style_dashed, width = 1) label.new(right, tr.ex, f_r(tr.r), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(c, 15), textcolor = color.white, size = txtSize, tooltip = tip + "\nR " + f_r(tr.r) + (costIn > 0 ? " after a cost of " + str.tostring(costIn, "0.00") + "R" : "")) else label.new(tr.t, tr.en, tr.issue == 1 ? "no R: stop on the wrong side" : "no R: stop at the entry", xloc = xloc.bar_time, style = label.style_label_down, color = color.new(color.gray, 15), textcolor = color.white, size = txtSize, tooltip = tip) // ---- the table if showTable color fg = chart.fg_color color dim = color.new(chart.fg_color, 45) int row = 0 row := f_wide(tbl, row, "Trade Log in R", fg, txtSize) if trades.size() == 0 and skipped == 0 and overCap == 0 row := f_wide(tbl, row, "No trades yet: open the settings and type\nor paste your trades, one per line.", dim, txtSize) else row := f_row(tbl, row, "Trades", str.tostring(n), fg, txtSize) row := f_row(tbl, row, "Wins", str.tostring(wins), fg, txtSize) row := f_row(tbl, row, "Losses", str.tostring(losses), fg, txtSize) if flat > 0 row := f_row(tbl, row, "At 0R", str.tostring(flat), fg, txtSize) row := f_row(tbl, row, "Win rate", n > 0 ? str.tostring(winRate * 100, "0.0") + "%" : "–", fg, txtSize) row := f_row(tbl, row, "Average win", f_r(avgWin), fg, txtSize) row := f_row(tbl, row, "Average loss", f_r(avgLoss), fg, txtSize) row := f_row(tbl, row, "Expectancy", f_r(expectancy), fg, txtSize) row := f_row(tbl, row, "Check: total R ÷ trades", f_r(perTrade), dim, txtSize) row := f_row(tbl, row, "Total R", f_r(n > 0 ? total : na), fg, txtSize) row := f_row(tbl, row, "Largest loss", f_r(worst), fg, txtSize) if costIn > 0 row := f_row(tbl, row, "Cost per trade", str.tostring(costIn, "0.00") + "R off each", dim, txtSize) if tagName.size() > 0 row := f_wide(tbl, row, "By tag, most trades first", fg, txtSize) array used = array.new_bool(tagName.size(), false) for j = 1 to math.min(5, tagName.size()) int best = -1 for i = 0 to tagName.size() - 1 if not used.get(i) and (best < 0 or tagN.get(i) > tagN.get(best)) best := i used.set(best, true) row := f_row(tbl, row, " " + tagName.get(best) + " (" + str.tostring(tagN.get(best)) + ")", f_r(tagR.get(best)), fg, txtSize) if skipped > 0 row := f_wide(tbl, row, str.tostring(skipped) + (skipped == 1 ? " line skipped" : " lines skipped") + ": not read", downCol, txtSize) if wrongSide > 0 row := f_wide(tbl, row, str.tostring(wrongSide) + " with the stop on the wrong side: not counted", downCol, txtSize) if atEntry > 0 row := f_wide(tbl, row, str.tostring(atEntry) + " with the stop at the entry: not counted", downCol, txtSize) if overCap > 0 row := f_wide(tbl, row, str.tostring(overCap) + " over the " + str.tostring(CAP) + "-trade cap: not read", downCol, txtSize) if offChart > 0 row := f_wide(tbl, row, str.tostring(offChart) + " outside this chart's bars: counted, not drawn", dim, txtSize) row := f_wide(tbl, row, "From the lines you typed, not your account", dim, txtSize)