@hoox-sh/pynets 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -44189,6 +44189,150 @@ class TaEngine {
44189
44189
  ev += returns[i] * probabilities[i];
44190
44190
  return finiteOut2(ev / totalProb) ?? 0;
44191
44191
  }
44192
+ gammaLevels(volatility, currentPrice, period) {
44193
+ const v = finiteCell2(volatility) ?? 0.02;
44194
+ const p = finiteCell2(currentPrice) ?? 100;
44195
+ const n = this.coerceTaInt(period, "period must be integer");
44196
+ const volAdjusted = v * Math.sqrt(n);
44197
+ const distance = p * volAdjusted;
44198
+ const out = new PineArray;
44199
+ out.push(finiteOut2(p + distance));
44200
+ out.push(finiteOut2(p - distance));
44201
+ return out;
44202
+ }
44203
+ intelligentStrategySynthesizer(site, trend, momentum, volatility, volume, marketCondition, riskProfile) {
44204
+ const th = this.windowFor(`${site}:trend`, trend);
44205
+ const mh = this.windowFor(`${site}:momentum`, momentum);
44206
+ const vh = this.windowFor(`${site}:volatility`, volatility);
44207
+ const volh = this.windowFor(`${site}:volume`, volume);
44208
+ const regime = typeof marketCondition === "string" ? marketCondition : "ranging";
44209
+ const profile = typeof riskProfile === "string" ? riskProfile : "balanced";
44210
+ const nums = (h) => h.filter((v) => v !== null);
44211
+ const trendVals = nums(th);
44212
+ const momentumVals = nums(mh);
44213
+ const volatilityVals = nums(vh);
44214
+ const volumeVals = nums(volh);
44215
+ const avg = (vals, fallback) => vals.length === 0 ? fallback : vals.reduce((a, b) => a + b, 0) / vals.length;
44216
+ const trendAvg = avg(trendVals, 0);
44217
+ const momentumAvg = avg(momentumVals, 0);
44218
+ const volatilityAvg = avg(volatilityVals, 0.5);
44219
+ const volumeAvg = avg(volumeVals, 0);
44220
+ const volatilityNormalized = Math.max(0, Math.min(1, volatilityAvg));
44221
+ const composite = trendAvg * 0.4 + momentumAvg * 0.35 + volumeAvg * 0.25;
44222
+ const compositeSignal = Math.max(-1, Math.min(1, composite));
44223
+ let agreement = 0;
44224
+ if (Math.abs(trendAvg) > 0.3)
44225
+ agreement += 1;
44226
+ if (Math.abs(momentumAvg) > 0.3)
44227
+ agreement += 1;
44228
+ if (Math.abs(volumeAvg) > 0.3)
44229
+ agreement += 1;
44230
+ const baseConfidence = agreement / 3;
44231
+ const confidence = Math.max(0.1, Math.min(0.99, baseConfidence - volatilityNormalized * 0.3));
44232
+ let recommendation;
44233
+ if (compositeSignal > 0.6) {
44234
+ recommendation = profile === "aggressive" ? "aggressive_long" : "conservative_long";
44235
+ } else if (compositeSignal > 0.2) {
44236
+ recommendation = "conservative_long";
44237
+ } else if (compositeSignal < -0.6) {
44238
+ recommendation = profile === "aggressive" ? "aggressive_short" : "conservative_short";
44239
+ } else if (compositeSignal < -0.2) {
44240
+ recommendation = "conservative_short";
44241
+ } else {
44242
+ recommendation = "hold";
44243
+ }
44244
+ const riskMult = profile === "conservative" ? 0.5 : profile === "aggressive" ? 1.5 : 1;
44245
+ const riskLevel = volatilityNormalized * 50 * riskMult;
44246
+ const absSignal = Math.abs(compositeSignal);
44247
+ const expectedReturn = absSignal * 3;
44248
+ let holdingPeriod;
44249
+ if (volatilityNormalized > 0.7)
44250
+ holdingPeriod = "scalp";
44251
+ else if (volatilityNormalized > 0.5)
44252
+ holdingPeriod = "day_trade";
44253
+ else if (absSignal > 0.5)
44254
+ holdingPeriod = "swing";
44255
+ else
44256
+ holdingPeriod = "position";
44257
+ const shortRec = recommendation === "aggressive_short" || recommendation === "conservative_short";
44258
+ const longRec = recommendation === "aggressive_long" || recommendation === "conservative_long";
44259
+ const stopLoss = shortRec ? -0.7 : longRec ? -0.2 : -0.15;
44260
+ const takeProfit = recommendation === "aggressive_long" || recommendation === "aggressive_short" ? 1.5 : longRec || shortRec ? 1 : 0.5;
44261
+ let alignment = 50;
44262
+ if (regime === "trending_up") {
44263
+ if (compositeSignal > 0)
44264
+ alignment = 90;
44265
+ else if (compositeSignal > -0.3)
44266
+ alignment = 60;
44267
+ else
44268
+ alignment = 30;
44269
+ } else if (regime === "trending_down") {
44270
+ if (compositeSignal < 0)
44271
+ alignment = 90;
44272
+ else if (compositeSignal < 0.3)
44273
+ alignment = 60;
44274
+ else
44275
+ alignment = 30;
44276
+ } else if (regime === "ranging") {
44277
+ alignment = 50 + (1 - absSignal) * 30;
44278
+ } else if (regime === "volatile") {
44279
+ alignment = 30 + absSignal * 40;
44280
+ } else {
44281
+ alignment = 40;
44282
+ }
44283
+ return taRec({
44284
+ composite_signal: finiteOut2(compositeSignal) ?? 0,
44285
+ confidence_level: finiteOut2(confidence) ?? 0,
44286
+ strategy_recommendation: recommendation,
44287
+ risk_level: finiteOut2(Math.max(0, Math.min(100, riskLevel))) ?? 0,
44288
+ expected_return: finiteOut2(expectedReturn) ?? 0,
44289
+ holding_period: holdingPeriod,
44290
+ stop_loss_priority: stopLoss,
44291
+ take_profit_priority: takeProfit,
44292
+ regime_alignment: finiteOut2(Math.max(0, Math.min(100, alignment))) ?? 0
44293
+ });
44294
+ }
44295
+ signalConfluence(signals) {
44296
+ let vals = [];
44297
+ if (signals != null && typeof signals === "object") {
44298
+ const obj = signals;
44299
+ if (typeof obj.values === "function") {
44300
+ const got = obj.values();
44301
+ vals = Array.isArray(got) ? got : [];
44302
+ } else if (!Array.isArray(signals)) {
44303
+ vals = Object.entries(signals).filter(([k]) => k !== "__taRecord").map(([, v]) => v);
44304
+ }
44305
+ }
44306
+ let signalCount = 0;
44307
+ let bullish = 0;
44308
+ let bearish = 0;
44309
+ for (const v of vals) {
44310
+ if (typeof v === "boolean") {
44311
+ if (v) {
44312
+ bullish += 1;
44313
+ signalCount += 1;
44314
+ }
44315
+ continue;
44316
+ }
44317
+ if (typeof v === "number" && Number.isFinite(v)) {
44318
+ if (v > 0) {
44319
+ bullish += 1;
44320
+ signalCount += 1;
44321
+ } else if (v < 0) {
44322
+ bearish += 1;
44323
+ signalCount += 1;
44324
+ }
44325
+ }
44326
+ }
44327
+ const total = vals.length > 0 ? vals.length : 1;
44328
+ const confluence = signalCount / total;
44329
+ const primary = bullish > bearish ? "buy" : bearish > bullish ? "sell" : "neutral";
44330
+ return taRec({
44331
+ signal_count: signalCount,
44332
+ confluence_level: finiteOut2(confluence) ?? 0,
44333
+ primary_signal: primary
44334
+ });
44335
+ }
44192
44336
  volumeProfileHigh(prices, volumes) {
44193
44337
  if (prices.length === 0 || volumes.length === 0 || prices.length !== volumes.length)
44194
44338
  return 0;
@@ -44830,7 +44974,10 @@ var TA_METHODS = new Set([
44830
44974
  "employment_cycle_indicator",
44831
44975
  "gdp_growth_proxy",
44832
44976
  "inflation_proxy_indicator",
44833
- "expected_value"
44977
+ "expected_value",
44978
+ "gamma_levels",
44979
+ "intelligent_strategy_synthesizer",
44980
+ "signal_confluence"
44834
44981
  ]);
44835
44982
  var TA_BARE_ATTRS = new Set([
44836
44983
  "accdist",
@@ -45199,6 +45346,9 @@ function hasSeriesArg(args) {
45199
45346
  const isSeries = (n) => n.kind === "Name" && BARE_SERIES_NAMES.has(n.id);
45200
45347
  return args.posNodes.some(isSeries) || Object.values(args.kwNodes).some(isSeries);
45201
45348
  }
45349
+ function isSeriesNode(n) {
45350
+ return n != null && n.kind === "Name" && BARE_SERIES_NAMES.has(n.id);
45351
+ }
45202
45352
  function hasAnyKw(args, names) {
45203
45353
  return names.some((n) => Object.hasOwn(args.kw, n));
45204
45354
  }
@@ -46951,10 +47101,11 @@ function emitTa(ctx, method, args) {
46951
47101
  ]);
46952
47102
  }
46953
47103
  if (method === "breakout_detection") {
47104
+ const arg = (i, names) => isSeriesNode(args.posNodes[i]) ? "null" : cellOf(pick(args, i, names, "null"));
46954
47105
  return taPure("breakoutDetection", [
46955
- cellOf(pick(args, 0, ["close", "source"], "null")),
46956
- cellOf(pick(args, 1, ["resistance"], "null")),
46957
- cellOf(pick(args, 2, ["support"], "null"))
47106
+ arg(0, ["close", "source"]),
47107
+ arg(1, ["resistance"]),
47108
+ arg(2, ["support"])
46958
47109
  ]);
46959
47110
  }
46960
47111
  if (method === "ema_cross_signal") {
@@ -47085,6 +47236,24 @@ function emitTa(ctx, method, args) {
47085
47236
  if (method === "expected_value") {
47086
47237
  return `__h.ta.expectedValue(${cellList(pick(args, 0, ["returns"], "null"))}, ${cellList(pick(args, 1, ["probabilities", "probs"], "null"))})`;
47087
47238
  }
47239
+ if (method === "gamma_levels") {
47240
+ const vol = isSeriesNode(args.posNodes[0]) ? "null" : cellOf(pick(args, 0, ["volatility"], "null"));
47241
+ const price = isSeriesNode(args.posNodes[1]) ? "null" : cellOf(pick(args, 1, ["current_price", "price"], "null"));
47242
+ return taPure("gammaLevels", [vol, price, cellOf(pick(args, 2, ["period", "length"], "null"))]);
47243
+ }
47244
+ if (method === "intelligent_strategy_synthesizer") {
47245
+ return taCall("intelligentStrategySynthesizer", site, [
47246
+ arrOrCell(pick(args, 0, ["trend_indicators", "trend"], "null")),
47247
+ arrOrCell(pick(args, 1, ["momentum_indicators", "momentum"], "null")),
47248
+ arrOrCell(pick(args, 2, ["volatility_indicators", "volatility"], "null")),
47249
+ arrOrCell(pick(args, 3, ["volume_indicators", "volume"], "null")),
47250
+ pick(args, 4, ["market_condition", "market_regime"], '"ranging"'),
47251
+ pick(args, 5, ["risk_profile"], '"balanced"')
47252
+ ]);
47253
+ }
47254
+ if (method === "signal_confluence") {
47255
+ return taPure("signalConfluence", [pick(args, 0, ["signals"], "null")]);
47256
+ }
47088
47257
  return "null";
47089
47258
  }
47090
47259
 
@@ -54200,7 +54369,11 @@ function evalExtraTa(fname, node, env, site) {
54200
54369
  }
54201
54370
  if (fname === "ta.breakout_detection" || fname === "breakout_detection") {
54202
54371
  const msg = "ta.breakout_detection() requires 3 arguments";
54203
- return env.ta.breakoutDetection(wave11n(0, ["close", "source"], msg), wave11n(1, ["resistance"], msg), wave11n(2, ["support"], msg));
54372
+ const arg = (i, names) => {
54373
+ const e = wave10aArg(i, names, msg);
54374
+ return isSeriesExpr(e, env) ? NA : num2(evalExpr(e, env));
54375
+ };
54376
+ return env.ta.breakoutDetection(arg(0, ["close", "source"]), arg(1, ["resistance"]), arg(2, ["support"]));
54204
54377
  }
54205
54378
  if (fname === "ta.ema_cross_signal" || fname === "ema_cross_signal") {
54206
54379
  const msg = "ta.ema_cross_signal() requires 3 arguments";
@@ -54294,6 +54467,22 @@ function evalExtraTa(fname, node, env, site) {
54294
54467
  runtimeError("Returns and probabilities must have same length");
54295
54468
  return env.ta.expectedValue(returns, probs);
54296
54469
  }
54470
+ if (fname === "ta.gamma_levels" || fname === "gamma_levels") {
54471
+ const msg = "ta.gamma_levels() requires 3 arguments";
54472
+ const volArg = wave10aArg(0, ["volatility"], msg);
54473
+ const priceArg = wave10aArg(1, ["current_price", "price"], msg);
54474
+ const vol = isSeriesExpr(volArg, env) ? NA : num2(evalExpr(volArg, env));
54475
+ const price = isSeriesExpr(priceArg, env) ? NA : num2(evalExpr(priceArg, env));
54476
+ return env.ta.gammaLevels(vol, price, wave11n(2, ["period", "length"], msg));
54477
+ }
54478
+ if (fname === "ta.intelligent_strategy_synthesizer" || fname === "intelligent_strategy_synthesizer") {
54479
+ const msg = "ta.intelligent_strategy_synthesizer() requires 6 arguments";
54480
+ return env.ta.intelligentStrategySynthesizer(site, wave13v(0, ["trend_indicators", "trend"], msg), wave13v(1, ["momentum_indicators", "momentum"], msg), wave13v(2, ["volatility_indicators", "volatility"], msg), wave13v(3, ["volume_indicators", "volume"], msg), wave13v(4, ["market_condition", "market_regime"], msg), wave13v(5, ["risk_profile"], msg));
54481
+ }
54482
+ if (fname === "ta.signal_confluence" || fname === "signal_confluence") {
54483
+ const msg = "ta.signal_confluence() requires 1 argument";
54484
+ return env.ta.signalConfluence(wave13v(0, ["signals"], msg));
54485
+ }
54297
54486
  return;
54298
54487
  }
54299
54488
  function hlc3(env) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoox-sh/pynets",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "jango_blockchained",
package/src/cli/rich.ts CHANGED
@@ -12,7 +12,7 @@ const MUTED = [203, 204, 189] as const;
12
12
  const FAIL = [231, 0, 11] as const;
13
13
  const WARN = [235, 169, 65] as const;
14
14
 
15
- export const VERSION = "0.7.0";
15
+ export const VERSION = "0.8.0";
16
16
 
17
17
  type Rgb = readonly [number, number, number];
18
18
 
@@ -211,6 +211,9 @@ const TA_METHODS = new Set([
211
211
  "gdp_growth_proxy",
212
212
  "inflation_proxy_indicator",
213
213
  "expected_value",
214
+ "gamma_levels",
215
+ "intelligent_strategy_synthesizer",
216
+ "signal_confluence",
214
217
  ]);
215
218
 
216
219
  /**
@@ -613,6 +616,10 @@ function hasSeriesArg(args: CallArgs): boolean {
613
616
  return args.posNodes.some(isSeries) || Object.values(args.kwNodes).some(isSeries);
614
617
  }
615
618
 
619
+ function isSeriesNode(n: expr | undefined): boolean {
620
+ return n != null && n.kind === "Name" && BARE_SERIES_NAMES.has(n.id);
621
+ }
622
+
616
623
  /** True when a keyword argument named one of *names* is present (interpret hasKwarg). */
617
624
  function hasAnyKw(args: CallArgs, names: string[]): boolean {
618
625
  return names.some((n) => Object.hasOwn(args.kw, n));
@@ -2493,10 +2500,12 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
2493
2500
  ]);
2494
2501
  }
