dexbot 1.4.0 → 1.4.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/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +20 -11
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +8 -13
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +170 -60
- 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 +59 -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 +14 -1
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.js +1 -1
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +29 -1
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +83 -1
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +14 -4
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +93 -8
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/package.json +1 -1
|
@@ -499,7 +499,7 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
499
499
|
return;
|
|
500
500
|
return await manager._gridLock.acquire(async () => {
|
|
501
501
|
try {
|
|
502
|
-
await manager._initializeAssets();
|
|
502
|
+
await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
|
|
503
503
|
}
|
|
504
504
|
catch (e) {
|
|
505
505
|
manager.logger?.log?.(`Asset initialization failed during grid load: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
|
|
@@ -596,7 +596,12 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
596
596
|
async function initializeGrid(manager) {
|
|
597
597
|
if (!manager)
|
|
598
598
|
throw new Error('initializeGrid requires a manager instance');
|
|
599
|
-
|
|
599
|
+
try {
|
|
600
|
+
await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
manager.logger?.log?.(`Asset initialization failed during grid init: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
|
|
604
|
+
}
|
|
600
605
|
// FIX: Add explicit state validation to prevent cryptic errors later
|
|
601
606
|
if (!manager.assets || !manager.assets.assetA || !manager.assets.assetB) {
|
|
602
607
|
throw new Error('Asset initialization did not complete properly - assetA or assetB undefined');
|
|
@@ -867,49 +872,82 @@ async function recalculateGrid(manager, opts) {
|
|
|
867
872
|
const { readOpenOrdersFn, chainOrders, account, privateKey } = opts;
|
|
868
873
|
// Suppress invariant warnings during full resync
|
|
869
874
|
manager.startBootstrap();
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const READ_OPEN_ORDERS_TIMEOUT_MS = 30000;
|
|
876
|
-
let _readOpenOrdersTimer;
|
|
877
|
-
let chainOpenOrders;
|
|
875
|
+
// Total timeout across all steps — prevents indefinite hang even if
|
|
876
|
+
// an individual withBlockchainRetry step pins the event loop.
|
|
877
|
+
const totalTimeoutMs = constants_1.PIPELINE_TIMING.TIMEOUT_MS * 2; // 10 min
|
|
878
|
+
let _resyncAborted = false;
|
|
879
|
+
const work = (async () => {
|
|
878
880
|
try {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
881
|
+
manager.logger?.log?.('Starting full resync...', 'info');
|
|
882
|
+
if (_resyncAborted)
|
|
883
|
+
return;
|
|
884
|
+
// #1: Initialize assets with timeout + retry + node failover
|
|
885
|
+
try {
|
|
886
|
+
await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
manager.logger?.log?.(`Asset initialization failed during resync: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
|
|
890
|
+
}
|
|
891
|
+
if (_resyncAborted)
|
|
892
|
+
return;
|
|
893
|
+
// #2: Fetch account totals with timeout + retry + node failover
|
|
894
|
+
await (0, system_1.withBlockchainRetry)(() => manager.fetchAccountTotals(), 'fetchAccountTotals', { logger: manager.logger });
|
|
895
|
+
if (_resyncAborted)
|
|
896
|
+
return;
|
|
897
|
+
// #3: Read open orders with timeout + retry + node failover
|
|
898
|
+
const chainOpenOrders = await (0, system_1.withBlockchainRetry)(() => readOpenOrdersFn(), 'readOpenOrders', { logger: manager.logger });
|
|
899
|
+
if (_resyncAborted)
|
|
900
|
+
return;
|
|
901
|
+
if (!Array.isArray(chainOpenOrders))
|
|
902
|
+
return;
|
|
903
|
+
await (0, system_1.withBlockchainRetry)(() => manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true }), 'syncFromOpenOrders', { logger: manager.logger });
|
|
904
|
+
if (_resyncAborted)
|
|
905
|
+
return;
|
|
906
|
+
manager.resetFunds();
|
|
907
|
+
if (_resyncAborted)
|
|
908
|
+
return;
|
|
909
|
+
await manager.persistGrid();
|
|
910
|
+
if (_resyncAborted)
|
|
911
|
+
return;
|
|
912
|
+
await initializeGrid(manager);
|
|
913
|
+
if (_resyncAborted)
|
|
914
|
+
return;
|
|
915
|
+
const { reconcileGridOrders } = require('./grid_reconcile');
|
|
916
|
+
// #5: Reconcile grid orders with timeout + retry + node failover
|
|
917
|
+
try {
|
|
918
|
+
await (0, system_1.withBlockchainRetry)(() => reconcileGridOrders({ manager, config: manager.config, account, privateKey, chainOrders, chainOpenOrders }), 'reconcileGridOrders',
|
|
919
|
+
// 5 min: Phase 2 of reconcile does sequential creates (~3s each);
|
|
920
|
+
// the default 30s timeout would kill mid-batch and cause duplicate-
|
|
921
|
+
// accumulation death spirals. PIPELINE_TIMING.TIMEOUT_MS gives enough
|
|
922
|
+
// headroom for all pending creates+updates to finish in one shot.
|
|
923
|
+
{ logger: manager.logger, timeoutMs: constants_1.PIPELINE_TIMING.TIMEOUT_MS });
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
manager.logger?.log?.(`Error during startup order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
927
|
+
throw new Error(`Grid recalculation failed during order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
928
|
+
}
|
|
929
|
+
if (_resyncAborted)
|
|
930
|
+
return;
|
|
931
|
+
manager.logger?.log?.('Full resync complete.', 'info');
|
|
885
932
|
}
|
|
886
933
|
finally {
|
|
887
|
-
|
|
934
|
+
manager.finishBootstrap();
|
|
888
935
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
manager.logger?.log?.(`Error during startup order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
905
|
-
throw new Error(`Grid recalculation failed during order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
906
|
-
}
|
|
907
|
-
// FIX: Use consistent optional chaining pattern for logger calls
|
|
908
|
-
manager.logger?.log?.('Full resync complete.', 'info');
|
|
909
|
-
}
|
|
910
|
-
finally {
|
|
911
|
-
manager.finishBootstrap();
|
|
912
|
-
}
|
|
936
|
+
})();
|
|
937
|
+
// Swallow late rejection if timeout wins the race
|
|
938
|
+
Promise.resolve(work).catch(() => { });
|
|
939
|
+
let timeoutId;
|
|
940
|
+
const result = await Promise.race([
|
|
941
|
+
work,
|
|
942
|
+
new Promise((_, reject) => {
|
|
943
|
+
timeoutId = setTimeout(() => {
|
|
944
|
+
_resyncAborted = true;
|
|
945
|
+
reject(new Error(`recalculateGrid timed out after ${totalTimeoutMs}ms`));
|
|
946
|
+
}, totalTimeoutMs);
|
|
947
|
+
})
|
|
948
|
+
]);
|
|
949
|
+
clearTimeout(timeoutId);
|
|
950
|
+
return result;
|
|
913
951
|
}
|
|
914
952
|
/**
|
|
915
953
|
* Check for grid divergence and trigger update if threshold is met.
|
|
@@ -1465,8 +1503,21 @@ async function checkSpreadCondition(manager, _BitShares, updateOrdersOnChainBatc
|
|
|
1465
1503
|
correction = await prepareSpreadCorrectionOrders(manager, decision.side);
|
|
1466
1504
|
if (!correction)
|
|
1467
1505
|
return false;
|
|
1468
|
-
|
|
1469
|
-
|
|
1506
|
+
let placeCount = correction.ordersToPlace?.length || 0;
|
|
1507
|
+
let updateCount = correction.ordersToUpdate?.length || 0;
|
|
1508
|
+
// STARVATION FALLBACK: If the selected side has no correctable slots (e.g.
|
|
1509
|
+
// all SPREAD slots already filled or misaligned), try the opposite side.
|
|
1510
|
+
if ((placeCount + updateCount) === 0) {
|
|
1511
|
+
const oppositeSide = decision.side === constants_1.ORDER_TYPES.BUY ? constants_1.ORDER_TYPES.SELL : constants_1.ORDER_TYPES.BUY;
|
|
1512
|
+
manager.logger?.log?.(`[SPREAD] Side ${decision.side} produced zero candidates; ` +
|
|
1513
|
+
`trying opposite side ${oppositeSide}.`, 'debug');
|
|
1514
|
+
const oppositeCorrection = await prepareSpreadCorrectionOrders(manager, oppositeSide);
|
|
1515
|
+
if (oppositeCorrection) {
|
|
1516
|
+
correction = oppositeCorrection;
|
|
1517
|
+
placeCount = correction.ordersToPlace?.length || 0;
|
|
1518
|
+
updateCount = correction.ordersToUpdate?.length || 0;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1470
1521
|
// Capture fund snapshot under lock for pre-flight verification before broadcast
|
|
1471
1522
|
fundSnapshot = _snapshotFundState(manager);
|
|
1472
1523
|
return (placeCount + updateCount) > 0;
|
|
@@ -1492,22 +1543,32 @@ async function checkSpreadCondition(manager, _BitShares, updateOrdersOnChainBatc
|
|
|
1492
1543
|
// Instead of silently aborting, re-plan with fresh funds so the
|
|
1493
1544
|
// correction still applies on this cycle. The pre-flight check
|
|
1494
1545
|
// still guards against placing orders based on stale fund snapshots.
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1546
|
+
// Re-acquire _gridLock for the re-plan to ensure consistent grid
|
|
1547
|
+
// state (the lock is re-entrant for this call chain — the outer
|
|
1548
|
+
// acquire's callback completed before we reach here, so there is
|
|
1549
|
+
// no nested lock to recurse into). If determineOrderSideByFunds
|
|
1550
|
+
// or prepareSpreadCorrectionOrders grow to hold the lock for
|
|
1551
|
+
// heavy work, hoist the result to avoid serial re-execution.
|
|
1552
|
+
const rePlanResult = await manager._gridLock.acquire(async () => {
|
|
1553
|
+
const decision = determineOrderSideByFunds(manager, lastPrice);
|
|
1554
|
+
if (!decision.side)
|
|
1555
|
+
return { side: false };
|
|
1556
|
+
const c = await prepareSpreadCorrectionOrders(manager, decision.side);
|
|
1557
|
+
return { side: true, correction: c };
|
|
1558
|
+
});
|
|
1559
|
+
if (!rePlanResult.side) {
|
|
1560
|
+
manager.logger?.log?.(`[SPREAD] Fund state changed; no side has sufficient funds for re-plan. Skipping cycle.`, 'warn');
|
|
1561
|
+
return { ordersPlaced: 0, partialsMoved: 0 };
|
|
1562
|
+
}
|
|
1563
|
+
correction = rePlanResult.correction;
|
|
1564
|
+
if (correction && ((correction.ordersToPlace?.length || 0) + (correction.ordersToUpdate?.length || 0) > 0)) {
|
|
1565
|
+
fundSnapshot = currentFunds;
|
|
1566
|
+
manager.logger?.log?.(`[SPREAD] Fund state changed between lock release and broadcast — ` +
|
|
1567
|
+
`re-planned with updated funds: ${correction.ordersToPlace?.length || 0} creates, ` +
|
|
1568
|
+
`${correction.ordersToUpdate?.length || 0} updates`, 'info');
|
|
1508
1569
|
}
|
|
1509
1570
|
else {
|
|
1510
|
-
manager.logger?.log?.(`[SPREAD] Fund state changed;
|
|
1571
|
+
manager.logger?.log?.(`[SPREAD] Fund state changed; re-plan produced no viable orders. Skipping cycle.`, 'warn');
|
|
1511
1572
|
return { ordersPlaced: 0, partialsMoved: 0 };
|
|
1512
1573
|
}
|
|
1513
1574
|
}
|
|
@@ -1825,16 +1886,65 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide) {
|
|
|
1825
1886
|
edgePartial = partials[0];
|
|
1826
1887
|
manager.logger?.log?.(`[SPREAD-CORRECTION] Identified partial order at ${edgePartial.price} for update`, 'debug');
|
|
1827
1888
|
}
|
|
1828
|
-
//
|
|
1889
|
+
// Boundary-correct type computation. Used by both candidate pools below to ensure
|
|
1890
|
+
// spread correction does not re-activate slots whose current boundary position
|
|
1891
|
+
// places them on the wrong side or in the spread zone — doing so would compound
|
|
1892
|
+
// inventory at prices where the bot already traded.
|
|
1893
|
+
//
|
|
1894
|
+
// The natural type of a slot is derived from its position in the price-sorted rail
|
|
1895
|
+
// relative to boundaryIdx + gapSlots: indices in [0, boundaryIdx] are BUY, indices
|
|
1896
|
+
// in [boundaryIdx + gapSlots + 1, N-1] are SELL, the middle band is SPREAD.
|
|
1897
|
+
const allSlotsByPrice = allOrders
|
|
1898
|
+
.filter((o) => o.price != null && Number.isFinite(o.price))
|
|
1899
|
+
.sort((a, b) => a.price - b.price);
|
|
1900
|
+
const slotIndexMap = new Map(allSlotsByPrice.map((o, i) => [o.id, i]));
|
|
1901
|
+
const gapSlots = calculateGapSlots(manager.config?.incrementPercent, manager.config?.targetSpreadPercent, manager.config?.gridLimits);
|
|
1902
|
+
// Sync boundary from current fund state to avoid stale boundaryIdx causing
|
|
1903
|
+
// getSlotCorrectType to misclassify slots (e.g. after fills shifted the
|
|
1904
|
+
// boundary but the COW commit hasn't updated it yet). This is safe under
|
|
1905
|
+
// the grid lock — no concurrent modifications can race with this read.
|
|
1906
|
+
const boundarySync = (0, system_1.syncBoundaryToFunds)(manager);
|
|
1907
|
+
if (boundarySync.changed && boundarySync.newIdx !== undefined) {
|
|
1908
|
+
manager.boundaryIdx = boundarySync.newIdx;
|
|
1909
|
+
}
|
|
1910
|
+
const bIdx = manager.boundaryIdx ?? 0;
|
|
1911
|
+
const buyEndIdx = bIdx;
|
|
1912
|
+
const sellStartIdx = bIdx + Number(gapSlots) + 1;
|
|
1913
|
+
const getSlotCorrectType = (slot) => {
|
|
1914
|
+
const idx = slotIndexMap.get(slot.id);
|
|
1915
|
+
if (idx === undefined)
|
|
1916
|
+
return slot.type;
|
|
1917
|
+
if (idx <= buyEndIdx)
|
|
1918
|
+
return constants_1.ORDER_TYPES.BUY;
|
|
1919
|
+
if (idx >= sellStartIdx)
|
|
1920
|
+
return constants_1.ORDER_TYPES.SELL;
|
|
1921
|
+
return constants_1.ORDER_TYPES.SPREAD;
|
|
1922
|
+
};
|
|
1923
|
+
// Primary candidates: SPREAD-type slots adjacent to the gap. Filter by
|
|
1924
|
+
// boundary-correct type so a SPREAD slot that, after a boundary shift, now sits
|
|
1925
|
+
// in the BUY or SELL zone is excluded — it would otherwise be placed on the
|
|
1926
|
+
// correction side at a price the grid already considers the opposite side.
|
|
1829
1927
|
const typedSpreadCandidates = allOrders
|
|
1830
|
-
.filter((o) => o.type === constants_1.ORDER_TYPES.SPREAD
|
|
1928
|
+
.filter((o) => o.type === constants_1.ORDER_TYPES.SPREAD
|
|
1929
|
+
&& (0, order_1.isSlotAvailable)(o)
|
|
1930
|
+
&& getSlotCorrectType(o) === railType)
|
|
1831
1931
|
.sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
|
|
1832
1932
|
.slice(0, missingSlots);
|
|
1833
1933
|
// Secondary candidates: orphaned virtual slots of the correct side-type that have
|
|
1834
1934
|
// lost their order (e.g. stale-cleaned after a race condition during a crash).
|
|
1835
1935
|
// These sit inside the active window and are invisible to the SPREAD-type filter above.
|
|
1936
|
+
//
|
|
1937
|
+
// IMPORTANT: Filter by boundary-correct type so that filled-then-virtualized slots
|
|
1938
|
+
// whose boundary position has moved into the spread or opposite zone are NOT
|
|
1939
|
+
// re-activated on the stale side — doing so would compound inventory at prices
|
|
1940
|
+
// where the bot already traded. The boundary-correct type is computed from the
|
|
1941
|
+
// current boundary index and the slot's price position in the sorted rail.
|
|
1836
1942
|
const orphanedVirtualCandidates = allOrders
|
|
1837
|
-
.filter((o) => o.type === railType
|
|
1943
|
+
.filter((o) => o.type === railType
|
|
1944
|
+
&& o.state === constants_1.ORDER_STATES.VIRTUAL
|
|
1945
|
+
&& !o.orderId
|
|
1946
|
+
&& Number(o.size || 0) === 0
|
|
1947
|
+
&& getSlotCorrectType(o) === railType)
|
|
1838
1948
|
.sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
|
|
1839
1949
|
.slice(0, missingSlots);
|
|
1840
1950
|
// Merge: prefer orphaned virtuals (they already occupy correct grid positions) then
|