dexbot 1.2.0 → 1.2.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.
Files changed (86) hide show
  1. package/dist/credential-daemon.d.ts.map +1 -1
  2. package/dist/credential-daemon.js +6 -3
  3. package/dist/credential-daemon.js.map +1 -1
  4. package/dist/modules/bitshares-native/lru_cache.d.ts +9 -0
  5. package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -1
  6. package/dist/modules/bitshares-native/lru_cache.js +12 -0
  7. package/dist/modules/bitshares-native/lru_cache.js.map +1 -1
  8. package/dist/modules/bitshares-native/tx/builder.d.ts +4 -0
  9. package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
  10. package/dist/modules/bitshares-native/tx/builder.js +19 -0
  11. package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
  12. package/dist/modules/bitshares-native/tx/tx_cache.d.ts +7 -0
  13. package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -1
  14. package/dist/modules/bitshares-native/tx/tx_cache.js +11 -0
  15. package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -1
  16. package/dist/modules/bitshares_client.d.ts.map +1 -1
  17. package/dist/modules/bitshares_client.js +5 -0
  18. package/dist/modules/bitshares_client.js.map +1 -1
  19. package/dist/modules/constants.d.ts +1 -0
  20. package/dist/modules/constants.d.ts.map +1 -1
  21. package/dist/modules/constants.js +1 -0
  22. package/dist/modules/constants.js.map +1 -1
  23. package/dist/modules/credential_policy.d.ts.map +1 -1
  24. package/dist/modules/credential_policy.js +7 -5
  25. package/dist/modules/credential_policy.js.map +1 -1
  26. package/dist/modules/dexbot_class.d.ts +0 -1
  27. package/dist/modules/dexbot_class.d.ts.map +1 -1
  28. package/dist/modules/dexbot_class.js +119 -57
  29. package/dist/modules/dexbot_class.js.map +1 -1
  30. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  31. package/dist/modules/dexbot_fill_runtime.js +3 -0
  32. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  33. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  34. package/dist/modules/dexbot_maintenance_runtime.js +4 -0
  35. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  36. package/dist/modules/node_manager.d.ts +1 -0
  37. package/dist/modules/node_manager.d.ts.map +1 -1
  38. package/dist/modules/node_manager.js +11 -1
  39. package/dist/modules/node_manager.js.map +1 -1
  40. package/dist/modules/order/accounting.d.ts +8 -2
  41. package/dist/modules/order/accounting.d.ts.map +1 -1
  42. package/dist/modules/order/accounting.js +29 -16
  43. package/dist/modules/order/accounting.js.map +1 -1
  44. package/dist/modules/order/grid.js +1 -1
  45. package/dist/modules/order/grid.js.map +1 -1
  46. package/dist/modules/order/grid_reconcile_internal.js +13 -2
  47. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  48. package/dist/modules/order/index.d.ts +3 -8
  49. package/dist/modules/order/index.d.ts.map +1 -1
  50. package/dist/modules/order/index.js +3 -10
  51. package/dist/modules/order/index.js.map +1 -1
  52. package/dist/modules/order/manager.d.ts +84 -16
  53. package/dist/modules/order/manager.d.ts.map +1 -1
  54. package/dist/modules/order/manager.js +131 -235
  55. package/dist/modules/order/manager.js.map +1 -1
  56. package/dist/modules/order/strategy.js +1 -1
  57. package/dist/modules/order/strategy.js.map +1 -1
  58. package/dist/modules/order/sync_engine.d.ts +2 -3
  59. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  60. package/dist/modules/order/sync_engine.js +53 -20
  61. package/dist/modules/order/sync_engine.js.map +1 -1
  62. package/dist/modules/order/utils/math.d.ts +28 -1
  63. package/dist/modules/order/utils/math.d.ts.map +1 -1
  64. package/dist/modules/order/utils/math.js +40 -2
  65. package/dist/modules/order/utils/math.js.map +1 -1
  66. package/dist/modules/order/utils/order.d.ts.map +1 -1
  67. package/dist/modules/order/utils/order.js +42 -6
  68. package/dist/modules/order/utils/order.js.map +1 -1
  69. package/dist/modules/types.d.ts +7 -12
  70. package/dist/modules/types.d.ts.map +1 -1
  71. package/dist/modules/utils/math_utils.d.ts +3 -6
  72. package/dist/modules/utils/math_utils.d.ts.map +1 -1
  73. package/dist/modules/utils/math_utils.js +1 -14
  74. package/dist/modules/utils/math_utils.js.map +1 -1
  75. package/dist/{modules/order → scripts}/runner.d.ts +15 -9
  76. package/dist/scripts/runner.d.ts.map +1 -0
  77. package/dist/{modules/order → scripts}/runner.js +24 -45
  78. package/dist/scripts/runner.js.map +1 -0
  79. package/package.json +1 -2
  80. package/scripts/README.md +13 -0
  81. package/dist/modules/cli_whitelist_args.d.ts +0 -6
  82. package/dist/modules/cli_whitelist_args.d.ts.map +0 -1
  83. package/dist/modules/cli_whitelist_args.js +0 -13
  84. package/dist/modules/cli_whitelist_args.js.map +0 -1
  85. package/dist/modules/order/runner.d.ts.map +0 -1
  86. package/dist/modules/order/runner.js.map +0 -1
