dexbot 1.6.4 → 1.6.6

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 (240) hide show
  1. package/CHANGELOG.md +75 -3
  2. package/README.md +109 -40
  3. package/analysis/README.md +19 -17
  4. package/analysis/account_resolver.ts +1 -11
  5. package/analysis/analyze_dynamic_weight.ts +9 -3
  6. package/analysis/bot_usage/kibana_bot_queries.ts +1 -1
  7. package/analysis/chain_pool.ts +1 -1
  8. package/analysis/fills_source.ts +0 -6
  9. package/analysis/grid_correction_check.ts +20 -26
  10. package/analysis/resolve_source.ts +1 -1
  11. package/analysis/trade_profitability.ts +2 -5
  12. package/analysis/tradingview/analyze_tradingview.ts +5 -0
  13. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +2 -2
  14. package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +35 -2
  15. package/analysis/trend_detection/README.md +1 -3
  16. package/analysis/trend_detection/dynamic_weight_chart_generator.ts +82 -10
  17. package/analysis/trend_detection/package.json +1 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/claw/tests/test_dexbot_profiles.ts +18 -1
  24. package/dist/analysis/account_resolver.d.ts +1 -4
  25. package/dist/analysis/account_resolver.d.ts.map +1 -1
  26. package/dist/analysis/account_resolver.js +1 -1
  27. package/dist/analysis/account_resolver.js.map +1 -1
  28. package/dist/analysis/analyze_dynamic_weight.js +9 -3
  29. package/dist/analysis/analyze_dynamic_weight.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +1 -2
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +1 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chain_pool.d.ts +1 -3
  35. package/dist/analysis/chain_pool.d.ts.map +1 -1
  36. package/dist/analysis/chain_pool.js +1 -1
  37. package/dist/analysis/chain_pool.js.map +1 -1
  38. package/dist/analysis/fills_source.d.ts +1 -51
  39. package/dist/analysis/fills_source.d.ts.map +1 -1
  40. package/dist/analysis/fills_source.js +1 -1
  41. package/dist/analysis/fills_source.js.map +1 -1
  42. package/dist/analysis/grid_correction_check.d.ts +2 -3
  43. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  44. package/dist/analysis/grid_correction_check.js +18 -26
  45. package/dist/analysis/grid_correction_check.js.map +1 -1
  46. package/dist/analysis/resolve_source.d.ts +1 -1
  47. package/dist/analysis/resolve_source.d.ts.map +1 -1
  48. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  49. package/dist/analysis/trade_profitability.js +2 -7
  50. package/dist/analysis/trade_profitability.js.map +1 -1
  51. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  52. package/dist/analysis/tradingview/analyze_tradingview.js +5 -0
  53. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  54. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +2 -2
  55. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
  56. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +82 -10
  57. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
  58. package/dist/dexbot.d.ts.map +1 -1
  59. package/dist/dexbot.js +85 -140
  60. package/dist/dexbot.js.map +1 -1
  61. package/dist/market_adapter/core/asymmetric_bounds.d.ts +2 -2
  62. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  63. package/dist/market_adapter/core/asymmetric_bounds.js +15 -12
  64. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  65. package/dist/market_adapter/core/kibana_candles.d.ts +1 -15
  66. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  67. package/dist/market_adapter/core/kibana_candles.js +1 -1
  68. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  69. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +1 -35
  70. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  71. package/dist/market_adapter/inputs/fetch_lp_data.js +10 -54
  72. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  73. package/dist/market_adapter/inputs/kibana_feed_source.d.ts +1 -8
  74. package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -1
  75. package/dist/market_adapter/inputs/kibana_feed_source.js +1 -1
  76. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  77. package/dist/market_adapter/inputs/window_cache.d.ts +2 -63
  78. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  79. package/dist/market_adapter/inputs/window_cache.js +22 -102
  80. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  81. package/dist/market_adapter/interval_utils.d.ts +1 -1
  82. package/dist/market_adapter/interval_utils.js +1 -1
  83. package/dist/modules/account_bots.d.ts +70 -6
  84. package/dist/modules/account_bots.d.ts.map +1 -1
  85. package/dist/modules/account_bots.js +353 -149
  86. package/dist/modules/account_bots.js.map +1 -1
  87. package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
  88. package/dist/modules/bitshares-native/crypto/ecc.browser.js +0 -31
  89. package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
  90. package/dist/modules/bot_defaults.d.ts +98 -0
  91. package/dist/modules/bot_defaults.d.ts.map +1 -0
  92. package/dist/modules/bot_defaults.js +169 -0
  93. package/dist/modules/bot_defaults.js.map +1 -0
  94. package/dist/modules/bot_settings.d.ts.map +1 -1
  95. package/dist/modules/bot_settings.js +4 -1
  96. package/dist/modules/bot_settings.js.map +1 -1
  97. package/dist/modules/config.d.ts +1 -0
  98. package/dist/modules/config.d.ts.map +1 -1
  99. package/dist/modules/config.js +1 -0
  100. package/dist/modules/config.js.map +1 -1
  101. package/dist/modules/constants.d.ts +11 -2
  102. package/dist/modules/constants.d.ts.map +1 -1
  103. package/dist/modules/constants.js +40 -27
  104. package/dist/modules/constants.js.map +1 -1
  105. package/dist/modules/credit_runtime.d.ts +0 -2
  106. package/dist/modules/credit_runtime.d.ts.map +1 -1
  107. package/dist/modules/credit_runtime.js +1 -10
  108. package/dist/modules/credit_runtime.js.map +1 -1
  109. package/dist/modules/dexbot_class.d.ts +13 -10
  110. package/dist/modules/dexbot_class.d.ts.map +1 -1
  111. package/dist/modules/dexbot_class.js +0 -9
  112. package/dist/modules/dexbot_class.js.map +1 -1
  113. package/dist/modules/dexbot_cow_runtime.d.ts +13 -0
  114. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  115. package/dist/modules/dexbot_cow_runtime.js +13 -1
  116. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  117. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
  118. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_maintenance_runtime.js +9 -4
  120. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  121. package/dist/modules/launcher/adapter_requirement.js +1 -1
  122. package/dist/modules/launcher/launch_modes.d.ts +17 -1
  123. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  124. package/dist/modules/launcher/launch_modes.js +18 -1
  125. package/dist/modules/launcher/launch_modes.js.map +1 -1
  126. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  127. package/dist/modules/launcher/monolithic_runtime.js +8 -1
  128. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  129. package/dist/modules/market_adapter_whitelist.d.ts +40 -1
  130. package/dist/modules/market_adapter_whitelist.d.ts.map +1 -1
  131. package/dist/modules/market_adapter_whitelist.js +161 -11
  132. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  133. package/dist/modules/order/accounting.d.ts +4 -18
  134. package/dist/modules/order/accounting.d.ts.map +1 -1
  135. package/dist/modules/order/accounting.js +4 -34
  136. package/dist/modules/order/accounting.js.map +1 -1
  137. package/dist/modules/order/export.d.ts +9 -12
  138. package/dist/modules/order/export.d.ts.map +1 -1
  139. package/dist/modules/order/export.js +9 -12
  140. package/dist/modules/order/export.js.map +1 -1
  141. package/dist/modules/order/grid.d.ts +44 -40
  142. package/dist/modules/order/grid.d.ts.map +1 -1
  143. package/dist/modules/order/grid.js +66 -48
  144. package/dist/modules/order/grid.js.map +1 -1
  145. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  146. package/dist/modules/order/grid_reconcile.js +111 -20
  147. package/dist/modules/order/grid_reconcile.js.map +1 -1
  148. package/dist/modules/order/grid_reconcile_internal.d.ts +5 -3
  149. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  150. package/dist/modules/order/grid_reconcile_internal.js +18 -3
  151. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  152. package/dist/modules/order/manager.d.ts +0 -8
  153. package/dist/modules/order/manager.d.ts.map +1 -1
  154. package/dist/modules/order/manager.js +3 -12
  155. package/dist/modules/order/manager.js.map +1 -1
  156. package/dist/modules/order/utils/order.d.ts +22 -7
  157. package/dist/modules/order/utils/order.d.ts.map +1 -1
  158. package/dist/modules/order/utils/order.js +103 -21
  159. package/dist/modules/order/utils/order.js.map +1 -1
  160. package/dist/modules/order/utils/system.d.ts +5 -1
  161. package/dist/modules/order/utils/system.d.ts.map +1 -1
  162. package/dist/modules/order/utils/system.js +5 -2
  163. package/dist/modules/order/utils/system.js.map +1 -1
  164. package/dist/modules/order/utils/withPoolRef.d.ts +12 -0
  165. package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
  166. package/dist/modules/order/utils/withPoolRef.js +17 -0
  167. package/dist/modules/order/utils/withPoolRef.js.map +1 -1
  168. package/dist/modules/order/working_grid.d.ts +8 -13
  169. package/dist/modules/order/working_grid.d.ts.map +1 -1
  170. package/dist/modules/order/working_grid.js +8 -29
  171. package/dist/modules/order/working_grid.js.map +1 -1
  172. package/dist/modules/settings_merge.d.ts +10 -1
  173. package/dist/modules/settings_merge.d.ts.map +1 -1
  174. package/dist/modules/settings_merge.js +27 -17
  175. package/dist/modules/settings_merge.js.map +1 -1
  176. package/dist/modules/validate_profiles.d.ts.map +1 -1
  177. package/dist/modules/validate_profiles.js +4 -3
  178. package/dist/modules/validate_profiles.js.map +1 -1
  179. package/dist/scripts/chart_command.d.ts +48 -0
  180. package/dist/scripts/chart_command.d.ts.map +1 -0
  181. package/dist/scripts/chart_command.js +504 -0
  182. package/dist/scripts/chart_command.js.map +1 -0
  183. package/dist/scripts/dw.d.ts +3 -0
  184. package/dist/scripts/dw.d.ts.map +1 -0
  185. package/dist/scripts/dw.js +30 -0
  186. package/dist/scripts/dw.js.map +1 -0
  187. package/dist/scripts/sync-version.js +0 -22
  188. package/dist/scripts/sync-version.js.map +1 -1
  189. package/dist/scripts/tv.d.ts +1 -31
  190. package/dist/scripts/tv.d.ts.map +1 -1
  191. package/dist/scripts/tv.js +10 -463
  192. package/dist/scripts/tv.js.map +1 -1
  193. package/dist/scripts/update.js +99 -62
  194. package/dist/scripts/update.js.map +1 -1
  195. package/dist/scripts/update_dist_freshness.d.ts +58 -0
  196. package/dist/scripts/update_dist_freshness.d.ts.map +1 -0
  197. package/dist/scripts/update_dist_freshness.js +144 -0
  198. package/dist/scripts/update_dist_freshness.js.map +1 -0
  199. package/dist/unlock.js +1 -1
  200. package/dist/unlock.js.map +1 -1
  201. package/docs/BITSHARES_ONBOARDING.md +16 -10
  202. package/docs/COW_INVARIANTS.md +1 -0
  203. package/docs/DEXBOT_COMPARISON.md +11 -11
  204. package/docs/EVOLUTION.md +9 -7
  205. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -4
  206. package/docs/GRID_PRICE_INVARIANT.md +518 -0
  207. package/docs/GRID_RECALCULATION.md +20 -24
  208. package/docs/GRID_RECONCILE.md +62 -41
  209. package/docs/LOGGING.md +1 -0
  210. package/docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md +556 -0
  211. package/docs/README.md +18 -18
  212. package/docs/WORKFLOW.md +13 -8
  213. package/docs/architecture.md +42 -40
  214. package/docs/developer_guide.md +5 -8
  215. package/market_adapter/README.md +45 -65
  216. package/modules/README.md +3 -2
  217. package/package.json +7 -4
  218. package/scripts/README.md +18 -34
  219. package/scripts/reset-settings.sh +1 -1
  220. package/analysis/ama_fitting/package.json +0 -12
  221. package/analysis/analyze_derivatives.ts +0 -320
  222. package/analysis/derivative_chart_generator.ts +0 -874
  223. package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +0 -403
  224. package/analysis/trend_detection/derivative_analyzer.ts +0 -1059
  225. package/dist/analysis/analyze_derivatives.d.ts +0 -68
  226. package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
  227. package/dist/analysis/analyze_derivatives.js +0 -287
  228. package/dist/analysis/analyze_derivatives.js.map +0 -1
  229. package/dist/analysis/derivative_chart_generator.d.ts +0 -12
  230. package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
  231. package/dist/analysis/derivative_chart_generator.js +0 -891
  232. package/dist/analysis/derivative_chart_generator.js.map +0 -1
  233. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -248
  234. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
  235. package/dist/analysis/trend_detection/derivative_analyzer.js +0 -910
  236. package/dist/analysis/trend_detection/derivative_analyzer.js.map +0 -1
  237. package/dist/scripts/generate_market_adapter_whitelist.d.ts +0 -15
  238. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +0 -1
  239. package/dist/scripts/generate_market_adapter_whitelist.js +0 -191
  240. package/dist/scripts/generate_market_adapter_whitelist.js.map +0 -1
