@hoox-sh/pynets 0.5.0 → 0.6.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.
@@ -114,7 +114,7 @@ import {
114
114
  mathToDegrees,
115
115
  mathToRadians,
116
116
  } from "./math.ts";
117
- import { utcPartsFromMs, timestamp, timeTradingDay, weekOfYear } from "./time.ts";
117
+ import { utcPartsFromMs, timestamp, timestampToMs, timeTradingDay, weekOfYear } from "./time.ts";
118
118
  import {
119
119
  LibraryModule,
120
120
  LibraryRegistry,
@@ -588,6 +588,7 @@ export function interpretTree(
588
588
  high: num(env.ctx.high) ?? undefined,
589
589
  low: num(env.ctx.low) ?? undefined,
590
590
  close: num(env.ctx.close) ?? undefined,
591
+ time: num(env.ctx.time) ?? undefined,
591
592
  });
592
593
  env.book.markOpenTrades(
593
594
  num(env.ctx.high) ?? num(env.ctx.close) ?? 0,
@@ -937,6 +938,35 @@ function seriesGet(env: Env, id: string): PineSeries | undefined {
937
938
  return env.series.get(id);
938
939
  }
939
940
 
941
+ /** Bare series names Python's evaluator exposes as `PineSeries` objects. */
942
+ const BARE_SERIES_NAMES = new Set([
943
+ "open",
944
+ "high",
945
+ "low",
946
+ "close",
947
+ "volume",
948
+ "hl2",
949
+ "hlc3",
950
+ "ohlc4",
951
+ "hlcc4",
952
+ "time",
953
+ "time_close",
954
+ ]);
955
+
956
+ /**
957
+ * True when an argument expression is a series reference rather than a scalar.
958
+ * Python handlers that do `args[0] if isinstance(args[0], list) else [args[0]]`
959
+ * (e.g. `ta.parkinson`, `ta.garman_klass`) see the `PineSeries` object in bar
960
+ * mode and therefore return `na`.
961
+ */
962
+ function isSeriesExpr(value: expr, env: Env): boolean {
963
+ if (value.kind === "Name") {
964
+ if (BARE_SERIES_NAMES.has(value.id)) return true;
965
+ return seriesGet(env, value.id) != null;
966
+ }
967
+ return false;
968
+ }
969
+
940
970
  function bindName(env: Env, id: string, value: Value): void {
941
971
  const stored: Value = typeof value === "number" && !Number.isFinite(value) ? NA : value;
942
972
  const frame = activeFrame(env);
@@ -2107,6 +2137,7 @@ function evalCall(node: Call, env: Env): Value {
2107
2137
  time: num(env.ctx.time) ?? undefined,
2108
2138
  oca_name: evalAsString(callArg(node.args, -1, ["oca_name"]), env),
2109
2139
  oca_type: evalAsString(callArg(node.args, -1, ["oca_type"]), env),
2140
+ deferMarket: true,
2110
2141
  });
2111
2142
  return NA;
2112
2143
  }
@@ -2115,12 +2146,10 @@ function evalCall(node: Call, env: Env): Value {
2115
2146
  const mark = num(env.ctx.close);
2116
2147
  const qtyExpr = callArg(node.args, 1, ["qty"]);
2117
2148
  const qtyRaw = qtyExpr == null ? undefined : unwrap(evalExpr(qtyExpr, env));
2118
- if (env.book.position.qty !== 0 && mark != null) {
2119
- env.book.fillClose(env.barIndex, id, mark, {
2120
- time: num(env.ctx.time) ?? undefined,
2121
- qty: qtyRaw ?? undefined,
2122
- });
2123
- } else env.book.close(env.barIndex, id);
2149
+ env.book.placeClose(env.barIndex, id, mark ?? undefined, {
2150
+ time: num(env.ctx.time) ?? undefined,
2151
+ qty: qtyRaw ?? undefined,
2152
+ });
2124
2153
  return NA;
2125
2154
  }
