dexbot 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +4 -3
- package/analysis/README.md +1 -2
- package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
- package/analysis/resolve_source.ts +9 -2
- package/analysis/tradingview/README.md +29 -11
- package/analysis/tradingview/analyze_tradingview.ts +92 -3
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
- package/analysis/trend_detection/package.json +1 -1
- package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
- package/claw/modules/dexbot_profiles.ts +22 -2
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/claw/tests/test_dexbot_profiles.ts +65 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +6 -2
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.js +3 -3
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/candle_utils.d.ts +6 -0
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +6 -0
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +101 -19
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +2 -0
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +31 -4
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +0 -6
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
- package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
- package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +10 -5
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
- package/dist/market_adapter/inputs/window_cache.js +500 -0
- package/dist/market_adapter/inputs/window_cache.js.map +1 -0
- package/dist/market_adapter/interval_utils.d.ts +8 -1
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +14 -1
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -0
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +21 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +25 -4
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +67 -4
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +3 -1
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +13 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +2 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +122 -28
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +59 -8
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +11 -0
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +68 -4
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +8 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +57 -2
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +157 -28
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +135 -5
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +554 -46
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +13 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +15 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +36 -7
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/fund_registry.d.ts +1 -1
- package/dist/modules/fund_registry.js +1 -1
- package/dist/modules/node_connect_policy.d.ts +52 -0
- package/dist/modules/node_connect_policy.d.ts.map +1 -0
- package/dist/modules/node_connect_policy.js +35 -0
- package/dist/modules/node_connect_policy.js.map +1 -0
- package/dist/modules/node_manager.d.ts +32 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +76 -9
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +30 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +176 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -0
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +143 -15
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +5 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +182 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +64 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +231 -14
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -0
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +79 -7
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +16 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +11 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +24 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +258 -13
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +662 -12
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +36 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +102 -6
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +6 -2
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +13 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +20 -0
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +16 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +31 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/pm2.js +7 -7
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +13 -3
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/dist/scripts/analyze-orders.js +2 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.js +0 -1
- package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/tv.d.ts +24 -2
- package/dist/scripts/tv.d.ts.map +1 -1
- package/dist/scripts/tv.js +190 -65
- package/dist/scripts/tv.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -2
- package/docs/COW_INVARIANTS.md +18 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +8 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
- package/docs/GRID_RECALCULATION.md +5 -6
- package/docs/GRID_RECONCILE.md +12 -2
- package/docs/MPA_CREDIT_USAGE.md +2 -2
- package/docs/README.md +5 -6
- package/docs/architecture.md +0 -58
- package/docs/developer_guide.md +2 -1
- package/docs/docker.md +1 -1
- package/market_adapter/README.md +4 -0
- package/modules/README.md +1 -1
- package/package.json +1 -1
- package/scripts/README.md +5 -3
- package/tests/README.md +2 -2
|
@@ -12,6 +12,7 @@ import { path } from './path_api.js';
|
|
|
12
12
|
import * as chainOrders from './chain_orders.js';
|
|
13
13
|
import * as grid from './order/grid.js';
|
|
14
14
|
import { ORDER_STATES, ORDER_TYPES, TIMING, BTS_PRECISION, NATIVE_CLIENT } from './constants.js';
|
|
15
|
+
import { BOT_LIVE_CONFIG_KEYS } from './runtime_settings.js';
|
|
15
16
|
import { acquireIfNotHeld } from './order/async_lock.js';
|
|
16
17
|
const { readOpenOrdersGuarded } = chainOrders;
|
|
17
18
|
import { PATHS } from './paths.js';
|
|
@@ -57,7 +58,12 @@ function updateDynamicGridSnapshotSync(...args) { return require('../market_adap
|
|
|
57
58
|
// would be circular; at call time both modules are fully loaded.
|
|
58
59
|
function scheduleFillConsumerRestartFn(...args) { return require('./dexbot_fill_runtime').scheduleFillConsumerRestart(...args); }
|
|
59
60
|
function reconcileGridOrders(...args) { return require('./order/grid_reconcile').reconcileGridOrders(...args); }
|
|
61
|
+
function resolveReserveCount(...args) { return require('./order/utils/order').resolveReserveCount(...args); }
|
|
62
|
+
function reserveEdgeIdSet(...args) { return require('./order/utils/order').reserveEdgeIdSet(...args); }
|
|
63
|
+
function liveWindowIdSet(...args) { return require('./order/utils/order').liveWindowIdSet(...args); }
|
|
64
|
+
function resolveLiveReserveEdgeAnchorPrice(...args) { return require('./order/utils/order').resolveLiveReserveEdgeAnchorPrice(...args); }
|
|
60
65
|
function formatUnmatchedChainOrder(...args) { return require('./order/utils/order').formatUnmatchedChainOrder(...args); }
|
|
66
|
+
function isNonBlockingUnmatchedOrder(...args) { return require('./order/utils/order').isNonBlockingUnmatchedOrder(...args); }
|
|
61
67
|
function getSideBudget(...args) { return require('./order/utils/order').getSideBudget(...args); }
|
|
62
68
|
function getActiveOrdersTotal(config) { return require('./order/utils/order').getActiveOrdersTotal(config); }
|
|
63
69
|
function correctAllPriceMismatches(...args) { return require('./order/utils/order').correctAllPriceMismatches(...args); }
|
|
@@ -147,6 +153,369 @@ function runtimeConfigNeedsMarketAdapter(snapshot, config) {
|
|
|
147
153
|
}
|
|
148
154
|
return usesAmaGridPrice(config);
|
|
149
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Live bot-config allowlist (BOT_LIVE_CONFIG_KEYS) is defined once in
|
|
158
|
+
* runtime_settings.ts (shared with the `dexbot bot` editor hint) and
|
|
159
|
+
* statically imported above. Everything outside it is hint-only:
|
|
160
|
+
* geometry needs `dexbot reset <bot>`, identity needs a restart.
|
|
161
|
+
*/
|
|
162
|
+
/**
|
|
163
|
+
* Top-level entry keys excluded from the live bot-config fingerprint.
|
|
164
|
+
* botKey/botIndex are runtime-assigned, accountId is auto-saved next to
|
|
165
|
+
* preferredAccount after the first chain resolution (not a user edit).
|
|
166
|
+
*/
|
|
167
|
+
const BOT_CONFIG_FINGERPRINT_IGNORED_KEYS = Object.freeze([
|
|
168
|
+
'botKey',
|
|
169
|
+
'botIndex',
|
|
170
|
+
'accountId',
|
|
171
|
+
]);
|
|
172
|
+
/**
|
|
173
|
+
* Deterministic JSON stringify with recursively sorted object keys.
|
|
174
|
+
* Key order in bots.json must not count as a config change.
|
|
175
|
+
* @param {any} value - Value to stringify
|
|
176
|
+
* @returns {string} Stable string representation
|
|
177
|
+
*/
|
|
178
|
+
function stableStringifyForBotConfig(value) {
|
|
179
|
+
if (value === null || value === undefined)
|
|
180
|
+
return 'null';
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return `[${value.map((v) => stableStringifyForBotConfig(v)).join(',')}]`;
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === 'object') {
|
|
185
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
|
|
186
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringifyForBotConfig(value[k])}`).join(',')}}`;
|
|
187
|
+
}
|
|
188
|
+
const serialized = JSON.stringify(value);
|
|
189
|
+
return serialized === undefined ? 'null' : serialized;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Normalize a raw bots.json entry for fingerprinting: drop volatile keys.
|
|
193
|
+
* @param {any} entry - Raw bot entry from bots.json
|
|
194
|
+
* @returns {any} Normalized plain object ({} when entry is missing)
|
|
195
|
+
*/
|
|
196
|
+
function normalizeBotEntryForFingerprint(entry) {
|
|
197
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
198
|
+
return {};
|
|
199
|
+
const out = {};
|
|
200
|
+
for (const key of Object.keys(entry)) {
|
|
201
|
+
if (BOT_CONFIG_FINGERPRINT_IGNORED_KEYS.includes(key))
|
|
202
|
+
continue;
|
|
203
|
+
out[key] = entry[key];
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build the full-entry fingerprint for one bot (stable across key order and
|
|
209
|
+
* comment/whitespace edits, since those never reach the parsed entry).
|
|
210
|
+
* @param {any} entry - Raw bot entry from bots.json
|
|
211
|
+
* @returns {string} Fingerprint ('' when entry is missing)
|
|
212
|
+
*/
|
|
213
|
+
function buildBotConfigFingerprint(entry) {
|
|
214
|
+
if (!entry || typeof entry !== 'object')
|
|
215
|
+
return '';
|
|
216
|
+
return stableStringifyForBotConfig(normalizeBotEntryForFingerprint(entry));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Plain-data deep clone (bots.json entries are JSON-parsed, always
|
|
220
|
+
* serializable; the fallback only guards exotic test doubles).
|
|
221
|
+
* Single helper replacing the previously triplicated inline clones.
|
|
222
|
+
* @param {any} value - Value to clone
|
|
223
|
+
* @returns {any} Deep clone (or the original when unserializable)
|
|
224
|
+
*/
|
|
225
|
+
function cloneJsonValue(value) {
|
|
226
|
+
if (value === undefined)
|
|
227
|
+
return undefined;
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(JSON.stringify(value));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Merge the given entry keys into the live runtime (bot.config +
|
|
237
|
+
* manager.config) and refresh the derived weight base when included.
|
|
238
|
+
* Shared by the live bot-config check (allowlisted keys) — the full
|
|
239
|
+
* grid-resync path uses replaceBotConfigFromEntryPreservingRuntime below.
|
|
240
|
+
* Synchronous in-memory merge only: no chain I/O, no fill-lock (assignments
|
|
241
|
+
* are atomic; maintenance / targeted-reconcile ticks heal shortfalls/excess).
|
|
242
|
+
* @param {any} bot - DEXBot instance
|
|
243
|
+
* @param {any} entry - Raw bot entry from bots.json
|
|
244
|
+
* @param {string[]} keys - Entry keys to merge
|
|
245
|
+
* @returns {string[]} Merged key names
|
|
246
|
+
*/
|
|
247
|
+
function mergeBotConfigKeysIntoRuntime(bot, entry, keys) {
|
|
248
|
+
const applied = [];
|
|
249
|
+
if (!bot?.config || !entry || typeof entry !== 'object')
|
|
250
|
+
return applied;
|
|
251
|
+
for (const key of keys) {
|
|
252
|
+
// A deleted key applies as null (mirrors the former ?? null diff
|
|
253
|
+
// semantics) so the running config can never keep a stale value.
|
|
254
|
+
// Cloned twice: bot.config and manager.config must never alias the
|
|
255
|
+
// same nested object, or a future in-place mutation of one would
|
|
256
|
+
// silently corrupt the other.
|
|
257
|
+
const cloned = cloneJsonValue(entry[key] ?? null);
|
|
258
|
+
bot.config[key] = cloned;
|
|
259
|
+
if (bot.manager?.config) {
|
|
260
|
+
bot.manager.config[key] = cloneJsonValue(entry[key] ?? null);
|
|
261
|
+
}
|
|
262
|
+
if (key === 'weightDistribution') {
|
|
263
|
+
bot._baseWeightDistribution = cloned && typeof cloned === 'object' ? { ...cloned } : cloned;
|
|
264
|
+
}
|
|
265
|
+
applied.push(key);
|
|
266
|
+
}
|
|
267
|
+
return applied;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Full config replace preserving runtime identity (botKey/botIndex) and the
|
|
271
|
+
* constructor-resolved runtime settings (timing, gridLimits, feeParams,
|
|
272
|
+
* etc.). Shared single implementation for performGridResync (previously an
|
|
273
|
+
* inline block duplicating the merge/weights/refresh steps).
|
|
274
|
+
* @param {any} bot - DEXBot instance
|
|
275
|
+
* @param {any} updatedBot - Raw bot entry from bots.json
|
|
276
|
+
* @param {string} contextLabel - Label for the weights-refresh log context
|
|
277
|
+
*/
|
|
278
|
+
function replaceBotConfigFromEntryPreservingRuntime(bot, updatedBot, contextLabel) {
|
|
279
|
+
const oldKey = bot.config.botKey;
|
|
280
|
+
const oldIndex = bot.config.botIndex;
|
|
281
|
+
// Preserve runtime-only properties set by the constructor
|
|
282
|
+
// (timing, gridLimits, feeParams, etc.) that are not present
|
|
283
|
+
// in the raw profile from bots.json.
|
|
284
|
+
const runtimeProps = {};
|
|
285
|
+
for (const key of getRuntimeSettingsKeys()) {
|
|
286
|
+
if (bot.config[key] !== undefined) {
|
|
287
|
+
runtimeProps[key] = bot.config[key];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
bot._log(`Reloaded configuration for bot '${bot.config.name}'`);
|
|
291
|
+
bot.config = { ...updatedBot, botKey: oldKey, botIndex: oldIndex, ...runtimeProps };
|
|
292
|
+
if (bot.manager?.config) {
|
|
293
|
+
bot.manager.config = { ...bot.manager.config, ...bot.config };
|
|
294
|
+
}
|
|
295
|
+
// bot.config is a new object now: re-point the credit runtime (its getter
|
|
296
|
+
// reads this.config) so the resync path can't leave it on the stale copy.
|
|
297
|
+
// The live-merge path mutates in place and never needs this.
|
|
298
|
+
if (bot._creditRuntime) {
|
|
299
|
+
bot._creditRuntime.config = bot.config;
|
|
300
|
+
}
|
|
301
|
+
bot._baseWeightDistribution = cloneWeightDistribution(updatedBot.weightDistribution, bot._baseWeightDistribution);
|
|
302
|
+
refreshDynamicWeightDistribution(bot, contextLabel);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Structural gate for live debtPolicy merges (mirrors the credit runtime's
|
|
306
|
+
* isEnabled gate plus the startup validator's required string fields in
|
|
307
|
+
* bot_settings.ts). Full numeric validation stays where it belongs:
|
|
308
|
+
* the `dexbot bot` editor at save time and per-offer at runtime.
|
|
309
|
+
* null/undefined (policy removal) is safe — it takes the disable path.
|
|
310
|
+
* @param {any} value - New debtPolicy value from bots.json
|
|
311
|
+
* @returns {boolean} True when safe to merge live
|
|
312
|
+
*/
|
|
313
|
+
function isDebtPolicyLiveApplySafe(value) {
|
|
314
|
+
if (value === null || value === undefined)
|
|
315
|
+
return true;
|
|
316
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
317
|
+
return false;
|
|
318
|
+
const lending = value.lending;
|
|
319
|
+
if (!Array.isArray(lending) || lending.length === 0)
|
|
320
|
+
return false;
|
|
321
|
+
return lending.every((item) => item && typeof item === 'object'
|
|
322
|
+
&& typeof item.type === 'string' && item.type.length > 0
|
|
323
|
+
&& typeof item.asset === 'string' && item.asset.length > 0
|
|
324
|
+
&& typeof item.collateralAsset === 'string' && item.collateralAsset.length > 0);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Reconcile the credit runtime after a live debtPolicy merge: (re)create +
|
|
328
|
+
* load state when newly enabled, rebind the watchdog interval to the
|
|
329
|
+
* current runtime, stop the interval when disabled. loadState is a no-op on
|
|
330
|
+
* an already-loaded runtime and both maintenance entry points bail while
|
|
331
|
+
* disabled or in-flight, so this is safe on any tick (bot-level seams
|
|
332
|
+
* guarded for stripped test doubles).
|
|
333
|
+
* @param {any} bot - DEXBot instance
|
|
334
|
+
*/
|
|
335
|
+
async function reconcileCreditRuntimeAfterPolicyChange(bot) {
|
|
336
|
+
try {
|
|
337
|
+
if (typeof bot._setupCreditRuntime === 'function') {
|
|
338
|
+
await bot._setupCreditRuntime();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
bot._warn?.(`Credit runtime setup failed after debtPolicy change: ${getErrorMessage(err)}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
if (bot._creditRuntime) {
|
|
347
|
+
if (typeof bot._setupCreditWatchdogInterval === 'function') {
|
|
348
|
+
bot._setupCreditWatchdogInterval();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (typeof bot._stopCreditWatchdogInterval === 'function') {
|
|
352
|
+
bot._stopCreditWatchdogInterval();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
bot._warn?.(`Credit watchdog reconcile failed after debtPolicy change: ${getErrorMessage(err)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Diff two normalized entries in a single pass over the key union.
|
|
361
|
+
* Replaces the former twin-loop diffLiveBotConfig +
|
|
362
|
+
* detectNonLiveBotConfigChanges pair.
|
|
363
|
+
* @param {any} oldNormalized - Previously seen normalized entry
|
|
364
|
+
* @param {any} newNormalized - New normalized entry
|
|
365
|
+
* @returns {{liveChanges: Array<{key: string, oldValue: any, newValue: any}>, otherKeys: string[]}} Split changes
|
|
366
|
+
*/
|
|
367
|
+
function diffBotConfigEntries(oldNormalized, newNormalized) {
|
|
368
|
+
const prev = oldNormalized && typeof oldNormalized === 'object' ? oldNormalized : {};
|
|
369
|
+
const next = newNormalized && typeof newNormalized === 'object' ? newNormalized : {};
|
|
370
|
+
const liveChanges = [];
|
|
371
|
+
const otherKeys = [];
|
|
372
|
+
for (const key of new Set([...Object.keys(prev), ...Object.keys(next)])) {
|
|
373
|
+
const before = stableStringifyForBotConfig(prev[key] ?? null);
|
|
374
|
+
const after = stableStringifyForBotConfig(next[key] ?? null);
|
|
375
|
+
if (before === after)
|
|
376
|
+
continue;
|
|
377
|
+
if (BOT_LIVE_CONFIG_KEYS.includes(key)) {
|
|
378
|
+
liveChanges.push({ key, oldValue: prev[key] ?? null, newValue: next[key] ?? null });
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
otherKeys.push(key);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { liveChanges, otherKeys };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Check bots.json for this bot's entry and live-apply allowlisted keys
|
|
388
|
+
* (BOT_LIVE_CONFIG_KEYS; debtPolicy additionally reconciles the credit
|
|
389
|
+
* runtime, malformed debtPolicy is hinted instead of merged).
|
|
390
|
+
* Best-effort and idempotent: synchronous in-memory merge only, no chain
|
|
391
|
+
* I/O, no fill-lock (assignments are atomic; the existing maintenance /
|
|
392
|
+
* targeted-reconcile ticks heal shortfalls/excess on the next cycle).
|
|
393
|
+
* Geometry changes are NOT applied (need `dexbot reset <bot>`), identity
|
|
394
|
+
* changes are NOT applied (need restart) — on the steady-state path both
|
|
395
|
+
* produce a one-time hint per fingerprint (the fingerprint advances past
|
|
396
|
+
* hinted keys, malformed debtPolicy included, so a persistently-broken file
|
|
397
|
+
* warns once until the file changes again). The first-check baseline only
|
|
398
|
+
* converges the race-safe keys and stays silent on the rest: a geometry
|
|
399
|
+
* edit landing in the process-load → first-tick window is absorbed into
|
|
400
|
+
* the baseline with no hint. Corrupt/unreadable files and missing entries
|
|
401
|
+
* never touch the stored fingerprint (except the missing-entry hint
|
|
402
|
+
* throttle) so the next tick re-evaluates.
|
|
403
|
+
* @param {any} bot - DEXBot instance
|
|
404
|
+
* @param {string} [context='bots-config poll'] - Context label for logging
|
|
405
|
+
* @param {any} [preloadedSnapshot=null] - Reuse an already-loaded snapshot
|
|
406
|
+
* @returns {Promise<any>} Result with applied/liveChanges/otherKeys or skipped
|
|
407
|
+
*/
|
|
408
|
+
async function checkAndApplyBotConfigChanges(bot, context = 'bots-config poll', preloadedSnapshot = null) {
|
|
409
|
+
try {
|
|
410
|
+
if (!bot || !bot.config) {
|
|
411
|
+
return { skipped: true, reason: 'missing-config' };
|
|
412
|
+
}
|
|
413
|
+
const snapshot = preloadedSnapshot
|
|
414
|
+
?? (typeof bot._loadBotsConfigSnapshot === 'function'
|
|
415
|
+
? await bot._loadBotsConfigSnapshot()
|
|
416
|
+
: loadBotsConfigSnapshot());
|
|
417
|
+
if (!snapshot || snapshot.corrupt || snapshot.readError) {
|
|
418
|
+
return {
|
|
419
|
+
skipped: true,
|
|
420
|
+
reason: snapshot?.corrupt ? 'corrupt-config' : snapshot?.readError ? 'unreadable-config' : 'missing-snapshot',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const entry = findSnapshotBotForRuntimeConfig(snapshot, bot.config);
|
|
424
|
+
const botName = String(bot.config?.name || bot.config?.botKey || 'bot');
|
|
425
|
+
if (!entry) {
|
|
426
|
+
const missingMarker = `missing:${String(bot.config?.botKey || bot.config?.name || '')}`;
|
|
427
|
+
if (bot._lastBotConfigHintFingerprint !== missingMarker) {
|
|
428
|
+
bot._lastBotConfigHintFingerprint = missingMarker;
|
|
429
|
+
bot._warn?.(`bots.json no longer contains an active entry for '${botName}' during ${context}; ` +
|
|
430
|
+
`roster changes need a stop/start.`);
|
|
431
|
+
}
|
|
432
|
+
return { skipped: true, reason: 'bot-not-found' };
|
|
433
|
+
}
|
|
434
|
+
const normalized = normalizeBotEntryForFingerprint(entry);
|
|
435
|
+
const fingerprint = stableStringifyForBotConfig(normalized);
|
|
436
|
+
if (bot._appliedBotConfigFingerprint === null || bot._appliedBotConfigFingerprint === undefined) {
|
|
437
|
+
bot._appliedBotConfigFingerprint = fingerprint;
|
|
438
|
+
bot._appliedBotConfigEntry = normalized;
|
|
439
|
+
// Startup race: the file may have changed between process config
|
|
440
|
+
// load and this first check. Converge the race-safe keys so the
|
|
441
|
+
// running bot can never sit on a value the file no longer
|
|
442
|
+
// contains. weightDistribution is excluded —
|
|
443
|
+
// refreshDynamicWeightDistribution owns the live value at runtime.
|
|
444
|
+
const racePrev = {};
|
|
445
|
+
for (const key of BOT_LIVE_CONFIG_KEYS) {
|
|
446
|
+
if (key === 'weightDistribution')
|
|
447
|
+
continue;
|
|
448
|
+
racePrev[key] = bot.config?.[key];
|
|
449
|
+
}
|
|
450
|
+
const raceDiff = diffBotConfigEntries(racePrev, normalized);
|
|
451
|
+
// Same invariant as the steady-state path: a malformed debtPolicy
|
|
452
|
+
// never reaches the runtime (warn once; the next save re-evaluates).
|
|
453
|
+
const raceLive = raceDiff.liveChanges.filter((c) => c.key !== 'debtPolicy' || isDebtPolicyLiveApplySafe(c.newValue));
|
|
454
|
+
if (raceLive.length !== raceDiff.liveChanges.length) {
|
|
455
|
+
bot._warn?.(`bots.json debtPolicy for '${botName}' failed validation at startup check; ` +
|
|
456
|
+
`keeping startup policy — fix the shape or run 'dexbot reset ${botName}'.`);
|
|
457
|
+
}
|
|
458
|
+
const raceApplied = mergeBotConfigKeysIntoRuntime(bot, entry, raceLive.map((c) => c.key));
|
|
459
|
+
if (raceApplied.includes('debtPolicy')) {
|
|
460
|
+
await reconcileCreditRuntimeAfterPolicyChange(bot);
|
|
461
|
+
}
|
|
462
|
+
if (raceApplied.length > 0) {
|
|
463
|
+
bot._log?.(`Picked up bots.json changes for '${botName}' at startup check (no restart needed): ${raceApplied.join(', ')}.`, 'info');
|
|
464
|
+
}
|
|
465
|
+
return { applied: raceApplied.length > 0, reason: 'baseline', fingerprint, liveChanges: raceApplied };
|
|
466
|
+
}
|
|
467
|
+
if (fingerprint === bot._appliedBotConfigFingerprint) {
|
|
468
|
+
return { applied: false, reason: 'unchanged', fingerprint };
|
|
469
|
+
}
|
|
470
|
+
const { liveChanges: rawLiveChanges, otherKeys: rawOtherKeys } = diffBotConfigEntries(bot._appliedBotConfigEntry, normalized);
|
|
471
|
+
// Malformed debtPolicy never reaches the runtime: hint reset/restart
|
|
472
|
+
// instead of merging garbage the credit cycle would choke on.
|
|
473
|
+
const liveChanges = rawLiveChanges.filter((c) => c.key !== 'debtPolicy' || isDebtPolicyLiveApplySafe(c.newValue));
|
|
474
|
+
const otherKeys = [...rawOtherKeys];
|
|
475
|
+
for (const c of rawLiveChanges) {
|
|
476
|
+
if (c.key === 'debtPolicy' && !isDebtPolicyLiveApplySafe(c.newValue) && !otherKeys.includes('debtPolicy')) {
|
|
477
|
+
otherKeys.push('debtPolicy');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const appliedKeys = mergeBotConfigKeysIntoRuntime(bot, entry, liveChanges.map((c) => c.key));
|
|
481
|
+
if (appliedKeys.includes('debtPolicy')) {
|
|
482
|
+
await reconcileCreditRuntimeAfterPolicyChange(bot);
|
|
483
|
+
}
|
|
484
|
+
if (appliedKeys.includes('weightDistribution')) {
|
|
485
|
+
// Same as the resync path: the file value is the base, the live
|
|
486
|
+
// effective weights (when whitelisted/ready) take precedence in
|
|
487
|
+
// bot/manager config immediately instead of next tick.
|
|
488
|
+
refreshDynamicWeightDistribution(bot, context);
|
|
489
|
+
}
|
|
490
|
+
bot._appliedBotConfigFingerprint = fingerprint;
|
|
491
|
+
bot._appliedBotConfigEntry = normalized;
|
|
492
|
+
if (liveChanges.length > 0) {
|
|
493
|
+
const summary = liveChanges
|
|
494
|
+
.map((c) => `${c.key} ${stableStringifyForBotConfig(c.oldValue)}->${stableStringifyForBotConfig(c.newValue)}`)
|
|
495
|
+
.join('; ');
|
|
496
|
+
bot._log?.(`Applied bots.json changes for '${botName}' live during ${context} (no restart needed): ${summary}. ` +
|
|
497
|
+
`Targeted maintenance will place missing / cancel excess orders.`, 'info');
|
|
498
|
+
}
|
|
499
|
+
if (otherKeys.length > 0) {
|
|
500
|
+
const shown = otherKeys.slice(0, 8).join(', ');
|
|
501
|
+
const suffix = otherKeys.length > 8 ? ` (+${otherKeys.length - 8} more)` : '';
|
|
502
|
+
bot._warn?.(`bots.json changed ${shown}${suffix} for '${botName}' during ${context}; ` +
|
|
503
|
+
`these were NOT applied live — run 'dexbot reset ${botName}' for grid geometry ` +
|
|
504
|
+
`or restart for market/account identity.`);
|
|
505
|
+
}
|
|
506
|
+
if (liveChanges.length === 0 && otherKeys.length === 0) {
|
|
507
|
+
bot._log?.(`Detected bots.json bookkeeping change for '${botName}' during ${context}; no tradable setting changed.`, 'debug');
|
|
508
|
+
}
|
|
509
|
+
return { applied: liveChanges.length > 0, liveChanges: liveChanges.map((c) => c.key), otherKeys, fingerprint };
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
try {
|
|
513
|
+
bot?._warn?.(`Bot-config live check failed during ${context}: ${getErrorMessage(err)}`);
|
|
514
|
+
}
|
|
515
|
+
catch { /* logging must never break the tick */ }
|
|
516
|
+
return { skipped: true, reason: 'error', error: getErrorMessage(err) };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
150
519
|
function countLiveGridOrders(manager, type) {
|
|
151
520
|
if (!manager)
|
|
152
521
|
return 0;
|
|
@@ -156,7 +525,86 @@ function countLiveGridOrders(manager, type) {
|
|
|
156
525
|
}
|
|
157
526
|
function getTargetActiveOrders(config, side) {
|
|
158
527
|
const configured = Number(config?.activeOrders?.[side]);
|
|
159
|
-
|
|
528
|
+
// Reserve ladder rests live on-chain: countLiveGridOrders sees window +
|
|
529
|
+
// edge orders, so the shortfall target must include reserves too —
|
|
530
|
+
// otherwise live reserves mask window shortfalls and a filled/cancelled
|
|
531
|
+
// reserve order never triggers targeted reconciliation.
|
|
532
|
+
const reserves = resolveReserveCount(config, side);
|
|
533
|
+
return Math.max(0, Number.isFinite(configured) ? configured : 1) + reserves;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Count live on-chain reserve orders for one side (issue #27 follow-up).
|
|
537
|
+
*
|
|
538
|
+
* The window shortfall check above compares live window+reserves against
|
|
539
|
+
* window+reserves, so a pre-existing window surplus masks a reserve deficit
|
|
540
|
+
* (e.g. target 6+2=8 vs 12 live after reserveOrders 0→2: no shortfall, no
|
|
541
|
+
* sync, zero reserve placements). Reserves get their own independent reason
|
|
542
|
+
* below — the mirror of the masking case getTargetActiveOrders handles.
|
|
543
|
+
*
|
|
544
|
+
* Classification reuses the single source of truth (reserveEdgeIdSet +
|
|
545
|
+
* resolveLiveReserveEdgeAnchorPrice, same anchor the placement pickers use)
|
|
546
|
+
* over the full master grid, intersected with live ACTIVE/PARTIAL orderIds
|
|
547
|
+
* (same live definition as countLiveGridOrders). The edge pick excludes the
|
|
548
|
+
* window (liveWindowIdSet — the same exclusion every placement picker
|
|
549
|
+
* applies): window + edge are additive in targets/fees/hold-back, but
|
|
550
|
+
* without the exclusion a window that reaches the grid edge (keep-low
|
|
551
|
+
* window = bottom slots = floor edge) makes the edge pick land on window
|
|
552
|
+
* members and the count reads N/N with zero dedicated reserves, so the
|
|
553
|
+
* deficit never fires (issue #27 follow-up).
|
|
554
|
+
* @param {any} manager - OrderManager
|
|
555
|
+
* @param {any} config - Bot configuration (reserve count source)
|
|
556
|
+
* @param {any} type - ORDER_TYPES.BUY or ORDER_TYPES.SELL
|
|
557
|
+
* @returns {number|null} Live reserve count, or null when unclassifiable
|
|
558
|
+
*/
|
|
559
|
+
function countLiveReserveOrders(manager, config, type) {
|
|
560
|
+
try {
|
|
561
|
+
const side = type === ORDER_TYPES.SELL ? 'sell' : 'buy';
|
|
562
|
+
const required = resolveReserveCount(config, side);
|
|
563
|
+
if (!(required > 0))
|
|
564
|
+
return 0;
|
|
565
|
+
if (!manager)
|
|
566
|
+
return null;
|
|
567
|
+
// Full master grid only: classifying from live-only indexed lookups
|
|
568
|
+
// would pick the "edge" of the live subset, not the grid edge, and
|
|
569
|
+
// mis-fire on stripped test doubles. Fail closed (null) without it.
|
|
570
|
+
if (!manager.orders || typeof manager.orders.values !== 'function')
|
|
571
|
+
return null;
|
|
572
|
+
const allSlots = Array.from(manager.orders.values());
|
|
573
|
+
if (allSlots.length === 0)
|
|
574
|
+
return null;
|
|
575
|
+
const anchor = resolveLiveReserveEdgeAnchorPrice(manager, side);
|
|
576
|
+
// Fail open on unknown window geometry (null): without a boundary the
|
|
577
|
+
// pickers cannot place reserves either, so keep the previous
|
|
578
|
+
// (exclusion-free) classification instead of guessing.
|
|
579
|
+
const windowIds = liveWindowIdSet(manager, type);
|
|
580
|
+
const reserveIds = reserveEdgeIdSet(allSlots, config, type, anchor, windowIds);
|
|
581
|
+
if (!reserveIds || reserveIds.size === 0)
|
|
582
|
+
return 0;
|
|
583
|
+
const liveIds = new Set();
|
|
584
|
+
if (typeof manager.getOrdersByTypeAndState === 'function') {
|
|
585
|
+
for (const state of [ORDER_STATES.ACTIVE, ORDER_STATES.PARTIAL]) {
|
|
586
|
+
for (const o of manager.getOrdersByTypeAndState(type, state) || []) {
|
|
587
|
+
if (o?.id != null && o?.orderId)
|
|
588
|
+
liveIds.add(String(o.id));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
for (const s of allSlots) {
|
|
594
|
+
if (s?.id != null && s?.orderId)
|
|
595
|
+
liveIds.add(String(s.id));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
let live = 0;
|
|
599
|
+
for (const id of reserveIds) {
|
|
600
|
+
if (liveIds.has(String(id)))
|
|
601
|
+
live++;
|
|
602
|
+
}
|
|
603
|
+
return live;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
160
608
|
}
|
|
161
609
|
function _hasBudgetForSide(manager, config, side) {
|
|
162
610
|
try {
|
|
@@ -192,6 +640,21 @@ function getTargetedSyncReason(bot) {
|
|
|
192
640
|
shortfalls.push(`sell ${liveSell}/${targetSell}`);
|
|
193
641
|
}
|
|
194
642
|
}
|
|
643
|
+
// Reserve deficit is independent of window surplus: a bot carrying extra
|
|
644
|
+
// rail/stranded orders can sit above target while its reserve edge is
|
|
645
|
+
// empty (live-applied reserveOrders 0→N, filled/cancelled reserve).
|
|
646
|
+
// Without this, placement waits for fills or the 4h fetch (issue #27).
|
|
647
|
+
for (const [type, side] of [[ORDER_TYPES.BUY, 'buy'], [ORDER_TYPES.SELL, 'sell']]) {
|
|
648
|
+
const required = resolveReserveCount(bot.config, side);
|
|
649
|
+
if (!(required > 0))
|
|
650
|
+
continue;
|
|
651
|
+
const liveReserves = countLiveReserveOrders(bot.manager, bot.config, type);
|
|
652
|
+
if (liveReserves !== null && liveReserves < required) {
|
|
653
|
+
if (_hasBudgetForSide(bot.manager, bot.config, side)) {
|
|
654
|
+
shortfalls.push(`${side} reserves ${liveReserves}/${required}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
195
658
|
const drift = bot.manager.checkFundDriftAfterFills?.();
|
|
196
659
|
if (drift && drift.isValid === false) {
|
|
197
660
|
return { reason: `fund drift: ${drift.reason}`, targetBuy, targetSell, liveBuy, liveSell, drift };
|
|
@@ -357,13 +820,35 @@ async function syncMarketAdapterOnPeriodicConfigCheck(bot, context = 'periodic')
|
|
|
357
820
|
if (typeof bot._syncMarketAdapterHook === 'function') {
|
|
358
821
|
return await bot._syncMarketAdapterHook(context);
|
|
359
822
|
}
|
|
823
|
+
// Single bots.json read shared by the live bot-config check and the
|
|
824
|
+
// adapter drive below (one read per tick, not two). Throwing reads
|
|
825
|
+
// (transient I/O) behave like corrupt: skip everything, keep stored
|
|
826
|
+
// fingerprints, retry next tick.
|
|
827
|
+
let snapshot = null;
|
|
828
|
+
try {
|
|
829
|
+
snapshot = typeof bot._loadBotsConfigSnapshot === 'function'
|
|
830
|
+
? await bot._loadBotsConfigSnapshot()
|
|
831
|
+
: loadBotsConfigSnapshot();
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
bot._warn(`Ignoring unreadable bots.json during ${context}; keeping previous market adapter state.`);
|
|
835
|
+
return { skipped: true, reason: 'unreadable-config' };
|
|
836
|
+
}
|
|
837
|
+
// Live bot-config check runs on EVERY path (including wrapper-owned):
|
|
838
|
+
// no adapter driving, best-effort. It live-applies allowlisted keys
|
|
839
|
+
// (BOT_LIVE_CONFIG_KEYS) and hints
|
|
840
|
+
// reset/restart for everything else (Issue #27 follow-up).
|
|
841
|
+
try {
|
|
842
|
+
await checkAndApplyBotConfigChanges(bot, context, snapshot);
|
|
843
|
+
}
|
|
844
|
+
catch { /* checkAndApply never throws; belt-and-suspenders */ }
|
|
360
845
|
// Centralized ownership: in monolithic mode the unlock wrapper watchdog
|
|
361
|
-
// (shared 1min tick) is the sole market-adapter spawner. A per-bot
|
|
362
|
-
// re-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// one-shot, isolated supervisor,
|
|
366
|
-
//
|
|
846
|
+
// (shared 1min tick) is the sole market-adapter spawner. A per-bot drive here would
|
|
847
|
+
// re-drive the adapter N times (once per bot) and fight the wrapper over
|
|
848
|
+
// the adapter child, so bots skip the adapter drive entirely and act as
|
|
849
|
+
// pure adapter-output consumers. The live bot-config check above still
|
|
850
|
+
// ran. Wrapper-less modes (dexbot test one-shot, isolated supervisor,
|
|
851
|
+
// PM2) keep the in-bot fallback below.
|
|
367
852
|
if (!isPm2Runtime() && isWrapperAdapterOwner()) {
|
|
368
853
|
return { skipped: true, reason: 'wrapper-owned' };
|
|
369
854
|
}
|
|
@@ -372,9 +857,6 @@ async function syncMarketAdapterOnPeriodicConfigCheck(bot, context = 'periodic')
|
|
|
372
857
|
}
|
|
373
858
|
bot._marketAdapterWatchdogInFlight = true;
|
|
374
859
|
try {
|
|
375
|
-
const snapshot = typeof bot._loadBotsConfigSnapshot === 'function'
|
|
376
|
-
? await bot._loadBotsConfigSnapshot()
|
|
377
|
-
: loadBotsConfigSnapshot();
|
|
378
860
|
// Corrupt/unreadable bots.json must not move the adapter: previously
|
|
379
861
|
// the parse/read throw aborted the check and left the adapter alone.
|
|
380
862
|
// Keep that behavior by skipping the sync without touching the stored
|
|
@@ -744,22 +1226,7 @@ function performGridResync(bot, options = {}) {
|
|
|
744
1226
|
const myName = self.config.name;
|
|
745
1227
|
const updatedBot = allBotsConfig.find((b) => isSameBotName(b.name, myName));
|
|
746
1228
|
if (updatedBot) {
|
|
747
|
-
self
|
|
748
|
-
const oldKey = self.config.botKey;
|
|
749
|
-
const oldIndex = self.config.botIndex;
|
|
750
|
-
// Preserve runtime-only properties set by the constructor
|
|
751
|
-
// (timing, gridLimits, feeParams, etc.) that are not present
|
|
752
|
-
// in the raw profile from bots.json.
|
|
753
|
-
const runtimeProps = {};
|
|
754
|
-
for (const key of getRuntimeSettingsKeys()) {
|
|
755
|
-
if (self.config[key] !== undefined) {
|
|
756
|
-
runtimeProps[key] = self.config[key];
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
self.config = { ...updatedBot, botKey: oldKey, botIndex: oldIndex, ...runtimeProps };
|
|
760
|
-
self.manager.config = { ...self.manager.config, ...self.config };
|
|
761
|
-
self._baseWeightDistribution = cloneWeightDistribution(updatedBot.weightDistribution, self._baseWeightDistribution);
|
|
762
|
-
refreshDynamicWeightDistribution(self, 'grid resync');
|
|
1229
|
+
replaceBotConfigFromEntryPreservingRuntime(self, updatedBot, 'grid resync');
|
|
763
1230
|
}
|
|
764
1231
|
}
|
|
765
1232
|
catch (e) {
|
|
@@ -977,6 +1444,29 @@ async function performPeriodicGridChecks(bot) {
|
|
|
977
1444
|
else {
|
|
978
1445
|
await runGridMaintenance(bot, 'periodic');
|
|
979
1446
|
}
|
|
1447
|
+
logDeferredHoldSummary(bot);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Summarize deliberate deferred holds once per count change. The per-order
|
|
1451
|
+
* sync logs report each hold when it first appears; this surfaces the running
|
|
1452
|
+
* total (funds stay locked until an operator clears them) without re-logging
|
|
1453
|
+
* the same count every tick.
|
|
1454
|
+
*
|
|
1455
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
1456
|
+
*/
|
|
1457
|
+
function logDeferredHoldSummary(bot) {
|
|
1458
|
+
const unmatched = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
1459
|
+
? bot.manager._lastUnmatchedChainOrders
|
|
1460
|
+
: [];
|
|
1461
|
+
const held = unmatched.filter((u) => isNonBlockingUnmatchedOrder(u)).length;
|
|
1462
|
+
if (held === 0) {
|
|
1463
|
+
bot._lastHeldChainOrderCount = 0;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (held === bot._lastHeldChainOrderCount)
|
|
1467
|
+
return;
|
|
1468
|
+
bot._lastHeldChainOrderCount = held;
|
|
1469
|
+
bot._log?.(`[HOLD] ${held} deferred chain order(s) held outside the active pipeline (funds stay locked until cleared)`, 'warn');
|
|
980
1470
|
}
|
|
981
1471
|
/**
|
|
982
1472
|
* Check if the continuous open-orders sync loop is enabled.
|
|
@@ -1217,17 +1707,15 @@ function stopBlockchainFetchInterval(bot) {
|
|
|
1217
1707
|
}
|
|
1218
1708
|
}
|
|
1219
1709
|
/**
|
|
1220
|
-
* Set up the periodic bots.json
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
* Uses the shared fingerprint via syncMarketAdapterOnPeriodicConfigCheck so
|
|
1230
|
-
* the market adapter start/stop logic stays in one place.
|
|
1710
|
+
* Set up the periodic bots.json poll interval.
|
|
1711
|
+
* Runs in ALL modes (including wrapper-owned monolithic): the tick drives
|
|
1712
|
+
* syncMarketAdapterOnPeriodicConfigCheck, which always runs the live
|
|
1713
|
+
* bot-config check (allowlisted keys applied without restart, Issue #27
|
|
1714
|
+
* follow-up) and additionally drives the market adapter only in
|
|
1715
|
+
* wrapper-less modes (dexbot test one-shot, isolated supervisor, PM2).
|
|
1716
|
+
* Decoupled from the heavy blockchain fetch interval (default 240min) so
|
|
1717
|
+
* config changes are visible within BOTS_CONFIG_POLL_INTERVAL_MS
|
|
1718
|
+
* (default 1min, shared with the wrapper watchdog interval).
|
|
1231
1719
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
1232
1720
|
*/
|
|
1233
1721
|
function setupBotsConfigPollInterval(bot) {
|
|
@@ -1237,13 +1725,14 @@ function setupBotsConfigPollInterval(bot) {
|
|
|
1237
1725
|
intervalMs = Number(TIMING.BOTS_CONFIG_POLL_INTERVAL_MS);
|
|
1238
1726
|
}
|
|
1239
1727
|
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
1240
|
-
bot._log(`Bots-config poll interval disabled (value: ${intervalMs}).
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (!isPm2Runtime() && isWrapperAdapterOwner()) {
|
|
1244
|
-
bot._log('Bots-config poll disabled (market adapter owned by unlock wrapper).');
|
|
1728
|
+
bot._log(`Bots-config poll interval disabled (value: ${intervalMs}). Adapter and live bot-config changes will only be seen on blockchain fetch ticks.`);
|
|
1245
1729
|
return;
|
|
1246
1730
|
}
|
|
1731
|
+
// NOTE: no wrapper-owned early return here on purpose. The adapter drive
|
|
1732
|
+
// inside syncMarketAdapterOnPeriodicConfigCheck still skips when the
|
|
1733
|
+
// wrapper owns it, but the live bot-config check at the top of that
|
|
1734
|
+
// function must run for every bot (otherwise monolithic bots would never
|
|
1735
|
+
// pick up bots.json edits until the 240min blockchain fetch tick).
|
|
1247
1736
|
// Ensure the fingerprint exists before the first interval fires.
|
|
1248
1737
|
// setupBlockchainFetchInterval already fires syncMarketAdapterOnPeriodicConfigCheck
|
|
1249
1738
|
// at startup, so this is a fallback for bots that never run the blockchain interval
|
|
@@ -1280,7 +1769,7 @@ function setupBotsConfigPollInterval(bot) {
|
|
|
1280
1769
|
if (typeof bot._botsConfigPollInterval.unref === 'function') {
|
|
1281
1770
|
bot._botsConfigPollInterval.unref();
|
|
1282
1771
|
}
|
|
1283
|
-
bot._log(`Started bots-config poll interval: every ${Math.round(intervalMs / 1000)}s (fingerprint check)`);
|
|
1772
|
+
bot._log(`Started bots-config poll interval: every ${Math.round(intervalMs / 1000)}s (adapter fingerprint + live bot-config check)`);
|
|
1284
1773
|
}
|
|
1285
1774
|
/**
|
|
1286
1775
|
* Stop the periodic bots.json fingerprint poll interval.
|
|
@@ -1584,7 +2073,9 @@ async function executeMaintenanceLogic(bot, context) {
|
|
|
1584
2073
|
if (divergence.needsUpdate) {
|
|
1585
2074
|
const hasRmsDivergence = !!(divergence.buy.rms || divergence.sell.rms);
|
|
1586
2075
|
if (divergence.buy.ratio || divergence.sell.ratio) {
|
|
1587
|
-
|
|
2076
|
+
const buyDir = divergence.buy.shrink ? '/shrink' : '';
|
|
2077
|
+
const sellDir = divergence.sell.shrink ? '/shrink' : '';
|
|
2078
|
+
bot._log(`Grid update triggered by funds during ${context} (buy: ${divergence.buy.ratio}${buyDir}, sell: ${divergence.sell.ratio}${sellDir})`);
|
|
1588
2079
|
}
|
|
1589
2080
|
if (hasRmsDivergence) {
|
|
1590
2081
|
bot._log(`Grid update triggered by structural divergence during ${context}: buy=${Format.formatPrice6(divergence.buy.metric)}, sell=${Format.formatPrice6(divergence.sell.metric)}`);
|
|
@@ -2032,6 +2523,7 @@ function drainFillQueueAfterPipelineClear(bot) {
|
|
|
2032
2523
|
if (!bot._incomingFillQueue || bot._incomingFillQueue.length === 0)
|
|
2033
2524
|
return;
|
|
2034
2525
|
bot._log(`[FILL-QUEUE] Pipeline cleared; draining ${bot._incomingFillQueue.length} deferred fill(s).`, 'info');
|
|
2526
|
+
bot._deferredFillsPending = true;
|
|
2035
2527
|
scheduleFillConsumerRestartFn(bot, chainOrders);
|
|
2036
2528
|
}
|
|
2037
2529
|
catch { }
|
|
@@ -2239,6 +2731,14 @@ function markGridActivity(bot, reason = 'activity') {
|
|
|
2239
2731
|
*/
|
|
2240
2732
|
function getMetrics(bot) {
|
|
2241
2733
|
bot.manager?._cleanExpiredLocks?.();
|
|
2734
|
+
const unmatched = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
2735
|
+
? bot.manager._lastUnmatchedChainOrders
|
|
2736
|
+
: [];
|
|
2737
|
+
// Deliberate holds (reason suffixed `-deferred`) never block the pipeline,
|
|
2738
|
+
// but they DO lock funds until an operator clears them. Surface the split
|
|
2739
|
+
// so the permanent holds are visible in status/metrics instead of only in
|
|
2740
|
+
// per-order sync logs.
|
|
2741
|
+
const heldUnmatched = unmatched.filter((u) => isNonBlockingUnmatchedOrder(u));
|
|
2242
2742
|
return {
|
|
2243
2743
|
...bot._metrics,
|
|
2244
2744
|
queueDepth: bot._incomingFillQueue.length,
|
|
@@ -2246,7 +2746,10 @@ function getMetrics(bot) {
|
|
|
2246
2746
|
divergenceLockActive: bot.manager?._divergenceLock?.isLocked() || false,
|
|
2247
2747
|
shadowLocksActive: bot.manager?.shadowOrderIds?.size || 0,
|
|
2248
2748
|
recoveryExhaustedAt: bot.manager?._recoveryExhaustedAt || null,
|
|
2249
|
-
recentFillsTracked: bot._recentlyProcessedFills.size
|
|
2749
|
+
recentFillsTracked: bot._recentlyProcessedFills.size,
|
|
2750
|
+
unmatchedChainOrders: unmatched.length,
|
|
2751
|
+
heldChainOrders: heldUnmatched.length,
|
|
2752
|
+
blockingChainOrders: unmatched.length - heldUnmatched.length
|
|
2250
2753
|
};
|
|
2251
2754
|
}
|
|
2252
2755
|
/**
|
|
@@ -2321,10 +2824,12 @@ async function syncOpenOrdersAndProcessFillsImpl(bot, tag) {
|
|
|
2321
2824
|
return { syncResult: null, aborted: true, hasUnmatched: -1, openOrders: null };
|
|
2322
2825
|
}
|
|
2323
2826
|
}
|
|
2324
|
-
export { loadBotsConfigSnapshot, isWrapperAdapterOwner, refreshDynamicWeightDistribution, performGridResync, updateBotGridResetMetadata, handlePendingTriggerReset, setupTriggerFileDetection, performPeriodicGridChecks, isOpenOrdersSyncLoopEnabled, startOpenOrdersSyncLoop, stopOpenOrdersSyncLoop, setupBlockchainFetchInterval, stopBlockchainFetchInterval, setupBotsConfigPollInterval, stopBotsConfigPollInterval, executeMaintenanceLogic, cancelDustOrders, isOrderDoesNotExistError, runGridMaintenance, stopMarketAdapterPm2, releaseMarketAdapterRuntime, syncMarketAdapterOnPeriodicConfigCheck, findSnapshotBotForRuntimeConfig, runtimeConfigNeedsMarketAdapter, usesAmaGridPrice, checkBtsBalanceAndAcquire, acquireBts, runDustHealthCheck, setupDustHealthCheckInterval, requestGridReset, wireStructuralGridResyncRequest, getPipelineSignals, markGridActivity, getMetrics, syncOpenOrdersAndProcessFills };
|
|
2827
|
+
export { loadBotsConfigSnapshot, isWrapperAdapterOwner, checkAndApplyBotConfigChanges, buildBotConfigFingerprint, refreshDynamicWeightDistribution, performGridResync, updateBotGridResetMetadata, handlePendingTriggerReset, setupTriggerFileDetection, performPeriodicGridChecks, isOpenOrdersSyncLoopEnabled, startOpenOrdersSyncLoop, stopOpenOrdersSyncLoop, setupBlockchainFetchInterval, stopBlockchainFetchInterval, setupBotsConfigPollInterval, stopBotsConfigPollInterval, executeMaintenanceLogic, getTargetedSyncReason, countLiveReserveOrders, maybeRunTargetedDriftReconciliation, cancelDustOrders, isOrderDoesNotExistError, runGridMaintenance, stopMarketAdapterPm2, releaseMarketAdapterRuntime, syncMarketAdapterOnPeriodicConfigCheck, findSnapshotBotForRuntimeConfig, runtimeConfigNeedsMarketAdapter, usesAmaGridPrice, checkBtsBalanceAndAcquire, acquireBts, runDustHealthCheck, setupDustHealthCheckInterval, requestGridReset, wireStructuralGridResyncRequest, getPipelineSignals, markGridActivity, getMetrics, syncOpenOrdersAndProcessFills };
|
|
2325
2828
|
export default {
|
|
2326
2829
|
loadBotsConfigSnapshot,
|
|
2327
2830
|
isWrapperAdapterOwner,
|
|
2831
|
+
checkAndApplyBotConfigChanges,
|
|
2832
|
+
buildBotConfigFingerprint,
|
|
2328
2833
|
refreshDynamicWeightDistribution,
|
|
2329
2834
|
performGridResync,
|
|
2330
2835
|
updateBotGridResetMetadata,
|
|
@@ -2339,6 +2844,9 @@ export default {
|
|
|
2339
2844
|
setupBotsConfigPollInterval,
|
|
2340
2845
|
stopBotsConfigPollInterval,
|
|
2341
2846
|
executeMaintenanceLogic,
|
|
2847
|
+
getTargetedSyncReason,
|
|
2848
|
+
countLiveReserveOrders,
|
|
2849
|
+
maybeRunTargetedDriftReconciliation,
|
|
2342
2850
|
cancelDustOrders,
|
|
2343
2851
|
isOrderDoesNotExistError,
|
|
2344
2852
|
runGridMaintenance,
|