@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.
@@ -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` once_per_bar. */
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
- /** Python `_emit_alert(..., source="alertcondition", freq=once_per_bar)`. */
2581
- function fireAlertCondition(env: Env, title: string, message: string): void {
2582
- const key = `alertcondition|${title}|${message}|once_per_bar`;
2583
- if (env.alertFireBars.get(key) === env.barIndex) return;
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 != null) ev.extra = { source: "alertcondition", title, freq: "once_per_bar" };
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): Cell {
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"]));
@@ -3951,6 +4109,157 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
3951
4109
  const z = env.ta.zigzag(site, srcArg(node, env), th);
3952
4110
  return tupleOf([z.high, z.low, z.dir]);
3953
4111
  }
4112
+ // wave10a-start
4113
+ const wave10aArg = (index: number, names: string[], message: string): expr => {
4114
+ const arg = callArg(node.args, index, names);
4115
+ if (arg == null) runtimeError(message);
4116
+ return arg;
4117
+ };
4118
+ if (fname === "ta.hammer" || fname === "hammer") {
4119
+ const msg = "ta.hammer() requires 4 arguments: open, high, low, close";
4120
+ return env.ta.hammer(
4121
+ unwrap(evalExpr(wave10aArg(0, ["open"], msg), env)),
4122
+ unwrap(evalExpr(wave10aArg(1, ["high"], msg), env)),
4123
+ unwrap(evalExpr(wave10aArg(2, ["low"], msg), env)),
4124
+ unwrap(evalExpr(wave10aArg(3, ["close"], msg), env)),
4125
+ );
4126
+ }
4127
+ if (fname === "ta.engulfing" || fname === "engulfing") {
4128
+ const msg = "ta.engulfing() requires 4 arguments: open, high, low, close";
4129
+ unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
4130
+ unwrap(evalExpr(wave10aArg(1, ["high"], msg), env));
4131
+ unwrap(evalExpr(wave10aArg(2, ["low"], msg), env));
4132
+ unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
4133
+ return env.ta.engulfing();
4134
+ }
4135
+ if (fname === "ta.gap_detector" || fname === "gap_detector") {
4136
+ const msg = "ta.gap_detector() requires 3 arguments: high, low, prev_close";
4137
+ return env.ta.gapDetector(
4138
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
4139
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
4140
+ unwrap(evalExpr(wave10aArg(2, ["prev_close", "prev"], msg), env)),
4141
+ );
4142
+ }
4143
+ if (fname === "ta.double_top_bottom" || fname === "double_top_bottom") {
4144
+ const msg = "ta.double_top_bottom() requires 3 arguments";
4145
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4146
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4147
+ return env.ta.doubleTopBottom(unwrap(evalExpr(wave10aArg(2, ["period", "length"], msg), env)));
4148
+ }
4149
+ if (fname === "ta.inside_bar_pattern" || fname === "inside_bar_pattern") {
4150
+ const msg = "ta.inside_bar_pattern() requires 2 arguments";
4151
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4152
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4153
+ return env.ta.insideBarPattern();
4154
+ }
4155
+ if (fname === "ta.macd_signal" || fname === "macd_signal") {
4156
+ const msg = "ta.macd_signal() requires 2 arguments: macd_line, signal_line";
4157
+ return env.ta.macdSignal(
4158
+ unwrap(evalExpr(wave10aArg(0, ["macd_line", "macd"], msg), env)),
4159
+ unwrap(evalExpr(wave10aArg(1, ["signal_line", "signal"], msg), env)),
4160
+ );
4161
+ }
4162
+ if (fname === "ta.acceleration_factor" || fname === "acceleration_factor") {
4163
+ const msg = "ta.acceleration_factor() requires 2 arguments";
4164
+ return env.ta.accelerationFactor(
4165
+ site,
4166
+ unwrap(evalExpr(wave10aArg(0, ["momentum_list", "momentum", "source", "series"], msg), env)),
4167
+ unwrap(evalExpr(wave10aArg(1, ["period", "length"], msg), env)),
4168
+ );
4169
+ }
4170
+ if (fname === "ta.stoch_smooth" || fname === "stoch_smooth") {
4171
+ const msg = "ta.stoch_smooth() requires 6 arguments: high, low, close, period, smooth_k, smooth_d";
4172
+ return env.ta.stochSmooth(
4173
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
4174
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
4175
+ unwrap(evalExpr(wave10aArg(2, ["close"], msg), env)),
4176
+ unwrap(evalExpr(wave10aArg(3, ["period", "length"], msg), env)),
4177
+ unwrap(evalExpr(wave10aArg(4, ["smooth_k", "k"], msg), env)),
4178
+ unwrap(evalExpr(wave10aArg(5, ["smooth_d", "d"], msg), env)),
4179
+ );
4180
+ }
4181
+ // wave10a-end
4182
+ // wave10b-start
4183
+ const wave10bList = (index: number, names: string[]): Array<number | null> => {
4184
+ const raw = evalExpr(callArg(node.args, index, names), env);
4185
+ const arr = asArray(raw);
4186
+ if (arr == null) return [unwrap(raw)];
4187
+ const out: Array<number | null> = [];
4188
+ for (let i = 0; i < arr.size(); i++) out.push(arr.get(i));
4189
+ return out;
4190
+ };
4191
+ if (fname === "ta.voi" || fname === "voi") {
4192
+ return env.ta.voi(
4193
+ unwrap(evalExpr(callArg(node.args, 0, ["buy_volume", "buy"]), env)),
4194
+ unwrap(evalExpr(callArg(node.args, 1, ["sell_volume", "sell"]), env)),
4195
+ );
4196
+ }
4197
+ if (fname === "ta.bid_ask_imbalance" || fname === "bid_ask_imbalance") {
4198
+ return env.ta.bidAskImbalance(
4199
+ unwrap(evalExpr(callArg(node.args, 0, ["bid_size", "bid"]), env)),
4200
+ unwrap(evalExpr(callArg(node.args, 1, ["ask_size", "ask"]), env)),
4201
+ unwrap(evalExpr(callArg(node.args, 2, ["bid_price"]), env)),
4202
+ unwrap(evalExpr(callArg(node.args, 3, ["ask_price"]), env)),
4203
+ );
4204
+ }
4205
+ if (fname === "ta.rsi_oversold_overbought" || fname === "rsi_oversold_overbought") {
4206
+ const os = unwrap(evalExpr(callArg(node.args, 1, ["oversold_level", "oversold"]), env));
4207
+ const ob = unwrap(evalExpr(callArg(node.args, 2, ["overbought_level", "overbought"]), env));
4208
+ return env.ta.rsiOversoldOverbought(
4209
+ unwrap(evalExpr(callArg(node.args, 0, ["rsi_series", "rsi", "source"]), env)),
4210
+ os ?? Number.NaN,
4211
+ ob ?? Number.NaN,
4212
+ );
4213
+ }
4214
+ if (fname === "ta.rsi_divergence" || fname === "rsi_divergence") {
4215
+ const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4216
+ return env.ta.rsiDivergence(wave10bList(0, ["rsi_series", "series", "source"]), period ?? 0);
4217
+ }
4218
+ if (fname === "ta.cumulative_delta" || fname === "cumulative_delta") {
4219
+ const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
4220
+ return env.ta.cumulativeDelta(
4221
+ site,
4222
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4223
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4224
+ period ?? 0,
4225
+ );
4226
+ }
4227
+ if (fname === "ta.volume_momentum" || fname === "volume_momentum") {
4228
+ const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4229
+ return env.ta.volumeMomentum(
4230
+ site,
4231
+ unwrap(evalExpr(callArg(node.args, 0, ["volume", "source"]), env)),
4232
+ period ?? 0,
4233
+ );
4234
+ }
4235
+ if (fname === "ta.volume_profile_high" || fname === "volume_profile_high") {
4236
+ return env.ta.volumeProfileHigh(
4237
+ wave10bList(0, ["close", "price", "source", "series"]),
4238
+ wave10bList(1, ["volume"]),
4239
+ );
4240
+ }
4241
+ if (fname === "ta.volume_profile_low" || fname === "volume_profile_low") {
4242
+ const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
4243
+ const levels = unwrap(evalExpr(callArg(node.args, 3, ["levels"]), env));
4244
+ return env.ta.volumeProfileLow(
4245
+ site,
4246
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4247
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4248
+ period ?? 0,
4249
+ levels ?? 0,
4250
+ );
4251
+ }
4252
+ if (fname === "ta.volume_thrust" || fname === "volume_thrust") {
4253
+ const sens = unwrap(evalExpr(callArg(node.args, 3, ["sensitivity"]), env));
4254
+ return env.ta.volumeThrust(
4255
+ site,
4256
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4257
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4258
+ unwrap(evalExpr(callArg(node.args, 2, ["volume_sma", "vol_sma", "sma"]), env)),
4259
+ sens ?? 0.3,
4260
+ );
4261
+ }
4262
+ // wave10b-end
3954
4263
  return undefined;
3955
4264
  }
3956
4265
 
@@ -4827,8 +5136,16 @@ function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | un
4827
5136
  return book.boxNew(env.barIndex, extra);
4828
5137
  }
4829
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;
4830
5141
  const msg = evalAlertMessage(callArg(node.args, 0, ["message"]), env);
4831
- book.alert(env.barIndex, msg);
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" });
4832
5149
  return NA;
4833
5150
  }
4834
5151
  if (fname === "table.new") {
@@ -9,8 +9,11 @@
9
9
  * is the host's job (`bucketIndex` / `shouldForwardFill`).
10
10
  * Lower-TF request without a feed is na — no invented intra-bar array.
11
11
  * Interpret should call this from `evalCall`; this module is host-agnostic.
12
+ * `request.footprint` is the Python mock (not foreign-na).
12
13
  */
13
14
 
15
+ import { createFootprint, seedFootprintRng } from "./footprint.ts";
16
+
14
17
  export type Cell = number | null;
15
18
 
16
19
  export interface SecurityArgs {
@@ -29,7 +32,7 @@ export interface RequestHost {
29
32
  * Named or positional args for `resolveRequest`.
30
33
  * Security uses `symbol` / `timeframe` / `sameSymbolValue`;
31
34
  * currency uses `from`/`to` (or `from_currency`/`to_currency`);
32
- * seed uses `seed`.
35
+ * seed uses `seed`; footprint uses `num_ticks` / `va_percentage`.
33
36
  */
34
37
  export interface RequestNamedArgs {
35
38
  symbol?: string | null;
@@ -41,6 +44,8 @@ export interface RequestNamedArgs {
41
44
  from_currency?: string | null;
42
45
  to_currency?: string | null;
43
46
  seed?: unknown;
47
+ num_ticks?: unknown;
48
+ va_percentage?: unknown;
44
49
  }
45
50
 
46
51
  export type RequestArgs = readonly unknown[] | RequestNamedArgs;
@@ -231,7 +236,7 @@ function bareTicker(symbol: string): string {
231
236
  return colon >= 0 ? s.slice(colon + 1) : s;
232
237
  }
233
238
 
234
- /** Missing / foreign fundamentals, economic, quandl, footprint → na. */
239
+ /** Missing / foreign fundamentals, economic, quandl → na. */
235
240
  export function resolveForeignNa(): Cell {
236
241
  return null;
237
242
  }
@@ -249,9 +254,10 @@ export function resolveCurrencyRate(
249
254
  return null;
250
255
  }
251
256
 
252
- /** `request.seed` — record the seed (Python returns None). */
257
+ /** `request.seed` — record the seed and seed the footprint RNG (Python returns None). */
253
258
  export function resolveSeed(seed?: unknown): Cell {
254
259
  lastSeed = coerceSeed(seed ?? 0);
260
+ seedFootprintRng(lastSeed);
255
261
  return null;
256
262
  }
257
263
 
@@ -262,7 +268,7 @@ export function lastRequestSeed(): number | null {
262
268
  /**
263
269
  * Dispatch a `request.*` name. Security keeps `resolveSecurity` semantics;
264
270
  * lower-TF without a feed is na; currency is 1.0 only for identical codes;
265
- * everything else is na.
271
+ * footprint is the Python mock; everything else is na.
266
272
  */
267
273
  export function resolveRequest(
268
274
  fname: string,
@@ -284,6 +290,10 @@ export function resolveRequest(
284
290
  if (fname === "request.seed") {
285
291
  return resolveSeed(seedFromArgs(args));
286
292
  }
293
+ if (fname === "request.footprint") {
294
+ const [numTicks, va] = footprintFromArgs(args);
295
+ return createFootprint(numTicks, va);
296
+ }
287
297
  return resolveForeignNa();
288
298
  }
289
299
 
@@ -326,6 +336,11 @@ function seedFromArgs(args: RequestArgs): unknown {
326
336
  return args.seed ?? 0;
327
337
  }
328
338
 
339
+ function footprintFromArgs(args: RequestArgs): [unknown, unknown] {
340
+ if (!isNamedArgs(args)) return [args[0] ?? 100, args[1] ?? 70];
341
+ return [args.num_ticks ?? 100, args.va_percentage ?? 70];
342
+ }
343
+
329
344
  function currencyPair(
330
345
  from: string | null | undefined,
331
346
  to: string | null | undefined,