2495
2502
  if (method === "breakout_detection") {
2503
+ const arg = (i: number, names: string[]): string =>
2504
+ isSeriesNode(args.posNodes[i]) ? "null" : cellOf(pick(args, i, names, "null"));
2496
2505
  return taPure("breakoutDetection", [
2497
- cellOf(pick(args, 0, ["close", "source"], "null")),
2498
- cellOf(pick(args, 1, ["resistance"], "null")),
2499
- cellOf(pick(args, 2, ["support"], "null")),
2506
+ arg(0, ["close", "source"]),
2507
+ arg(1, ["resistance"]),
2508
+ arg(2, ["support"]),
2500
2509
  ]);
2501
2510
  }
2502
2511
  if (method === "ema_cross_signal") {
@@ -2627,5 +2636,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
2627
2636
  if (method === "expected_value") {
2628
2637
  return `__h.ta.expectedValue(${cellList(pick(args, 0, ["returns"], "null"))}, ${cellList(pick(args, 1, ["probabilities", "probs"], "null"))})`;
2629
2638
  }
2639
+ if (method === "gamma_levels") {
2640
+ const vol = isSeriesNode(args.posNodes[0])
2641
+ ? "null"
2642
+ : cellOf(pick(args, 0, ["volatility"], "null"));
2643
+ const price = isSeriesNode(args.posNodes[1])
2644
+ ? "null"
2645
+ : cellOf(pick(args, 1, ["current_price", "price"], "null"));
2646
+ return taPure("gammaLevels", [vol, price, cellOf(pick(args, 2, ["period", "length"], "null"))]);
2647
+ }
2648
+ if (method === "intelligent_strategy_synthesizer") {
2649
+ return taCall("intelligentStrategySynthesizer", site, [
2650
+ arrOrCell(pick(args, 0, ["trend_indicators", "trend"], "null")),
2651
+ arrOrCell(pick(args, 1, ["momentum_indicators", "momentum"], "null")),
2652
+ arrOrCell(pick(args, 2, ["volatility_indicators", "volatility"], "null")),
2653
+ arrOrCell(pick(args, 3, ["volume_indicators", "volume"], "null")),
2654
+ pick(args, 4, ["market_condition", "market_regime"], '"ranging"'),
2655
+ pick(args, 5, ["risk_profile"], '"balanced"'),
2656
+ ]);
2657
+ }
2658
+ if (method === "signal_confluence") {
2659
+ return taPure("signalConfluence", [pick(args, 0, ["signals"], "null")]);
2660
+ }
2630
2661
  return "null";
2631
2662
  }