2126
2155
  if (fname === "strategy.exit") {
@@ -2140,17 +2169,12 @@ function evalCall(node: Call, env: Env): Value {
2140
2169
  trail_offset: unwrap(evalExpr(callArg(node.args, 10, ["trail_offset"]), env)),
2141
2170
  price: num(env.ctx.close) ?? undefined,
2142
2171
  time: num(env.ctx.time) ?? undefined,
2143
- ohlc: {
2144
- open: num(env.ctx.open) ?? undefined,
2145
- high: num(env.ctx.high) ?? undefined,
2146
- low: num(env.ctx.low) ?? undefined,
2147
- close: num(env.ctx.close) ?? undefined,
2148
- },
2172
+ deferMarket: true,
2149
2173
  });
2150
2174
  return NA;
2151
2175
  }
2152
2176
  if (fname === "strategy.close_all") {
2153
- env.book.closeAll(env.barIndex, num(env.ctx.close) ?? undefined);
2177
+ env.book.placeCloseAll(env.barIndex, num(env.ctx.close) ?? undefined);
2154
2178
  return NA;
2155
2179
  }
2156
2180
  if (fname === "strategy.cancel") {
@@ -2173,6 +2197,7 @@ function evalCall(node: Call, env: Env): Value {
2173
2197
  time: num(env.ctx.time) ?? undefined,
2174
2198
  oca_name: evalAsString(callArg(node.args, -1, ["oca_name"]), env),
2175
2199
  oca_type: evalAsString(callArg(node.args, -1, ["oca_type"]), env),
2200
+ deferMarket: true,
2176
2201
  });
2177
2202
  return NA;
2178
2203
  }
@@ -4126,11 +4151,11 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4126
4151
  }
4127
4152
  if (fname === "ta.engulfing" || fname === "engulfing") {
4128
4153
  const msg = "ta.engulfing() requires 4 arguments: open, high, low, close";
4129
- unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
4154
+ const open = unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
4130
4155
  unwrap(evalExpr(wave10aArg(1, ["high"], msg), env));
4131
4156
  unwrap(evalExpr(wave10aArg(2, ["low"], msg), env));
4132
- unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
4133
- return env.ta.engulfing();
4157
+ const close = unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
4158
+ return env.ta.engulfing(site, open, close);
4134
4159
  }
4135
4160
  if (fname === "ta.gap_detector" || fname === "gap_detector") {
4136
4161
  const msg = "ta.gap_detector() requires 3 arguments: high, low, prev_close";
@@ -4142,15 +4167,20 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4142
4167
  }
4143
4168
  if (fname === "ta.double_top_bottom" || fname === "double_top_bottom") {
4144
4169
  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)));
4170
+ const high = unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4171
+ const low = unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4172
+ return env.ta.doubleTopBottom(
4173
+ site,
4174
+ high,
4175
+ low,
4176
+ unwrap(evalExpr(wave10aArg(2, ["period", "length"], msg), env)),
4177
+ );
4148
4178
  }
4149
4179
  if (fname === "ta.inside_bar_pattern" || fname === "inside_bar_pattern") {
4150
4180
  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();
4181
+ const high = unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4182
+ const low = unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4183
+ return env.ta.insideBarPattern(site, high, low);
4154
4184
  }
