@forgecharts/sdk 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +50 -0
- package/src/__tests__/backwardCompatibility.test.ts +191 -0
- package/src/__tests__/candleInvariant.test.ts +500 -0
- package/src/__tests__/public-api-surface.ts +76 -0
- package/src/__tests__/timeframeBoundary.test.ts +583 -0
- package/src/api/DrawingManager.ts +188 -0
- package/src/api/EventBus.ts +53 -0
- package/src/api/IndicatorDAG.ts +389 -0
- package/src/api/IndicatorRegistry.ts +47 -0
- package/src/api/LayoutManager.ts +72 -0
- package/src/api/PaneManager.ts +129 -0
- package/src/api/ReferenceAPI.ts +195 -0
- package/src/api/TChart.ts +881 -0
- package/src/api/createChart.ts +43 -0
- package/src/api/drawing tools/fib gann menu/fibRetracement.ts +27 -0
- package/src/api/drawing tools/lines menu/crossLine.ts +21 -0
- package/src/api/drawing tools/lines menu/disjointChannel.ts +74 -0
- package/src/api/drawing tools/lines menu/extendedLine.ts +22 -0
- package/src/api/drawing tools/lines menu/flatTopBottom.ts +45 -0
- package/src/api/drawing tools/lines menu/horizontal.ts +24 -0
- package/src/api/drawing tools/lines menu/horizontalRay.ts +25 -0
- package/src/api/drawing tools/lines menu/infoLine.ts +127 -0
- package/src/api/drawing tools/lines menu/insidePitchfork.ts +21 -0
- package/src/api/drawing tools/lines menu/modifiedSchiffPitchfork.ts +18 -0
- package/src/api/drawing tools/lines menu/parallelChannel.ts +47 -0
- package/src/api/drawing tools/lines menu/pitchfork.ts +15 -0
- package/src/api/drawing tools/lines menu/ray.ts +28 -0
- package/src/api/drawing tools/lines menu/regressionTrend.ts +157 -0
- package/src/api/drawing tools/lines menu/schiffPitchfork.ts +18 -0
- package/src/api/drawing tools/lines menu/trendAngle.ts +64 -0
- package/src/api/drawing tools/lines menu/trendline.ts +16 -0
- package/src/api/drawing tools/lines menu/vertical.ts +16 -0
- package/src/api/drawing tools/pointers menu/crosshair.ts +17 -0
- package/src/api/drawing tools/pointers menu/cursor.ts +16 -0
- package/src/api/drawing tools/pointers menu/demonstration.ts +35 -0
- package/src/api/drawing tools/pointers menu/dot.ts +26 -0
- package/src/api/drawing tools/shapes menu/rectangle.ts +24 -0
- package/src/api/drawing tools/shapes menu/text.ts +30 -0
- package/src/api/drawingUtils.ts +82 -0
- package/src/core/CanvasLayer.ts +77 -0
- package/src/core/Chart.ts +917 -0
- package/src/core/CoordTransform.ts +282 -0
- package/src/core/Crosshair.ts +207 -0
- package/src/core/IndicatorEngine.ts +216 -0
- package/src/core/InteractionManager.ts +899 -0
- package/src/core/PriceScale.ts +133 -0
- package/src/core/Series.ts +132 -0
- package/src/core/TimeScale.ts +175 -0
- package/src/datafeed/DatafeedConnector.ts +300 -0
- package/src/engine/CandleEngine.ts +458 -0
- package/src/engine/__tests__/CandleEngine.test.ts +402 -0
- package/src/engine/candleInvariants.ts +172 -0
- package/src/engine/mergeUtils.ts +93 -0
- package/src/engine/timeframeUtils.ts +118 -0
- package/src/index.ts +190 -0
- package/src/internal.ts +41 -0
- package/src/licensing/ChartRuntimeResolver.ts +380 -0
- package/src/licensing/LicenseManager.ts +131 -0
- package/src/licensing/__tests__/ChartRuntimeResolver.test.ts +207 -0
- package/src/licensing/__tests__/LicenseManager.test.ts +180 -0
- package/src/licensing/licenseTypes.ts +19 -0
- package/src/pine/PineCompiler.ts +68 -0
- package/src/pine/diagnostics.ts +30 -0
- package/src/pine/index.ts +7 -0
- package/src/pine/pine-ast.ts +163 -0
- package/src/pine/pine-lexer.ts +265 -0
- package/src/pine/pine-parser.ts +439 -0
- package/src/pine/pine-transpiler.ts +301 -0
- package/src/pixi/LayerName.ts +35 -0
- package/src/pixi/PixiCandlestickRenderer.ts +125 -0
- package/src/pixi/PixiChart.ts +425 -0
- package/src/pixi/PixiCrosshairRenderer.ts +134 -0
- package/src/pixi/PixiDrawingRenderer.ts +121 -0
- package/src/pixi/PixiGridRenderer.ts +136 -0
- package/src/pixi/PixiLayerManager.ts +102 -0
- package/src/renderers/CandlestickRenderer.ts +130 -0
- package/src/renderers/HistogramRenderer.ts +63 -0
- package/src/renderers/LineRenderer.ts +77 -0
- package/src/theme/colors.ts +21 -0
- package/src/tools/barDivergenceCheck.ts +305 -0
- package/src/trading/TradingOverlayStore.ts +161 -0
- package/src/trading/UnmanagedIngestion.ts +156 -0
- package/src/trading/__tests__/ManagedTradingController.test.ts +338 -0
- package/src/trading/__tests__/TradingOverlayStore.test.ts +323 -0
- package/src/trading/__tests__/UnmanagedIngestion.test.ts +205 -0
- package/src/trading/managed/ManagedTradingController.ts +292 -0
- package/src/trading/managed/managedCapabilities.ts +98 -0
- package/src/trading/managed/managedTypes.ts +151 -0
- package/src/trading/tradingTypes.ts +135 -0
- package/src/tscript/TScriptIndicator.ts +54 -0
- package/src/tscript/ast.ts +105 -0
- package/src/tscript/lexer.ts +190 -0
- package/src/tscript/parser.ts +334 -0
- package/src/tscript/runtime.ts +525 -0
- package/src/tscript/series.ts +84 -0
- package/src/types/IChart.ts +56 -0
- package/src/types/IRenderer.ts +16 -0
- package/src/types/ISeries.ts +30 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +15 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TScript Parser — recursive-descent, produces a typed AST.
|
|
3
|
+
*
|
|
4
|
+
* Precedence (low → high):
|
|
5
|
+
* ternary ? :
|
|
6
|
+
* or
|
|
7
|
+
* and
|
|
8
|
+
* not
|
|
9
|
+
* comparison < > <= >= == !=
|
|
10
|
+
* addition + -
|
|
11
|
+
* multiply * / %
|
|
12
|
+
* unary - (prefix)
|
|
13
|
+
* primary literals, identifiers, index [], call()
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Program, Stmt, Expr,
|
|
18
|
+
IndicatorDecl, AssignStmt, ExprStmt,
|
|
19
|
+
NumberLit, StringLit, BoolLit, Identifier,
|
|
20
|
+
IndexExpr, CallExpr, BinaryExpr, UnaryExpr,
|
|
21
|
+
TernaryExpr, LogicalExpr,
|
|
22
|
+
} from './ast';
|
|
23
|
+
import { Lexer } from './lexer';
|
|
24
|
+
import type { Token, TokenKind } from './lexer';
|
|
25
|
+
|
|
26
|
+
export class Parser {
|
|
27
|
+
private _tokens: Token[];
|
|
28
|
+
private _pos: number = 0;
|
|
29
|
+
|
|
30
|
+
constructor(src: string) {
|
|
31
|
+
this._tokens = new Lexer(src).tokenize().filter((t) => t.kind !== 'NEWLINE' || this._isStatementBoundary(t));
|
|
32
|
+
// Re-filter: keep NEWLINEs as statement separators but collapse runs
|
|
33
|
+
this._tokens = this._collapseNewlines(new Lexer(src).tokenize());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
parse(): Program {
|
|
37
|
+
const stmts: Stmt[] = [];
|
|
38
|
+
this._skipNewlines();
|
|
39
|
+
while (!this._check('EOF')) {
|
|
40
|
+
stmts.push(this._stmt());
|
|
41
|
+
this._skipNewlines();
|
|
42
|
+
}
|
|
43
|
+
return { kind: 'Program', stmts };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Statements ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
private _stmt(): Stmt {
|
|
49
|
+
// indicator("title")
|
|
50
|
+
if (this._checkIdent('indicator')) {
|
|
51
|
+
return this._indicatorDecl();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// assignment: IDENT = expr (but not IDENT == ...)
|
|
55
|
+
if (this._check('IDENT') && this._peekKind(1) === 'EQ') {
|
|
56
|
+
return this._assign();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this._exprStmt();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _indicatorDecl(): IndicatorDecl {
|
|
63
|
+
this._consumeIdent('indicator');
|
|
64
|
+
this._consume('LPAREN', "Expected '(' after 'indicator'");
|
|
65
|
+
const title = this._consume('STRING', "Expected string title in indicator()").value;
|
|
66
|
+
this._consume('RPAREN', "Expected ')' after indicator title");
|
|
67
|
+
this._consumeNewlineOrEOF();
|
|
68
|
+
return { kind: 'IndicatorDecl', title };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _assign(): AssignStmt {
|
|
72
|
+
const name = this._consume('IDENT', 'Expected identifier').value;
|
|
73
|
+
this._consume('EQ', "Expected '='");
|
|
74
|
+
const value = this._expr();
|
|
75
|
+
this._consumeNewlineOrEOF();
|
|
76
|
+
return { kind: 'AssignStmt', name, value };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private _exprStmt(): ExprStmt {
|
|
80
|
+
const expr = this._expr();
|
|
81
|
+
this._consumeNewlineOrEOF();
|
|
82
|
+
return { kind: 'ExprStmt', expr };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Expressions ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
private _expr(): Expr {
|
|
88
|
+
return this._ternary();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _ternary(): Expr {
|
|
92
|
+
let left = this._or();
|
|
93
|
+
if (this._match('QMARK')) {
|
|
94
|
+
const consequent = this._expr();
|
|
95
|
+
this._consume('COLON', "Expected ':' in ternary expression");
|
|
96
|
+
const alternate = this._expr();
|
|
97
|
+
const node: TernaryExpr = { kind: 'TernaryExpr', condition: left, consequent, alternate };
|
|
98
|
+
left = node;
|
|
99
|
+
}
|
|
100
|
+
return left;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private _or(): Expr {
|
|
104
|
+
let left = this._and();
|
|
105
|
+
while (this._checkIdent('or')) {
|
|
106
|
+
this._advance();
|
|
107
|
+
const right = this._and();
|
|
108
|
+
const node: LogicalExpr = { kind: 'LogicalExpr', op: 'or', left, right };
|
|
109
|
+
left = node;
|
|
110
|
+
}
|
|
111
|
+
return left;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _and(): Expr {
|
|
115
|
+
let left = this._not();
|
|
116
|
+
while (this._checkIdent('and')) {
|
|
117
|
+
this._advance();
|
|
118
|
+
const right = this._not();
|
|
119
|
+
const node: LogicalExpr = { kind: 'LogicalExpr', op: 'and', left, right };
|
|
120
|
+
left = node;
|
|
121
|
+
}
|
|
122
|
+
return left;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _not(): Expr {
|
|
126
|
+
if (this._checkIdent('not')) {
|
|
127
|
+
this._advance();
|
|
128
|
+
const operand = this._comparison();
|
|
129
|
+
const node: UnaryExpr = { kind: 'UnaryExpr', op: 'not', operand };
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
return this._comparison();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private _comparison(): Expr {
|
|
136
|
+
let left = this._addition();
|
|
137
|
+
while (true) {
|
|
138
|
+
if (this._match('LTE')) { left = this._binop('<=', left, this._addition()); continue; }
|
|
139
|
+
if (this._match('GTE')) { left = this._binop('>=', left, this._addition()); continue; }
|
|
140
|
+
if (this._match('EQEQ')) { left = this._binop('==', left, this._addition()); continue; }
|
|
141
|
+
if (this._match('NEQ')) { left = this._binop('!=', left, this._addition()); continue; }
|
|
142
|
+
if (this._match('LT')) { left = this._binop('<', left, this._addition()); continue; }
|
|
143
|
+
if (this._match('GT')) { left = this._binop('>', left, this._addition()); continue; }
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return left;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _addition(): Expr {
|
|
150
|
+
let left = this._multiplication();
|
|
151
|
+
while (true) {
|
|
152
|
+
if (this._match('PLUS')) { left = this._binop('+', left, this._multiplication()); continue; }
|
|
153
|
+
if (this._match('MINUS')) { left = this._binop('-', left, this._multiplication()); continue; }
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return left;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _multiplication(): Expr {
|
|
160
|
+
let left = this._unary();
|
|
161
|
+
while (true) {
|
|
162
|
+
if (this._match('STAR')) { left = this._binop('*', left, this._unary()); continue; }
|
|
163
|
+
if (this._match('SLASH')) { left = this._binop('/', left, this._unary()); continue; }
|
|
164
|
+
if (this._match('PERCENT')) { left = this._binop('%', left, this._unary()); continue; }
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
return left;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private _unary(): Expr {
|
|
171
|
+
if (this._match('MINUS')) {
|
|
172
|
+
const operand = this._unary();
|
|
173
|
+
const node: UnaryExpr = { kind: 'UnaryExpr', op: '-', operand };
|
|
174
|
+
return node;
|
|
175
|
+
}
|
|
176
|
+
return this._primary();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private _primary(): Expr {
|
|
180
|
+
const tok = this._current();
|
|
181
|
+
|
|
182
|
+
// Number literal
|
|
183
|
+
if (tok.kind === 'NUMBER') {
|
|
184
|
+
this._advance();
|
|
185
|
+
const node: NumberLit = { kind: 'NumberLit', value: parseFloat(tok.value) };
|
|
186
|
+
return node;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// String literal
|
|
190
|
+
if (tok.kind === 'STRING') {
|
|
191
|
+
this._advance();
|
|
192
|
+
const node: StringLit = { kind: 'StringLit', value: tok.value };
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Bool literal
|
|
197
|
+
if (tok.kind === 'BOOL') {
|
|
198
|
+
this._advance();
|
|
199
|
+
const node: BoolLit = { kind: 'BoolLit', value: tok.value === 'true' };
|
|
200
|
+
return node;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// na (special NaN constant)
|
|
204
|
+
if (tok.kind === 'IDENT' && tok.value === 'na') {
|
|
205
|
+
this._advance();
|
|
206
|
+
const node: NumberLit = { kind: 'NumberLit', value: NaN };
|
|
207
|
+
return node;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Function call: IDENT '(' args ')'
|
|
211
|
+
if (tok.kind === 'IDENT' && this._peekKind(1) === 'LPAREN') {
|
|
212
|
+
return this._callExpr();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Identifier (and optional series index)
|
|
216
|
+
if (tok.kind === 'IDENT') {
|
|
217
|
+
this._advance();
|
|
218
|
+
let node: Expr = { kind: 'Identifier', name: tok.value } satisfies Identifier;
|
|
219
|
+
// Series index: ident[expr]
|
|
220
|
+
while (this._check('LBRACKET')) {
|
|
221
|
+
this._advance();
|
|
222
|
+
const index = this._expr();
|
|
223
|
+
this._consume('RBRACKET', "Expected ']'");
|
|
224
|
+
const indexNode: IndexExpr = { kind: 'IndexExpr', series: node, index };
|
|
225
|
+
node = indexNode;
|
|
226
|
+
}
|
|
227
|
+
return node;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Grouped: '(' expr ')'
|
|
231
|
+
if (tok.kind === 'LPAREN') {
|
|
232
|
+
this._advance();
|
|
233
|
+
const inner = this._expr();
|
|
234
|
+
this._consume('RPAREN', "Expected ')'");
|
|
235
|
+
return inner;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
throw new SyntaxError(
|
|
239
|
+
`[TScript] Unexpected token '${tok.value}' (${tok.kind}) at ${tok.line}:${tok.col}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private _callExpr(): CallExpr {
|
|
244
|
+
const callee = this._consume('IDENT', 'Expected function name').value;
|
|
245
|
+
this._consume('LPAREN', "Expected '('");
|
|
246
|
+
const args: Expr[] = [];
|
|
247
|
+
if (!this._check('RPAREN')) {
|
|
248
|
+
args.push(this._expr());
|
|
249
|
+
while (this._match('COMMA')) {
|
|
250
|
+
args.push(this._expr());
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this._consume('RPAREN', "Expected ')'");
|
|
254
|
+
return { kind: 'CallExpr', callee, args };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Utility helpers ────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
private _binop(
|
|
260
|
+
op: BinaryExpr['op'],
|
|
261
|
+
left: Expr,
|
|
262
|
+
right: Expr,
|
|
263
|
+
): BinaryExpr {
|
|
264
|
+
return { kind: 'BinaryExpr', op, left, right };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private _current(): Token {
|
|
268
|
+
return this._tokens[this._pos]!;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private _advance(): Token {
|
|
272
|
+
return this._tokens[this._pos++]!;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private _check(kind: TokenKind): boolean {
|
|
276
|
+
return this._current().kind === kind;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _checkIdent(name: string): boolean {
|
|
280
|
+
const t = this._current();
|
|
281
|
+
return t.kind === 'IDENT' && t.value === name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private _peekKind(offset: number): TokenKind {
|
|
285
|
+
return this._tokens[this._pos + offset]?.kind ?? 'EOF';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private _match(kind: TokenKind): boolean {
|
|
289
|
+
if (this._check(kind)) { this._advance(); return true; }
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private _consume(kind: TokenKind, msg: string): Token {
|
|
294
|
+
if (this._check(kind)) return this._advance();
|
|
295
|
+
const t = this._current();
|
|
296
|
+
throw new SyntaxError(`[TScript] ${msg} — got '${t.value}' at ${t.line}:${t.col}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private _consumeIdent(name: string): Token {
|
|
300
|
+
if (this._checkIdent(name)) return this._advance();
|
|
301
|
+
const t = this._current();
|
|
302
|
+
throw new SyntaxError(`[TScript] Expected '${name}' — got '${t.value}' at ${t.line}:${t.col}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private _consumeNewlineOrEOF(): void {
|
|
306
|
+
// Silently accept NEWLINE or EOF; required at end of every statement.
|
|
307
|
+
if (this._check('NEWLINE') || this._check('EOF')) {
|
|
308
|
+
if (this._check('NEWLINE')) this._advance();
|
|
309
|
+
}
|
|
310
|
+
// If neither, we just continue (expression statements don't need semicolons)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _skipNewlines(): void {
|
|
314
|
+
while (this._check('NEWLINE')) this._advance();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private _collapseNewlines(tokens: Token[]): Token[] {
|
|
318
|
+
const out: Token[] = [];
|
|
319
|
+
let lastWasNL = false;
|
|
320
|
+
for (const t of tokens) {
|
|
321
|
+
if (t.kind === 'NEWLINE') {
|
|
322
|
+
if (!lastWasNL) out.push(t);
|
|
323
|
+
lastWasNL = true;
|
|
324
|
+
} else {
|
|
325
|
+
lastWasNL = false;
|
|
326
|
+
out.push(t);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
333
|
+
private _isStatementBoundary(_t: Token): boolean { return true; }
|
|
334
|
+
}
|