dexbot 1.4.25 → 1.5.0

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 (182) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +6 -2
  3. package/analysis/README.md +14 -7
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_derivatives.ts +1 -1
  6. package/analysis/bot_key_utils.ts +272 -3
  7. package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
  8. package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
  9. package/analysis/chart_utils.ts +6 -3
  10. package/analysis/grid_correction_check.ts +56 -20
  11. package/analysis/resolve_bot_accounts.ts +199 -0
  12. package/analysis/trade_profitability.ts +36 -8
  13. package/analysis/tradingview/README.md +25 -0
  14. package/analysis/tradingview/analyze_tradingview.ts +2 -2
  15. package/analysis/trend_detection/package.json +1 -1
  16. package/claw/modules/claw_launcher.ts +5 -4
  17. package/claw/modules/credit_runtime_adapter.ts +2 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_key_utils.d.ts +28 -1
  25. package/dist/analysis/bot_key_utils.d.ts.map +1 -1
  26. package/dist/analysis/bot_key_utils.js +385 -3
  27. package/dist/analysis/bot_key_utils.js.map +1 -1
  28. package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
  29. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chart_utils.d.ts +2 -1
  35. package/dist/analysis/chart_utils.d.ts.map +1 -1
  36. package/dist/analysis/chart_utils.js +4 -1
  37. package/dist/analysis/chart_utils.js.map +1 -1
  38. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  39. package/dist/analysis/grid_correction_check.js +60 -20
  40. package/dist/analysis/grid_correction_check.js.map +1 -1
  41. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  43. package/dist/analysis/resolve_bot_accounts.js +216 -0
  44. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  45. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  46. package/dist/analysis/trade_profitability.js +40 -8
  47. package/dist/analysis/trade_profitability.js.map +1 -1
  48. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  49. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  50. package/dist/bot.d.ts.map +1 -1
  51. package/dist/bot.js +2 -1
  52. package/dist/bot.js.map +1 -1
  53. package/dist/dexbot.d.ts.map +1 -1
  54. package/dist/dexbot.js +73 -17
  55. package/dist/dexbot.js.map +1 -1
  56. package/dist/market_adapter/ama_signal_runner.js +2 -2
  57. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  59. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  62. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  63. package/dist/modules/account_bots.d.ts +42 -1
  64. package/dist/modules/account_bots.d.ts.map +1 -1
  65. package/dist/modules/account_bots.js +168 -17
  66. package/dist/modules/account_bots.js.map +1 -1
  67. package/dist/modules/bitshares_client.d.ts +4 -1
  68. package/dist/modules/bitshares_client.d.ts.map +1 -1
  69. package/dist/modules/bitshares_client.js +12 -4
  70. package/dist/modules/bitshares_client.js.map +1 -1
  71. package/dist/modules/bot_settings.d.ts.map +1 -1
  72. package/dist/modules/bot_settings.js +2 -1
  73. package/dist/modules/bot_settings.js.map +1 -1
  74. package/dist/modules/cli_colors.d.ts +39 -0
  75. package/dist/modules/cli_colors.d.ts.map +1 -0
  76. package/dist/modules/cli_colors.js +42 -0
  77. package/dist/modules/cli_colors.js.map +1 -0
  78. package/dist/modules/constants.d.ts.map +1 -1
  79. package/dist/modules/constants.js +15 -11
  80. package/dist/modules/constants.js.map +1 -1
  81. package/dist/modules/credit_pricing.d.ts +61 -0
  82. package/dist/modules/credit_pricing.d.ts.map +1 -0
  83. package/dist/modules/credit_pricing.js +237 -0
  84. package/dist/modules/credit_pricing.js.map +1 -0
  85. package/dist/modules/credit_runtime.d.ts +1 -0
  86. package/dist/modules/credit_runtime.d.ts.map +1 -1
  87. package/dist/modules/credit_runtime.js +49 -107
  88. package/dist/modules/credit_runtime.js.map +1 -1
  89. package/dist/modules/dexbot_class.d.ts +2 -1
  90. package/dist/modules/dexbot_class.d.ts.map +1 -1
  91. package/dist/modules/dexbot_class.js +22 -6
  92. package/dist/modules/dexbot_class.js.map +1 -1
  93. package/dist/modules/dexbot_cow_runtime.d.ts +19 -2
  94. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  95. package/dist/modules/dexbot_cow_runtime.js +249 -26
  96. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  97. package/dist/modules/dexbot_maintenance_runtime.d.ts +15 -16
  98. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  99. package/dist/modules/dexbot_maintenance_runtime.js +69 -42
  100. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  101. package/dist/modules/fund_registry.d.ts +1 -1
  102. package/dist/modules/fund_registry.js +1 -1
  103. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  104. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  105. package/dist/modules/launcher/adapter_requirement.js +141 -0
  106. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  107. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  108. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  109. package/dist/modules/launcher/bot_supervisor.js +32 -3
  110. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  111. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  112. package/dist/modules/launcher/launch_modes.js +5 -4
  113. package/dist/modules/launcher/launch_modes.js.map +1 -1
  114. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  115. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  116. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  117. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  118. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  119. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  120. package/dist/modules/launcher/status_reporting.js +9 -6
  121. package/dist/modules/launcher/status_reporting.js.map +1 -1
  122. package/dist/modules/order/export.d.ts +1 -1
  123. package/dist/modules/order/export.js +1 -1
  124. package/dist/modules/order/grid.d.ts.map +1 -1
  125. package/dist/modules/order/grid.js +47 -12
  126. package/dist/modules/order/grid.js.map +1 -1
  127. package/dist/modules/order/logger.d.ts +23 -1
  128. package/dist/modules/order/logger.d.ts.map +1 -1
  129. package/dist/modules/order/logger.js +60 -17
  130. package/dist/modules/order/logger.js.map +1 -1
  131. package/dist/modules/order/manager.d.ts.map +1 -1
  132. package/dist/modules/order/manager.js +43 -3
  133. package/dist/modules/order/manager.js.map +1 -1
  134. package/dist/modules/order/sync_engine.js +1 -1
  135. package/dist/modules/order/utils/system.d.ts +52 -4
  136. package/dist/modules/order/utils/system.d.ts.map +1 -1
  137. package/dist/modules/order/utils/system.js +106 -14
  138. package/dist/modules/order/utils/system.js.map +1 -1
  139. package/dist/modules/order/utils/validate.d.ts +1 -1
  140. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  141. package/dist/modules/order/utils/validate.js +43 -3
  142. package/dist/modules/order/utils/validate.js.map +1 -1
  143. package/dist/modules/utils/chain_logs.d.ts +11 -0
  144. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  145. package/dist/modules/utils/chain_logs.js +23 -0
  146. package/dist/modules/utils/chain_logs.js.map +1 -0
  147. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  148. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  149. package/dist/modules/utils/sanitize_key.js +15 -0
  150. package/dist/modules/utils/sanitize_key.js.map +1 -1
  151. package/dist/pm2.d.ts +11 -1
  152. package/dist/pm2.d.ts.map +1 -1
  153. package/dist/pm2.js +66 -15
  154. package/dist/pm2.js.map +1 -1
  155. package/dist/scripts/analyze-credit.d.ts +4 -0
  156. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  157. package/dist/scripts/analyze-credit.js +760 -0
  158. package/dist/scripts/analyze-credit.js.map +1 -0
  159. package/dist/scripts/analyze-orders.d.ts +1 -10
  160. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  161. package/dist/scripts/analyze-orders.js +11 -34
  162. package/dist/scripts/analyze-orders.js.map +1 -1
  163. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  164. package/dist/scripts/test-credit-renewal.js +37 -5
  165. package/dist/scripts/test-credit-renewal.js.map +1 -1
  166. package/dist/scripts/tv.d.ts +11 -0
  167. package/dist/scripts/tv.d.ts.map +1 -0
  168. package/dist/scripts/tv.js +357 -0
  169. package/dist/scripts/tv.js.map +1 -0
  170. package/dist/scripts/update.js +77 -10
  171. package/dist/scripts/update.js.map +1 -1
  172. package/dist/unlock.d.ts.map +1 -1
  173. package/dist/unlock.js +20 -8
  174. package/dist/unlock.js.map +1 -1
  175. package/docs/DEXBOT_COMPARISON.md +3 -3
  176. package/docs/EVOLUTION.md +7 -6
  177. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  178. package/docs/MPA_CREDIT_USAGE.md +3 -1
  179. package/docs/README.md +2 -2
  180. package/docs/WORKFLOW.md +3 -1
  181. package/package.json +2 -1
  182. package/scripts/README.md +12 -0
