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.
Files changed (217) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +6 -2
  3. package/analysis/README.md +14 -7
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_derivatives.ts +1 -1
  6. package/analysis/bot_key_utils.ts +272 -3
  7. package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
  8. package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
  9. package/analysis/chart_utils.ts +6 -3
  10. package/analysis/grid_correction_check.ts +56 -20
  11. package/analysis/resolve_bot_accounts.ts +199 -0
  12. package/analysis/trade_profitability.ts +36 -8
  13. package/analysis/tradingview/README.md +25 -0
  14. package/analysis/tradingview/analyze_tradingview.ts +2 -2
  15. package/analysis/trend_detection/package.json +1 -1
  16. package/claw/modules/claw_launcher.ts +5 -4
  17. package/claw/modules/credit_runtime_adapter.ts +2 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_key_utils.d.ts +28 -1
  25. package/dist/analysis/bot_key_utils.d.ts.map +1 -1
  26. package/dist/analysis/bot_key_utils.js +385 -3
  27. package/dist/analysis/bot_key_utils.js.map +1 -1
  28. package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
  29. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chart_utils.d.ts +2 -1
  35. package/dist/analysis/chart_utils.d.ts.map +1 -1
  36. package/dist/analysis/chart_utils.js +4 -1
  37. package/dist/analysis/chart_utils.js.map +1 -1
  38. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  39. package/dist/analysis/grid_correction_check.js +60 -20
  40. package/dist/analysis/grid_correction_check.js.map +1 -1
  41. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  43. package/dist/analysis/resolve_bot_accounts.js +216 -0
  44. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  45. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  46. package/dist/analysis/trade_profitability.js +40 -8
  47. package/dist/analysis/trade_profitability.js.map +1 -1
  48. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  49. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  50. package/dist/bot.d.ts.map +1 -1
  51. package/dist/bot.js +2 -1
  52. package/dist/bot.js.map +1 -1
  53. package/dist/dexbot.d.ts.map +1 -1
  54. package/dist/dexbot.js +73 -17
  55. package/dist/dexbot.js.map +1 -1
  56. package/dist/market_adapter/ama_signal_runner.js +2 -2
  57. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  59. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  62. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  63. package/dist/modules/account_bots.d.ts +42 -1
  64. package/dist/modules/account_bots.d.ts.map +1 -1
  65. package/dist/modules/account_bots.js +168 -17
  66. package/dist/modules/account_bots.js.map +1 -1
  67. package/dist/modules/account_orders.d.ts +7 -1
  68. package/dist/modules/account_orders.d.ts.map +1 -1
  69. package/dist/modules/account_orders.js +34 -1
  70. package/dist/modules/account_orders.js.map +1 -1
  71. package/dist/modules/bitshares_client.d.ts +4 -1
  72. package/dist/modules/bitshares_client.d.ts.map +1 -1
  73. package/dist/modules/bitshares_client.js +12 -4
  74. package/dist/modules/bitshares_client.js.map +1 -1
  75. package/dist/modules/bot_settings.d.ts.map +1 -1
  76. package/dist/modules/bot_settings.js +2 -1
  77. package/dist/modules/bot_settings.js.map +1 -1
  78. package/dist/modules/cli_colors.d.ts +39 -0
  79. package/dist/modules/cli_colors.d.ts.map +1 -0
  80. package/dist/modules/cli_colors.js +42 -0
  81. package/dist/modules/cli_colors.js.map +1 -0
  82. package/dist/modules/constants.d.ts +2 -0
  83. package/dist/modules/constants.d.ts.map +1 -1
  84. package/dist/modules/constants.js +27 -11
  85. package/dist/modules/constants.js.map +1 -1
  86. package/dist/modules/credit_pricing.d.ts +61 -0
  87. package/dist/modules/credit_pricing.d.ts.map +1 -0
  88. package/dist/modules/credit_pricing.js +237 -0
  89. package/dist/modules/credit_pricing.js.map +1 -0
  90. package/dist/modules/credit_runtime.d.ts +1 -0
  91. package/dist/modules/credit_runtime.d.ts.map +1 -1
  92. package/dist/modules/credit_runtime.js +49 -107
  93. package/dist/modules/credit_runtime.js.map +1 -1
  94. package/dist/modules/dexbot_class.d.ts +3 -1
  95. package/dist/modules/dexbot_class.d.ts.map +1 -1
  96. package/dist/modules/dexbot_class.js +22 -6
  97. package/dist/modules/dexbot_class.js.map +1 -1
  98. package/dist/modules/dexbot_cow_runtime.d.ts +36 -2
  99. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  100. package/dist/modules/dexbot_cow_runtime.js +434 -20
  101. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  102. package/dist/modules/dexbot_maintenance_runtime.d.ts +15 -16
  103. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  104. package/dist/modules/dexbot_maintenance_runtime.js +69 -42
  105. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  106. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  107. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  108. package/dist/modules/dexbot_startup_runtime.js +12 -1
  109. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  110. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  111. package/dist/modules/dexbot_state_recovery.js +7 -0
  112. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  113. package/dist/modules/fund_registry.d.ts +1 -1
  114. package/dist/modules/fund_registry.js +1 -1
  115. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  116. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  117. package/dist/modules/launcher/adapter_requirement.js +141 -0
  118. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  119. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  120. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  121. package/dist/modules/launcher/bot_supervisor.js +32 -3
  122. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  123. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  124. package/dist/modules/launcher/launch_modes.js +5 -4
  125. package/dist/modules/launcher/launch_modes.js.map +1 -1
  126. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  127. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  128. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  129. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  130. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  131. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  132. package/dist/modules/launcher/status_reporting.js +9 -6
  133. package/dist/modules/launcher/status_reporting.js.map +1 -1
  134. package/dist/modules/order/accounting.d.ts +34 -0
  135. package/dist/modules/order/accounting.d.ts.map +1 -1
  136. package/dist/modules/order/accounting.js +182 -5
  137. package/dist/modules/order/accounting.js.map +1 -1
  138. package/dist/modules/order/export.d.ts +1 -1
  139. package/dist/modules/order/export.js +1 -1
  140. package/dist/modules/order/grid.d.ts.map +1 -1
  141. package/dist/modules/order/grid.js +68 -28
  142. package/dist/modules/order/grid.js.map +1 -1
  143. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  144. package/dist/modules/order/grid_reconcile_internal.js +72 -0
  145. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  146. package/dist/modules/order/logger.d.ts +23 -1
  147. package/dist/modules/order/logger.d.ts.map +1 -1
  148. package/dist/modules/order/logger.js +60 -17
  149. package/dist/modules/order/logger.js.map +1 -1
  150. package/dist/modules/order/manager.d.ts +41 -1
  151. package/dist/modules/order/manager.d.ts.map +1 -1
  152. package/dist/modules/order/manager.js +186 -17
  153. package/dist/modules/order/manager.js.map +1 -1
  154. package/dist/modules/order/strategy.d.ts.map +1 -1
  155. package/dist/modules/order/strategy.js +17 -2
  156. package/dist/modules/order/strategy.js.map +1 -1
  157. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  158. package/dist/modules/order/sync_engine.js +59 -9
  159. package/dist/modules/order/sync_engine.js.map +1 -1
  160. package/dist/modules/order/utils/math.d.ts +42 -1
  161. package/dist/modules/order/utils/math.d.ts.map +1 -1
  162. package/dist/modules/order/utils/math.js +91 -1
  163. package/dist/modules/order/utils/math.js.map +1 -1
  164. package/dist/modules/order/utils/order.d.ts +52 -1
  165. package/dist/modules/order/utils/order.d.ts.map +1 -1
  166. package/dist/modules/order/utils/order.js +137 -7
  167. package/dist/modules/order/utils/order.js.map +1 -1
  168. package/dist/modules/order/utils/system.d.ts +66 -4
  169. package/dist/modules/order/utils/system.d.ts.map +1 -1
  170. package/dist/modules/order/utils/system.js +156 -16
  171. package/dist/modules/order/utils/system.js.map +1 -1
  172. package/dist/modules/order/utils/validate.d.ts +30 -3
  173. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  174. package/dist/modules/order/utils/validate.js +206 -11
  175. package/dist/modules/order/utils/validate.js.map +1 -1
  176. package/dist/modules/utils/chain_logs.d.ts +11 -0
  177. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  178. package/dist/modules/utils/chain_logs.js +23 -0
  179. package/dist/modules/utils/chain_logs.js.map +1 -0
  180. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  181. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  182. package/dist/modules/utils/sanitize_key.js +15 -0
  183. package/dist/modules/utils/sanitize_key.js.map +1 -1
  184. package/dist/pm2.d.ts +11 -1
  185. package/dist/pm2.d.ts.map +1 -1
  186. package/dist/pm2.js +66 -15
  187. package/dist/pm2.js.map +1 -1
  188. package/dist/scripts/analyze-credit.d.ts +4 -0
  189. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  190. package/dist/scripts/analyze-credit.js +760 -0
  191. package/dist/scripts/analyze-credit.js.map +1 -0
  192. package/dist/scripts/analyze-orders.d.ts +1 -10
  193. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  194. package/dist/scripts/analyze-orders.js +11 -34
  195. package/dist/scripts/analyze-orders.js.map +1 -1
  196. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  197. package/dist/scripts/test-credit-renewal.js +37 -5
  198. package/dist/scripts/test-credit-renewal.js.map +1 -1
  199. package/dist/scripts/tv.d.ts +11 -0
  200. package/dist/scripts/tv.d.ts.map +1 -0
  201. package/dist/scripts/tv.js +357 -0
  202. package/dist/scripts/tv.js.map +1 -0
  203. package/dist/scripts/update.js +77 -10
  204. package/dist/scripts/update.js.map +1 -1
  205. package/dist/unlock.d.ts.map +1 -1
  206. package/dist/unlock.js +20 -8
  207. package/dist/unlock.js.map +1 -1
  208. package/docs/DEXBOT_COMPARISON.md +3 -3
  209. package/docs/EVOLUTION.md +8 -6
  210. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
  211. package/docs/GRID_RECONCILE.md +1 -0
  212. package/docs/LOGGING.md +3 -0
  213. package/docs/MPA_CREDIT_USAGE.md +3 -1
  214. package/docs/README.md +2 -2
  215. package/docs/WORKFLOW.md +3 -1
  216. package/package.json +2 -1
  217. 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