Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 30, 2025

Adds PineScript v6 input functions and built-in variables to the oakscript-engine transpiler, enabling customizable indicators with user-configurable parameters.

Input Functions

  • input.int(), input.float(), input.bool(), input.string(), input.color(), input.source()
  • Generates TypeScript IndicatorInputs interface with proper types
  • Union types for options arrays in input.string()
  • Function signature updated to accept Partial<IndicatorInputs>
length = input.int(14, "Length", minval=1)
maType = input.string("SMA", "Type", options=["SMA", "EMA", "WMA"])
export interface IndicatorInputs {
  length: number;
  maType: "SMA" | "EMA" | "WMA";
}
export function MyIndicator(bars: any[], inputs: Partial<IndicatorInputs> = {})

Built-in Variables

  • Price sources: hl2, hlc3, ohlc4, hlcc4 as Series calculations
  • Time series: year, month, dayofmonth, dayofweek, hour, minute
  • Bar state: barstate.isfirst(i === 0), barstate.islast(i === bars.length - 1)
  • Symbol info: syminfo.ticker, syminfo.mintick, etc. (generates interface when used)
  • Timeframe: timeframe.period, timeframe.isintraday, etc.

Helper Functions

  • na() and nz() generated as local helpers in output

Parser

  • Added ArrayLiteral node type for parsing [...] in options parameter
Original prompt

Transpiler Phase 2: Input System & Built-in Variables

Objective

Enhance the oakscript-engine transpiler to support PineScript v6 input functions and built-in variables, enabling customizable indicators with user-configurable parameters.

Context

Phase 1 has been merged and the transpiler now supports:

  • ✅ Basic expressions & operators
  • ✅ Variable declarations (var, := reassignment)
  • if/else blocks, for/while loops, switch expressions
  • ✅ History operator [n], tuple destructuring, break/continue

Features to Implement

1. Input Functions (Critical)

input.int()

length = input.int(14, "Length", minval=1, maxval=100)
period = input.int(title="Period", defval=20)

input.float()

multiplier = input.float(2.0, "Multiplier", minval=0.1, maxval=10.0, step=0.1)

input.bool()

showMA = input.bool(true, "Show Moving Average")

input.string()

maType = input.string("SMA", "MA Type", options=["SMA", "EMA", "WMA", "HMA"])

input.color()

bullColor = input.color(color.green, "Bull Color")

input.source()

src = input.source(close, "Source")

TypeScript output - generate interface:

export interface IndicatorInputs {
  length: number;
  multiplier: number;
  showMA: boolean;
  maType: "SMA" | "EMA" | "WMA" | "HMA";
  bullColor: string;
  src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4";
}

const defaultInputs: IndicatorInputs = {
  length: 14,
  multiplier: 2.0,
  showMA: true,
  maType: "SMA",
  bullColor: "#00FF00",
  src: "close",
};

export function MyIndicator(bars: Bar[], inputs: Partial<IndicatorInputs> = {}): IndicatorResult {
  const { length, multiplier, showMA, maType, bullColor, src } = { ...defaultInputs, ...inputs };
  // ...
}

2. Calculated Price Sources

hl2    // (high + low) / 2
hlc3   // (high + low + close) / 3
ohlc4  // (open + high + low + close) / 4
hlcc4  // (high + low + close + close) / 4

TypeScript output:

const hl2 = high.add(low).div(2);
const hlc3 = high.add(low).add(close).div(3);
const ohlc4 = open.add(high).add(low).add(close).div(4);
const hlcc4 = high.add(low).add(close).add(close).div(4);

3. bar_index and barstate.* Variables

if barstate.isfirst
    // First bar logic
if barstate.islast
    // Last bar logic
position = bar_index

TypeScript output:

const bar_index = i;  // current index
const last_bar_index = bars.length - 1;
const barstate = {
  isfirst: bar_index === 0,
  islast: bar_index === bars.length - 1,
  isconfirmed: true,
  islastconfirmedhistory: bar_index === bars.length - 1,
  isrealtime: false,
  isnew: true,
};

4. Time Variables

y = year
m = month
d = dayofmonth
dow = dayofweek
h = hour

TypeScript output:

const year = new Series(bars, (bar) => new Date(bar.time).getFullYear());
const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1);
const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate());
const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1);
const hour = new Series(bars, (bar) => new Date(bar.time).getHours());
const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes());

5. Symbol Info Variables