@@ -1445,7 +1445,8 @@ function validateOrderSizeForExecution(bot, size, type, orderLike = null, fallba
1445
1445
  * LAST-FILL-GUARD helper — pivot ± halfIncrement (replaces price-tolerance).
1446
1446
  * last fill @x with increment i: BUY < x*(1 - i/2/100), SELL > x*(1 + i/2/100)
1447
1447
  * e.g. x=1000, i=0.5% => BUY < 997.5, SELL > 1002.5
1448
- * Cold (pivot null or lastType null) => disabled. Spread correction bypasses via cowResult.origin === 'spread-correction'.
1448
+ * Cold (pivot null or lastType null) => disabled. Spread-correction CREATES
1449
+ * bypass per-action (see broadcast sites); rotations never bypass.
1449
1450
  * @param {number} price - Target order price
1450
1451
  * @param {number} size - Order size (unused, kept for compat)
1451
1452
  * @param {string} type - ORDER_TYPES.BUY/SELL
@@ -1478,6 +1479,123 @@ function isLastFillGuardBlocked(price, _size, type, lastPrice, lastType, increme
1478
1479
  return { blocked: true, pivot, halfInc, threshold: sellThreshold };
1479
1480
  return { blocked: false, pivot: null, halfInc, threshold: null };
1480
1481
  }
1482
+ /**
1483
+ * Refresh the LAST-FILL guard pivot from fills that arrived while the current
1484
+ * batch was being planned or broadcast but have not been ingested yet.
1485
+ *
1486
+ * The pivot (_lastFilledPrice) is recorded per fill-processing batch, but a
1487
+ * multi-chunk COW broadcast spans tens of seconds — fills detected mid-cycle
1488
+ * sit in _incomingFillQueue until the cycle ends, so ops built for later
1489
+ * chunks would be checked against a stale pivot (production incident: a chunk
1490
+ * planned with a pre-crash pivot placed asks 0.6% below the true latest fill
1491
+ * that was already queued). Peek-only: never drains the queue, the owning
1492
+ * fill cycle still processes every entry. Best-effort: returns false when no
1493
+ * queued fill yields a usable side + price.
1494
+ * @param {import('./dexbot_class.js').DEXBot} bot
1495
+ * @returns {boolean} True when the pivot was refreshed from queued fills
1496
+ */
1497
+ function refreshLastFillPivotFromQueue(bot) {
1498
+ try {
1499
+ const queue = bot?._incomingFillQueue;
1500
+ if (!Array.isArray(queue) || queue.length === 0)
1501
+ return false;
1502
+ const mgr = bot?.manager;
1503
+ if (!mgr || !mgr.orders)
1504
+ return false;
1505
+ const assets = mgr.assets;
1506
+ const findSlotByOrderId = (orderId) => {
1507
+ if (!orderId)
1508
+ return null;
1509
+ try {
1510
+ for (const o of mgr.orders.values()) {
1511
+ if (o?.orderId === orderId)
1512
+ return o;
1513
+ }
1514
+ }
1515
+ catch { /* ignore iteration errors */ }
1516
+ return null;
1517
+ };
1518
+ let latest = null;
1519
+ for (const fill of queue) {
1520
+ const fillOp = fill?.op?.[1] || fill;
1521
+ const orderId = fillOp?.order_id || fill?.orderId;
1522
+ let type = null;
1523
+ let price = null;
1524
+ // Prefer the grid slot: a limit fill executes at (or better than)
1525
+ // its slot price, which is exactly the guard's price convention.
1526
+ const slot = findSlotByOrderId(orderId);
1527
+ if (slot && (slot.type === ORDER_TYPES.BUY || slot.type === ORDER_TYPES.SELL)) {
1528
+ type = slot.type;
1529
+ const slotPrice = Number(slot.price);
1530
+ if (Number.isFinite(slotPrice) && slotPrice > 0)
1531
+ price = slotPrice;
1532
+ }
1533
+ // Fall back to fill economics (B/A convention, mirroring
1534
+ // _computeFillContext's pays-asset side resolution, including
1535
+ // SPREAD slots carrying on-chain orders).
1536
+ if ((type == null || price == null) && fillOp?.pays && fillOp?.receives && assets?.assetA && assets?.assetB) {
1537
+ try {
1538
+ const paysId = fillOp.pays.asset_id;
1539
+ const paysAmt = Number(fillOp.pays.amount);
1540
+ const recvAmt = Number(fillOp.receives.amount);
1541
+ if (Number.isFinite(paysAmt) && paysAmt > 0 && Number.isFinite(recvAmt) && recvAmt > 0) {
1542
+ const paysFloat = (base, precision) => base / Math.pow(10, precision);
1543
+ if (paysId === assets.assetA.id) {
1544
+ type = ORDER_TYPES.SELL;
1545
+ price = paysFloat(recvAmt, assets.assetB.precision) / paysFloat(paysAmt, assets.assetA.precision);
1546
+ }
1547
+ else if (paysId === assets.assetB.id) {
1548
+ type = ORDER_TYPES.BUY;
1549
+ price = paysFloat(paysAmt, assets.assetB.precision) / paysFloat(recvAmt, assets.assetA.precision);
1550
+ }
1551
+ }
1552
+ }
1553
+ catch { /* best-effort */ }
1554
+ }
1555
+ if ((type === ORDER_TYPES.BUY || type === ORDER_TYPES.SELL)
1556
+ && Number.isFinite(price) && price > 0) {
1557
+ latest = { price: price, type };
1558
+ }
1559
+ }
1560
+ if (!latest)
1561
+ return false;
1562
+ mgr._lastFilledPrice = latest.price;
1563
+ mgr._lastFilledType = latest.type;
1564
+ if (latest.type === ORDER_TYPES.BUY) {
1565
+ mgr._lastFilledBuyPrice = latest.price;
1566
+ }
1567
+ else {
1568
+ mgr._lastFilledSellPrice = latest.price;
1569
+ }
1570
+ try {
1571
+ mgr.logger?.log?.(`[LAST-FILL-GUARD] Pivot refreshed from ${queue.length} pending queued fill(s): ` +
1572
+ `${latest.type} @${Format.formatPrice6(latest.price)}`, 'debug');
1573
+ }
1574
+ catch { /* ignore logging errors */ }
1575
+ return true;
1576
+ }
1577
+ catch {
1578
+ return false;
1579
+ }
1580
+ }
1581
+ /**
1582
+ * Resolve the grid increment percent for the LAST-FILL guard in one place so
1583
+ * every check site and the batch summary use (and print) the same value.
1584
+ * Falls back to the default 0.5 when unset/invalid.
1585
+ * @param {import('./dexbot_class.js').DEXBot} bot
1586
+ * @returns {number} Positive increment percent
1587
+ */
1588
+ function resolveLastFillGuardIncrement(bot) {
1589
+ const raw = Number(bot?.manager?.config?.incrementPercent
1590
+ ?? bot?.config?.incrementPercent
1591
+ ?? constantsModule?.DEFAULT_CONFIG?.incrementPercent
1592
+ ?? 0.5);
1593
+ if (Number.isFinite(raw) && raw > 0)
1594
+ return raw;
1595
+ return Number(constantsModule?.DEFAULT_CONFIG?.incrementPercent) > 0
1596
+ ? Number(constantsModule?.DEFAULT_CONFIG?.incrementPercent)
1597
+ : 0.5;
1598
+ }
1481
1599
  /**
1482
1600
  * Build COW actions array from a simple plan object.
1483
1601
  * @param {import('./dexbot_class.js').DEXBot} bot
@@ -1489,6 +1607,12 @@ function buildActionsFromPlan(_bot, plan) {
1489
1607
  ? { ordersToPlace: plan }
1490
1608
  : (plan || {});
1491
1609
  const { ordersToPlace = [], ordersToRotate = [], ordersToUpdate = [], ordersToCancel = [] } = normalizedPlan;
1610
+ // Per-action origin: guard bypasses are scoped per action (not per batch),
1611
+ // so a future rotation entry merged into a correction plan cannot silently
1612
+ // inherit the spread-correction bypass. Actions built outside this helper
1613
+ // carry no origin and default to guarded (safe default).
1614
+ const planOrigin = normalizedPlan?.origin;
1615
+ const withOrigin = (action) => (planOrigin ? { ...action, origin: planOrigin } : action);
1492
1616
  const actions = [];
1493
1617
  for (const o of ordersToCancel) {
1494
1618
  if (o?.orderId) {
@@ -1509,7 +1633,7 @@ function buildActionsFromPlan(_bot, plan) {
1509
1633
  const orderType = r?.type || oldOrder?.type;
1510
1634
  if (!id || !orderId || !newGridId || !orderType || !Number.isFinite(newPrice) || !(newSize > 0))
1511
1635
  continue;
1512
- actions.push({
1636
+ actions.push(withOrigin({
1513
1637
  type: COW_ACTIONS.UPDATE,
1514
1638
  id,
1515
1639
  orderId,
@@ -1522,7 +1646,7 @@ function buildActionsFromPlan(_bot, plan) {
1522
1646
  price: newPrice,
1523
1647
  size: newSize
1524
1648
  }
1525
- });
1649
+ }));
1526
1650
  }
1527
1651
  for (const o of ordersToUpdate) {
1528
1652
  const partialOrder = o?.partialOrder || o;
@@ -1534,7 +1658,7 @@ function buildActionsFromPlan(_bot, plan) {
1534
1658
  : Number(partialOrder?.size || 0);
1535
1659
  if (!id || !orderId)
1536
1660
  continue;
1537
- actions.push({
1661
+ actions.push(withOrigin({
1538
1662
  type: COW_ACTIONS.UPDATE,
1539
1663
  id,
1540
1664
  orderId,
@@ -1546,12 +1670,12 @@ function buildActionsFromPlan(_bot, plan) {
1546
1670
  type: orderType,
1547
1671
  size: newSize
1548
1672
  }
1549
- });
1673
+ }));
1550
1674
  }
1551
1675
  for (const o of ordersToPlace) {
1552
1676
  if (!o?.id)
1553
1677
  continue;
1554
- actions.push({ type: COW_ACTIONS.CREATE, id: o.id, order: o });
1678
+ actions.push(withOrigin({ type: COW_ACTIONS.CREATE, id: o.id, order: o }));
1555
1679
  }
1556
1680
  return actions;
1557
1681
  }
@@ -2343,6 +2467,14 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2343
2467
  const opContexts = [];
2344
2468
  const skippedUpdateSlotIds = new Set();
2345
2469
  let skippedUpdateCount = 0;
2470
+ // Per-batch LAST-FILL-GUARD disposition counters. Per-action pass lines
2471
+ // would spam big batches, so the guard emits one batch summary instead
2472
+ // (see the summary after the action loop below).
2473
+ const lastFillGuardStats = { checked: 0, passed: 0, skipped: 0, bypassed: 0 };
2474
+ // Whether any guard check in this batch refreshed the pivot from
2475
+ // still-queued fills — reported in the batch summary so a pivot change
2476
+ // that altered a guard decision is visible at info, not just debug.
2477
+ let lastFillGuardPivotRefreshed = false;
2346
2478
  // Slots whose size-update op was broadcast with a post-fill-clamped
2347
2479
  // target: the working grid still holds the planned (larger) size, so the
2348
2480
  // slots are re-synced from master before commit to keep the committed
@@ -2458,18 +2590,35 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2458
2590
  continue;
2459
2591
  }
2460
2592
  // LAST-FILL PRICE GUARD: pivot ± halfIncrement (BUY < pivot*(1-half), SELL > pivot*(1+half)).
2461
- // Spread correction bypasses so gap repair can close. Cold (null) => disabled.
2593
+ // Bypass is per-action: only spread-correction CREATES skip so gap repair can close.
2594
+ // The batch-level origin is honored only for actions without their own origin stamp
2595
+ // (back-compat for plans that bypass buildActionsFromPlan). Cold (null) => disabled.
2462
2596
  try {
2463
- if (cowResult?.origin !== 'spread-correction') {
2597
+ const actionOrigin = action?.origin;
2598
+ const batchOrigin = cowResult?.origin;
2599
+ const isCorrectionCreate = actionOrigin === 'spread-correction'
2600
+ || (actionOrigin == null && batchOrigin === 'spread-correction');
2601
+ if (!isCorrectionCreate) {
2602
+ try {
2603
+ if (refreshLastFillPivotFromQueue(bot))
2604
+ lastFillGuardPivotRefreshed = true;
2605
+ }
2606
+ catch { /* best-effort */ }
2464
2607
  const lastPrice = bot.manager?._lastFilledPrice;
