dexbot 1.6.0 → 1.6.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.
- package/CHANGELOG.md +33 -0
- package/README.md +5 -4
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +19 -5
- package/analysis/trend_detection/package.json +1 -1
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +19 -5
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/dexbot.js +1 -1
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.js +2 -2
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +22 -3
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +15 -15
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.js +2 -2
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +54 -8
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
- package/dist/market_adapter/inputs/window_cache.js +328 -87
- package/dist/market_adapter/inputs/window_cache.js.map +1 -1
- package/dist/modules/account_bots.js +2 -2
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +18 -4
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +10 -0
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/constants.d.ts +12 -3
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +66 -20
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +24 -6
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +34 -6
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +94 -17
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +57 -6
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +474 -38
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +53 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +277 -9
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +69 -0
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +277 -7
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +6 -1
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +32 -4
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/market_adapter_whitelist.js +1 -1
- package/dist/modules/market_adapter_whitelist.js.map +1 -1
- package/dist/modules/order/grid.d.ts +3 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +93 -16
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +73 -7
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/logger.js +1 -1
- package/dist/modules/order/logger.js.map +1 -1
- package/dist/modules/order/manager.d.ts +14 -1
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +54 -2
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +183 -10
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +65 -15
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +93 -22
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +51 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +122 -2
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +22 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +30 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.js +3 -2
- package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +14 -6
- package/docs/COPY_ON_WRITE_MASTER_PLAN.md +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -7
- package/docs/EVOLUTION.md +9 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -9
- package/docs/GRID_RECALCULATION.md +8 -7
- package/docs/LIFECYCLE.md +2 -2
- package/docs/README.md +9 -1
- package/docs/architecture.md +7 -7
- package/docs/developer_guide.md +4 -4
- package/market_adapter/README.md +14 -11
- package/package.json +1 -1
- package/scripts/README.md +4 -4
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +0 -2455
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +0 -218
- package/analysis/tradingview/h-bts_tradingview.html +0 -1570
- package/analysis/tradingview/t-bts_tradingview.html +0 -1570
|
@@ -64,11 +64,13 @@ function liveWindowIdSet(...args) { return require('./order/utils/order').liveWi
|
|
|
64
64
|
function resolveLiveReserveEdgeAnchorPrice(...args) { return require('./order/utils/order').resolveLiveReserveEdgeAnchorPrice(...args); }
|
|
65
65
|
function formatUnmatchedChainOrder(...args) { return require('./order/utils/order').formatUnmatchedChainOrder(...args); }
|
|
66
66
|
function isNonBlockingUnmatchedOrder(...args) { return require('./order/utils/order').isNonBlockingUnmatchedOrder(...args); }
|
|
67
|
+
function isStrandedHoldOrder(...args) { return require('./order/utils/order').isStrandedHoldOrder(...args); }
|
|
67
68
|
function getSideBudget(...args) { return require('./order/utils/order').getSideBudget(...args); }
|
|
68
69
|
function getActiveOrdersTotal(config) { return require('./order/utils/order').getActiveOrdersTotal(config); }
|
|
69
70
|
function correctAllPriceMismatches(...args) { return require('./order/utils/order').correctAllPriceMismatches(...args); }
|
|
70
71
|
function isOrderOnChain(...args) { return require('./order/utils/order').isOrderOnChain(...args); }
|
|
71
72
|
function parseChainOrder(...args) { return require('./order/utils/order').parseChainOrder(...args); }
|
|
73
|
+
function parseSlotIndex(...args) { return require('./order/utils/order').parseSlotIndex(...args); }
|
|
72
74
|
const CODE_ROOT = path.join(__dirname, '..');
|
|
73
75
|
const PROFILES_DIR = PATHS.PROFILES_DIR;
|
|
74
76
|
const PROFILES_BOTS_FILE = PATHS.PROFILES.BOTS_JSON;
|
|
@@ -521,7 +523,13 @@ function countLiveGridOrders(manager, type) {
|
|
|
521
523
|
return 0;
|
|
522
524
|
const active = manager.getOrdersByTypeAndState?.(type, ORDER_STATES.ACTIVE) || [];
|
|
523
525
|
const partial = manager.getOrdersByTypeAndState?.(type, ORDER_STATES.PARTIAL) || [];
|
|
524
|
-
|
|
526
|
+
// Slot-N gated: fork-kept shelf/manual orders (non-slot-N ids, e.g.
|
|
527
|
+
// deep-*) sit outside window accounting — same gate as reserve
|
|
528
|
+
// classification and startup cancel candidates (issue #27 follow-up).
|
|
529
|
+
// Without this, a live shelf inflates the live window+reserves count and
|
|
530
|
+
// masks a real window/reserve shortfall, so targeted sync never fires.
|
|
531
|
+
// No-op on grids that only mint slot-N ids.
|
|
532
|
+
return active.concat(partial).filter((o) => o?.orderId && parseSlotIndex(o?.id) !== null).length;
|
|
525
533
|
}
|
|
526
534
|
function getTargetActiveOrders(config, side) {
|
|
527
535
|
const configured = Number(config?.activeOrders?.[side]);
|
|
@@ -1452,21 +1460,201 @@ async function performPeriodicGridChecks(bot) {
|
|
|
1452
1460
|
* total (funds stay locked until an operator clears them) without re-logging
|
|
1453
1461
|
* the same count every tick.
|
|
1454
1462
|
*
|
|
1463
|
+
* The detail line names WHY each hold is held and how far it sits from the
|
|
1464
|
+
* grid, because "held" alone is ambiguous between deliberate deferral and a
|
|
1465
|
+
* stuck bot. A slow re-warn (rate-limited) additionally distinguishes a hold
|
|
1466
|
+
* that is still being actioned from one that has been static for a long time.
|
|
1467
|
+
*
|
|
1455
1468
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
1456
1469
|
*/
|
|
1457
1470
|
function logDeferredHoldSummary(bot) {
|
|
1458
1471
|
const unmatched = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
1459
1472
|
? bot.manager._lastUnmatchedChainOrders
|
|
1460
1473
|
: [];
|
|
1461
|
-
const
|
|
1474
|
+
const heldOrders = unmatched.filter((u) => isNonBlockingUnmatchedOrder(u));
|
|
1475
|
+
const held = heldOrders.length;
|
|
1462
1476
|
if (held === 0) {
|
|
1463
|
-
bot.
|
|
1477
|
+
bot._lastHeldChainOrderSignature = '';
|
|
1478
|
+
bot._lastHeldChainOrderWarnAt = 0;
|
|
1479
|
+
bot._lastHeldChainOrderSignatureSince = 0;
|
|
1480
|
+
bot._strandedHoldSince = new Map();
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
// Per-order age tracking for the stranded subset.
|
|
1484
|
+
//
|
|
1485
|
+
// The whole-set signature below includes every entry's reason, so an
|
|
1486
|
+
// UNRELATED hold flapping in and out (e.g. a per-sync
|
|
1487
|
+
// `broadcast-active-deferred`) changes the signature and restarts the age
|
|
1488
|
+
// clock every cycle. A genuinely stranded order then never reaches the
|
|
1489
|
+
// escalation age -- it is starved forever by churn it has nothing to do
|
|
1490
|
+
// with. Verified by simulation: with the whole-set clock, a stranded
|
|
1491
|
+
// order plus a 6-hourly flap reported `CLOCK RESET` on every single tick
|
|
1492
|
+
// and never accumulated age.
|
|
1493
|
+
//
|
|
1494
|
+
// So age is tracked per stranded order identity (id@price/size), keyed off
|
|
1495
|
+
// when THAT order was first seen stranded. Unrelated churn cannot touch it.
|
|
1496
|
+
if (!(bot._strandedHoldSince instanceof Map))
|
|
1497
|
+
bot._strandedHoldSince = new Map();
|
|
1498
|
+
const strandedHoldSince = bot._strandedHoldSince;
|
|
1499
|
+
const stranded = heldOrders.filter((u) => isStrandedHoldOrder(u));
|
|
1500
|
+
const seenKeys = new Set();
|
|
1501
|
+
const now = Date.now();
|
|
1502
|
+
let oldestStrandedMs = 0;
|
|
1503
|
+
for (const u of stranded) {
|
|
1504
|
+
const key = `${u?.chainOrderId ?? '?'}@${u?.price ?? '?'}/${u?.size ?? '?'}:${u?.reason ?? '?'}`;
|
|
1505
|
+
seenKeys.add(key);
|
|
1506
|
+
if (!strandedHoldSince.has(key))
|
|
1507
|
+
strandedHoldSince.set(key, now);
|
|
1508
|
+
oldestStrandedMs = Math.max(oldestStrandedMs, now - Number(strandedHoldSince.get(key)));
|
|
1509
|
+
}
|
|
1510
|
+
// Drop entries that are no longer stranded, so a slot that clears and is
|
|
1511
|
+
// later re-held starts a fresh age rather than inheriting the old one.
|
|
1512
|
+
for (const key of Array.from(strandedHoldSince.keys())) {
|
|
1513
|
+
if (!seenKeys.has(key))
|
|
1514
|
+
strandedHoldSince.delete(key);
|
|
1515
|
+
}
|
|
1516
|
+
const signature = heldOrders
|
|
1517
|
+
.map((u) => `${u?.chainOrderId ?? '?'}@${u?.price ?? '?'}/${u?.size ?? '?'}:${u?.reason ?? '?'}`)
|
|
1518
|
+
.sort()
|
|
1519
|
+
.join(',');
|
|
1520
|
+
// A count-only change gate hid same-count churn (one hold clearing while
|
|
1521
|
+
// another appeared looked like "no change" and was never logged). The
|
|
1522
|
+
// signature covers membership and terms, so any real change re-logs once.
|
|
1523
|
+
if (signature !== bot._lastHeldChainOrderSignature) {
|
|
1524
|
+
bot._lastHeldChainOrderSignature = signature;
|
|
1525
|
+
bot._lastHeldChainOrderWarnAt = Date.now();
|
|
1526
|
+
// Restart the whole-set logging clock: the signature changed, so this
|
|
1527
|
+
// is a new hold situation and the running total is worth re-logging.
|
|
1528
|
+
// NOTE: this clock drives LOGGING ONLY. Escalation below uses the
|
|
1529
|
+
// per-order stranded ages, because a signature change caused by
|
|
1530
|
+
// unrelated churn must not suppress escalation -- returning early here
|
|
1531
|
+
// (as an earlier revision did) meant any flapping hold skipped the
|
|
1532
|
+
// escalation call entirely, so a genuinely stranded order never
|
|
1533
|
+
// escalated at all.
|
|
1534
|
+
bot._lastHeldChainOrderSignatureSince = Date.now();
|
|
1535
|
+
bot._log?.(`[HOLD] ${held} deferred chain order(s) held outside the active pipeline ` +
|
|
1536
|
+
`(funds stay locked until cleared): ${describeDeferredHolds(bot, heldOrders)}`, 'warn');
|
|
1537
|
+
// Escalation is NOT gated on an unchanged signature: a stranded order
|
|
1538
|
+
// whose age has crossed the threshold must escalate even if an
|
|
1539
|
+
// unrelated transient hold is flapping in and out alongside it.
|
|
1540
|
+
considerDeferredHoldEscalation(bot, stranded, oldestStrandedMs);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
// The signature is unchanged.
|
|
1544
|
+
if (!(Number(bot._lastHeldChainOrderSignatureSince) > 0)) {
|
|
1545
|
+
bot._lastHeldChainOrderSignatureSince = Date.now();
|
|
1546
|
+
}
|
|
1547
|
+
// Escalate on the STRANDED subset's own age, not the whole held set's.
|
|
1548
|
+
considerDeferredHoldEscalation(bot, stranded, oldestStrandedMs);
|
|
1549
|
+
// Unchanged set: re-warn slowly so an indefinitely-held order is visibly
|
|
1550
|
+
// still held rather than silently indistinguishable from a stalled loop.
|
|
1551
|
+
const warnEvery = Number(TIMING.STALE_TOTALS_WARN_RATE_LIMIT_MS) || 60000;
|
|
1552
|
+
const sinceWarn = Date.now() - (Number(bot._lastHeldChainOrderWarnAt) || 0);
|
|
1553
|
+
if (sinceWarn >= warnEvery) {
|
|
1554
|
+
bot._lastHeldChainOrderWarnAt = Date.now();
|
|
1555
|
+
// Age from the SIGNATURE-STABLE clock, not `_lastUnmatchedChainOrdersAt`
|
|
1556
|
+
// (which refresh on every observing sync and would report "~0m" for a
|
|
1557
|
+
// hold that has really been stuck for hours).
|
|
1558
|
+
const heldSince = Number(bot._lastHeldChainOrderSignatureSince) || 0;
|
|
1559
|
+
const ageMin = heldSince > 0 ? Math.round((Date.now() - heldSince) / 60000) : 0;
|
|
1560
|
+
bot._log?.(`[HOLD] still holding ${held} deferred chain order(s) for ~${ageMin}m ` +
|
|
1561
|
+
`with no change (funds stay locked until cleared): ${describeDeferredHolds(bot, heldOrders)}`, 'warn');
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Escalate an INDEFINITELY-HELD deferred hold to a structural resync.
|
|
1566
|
+
*
|
|
1567
|
+
* Out-of-rail orphans hold locked funds and are never auto-cancelled per cycle.
|
|
1568
|
+
* That default is correct -- cancelling on ambiguous evidence is irreversible --
|
|
1569
|
+
* but it left "held indefinitely" with no exit. A hold that survives unchanged
|
|
1570
|
+
* for `DEFERRED_HOLD_ESCALATE_MS` is no longer ambiguous evidence: the grid has
|
|
1571
|
+
* had ample opportunity to resolve it and has not.
|
|
1572
|
+
*
|
|
1573
|
+
* The structural resync is the appropriate exit because the full reset's
|
|
1574
|
+
* reconcile is update-first: unmatched chain orders are price-updated onto rail
|
|
1575
|
+
* slots (emitting the rail's genesis level, so the reconcile-update guard does
|
|
1576
|
+
* not block it) and only true surplus is cancelled. Funds are released without
|
|
1577
|
+
* introducing any new cancellation policy.
|
|
1578
|
+
*
|
|
1579
|
+
* Fire-and-forget; the resync is itself debounced and batch-in-flight aware.
|
|
1580
|
+
*/
|
|
1581
|
+
function considerDeferredHoldEscalation(bot, strandedOrders, strandedMs) {
|
|
1582
|
+
const escalateMs = Number(TIMING?.DEFERRED_HOLD_ESCALATE_MS) > 0
|
|
1583
|
+
? Number(TIMING.DEFERRED_HOLD_ESCALATE_MS)
|
|
1584
|
+
: 24 * 60 * 60 * 1000;
|
|
1585
|
+
const held = strandedOrders.length;
|
|
1586
|
+
if (held === 0)
|
|
1587
|
+
return;
|
|
1588
|
+
// Age is supplied by the caller: the oldest STRANDED order's age, measured
|
|
1589
|
+
// from when that order was first seen stranded (per-order clock). It is
|
|
1590
|
+
// deliberately NOT the whole held-set signature age, which unrelated
|
|
1591
|
+
// flapping holds reset every cycle and which would starve escalation.
|
|
1592
|
+
const heldMs = Number(strandedMs) || 0;
|
|
1593
|
+
if (heldMs < escalateMs)
|
|
1594
|
+
return;
|
|
1595
|
+
if (typeof bot?.manager?.requestStructuralGridResync !== 'function') {
|
|
1596
|
+
bot?._log?.(`[HOLD] ${held} deferred chain order(s) held unchanged for ${Math.round(heldMs / 3600000)}h ` +
|
|
1597
|
+
`but requestStructuralGridResync is unavailable; funds stay locked`, 'error');
|
|
1464
1598
|
return;
|
|
1465
1599
|
}
|
|
1466
|
-
|
|
1600
|
+
const cooldownMs = Number(TIMING?.DEFERRED_HOLD_RESYNC_COOLDOWN_MS) > 0
|
|
1601
|
+
? Number(TIMING.DEFERRED_HOLD_RESYNC_COOLDOWN_MS)
|
|
1602
|
+
: 6 * 60 * 60 * 1000;
|
|
1603
|
+
const now = Date.now();
|
|
1604
|
+
const lastAt = Number(bot._lastDeferredHoldResyncAt) || 0;
|
|
1605
|
+
if (now - lastAt < cooldownMs)
|
|
1467
1606
|
return;
|
|
1468
|
-
bot.
|
|
1469
|
-
|
|
1607
|
+
bot._lastDeferredHoldResyncAt = now;
|
|
1608
|
+
const heldHours = Math.round(heldMs / 3600000);
|
|
1609
|
+
bot?._log?.(`[HOLD] ${held} deferred chain order(s) held unchanged for ~${heldHours}h; ` +
|
|
1610
|
+
`requesting structural resync to resolve the hold (reconcile is update-first, ` +
|
|
1611
|
+
`so funds are released by price-updating onto rail slots) — ` +
|
|
1612
|
+
`${describeDeferredHolds(bot, strandedOrders)}`, 'error');
|
|
1613
|
+
try {
|
|
1614
|
+
const res = bot.manager.requestStructuralGridResync('deferred-hold-stale', {
|
|
1615
|
+
reason: `${held} deferred chain order(s) held unchanged for ~${heldHours}h`,
|
|
1616
|
+
heldCount: held,
|
|
1617
|
+
heldMs,
|
|
1618
|
+
});
|
|
1619
|
+
res?.catch?.((err) => {
|
|
1620
|
+
bot.manager?.logger?.log?.(`[HOLD] Structural resync for stale hold failed: ${getErrorMessage(err)}`, 'error');
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
catch (err) {
|
|
1624
|
+
bot.manager?.logger?.log?.(`[HOLD] Structural resync for stale hold failed: ${getErrorMessage(err)}`, 'error');
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Render the per-order detail for a deferred-hold summary: side, price, size,
|
|
1629
|
+
* why it is held, and distance from the nearest grid bound when known.
|
|
1630
|
+
*
|
|
1631
|
+
* Distance is expressed as a percentage of the bound so the operator can tell
|
|
1632
|
+
* a near-miss (a few bps outside, likely a rounding/drift artifact) from a
|
|
1633
|
+
* deliberately-placed far order (dip protection after a grid reset).
|
|
1634
|
+
*/
|
|
1635
|
+
function describeDeferredHolds(bot, heldOrders) {
|
|
1636
|
+
const genesis = bot.manager?._genesis;
|
|
1637
|
+
const levels = Array.isArray(genesis?.priceLevels) ? genesis.priceLevels : [];
|
|
1638
|
+
const lower = levels.length > 0 ? Number(levels[0]) : NaN;
|
|
1639
|
+
const upper = levels.length > 0 ? Number(levels[levels.length - 1]) : NaN;
|
|
1640
|
+
return heldOrders
|
|
1641
|
+
.map((u) => {
|
|
1642
|
+
const side = u?.type === ORDER_TYPES.SELL ? 'sell' : u?.type === ORDER_TYPES.BUY ? 'buy' : 'unknown';
|
|
1643
|
+
const price = Number(u?.price);
|
|
1644
|
+
const size = u?.size != null ? u.size : '?';
|
|
1645
|
+
const reason = u?.reason || 'unspecified';
|
|
1646
|
+
const parts = [`${u?.chainOrderId ?? '?'} ${side} price=${Number.isFinite(price) ? price : '?'} size=${size} reason=${reason}`];
|
|
1647
|
+
if (Number.isFinite(price)) {
|
|
1648
|
+
if (Number.isFinite(lower) && price < lower) {
|
|
1649
|
+
parts.push(`below-grid by ${(((lower - price) / lower) * 100).toFixed(4)}% (bound ${lower})`);
|
|
1650
|
+
}
|
|
1651
|
+
else if (Number.isFinite(upper) && price > upper) {
|
|
1652
|
+
parts.push(`above-grid by ${(((price - upper) / upper) * 100).toFixed(4)}% (bound ${upper})`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return parts.join(' ');
|
|
1656
|
+
})
|
|
1657
|
+
.join(' | ');
|
|
1470
1658
|
}
|
|
1471
1659
|
/**
|
|
1472
1660
|
* Check if the continuous open-orders sync loop is enabled.
|
|
@@ -2140,10 +2328,14 @@ async function executeMaintenanceLogic(bot, context) {
|
|
|
2140
2328
|
const spreadResult = await bot.manager.checkSpreadCondition(BitShares, bot.updateOrdersOnChainPlan.bind(bot));
|
|
2141
2329
|
if (await bot._abortFlowIfIllegalState(`${context} spread check`))
|
|
2142
2330
|
return;
|
|
2143
|
-
|
|
2331
|
+
const spreadPlaced = Number(spreadResult?.ordersPlaced) || 0;
|
|
2332
|
+
if (spreadPlaced > 0) {
|
|
2144
2333
|
bot._log(`✓ Spread correction during ${context}: ${spreadResult.ordersPlaced} order(s) placed`);
|
|
2145
2334
|
await bot._persistAndRecoverIfNeeded();
|
|
2146
2335
|
}
|
|
2336
|
+
// Persistence watchdog: a correction that keeps placing nothing
|
|
2337
|
+
// while the spread stays wide is a stale grid, not patience.
|
|
2338
|
+
trackOutOfSpreadStaleness(bot, true, spreadPlaced);
|
|
2147
2339
|
}
|
|
2148
2340
|
}
|
|
2149
2341
|
catch (err) {
|
|
@@ -2158,6 +2350,82 @@ async function executeMaintenanceLogic(bot, context) {
|
|
|
2158
2350
|
}
|
|
2159
2351
|
}
|
|
2160
2352
|
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Out-of-spread persistence watchdog: the never-run-stale backstop.
|
|
2355
|
+
*
|
|
2356
|
+
* The spread check retries every pipeline-empty tick (level-triggered), but a
|
|
2357
|
+
* correction can keep producing zero candidates indefinitely (no funded side,
|
|
2358
|
+
* no correctable slots) while the grid sits stale — and the identical-held-
|
|
2359
|
+
* plan suppression blocks fill-less replans until a fresh fill that a stale
|
|
2360
|
+
* grid cannot produce. Time-based (not tick-counted) so it holds for any
|
|
2361
|
+
* maintenance cadence: warn once past SPREAD_STALE_WARN_MS, then request a
|
|
2362
|
+
* structural re-center past SPREAD_STALE_ESCALATE_MS (existing resync guards
|
|
2363
|
+
* dedupe concurrent requests; the re-center moves the boundary, which clears
|
|
2364
|
+
* any held-plan signature). Resets whenever the spread heals or a correction
|
|
2365
|
+
* places orders.
|
|
2366
|
+
* @param {any} bot
|
|
2367
|
+
* @param {boolean} spreadChecked - Whether the spread check ran this tick
|
|
2368
|
+
* @param {number} ordersPlaced - Correction orders placed this tick
|
|
2369
|
+
* @returns {{staleMs: number, escalated: boolean}}
|
|
2370
|
+
*/
|
|
2371
|
+
export function trackOutOfSpreadStaleness(bot, spreadChecked, ordersPlaced) {
|
|
2372
|
+
const outOfSpread = Number(bot?.manager?.outOfSpread) || 0;
|
|
2373
|
+
if (ordersPlaced > 0 || outOfSpread === 0) {
|
|
2374
|
+
bot._outOfSpreadSince = 0;
|
|
2375
|
+
bot._outOfSpreadStaleWarned = false;
|
|
2376
|
+
return { staleMs: 0, escalated: false };
|
|
2377
|
+
}
|
|
2378
|
+
if (!spreadChecked) {
|
|
2379
|
+
const since = Number(bot._outOfSpreadSince) || 0;
|
|
2380
|
+
return { staleMs: since > 0 ? Date.now() - since : 0, escalated: false };
|
|
2381
|
+
}
|
|
2382
|
+
const now = Date.now();
|
|
2383
|
+
if (!Number(bot._outOfSpreadSince))
|
|
2384
|
+
bot._outOfSpreadSince = now;
|
|
2385
|
+
const staleMs = now - Number(bot._outOfSpreadSince);
|
|
2386
|
+
const staleMin = Math.round(staleMs / 60000);
|
|
2387
|
+
const warnMs = Number(TIMING?.SPREAD_STALE_WARN_MS) > 0
|
|
2388
|
+
? Number(TIMING.SPREAD_STALE_WARN_MS)
|
|
2389
|
+
: 10 * 60 * 1000;
|
|
2390
|
+
const escalateMs = Number(TIMING?.SPREAD_STALE_ESCALATE_MS) > 0
|
|
2391
|
+
? Number(TIMING.SPREAD_STALE_ESCALATE_MS)
|
|
2392
|
+
: 30 * 60 * 1000;
|
|
2393
|
+
if (staleMs >= warnMs && !bot._outOfSpreadStaleWarned) {
|
|
2394
|
+
bot._outOfSpreadStaleWarned = true;
|
|
2395
|
+
bot._log(`[SPREAD-STALE] Spread out of tolerance for ${staleMin}min with no correction placed ` +
|
|
2396
|
+
`(outOfSpread=${outOfSpread}); structural re-center follows if unhealed`, 'warn');
|
|
2397
|
+
}
|
|
2398
|
+
if (staleMs >= escalateMs && typeof bot.manager?.requestStructuralGridResync === 'function') {
|
|
2399
|
+
// Dedicated spread-stale cooldown (NOT BOUNDARY_HOLD_RESYNC_COOLDOWN_MS:
|
|
2400
|
+
// the two watchdogs must tune independently). Falls back to the legacy
|
|
2401
|
+
// boundary-hold key only for operators who overrode it before the split,
|
|
2402
|
+
// then to the 5min default.
|
|
2403
|
+
const cooldownMs = Number(TIMING?.SPREAD_STALE_RESYNC_COOLDOWN_MS) > 0
|
|
2404
|
+
? Number(TIMING.SPREAD_STALE_RESYNC_COOLDOWN_MS)
|
|
2405
|
+
: Number(TIMING?.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS) > 0
|
|
2406
|
+
? Number(TIMING.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS)
|
|
2407
|
+
: 5 * 60 * 1000;
|
|
2408
|
+
const lastAt = Number(bot._lastSpreadStaleResyncAt) || 0;
|
|
2409
|
+
if (now - lastAt >= cooldownMs) {
|
|
2410
|
+
bot._lastSpreadStaleResyncAt = now;
|
|
2411
|
+
bot._log(`[SPREAD-STALE] Spread out of tolerance for ${staleMin}min despite corrections; ` +
|
|
2412
|
+
`requesting structural re-center`, 'warn');
|
|
2413
|
+
try {
|
|
2414
|
+
const res = bot.manager.requestStructuralGridResync('spread-stale-persistent', {
|
|
2415
|
+
reason: `Spread out of tolerance for ${staleMin}min with no effective correction (outOfSpread=${outOfSpread})`
|
|
2416
|
+
});
|
|
2417
|
+
res?.catch?.((err) => {
|
|
2418
|
+
bot.manager?.logger?.log?.(`[SPREAD-STALE] Structural re-center request failed: ${getErrorMessage(err)}`, 'error');
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
catch (err) {
|
|
2422
|
+
bot.manager?.logger?.log?.(`[SPREAD-STALE] Structural re-center request failed: ${getErrorMessage(err)}`, 'error');
|
|
2423
|
+
}
|
|
2424
|
+
return { staleMs, escalated: true };
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return { staleMs, escalated: false };
|
|
2428
|
+
}
|
|
2161
2429
|
/**
|
|
2162
2430
|
* Cancel a single order, deferring on an uncertain daemon broadcast.
|
|
2163
2431
|
* The credential daemon retries internally only failures that provably never
|
|
@@ -2850,6 +3118,7 @@ export default {
|
|
|
2850
3118
|
cancelDustOrders,
|
|
2851
3119
|
isOrderDoesNotExistError,
|
|
2852
3120
|
runGridMaintenance,
|
|
3121
|
+
trackOutOfSpreadStaleness,
|
|
2853
3122
|
stopMarketAdapterPm2,
|
|
2854
3123
|
releaseMarketAdapterRuntime,
|
|
2855
3124
|
syncMarketAdapterOnPeriodicConfigCheck,
|
|
@@ -2866,5 +3135,6 @@ export default {
|
|
|
2866
3135
|
markGridActivity,
|
|
2867
3136
|
getMetrics,
|
|
2868
3137
|
syncOpenOrdersAndProcessFills,
|
|
3138
|
+
_internalDeferredHold: { logDeferredHoldSummary, describeDeferredHolds, considerDeferredHoldEscalation },
|
|
2869
3139
|
};
|
|
2870
3140
|
//# sourceMappingURL=dexbot_maintenance_runtime.js.map
|