dexbot 1.6.4 → 1.6.5

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 (126) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -12
  3. package/analysis/README.md +8 -1
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_dynamic_weight.ts +9 -3
  6. package/analysis/tradingview/analyze_tradingview.ts +5 -0
  7. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +2 -2
  8. package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +35 -2
  9. package/analysis/trend_detection/dynamic_weight_chart_generator.ts +82 -10
  10. package/analysis/trend_detection/package.json +1 -1
  11. package/claw/modules/dexbot_profiles.ts +4 -3
  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/claw/tests/test_dexbot_profiles.ts +18 -1
  17. package/dist/analysis/analyze_dynamic_weight.js +9 -3
  18. package/dist/analysis/analyze_dynamic_weight.js.map +1 -1
  19. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  20. package/dist/analysis/tradingview/analyze_tradingview.js +5 -0
  21. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  22. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +2 -2
  23. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
  24. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +82 -10
  25. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
  26. package/dist/dexbot.d.ts.map +1 -1
  27. package/dist/dexbot.js +27 -68
  28. package/dist/dexbot.js.map +1 -1
  29. package/dist/market_adapter/core/asymmetric_bounds.d.ts +2 -2
  30. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  31. package/dist/market_adapter/core/asymmetric_bounds.js +15 -12
  32. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  33. package/dist/market_adapter/interval_utils.d.ts +1 -1
  34. package/dist/market_adapter/interval_utils.js +1 -1
  35. package/dist/modules/account_bots.d.ts +55 -5
  36. package/dist/modules/account_bots.d.ts.map +1 -1
  37. package/dist/modules/account_bots.js +258 -117
  38. package/dist/modules/account_bots.js.map +1 -1
  39. package/dist/modules/bot_defaults.d.ts +98 -0
  40. package/dist/modules/bot_defaults.d.ts.map +1 -0
  41. package/dist/modules/bot_defaults.js +169 -0
  42. package/dist/modules/bot_defaults.js.map +1 -0
  43. package/dist/modules/bot_settings.d.ts.map +1 -1
  44. package/dist/modules/bot_settings.js +4 -1
  45. package/dist/modules/bot_settings.js.map +1 -1
  46. package/dist/modules/config.d.ts +1 -0
  47. package/dist/modules/config.d.ts.map +1 -1
  48. package/dist/modules/config.js +1 -0
  49. package/dist/modules/config.js.map +1 -1
  50. package/dist/modules/constants.d.ts +11 -2
  51. package/dist/modules/constants.d.ts.map +1 -1
  52. package/dist/modules/constants.js +40 -27
  53. package/dist/modules/constants.js.map +1 -1
  54. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
  55. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  56. package/dist/modules/dexbot_maintenance_runtime.js +9 -4
  57. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  58. package/dist/modules/launcher/adapter_requirement.js +1 -1
  59. package/dist/modules/launcher/launch_modes.d.ts +17 -1
  60. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  61. package/dist/modules/launcher/launch_modes.js +18 -1
  62. package/dist/modules/launcher/launch_modes.js.map +1 -1
  63. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  64. package/dist/modules/launcher/monolithic_runtime.js +8 -1
  65. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  66. package/dist/modules/market_adapter_whitelist.d.ts +40 -1
  67. package/dist/modules/market_adapter_whitelist.d.ts.map +1 -1
  68. package/dist/modules/market_adapter_whitelist.js +161 -11
  69. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  70. package/dist/modules/order/grid.d.ts +9 -0
  71. package/dist/modules/order/grid.d.ts.map +1 -1
  72. package/dist/modules/order/grid.js +23 -5
  73. package/dist/modules/order/grid.js.map +1 -1
  74. package/dist/modules/order/manager.js +3 -2
  75. package/dist/modules/order/manager.js.map +1 -1
  76. package/dist/modules/order/utils/system.d.ts +5 -1
  77. package/dist/modules/order/utils/system.d.ts.map +1 -1
  78. package/dist/modules/order/utils/system.js +5 -2
  79. package/dist/modules/order/utils/system.js.map +1 -1
  80. package/dist/modules/settings_merge.d.ts +10 -1
  81. package/dist/modules/settings_merge.d.ts.map +1 -1
  82. package/dist/modules/settings_merge.js +27 -17
  83. package/dist/modules/settings_merge.js.map +1 -1
  84. package/dist/modules/validate_profiles.d.ts.map +1 -1
  85. package/dist/modules/validate_profiles.js +4 -3
  86. package/dist/modules/validate_profiles.js.map +1 -1
  87. package/dist/scripts/chart_command.d.ts +48 -0
  88. package/dist/scripts/chart_command.d.ts.map +1 -0
  89. package/dist/scripts/chart_command.js +504 -0
  90. package/dist/scripts/chart_command.js.map +1 -0
  91. package/dist/scripts/dw.d.ts +3 -0
  92. package/dist/scripts/dw.d.ts.map +1 -0
  93. package/dist/scripts/dw.js +30 -0
  94. package/dist/scripts/dw.js.map +1 -0
  95. package/dist/scripts/tv.d.ts +1 -31
  96. package/dist/scripts/tv.d.ts.map +1 -1
  97. package/dist/scripts/tv.js +10 -463
  98. package/dist/scripts/tv.js.map +1 -1
  99. package/dist/scripts/update.js +99 -62
  100. package/dist/scripts/update.js.map +1 -1
  101. package/dist/scripts/update_dist_freshness.d.ts +46 -0
  102. package/dist/scripts/update_dist_freshness.d.ts.map +1 -0
  103. package/dist/scripts/update_dist_freshness.js +115 -0
  104. package/dist/scripts/update_dist_freshness.js.map +1 -0
  105. package/dist/unlock.js +1 -1
  106. package/dist/unlock.js.map +1 -1
  107. package/docs/BITSHARES_ONBOARDING.md +10 -7
  108. package/docs/DEXBOT_COMPARISON.md +3 -3
  109. package/docs/EVOLUTION.md +8 -7
  110. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -4
  111. package/docs/GRID_RECALCULATION.md +20 -24
  112. package/docs/GRID_RECONCILE.md +2 -2
  113. package/docs/LOGGING.md +1 -0
  114. package/docs/README.md +16 -16
  115. package/docs/WORKFLOW.md +1 -2
  116. package/docs/architecture.md +2 -2
  117. package/docs/developer_guide.md +1 -0
  118. package/market_adapter/README.md +36 -60
  119. package/modules/README.md +2 -1
  120. package/package.json +1 -2
  121. package/scripts/README.md +18 -25
  122. package/scripts/reset-settings.sh +1 -1
  123. package/dist/scripts/generate_market_adapter_whitelist.d.ts +0 -15
  124. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +0 -1
  125. package/dist/scripts/generate_market_adapter_whitelist.js +0 -191
  126. package/dist/scripts/generate_market_adapter_whitelist.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.6.5] - 2026-09-23 - Editor-Managed Whitelist Flags, GridPrice Normalization, Pool/Health Cues, Log-Symmetric Range Tilt, Centralized Bot Defaults, Dynamic-Weight CLI, Update Self-Heal
