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