@@ -4673,10 +4673,14 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4673
4673
  }
4674
4674
  if (fname === "ta.breakout_detection" || fname === "breakout_detection") {
4675
4675
  const msg = "ta.breakout_detection() requires 3 arguments";
4676
+ const arg = (i: number, names: string[]): Cell => {
4677
+ const e = wave10aArg(i, names, msg);
4678
+ return isSeriesExpr(e, env) ? NA : num(evalExpr(e, env));
4679
+ };
4676
4680
  return env.ta.breakoutDetection(
4677
- wave11n(0, ["close", "source"], msg),
4678
- wave11n(1, ["resistance"], msg),
4679
- wave11n(2, ["support"], msg),
4681
+ arg(0, ["close", "source"]),
4682
+ arg(1, ["resistance"]),
4683
+ arg(2, ["support"]),
4680
4684
  );
4681
4685
  }
4682
4686
  // wave12-end
@@ -4860,6 +4864,35 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4860
4864
  return env.ta.expectedValue(returns, probs);
4861
4865
  }
4862
4866
  // wave13-end
4867
+ // wave14-start — list-return + capstone synthesis
4868
+ if (fname === "ta.gamma_levels" || fname === "gamma_levels") {
4869
+ const msg = "ta.gamma_levels() requires 3 arguments";
4870
+ const volArg = wave10aArg(0, ["volatility"], msg);
4871
+ const priceArg = wave10aArg(1, ["current_price", "price"], msg);
4872
+ const vol = isSeriesExpr(volArg, env) ? NA : num(evalExpr(volArg, env));
4873
+ const price = isSeriesExpr(priceArg, env) ? NA : num(evalExpr(priceArg, env));
4874
+ return env.ta.gammaLevels(vol, price, wave11n(2, ["period", "length"], msg));
4875
+ }
4876
+ if (
4877
+ fname === "ta.intelligent_strategy_synthesizer" ||
4878
+ fname === "intelligent_strategy_synthesizer"
4879
+ ) {
4880
+ const msg = "ta.intelligent_strategy_synthesizer() requires 6 arguments";
4881
+ return env.ta.intelligentStrategySynthesizer(
4882
+ site,
4883
+ wave13v(0, ["trend_indicators", "trend"], msg),
4884
+ wave13v(1, ["momentum_indicators", "momentum"], msg),
4885
+ wave13v(2, ["volatility_indicators", "volatility"], msg),
4886
+ wave13v(3, ["volume_indicators", "volume"], msg),
4887
+ wave13v(4, ["market_condition", "market_regime"], msg),
4888
+ wave13v(5, ["risk_profile"], msg),
4889
+ );
4890
+ }
4891
+ if (fname === "ta.signal_confluence" || fname === "signal_confluence") {
4892
+ const msg = "ta.signal_confluence() requires 1 argument";
4893
+ return env.ta.signalConfluence(wave13v(0, ["signals"], msg));
4894
+ }
4895
+ // wave14-end
4863
4896
  return undefined;
