dexbot 1.2.5 → 1.2.7

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.
Files changed (70) hide show
  1. package/README.md +30 -27
  2. package/dist/dexbot.d.ts +22 -22
  3. package/dist/dexbot.js +29 -29
  4. package/dist/dexbot.js.map +1 -1
  5. package/dist/modules/account_orders.d.ts +8 -1
  6. package/dist/modules/account_orders.d.ts.map +1 -1
  7. package/dist/modules/account_orders.js +24 -1
  8. package/dist/modules/account_orders.js.map +1 -1
  9. package/dist/modules/chain_orders.d.ts +16 -0
  10. package/dist/modules/chain_orders.d.ts.map +1 -1
  11. package/dist/modules/chain_orders.js +41 -0
  12. package/dist/modules/chain_orders.js.map +1 -1
  13. package/dist/modules/constants.d.ts +0 -3
  14. package/dist/modules/constants.d.ts.map +1 -1
  15. package/dist/modules/constants.js +0 -14
  16. package/dist/modules/constants.js.map +1 -1
  17. package/dist/modules/dexbot_class.d.ts +10 -2
  18. package/dist/modules/dexbot_class.d.ts.map +1 -1
  19. package/dist/modules/dexbot_class.js +176 -57
  20. package/dist/modules/dexbot_class.js.map +1 -1
  21. package/dist/modules/dexbot_fill_runtime.d.ts +2 -2
  22. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  23. package/dist/modules/dexbot_fill_runtime.js +3 -3
  24. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  25. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  26. package/dist/modules/dexbot_maintenance_runtime.js +8 -9
  27. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  28. package/dist/modules/node_manager.d.ts.map +1 -1
  29. package/dist/modules/node_manager.js +7 -0
  30. package/dist/modules/node_manager.js.map +1 -1
  31. package/dist/modules/order/accounting.d.ts +2 -18
  32. package/dist/modules/order/accounting.d.ts.map +1 -1
  33. package/dist/modules/order/accounting.js +22 -49
  34. package/dist/modules/order/accounting.js.map +1 -1
  35. package/dist/modules/order/async_lock.d.ts +10 -0
  36. package/dist/modules/order/async_lock.d.ts.map +1 -1
  37. package/dist/modules/order/async_lock.js +60 -5
  38. package/dist/modules/order/async_lock.js.map +1 -1
  39. package/dist/modules/order/grid.js +2 -2
  40. package/dist/modules/order/grid.js.map +1 -1
  41. package/dist/modules/order/manager.d.ts +8 -1
  42. package/dist/modules/order/manager.d.ts.map +1 -1
  43. package/dist/modules/order/manager.js +28 -14
  44. package/dist/modules/order/manager.js.map +1 -1
  45. package/dist/modules/order/strategy.d.ts +2 -49
  46. package/dist/modules/order/strategy.d.ts.map +1 -1
  47. package/dist/modules/order/strategy.js +4 -132
  48. package/dist/modules/order/strategy.js.map +1 -1
  49. package/dist/modules/order/sync_engine.d.ts +53 -6
  50. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  51. package/dist/modules/order/sync_engine.js +384 -204
  52. package/dist/modules/order/sync_engine.js.map +1 -1
  53. package/dist/modules/order/utils/math.d.ts.map +1 -1
  54. package/dist/modules/order/utils/math.js +8 -3
  55. package/dist/modules/order/utils/math.js.map +1 -1
  56. package/dist/modules/order/utils/order.js +1 -1
  57. package/dist/modules/order/utils/order.js.map +1 -1
  58. package/dist/modules/order/utils/system.d.ts +1 -1
  59. package/dist/modules/order/utils/system.d.ts.map +1 -1
  60. package/dist/modules/order/utils/system.js +3 -2
  61. package/dist/modules/order/utils/system.js.map +1 -1
  62. package/dist/modules/runtime_settings.d.ts +3 -0
  63. package/dist/modules/runtime_settings.d.ts.map +1 -1
  64. package/dist/modules/runtime_settings.js +34 -1
  65. package/dist/modules/runtime_settings.js.map +1 -1
  66. package/dist/modules/types.d.ts +29 -4
  67. package/dist/modules/types.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/scripts/README.md +9 -9
  70. package/scripts/reset-settings.sh +1 -1
