@hoox-sh/pynets 0.4.0 → 0.5.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 +13 -0
- package/README.md +3 -1
- package/dist/browser.js +63 -63
- package/dist/index.js +477 -9
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/runtime/compile/emit.ts +17 -0
- package/src/runtime/compile/emit_call.ts +54 -1
- package/src/runtime/compile/runtime.ts +51 -1
- package/src/runtime/footprint.ts +272 -0
- package/src/runtime/interpret.ts +174 -8
- package/src/runtime/request.ts +19 -4
|
@@ -46,6 +46,8 @@ const NS = new Set([
|
|
|
46
46
|
"syminfo",
|
|
47
47
|
"timenow",
|
|
48
48
|
"chart",
|
|
49
|
+
"footprint",
|
|
50
|
+
"volume_row",
|
|
49
51
|
]);
|
|
50
52
|
|
|
51
53
|
/** TaEngine methods we emit. Unknown `ta.*` → `null`. */
|
|
@@ -453,6 +455,8 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
|
|
|
453
455
|
|
|
454
456
|
const req = emitRequest(ctx, name, args);
|
|
455
457
|
if (req != null) return req;
|
|
458
|
+
const fp = emitFootprint(name, args);
|
|
459
|
+
if (fp != null) return fp;
|
|
456
460
|
|
|
457
461
|
return "null";
|
|
458
462
|
}
|
|
@@ -1018,7 +1022,6 @@ const SECURITY_FUNCS = new Set([
|
|
|
1018
1022
|
"security_lower_tf",
|
|
1019
1023
|
"request_security",
|
|
1020
1024
|
"request_security_lower_tf",
|
|
1021
|
-
"request_seed",
|
|
1022
1025
|
]);
|
|
1023
1026
|
|
|
1024
1027
|
const SIMPLE_CHART_EXPR = new Set([
|
|
@@ -1064,10 +1067,60 @@ function emitRequest(ctx: EmitCtx, name: string, args: CallArgs): string | null
|
|
|
1064
1067
|
if (name === "request_currency_rate" || name === "currency_rate") {
|
|
1065
1068
|
return `__h.currencyRate(${pick(args, 0, ["from", "from_currency"], "null")}, ${pick(args, 1, ["to", "to_currency"], "null")})`;
|
|
1066
1069
|
}
|
|
1070
|
+
if (name === "request_seed") {
|
|
1071
|
+
return `__h.requestSeed(${pick(args, 0, ["seed"], "0")})`;
|
|
1072
|
+
}
|
|
1073
|
+
if (name === "request_footprint") {
|
|
1074
|
+
return `__h.requestFootprint(${pick(args, 0, ["num_ticks"], "100")}, ${pick(args, 1, ["va_percentage"], "70")})`;
|
|
1075
|
+
}
|
|
1067
1076
|
if (name.startsWith("request_")) return "null";
|
|
1068
1077
|
return null;
|
|
1069
1078
|
}
|
|
1070
1079
|
|
|
1080
|
+
/** Python `request.footprint` mock + `footprint.*` / `volume_row.*` accessors. */
|
|
1081
|
+
function emitFootprint(name: string, args: CallArgs): string | null {
|
|
1082
|
+
const fp = pick(args, 0, ["id", "footprint"], "null");
|
|
1083
|
+
const vr = pick(args, 0, ["id", "volume_row"], "null");
|
|
1084
|
+
switch (name) {
|
|
1085
|
+
case "footprint_buy_volume":
|
|
1086
|
+
return `__h.footprint.buy_volume(${fp})`;
|
|
1087
|
+
case "footprint_sell_volume":
|
|
1088
|
+
return `__h.footprint.sell_volume(${fp})`;
|
|
1089
|
+
case "footprint_delta":
|
|
1090
|
+
return `__h.footprint.delta(${fp})`;
|
|
1091
|
+
case "footprint_total_volume":
|
|
1092
|
+
return `__h.footprint.total_volume(${fp})`;
|
|
1093
|
+
case "footprint_vah":
|
|
1094
|
+
return `__h.footprint.vah(${fp})`;
|
|
1095
|
+
case "footprint_val":
|
|
1096
|
+
return `__h.footprint.val(${fp})`;
|
|
1097
|
+
case "footprint_poc":
|
|
1098
|
+
return `__h.footprint.poc(${fp})`;
|
|
1099
|
+
case "footprint_rows":
|
|
1100
|
+
return `__h.footprint.rows(${fp})`;
|
|
1101
|
+
case "footprint_get_row_by_price":
|
|
1102
|
+
return `__h.footprint.get_row_by_price(${fp}, ${pick(args, 1, ["price"], "null")})`;
|
|
1103
|
+
case "volume_row_up_price":
|
|
1104
|
+
return `__h.footprint.volume_row.up_price(${vr})`;
|
|
1105
|
+
case "volume_row_down_price":
|
|
1106
|
+
return `__h.footprint.volume_row.down_price(${vr})`;
|
|
1107
|
+
case "volume_row_buy_volume":
|
|
1108
|
+
return `__h.footprint.volume_row.buy_volume(${vr})`;
|
|
1109
|
+
case "volume_row_sell_volume":
|
|
1110
|
+
return `__h.footprint.volume_row.sell_volume(${vr})`;
|
|
1111
|
+
case "volume_row_delta":
|
|
1112
|
+
return `__h.footprint.volume_row.delta(${vr})`;
|
|
1113
|
+
case "volume_row_total_volume":
|
|
1114
|
+
return `__h.footprint.volume_row.total_volume(${vr})`;
|
|
1115
|
+
case "volume_row_has_buy_imbalance":
|
|
1116
|
+
return `__h.footprint.volume_row.has_buy_imbalance(${vr})`;
|
|
1117
|
+
case "volume_row_has_sell_imbalance":
|
|
1118
|
+
return `__h.footprint.volume_row.has_sell_imbalance(${vr})`;
|
|
1119
|
+
default:
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1071
1124
|
function isSimpleSecurityExpr(expr: string): boolean {
|
|
1072
1125
|
const e = expr.trim();
|
|
1073
1126
|
if (!e) return false;
|
|
@@ -56,7 +56,28 @@ import {
|
|
|
56
56
|
strUpper,
|
|
57
57
|
} from "../str.ts";
|
|
58
58
|
import { LogBook, formatLogParts } from "../log.ts";
|
|
59
|
-
import {
|
|
59
|
+
import { PineArray } from "../array.ts";
|
|
60
|
+
import {
|
|
61
|
+
createFootprint,
|
|
62
|
+
footprintBuyVolume,
|
|
63
|
+
footprintDelta,
|
|
64
|
+
footprintGetRowByPrice,
|
|
65
|
+
footprintPoc,
|
|
66
|
+
footprintRows,
|
|
67
|
+
footprintSellVolume,
|
|
68
|
+
footprintTotalVolume,
|
|
69
|
+
footprintVah,
|
|
70
|
+
footprintVal,
|
|
71
|
+
volumeRowBuyVolume,
|
|
72
|
+
volumeRowDelta,
|
|
73
|
+
volumeRowDownPrice,
|
|
74
|
+
volumeRowHasBuyImbalance,
|
|
75
|
+
volumeRowHasSellImbalance,
|
|
76
|
+
volumeRowSellVolume,
|
|
77
|
+
volumeRowTotalVolume,
|
|
78
|
+
volumeRowUpPrice,
|
|
79
|
+
} from "../footprint.ts";
|
|
80
|
+
import { resolveCurrencyRate, resolveSeed } from "../request.ts";
|
|
60
81
|
import { TaEngine } from "../ta.ts";
|
|
61
82
|
import { utcPartsFromMs, timestamp, weekOfYear, timeTradingDay, type UtcParts } from "../time.ts";
|
|
62
83
|
import { TickerId } from "../ticker.ts";
|
|
@@ -227,12 +248,41 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
|
|
|
227
248
|
udtSet,
|
|
228
249
|
udtRegister,
|
|
229
250
|
currencyRate: resolveCurrencyRate,
|
|
251
|
+
requestSeed: resolveSeed,
|
|
252
|
+
requestFootprint: createFootprint,
|
|
253
|
+
footprint: {
|
|
254
|
+
buy_volume: footprintBuyVolume,
|
|
255
|
+
sell_volume: footprintSellVolume,
|
|
256
|
+
delta: footprintDelta,
|
|
257
|
+
total_volume: footprintTotalVolume,
|
|
258
|
+
vah: footprintVah,
|
|
259
|
+
val: footprintVal,
|
|
260
|
+
poc: footprintPoc,
|
|
261
|
+
rows: compileFootprintRows,
|
|
262
|
+
get_row_by_price: footprintGetRowByPrice,
|
|
263
|
+
volume_row: {
|
|
264
|
+
up_price: volumeRowUpPrice,
|
|
265
|
+
down_price: volumeRowDownPrice,
|
|
266
|
+
buy_volume: volumeRowBuyVolume,
|
|
267
|
+
sell_volume: volumeRowSellVolume,
|
|
268
|
+
delta: volumeRowDelta,
|
|
269
|
+
total_volume: volumeRowTotalVolume,
|
|
270
|
+
has_buy_imbalance: volumeRowHasBuyImbalance,
|
|
271
|
+
has_sell_imbalance: volumeRowHasSellImbalance,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
230
274
|
runtimeError(msg: unknown): never {
|
|
231
275
|
throw new Error(String(msg));
|
|
232
276
|
},
|
|
233
277
|
};
|
|
234
278
|
}
|
|
235
279
|
|
|
280
|
+
function compileFootprintRows(fp: unknown): PineArray {
|
|
281
|
+
const arr = new PineArray();
|
|
282
|
+
for (const row of footprintRows(fp)) arr.push(row);
|
|
283
|
+
return arr;
|
|
284
|
+
}
|
|
285
|
+
|
|
236
286
|
/** Pine `color.new(color, transp)` — first arg is a color, not r,g,b. */
|
|
237
287
|
function compileColorNew(color: unknown, transp?: unknown) {
|
|
238
288
|
const parsed = asColor(color);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine v6 `request.footprint` mock + `footprint.*` / `volume_row.*` accessors.
|
|
6
|
+
* Python `VolumeRow` / `Footprint` / `_handle_request_footprint` (request.py).
|
|
7
|
+
* Seeded mocks use CPython `random.Random` MT19937 so `request.seed` matches.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Volume row in a footprint (single price level). */
|
|
11
|
+
export class VolumeRow {
|
|
12
|
+
constructor(
|
|
13
|
+
public up_price = 0,
|
|
14
|
+
public down_price = 0,
|
|
15
|
+
public buy_volume = 0,
|
|
16
|
+
public sell_volume = 0,
|
|
17
|
+
public delta = 0,
|
|
18
|
+
public is_imbalance = false,
|
|
19
|
+
public is_poc = false,
|
|
20
|
+
public is_vah = false,
|
|
21
|
+
public is_val = false,
|
|
22
|
+
) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Footprint object: volume profile for one bar. */
|
|
26
|
+
export class Footprint {
|
|
27
|
+
constructor(
|
|
28
|
+
public num_ticks = 100,
|
|
29
|
+
public va_percentage = 70,
|
|
30
|
+
public buy_volume = 0,
|
|
31
|
+
public sell_volume = 0,
|
|
32
|
+
public delta = 0,
|
|
33
|
+
public total_volume = 0,
|
|
34
|
+
public vah_row: VolumeRow | null = null,
|
|
35
|
+
public val_row: VolumeRow | null = null,
|
|
36
|
+
public poc_row: VolumeRow | null = null,
|
|
37
|
+
public rows: VolumeRow[] = [],
|
|
38
|
+
) {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const N = 624;
|
|
42
|
+
const M = 397;
|
|
43
|
+
const MATRIX_A = 0x9908b0df;
|
|
44
|
+
const UPPER_MASK = 0x80000000;
|
|
45
|
+
const LOWER_MASK = 0x7fffffff;
|
|
46
|
+
|
|
47
|
+
/** CPython 3 `random.Random` (MT19937) for integer `random.seed`. */
|
|
48
|
+
class PyRandom {
|
|
49
|
+
private readonly mt = new Uint32Array(N);
|
|
50
|
+
private index = N + 1;
|
|
51
|
+
|
|
52
|
+
seed(a: number): void {
|
|
53
|
+
const n = Math.trunc(a);
|
|
54
|
+
const abs = n < 0 ? -n : n;
|
|
55
|
+
const key: number[] = [];
|
|
56
|
+
if (abs === 0) key.push(0);
|
|
57
|
+
else {
|
|
58
|
+
let x = abs;
|
|
59
|
+
while (x > 0) {
|
|
60
|
+
key.push(x >>> 0);
|
|
61
|
+
x = Math.floor(x / 0x100000000);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.initByArray(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
random(): number {
|
|
68
|
+
const a = this.genrand() >>> 5;
|
|
69
|
+
const b = this.genrand() >>> 6;
|
|
70
|
+
return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private initGenrand(s: number): void {
|
|
74
|
+
const mt = this.mt;
|
|
75
|
+
mt[0] = s >>> 0;
|
|
76
|
+
for (let mti = 1; mti < N; mti++) {
|
|
77
|
+
mt[mti] = (Math.imul(1812433253, mt[mti - 1]! ^ (mt[mti - 1]! >>> 30)) + mti) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
this.index = N;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private initByArray(initKey: number[]): void {
|
|
83
|
+
const mt = this.mt;
|
|
84
|
+
this.initGenrand(19650218);
|
|
85
|
+
let i = 1;
|
|
86
|
+
let j = 0;
|
|
87
|
+
const keyLength = initKey.length;
|
|
88
|
+
let k = N > keyLength ? N : keyLength;
|
|
89
|
+
for (; k > 0; k--) {
|
|
90
|
+
mt[i] =
|
|
91
|
+
((mt[i]! ^ Math.imul(mt[i - 1]! ^ (mt[i - 1]! >>> 30), 1664525)) + initKey[j]! + j) >>> 0;
|
|
92
|
+
i++;
|
|
93
|
+
j++;
|
|
94
|
+
if (i >= N) {
|
|
95
|
+
mt[0] = mt[N - 1]!;
|
|
96
|
+
i = 1;
|
|
97
|
+
}
|
|
98
|
+
if (j >= keyLength) j = 0;
|
|
99
|
+
}
|
|
100
|
+
for (k = N - 1; k > 0; k--) {
|
|
101
|
+
mt[i] = ((mt[i]! ^ Math.imul(mt[i - 1]! ^ (mt[i - 1]! >>> 30), 1566083941)) - i) >>> 0;
|
|
102
|
+
i++;
|
|
103
|
+
if (i >= N) {
|
|
104
|
+
mt[0] = mt[N - 1]!;
|
|
105
|
+
i = 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
mt[0] = 0x80000000;
|
|
109
|
+
this.index = N;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private genrand(): number {
|
|
113
|
+
const mt = this.mt;
|
|
114
|
+
if (this.index >= N) {
|
|
115
|
+
for (let kk = 0; kk < N - M; kk++) {
|
|
116
|
+
const y = (mt[kk]! & UPPER_MASK) | (mt[kk + 1]! & LOWER_MASK);
|
|
117
|
+
mt[kk] = (mt[kk + M]! ^ (y >>> 1) ^ (y & 1 ? MATRIX_A : 0)) >>> 0;
|
|
118
|
+
}
|
|
119
|
+
for (let kk = N - M; kk < N - 1; kk++) {
|
|
120
|
+
const y = (mt[kk]! & UPPER_MASK) | (mt[kk + 1]! & LOWER_MASK);
|
|
121
|
+
mt[kk] = (mt[kk + (M - N)]! ^ (y >>> 1) ^ (y & 1 ? MATRIX_A : 0)) >>> 0;
|
|
122
|
+
}
|
|
123
|
+
const y = (mt[N - 1]! & UPPER_MASK) | (mt[0]! & LOWER_MASK);
|
|
124
|
+
mt[N - 1] = (mt[M - 1]! ^ (y >>> 1) ^ (y & 1 ? MATRIX_A : 0)) >>> 0;
|
|
125
|
+
this.index = 0;
|
|
126
|
+
}
|
|
127
|
+
let y = mt[this.index++]!;
|
|
128
|
+
y ^= y >>> 11;
|
|
129
|
+
y ^= (y << 7) & 0x9d2c5680;
|
|
130
|
+
y ^= (y << 15) & 0xefc60000;
|
|
131
|
+
y ^= y >>> 18;
|
|
132
|
+
return y >>> 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let pyRng: PyRandom | null = null;
|
|
137
|
+
|
|
138
|
+
/** `request.seed` — CPython `random.seed(seed_int)`. */
|
|
139
|
+
export function seedFootprintRng(seed: number): void {
|
|
140
|
+
pyRng = new PyRandom();
|
|
141
|
+
pyRng.seed(seed);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function nextRandom(): number {
|
|
145
|
+
return pyRng != null ? pyRng.random() : Math.random();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function asCount(value: unknown, fallback: number): number {
|
|
149
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
|
|
150
|
+
return fallback;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rowAt(rows: VolumeRow[], idx: number): VolumeRow | null {
|
|
154
|
+
return idx >= 0 && idx < rows.length ? rows[idx]! : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `request.footprint(num_ticks, va_percentage)` mock.
|
|
159
|
+
* Missing ticks / VA % use Python defaults 100 / 70. No data_feed → vol_scale 1.
|
|
160
|
+
*/
|
|
161
|
+
export function createFootprint(numTicks: unknown = 100, vaPercentage: unknown = 70): Footprint {
|
|
162
|
+
const n = asCount(numTicks, 100);
|
|
163
|
+
const va = asCount(vaPercentage, 70);
|
|
164
|
+
const rows: VolumeRow[] = [];
|
|
165
|
+
const basePrice = 100.0;
|
|
166
|
+
const tickSize = 0.01;
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
const priceLevel = basePrice + i * tickSize;
|
|
169
|
+
rows.push(
|
|
170
|
+
new VolumeRow(
|
|
171
|
+
priceLevel + tickSize,
|
|
172
|
+
priceLevel,
|
|
173
|
+
1000.0 + nextRandom() * 500,
|
|
174
|
+
900.0 + nextRandom() * 500,
|
|
175
|
+
100.0 + (nextRandom() * 200 - 100),
|
|
176
|
+
nextRandom() < 0.1,
|
|
177
|
+
i === Math.floor(n / 2),
|
|
178
|
+
i === Math.trunc(n * 0.7),
|
|
179
|
+
i === Math.trunc(n * 0.3),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const totalBuy = rows.reduce((s, r) => s + r.buy_volume, 0);
|
|
184
|
+
const totalSell = rows.reduce((s, r) => s + r.sell_volume, 0);
|
|
185
|
+
return new Footprint(
|
|
186
|
+
n,
|
|
187
|
+
va,
|
|
188
|
+
totalBuy,
|
|
189
|
+
totalSell,
|
|
190
|
+
totalBuy - totalSell,
|
|
191
|
+
totalBuy + totalSell,
|
|
192
|
+
rowAt(rows, Math.trunc(n * 0.7)),
|
|
193
|
+
rowAt(rows, Math.trunc(n * 0.3)),
|
|
194
|
+
rowAt(rows, Math.floor(n / 2)),
|
|
195
|
+
rows,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function footprintBuyVolume(fp: unknown): number {
|
|
200
|
+
return fp instanceof Footprint ? fp.buy_volume : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function footprintSellVolume(fp: unknown): number {
|
|
204
|
+
return fp instanceof Footprint ? fp.sell_volume : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function footprintDelta(fp: unknown): number {
|
|
208
|
+
return fp instanceof Footprint ? fp.delta : 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function footprintTotalVolume(fp: unknown): number {
|
|
212
|
+
return fp instanceof Footprint ? fp.total_volume : 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function footprintVah(fp: unknown): VolumeRow | null {
|
|
216
|
+
return fp instanceof Footprint ? fp.vah_row : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function footprintVal(fp: unknown): VolumeRow | null {
|
|
220
|
+
return fp instanceof Footprint ? fp.val_row : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function footprintPoc(fp: unknown): VolumeRow | null {
|
|
224
|
+
return fp instanceof Footprint ? fp.poc_row : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function footprintRows(fp: unknown): VolumeRow[] {
|
|
228
|
+
return fp instanceof Footprint ? fp.rows.slice() : [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function footprintGetRowByPrice(fp: unknown, price: unknown): VolumeRow | null {
|
|
232
|
+
if (!(fp instanceof Footprint) || price == null) return null;
|
|
233
|
+
if (typeof price !== "number" || !Number.isFinite(price)) return null;
|
|
234
|
+
for (const row of fp.rows) {
|
|
235
|
+
const lo = Math.min(row.down_price, row.up_price);
|
|
236
|
+
const hi = Math.max(row.down_price, row.up_price);
|
|
237
|
+
if (lo <= price && price <= hi) return row;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function volumeRowUpPrice(vr: unknown): number {
|
|
243
|
+
return vr instanceof VolumeRow ? vr.up_price : 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function volumeRowDownPrice(vr: unknown): number {
|
|
247
|
+
return vr instanceof VolumeRow ? vr.down_price : 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function volumeRowBuyVolume(vr: unknown): number {
|
|
251
|
+
return vr instanceof VolumeRow ? vr.buy_volume : 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function volumeRowSellVolume(vr: unknown): number {
|
|
255
|
+
return vr instanceof VolumeRow ? vr.sell_volume : 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function volumeRowDelta(vr: unknown): number {
|
|
259
|
+
return vr instanceof VolumeRow ? vr.delta : 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function volumeRowTotalVolume(vr: unknown): number {
|
|
263
|
+
return vr instanceof VolumeRow ? vr.buy_volume + vr.sell_volume : 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function volumeRowHasBuyImbalance(vr: unknown): boolean {
|
|
267
|
+
return vr instanceof VolumeRow ? Boolean(vr.is_imbalance && vr.buy_volume > vr.sell_volume) : false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function volumeRowHasSellImbalance(vr: unknown): boolean {
|
|
271
|
+
return vr instanceof VolumeRow ? Boolean(vr.is_imbalance && vr.sell_volume > vr.buy_volume) : false;
|
|
272
|
+
}
|
package/src/runtime/interpret.ts
CHANGED
|
@@ -123,6 +123,27 @@ import {
|
|
|
123
123
|
type StubKwargs,
|
|
124
124
|
} from "./library.ts";
|
|
125
125
|
import { isRequestBuiltin, resolveRequest, resolveSecurity } from "./request.ts";
|
|
126
|
+
import {
|
|
127
|
+
Footprint,
|
|
128
|
+
footprintBuyVolume,
|
|
129
|
+
footprintDelta,
|
|
130
|
+
footprintGetRowByPrice,
|
|
131
|
+
footprintPoc,
|
|
132
|
+
footprintRows,
|
|
133
|
+
footprintSellVolume,
|
|
134
|
+
footprintTotalVolume,
|
|
135
|
+
footprintVah,
|
|
136
|
+
footprintVal,
|
|
137
|
+
volumeRowBuyVolume,
|
|
138
|
+
volumeRowDelta,
|
|
139
|
+
volumeRowDownPrice,
|
|
140
|
+
volumeRowHasBuyImbalance,
|
|
141
|
+
volumeRowHasSellImbalance,
|
|
142
|
+
volumeRowSellVolume,
|
|
143
|
+
volumeRowTotalVolume,
|
|
144
|
+
volumeRowUpPrice,
|
|
145
|
+
type VolumeRow,
|
|
146
|
+
} from "./footprint.ts";
|
|
126
147
|
import { NA, PineSeries, type Cell } from "./series.ts";
|
|
127
148
|
import {
|
|
128
149
|
strContains,
|
|
@@ -254,6 +275,8 @@ type Value =
|
|
|
254
275
|
| PineMap
|
|
255
276
|
| PineMatrix
|
|
256
277
|
| TickerId
|
|
278
|
+
| Footprint
|
|
279
|
+
| VolumeRow
|
|
257
280
|
| UdtType
|
|
258
281
|
| UdtInstance
|
|
259
282
|
| LibraryModule
|
|
@@ -337,7 +360,7 @@ interface Env {
|
|
|
337
360
|
onceFired: WeakSet<object>;
|
|
338
361
|
/** Python `_max_bars_back_decls` — recorded only; evaluation is unbounded. */
|
|
339
362
|
maxBarsBackDecls: Array<{ var: Value; num: Value }>;
|
|
340
|
-
/** Python `_alert_fire_bars` keys for `alertcondition`
|
|
363
|
+
/** Python `_alert_fire_bars` keys for `alert` / `alertcondition` freq gating. */
|
|
341
364
|
alertFireBars: Map<string, number>;
|
|
342
365
|
}
|
|
343
366
|
|
|
@@ -1510,6 +1533,22 @@ export const MATH_CONSTANTS: Record<string, Value> = {
|
|
|
1510
1533
|
"plot.linestyle_solid": "linestyle_solid",
|
|
1511
1534
|
"plot.linestyle_dashed": "linestyle_dashed",
|
|
1512
1535
|
"plot.linestyle_dotted": "linestyle_dotted",
|
|
1536
|
+
// Python plotting.py PlotStyle tokens — longer than hline's "solid".
|
|
1537
|
+
"plot.style_line": "style_line",
|
|
1538
|
+
"plot.style_linebr": "style_linebr",
|
|
1539
|
+
"plot.style_stepline": "style_stepline",
|
|
1540
|
+
"plot.style_steplinebr": "style_steplinebr",
|
|
1541
|
+
"plot.style_stepline_diamond": "style_stepline_diamond",
|
|
1542
|
+
"plot.style_histogram": "style_histogram",
|
|
1543
|
+
"plot.style_columns": "style_columns",
|
|
1544
|
+
"plot.style_cross": "style_cross",
|
|
1545
|
+
"plot.style_area": "style_area",
|
|
1546
|
+
"plot.style_areabr": "style_areabr",
|
|
1547
|
+
"plot.style_circles": "style_circles",
|
|
1548
|
+
// Python alerts.py `_alerts_builtin_map`.
|
|
1549
|
+
"alert.freq_once_per_bar": "once_per_bar",
|
|
1550
|
+
"alert.freq_once_per_bar_close": "once_per_bar_close",
|
|
1551
|
+
"alert.freq_all": "all",
|
|
1513
1552
|
"dayofweek.sunday": 1,
|
|
1514
1553
|
"dayofweek.monday": 2,
|
|
1515
1554
|
"dayofweek.tuesday": 3,
|
|
@@ -1889,6 +1928,8 @@ function evalCall(node: Call, env: Env): Value {
|
|
|
1889
1928
|
) {
|
|
1890
1929
|
return evalRequestCall(fname!, node, env);
|
|
1891
1930
|
}
|
|
1931
|
+
const fpCall = evalFootprintCall(fname, node, env);
|
|
1932
|
+
if (fpCall !== undefined) return fpCall;
|
|
1892
1933
|
const arrVal = evalArrayCall(fname, node, env);
|
|
1893
1934
|
if (arrVal !== undefined) return arrVal;
|
|
1894
1935
|
const colorVal = evalColorCall(fname, node, env);
|
|
@@ -2577,14 +2618,66 @@ function packDrawings(book: DrawingBook): DrawingEvent[] | undefined {
|
|
|
2577
2618
|
return book.items.length === 0 ? undefined : book.items;
|
|
2578
2619
|
}
|
|
2579
2620
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2621
|
+
const FREQ_ONCE_PER_BAR = "once_per_bar";
|
|
2622
|
+
const FREQ_ONCE_PER_BAR_CLOSE = "once_per_bar_close";
|
|
2623
|
+
const FREQ_ALL = "all";
|
|
2624
|
+
|
|
2625
|
+
const ALERT_FREQ_ALIASES: Record<string, string> = {
|
|
2626
|
+
once_per_bar: FREQ_ONCE_PER_BAR,
|
|
2627
|
+
freq_once_per_bar: FREQ_ONCE_PER_BAR,
|
|
2628
|
+
"alert.freq_once_per_bar": FREQ_ONCE_PER_BAR,
|
|
2629
|
+
once_per_bar_close: FREQ_ONCE_PER_BAR_CLOSE,
|
|
2630
|
+
freq_once_per_bar_close: FREQ_ONCE_PER_BAR_CLOSE,
|
|
2631
|
+
"alert.freq_once_per_bar_close": FREQ_ONCE_PER_BAR_CLOSE,
|
|
2632
|
+
all: FREQ_ALL,
|
|
2633
|
+
freq_all: FREQ_ALL,
|
|
2634
|
+
"alert.freq_all": FREQ_ALL,
|
|
2635
|
+
};
|
|
2636
|
+
|
|
2637
|
+
/** Python `normalize_alert_freq`. Unknown non-empty tokens are kept (still once-per-bar gated). */
|
|
2638
|
+
function normalizeAlertFreq(freq: unknown): string {
|
|
2639
|
+
if (freq == null) return FREQ_ONCE_PER_BAR;
|
|
2640
|
+
if (typeof freq === "number" && !Number.isFinite(freq)) return FREQ_ONCE_PER_BAR;
|
|
2641
|
+
const s = String(freq).trim().toLowerCase().replace(/ /g, "_");
|
|
2642
|
+
return ALERT_FREQ_ALIASES[s] ?? (s ? s : FREQ_ONCE_PER_BAR);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
/** Python `_bar_is_confirmed`. This host sets `barstate.isconfirmed` to 1 on every bar. */
|
|
2646
|
+
function barIsConfirmed(_env: Env): boolean {
|
|
2647
|
+
return true;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/** Python `_should_fire_alert`. */
|
|
2651
|
+
function shouldFireAlert(env: Env, source: string, title: string, message: string, freq: string): boolean {
|
|
2652
|
+
const freqN = normalizeAlertFreq(freq);
|
|
2653
|
+
if (freqN === FREQ_ALL) return true;
|
|
2654
|
+
if (freqN === FREQ_ONCE_PER_BAR_CLOSE && !barIsConfirmed(env)) return false;
|
|
2655
|
+
const key = `${source}|${title}|${message}|${freqN}`;
|
|
2656
|
+
if (env.alertFireBars.get(key) === env.barIndex) return false;
|
|
2584
2657
|
env.alertFireBars.set(key, env.barIndex);
|
|
2658
|
+
return true;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
/** Python `_emit_alert`. Records on the drawing book (no separate alerts array). */
|
|
2662
|
+
function emitAlert(
|
|
2663
|
+
env: Env,
|
|
2664
|
+
message: string,
|
|
2665
|
+
freq: unknown,
|
|
2666
|
+
opts?: { title?: string; source?: string },
|
|
2667
|
+
): void {
|
|
2668
|
+
const source = opts?.source ?? "alert";
|
|
2669
|
+
const title = opts?.title ?? "";
|
|
2670
|
+
const freqN = normalizeAlertFreq(freq);
|
|
2671
|
+
if (!shouldFireAlert(env, source, title, message, freqN)) return;
|
|
2585
2672
|
env.drawings.alert(env.barIndex, message);
|
|
2586
2673
|
const ev = env.drawings.items[env.drawings.items.length - 1];
|
|
2587
|
-
if (ev
|
|
2674
|
+
if (ev == null) return;
|
|
2675
|
+
ev.extra = title ? { source, title, freq: freqN } : { source, freq: freqN };
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/** Python `_emit_alert(..., source="alertcondition", freq=once_per_bar)`. */
|
|
2679
|
+
function fireAlertCondition(env: Env, title: string, message: string): void {
|
|
2680
|
+
emitAlert(env, message, FREQ_ONCE_PER_BAR, { title, source: "alertcondition" });
|
|
2588
2681
|
}
|
|
2589
2682
|
|
|
2590
2683
|
function inputTitle(node: Call): string | null {
|
|
@@ -2895,7 +2988,7 @@ function evalRequestSecurity(node: Call, env: Env): Cell {
|
|
|
2895
2988
|
);
|
|
2896
2989
|
}
|
|
2897
2990
|
|
|
2898
|
-
function evalRequestCall(fname: string, node: Call, env: Env):
|
|
2991
|
+
function evalRequestCall(fname: string, node: Call, env: Env): Value {
|
|
2899
2992
|
if (
|
|
2900
2993
|
fname === "request.security" ||
|
|
2901
2994
|
fname === "request.security_lower_tf" ||
|
|
@@ -2916,10 +3009,75 @@ function evalRequestCall(fname: string, node: Call, env: Env): Cell {
|
|
|
2916
3009
|
const v = resolveRequest(fname, host, { seed });
|
|
2917
3010
|
return typeof v === "number" || v === null ? v : NA;
|
|
2918
3011
|
}
|
|
3012
|
+
if (fname === "request.footprint") {
|
|
3013
|
+
const numTicks = unwrap(evalExpr(callArg(node.args, 0, ["num_ticks"]), env));
|
|
3014
|
+
const va = unwrap(evalExpr(callArg(node.args, 1, ["va_percentage"]), env));
|
|
3015
|
+
const v = resolveRequest(fname, host, {
|
|
3016
|
+
num_ticks: numTicks ?? 100,
|
|
3017
|
+
va_percentage: va ?? 70,
|
|
3018
|
+
});
|
|
3019
|
+
return v instanceof Footprint ? v : NA;
|
|
3020
|
+
}
|
|
2919
3021
|
const v = resolveRequest(fname, host, {});
|
|
2920
3022
|
return typeof v === "number" || v === null ? v : NA;
|
|
2921
3023
|
}
|
|
2922
3024
|
|
|
3025
|
+
function evalFootprintCall(fname: string | null, node: Call, env: Env): Value | undefined {
|
|
3026
|
+
if (fname == null) return undefined;
|
|
3027
|
+
if (fname.startsWith("footprint.")) {
|
|
3028
|
+
const fp = evalExpr(callArg(node.args, 0, ["id", "footprint"]), env);
|
|
3029
|
+
switch (fname) {
|
|
3030
|
+
case "footprint.buy_volume":
|
|
3031
|
+
return footprintBuyVolume(fp);
|
|
3032
|
+
case "footprint.sell_volume":
|
|
3033
|
+
return footprintSellVolume(fp);
|
|
3034
|
+
case "footprint.delta":
|
|
3035
|
+
return footprintDelta(fp);
|
|
3036
|
+
case "footprint.total_volume":
|
|
3037
|
+
return footprintTotalVolume(fp);
|
|
3038
|
+
case "footprint.vah":
|
|
3039
|
+
return footprintVah(fp);
|
|
3040
|
+
case "footprint.val":
|
|
3041
|
+
return footprintVal(fp);
|
|
3042
|
+
case "footprint.poc":
|
|
3043
|
+
return footprintPoc(fp);
|
|
3044
|
+
case "footprint.rows": {
|
|
3045
|
+
const arr = new PineArray();
|
|
3046
|
+
for (const row of footprintRows(fp)) arr.push(row);
|
|
3047
|
+
return arr;
|
|
3048
|
+
}
|
|
3049
|
+
case "footprint.get_row_by_price":
|
|
3050
|
+
return footprintGetRowByPrice(fp, unwrap(evalExpr(callArg(node.args, 1, ["price"]), env)));
|
|
3051
|
+
default:
|
|
3052
|
+
return undefined;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
if (fname.startsWith("volume_row.")) {
|
|
3056
|
+
const vr = evalExpr(callArg(node.args, 0, ["id", "volume_row"]), env);
|
|
3057
|
+
switch (fname) {
|
|
3058
|
+
case "volume_row.up_price":
|
|
3059
|
+
return volumeRowUpPrice(vr);
|
|
3060
|
+
case "volume_row.down_price":
|
|
3061
|
+
return volumeRowDownPrice(vr);
|
|
3062
|
+
case "volume_row.buy_volume":
|
|
3063
|
+
return volumeRowBuyVolume(vr);
|
|
3064
|
+
case "volume_row.sell_volume":
|
|
3065
|
+
return volumeRowSellVolume(vr);
|
|
3066
|
+
case "volume_row.delta":
|
|
3067
|
+
return volumeRowDelta(vr);
|
|
3068
|
+
case "volume_row.total_volume":
|
|
3069
|
+
return volumeRowTotalVolume(vr);
|
|
3070
|
+
case "volume_row.has_buy_imbalance":
|
|
3071
|
+
return volumeRowHasBuyImbalance(vr);
|
|
3072
|
+
case "volume_row.has_sell_imbalance":
|
|
3073
|
+
return volumeRowHasSellImbalance(vr);
|
|
3074
|
+
default:
|
|
3075
|
+
return undefined;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
return undefined;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
2923
3081
|
function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefined {
|
|
2924
3082
|
if (fname === "math.abs" || fname === "abs") {
|
|
2925
3083
|
return mathAbs(cellArg(node, env, 0, ["number", "x"]));
|
|
@@ -4978,8 +5136,16 @@ function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | un
|
|
|
4978
5136
|
return book.boxNew(env.barIndex, extra);
|
|
4979
5137
|
}
|
|
4980
5138
|
if (fname === "alert") {
|
|
5139
|
+
// Python `_builtin_alert`: zero-arg `alert()` is a soft no-op.
|
|
5140
|
+
if (asArgs(node.args).length === 0) return NA;
|
|
4981
5141
|
const msg = evalAlertMessage(callArg(node.args, 0, ["message"]), env);
|
|
4982
|
-
|
|
5142
|
+
const freqExpr = callArg(node.args, 1, ["freq"]);
|
|
5143
|
+
let freq: unknown = null;
|
|
5144
|
+
if (freqExpr != null) {
|
|
5145
|
+
const v = evalExpr(freqExpr, env);
|
|
5146
|
+
freq = typeof v === "string" ? v : unwrap(v);
|
|
5147
|
+
}
|
|
5148
|
+
emitAlert(env, msg, freq, { source: "alert" });
|
|
4983
5149
|
return NA;
|
|
4984
5150
|
}
|
|
4985
5151
|
if (fname === "table.new") {
|