dexbot 1.6.2 → 1.6.4
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 +53 -3
- package/README.md +11 -7
- package/analysis/README.md +20 -14
- package/analysis/account_resolver.ts +171 -0
- package/analysis/ama_fitting/analyze_lambda_vs_slow.ts +2 -5
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +20 -27
- package/analysis/chain_pool.ts +47 -0
- package/analysis/chart_utils.ts +33 -20
- package/analysis/derivative_chart_generator.ts +2 -3
- package/analysis/fills_source.ts +234 -0
- package/analysis/grid_correction_check.ts +104 -244
- 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/trade_profitability.ts +169 -304
- package/analysis/tradingview/README.md +2 -2
- 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 +3 -4
- package/analysis/trend_detection/dynamic_weight_chart_generator.ts +2 -3
- package/analysis/trend_detection/kalman_chart_generator.ts +2 -3
- package/analysis/trend_detection/package.json +1 -1
- package/analysis/trend_detection/regime_chart_generator.ts +2 -3
- package/analysis/trend_detection/volatility_chart_generator.ts +2 -3
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/account_resolver.d.ts +43 -0
- package/dist/analysis/account_resolver.d.ts.map +1 -0
- package/dist/analysis/account_resolver.js +137 -0
- package/dist/analysis/account_resolver.js.map +1 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -1
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +2 -4
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +20 -28
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/chain_pool.d.ts +10 -0
- package/dist/analysis/chain_pool.d.ts.map +1 -0
- package/dist/analysis/chain_pool.js +47 -0
- package/dist/analysis/chain_pool.js.map +1 -0
- package/dist/analysis/chart_utils.d.ts +14 -1
- package/dist/analysis/chart_utils.d.ts.map +1 -1
- package/dist/analysis/chart_utils.js +32 -22
- package/dist/analysis/chart_utils.js.map +1 -1
- package/dist/analysis/derivative_chart_generator.d.ts.map +1 -1
- package/dist/analysis/derivative_chart_generator.js +2 -3
- package/dist/analysis/derivative_chart_generator.js.map +1 -1
- package/dist/analysis/fills_source.d.ts +79 -0
- package/dist/analysis/fills_source.d.ts.map +1 -0
- package/dist/analysis/fills_source.js +186 -0
- package/dist/analysis/fills_source.js.map +1 -0
- package/dist/analysis/grid_correction_check.d.ts +4 -20
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +95 -248
- package/dist/analysis/grid_correction_check.js.map +1 -1
- package/dist/analysis/trade_profitability.d.ts +18 -19
- package/dist/analysis/trade_profitability.d.ts.map +1 -1
- package/dist/analysis/trade_profitability.js +136 -280
- package/dist/analysis/trade_profitability.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +3 -4
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/regime_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -1
- package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
- package/dist/market_adapter/lp_chart_core.js +2 -3
- package/dist/market_adapter/lp_chart_core.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -18
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +1 -1
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.d.ts +1 -84
- package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.js +1 -38
- package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
- package/dist/modules/cli_colors.d.ts +0 -1
- package/dist/modules/cli_colors.d.ts.map +1 -1
- package/dist/modules/constants.js +5 -5
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credential_policy.d.ts +1 -25
- package/dist/modules/credential_policy.d.ts.map +1 -1
- package/dist/modules/credential_policy.js +1 -1
- package/dist/modules/credential_policy.js.map +1 -1
- package/dist/modules/credential_runtime.d.ts +1 -6
- package/dist/modules/credential_runtime.d.ts.map +1 -1
- package/dist/modules/credential_runtime.js +1 -1
- package/dist/modules/credential_runtime.js.map +1 -1
- package/dist/modules/credential_session_cache.d.ts +1 -5
- package/dist/modules/credential_session_cache.d.ts.map +1 -1
- package/dist/modules/credential_session_cache.js +1 -1
- package/dist/modules/credential_session_cache.js.map +1 -1
- package/dist/modules/crypto/pure_secp256k1.d.ts +1 -2
- package/dist/modules/crypto/pure_secp256k1.d.ts.map +1 -1
- package/dist/modules/crypto/pure_secp256k1.js +1 -1
- package/dist/modules/crypto/pure_secp256k1.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +9 -8
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +9 -8
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +87 -7
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +489 -14
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +1 -17
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +1 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +21 -5
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +1 -7
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +1 -1
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/launcher/adapter_requirement.d.ts +1 -5
- package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -1
- package/dist/modules/launcher/adapter_requirement.js +1 -1
- package/dist/modules/launcher/adapter_requirement.js.map +1 -1
- package/dist/modules/launcher/bot_supervisor.d.ts +1 -6
- package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
- package/dist/modules/launcher/bot_supervisor.js +1 -1
- package/dist/modules/launcher/bot_supervisor.js.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.d.ts +1 -3
- package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.js +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
- package/dist/modules/launcher/launch_modes.d.ts +1 -2
- package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
- package/dist/modules/launcher/launch_modes.js +1 -1
- package/dist/modules/launcher/launch_modes.js.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.d.ts +1 -3
- package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.d.ts +1 -5
- package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.js +1 -1
- package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
- package/dist/modules/launcher/status_reporting.d.ts +1 -2
- package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
- package/dist/modules/launcher/status_reporting.js +1 -1
- package/dist/modules/launcher/status_reporting.js.map +1 -1
- package/dist/modules/order/export.d.ts +1 -8
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -1
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts +7 -0
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +52 -98
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +1 -10
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +2 -2
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +10 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +20 -7
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +25 -20
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +1 -10
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +1 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +45 -7
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +160 -15
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +0 -15
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +2 -2
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/process_discovery.d.ts +0 -3
- package/dist/modules/process_discovery.d.ts.map +1 -1
- package/dist/modules/process_discovery.js +1 -1
- package/dist/modules/process_discovery.js.map +1 -1
- package/dist/modules/utils/chain_logs.d.ts +1 -2
- package/dist/modules/utils/chain_logs.d.ts.map +1 -1
- package/dist/modules/utils/chain_logs.js +1 -1
- package/dist/modules/utils/chain_logs.js.map +1 -1
- package/dist/scripts/analyze-git.js +2 -2
- package/dist/scripts/analyze-git.js.map +1 -1
- package/dist/scripts/update.js +46 -31
- package/dist/scripts/update.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +110 -40
- package/docs/COPY_ON_WRITE_MASTER_PLAN.md +10 -207
- package/docs/COW_INVARIANTS.md +16 -10
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +75 -29
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +6 -6
- package/docs/GRID_RECALCULATION.md +14 -6
- package/docs/GRID_RECONCILE.md +28 -25
- package/docs/LIFECYCLE.md +12 -11
- package/docs/README.md +48 -42
- package/docs/architecture.md +19 -30
- package/docs/developer_guide.md +3 -3
- package/package.json +1 -1
- package/scripts/git-viewer.sh +1 -1
- package/tests/README.md +10 -1
|
@@ -323,17 +323,23 @@ function formatUnmatchedChainOrderForLog(order) {
|
|
|
323
323
|
* Record a pending CREATE broadcast on the manager.
|
|
324
324
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
325
325
|
* @param {Object} entry
|
|
326
|
+
* @returns {string|null} The fingerprint the entry was stored under (null
|
|
327
|
+
* when recording was skipped). Callers that must later remap the entry's
|
|
328
|
+
* stored opIndex/ctxIndex (the final pivot gate's compaction) collect
|
|
329
|
+
* these to identify exactly which pending entries belong to THIS batch —
|
|
330
|
+
* entry.batchId is unreliable because _currentBatchId is never populated
|
|
331
|
+
* in production (always null), so batchId scoping cannot discriminate.
|
|
326
332
|
*/
|
|
327
333
|
function recordPendingBroadcast(bot, entry) {
|
|
328
334
|
if (!bot.manager || !entry || !entry.order)
|
|
329
|
-
return;
|
|
335
|
+
return null;
|
|
330
336
|
if (!bot.manager._pendingBroadcasts || !(bot.manager._pendingBroadcasts instanceof Map)) {
|
|
331
337
|
bot.manager._pendingBroadcasts = new Map();
|
|
332
338
|
}
|
|
333
339
|
const fingerprint = createOpFingerprintForSlot(bot, entry.order, entry.finalInts, entry.order.id);
|
|
334
340
|
if (!fingerprint) {
|
|
335
341
|
bot.manager.logger.log?.(`[COW] Skipped pending-broadcast record: could not build fingerprint for ${entry.order?.id || 'unknown'}`, 'warn');
|
|
336
|
-
return;
|
|
342
|
+
return null;
|
|
337
343
|
}
|
|
338
344
|
bot.manager._pendingBroadcasts.set(fingerprint, {
|
|
339
345
|
fingerprint,
|
|
@@ -347,6 +353,7 @@ function recordPendingBroadcast(bot, entry) {
|
|
|
347
353
|
batchId: bot._currentBatchId || null,
|
|
348
354
|
recordedAt: Date.now()
|
|
349
355
|
});
|
|
356
|
+
return fingerprint;
|
|
350
357
|
}
|
|
351
358
|
/**
|
|
352
359
|
* Clear the pending-broadcast cache.
|
|
@@ -452,7 +459,7 @@ function popPushedWorkingGrid(bot, cowResult) {
|
|
|
452
459
|
*/
|
|
453
460
|
async function deferUncertainBroadcastRead(bot, detail, suffix, resyncReason, resyncOptions = {}) {
|
|
454
461
|
bot.manager.logger.log(`[COW][UNCERTAIN] ${detail}; keeping pending-broadcast protection ${suffix}`, 'warn');
|
|
455
|
-
// Fix #6 (docs/
|
|
462
|
+
// Fix #6 (docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md §2): an ambiguous/truncated chain read is
|
|
456
463
|
// node lag, not a missing order — the broadcast already succeeded. Previously every
|
|
457
464
|
// such read requested a structural resync, piling pending broadcasts (up to 14) and
|
|
458
465
|
// forcing a resync mid-broadcast (the T-BTS 06:43Z thrash). The pending-broadcast
|
|
@@ -1287,11 +1294,12 @@ function formatPartialBroadcastSummary(err) {
|
|
|
1287
1294
|
}
|
|
1288
1295
|
/**
|
|
1289
1296
|
* Execute a batch with retry-on-uncertain semantics, enforcing a gap-slot
|
|
1290
|
-
* per-broadcast operation cap (_getGapSlotBatchSize). When the
|
|
1291
|
-
* more operations than the cap, it is split into sequential
|
|
1292
|
-
* of at most `maxOps` operations each, so a single on-chain
|
|
1293
|
-
* holds more than gapSlots order operations (the
|
|
1294
|
-
* per broadcast" intent, applied at the op level rather
|
|
1297
|
+
* per-broadcast operation cap (_getGapSlotBatchSize = gapSlots + 1). When the
|
|
1298
|
+
* batch carries more operations than the cap, it is split into sequential
|
|
1299
|
+
* broadcast chunks of at most `maxOps` operations each, so a single on-chain
|
|
1300
|
+
* transaction never holds more than gapSlots + 1 order operations (the
|
|
1301
|
+
* original "N fills per broadcast" intent, applied at the op level rather
|
|
1302
|
+
* than the fill level).
|
|
1295
1303
|
*
|
|
1296
1304
|
* Failure isolation — no swallowed orders: if one chunk's broadcast is
|
|
1297
1305
|
* uncertain (BroadcastUncertainError), the remaining chunks are STILL
|
|
@@ -3028,7 +3036,12 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3028
3036
|
bot.manager.logger.log(`[COW] Draining ${pendingCorrectionCount} pending correction(s) before batch`, 'info');
|
|
3029
3037
|
const drainResult = await orderUtils.correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
|
|
3030
3038
|
if (drainResult?.failed > 0) {
|
|
3031
|
-
bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch
|
|
3039
|
+
bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch` +
|
|
3040
|
+
(drainResult.staleDropped > 0 ? `, ${drainResult.staleDropped} stale dropped` : '') +
|
|
3041
|
+
`; remaining entries retry on next sync/maintenance tick`, 'warn');
|
|
3042
|
+
}
|
|
3043
|
+
else if (drainResult?.staleDropped > 0) {
|
|
3044
|
+
bot.manager.logger.log(`[COW] Pre-batch drain resolved, ${drainResult.staleDropped} stale correction(s) dropped`, 'info');
|
|
3032
3045
|
}
|
|
3033
3046
|
}
|
|
3034
3047
|
catch (drainErr) {
|
|
@@ -3055,6 +3068,12 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3055
3068
|
// would spam big batches, so the guard emits one batch summary instead
|
|
3056
3069
|
// (see the summary after the action loop below).
|
|
3057
3070
|
const lastFillGuardStats = { checked: 0, passed: 0, skipped: 0, bypassed: 0, pivotOffGrid: 0 };
|
|
3071
|
+
// Fingerprints of the pending-broadcast entries recorded by THIS batch's
|
|
3072
|
+
// op-building (both CREATE paths). The final pivot gate's compaction
|
|
3073
|
+
// remaps these entries' stored opIndex/ctxIndex to their post-drop
|
|
3074
|
+
// positions; entry.batchId cannot discriminate batches (always null in
|
|
3075
|
+
// production), so the fingerprint set is the ownership marker.
|
|
3076
|
+
const batchPendingFps = new Set();
|
|
3058
3077
|
// Per-batch GRID-PRICE-INVARIANT counters (BLOCKING). Tracks emitted prices
|
|
3059
3078
|
// that are not the genesis level for their slot — such emissions are
|
|
3060
3079
|
// rejected, not placed. One batch summary.
|
|
@@ -3108,12 +3127,33 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3108
3127
|
// mutated the pivot mid-batch (02:03 pivots drifted 0.001523→0.001529
|
|
3109
3128
|
// across 20 checks), so early actions were judged against a different
|
|
3110
3129
|
// pivot than later ones. The batch summary still reports whether this
|
|
3111
|
-
// freeze moved the pivot under the plan.
|
|
3130
|
+
// freeze moved the pivot under the plan. The frozen value feeds the
|
|
3131
|
+
// final pre-broadcast gate below (runFinalPivotGate), which re-checks
|
|
3132
|
+
// built ops when a fill queued AFTER the freeze moved the pivot.
|
|
3133
|
+
// freezeQueueDepth is the Step-2 observability half: queue depth at
|
|
3134
|
+
// freeze time, paired with the gate's own queue readout.
|
|
3135
|
+
let freezeQueueDepth = null;
|
|
3136
|
+
try {
|
|
3137
|
+
freezeQueueDepth = Array.isArray(bot?._incomingFillQueue)
|
|
3138
|
+
? bot._incomingFillQueue.length
|
|
3139
|
+
: null;
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
freezeQueueDepth = null;
|
|
3143
|
+
}
|
|
3112
3144
|
try {
|
|
3113
3145
|
if (refreshLastFillPivotFromQueue(bot))
|
|
3114
3146
|
lastFillGuardPivotRefreshed = true;
|
|
3115
3147
|
}
|
|
3116
3148
|
catch { /* best-effort */ }
|
|
3149
|
+
// Captured AFTER the freeze refresh, not before: the refresh is part
|
|
3150
|
+
// of the freeze, so the baseline must be the pivot the ops are about
|
|
3151
|
+
// to be judged against. Capturing pre-refresh would make every batch
|
|
3152
|
+
// whose freeze picked up a pre-freeze queued fill look "moved" at the
|
|
3153
|
+
// gate — a spurious warn plus a redundant full re-check against the
|
|
3154
|
+
// identical pivot.
|
|
3155
|
+
const frozenPivotAtBatchStart = bot.manager?._lastFilledPrice;
|
|
3156
|
+
const frozenTypeAtBatchStart = bot.manager?._lastFilledType;
|
|
3117
3157
|
for (const action of actions) {
|
|
3118
3158
|
if (action.type === COW_ACTIONS.CANCEL) {
|
|
3119
3159
|
try {
|
|
@@ -3248,12 +3288,14 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3248
3288
|
operations.push(buildResult.op);
|
|
3249
3289
|
opContexts.push({ kind: 'create', id: order.id, order: effectiveOrder, args, finalInts: buildResult.finalInts });
|
|
3250
3290
|
intraBatchCandidates.push(effectiveOrder);
|
|
3251
|
-
recordPendingBroadcast(bot, {
|
|
3291
|
+
const recordedFp = recordPendingBroadcast(bot, {
|
|
3252
3292
|
opIndex: operations.length - 1,
|
|
3253
3293
|
ctxIndex: opContexts.length - 1,
|
|
3254
3294
|
order: effectiveOrder,
|
|
3255
3295
|
finalInts: buildResult.finalInts
|
|
3256
3296
|
});
|
|
3297
|
+
if (recordedFp)
|
|
3298
|
+
batchPendingFps.add(recordedFp);
|
|
3257
3299
|
}
|
|
3258
3300
|
catch (err) {
|
|
3259
3301
|
bot.manager.logger.log(`Failed to prepare create op for ${action.id}: ${getErrorMessage(err)}`, 'error');
|
|
@@ -3652,12 +3694,14 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3652
3694
|
args: { amountToSell: fbArgs.amountToSell, minToReceive: fbArgs.minToReceive },
|
|
3653
3695
|
finalInts: fbResult.finalInts
|
|
3654
3696
|
});
|
|
3655
|
-
recordPendingBroadcast(bot, {
|
|
3697
|
+
const fbRecordedFp = recordPendingBroadcast(bot, {
|
|
3656
3698
|
opIndex: operations.length - 1,
|
|
3657
3699
|
ctxIndex: opContexts.length - 1,
|
|
3658
3700
|
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
3659
3701
|
finalInts: fbResult.finalInts
|
|
3660
3702
|
});
|
|
3703
|
+
if (fbRecordedFp)
|
|
3704
|
+
batchPendingFps.add(fbRecordedFp);
|
|
3661
3705
|
bot.manager.logger.log(`[COW] Recovered "not found" for ${action.id}: converted UPDATE to CREATE for slot ${targetSlotId}`, 'warn');
|
|
3662
3706
|
continue;
|
|
3663
3707
|
}
|
|
@@ -3671,6 +3715,73 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3671
3715
|
}
|
|
3672
3716
|
}
|
|
3673
3717
|
}
|
|
3718
|
+
// FINAL PRE-BROADCAST PIVOT GATE (2026-09-13 incident on a live
|
|
3719
|
+
// market-pair bot): a
|
|
3720
|
+
// fill queued AFTER the batch-start freeze but BEFORE broadcast
|
|
3721
|
+
// passes every per-action check on a stale pivot (freeze .745, fill
|
|
3722
|
+
// queued .765, broadcast .910). Re-check the BUILT ops against a
|
|
3723
|
+
// re-refreshed pivot here — after op-building, before the batch
|
|
3724
|
+
// summary (drops count as skipped, not passed), fund validation
|
|
3725
|
+
// (snapshot reflects filtered ops) and single-flight claim.
|
|
3726
|
+
// op indexes recorded during op-building are rebuilt from kept
|
|
3727
|
+
// contexts below, so any future reader sees live indexes.
|
|
3728
|
+
// The gate runs on its OWN stats object: its re-checks would
|
|
3729
|
+
// otherwise double-count the build loop's checked/passed/skipped
|
|
3730
|
+
// totals in the batch summary below. Gate contributions are
|
|
3731
|
+
// reported separately (gateChecked=... fields).
|
|
3732
|
+
const skippedUpdateCountRef = { count: 0 };
|
|
3733
|
+
const finalGateStats = { checked: 0, passed: 0, skipped: 0, bypassed: 0, pivotOffGrid: 0 };
|
|
3734
|
+
let finalGate = null;
|
|
3735
|
+
try {
|
|
3736
|
+
finalGate = runFinalPivotGate(bot, operations, opContexts, {
|
|
3737
|
+
actions,
|
|
3738
|
+
cowResult,
|
|
3739
|
+
frozenPivot: frozenPivotAtBatchStart,
|
|
3740
|
+
frozenType: frozenTypeAtBatchStart,
|
|
3741
|
+
lastFillGuardStats: finalGateStats,
|
|
3742
|
+
skippedUpdateSlotIds,
|
|
3743
|
+
skippedCreateSlotIds,
|
|
3744
|
+
skippedUpdateCountRef,
|
|
3745
|
+
freezeQueueDepth,
|
|
3746
|
+
batchPendingFps,
|
|
3747
|
+
});
|
|
3748
|
+
if (finalGate.refreshed)
|
|
3749
|
+
lastFillGuardPivotRefreshed = true;
|
|
3750
|
+
if (finalGate.pivotChanged) {
|
|
3751
|
+
try {
|
|
3752
|
+
const fmtP = (v) => (v == null || !Number.isFinite(Number(v)) ? 'none' : Format.formatPrice6(Number(v)));
|
|
3753
|
+
bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate: pivot moved under batch ` +
|
|
3754
|
+
`${fmtP(frozenPivotAtBatchStart)}(${frozenTypeAtBatchStart ?? 'cold'})` +
|
|
3755
|
+
`->${fmtP(bot.manager?._lastFilledPrice)}(${bot.manager?._lastFilledType ?? 'cold'}) ` +
|
|
3756
|
+
`(freezeQueue=${freezeQueueDepth ?? '?'}) ` +
|
|
3757
|
+
`dropped=${finalGate.dropped.length}`, 'warn');
|
|
3758
|
+
}
|
|
3759
|
+
catch { /* logging is best-effort */ }
|
|
3760
|
+
}
|
|
3761
|
+
// Rebuild cancelOpIndexByOrderId from the KEPT contexts: the
|
|
3762
|
+
// gate compacted operations/opContexts in lockstep, so indexes
|
|
3763
|
+
// recorded during op-building are stale for every op after the
|
|
3764
|
+
// first drop. cancelOpIndexByOrderId has NO readers past this
|
|
3765
|
+
// point (verified: the op-building loop is its last use —
|
|
3766
|
+
// pair-mode/chunk grouping is computed lazily at broadcast from
|
|
3767
|
+
// opContexts with no stored indexes), so this rebuild is
|
|
3768
|
+
// defence-in-depth for future readers, not a live fix.
|
|
3769
|
+
cancelOpIndexByOrderId.clear();
|
|
3770
|
+
for (let ci = 0; ci < opContexts.length; ci++) {
|
|
3771
|
+
const cctx = opContexts[ci];
|
|
3772
|
+
const cord = cctx?.order;
|
|
3773
|
+
const cOrderId = cctx?.kind === 'cancel'
|
|
3774
|
+
? cctx?.order?.orderId || cctx?.orderId
|
|
3775
|
+
: cord?.orderId;
|
|
3776
|
+
if (cOrderId)
|
|
3777
|
+
cancelOpIndexByOrderId.set(cOrderId, ci);
|
|
3778
|
+
}
|
|
3779
|
+
// skippedUpdateCount is threaded via countRef so the restore
|
|
3780
|
+
// below covers gate-dropped rotations too.
|
|
3781
|
+
if (skippedUpdateCountRef.count > 0)
|
|
3782
|
+
skippedUpdateCount += skippedUpdateCountRef.count;
|
|
3783
|
+
}
|
|
3784
|
+
catch (_gateErr) { /* gate is fail-open: keep the built ops */ }
|
|
3674
3785
|
// Batch-level LAST-FILL-GUARD summary: per-action pass lines would spam
|
|
3675
3786
|
// big batches, so one line per batch records the mode, pivot, resolved
|
|
3676
3787
|
// increment, and pass/skip/bypass counts — the guard's pass decisions
|
|
@@ -3682,7 +3793,16 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3682
3793
|
// for the whole batch, so every action was checked against the same
|
|
3683
3794
|
// pivot printed here.
|
|
3684
3795
|
try {
|
|
3685
|
-
|
|
3796
|
+
// The final gate reports on its OWN counters (finalGateStats), so
|
|
3797
|
+
// checked/passed/skipped/bypassed here are the build loop's
|
|
3798
|
+
// verdicts only — the gate's re-checks never inflate them. The
|
|
3799
|
+
// gate's contributions ride along as gate* fields, omitted when
|
|
3800
|
+
// the gate did not re-check anything (same convention as
|
|
3801
|
+
// pivotOffGrid above). totalGuarded includes the gate so a
|
|
3802
|
+
// batch that was ONLY gate-checked (e.g. cold freeze armed
|
|
3803
|
+
// mid-batch) still prints.
|
|
3804
|
+
const gateGuarded = finalGateStats.checked + finalGateStats.bypassed;
|
|
3805
|
+
const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed + gateGuarded;
|
|
3686
3806
|
if (totalGuarded > 0) {
|
|
3687
3807
|
const sumPivotRaw = bot.manager?._lastFilledPrice;
|
|
3688
3808
|
const sumType = bot.manager?._lastFilledType;
|
|
@@ -3720,6 +3840,10 @@ async function updateOrdersOnChainBatchCOWBody(bot, cowResult, replanDepth, seam
|
|
|
3720
3840
|
// "counter unavailable".
|
|
3721
3841
|
(Number(lastFillGuardStats.pivotOffGrid) > 0
|
|
3722
3842
|
? ` pivotOffGrid=${lastFillGuardStats.pivotOffGrid}`
|
|
3843
|
+
: '') +
|
|
3844
|
+
(gateGuarded > 0
|
|
3845
|
+
? ` gateChecked=${finalGateStats.checked} gatePassed=${finalGateStats.passed} ` +
|
|
3846
|
+
`gateSkipped=${finalGateStats.skipped} gateBypassed=${finalGateStats.bypassed}`
|
|
3723
3847
|
: ''), cold || (batchPivot && batchPivot.idx == null) ? 'warn' : 'info');
|
|
3724
3848
|
}
|
|
3725
3849
|
}
|
|
@@ -4410,6 +4534,13 @@ function resolveOnGridPivot(manager, rawPrice) {
|
|
|
4410
4534
|
* skip logging stay at the call sites, which differ per action kind).
|
|
4411
4535
|
* Batch callers pass skipRefresh=true: the batch-start freeze owns refreshes
|
|
4412
4536
|
* so every action in a batch is judged against the same pivot.
|
|
4537
|
+
*
|
|
4538
|
+
* FINAL-GATE CONTRACT (see runFinalPivotGate): the gate re-checks BUILT ops
|
|
4539
|
+
* against a re-refreshed pivot AFTER the op-building loop. It must run BEFORE
|
|
4540
|
+
* any later mutation of operations/opContexts (fund validation snapshot,
|
|
4541
|
+
* pair-mode/chunk grouping) — those stages index op positions and read stale
|
|
4542
|
+
* indexes after a filter. Call sites after the gate must treat
|
|
4543
|
+
* operations/opContexts as the filtered arrays.
|
|
4413
4544
|
* @param {Object} bot
|
|
4414
4545
|
* @param {number} price - Target order price
|
|
4415
4546
|
* @param {number} size - Order size
|
|
@@ -4467,6 +4598,349 @@ function runLastFillGuardCheck(bot, price, size, type, stats, skipRefresh = fals
|
|
|
4467
4598
|
stats.checked++;
|
|
4468
4599
|
return { check, refreshed };
|
|
4469
4600
|
}
|
|
4601
|
+
/**
|
|
4602
|
+
* Final pre-broadcast pivot gate: re-check BUILT ops against a re-refreshed
|
|
4603
|
+
* pivot RIGHT BEFORE broadcast.
|
|
4604
|
+
*
|
|
4605
|
+
* Why this exists (2026-09-13 incident on a live market-pair bot): the batch-start freeze
|
|
4606
|
+
* refreshes the pivot once, then every CREATE / rotation-UPDATE /
|
|
4607
|
+
* fallback-CREATE is judged against that frozen value (skipRefresh=true).
|
|
4608
|
+
* A fill that is queued AFTER the freeze but BEFORE broadcast (in the
|
|
4609
|
+
* incident: freeze at .745, fill queued at .765, broadcast at .910) passes
|
|
4610
|
+
* every per-action check on a stale pivot and ships. The batch summary
|
|
4611
|
+
* prints pivotRefreshed=false — the freeze honestly found nothing — and
|
|
4612
|
+
* the violating ops broadcast anyway.
|
|
4613
|
+
*
|
|
4614
|
+
* Placement (see FINAL-GATE CONTRACT on runLastFillGuardCheck):
|
|
4615
|
+
* 1. Called after the op-building action loop, BEFORE the LAST-FILL-GUARD
|
|
4616
|
+
* batch summary line (so dropped ops are visible as skipped, not
|
|
4617
|
+
* passed) and BEFORE fund validation (so the VALIDATION snapshot
|
|
4618
|
+
* reflects the filtered ops).
|
|
4619
|
+
* 2. Must run before pair-mode/chunk grouping, which indexes op positions.
|
|
4620
|
+
* executeOperationsWithStrategy groups lazily at broadcast time from
|
|
4621
|
+
* the (filtered) opContexts it receives, so filtering here is safe —
|
|
4622
|
+
* but any future grouping computed between op-building and broadcast
|
|
4623
|
+
* must be rebuilt after the gate (same rule as the contract).
|
|
4624
|
+
*
|
|
4625
|
+
* Semantics:
|
|
4626
|
+
* - Peek-only refresh (never drains the fill queue — same as the freeze).
|
|
4627
|
+
* - Pivot UNCHANGED since the freeze => pure no-op: returns the input
|
|
4628
|
+
* arrays untouched, no extra log lines beyond queue-depth debug.
|
|
4629
|
+
* - Pivot CHANGED => re-run isLastFillGuardBlocked against each built op's
|
|
4630
|
+
* final price with the SAME bypass rules as the build loop
|
|
4631
|
+
* (spread-correction CREATEs, stamped gap-evacuation UPDATEs). Violating
|
|
4632
|
+
* ops + their contexts are dropped; their slots feed the existing
|
|
4633
|
+
* skippedUpdateSlotIds/skippedCreateSlotIds restore paths so the working
|
|
4634
|
+
* grid stays consistent (dropped rotations restore from master, dropped
|
|
4635
|
+
* creates count toward the boundary-hold intersect).
|
|
4636
|
+
* - fail-open on everything unjudgeable: unresolvable price/type, cold
|
|
4637
|
+
* pivot (null), or a refresh/inference throw => the op is KEPT. A gate
|
|
4638
|
+
* that cannot prove a violation must not invent one — dropping a healthy
|
|
4639
|
+
* op strands its slot, while a missed violation is still caught by the
|
|
4640
|
+
* next cycle's guard + commit chain adoption.
|
|
4641
|
+
* - size-update ops are NEVER gated (same-price, no repricing).
|
|
4642
|
+
* - cancel ops are NEVER gated or dropped.
|
|
4643
|
+
*
|
|
4644
|
+
* Stale-index hygiene: recordPendingBroadcast stores opIndex/ctxIndex
|
|
4645
|
+
* against the build-time arrays. Dropped CREATEs must have their pending
|
|
4646
|
+
* entries removed (else the reconcile path adopts a broadcast that never
|
|
4647
|
+
* shipped), and KEPT entries must have their stored indexes REMAPPED: a
|
|
4648
|
+
* lockstep compaction keeps operations/opContexts aligned with each other,
|
|
4649
|
+
* but the absolute indexes stored inside the pending entries are not
|
|
4650
|
+
* rewritten by it — after the first drop, an unremapped ctxIndex resolves
|
|
4651
|
+
* to a shifted position (undefined at best, a DIFFERENT create's context at
|
|
4652
|
+
* worst, which would let adoptMatchedEntries synchronize the wrong slot
|
|
4653
|
+
* with a matched chain order). The gate therefore builds an old→new index
|
|
4654
|
+
* map during compaction and remaps/removes entries identified by
|
|
4655
|
+
* opts.batchPendingFps (the fingerprints THIS batch recorded — entry.batchId
|
|
4656
|
+
* is always null in production, so it cannot discriminate). Sibling
|
|
4657
|
+
* batches' entries are never touched (their indexes refer to their own
|
|
4658
|
+
* build-time arrays).
|
|
4659
|
+
*
|
|
4660
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
4661
|
+
* @param {Array} operations - Built chain ops (mutated in place on drop)
|
|
4662
|
+
* @param {Array} opContexts - Built op contexts (mutated in place on drop)
|
|
4663
|
+
* @param {Object} opts - { actions, cowResult, frozenPivot, frozenType,
|
|
4664
|
+
* lastFillGuardStats, skippedUpdateSlotIds, skippedCreateSlotIds,
|
|
4665
|
+
* skippedUpdateCountRef: { count }, freezeQueueDepth, batchPendingFps }
|
|
4666
|
+
* @returns {{ dropped: Array, pivotChanged: boolean, refreshed: boolean }}
|
|
4667
|
+
*/
|
|
4668
|
+
function runFinalPivotGate(bot, operations, opContexts, opts = {}) {
|
|
4669
|
+
const empty = { dropped: [], pivotChanged: false, refreshed: false };
|
|
4670
|
+
try {
|
|
4671
|
+
if (!Array.isArray(operations) || !Array.isArray(opContexts) || operations.length === 0)
|
|
4672
|
+
return empty;
|
|
4673
|
+
const stats = opts?.lastFillGuardStats;
|
|
4674
|
+
const frozenPivot = Number(opts?.frozenPivot);
|
|
4675
|
+
const frozenType = opts?.frozenType;
|
|
4676
|
+
const frozenCold = !Number.isFinite(frozenPivot) || frozenType == null;
|
|
4677
|
+
const queueDepthBefore = Array.isArray(bot?._incomingFillQueue)
|
|
4678
|
+
? bot._incomingFillQueue.length
|
|
4679
|
+
: null;
|
|
4680
|
+
// Peek-only re-refresh: never drains the queue (same as the freeze).
|
|
4681
|
+
let refreshed = false;
|
|
4682
|
+
try {
|
|
4683
|
+
refreshed = !!refreshLastFillPivotFromQueue(bot);
|
|
4684
|
+
}
|
|
4685
|
+
catch {
|
|
4686
|
+
refreshed = false;
|
|
4687
|
+
}
|
|
4688
|
+
const queueDepthAfter = Array.isArray(bot?._incomingFillQueue)
|
|
4689
|
+
? bot._incomingFillQueue.length
|
|
4690
|
+
: null;
|
|
4691
|
+
const livePivot = Number(bot.manager?._lastFilledPrice);
|
|
4692
|
+
const liveType = bot.manager?._lastFilledType;
|
|
4693
|
+
const liveCold = !Number.isFinite(livePivot) || liveType == null;
|
|
4694
|
+
// No-op fast path: pivot unchanged (or uncomparable) since the freeze.
|
|
4695
|
+
// frozenCold + liveCold: guard stayed disabled — nothing to re-check.
|
|
4696
|
+
// frozenCold + liveArmed: the freeze ran cold but a fill arrived
|
|
4697
|
+
// mid-batch. The built ops were NEVER guarded; treat as changed so
|
|
4698
|
+
// they are checked below (fail-open keeps whatever is unjudgeable).
|
|
4699
|
+
let pivotChanged = refreshed;
|
|
4700
|
+
if (frozenCold && liveCold)
|
|
4701
|
+
pivotChanged = false;
|
|
4702
|
+
else if (!frozenCold && !liveCold)
|
|
4703
|
+
pivotChanged = livePivot !== frozenPivot || liveType !== frozenType;
|
|
4704
|
+
else
|
|
4705
|
+
pivotChanged = true;
|
|
4706
|
+
try {
|
|
4707
|
+
bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate: queue ${queueDepthBefore ?? '?'}->${queueDepthAfter ?? '?'} ` +
|
|
4708
|
+
`pivot ${Number.isFinite(frozenPivot) ? Format.formatPrice6(frozenPivot) : 'none'}(${frozenType ?? 'cold'})` +
|
|
4709
|
+
`->${Number.isFinite(livePivot) ? Format.formatPrice6(livePivot) : 'none'}(${liveType ?? 'cold'}) ` +
|
|
4710
|
+
`changed=${pivotChanged} refreshed=${refreshed}`, 'debug');
|
|
4711
|
+
}
|
|
4712
|
+
catch { /* logging is best-effort */ }
|
|
4713
|
+
if (!pivotChanged)
|
|
4714
|
+
return { ...empty, refreshed };
|
|
4715
|
+
// Pivot moved (or armed mid-batch): re-check every built op's FINAL
|
|
4716
|
+
// price. Same bypass rules as the build loop; unjudgeable => KEEP.
|
|
4717
|
+
const actions = Array.isArray(opts?.actions) ? opts.actions : [];
|
|
4718
|
+
const batchOrigin = opts?.cowResult?.origin;
|
|
4719
|
+
const actionBySlot = new Map();
|
|
4720
|
+
for (const a of actions) {
|
|
4721
|
+
if (!a)
|
|
4722
|
+
continue;
|
|
4723
|
+
// Rotation UPDATEs are keyed by DESTINATION slot (the emitted
|
|
4724
|
+
// price is the destination's level); plain CREATEs by slot id.
|
|
4725
|
+
const rotDest = a?.newGridId;
|
|
4726
|
+
const key = (a?.type === COW_ACTIONS.UPDATE && rotDest && rotDest !== a?.id) ? rotDest : a?.id;
|
|
4727
|
+
if (key && !actionBySlot.has(key))
|
|
4728
|
+
actionBySlot.set(key, a);
|
|
4729
|
+
}
|
|
4730
|
+
const dropIdx = new Set();
|
|
4731
|
+
const dropped = [];
|
|
4732
|
+
const inc = resolveLastFillGuardIncrement(bot);
|
|
4733
|
+
const onGrid = resolveOnGridPivot(bot.manager, bot.manager?._lastFilledPrice);
|
|
4734
|
+
for (let i = 0; i < opContexts.length; i++) {
|
|
4735
|
+
const ctx = opContexts[i];
|
|
4736
|
+
if (!ctx || ctx.kind === 'cancel' || ctx.kind === 'size-update')
|
|
4737
|
+
continue;
|
|
4738
|
+
let price = null;
|
|
4739
|
+
let type = null;
|
|
4740
|
+
let size = null;
|
|
4741
|
+
let slotId = null;
|
|
4742
|
+
let action = null;
|
|
4743
|
+
if (ctx.kind === 'create') {
|
|
4744
|
+
slotId = ctx.id || ctx.order?.id || null;
|
|
4745
|
+
price = Number(ctx.order?.price);
|
|
4746
|
+
type = ctx.order?.type || null;
|
|
4747
|
+
size = Number(ctx.order?.size);
|
|
4748
|
+
action = (slotId && actionBySlot.get(slotId)) || null;
|
|
4749
|
+
// Spread-correction CREATE bypass (mirrors the build loop:
|
|
4750
|
+
// per-action origin, batch origin as back-compat fallback).
|
|
4751
|
+
const actionOrigin = action?.origin;
|
|
4752
|
+
if (actionOrigin === 'spread-correction'
|
|
4753
|
+
|| (actionOrigin == null && batchOrigin === 'spread-correction')) {
|
|
4754
|
+
if (stats)
|
|
4755
|
+
stats.bypassed = (Number(stats.bypassed) || 0) + 1;
|
|
4756
|
+
continue;
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
else if (ctx.kind === 'rotation') {
|
|
4760
|
+
const rot = ctx.rotation || {};
|
|
4761
|
+
slotId = rot.newGridId || rot.oldOrder?.id || null;
|
|
4762
|
+
price = Number(rot.newPrice);
|
|
4763
|
+
type = rot.type || null;
|
|
4764
|
+
size = Number(rot.newSize);
|
|
4765
|
+
// Rotation UPDATEs are keyed by DESTINATION slot (the
|
|
4766
|
+
// emitted price is the destination's level) — EXCEPT the
|
|
4767
|
+
// same-slot size-only form (no newGridId, or newGridId ===
|
|
4768
|
+
// source id), which buildActionsFromPlan emits for
|
|
4769
|
+
// ordersToUpdate and which must resolve to the source
|
|
4770
|
+
// action. A same-slot UPDATE carries no repricing, so a
|
|
4771
|
+
// dest-keyed lookup that misses it would ALSO miss its
|
|
4772
|
+
// origin stamp — fall back to the source id before
|
|
4773
|
+
// judging the bypass.
|
|
4774
|
+
action = (slotId && actionBySlot.get(slotId)) || null;
|
|
4775
|
+
if (!action) {
|
|
4776
|
+
const srcId = rot.oldOrder?.id || null;
|
|
4777
|
+
if (srcId)
|
|
4778
|
+
action = actionBySlot.get(srcId) || null;
|
|
4779
|
+
}
|
|
4780
|
+
// Gap-evacuation UPDATE bypass mirrors the build loop's
|
|
4781
|
+
// stamped path ONLY — with one deliberate asymmetry (see
|
|
4782
|
+
// below): the build loop re-proves UNSTAMPED evacuations
|
|
4783
|
+
// live from the master grid; the gate guards them normally.
|
|
4784
|
+
// An unstamped rotation reaching the final gate was either
|
|
4785
|
+
// (a) probed-and-allowed at build time — in which case its
|
|
4786
|
+
// price already survived an evacuation proof and the guard
|
|
4787
|
+
// re-check here is harmless duplication, or (b) probe-
|
|
4788
|
+
// rejected/failed-closed — in which case it was SKIPPED at
|
|
4789
|
+
// build time and never reached op-building, so the gate
|
|
4790
|
+
// cannot see it either. Either way there is no live
|
|
4791
|
+
// unstamped evacuation in the built ops that needs
|
|
4792
|
+
// re-proving: re-proving here would need the master-grid
|
|
4793
|
+
// source read the build loop does, and the source may have
|
|
4794
|
+
// been pre-applied since. Stamped rotations carry origin +
|
|
4795
|
+
// evacBoundary/evacGapSlots.
|
|
4796
|
+
//
|
|
4797
|
+
// ASYMMETRY (fail-open, not fail-closed): the build loop's
|
|
4798
|
+
// unstamped path FAILS CLOSED (unresolvable source => skip
|
|
4799
|
+
// the op). The gate FAILS OPEN (unjudgeable => keep). A
|
|
4800
|
+
// dropped op strands its slot until the next cycle; a kept
|
|
4801
|
+
// op is still subject to the commit guard + chain adoption.
|
|
4802
|
+
// The gate must not invent a block it cannot prove —
|
|
4803
|
+
// especially not on an op the build loop already allowed.
|
|
4804
|
+
const rotOrigin = action?.origin;
|
|
4805
|
+
if (rotOrigin === 'gap-evacuation'
|
|
4806
|
+
&& Number.isFinite(Number(action?.evacBoundary))
|
|
4807
|
+
&& Number.isFinite(Number(action?.evacGapSlots))) {
|
|
4808
|
+
if (stats)
|
|
4809
|
+
stats.bypassed = (Number(stats.bypassed) || 0) + 1;
|
|
4810
|
+
continue;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
else {
|
|
4814
|
+
continue;
|
|
4815
|
+
}
|
|
4816
|
+
// Fail-open: unresolvable price/type => KEEP (never invent a
|
|
4817
|
+
// violation the gate cannot prove).
|
|
4818
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
4819
|
+
continue;
|
|
4820
|
+
if (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL)
|
|
4821
|
+
continue;
|
|
4822
|
+
const check = isLastFillGuardBlocked(price, size, type, onGrid.price, liveType, inc);
|
|
4823
|
+
if (stats)
|
|
4824
|
+
stats.checked = (Number(stats.checked) || 0) + 1;
|
|
4825
|
+
if (!check.blocked) {
|
|
4826
|
+
if (stats)
|
|
4827
|
+
stats.passed = (Number(stats.passed) || 0) + 1;
|
|
4828
|
+
continue;
|
|
4829
|
+
}
|
|
4830
|
+
// Blocked: drop the op + context, restore the slot below.
|
|
4831
|
+
dropIdx.add(i);
|
|
4832
|
+
if (stats)
|
|
4833
|
+
stats.skipped = (Number(stats.skipped) || 0) + 1;
|
|
4834
|
+
const dir = type === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
4835
|
+
dropped.push({ index: i, kind: ctx.kind, slotId, price, type });
|
|
4836
|
+
try {
|
|
4837
|
+
bot.manager?.logger?.log?.(`[LAST-FILL-GUARD] Final gate dropping ${type} ${ctx.kind} for ${slotId ?? 'unknown'} at ` +
|
|
4838
|
+
`${Format.formatPrice6(price)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} ` +
|
|
4839
|
+
`(halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'warn');
|
|
4840
|
+
}
|
|
4841
|
+
catch { /* logging is best-effort */ }
|
|
4842
|
+
}
|
|
4843
|
+
if (dropIdx.size === 0)
|
|
4844
|
+
return { dropped, pivotChanged, refreshed };
|
|
4845
|
+
// Compact operations/opContexts in lockstep so every surviving index
|
|
4846
|
+
// still lines up. The caller rebuilds cancelOpIndexByOrderId from the
|
|
4847
|
+
// kept contexts (it was built during op-building and goes stale for
|
|
4848
|
+
// every op after the first drop), and the remap pass below re-points
|
|
4849
|
+
// THIS batch's kept pending-broadcast entries at their new positions
|
|
4850
|
+
// — lockstep keeps the two arrays aligned with each other, but the
|
|
4851
|
+
// absolute indexes stored INSIDE pending entries are not rewritten
|
|
4852
|
+
// by a compaction, so they must be remapped explicitly.
|
|
4853
|
+
const keptOps = [];
|
|
4854
|
+
const keptCtxs = [];
|
|
4855
|
+
const oldToNew = new Map();
|
|
4856
|
+
for (let i = 0, ni = 0; i < opContexts.length; i++) {
|
|
4857
|
+
if (dropIdx.has(i))
|
|
4858
|
+
continue;
|
|
4859
|
+
oldToNew.set(i, ni);
|
|
4860
|
+
ni++;
|
|
4861
|
+
keptOps.push(operations[i]);
|
|
4862
|
+
keptCtxs.push(opContexts[i]);
|
|
4863
|
+
}
|
|
4864
|
+
operations.length = 0;
|
|
4865
|
+
operations.push(...keptOps);
|
|
4866
|
+
opContexts.length = 0;
|
|
4867
|
+
opContexts.push(...keptCtxs);
|
|
4868
|
+
// Slot restore: dropped rotations restore source+dest from master
|
|
4869
|
+
// (same sets the build loop feeds to restoreSkippedUpdateSlots...);
|
|
4870
|
+
// dropped creates count toward the refill-hold intersect. Pending
|
|
4871
|
+
// entries for dropped CREATEs are removed (never shipped); kept
|
|
4872
|
+
// CREATEs get their stored opIndex/ctxIndex remapped to the
|
|
4873
|
+
// compacted positions (see the remap pass below — an unremapped
|
|
4874
|
+
// absolute index would resolve to a SHIFTED context after the first
|
|
4875
|
+
// drop: undefined at best, a DIFFERENT create's context at worst,
|
|
4876
|
+
// which would let the uncertain-broadcast reconcile adopt a matched
|
|
4877
|
+
// chain order into the wrong slot).
|
|
4878
|
+
const skippedUpdateSlotIds = opts?.skippedUpdateSlotIds;
|
|
4879
|
+
const skippedCreateSlotIds = opts?.skippedCreateSlotIds;
|
|
4880
|
+
const countRef = opts?.skippedUpdateCountRef;
|
|
4881
|
+
try {
|
|
4882
|
+
const pending = bot.manager?._pendingBroadcasts;
|
|
4883
|
+
for (const d of dropped) {
|
|
4884
|
+
if (d.kind === 'rotation') {
|
|
4885
|
+
const act = (d.slotId && actionBySlot.get(d.slotId)) || null;
|
|
4886
|
+
const srcId = act?.id || null;
|
|
4887
|
+
if (srcId && skippedUpdateSlotIds instanceof Set)
|
|
4888
|
+
skippedUpdateSlotIds.add(srcId);
|
|
4889
|
+
if (d.slotId && skippedUpdateSlotIds instanceof Set)
|
|
4890
|
+
skippedUpdateSlotIds.add(d.slotId);
|
|
4891
|
+
if (countRef && typeof countRef === 'object')
|
|
4892
|
+
countRef.count = (Number(countRef.count) || 0) + 1;
|
|
4893
|
+
}
|
|
4894
|
+
else if (d.kind === 'create') {
|
|
4895
|
+
if (d.slotId && skippedCreateSlotIds instanceof Set)
|
|
4896
|
+
skippedCreateSlotIds.add(d.slotId);
|
|
4897
|
+
if (pending instanceof Map) {
|
|
4898
|
+
for (const [fp, entry] of pending) {
|
|
4899
|
+
// Match by slot only: the fingerprint embeds
|
|
4900
|
+
// side/amounts/slot (no op indexes), but entry.slotId
|
|
4901
|
+
// is the narrowest predicate that cannot touch a
|
|
4902
|
+
// sibling batch's entry for a different slot.
|
|
4903
|
+
if (entry?.slotId && entry.slotId === d.slotId) {
|
|
4904
|
+
pending.delete(fp);
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
// Remap THIS batch's kept pending entries (identified by the
|
|
4911
|
+
// fingerprint set the build loop collected — entry.batchId is
|
|
4912
|
+
// always null in production and cannot discriminate). A stored
|
|
4913
|
+
// ctxIndex whose old position was dropped means the entry was
|
|
4914
|
+
// not slot-matched above; it could never resolve post-compaction,
|
|
4915
|
+
// so it is removed. Everything else is re-pointed at the same
|
|
4916
|
+
// context object it was recorded with. Sibling batches' entries
|
|
4917
|
+
// are never touched: their indexes refer to their own long-gone
|
|
4918
|
+
// build-time arrays.
|
|
4919
|
+
if (pending instanceof Map && opts?.batchPendingFps instanceof Set) {
|
|
4920
|
+
for (const fp of opts.batchPendingFps) {
|
|
4921
|
+
const entry = pending.get(fp);
|
|
4922
|
+
if (!entry)
|
|
4923
|
+
continue; // dropped create: already removed by slot
|
|
4924
|
+
const newCtx = oldToNew.get(Number(entry.ctxIndex));
|
|
4925
|
+
if (newCtx == null) {
|
|
4926
|
+
// Referenced op was dropped (or the entry predates
|
|
4927
|
+
// lockstep indexing) — the index cannot be healed.
|
|
4928
|
+
pending.delete(fp);
|
|
4929
|
+
continue;
|
|
4930
|
+
}
|
|
4931
|
+
entry.ctxIndex = newCtx;
|
|
4932
|
+
const newOp = oldToNew.get(Number(entry.opIndex));
|
|
4933
|
+
entry.opIndex = newOp == null ? entry.opIndex : newOp;
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
catch { /* restore bookkeeping is best-effort */ }
|
|
4938
|
+
return { dropped, pivotChanged, refreshed };
|
|
4939
|
+
}
|
|
4940
|
+
catch {
|
|
4941
|
+
return empty;
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4470
4944
|
/**
|
|
4471
4945
|
* Apply BTS create-fee accounting for a batch that bypassed the normal
|
|
4472
4946
|
* processBatchResults pipeline (commit refused after broadcast, or
|
|
@@ -4709,7 +5183,7 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
4709
5183
|
updateOperationCount
|
|
4710
5184
|
};
|
|
4711
5185
|
}
|
|
4712
|
-
export { isLastFillGuardBlocked, resolveOnGridPivot, checkGridPriceInvariant, deriveRotationPrice, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries,
|
|
5186
|
+
export { isLastFillGuardBlocked, resolveOnGridPivot, checkGridPriceInvariant, deriveRotationPrice, refreshLastFillPivotFromQueue, runFinalPivotGate, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, popPushedWorkingGrid, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, buildActionsFromPlan, buildCowResultFromPlan, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet, trackBoundaryHold };
|
|
4713
5187
|
// Exported for regression tests (issue #23 sibling): the uncertain-broadcast
|
|
4714
5188
|
// discard path must never drop a placement silently when master lost the slot.
|
|
4715
5189
|
export { restoreDiscardedCreates };
|
|
@@ -4748,5 +5222,6 @@ export default {
|
|
|
4748
5222
|
pollChainForConfirmation,
|
|
4749
5223
|
updateOrdersOnChainBatchCOW,
|
|
4750
5224
|
processBatchResults,
|
|
5225
|
+
runFinalPivotGate,
|
|
4751
5226
|
};
|
|
4752
5227
|
//# sourceMappingURL=dexbot_cow_runtime.js.map
|