@@ -111,7 +111,6 @@ class DEXBot {
111
111
  _staleCleanupRetentionMs;
112
112
  _metrics;
113
113
  _shuttingDown;
114
- _shutdownStarted;
115
114
  _shutdownPromise;
116
115
  _blockchainFetchInterval;
117
116
  _blockchainFetchInFlight;
@@ -166,6 +165,8 @@ class DEXBot {
166
165
  this.config.feeParams = rs.feeParams;
167
166
  this.config.incrementBounds = rs.incrementBounds;
168
167
  this.config.timing = rs.timing;
168
+ this.config.fillProcessing = rs.fillProcessing;
169
+ this.config.pipelineTiming = rs.pipelineTiming;
169
170
  this.config.logging = rs.logging;
170
171
  this._fillDedupeWindowMs = this.config.timing.FILL_DEDUPE_WINDOW_MS;
171
172
  this._fillRecordRetentionMs = this.config.timing.FILL_RECORD_RETENTION_MS;
@@ -200,7 +201,6 @@ class DEXBot {
200
201
  };
201
202
  // Shutdown state
202
203
  this._shuttingDown = false;
203
- this._shutdownStarted = false;
204
204
  this._shutdownPromise = null;
205
205
  // Runtime handles for graceful lifecycle management
206
206
  this._blockchainFetchInterval = null;
@@ -322,6 +322,7 @@ class DEXBot {
322
322
  * @private
323
323
  */
324
324
  async _persistAndRecoverIfNeeded() {
325
+ this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
325
326
  const validation = await this.manager.persistGrid();
326
327
  if (!validation.isValid) {
327
328
  this._warn(`Startup validation failed: ${validation.reason}. Triggering immediate recovery...`);
@@ -329,6 +330,7 @@ class DEXBot {
329
330
  const recoveryValidation = await this.manager.accountant._performStateRecovery(this.manager);
330
331
  if (recoveryValidation.isValid) {
331
332
  this._log(`✓ Startup recovery successful. Persistent state restored.`);
333
+ this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
332
334
  await this.manager.persistGrid();
333
335
  }
334
336
  else {
@@ -336,6 +338,37 @@ class DEXBot {
336
338
  }
337
339
  }
338
340
  }
341
+ /**
342
+ * Snapshot the recently queued fill keys as a plain object for crash-durable persistence.
343
+ * Merges with any existing snapshot to avoid losing keys that were evicted from the
344
+ * in-memory map between persist cycles but remain within the dedup window.
345
+ * Only includes entries within the dedup window to avoid writing stale keys.
346
+ * @returns {Record<string, number>}
347
+ */
348
+ _getRecentFillKeysSnapshot() {
349
+ const snapshot = {};
350
+ const now = Date.now();
351
+ // 1. Collect live keys from the in-memory map
352
+ for (const [key, timestamp] of this._recentlyQueuedFills) {
353
+ if (now - Number(timestamp) < this._fillDedupeWindowMs) {
354
+ snapshot[key] = Number(timestamp);
355
+ }
356
+ }
357
+ // 2. Merge with the previous snapshot — carry forward any keys that
358
+ // were present in a prior persist cycle, are still within the
359
+ // dedup window, but have been evicted from _recentlyQueuedFills
360
+ // (e.g. by TTL-based pruning between two persistGrid calls).
361
+ // This prevents a crash + restart from losing keys that were
362
+ // processed but not yet persisted.
363
+ if (this.manager?._recentFillKeysSnapshot) {
364
+ for (const [key, timestamp] of Object.entries(this.manager._recentFillKeysSnapshot)) {
365
+ if (!(key in snapshot) && now - Number(timestamp) < this._fillDedupeWindowMs) {
366
+ snapshot[key] = Number(timestamp);
367
+ }
368
+ }
369
+ }
370
+ return snapshot;
371
+ }
339
372
  /**
340
373
  * Get current pipeline signal state for congestion checks.
341
374
  * @returns {{incomingFillQueueLength: number, shadowLocks: number, batchInFlight: boolean, recoveryInFlight: boolean, broadcasting: boolean}}
@@ -676,8 +709,6 @@ class DEXBot {
676
709
  * @returns {Promise<boolean>} True if all affected orders were repaired
677
710
  */
678
711
  async _targetedOrderRepair(orderIds) {
679
- const { BitShares } = require('./bitshares_client');
680
- const { virtualizeOrder } = require('./order/utils/order');
681
712
  try {
682
713
  const objects = await BitShares.db.get_objects(orderIds);
683
714
  if (!Array.isArray(objects) || objects.length !== orderIds.length)
@@ -801,11 +832,13 @@ class DEXBot {
801
832
  const persistedBtsFeesOwed = this.accountOrders.loadBtsFeesOwed();
802
833
  const persistedBoundaryIdx = this.accountOrders.loadBoundaryIdx();
803
834
  const persistedBtsBalance = this.accountOrders.loadBtsBalance();
835
+ const persistedRecentFillKeys = this.accountOrders.loadRecentFillKeys();
804
836
  return {
805
837
  persistedGrid: repairedGrid,
806
838
  persistedBtsFeesOwed,
807
839
  persistedBoundaryIdx,
808
840
  persistedBtsBalance,
841
+ persistedRecentFillKeys,
809
842
  };
810
843
  }
811
844
  finally {
@@ -930,10 +963,10 @@ class DEXBot {
930
963
  * @param {string} [options.context]
931
964
  * @param {Object} [options.logger]
932
965
  * @param {string} [options.replayMessage]
933
- * @param {string} [options.persistenceMode='immediate']
966
+ * @param {string} [options.persistenceMode='batched']
934
967
  * @returns {Promise<import('./types').ReplaySafeFillResult>}
935
968
  */
936
- async _applyReplaySafeOrphanFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = PROCESSED_FILL_PERSISTENCE_MODES.IMMEDIATE } = {}) {
969
+ async _applyReplaySafeOrphanFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = PROCESSED_FILL_PERSISTENCE_MODES.BATCHED } = {}) {
937
970
  return DexbotFillRuntime.applyReplaySafeOrphanFillAccounting(this, fill, fillOp, {
938
971
  context,
939
972
  logger,
@@ -956,7 +989,7 @@ class DEXBot {
956
989
  * @private
957
990
  */
958
991
  async _finishStartupSequence(startupState) {
959
- let { persistedGrid, persistedBtsFeesOwed, persistedBoundaryIdx, persistedBtsBalance, } = startupState;
992
+ let { persistedGrid, persistedBtsFeesOwed, persistedBoundaryIdx, persistedBtsBalance, persistedRecentFillKeys, } = startupState;
960
993
  try {
961
994
  // CRITICAL: Activate fill listener EARLY - before ANY operations that place orders
962
995
  // This ensures fills during trigger reset and grid initialization are captured
@@ -993,11 +1026,13 @@ class DEXBot {
993
1026
  if (this._shuttingDown)
994
1027
  return;
995
1028
  if (syncResult?.filledOrders?.length > 0) {
1029
+ this._refreshDynamicWeightDistribution('post-reconnect sync fill');
996
1030
  this._log(`Post-reconnect sync: ${syncResult.filledOrders.length} grid order(s) found filled.`, 'info');
997
1031
  await this._processFillsWithBatching(syncResult.filledOrders, new Set(), 'post-reconnect sync fill');
998
1032
  if (this._shuttingDown)
999
1033
  return;
1000
1034
  }
1035
+ this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
1001
1036
  await this.manager.persistGrid();
1002
1037
  // Cancel any dust created by reconnect fills immediately.
1003
1038
  if (!this._shuttingDown) {
@@ -1139,6 +1174,7 @@ class DEXBot {
1139
1174
  }
1140
1175
  }
1141
1176
  await this._flushProcessedFillPersistence('post-reset-batch');
1177
+ this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
1142
1178
  await this.manager.persistGrid();
1143
1179
  }
1144
1180
  // STEP 2: Refresh chain truth before spread correction. Trigger
@@ -1205,6 +1241,15 @@ class DEXBot {
1205
1241
  };
1206
1242
  }
1207
1243
  }
1244
+ // Restore recently queued fill keys for crash-durable dedup window.
1245
+ // All persisted keys are restored; the in-memory TTL (_fillDedupeWindowMs)
1246
+ // will naturally evict any that are already expired on the next fill cycle.
1247
+ if (persistedRecentFillKeys && typeof persistedRecentFillKeys === 'object') {
1248
+ for (const [fillKey, timestamp] of Object.entries(persistedRecentFillKeys)) {
1249
+ this._recentlyQueuedFills.set(fillKey, Number(timestamp));
1250
+ }
1251
+ this._log(`Restored ${Object.keys(persistedRecentFillKeys).length} recently queued fill key(s) from persisted snapshot`, 'debug');
1252
+ }
1208
1253
  if (!this.config.dryRun && !this.accountId) {
1209
1254
  throw new Error('Cannot start bot without a resolved account ID');
1210
1255
  }
@@ -1410,6 +1455,7 @@ class DEXBot {
1410
1455
  const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
1411
1456
  let aborted = false;
1412
1457
  if (syncResult?.filledOrders?.length > 0) {
1458
+ this._refreshDynamicWeightDistribution(`${tag} sync-fill`);
1413
1459
  this._log(`[SYNC-CHAIN] ${syncResult.filledOrders.length} filled order(s) found during ${tag}`, 'info');
1414
1460
  const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`);
1415
1461
  if (!batchResult?.aborted) {
@@ -1429,7 +1475,7 @@ class DEXBot {
1429
1475
  }
1430
1476
  }
1431
1477
  _maxConsecutiveFillConsumerFailures() {
1432
- return FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES;
1478
+ return this.config.fillProcessing?.MAX_CONSECUTIVE_CONSUMER_FAILURES ?? FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES;
1433
1479
  }
1434
1480
  /**
1435
1481
  * Compute the backoff delay for fill-consumer retries after the failure
@@ -1441,15 +1487,16 @@ class DEXBot {
1441
1487
  * @private
1442
1488
  */
1443
1489
  _computeFillConsumerBackoffMs(failures) {
1444
- const initial = FILL_PROCESSING.CONSUMER_BACKOFF_INITIAL_MS;
1445
- const max = FILL_PROCESSING.CONSUMER_BACKOFF_MAX_MS;
1446
- const stepAfterMax = Math.max(0, failures - FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES);
1490
+ const fp = this.config.fillProcessing || FILL_PROCESSING;
1491
+ const initial = fp.CONSUMER_BACKOFF_INITIAL_MS;
1492
+ const max = fp.CONSUMER_BACKOFF_MAX_MS;
1493
+ const stepAfterMax = Math.max(0, failures - this._maxConsecutiveFillConsumerFailures());
1447
1494
  // 0 -> initial, 1 -> 2*initial, 2 -> 4*initial, ... capped at max.
1448
1495
  return Math.min(max, initial * Math.pow(2, stepAfterMax));
1449
1496
  }
1450
1497
  _scheduleFillConsumerRestart(chainOrders) {
1451
1498
  const failures = this._consecutiveConsumeFailures;
1452
- if (failures >= FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES) {
1499
+ if (failures >= this._maxConsecutiveFillConsumerFailures()) {
1453
1500
  // Past the failure budget: switch from tight setImmediate loop to
1454
1501
  // exponential backoff. The consumer continues to retry — a transient
1455
1502
  // outage (e.g., credential daemon recovery) will resume normal
@@ -1731,24 +1778,51 @@ class DEXBot {
1731
1778
  let resolvedOrders = [];
1732
1779
  if (fillMode === 'history') {
1733
1780
  this.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (history mode)`, 'info');
1734
- for (const fill of fillsToSync) {
1735
- const resultHistory = await this.manager.syncFromFillHistory(fill, {
1736
- persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.IMMEDIATE
1737
- });
1738
- const fillKey = buildFillKey({
1739
- orderId: fill?.op?.[1]?.order_id,
1740
- blockNum: fill?.block_num,
1741
- historyId: fill?.id
1781
+ // Batch mode for 2+ fills: acquires _gridLock once, batches drift refetch
1782
+ if (fillsToSync.length >= 2) {
1783
+ const batchResult = await this.manager.syncFromFillHistoryBatch(fillsToSync, {
1784
+ persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
1742
1785
  });
1743
- if (fillKey)
1744
- pendingFillKeysForCurrentCycle.add(fillKey);
1745
- // Dust is handled by post-fill detection below.
1746
- if (resultHistory.filledOrders)
1747
- resolvedOrders.push(...resultHistory.filledOrders);
1748
- if (resultHistory.requiresOpenOrdersSync)
1786
+ for (const fill of fillsToSync) {
1787
+ const fillKey = buildFillKey({
1788
+ orderId: fill?.op?.[1]?.order_id,
1789
+ blockNum: fill?.block_num,
1790
+ historyId: fill?.id
1791
+ });
1792
+ if (fillKey)
1793
+ pendingFillKeysForCurrentCycle.add(fillKey);
1794
+ }
1795
+ if (batchResult.filledOrders)
1796
+ resolvedOrders.push(...batchResult.filledOrders);
1797
+ if (batchResult.requiresOpenOrdersSync)
1749
1798
  requiresOpenOrdersSync = true;
1750
- if (resultHistory.ghostOrderId)
1751
- pendingGhostOrders.add(resultHistory.ghostOrderId);
1799
+ if (batchResult.ghostOrderIds?.length > 0) {
1800
+ for (const id of batchResult.ghostOrderIds) {
1801
+ pendingGhostOrders.add(id);
1802
+ }
1803
+ }
1804
+ }
1805
+ else {
1806
+ // Single fill: use individual per-fill path
1807
+ for (const fill of fillsToSync) {
1808
+ const resultHistory = await this.manager.syncFromFillHistory(fill, {
1809
+ persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
1810
+ });
1811
+ const fillKey = buildFillKey({
1812
+ orderId: fill?.op?.[1]?.order_id,
1813
+ blockNum: fill?.block_num,
1814
+ historyId: fill?.id
1815
+ });
1816
+ if (fillKey)
1817
+ pendingFillKeysForCurrentCycle.add(fillKey);
1818
+ // Dust is handled by post-fill detection below.
1819
+ if (resultHistory.filledOrders)
1820
+ resolvedOrders.push(...resultHistory.filledOrders);
1821
+ if (resultHistory.requiresOpenOrdersSync)
1822
+ requiresOpenOrdersSync = true;
1823
+ if (resultHistory.ghostOrderId)
1824
+ pendingGhostOrders.add(resultHistory.ghostOrderId);
1825
+ }
1752
1826
  }
1753
1827
  }
1754
1828
  if (fillMode !== 'history' || requiresOpenOrdersSync) {
@@ -1845,26 +1919,77 @@ class DEXBot {
1845
1919
  // cancel tx to clean up the zombie order on chain.
1846
1920
  // Runs in the finally so it still fires even if processValidFills
1847
1921
  // throws after collecting ghost IDs into pendingGhostOrders.
1848
- // Each cancelOrder is individually try/caught so no per-order
1849
- // error can prevent resumeFundRecalc from running.
1922
+ //
1923
+ // OPTIMIZATION: Batch all new ghost cancels into one executeBatch
1924
+ // call with multiple cancelOrder ops, reducing per-order tx fees
1925
+ // and broadcast overhead. Falls back to individual cancels on
1926
+ // batch failure.
1850
1927
  if (pendingGhostOrders.size > 0) {
1851
1928
  if (!this._ghostOrderCancelAttempted)
1852
1929
  this._ghostOrderCancelAttempted = new Set();
1853
- for (const ghostOrderId of pendingGhostOrders) {
1854
- if (this._ghostOrderCancelAttempted.has(ghostOrderId))
1855
- continue;
1856
- try {
1857
- this.manager.logger.log(`[SYNC] Cancelling orphaned chain order ${ghostOrderId} (other-side full-fill residual)`, 'info');
1858
- await chainOrders.cancelOrder(this.account, this.privateKey, ghostOrderId);
1859
- this._ghostOrderCancelAttempted.add(ghostOrderId);
1930
+ const newGhostIds = [...pendingGhostOrders].filter(id => !this._ghostOrderCancelAttempted.has(id));
1931
+ if (newGhostIds.length > 0) {
1932
+ const MAX_OPS_PER_TX = 200;
1933
+ let batchFailed = false;
1934
+ // Build cancel ops with per-ID error tolerance.
1935
+ // Index correlates 1:1 with newGhostIds (allSettled preserves order).
1936
+ const buildResults = await Promise.allSettled(newGhostIds.map(id => chainOrders.buildCancelOrderOp(this.account, id)));
1937
+ const cancelOps = [];
1938
+ for (let i = 0; i < buildResults.length; i++) {
1939
+ const result = buildResults[i];
1940
+ const id = newGhostIds[i];
1941
+ if (result.status === 'fulfilled') {
1942
+ cancelOps.push(result.value);
1943
+ }
1944
+ else {
1945
+ this.manager.logger.log(`[SYNC] Failed to build cancel op for ghost order ${id}: ${result.reason?.message || result.reason}`, 'warn');
1946
+ }
1947
+ }
1948
+ // Chunk into batches respecting MAX_OPS_PER_TX
1949
+ for (let i = 0; i < cancelOps.length; i += MAX_OPS_PER_TX) {
1950
+ const chunk = cancelOps.slice(i, i + MAX_OPS_PER_TX);
1951
+ const batchIds = newGhostIds.slice(i, i + MAX_OPS_PER_TX);
1952
+ try {
1953
+ this.manager.logger.log(`[SYNC] Batch-cancelling ${chunk.length} orphaned chain order(s) ` +
1954
+ `(batch ${Math.floor(i / MAX_OPS_PER_TX) + 1}/${Math.ceil(cancelOps.length / MAX_OPS_PER_TX)})`, 'info');
1955
+ await chainOrders.executeBatch(this.account, this.privateKey, chunk);
1956
+ // Mark successfully cancelled IDs immediately
1957
+ for (const id of batchIds) {
1958
+ this._ghostOrderCancelAttempted.add(id);
1959
+ }
1960
+ }
1961
+ catch (batchErr) {
1962
+ batchFailed = true;
1963
+ this.manager.logger.log(`[SYNC] Batch ghost cancel failed for chunk ${Math.floor(i / MAX_OPS_PER_TX) + 1} ` +
1964
+ `(${chunk.length} orders): ${batchErr?.message || batchErr}`, 'warn');
1965
+ }
1860
1966
  }
1861
- catch (err) {
1862
- this.manager.logger.log(`[SYNC] Failed to cancel orphaned order ${ghostOrderId}: ${err?.message || err}`, 'warn');
1967
+ // Log overall success if all chunks and all builds succeeded
1968
+ if (!batchFailed && cancelOps.length === newGhostIds.length) {
1969
+ this.manager.logger.log(`[SYNC] Successfully batch-cancelled ${newGhostIds.length} orphaned chain order(s)`, 'info');
1970
+ }
1971
+ // Fallback: individual cancels for IDs that were not marked successful
1972
+ for (const ghostOrderId of newGhostIds) {
1973
+ if (this._ghostOrderCancelAttempted.has(ghostOrderId))
1974
+ continue;
1975
+ try {
1976
+ this.manager.logger.log(`[SYNC] Cancelling orphaned chain order ${ghostOrderId} (other-side full-fill residual)`, 'info');
1977
+ await chainOrders.cancelOrder(this.account, this.privateKey, ghostOrderId);
1978
+ this._ghostOrderCancelAttempted.add(ghostOrderId);
1979
+ }
1980
+ catch (err) {
1981
+ this.manager.logger.log(`[SYNC] Failed to cancel orphaned order ${ghostOrderId}: ${err?.message || err}`, 'warn');
1982
+ }
1863
1983
  }
1864
1984
  }
1865
1985
  }
1866
1986
  await this.manager.resumeFundRecalc();
1867
1987
  }
1988
+ // Refresh dynamic weight distribution before processing fills
1989
+ // so the rebalance uses the latest market adapter weights, not
1990
+ // stale values from the last periodic refresh cycle.
1991
+ // Lightweight: reads local JSON snapshot file.
1992
+ this._refreshDynamicWeightDistribution('fill queue');
1868
1993
  // 5. Fixed-Cap Fill Rebalance
1869
1994
  // - 1..MAX_FILL_BATCH_SIZE fills: unified full-set planning
1870
1995
  // - larger bursts: fixed-size chunking at MAX_FILL_BATCH_SIZE
@@ -1929,6 +2054,7 @@ class DEXBot {
1929
2054
  else if (pendingFillKeysForCurrentCycle.size > 0) {
1930
2055
  await this._flushProcessedFillPersistenceForKeys(pendingFillKeysForCurrentCycle, 'fill-batch-no-rotations');
1931
2056
  }
2057
+ this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
1932
2058
  await retryPersistenceIfNeeded(this.manager);
1933
2059
  // Periodically clean up old fill records after processing N fills.
1934
2060
  // Counter is protected by _fillProcessingLock during fill consumption.
@@ -2255,7 +2381,7 @@ class DEXBot {
2255
2381
  * @returns {number} Positive maximum number of fill-driven rotations per broadcast cycle
2256
2382
  */
2257
2383
  _getMaxFillBatchSize() {
2258
- return Math.max(1, FILL_PROCESSING.MAX_FILL_BATCH_SIZE);
2384
+ return Math.max(1, this.config.fillProcessing?.MAX_FILL_BATCH_SIZE ?? FILL_PROCESSING.MAX_FILL_BATCH_SIZE);
2259
2385
  }
2260
2386
  /**
2261
2387
  * Extract operation results from a batch transaction result.
@@ -2739,7 +2865,7 @@ class DEXBot {
2739
2865
  // BROADCAST_DEADLINE fires while the transaction may still be valid
2740
2866
  // and land in the next block. Wait for up to 3 blocks (~1 BitShares
2741
2867
  // block interval each) and re-check before permanently discarding.
2742
- const UNCERTAIN_RECHECK_MAX_ATTEMPTS = PIPELINE_TIMING.RETRY_MAX_ATTEMPTS; // 3
2868
+ const UNCERTAIN_RECHECK_MAX_ATTEMPTS = this.config.pipelineTiming?.RETRY_MAX_ATTEMPTS ?? PIPELINE_TIMING.RETRY_MAX_ATTEMPTS; // 3
2743
2869
  const UNCERTAIN_RECHECK_INTERVAL_MS = TIMING.MILLISECONDS_PER_SECOND * 3; // 3s
2744
2870
  let recheckRound = 0;
2745
2871
  while (discarded.length > 0 && recheckRound < UNCERTAIN_RECHECK_MAX_ATTEMPTS) {
@@ -3485,8 +3611,7 @@ class DEXBot {
3485
3611
  this._runCredentialRecoveryAfterDaemonRestored().catch(err => {
3486
3612
  this.manager?.logger?.log?.(`[CREDENTIAL] Deferred recovery failed: ${err.message}`, 'error');
3487
3613
  if (this.manager) {
3488
- this.manager._recoveryState = this.manager._recoveryState || {};
3489
- this.manager._recoveryState.lastFailureAt = Date.now();
3614
+ this.manager._recoveryState = { ...this.manager._recoveryState, lastFailureAt: Date.now() };
3490
3615
  }
3491
3616
  });
3492
3617
  }, 1000);
@@ -3873,7 +3998,7 @@ class DEXBot {
3873
3998
  `broadcast. Running recovery before placing replacement orders.`, 'error');
3874
3999
  if (typeof this.manager.requestStructuralGridResync === 'function') {
3875
4000
  if (this.manager._recoveryState)
3876
- this.manager._recoveryState.structuralResyncRequested = true;
4001
+ this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
3877
4002
  await this.manager.requestStructuralGridResync('pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) });
3878
4003
  }
3879
4004
  try {
@@ -3963,7 +4088,7 @@ class DEXBot {
3963
4088
  // Request structural resync to rebuild the grid on the next cycle.
3964
4089
  if (typeof this.manager.requestStructuralGridResync === 'function') {
3965
4090
  if (this.manager._recoveryState)
3966
- this.manager._recoveryState.structuralResyncRequested = true;
4091
+ this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
3967
4092
  await this.manager.requestStructuralGridResync('unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
3968
4093
  }
3969
4094
  this.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
@@ -4266,8 +4391,7 @@ class DEXBot {
4266
4391
  `(${retryResult.reason || 'no reason'}). Master grid in memory ` +
4267
4392
  `is ahead of disk snapshot; structural resync requested.`, 'error');
4268
4393
  if (typeof this.manager.requestStructuralGridResync === 'function') {
4269
- this.manager._recoveryState = this.manager._recoveryState || {};
4270
- this.manager._recoveryState.structuralResyncRequested = true;
4394
+ this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
4271
4395
  await this.manager.requestStructuralGridResync('persistence guard triggered after COW batch', { persistReason: retryResult.reason || 'unknown' });
4272
4396
  }
4273
4397
  }
@@ -4837,9 +4961,7 @@ class DEXBot {
4837
4961
  const persistedResult = await this._recoverFromPersistedGrid();
4838
4962
  if (persistedResult.success) {
4839
4963
  if (this.manager?._recoveryState) {
4840
- this.manager._recoveryState.attemptCount = 0;
4841
- this.manager._recoveryState.lastAttemptAt = 0;
4842
- this.manager._recoveryState.lastFailureAt = 0;
4964
+ this.manager._recoveryState = { ...this.manager._recoveryState, attemptCount: 0, lastAttemptAt: 0, lastFailureAt: 0 };
4843
4965
  }
4844
4966
  return;
4845
4967
  }
@@ -4849,9 +4971,7 @@ class DEXBot {
4849
4971
  refreshCenterPrice: true,
4850
4972
  });
4851
4973
  if (resetResult && this.manager?._recoveryState) {
4852
- this.manager._recoveryState.attemptCount = 0;
4853
- this.manager._recoveryState.lastAttemptAt = 0;
4854
- this.manager._recoveryState.lastFailureAt = 0;
4974
+ this.manager._recoveryState = { ...this.manager._recoveryState, attemptCount: 0, lastAttemptAt: 0, lastFailureAt: 0 };
4855
4975
  }
4856
4976
  }
4857
4977
  catch (err) {
@@ -4860,7 +4980,7 @@ class DEXBot {
4860
4980
  finally {
4861
4981
  this._structuralGridResyncRunning = false;
4862
4982
  if (this.manager?._recoveryState) {
4863
- this.manager._recoveryState.structuralResyncRequested = false;
4983
+ this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: false };
4864
4984
  }
4865
4985
  }
4866
4986
  }, 0);
@@ -4998,11 +5118,11 @@ class DEXBot {
4998
5118
  * @returns {Promise<void>}
4999
5119
  */
5000
5120
  async shutdown() {
5001
- if (this._shutdownStarted) {
5121
+ if (this._shuttingDown) {
5002
5122
  this._log('Shutdown already in progress; ignoring re-entrant call');
5003
5123
  return this._shutdownPromise || Promise.resolve();
5004
5124
  }
5005
- this._shutdownStarted = true;
5125
+ this._shuttingDown = true;
5006
5126
  const shutdownImpl = typeof this._shutdownImpl === 'function'
5007
5127
  ? this._shutdownImpl
5008
5128
  : DEXBot.prototype._shutdownImpl;
@@ -5011,7 +5131,6 @@ class DEXBot {
5011
5131
  }
5012
5132
  async _shutdownImpl() {
5013
5133
  this._log('Initiating graceful shutdown...');
5014
- this._shuttingDown = true;
5015
5134
  this._processedFillStore.setShuttingDown(true);
5016
5135
  // Stop accepting new work
5017
5136
  this._stopBlockchainFetchInterval();