dexbot 1.4.24 → 1.4.25
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 +39 -0
- package/README.md +1 -0
- package/analysis/README.md +49 -0
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/grid_correction_check.ts +896 -0
- package/analysis/trend_detection/package.json +1 -1
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/grid_correction_check.d.ts +90 -0
- package/dist/analysis/grid_correction_check.d.ts.map +1 -0
- package/dist/analysis/grid_correction_check.js +861 -0
- package/dist/analysis/grid_correction_check.js.map +1 -0
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +62 -6
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +53 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +7 -10
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +26 -2
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +21 -2
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/config.d.ts.map +1 -1
- package/dist/modules/config.js +27 -0
- package/dist/modules/config.js.map +1 -1
- package/dist/modules/constants.d.ts +14 -1
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +57 -6
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +24 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +66 -12
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +22 -17
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +646 -128
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +108 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +17 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +215 -26
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +58 -7
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +55 -3
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/graceful_shutdown.d.ts +12 -1
- package/dist/modules/graceful_shutdown.d.ts.map +1 -1
- package/dist/modules/graceful_shutdown.js +39 -1
- package/dist/modules/graceful_shutdown.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +38 -1
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +20 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/grid.d.ts +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +288 -31
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts +4 -2
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +242 -222
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +132 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +68 -14
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +241 -52
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +25 -8
- 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 +261 -166
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +47 -5
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +175 -11
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +16 -15
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +199 -32
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/slot.d.ts +8 -0
- package/dist/modules/order/utils/slot.d.ts.map +1 -0
- package/dist/modules/order/utils/slot.js +16 -0
- package/dist/modules/order/utils/slot.js.map +1 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +3 -2
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +5 -8
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +64 -82
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/order/working_grid.d.ts +5 -0
- package/dist/modules/order/working_grid.d.ts.map +1 -1
- package/dist/modules/order/working_grid.js +21 -0
- package/dist/modules/order/working_grid.js.map +1 -1
- package/dist/modules/paths.d.ts +4 -2
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +79 -24
- package/dist/modules/paths.js.map +1 -1
- package/dist/scripts/analyze-orders.d.ts.map +1 -1
- package/dist/scripts/analyze-orders.js +23 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
- package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
- package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +11 -11
- package/docs/EVOLUTION.md +9 -8
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
- package/docs/GRID_RECALCULATION.md +6 -0
- package/docs/README.md +2 -2
- package/market_adapter/README.md +10 -0
- package/package.json +2 -1
- package/scripts/README.md +4 -0
|
@@ -110,7 +110,7 @@ function _snapshotFundState(manager) {
|
|
|
110
110
|
sellLocked: Number(manager.accountTotals?.sellLocked || 0),
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
-
import { floatToBlockchainInt, getPrecisionByOrderType, getPrecisionsForManager, calculateOrderCreationFees, calculateOrderSizes, calculateRotationOrderSizes, calculateAvailableFundsValue, calculateGridSideDivergenceMetric, getPrecisionSlack, getMinOrderSize, getSingleDustThreshold, getGridBestPrices, calculateSpreadFromOrders, allocateFundsByWeights, calculateGapSlots as _mathGapSlots,
|
|
113
|
+
import { floatToBlockchainInt, getPrecisionByOrderType, getPrecisionsForManager, calculateOrderCreationFees, calculateOrderSizes, calculateRotationOrderSizes, calculateAvailableFundsValue, calculateGridSideDivergenceMetric, getPrecisionSlack, getMinOrderSize, getSingleDustThreshold, getGridBestPrices, calculateSpreadFromOrders, allocateFundsByWeights, calculateGapSlots as _mathGapSlots, priceSlotEqual, getBtsSide, getSellStartIdx, resolveGapBand, countGapBandSpread, validatePersistedBoundary, adjustBudgetForBtsFees, clamp, buildGenesisFromPriceLevels, assertSlotPriceInvariant, hashPriceLevels, } from './utils/math.js';
|
|
114
114
|
import { filterOrdersByType, checkSizesBeforeMinimum, checkSizeThreshold, resolveConfiguredPriceBound, shouldFlagOutOfSpread, isOrderHealthy, isPhantomOrder, isSlotAvailable, isOrderOnChain, isOrderPlaced, hasOnChainId, isEmptyGridSlot, parseSlotIndex, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType } from './utils/order.js';
|
|
115
115
|
import { loadAmaCenterPrice, loadAmaCenterSnapshot, withBlockchainRetry } from './utils/system.js';
|
|
116
116
|
import * as MathUtils from './utils/math.js';
|
|
@@ -361,6 +361,21 @@ export function createOrderGrid(config) {
|
|
|
361
361
|
}
|
|
362
362
|
// Sort all levels from lowest to highest (Master Rail order)
|
|
363
363
|
priceLevels.sort((a, b) => a - b);
|
|
364
|
+
// Dedupe geometric levels that collide at float precision (tiny increments);
|
|
365
|
+
// keeps slot-N ↔ index mapping stable vs migration dedupe (grid.ts:652)
|
|
366
|
+
{
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
const deduped = [];
|
|
369
|
+
for (const p of priceLevels) {
|
|
370
|
+
const key = Number(p).toFixed(12);
|
|
371
|
+
if (!seen.has(key)) {
|
|
372
|
+
seen.add(key);
|
|
373
|
+
deduped.push(p);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
priceLevels.length = 0;
|
|
377
|
+
priceLevels.push(...deduped);
|
|
378
|
+
}
|
|
364
379
|
if (priceLevels.length === 0) {
|
|
365
380
|
throw new Error(`Grid generation produced no price levels for startPrice=${startPrice}, ` +
|
|
366
381
|
`bounds=[${minPrice}, ${maxPrice}], incrementPercent=${incrementPercent}. ` +
|
|
@@ -402,7 +417,8 @@ export function createOrderGrid(config) {
|
|
|
402
417
|
buy: Math.floor(gapSlots / 2),
|
|
403
418
|
sell: gapSlots - Math.floor(gapSlots / 2)
|
|
404
419
|
};
|
|
405
|
-
|
|
420
|
+
const genesis = buildGenesisFromPriceLevels(startPrice, incrementPercent, gapSlots, priceLevels);
|
|
421
|
+
return { orders: updatedOrders, boundaryIdx, initialSpreadCount, gapSlots, priceLevels, genesis };
|
|
406
422
|
}
|
|
407
423
|
/**
|
|
408
424
|
* Internal utility to clear all order-related manager caches.
|
|
@@ -442,10 +458,165 @@ function _clearOrderCachesLogic(manager) {
|
|
|
442
458
|
* @param {number|null} [boundaryIdx=null] - The master boundary index.
|
|
443
459
|
* @returns {Promise<void>}
|
|
444
460
|
*/
|
|
445
|
-
export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
461
|
+
export async function loadGrid(manager, grid, boundaryIdx = null, genesisInput = null) {
|
|
446
462
|
if (!Array.isArray(grid))
|
|
447
463
|
return;
|
|
448
464
|
return await manager._gridLock.acquire(async () => {
|
|
465
|
+
// Genesis determinism: if snapshot provided genesis, validate slots
|
|
466
|
+
// against it; if legacy snapshot has no genesis, migrate by building
|
|
467
|
+
// genesis from live config and re-sorting to canonical price order.
|
|
468
|
+
let genesis = genesisInput || manager._genesis || null;
|
|
469
|
+
// Validate genesis hash if present (wired for stale-pair detection #3)
|
|
470
|
+
// On mismatch we only warn — genesis is still used for validation (log mode) / virtualization (enforce mode);
|
|
471
|
+
// a stale persisted snapshot vs fresh manager._genesis scenario is covered by the log/enforce gate above
|
|
472
|
+
if (genesis && Array.isArray(genesis.priceLevels) && typeof genesis.priceLevelsHash === 'string') {
|
|
473
|
+
try {
|
|
474
|
+
const recomputed = hashPriceLevels(genesis.priceLevels);
|
|
475
|
+
if (recomputed !== genesis.priceLevelsHash) {
|
|
476
|
+
manager.logger?.log?.(`[GENESIS] hash mismatch persisted=${genesis.priceLevelsHash} recomputed=${recomputed} — persisted genesis hash does not match recomputed hash; continuing with persisted genesis`, 'warn');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
}
|
|
481
|
+
const validationMode = (() => {
|
|
482
|
+
try {
|
|
483
|
+
const raw = (typeof process !== 'undefined' && process.env?.GRID_PRICE_SLOT_VALIDATION) || 'log';
|
|
484
|
+
return String(raw).toLowerCase() === 'enforce' ? 'enforce' : 'log';
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return 'log';
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
if (genesis && Array.isArray(genesis.priceLevels)) {
|
|
491
|
+
// Validate each slot's price against genesis; virtualize only in enforce mode (plan §13)
|
|
492
|
+
const newGrid = [];
|
|
493
|
+
for (const slot of grid) {
|
|
494
|
+
try {
|
|
495
|
+
assertSlotPriceInvariant(slot, genesis);
|
|
496
|
+
newGrid.push(slot);
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
const msg = `[GENESIS] Slot ${slot?.id} price mismatch vs genesis → ${validationMode === 'enforce' ? 'virtualize' : 'log-only'}: ${getErrorMessage(e)}`;
|
|
500
|
+
manager.logger?.log?.(msg, 'warn');
|
|
501
|
+
if (validationMode === 'enforce') {
|
|
502
|
+
newGrid.push({ ...slot, state: ORDER_STATES.VIRTUAL, size: 0, orderId: '' });
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
newGrid.push(slot);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
grid = newGrid;
|
|
510
|
+
// Ensure canonical price-sorted order matches slot-N order
|
|
511
|
+
// Quirk fix: previously `isPriceSorted && parsed!==i` gated re-sort, so a shuffled
|
|
512
|
+
// array with misaligned ids but unsorted prices incorrectly skipped re-sort.
|
|
513
|
+
// Now: re-sort if any parseable id != index (price order is authoritative via slot-N).
|
|
514
|
+
const idxOrderMatchesPriceOrder = (() => {
|
|
515
|
+
for (let i = 0; i < grid.length; i++) {
|
|
516
|
+
const parsed = parseSlotIndex(grid[i]?.id);
|
|
517
|
+
if (parsed === null)
|
|
518
|
+
return false;
|
|
519
|
+
if (parsed !== i)
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
})();
|
|
524
|
+
if (!idxOrderMatchesPriceOrder) {
|
|
525
|
+
const sorted = [...grid].sort((a, b) => {
|
|
526
|
+
const pa = parseSlotIndex(a?.id);
|
|
527
|
+
const pb = parseSlotIndex(b?.id);
|
|
528
|
+
if (pa !== null && pb !== null)
|
|
529
|
+
return pa - pb;
|
|
530
|
+
return Number(a.price) - Number(b.price);
|
|
531
|
+
});
|
|
532
|
+
// Detect if re-sort actually changes order
|
|
533
|
+
let changed = false;
|
|
534
|
+
for (let i = 0; i < grid.length; i++)
|
|
535
|
+
if (grid[i].id !== sorted[i].id) {
|
|
536
|
+
changed = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (changed) {
|
|
540
|
+
manager.logger?.log?.(`[GENESIS] Re-sorted persisted grid to canonical slot-N order`, 'warn');
|
|
541
|
+
grid = sorted;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Guard: explicit persistedGenesis should not blindly overwrite a fresher in-memory _genesis
|
|
545
|
+
// from an in-process rebuild (currently unreachable but latent hazard if flows change)
|
|
546
|
+
if (manager._genesis && manager._genesis.priceLevelsHash && genesis.priceLevelsHash && manager._genesis.priceLevelsHash !== genesis.priceLevelsHash) {
|
|
547
|
+
manager.logger?.log?.(`[GENESIS] persisted genesis hash ${genesis.priceLevelsHash} differs from in-memory ${manager._genesis.priceLevelsHash} — keeping in-memory genesis`, 'warn');
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
manager._genesis = genesis;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (grid.length > 0) {
|
|
554
|
+
// Migration: build genesis from live geometric rail (plan §4.1/§10)
|
|
555
|
+
// Do NOT derive priceLevels from persisted slot prices — a truncated
|
|
556
|
+
// persisted array would permanently shrink genesis. Recompute from
|
|
557
|
+
// startPrice/min/max/increment per createOrderGrid §2.1.
|
|
558
|
+
const startPrice = Number(manager.config?.startPrice);
|
|
559
|
+
const minPrice = Number(manager.config?.minPrice);
|
|
560
|
+
const maxPrice = Number(manager.config?.maxPrice);
|
|
561
|
+
const incPct = Number(manager.config?.incrementPercent);
|
|
562
|
+
if (Number.isFinite(startPrice) && Number.isFinite(minPrice) && Number.isFinite(maxPrice) && Number.isFinite(incPct)) {
|
|
563
|
+
try {
|
|
564
|
+
const stepUp = 1 + (incPct / 100);
|
|
565
|
+
const stepDown = 1 - (incPct / 100);
|
|
566
|
+
const priceLevels = [];
|
|
567
|
+
let upPrice = startPrice * Math.sqrt(stepUp);
|
|
568
|
+
while (upPrice <= maxPrice) {
|
|
569
|
+
priceLevels.push(upPrice);
|
|
570
|
+
upPrice *= stepUp;
|
|
571
|
+
}
|
|
572
|
+
let downPrice = startPrice * Math.sqrt(stepDown);
|
|
573
|
+
while (downPrice >= minPrice) {
|
|
574
|
+
priceLevels.push(downPrice);
|
|
575
|
+
downPrice *= stepDown;
|
|
576
|
+
}
|
|
577
|
+
priceLevels.sort((a, b) => a - b);
|
|
578
|
+
// Dedupe (floatToBlockchainInt equality via fixed 12-decimals)
|
|
579
|
+
const uniq = [];
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
for (const p of priceLevels) {
|
|
582
|
+
const key = Number(p).toFixed(12);
|
|
583
|
+
if (!seen.has(key)) {
|
|
584
|
+
seen.add(key);
|
|
585
|
+
uniq.push(p);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const gapSlotsForGenesis = calculateGapSlots(incPct, manager.config?.targetSpreadPercent, manager.config?.gridLimits);
|
|
589
|
+
const built = buildGenesisFromPriceLevels(startPrice, incPct, gapSlotsForGenesis, uniq);
|
|
590
|
+
// Cross-check: if user edited startPrice/min/max/increment across restarts on a legacy snapshot,
|
|
591
|
+
// the new-config rail will mismatch most persisted slot prices → noisy log / mass-virtualize
|
|
592
|
+
// in enforce mode and the mismatched genesis would be persisted. Count failures first.
|
|
593
|
+
let mismatchCount = 0;
|
|
594
|
+
for (const slot of grid) {
|
|
595
|
+
try {
|
|
596
|
+
assertSlotPriceInvariant(slot, built);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
mismatchCount++;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const mismatchRatio = grid.length > 0 ? mismatchCount / grid.length : 0;
|
|
603
|
+
if (mismatchRatio > 0.5) {
|
|
604
|
+
manager.logger?.log?.(`[GENESIS] Migration: ${mismatchCount}/${grid.length} slots mismatch new-config rail (ratio ${mismatchRatio.toFixed(2)}) — config may have changed since snapshot; NOT adopting migration genesis (validation would ${validationMode === 'enforce' ? 'mass-virtualize' : 'be noisy'}). Persisted grid will be kept as-is until a clean rebuild`, 'warn');
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
if (mismatchCount > 0) {
|
|
608
|
+
manager.logger?.log?.(`[GENESIS] Migration: ${mismatchCount}/${grid.length} slots mismatch new-config rail — will be logged${validationMode === 'enforce' ? '/virtualized' : ''} on next load`, 'warn');
|
|
609
|
+
}
|
|
610
|
+
manager._genesis = built;
|
|
611
|
+
genesis = built;
|
|
612
|
+
manager.logger?.log?.(`[GENESIS] Migrated legacy grid: built genesis with ${uniq.length} levels (hash ${built.priceLevelsHash})`, 'info');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
manager.logger?.log?.(`[GENESIS] Migration failed: ${getErrorMessage(e)}`, 'warn');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
449
620
|
try {
|
|
450
621
|
await withBlockchainRetry(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
|
|
451
622
|
}
|
|
@@ -517,11 +688,10 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
517
688
|
manager.logger?.log?.(`Restored boundary index: ${restoredBoundary}`, 'info');
|
|
518
689
|
}
|
|
519
690
|
// Reassign slot types based on current boundary.
|
|
520
|
-
// Every slot's type must match its
|
|
521
|
-
// relative to the boundary + gapSlots
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
// slot holding a SELL chain order) and auto-cancel + recreate them.
|
|
691
|
+
// Every slot's type must match its price-slot index (parseSlotIndex)
|
|
692
|
+
// relative to the boundary + gapSlots, not array position. After
|
|
693
|
+
// genesis freeze the persisted array may have been re-sorted, but
|
|
694
|
+
// slot-N is the canonical identity.
|
|
525
695
|
if (restoredBoundary !== null) {
|
|
526
696
|
const gapSlots = loadGapSlots;
|
|
527
697
|
manager._gapSlots = gapSlots;
|
|
@@ -529,9 +699,22 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
529
699
|
const sellStartIdx = getSellStartIdx(restoredBoundary, gapSlots);
|
|
530
700
|
let reassignCount = 0;
|
|
531
701
|
grid = grid.map((slot, i) => {
|
|
532
|
-
const
|
|
702
|
+
const parsedIdx = parseSlotIndex(slot?.id);
|
|
703
|
+
// Plan §4.3: unparseable ids are invalid per decision #3 — enforce virtualizes,
|
|
704
|
+
// log mode keeps index fallback (legacy grids with ids like 'b1'/'planned' need type via position)
|
|
705
|
+
if (parsedIdx === null) {
|
|
706
|
+
if (validationMode === 'enforce') {
|
|
707
|
+
manager.logger?.log?.(`[GENESIS] unparseable slot id ${slot?.id} at index ${i} → virtualize (SPREAD)`, 'warn');
|
|
708
|
+
reassignCount++;
|
|
709
|
+
return { ...slot, state: ORDER_STATES.VIRTUAL, size: 0, orderId: '', type: ORDER_TYPES.SPREAD };
|
|
710
|
+
}
|
|
711
|
+
manager.logger?.log?.(`[GENESIS] unparseable slot id ${slot?.id} at index ${i} → log-only, using index fallback ${i} for type`, 'warn');
|
|
712
|
+
// fall through with idx = i so legacy sized VIRTUAL rail slots (e.g. 'planned') keep their size/type
|
|
713
|
+
}
|
|
714
|
+
const idx = parsedIdx !== null ? parsedIdx : i;
|
|
715
|
+
const correctType = (idx <= buyEndIdx)
|
|
533
716
|
? ORDER_TYPES.BUY
|
|
534
|
-
: (
|
|
717
|
+
: (idx >= sellStartIdx)
|
|
535
718
|
? ORDER_TYPES.SELL
|
|
536
719
|
: ORDER_TYPES.SPREAD;
|
|
537
720
|
// DEFENSIVE BACKSTOP: a VIRTUAL slot with no orderId and
|
|
@@ -570,7 +753,7 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
570
753
|
// comparison false and wrongly resolves below-center
|
|
571
754
|
// slots to SELL.
|
|
572
755
|
if (newType === ORDER_TYPES.SPREAD && isOrderOnChain(slot)) {
|
|
573
|
-
newType = resolveOnChainRetypeType(slot,
|
|
756
|
+
newType = resolveOnChainRetypeType(slot, idx, buyEndIdx, ORDER_TYPES);
|
|
574
757
|
}
|
|
575
758
|
reassignCount++;
|
|
576
759
|
return { ...slot, type: newType };
|
|
@@ -605,6 +788,7 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
605
788
|
}
|
|
606
789
|
manager.pauseRecalcLogging();
|
|
607
790
|
manager.pauseFundRecalc();
|
|
791
|
+
let sanitizedSizedVirtual = [];
|
|
608
792
|
try {
|
|
609
793
|
// RC-2: Use applyOrderUpdate (PRIVATE/UNLOCKED)
|
|
610
794
|
for (const order of grid) {
|
|
@@ -613,8 +797,31 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
|
|
|
613
797
|
manager.logger?.log?.(`Sanitizing corrupted order ${order.id}: ACTIVE/PARTIAL without orderId -> VIRTUAL`, 'warn');
|
|
614
798
|
currentOrder = { ...order, state: ORDER_STATES.VIRTUAL };
|
|
615
799
|
}
|
|
800
|
+
// Sized VIRTUAL slot with no on-chain id AND durable orphan
|
|
801
|
+
// evidence (createUncertain: set only where a CREATE
|
|
802
|
+
// broadcast result was lost — COW uncertain-restore and
|
|
803
|
+
// startup uncertain-create handlers). Unflagged sized
|
|
804
|
+
// VIRTUAL slots are the NORMAL planned-but-unplaced grid
|
|
805
|
+
// state (budgeted sizes cover the whole rail and persist
|
|
806
|
+
// by design), so their sizes must survive reload; only
|
|
807
|
+
// flagged orphans get zeroed so re-derivation or spread
|
|
808
|
+
// correction re-places them next cycle. One aggregate
|
|
809
|
+
// warn instead of per-slot spam.
|
|
810
|
+
if (currentOrder.state === ORDER_STATES.VIRTUAL
|
|
811
|
+
&& !hasOnChainId(currentOrder)
|
|
812
|
+
&& Number(currentOrder.size || 0) > 0
|
|
813
|
+
&& currentOrder.createUncertain === true) {
|
|
814
|
+
sanitizedSizedVirtual.push(currentOrder.id);
|
|
815
|
+
currentOrder = { ...currentOrder, size: 0, createUncertain: false };
|
|
816
|
+
}
|
|
616
817
|
await manager._applyOrderUpdate(currentOrder, 'grid-load', { skipAccounting: true });
|
|
617
818
|
}
|
|
819
|
+
if (sanitizedSizedVirtual.length > 0) {
|
|
820
|
+
const preview = sanitizedSizedVirtual.slice(0, 10).join(', ');
|
|
821
|
+
manager.logger?.log?.(`Sanitized ${sanitizedSizedVirtual.length} creation-uncertain sized slot(s) on load ` +
|
|
822
|
+
`(VIRTUAL with size but no orderId -> size dropped): ${preview}` +
|
|
823
|
+
(sanitizedSizedVirtual.length > 10 ? ' …' : ''), 'warn');
|
|
824
|
+
}
|
|
618
825
|
// Gap-band occupancy for the spread metric. Empty slots are
|
|
619
826
|
// normalized to SPREAD (side-neutral) above, so a raw
|
|
620
827
|
// `type === SPREAD` count would include every empty slot on both
|
|
@@ -901,13 +1108,15 @@ export async function initializeGrid(manager) {
|
|
|
901
1108
|
// Continuing would create grid with 0 fund allocation, rendering it non-functional
|
|
902
1109
|
throw new Error(`Cannot initialize grid without account totals: ${getErrorMessage(e)}`);
|
|
903
1110
|
}
|
|
904
|
-
const { orders, boundaryIdx, initialSpreadCount, gapSlots } = createOrderGrid({
|
|
1111
|
+
const { orders, boundaryIdx, initialSpreadCount, gapSlots, genesis } = createOrderGrid({
|
|
905
1112
|
...manager.config,
|
|
906
1113
|
startPrice: gridStartPrice,
|
|
907
1114
|
minPrice: resolvedMinP,
|
|
908
1115
|
maxPrice: resolvedMaxP,
|
|
909
1116
|
});
|
|
910
1117
|
manager._gapSlots = gapSlots;
|
|
1118
|
+
if (genesis)
|
|
1119
|
+
manager._genesis = genesis;
|
|
911
1120
|
// RC-8: Update boundary with notification to dependent systems
|
|
912
1121
|
// Persist master boundary for StrategyEngine
|
|
913
1122
|
if (manager.boundaryIdx !== boundaryIdx) {
|
|
@@ -1063,6 +1272,18 @@ export async function recalculateGrid(manager, opts) {
|
|
|
1063
1272
|
manager.logger?.log?.(`Error during startup order reconciliation: ${getErrorMessage(err)}`, 'error');
|
|
1064
1273
|
throw new Error(`Grid recalculation failed during order reconciliation: ${getErrorMessage(err)}`);
|
|
1065
1274
|
}
|
|
1275
|
+
// Fix #5 (Mode D): the P3 boundary-evidence gate above may have
|
|
1276
|
+
// re-derived the boundary via manager._restoreBoundary. recalculateGrid's
|
|
1277
|
+
// earlier persistGrid (before initializeGrid) captured the stale value, so
|
|
1278
|
+
// without this flush the corrected boundary never reaches disk and the
|
|
1279
|
+
// [BOUNDARY-EVIDENCE] contradiction re-fires on every resync. Persist the
|
|
1280
|
+
// reconciled grid (corrected boundary included) now.
|
|
1281
|
+
try {
|
|
1282
|
+
await manager.persistGrid(undefined);
|
|
1283
|
+
}
|
|
1284
|
+
catch (persistErr) {
|
|
1285
|
+
manager.logger?.log?.(`Error persisting boundary-corrected grid after reconcile: ${getErrorMessage(persistErr)}`, 'warn');
|
|
1286
|
+
}
|
|
1066
1287
|
if (_resyncAborted)
|
|
1067
1288
|
return;
|
|
1068
1289
|
manager.logger?.log?.('Full resync complete.', 'info');
|
|
@@ -1838,13 +2059,25 @@ export async function checkWindowDust(manager) {
|
|
|
1838
2059
|
const topSellOrder = allOrders
|
|
1839
2060
|
.filter((o) => o.type === ORDER_TYPES.SELL && isLiveOrder(o))
|
|
1840
2061
|
.sort((a, b) => a.price - b.price)[0];
|
|
1841
|
-
// Check if an order has a duplicate
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2062
|
+
// Check if an order has a duplicate slot — an active sibling with same price via integer round-trip.
|
|
2063
|
+
const hasDuplicatePriceLevel = (order, assets) => {
|
|
2064
|
+
const precision = order.type === ORDER_TYPES.SELL ? assets.assetA.precision : assets.assetB.precision;
|
|
2065
|
+
for (const o of allOrders) {
|
|
2066
|
+
if (o.id === order.id)
|
|
2067
|
+
continue;
|
|
2068
|
+
if (o.type !== order.type || o.state !== ORDER_STATES.ACTIVE || !o.orderId || o.price == null)
|
|
2069
|
+
continue;
|
|
2070
|
+
try {
|
|
2071
|
+
if (priceSlotEqual(o.price, order.price, precision))
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2074
|
+
catch {
|
|
2075
|
+
if (o.price === order.price)
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return false;
|
|
2080
|
+
};
|
|
1848
2081
|
const assets = manager.assets;
|
|
1849
2082
|
const allPartials = allOrders.filter((o) => isLiveOrder(o) && o.state === ORDER_STATES.PARTIAL);
|
|
1850
2083
|
const isTopBuy = (o) => topBuyOrder && o.id === topBuyOrder.id;
|
|
@@ -2184,9 +2417,10 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2184
2417
|
.sort((a, b) => railType === ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
|
|
2185
2418
|
.slice(0, missingSlots);
|
|
2186
2419
|
// Secondary candidates: orphaned virtual slots that have lost their
|
|
2187
|
-
// order (e.g. stale-cleaned after a race condition during a crash
|
|
2188
|
-
//
|
|
2189
|
-
//
|
|
2420
|
+
// order (e.g. stale-cleaned after a race condition during a crash, or a
|
|
2421
|
+
// lowest-sell slot dust-cancelled and then left unplaced after a
|
|
2422
|
+
// boundary shift). These sit inside the active window and are invisible
|
|
2423
|
+
// to the gap-band SPREAD filter above.
|
|
2190
2424
|
//
|
|
2191
2425
|
// Empty slots are stored SPREAD (side-neutral) after normalization, so
|
|
2192
2426
|
// the stored type can be railType OR SPREAD — what matters is that the
|
|
@@ -2195,11 +2429,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2195
2429
|
// whose boundary position has moved into the spread or opposite zone are
|
|
2196
2430
|
// NOT re-activated on the stale side — doing so would compound inventory
|
|
2197
2431
|
// at prices where the bot already traded.
|
|
2432
|
+
//
|
|
2433
|
+
// NOTE: we deliberately accept BOTH zero-size and sized virtual slots
|
|
2434
|
+
// here. A sized sell/buy slot that lost its orderId (the classic
|
|
2435
|
+
// boundary-oscillation orphan) has size > 0 but no on-chain order — the
|
|
2436
|
+
// old `size === 0` guard excluded it, so spread correction silently
|
|
2437
|
+
// planned 0 orders and the gap never closed. Dropping the guard is safe:
|
|
2438
|
+
// create targets are rebuilt from `current: 0` + the fresh ideal from
|
|
2439
|
+
// allocateFundsByWeights (grid.ts:2790-2800, 2861-2870), so a sized
|
|
2440
|
+
// orphan's stale stored size never leaks into a placement; getMinOrderSize
|
|
2441
|
+
// is only a collision-tolerance fallback (grid.ts:2712), not the primary
|
|
2442
|
+
// sizer. Orphans are excluded from the sideSlots sizing denominator too,
|
|
2443
|
+
// so this also removes the prior dilution of every other order.
|
|
2198
2444
|
const orphanedVirtualCandidates = allOrders
|
|
2199
2445
|
.filter((o) => (o.type === railType || o.type === ORDER_TYPES.SPREAD)
|
|
2200
2446
|
&& o.state === ORDER_STATES.VIRTUAL
|
|
2201
2447
|
&& !o.orderId
|
|
2202
|
-
&& Number(o.size || 0) === 0
|
|
2203
2448
|
&& getSlotCorrectType(o) === railType)
|
|
2204
2449
|
.sort((a, b) => railType === ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
|
|
2205
2450
|
.slice(0, missingSlots);
|
|
@@ -2281,10 +2526,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2281
2526
|
spreadCandidates = spreadCandidates.filter((c) => {
|
|
2282
2527
|
if (c.price == null)
|
|
2283
2528
|
return false;
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2529
|
+
const precision = railType === ORDER_TYPES.SELL ? manager.assets.assetA.precision : manager.assets.assetB.precision;
|
|
2530
|
+
for (const o of allOrders) {
|
|
2531
|
+
if (!isOrderPlaced(o) || o.price == null)
|
|
2532
|
+
continue;
|
|
2533
|
+
if (o.id === c.id)
|
|
2534
|
+
continue;
|
|
2535
|
+
try {
|
|
2536
|
+
if (priceSlotEqual(o.price, c.price, precision))
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
if (o.price === c.price)
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return true;
|
|
2288
2545
|
});
|
|
2289
2546
|
const filteredCount = preFilter - spreadCandidates.length;
|
|
2290
2547
|
if (filteredCount > 0) {
|
|
@@ -2296,7 +2553,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2296
2553
|
}
|
|
2297
2554
|
if (!edgePartial && spreadCandidates.length === 0) {
|
|
2298
2555
|
manager.logger?.log?.(`[SPREAD-CORRECTION] No suitable partials, orphaned virtual slots, or spread slots found. Skipping.`, 'warn');
|
|
2299
|
-
return { ordersToPlace: [], ordersToUpdate: [] };
|
|
2556
|
+
return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
|
|
2300
2557
|
}
|
|
2301
2558
|
const orphanedIds = new Set(orphanedVirtualCandidates.map((o) => o.id));
|
|
2302
2559
|
const sideSlots = allOrders
|
|
@@ -2308,7 +2565,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2308
2565
|
].sort((a, b) => a.price - b.price);
|
|
2309
2566
|
const ctx = await _getSizingContext(manager, sideName);
|
|
2310
2567
|
if (!ctx || ctx.budget <= 0 || syntheticSideSlots.length === 0) {
|
|
2311
|
-
return { ordersToPlace: [], ordersToUpdate: [] };
|
|
2568
|
+
return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
|
|
2312
2569
|
}
|
|
2313
2570
|
const precisionEpsilon = getPrecisionSlack(ctx.precision, 1);
|
|
2314
2571
|
const idealSizes = allocateFundsByWeights(ctx.budget, syntheticSideSlots.length, manager.config.weightDistribution[sideName], manager.config.incrementPercent / 100, railType === ORDER_TYPES.BUY, 0, ctx.precision);
|
|
@@ -2345,7 +2602,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2345
2602
|
}
|
|
2346
2603
|
}
|
|
2347
2604
|
if (prioritizedTargets.length === 0) {
|
|
2348
|
-
return { ordersToPlace: [], ordersToUpdate: [] };
|
|
2605
|
+
return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
|
|
2349
2606
|
}
|
|
2350
2607
|
const totalNeeded = prioritizedTargets.reduce((sum, t) => sum + Math.max(0, Number(t.needed || 0)), 0);
|
|
2351
2608
|
let recoveredBudget = 0;
|
|
@@ -2479,6 +2736,6 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
|
|
|
2479
2736
|
if (combinedUpdates.length > 0 || ordersToPlace.length > 0) {
|
|
2480
2737
|
manager.logger?.log?.(`[SPREAD-CORRECTION] Prepared updates=${combinedUpdates.length}, creates=${ordersToPlace.length}, remainingBudget=${Format.formatSizeByOrderType(Math.max(0, remainingBudget), railType, manager.assets)}`, 'debug');
|
|
2481
2738
|
}
|
|
2482
|
-
return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }) };
|
|
2739
|
+
return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }), origin: 'spread-correction' };
|
|
2483
2740
|
}
|
|
2484
2741
|
//# sourceMappingURL=grid.js.map
|