Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Change Log

## [0.9.3] - 2026-03-06 - Streaming Support, request.security Fixes, Transpiler Robustness

### Added

- **`array.new_box` / `new_label` / `new_line` / `new_linefill` / `new_table` / `new_color`**: Added the six missing typed array factory methods so `array<box>`, `array<label>`, etc. can be created with a proper element type. The auto-generator (`scripts/generate-array-index.js`) now lists them as static factory functions (called with context) rather than instance delegates. `isValueOfType` in `array/utils.ts` was extended to accept object values for these types, allowing `array.push(label.new(...))` on typed arrays.
- **`request.security` — Live Streaming Support**: `request.security` now correctly handles live (streaming) bar updates. The secondary context is re-evaluated on each tick, and `findSecContextIdx` resolves the correct intra-bar index for the current live bar. Paired with drawing-object rollback (see below), streaming ticks no longer produce duplicate drawing objects.
- **`str.tostring` Format Patterns**: Added support for Pine Script's named and pattern-based format strings: `"#"`, `"#.#"`, `"#.##"`, `"0.00"`, and the `format.*` named constants. The formatter now applies these patterns before falling back to `toString()`.

### Fixed

- **While-Loop Test Condition Hoisting** (infinite-loop crash): `array.size()` and similar calls in a `while` condition were being hoisted to a temp variable *outside* the loop by the default CallExpression walker, making them one-shot evaluations and causing an infinite loop followed by a crash. `MainTransformer` now registers a `WhileStatement` handler and `transformWhileStatement` was rewritten to use a recursive walker with hoisting suppressed throughout the entire test condition.
- **Array Pattern Scoping Crash**: `isArrayPatternVar` was determined using a global (non-scoped) set in `ScopeManager`. A local function variable whose name happened to match an outer-scope destructured tuple element was falsely treated as an array pattern, causing a runtime crash. Fixed by adding a shape guard: the flag is only set when `decl.init` is a computed `MemberExpression` (the `_tmp_0[0]` pattern produced by the AnalysisPass destructuring rewrite).
- **For-Loop Namespace Wrapping** (`math.min` → `$.get(math, 0).min`): In the for-loop test condition walker, `MemberExpression` nodes unconditionally recursed into their object, causing the `Identifier` handler to wrap context-bound namespace objects (`math`, `array`, `ta`, …) with `$.get()`. Fixed by skipping recursion and `addArrayAccess` for identifiers that are the object of a `MemberExpression` and are context-bound namespaces.
- **`request.security` Cross-Timeframe Value Alignment**: `barmerge.gaps_off` / `barmerge.lookahead_off` were passed as strings; their truthiness caused `findLTFContextIdx` to take the wrong branch (returning the first intra-bar instead of the last). Fixed by converting barmerge string enums to booleans. Added `normalizeTimeframe()` to map non-canonical formats (`'1h'`→`'60'`, `'1d'`→`'D'`) so `isLTF` determination is correct. Fixed secondary context date-range derivation to use `effectiveSDate` from `marketData` and extend `secEDate` to cover the last bar's intra-bars.
- **`barmerge` Missing from `CONTEXT_BOUND_VARS`**: `barmerge.gaps_off` / `barmerge.lookahead_off` (used in `request.security()`) were not in the transpiler's context-bound list, so they were left as bare identifiers instead of being mapped to the runtime context. Added `'barmerge'` to `settings.ts`.
- **`barstate.isconfirmed` Wrong Bar**: Was checking whether the last bar's close time equalled the session close via `closeTime[length-1]` (always the last bar in history) instead of the currently-executing bar. Fixed to use `closeTime.data[context.idx]` for correct per-bar evaluation.
- **`array.get()` Out-of-Bounds → NaN**: `array.get(arr, -1)` and other out-of-bounds accesses returned `undefined` (native JS), causing crashes when Pine Script code accessed properties (e.g., `.strength`) on the result. The method now returns `NaN` (Pine's `na`) for negative or out-of-range indices.
- **Drawing Helpers — `na` Color Resolution**: Drawing object helpers' `_resolve()` method now detects `NAHelper` instances and returns `NaN`, fixing cases where `border_color=na` (and similar `na` arguments) were silently ignored in `box.new()`, `line.new()`, etc. `BoxHelper` also gains a dedicated `_resolveColor()` that preserves `NaN` instead of letting it fall through an `||` fallback to the default color.
- **Streaming Rollback for Drawing Objects**: All five drawing types (`box`, `line`, `label`, `linefill`, `polyline`) now track a `_createdAtBar` property and expose a `rollbackFromBar(barIndex)` method. `Context.rollbackDrawings()` calls this during `_runPaginated` / `updateTail` to remove any drawing objects created on the current streaming bar before re-running it, preventing duplicate objects from accumulating across live ticks.

---

## [0.9.2] - 2026-03-06 - Drawing Object Method Syntax, Gradient Fill, Matrix & Array Improvements

### Added
Expand Down
58 changes: 29 additions & 29 deletions docs/api-coverage/pinescript-v6/types.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
"backadjustment.on": true
},
"barmerge": {
"barmerge.gaps_off": false,
"barmerge.gaps_on": false,
"barmerge.lookahead_off": false,
"barmerge.lookahead_on": false
"barmerge.gaps_off": true,
"barmerge.gaps_on": true,
"barmerge.lookahead_off": true,
"barmerge.lookahead_on": true
},
"currency": {
"currency.AED": true,
Expand Down Expand Up @@ -113,14 +113,14 @@
"display.status_line": true
},
"extend": {
"extend.both": false,
"extend.left": false,
"extend.none": false,
"extend.right": false
"extend.both": true,
"extend.left": true,
"extend.none": true,
"extend.right": true
},
"font": {
"font.family_default": false,
"font.family_monospace": false
"font.family_default": true,
"font.family_monospace": true
},
"format": {
"format.inherit": true,
Expand Down Expand Up @@ -162,15 +162,15 @@
"plot.style_steplinebr": true
},
"position": {
"position.bottom_center": false,
"position.bottom_left": false,
"position.bottom_right": false,
"position.middle_center": false,
"position.middle_left": false,
"position.middle_right": false,
"position.top_center": false,
"position.top_left": false,
"position.top_right": false
"position.bottom_center": true,
"position.bottom_left": true,
"position.bottom_right": true,
"position.middle_center": true,
"position.middle_left": true,
"position.middle_right": true,
"position.top_center": true,
"position.top_left": true,
"position.top_right": true
},
"scale": {
"scale.left": false,
Expand Down Expand Up @@ -210,23 +210,23 @@
},
"text": {
"text.align_bottom": false,
"text.align_center": false,
"text.align_left": false,
"text.align_right": false,
"text.align_center": true,
"text.align_left": true,
"text.align_right": true,
"text.align_top": false,
"text.format_bold": false,
"text.format_italic": false,
"text.format_none": false,
"text.wrap_auto": false,
"text.wrap_none": false
"text.wrap_auto": true,
"text.wrap_none": true
},
"xloc": {
"xloc.bar_index": false,
"xloc.bar_time": false
"xloc.bar_index": true,
"xloc.bar_time": true
},
"yloc": {
"yloc.abovebar": false,
"yloc.belowbar": false,
"yloc.price": false
"yloc.abovebar": true,
"yloc.belowbar": true,
"yloc.price": true
}
}
58 changes: 29 additions & 29 deletions docs/api-coverage/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ parent: API Coverage

