dexbot 1.6.2 → 1.6.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/analysis/README.md +2 -2
  3. package/analysis/ama_fitting/package.json +1 -1
  4. package/analysis/grid_correction_check.ts +80 -7
  5. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  6. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  7. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  8. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  9. package/analysis/trend_detection/package.json +1 -1
  10. package/claw/package.json +1 -1
  11. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  12. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  13. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  14. package/dist/analysis/grid_correction_check.d.ts +5 -1
  15. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  16. package/dist/analysis/grid_correction_check.js +81 -7
  17. package/dist/analysis/grid_correction_check.js.map +1 -1
  18. package/dist/modules/dexbot_class.d.ts +1 -1
  19. package/dist/modules/dexbot_cow_runtime.d.ts +81 -2
  20. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  21. package/dist/modules/dexbot_cow_runtime.js +482 -8
  22. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  23. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  24. package/dist/modules/dexbot_maintenance_runtime.js +6 -1
  25. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  26. package/dist/modules/order/manager.d.ts.map +1 -1
  27. package/dist/modules/order/manager.js +3 -1
  28. package/dist/modules/order/manager.js.map +1 -1
  29. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  30. package/dist/modules/order/sync_engine.js +17 -5
  31. package/dist/modules/order/sync_engine.js.map +1 -1
  32. package/dist/modules/order/utils/order.d.ts +45 -7
  33. package/dist/modules/order/utils/order.d.ts.map +1 -1
  34. package/dist/modules/order/utils/order.js +160 -15
  35. package/dist/modules/order/utils/order.js.map +1 -1
  36. package/docs/DEXBOT_COMPARISON.md +3 -3
  37. package/docs/EVOLUTION.md +8 -7
  38. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  39. package/docs/README.md +1 -1
  40. package/package.json +1 -1
@@ -198,6 +198,117 @@ function _filterUnmatchedChainOrders(manager, chainOrderId) {
198
198
  manager._lastUnmatchedChainOrders = manager._lastUnmatchedChainOrders.filter((u) => (u?.id || u?.orderId || u?.chainOrderId) !== chainOrderId);
199
199
  }
200
200
  }
