@whyshu/ba 0.0.0-stage → 0.1.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/README.md +93 -2
- package/cordis.patch.yml +6 -0
- package/dist/client.js +559 -0
- package/icon.svg +6 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +51 -4
- package/src/binance.mjs +385 -0
- package/src/compat.mjs +305 -0
- package/src/index.js +115 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyshu/ba",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH 左侧栏币安面板:主视图展示现货资产、合约持仓与行情看板(只读,API Key 仅宿主侧)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24.2.0",
|
|
9
|
+
"dsh": ">=0.2.0-0 <0.3.0-0"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"dsh",
|
|
13
|
+
"deepseek-harness",
|
|
14
|
+
"dsh-plugin",
|
|
15
|
+
"cordis",
|
|
16
|
+
"binance",
|
|
17
|
+
"spot",
|
|
18
|
+
"futures",
|
|
19
|
+
"portfolio"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./src/index.js",
|
|
23
|
+
"./client": "./dist/client.js",
|
|
24
|
+
"./locale/*.json": "./locale/*.json",
|
|
25
|
+
"./icon": "./icon.svg"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src/",
|
|
29
|
+
"dist/",
|
|
30
|
+
"locale/",
|
|
31
|
+
"icon.svg",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mjs"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dsh": {
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web",
|
|
47
|
+
"immediately": true
|
|
48
|
+
},
|
|
49
|
+
"plugin": {
|
|
50
|
+
"displayName": "币安账户面板"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/binance.mjs
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whyshu-ba · 币安只读数据层(宿主侧)
|
|
3
|
+
*
|
|
4
|
+
* 设计红线:
|
|
5
|
+
* - API Key / Secret **只在宿主进程**读取与签名,永不回传浏览器;
|
|
6
|
+
* - 只读:只调用 GET 行情/账户/持仓接口,不调用任何下单/划转/提现接口;
|
|
7
|
+
* - 每次请求都重新读 `~/.dsh/argent/ba.env`,改 Key / 代理无需重启 dsh;
|
|
8
|
+
* - 代理可配(默认读 BINANCE_PROXY,其次 HTTPS_PROXY),未配置时直连。
|
|
9
|
+
*
|
|
10
|
+
* 配置(~/.dsh/argent/ba.env,权限 600):
|
|
11
|
+
* BINANCE_API_KEY=...
|
|
12
|
+
* BINANCE_API_SECRET=...
|
|
13
|
+
* BINANCE_PROXY=http://127.0.0.1:7890 # 可选;本机直连 api.binance.com 不通时必须配
|
|
14
|
+
* BINANCE_SYMBOLS=BTCUSDT,ETHUSDT,SOLUSDT # 可选,行情看板自选币对
|
|
15
|
+
*/
|
|
16
|
+
import { createHmac } from 'node:crypto'
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import http from 'node:http'
|
|
21
|
+
import https from 'node:https'
|
|
22
|
+
import tls from 'node:tls'
|
|
23
|
+
|
|
24
|
+
export const SPOT_BASE = 'https://api.binance.com'
|
|
25
|
+
export const FUTURES_BASE = 'https://fapi.binance.com'
|
|
26
|
+
export const ENV_FILE = join(homedir(), '.dsh', 'argent', 'ba.env')
|
|
27
|
+
export const DEFAULT_SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT']
|
|
28
|
+
const TIMEOUT_MS = 12000
|
|
29
|
+
const RECV_WINDOW = 5000
|
|
30
|
+
|
|
31
|
+
/** 币安接口返回的业务错误(code/msg);与网络错误分开,便于前端提示。 */
|
|
32
|
+
export class BinanceError extends Error {
|
|
33
|
+
constructor(message, { status = 0, code = 0 } = {}) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'BinanceError'
|
|
36
|
+
this.status = status
|
|
37
|
+
this.code = code
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 极简 KEY=VALUE 解析(只支持注释、空行、单/双引号),不引依赖。 */
|
|
42
|
+
export function parseEnvText(text) {
|
|
43
|
+
const out = {}
|
|
44
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
45
|
+
const line = raw.trim()
|
|
46
|
+
if (!line || line.startsWith('#')) continue
|
|
47
|
+
const eq = line.indexOf('=')
|
|
48
|
+
if (eq <= 0) continue
|
|
49
|
+
const key = line.slice(0, eq).trim()
|
|
50
|
+
let value = line.slice(eq + 1).trim()
|
|
51
|
+
if (value.length >= 2) {
|
|
52
|
+
const q = value[0]
|
|
53
|
+
if ((q === '"' || q === "'") && value[value.length - 1] === q) value = value.slice(1, -1)
|
|
54
|
+
}
|
|
55
|
+
if (key) out[key] = value
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 读设置:进程环境变量优先,其次 env 文件。envFile 可传(测试用隔离路径)。 */
|
|
61
|
+
export function loadSettings(envFile = ENV_FILE) {
|
|
62
|
+
let file = {}
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(envFile)) file = parseEnvText(readFileSync(envFile, 'utf8'))
|
|
65
|
+
} catch { /* 读不到就按未配置处理 */ }
|
|
66
|
+
const pick = (name) => {
|
|
67
|
+
const raw = process.env[name] !== undefined ? process.env[name] : file[name]
|
|
68
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
|
|
69
|
+
}
|
|
70
|
+
const symbols = pick('BINANCE_SYMBOLS')
|
|
71
|
+
.split(',')
|
|
72
|
+
.map((s) => s.trim().toUpperCase())
|
|
73
|
+
.filter((s) => /^[A-Z0-9]{5,20}$/.test(s))
|
|
74
|
+
const proxy = pick('BINANCE_PROXY')
|
|
75
|
+
|| process.env.HTTPS_PROXY || process.env.https_proxy
|
|
76
|
+
|| process.env.HTTP_PROXY || process.env.http_proxy
|
|
77
|
+
|| ''
|
|
78
|
+
return {
|
|
79
|
+
apiKey: pick('BINANCE_API_KEY'),
|
|
80
|
+
apiSecret: pick('BINANCE_API_SECRET'),
|
|
81
|
+
proxy,
|
|
82
|
+
symbols: symbols.length ? symbols : DEFAULT_SYMBOLS,
|
|
83
|
+
envFile,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function hasCredentials(settings) {
|
|
88
|
+
return Boolean(settings && settings.apiKey && settings.apiSecret)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 通过 HTTP 代理建立到目标主机的 CONNECT 隧道。
|
|
93
|
+
* @param {string} proxy 代理 URL(http://host:port)
|
|
94
|
+
* @param {URL} target 目标 URL
|
|
95
|
+
* @param {number} timeoutMs 超时
|
|
96
|
+
* @returns {Promise<import('node:net').Socket>} 已连通的裸 TCP socket
|
|
97
|
+
*/
|
|
98
|
+
function openTunnel(proxy, target, timeoutMs) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
let proxyUrl
|
|
101
|
+
try { proxyUrl = new URL(proxy) } catch { reject(new Error('BINANCE_PROXY 不是合法 URL: ' + proxy)); return }
|
|
102
|
+
if (proxyUrl.protocol !== 'http:') { reject(new Error('只支持 http:// 代理,收到: ' + proxyUrl.protocol)); return }
|
|
103
|
+
const port = target.port || '443'
|
|
104
|
+
const host = target.hostname
|
|
105
|
+
const req = http.request({
|
|
106
|
+
host: proxyUrl.hostname,
|
|
107
|
+
port: proxyUrl.port || 80,
|
|
108
|
+
method: 'CONNECT',
|
|
109
|
+
path: host + ':' + port,
|
|
110
|
+
headers: { host: host + ':' + port },
|
|
111
|
+
agent: false,
|
|
112
|
+
timeout: timeoutMs,
|
|
113
|
+
})
|
|
114
|
+
const fail = (e) => { try { req.destroy(e) } catch { reject(e) } }
|
|
115
|
+
req.on('timeout', () => fail(new Error('代理连接超时: ' + proxyUrl.host)))
|
|
116
|
+
req.on('error', (e) => reject(new Error('代理连接失败 ' + proxyUrl.host + ': ' + e.message)))
|
|
117
|
+
req.on('connect', (res, socket) => {
|
|
118
|
+
if (res.statusCode !== 200) {
|
|
119
|
+
socket.destroy()
|
|
120
|
+
reject(new Error('代理 CONNECT 返回 ' + res.statusCode))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
resolve(socket)
|
|
124
|
+
})
|
|
125
|
+
req.end()
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* GET 一个 JSON(带代理/超时);HTTP 非 2xx 或币安 {code,msg} 抛 BinanceError。
|
|
131
|
+
* 超时是**整段硬超时**:DNS / CONNECT / TLS / 响应任一环节卡住都会按时失败。
|
|
132
|
+
* @param {string} url
|
|
133
|
+
* @param {{headers?: Record<string,string>, proxy?: string, timeoutMs?: number}} [opts]
|
|
134
|
+
*/
|
|
135
|
+
export async function requestJson(url, opts = {}) {
|
|
136
|
+
const target = new URL(url)
|
|
137
|
+
const timeoutMs = opts.timeoutMs || TIMEOUT_MS
|
|
138
|
+
const headers = { accept: 'application/json', 'user-agent': 'whyshu-ba/0.1 (dsh plugin)', ...(opts.headers || {}) }
|
|
139
|
+
|
|
140
|
+
let tunnel = null
|
|
141
|
+
let agent = null
|
|
142
|
+
let req = null
|
|
143
|
+
let timer = null
|
|
144
|
+
let settled = false
|
|
145
|
+
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
if (timer) { clearTimeout(timer); timer = null }
|
|
148
|
+
if (agent) { try { agent.destroy() } catch { /* noop */ } }
|
|
149
|
+
if (tunnel && tunnel.destroyed === false) { try { tunnel.destroy() } catch { /* noop */ } }
|
|
150
|
+
if (req && req.destroyed === false) { try { req.destroy() } catch { /* noop */ } }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const finish = (fn, value) => {
|
|
155
|
+
if (settled) return
|
|
156
|
+
settled = true
|
|
157
|
+
cleanup()
|
|
158
|
+
fn(value)
|
|
159
|
+
}
|
|
160
|
+
const fail = (e) => finish(reject, e instanceof BinanceError ? e : new Error((e && e.message) || String(e)))
|
|
161
|
+
|
|
162
|
+
timer = setTimeout(() => {
|
|
163
|
+
fail(new Error('请求超时(' + timeoutMs + 'ms)' + (opts.proxy ? ',代理 ' + opts.proxy : '')))
|
|
164
|
+
}, timeoutMs)
|
|
165
|
+
|
|
166
|
+
;(async () => {
|
|
167
|
+
try {
|
|
168
|
+
if (opts.proxy) tunnel = await openTunnel(opts.proxy, target, timeoutMs)
|
|
169
|
+
if (settled) return
|
|
170
|
+
if (tunnel) {
|
|
171
|
+
// 走代理:自定义 Agent 把 CONNECT 后的裸 socket 包成 TLS。
|
|
172
|
+
agent = new https.Agent({ keepAlive: false, maxSockets: 1 })
|
|
173
|
+
agent.createConnection = (options) => tls.connect({ socket: tunnel, servername: options.host })
|
|
174
|
+
}
|
|
175
|
+
if (settled) return
|
|
176
|
+
req = https.request({
|
|
177
|
+
host: target.hostname,
|
|
178
|
+
port: target.port || 443,
|
|
179
|
+
path: target.pathname + target.search,
|
|
180
|
+
method: 'GET',
|
|
181
|
+
headers: { ...headers, host: target.hostname, connection: 'close' },
|
|
182
|
+
agent: agent || false,
|
|
183
|
+
}, (res) => {
|
|
184
|
+
const chunks = []
|
|
185
|
+
res.on('data', (c) => chunks.push(c))
|
|
186
|
+
res.on('end', () => {
|
|
187
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
188
|
+
let body
|
|
189
|
+
try { body = text ? JSON.parse(text) : {} } catch { body = undefined }
|
|
190
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
191
|
+
finish(resolve, body)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
const msg = body && (body.msg || body.message) ? String(body.msg || body.message) : text.slice(0, 200)
|
|
195
|
+
finish(reject, new BinanceError('HTTP ' + res.statusCode + ': ' + msg, { status: res.statusCode, code: body && typeof body.code === 'number' ? body.code : 0 }))
|
|
196
|
+
})
|
|
197
|
+
res.on('error', fail)
|
|
198
|
+
})
|
|
199
|
+
req.on('error', fail)
|
|
200
|
+
req.end()
|
|
201
|
+
} catch (e) {
|
|
202
|
+
fail(e)
|
|
203
|
+
}
|
|
204
|
+
})()
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 批量 symbols 参数:币安**要求原样**的 JSON 数组(`["A","B"]`),
|
|
210
|
+
* 百分号编码会被 `Illegal characters found in parameter 'symbols'` 拒绝。
|
|
211
|
+
*/
|
|
212
|
+
function symbolsParam(symbols) {
|
|
213
|
+
return JSON.stringify([...new Set(symbols)])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 公开行情:24h ticker 批量查询(无需鉴权);整批失败时逐个降级。 */
|
|
217
|
+
export async function fetchTickers24h(symbols, settings) {
|
|
218
|
+
if (!symbols || !symbols.length) return []
|
|
219
|
+
const normalize = (r) => ({
|
|
220
|
+
symbol: String(r.symbol || ''),
|
|
221
|
+
lastPrice: Number(r.lastPrice || 0),
|
|
222
|
+
priceChangePercent: Number(r.priceChangePercent || 0),
|
|
223
|
+
highPrice: Number(r.highPrice || 0),
|
|
224
|
+
lowPrice: Number(r.lowPrice || 0),
|
|
225
|
+
quoteVolume: Number(r.quoteVolume || 0),
|
|
226
|
+
})
|
|
227
|
+
try {
|
|
228
|
+
const rows = await requestJson(SPOT_BASE + '/api/v3/ticker/24hr?symbols=' + symbolsParam(symbols), { proxy: settings.proxy })
|
|
229
|
+
if (!Array.isArray(rows)) throw new BinanceError('行情返回格式异常')
|
|
230
|
+
return rows.map(normalize)
|
|
231
|
+
} catch (batchError) {
|
|
232
|
+
// 自选里混了不存在的币对时整批会失败:逐个取,成功的留下。
|
|
233
|
+
const settled = await Promise.all(symbols.map(async (symbol) => {
|
|
234
|
+
try {
|
|
235
|
+
const one = await requestJson(SPOT_BASE + '/api/v3/ticker/24hr?symbol=' + encodeURIComponent(symbol), { proxy: settings.proxy })
|
|
236
|
+
return one && one.symbol ? normalize(one) : null
|
|
237
|
+
} catch { return null }
|
|
238
|
+
}))
|
|
239
|
+
const rows = settled.filter(Boolean)
|
|
240
|
+
if (!rows.length) throw batchError
|
|
241
|
+
return rows
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 公开行情:批量现价(用于资产折 USDT);整批失败时逐个降级。 */
|
|
246
|
+
export async function fetchPrices(symbols, settings) {
|
|
247
|
+
if (!symbols || !symbols.length) return new Map()
|
|
248
|
+
try {
|
|
249
|
+
const rows = await requestJson(SPOT_BASE + '/api/v3/ticker/price?symbols=' + symbolsParam(symbols), { proxy: settings.proxy })
|
|
250
|
+
const map = new Map()
|
|
251
|
+
if (Array.isArray(rows)) for (const r of rows) map.set(String(r.symbol || ''), Number(r.price || 0))
|
|
252
|
+
return map
|
|
253
|
+
} catch {
|
|
254
|
+
// 某些资产没有 USDT 现货对(或已下线)会让整批失败:逐个取,失败的跳过。
|
|
255
|
+
const map = new Map()
|
|
256
|
+
await Promise.all([...new Set(symbols)].map(async (symbol) => {
|
|
257
|
+
try {
|
|
258
|
+
const one = await requestJson(SPOT_BASE + '/api/v3/ticker/price?symbol=' + encodeURIComponent(symbol), { proxy: settings.proxy })
|
|
259
|
+
if (one && one.price) map.set(String(one.symbol || symbol), Number(one.price))
|
|
260
|
+
} catch { /* 无对应交易对:该资产估值留空 */ }
|
|
261
|
+
}))
|
|
262
|
+
return map
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 签名 GET(现货/合约共用)。 */
|
|
267
|
+
async function signedGet(base, path, params, settings) {
|
|
268
|
+
const search = new URLSearchParams({
|
|
269
|
+
...params,
|
|
270
|
+
recvWindow: String(RECV_WINDOW),
|
|
271
|
+
timestamp: String(Date.now()),
|
|
272
|
+
}).toString()
|
|
273
|
+
const signature = createHmac('sha256', settings.apiSecret).update(search).digest('hex')
|
|
274
|
+
return requestJson(base + path + '?' + search + '&signature=' + signature, {
|
|
275
|
+
proxy: settings.proxy,
|
|
276
|
+
headers: { 'X-MBX-APIKEY': settings.apiKey },
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 现货账户:余额 + 现价折 USDT。 */
|
|
281
|
+
export async function fetchSpot(settings) {
|
|
282
|
+
const account = await signedGet(SPOT_BASE, '/api/v3/account', {}, settings)
|
|
283
|
+
const balances = Array.isArray(account.balances) ? account.balances : []
|
|
284
|
+
const rows = balances
|
|
285
|
+
.map((b) => {
|
|
286
|
+
const free = Number(b.free || 0)
|
|
287
|
+
const locked = Number(b.locked || 0)
|
|
288
|
+
return { asset: String(b.asset || ''), free, locked, total: free + locked }
|
|
289
|
+
})
|
|
290
|
+
.filter((b) => b.asset && b.total > 0 && b.total > 1e-12)
|
|
291
|
+
|
|
292
|
+
const pairs = rows
|
|
293
|
+
.filter((b) => b.asset !== 'USDT')
|
|
294
|
+
.map((b) => b.asset + 'USDT')
|
|
295
|
+
let prices = new Map()
|
|
296
|
+
if (pairs.length) {
|
|
297
|
+
try { prices = await fetchPrices(pairs, settings) } catch { /* 无对应现货对的币种不估值 */ }
|
|
298
|
+
}
|
|
299
|
+
const valued = rows.map((b) => {
|
|
300
|
+
const priceUSDT = b.asset === 'USDT' ? 1 : (prices.get(b.asset + 'USDT') || 0)
|
|
301
|
+
return { ...b, priceUSDT, valueUSDT: priceUSDT ? b.total * priceUSDT : null }
|
|
302
|
+
}).sort((a, b) => (b.valueUSDT || 0) - (a.valueUSDT || 0))
|
|
303
|
+
const totalUSDT = valued.reduce((sum, b) => sum + (b.valueUSDT || 0), 0)
|
|
304
|
+
return {
|
|
305
|
+
totalUSDT,
|
|
306
|
+
canTrade: Boolean(account.canTrade),
|
|
307
|
+
accountType: String(account.accountType || ''),
|
|
308
|
+
rows: valued,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** U 本位合约:账户余额 + 非零持仓。 */
|
|
313
|
+
export async function fetchFutures(settings) {
|
|
314
|
+
const [account, positions] = await Promise.all([
|
|
315
|
+
signedGet(FUTURES_BASE, '/fapi/v2/account', {}, settings),
|
|
316
|
+
signedGet(FUTURES_BASE, '/fapi/v2/positionRisk', {}, settings),
|
|
317
|
+
])
|
|
318
|
+
const rows = (Array.isArray(positions) ? positions : [])
|
|
319
|
+
.map((p) => {
|
|
320
|
+
const amt = Number(p.positionAmt || 0)
|
|
321
|
+
return {
|
|
322
|
+
symbol: String(p.symbol || ''),
|
|
323
|
+
side: amt > 0 ? 'LONG' : amt < 0 ? 'SHORT' : 'FLAT',
|
|
324
|
+
positionAmt: amt,
|
|
325
|
+
entryPrice: Number(p.entryPrice || 0),
|
|
326
|
+
markPrice: Number(p.markPrice || 0),
|
|
327
|
+
unrealizedProfit: Number(p.unRealizedProfit || p.unrealizedProfit || 0),
|
|
328
|
+
leverage: Number(p.leverage || 0),
|
|
329
|
+
liquidationPrice: Number(p.liquidationPrice || 0),
|
|
330
|
+
marginType: String(p.marginType || ''),
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.filter((p) => p.symbol && p.positionAmt !== 0)
|
|
334
|
+
return {
|
|
335
|
+
totalWalletBalance: Number(account.totalWalletBalance || 0),
|
|
336
|
+
totalUnrealizedProfit: Number(account.totalUnrealizedProfit || 0),
|
|
337
|
+
totalMarginBalance: Number(account.totalMarginBalance || 0),
|
|
338
|
+
availableBalance: Number(account.availableBalance || 0),
|
|
339
|
+
positions: rows,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 面板总览:行情总是有(公开接口);账户/持仓在配好 Key 后才有。
|
|
345
|
+
* 各段独立失败:一段报错不影响其他段。
|
|
346
|
+
* @param {ReturnType<typeof loadSettings>} [settingsIn]
|
|
347
|
+
*/
|
|
348
|
+
export async function fetchOverview(settingsIn) {
|
|
349
|
+
const settings = settingsIn || loadSettings()
|
|
350
|
+
const configured = hasCredentials(settings)
|
|
351
|
+
const errors = []
|
|
352
|
+
const mark = (section, e) => {
|
|
353
|
+
const message = e instanceof BinanceError ? e.message : String((e && e.message) || e)
|
|
354
|
+
errors.push({ section, message })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let tickers = []
|
|
358
|
+
try {
|
|
359
|
+
tickers = await fetchTickers24h(settings.symbols, settings)
|
|
360
|
+
} catch (e) {
|
|
361
|
+
mark('market', e)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let spot = null
|
|
365
|
+
let futures = null
|
|
366
|
+
if (configured) {
|
|
367
|
+
try { spot = await fetchSpot(settings) } catch (e) { mark('spot', e) }
|
|
368
|
+
try { futures = await fetchFutures(settings) } catch (e) { mark('futures', e) }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 行情按自选顺序返回(币安接口不保证顺序)
|
|
372
|
+
const bySymbol = new Map(tickers.map((k) => [k.symbol, k]))
|
|
373
|
+
const ordered = settings.symbols.map((s) => bySymbol.get(s)).filter(Boolean)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
at: new Date().toISOString(),
|
|
377
|
+
configured,
|
|
378
|
+
proxy: Boolean(settings.proxy),
|
|
379
|
+
symbols: settings.symbols,
|
|
380
|
+
tickers: ordered.length ? ordered : tickers,
|
|
381
|
+
spot,
|
|
382
|
+
futures,
|
|
383
|
+
errors,
|
|
384
|
+
}
|
|
385
|
+
}
|