Commit d12e9da
committed
fix: prevent and resolve stale dust duplicates at the same price level
Two on-chain orders at the same grid level (e.g. slot-119 with 0.0006 dust
remnant + slot-120 with the real 2.4052 order) were re-forming because the
sync engine adopted orphan remnants into virtual slots, and the dust auto-
cancel only targeted the top-of-window partial, leaving interior duplicates
in place indefinitely. Four coordinated fixes close both the formation
path and the persistence path.
## Sync engine: reject any orphan at a duplicate price level
File: modules/order/sync_engine.ts
- Problem: Pass-2 fallback adopted any orphan into a virtual/spread slot,
including tiny remnants of replaced orders, creating a duplicate price
level inside the active grid.
- Impact: Visible as two on-chain orders at the same price (real order
covered by a grid slot, dust remnant adopted into a different slot).
- Solution: Before adopting an orphan, check if an active grid order of
the same type already exists at the same price (within
`calculatePriceTolerance` using `Math.max(size)` to keep tolerance
tight). If so, the orphan is a duplicate — skip adoption and push to
`unmatchedChainOrders` so the reconcile layer cancels it on chain.
Size is irrelevant; any duplicate violates the grid invariant of one
order per price level.
## Reconcile: cancel duplicate chain orders unconditionally
File: modules/order/grid_reconcile.ts
- Problem: SUSPECTED_DUPLICATE block logged the duplicate but never
removed it from chain, so duplicates persisted across restarts.
- Impact: Two on-chain orders at the same price level after every restart
until manual intervention.
- Solution: When an unmatched order is within `looseTolerance` of an
active grid order, cancel it on chain via `_cancelChainOrder` with
`releaseUntrackedFunds: true`. Cancelled IDs are filtered out of
`unmatchedParsed` so `_reconcileStartupSide` doesn't reprocess them.
No size guard — any duplicate at the same price is a violation.
- Also removed `SUSPECTED_DUPLICATE_TOLERANCE_FLOOR = 0.01` — absolute
price floor was wrong for low-price assets (BTS/USD: floor is 100% of
price) and irrelevant for high-price assets. Now just
`tolerance * SUSPECTED_DUPLICATE_TOLERANCE_MULTIPLIER`.
## Grid: dust detection covers interior partials with duplicate price level
File: modules/order/grid.ts
- Problem: `checkWindowDust` only flagged the top-of-window partial per
side, so interior dust duplicates at the same price level as an active
sibling were never detected for the `DUST_CANCEL_DELAY_SEC` auto-cancel.
- Impact: Interior dust duplicates persisted indefinitely even when an
active sibling at the same price made them safe to cancel (no gap).
- Solution: Interior partials are eligible for dust detection if they
share a price level with an active sibling (within
`calculatePriceTolerance` using `Math.max(size)`). Top-of-window
partials remain always eligible. Only ACTIVE siblings are checked —
two PARTIALs sharing a price with no active sibling do not qualify;
that gap is left to the rebalancer.
## Math helper documentation
File: modules/order/utils/math.ts
- Added doc note on `calculatePriceTolerance`: `orderSize` controls
strictness (larger = tighter tolerance). For duplicate-price-level
detection, always pass `Math.max(sizeA, sizeB)` to avoid a tiny dust
order inflating the tolerance window and falsely matching distant
prices. This makes the symmetric `Math.max`/`Math.min` usage in
grid.ts and sync_engine.ts self-documenting.
## Testing Notes
- Verified numerically for BTS/XRP @ 1079.67 (precision 5/5) that the
old tolerance (orphan size 0.0006) was 18.01 for SELL / 19,450 for
BUY, falsely matching an orphan at 1079.67 to a grid order at the
next level (1085, +0.5%). New tolerance (`Math.max(0.0006, 2.4052)`)
is 0.0045 / 4.85 and correctly rejects the false match.
- tests/test_dust_rebalance_logic.ts — added
`testInteriorDustWithDuplicatePriceLevel` (interior partial + active
sibling at same price → eligible) and
`testInteriorDustAdjacentGridLevelNotEligible` (partial at adjacent
price → not eligible).
- tests/test_sync_logic.ts — updated
`testOrphanAtDuplicatePriceLevelIsNotAdopted` (orphan sharing price
with active order → NOT adopted, spread slot stays virtual).
- `npx tsc --noEmit` → clean.
- All relevant tests pass.1 parent 5cfe147 commit d12e9da
18 files changed
Lines changed: 276 additions & 44 deletions
File tree
- analysis/ama_fitting
- claw
- runtimes/openclaw-plugin
- tests
- docs
- modules/order
- utils
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
5 | 13 | | |
6 | 14 | | |
7 | 15 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| |||
181 | 181 | | |
182 | 182 | | |
183 | 183 | | |
184 | | - | |
| 184 | + | |
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
774 | 774 | | |
775 | 775 | | |
776 | 776 | | |
777 | | - | |
| 777 | + | |
778 | 778 | | |
779 | 779 | | |
780 | 780 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
173 | 176 | | |
174 | 177 | | |
175 | 178 | | |
| |||
218 | 221 | | |
219 | 222 | | |
220 | 223 | | |
221 | | - | |
| 224 | + | |
222 | 225 | | |
223 | 226 | | |
224 | 227 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
825 | 825 | | |
826 | 826 | | |
827 | 827 | | |
828 | | - | |
| 828 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | | - | |
| 111 | + | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| |||
0 commit comments