201
+ /**
202
+ * Remove a single correction entry by its full queue key
203
+ * (chainOrderId + surplus flag). The queue's upsert key is
204
+ * (chainOrderId, isSurplus), so a chain-order-only filter would silently
205
+ * discard a sibling entry (e.g. a cancel-only orphan sharing the id with
206
+ * a price update). Callers pass the entry's own isSurplus flag.
207
+ */
208
+ function _removeCorrectionEntry(manager, chainOrderId, isSurplus) {
209
+ const surplus = Boolean(isSurplus);
210
+ if (manager && Array.isArray(manager.ordersNeedingPriceCorrection)) {
211
+ manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c?.chainOrderId !== chainOrderId || Boolean(c?.isSurplus) !== surplus);
212
+ }
213
+ }
214
+ /**
215
+ * Stamp queue provenance on a correction entry: queued-at timestamp plus
216
+ * the detector that produced it. Existing provenance (e.g. a fresher
217
+ * re-queue refreshing queuedAt) is preserved on merge — the sync_engine
218
+ * upsert spreads the new entry over the old one, so a re-queued entry
219
+ * keeps its original queuedAt unless the caller explicitly refreshes it.
220
+ * @param {Object} entry - Correction entry being queued
221
+ * @param {string} source - provenance tag (see queuedBy values)
222
+ * @returns {Object} The same entry, stamped
223
+ */
224
+ function _stampCorrectionProvenance(entry, source) {
225
+ if (entry && typeof entry === 'object') {
226
+ if (entry.queuedAt == null)
227
+ entry.queuedAt = Date.now();
228
+ if (entry.queuedBy == null)
229
+ entry.queuedBy = source;
230
+ }
231
+ return entry;
232
+ }
233
+ /**
234
+ * Drain-time staleness validation for a price-update correction entry
235
+ * (Fix 1): verify the queued intent still matches the LIVE grid geometry
236
+ * before broadcasting.
237
+ *
238
+ * A correction entry snapshots {slot id, chainOrderId, expectedPrice} at
239
+ * queue time. Any geometry-changing resync (trigger-file resync,
240
+ * reconcileGridOrders startup path, COW commit re-map) can re-slot the
241
+ * chain order or move the slot's price afterwards, leaving the entry
242
+ * stale. Broadcasting it would REVERT the resync's placement — the
243
+ * duplicate-price-level incident class (stale UPDATE is the exact
244
+ * negation of the resync's placement, to the satoshi).
245
+ *
246
+ * An entry is actionable only when the live slot:
247
+ * 1. still exists in the master grid,
248
+ * 2. still owns this chainOrderId (not re-slotted / adopted elsewhere),
249
+ * 3. still targets the queued price — via priceSlotEqual on genesis
250
+ * grids (same integer-round-trip predicate the pass-1 detector
251
+ * uses) or calculatePriceTolerance on legacy grids (same predicate
252
+ * the detector uses there).
253
+ *
254
+ * Cancel-type entries (cancelOnly / isSurplus) are exempt — a cancel is
255
+ * idempotent (gone orders resolve via the orderGone path) and never
256
+ * re-prices onto a stale level.
257
+ *
258
+ * @param {Object} manager - OrderManager instance (live grid + assets)
259
+ * @param {Object} entry - Queued correction entry
260
+ * @returns {{valid: boolean, reason: string}} valid=false drops the entry
261
+ */
262
+ function _validatePriceCorrectionEntry(manager, entry) {
263
+ if (!entry || entry.cancelOnly === true || entry.isSurplus === true) {
264
+ return { valid: true, reason: 'cancel-type' };
265
+ }
266
+ const slotId = entry?.gridOrder?.id;
267
+ const slot = (slotId && manager?.orders instanceof Map) ? manager.orders.get(slotId) : null;
268
+ if (!slot) {
269
+ return { valid: false, reason: `slot ${slotId || '?'} no longer exists` };
270
+ }
271
+ if (slot.orderId !== entry.chainOrderId) {
272
+ return { valid: false, reason: `slot ${slotId} now owns ${slot.orderId || 'no order'} (entry targets ${entry.chainOrderId})` };
273
+ }
274
+ const assets = manager?.assets;
275
+ const precision = entry.type === ORDER_TYPES.SELL ? assets?.assetA?.precision : assets?.assetB?.precision;
276
+ let priceMatches = false;
277
+ try {
278
+ const genesis = manager?._genesis;
279
+ if (genesis && Array.isArray(genesis.priceLevels)) {
280
+ priceMatches = priceSlotEqual(slot.price, entry.expectedPrice, precision);
281
+ }
282
+ else {
283
+ const tolerance = MathUtils.calculatePriceTolerance(entry.expectedPrice, entry.size, entry.type, assets);
284
+ priceMatches = Math.abs(slot.price - entry.expectedPrice) <= (tolerance ?? 0);
285
+ }
286
+ }
287
+ catch {
288
+ priceMatches = slot.price === entry.expectedPrice;
289
+ }
290
+ if (!priceMatches) {
291
+ return { valid: false, reason: `slot ${slotId} now targets ${slot.price} (entry queued ${entry.expectedPrice})` };
292
+ }
293
+ // Size check: the broadcast sends amountToSell from the QUEUED snapshot.
294
+ // A partial fill between queue and drain changes the slot's booked size;
295
+ // pushing the stale size would over-write the fill (chain side rebuilds
296
+ // the delta from a live re-read, so it cannot corrupt, but it can still
297
+ // surprise). Integer-quantum comparison, same convention as the
298
+ // pass-1 size check — a fill-changed entry drops and the next sync
299
+ // re-queues from the fresh size if the order is still off-target.
300
+ try {
301
+ const sizePrecision = entry.type === ORDER_TYPES.SELL ? assets?.assetA?.precision : assets?.assetB?.precision;
302
+ if (isValidNumber(slot.size) && isValidNumber(entry.size)
303
+ && floatToBlockchainInt(slot.size, sizePrecision) !== floatToBlockchainInt(entry.size, sizePrecision)) {
304
+ return { valid: false, reason: `slot ${slotId} size moved ${entry.size} -> ${slot.size} (fill changed it after queueing)` };
305
+ }
306
+ }
307
+ catch {
308
+ // Precision unavailable — fail open on size (ownership + price hold).
309
+ }
310
+ return { valid: true, reason: 'live-slot-match' };
311
+ }
201
312
  // ================================================================================
202
313
  // SECTION 1: CHAIN ORDER MATCHING & RECONCILIATION
203
314
  // ================================================================================
@@ -416,7 +527,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
416
527
  }
417
528
  finally {
418
529
  if (shouldRemove) {
419
- manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c.chainOrderId !== chainOrderId);
530
+ _removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
420
531
  }
421
532
  }
422
533
  }
@@ -448,7 +559,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
448
559
  }
449
560
  finally {
450
561
  if (shouldRemove) {
451
- manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c.chainOrderId !== chainOrderId);
562
+ _removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
452
563
  }
453
564
  }
454
565
  }
@@ -478,14 +589,20 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
478
589
  // The guard returns before the try/finally below, so drop the entry
479
590
  // from the correction queue here — otherwise it would linger forever
480
591
  // and re-attempt on every sync cycle.
481
- manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c.chainOrderId !== chainOrderId);
592
+ _removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
482
593
  return { success: false, skipped: true, error: 'crossed-placement-guard' };