| Function | Status | Description |
| -------------- | ------ | ------------ |
| `extend.both` | | Extend both |
| `extend.left` | | Extend left |
| `extend.none` | | Extend none |
| `extend.right` | | Extend right |
| `extend.both` | | Extend both |
| `extend.left` | | Extend left |
| `extend.none` | | Extend none |
| `extend.right` | | Extend right |

### Font

| Function | Status | Description |
| ----------------------- | ------ | --------------------- |
| `font.family_default` | | Default font family |
| `font.family_monospace` | | Monospace font family |
| `font.family_default` | | Default font family |
| `font.family_monospace` | | Monospace font family |

### Format

Expand Down Expand Up @@ -165,15 +165,15 @@ parent: API Coverage

| Function | Status | Description |
| ------------------------ | ------ | ---------------------- |
| `position.bottom_center` | | Bottom center position |
| `position.bottom_left` | | Bottom left position |
| `position.bottom_right` | | Bottom right position |
| `position.middle_center` | | Middle center position |
| `position.middle_left` | | Middle left position |
| `position.middle_right` | | Middle right position |
| `position.top_center` | | Top center position |
| `position.top_left` | | Top left position |
| `position.top_right` | | Top right position |
| `position.bottom_center` | | Bottom center position |
| `position.bottom_left` | | Bottom left position |
| `position.bottom_right` | | Bottom right position |
| `position.middle_center` | | Middle center position |
| `position.middle_left` | | Middle left position |
| `position.middle_right` | | Middle right position |
| `position.top_center` | | Top center position |
| `position.top_left` | | Top left position |
| `position.top_right` | | Top right position |