@@ -11,8 +11,7 @@
11
11
  *
12
12
  * SECTION 1: EXTERNAL DEPENDENCIES
13
13
  * SECTION 2: COW REBALANCE ENGINE
14
- * SECTION 3: STATE MANAGER
15
- * SECTION 4: ORDER MANAGER CLASS
14
+ * SECTION 3: ORDER MANAGER CLASS
16
15
  * ===============================================================================
17
16
  */
18
17
  // ===============================================================================
@@ -57,7 +56,7 @@ const { toFiniteNumber, isValidNumber } = Format;
57
56
  // 1. Master grid NEVER modified during planning (immutable during rebalance)
58
57
  // 2. Fills that arrive during rebalance are QUEUED, not lost
59
58
  // 3. Working grid is DISPOSABLE - if rebalance fails, discard it
60
- // 4. Only ONE rebalance plan active at a time (StateManager.rebalance)
59
+ // 4. Only ONE rebalance plan active at a time (_rebalanceState)
61
60
  // 5. Staleness detection aborts if master changes (fills, manual commands)
62
61
  //
63
62
  // FILL HANDLING DURING REBALANCE:
@@ -138,189 +137,7 @@ class COWRebalanceEngine {
138
137
  }
139
138
  }
140
139
  // ===============================================================================