2465
2608
  const lastType = bot.manager?._lastFilledType;
2466
- const inc = Number(bot.manager?.config?.incrementPercent ?? bot?.config?.incrementPercent ?? constantsModule?.DEFAULT_CONFIG?.incrementPercent ?? 0.5);
2609
+ const inc = resolveLastFillGuardIncrement(bot);
2467
2610
  const check = isLastFillGuardBlocked(createPrice, order.size, order.type, lastPrice, lastType, inc);
2611
+ lastFillGuardStats.checked++;
2468
2612
  if (check.blocked) {
2613
+ lastFillGuardStats.skipped++;
2469
2614
  const dir = order.type === ORDER_TYPES.BUY ? 'above' : 'below';
2470
2615
  bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${order.type} CREATE for ${order.id} at ${Format.formatPrice6(createPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'warn');
2471
2616
  continue;
2472
2617
  }
2618
+ lastFillGuardStats.passed++;
2619
+ }
2620
+ else {
2621
+ lastFillGuardStats.bypassed++;
2473
2622
  }
2474
2623
  }
2475
2624
  catch (_e) { /* guard is best-effort */ }
@@ -2564,23 +2713,34 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2564
2713
  continue;
2565
2714
  }
2566
2715
  // LAST-FILL PRICE GUARD (UPDATE rotation): pivot ± halfIncrement — same as CREATE.