### Scale

Expand Down Expand Up @@ -231,30 +231,30 @@ parent: API Coverage
| Function | Status | Description |
| -------------------- | ------ | --------------------- |
| `text.align_bottom` | | Bottom text alignment |
| `text.align_center` | | Center text alignment |
| `text.align_left` | | Left text alignment |
| `text.align_right` | | Right text alignment |
| `text.align_center` | | Center text alignment |
| `text.align_left` | | Left text alignment |
| `text.align_right` | | Right text alignment |
| `text.align_top` | | Top text alignment |
| `text.format_bold` | | Bold text format |
| `text.format_italic` | | Italic text format |
| `text.format_none` | | No text format |
| `text.wrap_auto` | | Auto text wrap |
| `text.wrap_none` | | No text wrap |
| `text.wrap_auto` | | Auto text wrap |
| `text.wrap_none` | | No text wrap |

### Xloc

| Function | Status | Description |
| ---------------- | ------ | -------------------- |
| `xloc.bar_index` | | Bar index x-location |
| `xloc.bar_time` | | Bar time x-location |
| `xloc.bar_index` | | Bar index x-location |
| `xloc.bar_time` | | Bar time x-location |

### Yloc

| Function | Status | Description |
| --------------- | ------ | -------------------- |
| `yloc.abovebar` | | Above bar y-location |
| `yloc.belowbar` | | Below bar y-location |
| `yloc.price` | | Price y-location |
| `yloc.abovebar` | | Above bar y-location |
| `yloc.belowbar` | | Below bar y-location |
| `yloc.price` | | Price y-location |

### Dividends

Expand Down Expand Up @@ -306,7 +306,7 @@ parent: API Coverage

| Function | Status | Description |
| ------------------------ | ------ | ------------- |
| `barmerge.gaps_off` | | Gaps off |
| `barmerge.gaps_on` | | Gaps on |
| `barmerge.lookahead_off` | | Lookahead off |
| `barmerge.lookahead_on` | | Lookahead on |
| `barmerge.gaps_off` | | Gaps off |
| `barmerge.gaps_on` | | Gaps on |
| `barmerge.lookahead_off` | | Lookahead off |
| `barmerge.lookahead_on` | | Lookahead on |
2 changes: 1 addition & 1 deletion scripts/generate-array-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function generateIndex() {
return name === 'new' ? { file: name, export: 'new_fn', classProp: 'new' } : { file: name, export: name, classProp: name };
});

const staticMethods = ['new', 'new_bool', 'new_float', 'new_int', 'new_string', 'from', 'param'];
const staticMethods = ['new', 'new_bool', 'new_box', 'new_color', 'new_float', 'new_int', 'new_label', 'new_line', 'new_linefill', 'new_string', 'new_table', 'from', 'param'];