483
594
  }
484
595
  try {
485
596
  const updateResult = await accountOrders.updateOrder(accountName, privateKey, chainOrderId, { amountToSell, minToReceive });
486
597
  if (updateResult === null) {
598
+ // Zero-delta no-op: the chain order already equals the target
599
+ // (replayed correction, sub-unit rounding, or a landed update
600
+ // observed via a lagging read). Resolved, not failed — counting
601
+ // it as failed turns routine no-ops into permanent MAINT WARN
602
+ // noise ("Delta is 0; skipping" every cycle) that hides real
603
+ // reverts.
487
604
  shouldRemove = true;
488
- return { success: false, error: 'skipped' };
605
+ return { success: true, skipped: true };
489
606
  }
490
607
  shouldRemove = true;
491
608
  return { success: true };
@@ -510,7 +627,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
510
627
  }
511
628
  finally {
512
629
  if (shouldRemove) {
513
- manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c.chainOrderId !== chainOrderId);
630
+ _removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
514
631
  }
515
632
  }
516
633
  }
@@ -533,8 +650,7 @@ async function _resolveCancelledCorrection(manager, entry) {
533
650
  });
534
651
  }
535
652
  _filterUnmatchedChainOrders(manager, chainOrderId);
536
- manager.ordersNeedingPriceCorrection = (manager.ordersNeedingPriceCorrection || [])
537
- .filter((c) => c.chainOrderId !== chainOrderId);
653
+ _removeCorrectionEntry(manager, chainOrderId, entry?.isSurplus);
538
654
  }
