@hoox-sh/pynets 0.3.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 +30 -0
- package/README.md +7 -5
- package/dist/browser.js +66 -66
- package/dist/index.js +2009 -47
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/runtime/compile/collections.ts +4 -0
- package/src/runtime/compile/draw.ts +609 -19
- package/src/runtime/compile/emit.ts +98 -1
- package/src/runtime/compile/emit_call.ts +366 -10
- package/src/runtime/compile/runtime.ts +71 -1
- package/src/runtime/compile/strategy.ts +253 -1
- package/src/runtime/footprint.ts +272 -0
- package/src/runtime/interpret.ts +325 -8
- package/src/runtime/request.ts +19 -4
- package/src/runtime/ta.ts +392 -0
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
* Injected as `__h.strategy`. Wraps StrategyBook (fills + ledger + risk).
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
+
isStrategyCashAmount,
|
|
10
|
+
strategyCashAmount,
|
|
9
11
|
StrategyBook,
|
|
10
12
|
type BarOhlc,
|
|
11
13
|
type BrokerSettings,
|
|
12
14
|
type PlaceEntryOpts,
|
|
13
15
|
type PlaceExitOpts,
|
|
16
|
+
type StrategyCashAmount,
|
|
14
17
|
type StrategyDirection,
|
|
15
18
|
type StrategyEvent,
|
|
16
19
|
type StrategySummary,
|
|
17
20
|
} from "../strategy.ts";
|
|
21
|
+
import { naNum } from "./helpers.ts";
|
|
18
22
|
|
|
19
23
|
function asDir(v: unknown): StrategyDirection | number | string {
|
|
20
24
|
if (v == null) return "long";
|
|
@@ -92,9 +96,13 @@ export class CompileStrategy {
|
|
|
92
96
|
if (ml != null) broker.margin_long = ml;
|
|
93
97
|
const ms = optNum(rec.margin_short);
|
|
94
98
|
if (ms != null) broker.margin_short = ms;
|
|
95
|
-
|
|
99
|
+
// String sentinel or tagged free-cash (`strategy.cash`) both select cash sizing.
|
|
100
|
+
if (typeof rec.default_qty_type === "string" || isStrategyCashAmount(rec.default_qty_type)) {
|
|
101
|
+
broker.default_qty_type = rec.default_qty_type;
|
|
102
|
+
}
|
|
96
103
|
const dqv = optNum(rec.default_qty_value);
|
|
97
104
|
if (dqv != null) broker.default_qty_value = dqv;
|
|
105
|
+
if (typeof rec.currency === "string" && rec.currency !== "") broker.currency = rec.currency;
|
|
98
106
|
if (Object.keys(broker).length) this.book.configure(broker);
|
|
99
107
|
const capital = optNum(rec.initial_capital);
|
|
100
108
|
if (capital != null) this.book.initialCapital = capital;
|
|
@@ -212,6 +220,250 @@ export class CompileStrategy {
|
|
|
212
220
|
return this.book.initialCapital;
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
/** Free cash tagged so `default_qty_type=strategy.cash` still selects cash sizing. */
|
|
224
|
+
cash(): StrategyCashAmount {
|
|
225
|
+
return strategyCashAmount(this.book.cash(this.mark));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
position_entry_name(): string {
|
|
229
|
+
return this.book.position_entry_name;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
account_currency(): string {
|
|
233
|
+
return this.book.account_currency;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Zero-arg form is `percent_equity=100` (interpret `strategy.default_entry_qty`). */
|
|
237
|
+
default_entry_qty(percent?: unknown): number | null {
|
|
238
|
+
const pct = arguments.length === 0 || percent === undefined ? 100 : naNum(percent);
|
|
239
|
+
if (pct == null) return null;
|
|
240
|
+
return (this.book.initialCapital * (pct / 100)) / 100;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Identity FX stub. Missing arg → 1; na / non-finite → na. */
|
|
244
|
+
convert_to(value?: unknown): number | null {
|
|
245
|
+
if (arguments.length === 0 || value === undefined) return 1;
|
|
246
|
+
return naNum(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
max_contracts_held_all(): number {
|
|
250
|
+
return this.book.maxContractsHeldAll();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
max_contracts_held_long(): number {
|
|
254
|
+
return this.book.maxContractsHeldLong();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
max_contracts_held_short(): number {
|
|
258
|
+
return this.book.maxContractsHeldShort();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
capital_held(): number {
|
|
262
|
+
return this.book.capitalHeld();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
closedtrades_first_index(): number {
|
|
266
|
+
return this.book.closedtrades_first_index;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
wintrades(): number {
|
|
270
|
+
return this.book.wintrades;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
losstrades(): number {
|
|
274
|
+
return this.book.losstrades;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
eventrades(): number {
|
|
278
|
+
return this.book.eventrades;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
grossprofit(): number {
|
|
282
|
+
return this.book.grossprofit;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
grossloss(): number {
|
|
286
|
+
return this.book.grossloss;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
avg_trade(): number {
|
|
290
|
+
return this.book.avgTrade();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
avg_winning_trade(): number {
|
|
294
|
+
return this.book.avgWinningTrade();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
avg_losing_trade(): number {
|
|
298
|
+
return this.book.avgLosingTrade();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
netprofit_percent(): number {
|
|
302
|
+
return this.book.netprofitPercent(this.mark);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
openprofit_percent(): number {
|
|
306
|
+
return this.book.openprofitPercent(this.mark);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
grossprofit_percent(): number {
|
|
310
|
+
return this.book.grossprofitPercent();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
grossloss_percent(): number {
|
|
314
|
+
return this.book.grosslossPercent();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
avg_trade_percent(): number {
|
|
318
|
+
return this.book.avgTradePercent();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
avg_winning_trade_percent(): number {
|
|
322
|
+
return this.book.avgWinningTradePercent();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
avg_losing_trade_percent(): number {
|
|
326
|
+
return this.book.avgLosingTradePercent();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
max_drawdown(): number {
|
|
330
|
+
return this.book.maxDrawdown();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
max_drawdown_percent(): number {
|
|
334
|
+
return this.book.maxDrawdownPercent();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
max_runup(): number {
|
|
338
|
+
return this.book.maxRunup();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
max_runup_percent(): number {
|
|
342
|
+
return this.book.maxRunupPercent();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
percent_profitable(): number {
|
|
346
|
+
return this.book.percentProfitable();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
winrate(): number {
|
|
350
|
+
return this.book.percentProfitable();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
profitfactor(): number {
|
|
354
|
+
return this.book.profitFactor();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
profit_factor(): number {
|
|
358
|
+
return this.book.profitFactor();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
closedtrades_entry_bar_index(i?: unknown): number {
|
|
362
|
+
return this.book.closedEntryBar(this.idx(i)) ?? 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
closedtrades_entry_price(i?: unknown): number {
|
|
366
|
+
return this.book.closedEntryPrice(this.idx(i)) ?? 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
closedtrades_exit_bar_index(i?: unknown): number {
|
|
370
|
+
return this.book.closedExitBar(this.idx(i)) ?? 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
closedtrades_exit_price(i?: unknown): number {
|
|
374
|
+
return this.book.closedExitPrice(this.idx(i)) ?? 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
closedtrades_profit(i?: unknown): number {
|
|
378
|
+
return this.book.closedProfit(this.idx(i)) ?? 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
closedtrades_size(i?: unknown): number {
|
|
382
|
+
return this.book.closedSize(this.idx(i)) ?? 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
closedtrades_entry_id(i?: unknown): string {
|
|
386
|
+
return this.book.closedId(this.idx(i)) ?? "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
closedtrades_exit_id(i?: unknown): string {
|
|
390
|
+
return this.book.closedId(this.idx(i)) ?? "";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
closedtrades_commission(i?: unknown): number {
|
|
394
|
+
return this.book.closedCommission(this.idx(i)) ?? 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
closedtrades_entry_time(i?: unknown): number {
|
|
398
|
+
return this.book.closedEntryTime(this.idx(i));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
closedtrades_exit_time(i?: unknown): number {
|
|
402
|
+
return this.book.closedExitTime(this.idx(i));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
closedtrades_entry_comment(i?: unknown): string {
|
|
406
|
+
return this.book.closedEntryComment(this.idx(i));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
closedtrades_exit_comment(i?: unknown): string {
|
|
410
|
+
return this.book.closedExitComment(this.idx(i));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
closedtrades_max_drawdown(i?: unknown): number {
|
|
414
|
+
return this.book.closedMaxDrawdown(this.idx(i));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
closedtrades_max_runup(i?: unknown): number {
|
|
418
|
+
return this.book.closedMaxRunup(this.idx(i));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
opentrades_entry_bar_index(i?: unknown): number {
|
|
422
|
+
return this.book.openEntryBar(this.idx(i)) ?? 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
opentrades_entry_price(i?: unknown): number {
|
|
426
|
+
return this.book.openEntryPrice(this.idx(i)) ?? 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
opentrades_size(i?: unknown): number {
|
|
430
|
+
return this.book.openSize(this.idx(i)) ?? 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
opentrades_entry_id(i?: unknown): string {
|
|
434
|
+
return this.book.openId(this.idx(i)) ?? "";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
opentrades_profit(i?: unknown): number {
|
|
438
|
+
return this.book.openProfit(this.idx(i), this.mark) ?? 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
opentrades_entry_time(i?: unknown): number {
|
|
442
|
+
return this.book.openEntryTime(this.idx(i));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
opentrades_entry_comment(i?: unknown): string {
|
|
446
|
+
return this.book.openEntryComment(this.idx(i));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
opentrades_commission(i?: unknown): number {
|
|
450
|
+
return this.book.openCommission(this.idx(i));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
opentrades_max_drawdown(i?: unknown): number {
|
|
454
|
+
return this.book.openMaxDrawdown(this.idx(i));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
opentrades_max_runup(i?: unknown): number {
|
|
458
|
+
return this.book.openMaxRunup(this.idx(i));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private idx(i: unknown): number {
|
|
462
|
+
if (typeof i === "boolean") return i ? 1 : 0;
|
|
463
|
+
if (typeof i === "number" && Number.isFinite(i)) return Math.trunc(i);
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
215
467
|
risk_allow_entry_in(value?: unknown): void {
|
|
216
468
|
this.book.riskAllowEntryIn(value);
|
|
217
469
|
}
|
|
@@ -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
|
+
}
|