6
+
7
+ ### 2026-09-23
8
+
9
+ - **Fix(bot-editor)**: treat `gridPrice` deactivating entries as `startPrice` and normalize them to `null` — typing `n`/`no`/`false`/`0`/`f`/`s`/`start`/`none`/blank, or a lone space, now commits `null` and the editor renders it as red `startPrice`, matching the live echo. Previously a whitespace-only entry previewed `startPrice` but a bare-Enter path silently kept the old value (`readInput` trimmed the resolved line, so a typed space was indistinguishable from Enter). `readInput` gains a `trimInput` option (default true, existing callers unchanged); `askGridPriceMode` opts out and tells a bare Enter (keep current) from whitespace (overwrite null). New `GRID_PRICE_UNSET_INPUTS`/`isUnsetGridPrice` in `modules/bot_defaults.ts` is the single spelling set, shared by the editor prompt, the display, and `seedBotDraft`, so a hand-edited `"gridPrice": false`/`"no"` normalizes to null at seed time instead of lingering as a value the runtime only silently degrades. Tests: unset → null (`tests/test_bot_defaults_characterization.ts`), red-startPrice rendering (`tests/test_account_bots_adapter.ts`, which now exports `colorGridPriceValue`), and `trimInput` default-vs-opt-out (`tests/test_read_input.ts`) (`modules/account_bots.ts`, `modules/bot_defaults.ts`, `modules/order/utils/system.ts`).
10
+
11
+ - **Feat(bot-editor)**: clearer `Pool` and `Adapter` status cues — the `3) Price` summary's `Pool:` field no longer reads `none` when the runtime auto-selects the pair's default pool: it shows a green `default` when `startPrice` is `pool`, a grey `none` otherwise, and a pinned `poolRef` still shows its ID (display-only; `formatPoolRefLabel` never touches the stored value; a `gridPrice` of `pool`/`book` is discouraged market-price anchoring and stays red in its own field). `6) Adapter` Weight/Range `false` now render bright yellow (`COLORS.yellowBold`) as optional-rider warnings instead of red; `Price` false stays red as the gate. New `testFormatPoolRefLabel` and a red-`book` assertion in `tests/test_account_bots_adapter.ts` (which now exports `formatPoolRefLabel`).
12
+
13
+ - **Feat(settings)**: expose the AMA-slope reset trigger in General settings — `1) Grid Health` now also edits `MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT` (default `8%`, range 0.1-100; the slope-delta trigger as a percentage of max AMA slope), and the health line renders both thresholds with the Δ symbol and `%` (`AMA Δ: 1%, AMA-Slope Δ: 8%`); the input prompts are labelled `AMA Δ` / `AMA-Slope Δ` (the unit stays on the health line, not repeated at the prompt). `loadGeneralSettings` validates the new value with the same `>0` guard as the center-delta threshold, so a hand-edited `0`/negative/NaN cannot silently disable the slope reset. Also relabels the first Grid Health value `Ratio` → `Funds` (the trigger is available-funds/allocated-capital, so the old name was vague; config key `GRID_REGENERATION_PERCENTAGE` unchanged) and its prompt `Grid Ratio Regeneration %` → `Grid Funds Regeneration %`. Docs: `README.md` global-settings reference and the `docs/GRID_RECALCULATION.md` range-scaling trigger note updated (`modules/account_bots.ts`, `README.md`, `docs/GRID_RECALCULATION.md`).
14
+
15
+ - **Refactor(whitelist)**: remove the legacy `dexbot whitelist`/`white` command and its bulk generator — `scripts/generate_market_adapter_whitelist.ts` and the `market-adapter:whitelist` npm script are fully superseded by the per-bot editor (`dexbot bot` -> `2) Modify bot` -> `6) Adapter`), which reads/writes `profiles/market_adapter_whitelist.json` directly. The command, script, CLI help/alias/docstring entries, and all docs/tests that referenced it are gone; deleting a bot in the editor now prunes its whitelist entry via a new `removeWhitelistEntry(botKey)` helper (`modules/market_adapter_whitelist.ts`), run only after the `bots.json` save succeeds (`modules/account_bots.ts`), so a deleted bot cannot leave stale flags for a future same-name bot to inherit (a malformed whitelist file aborts the removal and is never silently replaced). Behavioral impact: `dexbot whitelist`/`white` is an unknown command; bots removed by hand-editing `bots.json` still need manual pruning (documented in `market_adapter/README.md`). New `tests/test_account_bots_adapter.ts` case (`dexbot.ts`, `package.json`, `scripts/README.md`, `scripts/reset-settings.sh`, `docs/README.md`, `docs/GRID_RECALCULATION.md`, `docs/WORKFLOW.md`, `market_adapter/README.md`).
16
+
17
+ - **Feat(bot-editor)**: default `gridPrice` to `ama3` and color AMA values as healthy — `DEFAULT_CONFIG.gridPrice` was null (delegating to `startPrice`), so a freshly created bot priced its bounds off `startPrice` while every other AMA-capable path expects a preset; the editor also greened pool/book/numeric exactly like valid AMA values. `DEFAULT_CONFIG.gridPrice` is now `"ama3"` (`modules/constants.ts`), the prompt label reads `gridPrice (ama1/ama2/ama3/ama4)` (bare `ama` still accepted), and `colorGridPriceValue()` greens only AMA values and reds pool/book/numeric/null across the Enter default, live input echo, and `3) Price` menu line. Section `6) Adapter` now renders Price/Weight/Range through `colorBooleanFlag(v, true)` instead of a local gray-for-false lambda, so each flag reads as a health state (green on, red off) like `Active` — the optional Weight/Range off-state was later softened to bright yellow (see the `Fix(bot-editor)` entry above). Input acceptance is unchanged — pool, book, bare ama, ama1..ama4, positive numbers, none/null/startprice, and Enter keeping the current value all still work; red flags "not an AMA value" rather than blocking it. New drafts and configs missing the key seed `ama3`; explicit values in existing bots entries are untouched (seeding fills only undefined), and whitelist flags stay false for a bot with no entry, so an ama3 bot starts in adapter dry-run and falls back to startPrice with a warning until Adapter -> Price is enabled. `modules/bot_defaults.ts` and `tests/test_bot_defaults_characterization.ts` follow the new default.
18
+
19
+ - **Refactor(launcher)**: spawn the supervised bot worker as `worker`, not `test` — `dexbot start` launched the bot as `dist/dexbot.js test`, which read like a test job in htop/ps even though it is the live runtime. `buildDexbotStartArgs` now emits `['worker', ...(dryrun ? ['--dryrun'] : []), botName?]` (`modules/launcher/monolithic_runtime.ts`); `unlock.ts` sets `DEXBOT_LAUNCHER_WORKER=1` on the child env (`modules/config.ts` exposes the flag) and `dexbot.ts` intercepts the internal `worker` command when the marker is set, before command validation, so it never re-enters the supervisor. Public `dexbot start` still delegates to `unlock`; the `test` help wording was clarified. Launcher/unlock tests updated, including an assertion that the child carries the worker marker.
20
+
21
+ - **Fix(update)**: rebuild when source is current but `dist/` is stale — the git update flow exited before `npm run build` when there were no incoming commits, and tsc's incremental cache will not re-emit an output it believes is current (it will not even recreate a deleted file), so a `dist/` that lagged its source stayed stale while every later run reported "already up to date"; the old guard only checked the `dist/modules/dexbot_class.js` mtime, so a stale `dist/dexbot.js` (holding the CLI alias table) went unnoticed. New `scripts/update_dist_freshness.ts` detects missing/stale outputs without invoking the compiler over exactly the root tsconfig include roots; `scripts/update.ts` self-heals on the no-op path (force a full rebuild, then restart), re-checks and force-emits on the normal path, and `assertDistBundleFresh()` replaces the single-marker guard with a full-bundle check. New `tests/test_update_dist_freshness.ts` covers missing entries, missing/stale counterparts, root-level entry points, and non-compiled files.
22
+
23
+ - **Feat(defaults)**: centralize bot defaults, settings-doc building, and whitelist flags — seven scattered producers applied `DEFAULT_CONFIG` and whitelist defaults independently and had already drifted. New `modules/bot_defaults.ts` (`seedBotDraft`/`seedBotEntry`/`seedBotRuntimeConfig` with classified key sets, `normalizeBotDraft`/`normalizeBotEntry`) now backs the editor draft, `bot_settings` + claw, and the `OrderManager` constructor. `buildDefaultGeneralSettings()` (`modules/constants.ts`) and `buildNodesView()` (`modules/settings_merge.ts`) give the first-run generator, the editor fallback (`account_bots.loadGeneralSettings`), and the local-overrides merge one shared document (`loadGeneralSettings`/`saveGeneralSettings` now exported). `DEFAULT_WHITELIST_FLAGS`/`AMA_ONLY`/`ALL_ENABLED` constants replace every inline flag literal. Behavioral impact: a draft's `gridPrice` follows a `DEFAULT_CONFIG.gridPrice` override (default stays null here); a missing `bot.active` sources `DEFAULT_CONFIG.active` (was hardcoded true) and claw no longer coerces present null/0, unifying on `active !== false`; `OrderManager` clones absent defaults so config mutations cannot leak into the global default; brand-new first-run settings files gain `NODE_MANAGEMENT`, drop `ANCHOR:{}`, and match the editor key order. Existing `bots.json`/`general.settings.json` are not rewritten (merge output verified byte-identical to a pre-refactor baseline). Tests: 295/295 files passed; new `tests/test_bot_defaults_characterization.ts`, `tests/test_bot_defaults_parity.ts`, and a claw delegation test.
24
+
25
+ - **Feat(bot-editor)**: manage market-adapter whitelist flags from the editor — the per-bot Price/Weight/Range flags (`ama`/`dynamicWeight`/`asymmetricBounds`) previously required running the `dexbot white` script with the correct `botKey`; a wrong key silently wrote a useless entry and the script remained the only way to flip a single flag. New `6) Adapter` section in `modules/account_bots.ts` stages the three flags and commits them only after `bots.json` saved (migrating the entry on rename); `parseBooleanInput` fixes `askBoolean` parsing `'true'` as false and accepting any garbage (`y`/`yes`/`true`, `n`/`no`/`false`, Enter keeps current). `modules/market_adapter_whitelist.ts` gains `setWhitelistFlags()`/`renameWhitelistEntry()` read-modify-write helpers — an occupied key refuses the rename, a malformed file aborts instead of clobbering, legacy array-form entries are preserved, and keys are written sorted. Docs now point to `dexbot bot` -> `6) Adapter`. New `tests/test_account_bots_adapter.ts` (boolean parsing, flag writes, rename/collision, legacy and malformed-file coverage); `npm test` 293 passed / 0 failed, `verify:browser-bundle` 39/39, `tsc --noEmit` clean. Risk: writes touch the same file `dexbot white` owned; unchanged flags are never rewritten.
26
+
27
+ - **Feat(market-adapter)**: log-symmetric range-scaling tilt — range scaling previously widened the trend side by `(1+a)` but tightened the opposite side by `(1-a)`, which in log space made the tighten ~40% more aggressive than the widen at the default cap of 0.333x, collapsing the non-trend side toward center. Both bounds now scale by the same factor (down `1/(1+a)`, up `(1+a)`), so `delta log = +/-ln(1+a)` exactly and total log-width (slot count) is preserved while only the band's geometric center translates toward the trend; the widened side is unchanged and the tightened side is now the reciprocal rather than the linear complement. The geometric safe-clamp is mirrored to `base* - 1` so the shifted tightened bound still contains the fixed AMA center (still required: the reciprocal prevents reaching zero, not crossing center). `market_adapter/core/asymmetric_bounds.ts`, `modules/constants.ts`, `market_adapter/README.md`, and tests updated; `tests/test_market_adapter_service.ts` now sets `process.exitCode` on failure so its catch no longer masks a real failure. Behavioral change: with range scaling enabled the non-trend bound sits further from center (a 1.55x base at full tilt lands at +16.5% instead of +3.9%). The live grid build, adapter metrics, analyze-orders display, and the TradingView chart (embedded canonical source) all share this function.
28
+
29
+ - **Docs**: sync documentation index and linked docs with runtime behavior — `docs/README.md` bullets still described pre-1.6.4 behavior (batch cap is `gapSlots+1`, log rotation is 1.1GB total budget / 10 files, the `maxPages` warning is debug-level, duplicate detection is exact slot-price); `docs/architecture.md`'s batcher diagram/table said default cap 4 after 1f2fd7ae raised it to `gapSlots+1`; `docs/GRID_RECONCILE.md` Phase-1 duplicate detection described the removed 5x fuzzy matcher (code uses `priceSlotEqual` exact equality); `docs/FUND_MOVEMENT_AND_ACCOUNTING.md` dust partials are cancelled immediately on detection, not marked for consolidation; `docs/EVOLUTION.md` footer/stats refreshed to repo HEAD. Docs-only.
30
+
31
+ - **Feat(analysis)**: add an `ema` knob to the dynamic-weight chart for AMA input smoothing — the AMA slope channel had no post-filter besides the nz% dead-band, so there was no way to research how much additional slope smoothing (peak trimming, fewer sign flips) improves weight stability before touching live code. The dynamic-weight chart gains an `ema` slider (0-32 bars, 0 = off = previous behavior) that EMA-filters the AMA input before the slope; the clip-percentile pool and canonical `computeDynamicWeightSeries` offsets derive from it, so one source feeds display, clipping, and pipeline. Panel 2 gains a gray dashed Raw fraction-per-mille reference line and legend entry; the knob participates in `SLIDER_RANGES` clamps, copy/paste (`amaEmaSpan`), and init form-restore. `analyze_dynamic_weight` gains `--ema` (absent/NaN/out-of-range all resolve to the clamped default). `DYNAMIC_WEIGHT_RESEARCH` documents the flag row, knob table row, span value table, tuning interactions, and the measured effect on ~8.7k hourly bars (std -3% / peaks -22% / crossings 86 -> 74 at span 9). Research only; default ema = 0 reproduces prior output, the live market adapter and shared `dynamic_weight_series` module are untouched.
32
+
33
+ - **Feat(cli)**: add `dexbot dw` and centralize the chart-command pipeline — `dexbot dw` renders the dynamic-weight research chart through the same fetch pipeline as `dexbot tv`, but the pipeline lived in a tv-branded module with the exporter hardcoded. New `scripts/chart_command.ts` holds the shared pipeline (bot/pool-id/pair target resolution, `--feed`/`--pool`/`--book` routing with pool-first orderbook fallback, MPA/prediction-market guards, cached 1-month candle chunks, temp-JSON handoff) plus a `RENDERERS` registry (analyzer path, exporter label, title kind, usage blurb) so both commands (and future chart commands) are a table row apart; `scripts/tv.ts` and new `scripts/dw.ts` are thin entries with per-command error labels (loading them never triggers a run, test-asserted). Explicit `--ama-er/fast/slow-period` forwarding to the analyzer was dropped — both renderers resolve `resolveAmaConfig()` from `--bot-key` themselves, which also stops silently dropping the 4th field (`erSmoothPeriod`); verified output-identical for tv, and tv stays intentionally 3-param AMA. `analysis/analyze_dynamic_weight.ts` gains `--title` and a clickable `file://` save link; `dexbot.ts` registers `dw` in `CLI_COMMANDS`/`HELP_OWNING_COMMANDS` and the shared tv/dw dispatch (advanced-only, root help/README unchanged; documented in `scripts/README.md`, `analysis/README.md`, `analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md`). Month flags: `--month` canonical (default 3), `--months` a pure alias, invalid values always report `--month`. New `tests/test_dw_cli.ts`; `test_tv_feed_routing` now imports `scripts/chart_command`.
34
+
35
+ ### 2026-09-22
36
+
37
+ - **Feat(log)**: tag RMS structural-divergence logs with `[RMS]` and show the threshold — the RMS reset line logged raw decimal metrics via `formatPrice6` (`buy=0.162000`) with no threshold context, so operators could not tell which level fired or which side breached. `resolveRmsThresholdPct()` is extracted as the single source of truth for the `GRID_COMPARISON.RMS_PERCENTAGE` override chain (previously duplicated inline in `compareGrids`) and surfaced as `thresholdPct` through `compareGrids`/`monitorDivergence`: `modules/order/grid.ts` emits a per-side `[RMS]` debug line (metric vs threshold -> TRIGGER-RESYNC/no trigger) each tick plus a "checks disabled" variant when the threshold is 0, and `modules/dexbot_maintenance_runtime.ts` now prefixes the reset line with `[RMS]` and formats it as 4 significant digits (`buy=16.20%`) with threshold, breaching sides, and the resync reason; the failure warn is also tagged. The threshold check itself is unchanged (same override chain, same percent->decimal scaling), but log line format changes, so log scrapers keyed on the old literals must be updated (no legacy format retained). `GRID_RECALCULATION.md` signatures/examples and the `LOGGING.md` `[RMS]` prefix row updated (including the threshold=0 debug variant). Also fixes a red test left by f859941e (`tests/test_market_adapter_log_format.ts` now expects `asymCap=33%`).
38
+
39
+ - **Fix(config)**: lower the default asymmetric bounds max factor to 0.333 — `ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR` moves 0.35 -> 0.333 so the slope-derived grid bound tilt is less aggressive. At the tight end of the supported span range (1.55x) full tilt previously pinched the tightened side to ~0.75% from the AMA center; 0.333 keeps ~3.3%, leaving the narrowing-side slot guard (`minScaleSlots`) more room before it overrides the tilt. Saturated band width moves from 87.75% to 88.91% of the symmetric base; wide 2x spans are effectively unchanged. The TradingView chart fallback and README defaults are synced to `modules/constants.ts`; local overrides in `profiles/general.settings.json` still take precedence at runtime.
40
+
41
+ - **Style(ui)**: normalize bot editor separators — group related fields with `|` in the `dexbot bot` editor summary and keep within-group fields comma-separated, matching the Funding line style: Identity (Name/Account grouped, Active/DryRun comma), Price (Range/Start+Pool/GridPrice grouped with `|`), Grid (Weights group `|`, Incr/Spread comma), Funding (Sell/Buy comma, Orders/Reserve grouped with `|`). Presentational only (`modules/account_bots.ts`).
42
+
43
+ - **Feat(tradingview)**: step the AMA ER stepper by 10 per click — the ER period defaults to 781, so the +/-1 per-click stepper (and its held-repeat) made any meaningful adjustment painfully slow. The step is now 10 (floor stays 1, integer rounding); other steppers (`sma-period`, `vwap-bars`, `fast`, `slow`) are unchanged (`analysis/tradingview/tradingview_uplot_chart_generator.ts`).
44
+
5
45
  ## [1.6.4] - 2026-09-22 - Fund-Driven Spread Correction, GapSlots+1 Batch Cap, Analysis Shared Modules
