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.
- package/CHANGELOG.md +76 -4
- package/README.md +108 -36
- package/analysis/README.md +11 -16
- package/analysis/account_resolver.ts +1 -11
- package/analysis/bot_usage/kibana_bot_queries.ts +1 -1
- package/analysis/chain_pool.ts +1 -1
- package/analysis/fills_source.ts +0 -6
- package/analysis/grid_correction_check.ts +20 -26
- package/analysis/resolve_source.ts +1 -1
- package/analysis/trade_profitability.ts +2 -5
- package/analysis/trend_detection/README.md +1 -3
- package/analysis/trend_detection/package.json +1 -1
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/account_resolver.d.ts +1 -4
- package/dist/analysis/account_resolver.d.ts.map +1 -1
- package/dist/analysis/account_resolver.js +1 -1
- package/dist/analysis/account_resolver.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +1 -2
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.js +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
- package/dist/analysis/chain_pool.d.ts +1 -3
- package/dist/analysis/chain_pool.d.ts.map +1 -1
- package/dist/analysis/chain_pool.js +1 -1
- package/dist/analysis/chain_pool.js.map +1 -1
- package/dist/analysis/fills_source.d.ts +1 -51
- package/dist/analysis/fills_source.d.ts.map +1 -1
- package/dist/analysis/fills_source.js +1 -1
- package/dist/analysis/fills_source.js.map +1 -1
- package/dist/analysis/grid_correction_check.d.ts +2 -3
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +18 -26
- package/dist/analysis/grid_correction_check.js.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -1
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/trade_profitability.d.ts.map +1 -1
- package/dist/analysis/trade_profitability.js +2 -7
- package/dist/analysis/trade_profitability.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +82 -83
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +1 -15
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +1 -1
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +1 -35
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +10 -54
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +1 -8
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.js +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +2 -63
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
- package/dist/market_adapter/inputs/window_cache.js +22 -102
- package/dist/market_adapter/inputs/window_cache.js.map +1 -1
- package/dist/modules/account_bots.d.ts +16 -2
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +108 -44
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +20 -2
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +63 -3
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +10 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +197 -28
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.browser.js +0 -31
- package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +312 -10
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +1 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +97 -3
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts +20 -0
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +29 -0
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/chain_keys.d.ts +37 -22
- package/dist/modules/chain_keys.d.ts.map +1 -1
- package/dist/modules/chain_keys.js +145 -44
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/cli_start_onboarding.d.ts +10 -0
- package/dist/modules/cli_start_onboarding.d.ts.map +1 -0
- package/dist/modules/cli_start_onboarding.js +15 -0
- package/dist/modules/cli_start_onboarding.js.map +1 -0
- package/dist/modules/cli_start_output.d.ts +6 -0
- package/dist/modules/cli_start_output.d.ts.map +1 -0
- package/dist/modules/cli_start_output.js +8 -0
- package/dist/modules/cli_start_output.js.map +1 -0
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +103 -1
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credit_runtime.d.ts +0 -2
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +1 -10
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +13 -10
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +0 -9
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +13 -0
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +62 -10
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +6 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +22 -3
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +24 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +45 -1
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +13 -1
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +4 -18
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +4 -34
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts +9 -12
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +9 -12
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts +35 -40
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +76 -44
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +111 -20
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +5 -3
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +29 -3
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/logger.d.ts +12 -2
- package/dist/modules/order/logger.d.ts.map +1 -1
- package/dist/modules/order/logger.js +29 -7
- package/dist/modules/order/logger.js.map +1 -1
- package/dist/modules/order/manager.d.ts +49 -8
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +72 -34
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +22 -7
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +200 -33
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +97 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +274 -2
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/withPoolRef.d.ts +12 -0
- package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
- package/dist/modules/order/utils/withPoolRef.js +17 -0
- package/dist/modules/order/utils/withPoolRef.js.map +1 -1
- package/dist/modules/order/working_grid.d.ts +8 -13
- package/dist/modules/order/working_grid.d.ts.map +1 -1
- package/dist/modules/order/working_grid.js +8 -29
- package/dist/modules/order/working_grid.js.map +1 -1
- package/dist/modules/paths.d.ts +7 -1
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +10 -3
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/utils/text_width.d.ts +19 -0
- package/dist/modules/utils/text_width.d.ts.map +1 -0
- package/dist/modules/utils/text_width.js +97 -0
- package/dist/modules/utils/text_width.js.map +1 -0
- package/dist/pm2.d.ts.map +1 -1
- package/dist/pm2.js +95 -18
- package/dist/pm2.js.map +1 -1
- package/dist/scripts/sync-version.js +0 -22
- package/dist/scripts/sync-version.js.map +1 -1
- package/dist/scripts/update.js +62 -13
- package/dist/scripts/update.js.map +1 -1
- package/dist/scripts/update_dist_freshness.d.ts +12 -0
- package/dist/scripts/update_dist_freshness.d.ts.map +1 -1
- package/dist/scripts/update_dist_freshness.js +30 -1
- package/dist/scripts/update_dist_freshness.js.map +1 -1
- package/dist/unlock.d.ts +14 -2
- package/dist/unlock.d.ts.map +1 -1
- package/dist/unlock.js +73 -4
- package/dist/unlock.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -6
- package/docs/COW_INVARIANTS.md +9 -0
- package/docs/CREDENTIAL_SECURITY.md +26 -0
- package/docs/DEXBOT_COMPARISON.md +11 -11
- package/docs/EVOLUTION.md +9 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
- package/docs/GRID_PRICE_INVARIANT.md +573 -0
- package/docs/GRID_RECALCULATION.md +1 -1
- package/docs/GRID_RECONCILE.md +60 -39
- package/docs/LOGGING.md +17 -2
- package/docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md +555 -0
- package/docs/README.md +3 -3
- package/docs/WORKFLOW.md +12 -6
- package/docs/architecture.md +40 -38
- package/docs/developer_guide.md +4 -8
- package/market_adapter/README.md +16 -12
- package/modules/README.md +1 -1
- package/package.json +8 -4
- package/scripts/README.md +0 -9
- package/scripts/reset-settings.sh +1 -1
- package/analysis/ama_fitting/package.json +0 -12
- package/analysis/analyze_derivatives.ts +0 -320
- package/analysis/derivative_chart_generator.ts +0 -874
- package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +0 -403
- package/analysis/trend_detection/derivative_analyzer.ts +0 -1059
- package/dist/analysis/analyze_derivatives.d.ts +0 -68
- package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
- package/dist/analysis/analyze_derivatives.js +0 -287
- package/dist/analysis/analyze_derivatives.js.map +0 -1
- package/dist/analysis/derivative_chart_generator.d.ts +0 -12
- package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
- package/dist/analysis/derivative_chart_generator.js +0 -891
- package/dist/analysis/derivative_chart_generator.js.map +0 -1
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -248
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
- package/dist/analysis/trend_detection/derivative_analyzer.js +0 -910
- 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
|
-
|
|
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
|
|