dexbot 1.6.5 → 1.6.7

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 (228) hide show
  1. package/CHANGELOG.md +76 -4
  2. package/README.md +108 -36
  3. package/analysis/README.md +11 -16
  4. package/analysis/account_resolver.ts +1 -11
  5. package/analysis/bot_usage/kibana_bot_queries.ts +1 -1
  6. package/analysis/chain_pool.ts +1 -1
  7. package/analysis/fills_source.ts +0 -6
  8. package/analysis/grid_correction_check.ts +20 -26
  9. package/analysis/resolve_source.ts +1 -1
  10. package/analysis/trade_profitability.ts +2 -5
  11. package/analysis/trend_detection/README.md +1 -3
  12. package/analysis/trend_detection/package.json +1 -1
  13. package/claw/package.json +1 -1
  14. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  15. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  16. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  17. package/dist/analysis/account_resolver.d.ts +1 -4
  18. package/dist/analysis/account_resolver.d.ts.map +1 -1
  19. package/dist/analysis/account_resolver.js +1 -1
  20. package/dist/analysis/account_resolver.js.map +1 -1
  21. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +1 -2
  22. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  23. package/dist/analysis/bot_usage/kibana_bot_queries.js +1 -1
  24. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  25. package/dist/analysis/chain_pool.d.ts +1 -3
  26. package/dist/analysis/chain_pool.d.ts.map +1 -1
  27. package/dist/analysis/chain_pool.js +1 -1
  28. package/dist/analysis/chain_pool.js.map +1 -1
  29. package/dist/analysis/fills_source.d.ts +1 -51
  30. package/dist/analysis/fills_source.d.ts.map +1 -1
  31. package/dist/analysis/fills_source.js +1 -1
  32. package/dist/analysis/fills_source.js.map +1 -1
  33. package/dist/analysis/grid_correction_check.d.ts +2 -3
  34. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  35. package/dist/analysis/grid_correction_check.js +18 -26
  36. package/dist/analysis/grid_correction_check.js.map +1 -1
  37. package/dist/analysis/resolve_source.d.ts +1 -1
  38. package/dist/analysis/resolve_source.d.ts.map +1 -1
  39. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  40. package/dist/analysis/trade_profitability.js +2 -7
  41. package/dist/analysis/trade_profitability.js.map +1 -1
  42. package/dist/dexbot.d.ts.map +1 -1
  43. package/dist/dexbot.js +82 -83
  44. package/dist/dexbot.js.map +1 -1
  45. package/dist/market_adapter/core/kibana_candles.d.ts +1 -15
  46. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  47. package/dist/market_adapter/core/kibana_candles.js +1 -1
  48. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  49. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +1 -35
  50. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  51. package/dist/market_adapter/inputs/fetch_lp_data.js +10 -54
  52. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  53. package/dist/market_adapter/inputs/kibana_feed_source.d.ts +1 -8
  54. package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -1
  55. package/dist/market_adapter/inputs/kibana_feed_source.js +1 -1
  56. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  57. package/dist/market_adapter/inputs/window_cache.d.ts +2 -63
  58. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  59. package/dist/market_adapter/inputs/window_cache.js +22 -102
  60. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  61. package/dist/modules/account_bots.d.ts +16 -2
  62. package/dist/modules/account_bots.d.ts.map +1 -1
  63. package/dist/modules/account_bots.js +108 -44
  64. package/dist/modules/account_bots.js.map +1 -1
  65. package/dist/modules/account_orders.d.ts +20 -2
  66. package/dist/modules/account_orders.d.ts.map +1 -1
  67. package/dist/modules/account_orders.js +63 -3
  68. package/dist/modules/account_orders.js.map +1 -1
  69. package/dist/modules/bitshares-native/chain_client.d.ts +10 -0
  70. package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
  71. package/dist/modules/bitshares-native/chain_client.js +197 -28
  72. package/dist/modules/bitshares-native/chain_client.js.map +1 -1
  73. package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
  74. package/dist/modules/bitshares-native/crypto/ecc.browser.js +0 -31
  75. package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
  76. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  77. package/dist/modules/bitshares-native/subscriptions.js +312 -10
  78. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  79. package/dist/modules/bitshares-native/transport.d.ts +1 -0
  80. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  81. package/dist/modules/bitshares-native/transport.js +97 -3
  82. package/dist/modules/bitshares-native/transport.js.map +1 -1
  83. package/dist/modules/bitshares_client.d.ts +20 -0
  84. package/dist/modules/bitshares_client.d.ts.map +1 -1
  85. package/dist/modules/bitshares_client.js +29 -0
  86. package/dist/modules/bitshares_client.js.map +1 -1
  87. package/dist/modules/chain_keys.d.ts +37 -22
  88. package/dist/modules/chain_keys.d.ts.map +1 -1
  89. package/dist/modules/chain_keys.js +145 -44
  90. package/dist/modules/chain_keys.js.map +1 -1
  91. package/dist/modules/cli_start_onboarding.d.ts +10 -0
  92. package/dist/modules/cli_start_onboarding.d.ts.map +1 -0
  93. package/dist/modules/cli_start_onboarding.js +15 -0
  94. package/dist/modules/cli_start_onboarding.js.map +1 -0
  95. package/dist/modules/cli_start_output.d.ts +6 -0
  96. package/dist/modules/cli_start_output.d.ts.map +1 -0
  97. package/dist/modules/cli_start_output.js +8 -0
  98. package/dist/modules/cli_start_output.js.map +1 -0
  99. package/dist/modules/constants.d.ts +13 -0
  100. package/dist/modules/constants.d.ts.map +1 -1
  101. package/dist/modules/constants.js +103 -1
  102. package/dist/modules/constants.js.map +1 -1
  103. package/dist/modules/credit_runtime.d.ts +0 -2
  104. package/dist/modules/credit_runtime.d.ts.map +1 -1
  105. package/dist/modules/credit_runtime.js +1 -10
  106. package/dist/modules/credit_runtime.js.map +1 -1
  107. package/dist/modules/dexbot_class.d.ts +13 -10
  108. package/dist/modules/dexbot_class.d.ts.map +1 -1
  109. package/dist/modules/dexbot_class.js +0 -9
  110. package/dist/modules/dexbot_class.js.map +1 -1
  111. package/dist/modules/dexbot_cow_runtime.d.ts +13 -0
  112. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  113. package/dist/modules/dexbot_cow_runtime.js +62 -10
  114. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  115. package/dist/modules/dexbot_fill_runtime.d.ts +6 -1
  116. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  117. package/dist/modules/dexbot_fill_runtime.js +22 -3
  118. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  119. package/dist/modules/dexbot_maintenance_runtime.d.ts +24 -1
  120. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  121. package/dist/modules/dexbot_maintenance_runtime.js +45 -1
  122. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  123. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  124. package/dist/modules/dexbot_state_recovery.js +13 -1
  125. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  126. package/dist/modules/order/accounting.d.ts +4 -18
  127. package/dist/modules/order/accounting.d.ts.map +1 -1
  128. package/dist/modules/order/accounting.js +4 -34
  129. package/dist/modules/order/accounting.js.map +1 -1
  130. package/dist/modules/order/export.d.ts +9 -12
  131. package/dist/modules/order/export.d.ts.map +1 -1
  132. package/dist/modules/order/export.js +9 -12
  133. package/dist/modules/order/export.js.map +1 -1
  134. package/dist/modules/order/grid.d.ts +35 -40
  135. package/dist/modules/order/grid.d.ts.map +1 -1
  136. package/dist/modules/order/grid.js +76 -44
  137. package/dist/modules/order/grid.js.map +1 -1
  138. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  139. package/dist/modules/order/grid_reconcile.js +111 -20
  140. package/dist/modules/order/grid_reconcile.js.map +1 -1
  141. package/dist/modules/order/grid_reconcile_internal.d.ts +5 -3
  142. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  143. package/dist/modules/order/grid_reconcile_internal.js +29 -3
  144. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  145. package/dist/modules/order/logger.d.ts +12 -2
  146. package/dist/modules/order/logger.d.ts.map +1 -1
  147. package/dist/modules/order/logger.js +29 -7
  148. package/dist/modules/order/logger.js.map +1 -1
  149. package/dist/modules/order/manager.d.ts +49 -8
  150. package/dist/modules/order/manager.d.ts.map +1 -1
  151. package/dist/modules/order/manager.js +72 -34
  152. package/dist/modules/order/manager.js.map +1 -1
  153. package/dist/modules/order/utils/order.d.ts +22 -7
  154. package/dist/modules/order/utils/order.d.ts.map +1 -1
  155. package/dist/modules/order/utils/order.js +200 -33
  156. package/dist/modules/order/utils/order.js.map +1 -1
  157. package/dist/modules/order/utils/system.d.ts +97 -0
  158. package/dist/modules/order/utils/system.d.ts.map +1 -1
  159. package/dist/modules/order/utils/system.js +274 -2
  160. package/dist/modules/order/utils/system.js.map +1 -1
  161. package/dist/modules/order/utils/withPoolRef.d.ts +12 -0
  162. package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
  163. package/dist/modules/order/utils/withPoolRef.js +17 -0
  164. package/dist/modules/order/utils/withPoolRef.js.map +1 -1
  165. package/dist/modules/order/working_grid.d.ts +8 -13
  166. package/dist/modules/order/working_grid.d.ts.map +1 -1
  167. package/dist/modules/order/working_grid.js +8 -29
  168. package/dist/modules/order/working_grid.js.map +1 -1
  169. package/dist/modules/paths.d.ts +7 -1
  170. package/dist/modules/paths.d.ts.map +1 -1
  171. package/dist/modules/paths.js +10 -3
  172. package/dist/modules/paths.js.map +1 -1
  173. package/dist/modules/utils/text_width.d.ts +19 -0
  174. package/dist/modules/utils/text_width.d.ts.map +1 -0
  175. package/dist/modules/utils/text_width.js +97 -0
  176. package/dist/modules/utils/text_width.js.map +1 -0
  177. package/dist/pm2.d.ts.map +1 -1
  178. package/dist/pm2.js +95 -18
  179. package/dist/pm2.js.map +1 -1
  180. package/dist/scripts/sync-version.js +0 -22
  181. package/dist/scripts/sync-version.js.map +1 -1
  182. package/dist/scripts/update.js +62 -13
  183. package/dist/scripts/update.js.map +1 -1
  184. package/dist/scripts/update_dist_freshness.d.ts +12 -0
  185. package/dist/scripts/update_dist_freshness.d.ts.map +1 -1
  186. package/dist/scripts/update_dist_freshness.js +30 -1
  187. package/dist/scripts/update_dist_freshness.js.map +1 -1
  188. package/dist/unlock.d.ts +14 -2
  189. package/dist/unlock.d.ts.map +1 -1
  190. package/dist/unlock.js +73 -4
  191. package/dist/unlock.js.map +1 -1
  192. package/docs/BITSHARES_ONBOARDING.md +9 -6
  193. package/docs/COW_INVARIANTS.md +9 -0
  194. package/docs/CREDENTIAL_SECURITY.md +26 -0
  195. package/docs/DEXBOT_COMPARISON.md +11 -11
  196. package/docs/EVOLUTION.md +9 -7
  197. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  198. package/docs/GRID_PRICE_INVARIANT.md +573 -0
  199. package/docs/GRID_RECALCULATION.md +1 -1
  200. package/docs/GRID_RECONCILE.md +60 -39
  201. package/docs/LOGGING.md +17 -2
  202. package/docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md +555 -0
  203. package/docs/README.md +3 -3
  204. package/docs/WORKFLOW.md +12 -6
  205. package/docs/architecture.md +40 -38
  206. package/docs/developer_guide.md +4 -8
  207. package/market_adapter/README.md +16 -12
  208. package/modules/README.md +1 -1
  209. package/package.json +8 -4
  210. package/scripts/README.md +0 -9
  211. package/scripts/reset-settings.sh +1 -1
  212. package/analysis/ama_fitting/package.json +0 -12
  213. package/analysis/analyze_derivatives.ts +0 -320
  214. package/analysis/derivative_chart_generator.ts +0 -874
  215. package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +0 -403
  216. package/analysis/trend_detection/derivative_analyzer.ts +0 -1059
  217. package/dist/analysis/analyze_derivatives.d.ts +0 -68
  218. package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
  219. package/dist/analysis/analyze_derivatives.js +0 -287
  220. package/dist/analysis/analyze_derivatives.js.map +0 -1
  221. package/dist/analysis/derivative_chart_generator.d.ts +0 -12
  222. package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
  223. package/dist/analysis/derivative_chart_generator.js +0 -891
  224. package/dist/analysis/derivative_chart_generator.js.map +0 -1
  225. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -248
  226. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
  227. package/dist/analysis/trend_detection/derivative_analyzer.js +0 -910
  228. package/dist/analysis/trend_detection/derivative_analyzer.js.map +0 -1