6
46
 
7
47
  ### 2026-09-21
package/README.md CHANGED
@@ -173,17 +173,18 @@ Keep the default settings first, and tune these:
173
173
  on AMA. Pick a specific preset if desired: `"ama1"` is the fastest,
174
174
  `"ama4"` the slowest, and `"ama"` uses the pair's default preset.
175
175
 
176
- 5. **Generate the market-adapter whitelist:**
176
+ 5. **Set the adapter flags** — `dexbot bot` → `2) Modify bot` → pick the bot
177
+ → `6) Adapter`:
177
178
 
178
- ```bash
179
- dexbot white
180
- ```
179
+ - `Price` — AMA pricing and live adapter writes (turn **on** for AMA bots;
180
+ without it the adapter only dry-runs)
181
+ - `Weight` — dynamic buy/sell weights (opt-in)
182
+ - `Range` — AMA-slope range scaling (opt-in)
181
183
 
182
- This writes `market_adapter_whitelist.json` in the profiles directory. New AMA bots get AMA
183
- live writes only; dynamic weights and range scaling are disabled by default. Use
184
- `dexbot white --dynamic-weight` or `dexbot white --asymmetric-bounds` to opt
185
- newly generated entries into those features; existing entries are preserved.
186
- To overwrite an existing bot, include `--bot <botKey>` with the desired flags.
184
+ The flags are stored per bot in `market_adapter_whitelist.json` in the
185
+ profiles directory and can be changed any time from the same editor
186
+ section. Boolean prompts accept `y`/`yes`/`true` and `n`/`no`/`false`;
187
+ Enter keeps the current value.
187
188
 