4864
4897
  }
4865
4898
 
package/src/runtime/ta.ts CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  export type Cell = number | null;
10
10
 
11
+ import { PineArray } from "./array.ts";
12
+
11
13
  interface SmaState {
12
14
  period: number;
13
15
  window: Cell[];
@@ -4520,6 +4522,168 @@ export class TaEngine {
4520
4522
  return finiteOut(ev / totalProb) ?? 0;
4521
4523
  }
4522
4524
 
4525
+ /** `advanced.py` `_builtin_ta_gamma_levels` — returns `[high, low]`. */
4526
+ gammaLevels(volatility: Cell, currentPrice: Cell, period: Cell): PineArray {
4527
+ const v = finiteCell(volatility) ?? 0.02;
4528
+ const p = finiteCell(currentPrice) ?? 100;
4529
+ const n = this.coerceTaInt(period, "period must be integer");
4530
+ const volAdjusted = v * Math.sqrt(n);
4531
+ const distance = p * volAdjusted;
4532
+ const out = new PineArray();
4533
+ out.push(finiteOut(p + distance));
4534
+ out.push(finiteOut(p - distance));
4535
+ return out;
4536
+ }
4537
+
4538
+ /** `advanced.py` `_builtin_ta_intelligent_strategy_synthesizer` (wins MRO). */
4539
+ intelligentStrategySynthesizer(
4540
+ site: string,
4541
+ trend: unknown,
4542
+ momentum: unknown,
4543
+ volatility: unknown,
4544
+ volume: unknown,
4545
+ marketCondition: unknown,
4546
+ riskProfile: unknown,
4547
+ ): TaRecord {
4548
+ const th = this.windowFor(`${site}:trend`, trend);
4549
+ const mh = this.windowFor(`${site}:momentum`, momentum);
4550
+ const vh = this.windowFor(`${site}:volatility`, volatility);
4551
+ const volh = this.windowFor(`${site}:volume`, volume);
4552
+ const regime = typeof marketCondition === "string" ? marketCondition : "ranging";
4553
+ const profile = typeof riskProfile === "string" ? riskProfile : "balanced";
4554
+
4555
+ const nums = (h: Cell[]): number[] => h.filter((v): v is number => v !== null);
4556
+ const trendVals = nums(th);
4557
+ const momentumVals = nums(mh);
4558
+ const volatilityVals = nums(vh);
4559
+ const volumeVals = nums(volh);
4560
+ const avg = (vals: number[], fallback: number): number =>
4561
+ vals.length === 0 ? fallback : vals.reduce((a, b) => a + b, 0) / vals.length;
4562
+ const trendAvg = avg(trendVals, 0);
4563
+ const momentumAvg = avg(momentumVals, 0);
4564
+ const volatilityAvg = avg(volatilityVals, 0.5);
4565
+ const volumeAvg = avg(volumeVals, 0);
4566
+
4567
+ const volatilityNormalized = Math.max(0, Math.min(1, volatilityAvg));
4568
+ const composite = trendAvg * 0.4 + momentumAvg * 0.35 + volumeAvg * 0.25;
4569
+ const compositeSignal = Math.max(-1, Math.min(1, composite));
4570
+
4571
+ let agreement = 0;
4572
+ if (Math.abs(trendAvg) > 0.3) agreement += 1;
4573
+ if (Math.abs(momentumAvg) > 0.3) agreement += 1;
4574
+ if (Math.abs(volumeAvg) > 0.3) agreement += 1;
4575
+ const baseConfidence = agreement / 3;
4576
+ const confidence = Math.max(0.1, Math.min(0.99, baseConfidence - volatilityNormalized * 0.3));
4577
+
4578
+ let recommendation: string;
4579
+ if (compositeSignal > 0.6) {
4580
+ recommendation = profile === "aggressive" ? "aggressive_long" : "conservative_long";
4581
+ } else if (compositeSignal > 0.2) {
4582
+ recommendation = "conservative_long";
4583
+ } else if (compositeSignal < -0.6) {
4584
+ recommendation = profile === "aggressive" ? "aggressive_short" : "conservative_short";
4585
+ } else if (compositeSignal < -0.2) {
4586
+ recommendation = "conservative_short";
4587
+ } else {
4588
+ recommendation = "hold";
4589
+ }
4590
+
4591
+ const riskMult = profile === "conservative" ? 0.5 : profile === "aggressive" ? 1.5 : 1;
4592
+ const riskLevel = volatilityNormalized * 50 * riskMult;
4593
+ const absSignal = Math.abs(compositeSignal);
4594
+ const expectedReturn = absSignal * 3;
4595
+
4596
+ let holdingPeriod: string;
4597
+ if (volatilityNormalized > 0.7) holdingPeriod = "scalp";
4598
+ else if (volatilityNormalized > 0.5) holdingPeriod = "day_trade";
4599
+ else if (absSignal > 0.5) holdingPeriod = "swing";
4600
+ else holdingPeriod = "position";
4601
+
4602
+ const shortRec = recommendation === "aggressive_short" || recommendation === "conservative_short";
4603
+ const longRec = recommendation === "aggressive_long" || recommendation === "conservative_long";
4604
+ const stopLoss = shortRec ? -0.7 : longRec ? -0.2 : -0.15;
4605
+ const takeProfit =
4606
+ recommendation === "aggressive_long" || recommendation === "aggressive_short"
4607
+ ? 1.5
4608
+ : longRec || shortRec
4609
+ ? 1.0
4610
+ : 0.5;
4611
+
4612
+ let alignment = 50;
4613
+ if (regime === "trending_up") {
4614
+ if (compositeSignal > 0) alignment = 90;
4615
+ else if (compositeSignal > -0.3) alignment = 60;
4616
+ else alignment = 30;
4617
+ } else if (regime === "trending_down") {
4618
+ if (compositeSignal < 0) alignment = 90;
4619
+ else if (compositeSignal < 0.3) alignment = 60;
4620
+ else alignment = 30;
4621
+ } else if (regime === "ranging") {
4622
+ alignment = 50 + (1 - absSignal) * 30;
4623
+ } else if (regime === "volatile") {
4624
+ alignment = 30 + absSignal * 40;
4625
+ } else {
4626
+ alignment = 40;
4627
+ }
4628
+
4629
+ return taRec({
4630
+ composite_signal: finiteOut(compositeSignal) ?? 0,
4631
+ confidence_level: finiteOut(confidence) ?? 0,
4632
+ strategy_recommendation: recommendation,
4633
+ risk_level: finiteOut(Math.max(0, Math.min(100, riskLevel))) ?? 0,
4634
+ expected_return: finiteOut(expectedReturn) ?? 0,
4635
+ holding_period: holdingPeriod,
4636
+ stop_loss_priority: stopLoss,
4637
+ take_profit_priority: takeProfit,
4638
+ regime_alignment: finiteOut(Math.max(0, Math.min(100, alignment))) ?? 0,
4639
+ });
4640
+ }
4641
+
4642
+ /** `advanced.py` `_builtin_ta_signal_confluence` (dict/map of signals). */
4643
+ signalConfluence(signals: unknown): TaRecord {
4644
+ let vals: unknown[] = [];
4645
+ if (signals != null && typeof signals === "object") {
4646
+ const obj = signals as { values?: () => unknown[] };
4647
+ if (typeof obj.values === "function") {
4648
+ const got = obj.values();
4649
+ vals = Array.isArray(got) ? got : [];
4650
+ } else if (!Array.isArray(signals)) {
4651
+ vals = Object.entries(signals)
4652
+ .filter(([k]) => k !== "__taRecord")
4653
+ .map(([, v]) => v);
4654
+ }
4655
+ }
4656
+ let signalCount = 0;
4657
+ let bullish = 0;
4658
+ let bearish = 0;
4659
+ for (const v of vals) {
4660
+ if (typeof v === "boolean") {
4661
+ if (v) {
4662
+ bullish += 1;
4663
+ signalCount += 1;
4664
+ }
4665
+ continue;
4666
+ }
4667
+ if (typeof v === "number" && Number.isFinite(v)) {
4668
+ if (v > 0) {
4669
+ bullish += 1;
4670
+ signalCount += 1;
4671
+ } else if (v < 0) {
4672
+ bearish += 1;
4673
+ signalCount += 1;
4674
+ }
4675
+ }
4676
+ }
4677
+ const total = vals.length > 0 ? vals.length : 1;
4678
+ const confluence = signalCount / total;
4679
+ const primary = bullish > bearish ? "buy" : bearish > bullish ? "sell" : "neutral";
4680
+ return taRec({
4681
+ signal_count: signalCount,
4682
+ confluence_level: finiteOut(confluence) ?? 0,
4683
+ primary_signal: primary,
4684
+ });
4685
+ }
4686
+
4523
4687
  /**
4524
4688
  * `ta.volume_profile_high` — numeric list only (`advanced.py`).
4525
4689
  * A one-element pair is that price when its volume is the strict max.