@@ -0,0 +1,573 @@
1
+ # The Grid-Price Invariant
2
+
3
+ Status: **implemented; the emission check is BLOCKING**
4
+ Last code-reviewed against the v1.6.7 release baseline `d4e0ce0b` (2026-09-26).
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
+ ### The pivot rides the grid snapshot
145
+
146
+ The guard reads `_lastFilledPrice`/`_lastFilledType`, so the pivot must be live
147
+ before the first decision of a run — including while the boundary is still
148
+ being rebuilt. Without a persisted row the guard starts every process cold, and
149
+ the startup book seed (`seedLastFilledPricesFromBook`, max resting buy / min
150
+ resting sell) is only a proxy: it goes wrong whenever the book is not adjacent
151
+ to the last fill (partials, rotations, reserve shelf, dust).
152
+
153
+ `persistGridSnapshot` therefore writes a `{price, type, fillsAt, genesisHash}`
154
+ row alongside the grid (`storeMasterGrid`'s `lastFillPivot` parameter, written
155
+ under the bot's persistence lock), and `loadGrid` restores it via
156
+ `restoreLastFillPivot` right after `_restoreBoundary` — with the grid re-typed
157
+ and genesis applied, before the first reconcile/broadcast — so startup,
158
+ price-match resume, and the recovery reload all inherit one call site.
159
+
160
+ Rules that keep the mirror honest:
161
+
162
+ - **The manager's in-memory pivot stays authoritative at runtime.** The row is a
163
+ mirror that rides the grid snapshot, so it invalidates in lockstep with the
164
+ boundary/genesis instead of forming a second ledger (the `_pendingFillCrawls`
165
+ lockstep contract).
166
+ - **Provenance gates persistence.** One writer, `setLastFillPivot(type, price,
167
+ 'fill' | 'book')`; only `'fill'` is persist-eligible, so a book seed never
168
+ fossilizes as market truth. One shared shape gate (`normalizeLastFillPivot`)
169
+ serves the snapshot sanitizer and the loader, so the two checks cannot drift.
170
+ - **Restore validates before it re-arms**, in order: TTL (24h,
171
+ `GRID_LIMITS.LAST_FILL_PIVOT_TTL_MS`, with the ORIGINAL `fillsAt` preserved
172
+ through `setLastFillPivot`'s `atMs` so it keeps meaning "age of the last
173
+ fill", not "time since last restart") → genesis binding (a row from a dead
174
+ generation is refused) → on-grid (the runtime's own `resolveOnGridPivot`
175
+ ladder validator, reused from `utils/system.ts`, so the restorer can never
176
+ accept a value the live guard would refuse per probe). TTL and
177
+ genesis-mismatch verdicts erase the row through one shared drop path
178
+ (`AccountOrders.clearPersistedLastFillPivot`); an off-ladder row is a no-op
179
+ that falls back to the book seed. The snapped ladder level is restored, never
180
+ the raw float.
181
+ - **The generation wipes the pivot.** `initializeGrid` and
182
+ `rejectCorruptedGridSnapshot` call `resetLastFillPivot`, which clears the
183
+ full scalar family including the per-side mirrors (so the book seed's cold
184
+ gate is not silently suppressed by a stale mirror), and `AccountOrders.clearGrid`
185
+ drops the persisted row with the snapshot.
186
+
187
+ Tested by LFP-1..8 (`tests/test_last_fill_pivot_persistence.ts`) for the
188
+ store/load round-trip shape gates, null-clears vs undefined no-ops, the
189
+ provenance gate, `fillsAt` preservation, TTL and genesis-mismatch erasure, and
190
+ off-ladder refusal.
191
+
192
+ ### This BLOCKS
193
+
194
+ `recordGridPriceInvariantCheck` / `reportGridPriceInvariant` return whether the
195
+ caller may emit. On a genuine off-grid price the emission is **skipped** (the
196
+ slot is marked skipped so the next reconcile cycle re-plans), the per-site
197
+ warning names the slot, expected level and drift, and the batch summary reports
198
+ the aggregate.
199
+
200
+ The guard **fails open on anything it cannot judge** — no genesis (pre-genesis
201
+ startup), unparseable or synthetic slot ids, out-of-ladder indices, non-finite
202
+ prices, and any checker exception all permit the emission. A guard that blocked
203
+ on missing metadata would halt legitimate trading, which is worse than the bug
204
+ it prevents. Only `off-grid-price` blocks.
205
+
206
+ That fail-open property is what makes blocking safe where the earlier tuned
207
+ guards were not: a legitimate order always equals its genesis level by
208
+ construction, so a rejection can only mean the price was written from something
209
+ other than the ladder.
210
+
211
+ ## Adoption: accept the chain price, don't adopt it
212
+
213
+ Adoption must accept a chain price that differs from the genesis level. That is
214
+ **not** a reason to write it into `slot.price`. Two different prices are
215
+ conflated here:
216
+
217
+ | Price | Meaning | Must equal a genesis level? |
218
+ |---|---|---|
219
+ | `order.price` | resting limit price on chain | **Yes** |
220
+ | `fill.price` | execution price of a fill | **No** — may be better |
221
+
222
+ A fill can execute better than its resting price, and a partial fill leaves the
223
+ order resting at its original price. So `fill.price != order.price` is normal
224
+ and is not corruption.
225
+
226
+ The real adoption case is **grid regeneration**: the grid is recentered but old
227
+ chain orders keep resting at the *old* grid's levels. With `incrementPercent
228
+ 0.5%`, a resting order at `0.32667340` sits a fraction of a slot from the new
229
+ grid's `slot--17` at `0.32685210` — a `0.0547%` difference. Correct handling:
230
+ adopt into `slot--17` (via `slotIndexForPrice`) and **let the slot keep
231
+ `priceForSlot(-17)`**. The old price is chain metadata about where the order *was
232
+ placed*; it is not the slot's price, and it disappears on the next
233
+ rotation/cancel.
234
+
235
+ This is what the genesis path (`adoptChainOrderIntoSlot`) already did — it sets
236
+ `orderId`, `state`, `size`, `rawOnChain` and **never touches `price`**. The
237
+ legacy fallback was the only path that got it wrong; it now matches.
238
+
239
+ **Both paths now state this explicitly, and warn when it is broken.** Keeping
240
+ the slot's price by *not assigning it* is correct but invisible: nothing would
241
+ catch a later edit that reintroduced `price: chainOrder.price`, and nothing
242
+ would catch a slot whose price was **already** corrupted before it reached
243
+ adoption — it would simply be re-adopted. `adoptedSlotKeepsItsOwnPrice` runs on
244
+ both paths and warns, naming the path, slot, expected level and drift, when an
245
+ adopted slot's price is not its genesis level.
246
+
247
+ That check is deliberately **non-blocking**: the chain order is real and must be
248
+ tracked, so refusing the adoption would strand it untracked on the book —
249
+ strictly worse than the corrupt slot it reports. It is a signal, not a gate.
250
+
251
+ Note the two adoption paths are covered separately, because a fixture with a
252
+ genesis takes the genesis path and a fixture without one takes the legacy
253
+ fallback: LEGACY-ADOPT-001 covers the legacy path, ADOPT-NAME-001 the genesis
254
+ path. Each is mutation-tested against its own path.
255
+
256
+ ## Healing a pre-existing off-grid slot price
257
+
258
+ The emission guard rejects an off-grid price, so a slot that *persists* one is
259
+ rejected on every cycle — a durable stall. `loadGrid` therefore **repairs** any
260
+ slot whose price disagrees with its genesis level, in both validation modes
261
+ (`modules/order/grid.ts`, at the genesis validation loop):
262
+
263
+ - the slot's price is set to `priceForSlot(idx, genesis)`;
264
+ - enforce mode still virtualizes it (`state: VIRTUAL`, `size: 0`, `orderId: ''`);
265
+ - both modes log the correction, naming the slot and the old → new price.
266
+
267
+ This is safe because **the slot id determines its price from the ladder**: no
268
+ legitimate slot can disagree with its own level, so there is no correct value
269
+ being overwritten. Before this, the mismatched price was preserved in *both*
270
+ modes, which meant a corrupt price survived a restart, re-activated into a
271
+ CREATE, and was rejected forever — the guard would have converted a corruption
272
+ bug into a permanent one.
273
+
274
+ Verified: a slot corrupted to 5% off its level is repaired in log and enforce
275
+ modes, and the repaired price passes the emission guard (otherwise the stall is
276
+ not actually healed). Mutation-checked: reverting the price assignment fails the
277
+ assertion.
278
+
279
+ ### After a repair: expect one churn cycle
280
+
281
+ A repair changes `slot.price`, so any *resting* order still sitting at the old
282
+ corrupt price no longer matches its slot. It resolves on the next reconcile via
283
+ price-drift auto-cancel, which means **one cycle of cancel/replace churn after a
284
+ repair is expected, not a new fault**. Operators seeing a burst of cancels
285
+ immediately following a `[GENESIS] Slot … price repaired from genesis` line are
286
+ watching the repair settle.
287
+
288
+ ### Repair only runs at load — so a persistent rejection escalates
289
+
290
+ `loadGrid` heals on reload, but an in-process corruption has no reload to wait
291
+ for. The recurring planner carries the slot's price straight from
292
+ `manager.orders`, so a corrupted slot is re-planned, rejected, and warned on
293
+ every cycle, forever. Nothing heals it short of a restart: the slot is dead
294
+ while the bot looks healthy, and the repeated warns train operators to ignore
295
+ them.
296
+
297
+ The guard therefore **counts consecutive rejecting batches per slot** and, after
298
+ `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` (3) of them, fires
299
+ `requestStructuralGridResync('grid-price-invariant-violation', {slotId, expected,
300
+ actual, site, streak})`. The resync is the same debounced, batch-in-flight-aware,
301
+ two-step (reload → full reset) path already used by the grid-bloat and
302
+ spread-stale detectors — a second repair mechanism would duplicate tested
303
+ machinery for no benefit. A dedicated
304
+ `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` (15 min) bounds repeats, and a **clean
305
+ check clears the streak** so escalation means "rejected N *consecutive* batches",
306
+ not "rejected N times ever".
307
+
308
+ **Why not heal in place** (`slot.price = expected` at rejection time): the
309
+ checker does compute the right value, but silently rewriting it destroys the
310
+ diagnostic signal. The streak is what distinguishes the four corruption sources
311
+ — legacy persisted state, migration fallback, genesis-identity mismatch, or an
312
+ unknown live writer. Auto-heal makes all four look identical. Count first,
313
+ escalate on persistence; consider an in-place fast path only after a soak shows
314
+ in-process corruption is the common case.
315
+
316
+ The streak is **bot-scoped**, not module-scoped: the monolithic runtime
317
+ (`dexbot.ts`) runs every active bot in one process, so a shared count would pool
318
+ unrelated bots' rejections — one bot's two rejections would put the next bot at
319
+ the threshold on its first rejection and fire a spurious structural resync on a
320
+ healthy bot (verified by GPI-WIRE-007/008).
321
+
322
+ ## Out-of-bounds policy: hold, and surface
323
+
324
+ **Decision: hold indefinitely, and warn.** Holding costs opportunity; wrong
325
+ placements cost capital. The grid geometry is not invalidated by the market
326
+ leaving it — every observed violation was on the *placement* side, not the
327
+ resting grid.
328
+
329
+ | Event | Action |
330
+ |---|---|
331
+ | Market moves beyond configured bounds | Keep the grid as-is. Do not recenter, rescale, or extend the boundary. |
332
+ | Chain order exists outside the grid range | Hold untouched (`out-of-grid-deferred`). Do not adopt, do not cancel. |
333
+ | Fill occurs while out of bounds | Apply its accounting; do **not** let its price become grid evidence. |
334
+ | A slot *inside* the grid empties | Refill at `priceForSlot(idx)` — **this continues even out of bounds.** |
335
+ | An order would need a non-grid price to "keep up" | Do not place it. |
336
+
337
+ The last two rows are the point: refilling an emptied slot at its genesis price
338
+ is always valid (the slot's price is still a grid price). What must stop is
339
+ emitting orders whose price is not a grid price.
340
+
341
+ Holds are classified non-blocking by `isNonBlockingUnmatchedOrder` (any
342
+ `*-deferred` reason), so they do not force a grid reset while they persist.
343
+
344
+ ### Hold observability
345
+
346
+ The `[HOLD]` line names side, price, size, reason, and distance from the nearest
347
+ grid bound (as a percentage, so a near-miss is distinguishable from a
348
+ deliberately-placed far order). An unchanged set is re-warned slowly
349
+ (`TIMING.STALE_TOTALS_WARN_RATE_LIMIT_MS`), because "held indefinitely" must not
350
+ be indistinguishable from "bot silently stuck".
351
+
352
+ The gate is content-based, not count-based: same-count churn (one hold clearing
353
+ as another appears) previously looked like "no change" and was never logged.
354
+
355
+ The signature is also the **logging gate**, but it is NOT the staleness clock.
356
+ Age is measured **per stranded order** (`id@price/size:reason`) from the first
357
+ cycle that order was seen stranded, tracked in a bot-scoped map. Two earlier
358
+ choices were wrong, and each was caught by a test that now pins it:
359
+
360
+ 1. `manager._lastUnmatchedChainOrdersAt` records "when we last looked", never
361
+ "when the hold started" — an age gate on it could never fire (HOLD-007).
362
+ 2. The whole-held-set signature clock looked correct but was **reset by
363
+ unrelated churn**, because the signature includes every entry's reason. An
364
+ unrelated hold flapping in and out changed the signature every cycle and
365
+ restarted the clock, so a genuinely stranded order was starved of escalation
366
+ forever (HOLD-010). A per-order clock is immune: age belongs to the order,
367
+ not to the set.
368
+
369
+ A third defect sat in the same path: the signature-change branch used to
370
+ `return` before reaching the escalation call, so any churn skipped escalation
371
+ entirely. Escalation is now invoked on BOTH branches — a signature change is a
372
+ reason to re-log, not a reason to stop evaluating age.
373
+
374
+ When a stranded hold reaches `DEFERRED_HOLD_ESCALATE_MS` (24h) it escalates at
375
+ `error` and fires the same `requestStructuralGridResync('deferred-hold-stale',
376
+ ...)` — the exit that a per-cycle hold-and-warn loop otherwise lacks.
377
+
378
+ Only genuinely **stranded** reasons are escalation triggers. Escalation uses a
379
+ narrow allow-list (`isStrandedHoldOrder`: `out-of-rail-deferred`,
380
+ `out-of-grid-deferred`) rather than the broad `-deferred` non-blocking filter.
381
+ A resync cannot end a broadcast region or re-evaluate an uncommitted boundary,
382
+ so `broadcast-active-deferred`, `boundary-hold-trailing-market`,
383
+ `boundary-unknown-deferred` and `held-plan-unchanged-deferred` are excluded:
384
+ escalating on them would spend a full grid reload on something the owning
385
+ machinery already resolves. The allow-list fails closed, so a future transient
386
+ reason is excluded by default instead of silently becoming a resync trigger
387
+ (HOLD-011).
388
+
389
+ That escalation is safe because the full reset's reconcile is **update-first**:
390
+ unmatched chain orders are price-updated onto rail slots (emitting the rail's
391
+ genesis level, so the RECONCILE-UPDATE guard does not block the resolution) and
392
+ only true surplus is cancelled. Funds are released by price-updating, not by
393
+ inventing a new cancellation policy. A dedicated
394
+ `DEFERRED_HOLD_RESYNC_COOLDOWN_MS` (6h) bounds repeats.
395
+
396
+ ### Known limitation: no market-aware "left the bounds" signal
397
+
398
+ The hold is per-order and reactive: the bot cannot distinguish "one stray
399
+ order outside the range" from "the market left the range entirely", so it
400
+ cannot warn *before* orders become stranded. The earlier anchor/divergence
401
+ constants (`ANCHOR.DIVERGENCE_INFO`, `DIVERGENCE_WARN`) were removed with the
402
+ anchor itself; only `calculateGridSideDivergenceMetric` survives, used for
403
+ side-divergence metrics in `grid.ts`. Until a market-aware signal exists, the
404
+ 24h escalation fires on a timer rather than on cause — whether the market left
405
+ the bounds or one order is simply stranded, because neither case is
406
+ distinguishable from the hold record alone. The blast radius is bounded by
407
+ the stranded-reasons allow-list above. The designated home for a real signal
408
+ is divergence telemetry built on `calculateGridSideDivergenceMetric`.
409
+
410
+ ## Key constants (`modules/constants.ts`, `TIMING`)
411
+
412
+ | Constant | Value | Meaning |
413
+ |---|---|---|
414
+ | `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` | 3 | Consecutive rejecting batches per slot before a structural resync |
415
+ | `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` | 15 min | Bounds repeat resyncs for the same corruption |
416
+ | `DEFERRED_HOLD_ESCALATE_MS` | 24 h | Age at which a stranded deferred hold escalates |
417
+ | `DEFERRED_HOLD_RESYNC_COOLDOWN_MS` | 6 h | Bounds repeat resyncs for stale holds |
418
+ | `STALE_TOTALS_WARN_RATE_LIMIT_MS` | 60 s | Slow re-warn interval for an unchanged hold set |
419
+
420
+ ## Implementation status
421
+
422
+ | Item | State |
423
+ |---|---|
424
+ | Legacy adoption keeps `slot.price`; gains the rail guard | **landed** |
425
+ | Materialize path derives price from genesis, not the descriptor | **landed** |
426
+ | Materialize path derives ORDER TYPE from the same ladder level, not the descriptor | **landed** |
427
+ | Persistent off-grid rejection escalates to a structural resync (per-slot streak) | **landed** |
428
+ | Stale deferred hold escalates to a structural resync (per-order stranded clock) | **landed** |
429
+ | Pre-broadcast substitution removed (CREATE) | **landed** |
430
+ | Pre-broadcast substitution removed (CREATE-fallback) | **landed** |
431
+ | Invariant check at 6 emission sites | **landed — BLOCKING (rejects off-grid emissions)** |
432
+ | Fill-guard pivot validated onto the ladder (`resolveOnGridPivot`) | **landed** |
433
+ | `[HOLD]` enrichment + slow re-warn | **landed** |
434
+ | Final pre-broadcast pivot gate re-checks BUILT ops on a refreshed pivot | **landed** |
435
+ | Fill-guard pivot persisted with the grid snapshot (provenance: fills only) | **landed** |
436
+ | Pivot restored with the boundary (TTL → genesis → on-grid validation chain) | **landed** |
437
+ | Pivot mutation behind one provenance-tagged writer (`setLastFillPivot`) | **landed** |
438
+ | Grid generation invalidates the pivot (in-memory + persisted row) | **landed** |
439
+
440
+ `resolveOnGridPivot` snaps a near-ladder pivot to its slot level but **refuses to
441
+ rewrite a far-off-ladder one** onto an edge slot — silently clamping would dress
442
+ a corrupt pivot up as a legitimate edge fill. Off-ladder pivots are counted and
443
+ reported.
444
+
445
+ ## Verification
446
+
447
+ - **Unit (GPI-001..015, `tests/test_grid_price_invariant_guard.ts`):** for
448
+ every emitted op, `price === priceForSlot(idx, genesis)`.
449
+ Rotation UPDATEs additionally assert `newPrice` matches the **destination**
450
+ slot's level (GPI-010); an off-grid emission is **refused**, a genesis level is
451
+ permitted (GPI-011); unjudgeable inputs fail open (GPI-012); the emitted
452
+ rotation price is derived from the destination's genesis level (GPI-013), and
453
+ the derivation declines — rather than inventing a price — with no ladder
454
+ (GPI-014) or an out-of-ladder index (GPI-015).
455
+ - **Unit:** a legacy-path out-of-grid order is held, not adopted, and keeps its
456
+ genesis price (LEGACY-ADOPT-001/002); the materialize path derives the slot
457
+ price AND order type from genesis and warns when it cannot
458
+ (MATERIALIZE-001/002/003); adoption
459
+ keeps the slot's own level on the genesis path and does not warn when correct
460
+ (ADOPT-NAME-001) (`tests/test_sync_out_of_grid_defer.ts`).
461
+ - **Unit:** pre-broadcast drift is reported at `warn` and the op is built from
462
+ the *planned* price (`tests/test_cow_orchestration_fixes.ts`).
463
+ - **Unit:** pivot snapping and off-ladder refusal
464
+ (`tests/test_last_fill_guard.ts`, PIVOT-001..003); the final pre-broadcast
465
+ pivot gate re-checks BUILT ops on a refreshed pivot, drops violators into the
466
+ skipped-slot restore paths, remaps pending indexes on compaction, and never
467
+ gates cancels or size-updates (`tests/test_final_pivot_gate.ts`, FG-1..11,
468
+ including the 2026-09-13 stale-pivot incident replay).
469
+ - **Unit:** pivot snapshot round-trip and its restore validation chain
470
+ (`tests/test_last_fill_pivot_persistence.ts`, LFP-1..8) — shape gates,
471
+ provenance, TTL, genesis binding, off-ladder refusal, full-family reset.
472
+ - **External gate:** `analysis/grid_correction_check.ts` — target 0 sustained
473
+ violations at 168h/720h. **The baseline is NOT clean:** 4 of 5 bots were
474
+ non-zero over 7 days, so this is a live signal, not a historical one.
475
+ - **Live status:** the blocking check has seen live traffic — 75 judgeable
476
+ checks (`site=COW`, `violated=0`, `unchecked=0`) across four live bot logs
477
+ on 2026-09-14. No live **rejection** or **escalation** has been observed
478
+ yet, so the resync thresholds (`GRID_PRICE_INVARIANT_RESYNC_THRESHOLD`,
479
+ `DEFERRED_HOLD_ESCALATE_MS`) remain validated only by mutation tests. The
480
+ first `violated>0` in production should be read as a real writer, not a
481
+ false positive: a legitimate order equals its genesis level by
482
+ construction. Treat the first escalation as a genuine signal about how
483
+ long an in-process corruption actually survives.
484
+
485
+ Each behavioural fix above is mutation-tested (revert the fix, confirm the test
486
+ fails) so the tests are known to discriminate rather than merely pass.
487
+
488
+ **Emission-site wiring.** The guard's own rules (`checkGridPriceInvariant`,
489
+ `reportGridPriceInvariant`) are unit-tested by GPI-001..015, but that says
490
+ nothing about whether a live batch *consults* them — a mutation audit found the
491
+ three COW sites could be neutralised entirely with every COW test still green.
492
+ `tests/test_grid_price_invariant_wiring.ts` (GPI-WIRE-001..009) closes that: it
493
+ drives `updateOrdersOnChainBatchCOW` end-to-end with a real DEXBot/OrderManager
494
+ and asserts that no op reaches `buildCreateOrderOp`/`buildUpdateOrderOp` and
495
+ nothing is broadcast. Mutation-verified: disabling the blocking CREATE check
496
+ fails GPI-WIRE-001; reverting the rotation price derivation fails GPI-WIRE-004;
497
+ feeding the guard the raw pivot instead of the validated one fails GPI-WIRE-005.
498
+ The escalation is pinned by GPI-WIRE-006..009: removing the escalation call,
499
+ removing the streak reset on a clean check, and disabling the cooldown are each
500
+ caught, and GPI-WIRE-009 pins that the per-slot streak is bot-scoped — the
501
+ monolithic runtime runs every active bot in ONE process, so a module-level
502
+ streak would let one bot's rejections push another to the threshold on its first
503
+ rejection, firing a spurious resync on a healthy bot. The hold escape hatch is
504
+ pinned by HOLD-006..011 (`tests/test_hold_and_center_guards.ts`). Three
505
+ mutation-verified discriminators: HOLD-007 fails if age comes from
506
+ `_lastUnmatchedChainOrdersAt` (the clock that could never fire); HOLD-010 fails
507
+ if age comes from the whole-held-set signature clock (reset by unrelated churn,
508
+ starving a stranded order forever); HOLD-011 fails if the narrow stranded
509
+ allow-list is replaced by the broad `-deferred` filter (transient holds would
510
+ spend a grid reload).
511
+
512
+ **One site is not black-box reachable, by design.** The UPDATE check
513
+ (`action.newGridId`) is a backstop: `deriveRotationPrice` computes the emitted
514
+ price *from* the destination's genesis level before the check runs, so at the
515
+ check the id and price agree by construction. Disabling that check cannot be
516
+ caught from outside because there is no reachable input that makes it fire. The
517
+ reachable property — that a planner-supplied `action.newPrice` disagreeing with
518
+ the destination's level never reaches the chain — is what GPI-WIRE-004 pins.
519
+ Treat the UPDATE check as defence for a missing genesis ladder (it fails open
520
+ there), not as the only thing standing between a bad plan and a live order.
521
+
522
+ **False-positive audit (blocking enabled).** Because the check now rejects, it
523
+ was audited against real grid builds before enabling: **1,153 slots across five
524
+ geometries** — standard, wide (1.65x-10x), tight, wide at 2% increment, and the
525
+ incident geometry (1.65x-10x around ~0.32) — produced **zero** off-grid
526
+ rejections and zero uncheckable slots. The identity-preserving transforms
527
+ (`virtualizeOrder`, `convertToSpreadPlaceholder`, `toRailHolePlaceholder`) also
528
+ pass, while an order object carrying its own drifted price is caught (50% drift
529
+ in the fixture). That is the evidence that blocking is safe: a legitimate order
530
+ equals its genesis level by construction, so only a genuinely mis-priced
531
+ emission can be rejected.
532
+
533
+ ## The removed placement gate: do NOT naively re-land
534
+
535
+ `d808c052` added `checkPlacementPriceSanity` (reject a planned price >5% from a
536
+ traded-range mid). It was removed the same day by `e2898e51` because it "blocked
537
+ legitimate order creates and updates".
538
+
539
+ **That framing is misleading, and the code shows the real mechanism.** The
540
+ removal was not an over-tuned threshold — it was a **design deadlock**:
541
+
542
+ - `ANCHOR.PRICE_OUTLIER_FACTOR: 2` bounded the fills considered plausible.
543
+ - Out-of-bounds fill prices were then **skipped entirely** by
544
+ `resolveFillPrice` / the burst fill loop.
545
+ - But those same fills were what boundary correction needed in order to *update*
546
+ the anchor.
547
+ - So after a genuine trend beyond 2x, the correction bound could never be
548
+ re-derived: the guard bounded the very evidence required to correct it.
549
+
550
+ Hence "too restrictive" describes a design flaw, not a tuning problem. Framing
551
+ it as tuning invited the wrong fix — deletion instead of repairing the
552
+ unbounded-anchor path. A re-land must address the deadlock, not retune a number.
553
+
554
+ The violator that ran free once the detector was gone was the **role-assignment
555
+ pass inside `calculateTargetGrid`** (`modules/order/strategy.ts`), which re-typed
556
+ slots relative to *fill prices* rather than the boundary — with an **unbounded**
557
+ BUY→SELL direction (`slot.price > minFilledBuyPrice` has no upper limit). With
558
+ `minFilledBuyPrice` inflated by a poisoned fill, every BUY below it flipped to
559
+ SELL and was re-created above it: the observed ratchet.
560
+
561
+ Implication: restoring `checkPlacementPriceSanity` alone would **not** fix that.
562
+ Any re-land must fix the out-of-bounds-skip deadlock *and* clamp the rotation
563
+ independently of the anchor. See `git show d808c052` for the removed
564
+ implementation; the removal provenance (symbol → built → removed) is recorded in
565
+ `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md`.
566
+
567
+ ## Caveats
568
+
569
+ - Figures (`2.45x`, `~180 slots`, `[0.196, 3.234]`) are derived from typical
570
+ configured multipliers and a representative reference price. Exact levels
571
+ should be read from the persisted grid to confirm.
572
+ - The removed-path behaviour was read from `d808c052`/`e2898e51`, not executed.
573
+ - No real account names, bot names, or live market pairs are included.
@@ -277,7 +277,7 @@ The snapshot fields involved are:
277
277
  `AMA_SLOPE_DELTA_THRESHOLD_PERCENT`, a percentage of the AMA max slope
278
278
  (`(value/100) × maxSlopePct`) — default `8%`, stored under `MARKET_ADAPTER` in
279
279
  `profiles/general.settings.json` and editable via `dexbot bot` → `1) Grid
280
- Health` (`AMA-Slope Δ`). An explicit `amaSlopeDeltaThresholdPercent` in
280
+ Drift` (`AMA-Slope Δ`). An explicit `amaSlopeDeltaThresholdPercent` in
281
281
  `profiles/market_adapter_settings.json` bypasses the factor and is used
282
282
  directly as an average percent-per-bar threshold.
283
283