188
189
  6. **Start DEXBot2** with `dexbot start`.
189
190
 
@@ -227,7 +228,7 @@ Global settings via `dexbot bot`, stored in `general.settings.json` in the profi
227
228
 
228
229
  <details><summary><mark>Global settings reference (click to expand)</mark></summary>
229
230
 
230
- - **Grid Health**: Grid Ratio Regeneration % (default `3%`), RMS Divergence Threshold % (default `14.3%`), AMA Delta Threshold % (default `1%`)
231
+ - **Grid Health**: Grid Funds Regeneration % (default `3%`), RMS Divergence Threshold % (default `14.3%`), AMA Δ Threshold % (default `1%`, AMA center move that triggers a grid reset), AMA-Slope Δ Threshold % (default `8%`, slope-delta trigger as a percentage of max AMA slope)
231
232
  - **Order Recovery**: Partial Dust Threshold % (default `5%`), Dust Cancel Delay (default `30s`, `-1` = off, `0` = instant)
232
233
  - **Node Configuration**: Node List (10 default public BitShares nodes), Health Check Interval (default `240 min`), Preferred Node (default `none`)
233
234
  - **Log Level**: `debug`, `info`, `warn`, `error`, `critical`. Fine-grained category control via `LOGGING_CONFIG` (see [Logging](docs/LOGGING.md))
