dexbot 1.5.0 → 1.5.2
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 +44 -0
- package/analysis/ama_fitting/package.json +1 -1
- 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/dexbot.js +5 -5
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_orders.d.ts +7 -1
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +34 -1
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +12 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +1 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +19 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +335 -78
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +7 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +54 -4
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +7 -0
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +34 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +199 -5
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts +122 -15
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +310 -46
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +22 -17
- 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 +7 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +98 -28
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +52 -6
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +156 -21
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +17 -2
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +213 -17
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +70 -4
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +136 -5
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +117 -5
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +274 -13
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +14 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +51 -3
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +29 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +207 -35
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +53 -33
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +7 -5
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
- package/docs/GRID_RECONCILE.md +1 -0
- package/docs/LOGGING.md +3 -0
- package/docs/README.md +2 -2
- package/docs/WORKFLOW.md +1 -1
- package/package.json +1 -1
|
@@ -95,16 +95,84 @@
|
|
|
95
95
|
*/
|
|
96
96
|
import { ORDER_TYPES, ORDER_STATES, TIMING, BTS_PRECISION } from '../constants.js';
|
|
97
97
|
import * as Format from './format.js';
|
|
98
|
-
import { lookupAsset } from './utils/system.js';
|
|
98
|
+
import { lookupAsset, sleep, resolveAccountRef } from './utils/system.js';
|
|
99
99
|
import * as chainOrders from '../chain_orders.js';
|
|
100
100
|
import * as client from '../bitshares_client.js';
|
|
101
101
|
const { BitShares } = client;
|
|
102
102
|
import { NATIVE_CLIENT } from '../constants.js';
|
|
103
103
|
const { toFiniteNumber } = Format;
|
|
104
|
-
import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, slotIndexForPrice, isSlotInRail, priceSlotEqual } from './utils/math.js';
|
|
104
|
+
import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, getSellStartIdx, slotIndexForPrice, isSlotInRail, isSlotIndexInGapBand, priceSlotEqual } from './utils/math.js';
|
|
105
105
|
import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, hasOnChainId, isOrderVirtual, resolveSpreadOrderSide, duplicateOrphanLogInfo } from './utils/order.js';
|
|
106
|
+
import { parseSlotIndex } from './utils/slot.js';
|
|
106
107
|
import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
|
|
107
108
|
import { getErrorMessage } from '../utils/errors.js';
|
|
109
|
+
/**
|
|
110
|
+
* Confirming re-read for the suspect-empty-read guard: after
|
|
111
|
+
* SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive 0-order snapshots with placed
|
|
112
|
+
* grid orders still present, one more read after
|
|
113
|
+
* SYNC_EMPTY_READ_CONFIRM_DELAY_MS decides whether the account is genuinely
|
|
114
|
+
* empty. A lagging node serves consecutive empties, so the count alone is
|
|
115
|
+
* not authoritative.
|
|
116
|
+
*
|
|
117
|
+
* Test seam: when manager._confirmEmptyReadFn is a function it is used
|
|
118
|
+
* instead of the chain read (must resolve to a raw order array, or null
|
|
119
|
+
* when the re-read itself is ambiguous). A throwing seam is 'ambiguous'.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} mgr - OrderManager instance (accountId, config, logger).
|
|
122
|
+
* @returns {Promise<string>} 'confirmed' (still empty — accept),
|
|
123
|
+
* 'contradicted' (non-empty — reset counter, refuse this round),
|
|
124
|
+
* 'ambiguous' (truncated/read-error — refuse, re-confirm next round), or
|
|
125
|
+
* 'unavailable' (no chain identity or dry-run — caller falls back to the
|
|
126
|
+
* legacy count-based acceptance, preserving unit-test semantics).
|
|
127
|
+
*/
|
|
128
|
+
async function confirmSuspectEmptyRead(mgr) {
|
|
129
|
+
try {
|
|
130
|
+
if (mgr?.config?.dryRun)
|
|
131
|
+
return 'unavailable';
|
|
132
|
+
const seam = mgr?._confirmEmptyReadFn;
|
|
133
|
+
const delay = Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
|
|
134
|
+
if (typeof seam === 'function') {
|
|
135
|
+
if (delay > 0)
|
|
136
|
+
await sleep(delay);
|
|
137
|
+
let fresh = null;
|
|
138
|
+
try {
|
|
139
|
+
fresh = await seam();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return 'ambiguous';
|
|
143
|
+
}
|
|
144
|
+
if (fresh === null || fresh === undefined)
|
|
145
|
+
return 'ambiguous';
|
|
146
|
+
if (Array.isArray(fresh) && fresh.length > 0)
|
|
147
|
+
return 'contradicted';
|
|
148
|
+
return 'confirmed';
|
|
149
|
+
}
|
|
150
|
+
let accountRef = null;
|
|
151
|
+
try {
|
|
152
|
+
accountRef = resolveAccountRef(mgr, null);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return 'unavailable';
|
|
156
|
+
}
|
|
157
|
+
if (!accountRef)
|
|
158
|
+
return 'unavailable';
|
|
159
|
+
if (delay > 0)
|
|
160
|
+
await sleep(delay);
|
|
161
|
+
const fresh = await chainOrders.readOpenOrdersGuarded(chainOrders, accountRef, {
|
|
162
|
+
log: (message, level) => mgr?.logger?.log?.(message, level),
|
|
163
|
+
label: 'SYNC-CONFIRM',
|
|
164
|
+
detail: 'suspect-empty confirm re-read',
|
|
165
|
+
});
|
|
166
|
+
if (fresh === null)
|
|
167
|
+
return 'ambiguous';
|
|
168
|
+
if (Array.isArray(fresh) && fresh.length > 0)
|
|
169
|
+
return 'contradicted';
|
|
170
|
+
return 'confirmed';
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return 'ambiguous';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
108
176
|
function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcTolerance, matchedGridOrderIds = null) {
|
|
109
177
|
if (!mgr?.orders || !chainOrder || typeof precision !== 'number')
|
|
110
178
|
return 'candidate diagnostics unavailable';
|
|
@@ -200,6 +268,15 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
|
|
|
200
268
|
let bestMatch = { ...slot };
|
|
201
269
|
const wasVirtual = slot.state === ORDER_STATES.VIRTUAL;
|
|
202
270
|
const wasPartial = slot.state === ORDER_STATES.PARTIAL;
|
|
271
|
+
// SPREAD slots are legitimate adoption targets (typeCompat allows them),
|
|
272
|
+
// but the slot must be re-typed to the real on-chain side BEFORE activation:
|
|
273
|
+
// validateOrder treats SPREAD + orderId/on-chain state as fatal
|
|
274
|
+
// ILLEGAL_SPREAD_STATE, and the size precision below depends on the
|
|
275
|
+
// resolved side. Every other activation path (COW rotation destinations,
|
|
276
|
+
// legacy fallback adoption, grid load) performs the same re-type.
|
|
277
|
+
if (bestMatch.type === ORDER_TYPES.SPREAD && (chainOrder.type === ORDER_TYPES.BUY || chainOrder.type === ORDER_TYPES.SELL)) {
|
|
278
|
+
bestMatch.type = chainOrder.type;
|
|
279
|
+
}
|
|
203
280
|
bestMatch.orderId = chainOrderId;
|
|
204
281
|
bestMatch.state = wasVirtual ? ORDER_STATES.ACTIVE : slot.state;
|
|
205
282
|
const bestMatchRaw = rawChainOrders.get(chainOrderId);
|
|
@@ -233,8 +310,17 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
|
|
|
233
310
|
}
|
|
234
311
|
else {
|
|
235
312
|
const spreadOrder = convertToSpreadPlaceholder(bestMatch);
|
|
313
|
+
const applied = await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
314
|
+
if (applied === false) {
|
|
315
|
+
// Rejected adoption must not poison the slot for the next
|
|
316
|
+
// chain order: the slot was marked matched above, so release
|
|
317
|
+
// it — B's adoption re-fails the same validation and gets
|
|
318
|
+
// the existing cancelOnly handling if the rejection was
|
|
319
|
+
// slot-specific.
|
|
320
|
+
matchedGridOrderIds.delete(bestMatch.id);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
236
323
|
filledOrders.push({ ...bestMatch });
|
|
237
|
-
await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
238
324
|
updatedOrders.push(spreadOrder);
|
|
239
325
|
chainOrderIdsOnGrid.add(chainOrderId);
|
|
240
326
|
return true; // filled path
|
|
@@ -243,10 +329,16 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
|
|
|
243
329
|
else if (wasPartial) {
|
|
244
330
|
bestMatch.state = ORDER_STATES.PARTIAL;
|
|
245
331
|
}
|
|
246
|
-
await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
|
|
332
|
+
const applied = await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
|
|
333
|
+
if (applied === false) {
|
|
334
|
+
// Same un-poisoning as the filled path above: the slot counts as
|
|
335
|
+
// matched only once the update lands.
|
|
336
|
+
matchedGridOrderIds.delete(bestMatch.id);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
247
339
|
updatedOrders.push(bestMatch);
|
|
248
340
|
chainOrderIdsOnGrid.add(chainOrderId);
|
|
249
|
-
return
|
|
341
|
+
return true;
|
|
250
342
|
}
|
|
251
343
|
function computeOutOfToleranceDriftTag(mgr, chainOrder, calcToleranceFn) {
|
|
252
344
|
if (!mgr?.orders || !chainOrder)
|
|
@@ -567,7 +659,8 @@ class SyncEngine {
|
|
|
567
659
|
// ("phantom" resets). Refuse to reconcile on a
|
|
568
660
|
// suspect empty; the guard is self-expiring: after
|
|
569
661
|
// TIMING.SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive empty reads the
|
|
570
|
-
// account really is empty and the sync accepts it
|
|
662
|
+
// account really is empty and the sync accepts it — but only after one
|
|
663
|
+
// confirming re-read (see below). Any non-empty read
|
|
571
664
|
// resets the counter.
|
|
572
665
|
if (parsedChainOrders.size === 0) {
|
|
573
666
|
const gridOrderIds = Array.from(mgr.orders.values()).filter((o) => o?.orderId).length;
|
|
@@ -583,7 +676,33 @@ class SyncEngine {
|
|
|
583
676
|
`refusing reconciliation (phantom protection); accepting after ${limit} consecutive empties or next non-empty read`, 'warn');
|
|
584
677
|
return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
|
|
585
678
|
}
|
|
586
|
-
|
|
679
|
+
// Limit reached: a lagging node serves CONSECUTIVE 0-order
|
|
680
|
+
// snapshots, so the count alone is not authoritative. Require
|
|
681
|
+
// one confirming re-read after SYNC_EMPTY_READ_CONFIRM_DELAY_MS
|
|
682
|
+
// before reconciling to empty; a contradicted re-read
|
|
683
|
+
// (non-empty) resets the counter and refuses this round (the
|
|
684
|
+
// next sync reconciles against the fresh snapshot), while an
|
|
685
|
+
// ambiguous re-read (truncated/read-error) refuses and retries
|
|
686
|
+
// the confirm on the next empty round.
|
|
687
|
+
const confirm = await confirmSuspectEmptyRead(mgr);
|
|
688
|
+
if (confirm === 'contradicted') {
|
|
689
|
+
mgr.logger?.log?.(`[SYNC] Suspect empty read contradicted by confirm re-read with ${gridOrderIds} grid orderIds — ` +
|
|
690
|
+
`resetting empty-read counter; reconciling on the next non-empty sync`, 'warn');
|
|
691
|
+
mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
|
|
692
|
+
return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
|
|
693
|
+
}
|
|
694
|
+
if (confirm === 'ambiguous') {
|
|
695
|
+
mgr.logger?.log?.(`[SYNC] Suspect empty read confirm re-read ambiguous with ${gridOrderIds} grid orderIds — ` +
|
|
696
|
+
`refusing reconciliation; re-confirming on the next empty read`, 'warn');
|
|
697
|
+
mgr._suspectEmptyReads = { count: limit, firstAt: suspect.firstAt || Date.now() };
|
|
698
|
+
return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
|
|
699
|
+
}
|
|
700
|
+
// 'confirmed' (still empty on re-read) or 'unavailable' (no
|
|
701
|
+
// chain identity / dry-run — legacy count-based acceptance,
|
|
702
|
+
// preserving unit-test semantics): reconcile to empty.
|
|
703
|
+
mgr.logger?.log?.(`[SYNC] Empty read confirmed after ${suspect.count} consecutive attempts` +
|
|
704
|
+
(confirm === 'unavailable' ? ' (no chain identity for confirm re-read)' : ' (+ confirming re-read)') +
|
|
705
|
+
` — reconciling to empty account`, 'warn');
|
|
587
706
|
mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
|
|
588
707
|
}
|
|
589
708
|
else {
|
|
@@ -829,9 +948,17 @@ class SyncEngine {
|
|
|
829
948
|
&& toFiniteNumber(reboundOrder.rawOnChain?.for_sale, 0) > 0) {
|
|
830
949
|
reboundOrder.state = ORDER_STATES.PARTIAL;
|
|
831
950
|
}
|
|
951
|
+
const applied = await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
|
|
952
|
+
if (applied === false) {
|
|
953
|
+
// Rejected rebind: the slot keeps its old binding
|
|
954
|
+
// (set ops must run only after the apply lands), so
|
|
955
|
+
// the old orderId stays matched on-grid and the swap
|
|
956
|
+
// candidate stays unmatched → pass 2 cancels it.
|
|
957
|
+
mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: rebind to ${swapMatch.id} rejected — slot keeps ${gridOrder.orderId}; duplicate falls to pass 2`, 'warn');
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
832
960
|
chainOrderIdsOnGrid.delete(gridOrder.orderId);
|
|
833
961
|
chainOrderIdsOnGrid.add(swapMatch.id);
|
|
834
|
-
await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
|
|
835
962
|
updatedOrders.push(reboundOrder);
|
|
836
963
|
mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: tracked order ${gridOrder.orderId} ` +
|
|
837
964
|
`(chain size ${chainOrder.size}) disagrees with booked size ${gridOrder.size}, but ` +
|
|
@@ -855,11 +982,23 @@ class SyncEngine {
|
|
|
855
982
|
updatedOrder.state = (chainSizeInt < currentSizeInt)
|
|
856
983
|
? ORDER_STATES.PARTIAL
|
|
857
984
|
: gridOrder.state;
|
|
858
|
-
await mgr._applyOrderUpdate(updatedOrder, 'sync-pass1-partial', { skipAccounting: skipAccounting, fee: 0 });
|
|
985
|
+
const partialApplied = await mgr._applyOrderUpdate(updatedOrder, 'sync-pass1-partial', { skipAccounting: skipAccounting, fee: 0 });
|
|
986
|
+
if (partialApplied === false) {
|
|
987
|
+
// Rejected resize: the slot keeps its booked size;
|
|
988
|
+
// divergence re-detects on the next sync.
|
|
989
|
+
mgr.logger?.log?.(`[SYNC] Partial-size update for ${gridOrder.id} rejected — slot keeps booked size ${gridOrder.size}`, 'warn');
|
|
990
|
+
}
|
|
859
991
|
}
|
|
860
992
|
else {
|
|
861
993
|
const spreadOrder = convertToSpreadPlaceholder(gridOrder);
|
|
862
|
-
await mgr._applyOrderUpdate(spreadOrder, 'sync-pass1-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
994
|
+
const filledApplied = await mgr._applyOrderUpdate(spreadOrder, 'sync-pass1-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
995
|
+
if (filledApplied === false) {
|
|
996
|
+
// Rejected virtualization must not book a fill:
|
|
997
|
+
// neither the filled order nor the placeholder
|
|
998
|
+
// update leaves this path.
|
|
999
|
+
mgr.logger?.log?.(`[SYNC] Filled virtualization for ${gridOrder.id} rejected — fill NOT booked`, 'error');
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
863
1002
|
// Push the filled order with its REAL side (chain order
|
|
864
1003
|
// type), not the SPREAD placeholder: downstream fill
|
|
865
1004
|
// processing (deriveTargetBoundary) derives boundary
|
|
@@ -913,7 +1052,14 @@ class SyncEngine {
|
|
|
913
1052
|
const currentGridOrder = mgr.orders.get(gridOrder.id) || gridOrder;
|
|
914
1053
|
const hadOrderId = Boolean(currentGridOrder?.orderId);
|
|
915
1054
|
const spreadOrder = convertToSpreadPlaceholder(currentGridOrder);
|
|
916
|
-
await mgr._applyOrderUpdate(spreadOrder, 'sync-cleanup-phantom', { skipAccounting: skipAccounting, fee: 0 });
|
|
1055
|
+
const phantomApplied = await mgr._applyOrderUpdate(spreadOrder, 'sync-cleanup-phantom', { skipAccounting: skipAccounting, fee: 0 });
|
|
1056
|
+
if (phantomApplied === false) {
|
|
1057
|
+
// Rejected virtualization books no phantom fill: the slot
|
|
1058
|
+
// keeps its order and the disappearance re-detects next
|
|
1059
|
+
// sync.
|
|
1060
|
+
mgr.logger?.log?.(`[SYNC] Phantom cleanup for ${gridOrder.id} rejected — fill NOT booked`, 'warn');
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
917
1063
|
// Only genuine disappearances (had orderId) count as fills.
|
|
918
1064
|
if (hadOrderId) {
|
|
919
1065
|
// A SPREAD slot can carry an on-chain order (spread-correction
|
|
@@ -961,6 +1107,19 @@ class SyncEngine {
|
|
|
961
1107
|
const slotId = `slot-${idx}`;
|
|
962
1108
|
const gapSlots = genesis.gapSlots ?? mgr._gapSlots ?? 0;
|
|
963
1109
|
const boundaryIdx = mgr.boundaryIdx;
|
|
1110
|
+
// Pre-boundary sync: gap geometry is unknown, so adoption is
|
|
1111
|
+
// deferred entirely — touch nothing (no adopt, no cancelOnly).
|
|
1112
|
+
// The orphan stays visible to the crossing guards and the
|
|
1113
|
+
// validate orphan layer via _lastUnmatchedChainOrders and is
|
|
1114
|
+
// re-evaluated once the boundary commits. Accepted cost: a
|
|
1115
|
+
// legitimate in-rail orphan waits one sync cycle
|
|
1116
|
+
// post-boundary-commit before adoption. Strictly better than
|
|
1117
|
+
// adopting a gap stray into the wrong slot.
|
|
1118
|
+
if (boundaryIdx == null || !Number.isFinite(Number(boundaryIdx))) {
|
|
1119
|
+
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'boundary-unknown-deferred', candidateSlotId: slotId });
|
|
1120
|
+
mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted: boundary unknown, deferred until boundary commits (nearest slot ${slotId})`, 'warn');
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
964
1123
|
// Duplicate-price guard becomes slotId equality: if placed order already occupies this slot
|
|
965
1124
|
const occupying = mgr.orders.get(slotId);
|
|
966
1125
|
if (occupying && isOrderPlaced(occupying) && occupying.type === chainOrder.type) {
|
|
@@ -970,8 +1129,9 @@ class SyncEngine {
|
|
|
970
1129
|
queueCorrection({ gridOrder: occupying, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
|
|
971
1130
|
continue;
|
|
972
1131
|
}
|
|
973
|
-
// Gap exclusion: nearest slot in SPREAD gap → no adopt
|
|
974
|
-
|
|
1132
|
+
// Gap exclusion: nearest slot in SPREAD gap → no adopt (boundary
|
|
1133
|
+
// is known here — the pre-boundary case continued above).
|
|
1134
|
+
{
|
|
975
1135
|
const inRail = isSlotInRail(boundaryIdx, gapSlots, chainOrder.type, { id: slotId });
|
|
976
1136
|
if (!inRail) {
|
|
977
1137
|
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
|
|
@@ -991,7 +1151,14 @@ class SyncEngine {
|
|
|
991
1151
|
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
|
|
992
1152
|
continue;
|
|
993
1153
|
}
|
|
994
|
-
await adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
|
|
1154
|
+
const adopted = await adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
|
|
1155
|
+
if (!adopted) {
|
|
1156
|
+
// Order update was rejected (fatal validation) — the chain
|
|
1157
|
+
// order stays untracked and must not dangle on the book.
|
|
1158
|
+
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'adoption-rejected', candidateSlotId: slotId });
|
|
1159
|
+
queueCorrection({ gridOrder: slot, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
|
|
1160
|
+
mgr.logger?.log?.(`[SYNC] Chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}) NOT adopted into slot ${slotId}: order update rejected — queued for cancellation`, 'error');
|
|
1161
|
+
}
|
|
995
1162
|
continue;
|
|
996
1163
|
}
|
|
997
1164
|
// Legacy fallback when genesis unavailable (migration)
|
|
@@ -1008,7 +1175,12 @@ class SyncEngine {
|
|
|
1008
1175
|
}
|
|
1009
1176
|
const match = findMatchingGridOrderByOpenOrder({ orderId: chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size }, { orders: mgr.orders, assets: mgr.assets, calcToleranceFn: (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets), logger: mgr.logger, allowSmallerChainSize: true, requireAvailableSlot: true, excludeGridOrderIds: matchedGridOrderIds });
|
|
1010
1177
|
if (match && !matchedGridOrderIds.has(match.id)) {
|
|
1011
|
-
await adoptChainOrderIntoSlot(mgr, match, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
|
|
1178
|
+
const adopted = await adoptChainOrderIntoSlot(mgr, match, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
|
|
1179
|
+
if (!adopted) {
|
|
1180
|
+
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'adoption-rejected', candidateSlotId: match.id });
|
|
1181
|
+
queueCorrection({ gridOrder: match, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
|
|
1182
|
+
mgr.logger?.log?.(`[SYNC] Chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}) NOT adopted into slot ${match.id}: order update rejected — queued for cancellation`, 'error');
|
|
1183
|
+
}
|
|
1012
1184
|
}
|
|
1013
1185
|
else if (match) {
|
|
1014
1186
|
mgr.logger?.log?.(`Warning: Orphan chain order ${chainOrderId} matched grid order ${match.id}, but grid order was already matched to another chain order. Queuing orphan for cancellation.`, 'warn');
|
|
@@ -1049,11 +1221,35 @@ class SyncEngine {
|
|
|
1049
1221
|
rawOnChain: adoptedRaw ? { ...adoptedRaw, fetchedAt: Date.now() } : adoptedRaw,
|
|
1050
1222
|
...(adoptedBtsFeeState ? { btsFeeState: adoptedBtsFeeState } : {}),
|
|
1051
1223
|
};
|
|
1224
|
+
const applied = await mgr._applyOrderUpdate(adoptedOrder, 'sync-pass2-adopt-orphan', { skipAccounting: skipAccounting, fee: 0 });
|
|
1225
|
+
if (applied === false) {
|
|
1226
|
+
// Fatal rejection: parity with the genesis adoption path —
|
|
1227
|
+
// the slot must not be marked matched and the chain order
|
|
1228
|
+
// must not dangle untracked on the book.
|
|
1229
|
+
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'adoption-rejected', candidateSlotId: adoptedSlot.id });
|
|
1230
|
+
queueCorrection({ gridOrder: adoptedSlot, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
|
|
1231
|
+
mgr.logger?.log?.(`[SYNC] Chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}) NOT adopted into slot ${adoptedSlot.id}: order update rejected — queued for cancellation`, 'error');
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1052
1234
|
matchedGridOrderIds.add(adoptedSlot.id);
|
|
1053
1235
|
chainOrderIdsOnGrid.add(chainOrderId);
|
|
1054
|
-
await mgr._applyOrderUpdate(adoptedOrder, 'sync-pass2-adopt-orphan', { skipAccounting: skipAccounting, fee: 0 });
|
|
1055
1236
|
updatedOrders.push(adoptedOrder);
|
|
1056
|
-
|
|
1237
|
+
// Phase 4 attribution: log the adoption slot's geometry
|
|
1238
|
+
// (idx vs frozen boundary/gap) so the next re-map incident
|
|
1239
|
+
// can tell an in-rail adoption from a gap-band re-map
|
|
1240
|
+
// without on-chain archaeology.
|
|
1241
|
+
let adoptGeo = '';
|
|
1242
|
+
try {
|
|
1243
|
+
const adoptIdx = parseSlotIndex(adoptedSlot.id);
|
|
1244
|
+
const adoptB = Number(mgr?.boundaryIdx);
|
|
1245
|
+
const adoptG = Number(mgr?._gapSlots);
|
|
1246
|
+
if (adoptIdx !== null && adoptIdx !== undefined && Number.isFinite(adoptB) && Number.isFinite(adoptG)) {
|
|
1247
|
+
const inBand = isSlotIndexInGapBand(adoptIdx, adoptB, adoptG);
|
|
1248
|
+
adoptGeo = ` geo(idx=${adoptIdx},boundary=${adoptB},gap=${adoptG},sellStart=${getSellStartIdx(adoptB, adoptG)},band=${inBand ? 'gap' : 'rail'})`;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
catch { /* geometry is diagnostic-only */ }
|
|
1252
|
+
mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) adopted into slot ${adoptedSlot.id} (was ${adoptedSlot.type})${adoptGeo}`, 'warn');
|
|
1057
1253
|
}
|
|
1058
1254
|
else {
|
|
1059
1255
|
const precision = (chainOrder.type === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
|