dexbot 1.4.25 → 1.5.1
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 +54 -0
- package/README.md +6 -2
- package/analysis/README.md +14 -7
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_key_utils.ts +272 -3
- package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
- package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
- package/analysis/chart_utils.ts +6 -3
- package/analysis/grid_correction_check.ts +56 -20
- package/analysis/resolve_bot_accounts.ts +199 -0
- package/analysis/trade_profitability.ts +36 -8
- package/analysis/tradingview/README.md +25 -0
- package/analysis/tradingview/analyze_tradingview.ts +2 -2
- package/analysis/trend_detection/package.json +1 -1
- package/claw/modules/claw_launcher.ts +5 -4
- package/claw/modules/credit_runtime_adapter.ts +2 -1
- package/claw/modules/dexbot_profiles.ts +4 -3
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_key_utils.d.ts +28 -1
- package/dist/analysis/bot_key_utils.d.ts.map +1 -1
- package/dist/analysis/bot_key_utils.js +385 -3
- package/dist/analysis/bot_key_utils.js.map +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
- package/dist/analysis/chart_utils.d.ts +2 -1
- package/dist/analysis/chart_utils.d.ts.map +1 -1
- package/dist/analysis/chart_utils.js +4 -1
- package/dist/analysis/chart_utils.js.map +1 -1
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +60 -20
- package/dist/analysis/grid_correction_check.js.map +1 -1
- package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
- package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
- package/dist/analysis/resolve_bot_accounts.js +216 -0
- package/dist/analysis/resolve_bot_accounts.js.map +1 -0
- package/dist/analysis/trade_profitability.d.ts.map +1 -1
- package/dist/analysis/trade_profitability.js +40 -8
- package/dist/analysis/trade_profitability.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +2 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +73 -17
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/modules/account_bots.d.ts +42 -1
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +168 -17
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +7 -1
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +34 -1
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts +4 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +12 -4
- 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 +2 -1
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/cli_colors.d.ts +39 -0
- package/dist/modules/cli_colors.d.ts.map +1 -0
- package/dist/modules/cli_colors.js +42 -0
- package/dist/modules/cli_colors.js.map +1 -0
- package/dist/modules/constants.d.ts +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +27 -11
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credit_pricing.d.ts +61 -0
- package/dist/modules/credit_pricing.d.ts.map +1 -0
- package/dist/modules/credit_pricing.js +237 -0
- package/dist/modules/credit_pricing.js.map +1 -0
- package/dist/modules/credit_runtime.d.ts +1 -0
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +49 -107
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +3 -1
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +22 -6
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +36 -2
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +434 -20
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +15 -16
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +69 -42
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +12 -1
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +7 -0
- 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/launcher/adapter_requirement.d.ts +38 -0
- package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
- package/dist/modules/launcher/adapter_requirement.js +141 -0
- package/dist/modules/launcher/adapter_requirement.js.map +1 -0
- package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
- package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
- package/dist/modules/launcher/bot_supervisor.js +32 -3
- package/dist/modules/launcher/bot_supervisor.js.map +1 -1
- package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
- package/dist/modules/launcher/launch_modes.js +5 -4
- package/dist/modules/launcher/launch_modes.js.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
- package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.js +12 -8
- package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
- package/dist/modules/launcher/status_reporting.d.ts +6 -6
- package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
- package/dist/modules/launcher/status_reporting.js +9 -6
- package/dist/modules/launcher/status_reporting.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +34 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +182 -5
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts +1 -1
- package/dist/modules/order/export.js +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +68 -28
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +72 -0
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/logger.d.ts +23 -1
- package/dist/modules/order/logger.d.ts.map +1 -1
- package/dist/modules/order/logger.js +60 -17
- package/dist/modules/order/logger.js.map +1 -1
- package/dist/modules/order/manager.d.ts +41 -1
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +186 -17
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +17 -2
- 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 +59 -9
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +42 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +91 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +52 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +137 -7
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +66 -4
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +156 -16
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +30 -3
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +206 -11
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/utils/chain_logs.d.ts +11 -0
- package/dist/modules/utils/chain_logs.d.ts.map +1 -0
- package/dist/modules/utils/chain_logs.js +23 -0
- package/dist/modules/utils/chain_logs.js.map +1 -0
- package/dist/modules/utils/sanitize_key.d.ts +7 -0
- package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
- package/dist/modules/utils/sanitize_key.js +15 -0
- package/dist/modules/utils/sanitize_key.js.map +1 -1
- package/dist/pm2.d.ts +11 -1
- package/dist/pm2.d.ts.map +1 -1
- package/dist/pm2.js +66 -15
- package/dist/pm2.js.map +1 -1
- package/dist/scripts/analyze-credit.d.ts +4 -0
- package/dist/scripts/analyze-credit.d.ts.map +1 -0
- package/dist/scripts/analyze-credit.js +760 -0
- package/dist/scripts/analyze-credit.js.map +1 -0
- package/dist/scripts/analyze-orders.d.ts +1 -10
- package/dist/scripts/analyze-orders.d.ts.map +1 -1
- package/dist/scripts/analyze-orders.js +11 -34
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
- package/dist/scripts/test-credit-renewal.js +37 -5
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/tv.d.ts +11 -0
- package/dist/scripts/tv.d.ts.map +1 -0
- package/dist/scripts/tv.js +357 -0
- package/dist/scripts/tv.js.map +1 -0
- package/dist/scripts/update.js +77 -10
- package/dist/scripts/update.js.map +1 -1
- package/dist/unlock.d.ts.map +1 -1
- package/dist/unlock.js +20 -8
- package/dist/unlock.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +8 -6
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
- package/docs/GRID_RECONCILE.md +1 -0
- package/docs/LOGGING.md +3 -0
- package/docs/MPA_CREDIT_USAGE.md +3 -1
- package/docs/README.md +2 -2
- package/docs/WORKFLOW.md +3 -1
- package/package.json +2 -1
- package/scripts/README.md +12 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
'use strict';
|
|
4
|
+
/**
|
|
5
|
+
* DEXBot Credit Analysis Script
|
|
6
|
+
*
|
|
7
|
+
* Live-chain overview of active MPA (margin / call-order) and credit
|
|
8
|
+
* (borrowed deal) positions, summed per asset per bot — the credit
|
|
9
|
+
* counterpart to `dexbot order` (scripts/analyze-orders.ts). Credit deals
|
|
10
|
+
* additionally report per-pair and average CR, but only for pairs that are
|
|
11
|
+
* both whitelisted in bots.json (debtPolicy.lending) and listed on the
|
|
12
|
+
* current credit offer (acceptable_collateral) — anything else is reported
|
|
13
|
+
* as ignored/unpriced and excluded from the average. A borrow-now preview
|
|
14
|
+
* per configured pair shows the rate a fresh borrow would get today.
|
|
15
|
+
*
|
|
16
|
+
* Chain source of truth (see bitshares-core):
|
|
17
|
+
* - MPA positions: database_api::get_margin_positions(account)
|
|
18
|
+
* (== get_call_orders_by_account(account, 1.3.0, api_limit_get_call_orders),
|
|
19
|
+
* libraries/app/database_api.cpp). call_order_object (chain/market_object.hpp):
|
|
20
|
+
* borrower, collateral (int, call_price.base.asset_id), debt (int,
|
|
21
|
+
* call_price.quote.asset_id), call_price { base, quote }.
|
|
22
|
+
* - Credit deals: database_api::get_credit_deals_by_borrower(account, ...)
|
|
23
|
+
* (libraries/app/database_api.cpp). credit_deal_object
|
|
24
|
+
* (chain/credit_offer_object.hpp): borrower, offer_id, offer_owner,
|
|
25
|
+
* debt_asset, debt_amount, collateral_asset, collateral_amount.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node dist/scripts/analyze-credit.js # credit-enabled bots only
|
|
29
|
+
* node dist/scripts/analyze-credit.js <bot> # single bot (name or key)
|
|
30
|
+
*/
|
|
31
|
+
const { BitShares, waitForConnected, disconnectClient, setSuppressConnectionLog } = require('../modules/bitshares_client');
|
|
32
|
+
const { FEE_PARAMETERS } = require('../modules/constants');
|
|
33
|
+
const { PATHS } = require('../modules/paths');
|
|
34
|
+
const { getErrorMessage } = require('../modules/utils/errors');
|
|
35
|
+
const { sanitizeKey } = require('../modules/utils/sanitize_key');
|
|
36
|
+
const { loadSettingsFile, resolveRawBotEntries, normalizeBotEntries } = require('../modules/bot_settings');
|
|
37
|
+
import { pathToFileURL } from 'node:url';
|
|
38
|
+
// Terminal colors: centralized palette (modules/cli_colors.ts), shared with
|
|
39
|
+
// `dexbot order` so both analyzers stay visually in lockstep.
|
|
40
|
+
import { CLI_COLORS as colors } from '../modules/cli_colors.js';
|
|
41
|
+
import { muteChainLogs } from '../modules/utils/chain_logs.js';
|
|
42
|
+
const BOTS_FILE = PATHS.PROFILES.BOTS_JSON;
|
|
43
|
+
const CONNECT_TIMEOUT_MS = 30000;
|
|
44
|
+
const PAGE_LIMIT = 300;
|
|
45
|
+
// Chain connection chatter is muted via the shared modules/utils/chain_logs.ts
|
|
46
|
+
// helper (console.error stays untouched so real failures still surface).
|
|
47
|
+
muteChainLogs();
|
|
48
|
+
function formatAmount(value) {
|
|
49
|
+
if (!Number.isFinite(value))
|
|
50
|
+
return 'N/A';
|
|
51
|
+
if (value === 0)
|
|
52
|
+
return '0';
|
|
53
|
+
const abs = Math.abs(value);
|
|
54
|
+
let quotient = value;
|
|
55
|
+
let suffix = '';
|
|
56
|
+
if (abs >= 1_000_000) {
|
|
57
|
+
quotient = value / 1_000_000;
|
|
58
|
+
suffix = 'M';
|
|
59
|
+
}
|
|
60
|
+
else if (abs >= 1000) {
|
|
61
|
+
quotient = value / 1000;
|
|
62
|
+
suffix = 'K';
|
|
63
|
+
}
|
|
64
|
+
const absQ = Math.abs(quotient);
|
|
65
|
+
const intDigits = Math.floor(Math.log10(Math.max(absQ, 1e-10))) + 1;
|
|
66
|
+
let formatted;
|
|
67
|
+
if (intDigits >= 4)
|
|
68
|
+
formatted = String(Math.round(quotient));
|
|
69
|
+
else {
|
|
70
|
+
const dp = Math.max(0, 4 - intDigits);
|
|
71
|
+
formatted = quotient.toFixed(dp).replace(/(\.[0-9]*?)0+$/, '$1').replace(/\.$/, '');
|
|
72
|
+
}
|
|
73
|
+
return formatted + suffix;
|
|
74
|
+
}
|
|
75
|
+
function hasLending(bot) {
|
|
76
|
+
return Boolean(bot && bot.debtPolicy && Array.isArray(bot.debtPolicy.lending) && bot.debtPolicy.lending.length > 0);
|
|
77
|
+
}
|
|
78
|
+
// Credit CR math lives in modules/credit_pricing.ts (single source of truth,
|
|
79
|
+
// shared with the live credit runtime) — no local copies here.
|
|
80
|
+
import { averageCollateralRatio as averageCreditCr, creditDealCollateralRatio as creditDealCr, dailyOfferFeeRate, extractOfferConversionRate, normalizeCollateralMap as normalizeOfferCollateralMap, } from '../modules/credit_pricing.js';
|
|
81
|
+
function lendingAssetsOfType(bot, type) {
|
|
82
|
+
if (!hasLending(bot))
|
|
83
|
+
return [];
|
|
84
|
+
return bot.debtPolicy.lending
|
|
85
|
+
.filter((item) => item && item.type === type && typeof item.asset === 'string' && item.asset.length > 0)
|
|
86
|
+
.map((item) => String(item.asset));
|
|
87
|
+
}
|
|
88
|
+
function allLendingAssets(bot) {
|
|
89
|
+
if (!hasLending(bot))
|
|
90
|
+
return [];
|
|
91
|
+
return bot.debtPolicy.lending
|
|
92
|
+
.filter((item) => item && typeof item.asset === 'string' && item.asset.length > 0)
|
|
93
|
+
.map((item) => String(item.asset));
|
|
94
|
+
}
|
|
95
|
+
async function dbCall(method, args) {
|
|
96
|
+
if (BitShares?.db && typeof BitShares.db.call === 'function') {
|
|
97
|
+
return BitShares.db.call(method, args);
|
|
98
|
+
}
|
|
99
|
+
throw new Error('BitShares DB client is unavailable');
|
|
100
|
+
}
|
|
101
|
+
async function fetchMarginPositions(account) {
|
|
102
|
+
try {
|
|
103
|
+
const res = await dbCall('get_margin_positions', [account]);
|
|
104
|
+
if (Array.isArray(res))
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
catch (_) { /* fall through to paged variant */ }
|
|
108
|
+
const res = await dbCall('get_call_orders_by_account', [account, '1.3.0', PAGE_LIMIT]);
|
|
109
|
+
return Array.isArray(res) ? res : [];
|
|
110
|
+
}
|
|
111
|
+
async function fetchBorrowerDeals(account) {
|
|
112
|
+
const all = [];
|
|
113
|
+
let start = null;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const args = start == null ? [account] : [account, PAGE_LIMIT, start];
|
|
116
|
+
const page = await dbCall('get_credit_deals_by_borrower', args);
|
|
117
|
+
if (!Array.isArray(page) || page.length === 0)
|
|
118
|
+
break;
|
|
119
|
+
// start_id is inclusive (>=) per database_api docs, so drop the overlap
|
|
120
|
+
// row when paginating to avoid double-counting one deal.
|
|
121
|
+
const rows = start != null && page[0]?.id === start ? page.slice(1) : page;
|
|
122
|
+
if (rows.length === 0)
|
|
123
|
+
break;
|
|
124
|
+
all.push(...rows);
|
|
125
|
+
if (page.length < PAGE_LIMIT)
|
|
126
|
+
break;
|
|
127
|
+
const lastId = page[page.length - 1]?.id;
|
|
128
|
+
if (typeof lastId !== 'string' || lastId === start)
|
|
129
|
+
break;
|
|
130
|
+
start = lastId;
|
|
131
|
+
if (all.length >= 5000)
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
return all;
|
|
135
|
+
}
|
|
136
|
+
function mpaDebtAssetId(order) {
|
|
137
|
+
const q = order?.call_price?.quote?.asset_id;
|
|
138
|
+
if (typeof q === 'string' && q)
|
|
139
|
+
return q;
|
|
140
|
+
const d = order?.debt?.asset_id;
|
|
141
|
+
if (typeof d === 'string' && d)
|
|
142
|
+
return d;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function mpaCollateralAssetId(order) {
|
|
146
|
+
const b = order?.call_price?.base?.asset_id;
|
|
147
|
+
if (typeof b === 'string' && b)
|
|
148
|
+
return b;
|
|
149
|
+
const c = order?.collateral?.asset_id;
|
|
150
|
+
if (typeof c === 'string' && c)
|
|
151
|
+
return c;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function mpaDebtRaw(order) {
|
|
155
|
+
const v = Number(order?.debt);
|
|
156
|
+
if (Number.isFinite(v))
|
|
157
|
+
return v;
|
|
158
|
+
const a = Number(order?.debt?.amount);
|
|
159
|
+
return Number.isFinite(a) ? a : NaN;
|
|
160
|
+
}
|
|
161
|
+
function mpaCollateralRaw(order) {
|
|
162
|
+
const v = Number(order?.collateral);
|
|
163
|
+
if (Number.isFinite(v))
|
|
164
|
+
return v;
|
|
165
|
+
const a = Number(order?.collateral?.amount);
|
|
166
|
+
return Number.isFinite(a) ? a : NaN;
|
|
167
|
+
}
|
|
168
|
+
async function main() {
|
|
169
|
+
const rawArgs = process.argv.slice(2);
|
|
170
|
+
if (rawArgs.includes('-h') || rawArgs.includes('--help')) {
|
|
171
|
+
console.log('Usage: dexbot credit [<bot>]');
|
|
172
|
+
console.log(' No args Summed MPA + borrowed-credit positions for credit-enabled bots.');
|
|
173
|
+
console.log(' <bot> Only the named bot (matches name or sanitized key).');
|
|
174
|
+
console.log('Live chain data via get_margin_positions + get_credit_deals_by_borrower per preferredAccount.');
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
const botFilter = rawArgs.find((a) => !a.startsWith('-'))?.trim().toLowerCase() || null;
|
|
178
|
+
const { config } = loadSettingsFile(BOTS_FILE);
|
|
179
|
+
let bots = normalizeBotEntries(resolveRawBotEntries(config))
|
|
180
|
+
.filter((b) => b && b.active !== false && hasLending(b));
|
|
181
|
+
if (botFilter) {
|
|
182
|
+
const sanitized = sanitizeKey(botFilter);
|
|
183
|
+
const matched = bots.filter((b) => String(b.name || '').toLowerCase() === botFilter ||
|
|
184
|
+
sanitizeKey(String(b.name || '')) === sanitized ||
|
|
185
|
+
String(b.botKey || '').toLowerCase() === botFilter);
|
|
186
|
+
if (matched.length === 0) {
|
|
187
|
+
console.log(`${colors.sell}No credit bot found for '${botFilter}'.${colors.reset}`);
|
|
188
|
+
console.log('Available bots:');
|
|
189
|
+
const all = normalizeBotEntries(resolveRawBotEntries(config));
|
|
190
|
+
all.forEach((b) => console.log(` - ${b.name}${hasLending(b) ? '' : ' (no debtPolicy)'}`));
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
bots = matched;
|
|
194
|
+
}
|
|
195
|
+
if (bots.length === 0) {
|
|
196
|
+
console.log(`No credit-enabled bots found in ${BOTS_FILE} (debtPolicy.lending is empty everywhere).`);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
const accountUsers = new Map();
|
|
200
|
+
for (const b of bots) {
|
|
201
|
+
const acc = String(b.preferredAccount || '');
|
|
202
|
+
if (acc)
|
|
203
|
+
accountUsers.set(acc, (accountUsers.get(acc) || 0) + 1);
|
|
204
|
+
}
|
|
205
|
+
setSuppressConnectionLog(true);
|
|
206
|
+
await waitForConnected(CONNECT_TIMEOUT_MS);
|
|
207
|
+
// Asset metadata cache: id -> { symbol, precision }
|
|
208
|
+
const assetCache = new Map();
|
|
209
|
+
async function assetInfo(assetId) {
|
|
210
|
+
if (assetCache.has(assetId))
|
|
211
|
+
return assetCache.get(assetId);
|
|
212
|
+
const res = await dbCall('get_assets', [[assetId]]);
|
|
213
|
+
const a = Array.isArray(res) ? res[0] : null;
|
|
214
|
+
const info = { symbol: String(a?.symbol || assetId), precision: Number.isFinite(Number(a?.precision)) ? Number(a.precision) : 5 };
|
|
215
|
+
assetCache.set(assetId, info);
|
|
216
|
+
return info;
|
|
217
|
+
}
|
|
218
|
+
async function toFloat(raw, assetId) {
|
|
219
|
+
if (!Number.isFinite(raw))
|
|
220
|
+
return null;
|
|
221
|
+
const info = await assetInfo(assetId);
|
|
222
|
+
return raw / Math.pow(10, info.precision);
|
|
223
|
+
}
|
|
224
|
+
async function symbolOf(assetId) {
|
|
225
|
+
return (await assetInfo(assetId)).symbol;
|
|
226
|
+
}
|
|
227
|
+
async function resolveLendingRef(ref) {
|
|
228
|
+
const s = String(ref);
|
|
229
|
+
if (/^1\.3\.\d+$/.test(s)) {
|
|
230
|
+
try {
|
|
231
|
+
return { id: s, symbol: await symbolOf(s) };
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return { id: s, symbol: s };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const res = await dbCall('lookup_asset_symbols', [[s]]);
|
|
239
|
+
const a = Array.isArray(res) ? res[0] : null;
|
|
240
|
+
if (a?.id) {
|
|
241
|
+
assetCache.set(String(a.id), { symbol: String(a.symbol || s), precision: Number(a.precision ?? 5) });
|
|
242
|
+
return { id: String(a.id), symbol: String(a.symbol || s) };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { /* keep raw symbol */ }
|
|
246
|
+
return { id: null, symbol: s };
|
|
247
|
+
}
|
|
248
|
+
async function fetchOfferObjects(offerIds) {
|
|
249
|
+
const out = new Map();
|
|
250
|
+
const unique = [...new Set(offerIds.filter(Boolean).map(String))];
|
|
251
|
+
for (let i = 0; i < unique.length; i += 100) {
|
|
252
|
+
const batch = unique.slice(i, i + 100);
|
|
253
|
+
try {
|
|
254
|
+
const res = await dbCall('get_objects', [batch]);
|
|
255
|
+
if (Array.isArray(res)) {
|
|
256
|
+
for (const o of res) {
|
|
257
|
+
if (o?.id)
|
|
258
|
+
out.set(String(o.id), o);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* leave missing; deal falls back to pool/market pricing */ }
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
// Paginate to exhaustion (no page cap — books with >300 offers must not
|
|
267
|
+
// silently lose pairs). MAX_OFFER_PAGES is only an infinite-loop guard
|
|
268
|
+
// against a misbehaving node; the loop normally ends on a short page or
|
|
269
|
+
// a repeated cursor.
|
|
270
|
+
const MAX_OFFER_PAGES = 100;
|
|
271
|
+
async function fetchLiveOffers(debtAssetId) {
|
|
272
|
+
const out = [];
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
let start = null;
|
|
275
|
+
for (let page = 0; page < MAX_OFFER_PAGES; page++) {
|
|
276
|
+
const args = start == null ? [debtAssetId, 100] : [debtAssetId, 100, start];
|
|
277
|
+
let rows;
|
|
278
|
+
try {
|
|
279
|
+
rows = await dbCall('get_credit_offers_by_asset', args);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
285
|
+
break;
|
|
286
|
+
for (const o of rows) {
|
|
287
|
+
if (o?.id && !seen.has(String(o.id))) {
|
|
288
|
+
seen.add(String(o.id));
|
|
289
|
+
out.push(o);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (rows.length < 100)
|
|
293
|
+
break;
|
|
294
|
+
const lastId = rows[rows.length - 1]?.id;
|
|
295
|
+
if (typeof lastId !== 'string' || lastId === start)
|
|
296
|
+
break;
|
|
297
|
+
start = lastId;
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
// Wallet (free) balances per asset id, raw ints. Parsed from
|
|
302
|
+
// get_full_accounts the same way chain_orders.getOnChainAssetBalances does.
|
|
303
|
+
const walletCache = new Map();
|
|
304
|
+
async function fetchWalletRaw(account) {
|
|
305
|
+
if (walletCache.has(account))
|
|
306
|
+
return walletCache.get(account);
|
|
307
|
+
const out = new Map();
|
|
308
|
+
try {
|
|
309
|
+
const res = await dbCall('get_full_accounts', [[account], false]);
|
|
310
|
+
const entry = Array.isArray(res) && Array.isArray(res[0]) && res[0].length >= 2 ? res[0][1] : res?.[0];
|
|
311
|
+
const balances = entry?.balances || [];
|
|
312
|
+
for (const b of balances) {
|
|
313
|
+
const aid = String(b?.asset_type || b?.asset_id || b?.asset || '');
|
|
314
|
+
const raw = Number(b?.balance ?? b?.amount);
|
|
315
|
+
if (aid && Number.isFinite(raw) && raw > 0) {
|
|
316
|
+
out.set(aid, (out.get(aid) || 0) + raw);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch { /* empty map; borrow-now shows N/A wallet */ }
|
|
321
|
+
walletCache.set(account, out);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
const precisionOf = (assetId) => {
|
|
325
|
+
const info = assetCache.get(String(assetId));
|
|
326
|
+
return info ? info.precision : null;
|
|
327
|
+
};
|
|
328
|
+
async function ensurePrecisions(assetIds) {
|
|
329
|
+
for (const id of assetIds) {
|
|
330
|
+
if (id && !assetCache.has(String(id))) {
|
|
331
|
+
try {
|
|
332
|
+
await assetInfo(String(id));
|
|
333
|
+
}
|
|
334
|
+
catch { /* leave missing */ }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
console.log(`\n${colors.cyan}💳 Credit Overview (live chain)${botFilter ? ` — filter: ${botFilter}` : ''}${colors.reset}`);
|
|
339
|
+
console.log(`${colors.cyan}${'='.repeat(62)}${colors.reset}`);
|
|
340
|
+
let totalMpa = 0;
|
|
341
|
+
let totalDeals = 0;
|
|
342
|
+
let analyzed = 0;
|
|
343
|
+
for (const bot of bots) {
|
|
344
|
+
const botName = String(bot.name || bot.botKey || 'unnamed');
|
|
345
|
+
const account = String(bot.preferredAccount || '');
|
|
346
|
+
if (!account) {
|
|
347
|
+
console.log(`\n${colors.yellowBold}📊 ${botName}${colors.reset} ${colors.gray}(no preferredAccount — skipped)${colors.reset}`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const shared = (accountUsers.get(account) || 0) > 1;
|
|
351
|
+
let callOrders = [];
|
|
352
|
+
let deals = [];
|
|
353
|
+
let fetchError = null;
|
|
354
|
+
try {
|
|
355
|
+
[callOrders, deals] = await Promise.all([fetchMarginPositions(account), fetchBorrowerDeals(account)]);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
fetchError = getErrorMessage(err) || String(err);
|
|
359
|
+
}
|
|
360
|
+
if (fetchError) {
|
|
361
|
+
console.log(`\n${colors.yellowBold}📊 ${botName}${colors.reset} ${colors.gray}(${account})${colors.reset}`);
|
|
362
|
+
console.log(` ${colors.sell}chain fetch failed: ${fetchError}${colors.reset}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// Per-bot policy filter: chain positions are per-account, so when an
|
|
366
|
+
// account is shared (or holds stray positions) only the debt assets in
|
|
367
|
+
// this bot's debtPolicy count towards this bot's sums.
|
|
368
|
+
const mpaRefs = lendingAssetsOfType(bot, 'mpa');
|
|
369
|
+
const creditRefs = lendingAssetsOfType(bot, 'creditOffer');
|
|
370
|
+
const unionRefs = allLendingAssets(bot);
|
|
371
|
+
const mpaFilterRefs = mpaRefs.length > 0 ? mpaRefs : unionRefs;
|
|
372
|
+
const creditFilterRefs = creditRefs.length > 0 ? creditRefs : unionRefs;
|
|
373
|
+
const resolvedMpa = await Promise.all(mpaFilterRefs.map(resolveLendingRef));
|
|
374
|
+
const resolvedCredit = await Promise.all(creditFilterRefs.map(resolveLendingRef));
|
|
375
|
+
const mpaIds = new Set(resolvedMpa.map((r) => r.id).filter(Boolean));
|
|
376
|
+
const mpaSyms = new Set(resolvedMpa.map((r) => r.symbol.toUpperCase()));
|
|
377
|
+
const creditIds = new Set(resolvedCredit.map((r) => r.id).filter(Boolean));
|
|
378
|
+
const creditSyms = new Set(resolvedCredit.map((r) => r.symbol.toUpperCase()));
|
|
379
|
+
const filterActive = unionRefs.length > 0;
|
|
380
|
+
async function debtMatches(debtAssetId, ids, syms) {
|
|
381
|
+
if (!filterActive)
|
|
382
|
+
return true;
|
|
383
|
+
if (!debtAssetId)
|
|
384
|
+
return false;
|
|
385
|
+
if (ids.has(debtAssetId))
|
|
386
|
+
return true;
|
|
387
|
+
try {
|
|
388
|
+
const sym = (await symbolOf(debtAssetId)).toUpperCase();
|
|
389
|
+
return syms.has(sym);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const mpaOrders = [];
|
|
396
|
+
for (const o of callOrders) {
|
|
397
|
+
if (await debtMatches(mpaDebtAssetId(o), mpaIds, mpaSyms))
|
|
398
|
+
mpaOrders.push(o);
|
|
399
|
+
}
|
|
400
|
+
const creditDeals = [];
|
|
401
|
+
for (const d of deals) {
|
|
402
|
+
const debtId = typeof d?.debt_asset === 'string' ? d.debt_asset : null;
|
|
403
|
+
let symOk = false;
|
|
404
|
+
if (!filterActive)
|
|
405
|
+
symOk = true;
|
|
406
|
+
else if (debtId && creditIds.has(debtId))
|
|
407
|
+
symOk = true;
|
|
408
|
+
else if (debtId) {
|
|
409
|
+
try {
|
|
410
|
+
symOk = creditSyms.has((await symbolOf(debtId)).toUpperCase());
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
symOk = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (symOk)
|
|
417
|
+
creditDeals.push(d);
|
|
418
|
+
}
|
|
419
|
+
// Sum per asset: debt totals keyed by debt asset, collateral totals keyed
|
|
420
|
+
// by collateral asset. Debt entries also track the biggest single
|
|
421
|
+
// position for the "(×N, ▲ …)" display.
|
|
422
|
+
async function sumPositions(kind, items) {
|
|
423
|
+
const debt = new Map();
|
|
424
|
+
const coll = new Map();
|
|
425
|
+
for (const it of items) {
|
|
426
|
+
if (kind === 'mpa') {
|
|
427
|
+
const dId = mpaDebtAssetId(it);
|
|
428
|
+
const cId = mpaCollateralAssetId(it);
|
|
429
|
+
const dRaw = mpaDebtRaw(it);
|
|
430
|
+
const cRaw = mpaCollateralRaw(it);
|
|
431
|
+
if (dId) {
|
|
432
|
+
const f = await toFloat(dRaw, dId);
|
|
433
|
+
if (f != null) {
|
|
434
|
+
const e = debt.get(dId) || { total: 0, count: 0, max: 0 };
|
|
435
|
+
e.total += f;
|
|
436
|
+
e.count += 1;
|
|
437
|
+
e.max = Math.max(e.max, f);
|
|
438
|
+
debt.set(dId, e);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (cId) {
|
|
442
|
+
const f = await toFloat(cRaw, cId);
|
|
443
|
+
if (f != null) {
|
|
444
|
+
const e = coll.get(cId) || { total: 0, count: 0, max: 0 };
|
|
445
|
+
e.total += f;
|
|
446
|
+
e.count += 1;
|
|
447
|
+
e.max = Math.max(e.max, f);
|
|
448
|
+
coll.set(cId, e);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const dId = typeof it?.debt_asset === 'string' ? it.debt_asset : null;
|
|
454
|
+
const cId = typeof it?.collateral_asset === 'string' ? it.collateral_asset : null;
|
|
455
|
+
const dRaw = Number(it?.debt_amount);
|
|
456
|
+
const cRaw = Number(it?.collateral_amount);
|
|
457
|
+
if (dId && Number.isFinite(dRaw)) {
|
|
458
|
+
const f = await toFloat(dRaw, dId);
|
|
459
|
+
if (f != null) {
|
|
460
|
+
const e = debt.get(dId) || { total: 0, count: 0, max: 0 };
|
|
461
|
+
e.total += f;
|
|
462
|
+
e.count += 1;
|
|
463
|
+
e.max = Math.max(e.max, f);
|
|
464
|
+
debt.set(dId, e);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (cId && Number.isFinite(cRaw)) {
|
|
468
|
+
const f = await toFloat(cRaw, cId);
|
|
469
|
+
if (f != null) {
|
|
470
|
+
const e = coll.get(cId) || { total: 0, count: 0, max: 0 };
|
|
471
|
+
e.total += f;
|
|
472
|
+
e.count += 1;
|
|
473
|
+
e.max = Math.max(e.max, f);
|
|
474
|
+
coll.set(cId, e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return { debt, coll };
|
|
480
|
+
}
|
|
481
|
+
const mpaSum = await sumPositions('mpa', mpaOrders);
|
|
482
|
+
const creditSum = await sumPositions('credit', creditDeals);
|
|
483
|
+
totalMpa += mpaOrders.length;
|
|
484
|
+
totalDeals += creditDeals.length;
|
|
485
|
+
analyzed++;
|
|
486
|
+
// ---- Credit CR: whitelisted pairs only ----
|
|
487
|
+
// A deal counts towards the CR numbers only when its exact
|
|
488
|
+
// debt←collateral pair is whitelisted in this bot's debtPolicy
|
|
489
|
+
// (type creditOffer) AND the pair is listed on the current credit
|
|
490
|
+
// offer (acceptable_collateral). Anything else is reported separately
|
|
491
|
+
// as ignored/unpriced and excluded from the average CR.
|
|
492
|
+
const lendingItems = hasLending(bot) && Array.isArray(bot.debtPolicy?.lending)
|
|
493
|
+
? bot.debtPolicy.lending
|
|
494
|
+
: [];
|
|
495
|
+
// Configured credit pairs drive the whitelist, per-pair CR lines, and
|
|
496
|
+
// the borrow-now preview.
|
|
497
|
+
const creditPairs = [];
|
|
498
|
+
for (const item of lendingItems) {
|
|
499
|
+
if (!item || item.type !== 'creditOffer' || typeof item.asset !== 'string' || typeof item.collateralAsset !== 'string')
|
|
500
|
+
continue;
|
|
501
|
+
const [debtR, collR] = await Promise.all([
|
|
502
|
+
resolveLendingRef(String(item.asset)),
|
|
503
|
+
resolveLendingRef(String(item.collateralAsset)),
|
|
504
|
+
]);
|
|
505
|
+
const maxCR = Number(item.maxCollateralRatio);
|
|
506
|
+
creditPairs.push({
|
|
507
|
+
debtId: debtR.id, debtSym: debtR.symbol,
|
|
508
|
+
collId: collR.id, collSym: collR.symbol,
|
|
509
|
+
maxCR: Number.isFinite(maxCR) && maxCR > 0 ? maxCR : null,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const pairIdKeys = new Set(creditPairs.filter((p) => p.debtId && p.collId).map((p) => `${p.debtId}←${p.collId}`));
|
|
513
|
+
const pairSymKeys = new Set(creditPairs.map((p) => `${p.debtSym.toUpperCase()}←${p.collSym.toUpperCase()}`));
|
|
514
|
+
// Offer objects for the deals' conversion rates (debt per collateral).
|
|
515
|
+
const dealOfferIds = creditDeals.map((d) => d?.offer_id).filter(Boolean).map(String);
|
|
516
|
+
const offerById = await fetchOfferObjects(dealOfferIds);
|
|
517
|
+
await ensurePrecisions((() => {
|
|
518
|
+
const legIds = new Set();
|
|
519
|
+
for (const o of offerById.values()) {
|
|
520
|
+
for (const price of normalizeOfferCollateralMap(o?.acceptable_collateral).values()) {
|
|
521
|
+
if (price?.base?.asset_id)
|
|
522
|
+
legIds.add(String(price.base.asset_id));
|
|
523
|
+
if (price?.quote?.asset_id)
|
|
524
|
+
legIds.add(String(price.quote.asset_id));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return legIds;
|
|
528
|
+
})());
|
|
529
|
+
const crRows = [];
|
|
530
|
+
for (const d of creditDeals) {
|
|
531
|
+
const debtId = typeof d?.debt_asset === 'string' ? d.debt_asset : null;
|
|
532
|
+
const collId = typeof d?.collateral_asset === 'string' ? d.collateral_asset : null;
|
|
533
|
+
const [debtFloat, collFloat] = await Promise.all([
|
|
534
|
+
debtId ? toFloat(Number(d?.debt_amount), debtId) : null,
|
|
535
|
+
collId ? toFloat(Number(d?.collateral_amount), collId) : null,
|
|
536
|
+
]);
|
|
537
|
+
const debtSym = debtId ? await symbolOf(debtId) : '?';
|
|
538
|
+
const collSym = collId ? await symbolOf(collId) : '?';
|
|
539
|
+
let supported = false;
|
|
540
|
+
if (debtId && collId && pairIdKeys.has(`${debtId}←${collId}`))
|
|
541
|
+
supported = true;
|
|
542
|
+
else if (pairSymKeys.size > 0 && debtId && collId) {
|
|
543
|
+
try {
|
|
544
|
+
supported = pairSymKeys.has(`${(await symbolOf(debtId)).toUpperCase()}←${(await symbolOf(collId)).toUpperCase()}`);
|
|
545
|
+
}
|
|
546
|
+
catch { /* stays false */ }
|
|
547
|
+
}
|
|
548
|
+
// Conversion rate comes ONLY from the deal's current credit offer
|
|
549
|
+
// (acceptable_collateral). A pair whose collateral is not listed on
|
|
550
|
+
// the offer gets no CR — no pool/market estimates.
|
|
551
|
+
let rate = null;
|
|
552
|
+
let source = null;
|
|
553
|
+
const offerId = d?.offer_id != null ? String(d.offer_id) : null;
|
|
554
|
+
const offer = offerId ? offerById.get(offerId) : null;
|
|
555
|
+
if (offer && collId && debtId) {
|
|
556
|
+
const offerRate = extractOfferConversionRate(offer?.acceptable_collateral, collId, debtId, precisionOf);
|
|
557
|
+
if (offerRate !== null) {
|
|
558
|
+
rate = offerRate;
|
|
559
|
+
source = 'offer';
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const value = debtFloat != null && collFloat != null && rate !== null ? collFloat * rate : null;
|
|
563
|
+
const cr = debtFloat != null && collFloat != null ? creditDealCr(debtFloat, collFloat, rate) : null;
|
|
564
|
+
crRows.push({
|
|
565
|
+
dealId: d?.id != null ? String(d.id) : null, debtId, debtSym, debtFloat,
|
|
566
|
+
collId, collSym, collFloat, supported, rate, source, offerId, value, cr,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const supportedRows = crRows.filter((r) => r.supported);
|
|
570
|
+
const ignoredRows = crRows.filter((r) => !r.supported);
|
|
571
|
+
const pricedSupported = supportedRows.filter((r) => r.cr !== null && r.value !== null && r.debtFloat !== null);
|
|
572
|
+
const unpricedSupported = supportedRows.filter((r) => r.cr === null);
|
|
573
|
+
const avgCr = averageCreditCr(pricedSupported.map((r) => ({ debt: r.debtFloat, value: r.value })));
|
|
574
|
+
async function fmtParts(m, showBiggest = false) {
|
|
575
|
+
if (m.size === 0)
|
|
576
|
+
return [];
|
|
577
|
+
const parts = [];
|
|
578
|
+
for (const [id, e] of [...m.entries()].sort((a, b) => b[1].total - a[1].total)) {
|
|
579
|
+
const sym = await symbolOf(id);
|
|
580
|
+
const biggest = showBiggest && e.count > 1 && Number.isFinite(e.max) && e.max > 0
|
|
581
|
+
? `, ▲ ${formatAmount(e.max)}`
|
|
582
|
+
: '';
|
|
583
|
+
parts.push(`${formatAmount(e.total)} ${sym}${e.count > 1 ? ` ${colors.gray}(×${e.count}${biggest})${colors.reset}` : ''}`);
|
|
584
|
+
}
|
|
585
|
+
return parts;
|
|
586
|
+
}
|
|
587
|
+
// One asset per line; continuation lines align with the value column
|
|
588
|
+
// (labels are all 11 chars wide, so ` <label>: ` is 16 chars).
|
|
589
|
+
// Debt labels print red (money owed), collateral labels green (backing
|
|
590
|
+
// locked) — same buy-green/sell-red semantics as `dexbot order`.
|
|
591
|
+
async function printAssetLines(label, m, showBiggest = false, labelColor = colors.yellowBold) {
|
|
592
|
+
const plainPrefix = ` ${label}: `;
|
|
593
|
+
const prefix = ` ${labelColor}${colors.bold}${label}:${colors.reset} `;
|
|
594
|
+
const cont = ' '.repeat(plainPrefix.length);
|
|
595
|
+
const parts = await fmtParts(m, showBiggest);
|
|
596
|
+
parts.forEach((p, i) => {
|
|
597
|
+
console.log(`${i === 0 ? prefix : cont}${p}`);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
console.log(`\n${colors.yellowBold}📊 ${botName}${colors.reset} ${colors.gray}(${account})${colors.reset}${shared ? ` ${colors.gray}[shared account]${colors.reset}` : ''}`);
|
|
601
|
+
// Only list sections with active positions — empty sides stay hidden.
|
|
602
|
+
if (mpaOrders.length > 0) {
|
|
603
|
+
await printAssetLines('MPA debt', mpaSum.debt, false, colors.sell);
|
|
604
|
+
console.log('');
|
|
605
|
+
await printAssetLines('MPA coll', mpaSum.coll, false, colors.buy);
|
|
606
|
+
}
|
|
607
|
+
if (creditDeals.length > 0) {
|
|
608
|
+
if (mpaOrders.length > 0)
|
|
609
|
+
console.log('');
|
|
610
|
+
await printAssetLines('Credit debt', creditSum.debt, true, colors.sell);
|
|
611
|
+
console.log('');
|
|
612
|
+
await printAssetLines('Credit coll', creditSum.coll, true, colors.buy);
|
|
613
|
+
if (creditPairs.length > 0)
|
|
614
|
+
console.log('');
|
|
615
|
+
// Compact CR summary: one Avar. CR line per bot, then one Curr. CR
|
|
616
|
+
// line per whitelisted pair. A CR exists only for pairs both whitelisted
|
|
617
|
+
// in bots.json and listed on the current credit offer.
|
|
618
|
+
const feeDenom = Number(FEE_PARAMETERS?.GRAPHENE_FEE_RATE_DENOM) || 1000000;
|
|
619
|
+
const walletRaw = await fetchWalletRaw(account);
|
|
620
|
+
const matchPair = (pair, r) => (pair.debtId !== null && r.debtId !== null && pair.debtId === r.debtId && pair.collId !== null && r.collId !== null && pair.collId === r.collId) ||
|
|
621
|
+
(pair.debtSym.toUpperCase() === r.debtSym.toUpperCase() && pair.collSym.toUpperCase() === r.collSym.toUpperCase());
|
|
622
|
+
if (avgCr !== null) {
|
|
623
|
+
// Label in orange to distinguish from Curr. CR; value keeps the
|
|
624
|
+
// original health color (single shared max → green/red, mixed or
|
|
625
|
+
// undefined max → white).
|
|
626
|
+
const avgMaxSet = new Set();
|
|
627
|
+
for (const r of pricedSupported) {
|
|
628
|
+
const pair = creditPairs.find((p) => matchPair(p, r));
|
|
629
|
+
if (pair?.maxCR != null)
|
|
630
|
+
avgMaxSet.add(pair.maxCR);
|
|
631
|
+
}
|
|
632
|
+
const avgMax = avgMaxSet.size === 1 ? [...avgMaxSet][0] : null;
|
|
633
|
+
const avgColor = avgMax === null ? colors.white : avgCr > avgMax ? colors.sell : colors.buy;
|
|
634
|
+
const pairTotals = new Map();
|
|
635
|
+
for (const r of pricedSupported) {
|
|
636
|
+
const key = `${r.debtSym}←${r.collSym}`;
|
|
637
|
+
if (!pairTotals.has(key)) {
|
|
638
|
+
pairTotals.set(key, { debtSym: r.debtSym, collSym: r.collSym, debt: 0, coll: 0 });
|
|
639
|
+
}
|
|
640
|
+
const t = pairTotals.get(key);
|
|
641
|
+
t.debt += r.debtFloat || 0;
|
|
642
|
+
t.coll += r.collFloat || 0;
|
|
643
|
+
}
|
|
644
|
+
const segments = [...pairTotals.values()]
|
|
645
|
+
.map((t) => `${formatAmount(t.debt)} ${t.debtSym} ← ${formatAmount(t.coll)} ${t.collSym}`)
|
|
646
|
+
.join(' + ');
|
|
647
|
+
console.log(` ${colors.orange}${colors.bold}Avar. CR:${colors.reset} ${avgColor}${formatAmount(avgCr)}${colors.reset}, ${segments} ${colors.gray}(x${pricedSupported.length})${colors.reset}`);
|
|
648
|
+
}
|
|
649
|
+
else if (supportedRows.length > 0) {
|
|
650
|
+
console.log(` ${colors.orange}${colors.bold}Avar. CR:${colors.reset} n/a (no priced, available credit)`);
|
|
651
|
+
}
|
|
652
|
+
if ((avgCr !== null || supportedRows.length > 0) && creditPairs.length > 0)
|
|
653
|
+
console.log('');
|
|
654
|
+
for (const pair of creditPairs) {
|
|
655
|
+
const pairRows = supportedRows.filter((r) => matchPair(pair, r));
|
|
656
|
+
const pricedPair = pairRows.filter((r) => r.cr !== null && r.value !== null && r.debtFloat !== null);
|
|
657
|
+
const pairDebt = pricedPair.reduce((s, r) => s + (r.debtFloat || 0), 0);
|
|
658
|
+
const pairValue = pricedPair.reduce((s, r) => s + (r.value || 0), 0);
|
|
659
|
+
const pairCr = pairDebt > 0 ? pairValue / pairDebt : null;
|
|
660
|
+
const walletFloat = pair.collId && walletRaw.has(pair.collId)
|
|
661
|
+
? await toFloat(walletRaw.get(pair.collId), pair.collId)
|
|
662
|
+
: null;
|
|
663
|
+
// Current credit for the pair: active deal's offer first, else the
|
|
664
|
+
// cheapest live offer listing the pair.
|
|
665
|
+
let liveOffer = null;
|
|
666
|
+
let liveRate = null;
|
|
667
|
+
const offerRows = pairRows.filter((r) => r.source === 'offer' && r.rate !== null && r.offerId);
|
|
668
|
+
if (offerRows.length > 0) {
|
|
669
|
+
const ranked = offerRows.map((r) => ({ r, daily: dailyOfferFeeRate(offerById.get(r.offerId), feeDenom) }))
|
|
670
|
+
.sort((a, b) => a.daily - b.daily || String(a.r.offerId).localeCompare(String(b.r.offerId)));
|
|
671
|
+
liveOffer = offerById.get(ranked[0].r.offerId) || null;
|
|
672
|
+
liveRate = ranked[0].r.rate;
|
|
673
|
+
}
|
|
674
|
+
else if (pair.debtId && pair.collId) {
|
|
675
|
+
const liveOffers = (await fetchLiveOffers(pair.debtId)).filter((o) => o?.enabled !== false);
|
|
676
|
+
const cands = [];
|
|
677
|
+
for (const o of liveOffers) {
|
|
678
|
+
await ensurePrecisions((() => {
|
|
679
|
+
const ids = new Set();
|
|
680
|
+
for (const price of normalizeOfferCollateralMap(o?.acceptable_collateral).values()) {
|
|
681
|
+
if (price?.base?.asset_id)
|
|
682
|
+
ids.add(String(price.base.asset_id));
|
|
683
|
+
if (price?.quote?.asset_id)
|
|
684
|
+
ids.add(String(price.quote.asset_id));
|
|
685
|
+
}
|
|
686
|
+
return ids;
|
|
687
|
+
})());
|
|
688
|
+
const rate = extractOfferConversionRate(o?.acceptable_collateral, pair.collId, pair.debtId, precisionOf);
|
|
689
|
+
if (rate !== null)
|
|
690
|
+
cands.push({ offer: o, rate, daily: dailyOfferFeeRate(o, feeDenom), id: String(o.id) });
|
|
691
|
+
}
|
|
692
|
+
cands.sort((a, b) => a.daily - b.daily || a.id.localeCompare(b.id));
|
|
693
|
+
if (cands.length > 0) {
|
|
694
|
+
liveOffer = cands[0].offer;
|
|
695
|
+
liveRate = cands[0].rate;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const avail = liveOffer && pair.debtId
|
|
699
|
+
? await toFloat(Number(liveOffer.current_balance), pair.debtId)
|
|
700
|
+
: null;
|
|
701
|
+
// No active debt: borrow-now CR — wallet collateral value against
|
|
702
|
+
// the offer's available funds, i.e. the CR a fresh borrow of the
|
|
703
|
+
// full available amount would land at.
|
|
704
|
+
let displayCr = pairCr;
|
|
705
|
+
if (displayCr === null && walletFloat !== null && walletFloat > 0 && liveRate !== null && avail !== null && avail > 0) {
|
|
706
|
+
displayCr = (walletFloat * liveRate) / avail;
|
|
707
|
+
}
|
|
708
|
+
// Green at/below the user max (borrow allowed), red above it, white
|
|
709
|
+
// when no max is defined, gray when there is no CR at all.
|
|
710
|
+
const crColor = displayCr === null
|
|
711
|
+
? colors.gray
|
|
712
|
+
: pair.maxCR === null
|
|
713
|
+
? colors.white
|
|
714
|
+
: displayCr > pair.maxCR ? colors.sell : colors.buy;
|
|
715
|
+
const crText = displayCr !== null ? formatAmount(displayCr) : 'n/a';
|
|
716
|
+
const availText = avail !== null && avail > 0
|
|
717
|
+
? `${formatAmount(avail)} ${pair.debtSym} avail.`
|
|
718
|
+
: 'no funds avail.';
|
|
719
|
+
console.log(` ${colors.white}${colors.bold}Curr. CR:${colors.reset} ${crColor}${crText}${colors.reset}, ${pair.debtSym}←${pair.collSym} | ${availText}`);
|
|
720
|
+
}
|
|
721
|
+
// Split by reason so the runtime/analyzer asymmetry is visible:
|
|
722
|
+
// "not whitelisted" never counts anywhere; "no offer price" is
|
|
723
|
+
// whitelisted but absent from the current offer (the live runtime
|
|
724
|
+
// still prices these via its pool/market fallback, the analyzer
|
|
725
|
+
// deliberately does not).
|
|
726
|
+
const ignored = ignoredRows.length;
|
|
727
|
+
const unpriced = unpricedSupported.length;
|
|
728
|
+
if (ignored + unpriced > 0) {
|
|
729
|
+
const reasons = [];
|
|
730
|
+
if (ignored > 0)
|
|
731
|
+
reasons.push(`${ignored} not whitelisted`);
|
|
732
|
+
if (unpriced > 0)
|
|
733
|
+
reasons.push(`${unpriced} no offer price`);
|
|
734
|
+
console.log(` ${colors.gray}Excluded: ${ignored + unpriced} deal${ignored + unpriced === 1 ? '' : 's'} (${reasons.join(', ')})${colors.reset}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (mpaOrders.length === 0 && creditDeals.length === 0) {
|
|
738
|
+
console.log(` ${colors.gray}no active MPA/credit positions${colors.reset}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
console.log(`${colors.cyan}${'='.repeat(62)}${colors.reset}`);
|
|
742
|
+
console.log(`${colors.cyan}Summary: ${analyzed} bots, ${totalMpa} MPA positions, ${totalDeals} credit deals${colors.reset}\n`);
|
|
743
|
+
try {
|
|
744
|
+
disconnectClient();
|
|
745
|
+
}
|
|
746
|
+
catch { /* already disconnected */ }
|
|
747
|
+
process.exit(0);
|
|
748
|
+
}
|
|
749
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
750
|
+
main().catch((err) => {
|
|
751
|
+
console.error(`credit: ${getErrorMessage(err) || err}`);
|
|
752
|
+
try {
|
|
753
|
+
disconnectClient();
|
|
754
|
+
}
|
|
755
|
+
catch { /* noop */ }
|
|
756
|
+
process.exit(1);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
export { formatAmount, hasLending };
|
|
760
|
+
//# sourceMappingURL=analyze-credit.js.map
|