141
- // SECTION 3: STATE MANAGER
142
- // ===============================================================================
143
- class StateManager {
144
- logger;
145
- rebalance;
146
- recovery;
147
- gridRegen;
148
- bootstrap;
149
- broadcast;
150
- signals;
151
- pipeline;
152
- constructor(options = {}) {
153
- this.logger = options.logger || null;
154
- this.reset();
155
- }
156
- reset() {
157
- this.rebalance = {
158
- state: REBALANCE_STATES.NORMAL,
159
- currentWorkingGrid: null
160
- };
161
- this.recovery = {
162
- phase: 'idle',
163
- attemptCount: 0,
164
- lastAttemptAt: 0,
165
- inFlight: false,
166
- lastFailureAt: 0
167
- };
168
- this.gridRegen = {
169
- buy: { armed: true, lastTriggeredAt: 0 },
170
- sell: { armed: true, lastTriggeredAt: 0 }
171
- };
172
- this.bootstrap = {
173
- isBootstrapping: false
174
- };
175
- this.broadcast = {
176
- isBroadcasting: false,
177
- startedAt: 0,
178
- };
179
- this.signals = {
180
- lastIllegalState: null,
181
- lastAccountingFailure: null
182
- };
183
- this.pipeline = {
184
- blockedSince: null,
185
- recoveryAttempted: false
186
- };
187
- }
188
- getRebalanceState() {
189
- return this.rebalance.state;
190
- }
191
- setRebalanceState(state) {
192
- this.rebalance.state = state;
193
- this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
194
- }
195
- isRebalancing() {
196
- return this.rebalance.state === REBALANCE_STATES.REBALANCING;
197
- }
198
- isBroadcasting() {
199
- return this.rebalance.state === REBALANCE_STATES.BROADCASTING;
200
- }
201
- setWorkingGrid(workingGrid) {
202
- this.rebalance.currentWorkingGrid = workingGrid;
203
- }
204
- getWorkingGrid() {
205
- return this.rebalance.currentWorkingGrid;
206
- }
207
- clearWorkingGrid() {
208
- this.rebalance.currentWorkingGrid = null;
209
- }
210
- recordRecoveryAttempt() {
211
- this.recovery.phase = 'scanning';
212
- this.recovery.attemptCount++;
213
- this.recovery.lastAttemptAt = Date.now();
214
- this.recovery.inFlight = true;
215
- }
216
- completeRecovery(success) {
217
- this.recovery.phase = success ? 'idle' : 'executing';
218
- this.recovery.inFlight = false;
219
- if (!success) {
220
- this.recovery.lastFailureAt = Date.now();
221
- }
222
- }
223
- isRecoveryInFlight() {
224
- return this.recovery.inFlight;
225
- }
226
- getRecoveryStats() {
227
- return { ...this.recovery };
228
- }
229
- isSideArmed(side) {
230
- return this.gridRegen[side]?.armed ?? false;
231
- }
232
- disarmSide(side) {
233
- if (this.gridRegen[side]) {
234
- this.gridRegen[side].armed = false;
235
- this.gridRegen[side].lastTriggeredAt = Date.now();
236
- }
237
- }
238
- armSide(side) {
239
- if (this.gridRegen[side]) {
240
- this.gridRegen[side].armed = true;
241
- }
242
- }
243
- startBootstrap() {
244
- this.bootstrap.isBootstrapping = true;
245
- this.logger?.log('[BOOTSTRAP] Started', 'debug');
246
- }
247
- finishBootstrap() {
248
- this.bootstrap.isBootstrapping = false;
249
- this.logger?.log('[BOOTSTRAP] Finished', 'debug');
250
- }
251
- isBootstrapping() {
252
- return this.bootstrap.isBootstrapping;
253
- }
254
- startBroadcasting() {
255
- this.broadcast.isBroadcasting = true;
256
- this.broadcast.startedAt = Date.now();
257
- this.logger?.log?.('[BROADCAST] Flag set — fill processing will be deferred until stopBroadcasting()', 'debug');
258
- }
259
- stopBroadcasting() {
260
- this.broadcast.isBroadcasting = false;
261
- this.broadcast.startedAt = 0;
262
- }
263
- isBroadcastingActive() {
264
- if (this.broadcast.isBroadcasting && this.broadcast.startedAt > 0) {
265
- const elapsed = Date.now() - this.broadcast.startedAt;
266
- if (elapsed > 120000) {
267
- this.logger?.log?.('[BROADCAST] Auto-clearing stale broadcast flag after 120s', 'warn');
268
- this.broadcast.isBroadcasting = false;
269
- this.broadcast.startedAt = 0;
270
- }
271
- }
272
- return this.broadcast.isBroadcasting;
273
- }
274
- setIllegalStateSignal(signal) {
275
- this.signals.lastIllegalState = {
276
- ...signal,
277
- at: Date.now()
278
- };
279
- }
280
- consumeIllegalStateSignal() {
281
- const signal = this.signals.lastIllegalState;
282
- this.signals.lastIllegalState = null;
283
- return signal;
284
- }
285
- setAccountingFailureSignal(signal) {
286
- this.signals.lastAccountingFailure = {
287
- ...signal,
288
- at: Date.now()
289
- };
290
- }
291
- consumeAccountingFailureSignal() {
292
- const signal = this.signals.lastAccountingFailure;
293
- this.signals.lastAccountingFailure = null;
294
- return signal;
295
- }
296
- markPipelineBlocked() {
297
- if (!this.pipeline.blockedSince) {
298
- this.pipeline.blockedSince = Date.now();
299
- }
300
- }
301
- markPipelineClear() {
302
- this.pipeline.blockedSince = null;
303
- this.pipeline.recoveryAttempted = false;
304
- }
305
- getPipelineBlockedDuration() {
306
- return this.pipeline.blockedSince ? Date.now() - this.pipeline.blockedSince : 0;
307
- }
308
- isPipelineBlocked() {
309
- return this.pipeline.blockedSince !== null;
310
- }
311
- getState() {
312
- return {
313
- rebalance: { ...this.rebalance, currentWorkingGrid: null },
314
- recovery: { ...this.recovery },
315
- gridRegen: { ...this.gridRegen },
316
- bootstrap: { ...this.bootstrap },
317
- broadcast: { ...this.broadcast },
318
- pipeline: { ...this.pipeline }
319
- };
320
- }
321
- }
322
- // ===============================================================================
323
- // SECTION 4: ORDER MANAGER CLASS
140
+ // SECTION 3: ORDER MANAGER CLASS
324
141
  // ===============================================================================
