dexbot 1.5.3 → 1.6.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.
- package/CHANGELOG.md +62 -0
- package/README.md +4 -3
- package/analysis/README.md +1 -2
- package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
- package/analysis/resolve_source.ts +9 -2
- package/analysis/tradingview/README.md +29 -11
- package/analysis/tradingview/analyze_tradingview.ts +92 -3
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
- package/analysis/trend_detection/package.json +1 -1
- package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
- package/claw/modules/dexbot_profiles.ts +22 -2
- 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/skills/launcher-ops/references/launcher-workflow.md +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/claw/tests/test_dexbot_profiles.ts +65 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +6 -2
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.js +3 -3
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/candle_utils.d.ts +6 -0
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +6 -0
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +101 -19
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +2 -0
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +31 -4
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +0 -6
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
- package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
- package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +10 -5
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
- package/dist/market_adapter/inputs/window_cache.js +500 -0
- package/dist/market_adapter/inputs/window_cache.js.map +1 -0
- package/dist/market_adapter/interval_utils.d.ts +8 -1
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +14 -1
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -0
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +21 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +25 -4
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +67 -4
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +3 -1
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +13 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +2 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +122 -28
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +59 -8
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +11 -0
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +68 -4
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +8 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +57 -2
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +157 -28
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +135 -5
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +554 -46
- 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 +13 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +15 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +36 -7
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/fund_registry.d.ts +1 -1
- package/dist/modules/fund_registry.js +1 -1
- package/dist/modules/node_connect_policy.d.ts +52 -0
- package/dist/modules/node_connect_policy.d.ts.map +1 -0
- package/dist/modules/node_connect_policy.js +35 -0
- package/dist/modules/node_connect_policy.js.map +1 -0
- package/dist/modules/node_manager.d.ts +32 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +76 -9
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +30 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +176 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -0
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +143 -15
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +5 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +182 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +64 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +231 -14
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -0
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +79 -7
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +16 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +11 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +24 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +258 -13
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +662 -12
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +36 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +102 -6
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +6 -2
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +13 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +20 -0
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +16 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +31 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/pm2.js +7 -7
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +13 -3
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/dist/scripts/analyze-orders.js +2 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.js +0 -1
- package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/tv.d.ts +24 -2
- package/dist/scripts/tv.d.ts.map +1 -1
- package/dist/scripts/tv.js +190 -65
- package/dist/scripts/tv.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -2
- package/docs/COW_INVARIANTS.md +18 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +8 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
- package/docs/GRID_RECALCULATION.md +5 -6
- package/docs/GRID_RECONCILE.md +12 -2
- package/docs/MPA_CREDIT_USAGE.md +2 -2
- package/docs/README.md +5 -6
- package/docs/architecture.md +0 -58
- package/docs/developer_guide.md +2 -1
- package/docs/docker.md +1 -1
- package/market_adapter/README.md +4 -0
- package/modules/README.md +1 -1
- package/package.json +1 -1
- package/scripts/README.md +5 -3
- package/tests/README.md +2 -2
|
@@ -60,10 +60,14 @@
|
|
|
60
60
|
* - buildDelta(masterGrid, workingGrid) - Build delta actions between grids
|
|
61
61
|
* - getOrderSize(order) - Extract order size with fallback
|
|
62
62
|
*
|
|
63
|
-
* SECTION 10: STRATEGY CALCULATIONS (
|
|
64
|
-
* -
|
|
65
|
-
* -
|
|
66
|
-
* -
|
|
63
|
+
* SECTION 10: STRATEGY CALCULATIONS (7 functions)
|
|
64
|
+
* - resolveReserveCount(config, side) - Clamped per-side reserve count (>=0 int, 0 disables)
|
|
65
|
+
* - resolveReserveOrders(config) - Total reserves buy+sell (fee/count totals)
|
|
66
|
+
* - resolveLiveReserveEdgeAnchorPrice(manager, side) - Live-grid edge anchor (ladder/rail extreme first, config bound last; null when unresolved)
|
|
67
|
+
* - resolveReserveEdgeAnchorPrice(config, side) - Config-bound anchor fallback (buy→minPrice, sell→maxPrice; null when unresolvable)
|
|
68
|
+
* - compareReserveEdge(a, b, edge, anchorPrice) - Shared anchored edge comparator (single ordering source)
|
|
69
|
+
* - reserveEdgeIdSet(allSlots, config, orderType, anchorPrice?) - Edge reserve id set (config count; shares the picker ordering)
|
|
70
|
+
* - selectReserveEdgeSlots(sortedAsc, count, excludeIds, edge, anchorPrice?) - Shared position picker (both edges anchor toward their bound)
|
|
67
71
|
*
|
|
68
72
|
* ===============================================================================
|
|
69
73
|
*/
|
|
@@ -1245,6 +1249,26 @@ function formatUnmatchedChainOrder(order) {
|
|
|
1245
1249
|
parts.push(`candidates=${order.candidateDiagnostics}`);
|
|
1246
1250
|
return parts.join(' ');
|
|
1247
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Whether an unmatched chain-order entry is a deliberate hold that must NOT
|
|
1254
|
+
* block CREATEs or snapshot recovery.
|
|
1255
|
+
*
|
|
1256
|
+
* Deferred entries (`reason` suffixed `-deferred`) are permanently
|
|
1257
|
+
* non-adoptable and non-cancellable: an out-of-grid hold sits outside the
|
|
1258
|
+
* frozen rail, and a boundary-unknown hold is re-evaluated once the boundary
|
|
1259
|
+
* commits. Treating one as a blocker freezes the whole grid (a single
|
|
1260
|
+
* dip-protection hold would stop every CREATE) and forces a full reset on
|
|
1261
|
+
* recovery. Classification is by the shared `-deferred` suffix, not an exact
|
|
1262
|
+
* reason string, so a new defer reason cannot silently regress into a
|
|
1263
|
+
* permanent blocker.
|
|
1264
|
+
*
|
|
1265
|
+
* @param {Object} order - Unmatched chain order entry.
|
|
1266
|
+
* @returns {boolean} True when the entry is a non-blocking deferred hold.
|
|
1267
|
+
*/
|
|
1268
|
+
function isNonBlockingUnmatchedOrder(order) {
|
|
1269
|
+
const reason = order?.reason;
|
|
1270
|
+
return typeof reason === 'string' && reason.endsWith('-deferred');
|
|
1271
|
+
}
|
|
1248
1272
|
/**
|
|
1249
1273
|
* Check if order is on blockchain (ACTIVE or PARTIAL state).
|
|
1250
1274
|
*
|
|
@@ -1416,6 +1440,13 @@ function checkSizesBeforeMinimum(sizes, minSize, precision) {
|
|
|
1416
1440
|
* Calculate ideal grid boundary based on reference price.
|
|
1417
1441
|
* Places boundary near reference price with gap spacing in mind.
|
|
1418
1442
|
*
|
|
1443
|
+
* A non-numeric reference (e.g. the unresolved "pool"/"book" mode strings)
|
|
1444
|
+
* makes every `price >= reference` comparison false, which used to resolve
|
|
1445
|
+
* `splitIdx` to `allSlots.length` and fabricate a top-of-rail boundary —
|
|
1446
|
+
* the degenerate all-buy geometry behind the 02:03 slot-77→slot-192 (+58%)
|
|
1447
|
+
* teleport plan. Fail toward rail-center instead; callers with a real
|
|
1448
|
+
* anchor (genesis startPrice, live center) override before calling.
|
|
1449
|
+
*
|
|
1419
1450
|
* @param {Array<Object>} allSlots - All grid slots sorted by price
|
|
1420
1451
|
* @param {number} referencePrice - Reference/anchor price
|
|
1421
1452
|
* @param {number} gapSlots - Number of gap slots between buy and sell
|
|
@@ -1424,6 +1455,10 @@ function checkSizesBeforeMinimum(sizes, minSize, precision) {
|
|
|
1424
1455
|
function calculateIdealBoundary(allSlots, referencePrice, gapSlots) {
|
|
1425
1456
|
if (!allSlots || allSlots.length === 0)
|
|
1426
1457
|
return -1;
|
|
1458
|
+
if (!Number.isFinite(Number(referencePrice))) {
|
|
1459
|
+
const gap = Number.isFinite(Number(gapSlots)) && Number(gapSlots) >= 0 ? Math.floor(Number(gapSlots)) : 0;
|
|
1460
|
+
return Math.max(0, Math.floor((allSlots.length - 1 - gap) / 2));
|
|
1461
|
+
}
|
|
1427
1462
|
let splitIdx = allSlots.findIndex((s) => s.price >= referencePrice);
|
|
1428
1463
|
if (splitIdx === -1)
|
|
1429
1464
|
splitIdx = allSlots.length;
|
|
@@ -1755,23 +1790,156 @@ function buildDelta(masterGrid, workingGrid, options = {}) {
|
|
|
1755
1790
|
function isShiftEligibleFill(fill) {
|
|
1756
1791
|
return fill?.isPartial !== true || fill?.isDelayedRotationTrigger === true;
|
|
1757
1792
|
}
|
|
1758
|
-
function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSlots, crossChunkBudget) {
|
|
1793
|
+
function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSlots, crossChunkBudget, pendingCrawls, edgeAnchors) {
|
|
1759
1794
|
let newBoundaryIdx = currentBoundaryIdx;
|
|
1760
|
-
//
|
|
1795
|
+
// Recovery when the committed boundary is unknown (GRID-LOAD rejected a
|
|
1796
|
+
// poisoned snapshot, re-derivation failed, and no fill has re-anchored
|
|
1797
|
+
// since). Anchor tiers are position signals, weakest last. What must
|
|
1798
|
+
// never happen is fabricating a rail-edge boundary from an unresolved
|
|
1799
|
+
// config mode string: startPrice "pool" NaN-matches every price
|
|
1800
|
+
// comparison, resolving to the rail top (Sep-10: base 213, ceiling 211,
|
|
1801
|
+
// then 209 after 4 buy crawls — teleporting the buy rail 113 slots).
|
|
1802
|
+
let recovered = false;
|
|
1803
|
+
let anchoredFromFills = false;
|
|
1761
1804
|
if (newBoundaryIdx === undefined || newBoundaryIdx === null) {
|
|
1762
|
-
|
|
1805
|
+
// Tier 1 — live fills: gap-side extreme (highest buy / lowest sell,
|
|
1806
|
+
// midpoint when both sides filled). Any fill price, eligible or
|
|
1807
|
+
// dust, is real market position and beats every config guess.
|
|
1808
|
+
let topBuy = -Infinity;
|
|
1809
|
+
let botSell = Infinity;
|
|
1810
|
+
for (const fill of fills ?? []) {
|
|
1811
|
+
const p = Number(fill?.price);
|
|
1812
|
+
if (!Number.isFinite(p))
|
|
1813
|
+
continue;
|
|
1814
|
+
if (fill?.type === ORDER_TYPES.BUY && p > topBuy)
|
|
1815
|
+
topBuy = p;
|
|
1816
|
+
if (fill?.type === ORDER_TYPES.SELL && p < botSell)
|
|
1817
|
+
botSell = p;
|
|
1818
|
+
}
|
|
1819
|
+
let referencePrice = null;
|
|
1820
|
+
if (topBuy > -Infinity && botSell < Infinity) {
|
|
1821
|
+
referencePrice = (topBuy + botSell) / 2;
|
|
1822
|
+
anchoredFromFills = true;
|
|
1823
|
+
}
|
|
1824
|
+
else if (topBuy > -Infinity) {
|
|
1825
|
+
referencePrice = topBuy;
|
|
1826
|
+
anchoredFromFills = true;
|
|
1827
|
+
}
|
|
1828
|
+
else if (botSell < Infinity) {
|
|
1829
|
+
referencePrice = botSell;
|
|
1830
|
+
anchoredFromFills = true;
|
|
1831
|
+
}
|
|
1832
|
+
// Tier 2 — explicit numeric config center.
|
|
1833
|
+
if (referencePrice === null) {
|
|
1834
|
+
const direct = Number(config?.startPrice);
|
|
1835
|
+
if (Number.isFinite(direct))
|
|
1836
|
+
referencePrice = direct;
|
|
1837
|
+
}
|
|
1838
|
+
// Tier 3 — frozen genesis center (forwarded by the strategy when
|
|
1839
|
+
// config.startPrice is an unresolved mode string).
|
|
1840
|
+
if (referencePrice === null) {
|
|
1841
|
+
const genesis = Number(config?.genesisStartPrice);
|
|
1842
|
+
if (Number.isFinite(genesis))
|
|
1843
|
+
referencePrice = genesis;
|
|
1844
|
+
}
|
|
1845
|
+
// Stale-center guard: a Tier-2 numeric config center or a Tier-3
|
|
1846
|
+
// genesis center that falls outside the live rail would clamp
|
|
1847
|
+
// calculateIdealBoundary onto an edge slot — the same rail-edge
|
|
1848
|
+
// fabrication the mode-string fix prevents, just from a stale numeric
|
|
1849
|
+
// value. Drop such a reference so the bounded Tier-4 rail center is
|
|
1850
|
+
// used instead. Tier-1 fill anchors are exempt: a real (possibly
|
|
1851
|
+
// out-of-grid) fill price is live market position and wins everywhere.
|
|
1852
|
+
if (!anchoredFromFills && referencePrice !== null && Array.isArray(allSlots) && allSlots.length > 0) {
|
|
1853
|
+
let railMin = Infinity;
|
|
1854
|
+
let railMax = -Infinity;
|
|
1855
|
+
for (const s of allSlots) {
|
|
1856
|
+
const p = Number(s?.price);
|
|
1857
|
+
if (!Number.isFinite(p))
|
|
1858
|
+
continue;
|
|
1859
|
+
if (p < railMin)
|
|
1860
|
+
railMin = p;
|
|
1861
|
+
if (p > railMax)
|
|
1862
|
+
railMax = p;
|
|
1863
|
+
}
|
|
1864
|
+
if ((Number.isFinite(railMin) && referencePrice < railMin)
|
|
1865
|
+
|| (Number.isFinite(railMax) && referencePrice > railMax)) {
|
|
1866
|
+
orderLogger.debug(`deriveTargetBoundary: recovery center ${referencePrice} outside live rail ` +
|
|
1867
|
+
`[${railMin}, ${railMax}]; using bounded rail center instead of pinning an edge`);
|
|
1868
|
+
referencePrice = null;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
// Tier 4 — rail center: bounded and wrong by at most half the rail,
|
|
1872
|
+
// never a rail-edge fabrication. The next fill batch re-anchors
|
|
1873
|
+
// from live prices via Tier 1.
|
|
1874
|
+
if (referencePrice === null && Array.isArray(allSlots) && allSlots.length > 0) {
|
|
1875
|
+
const gap = Number.isFinite(Number(gapSlots)) && Number(gapSlots) >= 0 ? Math.floor(Number(gapSlots)) : 0;
|
|
1876
|
+
const centerIdx = Math.max(0, Math.floor((allSlots.length - 1 - gap) / 2));
|
|
1877
|
+
const centerPrice = Number(allSlots[centerIdx]?.price);
|
|
1878
|
+
if (Number.isFinite(centerPrice))
|
|
1879
|
+
referencePrice = centerPrice;
|
|
1880
|
+
}
|
|
1881
|
+
if (referencePrice === null) {
|
|
1882
|
+
const fallbackCap = Math.max(Math.floor((config?.activeOrders?.sell ?? 1) / 2), Math.floor((config?.activeOrders?.buy ?? 1) / 2), 1);
|
|
1883
|
+
const effectiveBudget = crossChunkBudget ?? fallbackCap;
|
|
1884
|
+
return { boundaryIdx: null, remainingBudget: effectiveBudget };
|
|
1885
|
+
}
|
|
1763
1886
|
newBoundaryIdx = calculateIdealBoundary(allSlots, referencePrice, gapSlots);
|
|
1887
|
+
if (!Number.isFinite(newBoundaryIdx) || newBoundaryIdx < 0) {
|
|
1888
|
+
// Empty slot list with a Tier 1-3 reference: no honest index
|
|
1889
|
+
// exists (calculateIdealBoundary returns -1). Stay null rather
|
|
1890
|
+
// than letting the clamp below fabricate a slot-0 boundary.
|
|
1891
|
+
const fallbackCap = Math.max(Math.floor((config?.activeOrders?.sell ?? 1) / 2), Math.floor((config?.activeOrders?.buy ?? 1) / 2), 1);
|
|
1892
|
+
return { boundaryIdx: null, remainingBudget: crossChunkBudget ?? fallbackCap };
|
|
1893
|
+
}
|
|
1894
|
+
recovered = true;
|
|
1764
1895
|
}
|
|
1765
|
-
// Apply shift from fills with rate-limiting
|
|
1896
|
+
// Apply shift from fills with rate-limiting (reserve fills excluded: static insurance).
|
|
1766
1897
|
let netShift = 0;
|
|
1898
|
+
// Reserve ladder: fills from edge-pinned reserve slots never crawl the
|
|
1899
|
+
// boundary — they are static fat-finger insurance, not market movement.
|
|
1900
|
+
// `edgeAnchors` (resolveLiveReserveEdgeAnchorPrice) is the same anchor the
|
|
1901
|
+
// placement sites use; without it the classification falls back to the
|
|
1902
|
+
// config-bound anchor, which can disagree with the slots actually placed.
|
|
1903
|
+
const reserveBuyIds = reserveEdgeIdSet(allSlots, config, ORDER_TYPES.BUY, edgeAnchors?.buy ?? null);
|
|
1904
|
+
const reserveSellIds = reserveEdgeIdSet(allSlots, config, ORDER_TYPES.SELL, edgeAnchors?.sell ?? null);
|
|
1905
|
+
// Pending crawls: fills recorded by earlier batches whose derivation
|
|
1906
|
+
// never committed (refused broadcast, P4 abort, or pre-restart loss —
|
|
1907
|
+
// the Sep-10 case: 4 fills consumed under a null boundary, crawl lost,
|
|
1908
|
+
// restart refilled the holes same-side). Entries for slots in the
|
|
1909
|
+
// CURRENT batch are excluded — those fills crawl below as usual;
|
|
1910
|
+
// anything older is still owed and shifts here. Reserve-slot entries
|
|
1911
|
+
// never crawl (static insurance), same as live fills.
|
|
1912
|
+
const currentSlotIds = new Set((fills ?? []).map((f) => f?.id).filter((id) => typeof id === 'string' && id.length > 0));
|
|
1913
|
+
const owedPending = (pendingCrawls ?? []).filter((e) => e
|
|
1914
|
+
&& typeof e.slotId === 'string' && e.slotId.length > 0
|
|
1915
|
+
&& !currentSlotIds.has(e.slotId)
|
|
1916
|
+
&& (e.side === ORDER_TYPES.BUY || e.side === ORDER_TYPES.SELL)
|
|
1917
|
+
&& !(e.side === ORDER_TYPES.BUY && reserveBuyIds && reserveBuyIds.has(e.slotId))
|
|
1918
|
+
&& !(e.side === ORDER_TYPES.SELL && reserveSellIds && reserveSellIds.has(e.slotId)));
|
|
1767
1919
|
for (const fill of fills) {
|
|
1768
1920
|
if (!isShiftEligibleFill(fill))
|
|
1769
1921
|
continue;
|
|
1922
|
+
if (fill && fill.type === ORDER_TYPES.BUY && reserveBuyIds && reserveBuyIds.has(fill.id))
|
|
1923
|
+
continue;
|
|
1924
|
+
if (fill && fill.type === ORDER_TYPES.SELL && reserveSellIds && reserveSellIds.has(fill.id))
|
|
1925
|
+
continue;
|
|
1770
1926
|
if (fill.type === ORDER_TYPES.SELL)
|
|
1771
1927
|
netShift++;
|
|
1772
1928
|
else if (fill.type === ORDER_TYPES.BUY)
|
|
1773
1929
|
netShift--;
|
|
1774
1930
|
}
|
|
1931
|
+
if (!anchoredFromFills) {
|
|
1932
|
+
// Owed deltas from earlier uncommitted batches shift on top of the
|
|
1933
|
+
// current fills. Skipped under an absolute fill anchor: the anchor
|
|
1934
|
+
// positions from live market prices, which already reflect all
|
|
1935
|
+
// consumed fills — shifting again would double-count.
|
|
1936
|
+
for (const e of owedPending) {
|
|
1937
|
+
if (e.side === ORDER_TYPES.SELL)
|
|
1938
|
+
netShift++;
|
|
1939
|
+
else
|
|
1940
|
+
netShift--;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1775
1943
|
// Cap cumulative shift to prevent overreaction from burst fills.
|
|
1776
1944
|
// Uses a cross-chunk budget managed by the caller — each chunk
|
|
1777
1945
|
// consumes from the same pool so the total across all chunks
|
|
@@ -1780,6 +1948,16 @@ function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSl
|
|
|
1780
1948
|
const fallbackCap = Math.max(Math.floor((config.activeOrders?.sell ?? 1) / 2), Math.floor((config.activeOrders?.buy ?? 1) / 2), 1);
|
|
1781
1949
|
const effectiveBudget = crossChunkBudget ?? fallbackCap;
|
|
1782
1950
|
const cap = Math.min(Math.abs(effectiveBudget), fallbackCap);
|
|
1951
|
+
if (recovered && anchoredFromFills) {
|
|
1952
|
+
// The anchor already contains this batch's fill information —
|
|
1953
|
+
// crawling would double-count the same fills (Sep-10 batch 1:
|
|
1954
|
+
// dust-sell anchor 97 plus a +1 crawl would have moved it to 98).
|
|
1955
|
+
// The next batch crawls normally from the anchored boundary.
|
|
1956
|
+
return {
|
|
1957
|
+
boundaryIdx: Math.max(0, Math.min((allSlots.length - gapSlots - 1) >= 0 ? (allSlots.length - gapSlots - 1) : (allSlots.length - 1), newBoundaryIdx)),
|
|
1958
|
+
remainingBudget: effectiveBudget,
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1783
1961
|
if (Math.abs(netShift) > cap) {
|
|
1784
1962
|
netShift = Math.sign(netShift) * cap;
|
|
1785
1963
|
}
|
|
@@ -1799,16 +1977,488 @@ function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSl
|
|
|
1799
1977
|
remainingBudget,
|
|
1800
1978
|
};
|
|
1801
1979
|
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Apply recorded-but-uncommitted fill crawls to the committed boundary.
|
|
1982
|
+
* Fills are recorded at intake (strategy) and consumed by derivation on
|
|
1983
|
+
* commit — but a refused broadcast, a plan abort, or a restart in between
|
|
1984
|
+
* leaves their crawl owed and the boundary stale. The next derivation
|
|
1985
|
+
* incorporates them in-run (pendingCrawls param); this consumes them onto
|
|
1986
|
+
* a restored boundary at startup, before reconcile refills holes.
|
|
1987
|
+
*
|
|
1988
|
+
* Safety: entries are relative deltas, so they apply only onto a FINITE
|
|
1989
|
+
* restored boundary (a null boundary is re-anchored absolutely from live
|
|
1990
|
+
* fill prices instead — subsuming every owed delta). The candidate is
|
|
1991
|
+
* validated placed-order-aware like GRID-LOAD; on failure the entries are
|
|
1992
|
+
* dropped rather than stranding live orders. Commits clear the record, so
|
|
1993
|
+
* entries present here predate every commit since recording — always owed.
|
|
1994
|
+
*
|
|
1995
|
+
* @param {any} manager - OrderManager (boundaryIdx, orders, config restored)
|
|
1996
|
+
* @returns {{applied: boolean, from?: number, to?: number, count?: number, reason?: string}}
|
|
1997
|
+
*/
|
|
1998
|
+
export function consumePendingFillCrawls(manager) {
|
|
1999
|
+
const pending = Array.isArray(manager?._pendingFillCrawls) ? manager._pendingFillCrawls : [];
|
|
2000
|
+
if (pending.length === 0)
|
|
2001
|
+
return { applied: false };
|
|
2002
|
+
// Clearing marks the grid dirty so the cleared record reaches disk on
|
|
2003
|
+
// the next flush — including drop paths (unsafe/null/no-op), whose
|
|
2004
|
+
// decisions re-derive identically but whose stale disk entries would
|
|
2005
|
+
// otherwise linger until an unrelated write.
|
|
2006
|
+
const clear = () => {
|
|
2007
|
+
manager._pendingFillCrawls = [];
|
|
2008
|
+
if (typeof manager?._markGridDirty === 'function') {
|
|
2009
|
+
try {
|
|
2010
|
+
manager._markGridDirty();
|
|
2011
|
+
}
|
|
2012
|
+
catch { /* best-effort */ }
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
// NB: Number(null) === 0 — check null/undefined explicitly, or a
|
|
2016
|
+
// boundary-less manager would "apply" onto slot 0.
|
|
2017
|
+
if (manager?.boundaryIdx === null || manager?.boundaryIdx === undefined) {
|
|
2018
|
+
clear();
|
|
2019
|
+
return { applied: false, reason: 'null-boundary' };
|
|
2020
|
+
}
|
|
2021
|
+
const boundary = Number(manager?.boundaryIdx);
|
|
2022
|
+
if (!Number.isFinite(boundary)) {
|
|
2023
|
+
clear();
|
|
2024
|
+
return { applied: false, reason: 'null-boundary' };
|
|
2025
|
+
}
|
|
2026
|
+
const config = manager?.config ?? {};
|
|
2027
|
+
const slots = Array.from(manager?.orders instanceof Map ? manager.orders.values() : []);
|
|
2028
|
+
// Classify with the SAME live anchors the strategy derivation uses, or the
|
|
2029
|
+
// two disagree: the config-bound fallback is null for mode-string/relative
|
|
2030
|
+
// bounds, so a restart would rank a stale below-rail slot as a reserve and
|
|
2031
|
+
// silently drop a crawl the live run recorded as ordinary market movement.
|
|
2032
|
+
const reserveBuyIds = reserveEdgeIdSet(slots, config, ORDER_TYPES.BUY, resolveLiveReserveEdgeAnchorPrice(manager, 'buy'));
|
|
2033
|
+
const reserveSellIds = reserveEdgeIdSet(slots, config, ORDER_TYPES.SELL, resolveLiveReserveEdgeAnchorPrice(manager, 'sell'));
|
|
2034
|
+
let netShift = 0;
|
|
2035
|
+
let count = 0;
|
|
2036
|
+
for (const e of pending) {
|
|
2037
|
+
if (!e || typeof e.slotId !== 'string' || e.slotId.length === 0)
|
|
2038
|
+
continue;
|
|
2039
|
+
if (e.side !== ORDER_TYPES.BUY && e.side !== ORDER_TYPES.SELL)
|
|
2040
|
+
continue;
|
|
2041
|
+
if (e.side === ORDER_TYPES.BUY && reserveBuyIds && reserveBuyIds.has(e.slotId))
|
|
2042
|
+
continue;
|
|
2043
|
+
if (e.side === ORDER_TYPES.SELL && reserveSellIds && reserveSellIds.has(e.slotId))
|
|
2044
|
+
continue;
|
|
2045
|
+
netShift += e.side === ORDER_TYPES.SELL ? 1 : -1;
|
|
2046
|
+
count++;
|
|
2047
|
+
}
|
|
2048
|
+
if (count === 0) {
|
|
2049
|
+
clear();
|
|
2050
|
+
return { applied: false, reason: 'nothing-owed' };
|
|
2051
|
+
}
|
|
2052
|
+
const fallbackCap = Math.max(Math.floor((config?.activeOrders?.sell ?? 1) / 2), Math.floor((config?.activeOrders?.buy ?? 1) / 2), 1);
|
|
2053
|
+
if (Math.abs(netShift) > fallbackCap)
|
|
2054
|
+
netShift = Math.sign(netShift) * fallbackCap;
|
|
2055
|
+
let gapSlots = Number(manager?._gapSlots);
|
|
2056
|
+
if (!Number.isFinite(gapSlots)) {
|
|
2057
|
+
try {
|
|
2058
|
+
gapSlots = MathUtils.calculateGapSlots(config?.incrementPercent, config?.targetSpreadPercent, config?.gridLimits);
|
|
2059
|
+
}
|
|
2060
|
+
catch {
|
|
2061
|
+
gapSlots = 0;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const ceiling = (slots.length - gapSlots - 1) >= 0 ? (slots.length - gapSlots - 1) : (slots.length - 1);
|
|
2065
|
+
const candidate = Math.max(0, Math.min(ceiling, boundary + netShift));
|
|
2066
|
+
if (candidate === boundary) {
|
|
2067
|
+
clear();
|
|
2068
|
+
return { applied: false, reason: 'no-op' };
|
|
2069
|
+
}
|
|
2070
|
+
let check = { ok: true };
|
|
2071
|
+
try {
|
|
2072
|
+
check = MathUtils.validatePersistedBoundary(candidate, slots, gapSlots);
|
|
2073
|
+
}
|
|
2074
|
+
catch (err) {
|
|
2075
|
+
check = { ok: false, reason: 'validator-threw', detail: String(err?.message ?? err) };
|
|
2076
|
+
}
|
|
2077
|
+
if (!check || check.ok !== true) {
|
|
2078
|
+
clear();
|
|
2079
|
+
return { applied: false, reason: `unsafe: ${check?.reason ?? 'unknown'}${check?.detail ? ` ${check.detail}` : ''}` };
|
|
2080
|
+
}
|
|
2081
|
+
try {
|
|
2082
|
+
manager._restoreBoundary(candidate);
|
|
2083
|
+
}
|
|
2084
|
+
catch {
|
|
2085
|
+
return { applied: false, reason: 'restore-failed' };
|
|
2086
|
+
}
|
|
2087
|
+
clear();
|
|
2088
|
+
return { applied: true, from: boundary, to: candidate, count };
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Per-side reserve count (edge-pinned fat-finger insurance orders).
|
|
2092
|
+
* Buy reserves pin at the grid floor, sell reserves at the grid ceiling.
|
|
2093
|
+
* Non-finite/non-integer/negative values disable (0).
|
|
2094
|
+
*
|
|
2095
|
+
* @param {Object} config - Bot configuration
|
|
2096
|
+
* @param {string} side - 'buy' or 'sell'
|
|
2097
|
+
* @returns {number} Reserve count for the side (>= 0 integer)
|
|
2098
|
+
*/
|
|
2099
|
+
function resolveReserveCount(config, side) {
|
|
2100
|
+
const key = side === 'sell' ? 'sell' : 'buy';
|
|
2101
|
+
const raw = Number(config?.reserveOrders?.[key] ?? 0);
|
|
2102
|
+
if (!Number.isInteger(raw) || raw < 0)
|
|
2103
|
+
return 0;
|
|
2104
|
+
return raw;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Total reserve count across both sides (fee/count totals).
|
|
2108
|
+
*
|
|
2109
|
+
* @param {Object} config - Bot configuration
|
|
2110
|
+
* @returns {number} Total reserves (buy + sell)
|
|
2111
|
+
*/
|
|
2112
|
+
function resolveReserveOrders(config) {
|
|
2113
|
+
return resolveReserveCount(config, 'buy') + resolveReserveCount(config, 'sell');
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Edge-pinned reserve id set for one side, or null when disabled.
|
|
2117
|
+
* Type/price-filtered, then ordered by the SAME edge order the placement
|
|
2118
|
+
* pickers use (compareReserveEdge via selectReserveEdgeSlots) — single source
|
|
2119
|
+
* of truth, so the no-crawl classification can never drift from placement.
|
|
2120
|
+
* Anchor: explicit live-grid edge when supplied, otherwise the config-bound
|
|
2121
|
+
* fallback (unresolved -> plain rank). Shelf/manual ids (non-slot-N, e.g.
|
|
2122
|
+
* fork-kept deep-* orders below the rail) are never reserves: they would
|
|
2123
|
+
* otherwise win the cheapest-first rank and poison the deficit check while
|
|
2124
|
+
* the shelf is live (issue #27 follow-up). No-op upstream (grids only mint
|
|
2125
|
+
* slot-N).
|
|
2126
|
+
*
|
|
2127
|
+
* @param {Array<Object>} allSlots - All grid slots (need id/price/type)
|
|
2128
|
+
* @param {Object} config - Bot configuration (reserve count source)
|
|
2129
|
+
* @param {string} orderType - ORDER_TYPES.BUY (floor) or SELL (ceiling)
|
|
2130
|
+
* @param {number|null} [anchorPrice] - Explicit edge anchor (live grid edge);
|
|
2131
|
+
* callers that picked slots must pass the SAME anchor so no-crawl
|
|
2132
|
+
* classification matches placement. NB: null/undefined falls back to the
|
|
2133
|
+
* config-bound anchor (unresolved -> plain rank) — unlike
|
|
2134
|
+
* selectReserveEdgeSlots, where null alone means plain rank.
|
|
2135
|
+
* @param {Set<string>|null} [excludeIds] - Windowed ids to skip (same set the
|
|
2136
|
+
* placement pickers exclude). Window + edge are additive in every target
|
|
2137
|
+
* (order counts, fees, hold-back), so a window that reaches the grid edge
|
|
2138
|
+
* (e.g. a keep-low window sitting on the floor) must not let the edge pick
|
|
2139
|
+
* land on window members — otherwise counting reads N/N with zero
|
|
2140
|
+
* dedicated reserves and the deficit never fires (issue #27 follow-up).
|
|
2141
|
+
* @returns {Set<string>|null} Edge slot ids, or null when side disabled
|
|
2142
|
+
*/
|
|
2143
|
+
function reserveEdgeIdSet(allSlots, config, orderType, anchorPrice = null, excludeIds = null) {
|
|
2144
|
+
const isSell = orderType === ORDER_TYPES.SELL;
|
|
2145
|
+
const side = isSell ? 'sell' : 'buy';
|
|
2146
|
+
// Filter by the canonical side type, not the caller's token: the previous
|
|
2147
|
+
// per-side resolvers did the same, so a non-canonical token keeps the
|
|
2148
|
+
// floor behavior instead of silently matching nothing.
|
|
2149
|
+
const type = isSell ? ORDER_TYPES.SELL : ORDER_TYPES.BUY;
|
|
2150
|
+
const n = resolveReserveCount(config, side);
|
|
2151
|
+
if (n <= 0)
|
|
2152
|
+
return null;
|
|
2153
|
+
const ids = new Set();
|
|
2154
|
+
if (!Array.isArray(allSlots))
|
|
2155
|
+
return ids;
|
|
2156
|
+
// NB: Number(null) === 0 is finite — null/undefined must mean "no anchor".
|
|
2157
|
+
const anchor = anchorPrice == null ? resolveReserveEdgeAnchorPrice(config, side) : Number(anchorPrice);
|
|
2158
|
+
const ascending = allSlots
|
|
2159
|
+
.filter((s) => s && s.id != null && s.price != null && s.type === type && parseSlotIndex(s.id) !== null)
|
|
2160
|
+
.sort((a, b) => Number(a.price) - Number(b.price));
|
|
2161
|
+
for (const s of selectReserveEdgeSlots(ascending, n, excludeIds, isSell ? 'ceiling' : 'floor', anchor)) {
|
|
2162
|
+
ids.add(s.id);
|
|
2163
|
+
}
|
|
2164
|
+
return ids;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Window member ids for one side, mirroring the window every placement
|
|
2168
|
+
* picker excludes from its reserve pick: in-rail slots of the side (geometry
|
|
2169
|
+
* via resolveGapBand/isSlotInRail — the same source the reconcile pickers
|
|
2170
|
+
* use), ordered closest to market first (buys: highest price first; sells:
|
|
2171
|
+
* lowest first), sliced to the configured activeOrders count. The slice runs
|
|
2172
|
+
* over the FULL master rail, not just live orders — window membership is
|
|
2173
|
+
* geometric (a virtual hole inside the window still blocks the reserve pick
|
|
2174
|
+
* there), so live-only slices would misclassify live reserves as window
|
|
2175
|
+
* members whenever the window itself is under-filled.
|
|
2176
|
+
*
|
|
2177
|
+
* Returns null when the boundary geometry is unknown: the pickers cannot
|
|
2178
|
+
* place reserves without it either, so callers fail open (no exclusion)
|
|
2179
|
+
* and keep their previous classification instead of guessing.
|
|
2180
|
+
*
|
|
2181
|
+
* @param {any} manager - OrderManager (orders Map, config, boundaryIdx)
|
|
2182
|
+
* @param {string} orderType - ORDER_TYPES.BUY or ORDER_TYPES.SELL
|
|
2183
|
+
* @returns {Set<string>|null} Window slot ids, or null when geometry unknown
|
|
2184
|
+
*/
|
|
2185
|
+
function liveWindowIdSet(manager, orderType) {
|
|
2186
|
+
try {
|
|
2187
|
+
const isSell = orderType === ORDER_TYPES.SELL;
|
|
2188
|
+
const type = isSell ? ORDER_TYPES.SELL : ORDER_TYPES.BUY;
|
|
2189
|
+
const side = isSell ? 'sell' : 'buy';
|
|
2190
|
+
const count = Math.max(0, Math.floor(Number(manager?.config?.activeOrders?.[side])) || 0);
|
|
2191
|
+
if (!(count > 0) || !manager?.orders || typeof manager.orders.values !== 'function')
|
|
2192
|
+
return new Set();
|
|
2193
|
+
const resolved = MathUtils.resolveGapBand(manager);
|
|
2194
|
+
if (resolved?.boundaryIdx == null || resolved?.sellStartIdx == null)
|
|
2195
|
+
return null;
|
|
2196
|
+
const inRail = (o) => MathUtils.isSlotInRail(resolved.boundaryIdx, resolved.gapSlots, type, o);
|
|
2197
|
+
// Same type filter as the window pickers with known geometry: the
|
|
2198
|
+
// side's concrete type plus SPREAD placeholders (normalized empties
|
|
2199
|
+
// sitting in this side's rail).
|
|
2200
|
+
const typeFilter = (o) => o && o.id != null && o.price != null && (o.type === type || o.type === ORDER_TYPES.SPREAD);
|
|
2201
|
+
const ids = Array.from(manager.orders.values())
|
|
2202
|
+
.filter(typeFilter)
|
|
2203
|
+
.filter(inRail)
|
|
2204
|
+
.sort((a, b) => isSell ? Number(a.price) - Number(b.price) : Number(b.price) - Number(a.price))
|
|
2205
|
+
.slice(0, count)
|
|
2206
|
+
.map((o) => String(o.id));
|
|
2207
|
+
return new Set(ids);
|
|
2208
|
+
}
|
|
2209
|
+
catch {
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Refill-slot wire for the COW boundary hold (single source for both plan
|
|
2215
|
+
* producers: the fill-driven COW engine and the divergence fold).
|
|
2216
|
+
*
|
|
2217
|
+
* The hold keeps the committed boundary when a listed refill is guard-skipped
|
|
2218
|
+
* at broadcast — the refill is what justified the plan's boundary shift, so
|
|
2219
|
+
* committing the shift without it would strand an empty rail slot past the
|
|
2220
|
+
* new boundary. The wire must therefore list only placements that justify the
|
|
2221
|
+
* shift:
|
|
2222
|
+
*
|
|
2223
|
+
* - CREATE ids of the plan (the slots a fold did not convert into an
|
|
2224
|
+
* UPDATE), minus
|
|
2225
|
+
* - reserve-ladder ids. Reserves are static edge insurance; their fills
|
|
2226
|
+
* never crawl (deriveTargetBoundary filters them), so a guard-skipped
|
|
2227
|
+
* reserve must not pin geometry either. Without this exclusion a reserve
|
|
2228
|
+
* CREATE skipped at the wrong moment (e.g. a floor BUY above the last-fill
|
|
2229
|
+
* pivot while the market dumps below the grid) would hold the boundary for
|
|
2230
|
+
* a cycle although nothing was stranded.
|
|
2231
|
+
*
|
|
2232
|
+
* Absent/disabled reserves or an empty action list yield the plain CREATE ids,
|
|
2233
|
+
* so callers that never configured reserves keep the previous behavior.
|
|
2234
|
+
*
|
|
2235
|
+
* @param {Array<Object>} actions - Optimized COW actions
|
|
2236
|
+
* @param {Object} [options]
|
|
2237
|
+
* @param {Object} [options.config] - Bot configuration (reserve count source)
|
|
2238
|
+
* @param {Iterable<Object>} [options.slots] - Master slots (reserve classification)
|
|
2239
|
+
* @param {{buy?: number|null, sell?: number|null}} [options.edgeAnchors] - Live
|
|
2240
|
+
* edge anchors (same pair the strategy classifies reserve fills against)
|
|
2241
|
+
* @returns {string[]} Refill slot ids (CREATE ids minus reserve edge ids)
|
|
2242
|
+
*/
|
|
2243
|
+
function collectRefillSlotIds(actions, options = {}) {
|
|
2244
|
+
const { config = null, slots = null, edgeAnchors = null } = options;
|
|
2245
|
+
const out = [];
|
|
2246
|
+
if (!Array.isArray(actions))
|
|
2247
|
+
return out;
|
|
2248
|
+
const createIds = actions
|
|
2249
|
+
.filter((a) => a?.type === COW_ACTIONS.CREATE && typeof a?.id === 'string' && a.id.length > 0)
|
|
2250
|
+
.map((a) => a.id);
|
|
2251
|
+
if (createIds.length === 0)
|
|
2252
|
+
return out;
|
|
2253
|
+
let reserveIds = null;
|
|
2254
|
+
if (slots && config) {
|
|
2255
|
+
try {
|
|
2256
|
+
// Accept a Map (master grid), an array of slots, or any iterable of
|
|
2257
|
+
// slot objects. Map entries are [id, slot] pairs, so `.values()` is
|
|
2258
|
+
// required — Array.from(map) would hand reserveEdgeIdSet pairs.
|
|
2259
|
+
const allSlots = Array.isArray(slots)
|
|
2260
|
+
? slots
|
|
2261
|
+
: (typeof slots?.values === 'function'
|
|
2262
|
+
? Array.from(slots.values())
|
|
2263
|
+
: Array.from(slots));
|
|
2264
|
+
const buyIds = reserveEdgeIdSet(allSlots, config, ORDER_TYPES.BUY, edgeAnchors?.buy ?? null);
|
|
2265
|
+
const sellIds = reserveEdgeIdSet(allSlots, config, ORDER_TYPES.SELL, edgeAnchors?.sell ?? null);
|
|
2266
|
+
if (buyIds || sellIds)
|
|
2267
|
+
reserveIds = new Set([...(buyIds ?? []), ...(sellIds ?? [])]);
|
|
2268
|
+
}
|
|
2269
|
+
catch {
|
|
2270
|
+
reserveIds = null;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
for (const id of createIds) {
|
|
2274
|
+
// Fail-open on classification errors: an id we cannot prove is a reserve
|
|
2275
|
+
// stays in the wire, so the hold keeps its previous (conservative) reach.
|
|
2276
|
+
if (reserveIds && reserveIds.has(id))
|
|
2277
|
+
continue;
|
|
2278
|
+
out.push(id);
|
|
2279
|
+
}
|
|
2280
|
+
return out;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Resolved bound anchor for reserve edges from CONFIG alone.
|
|
2284
|
+
* BUY floor anchors toward minPrice (dip-insurance end), SELL ceiling toward
|
|
2285
|
+
* maxPrice (spike-insurance end). Resolves numeric and "Nx" relative forms
|
|
2286
|
+
* via resolveConfiguredPriceBound (startPrice-referenced); falls back to the
|
|
2287
|
+
* raw numeric bound. Returns null when unresolvable.
|
|
2288
|
+
*
|
|
2289
|
+
* Known limit: this is the statically resolved config bound, not the
|
|
2290
|
+
* gridPrice/AMA-referenced live rail bound. Placement call sites should use
|
|
2291
|
+
* resolveLiveReserveEdgeAnchorPrice(manager, side), which prefers the live
|
|
2292
|
+
* grid geometry and only falls back to this function.
|
|
2293
|
+
*
|
|
2294
|
+
* @param {Object} config - Bot configuration
|
|
2295
|
+
* @param {string} side - 'buy' or 'sell'
|
|
2296
|
+
* @returns {number|null} Finite anchor price, or null
|
|
2297
|
+
*/
|
|
2298
|
+
function resolveReserveEdgeAnchorPrice(config, side) {
|
|
2299
|
+
const isSell = side === 'sell';
|
|
2300
|
+
const bound = isSell ? config?.maxPrice : config?.minPrice;
|
|
2301
|
+
const mode = isSell ? 'max' : 'min';
|
|
2302
|
+
try {
|
|
2303
|
+
const anchor = resolveConfiguredPriceBound(bound, Number.NaN, Number(config?.startPrice), mode);
|
|
2304
|
+
if (Number.isFinite(anchor))
|
|
2305
|
+
return anchor;
|
|
2306
|
+
}
|
|
2307
|
+
catch (e) { /* fall through to raw bound */ }
|
|
2308
|
+
const raw = Number(bound);
|
|
2309
|
+
return Number.isFinite(raw) ? raw : null;
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Live-grid reserve edge anchor (single source for edge placement).
|
|
2313
|
+
*
|
|
2314
|
+
* The anchor must come from the geometry the bot is actually trading, never
|
|
2315
|
+
* from a config value that can be a mode string ("pool"/"book"), a relative
|
|
2316
|
+
* multiplier, or a stale bound. Tiers, strongest first:
|
|
2317
|
+
*
|
|
2318
|
+
* 1. Genesis ladder extreme — `_genesis.priceLevels` is the exact ladder the
|
|
2319
|
+
* loaded grid was built from: sorted ascending, index-aligned with
|
|
2320
|
+
* `slot-<idx>` (assertSlotPriceInvariant), refreshed by initializeGrid,
|
|
2321
|
+
* persisted with the grid, and unaffected by the raw-profile re-merge a
|
|
2322
|
+
* resync performs. Slot 0 is always on the buy rail and the last level
|
|
2323
|
+
* always on the sell rail, so the ladder extremes are the live rail
|
|
2324
|
+
* bounds.
|
|
2325
|
+
* 2. Live in-rail extreme of the master grid — geometry-only rail
|
|
2326
|
+
* membership (resolveGapBand + isSlotInRail, the same predicate the
|
|
2327
|
+
* selectors use) for snapshots without a genesis.
|
|
2328
|
+
* 3. Config bound (resolveReserveEdgeAnchorPrice) — the previous behavior,
|
|
2329
|
+
* kept as the last resolved tier.
|
|
2330
|
+
* 4. null — callers keep the legacy rank-based selection.
|
|
2331
|
+
*
|
|
2332
|
+
* @param {Object} manager - OrderManager (needs _genesis, orders, boundary)
|
|
2333
|
+
* @param {string} side - 'buy' or 'sell'
|
|
2334
|
+
* @returns {number|null} Finite anchor price, or null
|
|
2335
|
+
*/
|
|
2336
|
+
function resolveLiveReserveEdgeAnchorPrice(manager, side) {
|
|
2337
|
+
const isSell = side === 'sell';
|
|
2338
|
+
// Tier 1 — the ladder the loaded grid was generated from.
|
|
2339
|
+
const levels = manager?._genesis?.priceLevels;
|
|
2340
|
+
if (Array.isArray(levels) && levels.length > 0) {
|
|
2341
|
+
const extreme = Number(isSell ? levels[levels.length - 1] : levels[0]);
|
|
2342
|
+
if (Number.isFinite(extreme) && extreme > 0)
|
|
2343
|
+
return extreme;
|
|
2344
|
+
}
|
|
2345
|
+
// Tier 2 — live in-rail extreme of the master grid.
|
|
2346
|
+
const sideType = isSell ? ORDER_TYPES.SELL : ORDER_TYPES.BUY;
|
|
2347
|
+
if (manager?.orders && typeof manager.orders.values === 'function') {
|
|
2348
|
+
let best = null;
|
|
2349
|
+
try {
|
|
2350
|
+
const band = MathUtils.resolveGapBand(manager);
|
|
2351
|
+
for (const entry of manager.orders.values()) {
|
|
2352
|
+
if (!entry || typeof entry !== 'object')
|
|
2353
|
+
continue;
|
|
2354
|
+
if (!('type' in entry) || !('price' in entry))
|
|
2355
|
+
continue;
|
|
2356
|
+
if (entry.type !== sideType)
|
|
2357
|
+
continue;
|
|
2358
|
+
// Shelf/manual ids (e.g. fork-kept deep-* orders below the rail)
|
|
2359
|
+
// are never rail geometry: isSlotInRail is fail-open for
|
|
2360
|
+
// unparseable ids, so without this gate a cheap shelf order drags
|
|
2361
|
+
// the anchor down to itself and then qualifies as the reserve edge
|
|
2362
|
+
// (issue #27 follow-up). No-op upstream (grids only mint slot-N).
|
|
2363
|
+
if (parseSlotIndex(entry?.id) === null)
|
|
2364
|
+
continue;
|
|
2365
|
+
if (!MathUtils.isSlotInRail(band.boundaryIdx, band.gapSlots, sideType, entry))
|
|
2366
|
+
continue;
|
|
2367
|
+
const price = Number(entry.price);
|
|
2368
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
2369
|
+
continue;
|
|
2370
|
+
if (best === null || (isSell ? price > best : price < best))
|
|
2371
|
+
best = price;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
catch (e) {
|
|
2375
|
+
best = null;
|
|
2376
|
+
}
|
|
2377
|
+
if (best !== null)
|
|
2378
|
+
return best;
|
|
2379
|
+
}
|
|
2380
|
+
// Tier 3 — configured/resolved bound.
|
|
2381
|
+
return resolveReserveEdgeAnchorPrice(manager?.config, side);
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Shared anchored edge comparator for reserve selection (single source).
|
|
2385
|
+
* With a finite anchor: in-bound slots first, nearest the anchor first
|
|
2386
|
+
* (floor ascending, ceiling descending); stale out-of-bound slots last,
|
|
2387
|
+
* still nearest the anchor first. Without one: plain rank fallback
|
|
2388
|
+
* (floor rank-lowest, ceiling rank-highest).
|
|
2389
|
+
*
|
|
2390
|
+
* @param {Object} a - Slot/order (needs price)
|
|
2391
|
+
* @param {Object} b - Slot/order (needs price)
|
|
2392
|
+
* @param {string} edge - 'floor' or 'ceiling'
|
|
2393
|
+
* @param {number|null} anchorPrice - Resolved bound anchor (null = rank fallback)
|
|
2394
|
+
* @returns {number} Comparator result for Array.prototype.sort
|
|
2395
|
+
*/
|
|
2396
|
+
function compareReserveEdge(a, b, edge, anchorPrice) {
|
|
2397
|
+
const ceil = edge === 'ceiling';
|
|
2398
|
+
// NB: Number(null) === 0 is finite — null/undefined must mean "no anchor".
|
|
2399
|
+
const anchor = anchorPrice == null ? Number.NaN : Number(anchorPrice);
|
|
2400
|
+
if (!Number.isFinite(anchor)) {
|
|
2401
|
+
return ceil ? Number(b.price) - Number(a.price) : Number(a.price) - Number(b.price);
|
|
2402
|
+
}
|
|
2403
|
+
const pa = Number(a?.price);
|
|
2404
|
+
const pb = Number(b?.price);
|
|
2405
|
+
const aIn = ceil ? pa <= anchor : pa >= anchor;
|
|
2406
|
+
const bIn = ceil ? pb <= anchor : pb >= anchor;
|
|
2407
|
+
if (aIn !== bIn)
|
|
2408
|
+
return aIn ? -1 : 1;
|
|
2409
|
+
if (aIn)
|
|
2410
|
+
return ceil ? pb - pa : pa - pb;
|
|
2411
|
+
return ceil ? pa - pb : pb - pa;
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Central edge selector: take reserve slots from a price-ascending list,
|
|
2415
|
+
* skipping already-windowed ids. Both edges anchor at the live grid's own
|
|
2416
|
+
* edge (resolveLiveReserveEdgeAnchorPrice) when finite — floor: nearest
|
|
2417
|
+
* at/above the live floor first, slots below it rank last; ceiling: nearest
|
|
2418
|
+
* at/below the live ceiling first, slots above it rank last. Anchoring to the
|
|
2419
|
+
* live edge keeps the reserve on genuine live-rail slots when the grid still
|
|
2420
|
+
* carries leftovers from an older bound or the configured bound disagrees
|
|
2421
|
+
* with the geometry being traded. Callers pre-filter rail/type and apply
|
|
2422
|
+
* their own size gates; this only picks positions. A null anchor degrades to
|
|
2423
|
+
* plain rank (floor: lowest first; ceiling: highest first).
|
|
2424
|
+
*
|
|
2425
|
+
* @param {Array<Object>} sortedAsc - Slots sorted by price ascending
|
|
2426
|
+
* @param {number} count - Reserve count
|
|
2427
|
+
* @param {Set<string>|null} excludeIds - Windowed ids to skip
|
|
2428
|
+
* @param {string} edge - 'floor' or 'ceiling'
|
|
2429
|
+
* @param {number|null} [anchorPrice] - Live edge anchor for the side (null = rank-based)
|
|
2430
|
+
* @returns {Array<Object>} Reserve slots (ascending for floor, descending for ceiling)
|
|
2431
|
+
*/
|
|
2432
|
+
function selectReserveEdgeSlots(sortedAsc, count, excludeIds, edge, anchorPrice = null) {
|
|
2433
|
+
const n = Math.max(0, Math.floor(Number(count) || 0));
|
|
2434
|
+
if (n <= 0 || !Array.isArray(sortedAsc))
|
|
2435
|
+
return [];
|
|
2436
|
+
const avail = sortedAsc.filter((s) => s && s.id != null && (!excludeIds || !excludeIds.has(s.id)));
|
|
2437
|
+
// NB: Number(null) === 0 is finite — null/undefined must mean "no anchor".
|
|
2438
|
+
const anchor = anchorPrice == null ? Number.NaN : Number(anchorPrice);
|
|
2439
|
+
if (!Number.isFinite(anchor)) {
|
|
2440
|
+
// No anchor: plain rank fallback (avail arrives ascending) —
|
|
2441
|
+
// floor rank-lowest, ceiling rank-highest.
|
|
2442
|
+
return edge === 'ceiling' ? avail.slice(-n).reverse() : avail.slice(0, n);
|
|
2443
|
+
}
|
|
2444
|
+
// Anchored: shared comparator — in-bound slots nearest the bound first,
|
|
2445
|
+
// stale out-of-bound slots last.
|
|
2446
|
+
return avail
|
|
2447
|
+
.sort((x, y) => compareReserveEdge(x, y, edge, anchor))
|
|
2448
|
+
.slice(0, n);
|
|
2449
|
+
}
|
|
1802
2450
|
/**
|
|
1803
2451
|
* Total target order count across both sides (used for BTS fee calculation).
|
|
1804
2452
|
* Single source of truth so every budget derivation sizes identically.
|
|
2453
|
+
* Includes per-side reserves: they rest live on-chain and pay creation fees.
|
|
1805
2454
|
*
|
|
1806
2455
|
* @param {Object} config - Bot configuration
|
|
1807
2456
|
* @returns {number} Total target order count
|
|
1808
2457
|
*/
|
|
1809
2458
|
function getActiveOrdersTotal(config) {
|
|
1810
2459
|
return Math.max(0, config?.activeOrders?.buy ?? 1) +
|
|
1811
|
-
Math.max(0, config?.activeOrders?.sell ?? 1)
|
|
2460
|
+
Math.max(0, config?.activeOrders?.sell ?? 1) +
|
|
2461
|
+
resolveReserveOrders(config);
|
|
1812
2462
|
}
|
|
1813
2463
|
/**
|
|
1814
2464
|
* Calculate side budget after BTS fee deduction.
|
|
@@ -2035,7 +2685,7 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCr
|
|
|
2035
2685
|
}
|
|
2036
2686
|
}
|
|
2037
2687
|
// Existing chain ids referenced by non-create op contexts (cancel /
|
|
2038
|
-
// rotation / size-update)
|
|
2688
|
+
// rotation / size-update) are already live: they belong to the master set
|
|
2039
2689
|
// (cancels/fills in this batch), so they join the by-id set but never
|
|
2040
2690
|
// the lagging-create guard.
|
|
2041
2691
|
if (Array.isArray(placedContexts)) {
|
|
@@ -2058,6 +2708,6 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCr
|
|
|
2058
2708
|
const all = new Set([...masterIds, ...createIds]);
|
|
2059
2709
|
return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
|
|
2060
2710
|
}
|
|
2061
|
-
|
|
2062
|
-
export {
|
|
2711
|
+
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 };
|
|
2712
|
+
export { resolveReserveEdgeAnchorPrice, resolveLiveReserveEdgeAnchorPrice, compareReserveEdge, collectRefillSlotIds };
|
|
2063
2713
|
//# sourceMappingURL=order.js.map
|