@@ -0,0 +1,518 @@
1
+ # The Grid-Price Invariant
2
+
3
+ Status: **implemented; the emission check is BLOCKING**
4
+ Last code-reviewed against the v1.6.6 release baseline `224f92ee` (2026-09-24).
5
+
6
+ ## The invariant
7
+
8
+ > **`order.price` for a slot-`idx` order must equal `priceForSlot(idx, genesis)`.**
9
+
10
+ The genesis `priceLevels` table is the only authoritative source of a slot's
11
+ price. A slot id encodes an index; the genesis table turns that index into a
12
+ price. Nothing else is a legitimate price for a slot.
13
+
14
+ This is enforced at grid build/load by `assertSlotPriceInvariant(slot, genesis)`
15
+ (`modules/order/utils/math.ts`), called from `modules/order/grid.ts`.
16
+
17
+ **Historical failure:** that assertion was *never called on the broadcast path*.
18
+ Placement args were built from the mutable `slot.price` field, so any code that
19
+ wrote `slot.price` — or constructed an order object with its own `price` — could
20
+ broadcast a non-grid price, with no comparison against the genesis table on the
21
+ way out.
22
+
23
+ ### Not to be confused with two related but weaker checks
24
+
25
+ | Check | Question it answers | Sufficient? |
26
+ |---|---|---|
27
+ | `isChainPriceOutOfGrid` | Is the price inside `[levels[0], levels[last]]`? | **No** — bounds, not membership |
28
+ | `checkPlacementPriceSanity` (removed) | Is the price within 5% of a market reference? | **No** — different reference, flat threshold |
29
+ | `assertSlotPriceInvariant` | Is the price *this slot's* level? | **Yes** — true by construction for a valid order |
30
+
31
+ The distinction matters because the failure band is wide. With a typical
32
+ geometry (`minPrice 1.65x`, `maxPrice 10x` around mid), the configured span is
33
+ roughly `[0.196, 3.234]`, while the active window is 20 buy + 20 sell slots —
34
+ about `[0.293, 0.357]` at 0.5% increment. **Between the window edge and the
35
+ configured bound there is a band where a non-grid price is simultaneously "in
36
+ range" (passes every range guard) and "not a valid slot" (violates the
37
+ invariant), with no check in between.** Range guards cannot see it; only a
38
+ grid-membership check can.
39
+
40
+ ## Why this guard exists
41
+
42
+ `isChainPriceOutOfGrid` answers "is the price inside the configured range?", not
43
+ "is it *this slot's* level?" — so a non-grid price can sit inside the range and
44
+ pass every range guard (see the band explanation above). Historically the engine
45
+ held two sources of truth for a slot's price: the genesis ladder and the mutable
46
+ `slot.price` field written from chain data. The mutable field repeatedly won —
47
+ orphan adoption stored the chain price, the COW `CREATE` path re-broadcast
48
+ `liveSlot.price` in place of the planned price, and the fill-guard pivot was
49
+ seeded from unanchored fill prices — after which the off-grid price re-entered
50
+ the engine as "evidence" and ratcheted.
51
+
52
+ The guard closes that class by construction: a legitimate order equals its
53
+ genesis level, so only a genuinely non-grid price can be rejected. The full
54
+ incident trace — the individual writers, the observed ratchet, and the commit
55
+ provenance — is preserved in
56
+ [ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md](ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md) (Appendix A).
57
+
58
+ ## Enforcement: the six emission sites
59
+
60
+ Every site that builds a create/update op now runs the invariant check via
61
+ `checkGridPriceInvariant` / `reportGridPriceInvariant`
62
+ (`modules/order/utils/order.ts`):
63
+
64
+ | Site | Location |
65
+ |---|---|
66
+ | `CREATE` | `dexbot_cow_runtime.ts` |
67
+ | `UPDATE` (rotation) | `dexbot_cow_runtime.ts` |
68
+ | `CREATE-FALLBACK` | `dexbot_cow_runtime.ts` |
69
+ | `RECONCILE-CREATE` | `grid_reconcile_internal.ts` |
70
+ | `RECONCILE-UPDATE` | `grid_reconcile_internal.ts` |
71
+ | `STARTUP-CREATE` | `grid_reconcile_internal.ts` |
72
+
73
+ **The check fails open on anything unjudgeable** (full fail-open policy in
74
+ ["This BLOCKS"](#this-blocks) below), so it can only fire on a genuine mismatch
75
+ and cannot false-positive on a legitimately wide grid. That property is why it
76
+ is safe where the earlier tuned guards were not.
77
+
78
+ **Not every fix needed an emission site.** The `sync_engine.ts` materialize
79
+ path (a CREATE landing after master lost the slot) materializes a *slot*
80
+ rather than emitting an op, so the emission check never sees it — it needed
81
+ its own fix, deriving both price and order type from the ladder
82
+ (`priceForSlot(parseSlotIndex(gridOrderId), genesis)`) and keeping the carried
83
+ descriptor price only when there is no genesis ladder to derive from
84
+ (migration), warning when it does. `virtualizeOrder`,
85
+ `convertToSpreadPlaceholder` and `toRailHolePlaceholder` were audited and
86
+ verified identity-preserving, including under the checker.
87
+
88
+ The correct pattern already existed in the codebase:
89
+ `isUnknownFillOrderAdoptable` (`modules/dexbot_fill_runtime.ts`) derives the
90
+ index, checks `isChainPriceOutOfGrid`, checks `isSlotInRail`, and requires the
91
+ slot be free. **The fill path was the one place that faithfully used the genesis
92
+ table; the broadcast and adoption paths did not.** That is the pattern the rest
93
+ of the engine should follow, and it is the model for the adoption fix below.
94
+
95
+ For the rotation `UPDATE`, the check is against **`action.newGridId`** (the
96
+ destination slot): a rotation re-prices to the destination's level, so that is
97
+ the slot whose genesis price the emitted price must match. Every planner
98
+ producer derives `newPrice` from the object `newGridId` names, so the pairing is
99
+ consistent by construction.
100
+
101
+ **The emitted rotation price is derived, not merely checked.** The planner sets
102
+ `newPrice = hole.order.price`, a *mutable* slot field, so the emission would
103
+ otherwise be only as sound as whatever last wrote that object. The UPDATE path
104
+ therefore derives the price with `deriveRotationPrice` —
105
+ `priceForSlot(parseSlotIndex(action.newGridId), genesis)` — and uses the planned
106
+ price only when there is no ladder to derive from (migration), warning on any
107
+ disagreement. The invariant check stays as the backstop for that no-genesis
108
+ case. Note this also means the last-fill guard and the gap-evacuation probe
109
+ judge the price that will actually be emitted.
110
+
111
+ The check is self-validating in the strong sense: `checkGridPriceInvariant`
112
+ re-derives the expected level from the slot *id* and compares it against the
113
+ passed price, so it never validates a price against itself. A producer that
114
+ paired a source price with a destination id is caught (13.9% drift in the
115
+ fixture), as is a destination hole carrying an already-corrupted price (28%).
116
+
117
+ ### The seventh gate: final pre-broadcast pivot re-check
118
+
119
+ The per-action LAST-FILL-GUARD checks run against the pivot frozen at batch
120
+ start. A fill queued between that freeze and the broadcast passes every
121
+ per-action check on a stale pivot and ships (live incident: freeze at 0.745,
122
+ sell fill queued at 0.765, batch broadcast at 0.910 — the violating rotation
123
+ filled 6s later, 0.6% below the true threshold). `runFinalPivotGate`
124
+ (`modules/dexbot_cow_runtime.ts`) closes that window: after the op-building
125
+ loop and immediately before broadcast it re-refreshes the pivot (peek-only,
126
+ never drains the fill queue) and, if it changed, re-runs the guard against
127
+ every BUILT op:
128
+
129
+ - an unchanged pivot is a pure no-op; violators drop into the existing
130
+ skipped-slot restore paths (dropped rotations restore from master, dropped
131
+ creates count toward the boundary-hold intersect), so the summary reports
132
+ them as skipped, not passed;
133
+ - cancel and size-update ops are never gated; bypass parity with the build
134
+ loop (spread-correction CREATEs, stamped gap-evacuation UPDATEs);
135
+ - it fails open on anything unjudgeable — unresolvable price/type, cold pivot,
136
+ a refresh throw — the same policy as the emission guard;
137
+ - lockstep compaction remaps the stored pending-broadcast indexes (old→new),
138
+ so a dropped CREATE cannot leave the uncertain-broadcast reconcile adopting
139
+ a matched chain order into the wrong slot.
140
+
141
+ Tested by FG-1..11 (`tests/test_final_pivot_gate.ts`), including the incident
142
+ replay (FG-2) and the index-remap hygiene (FG-7/FG-11).
143
+
144
+ ### This BLOCKS
145
+
146
+ `recordGridPriceInvariantCheck` / `reportGridPriceInvariant` return whether the
147
+ caller may emit. On a genuine off-grid price the emission is **skipped** (the
148
+ slot is marked skipped so the next reconcile cycle re-plans), the per-site
149
+ warning names the slot, expected level and drift, and the batch summary reports
150
+ the aggregate.
151
+
152
+ The guard **fails open on anything it cannot judge** — no genesis (pre-genesis
153
+ startup), unparseable or synthetic slot ids, out-of-ladder indices, non-finite
154
+ prices, and any checker exception all permit the emission. A guard that blocked
155
+ on missing metadata would halt legitimate trading, which is worse than the bug
156
+ it prevents. Only `off-grid-price` blocks.
157
+
158
+ That fail-open property is what makes blocking safe where the earlier tuned
159
+ guards were not: a legitimate order always equals its genesis level by
160
+ construction, so a rejection can only mean the price was written from something
161
+ other than the ladder.
162
+
163
+ ## Adoption: accept the chain price, don't adopt it
164
+
165
+ Adoption must accept a chain price that differs from the genesis level. That is
166
+ **not** a reason to write it into `slot.price`. Two different prices are
167
+ conflated here:
168
+
169
+ | Price | Meaning | Must equal a genesis level? |
170
+ |---|---|---|
171
+ | `order.price` | resting limit price on chain | **Yes** |
172
+ | `fill.price` | execution price of a fill | **No** — may be better |
173
+
174
+ A fill can execute better than its resting price, and a partial fill leaves the
175
+ order resting at its original price. So `fill.price != order.price` is normal
176
+ and is not corruption.
177
+
178
+ The real adoption case is **grid regeneration**: the grid is recentered but old
179
+ chain orders keep resting at the *old* grid's levels. With `incrementPercent
180
+ 0.5%`, a resting order at `0.32667340` sits a fraction of a slot from the new
181
+ grid's `slot--17` at `0.32685210` — a `0.0547%` difference. Correct handling:
182
+ adopt into `slot--17` (via `slotIndexForPrice`) and **let the slot keep
183
+ `priceForSlot(-17)`**. The old price is chain metadata about where the order *was
184
+ placed*; it is not the slot's price, and it disappears on the next
185
+ rotation/cancel.
186
+
187
+ This is what the genesis path (`adoptChainOrderIntoSlot`) already did — it sets
188
+ `orderId`, `state`, `size`, `rawOnChain` and **never touches `price`**. The
189
+ legacy fallback was the only path that got it wrong; it now matches.
190
+
191
+ **Both paths now state this explicitly, and warn when it is broken.** Keeping
192
+ the slot's price by *not assigning it* is correct but invisible: nothing would
193
+ catch a later edit that reintroduced `price: chainOrder.price`, and nothing
194
+ would catch a slot whose price was **already** corrupted before it reached
195
+ adoption — it would simply be re-adopted. `adoptedSlotKeepsItsOwnPrice` runs on
196
+ both paths and warns, naming the path, slot, expected level and drift, when an
197
+ adopted slot's price is not its genesis level.
198
+
199
+ That check is deliberately **non-blocking**: the chain order is real and must be
200
+ tracked, so refusing the adoption would strand it untracked on the book —
201
+ strictly worse than the corrupt slot it reports. It is a signal, not a gate.
202
+
203
+ Note the two adoption paths are covered separately, because a fixture with a
204
+ genesis takes the genesis path and a fixture without one takes the legacy
205
+ fallback: LEGACY-ADOPT-001 covers the legacy path, ADOPT-NAME-001 the genesis
206
+ path. Each is mutation-tested against its own path.
207
+
208
+ ## Healing a pre-existing off-grid slot price
209
+
210
+ The emission guard rejects an off-grid price, so a slot that *persists* one is
211
+ rejected on every cycle — a durable stall. `loadGrid` therefore **repairs** any
212
+ slot whose price disagrees with its genesis level, in both validation modes
213
+ (`modules/order/grid.ts`, at the genesis validation loop):
214
+
215
+ - the slot's price is set to `priceForSlot(idx, genesis)`;
216
+ - enforce mode still virtualizes it (`state: VIRTUAL`, `size: 0`, `orderId: ''`);
217
+ - both modes log the correction, naming the slot and the old → new price.
218
+
219
+ This is safe because **the slot id determines its price from the ladder**: no
220
+ legitimate slot can disagree with its own level, so there is no correct value
221
+ being overwritten. Before this, the mismatched price was preserved in *both*
222
+ modes, which meant a corrupt price survived a restart, re-activated into a
223
+ CREATE, and was rejected forever — the guard would have converted a corruption
224
+ bug into a permanent one.
225
+
226
+ Verified: a slot corrupted to 5% off its level is repaired in log and enforce
227
+ modes, and the repaired price passes the emission guard (otherwise the stall is
228
+ not actually healed). Mutation-checked: reverting the price assignment fails the
229
+ assertion.
230
+
231
+ ### After a repair: expect one churn cycle
232
+
233
+ A repair changes `slot.price`, so any *resting* order still sitting at the old
234
+ corrupt price no longer matches its slot. It resolves on the next reconcile via
235
+ price-drift auto-cancel, which means **one cycle of cancel/replace churn after a
236
+ repair is expected, not a new fault**. Operators seeing a burst of cancels
237
+ immediately following a `[GENESIS] Slot … price repaired from genesis` line are
238
+ watching the repair settle.
239
+
240
+ ### Repair only runs at load — so a persistent rejection escalates
241
+
242
+ `loadGrid` heals on reload, but an in-process corruption has no reload to wait
243
+ for. The recurring planner carries the slot's price straight from
244
+ `manager.orders`, so a corrupted slot is re-planned, rejected, and warned on
245
+ every cycle, forever. Nothing heals it short of a restart: the slot is dead
246
+ while the bot looks healthy, and the repeated warns train operators to ignore
247
+ them.
248
+
249
+ The guard therefore **counts consecutive rejecting batches per slot** and, after
250
+ `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` (3) of them, fires
251
+ `requestStructuralGridResync('grid-price-invariant-violation', {slotId, expected,
252
+ actual, site, streak})`. The resync is the same debounced, batch-in-flight-aware,
253
+ two-step (reload → full reset) path already used by the grid-bloat and
254
+ spread-stale detectors — a second repair mechanism would duplicate tested
255
+ machinery for no benefit. A dedicated
256
+ `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` (15 min) bounds repeats, and a **clean
257
+ check clears the streak** so escalation means "rejected N *consecutive* batches",
258
+ not "rejected N times ever".
259
+
260
+ **Why not heal in place** (`slot.price = expected` at rejection time): the
261
+ checker does compute the right value, but silently rewriting it destroys the
262
+ diagnostic signal. The streak is what distinguishes the four corruption sources
263
+ — legacy persisted state, migration fallback, genesis-identity mismatch, or an
264
+ unknown live writer. Auto-heal makes all four look identical. Count first,
265
+ escalate on persistence; consider an in-place fast path only after a soak shows
266
+ in-process corruption is the common case.
267
+
268
+ The streak is **bot-scoped**, not module-scoped: the monolithic runtime
269
+ (`dexbot.ts`) runs every active bot in one process, so a shared count would pool
270
+ unrelated bots' rejections — one bot's two rejections would put the next bot at
271
+ the threshold on its first rejection and fire a spurious structural resync on a
272
+ healthy bot (verified by GPI-WIRE-007/008).
273
+
274
+ ## Out-of-bounds policy: hold, and surface
275
+
276
+ **Decision: hold indefinitely, and warn.** Holding costs opportunity; wrong
277
+ placements cost capital. The grid geometry is not invalidated by the market
278
+ leaving it — every observed violation was on the *placement* side, not the
279
+ resting grid.
280
+
281
+ | Event | Action |
282
+ |---|---|
283
+ | Market moves beyond configured bounds | Keep the grid as-is. Do not recenter, rescale, or extend the boundary. |
284
+ | Chain order exists outside the grid range | Hold untouched (`out-of-grid-deferred`). Do not adopt, do not cancel. |
285
+ | Fill occurs while out of bounds | Apply its accounting; do **not** let its price become grid evidence. |
286
+ | A slot *inside* the grid empties | Refill at `priceForSlot(idx)` — **this continues even out of bounds.** |
287
+ | An order would need a non-grid price to "keep up" | Do not place it. |
288
+
289
+ The last two rows are the point: refilling an emptied slot at its genesis price
290
+ is always valid (the slot's price is still a grid price). What must stop is
291
+ emitting orders whose price is not a grid price.
292
+
293
+ Holds are classified non-blocking by `isNonBlockingUnmatchedOrder` (any
294
+ `*-deferred` reason), so they do not force a grid reset while they persist.
295
+
296
+ ### Hold observability
297
+
298
+ The `[HOLD]` line names side, price, size, reason, and distance from the nearest
299
+ grid bound (as a percentage, so a near-miss is distinguishable from a
300
+ deliberately-placed far order). An unchanged set is re-warned slowly
301
+ (`TIMING.STALE_TOTALS_WARN_RATE_LIMIT_MS`), because "held indefinitely" must not
302
+ be indistinguishable from "bot silently stuck".
303
+
304
+ The gate is content-based, not count-based: same-count churn (one hold clearing
305
+ as another appears) previously looked like "no change" and was never logged.
306
+
307
+ The signature is also the **logging gate**, but it is NOT the staleness clock.
308
+ Age is measured **per stranded order** (`id@price/size:reason`) from the first
309
+ cycle that order was seen stranded, tracked in a bot-scoped map. Two earlier
310
+ choices were wrong, and each was caught by a test that now pins it:
311
+
312
+ 1. `manager._lastUnmatchedChainOrdersAt` records "when we last looked", never
313
+ "when the hold started" — an age gate on it could never fire (HOLD-007).
314
+ 2. The whole-held-set signature clock looked correct but was **reset by
315
+ unrelated churn**, because the signature includes every entry's reason. An
316
+ unrelated hold flapping in and out changed the signature every cycle and
317
+ restarted the clock, so a genuinely stranded order was starved of escalation
318
+ forever (HOLD-010). A per-order clock is immune: age belongs to the order,
319
+ not to the set.
320
+
321
+ A third defect sat in the same path: the signature-change branch used to
322
+ `return` before reaching the escalation call, so any churn skipped escalation
323
+ entirely. Escalation is now invoked on BOTH branches — a signature change is a
324
+ reason to re-log, not a reason to stop evaluating age.
325
+
326
+ When a stranded hold reaches `DEFERRED_HOLD_ESCALATE_MS` (24h) it escalates at
327
+ `error` and fires the same `requestStructuralGridResync('deferred-hold-stale',
328
+ ...)` — the exit that a per-cycle hold-and-warn loop otherwise lacks.
329
+
330
+ Only genuinely **stranded** reasons are escalation triggers. Escalation uses a
331
+ narrow allow-list (`isStrandedHoldOrder`: `out-of-rail-deferred`,
332
+ `out-of-grid-deferred`) rather than the broad `-deferred` non-blocking filter.
333
+ A resync cannot end a broadcast region or re-evaluate an uncommitted boundary,
334
+ so `broadcast-active-deferred`, `boundary-hold-trailing-market`,
335
+ `boundary-unknown-deferred` and `held-plan-unchanged-deferred` are excluded:
336
+ escalating on them would spend a full grid reload on something the owning
337
+ machinery already resolves. The allow-list fails closed, so a future transient
338
+ reason is excluded by default instead of silently becoming a resync trigger
339
+ (HOLD-011).
340
+
341
+ That escalation is safe because the full reset's reconcile is **update-first**:
342
+ unmatched chain orders are price-updated onto rail slots (emitting the rail's
343
+ genesis level, so the RECONCILE-UPDATE guard does not block the resolution) and
344
+ only true surplus is cancelled. Funds are released by price-updating, not by
345
+ inventing a new cancellation policy. A dedicated
346
+ `DEFERRED_HOLD_RESYNC_COOLDOWN_MS` (6h) bounds repeats.
347
+
348
+ ### Known limitation: no market-aware "left the bounds" signal
349
+
350
+ The hold is per-order and reactive: the bot cannot distinguish "one stray
351
+ order outside the range" from "the market left the range entirely", so it
352
+ cannot warn *before* orders become stranded. The earlier anchor/divergence
353
+ constants (`ANCHOR.DIVERGENCE_INFO`, `DIVERGENCE_WARN`) were removed with the
354
+ anchor itself; only `calculateGridSideDivergenceMetric` survives, used for
355
+ side-divergence metrics in `grid.ts`. Until a market-aware signal exists, the
356
+ 24h escalation fires on a timer rather than on cause — whether the market left
357
+ the bounds or one order is simply stranded, because neither case is
358
+ distinguishable from the hold record alone. The blast radius is bounded by
359
+ the stranded-reasons allow-list above. The designated home for a real signal
360
+ is divergence telemetry built on `calculateGridSideDivergenceMetric`.
361
+
362
+ ## Key constants (`modules/constants.ts`, `TIMING`)
363
+
364
+ | Constant | Value | Meaning |
365
+ |---|---|---|
366
+ | `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` | 3 | Consecutive rejecting batches per slot before a structural resync |
367
+ | `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` | 15 min | Bounds repeat resyncs for the same corruption |
368
+ | `DEFERRED_HOLD_ESCALATE_MS` | 24 h | Age at which a stranded deferred hold escalates |
369
+ | `DEFERRED_HOLD_RESYNC_COOLDOWN_MS` | 6 h | Bounds repeat resyncs for stale holds |
370
+ | `STALE_TOTALS_WARN_RATE_LIMIT_MS` | 60 s | Slow re-warn interval for an unchanged hold set |
371
+
372
+ ## Implementation status
373
+
374
+ | Item | State |
375
+ |---|---|
376
+ | Legacy adoption keeps `slot.price`; gains the rail guard | **landed** |
377
+ | Materialize path derives price from genesis, not the descriptor | **landed** |
378
+ | Materialize path derives ORDER TYPE from the same ladder level, not the descriptor | **landed** |
379
+ | Persistent off-grid rejection escalates to a structural resync (per-slot streak) | **landed** |
380
+ | Stale deferred hold escalates to a structural resync (per-order stranded clock) | **landed** |
381
+ | Pre-broadcast substitution removed (CREATE) | **landed** |
382
+ | Pre-broadcast substitution removed (CREATE-fallback) | **landed** |
383
+ | Invariant check at 6 emission sites | **landed — BLOCKING (rejects off-grid emissions)** |
384
+ | Fill-guard pivot validated onto the ladder (`resolveOnGridPivot`) | **landed** |
385
+ | `[HOLD]` enrichment + slow re-warn | **landed** |
386
+ | Final pre-broadcast pivot gate re-checks BUILT ops on a refreshed pivot | **landed** |
387
+
388
+ `resolveOnGridPivot` snaps a near-ladder pivot to its slot level but **refuses to
389
+ rewrite a far-off-ladder one** onto an edge slot — silently clamping would dress
390
+ a corrupt pivot up as a legitimate edge fill. Off-ladder pivots are counted and
391
+ reported.
392
+
393
+ ## Verification
394
+
395
+ - **Unit (GPI-001..015, `tests/test_grid_price_invariant_guard.ts`):** for
396
+ every emitted op, `price === priceForSlot(idx, genesis)`.
397
+ Rotation UPDATEs additionally assert `newPrice` matches the **destination**
398
+ slot's level (GPI-010); an off-grid emission is **refused**, a genesis level is
399
+ permitted (GPI-011); unjudgeable inputs fail open (GPI-012); the emitted
400
+ rotation price is derived from the destination's genesis level (GPI-013), and
401
+ the derivation declines — rather than inventing a price — with no ladder
402
+ (GPI-014) or an out-of-ladder index (GPI-015).
403
+ - **Unit:** a legacy-path out-of-grid order is held, not adopted, and keeps its
404
+ genesis price (LEGACY-ADOPT-001/002); the materialize path derives the slot
405
+ price AND order type from genesis and warns when it cannot
406
+ (MATERIALIZE-001/002/003); adoption
407
+ keeps the slot's own level on the genesis path and does not warn when correct
408
+ (ADOPT-NAME-001) (`tests/test_sync_out_of_grid_defer.ts`).
409
+ - **Unit:** pre-broadcast drift is reported at `warn` and the op is built from
410
+ the *planned* price (`tests/test_cow_orchestration_fixes.ts`).
411
+ - **Unit:** pivot snapping and off-ladder refusal
412
+ (`tests/test_last_fill_guard.ts`, PIVOT-001..003); the final pre-broadcast
413
+ pivot gate re-checks BUILT ops on a refreshed pivot, drops violators into the
414
+ skipped-slot restore paths, remaps pending indexes on compaction, and never
415
+ gates cancels or size-updates (`tests/test_final_pivot_gate.ts`, FG-1..11,
416
+ including the 2026-09-13 stale-pivot incident replay).
417
+ - **External gate:** `analysis/grid_correction_check.ts` — target 0 sustained
418
+ violations at 168h/720h. **The baseline is NOT clean:** 4 of 5 bots were
419
+ non-zero over 7 days, so this is a live signal, not a historical one.
420
+ - **Live status:** the blocking check has seen live traffic — 75 judgeable
421
+ checks (`site=COW`, `violated=0`, `unchecked=0`) across four live bot logs
422
+ on 2026-09-14. No live **rejection** or **escalation** has been observed
423
+ yet, so the resync thresholds (`GRID_PRICE_INVARIANT_RESYNC_THRESHOLD`,
424
+ `DEFERRED_HOLD_ESCALATE_MS`) remain validated only by mutation tests. The
425
+ first `violated>0` in production should be read as a real writer, not a
426
+ false positive: a legitimate order equals its genesis level by
427
+ construction. Treat the first escalation as a genuine signal about how
428
+ long an in-process corruption actually survives.
429
+
430
+ Each behavioural fix above is mutation-tested (revert the fix, confirm the test
431
+ fails) so the tests are known to discriminate rather than merely pass.
432
+
433
+ **Emission-site wiring.** The guard's own rules (`checkGridPriceInvariant`,
434
+ `reportGridPriceInvariant`) are unit-tested by GPI-001..015, but that says
435
+ nothing about whether a live batch *consults* them — a mutation audit found the
436
+ three COW sites could be neutralised entirely with every COW test still green.
437
+ `tests/test_grid_price_invariant_wiring.ts` (GPI-WIRE-001..009) closes that: it
438
+ drives `updateOrdersOnChainBatchCOW` end-to-end with a real DEXBot/OrderManager
439
+ and asserts that no op reaches `buildCreateOrderOp`/`buildUpdateOrderOp` and
440
+ nothing is broadcast. Mutation-verified: disabling the blocking CREATE check
441
+ fails GPI-WIRE-001; reverting the rotation price derivation fails GPI-WIRE-004;
442
+ feeding the guard the raw pivot instead of the validated one fails GPI-WIRE-005.
443
+ The escalation is pinned by GPI-WIRE-006..009: removing the escalation call,
444
+ removing the streak reset on a clean check, and disabling the cooldown are each
445
+ caught, and GPI-WIRE-009 pins that the per-slot streak is bot-scoped — the
446
+ monolithic runtime runs every active bot in ONE process, so a module-level
447
+ streak would let one bot's rejections push another to the threshold on its first
448
+ rejection, firing a spurious resync on a healthy bot. The hold escape hatch is
449
+ pinned by HOLD-006..011 (`tests/test_hold_and_center_guards.ts`). Three
450
+ mutation-verified discriminators: HOLD-007 fails if age comes from
451
+ `_lastUnmatchedChainOrdersAt` (the clock that could never fire); HOLD-010 fails
452
+ if age comes from the whole-held-set signature clock (reset by unrelated churn,
453
+ starving a stranded order forever); HOLD-011 fails if the narrow stranded
454
+ allow-list is replaced by the broad `-deferred` filter (transient holds would
455
+ spend a grid reload).
456
+
457
+ **One site is not black-box reachable, by design.** The UPDATE check
458
+ (`action.newGridId`) is a backstop: `deriveRotationPrice` computes the emitted
459
+ price *from* the destination's genesis level before the check runs, so at the
460
+ check the id and price agree by construction. Disabling that check cannot be
461
+ caught from outside because there is no reachable input that makes it fire. The
462
+ reachable property — that a planner-supplied `action.newPrice` disagreeing with
463
+ the destination's level never reaches the chain — is what GPI-WIRE-004 pins.
464
+ Treat the UPDATE check as defence for a missing genesis ladder (it fails open
465
+ there), not as the only thing standing between a bad plan and a live order.
466
+
467
+ **False-positive audit (blocking enabled).** Because the check now rejects, it
468
+ was audited against real grid builds before enabling: **1,153 slots across five
469
+ geometries** — standard, wide (1.65x-10x), tight, wide at 2% increment, and the
470
+ incident geometry (1.65x-10x around ~0.32) — produced **zero** off-grid
471
+ rejections and zero uncheckable slots. The identity-preserving transforms
472
+ (`virtualizeOrder`, `convertToSpreadPlaceholder`, `toRailHolePlaceholder`) also
473
+ pass, while an order object carrying its own drifted price is caught (50% drift
474
+ in the fixture). That is the evidence that blocking is safe: a legitimate order
475
+ equals its genesis level by construction, so only a genuinely mis-priced
476
+ emission can be rejected.
477
+
478
+ ## The removed placement gate: do NOT naively re-land
479
+
480
+ `d808c052` added `checkPlacementPriceSanity` (reject a planned price >5% from a
481
+ traded-range mid). It was removed the same day by `e2898e51` because it "blocked
482
+ legitimate order creates and updates".
483
+
484
+ **That framing is misleading, and the code shows the real mechanism.** The
485
+ removal was not an over-tuned threshold — it was a **design deadlock**:
486
+
487
+ - `ANCHOR.PRICE_OUTLIER_FACTOR: 2` bounded the fills considered plausible.
488
+ - Out-of-bounds fill prices were then **skipped entirely** by
489
+ `resolveFillPrice` / the burst fill loop.
490
+ - But those same fills were what boundary correction needed in order to *update*
491
+ the anchor.
492
+ - So after a genuine trend beyond 2x, the correction bound could never be
493
+ re-derived: the guard bounded the very evidence required to correct it.
494
+
495
+ Hence "too restrictive" describes a design flaw, not a tuning problem. Framing
496
+ it as tuning invited the wrong fix — deletion instead of repairing the
497
+ unbounded-anchor path. A re-land must address the deadlock, not retune a number.
498
+
499
+ The violator that ran free once the detector was gone was the **role-assignment
500
+ pass inside `calculateTargetGrid`** (`modules/order/strategy.ts`), which re-typed
501
+ slots relative to *fill prices* rather than the boundary — with an **unbounded**
502
+ BUY→SELL direction (`slot.price > minFilledBuyPrice` has no upper limit). With
503
+ `minFilledBuyPrice` inflated by a poisoned fill, every BUY below it flipped to
504
+ SELL and was re-created above it: the observed ratchet.
505
+
506
+ Implication: restoring `checkPlacementPriceSanity` alone would **not** fix that.
507
+ Any re-land must fix the out-of-bounds-skip deadlock *and* clamp the rotation
508
+ independently of the anchor. See `git show d808c052` for the removed
509
+ implementation; the removal provenance (symbol → built → removed) is recorded in
510
+ `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md`.
511
+
512
+ ## Caveats
513
+
514
+ - Figures (`2.45x`, `~180 slots`, `[0.196, 3.234]`) are derived from typical
515
+ configured multipliers and a representative reference price. Exact levels
516
+ should be read from the persisted grid to confirm.
517
+ - The removed-path behaviour was read from `d808c052`/`e2898e51`, not executed.
518
+ - No real account names, bot names, or live market pairs are included.
@@ -260,27 +260,10 @@ need a reset to move to the new asymmetric range and offset placement price.
260
260
  ### Configuration
261
261
 
262
262
  Range scaling is enabled by the whitelist's `asymmetricBounds: true` flag.
263
- Generate or update the AMA whitelist with:
264
-
265
- ```bash
266
- dexbot white
267
- ```
268
-
269
- This writes `profiles/market_adapter_whitelist.json`. The default generation
270
- enables AMA live writes for new AMA bots, while leaving dynamic weights and
271
- range scaling disabled.
272
-
273
- To opt new AMA entries into range scaling:
274
-
275
- ```bash
276
- dexbot white --asymmetric-bounds
277
- ```
278
-
279
- To overwrite one existing bot (otherwise preserved):
280
-
281
- ```bash
282
- dexbot white --asymmetric-bounds --bot <botKey>
283
- ```
263
+ Set it per bot in the editor: `dexbot bot` → `2) Modify bot` → pick the bot →
264
+ `6) Adapter` → **Range** = yes. The flag is stored in
265
+ `profiles/market_adapter_whitelist.json` next to **Price** (AMA pricing) and
266
+ **Weight** (dynamic weights); all three default to off until enabled.
284
267
 
285
268
  The snapshot fields involved are:
286
269
 
@@ -290,6 +273,14 @@ The snapshot fields involved are:
290
273
  - `amaSlopeDeltaPercent`: distance from the accepted baseline
291
274
  - `amaSlopeThresholdPercent`: threshold required to trigger the reset
292
275
 
276
+ **Trigger threshold.** By default the reset fires when the slope delta crosses
277
+ `AMA_SLOPE_DELTA_THRESHOLD_PERCENT`, a percentage of the AMA max slope
278
+ (`(value/100) × maxSlopePct`) — default `8%`, stored under `MARKET_ADAPTER` in
279
+ `profiles/general.settings.json` and editable via `dexbot bot` → `1) Grid
280
+ Drift` (`AMA-Slope Δ`). An explicit `amaSlopeDeltaThresholdPercent` in
281
+ `profiles/market_adapter_settings.json` bypasses the factor and is used
282
+ directly as an average percent-per-bar threshold.
283
+
293
284
  AMA slope values are stored and compared as average percent per bar. Older
294
285
  settings that used cumulative percent over the full lookback can either be
295
286
  divided by `amaSlope.lookbackBars`, or marked with
@@ -344,7 +335,7 @@ code paths and log differently:
344
335
 
345
336
  | Path | Fires from | Trigger | Log signature |
346
337
  |------|-----------|---------|---------------|
347
- | **Periodic divergence** | `dexbot_maintenance_runtime.ts` periodic sync loop | `Grid.monitorDivergence()` reports `buy.rms` or `sell.rms` above threshold | `Grid update triggered by structural divergence during periodic: buy=..., sell=...` |
338
+ | **Periodic divergence** | `dexbot_maintenance_runtime.ts` periodic sync loop | `Grid.monitorDivergence()` reports `buy.rms` or `sell.rms` above threshold | `[RMS] Grid update triggered by structural divergence during periodic: buy=...% sell=...% (threshold=...%) sides=... → TRIGGER-RESYNC (rms_structural_grid_resync)` |
348
339
  | **Structural recovery (COW guard)** | `dexbot_class.ts` `_wireStructuralGridResyncRequest()` | Order manager detects unmatched chain orders during copy-on-write placement | `[RECOVERY] Running structural full grid resync for <reason> (N unmatched chain order(s))` |
349
340
 
350
341
  The structural-recovery path is debounced through `_structuralGridResyncTimer`
@@ -566,11 +557,16 @@ market_adapter_ama_slope_delta_threshold
566
557
 
567
558
  **RMS Divergence Trigger:**
568
559
  ```
