# Pine To Typescript Port

> 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.

- Skill: `traderspost/pine-to-typescript-port` (Agent Skill)
- Install (CLI): `npx skillmds@latest add traderspost/pine-to-typescript-port`
- Raw SKILL.md: https://api.skillmd.com/api/skills/traderspost/pine-to-typescript-port/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: traderspost (https://skillmd.com/u/traderspost)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/traderspost/pine-to-typescript-port

---


# 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.

```ts
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_index` vs time.** Pine's `bar_index` is 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.regular` semantics.** Pine knows about RTH (regular trading hours) vs ETH (extended). If the indicator uses session filtering, you need a `bar.in_rth` flag (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.ts` module with `computeAnchoredVwap(bars, settings)` returning the value series + per-bar metadata (anchor-close flag, mitigation touch state)
- A `level-custom-series.ts` `ICustomSeriesPaneView` that consumes the metadata and draws everything in one Canvas2D pass (with `isWhitespace` returning false; `Number.isFinite` guards inside `draw()`)
- 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 targets
- `cme-futures-time-buckets` — if the indicator anchors to D/W/M, the bucket math has to respect futures sessions, not calendar boundaries
- `browser-verify-ui-changes` — the right verification for "does my port match the Pine?"

