dexbot 1.5.2 → 1.5.3

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/analysis/ama_fitting/package.json +1 -1
  3. package/analysis/resolve_source.ts +3 -0
  4. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  5. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  6. package/analysis/tradingview/README.md +5 -1
  7. package/analysis/tradingview/analyze_tradingview.ts +27 -1
  8. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  9. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  10. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +351 -22
  11. package/analysis/trend_detection/package.json +1 -1
  12. package/claw/package.json +1 -1
  13. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  14. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  15. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  16. package/dist/analysis/resolve_source.d.ts.map +1 -1
  17. package/dist/analysis/resolve_source.js +3 -0
  18. package/dist/analysis/resolve_source.js.map +1 -1
  19. package/dist/analysis/tradingview/analyze_tradingview.d.ts +3 -0
  20. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  21. package/dist/analysis/tradingview/analyze_tradingview.js +27 -2
  22. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  23. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  24. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +348 -22
  25. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  26. package/dist/market_adapter/core/asymmetric_bounds.d.ts +33 -1
  27. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  28. package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
  29. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  30. package/dist/modules/dexbot_class.d.ts +2 -18
  31. package/dist/modules/dexbot_class.d.ts.map +1 -1
  32. package/dist/modules/dexbot_cow_runtime.d.ts +37 -37
  33. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  34. package/dist/modules/dexbot_cow_runtime.js +655 -556
  35. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  36. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  37. package/dist/modules/dexbot_maintenance_runtime.js +31 -51
  38. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  39. package/dist/modules/order/grid.d.ts.map +1 -1
  40. package/dist/modules/order/grid.js +28 -37
  41. package/dist/modules/order/grid.js.map +1 -1
  42. package/dist/modules/order/manager.d.ts +2 -1
  43. package/dist/modules/order/manager.d.ts.map +1 -1
  44. package/dist/modules/order/manager.js +10 -1
  45. package/dist/modules/order/manager.js.map +1 -1
  46. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  47. package/dist/modules/order/sync_engine.js +74 -0
  48. package/dist/modules/order/sync_engine.js.map +1 -1
  49. package/dist/modules/order/utils/math.d.ts +9 -11
  50. package/dist/modules/order/utils/math.d.ts.map +1 -1
  51. package/dist/modules/order/utils/math.js +9 -11
  52. package/dist/modules/order/utils/math.js.map +1 -1
  53. package/dist/modules/order/utils/order.d.ts +51 -16
  54. package/dist/modules/order/utils/order.d.ts.map +1 -1
  55. package/dist/modules/order/utils/order.js +203 -29
  56. package/dist/modules/order/utils/order.js.map +1 -1
  57. package/dist/modules/order/utils/system.d.ts +1 -30
  58. package/dist/modules/order/utils/system.d.ts.map +1 -1
  59. package/dist/modules/order/utils/system.js +31 -98
  60. package/dist/modules/order/utils/system.js.map +1 -1
  61. package/dist/modules/order/utils/validate.d.ts +3 -2
  62. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  63. package/dist/modules/order/utils/validate.js +5 -1
  64. package/dist/modules/order/utils/validate.js.map +1 -1
  65. package/docs/DEXBOT_COMPARISON.md +3 -3
  66. package/docs/EVOLUTION.md +6 -5
  67. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  68. package/docs/README.md +2 -2
  69. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import { MARKET_ADAPTER } from '../../modules/constants.js';
5
- import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT } from '../chart_utils.js';
5
+ import { escapeHtml, serializeJsonForScript, UPLOT_SHARED_SCRIPT, embedFunctionSources } from '../chart_utils.js';
6
6
  import { cursorCSS, uplotBgCSS } from '../chart_css.js';
7
7
  import { zoomResetScript } from '../chart_ui.js';
8
8
  import { normalizeCandle } from '../math_utils.js';
@@ -10,6 +10,14 @@ import { PATHS } from '../../modules/paths.js';
10
10
  import { getStorage } from '../../modules/storage/index.js';
11
11
  const { readJSON } = getStorage();
12
12
  import { getErrorMessage } from '../../modules/utils/errors.js';
13
+ import { parseRelativeMultiplier } from '../../modules/order/utils/math.js';
14
+ import { computeAverageAmaSlopePct } from '../../market_adapter/core/strategies/dynamic_weight_series.js';
15
+ import {
16
+ resolveBaseBounds,
17
+ computeAsymmetricBoundsMetrics,
18
+ applyAsymmetricBounds,
19
+ applyNarrowingSideGuard,
20
+ } from '../../market_adapter/core/asymmetric_bounds.js';
13
21
 
14
22
 
