// This Pine Script(R) code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ // (c) FirmTape // // FirmTape Levels // // Draws FirmTape's dealer-gamma levels on your chart: the zero-gamma flip, call resistance, // put support, the open-interest flip and walls, and gamma by strike as bars at the right edge. // // These are MEASUREMENTS of dealer positioning, not trade signals. Nothing here tells you to // buy or sell. // // TradingView scripts cannot fetch data, so the levels come in as a text string. Get it from // the FirmTape terminal (LIVE plan): press "Copy levels" and paste the string into this // script's settings, "Levels string". One paste can hold ES, NQ, SPX, QQQ and more; the // script picks the block for the chart you are on: // ES1!, MES1!, ESZ2026 ... read ES NQ1!, MNQ1! ... read NQ // anything else reads the block with the same ticker (SPX on SPX, QQQ on QQQ). // "Symbol block" forces one. The table shows when the levels were taken and warns when they // are older than today: a pasted string is a snapshot and does not move by itself. // // String format FT1 (docs/levels-string.md in the integrations repository): // FT1;ES;ESZ26;2026-09-30T15:17;px=...;flip=...;cr=...;ps=...;oflip=...;ocr=...;ops=...; // ng=...;u=pt;s=o;b=...;gm=...;n=20;g=7700.25:-12,7705.25:4,... blocks joined by "|" //@version=6 indicator("FirmTape Levels", shorttitle = "FirmTape", overlay = true, max_lines_count = 20, max_labels_count = 20, max_boxes_count = 40) // ---------------------------------------------------------------- inputs const string G_SRC = "Levels" string i_str = input.text_area("", "Levels string", group = G_SRC, tooltip = "Paste the string from the FirmTape terminal, Copy levels (LIVE plan). It starts with FT1.") string i_sym = input.string("Auto", "Symbol block", group = G_SRC, tooltip = "Auto: ES1!/MES1! read ES, NQ1!/MNQ1! read NQ, other charts the block with the same ticker. Type a symbol (ES, NQ, SPX, QQQ) to force that block.") const string G_LVL = "Lines" color i_cFlip = input.color(color.new(#e0b000, 0), "Flip", group = G_LVL, inline = "f") color i_cCall = input.color(color.new(#26a69a, 0), "Call resistance", group = G_LVL, inline = "c") color i_cPut = input.color(color.new(#ef5350, 0), "Put support", group = G_LVL, inline = "p") int i_width = input.int(2, "Width", minval = 1, maxval = 4, group = G_LVL) string i_style = input.string("Solid", "Style", options = ["Solid", "Dashed", "Dotted"], group = G_LVL) bool i_oi = input.bool(false, "Show the open-interest book (dashed, lighter)", group = G_LVL) bool i_labels = input.bool(true, "Labels at the right", group = G_LVL) const string G_GAM = "Gamma by strike" bool i_gam = input.bool(true, "Show gamma by strike", group = G_GAM) int i_gOff = input.int(3, "Bars from the last bar", minval = 1, maxval = 200, group = G_GAM) int i_gWidth = input.int(30, "Longest bar, in bars", minval = 5, maxval = 250, group = G_GAM) color i_gPos = input.color(color.new(#26a69a, 45), "Positive", group = G_GAM, inline = "g") color i_gNeg = input.color(color.new(#ef5350, 45), "Negative", group = G_GAM, inline = "g") bool i_gText = input.bool(false, "Values on the bars", group = G_GAM) const string G_TAB = "Table" bool i_tab = input.bool(true, "Show the table", group = G_TAB) string i_tabPos = input.string("Top right", "Position", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = G_TAB) // ---------------------------------------------------------------- the parsed block type Block string sym = "" string contract = "" string asof = "" string err = "" float px = na float flip = na float cr = na float ps = na float oflip = na float ocr = na float ops = na float ng = na string unit = "" string sess = "" float basis = na string grade = "" float gm = na int n = -1 array gk array gv bool gbad = false string problem = "" // Remove the spaces and newlines a paste may pick up. clean(string s) => str.replace_all(str.replace_all(s, " ", ""), "\n", "") // One "FT1;..." block into a Block. Mirrors parseBlock() in test/levels_string.js. parseBlock(string raw) => Block b = Block.new(gk = array.new(), gv = array.new()) array parts = str.split(raw, ";") if array.size(parts) < 4 b.problem := "too short" else if array.get(parts, 0) != "FT1" b.problem := "unknown version " + array.get(parts, 0) + ": update the script" else b.sym := str.upper(array.get(parts, 1)) b.contract := array.get(parts, 2) == "-" ? "" : array.get(parts, 2) b.asof := array.get(parts, 3) == "-" ? "" : array.get(parts, 3) for [i, p] in parts if i >= 4 int eq = str.pos(p, "=") if not na(eq) and eq >= 1 string k = str.substring(p, 0, eq) string v = str.substring(p, eq + 1) if k == "px" b.px := str.tonumber(v) else if k == "flip" b.flip := str.tonumber(v) else if k == "cr" b.cr := str.tonumber(v) else if k == "ps" b.ps := str.tonumber(v) else if k == "oflip" b.oflip := str.tonumber(v) else if k == "ocr" b.ocr := str.tonumber(v) else if k == "ops" b.ops := str.tonumber(v) else if k == "ng" b.ng := str.tonumber(v) else if k == "u" b.unit := v else if k == "s" b.sess := v else if k == "b" b.basis := str.tonumber(v) else if k == "gr" b.grade := v else if k == "gm" b.gm := str.tonumber(v) else if k == "err" b.err := v else if k == "n" float nn = str.tonumber(v) b.n := na(nn) ? -1 : int(nn) else if k == "g" and v != "" for pr in str.split(v, ",") array kv = str.split(pr, ":") float sk = array.size(kv) == 2 ? str.tonumber(array.get(kv, 0)) : na float sv = array.size(kv) == 2 ? str.tonumber(array.get(kv, 1)) : na if not na(sk) and not na(sv) array.push(b.gk, sk) array.push(b.gv, sv) else b.gbad := true if b.err == "" if b.n < 0 b.problem := "block is cut off: copy the whole string again" else if b.gbad or b.n != array.size(b.gk) b.problem := "gamma list is cut off: copy the whole string again" array.clear(b.gk) array.clear(b.gv) b // The block this chart wants: override, then futures root, then the exact ticker. wantedSym() => string o = str.upper(clean(i_sym)) string r = str.upper(syminfo.root) string w = str.upper(syminfo.ticker) if o != "" and o != "AUTO" w := o else if syminfo.type == "futures" w := r == "ES" or r == "MES" ? "ES" : r == "NQ" or r == "MNQ" ? "NQ" : r else if w == "ES" or w == "NQ" // ES and NQ blocks are futures levels; a stock with that ticker (ES is Eversource) must not read them. w := "" w // The chart's dated contract as the API names it: ESH2027 or MESH2027 -> "ESH27" (micro // contracts read the full-size view). "" for a continuous chart (ES1!) and anything else. chartContract() => string t = str.upper(syminfo.ticker) string r = str.upper(syminfo.root) string v = r == "ES" or r == "MES" ? "ES" : r == "NQ" or r == "MNQ" ? "NQ" : "" string c = "" if syminfo.type == "futures" and v != "" and not str.endswith(t, "!") and str.length(t) >= str.length(r) + 3 string letter = str.substring(t, str.length(r), str.length(r) + 1) string yy = str.substring(t, str.length(t) - 2) if str.contains("FGHJKMNQUVXZ", letter) and not na(str.tonumber(yy)) c := v + letter + yy c // First day the API measures a contract: 22 days before its expiry, the third Friday of its // month. "ESH27" -> 2027-02-25 (UTC milliseconds). rollWindowStart(string c) => int m = str.pos("FGHJKMNQUVXZ", str.substring(c, 2, 3)) + 1 int y = 2000 + int(str.tonumber(str.substring(c, 3))) int first = timestamp("UTC", y, m, 1, 0, 0) // dayofweek: 1 is Sunday, 6 is Friday. int toFri = (7 + 6 - dayofweek(first, "UTC")) % 7 first + (toFri + 14 - 22) * 86400000 // The warning when the pasted block's contract is not the chart's (ESZ26 levels on an ESH2027 // chart are off by the calendar spread): before the chart's contract is measured, when it will // be; after, which symbol to pick in the terminal's Copy levels box. "" when they match or the // chart is continuous (ES1!), SPX or a stock. contractWarning(string contract) => string c = chartContract() string w = "" if c != "" and contract != "" and c != contract int start = rollWindowStart(c) w := timenow < start ? "levels are for " + contract + "; " + c + " is measured from " + str.format_time(start, "MMM d", "UTC") : "levels are for " + contract + "; for " + c + " pick " + c + " in Copy levels" w lineStyle() => i_style == "Dashed" ? line.style_dashed : i_style == "Dotted" ? line.style_dotted : line.style_solid tablePos() => i_tabPos == "Top left" ? position.top_left : i_tabPos == "Bottom right" ? position.bottom_right : i_tabPos == "Bottom left" ? position.bottom_left : position.top_right fmtLevel(float v) => str.tostring(v, "#.##") fmtAmount(float v, string unit) => str.tostring(v, "#.#") + (unit == "pct" ? "M$ per 1%" : unit == "pt" ? "M$ per pt" : "M$") // ---------------------------------------------------------------- drawing state var array gLines = array.new() var array