Welcome to DEXBot2! This guide will help you understand the codebase, navigate key concepts, and contribute effectively.
For system design overview, see architecture.md which provides high-level module relationships and data flows.
Read these files in order to get oriented:
- README.md - User documentation and setup
- architecture.md - System architecture and module relationships
- FUND_MOVEMENT_AND_ACCOUNTING.md - Core algorithms and formulas
Understand these fundamental concepts before diving into code:
- Grid Trading: Placing orders at geometric price levels to profit from volatility
- Order States: VIRTUAL → ACTIVE → PARTIAL lifecycle
- Fund Tracking: Atomic accounting to prevent overdrafts
- Boundary Crawl: Dynamic order rotation following price movement
- Copy-on-Write (COW): Master grid is immutable. All rebalancing runs on an isolated
WorkingGridclone; the master is replaced atomically only after blockchain success. See COPY_ON_WRITE_MASTER_PLAN.md.
Follow this path through the codebase:
1. modules/constants.js (5 min) - Configuration and tuning parameters
2. modules/order/manager.js (10 min) - Central coordinator, read constructor + _updateOrder() + _applySafeRebalanceCOW()
3. modules/order/working_grid.js (5 min) - COW working copy; read syncFromMaster() + buildDelta() + _commitWorkingGrid()
4. modules/order/accounting.js (5 min) - Fund tracking, read recalculateFunds() and resetRecoveryState()
5. modules/order/strategy.js (5 min) - Rebalancing logic, read rebalance()
6. modules/order/grid.js (5 min) - Grid creation, read createOrderGrid()
7. modules/dexbot_class.js::processFilledOrders (5-10 min) - Fill batch processing pipeline
Additional Resources:
modules/constants.js::FILL_PROCESSING- Batch configuration (MAX_FILL_BATCH_SIZE)modules/constants.js::PIPELINE_TIMING- Recovery configuration (RECOVERY_RETRY_INTERVAL_MS, MAX_RECOVERY_ATTEMPTS)modules/dexbot_class.js::_handleBatchHardAbort()- Hard-abort recovery handlermodules/dexbot_class.js::_staleCleanedOrderIds- Orphan-fill deduplication tracking
| Term | Meaning | Fund Impact |
|---|---|---|
| VIRTUAL | Order planned but not on-chain | Funds reserved in virtual pool |
| ACTIVE | Order placed on blockchain | Funds locked in committed.chain |
| PARTIAL | Order partially filled | Reduced committed, proceeds added to chainFree |
| SPREAD | Placeholder for spread zone | Always VIRTUAL, no funds |
A phantom order is an order in ACTIVE/PARTIAL state WITHOUT a valid orderId. This is an illegal state that corrupts fund tracking. The system implements a three-layer defense to prevent phantoms (see Phantom Orders Prevention section). If encountered, the order is automatically downgraded to VIRTUAL with error logging.
| Term | Meaning |
|---|---|
| Pipeline Timeout Safeguard | 5-minute timeout preventing indefinite blocking on pipeline checks |
| Pipeline Health Diagnostics | getPipelineHealth() method returning 8 diagnostic fields for monitoring |
| Stale Operation Clearing | Non-destructive recovery clearing operation flags without touching orders |
| Term | Meaning |
|---|---|
| Fixed-Cap Batch Fill Processing | Groups fills with a hard cap using MAX_FILL_BATCH_SIZE (default 4): <= cap uses one unified batch; > cap chunks at cap size. Reduces market divergence window from ~90s to ~24s for 29 fills. |
| Recovery Retry System | Count+time-based retry mechanism with periodic reset. Replaces one-shot _recoveryAttempted flag. Max 5 attempts per episode with 60s minimum interval between retries. |
| Orphan-Fill Deduplication | Map+TTL-based tracking of stale-cleaned order IDs to prevent double-crediting. Delayed orphan fill events are still blocked by checking _staleCleanedOrderIds. |
| Term | Meaning |
|---|---|
| Remainder Tracking | Remainder derived from actual allocated sizes during capped grid resizes (not ideal sizes). Ensures accurate available fund calculations for next rebalance cycle. |
| Stale-Order Fast-Path | Stale-cancel logic applies to single-op batches for fast recovery without triggering full state syncs. Prevents unnecessary expensive resynchronization. |
| Hard-Abort Cooldown Consistency | Both primary and retry batch hard-abort paths explicitly arm _maintenanceCooldownCycles to prevent premature maintenance after recovery. |
| Term | Meaning | Formula |
|---|---|---|
| chainFree | Unallocated blockchain balance | From accountTotals.buyFree/sellFree |
| committed.chain | Funds locked in on-chain orders | Sum of ACTIVE orders with orderId |
| committed.grid | Internal tracking of ACTIVE sizes | Sum of all ACTIVE order sizes |
| virtual | Reserved for VIRTUAL orders | Sum of VIRTUAL order sizes |
| available | Free funds for new orders | max(0, chainFree - virtual - fees) |
| total.chain | Total on-chain balance | chainFree + committed.chain |
| total.grid | Total grid allocation | committed.grid + virtual |
| Term | Meaning |
|---|---|
Master Grid (manager.orders) |
The immutable source of truth. Frozen with Object.freeze(). Never mutated in place—replaced atomically via _commitWorkingGrid() only after blockchain success. |
| WorkingGrid | Mutable clone of the master grid used during planning. Created by new WorkingGrid(masterGrid). Discarded on failure; committed on success. Lives in modules/order/working_grid.js. |
| COW Rebalance | The full plan→broadcast→commit cycle: _applySafeRebalanceCOW() → _updateOrdersOnChainBatchCOW() → _commitWorkingGrid() |
| Atomic Commit | _commitWorkingGrid(workingGrid, indexes, boundary, options = {}) swaps master to the working copy in a single operation, then increments _gridVersion. |
| Staleness / Version Mismatch | If a fill mutates the master grid while a rebalance is in progress, the working grid is marked stale. The commit guard rejects stale or version-mismatched working grids. |
_gridVersion |
Integer counter incremented on every master-grid mutation. Used by commit guards to detect concurrent master changes. |
_gridLock |
AsyncLock that serializes all master-grid mutations to prevent races. |
syncFromMaster() |
WorkingGrid method that selectively syncs one order from the master into the working copy during fills, avoiding a full abort. |
| Delta / COW Action | A { type, id, ... } object describing a single Create / Update / Cancel operation derived by comparing master vs. working grid. Only deltas are sent to the blockchain. |
| Rebalance States | NORMAL → REBALANCING → BROADCASTING → NORMAL. Fills during REBALANCING sync into working grid. Fills during BROADCASTING mark it stale. |
| Term | Meaning |
|---|---|
| Master Rail | Unified array of price levels (not separate buy/sell rails) |
| Boundary Index | Pivot point separating BUY/SPREAD/SELL zones |
| Spread Gap | Buffer of empty slots between best buy and best sell |
| Crawl Candidate | Furthest active order eligible for rotation |
| Shortage | Empty slot in the active window that needs an order |
| Surplus | Order outside the active window that can be rotated |
| Hard Surplus | Order beyond the configured activeOrders count |
| Dust | Partial order < 5% of ideal size |
| Doubled Side | Flag set when a dust partial is updated to ideal size; allows additional rebalancing actions on that side |
| Term | Meaning |
|---|---|
| Rotation | Moving an order from one price level to another |
| Consolidation | Updating dust partials to ideal size and flagging the side as "doubled" for additional rebalancing capacity |
| Rebalancing | Adjusting order sizes based on current funds |
| Global Side Capping | Scaling order sizes when insufficient funds |
| Atomic Check-and-Deduct | Verify funds + deduct in single operation |
| Divergence Detection | Comparing ideal grid vs. persisted grid |
| Invariant Verification | Checking fund accounting consistency |
| Batch Processing | Grouping multiple fills into a single rebalance cycle instead of one-at-a-time. Fixed-cap sizing: <= MAX_FILL_BATCH_SIZE unified, otherwise chunked at cap size (default max 4). |
| Fixed-Cap Batch Sizing | Deterministic chunking model with hard upper bound per broadcast. Keeps throughput high while avoiding tier-lookup complexity. |
| Stale-Order Recovery | Fast-path recovery for single-operation batches that encounter stale orders on-chain. Executes cleanup without full state sync. |
| Orphan-Fill Prevention | Deduplication mechanism that prevents double-crediting fills from stale-cleaned orders using timestamp-based ID tracking (TTL pruning). |
Critical Concept: All prices in DEXBot2 use B/A orientation (how much of asset B per 1 unit of asset A).
| Term | Meaning | Example |
|---|---|---|
| B/A Orientation | Price format representing "how much B per 1 A" | XRP/BTS: 1350 means 1 XRP = 1350 BTS |
| A/B Orientation | Price format representing "how much A per 1 B" (NOT used in bot) | XRP/BTS: 0.00074 means 0.00074 XRP per 1 BTS |
| Source | Raw Format | Conversion | Final Format |
|---|---|---|---|
BitShares get_order_book(A, B) |
A/B (base/quote) | 1 / mid |
B/A ✓ |
BitShares get_ticker(A, B) |
A/B (base/quote) | 1 / value |
B/A ✓ |
| Liquidity Pool Reserves | reserve_A / reserve_B |
floatB / floatA |
B/A ✓ |
deriveMarketPrice(BitShares, symA, symB):
// BitShares get_order_book(A, B) returns prices in A/B format
const mid = (bestBid + bestAsk) / 2; // e.g., 0.00074 (XRP per BTS)
return 1 / mid; // Convert to B/A: 1/0.00074 ≈ 1350 (BTS per XRP)derivePoolPrice(BitShares, symA, symB):
// Pool reserves come from blockchain in order [reserve_A, reserve_B]
const floatA = safeBlockchainToFloat(amtA, aMeta.precision);
const floatB = safeBlockchainToFloat(amtB, bMeta.precision);
return floatB / floatA; // Already B/A: 3000000 BTS / 20000 XRP = 150 (BTS/XRP)- Grid Placement:
startPricedetermines where BUY orders (below) and SELL orders (above) are placed - Consistency: Both market and pool prices use B/A so they're directly comparable
- Debugging: Inverted prices cause bot to place orders on the wrong side of the market (e.g., massive sells when market rises)
If you see prices like 0.000795 when expecting 1350:
- This is likely A/B format (raw from API)
- Check if inversion (
1 / price) is being applied - Verify which function is missing the conversion
Problem: Floating-point arithmetic accumulates rounding errors over many calculations. After repeated price derivations, fund allocations, and order sizing, float values drift from their true blockchain precision.
Solution: Use centralized quantization utilities from modules/order/utils/math.js to eliminate accumulation.
| Situation | Use Function | Why |
|---|---|---|
| Calculating order sizes | quantizeFloat() |
Geometric weighting produces float errors; snap to satoshi precision |
| Fund allocation | quantizeFloat() |
After dividing total by weights, accumulation errors occur |
| Price derivations | quantizeFloat() |
Pool/market calculations prone to float drift |
| Comparing sizes | normalizeInt() |
Ensure both values use same integer representation before == |
| Validating blockchain match | normalizeInt() |
Check internal ≈ chain by normalizing both sides |
// ❌ WRONG - Float accumulation errors
const sizes = [];
const base = 0.995;
const totalFunds = 100.12345678;
for (let i = 0; i < 5; i++) {
const weight = Math.pow(base, i);
sizes.push((weight / sumWeights) * totalFunds); // Drift accumulates!
}
// ✅ CORRECT - Quantized to precision
const { quantizeFloat } = require('./modules/order/utils/math');
const sizes = [];
const precision = 8;
const base = 0.995;
const totalFunds = 100.12345678;
for (let i = 0; i < 5; i++) {
const weight = Math.pow(base, i);
const size = (weight / sumWeights) * totalFunds;
sizes.push(quantizeFloat(size, precision)); // Snap to satoshi
}
// Result: All sizes align to 8 decimal places, no driftconst { quantizeFloat, normalizeInt } = require('../order/utils/math');
// Quantize a float value (e.g., 45.123456789 → 45.12345679)
const correctedPrice = quantizeFloat(derivedPrice, 8);
// Normalize an integer (e.g., ensure consistency in comparisons)
const normalized = normalizeInt(currentSizeInt, assetPrecision);See FUND_MOVEMENT_AND_ACCOUNTING.md § 5.5 for complete quantization guide and edge case handling.
Role: Central coordinator for all order operations
Key Responsibilities:
- Maintain immutable master grid (
ordersMap, frozen viaObject.freeze()) - Enforce Copy-on-Write rebalancing via
WorkingGridand atomic commits - Coordinate specialized engines (Accountant, Strategy, Sync, Grid)
- Manage order indices for fast lookups
- Handle order locking to prevent race conditions
Critical Methods:
// Central state update - ALWAYS use this, never modify orders Map directly
// Signature: _updateOrder(order, context = 'updateOrder', options = {})
_updateOrder(order, context, { skipAccounting, fee })
// COW rebalance pipeline
performSafeRebalance(fills, excludeIds) // Entry point; delegates to COW
_applySafeRebalanceCOW(fills, excludeIds) // Creates WorkingGrid, runs planning
_reconcileGridCOW(targetGrid, boundary, wg) // Computes delta against working copy
_commitWorkingGrid(workingGrid, indexes, boundary, { skipRecalc }) // Atomic swap: working → master
// COW state tracking
_setRebalanceState(state) // NORMAL | REBALANCING | BROADCASTING
_currentWorkingGrid // Reference to active WorkingGrid (for fill sync)
// Fast lookups using indices
getOrdersByTypeAndState(type, state)
// Concurrency control
lockOrders([orderId])
unlockOrders([orderId])
isOrderLocked(orderId)
// Batch optimization
pauseFundRecalc()
resumeFundRecalc()COW Rule: The
ordersMap is frozen. Never callmanager.orders.set()directly. All mutations go through_applyOrderUpdate()(for master changes) or the WorkingGrid (for planning). The master is only replaced inside_commitWorkingGrid()after blockchain success.
Common Patterns:
// Pattern 1: Simple order state update (uses defaults)
manager._updateOrder({
id: 'buy-5',
state: ORDER_STATES.ACTIVE,
type: ORDER_TYPES.BUY,
price: 0.5,
size: 100,
orderId: '1.7.12345'
});
// Pattern 2: Update with context and fee (for blockchain operations)
manager._updateOrder(
{ id: 'buy-5', state: ORDER_STATES.VIRTUAL, orderId: null },
'cancel-order', // context for logging
{ skipAccounting: false, fee: 0 } // update balances, no cancel fee
);
// Pattern 3: Batch updates with pause/resume
manager.pauseFundRecalc();
for (const order of orders) {
manager._updateOrder(order, 'rebalance-batch', { skipAccounting: false, fee: 0 });
}
manager.resumeFundRecalc(); // Recalculates once at end
// Pattern 4: Safe async operations
manager.lockOrders([orderId]);
try {
await chainOperation();
} finally {
manager.unlockOrders([orderId]);
}Phantom orders are orders that exist in memory as ACTIVE/PARTIAL state but lack a corresponding blockchain orderId. This causes fund tracking corruption:
- Memory shows orders locked in
committed.gridbut blockchain has no such orders - Leads to "doubled funds" warnings where
trackedTotal >> blockchainTotal - Causes high RMS divergence with many unmatched orders
- Can lock funds indefinitely if not detected
The Critical Validation:
// Centralized check - ALL state transitions go through here
if ((order.state === ORDER_STATES.ACTIVE || order.state === ORDER_STATES.PARTIAL) && !order.orderId) {
logger.log(
`ILLEGAL STATE: Refusing to set order ${id} to ${order.state} without orderId. ` +
`Context: ${context}. This would create a phantom order that doubles fund tracking. ` +
`Downgrading to VIRTUAL instead.`,
'error'
);
order.state = ORDER_STATES.VIRTUAL; // Auto-correct
}Why It Works:
- Every order state change must call
_updateOrder()(enforced throughout codebase) - Cannot be bypassed - direct state assignments are not used for order state
- Applies to ALL modules: grid, sync, strategy, dexbot_class
- Auto-correction with logging provides audit trail
Before (Vulnerable):
manager._updateOrder({ ...order, size: newSize, state: ORDER_STATES.ACTIVE }, 'grid-resize', ...);After (Safe):
manager._updateOrder({ ...order, size: newSize, state: order.state }, 'grid-resize', ...);Why It Matters: Preserves order's current state instead of forcing ACTIVE, preventing VIRTUAL → ACTIVE phantom creation during grid resizing.
Phantom Detection & Prevention:
// If order has no ID OR its ID is not on chain, it's a phantom/filled order
if (!currentGridOrder?.orderId || !parsedChainOrders.has(currentGridOrder.orderId)) {
const spreadOrder = convertToSpreadPlaceholder(currentGridOrder);
mgr._updateOrder(spreadOrder, 'sync-cleanup-phantom', ...);
// CRITICAL: Only trigger fill processing for GENUINE fills (had orderId)
// Phantoms (never had orderId) should NOT trigger rotations/rebalancing
if (currentGridOrder?.orderId) {
filledOrders.push({ ...currentGridOrder });
}
}Why It Matters:
- Detects phantoms on every sync
- Converts to SPREAD placeholders (releases locked funds)
- Prevents phantom fills from triggering unwarranted rotations/rebalancing
Strategy Module (strategy.js:484, 521):
// Only upgrade to ACTIVE if order has valid orderId
const newState = partial.orderId ? ORDER_STATES.ACTIVE : ORDER_STATES.VIRTUAL;Fallback Placements (dexbot_class.js:982):
const fallbackPlacements = unmetRotations.map(r => ({
id: r.newGridId,
price: r.newPrice,
size: r.newSize,
type: r.type,
state: ORDER_STATES.VIRTUAL // Start VIRTUAL, become ACTIVE after blockchain confirmation
}));See tests/repro_phantom_orders.js for comprehensive test coverage:
- Direct phantom creation attempt (blocked)
- Grid resize phantom prevention (verified)
- Sync cleanup of orphaned ACTIVE orders (verified)
- Valid ACTIVE order preservation (verified)
The COW architecture is the single standard for grid mutations. All new rebalancing code must follow these rules.
The master grid (
manager.orders) is read-only for strategy code. Only_applyOrderUpdate()(internal) and_commitWorkingGrid()(COW commit) may replace it.
// 1. Entry: performSafeRebalance() creates WorkingGrid automatically
// → calls _applySafeRebalanceCOW(fills, excludeIds)
// 2. Planning: Work on the working copy ONLY
const workingGrid = new WorkingGrid(manager.orders, { baseVersion: manager._gridVersion });
workingGrid.set(orderId, updatedOrder); // OK - modifies clone, not master
// 3. Broadcast: _updateOrdersOnChainBatchCOW() sends ops to chain
// → On success: _commitWorkingGrid() atomically replaces master
// → On failure: workingGrid discarded (master unchanged)// ❌ WRONG: Direct master mutation
manager.orders.set('buy-5', order); // Throws (frozen Map)
manager.orders.get('buy-5').size = 100; // Throws (deepFrozen object)
// ❌ WRONG: Bypassing _applyOrderUpdate for blockchain-confirmed changes
manager.orders = new Map([...manager.orders]); // Breaks version + index tracking
// ✅ CORRECT: State changes for confirmed blockchain events
manager._applyOrderUpdate(order, 'my-context'); // Updates master + increments version
// ✅ CORRECT: Planning changes during rebalance
workingGrid.set(orderId, newOrderState); // Safe - working copy only| COW State | Fill Arrives | Action |
|---|---|---|
NORMAL |
Individual fill | Process immediately, update master |
REBALANCING |
Individual fill | Update master + workingGrid.syncFromMaster(master, id) |
BROADCASTING |
Individual fill | Update master + mark working grid stale; commit guard will reject |
REBALANCING/BROADCASTING |
Full side update | Block until fills = 0 or abort |
For full details see COPY_ON_WRITE_MASTER_PLAN.md.
Location: modules/order/utils/order.js
Purpose: Single source of truth for order state logic, replacing 34+ inline checks scattered across the codebase.
Benefit: Semantic function names, centralized phantom detection, consistent patterns across all modules.
// Check if order exists on blockchain
// Returns true for ACTIVE or PARTIAL orders
if (isOrderOnChain(order)) {
// Order has presence on blockchain
}// Check if order is planned but not yet placed
// Returns true for VIRTUAL orders
if (isOrderVirtual(order)) {
// Order is reserved capital but not on-chain yet
}// Check if order has a valid blockchain orderId
// Returns true if orderId is non-null and non-empty
if (hasOnChainId(order)) {
// Order has been successfully placed and confirmed
}// Check if order is safely placed (on-chain with ID)
// Combines: state === ACTIVE/PARTIAL AND orderId exists
if (isOrderPlaced(order)) {
// Safe to use in calculations requiring blockchain confirmation
}// Detect phantom orders (on-chain state without ID - error state)
// Returns true for ACTIVE/PARTIAL orders WITHOUT orderId
if (isPhantomOrder(order)) {
// ERROR: This order shouldn't exist - fund tracking is corrupt
logger.error(`Phantom order detected: ${order.id}`);
}// Check if slot can be reused (VIRTUAL + no ID)
// Returns true for VIRTUAL orders without orderId
if (isSlotAvailable(order)) {
// Can overwrite this slot with a new order
}// Transition order to VIRTUAL state and clear blockchain metadata
// Safely clears orderId, filledSize, and other blockchain-specific fields
const virtualizedOrder = virtualizeOrder(order);
// Result: {
// ...order,
// state: ORDER_STATES.VIRTUAL,
// orderId: null,
// filledSize: 0
// }// Comprehensive validation: size > 0 AND not dust-threshold
// Prevents undersized orders that cause blockchain failures
const minHealthySize = getMinOrderSize(ORDER_TYPES.BUY, assets, 1.0);
if (isOrderHealthy(order, minHealthySize)) {
// Order is valid for placement/rotation
} else {
// Order is dust - consolidate or skip
}// Segregate partial orders by type efficiently
// Returns: { buy: [partial1, partial2], sell: [partial3] }
const { buy: buyPartials, sell: sellPartials } = getPartialsByType(orders);
// Use case: Consolidate dust partials per side
for (const partial of buyPartials) {
if (isDust(partial)) {
scheduleConsolidation(partial);
}
}Eliminates duplications in:
strategy.js::_getPartialOrdersByType()grid.js::compareGrids()startup_reconcile.js::selectPartialSlots()
// Validate both asset precisions simultaneously
// Checks: precision >= 0 AND precision <= MAX_PRECISION
const { buy, sell } = validateAssetPrecisions({
buy: assetB.precision,
sell: assetA.precision
});
if (!buy.valid || !sell.valid) {
throw new Error(`Invalid precisions: ${buy} / ${sell}`);
}// Calculate float comparison tolerance for given precision
// Returns: 10^(-precision) * factor (typically factor = 0.001 = 0.1%)
const slack = getPrecisionSlack(5, 0.001); // Returns 0.00001 * 0.001 = 0.00000001
// Use case: Floating-point safe comparisons
if (Math.abs(order.size - expected) <= slack) {
// Sizes match within precision tolerance
}Eliminates duplications in:
accounting.js::recalculateFunds()manager.js::_updateOrder()
Pattern 1: Type-safe Order Placement
// Only place orders that are truly safe
const ordersToPlace = orders.filter(o =>
isOrderHealthy(o, minSize) && !isOrderPlaced(o)
);Pattern 2: Phantom Detection During Sync
// Detect and cleanup phantoms during blockchain sync
for (const order of allOrders) {
if (isPhantomOrder(order)) {
const placeholder = convertToSpreadPlaceholder(order);
mgr._updateOrder(placeholder, 'sync-cleanup-phantom', { skipAccounting: false, fee: 0 });
}
}Pattern 3: Reusable Slot Identification
// Find slots available for overwriting
const reusableSlots = grid.filter(isSlotAvailable);
for (const slot of reusableSlots) {
slot = createNewOrder(slot.index, newPrice);
}Pattern 4: Side-Segregated Rebalancing
// Rebalance each side separately based on fill type
const { buy: buyPartials, sell: sellPartials } = getPartialsByType(orders);
rebalanceBuySide(buyPartials);
rebalanceSellSide(sellPartials);Role: Fund tracking and fee management
Key Responsibilities:
- Calculate available funds for each side
- Verify fund invariants
- Manage BTS transaction fees
- Atomic fund deduction
Critical Methods:
// Master fund calculation - called after every state change
recalculateFunds()
// Atomic fund operations
tryDeductFromChainFree(orderType, size, operation)
addToChainFree(orderType, size, operation)
// Fee management
deductBtsFees(requestedSide)
// Safety checks
_verifyFundInvariants(mgr, chainFreeBuy, chainFreeSell, chainBuy, chainSell)Fund Calculation Flow:
// 1. Reset all fund pools
resetFunds()
// 2. Iterate all orders and accumulate
for (const order of orders) {
if (order.state === VIRTUAL) {
funds.virtual[side] += order.size
} else if (order.state === ACTIVE || order.state === PARTIAL) {
funds.committed.grid[side] += order.size
if (order.orderId) {
funds.committed.chain[side] += order.size
}
}
}
// 3. Calculate available
funds.available[side] = max(0,
chainFree - virtual - btsFeesOwed - btsFeesReservation
)
// 4. Verify invariants
_verifyFundInvariants(...)Role: Grid rebalancing and order rotation
Key Responsibilities:
- Process filled orders
- Identify shortages and surpluses
- Execute order rotations
- Handle partial order consolidation
Critical Methods:
// Main entry point for rebalancing
rebalance(fills, excludeIds)
// Core rebalancing algorithm
rebalanceSideRobust(type, allSlots, sideSlots, direction, budget, available, excludeIds, reactionCap)
// Fill processing
processFilledOrders(filledOrders, excludeOrderIds)
// Partial order movement
preparePartialOrderMove(partialOrder, gridSlotsToMove, reservedGridIds)
completePartialOrderMove(moveInfo)Rebalancing Algorithm:
// 1. Identify shortages (empty slots in active window)
const shortages = sideSlots
.filter(slot => slot.state === VIRTUAL)
.slice(0, targetActiveCount);
// 2. Identify surpluses (orders outside active window)
const hardSurpluses = activeOrders.slice(targetActiveCount);
const crawlCandidates = activeOrders.slice(0, targetActiveCount);
// 3. For each shortage, find rotation candidate
for (const shortage of shortages) {
const candidate = findFurthestOrder(crawlCandidates);
if (shortage.price is better than candidate.price) {
// Rotate: cancel candidate, place at shortage
rotateOrder(candidate, shortage);
}
}
// 4. Apply Global Side Capping if needed
if (totalIncrease > availablePool) {
const scaleFactor = availablePool / totalIncrease;
for (const order of orders) {
order.size *= scaleFactor;
}
}Role: Grid creation, sizing, and divergence detection
Key Responsibilities:
- Create geometric price grids
- Calculate spread gap size
- Detect grid divergence
- Update order sizes from blockchain
Critical Methods:
// Create initial grid
createOrderGrid(config)
// Initialize with blockchain balances
initializeGrid(manager)
// Detect divergence and trigger updates
checkAndUpdateGridIfNeeded(manager)
// Compare ideal vs. persisted grid
compareGrids(calculatedGrid, persistedGrid, manager)Grid Creation Flow:
// 1. Calculate spread gap size
const stepFactor = 1 + (incrementPercent / 100);
const minSteps = MIN_SPREAD_FACTOR; // 2
const targetSteps = ceil(ln(1 + targetSpread/100) / ln(stepFactor));
const gapSlots = max(minSteps, targetSteps - 1); // Account for naturally occurring center gap
// 2. Generate price levels
const prices = [];
let price = startPrice;
for (let i = 0; i < totalLevels; i++) {
prices.push(price);
price *= stepFactor;
}
// 3. Assign roles based on boundary
for (let i = 0; i < prices.length; i++) {
if (i <= boundaryIdx) {
type = BUY;
} else if (i <= boundaryIdx + gapSlots) {
type = SPREAD;
} else {
type = SELL;
}
}Role: Blockchain synchronization and fill detection
Key Responsibilities:
- Sync grid state with blockchain
- Detect filled orders
- Update account balances
- Match chain orders to grid orders
Critical Methods:
// Main sync entry point
synchronizeWithChain(data, source)
// Sync from open orders
syncFromOpenOrders(openOrders, syncInfo)
// Sync from fill history
syncFromFillHistory(operation)
// Fetch account balances
fetchAccountBalancesAndSetTotals()The bot startup has been consolidated into a shared sequence used by all entry points (start(), startWithPrivateKey(), CLI, PM2). This ensures identical behavior and eliminates maintenance burden from duplicate code.
// Phase 1: Initialize state
_initializeStartupState()
├─ Verify account configuration
└─ Load existing or generate new grid state
// Phase 2: Set up account context
_setupAccountContext()
├─ Resolve account ID
├─ Load fund balances
└─ Initialize AccountOrders subscription
// Phase 3: Create order manager
// (OrderManager spawned with initial state)
// Phase 4: Run grid maintenance
_runGridMaintenance()
├─ Acquire _fillProcessingLock
└─ Execute maintenance logic:
├─ Threshold check (cache ratio)
├─ Divergence check (if threshold fails)
├─ Spread check (out-of-spread recovery)
└─ Health check (invariant verification)
// Phase 5: Finish startup
_finishStartupSequence()
├─ Mark bootstrap complete
├─ Begin fill processing
└─ Start periodic maintenance timerCritical Rule: Always acquire locks in canonical order
_fillProcessingLock → _divergenceLock
Why This Order?
- Fill processing is the most frequent operation (high contention)
- Grid maintenance is less frequent but synchronous
- By acquiring fill lock first, we ensure fills can't be blocked by slower divergence checks
- Reverse order (divergence first) would create deadlock when fills arrive during maintenance
Example: Safe Pattern
// ✅ CORRECT: Fill lock acquired, then divergence lock
async processFill(fill) {
await this._fillProcessingLock.acquire();
try {
// Do fill processing...
// If divergence check needed:
await this._divergenceLock.acquire();
try {
// Check divergence...
} finally {
this._divergenceLock.release();
}
} finally {
this._fillProcessingLock.release();
}
}
// ❌ WRONG: Would deadlock if fill arrives during divergence check
async checkDivergence() {
await this._divergenceLock.acquire(); // This blocks fills!
try {
// ...
}
}Lock Scope in Startup
The startup sequence extends lock scope to ensure atomic operations:
async _runGridMaintenance(fillLockAlreadyHeld = false) {
const lockHeld = fillLockAlreadyHeld || await this._fillProcessingLock.acquire();
try {
// All maintenance operations run atomically
// Fills cannot arrive mid-startup
} finally {
if (!fillLockAlreadyHeld) {
this._fillProcessingLock.release();
}
}
}Bootstrap Flag Safety
The isBootstrapping flag is guaranteed to be cleared using try-finally:
async start() {
this.isBootstrapping = true;
try {
// All startup phases...
} finally {
this.isBootstrapping = false; // Always cleared, even on error
}
}All new orders pass through two validation gates:
-
Strategy/Grid Logic (
strategy.js,grid.js):- Check:
size >= getMinOrderSize(type, assets, factor) - Double-dust threshold:
size >= minHealthySize - Prevents undersized placement attempts
- Check:
-
Broadcast Validation (
dexbot_class.js):- Check:
amount > 0for each order - Rejects zero-amount operations before blockchain transmission
- Triggers recovery sync on validation failure
- Check:
Recovery from Failed Batches
If a batch broadcast fails, the bot performs recovery:
try {
await broadcastBatch(orders); // Broadcasting
} catch (error) {
// Fresh balance fetch resets optimistic drift
await this.manager.fetchAccountTotals(this.accountId);
// Full sync aligns grid with blockchain reality
const openOrders = await chainOrders.readOpenOrders(this.accountId);
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true });
}The bot's operational parameters are defined in profiles/bots.json. While most settings are loaded at startup, the system is designed to pick up manual changes to critical valuation parameters during its runtime.
The startPrice is the anchor for valuation (calculating the relative value of Asset A and Asset B). It can be configured in three ways:
-
Fixed Numeric Price (e.g.,
105.5):- The bot treats this as a fixed anchor.
- Automatic price derivation from the market is disabled.
- Used as the base for all grid math during a Grid Reset.
-
"pool":
- The bot fetches the current BitShares Liquidity Pool price.
- Updated periodically every 4 hours.
-
"market":
- The bot derives the price from the current orderbook.
- Updated periodically every 4 hours.
When you set a numeric value like "startPrice": 105.5 in bots.json:
Behavior:
- ✅ Auto-refresh is DISABLED - numeric value is treated as absolute anchor
- ✅ Grid valuation uses this fixed price for all calculations
- ✅ Grid remains stable during market moves (fund-driven rebalancing only)
- ❌ Auto-derivation never happens - market/pool price is ignored
Use Case: You want absolute control over grid positioning regardless of current market conditions.
The bot performs a Periodic Configuration Refresh (every 4 hours by default).
For Dynamic Pricing (startPrice: "market" or startPrice: "pool"):
- Valuation Update: Fetches latest market/pool price and updates grid anchor
- Grid Reposition: Subsequent grid resets use updated valuation
- Operational Stability: During normal operation, the bot remains "fund-driven" and doesn't move orders on-chain
For Numeric Pricing (startPrice: 105.5):
-
No Changes: Fixed value is never updated
-
Valuation locked: All calculations use the configured numeric value
-
Manual Override Required: To change numeric anchor, edit
bots.jsonand use File Trigger -
Applying Changes: To force the bot to move orders to a new
startPriceimmediately, you must use the File Trigger (recalculate.{botKey}.trigger) to perform a full grid reset.
1. Update Constants
// modules/constants.js
ORDER_TYPES: {
BUY: 'buy',
SELL: 'sell',
SPREAD: 'spread',
LIMIT: 'limit' // NEW
}2. Update Manager Indices
// modules/order/manager.js - constructor
this._ordersByType = {
[ORDER_TYPES.BUY]: new Set(),
[ORDER_TYPES.SELL]: new Set(),
[ORDER_TYPES.SPREAD]: new Set(),
[ORDER_TYPES.LIMIT]: new Set() // NEW
};3. Update Fund Calculation
// modules/order/accounting.js - recalculateFunds()
for (const order of orders.values()) {
if (order.type === ORDER_TYPES.LIMIT) {
// Handle new type
funds.committed.grid[order.side] += order.size;
}
}4. Update Strategy Logic
// modules/order/strategy.js - rebalance()
const limitOrders = manager.getOrdersByTypeAndState(ORDER_TYPES.LIMIT, null);
// Process limit orders...5. Add Tests
// tests/test_manager.js
describe('LIMIT order type', () => {
it('should track LIMIT orders in indices', () => {
// Test implementation
});
});6. Update Documentation
// docs/architecture.md
### Order Types
- **LIMIT**: User-defined limit orders outside the grid7. Ensure COW Compliance
- Any new strategy or rebalancing logic must operate on a
WorkingGrid, notmanager.ordersdirectly. - New blockchain-confirmed state updates must go through
_applyOrderUpdate(). - If a new operation can race with fills, verify the rebalance state (
_setRebalanceState) gates it correctly. - Add tests for master-unchanged-on-failure and commit-only-on-success scenarios.
Located in tests/ (flat directory, no subdirectories):
test_accounting_logic.js- Fund calculation and accounting teststest_grid_logic.js- Grid creation, sizing, and divergence teststest_manager.js/test_manager_logic.js- State management and COW teststest_sync_logic.js- Blockchain synchronization teststest_strategy_logic.js- Rebalancing and rotation logictest_bts_fee_logic.js- BTS fee deduction and settlement
Run tests:
npm test1. Fund Invariant Check
// After any operation
manager.accountant._verifyFundInvariants(
manager,
chainFreeBuy,
chainFreeSell,
chainBuy,
chainSell
);2. Index Consistency Check
// Periodically
const isValid = manager.validateIndices();
if (!isValid) {
manager._repairIndices();
}3. Grid Diagnostics
// After fills or rotations
manager.logger.logGridDiagnostics(manager, 'AFTER FILL');// ✅ CORRECT - Planning on working copy (during rebalance)
workingGrid.set(orderId, { ...order, size: newSize });
// ✅ CORRECT - Confirmed blockchain event goes through _updateOrder()
manager._updateOrder({ ...order, state: ORDER_STATES.ACTIVE }, 'placed');
// ❌ WRONG - Direct master mutation (throws: Map is frozen)
manager.orders.set(orderId, order);// ✅ CORRECT - Uses proper signature with context
manager._updateOrder({
id: 'buy-5',
state: ORDER_STATES.ACTIVE,
type: ORDER_TYPES.BUY,
price: 0.5,
size: 100
}, 'order-update', { skipAccounting: false, fee: 0 });
// ✅ ALSO CORRECT - Using defaults (context='updateOrder', options={})
manager._updateOrder({
id: 'buy-5',
state: ORDER_STATES.ACTIVE,
type: ORDER_TYPES.BUY,
price: 0.5,
size: 100
});
// ❌ WRONG - Breaks indices
manager.orders.set('buy-5', order);// ✅ CORRECT - Recalculates once, with context for logging
manager.pauseFundRecalc();
for (const order of orders) {
manager._updateOrder(order, 'rebalance-batch', { skipAccounting: false, fee: 0 });
}
manager.resumeFundRecalc();
// ⚠️ ACCEPTABLE - Uses defaults but less ideal for debugging
manager.pauseFundRecalc();
for (const order of orders) {
manager._updateOrder(order);
}
manager.resumeFundRecalc();
// ❌ WRONG - Recalculates N times
for (const order of orders) {
manager._updateOrder(order); // Triggers recalc each time
}// ✅ CORRECT
manager.lockOrders([orderId]);
try {
await chainOperation();
} finally {
manager.unlockOrders([orderId]);
}
// ❌ WRONG - Race condition possible
await chainOperation();// ✅ CORRECT
if (manager.accountant.tryDeductFromChainFree(type, size)) {
// Funds deducted atomically
placeOrder();
} else {
// Insufficient funds
}
// ❌ WRONG - Race condition
if (manager.funds.available[type] >= size) {
manager.funds.available[type] -= size; // Not atomic!
placeOrder();
}// ✅ FAST - O(1) lookup
const activeBuys = manager.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.ACTIVE);
// ❌ SLOW - O(n) iteration
const activeBuys = Array.from(manager.orders.values())
.filter(o => o.type === ORDER_TYPES.BUY && o.state === ORDER_STATES.ACTIVE);// ✅ EFFICIENT - One recalc
manager.pauseFundRecalc();
// ... many updates ...
manager.resumeFundRecalc();
// ❌ INEFFICIENT - N recalcs
// ... many updates without pausing ...// ✅ GOOD - Uses fee cache (no amount = returns fee info object)
const feeInfo = getAssetFees('BTS');
console.log(feeInfo.createFee, feeInfo.updateFee);
// ✅ GOOD - With amount = returns net proceeds (number)
const netProceeds = getAssetFees('IOB.XRP', 100);
// ❌ BAD - Fetches every time
const fees = await BitShares.db.get_global_properties();When deriving pool prices, the bot caches Liquidity Pool IDs to avoid repeated blockchain scans.
// ✅ GOOD - Uses cached pool IDs
const price = await derivePoolPrice(assetA.symbol, assetB.symbol);
// First call: scans blockchain for pool
// Subsequent calls: uses cached ID if assets match
// Smart fallback: Cache miss triggers fresh scan
if (cachedPoolId.assets !== requestedAssets) {
// Cache invalidated - rescan blockchain
const newPoolId = await scanBlockchainForPool(assetA, assetB);
}
// ✅ Transparent: No manual cache management neededHow It Works:
- Cache validated against requested assets before use
- Stale pool IDs automatically detected and refreshed
- Concurrent access safe via lock protection
Performance Impact:
- Eliminates redundant blockchain scans during startup and config refresh
- Particularly effective during periodic 4-hour price refresh cycles
// ❌ BAD - Lock never released if error
manager.lockOrders([id]);
await operation(); // Might throw
manager.unlockOrders([id]); // Never reached
// ✅ GOOD - Always unlocks
manager.lockOrders([id]);
try {
await operation();
} finally {
manager.unlockOrders([id]);
}// ❌ BAD - Breaks indices
manager.orders.set(id, order);
// ✅ GOOD - Updates indices
manager._updateOrder(order);// ❌ BAD - Invalid transition
order.state = ORDER_STATES.VIRTUAL; // Was PARTIAL
manager._updateOrder(order); // Error logged
// ✅ GOOD - Valid transition
order.state = ORDER_STATES.ACTIVE; // PARTIAL → ACTIVE is valid
manager._updateOrder(order);// ❌ BAD - Might process locked order
processOrder(order);
// ✅ GOOD - Skip if locked
if (!manager.isOrderLocked(order.id)) {
processOrder(order);
}manager.logger.level = 'debug';manager.logger.logFundsStatus(manager, 'CONTEXT');manager.logger.logGridDiagnostics(manager, 'CONTEXT');console.log(manager.getMetrics());const isValid = manager.validateIndices();const order = manager.orders.get('buy-5');
console.log('State:', order.state);
console.log('Type:', order.type);
console.log('Locked?', manager.isOrderLocked(order.id));- Architecture - System design and module relationships
- Copy-on-Write Master Plan - COW architecture, phases, state machine, and test results
- Fund Movement Logic - Algorithms and formulas
- README - User documentation
- WORKFLOW - Git branch workflow
dexbot.js- CLI entry pointmodules/dexbot_class.js- Core bot classmodules/order/manager.js- Order management hub
modules/order/accounting.js- Fund trackingmodules/order/strategy.js- Rebalancing logicmodules/order/grid.js- Grid creationmodules/order/sync_engine.js- Blockchain syncmodules/order/working_grid.js- COW working copy (clone/delta/commit)modules/order/utils/order.js- Order state predicates and helpersmodules/order/utils/math.js- Precision, quantization, fund mathmodules/order/utils/validate.js- Validation and COW action buildingmodules/order/utils/system.js- Price derivation, deduplication
Fund calculations are critical to system stability. This section covers how the test suite validates fund logic and how to add tests for new features.
The test suite validates the following fund-related behaviors:
Order State Transitions:
✓ VIRTUAL → ACTIVE → PARTIAL lifecycle
✓ Fund movement between pools during transitions
✓ Index consistency during state changes
✓ Invariant preservation during transitionsFund Pool Integrity:
✓ virtual.buy + virtual.sell = sum of all VIRTUAL orders
✓ committed.chain.buy = sum of ACTIVE orders with orderId
✓ committed.grid.buy = sum of ACTIVE + PARTIAL orders
✓ available.buy = max(0, chainFree - virtual - cache - fees)Critical Invariants:
✓ Invariant 1: chainTotal = chainFree + chainCommitted
✓ Invariant 2: available ≤ chainFree
✓ Invariant 3: gridCommitted ≤ chainTotalEdge Cases:
✓ Zero-size orders
✓ Very large orders (precision handling)
✓ Multiple concurrent state changes (atomicity)
✓ Fund deductions and additions# All tests (native assert)
npm test
# Specific logic area
node tests/test_accounting_logic.js
# Specific integration test
node tests/test_fills.jsTests use a consistent pattern for fund validation:
describe('Fund Tracking - Fund Updates', () => {
let manager;
beforeEach(() => {
// Setup manager with known initial state
manager = new OrderManager(config);
manager.setAccountTotals({
buy: 10000,
sell: 100
});
manager.resetFunds();
});
it('should calculate virtual funds from VIRTUAL orders', () => {
// Add VIRTUAL order
manager._updateOrder({
id: 'virtual-1',
state: ORDER_STATES.VIRTUAL,
type: ORDER_TYPES.BUY,
size: 500
}, 'test-virtual', false, 0);
// Assert fund pool updated
expect(manager.funds.virtual.buy).toBe(500);
expect(manager.funds.total.grid.buy).toBeGreaterThanOrEqual(500);
});
});| File | Purpose | Test Count |
|---|---|---|
tests/test_strategy_logic.js |
Rebalancing, placement, rotation | 16 |
tests/test_accounting_logic.js |
Fund tracking, fees, precision | 10 |
tests/test_grid_logic.js |
Grid creation, sizing, divergence | 8 |
tests/test_manager_logic.js |
State machine, indexing | 8 |
tests/test_sync_logic.js |
Blockchain reconciliation | 6 |
When adding features that affect funds, follow this checklist:
1. Identify Fund Impact
// What fund pools are affected?
// - virtual (VIRTUAL orders)
// - committed.chain (ACTIVE orders with orderId)
// - committed.grid (ACTIVE + PARTIAL orders)
// - available (available pool)
// - available (spending power)2. Create Test Case
it('should [action] and update [fund pool]', () => {
// Setup
const initialFunds = manager.funds[poolName][side];
// Action
performAction();
// Assert
const finalFunds = manager.funds[poolName][side];
expect(finalFunds).toBe(expectedValue);
expect(manager.validateIndices()).toBe(true); // Indices OK?
});3. Verify Invariants
// After your action, verify invariants
expect(
manager.funds.total.chain.buy ===
manager.funds.total.chain.buy + manager.funds.committed.chain.buy
).toBe(true);4. Test Edge Cases
// Test with:
✓ Zero funds available
✓ Very large orders (precision)
✓ Multiple concurrent updates
✓ State transitionsPattern 1: Batch Fund Updates
manager.pauseFundRecalc(); // Batch mode
manager._updateOrder(order1, 'test-batch', { skipAccounting: false, fee: 0 });
manager._updateOrder(order2, 'test-batch', { skipAccounting: false, fee: 0 });
manager._updateOrder(order3, 'test-batch', { skipAccounting: false, fee: 0 });
manager.resumeFundRecalc(); // Recalc once
// Verify final state
expect(manager.funds.total.grid.buy).toBe(order1.size + order2.size + order3.size);Pattern 2: Fund Transitions
// VIRTUAL → ACTIVE
manager._updateOrder({
id: 'order-1',
state: ORDER_STATES.VIRTUAL,
size: 500
}, 'test-setup', false, 0);
const virtualBefore = manager.funds.virtual.buy;
manager._updateOrder({
id: 'order-1',
state: ORDER_STATES.ACTIVE,
orderId: 'chain-001',
size: 500
}, 'test-transition', false, 0);
// Verify movement
expect(manager.funds.virtual.buy).toBeLessThan(virtualBefore);
expect(manager.funds.committed.chain.buy).toBeGreaterThan(0);Pattern 3: Atomicity Check
// Verify operation is atomic (no partial state)
manager.lockOrders(['order-1']);
try {
// Perform operation
await fundDependentOperation();
// Check state consistency
expect(manager.validateIndices()).toBe(true);
} finally {
manager.unlockOrders(['order-1']);
}The test suite provides comprehensive coverage of fund calculations and rebalancing logic:
Key Areas Tested:
- ✅ VIRTUAL order placement with zero available pool
- ✅ PARTIAL order updates during rebalancing
- ✅ Grid divergence detection with stale cache
- ✅ BoundaryIdx persistence and recovery
- ✅ BUY side geometric weighting
- ✅ CacheFunds integration and deduction
- ✅ Rotation completion and skip prevention
- ✅ Fee calculation with isMaker parameter
- ✅ Market and blockchain taker fees
- ✅ Fund precision and delta validation
Running Tests:
# Test strategy rebalancing
node tests/test_strategy_logic.js
# Test grid divergence
node tests/test_grid_logic.js
# Test accounting precision
node tests/test_accounting_logic.js
# Run full suite
npm testSee TEST_UPDATES_SUMMARY.md for detailed coverage.
If a test fails due to fund calculation issues:
// 1. Print fund state
console.log('Fund state:', JSON.stringify(manager.funds, null, 2));
// 2. Check invariants
console.log('Invariants valid?', manager._verifyFundInvariants(
manager,
...values
));
// 3. Trace order state
manager.orders.forEach(order => {
console.log(`Order ${order.id}: state=${order.state}, size=${order.size}`);
});
// 4. Check index consistency
console.log('Indices valid?', manager.validateIndices());
// 5. Examine specific fund pool
console.log('Virtual buy:', manager.funds.virtual.buy);
console.log('Committed buy:', manager.funds.committed.grid.buy);
console.log('Available buy:', manager.funds.available.buy);Q: Where do I start reading the code?
A: Follow the Code Reading Roadmap above, starting with constants.js and manager.js.
Q: How do I debug fund issues?
A: Use manager.logger.logFundsStatus(manager) and check invariants with _verifyFundInvariants().
Q: Why is my order not rotating?
A: Check if it's locked (isOrderLocked()), in exclusion list, or below dust threshold.
Q: How do I add a new feature?
A: Follow the "How to Add New Features" section above.
Q: Where are the tests?
A: All tests are in the tests/ directory. Run npm test for the full suite, or node tests/<file>.js for individual test files.
test → dev → main
See WORKFLOW.md for detailed branching strategy.
- Run tests:
npm test - Verify fund invariants
- Check index consistency
- Update documentation
- Add inline comments for complex logic
- Read the Architecture: architecture.md
- Understand Fund Logic: FUND_MOVEMENT_AND_ACCOUNTING.md
- Follow Code Roadmap: Start with
constants.js→manager.js - Try Debugging: Enable debug logging and explore fund status
- Run Tests:
npm testto see how components work
Happy coding! 🚀