dexbot 1.6.1 → 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 (104) hide show
  1. package/CHANGELOG.md +34 -0
  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/tradingview/tradingview_uplot_chart_generator.ts +19 -5
  10. package/analysis/trend_detection/package.json +1 -1
  11. package/claw/package.json +1 -1
  12. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  13. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  14. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  15. package/dist/analysis/grid_correction_check.d.ts +5 -1
  16. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  17. package/dist/analysis/grid_correction_check.js +81 -7
  18. package/dist/analysis/grid_correction_check.js.map +1 -1
  19. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  20. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +19 -5
  21. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  22. package/dist/market_adapter/inputs/fetch_book_data.js +2 -2
  23. package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -1
  24. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +22 -3
  25. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  26. package/dist/market_adapter/inputs/fetch_lp_data.js +15 -15
  27. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  28. package/dist/market_adapter/inputs/kibana_feed_source.js +2 -2
  29. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  30. package/dist/market_adapter/inputs/window_cache.d.ts +54 -8
  31. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  32. package/dist/market_adapter/inputs/window_cache.js +328 -87
  33. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  34. package/dist/modules/bitshares-native/subscriptions.d.ts +1 -1
  35. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  36. package/dist/modules/bitshares-native/subscriptions.js +18 -4
  37. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  38. package/dist/modules/bitshares_client.d.ts.map +1 -1
  39. package/dist/modules/bitshares_client.js +10 -0
  40. package/dist/modules/bitshares_client.js.map +1 -1
  41. package/dist/modules/constants.d.ts +7 -3
  42. package/dist/modules/constants.d.ts.map +1 -1
  43. package/dist/modules/constants.js +44 -20
  44. package/dist/modules/constants.js.map +1 -1
  45. package/dist/modules/credit_runtime.d.ts.map +1 -1
  46. package/dist/modules/credit_runtime.js +24 -6
  47. package/dist/modules/credit_runtime.js.map +1 -1
  48. package/dist/modules/dexbot_class.d.ts +32 -7
  49. package/dist/modules/dexbot_class.d.ts.map +1 -1
  50. package/dist/modules/dexbot_class.js +55 -11
  51. package/dist/modules/dexbot_class.js.map +1 -1
  52. package/dist/modules/dexbot_cow_runtime.d.ts +137 -7
  53. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  54. package/dist/modules/dexbot_cow_runtime.js +955 -45
  55. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  56. package/dist/modules/dexbot_fill_runtime.d.ts +28 -1
  57. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  58. package/dist/modules/dexbot_fill_runtime.js +116 -3
  59. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  60. package/dist/modules/dexbot_maintenance_runtime.d.ts +46 -0
  61. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  62. package/dist/modules/dexbot_maintenance_runtime.js +193 -6
  63. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  64. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  65. package/dist/modules/dexbot_startup_runtime.js +6 -1
  66. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  67. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  68. package/dist/modules/dexbot_state_recovery.js +16 -3
  69. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  70. package/dist/modules/order/grid.d.ts +3 -1
  71. package/dist/modules/order/grid.d.ts.map +1 -1
  72. package/dist/modules/order/grid.js +47 -6
  73. package/dist/modules/order/grid.js.map +1 -1
  74. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  75. package/dist/modules/order/grid_reconcile_internal.js +39 -4
  76. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  77. package/dist/modules/order/manager.d.ts.map +1 -1
  78. package/dist/modules/order/manager.js +3 -1
  79. package/dist/modules/order/manager.js.map +1 -1
  80. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  81. package/dist/modules/order/sync_engine.js +200 -15
  82. package/dist/modules/order/sync_engine.js.map +1 -1
  83. package/dist/modules/order/utils/math.d.ts +60 -14
  84. package/dist/modules/order/utils/math.d.ts.map +1 -1
  85. package/dist/modules/order/utils/math.js +87 -20
  86. package/dist/modules/order/utils/math.js.map +1 -1
  87. package/dist/modules/order/utils/order.d.ts +95 -7
  88. package/dist/modules/order/utils/order.d.ts.map +1 -1
  89. package/dist/modules/order/utils/order.js +277 -15
  90. package/dist/modules/order/utils/order.js.map +1 -1
  91. package/dist/modules/utils/errors.d.ts +22 -0
  92. package/dist/modules/utils/errors.d.ts.map +1 -1
  93. package/dist/modules/utils/errors.js +30 -0
  94. package/dist/modules/utils/errors.js.map +1 -1
  95. package/docs/COPY_ON_WRITE_MASTER_PLAN.md +1 -1
  96. package/docs/DEXBOT_COMPARISON.md +3 -7
  97. package/docs/EVOLUTION.md +9 -7
  98. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -9
  99. package/docs/LIFECYCLE.md +2 -2
  100. package/docs/README.md +9 -1
  101. package/docs/architecture.md +7 -7
  102. package/docs/developer_guide.md +4 -4
  103. package/market_adapter/README.md +1 -1
  104. 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)