ticker = syminfo.ticker
mintick = syminfo.mintick
currency = syminfo.currency

TypeScript output:

export interface SymbolInfo {
  ticker: string;
  tickerid: string;
  currency: string;
  mintick: number;
  pointvalue: number;
  type: string;
}

const defaultSyminfo: SymbolInfo = {
  ticker: "UNKNOWN",
  tickerid: "UNKNOWN", 
  currency: "USD",
  mintick: 0.01,
  pointvalue: 1,
  type: "stock",
};

export function MyIndicator(
  bars: Bar[], 
  inputs?: Partial<IndicatorInputs>,
  syminfo?: Partial<SymbolInfo>
): IndicatorResult {
  const symbolInfo = { ...defaultSyminfo, ...syminfo };
  // ...
}

6. Timeframe Variables

tf = timeframe.period
isIntraday = timeframe.isintraday
isDaily = timeframe.isdaily

TypeScript output:

export interface TimeframeInfo {
  period: string;
  multiplier: number;
  isintraday: boolean;
  isdaily: boolean;
  isweekly: boolean;
  ismonthly: boolean;
}

7. na() and nz() Functions (Critical)

if na(close[100])
    // Handle missing data
safeValue = nz(value)
safeValue = nz(value, -1)

TypeScript output:

function na(value: number | null | undefined): boolean {
  return value === null || value === undefined || Number.isNaN(value);
}

function nz(value: number, replacement: number = 0): number {
  return na(value) ? replacement : value;
}

Test Cases

test-inputs.pine

//@version=6
indicator("Input Test", overlay=true)