2716
+ // No origin bypass here, ever: a rotation reprices, so it always obeys the guard —
2717
+ // even if a future correction plan carries rotation entries (per-action scoping).
2718
+ // The pivot is refreshed from still-queued fills first: fills detected mid-broadcast
2719
+ // sit in _incomingFillQueue until the fill cycle ends, and without this the later
2720
+ // chunks of a long broadcast would be checked against a stale pivot.
2567
2721
  try {
2568
- if (cowResult?.origin !== 'spread-correction') {
2569
- const lastPrice = bot.manager?._lastFilledPrice;
2570
- const lastType = bot.manager?._lastFilledType;
2571
- const inc = Number(bot.manager?.config?.incrementPercent ?? bot?.config?.incrementPercent ?? constantsModule?.DEFAULT_CONFIG?.incrementPercent ?? 0.5);
2572
- const check = isLastFillGuardBlocked(newPrice, newSize, orderType, lastPrice, lastType, inc);
2573
- if (check.blocked) {
2574
- skippedUpdateCount++;
2575
- if (action.id)
2576
- skippedUpdateSlotIds.add(action.id);
2577
- if (action.newGridId)
2578
- skippedUpdateSlotIds.add(action.newGridId);
2579
- const dir = orderType === ORDER_TYPES.BUY ? 'above' : 'below';
2580
- bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'warn');
2581
- continue;
2582
- }
2722
+ try {
2723
+ if (refreshLastFillPivotFromQueue(bot))
2724
+ lastFillGuardPivotRefreshed = true;
2725
+ }
2726
+ catch { /* best-effort */ }
2727
+ const lastPrice = bot.manager?._lastFilledPrice;
2728
+ const lastType = bot.manager?._lastFilledType;
2729
+ const inc = resolveLastFillGuardIncrement(bot);
2730
+ const check = isLastFillGuardBlocked(newPrice, newSize, orderType, lastPrice, lastType, inc);
2731
+ lastFillGuardStats.checked++;
2732
+ if (check.blocked) {
2733
+ lastFillGuardStats.skipped++;
2734
+ skippedUpdateCount++;
2735
+ if (action.id)
2736
+ skippedUpdateSlotIds.add(action.id);
2737
+ if (action.newGridId)
2738
+ skippedUpdateSlotIds.add(action.newGridId);
2739
+ const dir = orderType === ORDER_TYPES.BUY ? 'above' : 'below';
2740
+ bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'warn');
2741
+ continue;
2583
2742
  }