325
142
  class OrderManager {
326
143
  config;
@@ -332,7 +149,14 @@ class OrderManager {
332
149
  accountant;
333
150
  strategy;
334
151
  sync;
335
- _state;
152
+ _rebalanceState;
153
+ _bootstrapping;
154
+ _broadcastingFlag;
155
+ _broadcastingStartedAt;
156
+ _illegalStateSignal;
157
+ _accountingFailureSignal;
158
+ _recoveryStateValue;
159
+ _gridRegenStateValue;
336
160
  _ordersByState;
337
161
  _ordersByType;
338
162
  targetSpreadCount;
@@ -368,7 +192,9 @@ class OrderManager {
368
192
  _gridPersistenceSuspendedReason;
369
193
  _pendingBroadcasts;
370
194
  _committedOrderIds;
195
+ _committedOrderIdsBuiltAt;
371
196
  _gridDirtyAt;
197
+ _orphanFillsCreditedAt;
372
198
  _pendingRecovery;
373
199
  _metrics;
374
200
  _currentWorkingGrid;
@@ -395,7 +221,23 @@ class OrderManager {
395
221
  this.accountant = new Accountant(this);
396
222
  this.strategy = new StrategyEngine(this);
397
223
  this.sync = new SyncEngine(this);
398
- this._state = new StateManager({ logger: this.logger });
224
+ this._rebalanceState = REBALANCE_STATES.NORMAL;
225
+ this._bootstrapping = false;
226
+ this._broadcastingFlag = false;
227
+ this._broadcastingStartedAt = 0;
228
+ this._illegalStateSignal = null;
229
+ this._accountingFailureSignal = null;
230
+ this._recoveryStateValue = {
231
+ phase: 'idle',
232
+ attemptCount: 0,
233
+ lastAttemptAt: 0,
234
+ inFlight: false,
235
+ lastFailureAt: 0
236
+ };
237
+ this._gridRegenStateValue = {
238
+ buy: { armed: true, lastTriggeredAt: 0 },
239
+ sell: { armed: true, lastTriggeredAt: 0 }
240
+ };
399
241
  // Index Sets use mutable mutation patterns controlled via _applyOrderUpdate
400
242
  // These are private implementation details and must NOT be mutated directly
401
243
  // All external code must go through the COW pipeline
@@ -430,7 +272,7 @@ class OrderManager {
430
272
  // Level 2: _gridLock (grid mutations)
431
273
  // Level 3: _syncLock (sync operations)
432
274
  // Level 4: _fundLock (fund operations — innermost)
433
- // WARNING: AsyncLock is NOT reentrant.
275
+ // Note: AsyncLock IS re-entrant (acquire detects nested calls via _holding).
434
276
  this._divergenceLock = new AsyncLock({ level: 0 });
435
277
  this._fillProcessingLock = new AsyncLock({ level: 1 });
436
278
  this._gridLock = new AsyncLock({ level: 2 });
@@ -450,7 +292,9 @@ class OrderManager {
450
292
  this._gridPersistenceSuspendedReason = null;
451
293
  this._pendingBroadcasts = new Map();
452
294
  this._committedOrderIds = new Set();
295
+ this._committedOrderIdsBuiltAt = 0;
453
296
  this._gridDirtyAt = null;
297
+ this._orphanFillsCreditedAt = null;
454
298
  this._pendingRecovery = null;
455
299
  this._metrics = {
456
300
  fundRecalcCount: 0,
@@ -461,7 +305,8 @@ class OrderManager {
461
305
  lastSyncDurationMs: 0,
462
306
  metricsStartTime: Date.now()
463
307
  };
464
- this._state.startBootstrap();
308
+ this._bootstrapping = true;
309
+ this.logger?.log('[BOOTSTRAP] Started', 'debug');
465
310
  this._currentWorkingGrid = null;
466
311
  this._cowEngine = null;
467
312
  this._cleanExpiredLocks();
@@ -479,48 +324,77 @@ class OrderManager {
479
324
  }
480
325
  _clearWorkingGridRef() {
481
326
  this._currentWorkingGrid = null;
482
- this._state.clearWorkingGrid();
483
- this._state.setRebalanceState(REBALANCE_STATES.NORMAL);
327
+ this._rebalanceState = REBALANCE_STATES.NORMAL;
484
328
  }
485
329
  _setRebalanceState(state) {
486
- this._state.setRebalanceState(state);
330
+ this._rebalanceState = state;
331
+ this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
487
332
  }
488
333
  /**
489
334
  * @returns {boolean}
490
335
  */
491
336
  isRebalancing() {
492
- return this._state.isRebalancing();
337
+ return this._rebalanceState === REBALANCE_STATES.REBALANCING;
493
338
  }
494
339
  /**
495
340
  * @returns {boolean}
496
341
  */
497
342
  isBroadcasting() {
498
- return this._state.isBroadcasting();
343
+ return this._rebalanceState === REBALANCE_STATES.BROADCASTING;
344
+ }
345
+ /**
346
+ * @returns {boolean}
347
+ */
348
+ isBootstrapping() {
349
+ return this._bootstrapping;
350
+ }
351
+ /**
352
+ * Whether a broadcast is currently active (no side-effects).
353
+ * @returns {boolean}
354
+ */
355
+ isBroadcastingActive() {
356
+ return this._broadcastingFlag;
357
+ }
358
+ /**
359
+ * Auto-clears a stale broadcast flag after 120s so a hung
360
+ * broadcast cannot permanently block rebalancing.
361
+ * Separated from isBroadcastingActive() so the side-effect only
362
+ * runs from one well-defined location (maintenance logic) instead
363
+ * of from every read call-site.
364
+ * @returns {void}
365
+ */
366
+ _clearStaleBroadcastFlag() {
367
+ if (this._broadcastingFlag && this._broadcastingStartedAt > 0) {
368
+ const elapsed = Date.now() - this._broadcastingStartedAt;
369
+ if (elapsed > 120000) {
370
+ this.logger?.log?.('[BROADCAST] Auto-clearing stale broadcast flag after 120s', 'warn');
371
+ this._broadcastingFlag = false;
372
+ this._broadcastingStartedAt = 0;
373
+ }
374
+ }
499
375
  }
500
376
  /**
501
377
  * Whether the manager is in the middle of a rebalance plan or broadcast.
502
- * NOT a pure predicate: delegates to _state.isBroadcastingActive()
503
- * (see StateManager.isBroadcastingActive, manager.ts:362) which
504
- * auto-clears a stale broadcast flag after 120s (producing a warn log)
505
- * so that a hung broadcast cannot permanently block rebalancing.
506
378
  * @returns {boolean}
507
379
  */
508
380
  isPlanningActive() {
509
- return this.isRebalancing() || this._state.isBroadcastingActive();
381
+ return this.isRebalancing() || this.isBroadcastingActive();
510
382
  }
511
383
  /**
512
384
  * @returns {void}
513
385
  */
514
386
  startBootstrap() {
515
- this._state.startBootstrap();
387
+ this._bootstrapping = true;
388
+ this.logger?.log('[BOOTSTRAP] Started', 'debug');
516
389
  }
517
390
  /**
518
391
  * @returns {import('./types').BootstrapResult}
519
392
  */
520
393
  finishBootstrap() {
521
394
  const result = { hadDrift: false, driftInfo: null };
522
- if (this._state.isBootstrapping()) {
523
- this._state.finishBootstrap();
395
+ if (this._bootstrapping) {
396
+ this._bootstrapping = false;
397
+ this.logger?.log('[BOOTSTRAP] Finished', 'debug');
524
398
  // Validate fund state at bootstrap completion - if drift exists here,
525
399
  // it's not transient (grid is now stable) and indicates a potential bug
526
400
  const driftCheck = this.checkFundDriftAfterFills();
@@ -538,13 +412,16 @@ class OrderManager {
538
412
  * @returns {void}
539
413
  */
540
414
  startBroadcasting() {
541
- this._state.startBroadcasting();
415
+ this._broadcastingFlag = true;
416
+ this._broadcastingStartedAt = Date.now();
417
+ this.logger?.log?.('[BROADCAST] Flag set — fill processing will be deferred until stopBroadcasting()', 'debug');
542
418
  }
543
419
  /**
544
420
  * @returns {void}
545
421
  */
546
422
  stopBroadcasting() {
547
- this._state.stopBroadcasting();
423
+ this._broadcastingFlag = false;
424
+ this._broadcastingStartedAt = 0;
548
425
  }
549
426
  /**
550
427
  * @returns {void}
@@ -765,6 +642,9 @@ class OrderManager {
765
642
  */
766
643
  pauseRecalcLogging() {
767
644
  this._pauseRecalcLogging = true;
645
+ if (this._pauseRecalcLoggingWatchdog) {
646
+ clearTimeout(this._pauseRecalcLoggingWatchdog);
647
+ }
768
648
  this._pauseRecalcLoggingWatchdog = setTimeout(() => {
769
649
  this.logger?.log?.(`[MANAGER] pauseRecalcLogging safety watchdog: forcing resume after ${TIMING.SAFETY_PAUSE_TIMEOUT_MS}ms`, 'warn');
770
650
  this._pauseRecalcLogging = false;
@@ -895,11 +775,12 @@ class OrderManager {
895
775
  const fatalError = validation.errors.find(e => e.isFatal || e.code === 'ILLEGAL_SPREAD_STATE');
896
776
  if (fatalError) {
897
777
  this.logger.log(fatalError.message, 'error');
898
- this._state.setIllegalStateSignal({
778
+ this._illegalStateSignal = {
899
779
  id: order.id,
900
780
  context,
901
- message: fatalError.message
902
- });
781
+ message: fatalError.message,
782
+ at: Date.now()
783
+ };
903
784
  if (this._throwOnIllegalState) {
904
785
  const err = new Error(fatalError.message);
905
786
  err.code = fatalError.code;
@@ -1217,13 +1098,17 @@ class OrderManager {
1217
1098
  * @returns {Object|null} The consumed signal or null
1218
1099
  */
1219
1100
  consumeIllegalStateSignal() {
1220
- return this._state.consumeIllegalStateSignal();
1101
+ const signal = this._illegalStateSignal;
1102
+ this._illegalStateSignal = null;
1103
+ return signal;
1221
1104
  }
1222
1105
  /**
1223
1106
  * @returns {Object|null} The consumed signal or null
1224
1107
  */
1225
1108
  consumeAccountingFailureSignal() {
1226
- return this._state.consumeAccountingFailureSignal();
1109
+ const signal = this._accountingFailureSignal;
1110
+ this._accountingFailureSignal = null;
1111
+ return signal;
1227
1112
  }
1228
1113
  /**
1229
1114
  * @param {Object} BitShares - BitShares API instance
@@ -1289,16 +1174,14 @@ class OrderManager {
1289
1174
  if (recoveryInFlight) {
1290
1175
  reasons.push('recovery sync in-flight');
1291
1176
  }
1292
- if (broadcasting || this._state.isBroadcastingActive()) {
1177
+ if (broadcasting || this.isBroadcastingActive()) {
1293
1178
  reasons.push('broadcasting active orders');
1294
1179
  }
1295
1180
  if (reasons.length > 0 && !this._pipelineBlockedSince) {
1296
1181
  this._pipelineBlockedSince = Date.now();
1297
- this._state.markPipelineBlocked();
1298
1182
  }
1299
1183
  else if (reasons.length === 0) {
1300
1184
  this._pipelineBlockedSince = null;
1301
- this._state.markPipelineClear();
1302
1185
  }
1303
1186
  return {
1304
1187
  isEmpty: reasons.length === 0,
@@ -1316,7 +1199,6 @@ class OrderManager {
1316
1199
  return false;
1317
1200
  this.ordersNeedingPriceCorrection = [];
1318
1201
  this._pipelineBlockedSince = null;
1319
- this._state.markPipelineClear();
1320
1202
  return true;
1321
1203
  }
1322
1204
  /**
@@ -1492,6 +1374,7 @@ class OrderManager {
1492
1374
  newCommittedIds.add(order.orderId);
1493
1375
  }
1494
1376
  this._committedOrderIds = newCommittedIds;
1377
+ this._committedOrderIdsBuiltAt = Date.now();
1495
1378
  const freshIndexes = workingGrid.getIndexes();
1496
1379
  this._ordersByState = {
1497
1380
  [ORDER_STATES.VIRTUAL]: freshIndexes[ORDER_STATES.VIRTUAL] || new Set(),
@@ -1510,6 +1393,13 @@ class OrderManager {
1510
1393
  }
1511
1394
  try {
1512
1395
  if (!skipRecalc) {
1396
+ // If the last accounting failure was due to stale accountTotals,
1397
+ // the commit proceeded without locking funds. Refresh accountTotals
1398
+ // before recalculateFunds() to avoid a false-positive drift recovery.
1399
+ if (this._lastAccountingFailure?.reason === 'stale') {
1400
+ this.logger.log('[COW] Stale accountTotals during commit; refreshing before recalculateFunds to avoid false recovery', 'warn');
1401
+ await this.fetchAccountTotals(this.accountId);
1402
+ }
1513
1403
  await this.recalculateFunds();
1514
1404
  }
1515
1405
  const duration = Date.now() - startTime;
@@ -1520,8 +1410,10 @@ class OrderManager {
1520
1410
  }
1521
1411
  catch (recalcErr) {
1522
1412
  this.logger.log(`[COW] Fund recalculation failed post-commit: ${recalcErr.message}`, 'error');
1523
- this._recoveryState = this._recoveryState || {};
1524
- this._recoveryState.lastFailureAt = Date.now();
1413
+ if (!this._recoveryStateValue) {
1414
+ this._recoveryStateValue = { phase: 'idle', attemptCount: 0, lastAttemptAt: 0, inFlight: false, lastFailureAt: 0 };
1415
+ }
1416
+ this._recoveryStateValue.lastFailureAt = Date.now();
1525
1417
  }
1526
1418
  finally {
1527
1419
  this._clearWorkingGridRef();
@@ -1536,7 +1428,7 @@ class OrderManager {
1536
1428
  validateGridStateForPersistence(options = {}) {
1537
1429
  const result = validateGridForPersistence(this.orders, this.accountTotals);
1538
1430
  const allowBootstrapTransient = options.allowBootstrapTransient !== false;
1539
- if (!result.isValid && allowBootstrapTransient && this._state.isBootstrapping()) {
1431
+ if (!result.isValid && allowBootstrapTransient && this._bootstrapping) {
1540
1432
  this.logger.log(`[BOOTSTRAP] Transient state (expected): ${result.reason}`, 'debug');
1541
1433
  return { isValid: true, reason: null };
1542
1434
  }
@@ -1596,7 +1488,14 @@ class OrderManager {
1596
1488
  getMetrics() {
1597
1489
  return {
1598
1490
  ...this._metrics,
1599
- state: this._state.getState(),
1491
+ state: {
1492
+ rebalance: { state: this._rebalanceState, currentWorkingGrid: null },
1493
+ recovery: { ...this._recoveryStateValue },
1494
+ gridRegen: { ...this._gridRegenStateValue },
1495
+ bootstrap: { isBootstrapping: this._bootstrapping },
1496
+ broadcast: { isBroadcasting: this._broadcastingFlag, startedAt: this._broadcastingStartedAt },
1497
+ pipeline: { blockedSince: this._pipelineBlockedSince, recoveryAttempted: this._recoveryAttempted }
1498
+ },
1600
1499
  currentTime: Date.now()
1601
1500
  };
1602
1501
  }
@@ -1625,33 +1524,30 @@ class OrderManager {
1625
1524
  comparePrecisions
1626
1525
  });
1627
1526
  }
1628
- // Delegates to _state.rebalance (StateManager's internal field).
1629
- // Single source of truth — the getter/setter exists only for backward
1630
- // compat with code that reads _rebalanceState directly.
1631
- get _rebalanceState() {
1632
- return this._state.getRebalanceState();
1633
- }
1634
- set _rebalanceState(value) {
1635
- this._state.setRebalanceState(value);
1636
- }
1637
1527
  get _lastIllegalState() {
1638
- return this._state.signals?.lastIllegalState || null;
1528
+ return this._illegalStateSignal || null;
1639
1529
  }
1640
1530
  set _lastIllegalState(value) {
1641
1531
  if (value) {
1642
- this._state.setIllegalStateSignal(value);
1532
+ this._illegalStateSignal = {
1533
+ ...value,
1534
+ at: Date.now()
1535
+ };
1643
1536
  }
1644
1537
  }
1645
1538
  get _lastAccountingFailure() {
1646
- return this._state.signals?.lastAccountingFailure || null;
1539
+ return this._accountingFailureSignal || null;
1647
1540
  }
1648
1541
  set _lastAccountingFailure(value) {
1649
1542
  if (value) {
1650
- this._state.setAccountingFailureSignal(value);
1543
+ this._accountingFailureSignal = {
1544
+ ...value,
1545
+ at: Date.now()
1546
+ };
1651
1547
  }
1652
1548
  }
1653
1549
  get _recoveryState() {
1654
- return this._state.recovery;
1550
+ return this._recoveryStateValue;
1655
1551
  }
1656
1552
  set _recoveryState(value) {
1657
1553
  const fallback = {
@@ -1662,19 +1558,19 @@ class OrderManager {
1662
1558
  lastFailureAt: 0,
1663
1559
  structuralResyncRequested: false
1664
1560
  };
1665
- this._state.recovery = {
1561
+ this._recoveryStateValue = {
1666
1562
  ...fallback,
1667
1563
  ...(value && typeof value === 'object' ? value : {})
1668
1564
  };
1669
1565
  }
1670
1566
  get _gridRegenState() {
1671
- return this._state.gridRegen;
1567
+ return this._gridRegenStateValue;
1672
1568
  }
1673
1569
  set _gridRegenState(value) {
1674
1570
  if (!value || typeof value !== 'object')
1675
1571
  return;
1676
1572
  const defaultSide = { armed: true, lastTriggeredAt: 0 };
1677
- this._state.gridRegen = {
1573
+ this._gridRegenStateValue = {
1678
1574
  buy: { ...defaultSide, ...(value.buy || {}) },
1679
1575
  sell: { ...defaultSide, ...(value.sell || {}) }
1680
1576
  };