569
- Grid update triggered by structural divergence during periodic: buy=..., sell=...
560
+ [RMS] Grid update triggered by structural divergence during periodic: buy=16.20% sell=2.10% (threshold=14.3%) sides=buy → TRIGGER-RESYNC (rms_structural_grid_resync)
570
561
  Grid regeneration triggered. Performing full grid resync...
571
562
  Recorded grid reset metadata for dynamic grid state.
572
563
  ```
573
564
 
565
+ At `debug` level each tick also emits the per-side check detail (mirroring the `[DIVERGENCE]` ratio-check line):
566
+ ```
567
+ [RMS] BUY check: metric=...% (threshold=14.3%) → TRIGGER-RESYNC/no trigger | SELL check: metric=...% (threshold=14.3%) → TRIGGER-RESYNC/no trigger
568
+ ```
569
+
574
570
  **Available-Funds Resize Trigger:**
575
571
  ```
576
572
  Grid update triggered by funds during periodic (buy: ..., sell: ...)
@@ -621,6 +617,6 @@ Removed trigger file.
621
617
  - `modules/dexbot_class.ts` — `_performGridResync()`, `requestGridReset()`, and COW-guard structural recovery wiring
622
618
  - `modules/order/grid.ts` — RMS divergence check and grid comparison
623
619
  - `modules/order/manager.ts` — Available-funds resize threshold logic
624
- - `modules/market_adapter_whitelist.ts` / `scripts/generate_market_adapter_whitelist.ts` — Whitelist generation backing `dexbot white`
620
+ - `modules/market_adapter_whitelist.ts` — Whitelist storage/read helpers (the bot editor's `6) Adapter` reads and writes the same file)
625
621
  - `profiles/general.settings.json` — User-editable configuration
626
622
  - `profiles/bots.json` — Per-bot configuration including AMA