15
23
  function inferBaseIntervalSeconds(candles: any[], fallback: any = 3600) {
@@ -103,6 +111,28 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
103
111
 
104
112
  const marketProfiles = data.marketProfiles || loadMarketProfiles();
105
113
  const defaultAmaConfig = resolveAmaDefaults({ meta, data, marketProfiles });
114
+ const rangeSlope = {
115
+ lookbackBars: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_LOOKBACK_BARS,
116
+ maxSlopePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_MAX_SLOPE_PCT,
117
+ neutralZonePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_NEUTRAL_ZONE_PCT,
118
+ maxSlopeOffset: MARKET_ADAPTER.DYNAMIC_WEIGHT_ASYMMETRIC_OFFSET_CLAMP,
119
+ maxAsymmetryFactor: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR,
120
+ minScaleSlots: MARKET_ADAPTER.ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS,
121
+ };
122
+ function defaultRangeSpan(input: { rangeSpan?: unknown; grid?: { minPrice?: unknown; maxPrice?: unknown } | null }): number {
123
+ const cli = Number(input.rangeSpan);
124
+ if (Number.isFinite(cli)) return Math.min(2, Math.max(1.25, cli));
125
+ const down = parseRelativeMultiplier(input.grid?.minPrice);
126
+ const up = parseRelativeMultiplier(input.grid?.maxPrice);
127
+ if (down != null && down > 1 && up != null && up > 1) return Math.min(2, Math.max(1.25, (down + up) / 2));
128
+ const vd = Number(input.grid?.minPrice);
129
+ const vu = Number(input.grid?.maxPrice);
130
+ if (Number.isFinite(vd) && Number.isFinite(vu) && vd > 0 && vu > vd) {
131
+ const ref = Math.sqrt(vd * vu);
132
+ return Math.min(2, Math.max(1.25, (ref / vd + vu / ref) / 2));
133
+ }
134
+ return 1.55;
135
+ }
106
136
  const defaults = {
107
137
  smaPeriod: Math.max(1, Math.round(data.smaPeriod ?? 500)),
108
138
  amaDefaults: defaultAmaConfig,
@@ -111,6 +141,21 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
111
141
  vwapEnabled: data.vwapEnabled === true,
112
142
  vwapBars: Math.max(5, Math.round(data.vwapBars ?? 500)),
113
143
  priceScale: data.priceScale === 'linear' ? 'linear' : 'log',
144
+ rangeEnabled: data.rangeEnabled !== false,
145
+ rangeScaleEnabled: data.rangeScaleEnabled === true,
146
+ rangeWidthPct: Number.isFinite(Number(data.rangeWidthPct)) && Number(data.rangeWidthPct) > 0
147
+ ? Number(data.rangeWidthPct)
148
+ : 2,
149
+ rangeSpan: defaultRangeSpan(data),
150
+ grid: data.grid && data.grid.minPrice != null && data.grid.maxPrice != null ? {
151
+ minPrice: data.grid.minPrice,
152
+ maxPrice: data.grid.maxPrice,
153
+ incrementPercent: Number(data.grid.incrementPercent) > 0 ? Number(data.grid.incrementPercent) : null,
154
+ maxAsymmetryFactor: Number.isFinite(Number(data.grid.maxAsymmetryFactor)) && Number(data.grid.maxAsymmetryFactor) > 0
155
+ ? Number(data.grid.maxAsymmetryFactor)
156
+ : rangeSlope.maxAsymmetryFactor,
157
+ } : null,
158
+ rangeSlope,
114
159
  };
115
160
 
116
161
  const assetLabelA = meta.assetA?.symbol || meta.assetA?.id || 'Asset A';
@@ -134,6 +179,12 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
134
179
  vwapEnabled: defaults.vwapEnabled,
135
180
  vwapBars: defaults.vwapBars,
136
181
  priceScale: defaults.priceScale,
182
+ rangeEnabled: defaults.rangeEnabled,
183
+ rangeScaleEnabled: defaults.rangeScaleEnabled,
184
+ rangeWidthPct: defaults.rangeWidthPct,
185
+ rangeSpan: defaults.rangeSpan,
186
+ grid: defaults.grid,
187
+ rangeSlope,
137
188
  defaultPairMode,
138
189
  assetLabelA,
139
190
  assetLabelB,
@@ -177,7 +228,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
177
228
  #brand { display: flex; flex-direction: column; gap: 4px; min-width: 240px; }
178
229
  #title { font-size: 15px; font-weight: 700; color: #f5f8fb; }
179
230
  #subtitle { font-size: 11px; color: #b9c6d4; text-transform: uppercase; letter-spacing: 0.6px; }
180
- #toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
231
+ #toolbar { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
232
+ .toolbar-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; align-items: center; }
181
233
  .group { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; border-left: 1px solid #263241; }
182
234
  .group:first-child { padding-left: 0; border-left: 0; }
183
235
  :root {
@@ -399,7 +451,8 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
399
451
  ${cursorCSS()}
400
452
  @media (max-width: 980px) {
401
453
  #topbar { flex-direction: column; align-items: flex-start; }
402
- #toolbar { justify-content: flex-start; }
454
+ #toolbar { align-items: flex-start; }
455
+ .toolbar-row { justify-content: flex-start; }
403
456
  .group { padding-left: 0; border-left: 0; flex-wrap: wrap; }
404
457
  #legend { min-width: 0; max-width: calc(100vw - 24px); }
405
458
  }
@@ -413,9 +466,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
413
466
  <div id="subtitle">${escapeHtml(poolLabel)} · ${escapeHtml(pairLabelNormal)} · ${escapeHtml(intervalLabel)} base · indicators from 1h · volume · uPlot</div>
414
467
  </div>
415
468
  <div id="toolbar">
416
- <div class="group" id="tf-group">
417
- ${timeframes.map((item: any) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
418
- </div>
469
+ <div class="toolbar-row">
419
470
  <div class="group">
420
471
  <div class="indicator">
421
472
  <span class="tag">pair</span>
@@ -429,6 +480,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
429
480
  <span class="tag">scale</span>
430
481
  <button type="button" class="scale-toggle${defaults.priceScale === 'linear' ? ' is-linear' : ''}" id="scale-toggle" data-scale="${escapeHtml(defaults.priceScale)}" aria-label="Toggle price scale" title="Toggle price scale">${defaults.priceScale === 'linear' ? 'Linear' : 'Log'}</button>
431
482
  </div>
483
+ </div>
484
+ <div class="group" id="tf-group">
485
+ ${timeframes.map((item: any) => `<button class="time-btn${item.label === defaultTimeframe.label ? ' active' : ''}" data-timeframe="${escapeHtml(item.label)}"${item.enabled ? '' : ' disabled'}>${escapeHtml(item.label)}</button>`).join('')}
486
+ </div>
487
+ <div class="group">
432
488
  <div class="indicator">
433
489
  <label><input type="checkbox" id="sma-toggle"${defaults.smaEnabled ? ' checked' : ''}> SMA</label>
434
490
  <span class="tag">period</span>
@@ -447,6 +503,10 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
447
503
  <button type="button" class="step-btn" id="vwap-bars-dec">▼</button>
448
504
  </span>
449
505
  </div>
506
+ </div>
507
+ </div>
508
+ <div class="toolbar-row">
509
+ <div class="group">
450
510
  <div class="indicator">
451
511
  <label><input type="checkbox" id="ama-toggle"${defaults.amaEnabled ? ' checked' : ''}> AMA</label>
452
512
  <span class="tag">er</span>
@@ -469,13 +529,22 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
469
529
  </span>
470
530
  <button type="button" class="reset-btn" id="ama-reset">Reset</button>
471
531
  </div>
472
- <div class="indicator" style="margin-top:4px">
532
+ <div class="indicator" title="Range min/max built only from the live AMA price (green above, red below); Scale sizes it by AMA slope like the grid build">
533
+ <label><input type="checkbox" id="range-toggle"${defaults.rangeEnabled ? ' checked' : ''}> Range</label>
534
+ <label title="Range Scaling: size the band by AMA slope like the grid build (trend side widens, opposite tightens) and fit the price axis to it"><input type="checkbox" id="range-scale-toggle"${defaults.rangeScaleEnabled ? ' checked' : ''}> Scale</label>
535
+ <span id="range-grid-wrap" style="display:${defaults.grid ? 'inline' : 'none'}" title="x-range around AMA (default = bot grid setting)">
536
+ <span class="tag">grid</span>
537
+ <input type="range" id="range-span" min="1.25" max="2" step="0.05" value="${defaults.rangeSpan.toFixed(2)}" style="width:90px;vertical-align:middle">
538
+ <span id="range-span-val" style="font-size:11px;color:#8b949e;width:40px;display:inline-block;text-align:right">${defaults.rangeSpan.toFixed(2)}x</span>
539
+ </span>
540
+ </div>
541
+ <div class="indicator">
473
542
  <label><input type="checkbox" id="ama-init-offset-toggle"> Init Offset</label>
474
543
  <input type="range" id="ama-init-offset" min="-50" max="50" value="0" step="1" style="width:120px;vertical-align:middle" disabled>
475
544
  <span id="ama-init-offset-val" style="font-size:11px;color:#8b949e;width:32px;display:inline-block;text-align:right">0%</span>
476
545
  </div>
477
546
  </div>
478
- <div class="group"></div>
547
+ </div>
479
548
  </div>
480
549
  </div>
481
550
  <div id="chart-shell">
@@ -495,6 +564,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
495
564
  <span class="legend-item"><span class="legend-dot" style="background:#a855f7"></span><span class="legend-label">AMA Off</span> <span class="legend-value" id="legend-ama-off">-</span></span>
496
565
 
497
566
  <span class="legend-item"><span class="legend-dot" style="background:#93c5fd"></span><span class="legend-label">VWMA</span> <span class="legend-value" id="legend-vwap">-</span></span>
567
+ <span class="legend-item"><span class="legend-dot" id="legend-range-dot" style="background:#8b949e"></span><span class="legend-label">Range</span> <span class="legend-value" id="legend-range">-</span></span>
498
568
  </div>
499
569
  </div>
500
570
  <div id="price-chart" title="Wheel: zoom time (ulos = tyhjaa tilaa datan ymparilla) · Drag: siirra aika- ja hintanakymaa · Wheel/drag hinta-akselilla: zoomaa hintaa · Double-click hinta-akselia: autofit"></div>
@@ -530,6 +600,14 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
530
600
  : Number(payload.amaDefaults?.slowPeriod || ${MARKET_ADAPTER.AMAS.AMA3.slowPeriod});
531
601
  let currentVwapEnabled = state.vwapEnabled ?? !!payload.vwapEnabled;
532
602
  let currentVwapBars = Number.isFinite(state.vwapBars) ? state.vwapBars : Number(payload.vwapBars || 500);
603
+ let currentRangeEnabled = state.rangeEnabled ?? !!payload.rangeEnabled;
604
+ let currentRangeScaleEnabled = state.rangeScaleEnabled ?? !!payload.rangeScaleEnabled;
605
+ let currentRangeWidthPct = Number.isFinite(state.rangeWidthPct) && Number(state.rangeWidthPct) > 0
606
+ ? Number(state.rangeWidthPct)
607
+ : (Number.isFinite(Number(payload.rangeWidthPct)) && Number(payload.rangeWidthPct) > 0 ? Number(payload.rangeWidthPct) : 2);
608
+ let currentRangeSpan = Number.isFinite(state.rangeSpan) && Number(state.rangeSpan) > 0
609
+ ? Math.min(2, Math.max(1.25, Number(state.rangeSpan)))
610
+ : (Number.isFinite(Number(payload.rangeSpan)) && Number(payload.rangeSpan) > 0 ? Math.min(2, Math.max(1.25, Number(payload.rangeSpan))) : 1.55);
533
611
  let currentAmaInitOffsetEnabled = false;
534
612
  let currentAmaInitOffset = Number.isFinite(state.amaInitOffset) ? state.amaInitOffset : 0;
535
613
  let currentPriceScale = state.priceScale || payload.priceScale || 'log';
@@ -551,6 +629,9 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
551
629
  let currentSmaInit = [];
552
630
  let currentSmaInitOff = [];
553
631
  let currentAmaOff = [];
632
+ let currentRangeUpper = [];
633
+ let currentRangeLower = [];
634
+ let currentRangeTrend = [];
554
635
  let priceChart = null;
555
636
  let volumeChart = null;
556
637
  let lastRenderedPriceScale = null;
@@ -749,6 +830,10 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
749
830
  amaSlowPeriod: currentAmaSlowPeriod,
750
831
  vwapEnabled: currentVwapEnabled,
751
832
  vwapBars: currentVwapBars,
833
+ rangeEnabled: currentRangeEnabled,
834
+ rangeScaleEnabled: currentRangeScaleEnabled,
835
+ rangeWidthPct: currentRangeWidthPct,
836
+ rangeSpan: currentRangeSpan,
752
837
  amaInitOffset: currentAmaInitOffset,
753
838
  priceScale: currentPriceScale,
754
839
  pairMode: currentPairMode,
@@ -1078,6 +1163,106 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1078
1163
  vwapCache.set(cacheKey, out);
1079
1164
  return out;
1080
1165
  }
1166
+ // Bot-grid range band: min/max derive ONLY from the live AMA price.
1167
+ // All math below runs the canonical market_adapter/grid sources,
1168
+ // embedded verbatim (not hand-copied): computeAverageAmaSlopePct,
1169
+ // applyAsymmetricBounds (+ metrics/base), applyNarrowingSideGuard,
1170
+ // parseRelativeMultiplier. With Range Scaling on, the AMA slope sizes
1171
+ // the real range: trend side widens, opposite side tightens. Painted
1172
+ // symmetrically always: green [AMA, upper], red [lower, AMA].
1173
+ // Without usable grid shape falls back to a width% envelope around AMA.
1174
+ // Never reads candles, pair-display mapping, or axis/zoom state —
1175
+ // inversion and timeframe sampling apply to the AMA first.
1176
+ ${embedFunctionSources([computeAverageAmaSlopePct, resolveBaseBounds, computeAsymmetricBoundsMetrics, applyAsymmetricBounds, applyNarrowingSideGuard, parseRelativeMultiplier])}
1177
+ function computeRangeBand(baseAma) {
1178
+ const n = Array.isArray(baseAma) ? baseAma.length : 0;
1179
+ const upper = new Array(n).fill(null);
1180
+ const lower = new Array(n).fill(null);
1181
+ const trend = new Array(n).fill(0);
1182
+ const slopeCfg = payload.rangeSlope || {};
1183
+ const gridCfg = payload.grid || null;
1184
+ const lookback = Math.max(1, Math.round(Number(slopeCfg.lookbackBars) || 9));
1185
+ const maxSlope = Number(slopeCfg.maxSlopePct) > 0 ? Number(slopeCfg.maxSlopePct) : 0.09;
1186
+ const neutral = Number(slopeCfg.neutralZonePct) >= 0 ? Number(slopeCfg.neutralZonePct) : 0;
1187
+ const maxSlopeOffset = Number(slopeCfg.maxSlopeOffset) > 0 ? Number(slopeCfg.maxSlopeOffset) : 0.5;
1188
+ const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
1189
+ ? Number(gridCfg.maxAsymmetryFactor)
1190
+ : (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
1191
+ const widthPct = Number(currentRangeWidthPct) > 0 ? Number(currentRangeWidthPct) : 2;
1192
+ const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
1193
+ const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
1194
+ // User x-range span (slider, default = bot grid ratio): the only
1195
+ // width input — min = AMA/span, max = AMA*span. Grid config only
1196
+ // selects the grid path vs the width% fallback, never a price.
1197
+ const span = Math.min(2, Math.max(1.25, Number(currentRangeSpan) > 0 ? Number(currentRangeSpan) : 1.55));
1198
+ const gMinN = Number(gridCfg && gridCfg.minPrice);
1199
+ const gMaxN = Number(gridCfg && gridCfg.maxPrice);
1200
+ const multOk = (v) => { const m = parseRelativeMultiplier(v); return m != null && m > 1; };
1201
+ const hasGridShape = gridCfg != null && (
1202
+ (multOk(gridCfg.minPrice) && multOk(gridCfg.maxPrice)) ||
1203
+ (Number.isFinite(gMinN) && Number.isFinite(gMaxN) && gMinN > 0 && gMaxN > gMinN));
1204
+ const slopeScaling = !!currentRangeScaleEnabled;
1205
+ if (!currentAmaEnabled || n === 0) return { upper, lower, trend };
1206
+ for (let i = 0; i < n; i++) {
1207
+ const ama = baseAma[i];
1208
+ const past = i - lookback >= 0 ? baseAma[i - lookback] : null;
1209
+ if (!Number.isFinite(ama) || ama <= 0 || !Number.isFinite(past) || past <= 0) continue;
1210
+ const slopePct = computeAverageAmaSlopePct(ama, past, lookback);
1211
+ if (slopePct == null || !Number.isFinite(slopePct)) continue;
1212
+ const dir = Math.abs(slopePct) <= neutral ? 0 : (slopePct > 0 ? 1 : -1);
1213
+ const strength = Math.min(Math.abs(slopePct) / maxSlope, 1);
1214
+ if (!hasGridShape) {
1215
+ // Width% envelope: symmetric around AMA; scaling sizes it
1216
+ // by slope strength ("real range"), otherwise full width.
1217
+ const w = slopeScaling ? strength * widthPct : widthPct;
1218
+ if (!(w > 0)) continue;
1219
+ const half = ama * w / 100;
1220
+ if (!Number.isFinite(half) || half <= 0) continue;
1221
+ upper[i] = ama + half;
1222
+ lower[i] = Math.max(ama - half, ama * 1e-6);
1223
+ trend[i] = dir;
1224
+ continue;
1225
+ }
1226
+ // Symmetric base from this bar's AMA and the user span —
1227
+ // no chart or config price level.
1228
+ let rMin = ama / span;
1229
+ let rMax = ama * span;
1230
+ if (slopeScaling && dir !== 0) {
1231
+ // Canonical tilt: ama_slope_model slope offset applied
1232
+ // through asymmetric_bounds, then the narrowing-side guard.
1233
+ const trendName = dir > 0 ? 'UP' : 'DOWN';
1234
+ const slopeOffset = clamp(slopePct / maxSlope, -1, 1) * maxSlopeOffset;
1235
+ const tilt = applyAsymmetricBounds({
1236
+ centerPrice: ama,
1237
+ minPrice: ama / span,
1238
+ maxPrice: ama * span,
1239
+ trend: trendName,
1240
+ slopeOffset,
1241
+ maxSlopeOffset,
1242
+ maxAsymmetryFactor: maxAsym,
1243
+ });
1244
+ const guard = applyNarrowingSideGuard({
1245
+ centerPrice: ama,
1246
+ minPrice: tilt.resolvedMinPrice,
1247
+ maxPrice: tilt.resolvedMaxPrice,
1248
+ trend: trendName,
1249
+ incrementPercent: inc,
1250
+ minScaleSlots: minSlots,
1251
+ });
1252
+ rMin = guard.minPrice;
1253
+ rMax = guard.maxPrice;
1254
+ }
1255
+ if (!Number.isFinite(rMin) || !Number.isFinite(rMax) || rMin <= 0 || rMax <= rMin) continue;
1256
+ upper[i] = rMax;
1257
+ lower[i] = rMin;
1258
+ trend[i] = dir;
1259
+ }
1260
+ return { upper, lower, trend };
1261
+ }
1262
+ function sampleTrendByIndex(series, idxs) {
1263
+ if (!Array.isArray(series) || !Array.isArray(idxs)) return [];
1264
+ return idxs.map((idx) => (Number.isFinite(idx) && series[idx] != null ? series[idx] : 0));
1265
+ }
1081
1266
  function lowerBound(arr, value) {
1082
1267
  let lo = 0;
1083
1268
  let hi = arr.length;
@@ -1098,6 +1283,25 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1098
1283
  let end = Math.min(xs.length, lowerBound(xs, maxX) + 2);
1099
1284
  let min = Infinity;
1100
1285
  let max = -Infinity;
1286
+ // Range scaling active: fit the price axis to the full envelope
1287
+ // (lower ↔ upper through AMA) instead of the candles, so the
1288
+ // switch visibly adjusts the range. Falls through to the candle
1289
+ // fit when the band has no finite values in view.
1290
+ if (currentRangeEnabled && currentRangeScaleEnabled && currentAmaEnabled) {
1291
+ let bmin = Infinity;
1292
+ let bmax = -Infinity;
1293
+ for (let i = start; i < end; i++) {
1294
+ const a = currentAma[i];
1295
+ const up = currentRangeUpper[i];
1296
+ const lo = currentRangeLower[i];
1297
+ if (Number.isFinite(a) && a > 0) { if (a < bmin) bmin = a; if (a > bmax) bmax = a; }
1298
+ if (Number.isFinite(up) && up > 0) { if (up < bmin) bmin = up; if (up > bmax) bmax = up; }
1299
+ if (Number.isFinite(lo) && lo > 0) { if (lo < bmin) bmin = lo; if (lo > bmax) bmax = lo; }
1300
+ }
1301
+ if (Number.isFinite(bmin) && Number.isFinite(bmax) && bmin > 0 && bmax > bmin) {
1302
+ return [bmin * 0.97, bmax * 1.03];
1303
+ }
1304
+ }
1101
1305
  for (let i = start; i < end; i++) {
1102
1306
  const lo = currentLow[i];
1103
1307
  const hi = currentHigh[i];
@@ -1174,11 +1378,24 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1174
1378
  refreshLegend();
1175
1379
  saveState();
1176
1380
  }
1381
+ function hideRangeBandImmediate() {
1382
+ currentRangeUpper = new Array(currentCandles.length).fill(null);
1383
+ currentRangeLower = new Array(currentCandles.length).fill(null);
1384
+ currentRangeTrend = new Array(currentCandles.length).fill(0);
1385
+ // The band is a draw hook with no uPlot series: nothing else
1386
+ // repaints on hide, so redraw explicitly (stale pixels otherwise linger).
1387
+ if (priceChart && typeof priceChart.redraw === 'function') priceChart.redraw();
1388
+ refreshLegend();
1389
+ saveState();
1390
+ }
1177
1391
  function hideAmaSeriesImmediate() {
1178
1392
  currentAma = new Array(currentCandles.length).fill(null);
1179
1393
  currentAmaOff = new Array(currentCandles.length).fill(null);
1180
1394
  currentSmaInit = new Array(currentCandles.length).fill(null);
1181
1395
  currentSmaInitOff = new Array(currentCandles.length).fill(null);
1396
+ currentRangeUpper = new Array(currentCandles.length).fill(null);
1397
+ currentRangeLower = new Array(currentCandles.length).fill(null);
1398
+ currentRangeTrend = new Array(currentCandles.length).fill(0);
1182
1399
  if (Array.isArray(currentPriceData) && currentPriceData.length >= 11) {
1183
1400
  currentPriceData[6] = currentAma;
1184
1401
  currentPriceData[8] = currentSmaInit;
@@ -1219,6 +1436,18 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1219
1436
  document.getElementById('legend-sma-init').textContent = Number.isFinite(currentSmaInit[idx]) ? fmtPrice(currentSmaInit[idx]) : '-';
1220
1437
  document.getElementById('legend-off-init').textContent = Number.isFinite(currentSmaInitOff[idx]) ? fmtPrice(currentSmaInitOff[idx]) : '-';
1221
1438
  document.getElementById('legend-ama-off').textContent = Number.isFinite(currentAmaOff[idx]) ? fmtPrice(currentAmaOff[idx]) : '-';
1439
+ const rangeDir = currentRangeTrend[idx];
1440
+ const rangeDot = document.getElementById('legend-range-dot');
1441
+ const rangeVal = document.getElementById('legend-range');
1442
+ if (rangeDot) rangeDot.style.background = rangeDir === 1 ? '#26a69a' : (rangeDir === -1 ? '#ef5350' : '#8b949e');
1443
+ if (rangeVal) {
1444
+ const ru = currentRangeUpper[idx];
1445
+ const rl = currentRangeLower[idx];
1446
+ const ama = currentAma[idx];
1447
+ if (currentRangeEnabled && currentAmaEnabled && Number.isFinite(ru) && ru > ama && Number.isFinite(rl) && rl > 0 && rl < ama && ama > 0) {
1448
+ rangeVal.textContent = '+' + ((ru - ama) / ama * 100).toFixed(1) + '% / -' + ((ama - rl) / ama * 100).toFixed(1) + '%';
1449
+ } else rangeVal.textContent = '-';
1450
+ }
1222
1451
  document.getElementById('legend-vwap').textContent = Number.isFinite(currentVwap[idx]) ? fmtPrice(currentVwap[idx]) : '-';
1223
1452
  }
1224
1453
  function candlePlugin() {
@@ -1362,6 +1591,80 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1362
1591
  hooks: { drawAxes: [drawVolumeBars] },
1363
1592
  };
1364
1593
  }
1594
+ function rangeBandPlugin() {
1595
+ function drawRangeBand(u) {
1596
+ if (!currentRangeEnabled || !currentAmaEnabled || !currentCandles.length) return;
1597
+ const xs = u.data[0];
1598
+ if (!Array.isArray(xs) || xs.length === 0) return;
1599
+ u.ctx.save();
1600
+ u.ctx.beginPath();
1601
+ u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
1602
+ u.ctx.clip();
1603
+ // Behind candles/AMA lines (same trick as the dynamic-weight signal background).
1604
+ u.ctx.globalCompositeOperation = 'destination-over';
1605
+ const UP_FILL = 'rgba(38,166,154,0.16)';
1606
+ const DOWN_FILL = 'rgba(239,83,80,0.16)';
1607
+ // Symmetric envelope, always both sides: green [AMA, upper]
1608
+ // plus red [lower, AMA] along the whole AMA, tilted by the
1609
+ // grid's range scaling. Paint per contiguous valid segment.
1610
+ let segTop = [];
1611
+ let segBot = [];
1612
+ const flush = () => {
1613
+ if (segTop.length > 1) {
1614
+ u.ctx.fillStyle = UP_FILL;
1615
+ u.ctx.beginPath();
1616
+ u.ctx.moveTo(segTop[0][0], segTop[0][1]);
1617
+ for (let k = 1; k < segTop.length; k++) u.ctx.lineTo(segTop[k][0], segTop[k][1]);
1618
+ for (let k = segTop.length - 1; k >= 0; k--) u.ctx.lineTo(segTop[k][0], segTop[k][2]);
1619
+ u.ctx.closePath();
1620
+ u.ctx.fill();
1621
+ }
1622
+ if (segBot.length > 1) {
1623
+ u.ctx.fillStyle = DOWN_FILL;
1624
+ u.ctx.beginPath();
1625
+ u.ctx.moveTo(segBot[0][0], segBot[0][1]);
1626
+ for (let k = 1; k < segBot.length; k++) u.ctx.lineTo(segBot[k][0], segBot[k][1]);
1627
+ for (let k = segBot.length - 1; k >= 0; k--) u.ctx.lineTo(segBot[k][0], segBot[k][2]);
1628
+ u.ctx.closePath();
1629
+ u.ctx.fill();
1630
+ }
1631
+ u.ctx.strokeStyle = 'rgba(38,166,154,0.55)';
1632
+ u.ctx.lineWidth = 1;
1633
+ u.ctx.beginPath();
1634
+ segTop.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
1635
+ u.ctx.stroke();
1636
+ u.ctx.strokeStyle = 'rgba(239,83,80,0.55)';
1637
+ u.ctx.beginPath();
1638
+ segBot.forEach((p, k) => { if (k === 0) u.ctx.moveTo(p[0], p[1]); else u.ctx.lineTo(p[0], p[1]); });
1639
+ u.ctx.stroke();
1640
+ segTop = [];
1641
+ segBot = [];
1642
+ };
1643
+ for (let i = 0; i < xs.length; i++) {
1644
+ const a = currentAma[i];
1645
+ const up = currentRangeUpper[i];
1646
+ const lo = currentRangeLower[i];
1647
+ if (!Number.isFinite(a) || a <= 0 || !Number.isFinite(up) || up <= a
1648
+ || !Number.isFinite(lo) || lo <= 0 || lo >= a) {
1649
+ flush();
1650
+ continue;
1651
+ }
1652
+ const x = u.valToPos(xs[i], 'x', true);
1653
+ const yA = u.valToPos(a, 'y', true);
1654
+ const yU = u.valToPos(up, 'y', true);
1655
+ const yL = u.valToPos(lo, 'y', true);
1656
+ if (!Number.isFinite(x) || !Number.isFinite(yA) || !Number.isFinite(yU) || !Number.isFinite(yL)) {
1657
+ flush();
1658
+ continue;
1659
+ }
1660
+ segTop.push([x, yU, yA]);
1661
+ segBot.push([x, yL, yA]);
1662
+ }
1663
+ flush();
1664
+ u.ctx.restore();
1665
+ }
1666
+ return { hooks: { draw: [drawRangeBand] } };
1667
+ }
1365
1668
  function buildData() {
1366
1669
  const tf = timeframeMap.get(currentTimeframe) || timeframeMap.get(defaultTimeframe.label) || timeframes[0];
1367
1670
  currentSeriesState = getSeriesState(currentPairMode);
@@ -1396,6 +1699,10 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1396
1699
  currentSma = new Array(currentCandles.length).fill(null);
1397
1700
  }
1398
1701
  currentAma = currentAmaEnabled ? sampleSeriesByIndex(baseAma, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1702
+ const baseRange = computeRangeBand(baseAma);
1703
+ currentRangeUpper = sampleSeriesByIndex(baseRange.upper, aggregated.idxs);
1704
+ currentRangeLower = sampleSeriesByIndex(baseRange.lower, aggregated.idxs);
1705
+ currentRangeTrend = sampleTrendByIndex(baseRange.trend, aggregated.idxs);
1399
1706
  currentAmaOff = currentAmaEnabled && currentAmaInitOffset !== 0 ? sampleSeriesByIndex(baseAmaOff, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1400
1707
  currentVwap = currentVwapEnabled ? sampleSeriesByIndex(baseVwap, aggregated.idxs) : new Array(currentCandles.length).fill(null);
1401
1708
  const amaCfg = currentAmaConfig();
@@ -1500,7 +1807,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1500
1807
 
1501
1808
  const plugin = candlePlugin();
1502
1809
  plugin.opts(null, priceOpts);
1503
- priceOpts.plugins = [plugin];
1810
+ priceOpts.plugins = [rangeBandPlugin(), plugin];
1504
1811
  if (!priceChart) priceChart = new uPlot(priceOpts, data.priceData, priceEl);
1505
1812
  else priceChart.setData(data.priceData, false);
1506
1813
 
@@ -1657,12 +1964,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1657
1964
  priceBounds = null;
1658
1965
  return;
1659
1966
  }
1660
- const range = max - min;
1661
1967
  // Uloszoomauksen rajat: alin naytettava hinta voi painua viimeistaan
1662
- // ~35 % datan minimin alapuolelle, ylin ~1.6x datan maksimin, ja
1663
- // kokonaisvali saa olla enintaan ~1.6x datan hintahaarukasta —
1968
+ // ~35 % datan minimin alapuolelle, ylin ~1.6x datan maksimin —
1664
1969
  // ei siis paase "lipsahtamaan" candleita nakyvasta kadottaen.
1665
- priceBounds = { min: min, max: max, floor: min * 0.35, ceil: max * 1.6, maxSpan: range * 1.6 };
1970
+ // Kokonaisvalille ei ole ylarajaa: pystyakselia saa zoomata ulos vapaasti.
1971
+ priceBounds = { min: min, max: max, floor: min * 0.35, ceil: max * 1.6 };
1666
1972
  }
1667
1973
  function applyYRange(chart, min, max) {
1668
1974
  if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return false;
@@ -1675,13 +1981,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1675
1981
  if (priceBounds) {
1676
1982
  let nMin = Math.max(min, priceBounds.floor);
1677
1983
  let nMax = Math.min(max, priceBounds.ceil);
1678
- if (nMax - nMin > priceBounds.maxSpan) {
1679
- const c = (nMin + nMax) / 2;
1680
- nMin = c - priceBounds.maxSpan / 2;
1681
- nMax = c + priceBounds.maxSpan / 2;
1682
- nMin = Math.max(nMin, priceBounds.floor);
1683
- nMax = Math.min(nMax, priceBounds.ceil);
1684
- }
1685
1984
  if (nMax <= nMin) return false;
1686
1985
  min = nMin;
1687
1986
  max = nMax;
@@ -1765,6 +2064,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1765
2064
  document.getElementById('ama-slow').value = currentAmaSlowPeriod.toFixed(1);
1766
2065
  document.getElementById('vwap-toggle').checked = currentVwapEnabled;
1767
2066
  document.getElementById('vwap-bars').value = String(currentVwapBars);
2067
+ document.getElementById('range-toggle').checked = currentRangeEnabled;
2068
+ document.getElementById('range-scale-toggle').checked = currentRangeScaleEnabled;
2069
+ document.getElementById('range-span').value = Number(currentRangeSpan).toFixed(2);
2070
+ document.getElementById('range-span-val').textContent = Number(currentRangeSpan).toFixed(2) + 'x';
2071
+ document.getElementById('range-grid-wrap').style.display = payload.grid ? 'inline' : 'none';
1768
2072
  document.getElementById('ama-init-offset-toggle').checked = currentAmaInitOffsetEnabled;
1769
2073
  document.getElementById('ama-init-offset').value = String(currentAmaInitOffset);
1770
2074
  document.getElementById('ama-init-offset').disabled = !currentAmaInitOffsetEnabled;
@@ -1835,7 +2139,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1835
2139
 
1836
2140
  setControls();
1837
2141
  rerender(false);
1838
-
1839
2142
  document.querySelectorAll('.time-btn').forEach((btn) => {
1840
2143
  btn.addEventListener('click', () => {
1841
2144
  if (btn.disabled) return;
@@ -1846,7 +2149,6 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1846
2149
  rerender(true);
1847
2150
  });
1848
2151
  });
1849
-
1850
2152
  const scaleToggle = document.getElementById('scale-toggle');
1851
2153
  if (scaleToggle) {
1852
2154
  scaleToggle.addEventListener('click', () => {
@@ -1879,6 +2181,9 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1879
2181
  document.getElementById('ama-init-offset-val').textContent = currentAmaInitOffset + '%';
1880
2182
  currentVwapEnabled = document.getElementById('vwap-toggle').checked;
1881
2183
  currentVwapBars = clamp(Math.round(Number(document.getElementById('vwap-bars').value) || 500), 24, 2000);
2184
+ currentRangeEnabled = document.getElementById('range-toggle').checked;
2185
+ currentRangeScaleEnabled = document.getElementById('range-scale-toggle').checked;
2186
+ currentRangeSpan = Math.min(2, Math.max(1.25, Math.round((Number(document.getElementById('range-span').value) || 1.55) * 20) / 20));
1882
2187
  setControls();
1883
2188
  rerender(false);
1884
2189
  };
@@ -1917,6 +2222,25 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1917
2222
  rerender(false);
1918
2223
  });
1919
2224
  document.getElementById('ama-init-offset-toggle').addEventListener('change', syncInputs);
2225
+ document.getElementById('range-toggle').addEventListener('change', () => {
2226
+ const nextEnabled = document.getElementById('range-toggle').checked;
2227
+ if (currentRangeEnabled === nextEnabled) return;
2228
+ currentRangeEnabled = nextEnabled;
2229
+ setControls();
2230
+ if (!currentRangeEnabled) {
2231
+ hideRangeBandImmediate();
2232
+ return;
2233
+ }
2234
+ rerender(false);
2235
+ });
2236
+ document.getElementById('range-scale-toggle').addEventListener('change', () => {
2237
+ const nextEnabled = document.getElementById('range-scale-toggle').checked;
2238
+ if (currentRangeScaleEnabled === nextEnabled) return;
2239
+ currentRangeScaleEnabled = nextEnabled;
2240
+ manualYRange = null;
2241
+ setControls();
2242
+ rerender(false);
2243
+ });
1920
2244
  ['sma-period', 'ama-er', 'ama-fast', 'ama-slow', 'vwap-bars'].forEach((id) => {
1921
2245
  document.getElementById(id).addEventListener('change', syncInputs);
1922
2246
  document.getElementById(id).addEventListener('blur', syncInputs);
@@ -1968,6 +2292,11 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1968
2292
  document.getElementById('ama-init-offset-val').textContent = snapped + '%';
1969
2293
  syncInputs();
1970
2294
  });
2295
+ document.getElementById('range-span').addEventListener('input', () => {
2296
+ const input = document.getElementById('range-span');
2297
+ document.getElementById('range-span-val').textContent = Number(input.value).toFixed(2) + 'x';
2298
+ syncInputs();
2299
+ });
1971
2300
 
1972
2301
  window.addEventListener('resize', () => {
1973
2302
  if (!charts.length) return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trend-detection",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Kalman filter trend detection with tactical/modal state tracking",
5
5
  "main": "../../dist/analysis/trend_detection/kalman_trend_analyzer.js",
6
6
  "scripts": {
package/claw/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dexbot2-claw",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "private": true,
5
5
  "description": "Claw bridge and runtime integration layer for BitShares and DEXBot2.",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "id": "bitshares-claw",
3
3
  "name": "BitShares Claw",
4
4
  "description": "Native BitShares and DEXBot2 tools from the DEXBot2 claw integration",
5
- "version": "1.5.2",
5
+ "version": "1.5.3",
6
6
  "skills": [
7
7
  "skills"
8
8
  ],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bitshares-claw-openclaw-plugin",
3
3
  "private": true,
4
- "version": "1.5.2",
4
+ "version": "1.5.3",
5
5
  "type": "module",
6
6
  "main": "index.js"
7
7
  }
@@ -30,7 +30,7 @@ async function testParserAcceptsJsonlAcrossChunksAndSingleBuffer() {
30
30
  capabilities: {},
31
31
  clientInfo: {
32
32
  name: 'claw-mcp-transport-test',
33
- version: '1.5.2'
33
+ version: '1.5.3'
34
34
  },
35
35
  protocolVersion: '2024-11-05'
36
36
  }
@@ -187,7 +187,7 @@ async function testMainEntrypointHandlesRealProcessInitialize() {
187
187
  capabilities: {},
188
188
  clientInfo: {
189
189
  name: 'claw-mcp-transport-test',
190
- version: '1.5.2'
190
+ version: '1.5.3'
191
191
  },
192
192
  protocolVersion: '2024-11-05'
193
193
  }