-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathweb_console_html.go
More file actions
436 lines (435 loc) · 129 KB
/
Copy pathweb_console_html.go
File metadata and controls
436 lines (435 loc) · 129 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
package main
const consoleHTML = `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nexus Trade Bot</title>
<link rel="icon" type="image/png" href="/logo/nexus-logo.png">
<link rel="apple-touch-icon" href="/logo/nexus-logo.png">
<style>
:root{--bg:#06111a;--panel:#0d1a29;--panel-strong:#12263d;--line:#23435f;--line-soft:#163147;--text:#edf6ff;--muted:#89a3ba;--cyan:#6ee7ff;--cyan-strong:#22d3ee;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--orange:#fb923c;--shadow:0 24px 80px rgba(2,8,20,.38)}
*{box-sizing:border-box}
body{margin:0;background:
radial-gradient(circle at 12% 18%,rgba(34,211,238,.18),transparent 28%),
radial-gradient(circle at 88% 12%,rgba(59,130,246,.16),transparent 26%),
linear-gradient(145deg,#06111a 0,#091826 35%,#07111b 70%,#050b12 100%);
color:var(--text);font-family:"Segoe UI",system-ui,sans-serif}
body:before{content:'';position:fixed;inset:0;pointer-events:none;opacity:.32;background-image:linear-gradient(rgba(255,255,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.03) 1px,transparent 1px);background-size:24px 24px}
body.light{--bg:#f4f8fb;--panel:#ffffff;--panel-strong:#f8fbff;--line:#d7e3ee;--line-soft:#e7eef5;--text:#0f1728;--muted:#66768d;--shadow:0 24px 70px rgba(15,23,42,.10);background:
radial-gradient(circle at 8% 12%,rgba(34,211,238,.10),transparent 24%),
radial-gradient(circle at 90% 10%,rgba(59,130,246,.08),transparent 26%),
linear-gradient(180deg,#f7fbff,#eef4f8)}
button,input,select{font:inherit}
button{cursor:pointer}
.app{max-width:1520px;margin:0 auto;padding:28px;animation:appEnter .5s cubic-bezier(.2,.78,.2,1) both}
.settings-wrap{position:relative}
.settings-menu{position:absolute;right:0;top:58px;z-index:30;display:grid;gap:6px;min-width:246px;padding:10px;border:1px solid rgba(110,231,255,.18);border-radius:18px;background:rgba(8,17,28,.96);backdrop-filter:blur(18px);box-shadow:var(--shadow);transform-origin:top right;animation:menuIn .16s cubic-bezier(.2,.78,.2,1) both}
body.light .settings-menu{background:rgba(255,255,255,.96);border-color:#d7e0ec;box-shadow:var(--shadow)}
.menu-action{width:100%;border:0;background:transparent;color:var(--text);border-radius:10px;padding:9px 10px;text-align:left;display:flex;align-items:center;justify-content:space-between;gap:10px;transition:background .16s ease,transform .16s ease,color .16s ease}
body.light .menu-action{color:#101828}
.menu-action:hover{background:rgba(118,215,255,.14);transform:translateX(2px)}
.menu-sep{height:1px;background:rgba(70,100,150,.25);margin:2px 0}
.top{display:flex;justify-content:space-between;gap:18px;align-items:center;margin-bottom:24px;flex-wrap:wrap}
.brand{display:inline-flex;align-items:center;gap:12px;min-width:0;color:var(--text);text-decoration:none;transition:transform .18s ease,filter .18s ease}
.brand:hover{transform:translateY(-1px);filter:brightness(1.08)}
.brand-logo{width:54px;height:54px;object-fit:contain;border-radius:16px;background:linear-gradient(180deg,rgba(255,255,255,.12),rgba(255,255,255,.04));border:1px solid rgba(110,231,255,.22);padding:5px;flex:0 0 auto;box-shadow:0 16px 40px rgba(34,211,238,.18);transition:box-shadow .18s ease,transform .18s ease}
.brand:hover .brand-logo{box-shadow:0 20px 46px rgba(34,211,238,.28);transform:scale(1.03)}
body.light .brand-logo{background:#fff;border-color:#d7e0ec}
.brand-copy{display:flex;flex-direction:column;gap:4px;min-width:0}
.brand-name{font-size:22px;font-weight:900;letter-spacing:0;white-space:nowrap;line-height:1.05}
.brand-tagline{color:var(--muted);font-size:13px;font-weight:700;letter-spacing:0;line-height:1.25;white-space:nowrap}
.top-actions{display:flex;justify-content:flex-end;align-items:center;gap:10px;flex-wrap:wrap;position:relative;margin-left:auto}
.top-link{display:inline-flex;align-items:center;justify-content:center;gap:8px;color:var(--text);text-decoration:none}
.top-link:hover{text-decoration:none}
.title h1{margin:0;font-size:28px}
.title p{margin:8px 0 0;color:var(--muted);max-width:780px;line-height:1.6}
.row{display:flex;gap:10px;flex-wrap:wrap}
.auth-actions{justify-content:flex-end}
.btn,.btnp,.btnw,.btnd,.close,.icon-btn{position:relative;overflow:hidden;border:1px solid var(--line);background:linear-gradient(180deg,#11233a,#0d1b2d);color:var(--text);border-radius:16px;padding:10px 14px;transition:transform .16s cubic-bezier(.2,.78,.2,1),border-color .16s ease,background .16s ease,box-shadow .16s ease,opacity .16s ease,filter .16s ease}
.btn:before,.btnp:before,.btnw:before,.btnd:before,.icon-btn:before{content:'';position:absolute;inset:0;background:linear-gradient(120deg,transparent 20%,rgba(255,255,255,.18),transparent 80%);transform:translateX(-130%);transition:transform .5s ease;pointer-events:none}
button:hover:not(:disabled){transform:translateY(-2px);border-color:rgba(110,231,255,.58);box-shadow:0 16px 34px rgba(0,0,0,.22)}
button:hover:not(:disabled):before{transform:translateX(130%)}
button:active:not(:disabled){transform:translateY(0) scale(.985);box-shadow:0 6px 18px rgba(0,0,0,.16)}
button:disabled{cursor:wait;opacity:.68}
body.light .btn,body.light .close,body.light .icon-btn{background:linear-gradient(180deg,#fff,#f8fbff);color:#101828}
.btnp{background:linear-gradient(135deg,var(--cyan),#3b82f6 62%,#1d4ed8);color:#07111f;border:none;font-weight:800;box-shadow:0 16px 34px rgba(59,130,246,.24)}
.btnw{background:rgba(245,158,11,.14);border-color:rgba(245,158,11,.35);color:#fde68a}
body.light .btnw{color:#92400e}
.btnd{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.35);color:#fecaca}
body.light .btnd{color:#991b1b}
.close,.icon-btn{width:42px;height:42px;padding:0;display:inline-flex;align-items:center;justify-content:center}
.settings-btn{width:50px;height:50px;font-size:25px;line-height:1;border-radius:16px}
.close{font-size:18px}
.icon-btn img{width:24px;height:24px;object-fit:contain;display:block}
.theme-toggle img{width:26px;height:26px}
.lang-menu{position:absolute;right:0;top:58px;z-index:40;display:grid;gap:4px;min-width:178px;padding:8px;border:1px solid rgba(70,100,150,.35);border-radius:14px;background:rgba(9,16,28,.98);box-shadow:0 20px 50px rgba(0,0,0,.3);transform-origin:top right;animation:menuIn .16s cubic-bezier(.2,.78,.2,1) both}
body.light .lang-menu{background:#fff;border-color:#d7e0ec;box-shadow:0 20px 50px rgba(16,24,40,.12)}
.lang-option{width:100%;border:0;background:transparent;color:var(--text);border-radius:10px;padding:8px 10px;text-align:left;transition:background .16s ease,transform .16s ease}
body.light .lang-option{color:#101828}
.lang-option:hover,.lang-option.active{background:rgba(118,215,255,.14);transform:translateX(2px)}
.panel,.card,.modal-card,.login-shell,.glass{background:linear-gradient(180deg,rgba(13,26,41,.96),rgba(8,18,30,.96));border:1px solid rgba(110,231,255,.10);border-radius:22px;box-shadow:var(--shadow)}
body.light .panel,body.light .card,body.light .modal-card,body.light .login-shell,body.light .glass{background:linear-gradient(180deg,#ffffff,#fbfdff);border-color:#d7e0ec;box-shadow:var(--shadow)}
.panel{padding:22px;margin-bottom:20px;animation:panelIn .34s cubic-bezier(.2,.78,.2,1) both}
.metrics{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px;margin-top:18px}
.card{padding:18px;position:relative;overflow:hidden}
.card:before{content:'';position:absolute;inset:-1px auto auto -1px;width:48%;height:48%;background:radial-gradient(circle,rgba(110,231,255,.14),transparent 70%);pointer-events:none}
.card:after{content:'';position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg,rgba(110,231,255,.10),transparent 36%,transparent 68%,rgba(59,130,246,.08));opacity:0;transition:opacity .18s ease;pointer-events:none}
.card:hover:after{opacity:1}
.card.balance-card{grid-column:auto}
.k{color:var(--muted);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}
.v{margin-top:10px;font-size:26px;font-weight:900;white-space:nowrap;overflow:hidden;line-height:1.12;min-width:0}
.v.compact{font-size:24px;font-weight:800}
.account-balance-list{display:grid;gap:8px;margin-top:10px}
.balance-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:8px 10px;border-radius:12px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);transition:transform .16s ease,background .16s ease,border-color .16s ease}
.balance-row:hover{transform:translateY(-1px);background:rgba(255,255,255,.065);border-color:rgba(118,215,255,.24)}
body.light .balance-row{background:#f8fafc;border-color:#e4eaf2}
.balance-name{display:flex;align-items:center;gap:8px;min-width:0}
.balance-name span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.balance-amount{text-align:right;font-weight:800}
.balance-error{color:#fecaca;font-size:12px}
body.light .balance-error{color:#b91c1c}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:18px}
.robot{padding:20px;transition:transform .22s cubic-bezier(.2,.78,.2,1),border-color .18s ease,box-shadow .22s ease,background .22s ease;background:linear-gradient(180deg,rgba(13,26,41,.98),rgba(8,18,30,.98))}
.robot.is-new{animation:cardIn .28s cubic-bezier(.2,.78,.2,1) both}
.robot:hover{transform:translateY(-5px);border-color:rgba(110,231,255,.4);box-shadow:0 30px 84px rgba(0,0,0,.36)}
body.light .robot:hover{box-shadow:0 24px 54px rgba(16,24,40,.11)}
.robot-top{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:14px}
.rname{font-size:20px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px}
.rsub{margin-top:6px;color:var(--muted);font-size:13px}
.status{display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:999px;border:1px solid;transition:background .18s ease,border-color .18s ease,transform .18s ease}
.status:hover{transform:translateY(-1px)}
.dot{width:10px;height:10px;border-radius:50%}
.running{color:#bbf7d0;border-color:rgba(34,197,94,.35)}
body.light .running{color:#047857}
.running .dot{background:var(--green);box-shadow:0 0 0 0 rgba(34,197,94,.55);animation:statusBreath 1.8s ease-out infinite}
.stopped{color:#fecaca;border-color:rgba(239,68,68,.35)}
body.light .stopped{color:#b91c1c}
.stopped .dot{background:var(--red)}
.paused{color:#fde68a;border-color:rgba(245,158,11,.35)}
body.light .paused{color:#b45309}
.paused .dot{background:var(--yellow);box-shadow:0 0 0 0 rgba(245,158,11,.48);animation:statusBreath 2.2s ease-out infinite}
.pending{color:#bfdbfe;border-color:rgba(96,165,250,.4)}
body.light .pending{color:#1d4ed8}
.pending .dot{background:#60a5fa;animation:pulse .9s ease-in-out infinite}
.robot-stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:14px}
.stat{padding:15px;border-radius:16px;background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.02));border:1px solid rgba(110,231,255,.10);min-width:0;transition:transform .16s ease,border-color .16s ease,background .16s ease,box-shadow .16s ease}
.stat:hover{transform:translateY(-1px);background:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.03));border-color:rgba(110,231,255,.24);box-shadow:0 14px 28px rgba(0,0,0,.14)}
.price-stat .v,.position-stat .v{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:22px;letter-spacing:0}
body.light .stat,body.light .tag,body.light .item,body.light .empty,body.light .exchange-pill{background:#f8fafc;border-color:#e4eaf2}
.tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
.tag{padding:7px 10px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);font-size:12px;transition:background .16s ease,border-color .16s ease,transform .16s ease}
.tag:hover{transform:translateY(-1px);background:rgba(118,215,255,.09);border-color:rgba(118,215,255,.2)}
.robot-actions{align-items:center;flex-wrap:nowrap;overflow-x:auto;padding-bottom:2px}
.robot-actions button{min-width:clamp(58px,18%,82px);flex:1 1 0;padding:10px clamp(7px,1vw,14px);font-size:clamp(12px,1.1vw,14px)}
.robot-actions .btn-content{gap:clamp(4px,.6vw,8px)}
.btn-content{display:inline-flex;align-items:center;justify-content:center;gap:8px;white-space:nowrap}
.spinner{width:14px;height:14px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;display:inline-block;animation:spin .72s linear infinite}
.num{display:inline-block;white-space:pre;font-variant-numeric:tabular-nums;max-width:none;user-select:text}
.num.changed{animation:numPulse .26s ease-out}
.plain-num{display:inline-block;white-space:nowrap;font-variant-numeric:tabular-nums}
.trade-table-wrap{overflow:auto;border:1px solid rgba(70,100,150,.28);border-radius:14px}
.trade-table{width:100%;border-collapse:collapse;min-width:560px}
.trade-table th,.trade-table td{padding:12px 14px;border-bottom:1px solid rgba(70,100,150,.18);text-align:left;white-space:nowrap}
.trade-table th{color:var(--muted);font-size:12px;font-weight:700;background:rgba(255,255,255,.035)}
.trade-table tr:last-child td{border-bottom:0}
.trade-table tbody tr{transition:background .14s ease,transform .14s ease}
.trade-table tbody tr:hover{background:rgba(118,215,255,.07);transform:translateX(2px)}
body.light .trade-table-wrap{border-color:#e4eaf2}
body.light .trade-table th{background:#f8fafc}
.log-view{min-height:420px;max-height:68vh;overflow:auto;border:1px solid rgba(70,100,150,.28);border-radius:14px;background:rgba(2,6,16,.72);padding:14px;color:#d7e8ff;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}
body.light .log-view{background:#0f172a;color:#e5edf8;border-color:#d7e0ec}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{50%{opacity:.35}}
@keyframes numPulse{0%{filter:brightness(1.5);opacity:.55;transform:translateY(-1px)}100%{filter:brightness(1);opacity:1;transform:translateY(0)}}
@keyframes appEnter{from{opacity:0}to{opacity:1}}
@keyframes panelIn{from{opacity:0;transform:translateY(10px) scale(.992)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes cardIn{from{opacity:0;transform:translateY(10px) scale(.99)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes menuIn{from{opacity:0;transform:translateY(-6px) scale(.965)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes modalFade{from{opacity:0}to{opacity:1}}
@keyframes modalCardIn{from{opacity:0;transform:translateY(14px) scale(.975)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes statusBreath{0%{box-shadow:0 0 0 0 currentColor}70%{box-shadow:0 0 0 8px transparent}100%{box-shadow:0 0 0 0 transparent}}
.modal{position:fixed;inset:0;background:rgba(2,6,16,.74);backdrop-filter:blur(16px);display:flex;align-items:center;justify-content:center;padding:20px;animation:modalFade .18s ease both}
body.light .modal{background:rgba(15,23,42,.25)}
.hidden{display:none!important}
.modal-card{width:min(1080px,100%);padding:22px;max-height:92vh;overflow:auto;animation:modalCardIn .2s cubic-bezier(.2,.78,.2,1) both}
.head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;margin-bottom:16px}
.head p{margin:8px 0 0;color:var(--muted)}
.form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.field{display:flex;flex-direction:column;gap:8px}
.field.full{grid-column:1/-1}
.field label{font-size:13px;color:#b9cbed}
body.light .field label{color:#475467}
.field input,.field select{width:100%;border:1px solid rgba(73,96,138,.45);border-radius:14px;padding:13px 14px;background:linear-gradient(180deg,rgba(7,15,29,.95),rgba(10,18,32,.92));color:var(--text);appearance:none;transition:border-color .16s ease,box-shadow .16s ease,background .16s ease,transform .16s ease}
body.light .field input,body.light .field select{background:#fff;color:#101828;border-color:#d7e0ec}
.field input:hover,.field select:hover{border-color:rgba(118,215,255,.48)}
.field input:focus,.field select:focus{outline:0;border-color:rgba(118,215,255,.85);box-shadow:0 0 0 3px rgba(118,215,255,.18);transform:translateY(-1px)}
.symbol-help{color:var(--muted);font-size:12px;line-height:1.5}
.msg{margin-top:12px;padding:11px 14px;border-radius:14px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);white-space:pre-wrap;animation:panelIn .18s ease both}
body.light .msg{background:#f8fafc;border-color:#e4eaf2}
.ok{color:#bbf7d0;border-color:rgba(34,197,94,.45)}
body.light .ok{color:#047857}
.err{color:#fecaca;border-color:rgba(239,68,68,.45)}
body.light .err{color:#b91c1c}
.list{display:grid;gap:10px;margin:14px 0 18px}
.item{padding:14px;border-radius:14px;border:1px solid rgba(73,96,138,.22);background:rgba(255,255,255,.03);display:flex;justify-content:space-between;gap:12px;align-items:center;transition:transform .16s ease,border-color .16s ease,background .16s ease}
.item:hover{transform:translateY(-1px);background:rgba(255,255,255,.05);border-color:rgba(118,215,255,.24)}
.empty{padding:34px;border-radius:18px;border:1px dashed rgba(114,138,179,.35);text-align:center;color:var(--muted);background:rgba(8,15,28,.45);animation:panelIn .22s ease both}
.logo{width:20px;height:20px;object-fit:contain;border-radius:6px;background:rgba(255,255,255,.08)}
.logo-lg{width:42px;height:42px;object-fit:contain;border-radius:10px;background:rgba(255,255,255,.08);padding:6px}
.logo-text{width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;background:rgba(118,215,255,.14);font-size:10px;font-weight:800}
.logo-text.logo-lg{width:42px;height:42px;border-radius:10px;font-size:14px}
.ex-inline{display:inline-flex;align-items:center;gap:8px}
.login-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;background:radial-gradient(circle at top,#19396f 0,#0a1120 55%,#050813 100%)}
body.light .login-wrap{background:linear-gradient(180deg,#f7fbff,#edf2f7)}
.login-shell{width:min(980px,100%);display:grid;grid-template-columns:1.15fr .85fr;overflow:hidden}
.login-brand{padding:34px;background:linear-gradient(135deg,rgba(118,215,255,.18),rgba(118,215,255,.03))}
.login-brand h2{margin:0;font-size:34px}
.login-brand p{margin:12px 0 0;color:#d7e8ff;line-height:1.7}
body.light .login-brand p{color:#344054}
.login-card{padding:34px}
.badge{display:inline-flex;gap:8px;align-items:center;padding:8px 12px;border-radius:999px;background:rgba(118,215,255,.12);border:1px solid rgba(118,215,255,.22);color:#dff7ff;font-size:12px;margin-bottom:18px}
body.light .badge{color:#075985}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.hint{color:var(--muted);font-size:13px}
.exchange-strip{display:grid;grid-template-columns:repeat(6,44px);gap:10px;margin-top:18px}
.exchange-pill{display:flex;align-items:center;justify-content:center;min-width:0;padding:10px;border-radius:14px;background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.08);transition:transform .16s ease,border-color .16s ease,background .16s ease}
.exchange-pill:hover{transform:translateY(-2px);background:rgba(118,215,255,.09);border-color:rgba(118,215,255,.24)}
.exchange-pill span{display:none}
.select-shell{position:relative}
.select-shell:after{content:'▼';position:absolute;right:14px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:11px;pointer-events:none}
.select-shell select{padding-left:44px;padding-right:40px}
.select-shell .logo{position:absolute;left:14px;top:50%;transform:translateY(-50%)}
.select-shell.generic select{padding-left:14px}
.select-shell.generic .logo{display:none}
.symbol-combobox{position:relative}
.symbol-menu{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:60;display:grid;gap:4px;max-height:286px;overflow:auto;padding:8px;border:1px solid rgba(70,100,150,.38);border-radius:14px;background:rgba(9,16,28,.98);box-shadow:0 22px 55px rgba(0,0,0,.35)}
body.light .symbol-menu{background:#fff;border-color:#d7e0ec;box-shadow:0 22px 45px rgba(16,24,40,.14)}
.symbol-option{width:100%;border:0;background:transparent;color:var(--text);border-radius:10px;padding:9px 10px;display:flex;align-items:center;justify-content:space-between;gap:12px;text-align:left;transition:background .16s ease,transform .16s ease}
body.light .symbol-option{color:#101828}
.symbol-option:hover:not(:disabled),.symbol-option:focus{background:rgba(118,215,255,.14);transform:translateX(2px);outline:none}
.symbol-symbol{font-weight:800}
.symbol-meta{color:var(--muted);font-size:12px}
@media (max-width:1200px){.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media (max-width:900px){.login-shell{grid-template-columns:1fr}.login-brand{padding:24px}.exchange-strip{grid-template-columns:repeat(6,44px)}}
@media (max-width:760px){.app{padding:12px}.top{align-items:stretch;gap:12px}.brand{width:100%}.brand-logo{width:44px;height:44px}.brand-name{font-size:18px}.brand-tagline{font-size:12px;white-space:normal}.top-actions,.auth-actions{width:100%;justify-content:flex-end}.auth-actions .btn,.auth-actions .btnp{flex:0 1 auto;min-width:0;padding:10px 8px}.metrics,.form,.robot-stats,.grid{grid-template-columns:1fr}.grid{gap:12px}.card.balance-card{grid-column:auto}.panel,.card,.modal-card,.login-shell,.glass{border-radius:14px}.panel{padding:14px}.robot{padding:14px}.item{align-items:flex-start;flex-direction:column}.modal{align-items:flex-end;padding:0}.modal-card{width:100%;max-height:94vh;border-radius:18px 18px 0 0;padding:16px}.head{gap:10px}.field input,.field select{min-height:46px}.login-wrap{padding:0;align-items:stretch}.login-shell{min-height:100vh;border:0;border-radius:0}.login-card{padding:24px 18px}.login-brand{padding:22px 18px}.login-brand h2{font-size:28px}.exchange-strip{grid-template-columns:repeat(6,44px)}.metrics{gap:10px}.v{font-size:22px}.rname{font-size:18px}}
@media (max-width:420px){.brand-logo{width:38px;height:38px}.brand-name{font-size:16px}.brand-tagline{font-size:11px}.settings-btn{width:46px;height:46px;font-size:23px}.exchange-strip{grid-template-columns:repeat(3,44px)}.auth-actions{display:flex;justify-content:flex-end;flex-wrap:wrap}.auth-actions .btn,.auth-actions .btnp{font-size:13px}.lang-menu{right:0;min-width:160px}.robot-top{flex-direction:column}.status{align-self:flex-start}.login-brand .panel{margin-bottom:0}}
body.auth-active{min-height:100dvh;overflow-x:hidden}
body.auth-active .app{max-width:none;min-height:100dvh;margin:0;padding:0;display:flex;align-items:center;justify-content:center}
body.auth-active #loginScreen{width:100%}
.login-wrap{width:100%;min-height:100dvh;overflow:auto;padding:clamp(18px,4vw,40px)}
.login-card{display:flex;flex-direction:column;justify-content:center}
.modal{z-index:100;overflow:auto}
#passwordScreen,#changePasswordModal{align-items:center;justify-content:center}
#passwordScreen .modal-card,#changePasswordModal .modal-card{width:min(520px,calc(100vw - 32px));margin:auto}
@media (max-width:760px){body.auth-active .app{max-width:none;padding:0}.modal{align-items:center;padding:16px}.modal-card{width:min(100%,calc(100vw - 32px));max-height:calc(100dvh - 32px);border-radius:18px;padding:16px}.login-wrap{padding:16px;align-items:center}.login-shell{width:min(100%,560px);min-height:0;border-radius:18px;border:1px solid rgba(110,231,255,.10)}body.light .login-shell{border-color:#d7e0ec}.login-brand,.login-card{padding:22px 18px}.login-brand h2{font-size:28px}}
@media (prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}
</style>
</head>
<body>
<div class="app">
<div class="top hidden" id="topBar">
<a class="brand" href="/" aria-label="Nexus Trade Bot"><img class="brand-logo" src="/logo/nexus-logo.png" alt=""><span class="brand-copy"><span class="brand-name">Nexus Trade Bot</span><span class="brand-tagline" data-i18n="app.tagline">毫秒级高频加密货币做市商系统</span></span></a>
<div class="top-actions" id="authBar"><a class="btn top-link" href="https://t.me/nexustradebot8" target="_blank" rel="noopener"><strong data-i18n="community.title">加入用户群</strong></a><div class="settings-wrap"><button class="icon-btn settings-btn" id="settingsBtn" aria-label="设置" title="设置">⚙</button><div class="settings-menu hidden" id="settingsMenu"><button class="menu-action" id="languageBtn"><span data-i18n="settings.language">语言切换</span><span>›</span></button><button class="menu-action" id="timezoneBtn"><span data-i18n="settings.timezone">时区</span><span id="timezoneLabel">Asia/Hong_Kong</span></button><button class="menu-action" id="themeToggleBtn"><span data-i18n="settings.theme">黑白切换</span><img id="themeIcon" src="/logo/月亮.png" alt="" style="width:20px;height:20px"></button><button class="menu-action" id="passwordMenuBtn"><span data-i18n="settings.password">修改密码</span><span>›</span></button><div class="menu-sep"></div><button class="menu-action" id="logoutBtn"><span data-i18n="nav.logout">退出登录</span><span>↗</span></button></div><div class="lang-menu hidden" id="languageMenu"></div></div><div class="row auth-actions"><button class="btn" id="manageAccountsBtn" data-i18n="nav.api">API管理</button><button class="btnp" id="createRobotBtn" data-i18n="nav.newRobot">新建机器人</button></div></div>
</div>
<div id="console" class="hidden">
<div class="panel">
<div class="row" style="justify-content:space-between;align-items:flex-start"><div><h2 style="margin:0" data-i18n="dashboard.title">机器人总览</h2><p style="margin:8px 0 0;color:var(--muted)" data-i18n="dashboard.subtitle">实时查看每个机器人的运行状态、手续费、交易量和盈亏表现。</p></div><div class="status" id="globalStatus"><span class="dot"></span><span id="globalStatusText" data-i18n="status.loading">加载中</span></div></div>
<div class="metrics">
<div class="card balance-card"><div class="k" data-i18n="metrics.apiBalance">余额</div><div class="v" id="accountBalances">--</div></div>
<div class="card"><div class="k" data-i18n="metrics.todayFees">今日手续费</div><div class="v" id="sumTodayFees">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metrics.totalFees">累计手续费</div><div class="v" id="sumTotalFees">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metric.unrealized">未实现盈亏</div><div class="v" id="sumUnrealized">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metrics.todayPnl">今日已实现盈亏</div><div class="v" id="sumTodayRealized">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metrics.totalPnl">累计已实现盈亏</div><div class="v" id="sumTotalRealized">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metrics.todayVolume">今日交易量</div><div class="v" id="sumTodayVolume">0.00 USD</div></div>
<div class="card"><div class="k" data-i18n="metrics.totalVolume">累计交易量</div><div class="v" id="sumTotalVolume">0.00 USD</div></div>
</div>
</div>
<div id="robotsEmpty" class="empty hidden" data-i18n="empty.robots">暂无机器人。先添加 API,再点击“新建机器人”。</div>
<div id="robotGrid" class="grid"></div>
</div>
<div id="loginScreen" class="login-wrap hidden"><div class="login-shell"><div class="login-brand"><div class="glass panel"><div><strong data-i18n="login.exchanges">已支持的交易所</strong><div class="hint" data-i18n="login.exchangeHint">添加 API 后即可绑定机器人,系统会在保存前完成连通性校验。</div></div><div class="exchange-strip" id="loginExchangeList"></div></div></div><div class="login-card"><div class="head"><div><h3 style="margin:0" data-i18n="login.title">登录控制台</h3><p data-i18n="login.default">初始账号和密码均为 admin</p></div></div><div class="form"><div class="field full"><label data-i18n="field.username">用户名</label><input id="loginUser" autocomplete="username"></div><div class="field full"><label data-i18n="field.password">密码</label><input id="loginPass" type="password" autocomplete="current-password"></div></div><div class="row" style="margin-top:18px"><button class="btnp" id="loginBtn" data-i18n="login.button">进入控制台</button></div><div id="loginMsg" class="msg hidden"></div></div></div></div>
<div id="passwordScreen" class="modal hidden"><div class="modal-card" style="max-width:520px"><div class="head"><div><h3 style="margin:0" data-i18n="password.title">首次登录必须修改密码</h3></div></div><div class="form"><div class="field full"><label data-i18n="field.newPassword">新密码</label><input id="newPass" type="password" autocomplete="new-password"></div><div class="field full"><label data-i18n="field.confirmPassword">重复新密码</label><input id="confirmPass" type="password" autocomplete="new-password"></div></div><div class="row" style="margin-top:16px"><button class="btnp" id="changePassBtn" data-i18n="password.button">修改密码</button></div><div id="changeMsg" class="msg hidden"></div></div></div>
<div id="changePasswordModal" class="modal hidden"><div class="modal-card" style="max-width:520px"><div class="head"><div><h3 style="margin:0" data-i18n="settings.password">修改密码</h3></div><button class="close" id="closeChangePasswordBtn">×</button></div><div class="form"><div class="field full"><label data-i18n="field.oldPassword">旧密码</label><input id="menuOldPass" type="password" autocomplete="current-password"></div><div class="field full"><label data-i18n="field.newPassword">新密码</label><input id="menuNewPass" type="password" autocomplete="new-password"></div><div class="field full"><label data-i18n="field.confirmPassword">重复新密码</label><input id="menuConfirmPass" type="password" autocomplete="new-password"></div></div><div class="row" style="margin-top:16px"><button class="btnp" id="menuChangePassBtn" data-i18n="password.button">修改密码</button></div><div id="menuChangeMsg" class="msg hidden"></div></div></div>
<div id="accountModal" class="modal hidden"><div class="modal-card"><div class="head"><div><h3 style="margin:0" data-i18n="api.title">API管理</h3><p data-i18n="api.subtitle">选择交易所并填写 API 信息,保存前会自动校验连接。Bitget 和 OKX 需要填写 Passphrase。</p></div><button class="close" id="closeAccountModalBtn">×</button></div><div id="accountList" class="list"></div><div class="form"><div class="field"><label data-i18n="field.apiName">API名称</label><input id="accountName"></div><div class="field"><label data-i18n="field.exchange">交易所</label><div class="select-shell" id="accountExchangeWrap"><img class="logo" id="accountExchangeLogo"><select id="accountExchange"></select></div></div><div class="field"><label>API Key</label><input id="accountApiKey"></div><div class="field"><label>Secret Key</label><input id="accountSecretKey"></div><div class="field"><label data-i18n="field.feeRate">交易所费率 (%)</label><input id="accountFeeRate" inputmode="decimal" placeholder="0.02"></div><div class="field full hidden" id="passphraseField"><label>Passphrase</label><input id="accountPassphrase"></div></div><div class="row" style="margin-top:16px"><button class="btn" id="resetAccountFormBtn" data-i18n="action.clear">清空</button><button class="btnp" id="saveAccountBtn" data-i18n="action.saveApi">保存 API</button></div><div id="accountMsg" class="msg hidden"></div></div></div>
<div id="robotModal" class="modal hidden"><div class="modal-card"><div class="head"><div><h3 id="modalTitle" style="margin:0" data-i18n="robot.title">新建机器人</h3><p data-i18n="robot.subtitle">机器人暂停后可以调整参数;保存成功后,已暂停的机器人会按最新参数重新启动。</p></div><button class="close" id="closeModalBtn">×</button></div><div class="form"><div class="field"><label data-i18n="field.selectApi">选择 API</label><div class="select-shell generic"><select id="robotAccountId"></select></div></div><div class="field"><label data-i18n="field.exchange">交易所</label><div class="row" style="align-items:center;margin-top:4px"><img class="logo" id="robotExchangeLogo"><input id="robotExchangeDisplay" disabled style="flex:1"></div></div><div class="field"><label data-i18n="field.marketType">交易市场</label><div class="select-shell generic"><select id="app.market_type"><option value="futures" data-i18n="market.futures">合约</option><option value="spot" data-i18n="market.spot">现货</option></select></div><div class="symbol-help" data-i18n="field.marketHelp">默认使用合约;现货为做多网格,支持主流中心化交易所。</div></div><div class="field"><label data-i18n="field.direction">交易方向</label><div class="select-shell generic"><select id="trading.direction"><option value="long" data-i18n="dir.long">做多</option><option value="short" data-i18n="dir.short">做空</option><option value="neutral" data-i18n="dir.neutral">中性</option></select></div></div><div class="field"><label data-i18n="field.mode">运行模式</label><div class="select-shell generic"><select id="trading.mode"><option value="normal" data-i18n="mode.normal">普通模式</option><option value="aggressive" data-i18n="mode.aggressive">激进模式</option><option value="classic" data-i18n="mode.classic">经典网格</option></select></div><div class="symbol-help" id="modeHelp" data-i18n="field.modeHelp">普通模式优先控风险与平仓;激进模式优先更快扩展网格仓位;经典网格实时维持上下各50个网格单。</div></div><div class="field full"><label data-i18n="field.symbol">交易币种</label><div class="symbol-combobox"><input id="trading.symbol" autocomplete="off" placeholder="输入 E 试试,例如 ETH/USDT" data-i18n-placeholder="field.symbolPlaceholder"><div id="symbolMenu" class="symbol-menu hidden"></div></div><div class="symbol-help" id="symbolHelp" data-i18n="field.symbolHelp">已接入 Binance 与 Bitget 合约交易对,输入首字母即可筛选。</div></div><div class="field"><label data-i18n="field.risk">风险控制</label><div class="select-shell generic"><select id="risk_control.enabled"><option value="true" data-i18n="action.enabled">开启</option><option value="false" data-i18n="action.disabled">关闭</option></select></div></div><div class="field"><label data-i18n="field.priceInterval">价格间隔</label><input id="trading.price_interval"></div><div class="field"><label data-i18n="field.orderQty">每单金额</label><input id="trading.order_quantity"></div><div class="field"><label data-i18n="field.minOrder">最小订单价值</label><input id="trading.min_order_value"></div></div><div class="row" style="margin-top:16px"><button class="btnp" id="saveRobotBtn" data-i18n="action.saveRobot">保存机器人</button></div><div id="modalMsg" class="msg hidden"></div></div></div>
<div id="tradesModal" class="modal hidden"><div class="modal-card" style="max-width:920px"><div class="head"><div><h3 id="tradesTitle" style="margin:0" data-i18n="trades.title">近期成交</h3><p id="tradesSub" style="margin:8px 0 0;color:var(--muted)"></p></div><button class="close" id="closeTradesModalBtn">×</button></div><div class="trade-table-wrap"><table class="trade-table"><thead><tr><th data-i18n="trades.time">时间</th><th data-i18n="trades.qty">数量</th><th data-i18n="trades.price">价格</th><th data-i18n="trades.position">仓位变动</th></tr></thead><tbody id="tradeRows"></tbody></table></div><div id="tradesMsg" class="msg hidden"></div></div></div>
<div id="logsModal" class="modal hidden"><div class="modal-card" style="max-width:980px"><div class="head"><div><h3 id="logsTitle" style="margin:0" data-i18n="logs.title">机器人日志</h3><p id="logsSub" style="margin:8px 0 0;color:var(--muted)"></p></div><button class="close" id="closeLogsModalBtn">×</button></div><pre id="logContent" class="log-view"></pre><div id="logsMsg" class="msg hidden"></div></div></div>
<script>
let baseConfig=null,dashboard=null,accounts=[],editingRobotId=null,editingAccountId=null,refreshTimer=null,dashboardLoading=false,currentEditingConfig=null,editingRobotStatus='stopped',contractSymbols=[],robotActionPending=new Map(),robotHTMLCache=new Map(),animatedTextCache=new Map(),symbolRAF=0,consoleSettings={timezone:'Asia/Hong_Kong'};
const logos={binance:'/logo/binance.png',bitget:'/logo/Bitget.png',bybit:'/logo/logo_bybit_square_d.png',gate:'/logo/gate_io.png',okx:'/logo/okx.png',hyperliquid:'/logo/hyperliquid.png'};
const exchangeNames={binance:'Binance',bitget:'Bitget',bybit:'Bybit',gate:'Gate.io',okx:'OKX',hyperliquid:'Hyperliquid'};
const supportedExchanges=['binance','bitget','bybit','gate','okx','hyperliquid'];
const baseSymbols=['BTCUSDT','ETHUSDT','ETHUSDC','ETCUSDT','EOSUSDT','ENAUSDT','EIGENUSDT','SOLUSDT','XRPUSDT','DOGEUSDT','BNBUSDT','ADAUSDT','AVAXUSDT','LINKUSDT','LTCUSDT','BCHUSDT','DOTUSDT','POLUSDT','MATICUSDT','ARBUSDT','OPUSDT','SUIUSDT','PEPEUSDT','WIFUSDT','NEARUSDT','APTUSDT','FILUSDT','INJUSDT','ATOMUSDT','TIAUSDT','TONUSDT','UNIUSDT','AAVEUSDT','ORDIUSDT','SEIUSDT','JUPUSDT','PYTHUSDT','WLDUSDT','TRXUSDT','SHIBUSDT','1000PEPEUSDT'];
const languages={'en':'English','zh-CN':'简体中文','ru':'Русский','ko':'한국어','ja':'日本語','es':'Español','vi':'Tiếng Việt','hi':'हिन्दी','pt':'Português','ar':'العربية','zh-TW':'繁體中文'};
const I18N={
'en':{'app.subtitle':'','nav.api':'API Management','nav.newRobot':'New Bot','nav.logout':'Log out','dashboard.title':'Bot Overview','dashboard.subtitle':'Track each bot status, fees, volume, and PnL in real time.','status.loading':'Loading','metrics.robots':'Total bots','metrics.apiBalance':'Balance','metrics.todayPnl':'Today realized PnL','metrics.totalPnl':'Total realized PnL','metrics.todayVolume':'Today volume','metrics.totalVolume':'Total volume','metrics.todayFees':'Today fees','metrics.totalFees':'Total fees','empty.noApi':'No API yet','empty.robots':'No bots yet. Add an API, then click “New Bot”.','login.welcome':'Welcome back','login.lead':'','login.exchanges':'Supported exchanges','login.exchangeHint':'Add an API to bind bots. Connectivity is verified before saving.','login.title':'Sign in','login.default':'Default username and password are admin','field.username':'Username','field.password':'Password','login.button':'Enter console','password.title':'Change the default password first','field.oldPassword':'Old password','field.newPassword':'New password','password.button':'Change password','api.title':'API Management','api.subtitle':'Choose an exchange and enter API credentials. Connectivity is verified before saving. Bitget and OKX require a passphrase.','field.apiName':'API name','field.exchange':'Exchange','action.clear':'Clear','action.saveApi':'Save API','robot.title':'New Bot','robot.subtitle':'Pause a bot before editing parameters. If a paused bot is saved, it restarts with the latest parameters.','field.robotName':'Bot name','field.selectApi':'Select API','field.marketType':'Market','market.futures':'Futures','market.spot':'Spot','field.marketHelp':'Futures is the default. Spot uses long grids on supported centralized exchanges.','field.direction':'Direction','dir.long':'Long','dir.short':'Short','dir.neutral':'Neutral','field.mode':'Mode','mode.normal':'Normal mode','mode.aggressive':'Aggressive mode','mode.classic':'Classic grid','field.modeHelp':'Normal mode prioritizes risk control and exits. Aggressive mode prioritizes faster grid expansion. Classic grid keeps 50 live orders above and 50 below in real time.','field.symbol':'Symbol','field.symbolPlaceholder':'Type E, for example ETH/USDT','field.symbolHelp':'Binance and Bitget futures symbols are loaded. Type the first letters to filter.','field.symbolHelpSpot':'Binance, Bitget, Bybit, OKX, Gate, and Hyperliquid spot symbols are loaded.','field.risk':'Risk control','action.enabled':'Enabled','action.disabled':'Disabled','field.priceInterval':'Price interval','field.orderQty':'Order amount','field.minOrder':'Minimum order value','action.saveRobot':'Save bot','action.logs':'Logs','action.trades':'Recent trades','logs.title':'Bot logs','logs.loading':'Loading logs...','logs.empty':'No logs yet','trades.title':'Recent trades','trades.loading':'Loading trades...','trades.empty':'No recent trades','trades.time':'Time','trades.qty':'Quantity','trades.price':'Price','theme.light':'Switch to light theme','theme.dark':'Switch to dark theme','settings.timezone':'Timezone','settings.timezonePrompt':'Enter an IANA timezone, for example Asia/Hong_Kong','service.unavailable':'Service is unavailable. Please confirm the console backend is running.','msg.validating':'Verifying API Key, please wait...','msg.apiSaved':'API verified and saved','msg.robotSaved':'Bot saved','msg.robotRestarted':'Parameters saved. The bot restarted with the latest settings.','alert.addApi':'Please add an API first','alert.pauseBeforeEdit':'Pause the bot before editing it','status.running':'Running','status.paused':'Paused','status.stopped':'Stopped','status.starting':'Starting','status.stopping':'Stopping','status.deleting':'Deleting','status.noneRunning':'No bots are running','status.runningCount':'{count} bot(s) running','metric.balance':'Balance','metric.price':'Live price','metric.unrealized':'Unrealized PnL','metric.volume':'Total volume','metric.realized':'Realized PnL','metric.fees':'Total fees','tag.api':'API','tag.unbound':'Unbound','tag.exchange':'Exchange','tag.market':'Market','tag.risk':'Risk','tag.mode':'Mode','action.start':'Start','action.pause':'Pause','action.edit':'Edit','action.delete':'Delete','error.balance':'Failed','dir.label':'Direction'},
'zh-CN':{'app.subtitle':'','nav.api':'API管理','nav.newRobot':'新建机器人','nav.logout':'退出登录','dashboard.title':'机器人总览','dashboard.subtitle':'实时查看每个机器人的运行状态、手续费、交易量和盈亏表现。','status.loading':'加载中','metrics.robots':'总机器人数量','metrics.apiBalance':'余额','metrics.todayPnl':'今日已实现盈亏','metrics.totalPnl':'累计已实现盈亏','metrics.todayVolume':'今日交易量','metrics.totalVolume':'累计交易量','metrics.todayFees':'今日手续费','metrics.totalFees':'累计手续费','empty.noApi':'暂无 API','empty.robots':'暂无机器人。先添加 API,再点击“新建机器人”。','login.welcome':'欢迎回来','login.lead':'','login.exchanges':'已支持的交易所','login.exchangeHint':'添加 API 后即可绑定机器人,系统会在保存前完成连通性校验。','login.title':'登录控制台','login.default':'初始账号和密码均为 admin','field.username':'用户名','field.password':'密码','login.button':'进入控制台','password.title':'首次登录必须修改密码','field.oldPassword':'旧密码','field.newPassword':'新密码','password.button':'修改密码','api.title':'API管理','api.subtitle':'选择交易所并填写 API 信息,保存前会自动校验连接。Bitget 和 OKX 需要填写 Passphrase。','field.apiName':'API名称','field.exchange':'交易所','action.clear':'清空','action.saveApi':'保存 API','robot.title':'新建机器人','robot.subtitle':'机器人暂停后可以调整参数;保存成功后,已暂停的机器人会按最新参数重新启动。','field.robotName':'机器人名称','field.selectApi':'选择 API','field.marketType':'交易市场','market.futures':'合约','market.spot':'现货','field.marketHelp':'默认使用合约;现货为做多网格,支持主流中心化交易所。','field.direction':'交易方向','dir.long':'做多','dir.short':'做空','dir.neutral':'中性','field.mode':'运行模式','mode.normal':'普通模式','mode.aggressive':'激进模式','mode.classic':'经典网格','field.modeHelp':'普通模式优先控风险与平仓;激进模式优先更快扩展网格仓位;经典网格实时维持上下各50个网格单。','field.symbol':'交易币种','field.symbolPlaceholder':'输入 E 试试,例如 ETH/USDT','field.symbolHelp':'已接入 Binance 与 Bitget 合约交易对,输入首字母即可筛选。','field.symbolHelpSpot':'已接入 Binance、Bitget、Bybit、OKX、Gate 与 Hyperliquid 现货交易对。','field.risk':'风险控制','action.enabled':'开启','action.disabled':'关闭','field.priceInterval':'价格间隔','field.orderQty':'每单金额','field.minOrder':'最小订单价值','action.saveRobot':'保存机器人','action.logs':'日志','action.trades':'近期成交','logs.title':'机器人日志','logs.loading':'正在加载日志...','logs.empty':'暂无日志','trades.title':'近期成交','trades.loading':'正在加载成交...','trades.empty':'暂无近期成交','trades.time':'时间','trades.qty':'数量','trades.price':'价格','theme.light':'切换浅色主题','theme.dark':'切换深色主题','settings.timezone':'时区','settings.timezonePrompt':'请输入 IANA 时区,例如 Asia/Hong_Kong','service.unavailable':'服务暂时不可用,请确认控制台后端已启动','msg.validating':'正在校验 API Key,请稍候...','msg.apiSaved':'API 已通过校验并保存','msg.robotSaved':'机器人已保存','msg.robotRestarted':'参数已保存,机器人已按最新参数重新启动','alert.addApi':'请先添加 API','alert.pauseBeforeEdit':'编辑前必须先暂停机器人','status.running':'运行中','status.paused':'已暂停','status.stopped':'已停止','status.starting':'启动中','status.stopping':'停止中','status.deleting':'删除中','status.noneRunning':'当前没有运行中的机器人','status.runningCount':'有 {count} 个机器人正在运行','metric.balance':'账户余额','metric.price':'实时价格','metric.unrealized':'未实现盈亏','metric.volume':'累计交易量','metric.realized':'已实现盈亏','metric.fees':'累计手续费','tag.api':'API','tag.unbound':'未绑定','tag.exchange':'交易所','tag.market':'市场','tag.risk':'风控','tag.mode':'模式','action.start':'开始','action.pause':'暂停','action.edit':'编辑','action.delete':'删除','error.balance':'读取失败','dir.label':'交易方向'},
'ru':{'app.subtitle':'Управляйте API бирж, grid-ботами и балансами в одной консоли. Сохраните и проверьте API, затем создавайте ботов; статусы, PnL и балансы обновляются автоматически.','nav.api':'API','nav.newRobot':'Новый бот','nav.logout':'Выйти','dashboard.title':'Обзор ботов','dashboard.subtitle':'Статус, объем, баланс и PnL каждого бота в реальном времени.','status.loading':'Загрузка','metrics.robots':'Всего ботов','metrics.apiBalance':'Баланс API','metrics.todayPnl':'PnL сегодня','metrics.totalPnl':'Общий PnL','metrics.todayVolume':'Объем сегодня','metrics.totalVolume':'Общий объем','empty.noApi':'API нет','empty.robots':'Ботов нет. Добавьте API, затем нажмите “Новый бот”.','login.welcome':'С возвращением','login.lead':'Войдите, чтобы управлять API, grid-ботами и статусом. При первом входе смените пароль.','login.exchanges':'Поддерживаемые биржи','login.exchangeHint':'Добавьте API для привязки ботов. Соединение проверяется перед сохранением.','login.title':'Вход','login.default':'Логин и пароль по умолчанию: admin','field.username':'Логин','field.password':'Пароль','login.button':'Войти','password.title':'Сначала смените пароль','field.oldPassword':'Старый пароль','field.newPassword':'Новый пароль','password.button':'Сменить пароль','api.title':'Управление API','api.subtitle':'Выберите биржу и введите API. Bitget и OKX требуют Passphrase.','field.apiName':'Имя API','field.exchange':'Биржа','action.clear':'Очистить','action.saveApi':'Сохранить API','robot.title':'Новый бот','robot.subtitle':'Перед изменением параметров поставьте бота на паузу.','field.robotName':'Имя бота','field.selectApi':'Выбрать API','field.direction':'Направление','dir.long':'Лонг','dir.short':'Шорт','dir.neutral':'Нейтрально','field.symbol':'Пара','field.symbolPlaceholder':'Введите E, например ETH/USDT','field.symbolHelp':'Загружены фьючерсные пары Binance и Bitget.','field.risk':'Риск-контроль','action.enabled':'Вкл','action.disabled':'Выкл','field.priceInterval':'Шаг цены','field.orderQty':'Сумма ордера','field.minOrder':'Мин. стоимость ордера','action.saveRobot':'Сохранить бота'},
'ko':{'app.subtitle':'하나의 콘솔에서 거래소 API, 그리드 봇, 계정 자산을 관리합니다. API를 저장하고 검증한 뒤 봇을 만들 수 있으며 상태, 손익, 잔고가 자동 갱신됩니다.','nav.api':'API 관리','nav.newRobot':'새 봇','nav.logout':'로그아웃','dashboard.title':'봇 개요','dashboard.subtitle':'각 봇의 상태, 거래량, 잔고, 손익을 실시간으로 확인합니다.','status.loading':'로딩 중','metrics.robots':'전체 봇','metrics.apiBalance':'API 잔고','metrics.todayPnl':'오늘 실현 손익','metrics.totalPnl':'누적 실현 손익','metrics.todayVolume':'오늘 거래량','metrics.totalVolume':'누적 거래량','empty.noApi':'API 없음','empty.robots':'봇이 없습니다. API를 추가한 뒤 “새 봇”을 누르세요.','login.welcome':'환영합니다','login.lead':'로그인 후 API, 그리드 봇, 실행 상태를 관리하세요. 첫 로그인 후 기본 비밀번호를 변경하세요.','login.exchanges':'지원 거래소','login.exchangeHint':'API를 추가하면 봇에 연결할 수 있으며 저장 전 연결을 확인합니다.','login.title':'콘솔 로그인','login.default':'기본 계정과 비밀번호는 admin입니다','field.username':'사용자 이름','field.password':'비밀번호','login.button':'콘솔 입장','password.title':'먼저 기본 비밀번호를 변경하세요','field.oldPassword':'기존 비밀번호','field.newPassword':'새 비밀번호','password.button':'비밀번호 변경','api.title':'API 관리','api.subtitle':'거래소를 선택하고 API 정보를 입력하세요. Bitget과 OKX는 Passphrase가 필요합니다.','field.apiName':'API 이름','field.exchange':'거래소','action.clear':'지우기','action.saveApi':'API 저장','robot.title':'새 봇','robot.subtitle':'파라미터 변경 전 봇을 일시정지하세요.','field.robotName':'봇 이름','field.selectApi':'API 선택','field.direction':'방향','dir.long':'롱','dir.short':'숏','dir.neutral':'중립','field.symbol':'심볼','field.symbolPlaceholder':'E를 입력해 보세요, 예: ETH/USDT','field.symbolHelp':'Binance 및 Bitget 선물 심볼을 불러옵니다.','field.risk':'리스크 제어','action.enabled':'켜짐','action.disabled':'꺼짐','field.priceInterval':'가격 간격','field.orderQty':'주문 금액','field.minOrder':'최소 주문 가치','action.saveRobot':'봇 저장'},
'ja':{'app.subtitle':'取引所API、グリッドボット、資産を1つのコンソールで管理します。APIを保存して検証するとボットを作成でき、状態、損益、残高はリアルタイムで更新されます。','nav.api':'API管理','nav.newRobot':'新規ボット','nav.logout':'ログアウト','dashboard.title':'ボット概要','dashboard.subtitle':'各ボットの状態、出来高、残高、損益をリアルタイムで確認します。','status.loading':'読み込み中','metrics.robots':'ボット数','metrics.apiBalance':'API残高','metrics.todayPnl':'本日の実現損益','metrics.totalPnl':'累計実現損益','metrics.todayVolume':'本日の出来高','metrics.totalVolume':'累計出来高','empty.noApi':'APIなし','empty.robots':'ボットはありません。APIを追加して「新規ボット」をクリックしてください。','login.welcome':'おかえりなさい','login.lead':'ログインするとAPI、グリッドボット、稼働状態を管理できます。初回は既定パスワードを変更してください。','login.exchanges':'対応取引所','login.exchangeHint':'APIを追加するとボットに紐付けできます。保存前に接続を確認します。','login.title':'コンソールにログイン','login.default':'初期ユーザー名とパスワードは admin','field.username':'ユーザー名','field.password':'パスワード','login.button':'コンソールへ','password.title':'まず既定パスワードを変更してください','field.oldPassword':'旧パスワード','field.newPassword':'新パスワード','password.button':'変更','api.title':'API管理','api.subtitle':'取引所を選択しAPI情報を入力します。BitgetとOKXはPassphraseが必要です。','field.apiName':'API名','field.exchange':'取引所','action.clear':'クリア','action.saveApi':'APIを保存','robot.title':'新規ボット','robot.subtitle':'パラメータ変更前にボットを一時停止してください。','field.robotName':'ボット名','field.selectApi':'API選択','field.direction':'方向','dir.long':'ロング','dir.short':'ショート','dir.neutral':'中立','field.symbol':'銘柄','field.symbolPlaceholder':'Eを入力、例 ETH/USDT','field.symbolHelp':'BinanceとBitgetの先物銘柄を読み込みます。','field.risk':'リスク管理','action.enabled':'オン','action.disabled':'オフ','field.priceInterval':'価格間隔','field.orderQty':'注文金額','field.minOrder':'最小注文額','action.saveRobot':'ボット保存'},
'es':{'app.subtitle':'Administra APIs de exchanges, bots de grid y activos desde una consola. Guarda y verifica una API, crea bots y recibe estados, PnL y saldos en tiempo real.','nav.api':'API','nav.newRobot':'Nuevo bot','nav.logout':'Salir','dashboard.title':'Resumen de bots','dashboard.subtitle':'Consulta estado, volumen, saldo y PnL de cada bot en tiempo real.','status.loading':'Cargando','metrics.robots':'Bots totales','metrics.apiBalance':'Saldo API','metrics.todayPnl':'PnL realizado hoy','metrics.totalPnl':'PnL realizado total','metrics.todayVolume':'Volumen hoy','metrics.totalVolume':'Volumen total','empty.noApi':'Sin API','empty.robots':'No hay bots. Añade una API y pulsa “Nuevo bot”.','login.welcome':'Bienvenido','login.lead':'Inicia sesión para administrar APIs, bots de grid y estado. Cambia la contraseña por defecto al primer ingreso.','login.exchanges':'Exchanges compatibles','login.exchangeHint':'Añade una API para vincular bots. La conexión se verifica antes de guardar.','login.title':'Iniciar sesión','login.default':'Usuario y contraseña por defecto: admin','field.username':'Usuario','field.password':'Contraseña','login.button':'Entrar','password.title':'Cambia primero la contraseña','field.oldPassword':'Contraseña anterior','field.newPassword':'Nueva contraseña','password.button':'Cambiar','api.title':'Gestión de API','api.subtitle':'Elige exchange e introduce credenciales API. Bitget y OKX requieren Passphrase.','field.apiName':'Nombre API','field.exchange':'Exchange','action.clear':'Limpiar','action.saveApi':'Guardar API','robot.title':'Nuevo bot','robot.subtitle':'Pausa el bot antes de editar parámetros.','field.robotName':'Nombre del bot','field.selectApi':'Seleccionar API','field.direction':'Dirección','dir.long':'Largo','dir.short':'Corto','dir.neutral':'Neutral','field.symbol':'Símbolo','field.symbolPlaceholder':'Escribe E, por ejemplo ETH/USDT','field.symbolHelp':'Se cargan futuros de Binance y Bitget.','field.risk':'Control de riesgo','action.enabled':'Activado','action.disabled':'Desactivado','field.priceInterval':'Intervalo de precio','field.orderQty':'Importe por orden','field.minOrder':'Valor mínimo','action.saveRobot':'Guardar bot'},
'vi':{'app.subtitle':'Quản lý API sàn, bot lưới và tài sản trong một bảng điều khiển. Lưu và xác minh API rồi tạo bot; trạng thái, PnL và số dư tự động cập nhật.','nav.api':'Quản lý API','nav.newRobot':'Bot mới','nav.logout':'Đăng xuất','dashboard.title':'Tổng quan bot','dashboard.subtitle':'Theo dõi trạng thái, khối lượng, số dư và PnL của từng bot theo thời gian thực.','status.loading':'Đang tải','metrics.robots':'Tổng bot','metrics.apiBalance':'Số dư API','metrics.todayPnl':'PnL hôm nay','metrics.totalPnl':'Tổng PnL','metrics.todayVolume':'Khối lượng hôm nay','metrics.totalVolume':'Tổng khối lượng','empty.noApi':'Chưa có API','empty.robots':'Chưa có bot. Thêm API rồi bấm “Bot mới”.','login.welcome':'Chào mừng trở lại','login.lead':'Đăng nhập để quản lý API, bot lưới và trạng thái chạy. Hãy đổi mật khẩu mặc định ở lần đầu.','login.exchanges':'Sàn hỗ trợ','login.exchangeHint':'Thêm API để gắn bot. Kết nối được kiểm tra trước khi lưu.','login.title':'Đăng nhập','login.default':'Tài khoản và mật khẩu mặc định là admin','field.username':'Tên đăng nhập','field.password':'Mật khẩu','login.button':'Vào bảng điều khiển','password.title':'Vui lòng đổi mật khẩu mặc định trước','field.oldPassword':'Mật khẩu cũ','field.newPassword':'Mật khẩu mới','password.button':'Đổi mật khẩu','api.title':'Quản lý API','api.subtitle':'Chọn sàn và nhập thông tin API. Bitget và OKX cần Passphrase.','field.apiName':'Tên API','field.exchange':'Sàn','action.clear':'Xóa','action.saveApi':'Lưu API','robot.title':'Bot mới','robot.subtitle':'Tạm dừng bot trước khi sửa tham số.','field.robotName':'Tên bot','field.selectApi':'Chọn API','field.direction':'Hướng','dir.long':'Long','dir.short':'Short','dir.neutral':'Trung lập','field.symbol':'Cặp giao dịch','field.symbolPlaceholder':'Nhập E, ví dụ ETH/USDT','field.symbolHelp':'Đã tải hợp đồng Binance và Bitget.','field.risk':'Kiểm soát rủi ro','action.enabled':'Bật','action.disabled':'Tắt','field.priceInterval':'Khoảng giá','field.orderQty':'Số tiền mỗi lệnh','field.minOrder':'Giá trị tối thiểu','action.saveRobot':'Lưu bot'},
'hi':{'app.subtitle':'एक ही कंसोल से एक्सचेंज API, ग्रिड बॉट और खाते की संपत्ति प्रबंधित करें। API सेव और सत्यापित करें, फिर बॉट बनाएं; स्थिति, PnL और बैलेंस अपने आप अपडेट होते हैं।','nav.api':'API प्रबंधन','nav.newRobot':'नया बॉट','nav.logout':'लॉग आउट','dashboard.title':'बॉट अवलोकन','dashboard.subtitle':'हर बॉट की स्थिति, वॉल्यूम, बैलेंस और PnL रियल टाइम में देखें।','status.loading':'लोड हो रहा है','metrics.robots':'कुल बॉट','metrics.apiBalance':'API बैलेंस','metrics.todayPnl':'आज का PnL','metrics.totalPnl':'कुल PnL','metrics.todayVolume':'आज का वॉल्यूम','metrics.totalVolume':'कुल वॉल्यूम','empty.noApi':'API नहीं है','empty.robots':'कोई बॉट नहीं। पहले API जोड़ें, फिर “नया बॉट” क्लिक करें।','login.welcome':'वापसी पर स्वागत है','login.lead':'API, ग्रिड बॉट और रनटाइम स्थिति प्रबंधित करने के लिए लॉग इन करें। पहली बार डिफॉल्ट पासवर्ड बदलें।','login.exchanges':'समर्थित एक्सचेंज','login.exchangeHint':'API जोड़कर बॉट बांधें। सेव करने से पहले कनेक्शन जांचा जाता है।','login.title':'साइन इन','login.default':'डिफॉल्ट यूजर और पासवर्ड admin हैं','field.username':'यूजरनेम','field.password':'पासवर्ड','login.button':'कंसोल खोलें','password.title':'पहले डिफॉल्ट पासवर्ड बदलें','field.oldPassword':'पुराना पासवर्ड','field.newPassword':'नया पासवर्ड','password.button':'पासवर्ड बदलें','api.title':'API प्रबंधन','api.subtitle':'एक्सचेंज चुनें और API जानकारी भरें। Bitget और OKX को Passphrase चाहिए।','field.apiName':'API नाम','field.exchange':'एक्सचेंज','action.clear':'साफ करें','action.saveApi':'API सेव करें','robot.title':'नया बॉट','robot.subtitle':'पैरामीटर बदलने से पहले बॉट रोकें।','field.robotName':'बॉट नाम','field.selectApi':'API चुनें','field.direction':'दिशा','dir.long':'लॉन्ग','dir.short':'शॉर्ट','dir.neutral':'न्यूट्रल','field.symbol':'सिंबल','field.symbolPlaceholder':'E लिखें, जैसे ETH/USDT','field.symbolHelp':'Binance और Bitget futures symbols लोड हैं।','field.risk':'जोखिम नियंत्रण','action.enabled':'चालू','action.disabled':'बंद','field.priceInterval':'प्राइस अंतर','field.orderQty':'ऑर्डर राशि','field.minOrder':'न्यूनतम ऑर्डर वैल्यू','action.saveRobot':'बॉट सेव करें'},
'pt':{'app.subtitle':'Gerencie APIs de exchanges, bots de grid e ativos em um console. Salve e verifique uma API, crie bots e acompanhe status, PnL e saldos automaticamente.','nav.api':'Gerenciar API','nav.newRobot':'Novo bot','nav.logout':'Sair','dashboard.title':'Visão dos bots','dashboard.subtitle':'Acompanhe status, volume, saldo e PnL de cada bot em tempo real.','status.loading':'Carregando','metrics.robots':'Total de bots','metrics.apiBalance':'Saldo API','metrics.todayPnl':'PnL realizado hoje','metrics.totalPnl':'PnL realizado total','metrics.todayVolume':'Volume hoje','metrics.totalVolume':'Volume total','empty.noApi':'Sem API','empty.robots':'Nenhum bot. Adicione uma API e clique em “Novo bot”.','login.welcome':'Bem-vindo de volta','login.lead':'Entre para gerenciar APIs, bots de grid e status. Altere a senha padrão no primeiro acesso.','login.exchanges':'Exchanges suportadas','login.exchangeHint':'Adicione uma API para vincular bots. A conexão é verificada antes de salvar.','login.title':'Entrar','login.default':'Usuário e senha padrão são admin','field.username':'Usuário','field.password':'Senha','login.button':'Abrir console','password.title':'Altere primeiro a senha padrão','field.oldPassword':'Senha antiga','field.newPassword':'Nova senha','password.button':'Alterar senha','api.title':'Gerenciar API','api.subtitle':'Escolha a exchange e informe a API. Bitget e OKX exigem Passphrase.','field.apiName':'Nome da API','field.exchange':'Exchange','action.clear':'Limpar','action.saveApi':'Salvar API','robot.title':'Novo bot','robot.subtitle':'Pause o bot antes de editar parâmetros.','field.robotName':'Nome do bot','field.selectApi':'Selecionar API','field.direction':'Direção','dir.long':'Long','dir.short':'Short','dir.neutral':'Neutro','field.symbol':'Símbolo','field.symbolPlaceholder':'Digite E, por exemplo ETH/USDT','field.symbolHelp':'Símbolos futuros da Binance e Bitget carregados.','field.risk':'Controle de risco','action.enabled':'Ativado','action.disabled':'Desativado','field.priceInterval':'Intervalo de preço','field.orderQty':'Valor por ordem','field.minOrder':'Valor mínimo','action.saveRobot':'Salvar bot'},
'ar':{'app.subtitle':'إدارة واجهات API والبوتات الشبكية والأرصدة من لوحة واحدة. احفظ API وتحقق منه ثم أنشئ البوتات؛ يتم تحديث الحالة وPnL والأرصدة تلقائيا.','nav.api':'إدارة API','nav.newRobot':'بوت جديد','nav.logout':'تسجيل الخروج','dashboard.title':'نظرة عامة على البوتات','dashboard.subtitle':'تابع حالة كل بوت والحجم والرصيد وPnL مباشرة.','status.loading':'جار التحميل','metrics.robots':'إجمالي البوتات','metrics.apiBalance':'رصيد API','metrics.todayPnl':'PnL اليوم','metrics.totalPnl':'إجمالي PnL','metrics.todayVolume':'حجم اليوم','metrics.totalVolume':'إجمالي الحجم','empty.noApi':'لا يوجد API','empty.robots':'لا توجد بوتات. أضف API ثم اضغط “بوت جديد”.','login.welcome':'مرحبا بعودتك','login.lead':'سجل الدخول لإدارة API والبوتات وحالة التشغيل. غيّر كلمة المرور الافتراضية في أول دخول.','login.exchanges':'المنصات المدعومة','login.exchangeHint':'أضف API لربط البوتات. يتم التحقق من الاتصال قبل الحفظ.','login.title':'تسجيل الدخول','login.default':'اسم المستخدم وكلمة المرور الافتراضيان admin','field.username':'اسم المستخدم','field.password':'كلمة المرور','login.button':'دخول اللوحة','password.title':'غيّر كلمة المرور الافتراضية أولا','field.oldPassword':'كلمة المرور القديمة','field.newPassword':'كلمة المرور الجديدة','password.button':'تغيير كلمة المرور','api.title':'إدارة API','api.subtitle':'اختر المنصة وأدخل بيانات API. تتطلب Bitget وOKX عبارة مرور.','field.apiName':'اسم API','field.exchange':'المنصة','action.clear':'مسح','action.saveApi':'حفظ API','robot.title':'بوت جديد','robot.subtitle':'أوقف البوت مؤقتا قبل تعديل الإعدادات.','field.robotName':'اسم البوت','field.selectApi':'اختر API','field.direction':'الاتجاه','dir.long':'شراء','dir.short':'بيع','dir.neutral':'محايد','field.symbol':'الرمز','field.symbolPlaceholder':'اكتب E مثلا ETH/USDT','field.symbolHelp':'تم تحميل رموز عقود Binance وBitget.','field.risk':'إدارة المخاطر','action.enabled':'مفعل','action.disabled':'معطل','field.priceInterval':'فاصل السعر','field.orderQty':'مبلغ الأمر','field.minOrder':'الحد الأدنى للأمر','action.saveRobot':'حفظ البوت'},
'zh-TW':{'app.subtitle':'','nav.api':'API管理','nav.newRobot':'新增機器人','nav.logout':'登出','dashboard.title':'機器人總覽','dashboard.subtitle':'即時查看每個機器人的運行狀態、手續費、交易量和盈虧表現。','status.loading':'載入中','metrics.robots':'機器人總數','metrics.apiBalance':'餘額','metrics.todayPnl':'今日已實現盈虧','metrics.totalPnl':'累計已實現盈虧','metrics.todayVolume':'今日交易量','metrics.totalVolume':'累計交易量','empty.noApi':'暫無 API','empty.robots':'暫無機器人。先新增 API,再點擊「新增機器人」。','login.welcome':'歡迎回來','login.lead':'','login.exchanges':'已支援的交易所','login.exchangeHint':'新增 API 後即可綁定機器人,系統會在儲存前完成連通性校驗。','login.title':'登入控制台','login.default':'初始帳號和密碼均為 admin','field.username':'使用者名稱','field.password':'密碼','login.button':'進入控制台','password.title':'首次登入必須修改密碼','field.oldPassword':'舊密碼','field.newPassword':'新密碼','password.button':'修改密碼','api.title':'API管理','api.subtitle':'選擇交易所並填寫 API 資訊,儲存前會自動校驗連線。Bitget 和 OKX 需要填寫 Passphrase。','field.apiName':'API名稱','field.exchange':'交易所','action.clear':'清空','action.saveApi':'儲存 API','robot.title':'新增機器人','robot.subtitle':'機器人暫停後可以調整參數;儲存成功後,已暫停的機器人會按最新參數重新啟動。','field.robotName':'機器人名稱','field.selectApi':'選擇 API','field.marketType':'交易市場','market.futures':'合約','market.spot':'現貨','field.marketHelp':'預設使用合約;現貨為做多網格,支援主流中心化交易所。','field.direction':'交易方向','dir.long':'做多','dir.short':'做空','dir.neutral':'中性','field.mode':'運行模式','mode.normal':'普通模式','mode.aggressive':'激進模式','mode.classic':'經典網格','field.modeHelp':'普通模式優先控風險與平倉;激進模式優先更快擴展網格倉位;經典網格即時維持上下各50個網格單。','field.symbol':'交易幣種','field.symbolPlaceholder':'輸入 E 試試,例如 ETH/USDT','field.symbolHelp':'已接入 Binance 與 Bitget 合約交易對,輸入首字母即可篩選。','field.symbolHelpSpot':'已接入 Binance、Bitget、Bybit、OKX、Gate 與 Hyperliquid 現貨交易對。','field.risk':'風險控制','action.enabled':'開啟','action.disabled':'關閉','field.priceInterval':'價格間隔','field.orderQty':'每單金額','field.minOrder':'最小訂單價值','action.saveRobot':'儲存機器人','action.logs':'日誌','action.trades':'近期成交','logs.title':'機器人日誌','logs.loading':'正在載入日誌...','logs.empty':'暫無日誌','trades.title':'近期成交','trades.loading':'正在載入成交...','trades.empty':'暫無近期成交','trades.time':'時間','trades.qty':'數量','trades.price':'價格'}
};
I18N.en['field.confirmPassword']='Repeat new password';
I18N.en['field.feeRate']='Exchange fee rate (%)';
I18N.en['metric.position']='Live position';
I18N.en['trades.position']='Position change';
I18N.en['settings.title']='Settings';
I18N.en['settings.subtitle']='Version and preferences';
I18N.en['settings.language']='Language';
I18N.en['settings.theme']='Theme';
I18N.en['settings.password']='Change password';
I18N.en['community.title']='Join user group';
I18N['zh-CN']['field.confirmPassword']='重复新密码';
I18N['zh-CN']['field.feeRate']='交易所费率 (%)';
I18N['zh-CN']['metric.position']='实时持仓';
I18N['zh-CN']['trades.position']='仓位变动';
I18N['zh-CN']['settings.title']='设置';
I18N['zh-CN']['settings.subtitle']='版本与偏好';
I18N['zh-CN']['settings.language']='语言切换';
I18N['zh-CN']['settings.theme']='黑白切换';
I18N['zh-CN']['settings.password']='修改密码';
I18N['zh-CN']['community.title']='加入用户群';
I18N['zh-TW']['field.confirmPassword']='重複新密碼';
I18N['zh-TW']['field.feeRate']='交易所費率 (%)';
I18N['zh-TW']['metric.position']='即時持倉';
I18N['zh-TW']['trades.position']='倉位變動';
I18N['zh-TW']['settings.title']='設定';
I18N['zh-TW']['settings.subtitle']='版本與偏好';
I18N['zh-TW']['settings.language']='語言切換';
I18N['zh-TW']['settings.theme']='黑白切換';
I18N['zh-TW']['settings.password']='修改密碼';
I18N['zh-TW']['community.title']='加入使用者群';
const UI_TRANSLATIONS={
'en':{'app.tagline':'Millisecond-level high-frequency crypto market making system','field.confirmPassword':'Repeat new password','field.feeRate':'Exchange fee rate (%)','field.marketType':'Market','field.mode':'Mode','mode.normal':'Normal mode','mode.aggressive':'Aggressive mode','mode.classic':'Classic grid','field.modeHelp':'Normal mode prioritizes risk control and exits. Aggressive mode prioritizes faster grid expansion. Classic grid keeps 50 live orders above and 50 below in real time.','market.futures':'Futures','market.spot':'Spot','field.marketHelp':'Futures is the default. Spot uses long grids on supported centralized exchanges.','field.symbolHelpSpot':'Binance, Bitget, Bybit, OKX, Gate, and Hyperliquid spot symbols are loaded.','metric.position':'Live position','trades.position':'Position change','settings.language':'Language','settings.theme':'Theme','settings.password':'Change password','community.title':'Join user group','action.logs':'Logs','action.trades':'Recent trades','logs.title':'Bot logs','logs.loading':'Loading logs...','logs.empty':'No logs yet','trades.title':'Recent trades','trades.loading':'Loading trades...','trades.empty':'No recent trades','trades.time':'Time','trades.qty':'Quantity','trades.price':'Price','theme.light':'Switch to light theme','theme.dark':'Switch to dark theme','service.unavailable':'Service is unavailable. Please confirm the console backend is running.','msg.validating':'Verifying API Key, please wait...','msg.apiSaved':'API verified and saved','msg.robotSaved':'Bot saved','msg.robotRestarted':'Parameters saved. The bot restarted with the latest settings.','alert.addApi':'Please add an API first','alert.pauseBeforeEdit':'Pause the bot before editing it','status.starting':'Starting','status.stopping':'Stopping','status.deleting':'Deleting','status.noneRunning':'No bots are running','status.runningCount':'{count} bot(s) running','metric.balance':'Balance','metric.price':'Live price','metric.unrealized':'Unrealized PnL','metric.volume':'Total volume','metric.realized':'Realized PnL','tag.api':'API','tag.unbound':'Unbound','tag.exchange':'Exchange','tag.market':'Market','tag.risk':'Risk','tag.mode':'Mode','action.start':'Start','action.pause':'Pause','action.edit':'Edit','action.delete':'Delete','error.balance':'Failed','position.long':'Long','position.short':'Short'},
'zh-CN':{'app.tagline':'毫秒级高频加密货币做市商系统','field.confirmPassword':'重复新密码','field.feeRate':'交易所费率 (%)','field.marketType':'交易市场','field.mode':'运行模式','mode.normal':'普通模式','mode.aggressive':'激进模式','mode.classic':'经典网格','field.modeHelp':'普通模式优先控风险与平仓;激进模式优先更快扩展网格仓位;经典网格实时维持上下各50个网格单。','market.futures':'合约','market.spot':'现货','field.marketHelp':'默认使用合约;现货为做多网格,支持主流中心化交易所。','field.symbolHelpSpot':'已接入 Binance、Bitget、Bybit、OKX、Gate 与 Hyperliquid 现货交易对。','metric.position':'实时持仓','trades.position':'仓位变动','settings.language':'语言切换','settings.theme':'黑白切换','settings.password':'修改密码','community.title':'加入用户群','action.logs':'日志','action.trades':'近期成交','logs.title':'机器人日志','logs.loading':'正在加载日志...','logs.empty':'暂无日志','trades.title':'近期成交','trades.loading':'正在加载成交...','trades.empty':'暂无近期成交','trades.time':'时间','trades.qty':'数量','trades.price':'价格','theme.light':'切换浅色主题','theme.dark':'切换深色主题','service.unavailable':'服务暂时不可用,请确认控制台后端已启动','msg.validating':'正在校验 API Key,请稍候...','msg.apiSaved':'API 已通过校验并保存','msg.robotSaved':'机器人已保存','msg.robotRestarted':'参数已保存,机器人已按最新参数重新启动','alert.addApi':'请先添加 API','alert.pauseBeforeEdit':'编辑前必须先暂停机器人','status.starting':'启动中','status.stopping':'停止中','status.deleting':'删除中','status.noneRunning':'当前没有运行中的机器人','status.runningCount':'有 {count} 个机器人正在运行','metric.balance':'账户余额','metric.price':'实时价格','metric.unrealized':'未实现盈亏','metric.volume':'累计交易量','metric.realized':'已实现盈亏','tag.api':'API','tag.unbound':'未绑定','tag.exchange':'交易所','tag.market':'市场','tag.risk':'风控','tag.mode':'模式','action.start':'开始','action.pause':'暂停','action.edit':'编辑','action.delete':'删除','error.balance':'读取失败','position.long':'多','position.short':'空'},
'zh-TW':{'app.tagline':'毫秒級高頻加密貨幣做市商系統','field.confirmPassword':'重複新密碼','field.feeRate':'交易所費率 (%)','field.marketType':'交易市場','field.mode':'運行模式','mode.normal':'普通模式','mode.aggressive':'激進模式','mode.classic':'經典網格','field.modeHelp':'普通模式優先控風險與平倉;激進模式優先更快擴展網格倉位;經典網格即時維持上下各50個網格單。','market.futures':'合約','market.spot':'現貨','field.marketHelp':'預設使用合約;現貨為做多網格,支援主流中心化交易所。','field.symbolHelpSpot':'已接入 Binance、Bitget、Bybit、OKX、Gate 與 Hyperliquid 現貨交易對。','metric.position':'即時持倉','trades.position':'倉位變動','settings.language':'語言切換','settings.theme':'黑白切換','settings.password':'修改密碼','community.title':'加入使用者群','action.logs':'日誌','action.trades':'近期成交','logs.title':'機器人日誌','logs.loading':'正在載入日誌...','logs.empty':'暫無日誌','trades.title':'近期成交','trades.loading':'正在載入成交...','trades.empty':'暫無近期成交','trades.time':'時間','trades.qty':'數量','trades.price':'價格','theme.light':'切換淺色主題','theme.dark':'切換深色主題','service.unavailable':'服務暫時不可用,請確認控制台後端已啟動','msg.validating':'正在校驗 API Key,請稍候...','msg.apiSaved':'API 已通過校驗並儲存','msg.robotSaved':'機器人已儲存','msg.robotRestarted':'參數已儲存,機器人已按最新參數重新啟動','alert.addApi':'請先新增 API','alert.pauseBeforeEdit':'編輯前必須先暫停機器人','status.starting':'啟動中','status.stopping':'停止中','status.deleting':'刪除中','status.noneRunning':'目前沒有運行中的機器人','status.runningCount':'有 {count} 個機器人正在運行','metric.balance':'帳戶餘額','metric.price':'即時價格','metric.unrealized':'未實現盈虧','metric.volume':'累計交易量','metric.realized':'已實現盈虧','tag.api':'API','tag.unbound':'未綁定','tag.exchange':'交易所','tag.market':'市場','tag.risk':'風控','tag.mode':'模式','action.start':'開始','action.pause':'暫停','action.edit':'編輯','action.delete':'刪除','error.balance':'讀取失敗','position.long':'多','position.short':'空'},
'ru':{'app.tagline':'Высокочастотная система крипто-маркетмейкинга с миллисекундной реакцией','field.marketType':'Рынок','market.futures':'Фьючерсы','market.spot':'Спот','field.marketHelp':'По умолчанию используются фьючерсы. Спот работает как long grid на поддерживаемых централизованных биржах.','field.symbolHelpSpot':'Загружены спотовые пары Binance, Bitget, Bybit, OKX, Gate и Hyperliquid.','field.confirmPassword':'Повторите новый пароль','field.feeRate':'Комиссия биржи (%)','metric.position':'Позиция','trades.position':'Изменение позиции','settings.language':'Язык','settings.theme':'Тема','settings.password':'Сменить пароль','community.title':'Группа пользователей','action.logs':'Логи','action.trades':'Сделки','logs.title':'Логи бота','logs.loading':'Загрузка логов...','logs.empty':'Логов пока нет','trades.title':'Последние сделки','trades.loading':'Загрузка сделок...','trades.empty':'Сделок пока нет','trades.time':'Время','trades.qty':'Количество','trades.price':'Цена','theme.light':'Переключить на светлую тему','theme.dark':'Переключить на темную тему','service.unavailable':'Сервис недоступен. Проверьте, что backend консоли запущен.','msg.validating':'Проверяем API Key, подождите...','msg.apiSaved':'API проверен и сохранен','msg.robotSaved':'Бот сохранен','msg.robotRestarted':'Параметры сохранены, бот перезапущен','alert.addApi':'Сначала добавьте API','alert.pauseBeforeEdit':'Перед редактированием поставьте бота на паузу','status.starting':'Запуск','status.stopping':'Остановка','status.deleting':'Удаление','status.noneRunning':'Нет запущенных ботов','status.runningCount':'Запущено ботов: {count}','metric.balance':'Баланс','metric.price':'Текущая цена','metric.unrealized':'Нереализованный PnL','metric.volume':'Общий объем','metric.realized':'Реализованный PnL','tag.api':'API','tag.unbound':'Не привязан','tag.exchange':'Биржа','tag.market':'Рынок','tag.risk':'Риск','action.start':'Старт','action.pause':'Пауза','action.edit':'Изменить','action.delete':'Удалить','error.balance':'Ошибка','position.long':'Лонг','position.short':'Шорт'},
'ko':{'app.tagline':'밀리초급 고빈도 암호화폐 마켓메이킹 시스템','field.marketType':'시장','market.futures':'선물','market.spot':'현물','field.marketHelp':'기본값은 선물입니다. 현물은 지원되는 중앙화 거래소에서 롱 그리드로 작동합니다.','field.symbolHelpSpot':'Binance, Bitget, Bybit, OKX, Gate, Hyperliquid 현물 심볼을 불러왔습니다.','field.confirmPassword':'새 비밀번호 반복','field.feeRate':'거래소 수수료율 (%)','metric.position':'실시간 포지션','trades.position':'포지션 변화','settings.language':'언어','settings.theme':'테마','settings.password':'비밀번호 변경','community.title':'사용자 그룹 참여','action.logs':'로그','action.trades':'최근 체결','logs.title':'봇 로그','logs.loading':'로그 불러오는 중...','logs.empty':'아직 로그가 없습니다','trades.title':'최근 체결','trades.loading':'체결 불러오는 중...','trades.empty':'최근 체결이 없습니다','trades.time':'시간','trades.qty':'수량','trades.price':'가격','theme.light':'라이트 테마로 전환','theme.dark':'다크 테마로 전환','service.unavailable':'서비스를 사용할 수 없습니다. 콘솔 백엔드가 실행 중인지 확인하세요.','msg.validating':'API Key 확인 중입니다...','msg.apiSaved':'API 확인 후 저장되었습니다','msg.robotSaved':'봇이 저장되었습니다','msg.robotRestarted':'파라미터가 저장되었고 봇이 재시작되었습니다','alert.addApi':'먼저 API를 추가하세요','alert.pauseBeforeEdit':'편집 전에 봇을 일시정지하세요','status.starting':'시작 중','status.stopping':'정지 중','status.deleting':'삭제 중','status.noneRunning':'실행 중인 봇이 없습니다','status.runningCount':'{count}개 봇 실행 중','metric.balance':'잔고','metric.price':'실시간 가격','metric.unrealized':'미실현 손익','metric.volume':'누적 거래량','metric.realized':'실현 손익','tag.api':'API','tag.unbound':'미연결','tag.exchange':'거래소','tag.market':'시장','tag.risk':'리스크','action.start':'시작','action.pause':'일시정지','action.edit':'편집','action.delete':'삭제','error.balance':'실패','position.long':'롱','position.short':'숏'},
'ja':{'app.tagline':'ミリ秒級の高頻度暗号資産マーケットメイクシステム','field.marketType':'市場','market.futures':'先物','market.spot':'現物','field.marketHelp':'デフォルトは先物です。現物は対応する中央集権型取引所でロンググリッドとして動作します。','field.symbolHelpSpot':'Binance、Bitget、Bybit、OKX、Gate、Hyperliquid の現物銘柄を読み込みました。','field.confirmPassword':'新パスワードを再入力','field.feeRate':'取引所手数料率 (%)','metric.position':'現在ポジション','trades.position':'ポジション変化','settings.language':'言語','settings.theme':'テーマ','settings.password':'パスワード変更','community.title':'ユーザーグループ','action.logs':'ログ','action.trades':'最近の約定','logs.title':'ボットログ','logs.loading':'ログを読み込み中...','logs.empty':'ログはまだありません','trades.title':'最近の約定','trades.loading':'約定を読み込み中...','trades.empty':'最近の約定はありません','trades.time':'時間','trades.qty':'数量','trades.price':'価格','theme.light':'ライトテーマに切り替え','theme.dark':'ダークテーマに切り替え','service.unavailable':'サービスを利用できません。コンソール backend が起動しているか確認してください。','msg.validating':'API Key を確認中です...','msg.apiSaved':'API を確認して保存しました','msg.robotSaved':'ボットを保存しました','msg.robotRestarted':'設定を保存し、ボットを再起動しました','alert.addApi':'先に API を追加してください','alert.pauseBeforeEdit':'編集前にボットを一時停止してください','status.starting':'起動中','status.stopping':'停止中','status.deleting':'削除中','status.noneRunning':'実行中のボットはありません','status.runningCount':'{count} 個のボットが実行中','metric.balance':'残高','metric.price':'現在価格','metric.unrealized':'未実現損益','metric.volume':'累計出来高','metric.realized':'実現損益','tag.api':'API','tag.unbound':'未接続','tag.exchange':'取引所','tag.market':'市場','tag.risk':'リスク','action.start':'開始','action.pause':'一時停止','action.edit':'編集','action.delete':'削除','error.balance':'失敗','position.long':'ロング','position.short':'ショート'},
'es':{'app.tagline':'Sistema de market making cripto de alta frecuencia a nivel de milisegundos','field.marketType':'Mercado','market.futures':'Futuros','market.spot':'Spot','field.marketHelp':'Futuros es el valor predeterminado. Spot usa grids long en exchanges centralizados compatibles.','field.symbolHelpSpot':'Se cargaron símbolos spot de Binance, Bitget, Bybit, OKX, Gate e Hyperliquid.','field.confirmPassword':'Repetir nueva contraseña','field.feeRate':'Comisión del exchange (%)','metric.position':'Posición en vivo','trades.position':'Cambio de posición','settings.language':'Idioma','settings.theme':'Tema','settings.password':'Cambiar contraseña','community.title':'Grupo de usuarios','action.logs':'Logs','action.trades':'Operaciones recientes','logs.title':'Logs del bot','logs.loading':'Cargando logs...','logs.empty':'Aún no hay logs','trades.title':'Operaciones recientes','trades.loading':'Cargando operaciones...','trades.empty':'Sin operaciones recientes','trades.time':'Hora','trades.qty':'Cantidad','trades.price':'Precio','theme.light':'Cambiar a tema claro','theme.dark':'Cambiar a tema oscuro','service.unavailable':'Servicio no disponible. Confirma que el backend de la consola esté ejecutándose.','msg.validating':'Verificando API Key, espera...','msg.apiSaved':'API verificada y guardada','msg.robotSaved':'Bot guardado','msg.robotRestarted':'Parámetros guardados. El bot se reinició con la configuración nueva.','alert.addApi':'Añade una API primero','alert.pauseBeforeEdit':'Pausa el bot antes de editarlo','status.starting':'Iniciando','status.stopping':'Deteniendo','status.deleting':'Eliminando','status.noneRunning':'No hay bots en ejecución','status.runningCount':'{count} bot(s) en ejecución','metric.balance':'Saldo','metric.price':'Precio en vivo','metric.unrealized':'PnL no realizado','metric.volume':'Volumen total','metric.realized':'PnL realizado','tag.api':'API','tag.unbound':'Sin vincular','tag.exchange':'Exchange','tag.market':'Mercado','tag.risk':'Riesgo','action.start':'Iniciar','action.pause':'Pausar','action.edit':'Editar','action.delete':'Eliminar','error.balance':'Falló','position.long':'Long','position.short':'Short'},
'vi':{'app.tagline':'Hệ thống tạo lập thị trường crypto tần suất cao cấp mili giây','field.marketType':'Thị trường','market.futures':'Hợp đồng','market.spot':'Spot','field.marketHelp':'Mặc định dùng hợp đồng. Spot dùng lưới long trên các sàn tập trung được hỗ trợ.','field.symbolHelpSpot':'Đã tải symbol spot Binance, Bitget, Bybit, OKX, Gate và Hyperliquid.','field.confirmPassword':'Nhập lại mật khẩu mới','field.feeRate':'Phí sàn (%)','metric.position':'Vị thế hiện tại','trades.position':'Thay đổi vị thế','settings.language':'Ngôn ngữ','settings.theme':'Giao diện','settings.password':'Đổi mật khẩu','community.title':'Nhóm người dùng','action.logs':'Nhật ký','action.trades':'Giao dịch gần đây','logs.title':'Nhật ký bot','logs.loading':'Đang tải nhật ký...','logs.empty':'Chưa có nhật ký','trades.title':'Giao dịch gần đây','trades.loading':'Đang tải giao dịch...','trades.empty':'Chưa có giao dịch gần đây','trades.time':'Thời gian','trades.qty':'Số lượng','trades.price':'Giá','theme.light':'Chuyển sang giao diện sáng','theme.dark':'Chuyển sang giao diện tối','service.unavailable':'Dịch vụ không khả dụng. Vui lòng kiểm tra backend console đang chạy.','msg.validating':'Đang xác minh API Key...','msg.apiSaved':'API đã xác minh và lưu','msg.robotSaved':'Đã lưu bot','msg.robotRestarted':'Đã lưu tham số, bot đã khởi động lại','alert.addApi':'Vui lòng thêm API trước','alert.pauseBeforeEdit':'Tạm dừng bot trước khi chỉnh sửa','status.starting':'Đang khởi động','status.stopping':'Đang dừng','status.deleting':'Đang xóa','status.noneRunning':'Không có bot nào đang chạy','status.runningCount':'{count} bot đang chạy','metric.balance':'Số dư','metric.price':'Giá hiện tại','metric.unrealized':'PnL chưa thực hiện','metric.volume':'Tổng khối lượng','metric.realized':'PnL đã thực hiện','tag.api':'API','tag.unbound':'Chưa liên kết','tag.exchange':'Sàn','tag.market':'Thị trường','tag.risk':'Rủi ro','action.start':'Bắt đầu','action.pause':'Tạm dừng','action.edit':'Sửa','action.delete':'Xóa','error.balance':'Lỗi','position.long':'Long','position.short':'Short'},
'hi':{'app.tagline':'मिलीसेकंड-स्तर की हाई-फ्रीक्वेंसी क्रिप्टो मार्केट मेकिंग प्रणाली','field.marketType':'मार्केट','market.futures':'फ्यूचर्स','market.spot':'स्पॉट','field.marketHelp':'डिफॉल्ट फ्यूचर्स है। स्पॉट समर्थित केंद्रीकृत एक्सचेंजों पर long grid के रूप में काम करता है।','field.symbolHelpSpot':'Binance, Bitget, Bybit, OKX, Gate और Hyperliquid spot symbols लोड हैं।','field.confirmPassword':'नया पासवर्ड दोहराएँ','field.feeRate':'एक्सचेंज fee rate (%)','metric.position':'लाइव पोजीशन','trades.position':'पोजीशन बदलाव','settings.language':'भाषा','settings.theme':'थीम','settings.password':'पासवर्ड बदलें','community.title':'यूज़र ग्रुप','action.logs':'लॉग','action.trades':'हालिया ट्रेड','logs.title':'बॉट लॉग','logs.loading':'लॉग लोड हो रहे हैं...','logs.empty':'अभी कोई लॉग नहीं','trades.title':'हालिया ट्रेड','trades.loading':'ट्रेड लोड हो रहे हैं...','trades.empty':'कोई हालिया ट्रेड नहीं','trades.time':'समय','trades.qty':'मात्रा','trades.price':'कीमत','theme.light':'लाइट थीम पर जाएँ','theme.dark':'डार्क थीम पर जाएँ','service.unavailable':'सेवा उपलब्ध नहीं है। कृपया console backend चल रहा है या नहीं जांचें।','msg.validating':'API Key सत्यापित हो रही है...','msg.apiSaved':'API सत्यापित और सेव हो गई','msg.robotSaved':'बॉट सेव हो गया','msg.robotRestarted':'पैरामीटर सेव हुए, बॉट फिर से शुरू हुआ','alert.addApi':'पहले API जोड़ें','alert.pauseBeforeEdit':'एडिट से पहले बॉट रोकें','status.starting':'शुरू हो रहा है','status.stopping':'रुक रहा है','status.deleting':'हट रहा है','status.noneRunning':'कोई बॉट चल नहीं रहा','status.runningCount':'{count} बॉट चल रहे हैं','metric.balance':'बैलेंस','metric.price':'लाइव कीमत','metric.unrealized':'अवास्तविक PnL','metric.volume':'कुल वॉल्यूम','metric.realized':'वास्तविक PnL','tag.api':'API','tag.unbound':'अनबाउंड','tag.exchange':'एक्सचेंज','tag.market':'मार्केट','tag.risk':'जोखिम','action.start':'शुरू','action.pause':'रोकें','action.edit':'एडिट','action.delete':'हटाएँ','error.balance':'असफल','position.long':'लॉन्ग','position.short':'शॉर्ट'},
'pt':{'app.tagline':'Sistema de market making cripto de alta frequência em nível de milissegundos','field.marketType':'Mercado','market.futures':'Futuros','market.spot':'Spot','field.marketHelp':'Futuros é o padrão. Spot usa grids long em exchanges centralizadas suportadas.','field.symbolHelpSpot':'Símbolos spot de Binance, Bitget, Bybit, OKX, Gate e Hyperliquid carregados.','field.confirmPassword':'Repetir nova senha','field.feeRate':'Taxa da exchange (%)','metric.position':'Posição ao vivo','trades.position':'Mudança de posição','settings.language':'Idioma','settings.theme':'Tema','settings.password':'Alterar senha','community.title':'Grupo de usuários','action.logs':'Logs','action.trades':'Trades recentes','logs.title':'Logs do bot','logs.loading':'Carregando logs...','logs.empty':'Ainda sem logs','trades.title':'Trades recentes','trades.loading':'Carregando trades...','trades.empty':'Sem trades recentes','trades.time':'Hora','trades.qty':'Quantidade','trades.price':'Preço','theme.light':'Mudar para tema claro','theme.dark':'Mudar para tema escuro','service.unavailable':'Serviço indisponível. Confirme se o backend do console está rodando.','msg.validating':'Verificando API Key, aguarde...','msg.apiSaved':'API verificada e salva','msg.robotSaved':'Bot salvo','msg.robotRestarted':'Parâmetros salvos. O bot reiniciou com as novas configurações.','alert.addApi':'Adicione uma API primeiro','alert.pauseBeforeEdit':'Pause o bot antes de editar','status.starting':'Iniciando','status.stopping':'Parando','status.deleting':'Excluindo','status.noneRunning':'Nenhum bot em execução','status.runningCount':'{count} bot(s) em execução','metric.balance':'Saldo','metric.price':'Preço ao vivo','metric.unrealized':'PnL não realizado','metric.volume':'Volume total','metric.realized':'PnL realizado','tag.api':'API','tag.unbound':'Não vinculado','tag.exchange':'Exchange','tag.market':'Mercado','tag.risk':'Risco','action.start':'Iniciar','action.pause':'Pausar','action.edit':'Editar','action.delete':'Excluir','error.balance':'Falhou','position.long':'Long','position.short':'Short'},
'ar':{'app.tagline':'نظام صناعة سوق للعملات المشفرة عالي التردد على مستوى المللي ثانية','field.marketType':'السوق','market.futures':'العقود','market.spot':'الفوري','field.marketHelp':'الوضع الافتراضي هو العقود. يستخدم الفوري شبكات شراء على المنصات المركزية المدعومة.','field.symbolHelpSpot':'تم تحميل رموز Binance وBitget وBybit وOKX وGate وHyperliquid الفورية.','field.confirmPassword':'كرر كلمة المرور الجديدة','field.feeRate':'معدل رسوم المنصة (%)','metric.position':'المركز الحالي','trades.position':'تغير المركز','settings.language':'اللغة','settings.theme':'السمة','settings.password':'تغيير كلمة المرور','community.title':'مجموعة المستخدمين','action.logs':'السجلات','action.trades':'الصفقات الأخيرة','logs.title':'سجلات البوت','logs.loading':'جار تحميل السجلات...','logs.empty':'لا توجد سجلات بعد','trades.title':'الصفقات الأخيرة','trades.loading':'جار تحميل الصفقات...','trades.empty':'لا توجد صفقات حديثة','trades.time':'الوقت','trades.qty':'الكمية','trades.price':'السعر','theme.light':'التبديل إلى السمة الفاتحة','theme.dark':'التبديل إلى السمة الداكنة','service.unavailable':'الخدمة غير متاحة. تأكد من تشغيل backend للوحة التحكم.','msg.validating':'جار التحقق من API Key...','msg.apiSaved':'تم التحقق من API وحفظه','msg.robotSaved':'تم حفظ البوت','msg.robotRestarted':'تم حفظ الإعدادات وإعادة تشغيل البوت','alert.addApi':'أضف API أولاً','alert.pauseBeforeEdit':'أوقف البوت مؤقتاً قبل التعديل','status.starting':'جار التشغيل','status.stopping':'جار الإيقاف','status.deleting':'جار الحذف','status.noneRunning':'لا توجد بوتات تعمل','status.runningCount':'{count} بوت قيد التشغيل','metric.balance':'الرصيد','metric.price':'السعر الحالي','metric.unrealized':'PnL غير محقق','metric.volume':'إجمالي الحجم','metric.realized':'PnL محقق','tag.api':'API','tag.unbound':'غير مرتبط','tag.exchange':'المنصة','tag.market':'السوق','tag.risk':'المخاطر','action.start':'بدء','action.pause':'إيقاف مؤقت','action.edit':'تعديل','action.delete':'حذف','error.balance':'فشل','position.long':'شراء','position.short':'بيع'}
};
['ru','ko','ja','es','vi','hi','pt','ar','zh-TW'].forEach(lang=>I18N[lang]=Object.assign({},I18N.en,I18N[lang]));
Object.keys(UI_TRANSLATIONS).forEach(lang=>{I18N[lang]=Object.assign({},I18N[lang]||{},UI_TRANSLATIONS[lang]);});
let currentLang='en';
const byId=id=>document.getElementById(id),qsa=s=>Array.from(document.querySelectorAll(s));
function t(key,vars={}){let s=(I18N[currentLang]&&I18N[currentLang][key])||I18N.en[key]||key;Object.keys(vars).forEach(k=>s=s.replaceAll('{'+k+'}',vars[k]));return s}
function detectLang(){const urlLang=new URLSearchParams(location.search).get('lang');if(urlLang&&I18N[urlLang]){localStorage.setItem('nexus_trade_bot_lang',urlLang);return urlLang}const saved=localStorage.getItem('nexus_trade_bot_lang');if(saved&&I18N[saved])return saved;const nav=(navigator.language||'en').toLowerCase();if(nav.startsWith('zh-tw')||nav.startsWith('zh-hk'))return 'zh-TW';if(nav.startsWith('zh'))return 'zh-CN';for(const code of Object.keys(I18N))if(nav.startsWith(code.toLowerCase()))return code;return 'en'}
function initLanguageMenu(){const menu=byId('languageMenu');menu.innerHTML=Object.entries(languages).map(([code,label])=>'<button class="lang-option" data-lang="'+code+'">'+esc(label)+'</button>').join('');qsa('[data-lang]').forEach(btn=>btn.onclick=()=>{applyLanguage(btn.dataset.lang);show('languageMenu',false)})}
function applyLanguage(lang){currentLang=I18N[lang]?lang:'en';localStorage.setItem('nexus_trade_bot_lang',currentLang);document.documentElement.lang=currentLang;document.documentElement.dir=currentLang==='ar'?'rtl':'ltr';qsa('[data-lang]').forEach(btn=>btn.classList.toggle('active',btn.dataset.lang===currentLang));byId('languageBtn').title=languages[currentLang]||'Language';qsa('[data-i18n]').forEach(el=>{el.textContent=t(el.dataset.i18n)});qsa('[data-i18n-placeholder]').forEach(el=>{el.placeholder=t(el.dataset.i18nPlaceholder)});applyTheme(document.body.classList.contains('light')?'light':'dark');renderLoginExchanges();if(accounts.length)renderAccounts();renderRobotAccountOptions(byId('robotAccountId')?byId('robotAccountId').value:'');if(dashboard)renderDashboard();updateMarketTypeUI();updateSymbolOptions()}
async function api(url,opt={}){const r=await fetch(url,{headers:{'Content-Type':'application/json'},credentials:'same-origin',...opt});if(!r.ok){let text=(await r.text()).replace(/^Error:\s*/,'').trim();if(text.startsWith('<!DOCTYPE')||text.startsWith('<html'))text=t('service.unavailable');throw new Error(text)}return r.json()}
function show(id,v){byId(id).classList.toggle('hidden',!v)}
function fmt(n){return Number(n||0).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2})}
function fmtPrice(n){const v=Number(n||0);if(!Number.isFinite(v)||v<=0)return'--';const a=Math.abs(v);const max=a>=100?2:a>=1?5:a>=.01?6:8;const min=a>=1?2:Math.min(4,max);return v.toLocaleString('en-US',{minimumFractionDigits:min,maximumFractionDigits:max})}
function fmtQty(n){const v=Number(n||0);if(!Number.isFinite(v))return'0';const a=Math.abs(v);const max=a>=1?6:a>=.0001?8:10;return v.toLocaleString('en-US',{minimumFractionDigits:0,maximumFractionDigits:max})}
function signedQty(n){const v=Number(n||0);return(v>0?'+':'')+fmtQty(v)}
function positionText(m){const long=Number(m.long_position||0),short=Number(m.short_position||0);if(short>0&&short>=long)return t('position.short')+' '+fmtQty(short);if(long>0)return t('position.long')+' '+fmtQty(long);return'--'}
function tradePositionText(x){const delta=Number(x.position_delta||0),after=Number(x.position_after||0);if(!delta&&!after)return'--';return signedQty(delta)+' → '+signedQty(after)}
function usd(n){return fmt(n)+' USD'}
function signedUsd(n){return (Number(n||0)>=0?'+':'')+usd(n)}
function feeRatePercent(v){const n=Number(v);return Number.isFinite(n)&&n>0?(n*100).toFixed(4).replace(/0+$/,'').replace(/\.$/,''):'0.02'}
function feeRateDecimalFromInput(){const raw=String(byId('accountFeeRate').value||'').trim();if(raw==='')return 0.0002;const n=Number(raw);return Number.isFinite(n)&&n>=0?n/100:0.0002}
function digitSeq(from,to){let a=Number(from),b=Number(to),seq=[String(a)];if(!Number.isInteger(a)||!Number.isInteger(b))return[String(to)];let guard=0;while(a!==b&&guard<12){a=(a+1)%10;seq.push(String(a));guard++}return seq}
function numHTML(v,key=''){const text=String(v),prev=key?animatedTextCache.get(key):'';if(key) animatedTextCache.set(key,text);return'<span class="num '+(prev&&prev!==text?'changed':'')+'">'+esc(text)+'</span>'}
function plainNumHTML(v){return'<span class="plain-num">'+esc(v)+'</span>'}
function fitNumber(el){if(!el)return;el.style.fontSize='';const min=12;let size=parseFloat(getComputedStyle(el).fontSize)||24;let guard=0;while(el.scrollWidth>el.clientWidth&&size>min&&guard<24){size=Math.max(min,size-1);el.style.fontSize=size+'px';guard++}}
function fitNumbers(root=document){requestAnimationFrame(()=>Array.from((root&&root.querySelectorAll?root:document).querySelectorAll('.v')).forEach(fitNumber))}
function setAnimatedText(id,value){const el=byId(id);if(!el)return;const text=String(value),key='metric:'+id;if(animatedTextCache.get(key)===text){fitNumber(el);return}el.innerHTML=numHTML(text,key);fitNumber(el)}
function msg(id,t,k){const e=byId(id);if(!t){e.className='msg hidden';e.textContent='';return}e.className='msg '+(k||'err');e.textContent=t}
function clone(v){return JSON.parse(JSON.stringify(v))}
function get(o,p){return p.split('.').reduce((a,k)=>a&&a[k],o)}
function setv(o,p,v){const ks=p.split('.');let c=o;for(let i=0;i<ks.length-1;i++)c=c[ks[i]];c[ks[ks.length-1]]=v}
function normalizeSymbol(v){return String(v||'').replace('/','').replace(/\s+/g,'').toUpperCase()}
function formatSymbol(v){const s=normalizeSymbol(v);for(const q of ['USDT','USDC','USD'])if(s.endsWith(q)&&s.length>q.length)return s.slice(0,-q.length)+'/'+q;return s}
function currentMarketType(){return (byId('app.market_type')&&byId('app.market_type').value)||get(currentEditingConfig||{},'app.market_type')||get(baseConfig||{},'app.market_type')||'futures'}
function currentTradingMode(){return (byId('trading.mode')&&byId('trading.mode').value)||get(currentEditingConfig||{},'trading.mode')||get(baseConfig||{},'trading.mode')||'normal'}
function marketLabel(v){return v==='spot'?t('market.spot'):t('market.futures')}
function updateMarketTypeUI(){const spot=currentMarketType()==='spot';const classic=currentTradingMode()==='classic';const marketSel=byId('app.market_type');if(marketSel){Array.from(marketSel.options).forEach(o=>{o.disabled=classic&&o.value!=='futures'});if(classic)marketSel.value='futures'}const dirSel=byId('trading.direction');if(dirSel){Array.from(dirSel.options).forEach(o=>{if(classic)o.disabled=o.value!=='neutral';else o.disabled=spot&&o.value!=='long'});if(classic)dirSel.value='neutral';else if(spot)dirSel.value='long'}const help=byId('symbolHelp');if(help)help.textContent=t(spot?'field.symbolHelpSpot':'field.symbolHelp')}
function symbolSource(){return contractSymbols.length?contractSymbols:baseSymbols}
function splitSymbol(s){for(const q of ['USDT','USDC','USD'])if(s.endsWith(q)&&s.length>q.length)return[s.slice(0,-q.length),q];return[s,'']}
function symbolMatches(raw){const source=symbolSource(),out=[],seen=new Set(),limit=30;for(const s of source){if((!raw||s.startsWith(raw))&&!seen.has(s)){out.push(s);seen.add(s);if(out.length>=limit)return out}}if(raw){for(const s of source){if(s.includes(raw)&&!seen.has(s)){out.push(s);seen.add(s);if(out.length>=limit)return out}}}return out}
function hideSymbolMenu(){const menu=byId('symbolMenu');if(menu)menu.classList.add('hidden')}
function renderSymbolMenu(){symbolRAF=0;const input=byId('trading.symbol'),menu=byId('symbolMenu');if(!input||!menu)return;const raw=normalizeSymbol(input.value);const matches=symbolMatches(raw);menu.innerHTML=matches.map(s=>{const parts=splitSymbol(s);return'<button type="button" class="symbol-option" data-symbol="'+esc(s)+'"><span class="symbol-symbol">'+esc(formatSymbol(s))+'</span><span class="symbol-meta">'+esc(parts[1]||currentMarketType().toUpperCase())+'</span></button>'}).join('');menu.classList.toggle('hidden',matches.length===0||document.activeElement!==input)}
function updateSymbolOptions(){if(symbolRAF)cancelAnimationFrame(symbolRAF);symbolRAF=requestAnimationFrame(renderSymbolMenu)}
function pickSymbol(s){byId('trading.symbol').value=formatSymbol(s);hideSymbolMenu()}
function fill(cfg){if(!get(cfg,'app.market_type'))setv(cfg,'app.market_type','futures');qsa('#robotModal input[id*="."],#robotModal select[id*="."]').forEach(el=>{const v=get(cfg,el.id);if(v!==undefined)el.value=el.id==='trading.symbol'?formatSymbol(v):String(v)});updateMarketTypeUI();updateSymbolOptions()}
function collect(cfg){qsa('#robotModal input[id*="."],#robotModal select[id*="."]').forEach(el=>{const old=get(cfg,el.id);if(old!==undefined)setv(cfg,el.id,typeof old==='boolean'?el.value==='true':typeof old==='number'?Number(el.value):el.value)});cfg.app.market_type=cfg.app.market_type||'futures';if(cfg.trading.mode==='classic'){cfg.app.market_type='futures';cfg.trading.direction='neutral'}else if(cfg.app.market_type==='spot'){cfg.trading.direction='long'}cfg.trading.symbol=normalizeSymbol(cfg.trading.symbol);return cfg}
function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
function dir(v){return v==='long'?t('dir.long'):v==='short'?t('dir.short'):t('dir.neutral')}
function modeLabel(v){return v==='classic'?t('mode.classic'):v==='aggressive'?t('mode.aggressive'):t('mode.normal')}
function logoFor(ex){return logos[ex]||''}
function exLabel(ex){return exchangeNames[ex]||String(ex||'').toUpperCase()}
function logoHTML(ex,cls){const url=logoFor(ex);return url?'<img class="'+cls+'" src="'+esc(url)+'">':'<span class="logo-text '+cls+'">'+esc(exLabel(ex).slice(0,2).toUpperCase())+'</span>'}
function setExchangeLogo(imgId, ex){const img=byId(imgId);const url=logoFor(ex);img.src=url;img.style.visibility=url?'visible':'hidden'}
function renderLoginExchanges(){const root=byId('loginExchangeList');if(!root)return;root.innerHTML=supportedExchanges.map(ex=>'<div class="exchange-pill" title="'+esc(exLabel(ex))+'">'+logoHTML(ex,'logo')+'</div>').join('')}
function applyTheme(theme){document.body.classList.toggle('light',theme==='light');const btn=byId('themeToggleBtn');const icon=byId('themeIcon');btn.setAttribute('aria-label',theme==='light'?t('theme.dark'):t('theme.light'));btn.setAttribute('title',theme==='light'?t('theme.dark'):t('theme.light'));icon.src=theme==='light'?'/logo/太阳.png':'/logo/月亮.png';localStorage.setItem('nexus_trade_bot_theme',theme)}
function initTheme(){applyTheme(localStorage.getItem('nexus_trade_bot_theme')||'dark')}
function quickDashboardRefresh(times=4,intervalMs=500){let left=Math.max(1,Number(times)||1);const tick=()=>{loadDashboard(true);left--;if(left>0)setTimeout(tick,intervalMs)};tick()}
function startAuto(){stopAuto();refreshTimer=setInterval(()=>{if(!byId('console').classList.contains('hidden'))loadDashboard(true)},1200)}
function stopAuto(){if(refreshTimer){clearInterval(refreshTimer);refreshTimer=null}}
async function init(){try{const s=await api('/api/status');const ready=s.authenticated&&!s.must_change_password;document.body.classList.toggle('auth-active',!ready);show('loginScreen',!s.authenticated);show('passwordScreen',s.authenticated&&s.must_change_password);show('console',ready);show('authBar',ready);show('topBar',ready);if(ready){await loadBaseConfig();await loadSymbols();await loadAccounts();await loadDashboard();startAuto()}else stopAuto()}catch(e){document.body.classList.add('auth-active');msg('loginMsg',e.message,'err');show('loginScreen',true);show('console',false);show('authBar',false);show('topBar',false);stopAuto()}}
async function loadBaseConfig(){const payload=await api('/api/base-config');baseConfig=payload.config||payload;consoleSettings.timezone=payload.timezone||consoleSettings.timezone;updateTimezoneLabel();const exchanges=Object.keys(baseConfig.exchanges||{}).sort((a,b)=>a==='bitget'?-1:b==='bitget'?1:exLabel(a).localeCompare(exLabel(b)));byId('accountExchange').innerHTML=exchanges.map(x=>'<option value="'+x+'">'+esc(exLabel(x))+'</option>').join('');if(exchanges.includes('bitget'))byId('accountExchange').value='bitget';syncAccountExchangeUI();updateSymbolOptions()}
function updateTimezoneLabel(){if(byId('timezoneLabel'))byId('timezoneLabel').textContent=consoleSettings.timezone||'Asia/Hong_Kong'}
async function saveTimezone(){const input=window.prompt(t('settings.timezonePrompt')||'Timezone',consoleSettings.timezone||'Asia/Hong_Kong');if(input===null)return;const value=String(input).trim();if(!value)return;try{consoleSettings=await api('/api/settings',{method:'PUT',body:JSON.stringify({timezone:value})});updateTimezoneLabel();await loadDashboard(true)}catch(e){alert(e.message)}}
async function loadSymbols(){const market=currentMarketType();try{const data=await api('/api/symbols?market_type='+encodeURIComponent(market));contractSymbols=(data.symbols||[]).map(normalizeSymbol).filter(Boolean);updateMarketTypeUI();updateSymbolOptions()}catch(e){contractSymbols=[...baseSymbols];updateMarketTypeUI();updateSymbolOptions()}}
async function loadAccounts(){accounts=await api('/api/accounts');renderAccounts();renderRobotAccountOptions()}
function renderAccounts(){const root=byId('accountList');root.innerHTML='';accounts.forEach(a=>root.insertAdjacentHTML('beforeend','<div class="item"><div class="ex-inline">'+logoHTML(a.exchange,'logo')+'<div><strong>'+esc(a.name)+'</strong><div style="color:var(--muted);margin-top:4px">'+esc(exLabel(a.exchange))+'</div></div></div><div class="row"><button class="btn" data-ea="'+a.id+'">'+t('action.edit')+'</button><button class="btnd" data-da="'+a.id+'">'+t('action.delete')+'</button></div></div>'));qsa('[data-ea]').forEach(b=>b.onclick=()=>editAccount(b.dataset.ea));qsa('[data-da]').forEach(b=>b.onclick=()=>deleteAccount(b.dataset.da))}
function renderRobotAccountOptions(selected){byId('robotAccountId').innerHTML=accounts.map(a=>'<option value="'+a.id+'">'+esc(a.name)+' · '+esc(exLabel(a.exchange))+'</option>').join('');if(selected)byId('robotAccountId').value=selected;syncRobotAccountDisplay()}
function syncRobotAccountDisplay(){const a=accounts.find(x=>x.id===byId('robotAccountId').value);const ex=a?a.exchange:'';byId('robotExchangeDisplay').value=ex?exLabel(ex):'';setExchangeLogo('robotExchangeLogo',ex)}
function syncAccountExchangeUI(){const ex=byId('accountExchange').value;setExchangeLogo('accountExchangeLogo',ex);show('passphraseField',ex==='bitget'||ex==='okx')}
function toggleSettingsMenu(){show('settingsMenu',byId('settingsMenu').classList.contains('hidden'));show('languageMenu',false)}
function closeSettingsMenus(){show('settingsMenu',false);show('languageMenu',false)}
function openChangePasswordModal(){byId('menuOldPass').value='';byId('menuNewPass').value='';byId('menuConfirmPass').value='';msg('menuChangeMsg','');show('changePasswordModal',true);closeSettingsMenus()}
function closeChangePasswordModal(){show('changePasswordModal',false);msg('menuChangeMsg','')}
async function changePasswordFromMenu(){try{await api('/api/change-password',{method:'POST',body:JSON.stringify({old_password:byId('menuOldPass').value,new_password:byId('menuNewPass').value,confirm_password:byId('menuConfirmPass').value})});msg('menuChangeMsg',t('password.button')+' OK','ok');byId('menuOldPass').value='';byId('menuNewPass').value='';byId('menuConfirmPass').value='';setTimeout(closeChangePasswordModal,400)}catch(e){msg('menuChangeMsg',e.message,'err')}}
function openAccountModal(){editingAccountId=null;byId('accountName').value='';byId('accountApiKey').value='';byId('accountSecretKey').value='';byId('accountPassphrase').value='';byId('accountFeeRate').value='0.02';if(byId('accountExchange').options.length)byId('accountExchange').value=Array.from(byId('accountExchange').options).some(o=>o.value==='bitget')?'bitget':byId('accountExchange').options[0].value;syncAccountExchangeUI();msg('accountMsg','');show('accountModal',true)}
function editAccount(id){const a=accounts.find(x=>x.id===id);if(!a)return;editingAccountId=id;byId('accountName').value=a.name;byId('accountExchange').value=a.exchange;byId('accountApiKey').value=a.config.api_key||'';byId('accountSecretKey').value=a.config.secret_key||'';byId('accountPassphrase').value=a.config.passphrase||'';byId('accountFeeRate').value=feeRatePercent(a.config.fee_rate);syncAccountExchangeUI();msg('accountMsg','');show('accountModal',true)}
function closeAccountModal(){show('accountModal',false)}
async function saveAccount(){try{msg('accountMsg',t('msg.validating'));const payload={name:byId('accountName').value.trim(),exchange:byId('accountExchange').value,config:{api_key:byId('accountApiKey').value.trim(),secret_key:byId('accountSecretKey').value.trim(),passphrase:byId('accountPassphrase').value.trim(),fee_rate:feeRateDecimalFromInput()}};if(editingAccountId)await api('/api/accounts/'+editingAccountId,{method:'PUT',body:JSON.stringify(payload)});else await api('/api/accounts',{method:'POST',body:JSON.stringify(payload)});msg('accountMsg',t('msg.apiSaved'),'ok');await loadAccounts();setTimeout(closeAccountModal,200)}catch(e){msg('accountMsg',e.message,'err')}}
async function deleteAccount(id){try{await api('/api/accounts/'+id,{method:'DELETE'});await loadAccounts();await loadDashboard(true)}catch(e){alert(e.message)}}
function openRobotModal(robot){if(!accounts.length){alert(t('alert.addApi'));openAccountModal();return}editingRobotId=robot?robot.id:null;editingRobotStatus=robot?robot.status:'stopped';currentEditingConfig=clone(robot?robot.config:baseConfig);fill(currentEditingConfig);renderRobotAccountOptions(robot?robot.account_id:accounts[0].id);loadSymbols();msg('modalMsg','');show('robotModal',true)}
function closeRobotModal(){show('robotModal',false);editingRobotId=null;editingRobotStatus='stopped';msg('modalMsg','')}
async function loadDashboard(silent){if(dashboardLoading)return;dashboardLoading=true;try{dashboard=await api('/api/dashboard');renderDashboard()}catch(e){if(!silent)alert(e.message)}finally{dashboardLoading=false}}
function robotById(id){return dashboard&&dashboard.robots?dashboard.robots.find(x=>x.id===id):null}
function robotElement(id){return Array.from(byId('robotGrid').children).find(el=>el.dataset.robotId===String(id))}
function setRobotStatus(id,status){const r=robotById(id);if(!r)return;r.status=status;if(status==='running'){r.started_at=new Date().toISOString();r.last_error='';r.exit_reason=''}renderDashboard()}
function setRobotPending(id,action){if(action)robotActionPending.set(id,action);else robotActionPending.delete(id);renderRobotCard(id)}
function renderAccountBalances(list){const root=byId('accountBalances');const items=list||[];if(!items.length){setAnimatedText('accountBalances',t('empty.noApi'));return}const totals={};let ok=0;items.forEach(a=>{if(a.error)return;const quote=a.quote_asset||'USDT';totals[quote]=(totals[quote]||0)+(Number(a.balance)||0);ok++});if(!ok){root.innerHTML='<span class="balance-error">'+t('error.balance')+'</span>';animatedTextCache.set('accountBalances',t('error.balance'));return}setAnimatedText('accountBalances',Object.keys(totals).sort().map(q=>fmt(totals[q])+' '+q).join(' + '))}
function cardFromHTML(html){const tpl=document.createElement('template');tpl.innerHTML=html.trim();return tpl.content.firstElementChild}
function mountRobotCard(grid,html){const next=cardFromHTML(html);if(!next)return;next.classList.add('is-new');grid.appendChild(next);setTimeout(()=>next.classList.remove('is-new'),360)}
function patchRobotCard(old,html){const next=cardFromHTML(html);if(!next)return;old.className=next.className;old.dataset.robotId=next.dataset.robotId||old.dataset.robotId;old.innerHTML=next.innerHTML}
function upsertRobotCard(grid,r){const html=robotCard(r),old=robotElement(r.id);if(!old){mountRobotCard(grid,html)}else if(robotHTMLCache.get(r.id)!==html){patchRobotCard(old,html)}robotHTMLCache.set(r.id,html)}
function renderDashboard(){const s=dashboard.summary;renderAccountBalances(dashboard.accounts||[]);setAnimatedText('sumTodayFees',usd(s.today_fees||0));setAnimatedText('sumTotalFees',usd(s.total_fees||0));setAnimatedText('sumUnrealized',signedUsd(s.total_unrealized_pnl));setAnimatedText('sumTodayRealized',signedUsd(s.today_realized_pnl));setAnimatedText('sumTotalRealized',signedUsd(s.total_realized_pnl));setAnimatedText('sumTodayVolume',usd(s.today_volume));setAnimatedText('sumTotalVolume',usd(s.total_volume));const running=s.running_count>0;byId('globalStatus').className='status '+(running?'running':'stopped');byId('globalStatusText').textContent=running?t('status.runningCount',{count:s.running_count}):t('status.noneRunning');const grid=byId('robotGrid');const seen=new Set();show('robotsEmpty',dashboard.robots.length===0);dashboard.robots.forEach(r=>{seen.add(String(r.id));upsertRobotCard(grid,r)});Array.from(grid.children).forEach(el=>{if(!seen.has(el.dataset.robotId)){robotHTMLCache.delete(el.dataset.robotId);el.remove()}});fitNumbers(byId('console'))}
function renderRobotCard(id){const r=robotById(id),grid=byId('robotGrid');if(!r||!grid)return;upsertRobotCard(grid,r);fitNumbers(grid)}
function pendingText(action){return action==='start'?t('status.starting'):action==='pause'?t('status.stopping'):action==='delete'?t('status.deleting'):''}
function actionButton(cls,attr,id,label,loading,disabled){return'<button class="'+cls+(loading?' loading':'')+'" '+attr+'="'+esc(id)+'" '+(loading||disabled?'disabled aria-busy="'+(loading?'true':'false')+'"':'')+'><span class="btn-content">'+(loading?'<span class="spinner"></span>':'')+'<span>'+label+'</span></span></button>'}
function robotCard(r){const pending=robotActionPending.get(r.id)||'';const statusText=pending?pendingText(pending):(r.status==='running'?t('status.running'):r.status==='paused'?t('status.paused'):t('status.stopped'));const statusClass=pending?'pending':r.status;const pnl=signedUsd(r.metric.unrealized_pnl);const realized=signedUsd(r.metric.total_realized_pnl);const volume=usd(r.metric.total_volume);const fees=usd(r.metric.total_fees||0);const price=r.metric.current_price>0?fmtPrice(r.metric.current_price):(r.metric.price_error?t('error.balance'):'--');const pos=positionText(r.metric);const ex=(r.account&&r.account.exchange?r.account.exchange:r.config.app.current_exchange);const market=(r.config.app&&r.config.app.market_type)||'futures';const mode=(r.config.trading&&r.config.trading.mode)||'normal';const errMsg=r.last_error||r.metric.error||r.metric.price_error||'';const busy=!!pending;const primary=r.status==='running'?actionButton('btnw','data-pause',r.id,pending==='pause'?pendingText(pending):t('action.pause'),pending==='pause',busy&&pending!=='pause'):actionButton('btnp','data-start',r.id,pending==='start'?pendingText(pending):t('action.start'),pending==='start',busy&&pending!=='start');const edit=actionButton('btn','data-edit',r.id,t('action.edit'),false,busy);const logs=actionButton('btn','data-logs',r.id,t('action.logs'),false,pending==='delete');const trades=actionButton('btn','data-trades',r.id,t('action.trades'),false,pending==='delete');const del=actionButton('btnd','data-delete',r.id,pending==='delete'?pendingText(pending):t('action.delete'),pending==='delete',busy&&pending!=='delete');return '<div class="robot card" data-robot-id="'+esc(r.id)+'"><div class="robot-top"><div><div class="ex-inline">'+logoHTML(ex,'logo-lg')+'<div><div class="rname" title="'+esc(r.name)+'">'+esc(r.name)+'</div><div class="rsub">'+esc(formatSymbol(r.config.trading.symbol))+' · '+marketLabel(market)+' · '+dir(r.config.trading.direction)+'</div></div></div></div><div class="status '+statusClass+'"><span class="dot"></span><span>'+statusText+'</span></div></div><div class="robot-stats"><div class="stat price-stat"><div class="k">'+t('metric.price')+'</div><div class="v">'+numHTML(price,'robot:'+r.id+':price')+'</div></div><div class="stat position-stat"><div class="k">'+t('metric.position')+'</div><div class="v">'+plainNumHTML(pos)+'</div></div><div class="stat"><div class="k">'+t('metric.fees')+'</div><div class="v">'+numHTML(fees,'robot:'+r.id+':fees')+'</div></div><div class="stat"><div class="k">'+t('metric.unrealized')+'</div><div class="v">'+numHTML(pnl,'robot:'+r.id+':unrealized')+'</div></div><div class="stat"><div class="k">'+t('metric.volume')+'</div><div class="v">'+numHTML(volume,'robot:'+r.id+':volume')+'</div></div><div class="stat"><div class="k">'+t('metric.realized')+'</div><div class="v">'+numHTML(realized,'robot:'+r.id+':realized')+'</div></div></div><div class="tags"><span class="tag">'+t('tag.api')+' '+esc(r.account?r.account.name:t('tag.unbound'))+'</span><span class="tag">'+t('tag.exchange')+' '+esc(exLabel(ex))+'</span><span class="tag">'+t('tag.market')+' '+marketLabel(market)+'</span><span class="tag">'+t('tag.mode')+' '+modeLabel(mode)+'</span><span class="tag">'+t('tag.risk')+' '+(r.config.risk_control.enabled?t('action.enabled'):t('action.disabled'))+'</span></div><div class="row robot-actions">'+primary+edit+logs+trades+del+'</div>'+(errMsg?'<div class="msg err">'+esc(errMsg)+'</div>':'')+'</div>'}
function bindRobotGridActions(){byId('robotGrid').addEventListener('click',e=>{const b=e.target.closest('button');if(!b||b.disabled)return;if(b.dataset.start)startRobot(b.dataset.start);else if(b.dataset.pause)pauseRobot(b.dataset.pause);else if(b.dataset.edit)editRobot(b.dataset.edit);else if(b.dataset.logs)openLogsModal(b.dataset.logs);else if(b.dataset.trades)openTradesModal(b.dataset.trades);else if(b.dataset.delete)deleteRobot(b.dataset.delete)})}
function editRobot(id){const r=dashboard.robots.find(x=>x.id===id);if(!r)return;if(r.status==='running'){alert(t('alert.pauseBeforeEdit'));return}openRobotModal(r)}
function formatTradeTime(v){const d=new Date(v);if(Number.isNaN(d.getTime()))return'--';return d.toLocaleString(undefined,{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'})}
function renderTrades(trades){const rows=trades||[];const body=byId('tradeRows');if(!rows.length){body.innerHTML='<tr><td colspan="4" style="text-align:center;color:var(--muted);padding:28px">'+esc(t('trades.empty'))+'</td></tr>';return}body.innerHTML=rows.map(x=>'<tr><td>'+esc(formatTradeTime(x.time))+'</td><td>'+numHTML(fmtQty(x.quantity),'trade:'+x.client_order_id+':'+x.time+':qty')+'</td><td>'+numHTML(fmtPrice(x.price),'trade:'+x.client_order_id+':'+x.time+':price')+'</td><td>'+plainNumHTML(tradePositionText(x))+'</td></tr>').join('');fitNumbers(body)}
function closeTradesModal(){show('tradesModal',false);msg('tradesMsg','')}
function closeLogsModal(){show('logsModal',false);msg('logsMsg','')}
async function openLogsModal(id){const r=robotById(id);const title=r?(r.name+' · '+formatSymbol(r.config.trading.symbol)):'';byId('logsTitle').textContent=t('logs.title');byId('logsSub').textContent=title;byId('logContent').textContent=t('logs.loading');msg('logsMsg','');show('logsModal',true);try{const data=await api('/api/robots/'+id+'/logs');byId('logsSub').textContent=title+(data.path?' · '+data.path:'');byId('logContent').textContent=(data.logs&&data.logs.trim())?data.logs:t('logs.empty');byId('logContent').scrollTop=byId('logContent').scrollHeight}catch(e){byId('logContent').textContent='';msg('logsMsg',e.message,'err')}}
async function openTradesModal(id){const r=robotById(id);byId('tradesTitle').textContent=t('trades.title');byId('tradesSub').textContent=r?(r.name+' · '+formatSymbol(r.config.trading.symbol)):'';byId('tradeRows').innerHTML='<tr><td colspan="4" style="text-align:center;color:var(--muted);padding:28px"><span class="spinner"></span> '+esc(t('trades.loading'))+'</td></tr>';msg('tradesMsg','');show('tradesModal',true);try{const data=await api('/api/robots/'+id+'/trades');renderTrades(data.trades||[])}catch(e){byId('tradeRows').innerHTML='';msg('tradesMsg',e.message,'err')}}
async function saveRobot(){try{const cfg=collect(currentEditingConfig||clone(baseConfig));const payload={config:cfg,account_id:byId('robotAccountId').value};const wasPaused=editingRobotStatus==='paused';if(editingRobotId){await api('/api/robots/'+editingRobotId,{method:'PUT',body:JSON.stringify(payload)});if(wasPaused){await api('/api/robots/'+editingRobotId+'/start',{method:'POST'});msg('modalMsg',t('msg.robotRestarted'),'ok')}else{msg('modalMsg',t('msg.robotSaved'),'ok')}}else{await api('/api/robots',{method:'POST',body:JSON.stringify(payload)});msg('modalMsg',t('msg.robotSaved'),'ok')}}catch(e){msg('modalMsg',e.message,'err');return}await loadDashboard();setTimeout(closeRobotModal,250)}
async function startRobot(id){if(robotActionPending.has(id))return;setRobotPending(id,'start');try{await api('/api/robots/'+id+'/start',{method:'POST'});setRobotPending(id,'');setRobotStatus(id,'running');quickDashboardRefresh(6,350)}catch(e){setRobotPending(id,'');alert(e.message)}}
async function pauseRobot(id){if(robotActionPending.has(id))return;setRobotPending(id,'pause');try{await api('/api/robots/'+id+'/pause',{method:'POST'});setRobotPending(id,'');await loadDashboard(true);quickDashboardRefresh(6,350)}catch(e){setRobotPending(id,'');alert(e.message)}}
async function deleteRobot(id){if(robotActionPending.has(id))return;setRobotPending(id,'delete');try{await api('/api/robots/'+id,{method:'DELETE'});robotActionPending.delete(id);await loadDashboard(true);quickDashboardRefresh(4,350)}catch(e){setRobotPending(id,'');alert(e.message)}}
async function logout(){try{await api('/api/logout',{method:'POST'});dashboard=null;accounts=[];stopAuto();await init()}catch(e){alert(e.message)}}
byId('loginBtn').onclick=async()=>{try{await api('/api/login',{method:'POST',body:JSON.stringify({username:byId('loginUser').value,password:byId('loginPass').value})});await init()}catch(e){msg('loginMsg',e.message,'err')}};
byId('changePassBtn').onclick=async()=>{try{await api('/api/change-password',{method:'POST',body:JSON.stringify({new_password:byId('newPass').value,confirm_password:byId('confirmPass').value})});await init()}catch(e){msg('changeMsg',e.message,'err')}};
byId('manageAccountsBtn').onclick=openAccountModal;byId('createRobotBtn').onclick=()=>openRobotModal();byId('logoutBtn').onclick=logout;byId('settingsBtn').onclick=e=>{e.stopPropagation();toggleSettingsMenu()};byId('passwordMenuBtn').onclick=openChangePasswordModal;byId('timezoneBtn').onclick=saveTimezone;byId('closeChangePasswordBtn').onclick=closeChangePasswordModal;byId('menuChangePassBtn').onclick=changePasswordFromMenu;byId('closeAccountModalBtn').onclick=closeAccountModal;byId('saveAccountBtn').onclick=saveAccount;byId('resetAccountFormBtn').onclick=openAccountModal;byId('accountExchange').onchange=syncAccountExchangeUI;byId('closeModalBtn').onclick=closeRobotModal;byId('closeTradesModalBtn').onclick=closeTradesModal;byId('closeLogsModalBtn').onclick=closeLogsModal;byId('saveRobotBtn').onclick=saveRobot;byId('robotAccountId').onchange=()=>{syncRobotAccountDisplay();updateMarketTypeUI()};byId('app.market_type').onchange=()=>{updateMarketTypeUI();loadSymbols()};byId('trading.mode').onchange=()=>{updateMarketTypeUI();loadSymbols()};byId('trading.symbol').addEventListener('focus',updateSymbolOptions);byId('trading.symbol').addEventListener('input',updateSymbolOptions);byId('trading.symbol').addEventListener('change',e=>{e.target.value=formatSymbol(e.target.value);updateSymbolOptions()});byId('symbolMenu').addEventListener('mousedown',e=>{const b=e.target.closest('[data-symbol]');if(!b)return;e.preventDefault();pickSymbol(b.dataset.symbol)});bindRobotGridActions();byId('themeToggleBtn').onclick=()=>{applyTheme(document.body.classList.contains('light')?'dark':'light');closeSettingsMenus()};byId('languageBtn').onclick=e=>{e.stopPropagation();show('languageMenu',byId('languageMenu').classList.contains('hidden'))};document.addEventListener('click',e=>{if(!e.target.closest('.settings-wrap'))closeSettingsMenus();if(!e.target.closest('.symbol-combobox'))hideSymbolMenu()});window.addEventListener('resize',()=>fitNumbers());window.addEventListener('keydown',e=>{if(e.key==='Escape'){closeSettingsMenus();hideSymbolMenu();closeRobotModal();closeAccountModal();closeTradesModal();closeLogsModal();closeChangePasswordModal()}});initLanguageMenu();initTheme();applyLanguage(detectLang());updateTimezoneLabel();updateSymbolOptions();init();
</script>
</body>
</html>`