4155
4185
  if (fname === "ta.macd_signal" || fname === "macd_signal") {
4156
4186
  const msg = "ta.macd_signal() requires 2 arguments: macd_line, signal_line";
@@ -4170,6 +4200,7 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4170
4200
  if (fname === "ta.stoch_smooth" || fname === "stoch_smooth") {
4171
4201
  const msg = "ta.stoch_smooth() requires 6 arguments: high, low, close, period, smooth_k, smooth_d";
4172
4202
  return env.ta.stochSmooth(
4203
+ site,
4173
4204
  unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
4174
4205
  unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
4175
4206
  unwrap(evalExpr(wave10aArg(2, ["close"], msg), env)),
@@ -4213,7 +4244,11 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4213
4244
  }
4214
4245
  if (fname === "ta.rsi_divergence" || fname === "rsi_divergence") {
4215
4246
  const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4216
- return env.ta.rsiDivergence(wave10bList(0, ["rsi_series", "series", "source"]), period ?? 0);
4247
+ return env.ta.rsiDivergence(
4248
+ site,
4249
+ seriesLastCell(evalExpr(callArg(node.args, 0, ["rsi_series", "series", "source"]), env)),
4250
+ period ?? 0,
4251
+ );
4217
4252
  }
4218
4253
  if (fname === "ta.cumulative_delta" || fname === "cumulative_delta") {
4219
4254
  const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
@@ -4260,6 +4295,366 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4260
4295
  );
4261
4296
  }
4262
4297
  // wave10b-end
4298
+ // wave11-start — advanced / statistical ta.* surface
4299
+ const wave11 = (index: number, names: string[], message: string): Value =>
4300
+ unwrap(evalExpr(wave10aArg(index, names, message), env));
4301
+ const wave11n = (index: number, names: string[], message: string): Cell =>
4302
+ num(wave11(index, names, message));
4303
+ if (fname === "ta.sma_weighted" || fname === "sma_weighted") {
4304
+ const msg = "ta.sma_weighted() requires at least 2 arguments: series, period";
4305
+ const weightArg = callArg(node.args, 2, ["weight_type", "weight"]);
4306
+ return env.ta.smaWeighted(
4307
+ site,
4308
+ wave11n(0, ["series", "source"], msg),
4309
+ wave11n(1, ["period", "length"], msg),
4310
+ weightArg == null ? "linear" : evalExpr(weightArg, env),
4311
+ );
4312
+ }
4313
+ if (fname === "ta.apo" || fname === "apo") {
4314
+ const msg = "ta.apo() requires 3 arguments: series, fast_period, slow_period";
4315
+ return env.ta.apo(
4316
+ site,
4317
+ wave11n(0, ["series", "source"], msg),
4318
+ wave11n(1, ["fast_period", "fast"], msg),
4319
+ wave11n(2, ["slow_period", "slow"], msg),
4320
+ );
4321
+ }
4322
+ if (fname === "ta.atr_normalized" || fname === "atr_normalized") {
4323
+ const msg = "ta.atr_normalized() requires 4 arguments: high, low, close, period";
4324
+ return env.ta.atrNormalized(
4325
+ site,
4326
+ wave11n(0, ["high"], msg),
4327
+ wave11n(1, ["low"], msg),
4328
+ wave11n(2, ["close"], msg),
4329
+ wave11n(3, ["period", "length"], msg),
4330
+ );
4331
+ }
4332
+ if (fname === "ta.beta" || fname === "beta") {
4333
+ const msg = "ta.beta() requires 3 arguments: series1, series2, length";
4334
+ return env.ta.beta(
4335
+ site,
4336
+ wave11n(0, ["series1", "series", "source"], msg),
4337
+ wave11n(1, ["series2", "series_b", "other"], msg),
4338
+ wave11n(2, ["length", "period"], msg),
4339
+ );
4340
+ }
4341
+ if (fname === "ta.r_squared" || fname === "r_squared") {
4342
+ const msg = "ta.r_squared() requires 3 arguments: series1, series2, length";
4343
+ return env.ta.rSquared(
4344
+ site,
4345
+ wave11n(0, ["series1", "series", "source"], msg),
4346
+ wave11n(1, ["series2", "series_b", "other"], msg),
4347
+ wave11n(2, ["length", "period"], msg),
4348
+ );
4349
+ }
4350
+ if (fname === "ta.comovement" || fname === "comovement") {
4351
+ const msg = "ta.comovement() requires 3 arguments: series1, series2, length";
4352
+ return env.ta.comovement(
4353
+ site,
4354
+ wave11n(0, ["series1", "series", "source"], msg),
4355
+ wave11n(1, ["series2", "series_b", "other"], msg),
4356
+ wave11n(2, ["length", "period"], msg),
4357
+ );
4358
+ }
4359
+ if (fname === "ta.skewness" || fname === "skewness") {
4360
+ const msg = "ta.skewness() requires 2 arguments: series, period";
4361
+ return env.ta.skewness(
4362
+ site,
4363
+ wave11n(0, ["series", "source"], msg),
4364
+ wave11n(1, ["period", "length"], msg),
4365
+ );
4366
+ }
4367
+ if (fname === "ta.kurtosis" || fname === "kurtosis") {
4368
+ const msg = "ta.kurtosis() requires 2 arguments: series, period";
4369
+ return env.ta.kurtosis(
4370
+ site,
4371
+ wave11n(0, ["series", "source"], msg),
4372
+ wave11n(1, ["period", "length"], msg),
4373
+ );
4374
+ }
4375
+ if (fname === "ta.volume_weighted_momentum" || fname === "volume_weighted_momentum") {
4376
+ const msg = "ta.volume_weighted_momentum() requires 3 arguments: series, volume, length";
4377
+ return env.ta.volumeWeightedMomentum(
4378
+ site,
4379
+ wave11n(0, ["series", "source"], msg),
4380
+ wave11n(1, ["volume"], msg),
4381
+ wave11n(2, ["length", "period"], msg),
4382
+ );
4383
+ }
4384
+ if (fname === "ta.parkinson" || fname === "parkinson") {
4385
+ const msg = "ta.parkinson() requires 2 arguments: high, low";
4386
+ const highArg = wave10aArg(0, ["high"], msg);
4387
+ const lowArg = wave10aArg(1, ["low"], msg);
4388
+ if (isSeriesExpr(highArg, env) || isSeriesExpr(lowArg, env)) return NA;
4389
+ return env.ta.parkinson(num(evalExpr(highArg, env)), num(evalExpr(lowArg, env)));
4390
+ }
4391
+ if (fname === "ta.garman_klass" || fname === "garman_klass") {
4392
+ const msg = "ta.garman_klass() requires 4 arguments: high, low, close, open";
4393
+ const argsExpr = [
4394
+ wave10aArg(0, ["high"], msg),
4395
+ wave10aArg(1, ["low"], msg),
4396
+ wave10aArg(2, ["close"], msg),
4397
+ wave10aArg(3, ["open"], msg),
4398
+ ];
4399
+ if (argsExpr.some((a) => isSeriesExpr(a, env))) return NA;
4400
+ return env.ta.garmanKlass(
4401
+ num(evalExpr(argsExpr[0]!, env)),
4402
+ num(evalExpr(argsExpr[1]!, env)),
4403
+ num(evalExpr(argsExpr[2]!, env)),
4404
+ num(evalExpr(argsExpr[3]!, env)),
4405
+ );
4406
+ }
4407
+ if (fname === "ta.mean_reversion_score" || fname === "mean_reversion_score") {
4408
+ const msg = "ta.mean_reversion_score() requires 4 arguments";
4409
+ return env.ta.meanReversionScore(
4410
+ site,
4411
+ wave11n(0, ["close", "source"], msg),
4412
+ wave11n(1, ["sma"], msg),
4413
+ wave11n(2, ["stdev", "std_dev"], msg),
4414
+ wave11n(3, ["period", "length"], msg),
4415
+ );
4416
+ }
4417
+ if (fname === "ta.market_condition" || fname === "market_condition") {
4418
+ const msg = "ta.market_condition() requires 4 arguments";
4419
+ return env.ta.marketCondition(
4420
+ site,
4421
+ wave11n(0, ["close", "source"], msg),
4422
+ wave11n(1, ["atr"], msg),
4423
+ wave11n(2, ["sma_period", "sma_length"], msg),
4424
+ wave11n(3, ["stdev_period", "stdev_length"], msg),
4425
+ );
4426
+ }
4427
+ if (fname === "ta.order_flow_imbalance" || fname === "order_flow_imbalance") {
4428
+ const msg = "ta.order_flow_imbalance() requires 5 arguments";
4429
+ return env.ta.orderFlowImbalance(
4430
+ site,
4431
+ wave11n(0, ["high"], msg),
4432
+ wave11n(1, ["low"], msg),
4433
+ wave11n(2, ["close"], msg),
4434
+ wave11n(3, ["volume"], msg),
4435
+ wave11n(4, ["period", "length"], msg),
4436
+ );
4437
+ }
4438
+ if (fname === "ta.liquidity_score" || fname === "liquidity_score") {
4439
+ const msg = "ta.liquidity_score() requires 4 arguments";
4440
+ return env.ta.liquidityScore(
4441
+ site,
4442
+ wave11n(0, ["volume"], msg),
4443
+ wave11n(1, ["volatility"], msg),
4444
+ wave11n(2, ["spread"], msg),
4445
+ wave11n(3, ["period", "length"], msg),
4446
+ );
4447
+ }
4448
+ if (fname === "ta.fear_greed_index" || fname === "fear_greed_index") {
4449
+ const msg = "ta.fear_greed_index() requires 4 arguments";
4450
+ return env.ta.fearGreedIndex(
4451
+ wave11n(0, ["rsi"], msg),
4452
+ wave11n(1, ["vix"], msg),
4453
+ wave11n(2, ["put_call", "put_call_ratio"], msg),
4454
+ wave11n(3, ["breadth"], msg),
4455
+ );
4456
+ }
4457
+ if (fname === "ta.crowd_sentiment" || fname === "crowd_sentiment") {
4458
+ const msg = "ta.crowd_sentiment() requires 3 arguments";
4459
+ return env.ta.crowdSentiment(
4460
+ wave11n(0, ["price_agreement", "price"], msg),
4461
+ wave11n(1, ["volume_agreement", "volume"], msg),
4462
+ wave11n(2, ["time_agreement", "time"], msg),
4463
+ );
4464
+ }
4465
+ if (fname === "ta.smart_money_flow" || fname === "smart_money_flow") {
4466
+ const msg = "ta.smart_money_flow() requires 4 arguments";
4467
+ return env.ta.smartMoneyFlow(
4468
+ wave11n(0, ["price_change"], msg),
4469
+ wave11n(1, ["volume"], msg),
4470
+ wave11n(2, ["time_since_high", "time_high"], msg),
4471
+ wave11n(3, ["time_since_low", "time_low"], msg),
4472
+ );
4473
+ }
4474
+ if (fname === "ta.trend_confirmation_score" || fname === "trend_confirmation_score") {
4475
+ const msg = "ta.trend_confirmation_score() requires 6 arguments";
4476
+ wave11n(5, ["support_distance"], msg); // required, unused (Python parity)
4477
+ return env.ta.trendConfirmationScore(
4478
+ wave11n(0, ["momentum"], msg),
4479
+ wave11n(1, ["trend_alignment"], msg),
4480
+ wave11n(2, ["strength"], msg),
4481
+ wave11n(3, ["rsi"], msg),
4482
+ wave11n(4, ["rsi_alignment"], msg),
4483
+ );
4484
+ }
4485
+ if (fname === "ta.max_loss_level" || fname === "max_loss_level") {
4486
+ const msg = "ta.max_loss_level() requires 3 arguments";
4487
+ return env.ta.maxLossLevel(
4488
+ wave11n(0, ["entry", "entry_price"], msg),
4489
+ wave11n(1, ["account", "account_size"], msg),
4490
+ wave11n(2, ["max_loss_pct", "max_loss_percent"], msg),
4491
+ );
4492
+ }
4493
+ if (fname === "ta.profit_lock_level" || fname === "profit_lock_level") {
4494
+ const msg = "ta.profit_lock_level() requires 4 arguments";
4495
+ return env.ta.profitLockLevel(
4496
+ wave11n(0, ["entry", "entry_price"], msg),
4497
+ wave11n(1, ["current", "current_price"], msg),
4498
+ wave11n(2, ["trail_pct", "trail_percent"], msg),
4499
+ wave11n(3, ["direction"], msg),
4500
+ );
4501
+ }
4502
+ // wave11-end
4503
+ // wave12-start — risk / sizing / timing analytics
4504
+ const wave12s = (index: number, names: string[], message: string): Value =>
4505
+ evalExpr(wave10aArg(index, names, message), env);
4506
+ if (fname === "ta.position_sizing" || fname === "position_sizing") {
4507
+ const msg = "ta.position_sizing() requires 4 arguments";
4508
+ return env.ta.positionSizing(
4509
+ wave11n(0, ["account", "account_size"], msg),
4510
+ wave11n(1, ["risk_pct", "risk_percent"], msg),
4511
+ wave11n(2, ["entry", "entry_price"], msg),
4512
+ wave11n(3, ["stop", "stop_price"], msg),
4513
+ );
4514
+ }
4515
+ if (fname === "ta.kelly_criterion" || fname === "kelly_criterion") {
4516
+ const msg = "ta.kelly_criterion() requires 3 arguments";
4517
+ return env.ta.kellyCriterion(
4518
+ wave11n(0, ["win_rate"], msg),
4519
+ wave11n(1, ["avg_win"], msg),
4520
+ wave11n(2, ["avg_loss"], msg),
4521
+ );
4522
+ }
4523
+ if (fname === "ta.risk_reward_ratio" || fname === "risk_reward_ratio") {
4524
+ const msg = "ta.risk_reward_ratio() requires 3 arguments";
4525
+ return env.ta.riskRewardRatio(
4526
+ wave11n(0, ["entry", "entry_price"], msg),
4527
+ wave11n(1, ["stop", "stop_price"], msg),
4528
+ wave11n(2, ["target", "target_price"], msg),
4529
+ );
4530
+ }
4531
+ if (fname === "ta.risk_reward_asymmetry" || fname === "risk_reward_asymmetry") {
4532
+ const msg = "ta.risk_reward_asymmetry() requires 4 arguments";
4533
+ return env.ta.riskRewardAsymmetry(
4534
+ wave11n(0, ["entry", "entry_price"], msg),
4535
+ wave11n(1, ["stop", "stop_price"], msg),
4536
+ wave11n(2, ["target", "target_price"], msg),
4537
+ wave11n(3, ["win_rate"], msg),
4538
+ );
4539
+ }
4540
+ if (fname === "ta.probability_of_movement" || fname === "probability_of_movement") {
4541
+ const msg = "ta.probability_of_movement() requires 4 arguments";
4542
+ return env.ta.probabilityOfMovement(
4543
+ wave11n(0, ["current", "current_price"], msg),
4544
+ wave11n(1, ["target", "target_price"], msg),
4545
+ wave11n(2, ["atr"], msg),
4546
+ wave11n(3, ["period", "length"], msg),
4547
+ );
4548
+ }
4549
+ if (fname === "ta.trend_strength" || fname === "trend_strength") {
4550
+ const msg = "ta.trend_strength() requires 3 arguments";
4551
+ return env.ta.trendStrength(
4552
+ wave11n(0, ["price", "source"], msg),
4553
+ wave11n(1, ["adx"], msg),
4554
+ wave11n(2, ["rsi"], msg),
4555
+ );
4556
+ }
4557
+ if (fname === "ta.breakeven_level" || fname === "breakeven_level") {
4558
+ const msg = "ta.breakeven_level() requires 5 arguments";
4559
+ return env.ta.breakevenLevel(
4560
+ wave11n(0, ["entry", "entry_price"], msg),
4561
+ wave11n(1, ["quantity", "qty"], msg),
4562
+ wave11n(2, ["commission"], msg),
4563
+ wave11n(3, ["slippage"], msg),
4564
+ wave11n(4, ["direction"], msg),
4565
+ );
4566
+ }
4567
+ if (fname === "ta.market_structure_pivot" || fname === "market_structure_pivot") {
4568
+ const msg = "ta.market_structure_pivot() requires 5 arguments";
4569
+ wave11n(0, ["high"], msg);
4570
+ wave11n(1, ["low"], msg);
4571
+ const close = wave11n(2, ["close"], msg);
4572
+ wave11n(3, ["left", "left_bars"], msg);
4573
+ wave11n(4, ["right", "right_bars"], msg);
4574
+ return env.ta.marketStructurePivot(close);
4575
+ }
4576
+ if (fname === "ta.position_sizing_score" || fname === "position_sizing_score") {
4577
+ const msg = "ta.position_sizing_score() requires 4 arguments";
4578
+ return env.ta.positionSizingScore(
4579
+ wave11n(0, ["account_risk", "risk"], msg),
4580
+ wave11n(1, ["volatility"], msg),
4581
+ wave11n(2, ["conviction"], msg),
4582
+ wave11n(3, ["correlation"], msg),
4583
+ );
4584
+ }
4585
+ if (fname === "ta.trailing_exit_level" || fname === "trailing_exit_level") {
4586
+ const msg = "ta.trailing_exit_level() requires 5 arguments";
4587
+ return env.ta.trailingExitLevel(
4588
+ wave11n(0, ["entry", "entry_price"], msg),
4589
+ wave11n(1, ["current", "current_price"], msg),
4590
+ wave11n(2, ["atr"], msg),
4591
+ wave11n(3, ["multiplier", "mult"], msg),
4592
+ wave11n(4, ["step"], msg),
4593
+ );
4594
+ }
4595
+ if (fname === "ta.optimal_entry_zone" || fname === "optimal_entry_zone") {
4596
+ const msg = "ta.optimal_entry_zone() requires 4 arguments";
4597
+ wave11n(0, ["high"], msg);
4598
+ wave11n(1, ["low"], msg);
4599
+ const close = wave11n(2, ["close"], msg);
4600
+ wave11n(3, ["period", "length"], msg);
4601
+ return env.ta.optimalEntryZone(close);
4602
+ }
4603
+ if (fname === "ta.mean_reversion_entry" || fname === "mean_reversion_entry") {
4604
+ const msg = "ta.mean_reversion_entry() requires 5 arguments";
4605
+ return env.ta.meanReversionEntry(
4606
+ wave11n(0, ["price", "source"], msg),
4607
+ wave11n(1, ["sma"], msg),
4608
+ wave11n(2, ["stdev", "std_dev"], msg),
4609
+ wave11n(4, ["threshold"], msg),
4610
+ );
4611
+ }
4612
+ if (fname === "ta.multi_timeframe_signal" || fname === "multi_timeframe_signal") {
4613
+ const msg = "ta.multi_timeframe_signal() requires 6 arguments";
4614
+ return env.ta.multiTimeframeSignal(
4615
+ wave11n(0, ["s1", "signal1"], msg),
4616
+ wave11n(1, ["s2", "signal2"], msg),
4617
+ wave11n(2, ["s3", "signal3"], msg),
4618
+ wave11n(3, ["w1", "weight1"], msg),
4619
+ wave11n(4, ["w2", "weight2"], msg),
4620
+ wave11n(5, ["w3", "weight3"], msg),
4621
+ );
4622
+ }
4623
+ if (fname === "ta.market_timing_index" || fname === "market_timing_index") {
4624
+ const msg = "ta.market_timing_index() requires 4 arguments";
4625
+ return env.ta.marketTimingIndex(
4626
+ wave11n(0, ["trend"], msg),
4627
+ wave11n(1, ["momentum"], msg),
4628
+ wave11n(2, ["volatility"], msg),
4629
+ wave11n(3, ["sentiment"], msg),
4630
+ );
4631
+ }
4632
+ if (fname === "ta.regime_adaptive_signal" || fname === "regime_adaptive_signal") {
4633
+ const msg = "ta.regime_adaptive_signal() requires 4 arguments";
4634
+ const strength = wave11n(0, ["signal_strength", "strength"], msg);
4635
+ const volRegime = wave12s(1, ["volatility_regime", "vol_regime"], msg);
4636
+ const trendRegime = wave12s(2, ["trend_regime"], msg);
4637
+ wave11n(3, ["period", "length"], msg);
4638
+ return env.ta.regimeAdaptiveSignal(strength, volRegime, trendRegime);
4639
+ }
4640
+ if (fname === "ta.strategy_score" || fname === "strategy_score") {
4641
+ const msg = "ta.strategy_score() requires 4 arguments";
4642
+ return env.ta.strategyScore(
4643
+ wave11n(0, ["rsi"], msg),
4644
+ wave11n(1, ["macd"], msg),
4645
+ wave11n(2, ["ema_cross"], msg),
4646
+ wave11n(3, ["trend"], msg),
4647
+ );
4648
+ }
4649
+ if (fname === "ta.breakout_detection" || fname === "breakout_detection") {
4650
+ const msg = "ta.breakout_detection() requires 3 arguments";
4651
+ return env.ta.breakoutDetection(
4652
+ wave11n(0, ["close", "source"], msg),
4653
+ wave11n(1, ["resistance"], msg),
4654
+ wave11n(2, ["support"], msg),
4655
+ );
4656
+ }
4657
+ // wave12-end
4263
4658
  return undefined;
4264
4659
  }