798
+ return false;
799
+ const key = `${c.chainOrderId}|${Boolean(c.isSurplus)}`;
800
+ if (seen.has(key))
677
801
  return false;
678
- seen.add(c.chainOrderId);
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
  // ================================================================================
@@ -1269,6 +1414,123 @@ function isNonBlockingUnmatchedOrder(order) {
1269
1414
  const reason = order?.reason;
1270
1415
  return typeof reason === 'string' && reason.endsWith('-deferred');
1271
1416
  }
1417
+ /**
1418
+ * Reasons a deferred hold is a candidate for STRUCTURAL RESYNC escalation.
1419
+ *
1420
+ * Deliberately narrower than `isNonBlockingUnmatchedOrder`. That predicate is
1421
+ * a broad "do not treat this as a blocker" net (by design, so a new defer
1422
+ * reason cannot silently freeze the grid). Escalation asks a stricter
1423
+ * question: can a structural resync plausibly resolve this hold?
1424
+ *
1425
+ * The distinction matters because a resync cannot end a broadcast region or
1426
+ * re-evaluate a boundary that has not committed. Escalating on those holds
1427
+ * spends a full grid reload (and possibly a reset) on something the owning
1428
+ * machinery already resolves on its own. Excluded deliberately:
1429
+ * - `broadcast-active-deferred`: transient; "rebalance retries after the
1430
+ * region ends". Self-resolving, and unaffected by a resync.
1431
+ * - `boundary-hold-trailing-market`: owned by boundary/trailing-market
1432
+ * machinery, which re-evaluates when the market moves.
1433
+ * - `held-plan-unchanged-deferred`: an identical replan was suppressed
1434
+ * because nothing changed; it clears on the next fill, not on a reload.
1435
+ * - `boundary-unknown-deferred`: gap geometry is unknown pre-boundary; the
1436
+ * comment at sync_engine.ts names the accepted cost as one sync cycle
1437
+ * after the boundary commits. Self-resolving, not stranded.
1438
+ *
1439
+ * Anything not listed is treated as NOT escalatable, so a future transient
1440
+ * reason is excluded by default rather than silently becoming a resync
1441
+ * trigger.
1442
+ */
1443
+ const STRANDED_HOLD_REASONS = new Set([
1444
+ 'out-of-rail-deferred',
1445
+ 'out-of-grid-deferred',
1446
+ ]);
1447
+ /**
1448
+ * @param {Object} order - Unmatched chain order entry.
1449
+ * @returns {boolean} True when the hold is stranded and a structural resync is
1450
+ * a plausible remedy (see STRANDED_HOLD_REASONS).
1451
+ */
1452
+ function isStrandedHoldOrder(order) {
1453
+ const reason = order?.reason;
1454
+ return typeof reason === 'string' && STRANDED_HOLD_REASONS.has(reason);
1455
+ }
1456
+ /**
1457
+ * GRID-PRICE-INVARIANT — the price emitted for a slot must be its genesis
1458
+ * level. Range guards (isChainPriceOutOfGrid) only test the configured min/max
1459
+ * bounds, so an off-grid price can sit inside the bounds while being far
1460
+ * outside the active window; that band has no check unless this one runs.
1461
+ *
1462
+ * Unjudgeable inputs evaluate to ok:true (missing genesis, unparseable slot id,
1463
+ * non-finite price) — the guard must never fire on bad metadata, only on a
1464
+ * genuine mismatch. Never throws.
1465
+ *
1466
+ * See docs/GRID_PRICE_INVARIANT.md.
1467
+ *
1468
+ * @param {string} slotId - Grid slot id (slot-<idx>)
1469
+ * @param {number} price - Price about to be emitted
1470
+ * @param {any} genesis - manager._genesis (priceLevels table)
1471
+ * @returns {{ok: boolean, reason: string, expected: number|null, idx: number|null, drift: number|null}}
1472
+ */
1473
+ function checkGridPriceInvariant(slotId, price, genesis) {
1474
+ const pass = { ok: true, expected: null, idx: null, drift: null };
1475
+ try {
1476
+ if (!genesis || !Array.isArray(genesis?.priceLevels) || genesis.priceLevels.length === 0) {
1477
+ return { ...pass, reason: 'no-genesis' };
1478
+ }
1479
+ const idx = (typeof slotId === 'string') ? parseSlotIndex(slotId) : null;
1480
+ if (idx === null || !Number.isFinite(idx) || idx < 0 || idx >= genesis.priceLevels.length) {
1481
+ return { ...pass, reason: 'uncheckable-slot' };
1482
+ }
1483
+ const p = Number(price);
1484
+ if (!Number.isFinite(p) || p <= 0)
1485
+ return { ...pass, reason: 'invalid-price', idx };
1486
+ const expected = Number(MathUtils.priceForSlot(idx, genesis));
1487
+ if (!Number.isFinite(expected) || expected <= 0)
1488
+ return { ...pass, reason: 'invalid-level', idx };
1489
+ // Same equality as assertSlotPriceInvariant at grid build/load:
1490
+ // relative 1e-9 with an absolute floor.
1491
+ const diff = Math.abs(p - expected);
1492
+ const rel = diff / Math.max(1e-12, Math.abs(expected));
1493
+ const ok = !(rel > 1e-9 && diff > 1e-12);
1494
+ return { ok, reason: ok ? 'ok' : 'off-grid-price', expected, idx, drift: rel };
1495
+ }
1496
+ catch {
1497
+ return { ...pass, reason: 'check-failed' };
1498
+ }
1499
+ }
1500
+ /**
1501
+ * Report one GRID-PRICE-INVARIANT check. Never throws: an unjudgeable check
1502
+ * returns true so a bad check can never block a legitimate emission. Warns on a
1503
+ * mismatch so the offending slot, site, expected level and drift are named at
1504
+ * the point of emission. Silent when the check passes or the price is not
1505
+ * checkable.
1506
+ *
1507
+ * Returns the check result so calling sites can choose their policy: the
1508
+ * reconcile sites treat `false` as blocking (skip the emission and let the next
1509
+ * cycle re-plan). See docs/GRID_PRICE_INVARIANT.md.
1510
+ *
1511
+ * @param {any} manager - OrderManager (reads _genesis, writes logger)
1512
+ * @param {string} slotId - Destination slot id whose level the price must match
1513
+ * @param {number} price - Price about to be emitted
1514
+ * @param {string} site - Emitting site label (CREATE / UPDATE / RECONCILE-*)
1515
+ * @returns {boolean} True when the price is acceptable (or unjudgeable). Callers
1516
+ * at the reconcile sites treat `false` as a blocking mismatch and skip the
1517
+ * emission, so this must return the check result rather than void.
1518
+ */
1519
+ function reportGridPriceInvariant(manager, slotId, price, site) {
1520
+ try {
1521
+ const inv = checkGridPriceInvariant(slotId, price, manager?._genesis);
1522
+ if (inv.ok)
1523
+ return true;
1524
+ manager?.logger?.log?.(`[GRID-PRICE-INVARIANT] ${site} for ${slotId} at ${Format.formatPrice6(Number(price))} ` +
1525
+ `is NOT the genesis level ${inv.expected != null ? Format.formatPrice6(inv.expected) : 'n/a'} ` +
1526
+ `(slot idx ${inv.idx}, drift ${inv.drift != null ? (inv.drift * 100).toFixed(4) + '%' : 'n/a'}) ` +
1527
+ `— off-grid price SKIPPED (emission blocked at this site)`, 'warn');
1528
+ return false;
1529
+ }
1530
+ catch { /* invariant check is best-effort: never block on a thrown check */
1531
+ return true;
1532
+ }
1533
+ }
1272
1534
  /**
1273
1535
  * Check if order is on blockchain (ACTIVE or PARTIAL state).
1274
1536
  *
@@ -2711,6 +2973,6 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCr
2711
2973
  const all = new Set([...masterIds, ...createIds]);
2712
2974
  return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
2713
2975
  }
2714
- 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, 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 };
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 };
2715
2977
  export { resolveReserveEdgeAnchorPrice, resolveLiveReserveEdgeAnchorPrice, compareReserveEdge, collectRefillSlotIds };
2716
2978
  //# sourceMappingURL=order.js.map