dexbot 1.5.2 → 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 +79 -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 +11 -1
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/tradingview/README.md +32 -10
- package/analysis/tradingview/analyze_tradingview.ts +118 -3
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1392 -125
- 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 +8 -1
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +8 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +137 -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 +1387 -125
- 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/asymmetric_bounds.d.ts +33 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
- package/dist/market_adapter/core/asymmetric_bounds.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 +10 -18
- 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 +62 -37
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +797 -569
- 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 +585 -97
- 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 +170 -51
- 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 +66 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +238 -12
- 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 +90 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +20 -12
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +33 -12
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +308 -28
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +862 -38
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +37 -30
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +130 -101
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +3 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +11 -3
- 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 +9 -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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Includes grid indexing, order comparison, delta building, and strategy calculations.
|
|
6
6
|
*
|
|
7
7
|
* ===============================================================================
|
|
8
|
-
* TABLE OF CONTENTS (
|
|
8
|
+
* TABLE OF CONTENTS (35 exported functions)
|
|
9
9
|
* ===============================================================================
|
|
10
10
|
*
|
|
11
11
|
* SECTION 1: CHAIN ORDER MATCHING & RECONCILIATION (5 functions)
|
|
@@ -46,9 +46,8 @@
|
|
|
46
46
|
* - checkSizeThreshold(size, threshold) - Check if size exceeds threshold
|
|
47
47
|
* - checkSizesBeforeMinimum(sizes, minSize) - Check sizes against minimum
|
|
48
48
|
*
|
|
49
|
-
* SECTION 7: GRID BOUNDARY & ROLES (
|
|
49
|
+
* SECTION 7: GRID BOUNDARY & ROLES (3 functions)
|
|
50
50
|
* - calculateIdealBoundary(allSlots, startPrice, gapSlots) - Calculate ideal boundary
|
|
51
|
-
* - calculateFundDrivenBoundary(allSlots, availA, availB, startPrice, gapSlots) - Fund-driven boundary
|
|
52
51
|
* - assignGridRoles(allSlots, boundaryIdx, gapSlots, ...) - Assign BUY/SELL roles
|
|
53
52
|
* - shouldFlagOutOfSpread(order, startPrice, configSpread) - Check if order is out of spread
|
|
54
53
|
*
|
|
@@ -61,14 +60,18 @@
|
|
|
61
60
|
* - buildDelta(masterGrid, workingGrid) - Build delta actions between grids
|
|
62
61
|
* - getOrderSize(order) - Extract order size with fallback
|
|
63
62
|
*
|
|
64
|
-
* SECTION 10: STRATEGY CALCULATIONS (
|
|
65
|
-
* -
|
|
66
|
-
* -
|
|
67
|
-
* -
|
|
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)
|
|
68
71
|
*
|
|
69
72
|
* ===============================================================================
|
|
70
73
|
*/
|
|
71
|
-
import { ORDER_TYPES, ORDER_STATES, TIMING, FEE_PARAMETERS, GRID_LIMITS, NATIVE_CLIENT, COW_PERFORMANCE } from '../../constants.js';
|
|
74
|
+
import { ORDER_TYPES, ORDER_STATES, TIMING, FEE_PARAMETERS, GRID_LIMITS, NATIVE_CLIENT, COW_PERFORMANCE, COW_ACTIONS } from '../../constants.js';
|
|
72
75
|
import * as Format from '../format.js';
|
|
73
76
|
import * as MathUtils from './math.js';
|
|
74
77
|
import Logger from '../../order/logger.js';
|
|
@@ -1246,6 +1249,26 @@ function formatUnmatchedChainOrder(order) {
|
|
|
1246
1249
|
parts.push(`candidates=${order.candidateDiagnostics}`);
|
|
1247
1250
|
return parts.join(' ');
|
|
1248
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
|
+
}
|
|
1249
1272
|
/**
|
|
1250
1273
|
* Check if order is on blockchain (ACTIVE or PARTIAL state).
|
|
1251
1274
|
*
|
|
@@ -1417,6 +1440,13 @@ function checkSizesBeforeMinimum(sizes, minSize, precision) {
|
|
|
1417
1440
|
* Calculate ideal grid boundary based on reference price.
|
|
1418
1441
|
* Places boundary near reference price with gap spacing in mind.
|
|
1419
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
|
+
*
|
|
1420
1450
|
* @param {Array<Object>} allSlots - All grid slots sorted by price
|
|
1421
1451
|
* @param {number} referencePrice - Reference/anchor price
|
|
1422
1452
|
* @param {number} gapSlots - Number of gap slots between buy and sell
|
|
@@ -1425,32 +1455,16 @@ function checkSizesBeforeMinimum(sizes, minSize, precision) {
|
|
|
1425
1455
|
function calculateIdealBoundary(allSlots, referencePrice, gapSlots) {
|
|
1426
1456
|
if (!allSlots || allSlots.length === 0)
|
|
1427
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
|
+
}
|
|
1428
1462
|
let splitIdx = allSlots.findIndex((s) => s.price >= referencePrice);
|
|
1429
1463
|
if (splitIdx === -1)
|
|
1430
1464
|
splitIdx = allSlots.length;
|
|
1431
1465
|
const buySpread = Math.floor(gapSlots / 2);
|
|
1432
1466
|
return Math.max(0, Math.min(allSlots.length - 1, splitIdx - buySpread - 1));
|
|
1433
1467
|
}
|
|
1434
|
-
/**
|
|
1435
|
-
* Calculate grid boundary based on available funds ratio.
|
|
1436
|
-
* Distributes buy/sell slots proportional to fund values.
|
|
1437
|
-
*
|
|
1438
|
-
* @param {Array<Object>} allSlots - All grid slots sorted by price
|
|
1439
|
-
* @param {number} availA - Available assetA (sell-side capital)
|
|
1440
|
-
* @param {number} availB - Available assetB (buy-side capital)
|
|
1441
|
-
* @param {number} price - Current reference price for valuation
|
|
1442
|
-
* @param {number} gapSlots - Number of gap slots between buy and sell
|
|
1443
|
-
* @returns {number} Fund-driven boundary index
|
|
1444
|
-
*/
|
|
1445
|
-
function calculateFundDrivenBoundary(allSlots, availA, availB, price, gapSlots) {
|
|
1446
|
-
const valA = toFiniteNumber(availA) * toFiniteNumber(price);
|
|
1447
|
-
const valB = toFiniteNumber(availB);
|
|
1448
|
-
const totalVal = valA + valB;
|
|
1449
|
-
if (totalVal <= 0)
|
|
1450
|
-
return Math.floor((allSlots.length - gapSlots) / 2);
|
|
1451
|
-
const targetBuySlots = Math.round((allSlots.length - gapSlots) * (valB / totalVal));
|
|
1452
|
-
return Math.max(0, Math.min(allSlots.length - gapSlots - 1, targetBuySlots - 1));
|
|
1453
|
-
}
|
|
1454
1468
|
/**
|
|
1455
1469
|
* Assign BUY/SELL/SPREAD roles to grid slots based on boundary.
|
|
1456
1470
|
* Slots below boundary are BUY, above boundary are SELL, between are SPREAD.
|
|
@@ -1776,23 +1790,156 @@ function buildDelta(masterGrid, workingGrid, options = {}) {
|
|
|
1776
1790
|
function isShiftEligibleFill(fill) {
|
|
1777
1791
|
return fill?.isPartial !== true || fill?.isDelayedRotationTrigger === true;
|
|
1778
1792
|
}
|
|
1779
|
-
function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSlots, crossChunkBudget) {
|
|
1793
|
+
function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSlots, crossChunkBudget, pendingCrawls, edgeAnchors) {
|
|
1780
1794
|
let newBoundaryIdx = currentBoundaryIdx;
|
|
1781
|
-
//
|
|
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;
|
|
1782
1804
|
if (newBoundaryIdx === undefined || newBoundaryIdx === null) {
|
|
1783
|
-
|
|
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
|
+
}
|
|
1784
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;
|
|
1785
1895
|
}
|
|
1786
|
-
// Apply shift from fills with rate-limiting
|
|
1896
|
+
// Apply shift from fills with rate-limiting (reserve fills excluded: static insurance).
|
|
1787
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)));
|
|
1788
1919
|
for (const fill of fills) {
|
|
1789
1920
|
if (!isShiftEligibleFill(fill))
|
|
1790
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;
|
|
1791
1926
|
if (fill.type === ORDER_TYPES.SELL)
|
|
1792
1927
|
netShift++;
|
|
1793
1928
|
else if (fill.type === ORDER_TYPES.BUY)
|
|
1794
1929
|
netShift--;
|
|
1795
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
|
+
}
|
|
1796
1943
|
// Cap cumulative shift to prevent overreaction from burst fills.
|
|
1797
1944
|
// Uses a cross-chunk budget managed by the caller — each chunk
|
|
1798
1945
|
// consumes from the same pool so the total across all chunks
|
|
@@ -1801,15 +1948,25 @@ function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSl
|
|
|
1801
1948
|
const fallbackCap = Math.max(Math.floor((config.activeOrders?.sell ?? 1) / 2), Math.floor((config.activeOrders?.buy ?? 1) / 2), 1);
|
|
1802
1949
|
const effectiveBudget = crossChunkBudget ?? fallbackCap;
|
|
1803
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
|
+
}
|
|
1804
1961
|
if (Math.abs(netShift) > cap) {
|
|
1805
1962
|
netShift = Math.sign(netShift) * cap;
|
|
1806
1963
|
}
|
|
1807
1964
|
const remainingBudget = effectiveBudget - Math.abs(netShift);
|
|
1808
1965
|
newBoundaryIdx += netShift;
|
|
1809
|
-
// Clamp boundary — cap at one slot before the gap band's SELL rail
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1966
|
+
// Clamp boundary — cap at one slot before the gap band's SELL rail.
|
|
1967
|
+
// Degenerate geometries (fewer slots than the gap needs) fall back to the
|
|
1968
|
+
// legacy length-1 ceiling instead of collapsing the boundary below its
|
|
1969
|
+
// current position.
|
|
1813
1970
|
const gapAwareCeiling = allSlots.length - gapSlots - 1;
|
|
1814
1971
|
const legacyCeiling = allSlots.length - 1;
|
|
1815
1972
|
const ceiling = gapAwareCeiling >= 0
|
|
@@ -1820,16 +1977,488 @@ function deriveTargetBoundary(fills, currentBoundaryIdx, allSlots, config, gapSl
|
|
|
1820
1977
|
remainingBudget,
|
|
1821
1978
|
};
|
|
1822
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
|
+
}
|
|
1823
2450
|
/**
|
|
1824
2451
|
* Total target order count across both sides (used for BTS fee calculation).
|
|
1825
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.
|
|
1826
2454
|
*
|
|
1827
2455
|
* @param {Object} config - Bot configuration
|
|
1828
2456
|
* @returns {number} Total target order count
|
|
1829
2457
|
*/
|
|
1830
2458
|
function getActiveOrdersTotal(config) {
|
|
1831
2459
|
return Math.max(0, config?.activeOrders?.buy ?? 1) +
|
|
1832
|
-
Math.max(0, config?.activeOrders?.sell ?? 1)
|
|
2460
|
+
Math.max(0, config?.activeOrders?.sell ?? 1) +
|
|
2461
|
+
resolveReserveOrders(config);
|
|
1833
2462
|
}
|
|
1834
2463
|
/**
|
|
1835
2464
|
* Calculate side budget after BTS fee deduction.
|
|
@@ -1885,5 +2514,200 @@ function calculateBudgetedSizes(slots, side, budget, weightDist, incrementPercen
|
|
|
1885
2514
|
0, precision);
|
|
1886
2515
|
}
|
|
1887
2516
|
// ================================================================================
|
|
1888
|
-
|
|
2517
|
+
// SECTION: COW batch-shared pure helpers (moved from dexbot_cow_runtime.ts —
|
|
2518
|
+
// no bot dependency; shared by the COW runtime and any future consumer).
|
|
2519
|
+
// ================================================================================
|
|
2520
|
+
/**
|
|
2521
|
+
* Whether a chain order still matches the cached pre-update state the
|
|
2522
|
+
* limit_order_update delta was built from. Only a provably-unchanged order
|
|
2523
|
+
* makes a re-broadcast of the identical delta safe (it applies to the same
|
|
2524
|
+
* base). Any other state (target applied, filled, resized) must defer.
|
|
2525
|
+
* @param {Object} chainOrder - Raw chain order object (get_full_accounts)
|
|
2526
|
+
* @param {Object|null} cachedRaw - The rawOnChain cache captured at build time
|
|
2527
|
+
* @returns {boolean}
|
|
2528
|
+
*/
|
|
2529
|
+
function chainOrderUnchangedFromCache(chainOrder, cachedRaw) {
|
|
2530
|
+
if (!chainOrder || !cachedRaw)
|
|
2531
|
+
return false;
|
|
2532
|
+
const base = chainOrder.sell_price?.base;
|
|
2533
|
+
const quote = chainOrder.sell_price?.quote;
|
|
2534
|
+
const cachedBase = cachedRaw.sell_price?.base?.amount;
|
|
2535
|
+
const cachedQuote = cachedRaw.sell_price?.quote?.amount;
|
|
2536
|
+
const cachedForSale = cachedRaw.for_sale;
|
|
2537
|
+
if (base === undefined || quote === undefined)
|
|
2538
|
+
return false;
|
|
2539
|
+
if (cachedForSale === undefined || cachedBase === undefined || cachedQuote === undefined)
|
|
2540
|
+
return false;
|
|
2541
|
+
return String(base.amount ?? '') === String(cachedBase)
|
|
2542
|
+
&& String(quote.amount ?? '') === String(cachedQuote)
|
|
2543
|
+
&& String(chainOrder.for_sale ?? '') === String(cachedForSale);
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* PRE-BROADCAST CROSSED-BOOK ASSERT (defense-in-depth, any-writer detection).
|
|
2547
|
+
*
|
|
2548
|
+
* Simulates the post-batch book: currently placed master orders plus this
|
|
2549
|
+
* batch's action overlay (CREATEs add, CANCELs remove, UPDATEs reprice/move).
|
|
2550
|
+
* Returns a detail string when a planned BUY would price at-or-above a planned
|
|
2551
|
+
* SELL — a state no honest planner produces — so the caller can refuse the
|
|
2552
|
+
* broadcast instead of paying for adverse fills. Placed order prices are
|
|
2553
|
+
* independent of grid geometry, so this catches boundary overruns regardless
|
|
2554
|
+
* of which writer produced them.
|
|
2555
|
+
*
|
|
2556
|
+
* Detector only: any internal failure returns null (never blocks a broadcast).
|
|
2557
|
+
*/
|
|
2558
|
+
function detectCrossedBookPlan(manager, actions) {
|
|
2559
|
+
try {
|
|
2560
|
+
const startPrice = Number(manager?.config?.startPrice);
|
|
2561
|
+
const book = new Map();
|
|
2562
|
+
for (const o of Array.from(manager?.orders?.values?.() ?? [])) {
|
|
2563
|
+
if (!o || !o.orderId || o.price == null)
|
|
2564
|
+
continue;
|
|
2565
|
+
const price = Number(o.price);
|
|
2566
|
+
if (!Number.isFinite(price))
|
|
2567
|
+
continue;
|
|
2568
|
+
let type = o.type;
|
|
2569
|
+
if (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL) {
|
|
2570
|
+
// Legacy SPREAD-typed placed order: derive side from the same
|
|
2571
|
+
// price-vs-startPrice convention used across the codebase.
|
|
2572
|
+
if (!Number.isFinite(startPrice))
|
|
2573
|
+
continue;
|
|
2574
|
+
type = price < startPrice ? ORDER_TYPES.BUY : ORDER_TYPES.SELL;
|
|
2575
|
+
}
|
|
2576
|
+
book.set(String(o.id), { type, price });
|
|
2577
|
+
}
|
|
2578
|
+
for (const a of actions ?? []) {
|
|
2579
|
+
const id = String(a.id ?? a.orderId ?? '');
|
|
2580
|
+
if (a.type === COW_ACTIONS.CANCEL) {
|
|
2581
|
+
if (id)
|
|
2582
|
+
book.delete(id);
|
|
2583
|
+
}
|
|
2584
|
+
else if (a.type === COW_ACTIONS.UPDATE) {
|
|
2585
|
+
const newPrice = Number(a.newPrice ?? a.order?.price);
|
|
2586
|
+
const newType = a.order?.type;
|
|
2587
|
+
if (id && Number.isFinite(newPrice)) {
|
|
2588
|
+
const entry = book.get(id);
|
|
2589
|
+
const type = (newType === ORDER_TYPES.BUY || newType === ORDER_TYPES.SELL)
|
|
2590
|
+
? newType
|
|
2591
|
+
: entry?.type;
|
|
2592
|
+
if (entry)
|
|
2593
|
+
book.delete(id);
|
|
2594
|
+
const key = String(a.newGridId ?? id);
|
|
2595
|
+
if (type === ORDER_TYPES.BUY || type === ORDER_TYPES.SELL) {
|
|
2596
|
+
book.set(key, { type, price: newPrice });
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
else if (a.type === COW_ACTIONS.CREATE) {
|
|
2601
|
+
const price = Number(a.order?.price);
|
|
2602
|
+
const type = a.order?.type;
|
|
2603
|
+
if (!Number.isFinite(price) || (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL))
|
|
2604
|
+
continue;
|
|
2605
|
+
if (id)
|
|
2606
|
+
book.set(id, { type, price });
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
let maxBuy = -Infinity;
|
|
2610
|
+
let minSell = Infinity;
|
|
2611
|
+
for (const { type, price } of book.values()) {
|
|
2612
|
+
if (type === ORDER_TYPES.BUY && price > maxBuy)
|
|
2613
|
+
maxBuy = price;
|
|
2614
|
+
else if (type === ORDER_TYPES.SELL && price < minSell)
|
|
2615
|
+
minSell = price;
|
|
2616
|
+
}
|
|
2617
|
+
if (Number.isFinite(maxBuy) && Number.isFinite(minSell) && minSell <= maxBuy) {
|
|
2618
|
+
return `bestPlacedBuy=${maxBuy} >= bestPlacedSell=${minSell}`;
|
|
2619
|
+
}
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
catch {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Collect every on-chain order id master currently needs to converge against,
|
|
2628
|
+
* so adoption can re-read them by id (immune to the get_full_accounts window
|
|
2629
|
+
* truncation) instead of relying on a partial window read.
|
|
2630
|
+
*
|
|
2631
|
+
* Sources:
|
|
2632
|
+
* - master's own tracked order ids (existing on-chain orders);
|
|
2633
|
+
* - the batch's fresh CREATE ids extracted from the broadcast result
|
|
2634
|
+
* (operation_results[i][1] aligns positionally with placedContexts[i]).
|
|
2635
|
+
*
|
|
2636
|
+
* @param {any} mgr - bot.manager
|
|
2637
|
+
* @param {any} placedResults - broadcast result (has operation_results); null when unavailable
|
|
2638
|
+
* @param {any[]} placedContexts - opContexts (aligned with operation_results); null when unavailable
|
|
2639
|
+
* @param {string[]|null} [extraCreateIds=null] - fresh CREATE chain ids from another
|
|
2640
|
+
* authoritative source (e.g. the uncertain-broadcast poll confirmation) when
|
|
2641
|
+
* no broadcast result exists; merged into createIds so the lagging-create
|
|
2642
|
+
* retry guards them
|
|
2643
|
+
* @returns {string[]} Unique, well-formed 1.7.x order ids
|
|
2644
|
+
*/
|
|
2645
|
+
function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCreateIds = null) {
|
|
2646
|
+
const masterIds = new Set();
|
|
2647
|
+
const grid = mgr && mgr.grid;
|
|
2648
|
+
if (Array.isArray(grid)) {
|
|
2649
|
+
for (const slot of grid) {
|
|
2650
|
+
if (slot && slot.orderId && /^1\.7\.\d+$/.test(String(slot.orderId))) {
|
|
2651
|
+
masterIds.add(String(slot.orderId));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
// Master tracked ids live in the orders Map (mgr.grid is legacy and
|
|
2656
|
+
// unset on OrderManager — without this the by-id set omits every
|
|
2657
|
+
// pre-existing ACTIVE order and pass-1 phantom cleanup would virtualize
|
|
2658
|
+
// them as fills on a partial snapshot).
|
|
2659
|
+
if (mgr && mgr.orders instanceof Map) {
|
|
2660
|
+
for (const slot of mgr.orders.values()) {
|
|
2661
|
+
if (slot && slot.orderId && /^1\.7\.\d+$/.test(String(slot.orderId))) {
|
|
2662
|
+
masterIds.add(String(slot.orderId));
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
const createIds = new Set();
|
|
2667
|
+
if (placedResults && Array.isArray(placedContexts)) {
|
|
2668
|
+
const opResults = extractBatchOperationResults(placedResults);
|
|
2669
|
+
if (Array.isArray(opResults)) {
|
|
2670
|
+
for (let i = 0; i < placedContexts.length; i++) {
|
|
2671
|
+
const ctx = placedContexts[i];
|
|
2672
|
+
if (!ctx || ctx.kind !== 'create')
|
|
2673
|
+
continue;
|
|
2674
|
+
const opResult = opResults[i] && opResults[i][1];
|
|
2675
|
+
if (opResult && /^1\.7\.\d+$/.test(String(opResult))) {
|
|
2676
|
+
createIds.add(String(opResult));
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (Array.isArray(extraCreateIds)) {
|
|
2682
|
+
for (const id of extraCreateIds) {
|
|
2683
|
+
if (id && /^1\.7\.\d+$/.test(String(id)))
|
|
2684
|
+
createIds.add(String(id));
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
// Existing chain ids referenced by non-create op contexts (cancel /
|
|
2688
|
+
// rotation / size-update) are already live: they belong to the master set
|
|
2689
|
+
// (cancels/fills in this batch), so they join the by-id set but never
|
|
2690
|
+
// the lagging-create guard.
|
|
2691
|
+
if (Array.isArray(placedContexts)) {
|
|
2692
|
+
for (const ctx of placedContexts) {
|
|
2693
|
+
if (!ctx || ctx.kind === 'create')
|
|
2694
|
+
continue;
|
|
2695
|
+
const refs = [];
|
|
2696
|
+
if (ctx.kind === 'cancel' && ctx.order)
|
|
2697
|
+
refs.push(ctx.order.orderId);
|
|
2698
|
+
else if (ctx.kind === 'rotation' && ctx.rotation?.oldOrder)
|
|
2699
|
+
refs.push(ctx.rotation.oldOrder.orderId);
|
|
2700
|
+
else if (ctx.kind === 'size-update' && ctx.updateInfo?.partialOrder)
|
|
2701
|
+
refs.push(ctx.updateInfo.partialOrder.orderId);
|
|
2702
|
+
for (const id of refs) {
|
|
2703
|
+
if (id && /^1\.7\.\d+$/.test(String(id)))
|
|
2704
|
+
masterIds.add(String(id));
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
const all = new Set([...masterIds, ...createIds]);
|
|
2709
|
+
return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
|
|
2710
|
+
}
|
|
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 };
|
|
1889
2713
|
//# sourceMappingURL=order.js.map
|