@hoox-sh/pynets 0.1.0 → 0.3.0
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/CHANGELOG.md +59 -0
- package/README.md +20 -9
- package/dist/browser.js +95 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23002 -12427
- package/package.json +1 -1
- package/src/ast/builder.ts +235 -12
- package/src/ast/nodes.ts +114 -4
- package/src/ast/unparse.ts +192 -17
- package/src/cli/rich.ts +4 -3
- package/src/cli.ts +29 -7
- package/src/generated/PinescriptLexer.interp +5 -1
- package/src/generated/PinescriptLexer.tokens +97 -95
- package/src/generated/PinescriptLexer.ts +512 -502
- package/src/generated/PinescriptParser.interp +8 -1
- package/src/generated/PinescriptParser.tokens +97 -95
- package/src/generated/PinescriptParser.ts +1907 -1287
- package/src/generated/PinescriptParserVisitor.ts +36 -1
- package/src/index.ts +22 -0
- package/src/runtime/array.ts +238 -68
- package/src/runtime/compile/collections.ts +492 -0
- package/src/runtime/compile/draw.ts +181 -0
- package/src/runtime/compile/emit.ts +1421 -0
- package/src/runtime/compile/emit_call.ts +1796 -0
- package/src/runtime/compile/emit_udf.ts +326 -0
- package/src/runtime/compile/engine.ts +265 -0
- package/src/runtime/compile/helpers.ts +140 -0
- package/src/runtime/compile/index.ts +33 -0
- package/src/runtime/compile/runtime.ts +357 -0
- package/src/runtime/compile/strategy.ts +250 -0
- package/src/runtime/compile/types.ts +127 -0
- package/src/runtime/compile/udt_runtime.ts +50 -0
- package/src/runtime/drawings.ts +405 -8
- package/src/runtime/interpret.ts +2387 -281
- package/src/runtime/matrix.ts +371 -42
- package/src/runtime/strategy.ts +695 -33
- package/src/runtime/ta.ts +654 -20
- package/src/runtime/timeframe.ts +199 -0
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Call lowering for JS compile. Pine builtins → `__h` / `__h.ta`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Attribute, Call, Constant, expr, Name } from "../../ast/nodes.ts";
|
|
8
|
+
import { emitUserFuncCall } from "./emit_udf.ts";
|
|
9
|
+
import type { EmitCtx, VisitFn } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
const HIGH = "high_arr[__bar_idx]";
|
|
12
|
+
const LOW = "low_arr[__bar_idx]";
|
|
13
|
+
const CLOSE = "close_arr[__bar_idx]";
|
|
14
|
+
const VOL = "vol_arr[__bar_idx]";
|
|
15
|
+
const OPEN = "open_arr[__bar_idx]";
|
|
16
|
+
// Interpret's bare `ta.vwap` sources hlc3 = (h+l+c)/3, falling back to close
|
|
17
|
+
// when any component is na — mirror that exactly on the compile side.
|
|
18
|
+
const HLC3 =
|
|
19
|
+
"((high_arr[__bar_idx] != null && low_arr[__bar_idx] != null && close_arr[__bar_idx] != null)" +
|
|
20
|
+
" ? (high_arr[__bar_idx] + low_arr[__bar_idx] + close_arr[__bar_idx]) / 3" +
|
|
21
|
+
" : close_arr[__bar_idx])";
|
|
22
|
+
// Interpret `hlc3(env)` for the ta.cci period-only form: falls back through
|
|
23
|
+
// close → high → low when any component is na (interpret.ts hlc3).
|
|
24
|
+
const TP =
|
|
25
|
+
"((high_arr[__bar_idx] != null && low_arr[__bar_idx] != null && close_arr[__bar_idx] != null)" +
|
|
26
|
+
" ? (high_arr[__bar_idx] + low_arr[__bar_idx] + close_arr[__bar_idx]) / 3" +
|
|
27
|
+
" : (close_arr[__bar_idx] ?? high_arr[__bar_idx] ?? low_arr[__bar_idx]))";
|
|
28
|
+
|
|
29
|
+
const NS = new Set([
|
|
30
|
+
"ta",
|
|
31
|
+
"math",
|
|
32
|
+
"str",
|
|
33
|
+
"array",
|
|
34
|
+
"matrix",
|
|
35
|
+
"map",
|
|
36
|
+
"color",
|
|
37
|
+
"strategy",
|
|
38
|
+
"input",
|
|
39
|
+
"request",
|
|
40
|
+
"ticker",
|
|
41
|
+
"timeframe",
|
|
42
|
+
"runtime",
|
|
43
|
+
"log",
|
|
44
|
+
"barstate",
|
|
45
|
+
"session",
|
|
46
|
+
"syminfo",
|
|
47
|
+
"timenow",
|
|
48
|
+
"chart",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** TaEngine methods we emit. Unknown `ta.*` → `null`. */
|
|
52
|
+
const TA_METHODS = new Set([
|
|
53
|
+
"sma",
|
|
54
|
+
"ema",
|
|
55
|
+
"rma",
|
|
56
|
+
"rsi",
|
|
57
|
+
"atr",
|
|
58
|
+
"tr",
|
|
59
|
+
"change",
|
|
60
|
+
"mom",
|
|
61
|
+
"roc",
|
|
62
|
+
"stdev",
|
|
63
|
+
"highest",
|
|
64
|
+
"lowest",
|
|
65
|
+
"sum",
|
|
66
|
+
"cross",
|
|
67
|
+
"crossover",
|
|
68
|
+
"crossunder",
|
|
69
|
+
"macd",
|
|
70
|
+
"bb",
|
|
71
|
+
"stoch",
|
|
72
|
+
"wma",
|
|
73
|
+
"vwma",
|
|
74
|
+
"hma",
|
|
75
|
+
"cci",
|
|
76
|
+
"cmo",
|
|
77
|
+
"mfi",
|
|
78
|
+
"tsi",
|
|
79
|
+
"dmi",
|
|
80
|
+
"supertrend",
|
|
81
|
+
"alma",
|
|
82
|
+
"kama",
|
|
83
|
+
"linreg",
|
|
84
|
+
"rising",
|
|
85
|
+
"falling",
|
|
86
|
+
"barssince",
|
|
87
|
+
"valuewhen",
|
|
88
|
+
"pivothigh",
|
|
89
|
+
"pivotlow",
|
|
90
|
+
"swma",
|
|
91
|
+
"kc",
|
|
92
|
+
"willr",
|
|
93
|
+
"wpr",
|
|
94
|
+
"vwap",
|
|
95
|
+
"sar",
|
|
96
|
+
"adx",
|
|
97
|
+
"correlation",
|
|
98
|
+
"cog",
|
|
99
|
+
"cum",
|
|
100
|
+
"range",
|
|
101
|
+
"accdist",
|
|
102
|
+
"ad",
|
|
103
|
+
"pvt",
|
|
104
|
+
"vpt",
|
|
105
|
+
"wad",
|
|
106
|
+
"nvi",
|
|
107
|
+
"pvi",
|
|
108
|
+
"wvad",
|
|
109
|
+
"cmf",
|
|
110
|
+
"klinger",
|
|
111
|
+
"iii",
|
|
112
|
+
"obv",
|
|
113
|
+
"ao",
|
|
114
|
+
"aroon",
|
|
115
|
+
"highestbars",
|
|
116
|
+
"lowestbars",
|
|
117
|
+
"dev",
|
|
118
|
+
"variance",
|
|
119
|
+
"median",
|
|
120
|
+
"mode",
|
|
121
|
+
"percentrank",
|
|
122
|
+
"dema",
|
|
123
|
+
"tema",
|
|
124
|
+
"bbw",
|
|
125
|
+
"max",
|
|
126
|
+
"min",
|
|
127
|
+
"uo",
|
|
128
|
+
"rci",
|
|
129
|
+
"dpo",
|
|
130
|
+
"kst",
|
|
131
|
+
"stochrsi",
|
|
132
|
+
"donchian",
|
|
133
|
+
"ichimoku",
|
|
134
|
+
"bb_pct",
|
|
135
|
+
"emv",
|
|
136
|
+
"fractal",
|
|
137
|
+
"atr_stop",
|
|
138
|
+
"zigzag",
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Bare `ta.<attr>` auto-call whitelist. Mirrors interpret's evalTaAttr
|
|
143
|
+
* (interpret.ts) exactly: only these attributes auto-invoke a zero-arg TA
|
|
144
|
+
* kernel on a bare attribute load; anything else is na on both backends.
|
|
145
|
+
* `aroon` is deliberately absent — interpret's evalTaAttr does not handle it.
|
|
146
|
+
*/
|
|
147
|
+
export const TA_BARE_ATTRS = new Set([
|
|
148
|
+
"accdist",
|
|
149
|
+
"ad",
|
|
150
|
+
"obv",
|
|
151
|
+
"tr",
|
|
152
|
+
"vwap",
|
|
153
|
+
"pvt",
|
|
154
|
+
"vpt",
|
|
155
|
+
"nvi",
|
|
156
|
+
"pvi",
|
|
157
|
+
"wad",
|
|
158
|
+
"wvad",
|
|
159
|
+
"cmf",
|
|
160
|
+
"ao",
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const SRC_LEN = new Set([
|
|
164
|
+
"sma",
|
|
165
|
+
"ema",
|
|
166
|
+
"rma",
|
|
167
|
+
"rsi",
|
|
168
|
+
"stdev",
|
|
169
|
+
"wma",
|
|
170
|
+
"hma",
|
|
171
|
+
"cmo",
|
|
172
|
+
"sum",
|
|
173
|
+
"dema",
|
|
174
|
+
"tema",
|
|
175
|
+
"dev",
|
|
176
|
+
"variance",
|
|
177
|
+
"median",
|
|
178
|
+
"mode",
|
|
179
|
+
"percentrank",
|
|
180
|
+
"range",
|
|
181
|
+
"cog",
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const SRC_LEN1 = new Set(["change", "mom", "roc", "rising", "falling"]);
|
|
185
|
+
|
|
186
|
+
const PLOT_FUNCS = new Set([
|
|
187
|
+
"plot",
|
|
188
|
+
"plotshape",
|
|
189
|
+
"plotchar",
|
|
190
|
+
"plotarrow",
|
|
191
|
+
"hline",
|
|
192
|
+
"bgcolor",
|
|
193
|
+
"barcolor",
|
|
194
|
+
"plotcandle",
|
|
195
|
+
"plotbar",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const STRATEGY_ACTIONS = new Set([
|
|
199
|
+
"strategy_entry",
|
|
200
|
+
"strategy_close",
|
|
201
|
+
"strategy_close_all",
|
|
202
|
+
"strategy_exit",
|
|
203
|
+
"strategy_order",
|
|
204
|
+
"strategy_cancel",
|
|
205
|
+
"strategy_cancel_all",
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const STRATEGY_QUERIES = new Set([
|
|
209
|
+
"strategy_position_size",
|
|
210
|
+
"strategy_netprofit",
|
|
211
|
+
"strategy_equity",
|
|
212
|
+
"strategy_openprofit",
|
|
213
|
+
"strategy_opentrades",
|
|
214
|
+
"strategy_closedtrades",
|
|
215
|
+
"strategy_leverage",
|
|
216
|
+
"strategy_margin_liquidation_price",
|
|
217
|
+
"strategy_position_avg_price",
|
|
218
|
+
"strategy_initial_capital",
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const CHART_SERIES_ARR: Record<string, string> = {
|
|
222
|
+
close: "close_arr",
|
|
223
|
+
open: "open_arr",
|
|
224
|
+
high: "high_arr",
|
|
225
|
+
low: "low_arr",
|
|
226
|
+
volume: "vol_arr",
|
|
227
|
+
time: "time_arr",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const MATH_UNARY: Record<string, { fn: string; guard?: string }> = {
|
|
231
|
+
abs: { fn: "Math.abs(Number(__x))" },
|
|
232
|
+
sqrt: { fn: "Math.sqrt(Number(__x))", guard: "Number(__x) < 0" },
|
|
233
|
+
log: { fn: "Math.log(Number(__x))", guard: "Number(__x) <= 0" },
|
|
234
|
+
log10: { fn: "Math.log10(Number(__x))", guard: "Number(__x) <= 0" },
|
|
235
|
+
exp: { fn: "Math.exp(Number(__x))" },
|
|
236
|
+
sign: { fn: "(Number(__x) > 0 ? 1 : Number(__x) < 0 ? -1 : 0)" },
|
|
237
|
+
floor: { fn: "Math.floor(Number(__x))" },
|
|
238
|
+
ceil: { fn: "Math.ceil(Number(__x))" },
|
|
239
|
+
sin: { fn: "Math.sin(Number(__x))" },
|
|
240
|
+
cos: { fn: "Math.cos(Number(__x))" },
|
|
241
|
+
tan: { fn: "Math.tan(Number(__x))" },
|
|
242
|
+
asin: { fn: "Math.asin(Number(__x))", guard: "Number(__x) < -1 || Number(__x) > 1" },
|
|
243
|
+
acos: { fn: "Math.acos(Number(__x))", guard: "Number(__x) < -1 || Number(__x) > 1" },
|
|
244
|
+
atan: { fn: "Math.atan(Number(__x))" },
|
|
245
|
+
todegrees: { fn: "Number(__x) * 180 / Math.PI" },
|
|
246
|
+
toradians: { fn: "Number(__x) * Math.PI / 180" },
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
interface CallArgs {
|
|
250
|
+
pos: string[];
|
|
251
|
+
kw: Record<string, string>;
|
|
252
|
+
posNodes: expr[];
|
|
253
|
+
kwNodes: Record<string, expr>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
|
|
257
|
+
const { name: rawName, methodSrc } = resolveCallee(node.func);
|
|
258
|
+
const args = collectArgs(node, visit);
|
|
259
|
+
|
|
260
|
+
// `import lib as alias` → alias.foo. Inlined export is a user func; else stub null.
|
|
261
|
+
// Must run before methodSrc unshift / UDT-method (do not pass alias as receiver).
|
|
262
|
+
if (methodSrc != null && methodSrc.kind === "Name") {
|
|
263
|
+
const alias = (methodSrc as Name).id;
|
|
264
|
+
if (ctx.importAliases.has(alias)) {
|
|
265
|
+
if (ctx.userFuncs.has(rawName)) {
|
|
266
|
+
return emitUserFuncCall(ctx, rawName, packUserFuncArgs(ctx, rawName, args));
|
|
267
|
+
}
|
|
268
|
+
return "null";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const drawn = emitDraw(ctx, rawName, methodSrc, args);
|
|
273
|
+
if (drawn != null) return drawn;
|
|
274
|
+
const udt = emitUdt(ctx, rawName, methodSrc, args);
|
|
275
|
+
if (udt != null) return udt;
|
|
276
|
+
const udtMethod = emitUdtMethod(ctx, rawName, methodSrc, args, visit);
|
|
277
|
+
if (udtMethod != null) return udtMethod;
|
|
278
|
+
|
|
279
|
+
if (methodSrc != null) {
|
|
280
|
+
args.pos.unshift(visit(methodSrc));
|
|
281
|
+
args.posNodes.unshift(methodSrc);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let name = rawName;
|
|
285
|
+
if (methodSrc != null && TA_METHODS.has(name)) name = `ta_${name}`;
|
|
286
|
+
if (methodSrc != null && (name === "tostring" || name === "format")) name = `str_${name}`;
|
|
287
|
+
|
|
288
|
+
if (PLOT_FUNCS.has(name)) return emitPlot(ctx, name, args);
|
|
289
|
+
|
|
290
|
+
if (name === "indicator" || name === "study" || name === "library") return "null";
|
|
291
|
+
if (name === "strategy") return emitStrategyDecl(ctx, args);
|
|
292
|
+
|
|
293
|
+
if (name === "na") {
|
|
294
|
+
if (args.pos.length === 0 && args.kw.x == null && args.kw.source == null) return "null";
|
|
295
|
+
const x = pick(args, 0, ["x", "source"], "null");
|
|
296
|
+
return `(__h.isNa(${x}) ? 1 : 0)`;
|
|
297
|
+
}
|
|
298
|
+
if (name === "nz") {
|
|
299
|
+
const x = pick(args, 0, ["source", "x"], "null");
|
|
300
|
+
if (hasArg(args, 1, ["replacement", "y"])) return `__h.nz(${x}, ${pick(args, 1, ["replacement", "y"], "0")})`;
|
|
301
|
+
return `__h.nz(${x})`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (name === "input" || name.startsWith("input_")) return emitInput(ctx, args);
|
|
305
|
+
|
|
306
|
+
if (STRATEGY_ACTIONS.has(name) || STRATEGY_QUERIES.has(name) || name.startsWith("strategy_")) {
|
|
307
|
+
return emitStrategy(ctx, name, args);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (ctx.userFuncs.has(name) || ctx.funcSeriesParams.has(name)) {
|
|
311
|
+
return emitUserFuncCall(ctx, name, packUserFuncArgs(ctx, name, args));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (name === "year" || name === "month" || name === "dayofmonth" || name === "hour" || name === "minute" || name === "second" || name === "dayofweek") {
|
|
315
|
+
const t = pick(args, 0, ["time"], "time");
|
|
316
|
+
return `__h.${name}(${t})`;
|
|
317
|
+
}
|
|
318
|
+
if (name === "timestamp") {
|
|
319
|
+
const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
|
|
320
|
+
return `__h.timestamp(${parts.join(", ")})`;
|
|
321
|
+
}
|
|
322
|
+
if (name === "weekofyear") {
|
|
323
|
+
return `__h.weekOfYear(${pick(args, 0, ["time", "timestamp"], "time")})`;
|
|
324
|
+
}
|
|
325
|
+
if (name === "time_tradingday") {
|
|
326
|
+
return `__h.timeTradingDay(${pick(args, 0, ["time", "timestamp"], "time")})`;
|
|
327
|
+
}
|
|
328
|
+
if (name === "timeframe_in_seconds") {
|
|
329
|
+
if (hasArg(args, 0, ["timeframe", "period"])) {
|
|
330
|
+
return `__h.timeframeInSeconds(${pick(args, 0, ["timeframe", "period"], "null")})`;
|
|
331
|
+
}
|
|
332
|
+
return "86400";
|
|
333
|
+
}
|
|
334
|
+
if (name === "timeframe_change") {
|
|
335
|
+
// Prev bar time is `time_arr[__bar_idx-1]`; bar 0 has no prev (interpret series.get(1)).
|
|
336
|
+
return `__h.timeframeChange(${pick(args, 0, ["timeframe", "tf"], "null")}, time, (__bar_idx > 0 ? time_arr[__bar_idx - 1] : null), __bar_idx)`;
|
|
337
|
+
}
|
|
338
|
+
if (name === "time_close") {
|
|
339
|
+
return `__h.timeClose(__bar_idx, time_arr)`;
|
|
340
|
+
}
|
|
341
|
+
if (name === "syminfo_prefix") {
|
|
342
|
+
if (hasArg(args, 0, ["tickerid", "symbol"])) {
|
|
343
|
+
return `__h.syminfoPrefix(${pick(args, 0, ["tickerid", "symbol"], "null")})`;
|
|
344
|
+
}
|
|
345
|
+
return `__h.syminfoPrefix()`;
|
|
346
|
+
}
|
|
347
|
+
if (name === "ticker_heikinashi" || name === "heikinashi") {
|
|
348
|
+
ctx.needsHeikinashi = true;
|
|
349
|
+
return `"__HEIKINASHI__"`;
|
|
350
|
+
}
|
|
351
|
+
// ticker.new / standard / modify / renko / kagi / linebreak / pointfigure → first arg or "SYMBOL"
|
|
352
|
+
if (name.startsWith("ticker_")) {
|
|
353
|
+
return pick(args, 0, ["symbol", "ticker", "tickerid"], `"SYMBOL"`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (name === "log_info" || name === "log_warning" || name === "log_error") {
|
|
357
|
+
const level = name.slice("log_".length);
|
|
358
|
+
const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
|
|
359
|
+
return parts.length > 0
|
|
360
|
+
? `__h.log.${level}(__bar_idx, ${parts.join(", ")})`
|
|
361
|
+
: `__h.log.${level}(__bar_idx)`;
|
|
362
|
+
}
|
|
363
|
+
if (name === "log_clear") return "__h.log.clear()";
|
|
364
|
+
|
|
365
|
+
if (name === "runtime_error") {
|
|
366
|
+
const msg = pick(args, 0, ["message", "msg"], `"runtime.error"`);
|
|
367
|
+
return `(() => { throw new Error(String(${msg})); })()`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (name === "alertcondition") return "null";
|
|
371
|
+
if (name === "str_tostring" || name === "tostring") {
|
|
372
|
+
const x = pick(args, 0, ["value", "source", "x"], "null");
|
|
373
|
+
return `String(${x} ?? "")`;
|
|
374
|
+
}
|
|
375
|
+
if (name === "str_format" || name === "format") {
|
|
376
|
+
const fmt = pick(args, 0, ["formatString", "format", "fmt"], "null");
|
|
377
|
+
const fmtFromKw =
|
|
378
|
+
Object.hasOwn(args.kw, "formatString") ||
|
|
379
|
+
Object.hasOwn(args.kw, "format") ||
|
|
380
|
+
Object.hasOwn(args.kw, "fmt");
|
|
381
|
+
const extras = fmtFromKw ? args.pos.slice() : args.pos.slice(1);
|
|
382
|
+
return extras.length > 0 ? `__h.strFormat(${fmt}, ${extras.join(", ")})` : `__h.strFormat(${fmt})`;
|
|
383
|
+
}
|
|
384
|
+
if (name === "str_format_time" || name === "format_time") {
|
|
385
|
+
return `__h.strFormatTime(${pick(args, 0, ["time", "timestamp"], "null")}, ${pick(args, 1, ["format"], "undefined")}, ${pick(args, 2, ["timezone"], "undefined")})`;
|
|
386
|
+
}
|
|
387
|
+
if (name.startsWith("str_")) {
|
|
388
|
+
const meth = name.slice(4);
|
|
389
|
+
const alias: Record<string, string> = {
|
|
390
|
+
length: "length",
|
|
391
|
+
contains: "contains",
|
|
392
|
+
startswith: "starts_with",
|
|
393
|
+
ends_with: "ends_with",
|
|
394
|
+
endswith: "ends_with",
|
|
395
|
+
lower: "lower",
|
|
396
|
+
upper: "upper",
|
|
397
|
+
replace: "replace",
|
|
398
|
+
substring: "substring",
|
|
399
|
+
tonumber: "tonumber",
|
|
400
|
+
trim: "trim",
|
|
401
|
+
};
|
|
402
|
+
const key = alias[meth];
|
|
403
|
+
if (key) {
|
|
404
|
+
const a0 = pick(args, 0, ["source", "string", "value"], "null");
|
|
405
|
+
const a1 = args.pos[1] ?? args.kw.substring ?? args.kw.str ?? args.kw.replacement ?? "undefined";
|
|
406
|
+
if (meth === "length" || meth === "lower" || meth === "upper" || meth === "tonumber" || meth === "trim") {
|
|
407
|
+
return `__h.str.${key}(${a0})`;
|
|
408
|
+
}
|
|
409
|
+
return `__h.str.${key}(${a0}, ${a1})`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const color = emitColor(name, args);
|
|
414
|
+
if (color != null) return color;
|
|
415
|
+
|
|
416
|
+
const math = emitMath(name, args);
|
|
417
|
+
if (math != null) return math;
|
|
418
|
+
|
|
419
|
+
if (TA_METHODS.has(name) && !ctx.userFuncs.has(name)) name = `ta_${name}`;
|
|
420
|
+
if (name.startsWith("ta_")) return emitTa(ctx, name.slice(3), args);
|
|
421
|
+
|
|
422
|
+
const arr = emitArray(name, args);
|
|
423
|
+
if (arr != null) return arr;
|
|
424
|
+
const mp = emitMap(name, args);
|
|
425
|
+
if (mp != null) return mp;
|
|
426
|
+
const mx = emitMatrix(name, args);
|
|
427
|
+
if (mx != null) return mx;
|
|
428
|
+
|
|
429
|
+
const req = emitRequest(ctx, name, args);
|
|
430
|
+
if (req != null) return req;
|
|
431
|
+
|
|
432
|
+
return "null";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function resolveCallee(func: expr): { name: string; methodSrc: expr | null } {
|
|
436
|
+
func = unwrapSpecialize(func);
|
|
437
|
+
if (func.kind === "Name") return { name: (func as Name).id, methodSrc: null };
|
|
438
|
+
if (func.kind !== "Attribute") return { name: "unknown_func", methodSrc: null };
|
|
439
|
+
|
|
440
|
+
const attrNode = func as Attribute;
|
|
441
|
+
const attr = attrNode.attr;
|
|
442
|
+
const recv = unwrapSpecialize(attrNode.value);
|
|
443
|
+
|
|
444
|
+
if (recv.kind === "Name") {
|
|
445
|
+
const id = (recv as Name).id;
|
|
446
|
+
if (NS.has(id)) return { name: `${id}_${attr}`, methodSrc: null };
|
|
447
|
+
return { name: attr, methodSrc: recv };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (recv.kind === "Attribute") {
|
|
451
|
+
const mid = recv as Attribute;
|
|
452
|
+
const inner = unwrapSpecialize(mid.value);
|
|
453
|
+
if (inner.kind === "Name" && NS.has((inner as Name).id)) {
|
|
454
|
+
return { name: `${(inner as Name).id}_${mid.attr}_${attr}`, methodSrc: null };
|
|
455
|
+
}
|
|
456
|
+
return { name: attr, methodSrc: attrNode.value };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return { name: attr, methodSrc: attrNode.value };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function unwrapSpecialize(node: expr): expr {
|
|
463
|
+
const n = node as unknown as { kind?: string; value?: expr };
|
|
464
|
+
if (n.kind === "Specialize" && n.value != null) return n.value;
|
|
465
|
+
return node;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function collectArgs(node: Call, visit: VisitFn): CallArgs {
|
|
469
|
+
const pos: string[] = [];
|
|
470
|
+
const kw: Record<string, string> = {};
|
|
471
|
+
const posNodes: expr[] = [];
|
|
472
|
+
const kwNodes: Record<string, expr> = {};
|
|
473
|
+
const list = node.args ?? [];
|
|
474
|
+
for (const arg of list) {
|
|
475
|
+
const value = arg.value;
|
|
476
|
+
const emitted = visit(value);
|
|
477
|
+
const key = arg.name != null && arg.name !== "" ? arg.name : null;
|
|
478
|
+
if (key != null) {
|
|
479
|
+
kw[key] = emitted;
|
|
480
|
+
kwNodes[key] = value;
|
|
481
|
+
} else {
|
|
482
|
+
pos.push(emitted);
|
|
483
|
+
posNodes.push(value);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { pos, kw, posNodes, kwNodes };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function pick(args: CallArgs, index: number, names: string[], fallback: string): string {
|
|
490
|
+
for (const n of names) {
|
|
491
|
+
if (Object.hasOwn(args.kw, n)) return args.kw[n]!;
|
|
492
|
+
}
|
|
493
|
+
if (index >= 0 && index < args.pos.length) return args.pos[index]!;
|
|
494
|
+
return fallback;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function hasArg(args: CallArgs, index: number, names: string[]): boolean {
|
|
498
|
+
for (const n of names) {
|
|
499
|
+
if (Object.hasOwn(args.kw, n)) return true;
|
|
500
|
+
}
|
|
501
|
+
return index >= 0 && index < args.pos.length;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** True when a keyword argument named one of *names* is present (interpret hasKwarg). */
|
|
505
|
+
function hasAnyKw(args: CallArgs, names: string[]): boolean {
|
|
506
|
+
return names.some((n) => Object.hasOwn(args.kw, n));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Runtime-safe `_is_period_like` gate (interpret `periodLikeCell`): only whole
|
|
511
|
+
* numbers pass and are returned as-is; everything else (na, fractional,
|
|
512
|
+
* series samples) collapses to *fb* — mirroring `periodLikeArg(...) ?? fb`.
|
|
513
|
+
* Constant args could be decided at emit time, but the runtime form is
|
|
514
|
+
* equally exact and keeps one code path for constants and series alike.
|
|
515
|
+
*/
|
|
516
|
+
function periodGate(expr: string, fb: string): string {
|
|
517
|
+
return `((__p) => (Number.isInteger(__p) ? __p : ${fb}))(${expr})`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Interpret `lenArg` / `lenOrDefault` analog: a null (na) or non-finite slot
|
|
522
|
+
* collapses to *fb* instead of reaching the kernel raw.
|
|
523
|
+
*/
|
|
524
|
+
function finiteOr(expr: string, fb: string): string {
|
|
525
|
+
return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
|
|
529
|
+
|
|
530
|
+
const ARRAY_SIGS: Record<string, string[][]> = {
|
|
531
|
+
new: [["size"], ["initial_value", "initial"]],
|
|
532
|
+
get: [["id"], ["index"]],
|
|
533
|
+
set: [["id"], ["index"], ["value"]],
|
|
534
|
+
push: [["id"], ["value"]],
|
|
535
|
+
pop: [["id"]],
|
|
536
|
+
unshift: [["id"], ["value"]],
|
|
537
|
+
shift: [["id"]],
|
|
538
|
+
size: [["id"]],
|
|
539
|
+
includes: [["id"], ["value"]],
|
|
540
|
+
first: [["id"]],
|
|
541
|
+
last: [["id"]],
|
|
542
|
+
insert: [["id"], ["index"], ["value"]],
|
|
543
|
+
remove: [["id"], ["index"]],
|
|
544
|
+
clear: [["id"]],
|
|
545
|
+
fill: [["id"], ["value"]],
|
|
546
|
+
copy: [["id"]],
|
|
547
|
+
reverse: [["id"]],
|
|
548
|
+
sort: [["id"], ["order"], ["sort_field"]],
|
|
549
|
+
concat: [["id"], ["other", "id2"]],
|
|
550
|
+
slice: [["id"], ["index_from", "from"], ["index_to", "to"]],
|
|
551
|
+
indexof: [["id"], ["value"]],
|
|
552
|
+
lastindexof: [["id"], ["value"]],
|
|
553
|
+
avg: [["id"]],
|
|
554
|
+
min: [["id"]],
|
|
555
|
+
max: [["id"]],
|
|
556
|
+
sum: [["id"]],
|
|
557
|
+
stdev: [["id"], ["biased"]],
|
|
558
|
+
variance: [["id"], ["biased"]],
|
|
559
|
+
join: [["id"], ["separator", "sep"]],
|
|
560
|
+
every: [["id"]],
|
|
561
|
+
some: [["id"]],
|
|
562
|
+
abs: [["id"]],
|
|
563
|
+
range: [["id"]],
|
|
564
|
+
median: [["id"]],
|
|
565
|
+
mode: [["id"]],
|
|
566
|
+
standardize: [["id"]],
|
|
567
|
+
percentrank: [["id"], ["value"]],
|
|
568
|
+
percentile_linear: [["id"], ["percentage", "percent"]],
|
|
569
|
+
percentile_linear_interpolation: [["id"], ["percentage", "percent"]],
|
|
570
|
+
percentile_nearest: [["id"], ["percentage", "percent"]],
|
|
571
|
+
percentile_nearest_rank: [["id"], ["percentage", "percent"]],
|
|
572
|
+
binary_search: [["id"], ["value"], ["sort_field"]],
|
|
573
|
+
binary_search_leftmost: [["id"], ["value"], ["sort_field"]],
|
|
574
|
+
binary_search_rightmost: [["id"], ["value"], ["sort_field"]],
|
|
575
|
+
sort_indices: [["id"], ["order"], ["sort_field"]],
|
|
576
|
+
covariance: [["id"], ["id2", "other"], ["biased"]],
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const MAP_SIGS: Record<string, string[][]> = {
|
|
580
|
+
new: [],
|
|
581
|
+
put: [["id"], ["key"], ["value"]],
|
|
582
|
+
get: [["id"], ["key"]],
|
|
583
|
+
contains: [["id"], ["key"]],
|
|
584
|
+
remove: [["id"], ["key"]],
|
|
585
|
+
clear: [["id"]],
|
|
586
|
+
size: [["id"]],
|
|
587
|
+
keys: [["id"]],
|
|
588
|
+
values: [["id"]],
|
|
589
|
+
copy: [["id"]],
|
|
590
|
+
put_all: [["id"], ["id2", "other"]],
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const MATRIX_SIGS: Record<string, string[][]> = {
|
|
594
|
+
new: [["rows"], ["columns", "cols"], ["initial_value", "initial"]],
|
|
595
|
+
get: [["id"], ["row"], ["column", "col"]],
|
|
596
|
+
set: [["id"], ["row"], ["column", "col"], ["value"]],
|
|
597
|
+
rows: [["id"]],
|
|
598
|
+
columns: [["id"]],
|
|
599
|
+
fill: [["id"], ["value"]],
|
|
600
|
+
transpose: [["id"]],
|
|
601
|
+
copy: [["id"]],
|
|
602
|
+
inv: [["id"]],
|
|
603
|
+
pinv: [["id"]],
|
|
604
|
+
rank: [["id"]],
|
|
605
|
+
det: [["id"]],
|
|
606
|
+
trace: [["id"]],
|
|
607
|
+
elements_count: [["id"]],
|
|
608
|
+
is_square: [["id"]],
|
|
609
|
+
is_zero: [["id"]],
|
|
610
|
+
is_identity: [["id"]],
|
|
611
|
+
is_diagonal: [["id"]],
|
|
612
|
+
is_symmetric: [["id"]],
|
|
613
|
+
is_antisymmetric: [["id"]],
|
|
614
|
+
is_triangular: [["id"]],
|
|
615
|
+
is_binary: [["id"]],
|
|
616
|
+
is_stochastic: [["id"]],
|
|
617
|
+
is_antidiagonal: [["id"]],
|
|
618
|
+
mult: [["id"], ["other", "matrix_2"]],
|
|
619
|
+
row: [["id"], ["row", "index"]],
|
|
620
|
+
col: [["id"], ["column", "col", "index"]],
|
|
621
|
+
column: [["id"], ["column", "col", "index"]],
|
|
622
|
+
sum: [["id"]],
|
|
623
|
+
sum_all: [["id"]],
|
|
624
|
+
avg: [["id"]],
|
|
625
|
+
avg_all: [["id"]],
|
|
626
|
+
min: [["id"]],
|
|
627
|
+
min_all: [["id"]],
|
|
628
|
+
max: [["id"]],
|
|
629
|
+
max_all: [["id"]],
|
|
630
|
+
median: [["id"]],
|
|
631
|
+
mode: [["id"]],
|
|
632
|
+
kron: [["id"], ["other"]],
|
|
633
|
+
diff: [["id"], ["other"]],
|
|
634
|
+
concat: [["id"], ["other"]],
|
|
635
|
+
reshape: [["id"], ["rows"], ["columns", "cols"]],
|
|
636
|
+
reverse: [["id"]],
|
|
637
|
+
sort: [["id"], ["column", "col"], ["order"], ["sort_field"]],
|
|
638
|
+
sort_indices: [["id"], ["column", "col"], ["order"], ["sort_field"]],
|
|
639
|
+
eigenvalues: [["id"]],
|
|
640
|
+
eigenvectors: [["id"]],
|
|
641
|
+
pow: [["id"], ["power", "n"]],
|
|
642
|
+
swap_rows: [["id"], ["row1", "i"], ["row2", "j"]],
|
|
643
|
+
swap_columns: [["id"], ["column1", "i"], ["column2", "j"]],
|
|
644
|
+
add_row: [["id"], ["row", "index"], ["array"]],
|
|
645
|
+
add_col: [["id"], ["column", "index"], ["array"]],
|
|
646
|
+
add_column: [["id"], ["column", "index"], ["array"]],
|
|
647
|
+
remove_row: [["id"], ["row", "index"]],
|
|
648
|
+
remove_col: [["id"], ["column", "index"]],
|
|
649
|
+
remove_column: [["id"], ["column", "index"]],
|
|
650
|
+
submatrix: [["id"], ["from_row"], ["to_row"], ["from_column", "from_col"], ["to_column", "to_col"]],
|
|
651
|
+
sum_row: [["id"], ["row", "index"]],
|
|
652
|
+
avg_row: [["id"], ["row", "index"]],
|
|
653
|
+
min_row: [["id"], ["row", "index"]],
|
|
654
|
+
max_row: [["id"], ["row", "index"]],
|
|
655
|
+
mode_row: [["id"], ["row", "index"]],
|
|
656
|
+
sum_col: [["id"], ["column", "col", "index"]],
|
|
657
|
+
avg_col: [["id"], ["column", "col", "index"]],
|
|
658
|
+
min_col: [["id"], ["column", "col", "index"]],
|
|
659
|
+
max_col: [["id"], ["column", "col", "index"]],
|
|
660
|
+
mode_col: [["id"], ["column", "col", "index"]],
|
|
661
|
+
copy_row: [["id"], ["row", "index"]],
|
|
662
|
+
copy_col: [["id"], ["column", "col", "index"]],
|
|
663
|
+
fill_row: [["id"], ["row", "index"], ["value"]],
|
|
664
|
+
fill_col: [["id"], ["column", "col", "index"], ["value"]],
|
|
665
|
+
fill_diagonal: [["id"], ["value"]],
|
|
666
|
+
reverse_rows: [["id"]],
|
|
667
|
+
reverse_cols: [["id"]],
|
|
668
|
+
stdev: [["id"]],
|
|
669
|
+
variance: [["id"]],
|
|
670
|
+
mode_all: [["id"]],
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
function nsMethod(name: string, prefix: string): string | null {
|
|
674
|
+
if (!name.startsWith(prefix)) return null;
|
|
675
|
+
return name.slice(prefix.length);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function ctorMethod(method: string): string {
|
|
679
|
+
// Every Python `array.new*` spelling (newint, new_float, new_chart.point, …)
|
|
680
|
+
// lowers to the same PineArray constructor as `array.new`.
|
|
681
|
+
return method === "from" || method.startsWith("new") ? "new" : method;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const ARRAY_HOST_ALIAS: Record<string, string> = {
|
|
685
|
+
percentile_linear_interpolation: "percentile_linear",
|
|
686
|
+
percentile_nearest_rank: "percentile_nearest",
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const MATRIX_HOST_ALIAS: Record<string, string> = {
|
|
690
|
+
column: "col",
|
|
691
|
+
add_column: "add_col",
|
|
692
|
+
remove_column: "remove_col",
|
|
693
|
+
sum_all: "sum",
|
|
694
|
+
avg_all: "avg",
|
|
695
|
+
min_all: "min",
|
|
696
|
+
max_all: "max",
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
function emitHostCall(ns: string, method: string, args: CallArgs, params: string[][]): string {
|
|
700
|
+
const parts = params.map((names, i) => pick(args, i, names, "null"));
|
|
701
|
+
return `__h.${ns}.${method}(${parts.join(", ")})`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function emitArray(name: string, args: CallArgs): string | null {
|
|
705
|
+
const raw = nsMethod(name, "array_");
|
|
706
|
+
if (raw == null) return null;
|
|
707
|
+
const method = ctorMethod(raw);
|
|
708
|
+
const params = ARRAY_SIGS[method];
|
|
709
|
+
if (params == null) return null;
|
|
710
|
+
return emitHostCall("array", ARRAY_HOST_ALIAS[method] ?? method, args, params);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function emitMap(name: string, args: CallArgs): string | null {
|
|
714
|
+
const raw = nsMethod(name, "map_");
|
|
715
|
+
if (raw == null) return null;
|
|
716
|
+
const method = ctorMethod(raw);
|
|
717
|
+
const params = MAP_SIGS[method];
|
|
718
|
+
if (params == null) return null;
|
|
719
|
+
return emitHostCall("map", method, args, params);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function emitMatrix(name: string, args: CallArgs): string | null {
|
|
723
|
+
const raw = nsMethod(name, "matrix_");
|
|
724
|
+
if (raw == null) return null;
|
|
725
|
+
const method = ctorMethod(raw);
|
|
726
|
+
const params = MATRIX_SIGS[method];
|
|
727
|
+
if (params == null) return null;
|
|
728
|
+
return emitHostCall("matrix", MATRIX_HOST_ALIAS[method] ?? method, args, params);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function drawKindAndMethod(name: string, methodSrc: expr | null): { kind: string; method: string } | null {
|
|
732
|
+
if (methodSrc != null && methodSrc.kind === "Name") {
|
|
733
|
+
const id = (methodSrc as Name).id;
|
|
734
|
+
if (DRAW_KINDS.has(id)) return { kind: id, method: name };
|
|
735
|
+
}
|
|
736
|
+
const us = name.indexOf("_");
|
|
737
|
+
if (us > 0) {
|
|
738
|
+
const kind = name.slice(0, us);
|
|
739
|
+
if (DRAW_KINDS.has(kind)) return { kind, method: name.slice(us + 1) };
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
745
|
+
label: {
|
|
746
|
+
set_text: [["id"], ["text"]],
|
|
747
|
+
set_xy: [["id"], ["x"], ["y"]],
|
|
748
|
+
set_color: [["id"], ["color"]],
|
|
749
|
+
set_style: [["id"], ["style"]],
|
|
750
|
+
set_size: [["id"], ["size"]],
|
|
751
|
+
set_tooltip: [["id"], ["tooltip"]],
|
|
752
|
+
set_textalign: [["id"], ["textalign"]],
|
|
753
|
+
delete: [["id"]],
|
|
754
|
+
get_text: [["id"]],
|
|
755
|
+
get_x: [["id"]],
|
|
756
|
+
get_y: [["id"]],
|
|
757
|
+
},
|
|
758
|
+
line: {
|
|
759
|
+
set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
|
|
760
|
+
set_xy1: [["id"], ["x1"], ["y1"]],
|
|
761
|
+
set_xy2: [["id"], ["x2"], ["y2"]],
|
|
762
|
+
set_color: [["id"], ["color"]],
|
|
763
|
+
set_width: [["id"], ["width"]],
|
|
764
|
+
set_style: [["id"], ["style"]],
|
|
765
|
+
set_extend: [["id"], ["extend"]],
|
|
766
|
+
delete: [["id"]],
|
|
767
|
+
get_price: [["id"], ["x"]],
|
|
768
|
+
get_x1: [["id"]],
|
|
769
|
+
get_y1: [["id"]],
|
|
770
|
+
get_x2: [["id"]],
|
|
771
|
+
get_y2: [["id"]],
|
|
772
|
+
},
|
|
773
|
+
box: {
|
|
774
|
+
set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
775
|
+
set_lefttop: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
776
|
+
set_bgcolor: [["id"], ["bgcolor", "color"]],
|
|
777
|
+
set_border_color: [["id"], ["color", "border_color"]],
|
|
778
|
+
set_text: [["id"], ["text"]],
|
|
779
|
+
set_extend: [["id"], ["extend"]],
|
|
780
|
+
delete: [["id"]],
|
|
781
|
+
},
|
|
782
|
+
table: {
|
|
783
|
+
cell: [["id"], ["column", "col"], ["row"], ["text"]],
|
|
784
|
+
cell_set_text: [["id"], ["column", "col"], ["row"], ["text"]],
|
|
785
|
+
set_position: [["id"], ["position"]],
|
|
786
|
+
delete: [["id"]],
|
|
787
|
+
},
|
|
788
|
+
polyline: {
|
|
789
|
+
delete: [["id"]],
|
|
790
|
+
},
|
|
791
|
+
linefill: {
|
|
792
|
+
set_color: [["id"], ["color"]],
|
|
793
|
+
delete: [["id"]],
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
|
|
798
|
+
if (name === "alert" && methodSrc == null) {
|
|
799
|
+
ctx.usesDrawings = true;
|
|
800
|
+
return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
|
|
801
|
+
}
|
|
802
|
+
const parsed = drawKindAndMethod(name, methodSrc);
|
|
803
|
+
if (parsed == null) return null;
|
|
804
|
+
const { kind, method } = parsed;
|
|
805
|
+
let code: string | null = null;
|
|
806
|
+
if (method === "new") {
|
|
807
|
+
if (kind === "label") {
|
|
808
|
+
code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")})`;
|
|
809
|
+
} else if (kind === "linefill") {
|
|
810
|
+
code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1"], "null")}, ${pick(args, 1, ["id2"], "null")})`;
|
|
811
|
+
} else if (DRAW_KINDS.has(kind)) {
|
|
812
|
+
code = `__h.draw.${kind}_new(__bar_idx)`;
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
|
|
816
|
+
const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
|
|
817
|
+
if (params != null) {
|
|
818
|
+
code = emitHostCall("draw", `${kind}_${aliased}`, args, params);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (code != null) ctx.usesDrawings = true;
|
|
822
|
+
return code;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
|
|
826
|
+
if (methodSrc == null || methodSrc.kind !== "Name") return null;
|
|
827
|
+
const id = (methodSrc as Name).id;
|
|
828
|
+
const spec = ctx.udtTypes.get(id);
|
|
829
|
+
if (spec == null) return null;
|
|
830
|
+
if (name === "copy") {
|
|
831
|
+
return `({...${pick(args, 0, ["id", "this", "source"], "null")}})`;
|
|
832
|
+
}
|
|
833
|
+
if (name !== "new") return null;
|
|
834
|
+
const parts: string[] = [];
|
|
835
|
+
const usedKw = new Set<string>();
|
|
836
|
+
let posIdx = 0;
|
|
837
|
+
for (const field of spec.fields) {
|
|
838
|
+
let expr: string | undefined;
|
|
839
|
+
if (Object.hasOwn(args.kw, field)) {
|
|
840
|
+
expr = args.kw[field];
|
|
841
|
+
usedKw.add(field);
|
|
842
|
+
} else if (posIdx < args.pos.length) {
|
|
843
|
+
expr = args.pos[posIdx++];
|
|
844
|
+
} else if (Object.hasOwn(spec.defaults, field)) {
|
|
845
|
+
expr = spec.defaults[field];
|
|
846
|
+
}
|
|
847
|
+
if (expr != null) parts.push(`${field}: ${expr}`);
|
|
848
|
+
}
|
|
849
|
+
for (const [k, v] of Object.entries(args.kw)) {
|
|
850
|
+
if (usedKw.has(k)) continue;
|
|
851
|
+
parts.push(`${k}: ${v}`);
|
|
852
|
+
}
|
|
853
|
+
return `__h.udtNew(${JSON.stringify(id)}, {${parts.join(", ")}})`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const SECURITY_FUNCS = new Set([
|
|
857
|
+
"security",
|
|
858
|
+
"request_security",
|
|
859
|
+
"request_security_lower_tf",
|
|
860
|
+
"request_seed",
|
|
861
|
+
]);
|
|
862
|
+
|
|
863
|
+
const SIMPLE_CHART_EXPR = new Set([
|
|
864
|
+
"close",
|
|
865
|
+
"open",
|
|
866
|
+
"high",
|
|
867
|
+
"low",
|
|
868
|
+
"volume",
|
|
869
|
+
"time",
|
|
870
|
+
"hl2",
|
|
871
|
+
"hlc3",
|
|
872
|
+
"ohlc4",
|
|
873
|
+
]);
|
|
874
|
+
|
|
875
|
+
const SIMPLE_CHART_ARR = /^(?:open|high|low|close|vol|volume|time|hl2|hlc3|ohlc4)_arr\[__bar_idx\]$/;
|
|
876
|
+
|
|
877
|
+
/** `p.plus(1)` → method UDF with instance first. TA methods stay TA. */
|
|
878
|
+
function emitUdtMethod(
|
|
879
|
+
ctx: EmitCtx,
|
|
880
|
+
name: string,
|
|
881
|
+
methodSrc: expr | null,
|
|
882
|
+
args: CallArgs,
|
|
883
|
+
visit: VisitFn,
|
|
884
|
+
): string | null {
|
|
885
|
+
if (methodSrc == null || methodSrc.kind !== "Name") return null;
|
|
886
|
+
if (!ctx.udtMethodNames.has(name) && !ctx.userFuncs.has(name)) return null;
|
|
887
|
+
if (TA_METHODS.has(name)) return null;
|
|
888
|
+
return emitUserFuncCall(ctx, name, [visit(methodSrc), ...args.pos]);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Same-symbol OHLCV passthrough only. HA marker remaps OHLC. Foreign / complex expr → `null`. */
|
|
892
|
+
function emitRequest(ctx: EmitCtx, name: string, args: CallArgs): string | null {
|
|
893
|
+
if (SECURITY_FUNCS.has(name)) {
|
|
894
|
+
const symbol = pick(args, 0, ["symbol", "ticker"], "null");
|
|
895
|
+
const expression = pick(args, 2, ["expression", "expr"], "close");
|
|
896
|
+
if (isHeikinashiSecurity(symbol) && isSimpleSecurityExpr(expression)) {
|
|
897
|
+
ctx.needsHeikinashi = true;
|
|
898
|
+
return mapOhlcvExprToHeikinashi(expression);
|
|
899
|
+
}
|
|
900
|
+
if (isSimpleSecurityExpr(expression) && isChartSecuritySymbol(symbol)) return expression;
|
|
901
|
+
return "null";
|
|
902
|
+
}
|
|
903
|
+
if (name === "request_currency_rate" || name === "currency_rate") {
|
|
904
|
+
return `__h.currencyRate(${pick(args, 0, ["from", "from_currency"], "null")}, ${pick(args, 1, ["to", "to_currency"], "null")})`;
|
|
905
|
+
}
|
|
906
|
+
if (name.startsWith("request_")) return "null";
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function isSimpleSecurityExpr(expr: string): boolean {
|
|
911
|
+
const e = expr.trim();
|
|
912
|
+
if (!e) return false;
|
|
913
|
+
if (SIMPLE_CHART_EXPR.has(e)) return true;
|
|
914
|
+
return SIMPLE_CHART_ARR.test(e);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function unquoteJsString(s: string): string {
|
|
918
|
+
if (s.length >= 2 && ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))) {
|
|
919
|
+
if (s.startsWith('"')) {
|
|
920
|
+
try {
|
|
921
|
+
return JSON.parse(s) as string;
|
|
922
|
+
} catch {
|
|
923
|
+
return s.slice(1, -1);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return s.slice(1, -1);
|
|
927
|
+
}
|
|
928
|
+
return s;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function isHeikinashiSecurity(sym: string): boolean {
|
|
932
|
+
const s = sym.trim();
|
|
933
|
+
if (s === "__HEIKINASHI__" || s === "'__HEIKINASHI__'" || s === '"__HEIKINASHI__"') return true;
|
|
934
|
+
if (/^['"]__HEIKINASHI__['"]$/.test(s)) return true;
|
|
935
|
+
if (s.includes("__HEIKINASHI__") && !s.toUpperCase().includes("HA(")) return true;
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const HA_OHLC_MAP: Record<string, string> = {
|
|
940
|
+
open: "ha_open_arr[__bar_idx]",
|
|
941
|
+
high: "ha_high_arr[__bar_idx]",
|
|
942
|
+
low: "ha_low_arr[__bar_idx]",
|
|
943
|
+
close: "ha_close_arr[__bar_idx]",
|
|
944
|
+
Open: "ha_open_arr[__bar_idx]",
|
|
945
|
+
High: "ha_high_arr[__bar_idx]",
|
|
946
|
+
Low: "ha_low_arr[__bar_idx]",
|
|
947
|
+
Close: "ha_close_arr[__bar_idx]",
|
|
948
|
+
open_arr: "ha_open_arr",
|
|
949
|
+
high_arr: "ha_high_arr",
|
|
950
|
+
low_arr: "ha_low_arr",
|
|
951
|
+
close_arr: "ha_close_arr",
|
|
952
|
+
Open_arr: "ha_open_arr",
|
|
953
|
+
High_arr: "ha_high_arr",
|
|
954
|
+
Low_arr: "ha_low_arr",
|
|
955
|
+
Close_arr: "ha_close_arr",
|
|
956
|
+
"open_arr[__bar_idx]": "ha_open_arr[__bar_idx]",
|
|
957
|
+
"high_arr[__bar_idx]": "ha_high_arr[__bar_idx]",
|
|
958
|
+
"low_arr[__bar_idx]": "ha_low_arr[__bar_idx]",
|
|
959
|
+
"close_arr[__bar_idx]": "ha_close_arr[__bar_idx]",
|
|
960
|
+
"Open_arr[__bar_idx]": "ha_open_arr[__bar_idx]",
|
|
961
|
+
"High_arr[__bar_idx]": "ha_high_arr[__bar_idx]",
|
|
962
|
+
"Low_arr[__bar_idx]": "ha_low_arr[__bar_idx]",
|
|
963
|
+
"Close_arr[__bar_idx]": "ha_close_arr[__bar_idx]",
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
/** Chart OHLC → HA arrays. volume / time stay as chart. */
|
|
967
|
+
function mapOhlcvExprToHeikinashi(expr: string): string {
|
|
968
|
+
return HA_OHLC_MAP[expr.trim()] ?? expr;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function isChartSecuritySymbol(sym: string): boolean {
|
|
972
|
+
const s = sym.trim();
|
|
973
|
+
if (!s || s === "null" || s === "na" || s === "undefined" || s === "None" || s === "np.nan") {
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
if (isHeikinashiSecurity(s)) return true;
|
|
977
|
+
const inner = unquoteJsString(s).trim();
|
|
978
|
+
if (!inner || inner === "na" || inner === "null" || inner === "SYMBOL") return true;
|
|
979
|
+
if (s === "SYMBOL" || s === "tickerid" || s === "ticker") return true;
|
|
980
|
+
if (s === "syminfo_ticker" || s === "syminfo_tickerid") return true;
|
|
981
|
+
const low = s.toLowerCase();
|
|
982
|
+
if (low.includes("syminfo") && (s.includes("ticker") || s.includes("tickerid") || s.includes("root") || s.includes("prefix"))) {
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function constString(node: expr | undefined): string | null {
|
|
989
|
+
if (node == null || node.kind !== "Constant") return null;
|
|
990
|
+
const v = (node as Constant).value;
|
|
991
|
+
return typeof v === "string" ? v : null;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isStrategyAction(name: string, method: string): boolean {
|
|
995
|
+
return (
|
|
996
|
+
STRATEGY_ACTIONS.has(name) ||
|
|
997
|
+
method === "entry" ||
|
|
998
|
+
method === "close" ||
|
|
999
|
+
method === "close_all" ||
|
|
1000
|
+
method === "exit" ||
|
|
1001
|
+
method === "order" ||
|
|
1002
|
+
method === "cancel" ||
|
|
1003
|
+
method === "cancel_all" ||
|
|
1004
|
+
method.startsWith("risk_") ||
|
|
1005
|
+
name.startsWith("strategy_entry") ||
|
|
1006
|
+
name.startsWith("strategy_risk")
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** `strategy(...)` declaration — kwargs only, matching interpret `applyStrategyDecl`. */
|
|
1011
|
+
function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
|
|
1012
|
+
ctx.usesStrategy = true;
|
|
1013
|
+
const fields: string[] = [];
|
|
1014
|
+
const add = (names: string[], key: string): void => {
|
|
1015
|
+
if (hasArg(args, -1, names)) fields.push(`${key}: ${pick(args, -1, names, "null")}`);
|
|
1016
|
+
};
|
|
1017
|
+
add(["commission", "commission_value"], "commission");
|
|
1018
|
+
add(["slippage"], "slippage");
|
|
1019
|
+
add(["pyramiding"], "pyramiding");
|
|
1020
|
+
add(["avg_price_model"], "avg_price_model");
|
|
1021
|
+
add(["leverage"], "leverage");
|
|
1022
|
+
add(["margin_long"], "margin_long");
|
|
1023
|
+
add(["margin_short"], "margin_short");
|
|
1024
|
+
add(["default_qty_type"], "default_qty_type");
|
|
1025
|
+
add(["default_qty_value"], "default_qty_value");
|
|
1026
|
+
add(["initial_capital"], "initial_capital");
|
|
1027
|
+
return `__h.strategy.configure({${fields.join(", ")}})`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function emitStrategyEntry(method: "entry" | "order", args: CallArgs): string {
|
|
1031
|
+
const id = pick(args, 0, ["id"], "null");
|
|
1032
|
+
const direction = pick(args, 1, ["direction"], "null");
|
|
1033
|
+
const qty = pick(args, 2, ["qty"], "null");
|
|
1034
|
+
const fields: string[] = [];
|
|
1035
|
+
if (hasArg(args, 3, ["limit"])) fields.push(`limit: ${pick(args, 3, ["limit"], "null")}`);
|
|
1036
|
+
if (hasArg(args, 4, ["stop"])) fields.push(`stop: ${pick(args, 4, ["stop"], "null")}`);
|
|
1037
|
+
if (hasArg(args, 5, ["comment"])) fields.push(`comment: ${pick(args, 5, ["comment"], "null")}`);
|
|
1038
|
+
return `__h.strategy.${method}(${id}, ${direction}, ${qty}, {${fields.join(", ")}})`;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
|
|
1042
|
+
const method = name.startsWith("strategy_") ? name.slice("strategy_".length) : name;
|
|
1043
|
+
|
|
1044
|
+
if (method === "long" || method === "short") return JSON.stringify(method);
|
|
1045
|
+
|
|
1046
|
+
if (isStrategyAction(name, method) || STRATEGY_QUERIES.has(name)) ctx.usesStrategy = true;
|
|
1047
|
+
|
|
1048
|
+
switch (method) {
|
|
1049
|
+
case "entry":
|
|
1050
|
+
case "order":
|
|
1051
|
+
ctx.usesStrategy = true;
|
|
1052
|
+
return emitStrategyEntry(method, args);
|
|
1053
|
+
case "close": {
|
|
1054
|
+
ctx.usesStrategy = true;
|
|
1055
|
+
const id = pick(args, 0, ["id"], "null");
|
|
1056
|
+
return hasArg(args, 1, ["qty"])
|
|
1057
|
+
? `__h.strategy.close(${id}, ${pick(args, 1, ["qty"], "null")})`
|
|
1058
|
+
: `__h.strategy.close(${id})`;
|
|
1059
|
+
}
|
|
1060
|
+
case "close_all":
|
|
1061
|
+
ctx.usesStrategy = true;
|
|
1062
|
+
return "__h.strategy.close_all()";
|
|
1063
|
+
case "exit": {
|
|
1064
|
+
ctx.usesStrategy = true;
|
|
1065
|
+
const id = pick(args, 0, ["id"], "null");
|
|
1066
|
+
const fields: string[] = [];
|
|
1067
|
+
const add = (index: number, names: string[], key: string): void => {
|
|
1068
|
+
if (hasArg(args, index, names)) fields.push(`${key}: ${pick(args, index, names, "null")}`);
|
|
1069
|
+
};
|
|
1070
|
+
add(1, ["from_entry"], "from_entry");
|
|
1071
|
+
add(2, ["qty"], "qty");
|
|
1072
|
+
add(3, ["qty_percent"], "qty_percent");
|
|
1073
|
+
add(4, ["profit"], "profit");
|
|
1074
|
+
add(5, ["limit"], "limit");
|
|
1075
|
+
add(6, ["loss"], "loss");
|
|
1076
|
+
add(7, ["stop"], "stop");
|
|
1077
|
+
add(8, ["trail_price"], "trail_price");
|
|
1078
|
+
add(9, ["trail_points"], "trail_points");
|
|
1079
|
+
add(10, ["trail_offset"], "trail_offset");
|
|
1080
|
+
return `__h.strategy.exit(${id}, {${fields.join(", ")}})`;
|
|
1081
|
+
}
|
|
1082
|
+
case "cancel":
|
|
1083
|
+
ctx.usesStrategy = true;
|
|
1084
|
+
return `__h.strategy.cancel(${pick(args, 0, ["id"], "null")})`;
|
|
1085
|
+
case "cancel_all":
|
|
1086
|
+
ctx.usesStrategy = true;
|
|
1087
|
+
return "__h.strategy.cancel_all()";
|
|
1088
|
+
case "position_size":
|
|
1089
|
+
case "netprofit":
|
|
1090
|
+
case "equity":
|
|
1091
|
+
case "openprofit":
|
|
1092
|
+
case "opentrades":
|
|
1093
|
+
case "closedtrades":
|
|
1094
|
+
case "leverage":
|
|
1095
|
+
case "margin_liquidation_price":
|
|
1096
|
+
case "position_avg_price":
|
|
1097
|
+
case "initial_capital":
|
|
1098
|
+
ctx.usesStrategy = true;
|
|
1099
|
+
return `__h.strategy.${method}()`;
|
|
1100
|
+
case "risk_allow_entry_in":
|
|
1101
|
+
ctx.usesStrategy = true;
|
|
1102
|
+
return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
|
|
1103
|
+
case "risk_max_position_size":
|
|
1104
|
+
ctx.usesStrategy = true;
|
|
1105
|
+
return `__h.strategy.risk_max_position_size(${pick(args, 0, ["percent", "value"], "null")})`;
|
|
1106
|
+
case "risk_max_drawdown":
|
|
1107
|
+
ctx.usesStrategy = true;
|
|
1108
|
+
return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
|
|
1109
|
+
default:
|
|
1110
|
+
if (isStrategyAction(name, method)) ctx.usesStrategy = true;
|
|
1111
|
+
return "null";
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function seriesNameRef(ctx: EmitCtx, id: string): string {
|
|
1116
|
+
const chart = CHART_SERIES_ARR[id];
|
|
1117
|
+
if (chart != null) return chart;
|
|
1118
|
+
const raw = `${id}_arr`;
|
|
1119
|
+
const safe = `__u_${id}_arr`;
|
|
1120
|
+
if (ctx.arrays.has(raw)) return raw;
|
|
1121
|
+
if (ctx.arrays.has(safe)) return safe;
|
|
1122
|
+
return raw;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function seriesArg(ctx: EmitCtx, node: expr | undefined, visited: string): string {
|
|
1126
|
+
if (node != null && node.kind === "Name") return seriesNameRef(ctx, (node as Name).id);
|
|
1127
|
+
return visited || "null";
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function packUserFuncArgs(ctx: EmitCtx, name: string, args: CallArgs): string[] {
|
|
1131
|
+
const formals = ctx.funcParamNames.get(name);
|
|
1132
|
+
const series = ctx.funcSeriesParams.get(name);
|
|
1133
|
+
if (formals == null || formals.length === 0) return args.pos.slice();
|
|
1134
|
+
|
|
1135
|
+
const packed: string[] = [];
|
|
1136
|
+
for (let i = 0; i < formals.length; i++) {
|
|
1137
|
+
const formal = formals[i]!;
|
|
1138
|
+
const fromPos = i < args.pos.length;
|
|
1139
|
+
const node = fromPos ? args.posNodes[i] : args.kwNodes[formal];
|
|
1140
|
+
const visited = fromPos ? args.pos[i]! : (args.kw[formal] ?? "null");
|
|
1141
|
+
packed.push(series?.has(formal) ? seriesArg(ctx, node, visited) : visited);
|
|
1142
|
+
}
|
|
1143
|
+
for (let i = formals.length; i < args.pos.length; i++) packed.push(args.pos[i]!);
|
|
1144
|
+
return packed;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
|
|
1148
|
+
let title: string | null = constString(args.kwNodes.title);
|
|
1149
|
+
if (title == null) {
|
|
1150
|
+
for (const n of args.posNodes) {
|
|
1151
|
+
const s = constString(n);
|
|
1152
|
+
if (s != null) {
|
|
1153
|
+
title = s;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (title == null || title === "") {
|
|
1159
|
+
if (fname === "plotcandle") title = "candles";
|
|
1160
|
+
else if (fname === "plotbar") title = "bars";
|
|
1161
|
+
else title = `plot_${ctx.plots.length}`;
|
|
1162
|
+
}
|
|
1163
|
+
// Python primary column for plotcandle/plotbar is close (arg 3), not open.
|
|
1164
|
+
const ohlc = fname === "plotcandle" || fname === "plotbar";
|
|
1165
|
+
const series = ohlc
|
|
1166
|
+
? pick(args, 3, ["close"], "null")
|
|
1167
|
+
: pick(args, 0, ["series", "source", "price"], "null");
|
|
1168
|
+
const i = ctx.plots.length;
|
|
1169
|
+
ctx.plots.push({ title });
|
|
1170
|
+
return `plot_${i}[__bar_idx] = __h.naNum(${series})`;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function emitColor(name: string, args: CallArgs): string | null {
|
|
1174
|
+
if (name === "color_r") return `__h.colorR(${pick(args, 0, ["color"], "null")})`;
|
|
1175
|
+
if (name === "color_g") return `__h.colorG(${pick(args, 0, ["color"], "null")})`;
|
|
1176
|
+
if (name === "color_b") return `__h.colorB(${pick(args, 0, ["color"], "null")})`;
|
|
1177
|
+
if (name === "color_t") return `__h.colorT(${pick(args, 0, ["color"], "null")})`;
|
|
1178
|
+
if (name === "color_rgb") {
|
|
1179
|
+
const r = pick(args, 0, ["red", "r"], "null");
|
|
1180
|
+
const g = pick(args, 1, ["green", "g"], "null");
|
|
1181
|
+
const b = pick(args, 2, ["blue", "b"], "null");
|
|
1182
|
+
if (hasArg(args, 3, ["transp", "t", "transparency"])) {
|
|
1183
|
+
return `__h.colorRgb(${r}, ${g}, ${b}, ${pick(args, 3, ["transp", "t", "transparency"], "null")})`;
|
|
1184
|
+
}
|
|
1185
|
+
return `__h.colorRgb(${r}, ${g}, ${b})`;
|
|
1186
|
+
}
|
|
1187
|
+
if (name === "color_from_gradient") {
|
|
1188
|
+
return `__h.colorFromGradient(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["bottom_value", "bottom"], "null")}, ${pick(args, 2, ["top_value", "top"], "null")}, ${pick(args, 3, ["color1"], "null")}, ${pick(args, 4, ["color2"], "null")})`;
|
|
1189
|
+
}
|
|
1190
|
+
if (name === "color_new" || name === "color") {
|
|
1191
|
+
const color = pick(args, 0, ["color"], "null");
|
|
1192
|
+
if (hasArg(args, 1, ["transp", "transparency"])) {
|
|
1193
|
+
return `__h.colorNew(${color}, ${pick(args, 1, ["transp", "transparency"], "null")})`;
|
|
1194
|
+
}
|
|
1195
|
+
return `__h.colorNew(${color})`;
|
|
1196
|
+
}
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function emitInput(ctx: EmitCtx, args: CallArgs): string {
|
|
1201
|
+
const def = pick(args, 0, ["defval"], "null");
|
|
1202
|
+
return `__h.input(${JSON.stringify(inputKey(ctx, args))}, ${def})`;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function inputKey(ctx: EmitCtx, args: CallArgs): string {
|
|
1206
|
+
const titleKw = constString(args.kwNodes.title);
|
|
1207
|
+
if (titleKw != null && titleKw !== "") return titleKw;
|
|
1208
|
+
const defvalKw = constString(args.kwNodes.defval);
|
|
1209
|
+
if (defvalKw != null && defvalKw !== "") return defvalKw;
|
|
1210
|
+
if (args.posNodes.length > 1) {
|
|
1211
|
+
const posTitle = constString(args.posNodes[1]);
|
|
1212
|
+
if (posTitle != null && posTitle !== "") return posTitle;
|
|
1213
|
+
}
|
|
1214
|
+
const bag = ctx as EmitCtx & { nextInput?: number };
|
|
1215
|
+
const i = bag.nextInput ?? 0;
|
|
1216
|
+
bag.nextInput = i + 1;
|
|
1217
|
+
return i === 0 ? "input" : `input${i}`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function emitMath(name: string, args: CallArgs): string | null {
|
|
1221
|
+
const bare = name.startsWith("math_") ? name.slice(5) : name;
|
|
1222
|
+
const isNs = name.startsWith("math_");
|
|
1223
|
+
const isBareMath =
|
|
1224
|
+
name === "abs" ||
|
|
1225
|
+
name === "max" ||
|
|
1226
|
+
name === "min" ||
|
|
1227
|
+
name === "sqrt" ||
|
|
1228
|
+
name === "log" ||
|
|
1229
|
+
name === "log10" ||
|
|
1230
|
+
name === "pow" ||
|
|
1231
|
+
name === "sign" ||
|
|
1232
|
+
name === "floor" ||
|
|
1233
|
+
name === "ceil" ||
|
|
1234
|
+
name === "round" ||
|
|
1235
|
+
name === "exp" ||
|
|
1236
|
+
name === "sin" ||
|
|
1237
|
+
name === "cos" ||
|
|
1238
|
+
name === "tan" ||
|
|
1239
|
+
name === "asin" ||
|
|
1240
|
+
name === "acos" ||
|
|
1241
|
+
name === "atan" ||
|
|
1242
|
+
name === "avg" ||
|
|
1243
|
+
name === "random" ||
|
|
1244
|
+
name === "todegrees" ||
|
|
1245
|
+
name === "toradians" ||
|
|
1246
|
+
name === "iff" ||
|
|
1247
|
+
name === "fixnan" ||
|
|
1248
|
+
name === "round_to_mintick";
|
|
1249
|
+
if (!isNs && !isBareMath) return null;
|
|
1250
|
+
|
|
1251
|
+
if (bare === "round_to_mintick") {
|
|
1252
|
+
return `__h.roundToMintick(${pick(args, 0, ["number", "x"], "null")})`;
|
|
1253
|
+
}
|
|
1254
|
+
if (bare === "iff") {
|
|
1255
|
+
const cond = pick(args, 0, ["condition", "cond"], "null");
|
|
1256
|
+
const a = pick(args, 1, ["then", "if_true"], "null");
|
|
1257
|
+
const b = pick(args, 2, ["else", "if_false"], "null");
|
|
1258
|
+
return `(__h.nz(${cond}) ? ${a} : ${b})`;
|
|
1259
|
+
}
|
|
1260
|
+
if (bare === "fixnan") {
|
|
1261
|
+
return `__h.nz(${pick(args, 0, ["source", "x"], "null")}, 0)`;
|
|
1262
|
+
}
|
|
1263
|
+
if (bare === "max" || bare === "min") {
|
|
1264
|
+
const xs = variadic(args);
|
|
1265
|
+
if (xs.length === 0) return "null";
|
|
1266
|
+
if (xs.length === 1) return naUnary(xs[0]!, "__x");
|
|
1267
|
+
const init = bare === "max" ? "-Infinity" : "Infinity";
|
|
1268
|
+
const cmp = bare === "max" ? "__x > __m" : "__x < __m";
|
|
1269
|
+
return `((...__xs) => { if (__xs.length === 0) return null; let __m = ${init}; for (const __x of __xs) { if (__h.isNa(__x)) return null; if (${cmp}) __m = Number(__x); } return Number.isFinite(__m) ? __m : null; })(${xs.join(", ")})`;
|
|
1270
|
+
}
|
|
1271
|
+
if (bare === "avg") {
|
|
1272
|
+
const xs = variadic(args);
|
|
1273
|
+
if (xs.length === 0) return "null";
|
|
1274
|
+
if (xs.length === 1) return naUnary(xs[0]!, "__x");
|
|
1275
|
+
return `((...__xs) => { let __s = 0; for (const __x of __xs) { if (__h.isNa(__x)) return null; __s += Number(__x); } const __r = __s / __xs.length; return Number.isFinite(__r) ? __r : null; })(${xs.join(", ")})`;
|
|
1276
|
+
}
|
|
1277
|
+
if (bare === "sum" && isNs) {
|
|
1278
|
+
const xs = variadic(args);
|
|
1279
|
+
if (xs.length === 0) return "null";
|
|
1280
|
+
return `((...__xs) => { let __s = 0; for (const __x of __xs) { if (__h.isNa(__x)) return null; __s += Number(__x); } return Number.isFinite(__s) ? __s : null; })(${xs.join(", ")})`;
|
|
1281
|
+
}
|
|
1282
|
+
if (bare === "pow") {
|
|
1283
|
+
const b = pick(args, 0, ["base", "number", "x"], "null");
|
|
1284
|
+
const e = hasArg(args, 1, ["exponent", "exp", "y"]) ? pick(args, 1, ["exponent", "exp", "y"], "2") : "2";
|
|
1285
|
+
return `((__b, __e) => { if (__h.isNa(__b) || __h.isNa(__e)) return null; const __r = Math.pow(Number(__b), Number(__e)); return Number.isFinite(__r) ? __r : null; })(${b}, ${e})`;
|
|
1286
|
+
}
|
|
1287
|
+
if (bare === "round") {
|
|
1288
|
+
const x = pick(args, 0, ["number", "x"], "null");
|
|
1289
|
+
if (!hasArg(args, 1, ["precision"])) {
|
|
1290
|
+
return `((__x) => { if (__h.isNa(__x)) return null; const __n = Number(__x); const __r = __n >= 0 ? Math.round(__n) : -Math.round(-__n); return Number.isFinite(__r) ? __r : null; })(${x})`;
|
|
1291
|
+
}
|
|
1292
|
+
const p = pick(args, 1, ["precision"], "0");
|
|
1293
|
+
return `((__x, __p) => { if (__h.isNa(__x) || __h.isNa(__p)) return null; const __n = Number(__x); const __f = 10 ** Math.trunc(Number(__p)); if (!Number.isFinite(__f)) return null; const __y = __n * __f; const __r = (__y >= 0 ? Math.round(__y) : -Math.round(-__y)) / __f; return Number.isFinite(__r) ? __r : null; })(${x}, ${p})`;
|
|
1294
|
+
}
|
|
1295
|
+
if (bare === "random") {
|
|
1296
|
+
const a0 = hasArg(args, 0, ["min"]) ? pick(args, 0, ["min"], "null") : null;
|
|
1297
|
+
const a1 = hasArg(args, 1, ["max"]) ? pick(args, 1, ["max"], "null") : null;
|
|
1298
|
+
if (a0 == null) {
|
|
1299
|
+
return "((() => { const __r = Math.random(); return Number.isFinite(__r) ? __r : null; })())";
|
|
1300
|
+
}
|
|
1301
|
+
if (a1 == null) {
|
|
1302
|
+
return `((__hi) => { if (__h.isNa(__hi)) return null; const __r = Number(__hi) * Math.random(); return Number.isFinite(__r) ? __r : null; })(${a0})`;
|
|
1303
|
+
}
|
|
1304
|
+
return `((__lo, __hi) => { if (__h.isNa(__lo) || __h.isNa(__hi)) return null; const __r = Number(__lo) + (Number(__hi) - Number(__lo)) * Math.random(); return Number.isFinite(__r) ? __r : null; })(${a0}, ${a1})`;
|
|
1305
|
+
}
|
|
1306
|
+
if (bare === "log" && hasArg(args, 1, ["base"])) {
|
|
1307
|
+
const x = pick(args, 0, ["number", "x"], "null");
|
|
1308
|
+
const base = pick(args, 1, ["base"], "null");
|
|
1309
|
+
return `((__x, __b) => { if (__h.isNa(__x) || __h.isNa(__b) || Number(__x) <= 0 || Number(__b) <= 0 || Number(__b) === 1) return null; const __r = Math.log(Number(__x)) / Math.log(Number(__b)); return Number.isFinite(__r) ? __r : null; })(${x}, ${base})`;
|
|
1310
|
+
}
|
|
1311
|
+
const spec = MATH_UNARY[bare];
|
|
1312
|
+
if (spec != null) {
|
|
1313
|
+
const x = pick(args, 0, ["number", "x", "angle", "radians", "degrees"], "null");
|
|
1314
|
+
return naMath(x, spec.fn, spec.guard);
|
|
1315
|
+
}
|
|
1316
|
+
return isNs ? "null" : null;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function variadic(args: CallArgs): string[] {
|
|
1320
|
+
if (args.pos.length > 0) return args.pos;
|
|
1321
|
+
return Object.values(args.kw);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function naUnary(x: string, _bind: string): string {
|
|
1325
|
+
return `((__x) => (__h.isNa(__x) ? null : Number.isFinite(Number(__x)) ? Number(__x) : null))(${x})`;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function naMath(x: string, expr: string, guard?: string): string {
|
|
1329
|
+
const extra = guard != null ? ` || (${guard})` : "";
|
|
1330
|
+
return `((__x) => { if (__h.isNa(__x)${extra}) return null; const __r = ${expr}; return Number.isFinite(__r) ? __r : null; })(${x})`;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function allocSite(ctx: EmitCtx): string {
|
|
1334
|
+
return JSON.stringify("c" + ctx.nextSite++);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function taCall(method: string, site: string, parts: string[]): string {
|
|
1338
|
+
return `__h.ta.${method}(${site}, ${parts.join(", ")})`;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
|
|
1342
|
+
if (!TA_METHODS.has(method)) return "null";
|
|
1343
|
+
if (method === "ad") method = "accdist";
|
|
1344
|
+
const site = allocSite(ctx);
|
|
1345
|
+
const src = () => pick(args, 0, ["source", "series", "src"], CLOSE);
|
|
1346
|
+
const len = (index: number, fb: string, names: string[] = ["length", "len", "period"]) =>
|
|
1347
|
+
pick(args, index, names, fb);
|
|
1348
|
+
|
|
1349
|
+
if (SRC_LEN.has(method)) {
|
|
1350
|
+
return taCall(method, site, [src(), len(1, "14")]);
|
|
1351
|
+
}
|
|
1352
|
+
if (SRC_LEN1.has(method)) {
|
|
1353
|
+
return taCall(method, site, [src(), len(1, "1")]);
|
|
1354
|
+
}
|
|
1355
|
+
if (method === "highest" || method === "lowest") {
|
|
1356
|
+
const defSrc = method === "lowest" ? LOW : HIGH;
|
|
1357
|
+
if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
|
|
1358
|
+
// Python 1-arg period-only form (basic.py:222-246): source defaults to
|
|
1359
|
+
// the context high/low series and the length gates on _is_period_like
|
|
1360
|
+
// → non-period-like → na period 0 (interpret evalExtraTa).
|
|
1361
|
+
return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
|
|
1362
|
+
}
|
|
1363
|
+
return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
|
|
1364
|
+
}
|
|
1365
|
+
if (method === "highestbars" || method === "lowestbars") {
|
|
1366
|
+
const defSrc = method === "lowestbars" ? LOW : HIGH;
|
|
1367
|
+
if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
|
|
1368
|
+
// Python 1-arg period-only form (basic.py:248-291): context high/low
|
|
1369
|
+
// source, length gated like ta.highest (interpret evalExtraTa).
|
|
1370
|
+
return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
|
|
1371
|
+
}
|
|
1372
|
+
return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
|
|
1373
|
+
}
|
|
1374
|
+
if (method === "max" || method === "min") {
|
|
1375
|
+
const defSrc = method === "min" ? LOW : HIGH;
|
|
1376
|
+
if (args.pos.length >= 2 || Object.hasOwn(args.kw, "source") || Object.hasOwn(args.kw, "series")) {
|
|
1377
|
+
return taCall(method, site, [src(), len(1, "14")]);
|
|
1378
|
+
}
|
|
1379
|
+
if (args.pos.length === 1 || Object.hasOwn(args.kw, "length")) {
|
|
1380
|
+
return taCall(method, site, [defSrc, len(0, "14")]);
|
|
1381
|
+
}
|
|
1382
|
+
return taCall(method, site, [defSrc, "14"]);
|
|
1383
|
+
}
|
|
1384
|
+
if (method === "atr") {
|
|
1385
|
+
if (args.pos.length >= 4) {
|
|
1386
|
+
return taCall("atr", site, [args.pos[0]!, args.pos[1]!, args.pos[2]!, args.pos[3]!]);
|
|
1387
|
+
}
|
|
1388
|
+
return taCall("atr", site, [HIGH, LOW, CLOSE, len(0, "14")]);
|
|
1389
|
+
}
|
|
1390
|
+
if (method === "tr") {
|
|
1391
|
+
if (args.pos.length >= 3) {
|
|
1392
|
+
return taCall("tr", site, [args.pos[0]!, args.pos[1]!, args.pos[2]!]);
|
|
1393
|
+
}
|
|
1394
|
+
return taCall("tr", site, [HIGH, LOW, CLOSE]);
|
|
1395
|
+
}
|
|
1396
|
+
if (method === "cross" || method === "crossover" || method === "crossunder") {
|
|
1397
|
+
const a = pick(args, 0, ["source", "series", "a"], CLOSE);
|
|
1398
|
+
const b = pick(args, 1, ["source2", "b", "series2"], "0");
|
|
1399
|
+
return taCall(method, site, [a, b]);
|
|
1400
|
+
}
|
|
1401
|
+
if (method === "macd") {
|
|
1402
|
+
const call = taCall("macd", site, [
|
|
1403
|
+
src(),
|
|
1404
|
+
pick(args, 1, ["fastlen", "fastLength", "fast"], "12"),
|
|
1405
|
+
pick(args, 2, ["slowlen", "slowLength", "slow"], "26"),
|
|
1406
|
+
pick(args, 3, ["signal", "signalLength", "siglen"], "9"),
|
|
1407
|
+
]);
|
|
1408
|
+
return `(() => { const __r = ${call}; return [__r.macd, __r.signal, __r.hist]; })()`;
|
|
1409
|
+
}
|
|
1410
|
+
if (method === "bb") {
|
|
1411
|
+
if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
|
|
1412
|
+
// Python period-first form (basic.py:394-401): (length, mult) with the
|
|
1413
|
+
// series defaulting to the context close. Dispatch gates slot 0 on
|
|
1414
|
+
// _is_period_like per bar (kwarg-first resolution, like interpret
|
|
1415
|
+
// callArg) — a non-period-like slot falls through to the series arm on
|
|
1416
|
+
// the raw slot-0 value, exactly like interpret.
|
|
1417
|
+
const p0Expr = pick(args, 0, ["source", "series", "length"], "null");
|
|
1418
|
+
const multPf = finiteOr(pick(args, 1, ["mult", "multiplier"], "null"), "NaN");
|
|
1419
|
+
const lenFt = finiteOr(pick(args, 1, ["length"], "null"), "0");
|
|
1420
|
+
const multFt = finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2");
|
|
1421
|
+
const pf = `(() => { const __r = __h.ta.bb(${site}, close_arr[__bar_idx], __p0, ${multPf}); return [__r.mid, __r.up, __r.lo]; })()`;
|
|
1422
|
+
const ft = `(() => { const __r = __h.ta.bb(${site}, __s0, ${lenFt}, ${multFt}); return [__r.mid, __r.up, __r.lo]; })()`;
|
|
1423
|
+
return `((__p0, __s0) => (Number.isInteger(__p0) ? ${pf} : ${ft}))(${p0Expr}, ${pick(args, 0, ["source", "series"], "null")})`;
|
|
1424
|
+
}
|
|
1425
|
+
const call = taCall("bb", site, [
|
|
1426
|
+
src(),
|
|
1427
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1428
|
+
finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2"),
|
|
1429
|
+
]);
|
|
1430
|
+
return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
|
|
1431
|
+
}
|
|
1432
|
+
if (method === "bbw") {
|
|
1433
|
+
return taCall("bbw", site, [src(), len(1, "20"), pick(args, 2, ["mult", "multiplier"], "2")]);
|
|
1434
|
+
}
|
|
1435
|
+
if (method === "kc") {
|
|
1436
|
+
const call = taCall("kc", site, [
|
|
1437
|
+
HIGH,
|
|
1438
|
+
LOW,
|
|
1439
|
+
src(),
|
|
1440
|
+
len(1, "20"),
|
|
1441
|
+
pick(args, 2, ["mult", "multiplier"], "1"),
|
|
1442
|
+
]);
|
|
1443
|
+
return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
|
|
1444
|
+
}
|
|
1445
|
+
if (method === "stoch") {
|
|
1446
|
+
if (args.kw.high == null && args.pos.length <= 2) {
|
|
1447
|
+
// Python period-only / community forms (oscillators.py:58-78): %K over
|
|
1448
|
+
// the context close/high/low. Both slots gate on _is_period_like; in
|
|
1449
|
+
// the 2-arg (kLength, dPeriod) form dPeriod is ignored (Python quirk).
|
|
1450
|
+
const length =
|
|
1451
|
+
args.pos.length === 2
|
|
1452
|
+
? `(((__k, __d) => ((Number.isInteger(__k) && Number.isInteger(__d)) ? __k : 0))(${pick(args, 0, ["length"], "null")}, ${pick(args, 1, ["d", "smooth"], "null")}))`
|
|
1453
|
+
: periodGate(pick(args, 0, ["length"], "null"), "0");
|
|
1454
|
+
return taCall("stoch", site, [CLOSE, HIGH, LOW, length]);
|
|
1455
|
+
}
|
|
1456
|
+
return taCall("stoch", site, [
|
|
1457
|
+
src(),
|
|
1458
|
+
pick(args, 1, ["high"], HIGH),
|
|
1459
|
+
pick(args, 2, ["low"], LOW),
|
|
1460
|
+
finiteOr(pick(args, 3, ["length"], "null"), "0"),
|
|
1461
|
+
]);
|
|
1462
|
+
}
|
|
1463
|
+
if (method === "vwma") {
|
|
1464
|
+
if (args.pos.length <= 1 && !hasAnyKw(args, ["volume"])) {
|
|
1465
|
+
// Python 1-arg period-only form (basic.py:80-88): close + volume from
|
|
1466
|
+
// the chart context; length gates on _is_period_like → na period 0.
|
|
1467
|
+
return taCall("vwma", site, [CLOSE, VOL, periodGate(pick(args, 0, ["length"], "null"), "0")]);
|
|
1468
|
+
}
|
|
1469
|
+
if (args.pos.length >= 3 || hasAnyKw(args, ["volume"])) {
|
|
1470
|
+
// Python 3-arg community form (basic.py:89-100): (source, volume,
|
|
1471
|
+
// length). A period-like middle slot swaps volume back to the context
|
|
1472
|
+
// series (basic.py:93/97) — mirror interpret's per-bar swap.
|
|
1473
|
+
const vol = `((__v) => (Number.isInteger(__v) ? ${VOL} : __v))(${pick(args, 1, ["volume"], "null")})`;
|
|
1474
|
+
return taCall("vwma", site, [src(), vol, finiteOr(pick(args, 2, ["length", "period"], "null"), "0")]);
|
|
1475
|
+
}
|
|
1476
|
+
return taCall("vwma", site, [src(), VOL, finiteOr(pick(args, 1, ["length"], "null"), "0")]);
|
|
1477
|
+
}
|
|
1478
|
+
if (method === "cci") {
|
|
1479
|
+
if (args.pos.length >= 4) {
|
|
1480
|
+
// Python legacy 4-arg form (oscillators.py:178-187): typical price of
|
|
1481
|
+
// the given (high, low, close) series. Slots resolve kwarg-first.
|
|
1482
|
+
const tp = `((__h, __l, __c) => ((__h != null && __l != null && __c != null) ? (__h + __l + __c) / 3 : __c))(${pick(args, 0, ["high"], "null")}, ${pick(args, 1, ["low"], "null")}, ${pick(args, 2, ["close"], "null")})`;
|
|
1483
|
+
return taCall("cci", site, [tp, finiteOr(pick(args, 3, ["length"], "null"), "0")]);
|
|
1484
|
+
}
|
|
1485
|
+
if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
|
|
1486
|
+
// Python 1-arg period-only form (oscillators.py:159-171): typical price
|
|
1487
|
+
// from the context high/low/close; length gates on _is_period_like.
|
|
1488
|
+
return taCall("cci", site, [TP, periodGate(pick(args, 0, ["length"], "null"), "0")]);
|
|
1489
|
+
}
|
|
1490
|
+
// Python 2-arg form (oscillators.py:172-177): CCI over the source alone.
|
|
1491
|
+
return taCall("cci", site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
|
|
1492
|
+
}
|
|
1493
|
+
if (method === "mfi") {
|
|
1494
|
+
if (args.pos.length <= 1) {
|
|
1495
|
+
return taCall("mfi", site, [HIGH, LOW, CLOSE, VOL, len(0, "14")]);
|
|
1496
|
+
}
|
|
1497
|
+
if (args.pos.length === 2) {
|
|
1498
|
+
return taCall("mfi", site, [src(), src(), src(), VOL, len(1, "14")]);
|
|
1499
|
+
}
|
|
1500
|
+
return taCall("mfi", site, [
|
|
1501
|
+
pick(args, 0, ["high"], HIGH),
|
|
1502
|
+
pick(args, 1, ["low"], LOW),
|
|
1503
|
+
pick(args, 2, ["close"], CLOSE),
|
|
1504
|
+
pick(args, 3, ["volume"], VOL),
|
|
1505
|
+
len(4, "14"),
|
|
1506
|
+
]);
|
|
1507
|
+
}
|
|
1508
|
+
if (method === "tsi") {
|
|
1509
|
+
if (args.pos.length === 2) {
|
|
1510
|
+
return taCall("tsi", site, [CLOSE, pick(args, 1, ["long", "long_length"], "25"), pick(args, 0, ["short", "short_length"], "13")]);
|
|
1511
|
+
}
|
|
1512
|
+
return taCall("tsi", site, [
|
|
1513
|
+
src(),
|
|
1514
|
+
pick(args, 2, ["long", "long_length"], "25"),
|
|
1515
|
+
pick(args, 1, ["short", "short_length"], "13"),
|
|
1516
|
+
]);
|
|
1517
|
+
}
|
|
1518
|
+
if (method === "dmi") {
|
|
1519
|
+
const call = taCall("dmi", site, [
|
|
1520
|
+
HIGH,
|
|
1521
|
+
LOW,
|
|
1522
|
+
CLOSE,
|
|
1523
|
+
pick(args, 0, ["diLength", "length"], "14"),
|
|
1524
|
+
pick(args, 1, ["adxSmoothing", "adxlen"], "14"),
|
|
1525
|
+
]);
|
|
1526
|
+
return `(() => { const __r = ${call}; return [__r.plus, __r.minus, __r.adx]; })()`;
|
|
1527
|
+
}
|
|
1528
|
+
if (method === "supertrend") {
|
|
1529
|
+
const call = taCall("supertrend", site, [
|
|
1530
|
+
HIGH,
|
|
1531
|
+
LOW,
|
|
1532
|
+
CLOSE,
|
|
1533
|
+
pick(args, 0, ["factor"], "3"),
|
|
1534
|
+
pick(args, 1, ["atrPeriod", "length"], "10"),
|
|
1535
|
+
]);
|
|
1536
|
+
return `(() => { const __r = ${call}; return [__r.st, __r.dir]; })()`;
|
|
1537
|
+
}
|
|
1538
|
+
if (method === "alma") {
|
|
1539
|
+
return taCall("alma", site, [
|
|
1540
|
+
src(),
|
|
1541
|
+
len(1, "9"),
|
|
1542
|
+
pick(args, 2, ["offset"], "0.85"),
|
|
1543
|
+
pick(args, 3, ["sigma"], "6"),
|
|
1544
|
+
]);
|
|
1545
|
+
}
|
|
1546
|
+
if (method === "kama") {
|
|
1547
|
+
return taCall("kama", site, [
|
|
1548
|
+
src(),
|
|
1549
|
+
len(1, "14"),
|
|
1550
|
+
pick(args, 2, ["fastLength", "fast", "fastlen"], "2"),
|
|
1551
|
+
pick(args, 3, ["slowLength", "slow", "slowlen"], "30"),
|
|
1552
|
+
]);
|
|
1553
|
+
}
|
|
1554
|
+
if (method === "linreg") {
|
|
1555
|
+
return taCall("linreg", site, [src(), len(1, "14"), pick(args, 2, ["offset"], "0")]);
|
|
1556
|
+
}
|
|
1557
|
+
if (method === "barssince") {
|
|
1558
|
+
return taCall("barssince", site, [pick(args, 0, ["condition", "cond"], "null")]);
|
|
1559
|
+
}
|
|
1560
|
+
if (method === "valuewhen") {
|
|
1561
|
+
return taCall("valuewhen", site, [
|
|
1562
|
+
pick(args, 0, ["condition", "cond"], "null"),
|
|
1563
|
+
pick(args, 1, ["source", "series"], CLOSE),
|
|
1564
|
+
pick(args, 2, ["occurrence", "occ"], "0"),
|
|
1565
|
+
]);
|
|
1566
|
+
}
|
|
1567
|
+
if (method === "pivothigh" || method === "pivotlow") {
|
|
1568
|
+
if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
|
|
1569
|
+
// Python period-first form (basic.py:1007-1039): both slots gate on
|
|
1570
|
+
// _is_period_like and the source defaults to the context high/low
|
|
1571
|
+
// series. A non-period-like slot falls through to the series arm on
|
|
1572
|
+
// the raw slot values (never confirms on a scalar) — mirror interpret.
|
|
1573
|
+
// Slots resolve kwarg-first (leftbars/rightbars), like interpret callArg.
|
|
1574
|
+
const defSrc = method === "pivotlow" ? LOW : HIGH;
|
|
1575
|
+
const lExpr = pick(args, 0, ["leftbars", "left"], "null");
|
|
1576
|
+
const rExpr = pick(args, 1, ["rightbars", "right"], "null");
|
|
1577
|
+
const leftFt = finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5");
|
|
1578
|
+
const rightFt = finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5");
|
|
1579
|
+
const pf = taCall(method, site, [defSrc, "__l", "__r"]);
|
|
1580
|
+
const ft = taCall(method, site, ["__s", leftFt, rightFt]);
|
|
1581
|
+
return `((__l, __r, __s) => (((Number.isInteger(__l) && Number.isInteger(__r))) ? ${pf} : ${ft}))(${lExpr}, ${rExpr}, ${pick(args, 0, ["source", "series"], "null")})`;
|
|
1582
|
+
}
|
|
1583
|
+
// Interpret srcArg: source slot names ["source", "series"], missing → na
|
|
1584
|
+
// (a bare ta.pivothigh() never confirms, like interpret).
|
|
1585
|
+
return taCall(method, site, [
|
|
1586
|
+
pick(args, 0, ["source", "series"], "null"),
|
|
1587
|
+
finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5"),
|
|
1588
|
+
finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5"),
|
|
1589
|
+
]);
|
|
1590
|
+
}
|
|
1591
|
+
if (method === "swma") {
|
|
1592
|
+
if (hasArg(args, 1, ["length", "period"])) {
|
|
1593
|
+
return taCall("swma", site, [src(), len(1, "4")]);
|
|
1594
|
+
}
|
|
1595
|
+
return taCall("swma", site, [src()]);
|
|
1596
|
+
}
|
|
1597
|
+
if (method === "willr" || method === "wpr") {
|
|
1598
|
+
if (args.pos.length >= 4) {
|
|
1599
|
+
return taCall("willr", site, [args.pos[0]!, args.pos[1]!, args.pos[2]!, args.pos[3]!]);
|
|
1600
|
+
}
|
|
1601
|
+
return taCall("willr", site, [HIGH, LOW, CLOSE, len(0, "14")]);
|
|
1602
|
+
}
|
|
1603
|
+
if (method === "vwap") {
|
|
1604
|
+
return taCall("vwap", site, [
|
|
1605
|
+
pick(args, 0, ["source", "series", "src"], HLC3),
|
|
1606
|
+
pick(args, 1, ["volume"], VOL),
|
|
1607
|
+
]);
|
|
1608
|
+
}
|
|
1609
|
+
if (method === "sar") {
|
|
1610
|
+
return taCall("sar", site, [
|
|
1611
|
+
HIGH,
|
|
1612
|
+
LOW,
|
|
1613
|
+
pick(args, 0, ["start"], "0.02"),
|
|
1614
|
+
pick(args, 1, ["increment"], "0.02"),
|
|
1615
|
+
pick(args, 2, ["maximum", "max"], "0.2"),
|
|
1616
|
+
]);
|
|
1617
|
+
}
|
|
1618
|
+
if (method === "adx") {
|
|
1619
|
+
if (args.pos.length >= 4) {
|
|
1620
|
+
// Python legacy 4-arg form (common.py:718-726): (high, low, close,
|
|
1621
|
+
// length) with explicit series. Slots resolve kwarg-first.
|
|
1622
|
+
return taCall("adx", site, [
|
|
1623
|
+
pick(args, 0, ["source", "series"], "null"),
|
|
1624
|
+
pick(args, 1, ["low"], "null"),
|
|
1625
|
+
pick(args, 2, ["close"], "null"),
|
|
1626
|
+
finiteOr(pick(args, 3, ["length"], "null"), "0"),
|
|
1627
|
+
]);
|
|
1628
|
+
}
|
|
1629
|
+
if (args.pos.length === 2 && !hasAnyKw(args, ["high", "low", "close"])) {
|
|
1630
|
+
// Python 2-arg form (diLength, adxSmoothing) (common.py:709-717): both
|
|
1631
|
+
// slots gate on _is_period_like (kwarg-first resolution) and the ADX
|
|
1632
|
+
// period is the SECOND slot — diLength is accepted but unused. A
|
|
1633
|
+
// non-period-like slot → default 14.
|
|
1634
|
+
const period = `(((__di, __sm) => ((Number.isInteger(__di) && Number.isInteger(__sm)) ? __sm : 14))(${pick(args, 0, ["diLength", "length"], "null")}, ${pick(args, 1, ["adxSmoothing", "adxlen"], "null")}))`;
|
|
1635
|
+
return taCall("adx", site, [HIGH, LOW, CLOSE, period]);
|
|
1636
|
+
}
|
|
1637
|
+
// Python 1-arg period-only form gates length via _is_period_like
|
|
1638
|
+
// (common.py ~701): non-period-like → ignored → default 14 (as absent).
|
|
1639
|
+
return taCall("adx", site, [HIGH, LOW, CLOSE, periodGate(pick(args, 0, ["length"], "null"), "14")]);
|
|
1640
|
+
}
|
|
1641
|
+
if (method === "correlation") {
|
|
1642
|
+
return taCall("correlation", site, [
|
|
1643
|
+
pick(args, 0, ["source1", "source", "a"], CLOSE),
|
|
1644
|
+
pick(args, 1, ["source2", "b"], CLOSE),
|
|
1645
|
+
len(2, "14"),
|
|
1646
|
+
]);
|
|
1647
|
+
}
|
|
1648
|
+
if (method === "cum") {
|
|
1649
|
+
return taCall("cum", site, [src()]);
|
|
1650
|
+
}
|
|
1651
|
+
if (method === "accdist") {
|
|
1652
|
+
return taCall("accdist", site, [
|
|
1653
|
+
pick(args, 0, ["high"], HIGH),
|
|
1654
|
+
pick(args, 1, ["low"], LOW),
|
|
1655
|
+
pick(args, 2, ["close"], CLOSE),
|
|
1656
|
+
pick(args, 3, ["volume"], VOL),
|
|
1657
|
+
]);
|
|
1658
|
+
}
|
|
1659
|
+
// `vpt` is an alias of the pvt kernel on interpret (no ta.vpt engine fn).
|
|
1660
|
+
if (method === "pvt" || method === "vpt" || method === "obv" || method === "nvi" || method === "pvi") {
|
|
1661
|
+
return taCall(method === "vpt" ? "pvt" : method, site, [
|
|
1662
|
+
pick(args, 0, ["source", "close"], CLOSE),
|
|
1663
|
+
pick(args, 1, ["volume"], VOL),
|
|
1664
|
+
]);
|
|
1665
|
+
}
|
|
1666
|
+
if (method === "wad") {
|
|
1667
|
+
return taCall("wad", site, [
|
|
1668
|
+
pick(args, 0, ["high"], HIGH),
|
|
1669
|
+
pick(args, 1, ["low"], LOW),
|
|
1670
|
+
pick(args, 2, ["close"], CLOSE),
|
|
1671
|
+
pick(args, 3, ["volume"], VOL),
|
|
1672
|
+
]);
|
|
1673
|
+
}
|
|
1674
|
+
if (method === "iii") {
|
|
1675
|
+
return taCall("iii", site, [HIGH, LOW, CLOSE, VOL]);
|
|
1676
|
+
}
|
|
1677
|
+
if (method === "wvad") {
|
|
1678
|
+
if (args.pos.length >= 4 || Object.hasOwn(args.kw, "high")) {
|
|
1679
|
+
return taCall("wvad", site, [
|
|
1680
|
+
pick(args, 0, ["high"], HIGH),
|
|
1681
|
+
pick(args, 1, ["low"], LOW),
|
|
1682
|
+
pick(args, 2, ["close"], CLOSE),
|
|
1683
|
+
pick(args, 3, ["volume"], VOL),
|
|
1684
|
+
pick(args, 4, ["length", "period"], "20"),
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
return taCall("wvad", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
|
|
1688
|
+
}
|
|
1689
|
+
if (method === "cmf") {
|
|
1690
|
+
if (args.pos.length >= 5) {
|
|
1691
|
+
return taCall("cmf", site, [
|
|
1692
|
+
pick(args, 1, ["high"], HIGH),
|
|
1693
|
+
pick(args, 2, ["low"], LOW),
|
|
1694
|
+
pick(args, 0, ["close", "source"], CLOSE),
|
|
1695
|
+
pick(args, 3, ["volume"], VOL),
|
|
1696
|
+
pick(args, 4, ["length", "period"], "20"),
|
|
1697
|
+
]);
|
|
1698
|
+
}
|
|
1699
|
+
return taCall("cmf", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
|
|
1700
|
+
}
|
|
1701
|
+
if (method === "klinger") {
|
|
1702
|
+
return taCall("klinger", site, [
|
|
1703
|
+
pick(args, 2, ["close"], CLOSE),
|
|
1704
|
+
pick(args, 3, ["volume"], VOL),
|
|
1705
|
+
pick(args, 4, ["fast_period", "fast", "fastlen"], "0"),
|
|
1706
|
+
pick(args, 5, ["slow_period", "slow", "slowlen"], "0"),
|
|
1707
|
+
]);
|
|
1708
|
+
}
|
|
1709
|
+
if (method === "ao") {
|
|
1710
|
+
// SMA(hl2, fast) − SMA(hl2, slow); high/low always come from the bar context.
|
|
1711
|
+
return taCall("ao", site, [
|
|
1712
|
+
HIGH,
|
|
1713
|
+
LOW,
|
|
1714
|
+
pick(args, 0, ["fast", "fastlen"], "5"),
|
|
1715
|
+
pick(args, 1, ["slow", "slowlen"], "34"),
|
|
1716
|
+
]);
|
|
1717
|
+
}
|
|
1718
|
+
if (method === "aroon") {
|
|
1719
|
+
// Kernel returns {down, up}; interpret exposes the tuple [down, up].
|
|
1720
|
+
const call = taCall("aroon", site, [HIGH, LOW, pick(args, 0, ["length"], "14")]);
|
|
1721
|
+
return `(() => { const __r = ${call}; return [__r.down, __r.up]; })()`;
|
|
1722
|
+
}
|
|
1723
|
+
if (method === "uo") {
|
|
1724
|
+
return taCall("uo", site, [
|
|
1725
|
+
HIGH,
|
|
1726
|
+
LOW,
|
|
1727
|
+
CLOSE,
|
|
1728
|
+
finiteOr(pick(args, 0, ["length"], "null"), "0"),
|
|
1729
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1730
|
+
finiteOr(pick(args, 2, ["length"], "null"), "0"),
|
|
1731
|
+
]);
|
|
1732
|
+
}
|
|
1733
|
+
if (method === "rci") {
|
|
1734
|
+
// srcArg: missing source is na, not close.
|
|
1735
|
+
return taCall("rci", site, [
|
|
1736
|
+
pick(args, 0, ["source", "series"], "null"),
|
|
1737
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1738
|
+
]);
|
|
1739
|
+
}
|
|
1740
|
+
if (method === "dpo") {
|
|
1741
|
+
return taCall("dpo", site, [CLOSE, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
|
|
1742
|
+
}
|
|
1743
|
+
if (method === "kst") {
|
|
1744
|
+
return taCall("kst", site, [
|
|
1745
|
+
CLOSE,
|
|
1746
|
+
finiteOr(pick(args, 0, ["length"], "null"), "0"),
|
|
1747
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1748
|
+
finiteOr(pick(args, 2, ["length"], "null"), "0"),
|
|
1749
|
+
finiteOr(pick(args, 3, ["length"], "null"), "0"),
|
|
1750
|
+
]);
|
|
1751
|
+
}
|
|
1752
|
+
if (method === "stochrsi") {
|
|
1753
|
+
// Engine method is stochRsi; record already branded {stochrsi, signal}.
|
|
1754
|
+
return taCall("stochRsi", site, [
|
|
1755
|
+
CLOSE,
|
|
1756
|
+
finiteOr(pick(args, 0, ["length"], "null"), "0"),
|
|
1757
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1758
|
+
]);
|
|
1759
|
+
}
|
|
1760
|
+
if (method === "donchian") {
|
|
1761
|
+
return taCall("donchian", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
|
|
1762
|
+
}
|
|
1763
|
+
if (method === "ichimoku") {
|
|
1764
|
+
return taCall("ichimoku", site, [
|
|
1765
|
+
HIGH,
|
|
1766
|
+
LOW,
|
|
1767
|
+
finiteOr(pick(args, 0, ["length"], "null"), "0"),
|
|
1768
|
+
finiteOr(pick(args, 1, ["length"], "null"), "0"),
|
|
1769
|
+
]);
|
|
1770
|
+
}
|
|
1771
|
+
if (method === "bb_pct") {
|
|
1772
|
+
return taCall("bbPct", site, [
|
|
1773
|
+
CLOSE,
|
|
1774
|
+
finiteOr(pick(args, 0, ["length"], "null"), "0"),
|
|
1775
|
+
finiteOr(pick(args, 1, ["std_dev", "mult", "std"], "null"), "2"),
|
|
1776
|
+
]);
|
|
1777
|
+
}
|
|
1778
|
+
if (method === "emv") {
|
|
1779
|
+
return taCall("emv", site, [HIGH, LOW, VOL, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
|
|
1780
|
+
}
|
|
1781
|
+
if (method === "fractal") {
|
|
1782
|
+
return taCall("fractal", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
|
|
1783
|
+
}
|
|
1784
|
+
if (method === "atr_stop") {
|
|
1785
|
+
// Kernel has no call-site; missing multiplier → 2 (interpret numArg).
|
|
1786
|
+
return `__h.ta.atrStop(${CLOSE}, ${pick(args, 0, ["atr", "atr_value"], "null")}, ${finiteOr(pick(args, 1, ["multiplier", "mult"], "null"), "2")})`;
|
|
1787
|
+
}
|
|
1788
|
+
if (method === "zigzag") {
|
|
1789
|
+
const call = taCall("zigzag", site, [
|
|
1790
|
+
pick(args, 0, ["source", "series"], "null"),
|
|
1791
|
+
finiteOr(pick(args, 1, ["threshold", "percent", "deviation"], "null"), "5"),
|
|
1792
|
+
]);
|
|
1793
|
+
return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
|
|
1794
|
+
}
|
|
1795
|
+
return "null";
|
|
1796
|
+
}
|