name: pine-to-typescript-port description: Use when porting a TradingView Pine Script indicator (anchored VWAP, ATR bands, RSI divergence, custom Level, etc.) to TypeScript for a lightweight-charts based app. Pine is a bar-by-bar series-oriented DSL with specific semantics — anchor-reset, ohlc4, dotted vs dashed, color.new, plot vs hline, request.security() — that don't translate one-to-one to vanilla TS. This skill names the Pine concepts, their TS equivalents, and the gotchas that cost iterations. metadata: category: charting
Porting Pine Script to TypeScript
Pine Script is TradingView's bar-by-bar series DSL. It has powerful primitives (anchor-reset, request.security(), plot() overloads, color helpers like color.new()) that compile to TradingView's renderer. When porting a Pine indicator to a custom TypeScript app — typically with lightweight-charts as the renderer — naive translation produces "looks like Pine but is wrong" output. The semantics need to be ported, not just the syntax.
This skill catalogs the non-obvious concept mappings learned while porting an Anchored-VWAP / Level indicator (with std-dev bands, mitigation lines, zone boxes, and per-anchor labels).
When to use
Reach for this skill if you're:
- Porting any Pine indicator to TypeScript (or any non-Pine renderer)
- Implementing "Anchored VWAP" / "Level" / "Session VWAP" / similar
- Trying to match a Pine indicator's output pixel-for-pixel
- Confused why your TS port "looks 90% right but X is wrong"
The pattern — concept-by-concept mapping
1. Bar series + anchor-reset
Pine's signature trick: var float cum_pv = 0.0 with an if anchor_changed : cum_pv := 0.0 resets the cumulator at each anchor boundary (new day, new session, new week, etc.). Each anchor period builds its own VWAP from scratch.
In TS: walk the bars once, detect anchor boundaries, reset cumulators. Use functions like localDayKey() to detect "this bar is in a different day from the previous" — relative to the right timezone.
function anchorKey(unixSeconds: number, anchor: AnchorTf, tz: string): string {
// E.g. "D" → date key in NY-local; "W" → ISO week key; etc.
// For CME futures, use a FUTURES_DAY_SHIFT to make the 18:00 ET
// session boundary land on a calendar-day flip.
}
let cumPV = 0, cumV = 0, prevKey: string | null = null;
for (const bar of bars) {
const key = anchorKey(bar.time, anchor, tz);
if (key !== prevKey) { cumPV = 0; cumV = 0; }
const typical = (bar.open + bar.high + bar.low + bar.close) / 4; // ohlc4
cumPV += typical * bar.volume;
cumV += bar.volume;
vwap[i] = cumPV / cumV;
prevKey = key;
}
2. ohlc4 and other Pine source helpers
Pine has close, open, high, low, hl2, hlc3, ohlc4, volume. These map directly:
| Pine | TypeScript |
|---|---|
close |
bar.close |
hl2 |
(bar.high + bar.low) / 2 |
hlc3 |
(bar.high + bar.low + bar.close) / 3 |
ohlc4 |
(bar.open + bar.high + bar.low + bar.close) / 4 |
Anchored VWAP almost always uses ohlc4 as the typical price. If the Pine source says close, port it as close — don't "improve" it by switching to ohlc4.
3. Standard deviation bands
Pine's anchored-VWAP-with-bands indicators use a running variance:
Var = E[X²] − (E[X])²
= cum_pv2 / cum_vol − vwap²
The trick is the second moment cumulator: cumPV2 += typical * typical * volume. Then sd = sqrt(cumPV2 / cumV - vwap*vwap) gives bands at vwap ± mult * sd. This is numerically more efficient than re-walking bars to compute variance.
4. plot() vs hline() vs custom series
Pine has multiple drawing primitives:
| Pine call | What it draws | lightweight-charts equivalent |
|---|---|---|
plot(value, style=plot.style_line) |
Per-bar series | ISeriesApi<"Line"> or a custom series for multi-line |
plot(value, style=plot.style_stepline) |
Stepped line | Custom series renderer; v5 lacks built-in |
hline(value) |
Horizontal at a fixed price | IPriceLine via series.createPriceLine({...}) |
bgcolor() |
Background tint | Custom series renderer; v5 lacks built-in bgcolor |
label.new() |
Anchored text label | DOM overlay div positioned via timeScale.timeToCoordinate |
box.new() |
Rectangle (zone box) | Custom series renderer (canvas rect) OR DOM overlay div |
For indicators with MANY plot primitives per anchor (e.g. drawing ~60 segments per anchor), using one chart series per segment is slow. Roll them into a single ICustomSeriesPaneView that draws everything in one Canvas2D pass. See lightweight-charts-integration.
5. Color helpers
| Pine | TypeScript / CSS |
|---|---|
color.new(c, transp) where transp is 0-100 |
withAlpha(c, 1 - transp/100) — Pine inverts the alpha convention! |
color.green / color.red |
Hex from your design tokens, NOT a name |
color.from_gradient(...) |
Compute a hex blend at the right step |
The transparency inversion is non-obvious. Pine's color.new(c, 85) means "85% transparent" → output alpha = 0.15. JS's typical rgba(..., 0.85) means "85% opaque" → output alpha = 0.85. Read the Pine source carefully; port to your withAlphaCss(color, 1 - pineTransp/100).
6. request.security() — higher-timeframe context
Pine's request.security(syminfo.tickerid, "D", close) fetches the daily close while running on a 5m chart. There's no direct TS equivalent — you need a separate data fetch for the higher TF, joined to your bars by time.
For an Anchored-VWAP that's anchored to daily/weekly anchors, you DON'T need request.security() — you can detect the anchor from the bar timestamps themselves (anchorKey(bar.time, "D", tz) returns a date key). Only reach for the dual-data-source pattern if the indicator computes something on the HTF and overlays on the LTF.
7. var vs regular assignment
Pine's var float cumulator = 0.0 declares a persistent-across-bars variable. Regular cumulator = 0.0 reassigns on every bar. In TS this is the difference between a let outside the loop (persistent) vs inside (per-iteration). Don't confuse the two.
8. na (not-available) handling
Pine has a first-class na value distinct from 0. TS doesn't. The right port is NaN for numeric "no value," and the chart-side renderer needs Number.isFinite() checks before drawing. See lightweight-charts-integration for the isWhitespace gotcha — DON'T strip NaN rows from data.bars; let the renderer skip them at draw time.
9. "Same code path in live + backtest + ML"
If your indicator runs in production, in backtests, and in ML feature pipelines, the same code path must produce the same numbers in all three. Don't fork — write the core computation once and call it from each context.
Workflow — the right port sequence
1. Read the Pine source. ALL OF IT. (See read-reference-source-first.)
Use mcp__pinescript__pine_search / pine_reference / pine_examples if
the source uses any unfamiliar built-ins.
2. Annotate the Pine. Comment in the TS port file naming the Pine
lines you're translating from. Easier to verify line-by-line.
3. Translate the math. Start with the cumulator-reset loop and the
value computations. Skip styling for now.
4. Translate the drawing. Map plot/hline/box.new/label.new to your
renderer's primitives. Pick "one custom series per indicator
instance" if drawing > ~10 primitives per bar.
5. Translate the styling. Colors, line widths, dash patterns, alpha
conventions. Pine's transp=85 ≠ alpha=0.85.
6. Browser-verify against the Pine reference. Take a screenshot of
the Pine indicator on TradingView. Take a screenshot of your TS
port. Diff visually. ANY divergence is a bug to find.
Gotchas
- Color transparency inversion. Pine
color.new(c, 85)= 15% opaque. Easy to miss. bar_indexvs time. Pine'sbar_indexis the sequential bar number. Your TS chart probably uses time. Don't confuse them in width calculations.time()returns ms in Pine, but your bars probably use seconds. Watch for the 1000× factor.syminfo.session.regularsemantics. Pine knows about RTH (regular trading hours) vs ETH (extended). If the indicator uses session filtering, you need abar.in_rthflag (computed from the bar's NY-local time vs the symbol's RTH window) before you can port.- Pine's stepped-line vs lightweight-charts. Pine's
plot(..., style=plot.style_stepline)renders a step. lightweight-charts has no built-in step series; you draw it in a custom series. - Don't over-engineer. Many Pine indicators have decorations (label boxes, info tables, alert conditions) that aren't core to the math. Implement the math first, ship a minimal renderer, add decorations iteratively.
- Mitigation / touched-state. Many Pine indicators track "has price touched this level since the anchor closed?" via a per-anchor flag. Port this state EXPLICITLY — don't let it emerge from re-walking the bars in the renderer.
Reference implementation
The mapping above was extracted while porting an Anchored-VWAP "Level" indicator (multi-anchor: 15m, 2H, D, W, M, Q) with std-dev bands, mitigation horizontal lines, zone boxes, and per-anchor text labels. The eventual TS shape:
- A pure
anchored-vwap.tsmodule withcomputeAnchoredVwap(bars, settings)returning the value series + per-bar metadata (anchor-close flag, mitigation touch state) - A
level-custom-series.tsICustomSeriesPaneViewthat consumes the metadata and draws everything in one Canvas2D pass (withisWhitespacereturning false;Number.isFiniteguards insidedraw()) - A separate DOM overlay layer for the few primitives that don't fit in a canvas series (text labels positioned via
timeToCoordinate) - Parity tests against known Pine values for a handful of anchor windows
Related skills
read-reference-source-first— read the Pine source FIRST. Multiple times. This is THE highest-leverage step.lightweight-charts-integration— the chart-side primitives (custom series, time domains, isWhitespace) the ported indicator targetscme-futures-time-buckets— if the indicator anchors to D/W/M, the bucket math has to respect futures sessions, not calendar boundariesbrowser-verify-ui-changes— the right verification for "does my port match the Pine?"