@hoox-sh/pynets 0.1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +46 -0
  4. package/README.md +327 -0
  5. package/dist/browser.js +44 -0
  6. package/dist/index.d.ts +348 -0
  7. package/dist/index.js +41741 -0
  8. package/package.json +77 -0
  9. package/src/ast/builder.ts +1002 -0
  10. package/src/ast/dump.ts +43 -0
  11. package/src/ast/helper.ts +77 -0
  12. package/src/ast/nodes.ts +570 -0
  13. package/src/ast/unparse.ts +360 -0
  14. package/src/cli/rich.ts +378 -0
  15. package/src/cli.ts +627 -0
  16. package/src/generated/PinescriptLexer.interp +267 -0
  17. package/src/generated/PinescriptLexer.tokens +131 -0
  18. package/src/generated/PinescriptLexer.ts +673 -0
  19. package/src/generated/PinescriptLexerBase.ts +395 -0
  20. package/src/generated/PinescriptParser.interp +267 -0
  21. package/src/generated/PinescriptParser.tokens +131 -0
  22. package/src/generated/PinescriptParser.ts +8863 -0
  23. package/src/generated/PinescriptParserBase.ts +21 -0
  24. package/src/generated/PinescriptParserVisitor.ts +840 -0
  25. package/src/index.ts +55 -0
  26. package/src/parser/LexerBase.ts +395 -0
  27. package/src/parser/ParserBase.ts +21 -0
  28. package/src/parser/error.ts +33 -0
  29. package/src/runtime/array.ts +486 -0
  30. package/src/runtime/color.ts +164 -0
  31. package/src/runtime/drawings.ts +510 -0
  32. package/src/runtime/input.ts +102 -0
  33. package/src/runtime/interpret.ts +3897 -0
  34. package/src/runtime/library.ts +282 -0
  35. package/src/runtime/log.ts +69 -0
  36. package/src/runtime/map.ts +85 -0
  37. package/src/runtime/math.ts +231 -0
  38. package/src/runtime/matrix.ts +1154 -0
  39. package/src/runtime/provider.ts +232 -0
  40. package/src/runtime/request.ts +372 -0
  41. package/src/runtime/series.ts +44 -0
  42. package/src/runtime/str.ts +433 -0
  43. package/src/runtime/strategy.ts +1105 -0
  44. package/src/runtime/ta.ts +2093 -0
  45. package/src/runtime/ticker.ts +81 -0
  46. package/src/runtime/time.ts +439 -0
  47. package/src/runtime/timeframe.ts +85 -0
  48. package/src/runtime/udt.ts +119 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Copyright (C) 2024-2026 jango_blockchained
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ *
5
+ * Bar source adapters. Hosts feed OHLCV; this module does not invent
6
+ * foreign market data. Fetch adapters implement `BarProvider`.
7
+ */
8
+
9
+ export interface ProviderBar {
10
+ open?: number;
11
+ high?: number;
12
+ low?: number;
13
+ close?: number;
14
+ volume?: number;
15
+ time?: number;
16
+ }
17
+
18
+ export interface BarProvider {
19
+ fetch(opts: {
20
+ symbol: string;
21
+ timeframe?: string | null;
22
+ limit?: number;
23
+ }): Promise<ProviderBar[]>;
24
+ }
25
+
26
+ /** HTTP / parse failure from `JsonBarProvider`. Hosts should catch this. */
27
+ export class ProviderError extends Error {
28
+ readonly status?: number;
29
+
30
+ constructor(message: string, status?: number) {
31
+ super(message);
32
+ this.name = "ProviderError";
33
+ this.status = status;
34
+ }
35
+ }
36
+
37
+ /** In-memory bars — tests and custom feeds. */
38
+ export class MemoryProvider implements BarProvider {
39
+ constructor(private readonly bars: ProviderBar[]) {}
40
+
41
+ async fetch(opts: { limit?: number }): Promise<ProviderBar[]> {
42
+ const n = opts.limit;
43
+ if (n == null || n >= this.bars.length) return this.bars.slice();
44
+ return this.bars.slice(-n);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * In-process foreign-symbol map. Looks up `symbol` then `symbol:timeframe`.
50
+ * Missing keys return `[]` — no network, no invented data.
51
+ */
52
+ export class StaticMapProvider implements BarProvider {
53
+ constructor(private readonly feeds: Record<string, ProviderBar[]>) {}
54
+
55
+ async fetch(opts: {
56
+ symbol: string;
57
+ timeframe?: string | null;
58
+ limit?: number;
59
+ }): Promise<ProviderBar[]> {
60
+ const tf = opts.timeframe;
61
+ const keyed = tf != null && tf !== "" ? `${opts.symbol}:${tf}` : undefined;
62
+ const bars =
63
+ (keyed !== undefined ? this.feeds[keyed] : undefined) ??
64
+ this.feeds[opts.symbol] ??
65
+ [];
66
+ return sliceTail(bars, opts.limit);
67
+ }
68
+ }
69
+
70
+ export interface JsonBarProviderOptions {
71
+ /** Build a URL from symbol/timeframe/limit. Required unless `fetchBars` is given. */
72
+ url?: (opts: {
73
+ symbol: string;
74
+ timeframe?: string | null;
75
+ limit?: number;
76
+ }) => string;
77
+ /** Injected fetch (defaults to globalThis.fetch). Tests pass a stub. */
78
+ fetch?: typeof fetch;
79
+ /** Map a JSON body to ProviderBar[]. Default: `mapJsonBars`. */
80
+ map?: (json: unknown) => ProviderBar[];
81
+ /** Optional extra headers */
82
+ headers?: Record<string, string>;
83
+ /** Return a JSON body without HTTP. Used instead of `url` when provided. */
84
+ fetchBars?: (opts: {
85
+ symbol: string;
86
+ timeframe?: string | null;
87
+ limit?: number;
88
+ }) => Promise<unknown> | unknown;
89
+ }
90
+
91
+ /**
92
+ * Generic JSON/OHLCV adapter. Non-OK HTTP and invalid JSON throw
93
+ * `ProviderError` so hosts can catch. After a successful response,
94
+ * empty or unrecognized JSON maps to `[]`.
95
+ */
96
+ export class JsonBarProvider implements BarProvider {
97
+ private readonly url?: JsonBarProviderOptions["url"];
98
+ private readonly fetchImpl: typeof fetch;
99
+ private readonly mapFn: (json: unknown) => ProviderBar[];
100
+ private readonly headers?: Record<string, string>;
101
+ private readonly fetchBars?: JsonBarProviderOptions["fetchBars"];
102
+
103
+ constructor(opts: JsonBarProviderOptions) {
104
+ this.url = opts.url;
105
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
106
+ this.mapFn = opts.map ?? mapJsonBars;
107
+ this.headers = opts.headers;
108
+ this.fetchBars = opts.fetchBars;
109
+ }
110
+
111
+ async fetch(opts: {
112
+ symbol: string;
113
+ timeframe?: string | null;
114
+ limit?: number;
115
+ }): Promise<ProviderBar[]> {
116
+ const json = this.fetchBars
117
+ ? await this.fetchBars(opts)
118
+ : await this.getJson(opts);
119
+ return sliceTail(this.mapFn(json), opts.limit);
120
+ }
121
+
122
+ private async getJson(opts: {
123
+ symbol: string;
124
+ timeframe?: string | null;
125
+ limit?: number;
126
+ }): Promise<unknown> {
127
+ if (this.url == null) {
128
+ throw new ProviderError("JsonBarProvider requires `url` or `fetchBars`");
129
+ }
130
+ const href = this.url(opts);
131
+ const res = await this.fetchImpl(href, {
132
+ method: "GET",
133
+ headers: this.headers,
134
+ });
135
+ if (!res.ok) {
136
+ throw new ProviderError(
137
+ `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`.trim(),
138
+ res.status,
139
+ );
140
+ }
141
+ try {
142
+ return await res.json();
143
+ } catch (err) {
144
+ const detail = err instanceof Error ? err.message : String(err);
145
+ throw new ProviderError(`invalid JSON: ${detail}`, res.status);
146
+ }
147
+ }
148
+ }
149
+
150
+ const BAR_KEYS = ["open", "high", "low", "close", "volume", "time"] as const;
151
+
152
+ /**
153
+ * Accept arrays of `{open,high,low,close,volume,time}` (or `t/o/h/l/c/v`),
154
+ * tuples `[time, open, high, low, close, volume, ...]`, and `{ bars | data }`
155
+ * wrappers. Extra trailing tuple fields are ignored. Finite `time`/`t` values
156
+ * below `1e12` are treated as Unix seconds and stored as milliseconds.
157
+ * Non-finite numbers omit that field. Unknown shape → `[]`.
158
+ */
159
+ export function mapJsonBars(json: unknown): ProviderBar[] {
160
+ const rows = unwrapRows(json);
161
+ if (rows == null) return [];
162
+ const out: ProviderBar[] = [];
163
+ for (const item of rows) {
164
+ const bar = mapOneBar(item);
165
+ if (bar != null) out.push(bar);
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function unwrapRows(json: unknown): unknown[] | null {
171
+ if (Array.isArray(json)) return json;
172
+ if (json != null && typeof json === "object") {
173
+ const rec = json as Record<string, unknown>;
174
+ if (Array.isArray(rec.bars)) return rec.bars;
175
+ if (Array.isArray(rec.data)) return rec.data;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function mapOneBar(item: unknown): ProviderBar | null {
181
+ if (item == null || typeof item !== "object") return null;
182
+ if (Array.isArray(item)) return mapTupleBar(item);
183
+ const rec = item as Record<string, unknown>;
184
+ const bar: ProviderBar = {};
185
+ assignFinite(bar, "time", rec.time ?? rec.t);
186
+ assignFinite(bar, "open", rec.open ?? rec.o);
187
+ assignFinite(bar, "high", rec.high ?? rec.h);
188
+ assignFinite(bar, "low", rec.low ?? rec.l);
189
+ assignFinite(bar, "close", rec.close ?? rec.c);
190
+ assignFinite(bar, "volume", rec.volume ?? rec.v);
191
+ return hasBarField(bar) ? bar : null;
192
+ }
193
+
194
+ function mapTupleBar(item: unknown[]): ProviderBar | null {
195
+ if (item.length < 5) return null;
196
+ const bar: ProviderBar = {};
197
+ assignFinite(bar, "time", item[0]);
198
+ assignFinite(bar, "open", item[1]);
199
+ assignFinite(bar, "high", item[2]);
200
+ assignFinite(bar, "low", item[3]);
201
+ assignFinite(bar, "close", item[4]);
202
+ if (item.length > 5) assignFinite(bar, "volume", item[5]);
203
+ return hasBarField(bar) ? bar : null;
204
+ }
205
+
206
+ function assignFinite(
207
+ bar: ProviderBar,
208
+ key: (typeof BAR_KEYS)[number],
209
+ value: unknown,
210
+ ): void {
211
+ const n = asFinite(value);
212
+ if (n === undefined) return;
213
+ bar[key] = key === "time" ? normalizeBarTime(n) : n;
214
+ }
215
+
216
+ /** Unix seconds (`< 1e12`) → milliseconds; already-ms values are unchanged. */
217
+ function normalizeBarTime(msOrSec: number): number {
218
+ return msOrSec < 1e12 ? msOrSec * 1000 : msOrSec;
219
+ }
220
+
221
+ function asFinite(value: unknown): number | undefined {
222
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
223
+ }
224
+
225
+ function hasBarField(bar: ProviderBar): boolean {
226
+ return BAR_KEYS.some((key) => bar[key] !== undefined);
227
+ }
228
+
229
+ function sliceTail(bars: ProviderBar[], limit?: number): ProviderBar[] {
230
+ if (limit == null || limit >= bars.length) return bars.slice();
231
+ return bars.slice(-limit);
232
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Copyright (C) 2024-2026 jango_blockchained
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ *
5
+ * `request.*` stubs matching Python foreign-na policy:
6
+ * same-symbol simple OHLCV passthrough, including HTF last-close;
7
+ * foreign / missing fundamentals → na. HTF *value* passthrough;
8
+ * no invented foreign data. Forward-fill of the last completed HTF bar
9
+ * is the host's job (`bucketIndex` / `shouldForwardFill`).
10
+ * Lower-TF request without a feed is na — no invented intra-bar array.
11
+ * Interpret should call this from `evalCall`; this module is host-agnostic.
12
+ */
13
+
14
+ export type Cell = number | null;
15
+
16
+ export interface SecurityArgs {
17
+ symbol?: string | null;
18
+ timeframe?: string | null;
19
+ expression?: unknown;
20
+ }
21
+
22
+ /** Chart identity passed into `resolveRequest`. */
23
+ export interface RequestHost {
24
+ symbol: string;
25
+ timeframe?: string | null;
26
+ }
27
+
28
+ /**
29
+ * Named or positional args for `resolveRequest`.
30
+ * Security uses `symbol` / `timeframe` / `sameSymbolValue`;
31
+ * currency uses `from`/`to` (or `from_currency`/`to_currency`);
32
+ * seed uses `seed`.
33
+ */
34
+ export interface RequestNamedArgs {
35
+ symbol?: string | null;
36
+ timeframe?: string | null;
37
+ expression?: unknown;
38
+ sameSymbolValue?: Cell;
39
+ from?: string | null;
40
+ to?: string | null;
41
+ from_currency?: string | null;
42
+ to_currency?: string | null;
43
+ seed?: unknown;
44
+ }
45
+
46
+ export type RequestArgs = readonly unknown[] | RequestNamedArgs;
47
+
48
+ const REQUEST_FNS = new Set([
49
+ "request.security",
50
+ "request.security_lower_tf",
51
+ "request.dividends",
52
+ "request.earnings",
53
+ "request.splits",
54
+ "request.financial",
55
+ "request.economic",
56
+ "request.quandl",
57
+ "request.currency_rate",
58
+ "request.seed",
59
+ "request.footprint",
60
+ ]);
61
+
62
+ /** Last `request.seed` value (Python stores it on evaluator context). */
63
+ let lastSeed: number | null = null;
64
+
65
+ const MINUTES_PER_HOUR = 60;
66
+ const MINUTES_PER_DAY = 1440;
67
+ const MINUTES_PER_WEEK = 10080;
68
+ const MINUTES_PER_MONTH = 43200; // 30-day month
69
+
70
+ /** Bare / alias tokens used as chart periods. */
71
+ const NAMED_TF_MINUTES: Record<string, number> = {
72
+ H: MINUTES_PER_HOUR,
73
+ "1H": MINUTES_PER_HOUR,
74
+ D: MINUTES_PER_DAY,
75
+ "1D": MINUTES_PER_DAY,
76
+ W: MINUTES_PER_WEEK,
77
+ "1W": MINUTES_PER_WEEK,
78
+ M: MINUTES_PER_MONTH,
79
+ MO: MINUTES_PER_MONTH,
80
+ "1M": MINUTES_PER_MONTH,
81
+ "1MO": MINUTES_PER_MONTH,
82
+ };
83
+
84
+ export function isRequestBuiltin(fname: string | null | undefined): boolean {
85
+ return fname != null && REQUEST_FNS.has(fname);
86
+ }
87
+
88
+ /**
89
+ * Pine timeframe → minutes. `"1"` `"5"` `"15"` `"60"` `"1h"` `"240"`
90
+ * `"1D"` `"1W"` (and aliases `D`/`W`/`H`) → minutes; unknown → `null`.
91
+ */
92
+ export function timeframeMinutes(tf: string | null | undefined): number | null {
93
+ if (tf == null) return null;
94
+ const raw = tf.trim();
95
+ if (raw === "") return null;
96
+ if (/^\d+$/.test(raw)) {
97
+ const n = Number(raw);
98
+ return n > 0 ? n : null;
99
+ }
100
+ const u = raw.toUpperCase();
101
+ const named = NAMED_TF_MINUTES[u];
102
+ if (named != null) return named;
103
+ const m = /^(\d+)(MO|H|D|W|M)$/.exec(u);
104
+ if (m == null) return null;
105
+ const n = Number(m[1]);
106
+ if (!(n > 0)) return null;
107
+ switch (m[2]) {
108
+ case "MO":
109
+ return n * MINUTES_PER_MONTH;
110
+ case "H":
111
+ return n * MINUTES_PER_HOUR;
112
+ case "D":
113
+ return n * MINUTES_PER_DAY;
114
+ case "W":
115
+ return n * MINUTES_PER_WEEK;
116
+ case "M":
117
+ return n; // "15M" is minutes; "1M" is monthly via NAMED_TF_MINUTES
118
+ default:
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /** Chart cell: finite number stays; NaN / Inf / non-number → na. */
124
+ function finiteCell(value: Cell): Cell {
125
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
126
+ }
127
+
128
+ /**
129
+ * Same-symbol last-close → `sameSymbolValue` (same TF, empty TF, or HTF).
130
+ * Foreign ticker → na. Does not invent foreign closes.
131
+ *
132
+ * Empty / missing request symbol is the chart. Null / empty request TF is
133
+ * the host TF (passthrough). Non-finite host samples resolve to na.
134
+ *
135
+ * HTF *value* passthrough: when request TF minutes > host TF minutes the
136
+ * current host sample is returned; forward-fill of last completed HTF bar
137
+ * is the host's job. Different TF on the same symbol is allowed.
138
+ */
139
+ export function resolveSecurity(
140
+ hostSymbol: string,
141
+ hostTimeframe: string | null,
142
+ args: SecurityArgs,
143
+ sameSymbolValue: Cell,
144
+ ): Cell {
145
+ const a = args ?? {};
146
+ if (!sameSymbol(a.symbol, hostSymbol ?? "")) return null;
147
+ const value = finiteCell(sameSymbolValue);
148
+ const reqMin = timeframeMinutes(a.timeframe);
149
+ const hostMin = timeframeMinutes(hostTimeframe);
150
+ // HTF last-close (req minutes > host): passthrough; host forward-fills.
151
+ if (reqMin != null && hostMin != null && reqMin > hostMin) {
152
+ return value;
153
+ }
154
+ return value;
155
+ }
156
+
157
+ /** Minutes rank of a TF token; unknown / empty / na → null. */
158
+ export function timeframeRank(tf: string | null | undefined): number | null {
159
+ return timeframeMinutes(tf);
160
+ }
161
+
162
+ /** True when both TFs are known and *req* minutes > *host* minutes. */
163
+ export function isHigherTimeframe(
164
+ req: string | null | undefined,
165
+ host: string | null | undefined,
166
+ ): boolean {
167
+ const reqMin = timeframeMinutes(req);
168
+ const hostMin = timeframeMinutes(host);
169
+ return reqMin != null && hostMin != null && reqMin > hostMin;
170
+ }
171
+
172
+ /** True when both TFs are known and *req* minutes < *host* minutes. */
173
+ export function isLowerTimeframe(
174
+ req: string | null | undefined,
175
+ host: string | null | undefined,
176
+ ): boolean {
177
+ const reqMin = timeframeMinutes(req);
178
+ const hostMin = timeframeMinutes(host);
179
+ return reqMin != null && hostMin != null && reqMin < hostMin;
180
+ }
181
+
182
+ /**
183
+ * `request.security_lower_tf` without a lower-TF feed is na.
184
+ * Same-symbol + finer request TF → null (no invented intra-bar array).
185
+ * Foreign → null. Same TF (or not-finer) → `resolveSecurity` scalar.
186
+ */
187
+ export function resolveSecurityLowerTf(
188
+ hostSymbol: string,
189
+ hostTimeframe: string | null,
190
+ args: SecurityArgs,
191
+ sameSymbolValue: Cell,
192
+ ): Cell {
193
+ const a = args ?? {};
194
+ if (!sameSymbol(a.symbol, hostSymbol ?? "")) return null;
195
+ if (isLowerTimeframe(a.timeframe, hostTimeframe)) return null;
196
+ return resolveSecurity(hostSymbol, hostTimeframe, a, sameSymbolValue);
197
+ }
198
+
199
+ /** HTF bucket: floor(timeMs / (tfMinutes * 60 * 1000)); non-finite → null. */
200
+ export function bucketIndex(barTimeMs: number, tfMinutes: number): number | null {
201
+ if (!Number.isFinite(barTimeMs) || !Number.isFinite(tfMinutes) || tfMinutes <= 0) {
202
+ return null;
203
+ }
204
+ return Math.floor(barTimeMs / (tfMinutes * 60 * 1000));
205
+ }
206
+
207
+ /** True when the HTF bucket changed (new HTF bar started). */
208
+ export function shouldForwardFill(
209
+ prevBucket: number | null,
210
+ curBucket: number | null,
211
+ ): boolean {
212
+ return prevBucket != null && curBucket != null && prevBucket !== curBucket;
213
+ }
214
+
215
+ /**
216
+ * Empty / missing request symbol is the chart. Compare case-insensitively after
217
+ * stripping an exchange prefix (`NASDAQ:AAPL` ≡ `AAPL`).
218
+ */
219
+ function sameSymbol(
220
+ requestSymbol: string | null | undefined,
221
+ hostSymbol: string,
222
+ ): boolean {
223
+ const req = (requestSymbol ?? "").trim();
224
+ if (req === "") return true;
225
+ return bareTicker(req) === bareTicker(hostSymbol);
226
+ }
227
+
228
+ function bareTicker(symbol: string): string {
229
+ const s = symbol.trim().toUpperCase();
230
+ const colon = s.lastIndexOf(":");
231
+ return colon >= 0 ? s.slice(colon + 1) : s;
232
+ }
233
+
234
+ /** Missing / foreign fundamentals, economic, quandl, footprint → na. */
235
+ export function resolveForeignNa(): Cell {
236
+ return null;
237
+ }
238
+
239
+ /**
240
+ * `request.currency_rate`. Identical codes (including `USDUSD`) → 1.0.
241
+ * Cross rates have no feed here → na (do not invent Python mock FX).
242
+ */
243
+ export function resolveCurrencyRate(
244
+ from: string | null | undefined,
245
+ to?: string | null,
246
+ ): Cell {
247
+ const [a, b] = currencyPair(from, to);
248
+ if (a !== "" && a === b) return 1;
249
+ return null;
250
+ }
251
+
252
+ /** `request.seed` — record the seed (Python returns None). */
253
+ export function resolveSeed(seed?: unknown): Cell {
254
+ lastSeed = coerceSeed(seed ?? 0);
255
+ return null;
256
+ }
257
+
258
+ export function lastRequestSeed(): number | null {
259
+ return lastSeed;
260
+ }
261
+
262
+ /**
263
+ * Dispatch a `request.*` name. Security keeps `resolveSecurity` semantics;
264
+ * lower-TF without a feed is na; currency is 1.0 only for identical codes;
265
+ * everything else is na.
266
+ */
267
+ export function resolveRequest(
268
+ fname: string,
269
+ host: RequestHost,
270
+ args: RequestArgs = {},
271
+ ): Cell | unknown {
272
+ if (fname === "request.security") {
273
+ const { sec, value } = securityFromArgs(args);
274
+ return resolveSecurity(host.symbol, host.timeframe ?? null, sec, value);
275
+ }
276
+ if (fname === "request.security_lower_tf") {
277
+ const { sec, value } = securityFromArgs(args);
278
+ return resolveSecurityLowerTf(host.symbol, host.timeframe ?? null, sec, value);
279
+ }
280
+ if (fname === "request.currency_rate") {
281
+ const [from, to] = currencyFromArgs(args);
282
+ return resolveCurrencyRate(from, to);
283
+ }
284
+ if (fname === "request.seed") {
285
+ return resolveSeed(seedFromArgs(args));
286
+ }
287
+ return resolveForeignNa();
288
+ }
289
+
290
+ function isNamedArgs(args: RequestArgs): args is RequestNamedArgs {
291
+ return !Array.isArray(args);
292
+ }
293
+
294
+ function securityFromArgs(args: RequestArgs): { sec: SecurityArgs; value: Cell } {
295
+ if (!isNamedArgs(args)) {
296
+ return {
297
+ sec: {
298
+ symbol: asOptString(args[0]),
299
+ timeframe: asOptString(args[1]),
300
+ expression: args[2],
301
+ },
302
+ value: asCell(args[2]),
303
+ };
304
+ }
305
+ return {
306
+ sec: {
307
+ symbol: args.symbol,
308
+ timeframe: args.timeframe,
309
+ expression: args.expression,
310
+ },
311
+ value: args.sameSymbolValue ?? null,
312
+ };
313
+ }
314
+
315
+ function currencyFromArgs(args: RequestArgs): [string | null, string | null] {
316
+ if (!isNamedArgs(args)) {
317
+ return [asOptString(args[0]), asOptString(args[1])];
318
+ }
319
+ const from = args.from ?? args.from_currency ?? null;
320
+ const to = args.to ?? args.to_currency ?? null;
321
+ return [from, to];
322
+ }
323
+
324
+ function seedFromArgs(args: RequestArgs): unknown {
325
+ if (!isNamedArgs(args)) return args[0] ?? 0;
326
+ return args.seed ?? 0;
327
+ }
328
+
329
+ function currencyPair(
330
+ from: string | null | undefined,
331
+ to: string | null | undefined,
332
+ ): [string, string] {
333
+ const a = (from ?? "").trim().toUpperCase();
334
+ const b = (to ?? "").trim().toUpperCase();
335
+ if (b !== "") return [a, b];
336
+ const slash = a.indexOf("/");
337
+ if (slash >= 0) return [a.slice(0, slash), a.slice(slash + 1)];
338
+ if (/^[A-Z]{6}$/.test(a)) return [a.slice(0, 3), a.slice(3)];
339
+ return [a, b];
340
+ }
341
+
342
+ function coerceSeed(seed: unknown): number {
343
+ if (typeof seed === "number" && Number.isFinite(seed)) return Math.trunc(seed);
344
+ if (typeof seed === "string") {
345
+ const n = Number(seed);
346
+ if (seed.trim() !== "" && Number.isFinite(n)) return Math.trunc(n);
347
+ return stringHash(seed);
348
+ }
349
+ if (seed == null) return 0;
350
+ return stringHash(String(seed));
351
+ }
352
+
353
+ /** Deterministic stand-in for Python `hash(str) & 0xFFFFFFFF`. */
354
+ function stringHash(s: string): number {
355
+ let h = 2166136261;
356
+ for (let i = 0; i < s.length; i++) {
357
+ h ^= s.charCodeAt(i);
358
+ h = Math.imul(h, 16777619);
359
+ }
360
+ return h >>> 0;
361
+ }
362
+
363
+ function asOptString(value: unknown): string | null {
364
+ if (value == null) return null;
365
+ return typeof value === "string" ? value : String(value);
366
+ }
367
+
368
+ function asCell(value: unknown): Cell {
369
+ if (value == null) return null;
370
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
371
+ return null;
372
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Copyright (C) 2024-2026 jango_blockchained
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ *
5
+ * Pine series lookback polarity matches Python `PineSeries` / TradingView:
6
+ * get(0) = current bar, get(1) = previous bar, OOB / negative → na (null).
7
+ */
8
+ export const NA = null;
9
+ export type Na = null;
10
+ export type Cell = number | Na;
11
+
12
+ export class PineSeries {
13
+ private readonly data: Cell[] = [];
14
+
15
+ push(value: Cell): void {
16
+ this.data.push(value);
17
+ }
18
+
19
+ setCurrent(value: Cell): void {
20
+ if (this.data.length === 0) this.data.push(value);
21
+ else this.data[this.data.length - 1] = value;
22
+ }
23
+
24
+ get current(): Cell {
25
+ return this.data.length === 0 ? NA : this.data[this.data.length - 1]!;
26
+ }
27
+
28
+ /** `offset` bars ago. `0` is current; negative / non-finite / OOB is `na`. */
29
+ get(offset: number): Cell {
30
+ if (typeof offset !== "number" || !Number.isFinite(offset) || offset < 0) return NA;
31
+ const n = this.data.length;
32
+ if (n === 0) return NA;
33
+ const idx = n - 1 - Math.trunc(offset);
34
+ if (idx < 0 || idx >= n) return NA;
35
+ const v = this.data[idx];
36
+ if (v == null) return NA;
37
+ if (typeof v === "number" && !Number.isFinite(v)) return NA;
38
+ return v;
39
+ }
40
+
41
+ get length(): number {
42
+ return this.data.length;
43
+ }
44
+ }