// --- Generate PineArrayObject.ts ---
const objectMethods = methods.filter((m) => !staticMethods.includes(m.classProp));
Expand Down
17 changes: 17 additions & 0 deletions src/Context.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ export class Context {
public cache: any = {};
public taState: any = {}; // State for incremental TA calculations
public isSecondaryContext: boolean = false; // Flag to prevent infinite recursion in request.security
public dataVersion: number = 0; // Incremented when market data changes (streaming mode)

public NA: any = NaN;

public lang: any;
public length: number = 0;

/** References to drawing helpers for streaming rollback */
public _drawingHelpers: { rollbackFromBar(barIdx: number): void }[] = [];

// Combined namespace and core functions - the default way to access everything
public pine: {
// input: Input;
Expand Down Expand Up @@ -416,6 +420,9 @@ export class Context {
get: () => polylineHelper.all,
});

// Register drawing helpers for streaming rollback
this._drawingHelpers = [labelHelper, lineHelper, boxHelper, linefillHelper, polylineHelper];

// table namespace
const tableHelper = new TableHelper(this);
this.bindContextObject(
Expand Down Expand Up @@ -452,6 +459,16 @@ export class Context {
});
}

/**
* Roll back all drawing objects created at or after the given bar index.
* Called during streaming updates to prevent accumulation when bars are re-processed.
*/
rollbackDrawings(fromBarIdx: number): void {
for (const helper of this._drawingHelpers) {
helper.rollbackFromBar(fromBarIdx);
}
}

private bindContextObject(instance: any, entries: string[], root: string = '') {
if (root && !this.pine[root]) this.pine[root] = {};

Expand Down
32 changes: 31 additions & 1 deletion src/PineTS.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,20 @@ export class PineTS {
continue;
}

// #4: Always recalculate last candle + new ones
// #4: Data changed — bump version so secondary contexts know to refresh
context.dataVersion++;

// Always recalculate last candle + new ones
// Remove last result (will be recalculated with fresh data)
this._removeLastResult(context);

// Step back one position to reprocess last candle
processedUpToIdx = this.data.length - (newCandles + 1);

// Roll back drawing objects created during the previous processing of
// these bars so they don't accumulate on each streaming tick.
context.rollbackDrawings(processedUpToIdx);

// Next iteration of loop will process from updated position (#1)

//barstate.isnew becomes false on live bars
Expand Down Expand Up @@ -503,6 +510,29 @@ export class PineTS {
this.closeTime.push(candle.closeTime);
}

/**
* Update the secondary context's tail with fresh market data.
* Mirrors the streaming update logic in _runPaginated:
* fetches new/updated candles, rolls back the last result, and re-executes
* only the affected bars.
* @param context - The cached secondary context to update
* @returns true if data was updated, false if no changes
*/
public async updateTail(context: Context): Promise<boolean> {
// Guard: skip if no data (e.g. secondary context failed to load from provider)
if (this.data.length === 0 || Array.isArray(this.source)) return false;

const { newCandles, updatedLastCandle } = await this._updateMarketData();
if (newCandles === 0 && !updatedLastCandle) return false;

this._removeLastResult(context);
context.length = this.data.length;
const processFrom = this.data.length - (newCandles + 1);
context.rollbackDrawings(processFrom);
await this._executeIterations(context, this._transpiledCode as Function, processFrom, this.data.length);
return true;
}

/**
* Remove the last result from context (for updating an open candle)
* @private
Expand Down
14 changes: 10 additions & 4 deletions src/namespaces/Barstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ export class Barstate {
}

public get isconfirmed() {
return this.context.data.closeTime[this.context.data.closeTime.length - 1] <= new Date().getTime();
// Check if the CURRENT bar (not the last bar) has closed.
// Historical bars are always confirmed; only the live bar is unconfirmed.
// closeTime is a Series object — access .data[] for raw array indexing.
const closeTime = this.context.data.closeTime.data[this.context.idx];
return closeTime <= Date.now();
}

public get islastconfirmedhistory() {
//FIXME : this is a temporary solution to get the islastconfirmedhistory value,
//we need to implement a better way to handle it based on market data
return this.context.data.closeTime[this.context.data.closeTime.length - 1] <= new Date().getTime();
// True when this is the last bar whose close time is in the past
// (the bar right before the current live bar).
const closeTime = this.context.data.closeTime.data[this.context.idx];
const nextCloseTime = this.context.data.closeTime.data[this.context.idx + 1];
return closeTime <= Date.now() && (nextCloseTime === undefined || nextCloseTime > Date.now());
}
}
Loading