@@ -257,8 +258,7 @@ First-run details and common mistakes are covered in the [BitShares Onboarding T
257
258
 
258
259
  ```bash
259
260
  dexbot key # Master password/keyring
260
- dexbot bot # Interactive bot configurator
261
- dexbot white # AMA whitelist; dynamic weights and range scaling off by default
261
+ dexbot bot # Interactive bot configurator (adapter Price/Weight/Range flags: 6) Adapter)
262
262
 
263
263
  dexbot reset {all|<bot>} # Regenerate grid
264
264
  dexbot disable {all|<bot>} # Disable bot in config
@@ -25,7 +25,7 @@ Tools that inspect DEXBot trading behavior and the market data it operates on. O
25
25
  | [`analyze_risk_profile.ts`](#risk-profile-analyzer-analyze_risk_profilets) | "How wide should my Safe Range clamps be?" | `node dist/analysis/analyze_risk_profile.js --bot-key <bot-key>` |
26
26
  | [`analyze_trade_heatmap.ts`](#trade-heatmap-analyze_trade_heatmapts) | "Where did trade volume cluster vs the AMA?" | `node dist/analysis/analyze_trade_heatmap.js --bot-key <bot-key>` |
27
27
  | [`tradingview/analyze_tradingview.ts`](#tradingview-chart-tradingviewanalyze_tradingviewts) | "Just give me a candle chart" | `dexbot tv <bot-key>` |
28
- | [`analyze_dynamic_weight.ts`](#dynamic-weight-research-analyze_dynamic_weightts) | "Are buy/sell weights tuned for this regime?" | `node dist/analysis/analyze_dynamic_weight.js --bot-key <bot-key>` |
28
+ | [`analyze_dynamic_weight.ts`](#dynamic-weight-research-analyze_dynamic_weightts) | "Are buy/sell weights tuned for this regime?" | `dexbot dw <bot-key>` |
29
29
  | [`analyze_volatility.ts`](#volatility-analyze_volatilityts) | "Both weights clipped too hard / not enough?" | `node dist/analysis/analyze_volatility.js --bot-key <bot-key>` |
30
30
  | [`analyze_regime.ts`](#supporting-sub-signals) | "Is the trend/chaos gate too aggressive?" | `node dist/analysis/analyze_regime.js --bot-key <bot-key>` |
31
31
  | [`analyze_kalman.ts`](#supporting-sub-signals) | "Is Kalman's contribution to the blend right?" | `node dist/analysis/analyze_kalman.js --bot-key <bot-key>` |
@@ -337,6 +337,13 @@ Two weight-tuning paths feed into the market adapter:
337
337
  Interactive 4-panel chart for the asymmetric path: AMA slope plus Kalman confirmation, gated by Hurst Exponent and Permutation Entropy. Use this when tuning buy/sell weight bias, AMA slope offset behavior, and regime damping.
338
338
 
339
339
  ```bash
340
+ # Recommended one-step (advanced): bot, pool, or pair — fetches candles and renders,
341
+ # same pipeline as `dexbot tv`, default 3 months
342
+ dexbot dw <bot-key>
343
+ dexbot dw 133 --month 6
344
+ dexbot dw TOKENA/TOKENB --month 1
345
+
346
+ # Direct analyzer (own candle files, research knobs --alpha/--gain/--dw/--lb/--clip)
340
347
  node dist/analysis/analyze_dynamic_weight.js --bot-key <bot-key>
341
348
 
342
349
  # From LP candle file with custom parameters
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ama_fitting",
3
- "version": "1.6.4",
3
+ "version": "1.6.5",
4
4
  "description": "Tools for fitting AMA parameters to market data",
5
5
  "main": "../../dist/analysis/ama_fitting/optimizer_high_resolution.js",
6
6
  "scripts": {
@@ -22,7 +22,7 @@ import { calculateAMA } from '../market_adapter/core/strategies/ama.js';
22
22
  import { computeAmaSlopeWeights, createAmaSlopeClipTracker } from '../market_adapter/core/strategies/ama_slope_model.js';
23
23
  import { MARKET_ADAPTER } from '../modules/constants.js';
24
24
  import { PATHS } from '../modules/paths.js';
25
- import { writeChartFile } from './chart_utils.js';
25
+ import { writeChartFile, toFileUrl } from './chart_utils.js';
26
26
  import { getCandleClose } from './math_utils.js';
27
27
  import { resolveSource, listAvailableBots, type SourceConfig } from './resolve_source.js';
28
28
 
@@ -56,6 +56,7 @@ function parseArgs() {
56
56
  const config: {
57
57
  source: { type: string; config: SourceConfig };
58
58
  chartFile: string;
59
+ title: string | null;
59
60
  alpha: any;
60
61
  gain: any;
61
62
  dispWeight: any;
@@ -64,9 +65,11 @@ function parseArgs() {
64
65
  listBots: boolean;
65
66
  lookbackBars?: number;
66
67
  dispScaleMinPct?: number;
68
+ amaEmaSpan?: number;
67
69
  } = {
68
70
  source: { type: 'market_adapter', config: { botKey: '' } },
69
71
  chartFile: path.join(PATHS.ANALYSIS.CHARTS_DIR, 'dynamic_weight_chart.html'),
72
+ title: null,
70
73
  alpha: MARKET_ADAPTER.DYNAMIC_WEIGHT_ALPHA,
71
74
  gain: MARKET_ADAPTER.DYNAMIC_WEIGHT_GAIN,
72
75
  dispWeight: MARKET_ADAPTER.DYNAMIC_WEIGHT_DW,
@@ -84,10 +87,12 @@ function parseArgs() {
84
87
  config.source.type = 'json';
85
88
  }
86
89
  else if (arg === '--chart') config.chartFile = args[++i];
90
+ else if (arg === '--title') config.title = args[++i] ?? null;
87
91
  else if (arg === '--alpha') config.alpha = parseFloat(args[++i]);
88
92
  else if (arg === '--gain') config.gain = parseFloat(args[++i]);
89
93
  else if (arg === '--dw') config.dispWeight = parseFloat(args[++i]);
90
94
  else if (arg === '--lb') config.lookbackBars = parseInt(args[++i], 10);
95
+ else if (arg === '--ema') config.amaEmaSpan = parseInt(args[++i], 10);
91
96
  else if (arg === '--clip') config.clipPct = parseFloat(args[++i]);
92
97
  else if (arg === '--list-bots') config.listBots = true;
93
98
  else if (arg === '--quiet') config.quiet = true;
@@ -201,6 +206,7 @@ async function main() {
201
206
  ...AMA_WEIGHT_CONFIG,
202
207
  lookbackBars: config.lookbackBars ?? AMA_WEIGHT_CONFIG.lookbackBars,
203
208
  },
209
+ amaEmaSpan: config.amaEmaSpan,
204
210
  alpha: config.alpha,
205
211
  gain: config.gain,
206
212
  dispWeight: config.dispWeight,
@@ -223,11 +229,11 @@ async function main() {
223
229
  amaNeutralZonePct: MARKET_ADAPTER.DYNAMIC_WEIGHT_AMA_NEUTRAL_ZONE_PCT,
224
230
  dispScaleMinPct: MARKET_ADAPTER.DYNAMIC_WEIGHT_DISP_SCALE_MIN_PCT,
225
231
  },
226
- }, 'Dynamic Weight Research Tool');
232
+ }, config.title || 'Dynamic Weight Research Tool');
227
233
 
228
234
  writeChartFile(config.chartFile, html);
229
235
 
230
- if (!config.quiet) console.log(`[DynamicWeight] ✓ Chart saved to ${config.chartFile}`);
236
+ if (!config.quiet) console.log(`\n[DynamicWeight] ✓ Chart saved. Open chart: (${toFileUrl(config.chartFile)})`);
231
237
  } catch (err: unknown) {
232
238
  console.error(`[DynamicWeight] Error: ${(err as any)?.message ?? err}`);
233
239
  process.exit(1);
@@ -236,6 +236,11 @@ async function main() {
236
236
  },
237
237
  smaPeriod: config.smaPeriod,
238
238
  amaDefaults: {
239
+ // Intentionally the 3-param AMA (er/fast/slow) only: the in-page
240
+ // recomputation has no erSmoothPeriod input by decision — `dexbot tv`
241
+ // stays a plain candle chart. The dw research chart and the live
242
+ // adapter are the mirrors of the full bot AMA config (incl.
243
+ // ama.erSmoothPeriod); the knob is inert by default (global 0).
239
244
  erPeriod: config.amaErPeriod ?? amaConfig.erPeriod,
240
245
  fastPeriod: config.amaFastPeriod ?? amaConfig.fastPeriod,
241
246
  slowPeriod: config.amaSlowPeriod ?? amaConfig.slowPeriod,
@@ -1469,7 +1469,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
1469
1469
  const maxSlopeOffset = Number(slopeCfg.maxSlopeOffset) > 0 ? Number(slopeCfg.maxSlopeOffset) : 0.5;
1470
1470
  const maxAsym = gridCfg && Number(gridCfg.maxAsymmetryFactor) > 0
1471
1471
  ? Number(gridCfg.maxAsymmetryFactor)
1472
- : (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.35);
1472
+ : (Number(slopeCfg.maxAsymmetryFactor) > 0 ? Number(slopeCfg.maxAsymmetryFactor) : 0.333);
1473
1473
  const inc = gridCfg && Number(gridCfg.incrementPercent) > 0 ? Number(gridCfg.incrementPercent) : null;
1474
1474
  const minSlots = Math.floor(Number(gridCfg && gridCfg.minScaleSlots) > 0 ? Number(gridCfg.minScaleSlots) : (Number(slopeCfg.minScaleSlots) || 0));
1475
1475
  // User x-range span (slider 1.2x–2.0x, default 1.55x): the only
@@ -3066,7 +3066,7 @@ function generateHTML(data: any, title: any = 'TradingView Style Research') {
3066
3066
  }
3067
3067
  makeStepper('sma-period', 1, 1, 0);
3068
3068
  makeStepper('vwap-bars', 1, 24, 0);
3069
- makeStepper('ama-er', 1, 1, 0);
3069
+ makeStepper('ama-er', 10, 1, 0);
3070
3070
  makeStepper('ama-fast', 0.1, 0.1, 1);
3071
3071
  makeStepper('ama-slow', 1, 0.1, 1);
3072
3072
  document.getElementById('ama-init-offset').addEventListener('input', () => {
@@ -63,6 +63,11 @@ On top of this blend, **Hurst Exponent** and **Permutation Entropy** act as a re
63
63
  ## Quick Start
64
64
 
65
65
  ```bash
66
+ # One-step (recommended): same fetch pipeline as `dexbot tv` — bot, pool, or pair
67
+ dexbot dw <bot-key>
68
+ dexbot dw 133 --month 6
69
+ dexbot dw TOKENA/TOKENB --month 1
70
+
66
71
  # From JSON candle file
67
72
  node dist/analysis/analyze_dynamic_weight.js \
68
73
  --file market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
@@ -77,6 +82,8 @@ node dist/analysis/analyze_dynamic_weight.js \
77
82
 
78
83
  Output: `analysis/charts/dynamic_weight_chart.html` (open in browser)
79
84
 
85
+ `dexbot dw` writes an auto-named chart instead: `analysis/charts/dw_<bot|pool_<id>|<a>_<b>>_1h_<N>m.html`.
86
+
80
87
  **Note**: Hurst requires 256 bars and PE requires 58 bars before their regime signals become active. The first portion of the chart will show the full weight without regime gating.
81
88
 
82
89
  ## CLI Flags
@@ -90,6 +97,7 @@ Output: `analysis/charts/dynamic_weight_chart.html` (open in browser)
90
97
  | `--alpha` | `0.5` | Initial α blend (0 = pure Kalman, 1 = pure AMA) |
91
98
  | `--dw` | `0.50` | Initial displacement weight (0 = pure velocity, 1 = full displacement) |
92
99
  | `--lb` | `9` | Initial lookback bars (1-32) for AMA slope calculation |
100
+ | `--ema` | `0` | Initial AMA input EMA span in bars (0 = off, 0-32) |
93
101
  | `--gain` | `1.0` | Initial gain multiplier |
94
102
  | `--clip` | `10` | Initial clip percentile |
95
103
  | `--quiet` | `false` | Suppress console output |
@@ -107,6 +115,7 @@ All panels share aligned vertical time grid lines, and the bottom output panel s
107
115
 
108
116
  ### Panel 2 — AMA Slope Input (14%)
109
117
  - **Orange line**: AMA3 slope percentage
118
+ - **Gray dashed line**: raw (unfiltered) slope — the reference when the `ema` knob > 0
110
119
  - Shows the directional strength of the slow KAMA's trend movement
111
120
  - Values outside the clip threshold are flattened before entering the offset formula
112
121
  - Background shading mirrors the output signal direction
@@ -159,6 +168,7 @@ All panels share aligned vertical time grid lines, and the bottom output panel s
159
168
  |------|-------|---------|---------|
160
169
  | **nz%** | 0–1 | 0.00 | Neutral zone: dead-band below which offset is forced to 0 |
161
170
  | **lb** | 1–32 | 9 | Logarithmic. Lookback bars for AMA slope calculation |
171
+ | **ema** | 0–32 | 0 | AMA input EMA span in bars (0 = off). Low-pass filters the AMA before the slope is taken; the gray dashed Raw‰ line shows the unfiltered slope for comparison |
162
172
  | **amaS%** | 0.06–0.12 | 0.09 | Logarithmic. Gear ratio for average per-bar AMA slope saturation |
163
173
  | **kalS%** | 0.5–1.5 | 1.0 | Logarithmic. Gear ratio for Kalman composite saturation |
164
174
  | **clip%** | 0–55 | 10 | Percentile clip: filters extreme inputs (0 = off) |
@@ -172,7 +182,7 @@ All panels share aligned vertical time grid lines, and the bottom output panel s
172
182
 
173
183
  ## Copy / Paste Parameters
174
184
 
175
- The **copy** button serializes all knob values (α, dw, kf, kfd, dsp, kdt, kfs, cf, lb, amaS%, kalS%, th%, gain, clip%, nz%, regi) to JSON and writes them to both the clipboard and `localStorage`.
185
+ The **copy** button serializes all knob values (α, dw, kf, kfd, dsp, kdt, kfs, cf, lb, ema, amaS%, kalS%, th%, gain, clip%, nz%, regi) to JSON and writes them to both the clipboard and `localStorage`.
176
186
 
177
187
  The **paste** button first checks `localStorage` for parameters from a previous copy in the same browser session. If none found, it prompts for Ctrl+V input. A confirmation popup shows the parsed values before applying them. Click **Apply** to set the knobs, **Cancel** or press **Escape** to dismiss.
178
188
 
@@ -182,7 +192,7 @@ The **paste** button first checks `localStorage` for parameters from a previous
182
192
 
183
193
  ### AMA slope
184
194
 
185
- Taking the slope of the AMA produces a signal that is already noise-filtered at the source — a near-zero slope genuinely means sideways, not oscillation that happened to average out. The neutral zone (`nz%` knob) dead-bands any remaining micro-slope residuals, which is all the additional filtering the slope channel needs.
195
+ Taking the slope of the AMA produces a signal that is already noise-filtered at the source — a near-zero slope genuinely means sideways, not oscillation that happened to average out. The neutral zone (`nz%` knob) dead-bands any remaining micro-slope residuals. When that is still too jumpy, the optional `ema` knob low-pass filters the AMA input itself before the slope is taken (0 = off keeps the raw input).
186
196
 
187
197
  ### Kalman filter
188
198
 
@@ -277,6 +287,7 @@ The lookup table can be customized per-market or per-bot (see [Custom Configurat
277
287
  ## Formulas
278
288
 
279
289
  ### AMA Offset
290
+ With `ema` > 0, `AMA_now` / `AMA_lb_bars_ago` below are read from the EMA-filtered AMA input instead of the raw AMA series (everything else identical):
280
291
  ```
281
292
  amaSlope% = ((AMA_now − AMA_lb_bars_ago) / AMA_lb_bars_ago × 100) / lb
282
293
  amaClip = clamp(amaSlope%, ±clipThreshold) // percentile-based clip
@@ -354,6 +365,27 @@ Number of bars to look back when computing AMA slope:
354
365
 
355
366
  AMA slope is normalized to an average percent per bar, not the cumulative move across the full lookback. Lower values = more noise, faster reaction. Higher values = smoother signals, more lag without adding gain just because the measurement window is longer.
356
367
 
368
+ ### ema (AMA input EMA span)
369
+ Optional low-pass EMA over the AMA values before the slope is taken (span in bars, α = 2/(span+1), `0` = off). Because differencing and an EMA are both linear filters, they commute — smoothing the AMA input produces exactly the smoothed slope series, modulo the seeding transient.
370
+
371
+ | `ema` | noise reduction* | added lag | verdict |
372
+ |---|---|---|---|
373
+ | 0 | 1× (off) | 0 bars | default, current behavior |
374
+ | 4 | ~2× | ~+1.5 bars | light |
375
+ | 9 (= lb) | ~3× | ~+4 bars | sweet spot |
376
+ | 16 | ~4× | ~+7.5 bars | heavy but usable |
377
+ | 32 | ~5.7× | ~+15.5 bars | upper-bound demo, too laggy to trade |
378
+
379
+ \* standard-deviation reduction for white-ish noise ≈ `sqrt(span)`; lag ≈ `(span-1)/2` bars (EMA group delay) on top of the slope window's `(lb-1)/2`.
380
+
381
+ Measured on ~8.7k hourly bars with the default AMA profile (`lb = 9`): the slow AMA's slope is dominated by low-frequency moves rather than white noise, so the `sqrt(span)` rule overstates the standard-deviation gain — `ema = 9` cut slope std only ~3% but trimmed the largest peaks ~22% and zero-crossings 86 → 74; `ema = 32` cut std ~11%, peaks ~48%, crossings 86 → 44, with a measured lag of ~3 bars at span 9 (theory 4). On this input the knob acts mainly as a peak-trimmer and sign-flip reducer, not as a general noise killer.
382
+
383
+ Effects while tuning:
384
+ - Zero-crossings get rarer, so the `cf` latch matters less and the `nz%` dead-band sits on a smooth curve instead of on noise.
385
+ - |slope| peaks shrink, so the same `amaS%` reaches full offset strength less often — expect to lower `amaS%` (or raise `gain`) to restore output amplitude. The `clip%` percentile pool is rebuilt from the filtered input, which partially self-compensates.
386
+ - Keep total lag in mind: ~`lb/2 + ema/2` bars before the slope reflects a move; at defaults that is ~8 bars (~8 h on the 1 h chart).
387
+ - Panel 2 plots the smoothed slope (orange) over the raw slope (gray dashed) so the noise/lag trade-off is directly visible.
388
+
357
389
  ### regi (regime sensitivity)
358
390
  - 0 = regime multiplier is always 1.0 (Hurst+PE ignored)
359
391
  - 1 = default table values used as-is
@@ -383,6 +415,7 @@ The 3×3 regime multiplier table can be customized per-market or per-bot (defaul
383
415
  Candle Data
384
416
  ├── AMA3 (slow KAMA, erPeriod=781)
385
417
  │ ├── calculateAMA() → AMA3 values per bar [price panel overlay]
418
+ │ ├── `ema` knob (0 = off) → EMA-filtered AMA input [research chart only]
386
419
  │ ├── computeAmaSlopeWeights() → slope%
387
420
  │ └── Percentile clip → amaClip → amaOff
388
421
  │
@@ -40,6 +40,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
40
40
  const AMA_MS_MIN = 0.06, AMA_MS_MAX = 0.12;
41
41
  const KAL_MS_MIN = 0.5, KAL_MS_MAX = 1.5;
42
42
  const CLIP_PCT_MAX = 20;
43
+ const EMA_MIN = 0, EMA_MAX = 32;
43
44
  const TH_MIN = 0, TH_MAX = 0.5;
44
45
  const KF_MIN = 0, KF_MAX = 200;
45
46
  const KFD_MIN = 1.0, KFD_MAX = 3.0;
@@ -53,6 +54,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
53
54
  dispWeight: [0, 1],
54
55
  neutralZonePct: [0, 1],
55
56
  lookbackBars: [LB_MIN, LB_MAX],
57
+ amaEmaSpan: [EMA_MIN, EMA_MAX],
56
58
  amaMaxSlopePct: [AMA_MS_MIN, AMA_MS_MAX],
57
59
  kalmanMaxSlopePct: [KAL_MS_MIN, KAL_MS_MAX],
58
60
  clipPct: [0, CLIP_PCT_MAX],
@@ -117,6 +119,10 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
117
119
  const defaultKalmanDispThresholdMult = data.kalmanDispThresholdMult ?? MARKET_ADAPTER.DYNAMIC_WEIGHT_KALMAN_DISP_THRESHOLD_MULT_DEFAULT;
118
120
  const defaultKalmanSmoothSpanPct = data.kalmanSmoothSpanPct ?? MARKET_ADAPTER.DYNAMIC_WEIGHT_KALMAN_SMOOTH_SPAN_PCT_DEFAULT;
119
121
  const defaultSignalConfirmBars = data.signalConfirmBars ?? MARKET_ADAPTER.DYNAMIC_WEIGHT_SIGNAL_CONFIRM_BARS_DEFAULT;
122
+ // Research-only `ema` knob: EMA span in bars over the AMA input before the
123
+ // slope is taken. 0 (default) = off, keeps the raw AMA input.
124
+ const defaultAmaEmaSpanRaw = data.amaEmaSpan ?? ma.amaEmaSpan ?? 0;
125
+ const defaultAmaEmaSpan = Math.round(Math.min(EMA_MAX, Math.max(EMA_MIN, Number.isFinite(defaultAmaEmaSpanRaw) ? defaultAmaEmaSpanRaw : EMA_MIN)));
120
126
  const regimeInitSlider = Math.round(defaultRegimeSensitivity * 100);
121
127
 
122
128
  const interval = results.length > 1 ?
@@ -235,7 +241,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
235
241
  .legend-item { gap: 3px; }
236
242
  .legend-val { font-family: monospace; display: inline-block; text-align: right; min-width: 45px; }
237
243
  #l-price, #l-ama3 { min-width: 62px; }
238
- #l-ama-slope, #l-kal-vel, #l-kal-disp { min-width: 54px; }
244
+ #l-ama-slope, #l-ama-slope-raw, #l-kal-vel, #l-kal-disp { min-width: 54px; }
239
245
  #l-signal, #l-signal-latched { min-width: 70px; text-align: left; }
240
246
  #l-combined-raw, #l-combined-echo { min-width: 42px; }
241
247
  #l-mult { min-width: 38px; }
@@ -280,6 +286,9 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
280
286
  .ctrl.lb label { min-width: 20px; }
281
287
  .ctrl.lb input[type="range"] { accent-color: #39d0d8; width: 80px; }
282
288
  .ctrl.lb .val { color: #39d0d8; }
289
+ .ctrl.ema label { min-width: 20px; }
290
+ .ctrl.ema input[type="range"] { accent-color: #f0a000; width: 80px; }
291
+ .ctrl.ema .val { color: #f0a000; }
283
292
  .ctrl.off input[type="range"] { accent-color: #3fb950; }
284
293
  .ctrl.off .val { color: #3fb950; }
285
294
  .ctrl.nz input[type="range"] { accent-color: #8b949e; }
@@ -325,6 +334,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
325
334
  <div class="section-label">AMA SLOPE INPUT</div>
326
335
  <div class="legend">
327
336
  <div class="legend-item"><div class="dot" style="background:#f0a000;"></div>Slope‰: <span id="l-ama-slope" class="legend-val" style="font-weight:bold;">-</span></div>
337
+ <div class="legend-item"><div class="dot" style="background:#8b949e;"></div>Raw‰: <span id="l-ama-slope-raw" class="legend-val">-</span></div>
328
338
  </div>
329
339
  <div id="ama-chart"></div>
330
340
  </div>
@@ -355,6 +365,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
355
365
  <div class="group-sep"></div>
356
366
  <div class="ctrl nz"><label for="nz-slider">nz%</label><input type="range" id="nz-slider" min="0" max="100" value="${Math.round(defaultNeutralZone * 100)}" title="Neutral Zone %"><span class="val" id="nz-value">${defaultNeutralZone.toFixed(2)}</span></div>
357
367
  <div class="ctrl lb"><label for="lb-slider">lb</label><input type="range" id="lb-slider" min="0" max="1000" value="${lbInitSlider}" title="Lookback Bars (${LB_MIN}-${LB_MAX})"><span class="val" id="lb-value">${lookbackBars}</span></div>
368
+ <div class="ctrl ema"><label for="ema-slider">ema</label><input type="range" id="ema-slider" min="${EMA_MIN}" max="${EMA_MAX}" value="${defaultAmaEmaSpan}" title="AMA input EMA span in bars (${EMA_MIN} = off; noise std ~ 1/sqrt(span), lag ~ (span-1)/2 bars; span ~ lb is the balance point)"><span class="val" id="ema-value">${defaultAmaEmaSpan}</span></div>
358
369
  <div class="ctrl ms-ama"><label for="ama-ms-slider">amaS%</label><input type="range" id="ama-ms-slider" min="0" max="1000" value="${amaMsInitSlider}" title="AMA Max Slope % per bar (${AMA_MS_MIN}-${AMA_MS_MAX})"><span class="val" id="ama-ms-value">${defaultAmaMaxSlopePct.toFixed(4)}</span></div>
359
370
  <div class="ctrl clip"><label for="clip-slider">clip%</label><input type="range" id="clip-slider" min="0" max="${CLIP_PCT_MAX}" value="${Math.min(defaultClipPct, CLIP_PCT_MAX)}" title="Outlier Clip %"><span class="val" id="clip-value">${Math.min(defaultClipPct, CLIP_PCT_MAX)}%</span></div>
360
371
 
@@ -388,7 +399,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
388
399
  </div>
389
400
  </div>
390
401
 
391
- <script id="payload" type="application/json">${serializeJsonForScript({ dates, prices, hurstArr, peArr, hurstSegments, peSegments, ama3Prices, amaSeedSmaLine, amaSlopePct, kalmanVelocityPctRaw, kalmanVelocityPct, kalmanDisplacementPct, kalmanIsReady, signals, alpha: defaultAlpha, gain: defaultGain, kalmanSmoothPct: defaultKalmanSmoothPct, kalmanDispScaleMult: defaultKalmanDispScaleMult, kalmanDispThresholdMult: defaultKalmanDispThresholdMult, kalmanSmoothSpanPct: defaultKalmanSmoothSpanPct, signalConfirmBars: defaultSignalConfirmBars, neutralZonePct: defaultNeutralZone, dispWeight: defaultDispWeight, amaMaxSlopePct: defaultAmaMaxSlopePct, kalmanMaxSlopePct: defaultKalmanMaxSlopePct, maxDispPct, clipPct: defaultClipPct, minOutputThreshold: defaultMinOutputThreshold, outputClamp: defaultOutputClamp, regimeSensitivity: defaultRegimeSensitivity, absoluteThreshold: defaultAbsoluteThreshold, lookbackBars, amaErPeriod, amaSlowPeriod, amaWarmupBars, amaSlopeReadyBars, realBarCount, amaPctMax, kalPctMax, amaPercentiles, kalPercentiles, amaSlopeLogMin: AMA_MS_LOG_MIN_N, amaSlopeLogMax: AMA_MS_LOG_MAX_N, kalSlopeLogMin: KAL_MS_LOG_MIN_N, kalSlopeLogMax: KAL_MS_LOG_MAX_N, lbLogMin: LB_LOG_MIN_N, lbLogMax: LB_LOG_MAX_N, gainLogMin: GAIN_LOG_MIN_N, gainLogMax: GAIN_LOG_MAX_N, sliderRanges: SLIDER_RANGES, dispScaleMinPct: defaultDispScaleMinPct, weightMin: MARKET_ADAPTER.DYNAMIC_WEIGHT_MIN_WEIGHT, weightMax: MARKET_ADAPTER.DYNAMIC_WEIGHT_MAX_WEIGHT, marketAdapter: ma, amaWeightConfig, hNodes: [0.5 + MARKET_ADAPTER.HURST_ZONE_BAND, 0.5, 0.5 - MARKET_ADAPTER.HURST_ZONE_BAND], pNodes: MARKET_ADAPTER.PE_NODES, regimeTable: MARKET_ADAPTER.REGIME_TABLE })}</script>
402
+ <script id="payload" type="application/json">${serializeJsonForScript({ dates, prices, hurstArr, peArr, hurstSegments, peSegments, ama3Prices, amaSeedSmaLine, amaSlopePct, kalmanVelocityPctRaw, kalmanVelocityPct, kalmanDisplacementPct, kalmanIsReady, signals, alpha: defaultAlpha, gain: defaultGain, kalmanSmoothPct: defaultKalmanSmoothPct, kalmanDispScaleMult: defaultKalmanDispScaleMult, kalmanDispThresholdMult: defaultKalmanDispThresholdMult, kalmanSmoothSpanPct: defaultKalmanSmoothSpanPct, signalConfirmBars: defaultSignalConfirmBars, neutralZonePct: defaultNeutralZone, dispWeight: defaultDispWeight, amaMaxSlopePct: defaultAmaMaxSlopePct, kalmanMaxSlopePct: defaultKalmanMaxSlopePct, maxDispPct, clipPct: defaultClipPct, minOutputThreshold: defaultMinOutputThreshold, outputClamp: defaultOutputClamp, regimeSensitivity: defaultRegimeSensitivity, absoluteThreshold: defaultAbsoluteThreshold, lookbackBars, amaErPeriod, amaSlowPeriod, amaWarmupBars, amaSlopeReadyBars, realBarCount, amaPctMax, kalPctMax, amaPercentiles, kalPercentiles, amaSlopeLogMin: AMA_MS_LOG_MIN_N, amaSlopeLogMax: AMA_MS_LOG_MAX_N, kalSlopeLogMin: KAL_MS_LOG_MIN_N, kalSlopeLogMax: KAL_MS_LOG_MAX_N, lbLogMin: LB_LOG_MIN_N, lbLogMax: LB_LOG_MAX_N, amaEmaSpan: defaultAmaEmaSpan, gainLogMin: GAIN_LOG_MIN_N, gainLogMax: GAIN_LOG_MAX_N, sliderRanges: SLIDER_RANGES, dispScaleMinPct: defaultDispScaleMinPct, weightMin: MARKET_ADAPTER.DYNAMIC_WEIGHT_MIN_WEIGHT, weightMax: MARKET_ADAPTER.DYNAMIC_WEIGHT_MAX_WEIGHT, marketAdapter: ma, amaWeightConfig, hNodes: [0.5 + MARKET_ADAPTER.HURST_ZONE_BAND, 0.5, 0.5 - MARKET_ADAPTER.HURST_ZONE_BAND], pNodes: MARKET_ADAPTER.PE_NODES, regimeTable: MARKET_ADAPTER.REGIME_TABLE })}</script>
392
403
 
393
404
  <script>
394
405
  const data = JSON.parse(document.getElementById('payload').textContent);
@@ -430,6 +441,8 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
430
441
  const kalMsSliderToVal = (pos) => Math.exp(KAL_MS_LOG_MIN + (pos / 1000) * (KAL_MS_LOG_MAX - KAL_MS_LOG_MIN));
431
442
 
432
443
  let currentLookbackBars = data.lookbackBars ?? ${JSON.stringify(lookbackBars)};
444
+ let currentAmaEmaSpan = Math.max(SR.amaEmaSpan[0], Math.min(SR.amaEmaSpan[1], Math.round(data.amaEmaSpan ?? 0)));
445
+ let currentAmaSource = data.ama3Prices;
433
446
  const LB_LOG_MIN = data.lbLogMin;
434
447
  const LB_LOG_MAX = data.lbLogMax;
435
448
  const lbSliderToVal = (pos) => Math.round(Math.exp(LB_LOG_MIN + (pos / 1000) * (LB_LOG_MAX - LB_LOG_MIN)));
@@ -468,6 +481,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
468
481
  const currentLatchedSignals = new Array(data.dates.length).fill(null);
469
482
  const dynamicAmaOff = new Array(data.dates.length).fill(null);
470
483
  const dynamicAmaSlopePct = new Array(data.dates.length).fill(null);
484
+ const dynamicAmaSlopePctRaw = new Array(data.dates.length).fill(null);
471
485
  const dynamicKalOff = new Array(data.dates.length).fill(null);
472
486
  const combinedOff = new Array(data.dates.length).fill(null);
473
487
  const combinedSell = new Array(data.dates.length).fill(null);
@@ -582,9 +596,40 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
582
596
  }
583
597
  }
584
598
 
585
- function computeSlopeAtIndex(idx, lb) {
599
+ /**
600
+ * Research-only ema knob — EMA low-pass over the AMA input before the
601
+ * slope is taken (EMA-then-difference == difference-then-EMA, both are
602
+ * linear filters). span bars, alpha = 2/(span+1): noise std shrinks ~
603
+ * 1/sqrt(span), lag grows ~ (span-1)/2 bars. Seeded with an SMA over the
604
+ * first 'span' finite values (mirrors the AMA seed SMA) so the transient
605
+ * is gone before any slope bar can go ready. Nulls (warmup / trailing
606
+ * padding) stay null so no fabricated values reach the clip-percentile
607
+ * pool or the offset pipeline. span <= 0 returns the input untouched.
608
+ */
609
+ function buildEmaSeries(values, span) {
610
+ if (!(span > 0) || !Array.isArray(values)) return values;
611
+ const n = values.length;
612
+ const out = new Array(n);
613
+ const alpha = 2 / (span + 1);
614
+ let seedSum = 0, seedCount = 0;
615
+ for (let i = 0; i < n && seedCount < span; i++) {
616
+ const v = values[i];
617
+ if (v == null || !Number.isFinite(v)) continue;
618
+ seedSum += v; seedCount++;
619
+ }
620
+ let prev = seedCount > 0 ? seedSum / seedCount : null;
621
+ for (let i = 0; i < n; i++) {
622
+ const v = values[i];
623
+ if (v == null || !Number.isFinite(v)) { out[i] = null; continue; }
624
+ prev = prev == null ? v : alpha * v + (1 - alpha) * prev;
625
+ out[i] = prev;
626
+ }
627
+ return out;
628
+ }
629
+
630
+ function computeSlopeAtIndex(idx, lb, values) {
586
631
  // Canonical AMA slope % (computeAverageAmaSlopePct injected above).
587
- const sp = computeAverageAmaSlopePct(data.ama3Prices[idx], data.ama3Prices[idx - lb], lb);
632
+ const sp = computeAverageAmaSlopePct(values[idx], values[idx - lb], lb);
588
633
  return sp == null ? 0 : sp;
589
634
  }
590
635
 
@@ -592,6 +637,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
592
637
  recalcKalmanVelocity();
593
638
  recalcKalmanClipThreshold();
594
639
  recalcLatchedSignals();
640
+ currentAmaSource = buildEmaSeries(data.ama3Prices, currentAmaEmaSpan);
595
641
  const lb = currentLookbackBars;
596
642
  const amaErWarmup = Math.max(0, Number.isFinite(data.amaErPeriod) ? Math.ceil(data.amaErPeriod) : ${JSON.stringify(MARKET_ADAPTER.AMAS[MARKET_ADAPTER.DEFAULT_AMA_KEY as keyof typeof MARKET_ADAPTER.AMAS].erPeriod)});
597
643
  const amaReadyBar = Math.max(lb, amaErWarmup + lb);
@@ -600,14 +646,20 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
600
646
  // implementation (computeAmaSlopeClipThreshold injected above),
601
647
  // shared with the live market adapter service. Trailing padded
602
648
  // nulls beyond realBarCount are skipped by its finiteness guards.
603
- dynamicClipThreshold = computeAmaSlopeClipThreshold(data.ama3Prices, amaErWarmup, lb, currentClipPct);
649
+ // Clip pool follows the (possibly EMA-filtered) AMA input so the
650
+ // percentile threshold matches the series the offsets derive from.
651
+ dynamicClipThreshold = computeAmaSlopeClipThreshold(currentAmaSource, amaErWarmup, lb, currentClipPct);
604
652
 
605
653
  for (let i = 0; i < data.realBarCount; i++) {
606
- // AMA raw slope % for the display panel (offsets computed in recalcWeights)
607
- dynamicAmaSlopePct[i] = i < amaReadyBar ? null : computeSlopeAtIndex(i, lb);
654
+ // AMA slope % for the display panel (offsets computed in
655
+ // recalcWeights): the raw reference line from the unfiltered AMA
656
+ // input, the plotted series from the EMA-filtered input.
657
+ dynamicAmaSlopePctRaw[i] = i < amaReadyBar ? null : computeSlopeAtIndex(i, lb, data.ama3Prices);
658
+ dynamicAmaSlopePct[i] = i < amaReadyBar ? null : computeSlopeAtIndex(i, lb, currentAmaSource);
608
659
  }
609
660
  for (let i = data.realBarCount; i < data.dates.length; i++) {
610
661
  dynamicAmaSlopePct[i] = null;
662
+ dynamicAmaSlopePctRaw[i] = null;
611
663
  }
612
664
  }
613
665
 
@@ -633,7 +685,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
633
685
  // Per-bar offset pipeline — canonical implementation (computeDynamicWeightSeries
634
686
  // injected above), shared with the live market adapter service.
635
687
  const res = computeDynamicWeightSeries({
636
- amaValues: data.ama3Prices,
688
+ amaValues: currentAmaSource,
637
689
  kalmanVelocityPct: currentKalmanVelocityPct,
638
690
  kalmanDisplacementPct: data.kalmanDisplacementPct,
639
691
  kalmanIsReady: data.kalmanIsReady,
@@ -717,7 +769,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
717
769
  max: Number.isFinite(xs.max) ? xs.max : xMax,
718
770
  } : null;
719
771
 
720
- amaChart.setData([data.dates, dynamicAmaSlopePct], false);
772
+ amaChart.setData([data.dates, dynamicAmaSlopePctRaw, dynamicAmaSlopePct], false);
721
773
  kalmanChart.setData([data.dates, currentKalmanVelocityPct, data.kalmanDisplacementPct], false);
722
774
  outputChart.setData([data.dates, combinedOff, echoCombinedOff], false);
723
775
 
@@ -752,6 +804,11 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
752
804
  if (sp == null) { spEl.textContent = '-'; spEl.style.color = '#8b949e'; }
753
805
  else { spEl.textContent = (sp >= 0 ? '+' : '') + (sp * 10).toFixed(3) + '‰'; spEl.style.color = sp > 0.01 ? '#2ea043' : sp < -0.01 ? '#f85149' : '#8b949e'; }
754
806
 
807
+ const spRaw = dynamicAmaSlopePctRaw[idx];
808
+ const spRawEl = document.getElementById('l-ama-slope-raw');
809
+ if (spRaw == null) { spRawEl.textContent = '-'; spRawEl.style.color = '#8b949e'; }
810
+ else { spRawEl.textContent = (spRaw >= 0 ? '+' : '') + (spRaw * 10).toFixed(3) + '‰'; spRawEl.style.color = '#8b949e'; }
811
+
755
812
  const vp = currentKalmanVelocityPct[idx];
756
813
  const vpEl = document.getElementById('l-kal-vel');
757
814
  if (vp == null) { vpEl.textContent = '-'; vpEl.style.color = '#8b949e'; }
@@ -975,6 +1032,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
975
1032
  document.getElementById('clip-slider').value = currentClipPct;
976
1033
  document.getElementById('nz-slider').value = Math.round(currentNz * 100);
977
1034
  document.getElementById('lb-slider').value = lbValToSlider(currentLookbackBars);
1035
+ document.getElementById('ema-slider').value = currentAmaEmaSpan;
978
1036
 
979
1037
  const priceEl = document.getElementById('price-panel');
980
1038
  const amaEl = document.getElementById('ama-panel');
@@ -1010,6 +1068,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
1010
1068
  scales: { x: { time: true }, p: { auto: true } },
1011
1069
  series: [
1012
1070
  { label: 'Time' },
1071
+ { label: 'Raw‰', stroke: '#8b949e', width: 1, dash: [3, 3], scale: 'p', points: { show: false } },
1013
1072
  { label: 'Slope‰', stroke: '#f0a000', width: 2, scale: 'p', points: { show: false } },
1014
1073
  ],
1015
1074
  axes: [
@@ -1019,7 +1078,7 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
1019
1078
  ],
1020
1079
  cursor: cursorCfg,
1021
1080
  hooks: { draw: [makePctFillHook(dynamicAmaSlopePct, 'p', 'rgba(46,160,67,0.20)', 'rgba(248,81,73,0.20)'), makeSignalBgHook('p')] }
1022
- }, [data.dates, dynamicAmaSlopePct], document.getElementById('ama-chart'));
1081
+ }, [data.dates, dynamicAmaSlopePctRaw, dynamicAmaSlopePct], document.getElementById('ama-chart'));
1023
1082
 
1024
1083
  kalmanChart = new uPlot({
1025
1084
  width: kalmanEl.offsetWidth, height: kalmanEl.offsetHeight,
@@ -1181,6 +1240,12 @@ function generateHTML(data: any, title = 'Dynamic Weight Research') {
1181
1240
  onSliderChange();
1182
1241
  });
1183
1242
 
1243
+ document.getElementById('ema-slider').addEventListener('input', (e) => {
1244
+ currentAmaEmaSpan = parseInt(e.target.value, 10);
1245
+ document.getElementById('ema-value').textContent = currentAmaEmaSpan;
1246
+ onSliderChange();
1247
+ });
1248
+
1184
1249
  document.getElementById('regime-slider').addEventListener('input', (e) => {
1185
1250
  currentRegimeSensitivity = parseInt(e.target.value, 10) / 100;
1186
1251
  document.getElementById('regime-value').textContent = currentRegimeSensitivity.toFixed(2);
@@ -1276,6 +1341,11 @@ function applyParams(p, btn) {
1276
1341
  document.getElementById('lb-slider').value = lbValToSlider(currentLookbackBars);
1277
1342
  document.getElementById('lb-value').textContent = currentLookbackBars;
1278
1343
  }
1344
+ if (p.amaEmaSpan != null) {
1345
+ currentAmaEmaSpan = Math.max(SR.amaEmaSpan[0], Math.min(SR.amaEmaSpan[1], Math.round(p.amaEmaSpan)));
1346
+ document.getElementById('ema-slider').value = currentAmaEmaSpan;
1347
+ document.getElementById('ema-value').textContent = currentAmaEmaSpan;
1348
+ }
1279
1349
  recalcInputs();
1280
1350
  recalcWeights();
1281
1351
  refreshChartsPreservingZoom();
@@ -1302,6 +1372,7 @@ function applyParams(p, btn) {
1302
1372
  regimeSensitivity: 'regime',
1303
1373
  dispWeight: 'dw',
1304
1374
  lookbackBars: 'lb',
1375
+ amaEmaSpan: 'ema',
1305
1376
  };
1306
1377
  document.getElementById('paste-confirm-vals').innerHTML = Object.entries(labels)
1307
1378
  .filter(([k]) => p[k] != null)
@@ -1355,6 +1426,7 @@ function applyParams(p, btn) {
1355
1426
  regimeSensitivity: +currentRegimeSensitivity.toFixed(2),
1356
1427
  dispWeight: +currentDw.toFixed(2),
1357
1428
  lookbackBars: currentLookbackBars,
1429
+ amaEmaSpan: currentAmaEmaSpan,
1358
1430
  };
1359
1431
  const json = JSON.stringify(params, null, 2);
1360
1432
  localStorage.setItem(LS_KEY, json);