539
655
  /**
540
656
  * Broadcast all cancel-type corrections (cancelOnly duplicate orphans +
@@ -671,20 +787,49 @@ async function correctAllPriceMismatches(manager, accountName, privateKey, accou
671
787
  const results = [];
672
788
  let corrected = 0;
673
789
  let failed = 0;
790
+ let staleDropped = 0;
791
+ // Dedupe on the full queue key (chainOrderId + surplus flag),
792
+ // matching the sync upsert key. A chain-order-only key would drop a
793
+ // sibling entry (price update + cancel sharing one chain id) before
794
+ // it ever drains.
674
795
  const seen = new Set();
675
796
  const ordersToCorrect = (manager.ordersNeedingPriceCorrection || []).filter((c) => {
676
- if (!c.chainOrderId || seen.has(c.chainOrderId))
797
+ if (!c.chainOrderId)
677
798
  return false;
678
- seen.add(c.chainOrderId);
799
+ const key = `${c.chainOrderId}|${Boolean(c.isSurplus)}`;
800
+ if (seen.has(key))
801
+ return false;
802
+ seen.add(key);
679
803
  return true;
680
804
  });
681
- const canBatch = ordersToCorrect.length > 1
805
+ // Fix 1 — drain-time staleness validation for price-update entries:
806
+ // a geometry-changing resync between queue and drain leaves entries
807
+ // whose slot was re-slotted or re-priced. Broadcasting them would
808
+ // revert the resync's placement (duplicate-price-level incident).
809
+ // Validate against the LIVE slot; drop stale entries (the next sync
810
+ // re-queues if the order is genuinely still off-target). Cancel-type
811
+ // entries are exempt (idempotent, never re-price).
812
+ const liveEntries = [];
813
+ for (const entry of ordersToCorrect) {
814
+ const check = _validatePriceCorrectionEntry(manager, entry);
815
+ if (check.valid) {
816
+ liveEntries.push(entry);
817
+ continue;
818
+ }
819
+ _removeCorrectionEntry(manager, entry.chainOrderId, entry?.isSurplus);
820
+ staleDropped++;
821
+ results.push({ ...entry, result: { success: true, skipped: true, staleDropped: true, staleReason: check.reason } });
822
+ manager?.logger?.log?.(`[CORRECTION] Dropping stale price correction for ${entry.chainOrderId} ` +
823
+ `(queued ${entry.queuedBy || 'unknown-source'}@${entry.queuedAt ? new Date(entry.queuedAt).toISOString() : 'unknown-time'}): ` +
824
+ `${check.reason}; re-queued by next sync if still off-target`, 'info');
825
+ }
826
+ const canBatch = liveEntries.length > 1
682
827
  && typeof accountOrders?.buildCancelOrderOp === 'function'
683
828
  && typeof accountOrders?.executeBatch === 'function';
684
- let serialEntries = ordersToCorrect;
829
+ let serialEntries = liveEntries;
685
830
  if (canBatch) {
686
- const cancelEntries = ordersToCorrect.filter((c) => c.cancelOnly === true || c.isSurplus === true);
687
- const updateEntries = ordersToCorrect.filter((c) => !(c.cancelOnly === true || c.isSurplus === true));
831
+ const cancelEntries = liveEntries.filter((c) => c.cancelOnly === true || c.isSurplus === true);
832
+ const updateEntries = liveEntries.filter((c) => !(c.cancelOnly === true || c.isSurplus === true));
688
833
  if (cancelEntries.length > 1) {
689
834
  const batchOutcome = await _batchCancelCorrections(manager, cancelEntries, accountName, privateKey, accountOrders);
690
835
  corrected += batchOutcome.corrected;
@@ -708,7 +853,7 @@ async function correctAllPriceMismatches(manager, accountName, privateKey, accou
708
853
  if (corrected > 0 && typeof manager.persistGrid === 'function') {
709
854
  await manager.persistGrid();
710
855
  }
711
- return { corrected, failed, results };
856
+ return { corrected, failed, results, staleDropped };
712
857
  });
713
858
  }
714
859
  // ================================================================================
@@ -2828,6 +2973,6 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCr
2828
2973
  const all = new Set([...masterIds, ...createIds]);
2829
2974
  return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
2830
2975
  }
2831
- export { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, buildFillKey, correctOrderPriceOnChain, correctAllPriceMismatches, buildCreateOrderArgs, getOrderTypeFromUpdatedFlags, resolveConfiguredPriceBound, virtualizeOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, geometryTypeForSlotIndex, detectGapEvacuationCandidates, updateGapEvacuationStreaks, resolveSpreadOrderSide, chainOrderMatchesSlot, chainOrderMatchesSlotWithTolerance, crossingCandidateChainId, isCrossingCheckCandidate, buildCrossingCheckCandidates, parseSlotIndex, filterOrdersByType, buildOutsideInPairGroups, extractBatchOperationResults, formatUnmatchedChainOrder, isNonBlockingUnmatchedOrder, isStrandedHoldOrder, isOrderOnChain, isOrderVirtual, hasOnChainId, isOrderPlaced, isPhantomOrder, isSlotAvailable, isEmptyGridSlot, isOrderHealthy, checkSizeThreshold, checkSizesBeforeMinimum, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType, shouldFlagOutOfSpread, buildIndexes, validateIndexes, ordersEqual, buildDelta, deriveTargetBoundary, isShiftEligibleFill, resolveReserveCount, resolveReserveOrders, selectReserveEdgeSlots, getActiveOrdersTotal, getSideBudget, calculateBudgetedSizes, buildCreateOpFingerprint, isOrderGoneErrorMessage, recordDuplicateOrphanDetection, clearDuplicateOrphanDetection, duplicateOrphanLogInfo, chainOrderUnchangedFromCache, detectCrossedBookPlan, collectKnownOnChainOrderIds, reserveEdgeIdSet, liveWindowIdSet, checkGridPriceInvariant, reportGridPriceInvariant };
2976
+ export { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, buildFillKey, correctOrderPriceOnChain, correctAllPriceMismatches, _validatePriceCorrectionEntry, _stampCorrectionProvenance, buildCreateOrderArgs, getOrderTypeFromUpdatedFlags, resolveConfiguredPriceBound, virtualizeOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, geometryTypeForSlotIndex, detectGapEvacuationCandidates, updateGapEvacuationStreaks, resolveSpreadOrderSide, chainOrderMatchesSlot, chainOrderMatchesSlotWithTolerance, crossingCandidateChainId, isCrossingCheckCandidate, buildCrossingCheckCandidates, parseSlotIndex, filterOrdersByType, buildOutsideInPairGroups, extractBatchOperationResults, formatUnmatchedChainOrder, isNonBlockingUnmatchedOrder, isStrandedHoldOrder, isOrderOnChain, isOrderVirtual, hasOnChainId, isOrderPlaced, isPhantomOrder, isSlotAvailable, isEmptyGridSlot, isOrderHealthy, checkSizeThreshold, checkSizesBeforeMinimum, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType, shouldFlagOutOfSpread, buildIndexes, validateIndexes, ordersEqual, buildDelta, deriveTargetBoundary, isShiftEligibleFill, resolveReserveCount, resolveReserveOrders, selectReserveEdgeSlots, getActiveOrdersTotal, getSideBudget, calculateBudgetedSizes, buildCreateOpFingerprint, isOrderGoneErrorMessage, recordDuplicateOrphanDetection, clearDuplicateOrphanDetection, duplicateOrphanLogInfo, chainOrderUnchangedFromCache, detectCrossedBookPlan, collectKnownOnChainOrderIds, reserveEdgeIdSet, liveWindowIdSet, checkGridPriceInvariant, reportGridPriceInvariant };
2832
2977
  export { resolveReserveEdgeAnchorPrice, resolveLiveReserveEdgeAnchorPrice, compareReserveEdge, collectRefillSlotIds };
2833
2978
  //# sourceMappingURL=order.js.map