2743
+ lastFillGuardStats.passed++;
2584
2744
  }
2585
2745
  catch (_e) { /* best-effort */ }
2586
2746
  const { amountToSell, minToReceive } = buildCreateOrderArgs({ type: orderType, size: newSize, price: newPrice }, assetA, assetB);
@@ -2693,6 +2853,31 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2693
2853
  `${crossedOrderLabel(fbCrossed)}; re-planned after its cancel confirms.`, 'warn');
2694
2854
  continue;
2695
2855
  }
2856
+ // LAST-FILL GUARD (fallback variant): this CREATE
2857
+ // replaces a rotation UPDATE at a repriced level,
2858
+ // so it obeys the same guard with a refreshed
2859
+ // pivot — no origin bypass, same as rotations.
2860
+ try {
2861
+ if (refreshLastFillPivotFromQueue(bot))
2862
+ lastFillGuardPivotRefreshed = true;
2863
+ }
2864
+ catch { /* best-effort */ }
2865
+ try {
2866
+ const fbInc = resolveLastFillGuardIncrement(bot);
2867
+ const fbCheck = isLastFillGuardBlocked(fbPrice, fbSize, fbType, bot.manager?._lastFilledPrice, bot.manager?._lastFilledType, fbInc);
2868
+ lastFillGuardStats.checked++;
2869
+ if (fbCheck.blocked) {
2870
+ lastFillGuardStats.skipped++;
2871
+ const fbDir = fbType === ORDER_TYPES.BUY ? 'above' : 'below';
2872
+ bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping CREATE fallback for ${targetSlotId} at ` +
2873
+ `${Format.formatPrice6(fbPrice)}: ${fbDir} last filled ` +
2874
+ `${Format.formatPrice6(fbCheck.pivot)} (halfInc ${fbCheck.halfInc}% thr ` +
2875
+ `${Format.formatPrice6(fbCheck.threshold)}); re-planned after market moves`, 'warn');
2876
+ continue;
2877
+ }
2878
+ lastFillGuardStats.passed++;
2879
+ }
2880
+ catch (_fbGuardErr) { /* guard is best-effort */ }
2696
2881
  const fbArgs = buildCreateOrderArgs({ type: fbType, size: fbSize, price: fbPrice }, assetA, assetB);
2697
2882
  const fbResult = await chainOrders.buildCreateOrderOp(bot.account, fbArgs.amountToSell, fbArgs.sellAssetId, fbArgs.minToReceive, fbArgs.receiveAssetId, null);
2698
2883
  if (fbResult) {
@@ -2723,6 +2908,39 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2723
2908
  }
2724
2909
  }
2725
2910
  }
2911
+ // Batch-level LAST-FILL-GUARD summary: per-action pass lines would spam
2912
+ // big batches, so one line per batch records the mode, pivot, resolved
2913
+ // increment, and pass/skip/bypass counts — the guard's pass decisions
2914
+ // are what incident reconstruction needs. Origin folds in here as
2915
+ // mode=bypassed(<origin>); there is no second source of truth.
2916
+ // Cold (guard off) is warn, not info — a disabled guard must say so.
2917
+ // pivotRefreshed surfaces whether a mid-broadcast queued fill moved the
2918
+ // pivot under this batch's checks (the refresh itself stays debug).
2919
+ // Note the printed pivot is end-of-batch state: when pivotRefreshed is
2920
+ // true, early actions were checked against the older pivot.
2921
+ try {
2922
+ const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed;
2923
+ if (totalGuarded > 0) {
2924
+ const sumPivotRaw = bot.manager?._lastFilledPrice;
2925
+ const sumType = bot.manager?._lastFilledType;
2926
+ const sumInc = resolveLastFillGuardIncrement(bot);
2927
+ const cold = sumPivotRaw == null || !Number.isFinite(Number(sumPivotRaw)) || sumType == null;
2928
+ const batchOrigin = cowResult?.origin;
2929
+ const mode = cold
2930
+ ? 'disabled(cold)'
2931
+ : (lastFillGuardStats.bypassed > 0 && lastFillGuardStats.checked === 0)
2932
+ ? `bypassed(${batchOrigin || 'unknown'})`
2933
+ : (lastFillGuardStats.bypassed > 0
2934
+ ? `active+bypassed(${batchOrigin || 'unknown'})`
2935
+ : 'active');
2936
+ const pivotStr = cold ? 'none' : `${Format.formatPrice6(Number(sumPivotRaw))}(${sumType})`;
2937
+ bot.manager.logger.log(`[LAST-FILL-GUARD] mode=${mode} pivot=${pivotStr} inc=${sumInc}% ` +
2938
+ `pivotRefreshed=${lastFillGuardPivotRefreshed} ` +
2939
+ `checked=${lastFillGuardStats.checked} passed=${lastFillGuardStats.passed} ` +
2940
+ `skipped=${lastFillGuardStats.skipped} bypassed=${lastFillGuardStats.bypassed}`, cold ? 'warn' : 'info');
2941
+ }
2942
+ }
2943
+ catch { /* summary is best-effort */ }
2726
2944
  if (skippedUpdateCount > 0) {
2727
2945
  restoreSkippedUpdateSlotsInWorkingGrid(bot, workingGrid, skippedUpdateSlotIds, skippedUpdateCount);
2728
2946
  }
@@ -3470,7 +3688,12 @@ async function processBatchResults(bot, result, opContexts) {
3470
3688
  });
3471
3689
  }
3472
3690
  }
3473
- bot.manager.logger.log(`Placed ${ctx.order.type} order ${ctx.order.id} -> ${chainOrderId}`, 'info');
3691
+ // Success line mirrors the failure-path fingerprint below
3692
+ // (type/price/size) so both are greppable by the same keys —
3693
+ // this is what ties a fill back to the slot that placed it.
3694
+ bot.manager.logger.log(`Placed ${ctx.order.type} order ${ctx.order.id} ` +
3695
+ `@${Format.formatPrice6(ctx.order.price)} x${Format.formatAmount(ctx.order.size)} ` +
3696
+ `-> ${chainOrderId}`, 'info');
3474
3697
  }
3475
3698
  else {
3476
3699
  const fingerprint = [
@@ -3571,7 +3794,7 @@ async function processBatchResults(bot, result, opContexts) {
3571
3794
  updateOperationCount
3572
3795
  };
3573
3796
  }
3574
- export { isLastFillGuardBlocked, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain };
3797
+ export { isLastFillGuardBlocked, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain };
3575
3798
  export default {
3576
3799
  buildOutsideInPairGroupsForOrders,
3577
3800
  buildOutsideInPairGroupsForCreateEntries,