diff --git a/src/http/indicators.ts b/src/http/indicators.ts index 75cfcdf7..51e5bbbf 100644 --- a/src/http/indicators.ts +++ b/src/http/indicators.ts @@ -125,6 +125,31 @@ function parsePlots(styles: Record, items: any[]): Record ( + typeof value === 'string' && value.startsWith(COMPILED_SCRIPT_PREFIX) ? value : undefined +); + +/** + * The compiled script (`ILScript`) of a `pine-facade/translate` result. + * + * TradingView moved it twice in October 2026: `result.ilTemplate` (historical), + * then the `defval` of the hidden `text` input (27 Oct), then + * `metaInfo.defaults.inputs.text` (by 30 Oct). All three are read, newest + * shape last, so a rollback on their side keeps working too. Without it the + * chart server answers `study_error` "Can't parse pine". + */ +export function compiledScriptOf(result: any): string | undefined { + if (typeof result?.ilTemplate === 'string' && result.ilTemplate) return result.ilTemplate; + const meta = result?.metaInfo; + const textInput = Array.isArray(meta?.inputs) + ? meta.inputs.find((input: any) => input?.id === 'text') + : undefined; + return compiledScriptOrUndefined(textInput?.defval) + ?? compiledScriptOrUndefined(meta?.defaults?.inputs?.text); +} + /** Converts a `pine-facade/translate` result into a `PineIndicator`. */ export function parseIndicatorDefinition(result: any, id: string, version: string): PineIndicator { const meta = result.metaInfo; @@ -135,7 +160,7 @@ export function parseIndicatorDefinition(result: any, id: string, version: strin shortDescription: meta.shortDescription, inputs: parseInputs(meta.inputs), plots: parsePlots(meta.styles ?? {}, meta.plots ?? []), - script: result.ilTemplate, + script: compiledScriptOf(result) ?? '', }); } @@ -155,6 +180,13 @@ export async function getIndicator(id: string, options: GetIndicatorOptions = {} if (!data?.success || !data.result?.metaInfo?.inputs) { throw new TradingViewError('NOT_FOUND', `Inexistent or unsupported indicator: "${data?.reason}"`, { details: data }); } + if (!compiledScriptOf(data.result)) { + throw new TradingViewError( + 'PARSE_ERROR', + `No compiled script in the definition of "${id}": TradingView changed the pine-facade/translate format again`, + { details: data }, + ); + } return parseIndicatorDefinition(data.result, encodedId, version); } diff --git a/tests/unit/indicators.test.ts b/tests/unit/indicators.test.ts index 46f65d70..f8ba60cd 100644 --- a/tests/unit/indicators.test.ts +++ b/tests/unit/indicators.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { BuiltInIndicator } from '../../src/indicators/builtin-indicator.js'; -import { parseIndicatorDefinition } from '../../src/http/indicators.js'; +import { compiledScriptOf, parseIndicatorDefinition } from '../../src/http/indicators.js'; import { makePine } from '../helpers/indicators.js'; describe('PineIndicator', () => { @@ -90,3 +90,37 @@ describe('parseIndicatorDefinition', () => { }); }); }); + +describe('compiledScriptOf', () => { + const IL = 'bmI9Ks46_abc==_xyz'; + const meta = (text = '', extra: Record = {}): any => ({ + scriptIdPart: 'STD;RSI', + inputs: [ + { id: 'text', name: 'ILScript', defval: text, type: 'text', isHidden: true }, + { id: 'in_0', name: 'Length', defval: 14, type: 'integer' }, + ], + ...extra, + }); + + it('reads result.ilTemplate (historical shape)', () => { + expect(compiledScriptOf({ ilTemplate: IL, metaInfo: meta() })).toBe(IL); + }); + + it('reads the hidden text input defval (27 Oct 2026 shape)', () => { + expect(compiledScriptOf({ metaInfo: meta(IL) })).toBe(IL); + }); + + it('reads metaInfo.defaults.inputs.text (30 Oct 2026 shape)', () => { + const result = { metaInfo: meta('', { defaults: { inputs: { text: IL, in_0: 14 } } }) }; + expect(compiledScriptOf(result)).toBe(IL); + const indicator = parseIndicatorDefinition(result, 'STD%3BRSI', 'last'); + expect(indicator.script).toBe(IL); + expect(Object.keys(indicator.inputs)).toEqual(['in_0']); + }); + + it('ignores values that are not a compiled script', () => { + expect(compiledScriptOf({ metaInfo: meta('', { defaults: { inputs: { text: 'plot(close)' } } }) })).toBeUndefined(); + expect(compiledScriptOf({ metaInfo: meta() })).toBeUndefined(); + expect(compiledScriptOf(undefined)).toBeUndefined(); + }); +});