4265
4660
 
@@ -4271,6 +4666,16 @@ function hlc3(env: Env): Cell {
4271
4666
  return (h + l + c) / 3;
4272
4667
  }
4273
4668
 
4669
+ /** Python `_series_last`: array → last cell; scalar → the value itself. */
4670
+ function seriesLastCell(value: Value): number | null {
4671
+ if (value instanceof PineArray) {
4672
+ const n = value.size();
4673
+ return n > 0 ? value.get(n - 1) : null;
4674
+ }
4675
+ const v = unwrap(value);
4676
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
4677
+ }
4678
+
4274
4679
  function asArray(value: Value): PineArray | null {
4275
4680
  if (value instanceof PineArray) return value;
4276
4681
  if (value && typeof value === "object" && "__array" in value) {
@@ -5149,11 +5554,24 @@ function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | un
5149
5554
  return NA;
5150
5555
  }
5151
5556
  if (fname === "table.new") {
5557
+ // Pine positional order is (position, columns, rows, bgcolor,
5558
+ // frame_color, frame_width, border_color, border_width, force_overlay).
5559
+ // Python `_handle_table_new` SoT (columns then rows, not rows then columns).
5152
5560
  const extra: Record<string, unknown> = {};
5153
5561
  const position = drawRaw(node, env, 0, ["position"]);
5154
5562
  if (position !== undefined) extra.position = position;
5155
- putFinite(extra, "rows", drawNum(node, env, 1, ["rows"]));
5156
- putFinite(extra, "columns", drawNum(node, env, 2, ["columns"]));
5563
+ putFinite(extra, "columns", drawNum(node, env, 1, ["columns"]));
5564
+ putFinite(extra, "rows", drawNum(node, env, 2, ["rows"]));
5565
+ const bgcolor = drawRaw(node, env, 3, ["bgcolor"]);
5566
+ if (bgcolor !== undefined) extra.bgcolor = bgcolor;
5567
+ const frameColor = drawRaw(node, env, 4, ["frame_color"]);
5568
+ if (frameColor !== undefined) extra.frame_color = frameColor;
5569
+ putFinite(extra, "frame_width", drawNum(node, env, 5, ["frame_width"]));
5570
+ const borderColor = drawRaw(node, env, 6, ["border_color"]);
5571
+ if (borderColor !== undefined) extra.border_color = borderColor;
5572
+ putFinite(extra, "border_width", drawNum(node, env, 7, ["border_width"]));
5573
+ const forceOverlay = drawRaw(node, env, 8, ["force_overlay"]);
5574
+ if (forceOverlay !== undefined) extra.force_overlay = forceOverlay;
5157
5575
  return book.tableNew(env.barIndex, extra);
5158
5576
  }
5159
5577
  if (fname === "polyline.new") {
@@ -6194,9 +6612,10 @@ function syminfoPrefix(node: Call, env: Env): string {
6194
6612
  }
6195
6613
 
6196
6614
  function calendarMs(node: Call, env: Env): number | null {
6615
+ // Python `_timestamp_to_ms`: chart hosts store opens in seconds.
6197
6616
  const arg = callArg(node.args, 0, ["time", "timestamp"]);
6198
- if (arg == null) return num(env.ctx.time);
6199
- return unwrap(evalExpr(arg, env));
6617
+ if (arg == null) return timestampToMs(num(env.ctx.time));
6618
+ return timestampToMs(unwrap(evalExpr(arg, env)));
6200
6619
  }
6201
6620
 
6202
6621
  function evalLibraryMemberCall(mod: LibraryModule, attr: string, node: Call, env: Env): Value {
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Pine `str.*` helpers. `null` is na.
6
6
  */
7
+ import { timestampToMs } from "./time.ts";
7
8
 
8
9
  function asStr(x: unknown): string | null {
9
10
  if (x == null) return null;
@@ -303,8 +304,8 @@ function coerceFormatTimeMs(time: unknown): number | null {
303
304
  if (raw == null || typeof raw === "boolean" || typeof raw === "string") return null;
304
305
  if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
305
306
  let ts = Math.trunc(raw);
306
- // Python: 0 < timestamp < 10_000_000_000 → Unix seconds, coerce to ms.
307
- if (ts > 0 && ts < 10_000_000_000) ts *= 1000;
307
+ // Python `utility._timestamp_to_ms`: Unix seconds (< 1e11) become ms.
308
+ ts = timestampToMs(ts) ?? ts;
308
309
  return ts;
309
310
  }
310
311