dexbot 1.5.1 → 1.5.3
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 +35 -0
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/resolve_source.ts +3 -0
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/tradingview/README.md +5 -1
- package/analysis/tradingview/analyze_tradingview.ts +27 -1
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +351 -22
- 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/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +3 -0
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +3 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +27 -2
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +348 -22
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/dexbot.js +5 -5
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts +33 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
- package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +2 -18
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +38 -37
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +722 -557
- 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_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +31 -51
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +42 -3
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +19 -2
- 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 +29 -38
- 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 +31 -33
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +13 -6
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +22 -7
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +231 -11
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +38 -15
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +55 -16
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +116 -20
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +340 -35
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +1 -30
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +31 -98
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +3 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +49 -28
- 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 +1 -1
- package/docs/README.md +2 -2
- package/docs/WORKFLOW.md +1 -1
- package/package.json +1 -1
|
@@ -95,7 +95,7 @@
|
|
|
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;
|
|
@@ -106,6 +106,73 @@ import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGrid
|
|
|
106
106
|
import { parseSlotIndex } from './utils/slot.js';
|
|
107
107
|
import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
|
|
108
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
|
+
}
|
|
109
176
|
function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcTolerance, matchedGridOrderIds = null) {
|
|
110
177
|
if (!mgr?.orders || !chainOrder || typeof precision !== 'number')
|
|
111
178
|
return 'candidate diagnostics unavailable';
|
|
@@ -244,8 +311,15 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
|
|
|
244
311
|
else {
|
|
245
312
|
const spreadOrder = convertToSpreadPlaceholder(bestMatch);
|
|
246
313
|
const applied = await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
247
|
-
if (applied === false)
|
|
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);
|
|
248
321
|
return false;
|
|
322
|
+
}
|
|
249
323
|
filledOrders.push({ ...bestMatch });
|
|
250
324
|
updatedOrders.push(spreadOrder);
|
|
251
325
|
chainOrderIdsOnGrid.add(chainOrderId);
|
|
@@ -256,8 +330,12 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
|
|
|
256
330
|
bestMatch.state = ORDER_STATES.PARTIAL;
|
|
257
331
|
}
|
|
258
332
|
const applied = await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
|
|
259
|
-
if (applied === false)
|
|
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);
|
|
260
337
|
return false;
|
|
338
|
+
}
|
|
261
339
|
updatedOrders.push(bestMatch);
|
|
262
340
|
chainOrderIdsOnGrid.add(chainOrderId);
|
|
263
341
|
return true;
|
|
@@ -581,7 +659,8 @@ class SyncEngine {
|
|
|
581
659
|
// ("phantom" resets). Refuse to reconcile on a
|
|
582
660
|
// suspect empty; the guard is self-expiring: after
|
|
583
661
|
// TIMING.SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive empty reads the
|
|
584
|
-
// 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
|
|
585
664
|
// resets the counter.
|
|
586
665
|
if (parsedChainOrders.size === 0) {
|
|
587
666
|
const gridOrderIds = Array.from(mgr.orders.values()).filter((o) => o?.orderId).length;
|
|
@@ -597,7 +676,33 @@ class SyncEngine {
|
|
|
597
676
|
`refusing reconciliation (phantom protection); accepting after ${limit} consecutive empties or next non-empty read`, 'warn');
|
|
598
677
|
return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
|
|
599
678
|
}
|
|
600
|
-
|
|
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');
|
|
601
706
|
mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
|
|
602
707
|
}
|
|
603
708
|
else {
|
|
@@ -843,9 +948,17 @@ class SyncEngine {
|
|
|
843
948
|
&& toFiniteNumber(reboundOrder.rawOnChain?.for_sale, 0) > 0) {
|
|
844
949
|
reboundOrder.state = ORDER_STATES.PARTIAL;
|
|
845
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
|
+
}
|
|
846
960
|
chainOrderIdsOnGrid.delete(gridOrder.orderId);
|
|
847
961
|
chainOrderIdsOnGrid.add(swapMatch.id);
|
|
848
|
-
await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
|
|
849
962
|
updatedOrders.push(reboundOrder);
|
|
850
963
|
mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: tracked order ${gridOrder.orderId} ` +
|
|
851
964
|
`(chain size ${chainOrder.size}) disagrees with booked size ${gridOrder.size}, but ` +
|
|
@@ -869,11 +982,23 @@ class SyncEngine {
|
|
|
869
982
|
updatedOrder.state = (chainSizeInt < currentSizeInt)
|
|
870
983
|
? ORDER_STATES.PARTIAL
|
|
871
984
|
: gridOrder.state;
|
|
872
|
-
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
|
+
}
|
|
873
991
|
}
|
|
874
992
|
else {
|
|
875
993
|
const spreadOrder = convertToSpreadPlaceholder(gridOrder);
|
|
876
|
-
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
|
+
}
|
|
877
1002
|
// Push the filled order with its REAL side (chain order
|
|
878
1003
|
// type), not the SPREAD placeholder: downstream fill
|
|
879
1004
|
// processing (deriveTargetBoundary) derives boundary
|
|
@@ -927,7 +1052,14 @@ class SyncEngine {
|
|
|
927
1052
|
const currentGridOrder = mgr.orders.get(gridOrder.id) || gridOrder;
|
|
928
1053
|
const hadOrderId = Boolean(currentGridOrder?.orderId);
|
|
929
1054
|
const spreadOrder = convertToSpreadPlaceholder(currentGridOrder);
|
|
930
|
-
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
|
+
}
|
|
931
1063
|
// Only genuine disappearances (had orderId) count as fills.
|
|
932
1064
|
if (hadOrderId) {
|
|
933
1065
|
// A SPREAD slot can carry an on-chain order (spread-correction
|
|
@@ -975,6 +1107,19 @@ class SyncEngine {
|
|
|
975
1107
|
const slotId = `slot-${idx}`;
|
|
976
1108
|
const gapSlots = genesis.gapSlots ?? mgr._gapSlots ?? 0;
|
|
977
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
|
+
}
|
|
978
1123
|
// Duplicate-price guard becomes slotId equality: if placed order already occupies this slot
|
|
979
1124
|
const occupying = mgr.orders.get(slotId);
|
|
980
1125
|
if (occupying && isOrderPlaced(occupying) && occupying.type === chainOrder.type) {
|
|
@@ -984,8 +1129,9 @@ class SyncEngine {
|
|
|
984
1129
|
queueCorrection({ gridOrder: occupying, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
|
|
985
1130
|
continue;
|
|
986
1131
|
}
|
|
987
|
-
// Gap exclusion: nearest slot in SPREAD gap → no adopt
|
|
988
|
-
|
|
1132
|
+
// Gap exclusion: nearest slot in SPREAD gap → no adopt (boundary
|
|
1133
|
+
// is known here — the pre-boundary case continued above).
|
|
1134
|
+
{
|
|
989
1135
|
const inRail = isSlotInRail(boundaryIdx, gapSlots, chainOrder.type, { id: slotId });
|
|
990
1136
|
if (!inRail) {
|
|
991
1137
|
unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
|
|
@@ -1799,6 +1945,11 @@ class SyncEngine {
|
|
|
1799
1945
|
switch (source) {
|
|
1800
1946
|
case 'createOrder': {
|
|
1801
1947
|
const { gridOrderId, chainOrderId, isPartialPlacement, expectedType, fee } = chainData;
|
|
1948
|
+
// Optional placement descriptor for unknown-id recovery. Callers
|
|
1949
|
+
// pass the placed slot's price/size/type so a grid reset racing
|
|
1950
|
+
// the broadcast can't strand the live chain order untracked.
|
|
1951
|
+
// Callers that omit it take the no-descriptor error path.
|
|
1952
|
+
const placedDescriptor = chainData.order ?? null;
|
|
1802
1953
|
const runCreate = async () => {
|
|
1803
1954
|
// Lock order to prevent concurrent modifications during state transition
|
|
1804
1955
|
mgr.lockOrders([gridOrderId]);
|
|
@@ -1861,6 +2012,68 @@ class SyncEngine {
|
|
|
1861
2012
|
fee: actualFee
|
|
1862
2013
|
});
|
|
1863
2014
|
}
|
|
2015
|
+
else {
|
|
2016
|
+
// Unknown grid id: the broadcast landed but master no
|
|
2017
|
+
// longer holds the slot (grid reset/regen raced the
|
|
2018
|
+
// CREATE, or a first placement for an id never
|
|
2019
|
+
// projected via projectTargetToWorkingGrid). The old
|
|
2020
|
+
// code dropped the linkage silently, so the next
|
|
2021
|
+
// cycle re-placed the same level (duplicate CREATEs,
|
|
2022
|
+
// funds locked xN). Materialize-or-error: rebind when
|
|
2023
|
+
// the chain id is already tracked, materialize the
|
|
2024
|
+
// slot when the caller supplied its descriptor, and
|
|
2025
|
+
// log an error otherwise so the order never vanishes
|
|
2026
|
+
// quietly again.
|
|
2027
|
+
const rebound = Array.from(mgr.orders.values()).find((o) => o.orderId === chainOrderId);
|
|
2028
|
+
if (rebound) {
|
|
2029
|
+
mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: chain order ${chainOrderId} already tracked on ${rebound.id} — linkage up to date, skipping`, 'warn');
|
|
2030
|
+
}
|
|
2031
|
+
else {
|
|
2032
|
+
const rawType = placedDescriptor?.type ?? expectedType;
|
|
2033
|
+
let materializeType = (rawType === ORDER_TYPES.BUY || rawType === ORDER_TYPES.SELL)
|
|
2034
|
+
? rawType
|
|
2035
|
+
: null;
|
|
2036
|
+
const descriptorPrice = toFiniteNumber(placedDescriptor?.price, NaN);
|
|
2037
|
+
if (!materializeType && Number.isFinite(descriptorPrice)) {
|
|
2038
|
+
materializeType = resolveSpreadOrderSide(descriptorPrice, mgr.config.startPrice);
|
|
2039
|
+
}
|
|
2040
|
+
const descriptorSize = toFiniteNumber(placedDescriptor?.size, NaN);
|
|
2041
|
+
const hasDescriptor = !!materializeType
|
|
2042
|
+
&& Number.isFinite(descriptorPrice) && descriptorPrice > 0
|
|
2043
|
+
&& Number.isFinite(descriptorSize) && descriptorSize > 0;
|
|
2044
|
+
if (!hasDescriptor) {
|
|
2045
|
+
mgr.logger?.log?.(`[SYNC] createOrder linkage LOST: grid order ${gridOrderId} not in master — chain order ${chainOrderId} placed but untracked (no placement descriptor; next readOpenOrders sync must adopt it as an orphan before the slot re-places)`, 'error');
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
// Minimal slot shape: the raw chain object
|
|
2049
|
+
// isn't available here, so rawOnChain and
|
|
2050
|
+
// slot-scheme metadata are absent. The next
|
|
2051
|
+
// readOpenOrders pass repopulates rawOnChain
|
|
2052
|
+
// from the live order (slot-scheme ids only).
|
|
2053
|
+
const materializedOrder = {
|
|
2054
|
+
id: gridOrderId,
|
|
2055
|
+
type: materializeType,
|
|
2056
|
+
price: descriptorPrice,
|
|
2057
|
+
size: descriptorSize,
|
|
2058
|
+
state: isPartialPlacement ? ORDER_STATES.PARTIAL : ORDER_STATES.ACTIVE,
|
|
2059
|
+
orderId: chainOrderId,
|
|
2060
|
+
};
|
|
2061
|
+
if (chainData.deferredFee !== undefined && chainData.deferredFee !== null) {
|
|
2062
|
+
materializedOrder.btsFeeState = { deferredFee: Math.max(0, chainData.deferredFee) };
|
|
2063
|
+
}
|
|
2064
|
+
const materialized = await mgr._applyOrderUpdate(materializedOrder, 'createOrder-unknown-id-adopt', {
|
|
2065
|
+
skipAccounting: chainData.skipAccounting || false,
|
|
2066
|
+
fee: fee,
|
|
2067
|
+
});
|
|
2068
|
+
if (materialized === false) {
|
|
2069
|
+
mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${descriptorPrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${descriptorPrice} x${descriptorSize} -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1864
2077
|
}
|
|
1865
2078
|
finally {
|
|
1866
2079
|
mgr.unlockOrders([gridOrderId]);
|
|
@@ -1912,6 +2125,13 @@ class SyncEngine {
|
|
|
1912
2125
|
fee: btsFeeData?.cancelFee || 0
|
|
1913
2126
|
});
|
|
1914
2127
|
}
|
|
2128
|
+
else {
|
|
2129
|
+
// Slot vanished or was rebound between lookup and
|
|
2130
|
+
// lock — never skip quietly: the next
|
|
2131
|
+
// readOpenOrders sync reconciles the slot, but the
|
|
2132
|
+
// race must be visible when it happens.
|
|
2133
|
+
mgr.logger?.log?.(`[SYNC] cancelOrder for ${orderId}: slot ${gridOrder.id} changed underneath (missing or rebound to ${currentGridOrder?.orderId ?? 'none'}) — linkage skipped, next sync reconciles`, 'warn');
|
|
2134
|
+
}
|
|
1915
2135
|
}
|
|
1916
2136
|
finally {
|
|
1917
2137
|
mgr.unlockOrders(orderIds);
|