Skip to content

Commit 66ae47f

Browse files
authored
Merge pull request froooze#3 from froooze/dev
Fee Calculation and Startup and Order Update
2 parents b7b5e00 + 0cef3a9 commit 66ae47f

17 files changed

+1999
-275
lines changed

bot.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const chainKeys = require('./modules/chain_keys');
6262
const chainOrders = require('./modules/chain_orders');
6363
const { OrderManager, grid: Grid, utils: OrderUtils } = require('./modules/order');
6464
const { ORDER_STATES } = require('./modules/order/constants');
65+
const { attemptResumePersistedGridByPriceMatch, decideStartupGridAction } = require('./modules/order/startup_reconcile');
6566
const { AccountOrders, createBotKey } = require('./modules/account_orders');
6667
const accountBots = require('./modules/account_bots');
6768
const { parseJsonWithComments } = accountBots;
@@ -507,6 +508,14 @@ class DEXBot {
507508
this.manager.accountId = this.accountId;
508509
}
509510

511+
// Ensure fee cache is initialized before any fill processing that calls getAssetFees().
512+
// This must run after we have a BitShares connection and before we can process offline fills.
513+
try {
514+
await OrderUtils.initializeFeeCache([this.config || {}], BitShares);
515+
} catch (err) {
516+
console.warn(`[bot.js] Fee cache initialization failed: ${err.message}`);
517+
}
518+
510519
// Start listening for fills
511520
await chainOrders.listenForFills(this.account || undefined, async (fills) => {
512521
if (this.manager && !this.isResyncing && !this.config.dryRun) {
@@ -641,18 +650,36 @@ class DEXBot {
641650
});
642651

643652
const persistedGrid = accountOrders.loadBotGrid(this.config.botKey);
653+
// Use this.accountId which was set during initialize()
644654
const chainOpenOrders = this.config.dryRun ? [] : await chainOrders.readOpenOrders(this.accountId);
645655

656+
const debugStartup = process.env.DEBUG_STARTUP === '1';
657+
if (debugStartup) {
658+
console.log(`[bot.js] DEBUG STARTUP: chainOpenOrders.length = ${chainOpenOrders.length}`);
659+
console.log(`[bot.js] DEBUG STARTUP: persistedGrid.length = ${persistedGrid ? persistedGrid.length : 0}`);
660+
}
661+
646662
let shouldRegenerate = false;
647663
if (!persistedGrid || persistedGrid.length === 0) {
648664
shouldRegenerate = true;
649665
console.log('[bot.js] No persisted grid found. Generating new grid.');
650666
} else {
651667
await this.manager._initializeAssets();
652-
const chainOrderIds = new Set(chainOpenOrders.map(o => o.id));
653-
const hasActiveMatch = persistedGrid.some(order => order.state === 'active' && chainOrderIds.has(order.orderId));
654-
if (!hasActiveMatch) {
655-
shouldRegenerate = true;
668+
const decision = await decideStartupGridAction({
669+
persistedGrid,
670+
chainOpenOrders,
671+
manager: this.manager,
672+
logger: { log: (msg) => console.log(`[bot.js] ${msg}`) },
673+
storeGrid: (orders) => accountOrders.storeMasterGrid(this.config.botKey, orders),
674+
attemptResumeFn: attemptResumePersistedGridByPriceMatch,
675+
});
676+
shouldRegenerate = decision.shouldRegenerate;
677+
678+
if (debugStartup) {
679+
console.log(`[bot.js] DEBUG STARTUP: hasActiveMatch = ${decision.hasActiveMatch}`);
680+
}
681+
682+
if (shouldRegenerate && chainOpenOrders.length === 0) {
656683
console.log('[bot.js] Persisted grid found, but no matching active orders on-chain. Generating new grid.');
657684
}
658685
}

dexbot.js

Lines changed: 104 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const chainOrders = require('./modules/chain_orders');
2121
const chainKeys = require('./modules/chain_keys');
2222
const { OrderManager, grid: Grid, utils: OrderUtils } = require('./modules/order');
2323
const { ORDER_STATES } = require('./modules/order/constants');
24+
const { reconcileStartupOrders, attemptResumePersistedGridByPriceMatch, decideStartupGridAction } = require('./modules/order/startup_reconcile');
2425
const accountKeys = require('./modules/chain_keys');
2526
const accountBots = require('./modules/account_bots');
2627
const { parseJsonWithComments } = accountBots;
@@ -615,7 +616,6 @@ class DEXBot {
615616
return;
616617
}
617618
this._processingFill = true;
618-
619619
try {
620620
const allFills = [...fills, ...this._pendingFills];
621621
this._pendingFills = [];
@@ -697,27 +697,7 @@ class DEXBot {
697697
this.manager.logger.log(`Processing batch of ${validFills.length} fills using 'open' mode`, 'info');
698698
const chainOpenOrders = await chainOrders.readOpenOrders(this.account);
699699

700-
// We need to accumulate results.
701-
// Note: syncFromOpenOrders might return the SAME filled orders if we call it multiple times
702-
// without updating the grid state in between?
703-
// Actually, syncFromOpenOrders DOES update the grid (marking as filled).
704-
// So subsequent calls with same open orders list will work fine?
705-
// Yes, if Order A is missing, first call marks it filled. Second call Order A is filled?
706-
// Wait, syncFromOpenOrders iterates order grid and checks against chain.
707-
// If chainOpenOrders is constant, and grid is updated...
708-
// Re-running it is redundant but safe if implementation is idempotent-ish.
709-
// But why run it multiple times? Just run it ONCE without fillOp specific logging,
710-
// or run it once and pass the last fillOp?
711-
// The fillOp arg is only used for:
712-
// "Order ${gridOrder.id} ... matched fill ${fillOp.order_id}"
713-
// If we want detailed logs for each, we'd need to loop.
714-
// But fundamentally, we just need to know what's missing.
715-
716700
const result = this.manager.syncFromOpenOrders(chainOpenOrders, validFills[0].op[1]);
717-
// ^ This detects ALL missing orders at once.
718-
// If validFills has 5 fills, and open orders is missing 5 orders, this one call catches them all.
719-
// The only downside is the log message might only reference the first fill ID or be generic.
720-
721701
if (result.filledOrders) allFilledOrders.push(...result.filledOrders);
722702
if (result.ordersNeedingCorrection) ordersNeedingCorrection = result.ordersNeedingCorrection;
723703
}
@@ -778,8 +758,13 @@ class DEXBot {
778758
try {
779759
this.manager.logger.log('Grid regeneration triggered. Performing full grid resync...', 'info');
780760
const readFn = () => chainOrders.readOpenOrders(this.accountId);
781-
const cancelFn = (orderId) => chainOrders.cancelOrder(this.account, this.privateKey, orderId);
782-
await Grid.recalculateGrid(this.manager, readFn, cancelFn);
761+
await Grid.recalculateGrid(this.manager, {
762+
readOpenOrdersFn: readFn,
763+
chainOrders,
764+
account: this.account,
765+
privateKey: this.privateKey,
766+
config: this.config,
767+
});
783768
accountOrders.storeMasterGrid(this.config.botKey, Array.from(this.manager.orders.values()));
784769

785770
if (fs.existsSync(this.triggerFile)) {
@@ -805,39 +790,52 @@ class DEXBot {
805790
this.manager.logger.log('No persisted grid found. Generating new grid.', 'info');
806791
} else {
807792
await this.manager._initializeAssets();
808-
const chainOrderIds = new Set(chainOpenOrders.map(o => o.id));
809-
const hasActiveMatch = persistedGrid.some(order => order.state === 'active' && chainOrderIds.has(order.orderId));
810-
if (!hasActiveMatch) {
811-
shouldRegenerate = true;
793+
const decision = await decideStartupGridAction({
794+
persistedGrid,
795+
chainOpenOrders,
796+
manager: this.manager,
797+
logger: this.manager.logger,
798+
storeGrid: (orders) => accountOrders.storeMasterGrid(this.config.botKey, orders),
799+
attemptResumeFn: attemptResumePersistedGridByPriceMatch,
800+
});
801+
shouldRegenerate = decision.shouldRegenerate;
802+
803+
if (shouldRegenerate && chainOpenOrders.length === 0) {
812804
this.manager.logger.log('Persisted grid found, but no matching active orders on-chain. Generating new grid.', 'info');
813805
}
814806
}
815807

816808
if (shouldRegenerate) {
817-
// Cancel unmatched on-chain orders immediately (before fetching
818-
// any account totals). Use the persisted grid to determine which
819-
// on-chain orders are expected; any others will be cancelled.
820-
if (!this.config.dryRun) {
821-
try {
822-
const persistedIds = new Set((persistedGrid || []).map(o => o.orderId).filter(Boolean));
823-
if (Array.isArray(chainOpenOrders) && chainOpenOrders.length > 0) {
824-
for (const co of chainOpenOrders) {
825-
try {
826-
if (!persistedIds.has(co.id)) {
827-
this.manager && this.manager.logger && this.manager.logger.log && this.manager.logger.log(`Cancelling unmatched on-chain order ${co.id} before initializing grid.`, 'info');
828-
await chainOrders.cancelOrder(this.account, this.privateKey, co.id);
829-
}
830-
} catch (err) {
831-
this.manager && this.manager.logger && this.manager.logger.log && this.manager.logger.log(`Failed to cancel order ${co.id}: ${err && err.message ? err.message : err}`, 'error');
832-
}
833-
}
834-
}
835-
} catch (err) {
836-
this.manager && this.manager.logger && this.manager.logger.log && this.manager.logger.log(`Failed to cancel unmatched on-chain orders: ${err && err.message ? err.message : err}`, 'error');
837-
}
809+
// Initialize assets first
810+
await this.manager._initializeAssets();
811+
812+
// Always generate a full virtual grid so orders.json contains the complete grid
813+
// (virtual + spread placeholders), not only currently active on-chain orders.
814+
this.manager.logger.log('Generating new grid.', 'info');
815+
await Grid.initializeGrid(this.manager);
816+
817+
// If there are existing on-chain orders, sync them onto the new grid
818+
// using price+size matching, then reconcile counts by updating/cancelling/creating.
819+
if (Array.isArray(chainOpenOrders) && chainOpenOrders.length > 0) {
820+
this.manager.logger.log(`Found ${chainOpenOrders.length} existing chain orders. Syncing them onto the new grid (price+size matching).`, 'info');
821+
const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders');
822+
823+
await reconcileStartupOrders({
824+
manager: this.manager,
825+
config: this.config,
826+
account: this.account,
827+
privateKey: this.privateKey,
828+
chainOrders,
829+
chainOpenOrders,
830+
syncResult,
831+
});
832+
} else {
833+
// No existing orders: place initial orders on-chain
834+
this.manager.logger.log('No existing chain orders found. Placing initial orders.', 'info');
835+
await this.placeInitialOrders();
838836
}
839837

840-
await this.placeInitialOrders();
838+
accountOrders.storeMasterGrid(this.config.botKey, Array.from(this.manager.orders.values()));
841839
} else {
842840
this.manager.logger.log('Found active session. Loading and syncing existing grid.', 'info');
843841

@@ -892,6 +890,20 @@ class DEXBot {
892890
}
893891
}
894892

893+
// Reconcile existing on-chain orders to the configured target counts.
894+
// This logic is shared with the "regenerate" startup path and lives in one place.
895+
if (syncResult && (syncResult.unmatchedChainOrders || syncResult.unmatchedGridOrders)) {
896+
await reconcileStartupOrders({
897+
manager: this.manager,
898+
config: this.config,
899+
account: this.account,
900+
privateKey: this.privateKey,
901+
chainOrders,
902+
chainOpenOrders,
903+
syncResult,
904+
});
905+
}
906+
895907
// Correct any orders with price mismatches at startup
896908
if (syncResult.ordersNeedingCorrection && syncResult.ordersNeedingCorrection.length > 0) {
897909
this.manager.logger.log(`Startup: Correcting ${syncResult.ordersNeedingCorrection.length} order(s) with price mismatch...`, 'info');
@@ -1104,6 +1116,15 @@ async function runBotInstances(botEntries, { forceDryRun = false, sourceName = '
11041116
}
11051117
}
11061118

1119+
// Fee cache is required for fill processing (getAssetFees), including offline fill reconciliation at startup.
1120+
// Initialize it once per process for the assets used by active bots.
1121+
try {
1122+
await waitForConnected();
1123+
await OrderUtils.initializeFeeCache(prepared.filter(b => b.active), BitShares);
1124+
} catch (err) {
1125+
console.warn(`Fee cache initialization failed: ${err.message}`);
1126+
}
1127+
11071128
const instances = [];
11081129
for (const entry of prepared) {
11091130
if (!entry.active) {
@@ -1210,8 +1231,8 @@ async function stopBotByName(botName) {
12101231
* This method:
12111232
* 1. Generates a new order grid from current configuration
12121233
* 2. Persists the grid snapshot to profiles/orders.json
1213-
* 3. Starts the bot with the new grid
1214-
*
1234+
* 3. Starts the bot with the new grid (password authentication happens during startup)
1235+
*
12151236
* @param {string|null} botName - Name of the bot to restart, or null for all active
12161237
*/
12171238
async function restartBotByName(botName) {
@@ -1236,6 +1257,38 @@ async function restartBotByName(botName) {
12361257
try {
12371258
// Build an OrderManager with the bot config and initialize the order grid
12381259
const manager = new OrderManager(bot);
1260+
1261+
// Set account info on the manager so _fetchAccountBalancesAndSetTotals can work
1262+
if (bot.preferredAccount) {
1263+
manager.account = bot.preferredAccount;
1264+
// Try to look up the account ID from the blockchain
1265+
try {
1266+
const { BitShares } = require('./modules/bitshares_client');
1267+
const full = await BitShares.db.get_full_accounts([bot.preferredAccount], false);
1268+
if (full && full[0] && full[0][1] && full[0][1].account && full[0][1].account.id) {
1269+
manager.accountId = full[0][1].account.id;
1270+
} else if (full && full[0] && String(full[0][0]).startsWith('1.2.')) {
1271+
manager.accountId = full[0][0];
1272+
}
1273+
} catch (err) {
1274+
console.warn(`[dexbot restart] Could not look up account ID for '${bot.preferredAccount}': ${err.message}`);
1275+
}
1276+
}
1277+
1278+
// If botFunds are percentage-based and account info is available, try to
1279+
// fetch on-chain balances first so percentages resolve correctly.
1280+
try {
1281+
const botFunds = bot && bot.botFunds ? bot.botFunds : {};
1282+
const needsPercent = (v) => typeof v === 'string' && v.includes('%');
1283+
if ((needsPercent(botFunds.buy) || needsPercent(botFunds.sell)) && (manager.accountId || manager.account)) {
1284+
if (typeof manager._fetchAccountBalancesAndSetTotals === 'function') {
1285+
await manager._fetchAccountBalancesAndSetTotals();
1286+
}
1287+
}
1288+
} catch (errFetch) {
1289+
console.warn(`[dexbot restart] Could not fetch account totals before initializing grid for '${bot.name}': ${errFetch && errFetch.message ? errFetch.message : errFetch}`);
1290+
}
1291+
12391292
// Populate assets/marketPrice and compute the virtual grid (best-effort)
12401293
try {
12411294
await Grid.initializeGrid(manager);

0 commit comments

Comments
 (0)