length = input.int(14, "Length", minval=1, maxval=...

</details>

*This pull request was created as a result of the following prompt from Copilot chat.*
> # Transpiler Phase 2: Input System & Built-in Variables
> 
> ## Objective
> Enhance the oakscript-engine transpiler to support PineScript v6 input functions and built-in variables, enabling customizable indicators with user-configurable parameters.
> 
> ## Context
> Phase 1 has been merged and the transpiler now supports:
> - ✅ Basic expressions & operators
> - ✅ Variable declarations (`var`, `:=` reassignment)
> - ✅ `if`/`else` blocks, `for`/`while` loops, `switch` expressions
> - ✅ History operator `[n]`, tuple destructuring, `break`/`continue`
> 
> ## Features to Implement
> 
> ### 1. Input Functions (Critical)
> 
> #### `input.int()`
> ```pinescript
> length = input.int(14, "Length", minval=1, maxval=100)
> period = input.int(title="Period", defval=20)
> ```
> 
> #### `input.float()`
> ```pinescript
> multiplier = input.float(2.0, "Multiplier", minval=0.1, maxval=10.0, step=0.1)
> ```
> 
> #### `input.bool()`
> ```pinescript
> showMA = input.bool(true, "Show Moving Average")
> ```
> 
> #### `input.string()`
> ```pinescript
> maType = input.string("SMA", "MA Type", options=["SMA", "EMA", "WMA", "HMA"])
> ```
> 
> #### `input.color()`
> ```pinescript
> bullColor = input.color(color.green, "Bull Color")
> ```
> 
> #### `input.source()`
> ```pinescript
> src = input.source(close, "Source")
> ```
> 
> **TypeScript output - generate interface:**
> ```typescript
> export interface IndicatorInputs {
>   length: number;
>   multiplier: number;
>   showMA: boolean;
>   maType: "SMA" | "EMA" | "WMA" | "HMA";
>   bullColor: string;
>   src: "open" | "high" | "low" | "close" | "hl2" | "hlc3" | "ohlc4";
> }
> 
> const defaultInputs: IndicatorInputs = {
>   length: 14,
>   multiplier: 2.0,
>   showMA: true,
>   maType: "SMA",
>   bullColor: "#00FF00",
>   src: "close",
> };
> 
> export function MyIndicator(bars: Bar[], inputs: Partial<IndicatorInputs> = {}): IndicatorResult {
>   const { length, multiplier, showMA, maType, bullColor, src } = { ...defaultInputs, ...inputs };
>   // ...
> }
> ```
> 
> ### 2. Calculated Price Sources
> ```pinescript
> hl2    // (high + low) / 2
> hlc3   // (high + low + close) / 3
> ohlc4  // (open + high + low + close) / 4
> hlcc4  // (high + low + close + close) / 4
> ```
> 
> **TypeScript output:**
> ```typescript
> const hl2 = high.add(low).div(2);
> const hlc3 = high.add(low).add(close).div(3);
> const ohlc4 = open.add(high).add(low).add(close).div(4);
> const hlcc4 = high.add(low).add(close).add(close).div(4);
> ```
> 
> ### 3. `bar_index` and `barstate.*` Variables
> ```pinescript
> if barstate.isfirst
>     // First bar logic
> if barstate.islast
>     // Last bar logic
> position = bar_index
> ```
> 
> **TypeScript output:**
> ```typescript
> const bar_index = i;  // current index
> const last_bar_index = bars.length - 1;
> const barstate = {
>   isfirst: bar_index === 0,
>   islast: bar_index === bars.length - 1,
>   isconfirmed: true,
>   islastconfirmedhistory: bar_index === bars.length - 1,
>   isrealtime: false,
>   isnew: true,
> };
> ```
> 
> ### 4. Time Variables
> ```pinescript
> y = year
> m = month
> d = dayofmonth
> dow = dayofweek
> h = hour
> ```
> 
> **TypeScript output:**
> ```typescript
> const year = new Series(bars, (bar) => new Date(bar.time).getFullYear());
> const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1);
> const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate());
> const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1);
> const hour = new Series(bars, (bar) => new Date(bar.time).getHours());
> const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes());
> ```
> 
> ### 5. Symbol Info Variables
> ```pinescript
> ticker = syminfo.ticker
> mintick = syminfo.mintick
> currency = syminfo.currency
> ```
> 
> **TypeScript output:**
> ```typescript
> export interface SymbolInfo {
>   ticker: string;
>   tickerid: string;
>   currency: string;
>   mintick: number;
>   pointvalue: number;
>   type: string;
> }
> 
> const defaultSyminfo: SymbolInfo = {
>   ticker: "UNKNOWN",
>   tickerid: "UNKNOWN", 
>   currency: "USD",
>   mintick: 0.01,
>   pointvalue: 1,
>   type: "stock",
> };
> 
> export function MyIndicator(
>   bars: Bar[], 
>   inputs?: Partial<IndicatorInputs>,
>   syminfo?: Partial<SymbolInfo>
> ): IndicatorResult {
>   const symbolInfo = { ...defaultSyminfo, ...syminfo };
>   // ...
> }
> ```
> 
> ### 6. Timeframe Variables
> ```pinescript
> tf = timeframe.period
> isIntraday = timeframe.isintraday
> isDaily = timeframe.isdaily
> ```
> 
> **TypeScript output:**
> ```typescript
> export interface TimeframeInfo {
>   period: string;
>   multiplier: number;
>   isintraday: boolean;
>   isdaily: boolean;
>   isweekly: boolean;
>   ismonthly: boolean;
> }
> ```
> 
> ### 7. `na()` and `nz()` Functions (Critical)
> ```pinescript
> if na(close[100])
>     // Handle missing data
> safeValue = nz(value)
> safeValue = nz(value, -1)
> ```
> 
> **TypeScript output:**
> ```typescript
> function na(value: number | null | undefined): boolean {
>   return value === null || value === undefined || Number.isNaN(value);
> }
> 
> function nz(value: number, replacement: number = 0): number {
>   return na(value) ? replacement : value;
> }
> ```
> 
> ## Test Cases
> 
> ### test-inputs.pine
> ```pinescript
> //@version=6
> indicator("Input Test", overlay=true)
> 
> length = input.int(14, "Length", minval=1, maxval=200)
> multiplier = input.float(2.0, "Multiplier")
> showBands = input.bool(true, "Show Bands")
> maType = input.string("EMA", "MA Type", options=["SMA", "EMA", "WMA"])
> src = input.source(close, "Source")
> 
> ma = switch maType
>     "SMA" => ta.sma(src, length)
>     "EMA" => ta.ema(src, length)
>     "WMA" => ta.wma(src, length)
> 
> plot(ma)
> ```
> 
> ### test-barstate.pine
> ```pinescript
> //@version=6
> indicator("Barstate Test")
> 
> var float firstClose = na
> if barstate.isfirst
>     firstClose := close
> 
> plot(firstClose)
> plot(bar_index)
> ```
> 
> ### test-time.pine
> ```pinescript
> //@version=6
> indicator("Time Test")
> 
> isMonday = dayofweek == 2
> isMarketOpen = hour >= 9 and hour < 16
> 
> bgcolor(isMonday ? color.new(color.blue, 90) : na)
> plot(hour)
> ```
> 
> ## Files to Modify
> 
> 1. `packages/oakscript-engine/src/transpiler/PineParser.ts` - Parse input.* functions
> 2. `packages/oakscript-engine/src/transpiler/PineToTS.ts` - Generate interfaces, handle built-ins
> 3. `packages/oakscript-engine/src/transpiler/types.ts` - Add new types
> 4. `packages/oakscript-engine/tests/` - Add test cases
> 
> ## Success Criteria
> 
> 1. All input types parse and generate correct TypeScript interfaces
> 2. Default values extracted correctly
> 3. Built-in variables (barstate, syminfo, timeframe, time) work
> 4. `na()` and `nz()` functions work correctly
> 5. Generated code compiles and runs correctly

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 2 commits November 30, 2025 21:47
Co-authored-by: deepentropy <8287111+deepentropy@users.noreply.github.com>
Co-authored-by: deepentropy <8287111+deepentropy@users.noreply.github.com>
Copilot AI changed the title [WIP] Enhance transpiler to support PineScript input functions Transpiler Phase 2: Input System & Built-in Variables Nov 30, 2025
Copilot AI requested a review from deepentropy November 30, 2025 21:54
@deepentropy deepentropy marked this pull request as ready for review November 30, 2025 22:03
Copilot AI review requested due to automatic review settings November 30, 2025 22:03
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@deepentropy deepentropy merged commit c3d8bd2 into main Nov 30, 2025
6 checks passed
@deepentropy deepentropy deleted the copilot/add-input-functions-support branch November 30, 2025 22:04
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Phase 2 of the PineScript transpiler, adding support for input functions and built-in variables to enable user-configurable indicator parameters. The implementation adds input system handling, calculated price sources, time series, bar state variables, and helper functions for null handling.

Key Changes:

  • Input function parsing (input.int, input.float, input.bool, input.string, input.color, input.source) with TypeScript interface generation
  • Built-in variables for bar state (bar_index, barstate.*), time series (year, month, etc.), and metadata (syminfo.*, timeframe.*)
  • Helper functions na() and nz() for null/undefined handling
  • Array literal parsing support in the parser

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
packages/oakscript-engine/src/transpiler/types.ts Added type definitions for InputDefinition, SymbolInfo, TimeframeInfo, and SourceType
packages/oakscript-engine/src/transpiler/PineParser.ts Added parseArrayLiteral() method to parse array syntax for input options parameter
packages/oakscript-engine/src/transpiler/PineToTS.ts Core implementation: input parsing, interface generation, built-in variable translation, and helper function generation
packages/oakscript-engine/tests/transpiler-phase2.test.ts Comprehensive test suite covering all Phase 2 features including inputs, built-ins, and integration tests
Comments suppressed due to low confidence (2)

packages/oakscript-engine/src/transpiler/PineToTS.ts:728

  • [nitpick] The generateAssignment function returns void but can be called from generateExpression (line 1000-1006) which expects a string return value. When an Assignment expression with an input.* function returns early (line 711), the caller in generateExpression will return an empty string, which is correct. However, when called directly as a statement (lines 696-728), the void return is appropriate. This dual usage pattern is correct, but consider adding a return type annotation to make the void return explicit: private generateAssignment(node: ASTNode): void.
  private generateAssignment(node: ASTNode): void {
    if (!node.children || node.children.length < 2) return;

    const left = node.children[0]!;
    const right = node.children[1]!;

    // Skip input.* function assignments - they're handled as function parameters
    if (right.type === 'FunctionCall') {
      const funcName = String(right.value || '');
      if (funcName.startsWith('input.')) {
        // Mark this variable as defined (it's a function parameter)
        if (left.type === 'Identifier') {
          const name = String(left.value || 'unknown');
          this.variables.set(name, name);
        }
        return;
      }
    }

    if (left.type === 'Identifier') {
      const name = String(left.value || 'unknown');
      const tsName = this.sanitizeIdentifier(name);
      
      if (!this.variables.has(name)) {
        this.variables.set(name, tsName);
        const rightExpr = this.generateExpression(right);
        this.emit(`const ${tsName} = ${rightExpr};`);
      } else {
        const rightExpr = this.generateExpression(right);
        this.emit(`${tsName} = ${rightExpr};`);
      }
    }
  }

packages/oakscript-engine/src/transpiler/PineParser.ts:803

  • Unused variable startPos.
      const startPos = this.position;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment on lines +149 to +165
// Generate calculated price sources
this.emit('// Calculated price sources');
this.emit('const hl2 = high.add(low).div(2);');
this.emit('const hlc3 = high.add(low).add(close).div(3);');
this.emit('const ohlc4 = open.add(high).add(low).add(close).div(4);');
this.emit('const hlcc4 = high.add(low).add(close).add(close).div(4);');
this.emit('');

// Generate time series
this.emit('// Time series');
this.emit('const year = new Series(bars, (bar) => new Date(bar.time).getFullYear());');
this.emit('const month = new Series(bars, (bar) => new Date(bar.time).getMonth() + 1);');
this.emit('const dayofmonth = new Series(bars, (bar) => new Date(bar.time).getDate());');
this.emit('const dayofweek = new Series(bars, (bar) => new Date(bar.time).getDay() + 1);');
this.emit('const hour = new Series(bars, (bar) => new Date(bar.time).getHours());');
this.emit('const minute = new Series(bars, (bar) => new Date(bar.time).getMinutes());');
this.emit('');
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All calculated price sources and time series are always generated regardless of whether they're used in the indicator code. This could lead to unnecessary computation overhead. Consider only generating these variables when they are actually referenced in the code by tracking their usage during the collectInfo phase, similar to how usesSyminfo and usesTimeframe are tracked.

Copilot uses AI. Check for mistakes.
'na': 'NaN',
'true': 'true',
'false': 'false',
'bar_index': 'i',
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bar_index variable is mapped to i (line 1208), but the generated code doesn't create a loop with variable i. The transpiler generates code using Series objects without an explicit loop. This creates a mismatch: when users reference bar_index, it translates to undefined variable i.

Consider one of these approaches:

  1. If indicators should run in a loop context, wrap the body in for (let i = 0; i < bars.length; i++)
  2. If using Series-based approach, bar_index should be handled differently (e.g., as a parameter to Series callbacks or removed as a standalone variable)
Suggested change
'bar_index': 'i',
// 'bar_index' is not supported in Series context

Copilot uses AI. Check for mistakes.
Comment on lines +1224 to +1236
// Handle barstate variables
const barstateMap: Record<string, string> = {
'barstate.isfirst': '(i === 0)',
'barstate.islast': '(i === bars.length - 1)',
'barstate.isconfirmed': 'true',
'barstate.islastconfirmedhistory': '(i === bars.length - 1)',
'barstate.isrealtime': 'false',
'barstate.isnew': 'true',
};

if (barstateMap[name]) {
return barstateMap[name];
}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The barstate.isfirst and barstate.islast translations reference variable i which doesn't exist in the generated code. Similar to the bar_index issue, the transpiler uses a Series-based approach without an explicit loop, making these translations invalid. These should either be implemented within a loop context or handled differently (e.g., using Series methods or removing support for these variables).

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +239
this.emit('export interface SymbolInfo {');
this.indent++;
this.emit('ticker: string;');
this.emit('tickerid: string;');
this.emit('currency: string;');
this.emit('mintick: number;');
this.emit('pointvalue: number;');
this.emit('type: string;');
this.indent--;
this.emit('}');
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported interface SymbolInfo may conflict with the same-named interface defined in types.ts. Since both are exported, this could cause TypeScript compilation errors if both are imported in the same file. Consider renaming the generated interface to something like GeneratedSymbolInfo or use a type re-export approach to avoid conflicts.

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +265
this.emit('export interface TimeframeInfo {');
this.indent++;
this.emit('period: string;');
this.emit('multiplier: number;');
this.emit('isintraday: boolean;');
this.emit('isdaily: boolean;');
this.emit('isweekly: boolean;');
this.emit('ismonthly: boolean;');
this.indent--;
this.emit('}');
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported interface TimeframeInfo may conflict with the same-named interface defined in types.ts. Since both are exported, this could cause TypeScript compilation errors if both are imported in the same file. Consider renaming the generated interface to something like GeneratedTimeframeInfo or use a type re-export approach to avoid conflicts.

Copilot uses AI. Check for mistakes.
'color.gray': '#808080',
'color.silver': '#C0C0C0',
'color.aqua': '#00FFFF',
'color.lime': '#00FF00',
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate color mapping: both color.green (line 359) and color.lime map to #00FF00. In standard web colors, lime is typically #00FF00 while green is #008000. Consider updating color.green to #008000 or documenting if this is intentional for PineScript compatibility.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants