neuroalpha 0.3.16 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/config/certs/russian_trusted_root_ca.pem +33 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -6
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +2 -2
- package/package.json +60 -53
- package/public/activate.html +36 -0
- package/public/activate.js +33 -0
- package/public/admin.html +126 -7
- package/public/app.js +608 -182
- package/public/index.html +57 -47
- package/public/landing.html +106 -37
- package/public/legal.html +4 -4
- package/public/styles.css +152 -36
- package/server/api/routes.js +308 -37
- package/server/auth/index.js +79 -15
- package/server/brokers/index.js +1022 -172
- package/server/build-info.js +7 -0
- package/server/config.js +13 -6
- package/server/data/moex.js +6 -4
- package/server/db.js +22 -18
- package/server/desktop-entry.js +103 -0
- package/server/hub.js +28 -14
- package/server/index.js +82 -12
- package/server/instance/agent.js +259 -0
- package/server/instance/central.js +491 -0
- package/server/instance/identity.js +79 -0
- package/server/instance/provision.js +166 -0
- package/server/instance/signing.js +71 -0
- package/server/model/edge.js +19 -0
- package/server/model/forecast.js +107 -57
- package/server/model/live.js +1 -1
- package/server/model/models.js +12 -3
- package/server/model/research.js +218 -30
- package/server/notify/index.js +51 -16
- package/server/params-meta.js +3 -3
- package/server/role.js +44 -0
- package/server/security.js +40 -0
- package/server/strategist/index.js +462 -488
- package/server/strategist/tools.js +675 -532
- package/server/tls-ca.js +32 -0
|
@@ -1,532 +1,675 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
-
import { PARAMS, PARAM_MAP } from '../params-meta.js';
|
|
3
|
-
import { getSettings, updateSettings, validateChanges } from '../settings.js';
|
|
4
|
-
import { getRun, listRuns
|
|
5
|
-
import { analyzeRun, compareRuns, recentRuns
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
+
import { PARAMS, PARAM_MAP } from '../params-meta.js';
|
|
3
|
+
import { getSettings, updateSettings, validateChanges } from '../settings.js';
|
|
4
|
+
import { getRun, listRuns } from '../db.js';
|
|
5
|
+
import { analyzeRun, compareRuns, recentRuns } from './analytics.js';
|
|
6
|
+
import { forecastNow } from '../model/forecast.js';
|
|
7
|
+
import { listNews } from '../news/index.js';
|
|
8
|
+
import { BROKER_META, brokersState, discoverBrokerAccounts, saveBrokerAccount, syncBrokerAccount, prepareBrokerOrders, fetchBrokerHistory } from '../brokers/index.js';
|
|
9
|
+
import { fmtBarMsk, fmtDayMsk, MSK, DAY, round } from '../util.js';
|
|
10
|
+
|
|
11
|
+
export const OOS_LIMIT = 3;
|
|
12
|
+
const AI_LOCKED = PARAMS.filter((p) => p.ai === false).map((p) => p.key);
|
|
13
|
+
|
|
14
|
+
const changesSchema = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
description: 'Изменения настроек: ключ — точное название настройки из списка в системной инструкции (например modelShortFeePct, newsFilter), значение — новое значение.',
|
|
17
|
+
properties: Object.fromEntries(PARAMS.filter((p) => p.ai !== false && !p.hidden && !p.fixed).map((p) => [p.key, {
|
|
18
|
+
type: p.type === 'checkbox' ? 'boolean' : p.type === 'select' ? 'string' : 'number',
|
|
19
|
+
description: `${p.label}${p.min !== undefined ? ` (${p.min}–${p.max})` : ''}`,
|
|
20
|
+
}])),
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function checkChanges(changes) {
|
|
25
|
+
const locked = Object.keys(changes || {}).filter((k) => AI_LOCKED.includes(k));
|
|
26
|
+
const { values, errors } = validateChanges(changes);
|
|
27
|
+
for (const k of locked) { errors.push(`${k}: эту настройку ИИ менять нельзя`); delete values[k]; }
|
|
28
|
+
return { values, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const describeChanges = (values) => Object.entries(values).map(([k, v]) => `${PARAM_MAP[k]?.label || k}: ${v}`).join('; ') || 'без изменений';
|
|
32
|
+
|
|
33
|
+
export const TOOLS = {
|
|
34
|
+
get_brokers_status: {
|
|
35
|
+
kind: 'read',
|
|
36
|
+
label: 'Смотрит брокерские счета и портфель',
|
|
37
|
+
description: 'Текущий снимок подключённых брокерских счетов ПРЯМО СЕЙЧАС: баланс, свободные рубли, открытые позиции в портфеле, активные заявки в стакане и подготовленный пакет заявок. ВНИМАНИЕ: чтобы посмотреть историю прошлых сделок (как прошли торги вчера, сегодня или за неделю), вызывай get_broker_operations!',
|
|
38
|
+
parameters: { type: 'object', properties: {} },
|
|
39
|
+
async run() {
|
|
40
|
+
const st = brokersState();
|
|
41
|
+
const accs = (st.accounts || []).map((a) => ({
|
|
42
|
+
id: a.id,
|
|
43
|
+
название: a.name,
|
|
44
|
+
брокер: a.broker,
|
|
45
|
+
субсчёт: a.accountId || '—',
|
|
46
|
+
режим_работы: a.mode,
|
|
47
|
+
песочница: Boolean(a.sandbox),
|
|
48
|
+
режим_старого_портфеля: a.legacyMode || 'isolate',
|
|
49
|
+
лимит_на_стратегию_руб: a.maxCapitalRub,
|
|
50
|
+
дневной_стоп_проц: a.maxDailyDrawdownPct,
|
|
51
|
+
отступ_лимитки_проц: a.limitOffsetPct,
|
|
52
|
+
пауза_причина: a.haltedReason || null,
|
|
53
|
+
время_снимка_портфеля_мск: a.lastSync?.at ? fmtBarMsk(a.lastSync.at) : null,
|
|
54
|
+
баланс_руб: a.lastSync?.totalRub ?? null,
|
|
55
|
+
свободные_рубли: a.lastSync?.cashRub ?? null,
|
|
56
|
+
маржинальная_торговля: a.lastSync?.marginEnabled ?? null,
|
|
57
|
+
текущие_позиции_в_портфеле: (a.lastSync?.positions || []).map((p) => ({
|
|
58
|
+
тикер: p.symbol,
|
|
59
|
+
название: p.name,
|
|
60
|
+
кол_во_шт: p.qty,
|
|
61
|
+
средняя_цена: p.avgPrice,
|
|
62
|
+
текущая_цена: p.price,
|
|
63
|
+
стоимость_руб: p.valueRub,
|
|
64
|
+
доход_руб: p.pnlRub,
|
|
65
|
+
...(a.entryTsBySymbol?.[p.symbol] ? { время_входа_робота_мск: fmtBarMsk(a.entryTsBySymbol[p.symbol]) } : {}),
|
|
66
|
+
})),
|
|
67
|
+
активные_заявки_в_стакане: a.lastSync?.activeOrders || [],
|
|
68
|
+
подготовленный_пакет_заявок: a.pendingBatch ? {
|
|
69
|
+
свеча: a.pendingBatch.bar,
|
|
70
|
+
статус: a.pendingBatch.status,
|
|
71
|
+
рабочий_бюджет_руб: a.pendingBatch.budgetRub,
|
|
72
|
+
свободно_под_вход_руб: a.pendingBatch.availRub,
|
|
73
|
+
направление: a.pendingBatch.effectiveDirection,
|
|
74
|
+
диагностика: a.pendingBatch.diagNote || null,
|
|
75
|
+
тарифная_заметка: a.pendingBatch.planThrottleNote || null,
|
|
76
|
+
заявки: a.pendingBatch.orders || [],
|
|
77
|
+
} : null,
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
result: {
|
|
81
|
+
текущее_время_мск: fmtBarMsk(Date.now()),
|
|
82
|
+
подключено_счетов: accs.length,
|
|
83
|
+
счета: accs,
|
|
84
|
+
},
|
|
85
|
+
summary: accs.length ? `Проверил брокерские счета (${accs.length})` : 'Брокерские счета пока не подключены',
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
get_broker_operations: {
|
|
91
|
+
kind: 'read',
|
|
92
|
+
label: 'Смотрит историю сделок по счёту',
|
|
93
|
+
description: 'Исполненные сделки и операции по брокерскому счёту за выбранный период (вчера, сегодня, за неделю или за конкретные даты YYYY-MM-DD): покупки, продажи, время сделок по МСК, цены, суммы, комиссии и итог торгов за период.',
|
|
94
|
+
parameters: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
period: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
enum: ['yesterday', 'today', 'week', 'month', 'custom'],
|
|
100
|
+
description: 'yesterday — вчерашний день, today — сегодня, week — последние 7 дней, month — последние 30 дней, custom — по датам fromDate/toDate',
|
|
101
|
+
},
|
|
102
|
+
fromDate: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'начальная дата в формате YYYY-MM-DD по МСК (необязательно, если задан period)',
|
|
105
|
+
},
|
|
106
|
+
toDate: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'конечная дата в формате YYYY-MM-DD по МСК (необязательно, если задан period)',
|
|
109
|
+
},
|
|
110
|
+
accountId: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'id счёта в платформе (brk_…), если не указан — проверяются все подключённые счета',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
async run({ period = 'yesterday', fromDate, toDate, accountId } = {}) {
|
|
117
|
+
let st = brokersState();
|
|
118
|
+
const targetAccs = (st.accounts || []).filter((a) => !accountId || a.id === accountId || a.accountId === accountId);
|
|
119
|
+
if (!targetAccs.length) {
|
|
120
|
+
return { result: { ошибка: 'Подключённые брокерские счета не найдены' }, summary: 'Счета не найдены', ok: false };
|
|
121
|
+
}
|
|
122
|
+
// Освежаем историю операций у брокера, если она ещё не загружалась или старше 2 минут
|
|
123
|
+
for (const a of targetAccs) {
|
|
124
|
+
if (a.broker === 'tbank' && a.hasToken && (!a.lastHistory?.at || Date.now() - a.lastHistory.at > 120000)) {
|
|
125
|
+
try { st = await fetchBrokerHistory(a.id); } catch { /* ignore */ }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const nowMskDate = new Date(Date.now() + MSK);
|
|
129
|
+
const todayIso = nowMskDate.toISOString().slice(0, 10);
|
|
130
|
+
const yesterdayIso = new Date(Date.now() + MSK - DAY).toISOString().slice(0, 10);
|
|
131
|
+
|
|
132
|
+
let startIso = yesterdayIso;
|
|
133
|
+
let endIso = yesterdayIso;
|
|
134
|
+
let periodLabel = `вчера (${yesterdayIso.split('-').reverse().join('.')})`;
|
|
135
|
+
|
|
136
|
+
if (fromDate || toDate) {
|
|
137
|
+
startIso = (fromDate || toDate).slice(0, 10);
|
|
138
|
+
endIso = (toDate || fromDate || todayIso).slice(0, 10);
|
|
139
|
+
periodLabel = startIso === endIso ? startIso.split('-').reverse().join('.') : `${startIso.split('-').reverse().join('.')} – ${endIso.split('-').reverse().join('.')}`;
|
|
140
|
+
} else if (period === 'today') {
|
|
141
|
+
startIso = todayIso;
|
|
142
|
+
endIso = todayIso;
|
|
143
|
+
periodLabel = `сегодня (${todayIso.split('-').reverse().join('.')})`;
|
|
144
|
+
} else if (period === 'week') {
|
|
145
|
+
startIso = new Date(Date.now() + MSK - 7 * DAY).toISOString().slice(0, 10);
|
|
146
|
+
endIso = todayIso;
|
|
147
|
+
periodLabel = `7 дней (${startIso.split('-').reverse().join('.')} – ${endIso.split('-').reverse().join('.')})`;
|
|
148
|
+
} else if (period === 'month') {
|
|
149
|
+
startIso = new Date(Date.now() + MSK - 30 * DAY).toISOString().slice(0, 10);
|
|
150
|
+
endIso = todayIso;
|
|
151
|
+
periodLabel = `30 дней (${startIso.split('-').reverse().join('.')} – ${endIso.split('-').reverse().join('.')})`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const freshAccs = (st.accounts || []).filter((a) => !accountId || a.id === accountId || a.accountId === accountId);
|
|
155
|
+
const reports = freshAccs.map((a) => {
|
|
156
|
+
const allOps = a.lastHistory?.operations || [];
|
|
157
|
+
const opsInPeriod = allOps.filter((op) => {
|
|
158
|
+
if (!op.date) return false;
|
|
159
|
+
const opDayIso = new Date(op.date + MSK).toISOString().slice(0, 10);
|
|
160
|
+
return opDayIso >= startIso && opDayIso <= endIso;
|
|
161
|
+
}).sort((x, y) => (x.date || 0) - (y.date || 0));
|
|
162
|
+
|
|
163
|
+
let buyCount = 0, sellCount = 0, buySumRub = 0, sellSumRub = 0, feeSumRub = 0, divSumRub = 0;
|
|
164
|
+
const bySymbol = {};
|
|
165
|
+
for (const op of opsInPeriod) {
|
|
166
|
+
const pay = Number(op.paymentRub) || 0;
|
|
167
|
+
if (op.kind === 'buy') {
|
|
168
|
+
buyCount++;
|
|
169
|
+
buySumRub += Math.abs(pay);
|
|
170
|
+
} else if (op.kind === 'sell') {
|
|
171
|
+
sellCount++;
|
|
172
|
+
sellSumRub += Math.abs(pay);
|
|
173
|
+
} else if (op.kind === 'fee') {
|
|
174
|
+
feeSumRub += Math.abs(pay);
|
|
175
|
+
} else if (op.kind === 'div') {
|
|
176
|
+
divSumRub += pay;
|
|
177
|
+
}
|
|
178
|
+
if (op.symbol && (op.kind === 'buy' || op.kind === 'sell' || op.kind === 'fee')) {
|
|
179
|
+
if (!bySymbol[op.symbol]) {
|
|
180
|
+
bySymbol[op.symbol] = { тикер: op.symbol, название: op.name || op.symbol, куплено_шт: 0, сумма_покупок_руб: 0, продано_шт: 0, сумма_продаж_руб: 0, комиссия_руб: 0, финрезультат_закрытого_объёма_руб: 0 };
|
|
181
|
+
}
|
|
182
|
+
const s = bySymbol[op.symbol];
|
|
183
|
+
if (op.kind === 'buy') {
|
|
184
|
+
s.куплено_шт += op.qty || 0;
|
|
185
|
+
s.сумма_покупок_руб = round(s.сумма_покупок_руб + Math.abs(pay), 2);
|
|
186
|
+
} else if (op.kind === 'sell') {
|
|
187
|
+
s.продано_шт += op.qty || 0;
|
|
188
|
+
s.сумма_продаж_руб = round(s.сумма_продаж_руб + Math.abs(pay), 2);
|
|
189
|
+
} else if (op.kind === 'fee') {
|
|
190
|
+
s.комиссия_руб = round(s.комиссия_руб + Math.abs(pay), 2);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let totalClosedNetPnlRub = 0;
|
|
195
|
+
for (const s of Object.values(bySymbol)) {
|
|
196
|
+
const matchedQ = Math.min(s.куплено_шт, s.продано_шт);
|
|
197
|
+
if (matchedQ > 0) {
|
|
198
|
+
const avgBuy = s.сумма_покупок_руб / s.куплено_шт;
|
|
199
|
+
const avgSell = s.сумма_продаж_руб / s.продано_шт;
|
|
200
|
+
const grossPnl = matchedQ * (avgSell - avgBuy);
|
|
201
|
+
const feeShare = s.комиссия_руб * ((2 * matchedQ) / (s.куплено_шт + s.продано_шт));
|
|
202
|
+
s.финрезультат_закрытого_объёма_руб = round(grossPnl - feeShare, 2);
|
|
203
|
+
totalClosedNetPnlRub = round(totalClosedNetPnlRub + s.финрезультат_закрытого_объёма_руб, 2);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Если за выбранный день сделок не было, подскажем ближайшую дату, когда сделки были
|
|
208
|
+
let prevActiveDay = null;
|
|
209
|
+
if (!opsInPeriod.length && allOps.length) {
|
|
210
|
+
const prevOp = allOps.find((op) => op.date && new Date(op.date + MSK).toISOString().slice(0, 10) < startIso);
|
|
211
|
+
if (prevOp) prevActiveDay = fmtDayMsk(prevOp.date);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
счёт: a.name,
|
|
216
|
+
брокер: a.broker,
|
|
217
|
+
период_мск: periodLabel,
|
|
218
|
+
всего_операций_за_период: opsInPeriod.length,
|
|
219
|
+
сделок_покупки: buyCount,
|
|
220
|
+
сделок_продажи: sellCount,
|
|
221
|
+
оборот_покупок_руб: round(buySumRub, 2),
|
|
222
|
+
оборот_продаж_руб: round(sellSumRub, 2),
|
|
223
|
+
уплачено_комиссий_руб: round(feeSumRub, 2),
|
|
224
|
+
чистый_финрезультат_закрытых_сделок_руб: totalClosedNetPnlRub,
|
|
225
|
+
...(divSumRub ? { дивиденды_купоны_руб: round(divSumRub, 2) } : {}),
|
|
226
|
+
сводка_по_бумагам: Object.values(bySymbol),
|
|
227
|
+
хронология_операций: opsInPeriod.slice(-80).map((op) => ({
|
|
228
|
+
время_мск: fmtBarMsk(op.date),
|
|
229
|
+
тип: op.kindLabel,
|
|
230
|
+
тикер: op.symbol || '—',
|
|
231
|
+
название: op.name || '',
|
|
232
|
+
кол_во_шт: op.qty || 0,
|
|
233
|
+
цена: op.price || 0,
|
|
234
|
+
сумма_руб: op.paymentRub,
|
|
235
|
+
})),
|
|
236
|
+
...(prevActiveDay ? { подсказка: `За период ${periodLabel} операций не было; ближайший предыдущий день со сделками: ${prevActiveDay}` } : {}),
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const totalTrades = reports.reduce((s, r) => s + r.сделок_покупки + r.сделок_продажи, 0);
|
|
241
|
+
return {
|
|
242
|
+
result: {
|
|
243
|
+
текущее_время_мск: fmtBarMsk(Date.now()),
|
|
244
|
+
выбранный_период: periodLabel,
|
|
245
|
+
отчёты_по_счетам: reports,
|
|
246
|
+
},
|
|
247
|
+
summary: `История торгов за ${periodLabel}: сделок ${totalTrades}`,
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
model_forecast: {
|
|
253
|
+
kind: 'read',
|
|
254
|
+
label: 'Смотрит прогноз на ближайшие часы',
|
|
255
|
+
description: 'Живой прогноз модели по ~100 акциям Мосбиржи: какие акции ожидаются сильнее или слабее рынка в ближайшие 1,5–3,5 часа, пороги входа в лонг и шорт, живой импульс текущей свечи и цепочка влияния соседей по карте рынка.',
|
|
256
|
+
parameters: { type: 'object', properties: { top: { type: 'number', description: 'сколько акций показать, по умолчанию 10' } } },
|
|
257
|
+
async run({ top = 10 }) {
|
|
258
|
+
const r = await forecastNow(getSettings());
|
|
259
|
+
const buyThr = r.buyThresholdPct ?? r.thresholdPct;
|
|
260
|
+
const shortThr = r.shortThresholdPct ?? r.thresholdPct;
|
|
261
|
+
const pick = (x) => {
|
|
262
|
+
const buyBlockedByDip = !x.signal && x.liquid && x.expectedPct >= buyThr && (x.lastMovePct ?? 0) < -0.35;
|
|
263
|
+
const shortBlockedBySpike = !x.shortSignal && !x.shortBlockedByBroker && x.liquid && x.expectedPct <= -shortThr && (x.lastMovePct ?? 0) > 0.35;
|
|
264
|
+
return {
|
|
265
|
+
акция: x.symbol,
|
|
266
|
+
название: x.name,
|
|
267
|
+
цена_руб: x.price,
|
|
268
|
+
ожидание_проц: x.expectedPct,
|
|
269
|
+
импульс_текущей_свечи_проц: x.lastMovePct ?? 0,
|
|
270
|
+
динамика_проц: x.deltaPct || 0,
|
|
271
|
+
сигнал_покупки: !!x.signal,
|
|
272
|
+
...(buyBlockedByDip ? { покупка_заблокирована: `сейчас бумага падает на текущей свече (${x.lastMovePct}%), сработала защита от ловли падающего ножа` } : {}),
|
|
273
|
+
...(x.shortSignal !== undefined ? { сигнал_шорта: !!x.shortSignal } : {}),
|
|
274
|
+
...(shortBlockedBySpike ? { шорт_заблокирован: `сейчас бумага резко растёт на текущей свече (+${x.lastMovePct}%), сработала защита от шорт-сквиза` } : {}),
|
|
275
|
+
...(x.shortBlockedByBroker ? { шорт_запрещён_брокером: true } : {}),
|
|
276
|
+
ликвидна: x.liquid,
|
|
277
|
+
влияют_соседи: (x.influences || []).map((n) => `${n.symbol} (${n.contribPct > 0 ? '+' : ''}${n.contribPct}%, близость ${n.closeness})`),
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
result: {
|
|
282
|
+
текущее_время_мск: fmtBarMsk(Date.now()),
|
|
283
|
+
свеча: r.bar,
|
|
284
|
+
горизонт_часов: r.horizon,
|
|
285
|
+
порог_покупки_проц: buyThr,
|
|
286
|
+
порог_шорта_проц: shortThr,
|
|
287
|
+
тренд_индекса: r.marketDown ? 'падающий (облегчён шорт, ужесточён лонг)' : 'растущий (стандартный лонг, ужесточён шорт)',
|
|
288
|
+
сильнее_рынка: r.rows.slice(0, top).map(pick),
|
|
289
|
+
слабее_рынка: r.rows.slice(-Math.min(6, top)).reverse().map(pick),
|
|
290
|
+
},
|
|
291
|
+
summary: `Прогноз на свече ${r.bar}: сигналов покупки ${r.rows.filter((x) => x.signal).length}, шорта ${r.rows.filter((x) => x.shortSignal).length}`,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
get_news: {
|
|
297
|
+
kind: 'read',
|
|
298
|
+
label: 'Смотрит свежие новости',
|
|
299
|
+
description: 'Свежие новости и раскрытия эмитентов Мосбиржи: каких акций касаются, тон −1…+1 (негатив/позитив), важность 0…1 и краткая суть.',
|
|
300
|
+
parameters: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
symbol: { type: 'string', description: 'тикер, например SBER; без него — все важные новости по рынку' },
|
|
304
|
+
hours: { type: 'number', description: 'за сколько часов смотреть, по умолчанию 24' },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
async run({ symbol, hours = 24 }) {
|
|
308
|
+
const items = listNews({ symbol, hours, limit: 25 });
|
|
309
|
+
if (!items.length) return { result: { новостей: 'нет' }, summary: `Новостей ${symbol ? `по ${symbol}` : ''} за последние ${hours} ч нет` };
|
|
310
|
+
const nowTs = Date.now();
|
|
311
|
+
const todayIso = new Date(nowTs + MSK).toISOString().slice(0, 10);
|
|
312
|
+
const yesterdayIso = new Date(nowTs + MSK - DAY).toISOString().slice(0, 10);
|
|
313
|
+
return {
|
|
314
|
+
result: {
|
|
315
|
+
текущее_время_мск: fmtBarMsk(nowTs),
|
|
316
|
+
новости: items.map((n) => {
|
|
317
|
+
const dIso = new Date(n.ts + MSK).toISOString().slice(0, 10);
|
|
318
|
+
const dayTag = dIso === todayIso ? 'сегодня' : dIso === yesterdayIso ? 'вчера' : fmtDayMsk(n.ts);
|
|
319
|
+
const ageHours = round(Math.max(0, (nowTs - n.ts) / 3600000), 1);
|
|
320
|
+
return {
|
|
321
|
+
время_мск: fmtBarMsk(n.ts),
|
|
322
|
+
день: dayTag,
|
|
323
|
+
давность_часов: ageHours,
|
|
324
|
+
источник: n.src,
|
|
325
|
+
...(n.symbols.length ? { акции: n.symbols.map((s) => `${s.symbol} (тон ${s.tone ?? '—'})`) } : {}),
|
|
326
|
+
...(n.imp !== null ? { важность: n.imp } : {}),
|
|
327
|
+
суть: n.gist || n.title,
|
|
328
|
+
ссылка: n.link,
|
|
329
|
+
};
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
summary: `Новости за ${hours} ч: ${items.length}${symbol ? ` по ${symbol}` : ''}`,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
market_neighbors: {
|
|
338
|
+
kind: 'read',
|
|
339
|
+
label: 'Смотрит соседей акции на карте рынка',
|
|
340
|
+
description: 'Ближайшие соседи акции на спектральной карте рынка (какие бумаги движутся синхронно и передают импульс).',
|
|
341
|
+
parameters: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] },
|
|
342
|
+
async run({ symbol }) {
|
|
343
|
+
const symUp = String(symbol || '').toUpperCase().trim();
|
|
344
|
+
const fc = await forecastNow(getSettings()).catch(() => null);
|
|
345
|
+
const row = fc?.rows?.find((r) => r.symbol === symUp);
|
|
346
|
+
if (row?.influences?.length) {
|
|
347
|
+
return {
|
|
348
|
+
result: { акция: symUp, название: row.name, цена_руб: row.price, ожидание_проц: row.expectedPct, ближайшие_влияющие_соседи: row.influences },
|
|
349
|
+
summary: `Соседи ${symUp}: ${row.influences.slice(0, 4).map((x) => x.symbol).join(', ')}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const run = listRuns().find((r) => r.mode === 'model' && r.status === 'done');
|
|
353
|
+
const ex = run && getRun(run.id)?.extra;
|
|
354
|
+
if (!ex) return { result: { ошибка: 'нет данных по соседям' }, summary: 'Нет данных по соседям', ok: false };
|
|
355
|
+
const i = ex.symbols.indexOf(symUp);
|
|
356
|
+
if (i < 0) return { result: { ошибка: `${symUp} нет в словаре модели` }, summary: `${symUp} нет в словаре`, ok: false };
|
|
357
|
+
return {
|
|
358
|
+
result: { акция: ex.symbols[i], соседи: ex.neighbors[i].map(([k, c]) => ({ акция: ex.symbols[k], близость: c })) },
|
|
359
|
+
summary: `Соседи ${ex.symbols[i]}: ${ex.neighbors[i].slice(0, 4).map(([k]) => ex.symbols[k]).join(', ')}`,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
get_settings: {
|
|
365
|
+
kind: 'read',
|
|
366
|
+
label: 'Смотрит настройки издержек и фильтра новостей',
|
|
367
|
+
description: 'Текущие настройки комиссий брокера, проскальзывания, ставки за шорт и новостного фильтра.',
|
|
368
|
+
parameters: { type: 'object', properties: {} },
|
|
369
|
+
async run() {
|
|
370
|
+
const s = getSettings();
|
|
371
|
+
const visible = PARAMS.filter((p) => !p.hidden);
|
|
372
|
+
return {
|
|
373
|
+
result: Object.fromEntries(visible.map((p) => [p.key, { значение: s[p.key], что_это: p.label, пояснение: p.hint }])),
|
|
374
|
+
summary: 'Прочитал настройки издержек и новостей',
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
set_settings: {
|
|
380
|
+
kind: 'write',
|
|
381
|
+
confirmAlways: true,
|
|
382
|
+
label: 'Меняет настройки',
|
|
383
|
+
description: 'Предложить изменение настроек в разделе «Издержки и новости» (с подтверждением пользователя кнопкой).',
|
|
384
|
+
parameters: { type: 'object', properties: { changes: changesSchema, reason: { type: 'string', description: 'пояснение для пользователя' } }, required: ['changes', 'reason'] },
|
|
385
|
+
preview(args) {
|
|
386
|
+
const { values, errors } = checkChanges(args.changes);
|
|
387
|
+
return `${args.reason || ''}\nИзменения: ${describeChanges(values)}${errors.length ? `\nОшибки: ${errors.join('; ')}` : ''}`;
|
|
388
|
+
},
|
|
389
|
+
async run(args) {
|
|
390
|
+
const { values, errors } = checkChanges(args.changes);
|
|
391
|
+
const r = updateSettings(values, { source: 'ai', reason: args.reason });
|
|
392
|
+
const all = [...errors, ...r.errors];
|
|
393
|
+
return {
|
|
394
|
+
result: { изменено: r.changed, ошибки: all.length ? all : undefined },
|
|
395
|
+
summary: Object.keys(r.changed).length ? `Настройки изменены: ${describeChanges(Object.fromEntries(Object.entries(r.changed).map(([k, v]) => [k, v.стало])))}` : `Настройки не изменились${all.length ? `: ${all.join('; ')}` : ''}`,
|
|
396
|
+
ok: !all.length,
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
broker_onboarding_guide: {
|
|
402
|
+
kind: 'read',
|
|
403
|
+
label: 'Справка по открытию и подключению брокерского счёта',
|
|
404
|
+
description: 'Получить подробную пошаговую инструкцию по открытию брокерского счёта, включению маржинальной торговли, выпуску API-токена и список уже подключённых счетов пользователя.',
|
|
405
|
+
parameters: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
broker: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital'],
|
|
411
|
+
description: 'код выбранного брокера',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
async run({ broker = 'tbank' } = {}) {
|
|
416
|
+
const st = brokersState();
|
|
417
|
+
const meta = BROKER_META.find((b) => b.id === broker) || BROKER_META[0];
|
|
418
|
+
const guides = {
|
|
419
|
+
tbank: {
|
|
420
|
+
openAccountUrl: 'https://www.tbank.ru/invest/',
|
|
421
|
+
tokenUrl: 'https://www.tbank.ru/invest/settings/',
|
|
422
|
+
accountTypeAdvice: 'Открывайте обычный «Брокерский счёт» (не Инвесткопилку и не счёт автоследования; для шортов на тарифах Профи/VIP нужен обычный брокерский счёт с включённой маржинальной торговлей).',
|
|
423
|
+
stepsToGetToken: [
|
|
424
|
+
'1. Откройте веб-кабинет Т-Инвестиций: https://www.tbank.ru/invest/settings/ (раздел «Токены T-Invest API»).',
|
|
425
|
+
'2. Нажмите «Выпустить токен» → выберите «Полный доступ» (для выставления заявок) и «Все счета».',
|
|
426
|
+
'3. Скопируйте токен (начинается с t.…) и пришлите его сюда в чат или вставьте в форму — я сам найду все ваши субсчета через discover_broker_accounts и подключу нужный счёт.',
|
|
427
|
+
'4. Чтобы стратегия зарабатывала и на падении акций (шорт), в мобильном приложении Т-Инвестиций зайдите в настройки выбранного счёта (шестерёнка вверху справа) и включите «Маржинальная торговля».',
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
finam: {
|
|
431
|
+
openAccountUrl: 'https://www.finam.ru/',
|
|
432
|
+
tokenUrl: 'https://edox.finam.ru/',
|
|
433
|
+
accountTypeAdvice: 'Единый брокерский счёт Мосбиржи (КлФ / ЕДП).',
|
|
434
|
+
stepsToGetToken: [
|
|
435
|
+
'1. В кабинете клиента Финам (edox.finam.ru) откройте «Торговля → Токены доступа (Finam Trade API)».',
|
|
436
|
+
'2. Выпустите новый токен (начинается на CA…) и скопируйте ваш торговый код клиента ClientId (например 123456R).',
|
|
437
|
+
'3. Пришлите токен и ClientId или нажмите кнопку подтверждения — я подключу счёт через connect_broker_account.',
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
bcs: {
|
|
441
|
+
openAccountUrl: 'https://bcs.ru/',
|
|
442
|
+
tokenUrl: 'https://my.broker.ru/',
|
|
443
|
+
accountTypeAdvice: 'Обычный брокерский счёт БКС Мир инвестиций с доступом к Мосбирже TQBR.',
|
|
444
|
+
stepsToGetToken: [
|
|
445
|
+
'1. В веб-терминале «БКС Мир инвестиций» откройте Настройки профиля → «Торговый API (BCS Trade API)».',
|
|
446
|
+
'2. Выпустите Refresh-токен с правом «Чтение и торговля» (trade-api-write).',
|
|
447
|
+
'3. Передайте Refresh-токен — я создам и синхронизирую счёт.',
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
alor: {
|
|
451
|
+
openAccountUrl: 'https://www.alorbroker.ru/',
|
|
452
|
+
tokenUrl: 'https://alor.dev/',
|
|
453
|
+
accountTypeAdvice: 'Фондовый счёт Мосбиржи (портфель с литерой D, например D12345).',
|
|
454
|
+
stepsToGetToken: [
|
|
455
|
+
'1. Авторизуйтесь под брокерским аккаунтом на портале разработчика https://alor.dev (или в кабинете my.alor.ru → OpenAPI).',
|
|
456
|
+
'2. Выпустите Refresh Token для боевого контура и укажите номер портфеля D…',
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
cifra: {
|
|
460
|
+
openAccountUrl: 'https://cifra-broker.ru/',
|
|
461
|
+
tokenUrl: 'https://tradernet.ru/',
|
|
462
|
+
accountTypeAdvice: 'Брокерский счёт Tradernet (Мосбиржа).',
|
|
463
|
+
stepsToGetToken: [
|
|
464
|
+
'1. В кабинете tradernet.ru откройте Настройки профиля → «API ключи».',
|
|
465
|
+
'2. Сгенерируйте пару ключей PublicKey и SecretKey и передайте их через двоеточие: PublicKey:SecretKey.',
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
iticapital: {
|
|
469
|
+
openAccountUrl: 'https://iticapital.ru/',
|
|
470
|
+
tokenUrl: 'https://iticapital.ru/',
|
|
471
|
+
accountTypeAdvice: 'Фондовый портфель Мосбиржи вида BP12345.',
|
|
472
|
+
stepsToGetToken: [
|
|
473
|
+
'1. В личном кабинете ITI Capital откройте «Торговые системы → OpenAPI» и выпустите Refresh Token.',
|
|
474
|
+
'2. Укажите Refresh-токен и код портфеля BP…',
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
sber: {
|
|
478
|
+
openAccountUrl: 'https://www.sberbank.ru/ru/person/investments',
|
|
479
|
+
accountTypeAdvice: 'Обычный брокерский счёт СберИнвестиций.',
|
|
480
|
+
stepsToGetToken: [
|
|
481
|
+
'У СберИнвестиций нет облачного REST-токена для физлиц, поэтому счёт подключается БЕЗ токена в режиме «Подтверждение заявок» (или через адрес локального HTTP-шлюза QUIK).',
|
|
482
|
+
'Достаточно указать желаемый лимит суммы на стратегию (например 100 000 ₽) и номер счёта — я сразу создам счёт в платформе через connect_broker_account и рассчитаю первые заявки в лотах Мосбиржи!',
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
vtb: {
|
|
486
|
+
openAccountUrl: 'https://broker.vtb.ru/',
|
|
487
|
+
accountTypeAdvice: 'Обычный брокерский счёт ВТБ Мои Инвестиции.',
|
|
488
|
+
stepsToGetToken: [
|
|
489
|
+
'У ВТБ Мои Инвестиции нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» (с уведомлениями в Telegram/MAX и учётом портфеля) или через адрес моста QUIK.',
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
alfa: {
|
|
493
|
+
openAccountUrl: 'https://alfabank.ru/make-money/investments/',
|
|
494
|
+
accountTypeAdvice: 'Обычный брокерский счёт Альфа-Инвестиций.',
|
|
495
|
+
stepsToGetToken: [
|
|
496
|
+
'У Альфа-Инвестиций нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» или через адрес моста QUIK.',
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
return {
|
|
501
|
+
result: {
|
|
502
|
+
брокер: meta,
|
|
503
|
+
инструкция: guides[broker] || guides.tbank,
|
|
504
|
+
уже_подключённые_счета: st.accounts.map((a) => ({
|
|
505
|
+
id: a.id,
|
|
506
|
+
название: a.name,
|
|
507
|
+
брокер: a.broker,
|
|
508
|
+
субсчёт: a.accountId,
|
|
509
|
+
режим: a.mode,
|
|
510
|
+
режим_старого_портфеля: a.legacyMode || 'isolate',
|
|
511
|
+
лимит_руб: a.maxCapitalRub,
|
|
512
|
+
баланс_руб: a.lastSync?.totalRub ?? null,
|
|
513
|
+
маржиналка: a.lastSync?.marginEnabled ?? null,
|
|
514
|
+
})),
|
|
515
|
+
},
|
|
516
|
+
summary: `Загрузил инструкцию по подключению брокера «${meta.name}»`,
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
discover_broker_accounts: {
|
|
522
|
+
kind: 'read',
|
|
523
|
+
label: 'Ищет субсчета брокера по сохранённому токену',
|
|
524
|
+
description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке по токену УЖЕ СОХРАНЁННОГО в платформе счёта (id из get_brokers_status): баланс, маржинальная торговля, отсутствие автоследования. Токен никогда не передаётся через чат: если счёта с токеном ещё нет, попроси пользователя вставить токен в форму «+ Добавить счёт» (поле «Токен») и сохранить счёт.',
|
|
525
|
+
parameters: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
id: { type: 'string', description: 'ID сохранённого счёта Т-Банка в платформе (brk_…)' },
|
|
529
|
+
sandbox: { type: 'boolean', description: 'Контур песочницы (по умолчанию false)' },
|
|
530
|
+
},
|
|
531
|
+
required: ['id'],
|
|
532
|
+
},
|
|
533
|
+
async run({ id, sandbox = false }) {
|
|
534
|
+
const r = await discoverBrokerAccounts({ broker: 'tbank', id, sandbox });
|
|
535
|
+
return {
|
|
536
|
+
result: {
|
|
537
|
+
найдено_счетов: r.accounts.length,
|
|
538
|
+
счета: r.accounts.map((a) => ({
|
|
539
|
+
accountId: a.id,
|
|
540
|
+
название: a.name,
|
|
541
|
+
тип: a.type,
|
|
542
|
+
доступ: a.access,
|
|
543
|
+
баланс_руб: a.totalRub,
|
|
544
|
+
свободные_рубли: a.cashRub,
|
|
545
|
+
позиций: a.positionsCount,
|
|
546
|
+
маржинальная_торговля: a.marginEnabled ? 'включена (шорты доступны)' : 'выключена (только в рост)',
|
|
547
|
+
автоследование: a.autoFollowDetected ? 'ОБНАРУЖЕНО (подключать запрещено Офертой)' : 'нет (можно подключать)',
|
|
548
|
+
пригодность: a.eligibleForTrading ? 'подходит для торговли' : 'только просмотр / копилка',
|
|
549
|
+
})),
|
|
550
|
+
},
|
|
551
|
+
summary: `Найдено счетов в Т-Банке: ${r.accounts.length}`,
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
connect_broker_account: {
|
|
557
|
+
kind: 'write',
|
|
558
|
+
confirmAlways: true,
|
|
559
|
+
label: 'Подключает или настраивает брокерский счёт',
|
|
560
|
+
description: 'Добавить или обновить настройки брокерского счёта в платформе «НейроАльфа», сразу выполнить синхронизацию портфеля и рассчитать пакет заявок. Токен брокера этим инструментом НЕ задаётся и в чат не запрашивается — пользователь вставляет его сам в форму счёта. Режим «auto» доступен только для Т-Банка.',
|
|
561
|
+
parameters: {
|
|
562
|
+
type: 'object',
|
|
563
|
+
properties: {
|
|
564
|
+
id: { type: 'string', description: 'ID существующего счёта в платформе (если обновляем существующий счёт)' },
|
|
565
|
+
broker: {
|
|
566
|
+
type: 'string',
|
|
567
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital'],
|
|
568
|
+
description: 'Код брокера (полная поддержка — tbank; остальные в бете, только с подтверждением заявок)',
|
|
569
|
+
},
|
|
570
|
+
name: { type: 'string', description: 'Понятное название счёта (например «Т-Банк основной»)' },
|
|
571
|
+
accountId: { type: 'string', description: 'ID субсчёта / ClientId / номер портфеля' },
|
|
572
|
+
mode: { type: 'string', enum: ['readonly', 'confirm', 'auto'], description: 'Режим работы: confirm (подтверждение заявок), auto (автомат, только Т-Банк) или readonly' },
|
|
573
|
+
legacyMode: { type: 'string', enum: ['isolate', 'smooth', 'takeover'], description: 'Режим для старых бумаг на счёте: isolate (не трогать старые бумаги), smooth (плавный переход) или takeover (полное управление)' },
|
|
574
|
+
maxCapitalRub: { type: 'number', description: 'Лимит суммы на стратегию в рублях (например 100000 или 1000000)' },
|
|
575
|
+
maxDailyDrawdownPct: { type: 'number', description: 'Дневной стоп по просадке в % (по умолчанию 3)' },
|
|
576
|
+
limitOffsetPct: { type: 'number', description: 'Защитный отступ лимитной заявки в % (по умолчанию 0.1)' },
|
|
577
|
+
sandbox: { type: 'boolean', description: 'Тестовый режим — песочница брокера (по умолчанию false)' },
|
|
578
|
+
},
|
|
579
|
+
required: ['broker'],
|
|
580
|
+
},
|
|
581
|
+
preview(args) {
|
|
582
|
+
const meta = BROKER_META.find((b) => b.id === args.broker);
|
|
583
|
+
const modeLbl = { readonly: 'Только чтение', confirm: 'Подтверждение заявок', auto: 'Автомат' }[args.mode || 'confirm'] || 'Подтверждение заявок';
|
|
584
|
+
const legLbl = { isolate: 'Изолированный карман', smooth: 'Плавный переход', takeover: 'Полное управление' }[args.legacyMode || 'isolate'] || 'Изолированный карман';
|
|
585
|
+
return `Брокер: ${meta?.name || args.broker}${args.name ? ` («${args.name}»)` : ''}${args.accountId ? ` · счёт ${args.accountId}` : ''} · режим «${modeLbl}» · старый портфель: «${legLbl}» · лимит ${args.maxCapitalRub || 100000} ₽`;
|
|
586
|
+
},
|
|
587
|
+
async run(args) {
|
|
588
|
+
const beforeIds = new Set(brokersState().accounts.map((a) => a.id));
|
|
589
|
+
const st = saveBrokerAccount({
|
|
590
|
+
id: args.id,
|
|
591
|
+
broker: args.broker,
|
|
592
|
+
name: args.name,
|
|
593
|
+
accountId: args.accountId,
|
|
594
|
+
mode: args.mode || 'confirm',
|
|
595
|
+
legacyMode: args.legacyMode || 'isolate',
|
|
596
|
+
maxCapitalRub: args.maxCapitalRub || 100000,
|
|
597
|
+
maxDailyDrawdownPct: args.maxDailyDrawdownPct ?? 3,
|
|
598
|
+
limitOffsetPct: args.limitOffsetPct ?? 0.1,
|
|
599
|
+
sandbox: Boolean(args.sandbox),
|
|
600
|
+
});
|
|
601
|
+
const created = args.id ? st.accounts.find((a) => a.id === args.id) : st.accounts.find((a) => !beforeIds.has(a.id)) || st.accounts[st.accounts.length - 1];
|
|
602
|
+
let syncRes = null;
|
|
603
|
+
if (created && created.hasToken) {
|
|
604
|
+
try {
|
|
605
|
+
const synced = await syncBrokerAccount(created.id);
|
|
606
|
+
await prepareBrokerOrders(created.id).catch(() => {});
|
|
607
|
+
const accNow = synced.accounts.find((a) => a.id === created.id);
|
|
608
|
+
syncRes = {
|
|
609
|
+
синхронизировано: Boolean(accNow?.lastSync?.ok),
|
|
610
|
+
ошибка_синхронизации: accNow?.lastSync?.error || null,
|
|
611
|
+
баланс_руб: accNow?.lastSync?.totalRub ?? null,
|
|
612
|
+
свободные_рубли: accNow?.lastSync?.cashRub ?? null,
|
|
613
|
+
маржинальная_торговля: accNow?.lastSync?.marginEnabled ?? null,
|
|
614
|
+
подготовлено_заявок: accNow?.pendingBatch?.orders?.length ?? 0,
|
|
615
|
+
};
|
|
616
|
+
} catch (e) {
|
|
617
|
+
syncRes = { синхронизировано: false, ошибка_синхронизации: e.message };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
result: {
|
|
622
|
+
ok: true,
|
|
623
|
+
счёт: created ? { id: created.id, название: created.name, брокер: created.broker, субсчёт: created.accountId, режим: created.mode, лимит_руб: created.maxCapitalRub } : null,
|
|
624
|
+
статус_подключения: syncRes,
|
|
625
|
+
},
|
|
626
|
+
summary: `Подключён брокерский счёт «${created?.name || args.broker}»${syncRes?.баланс_руб !== null && syncRes?.баланс_руб !== undefined ? ` (баланс ${syncRes.баланс_руб} ₽)` : ''}`,
|
|
627
|
+
};
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
list_runs: {
|
|
632
|
+
kind: 'read',
|
|
633
|
+
label: 'Смотрит список проведённых исследований',
|
|
634
|
+
description: 'Посмотреть список завершённых исторических исследований модели.',
|
|
635
|
+
parameters: { type: 'object', properties: { limit: { type: 'number', description: 'сколько последних, по умолчанию 6' } } },
|
|
636
|
+
async run({ limit = 6 }) {
|
|
637
|
+
return { result: recentRuns(Math.min(20, limit)), summary: 'Просмотрел список исследований' };
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
analyze_run: {
|
|
642
|
+
kind: 'read',
|
|
643
|
+
label: 'Разбирает результаты исследования',
|
|
644
|
+
description: 'Подробный разбор завершённого исследования по его номеру: вклад акций, часов входа, сроков удержания и голов внимания.',
|
|
645
|
+
parameters: { type: 'object', properties: { run_id: { type: 'number', description: 'номер исследования' } }, required: ['run_id'] },
|
|
646
|
+
async run({ run_id }) {
|
|
647
|
+
const r = analyzeRun(Number(run_id));
|
|
648
|
+
return { result: r, summary: r.ошибка || `Разобрал исследование №${run_id}` };
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
compare_runs: {
|
|
653
|
+
kind: 'read',
|
|
654
|
+
label: 'Сравнивает два исследования',
|
|
655
|
+
description: 'Сравнить два готовых исследования по их номерам.',
|
|
656
|
+
parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },
|
|
657
|
+
async run({ a, b }) {
|
|
658
|
+
return { result: compareRuns(Number(a), Number(b)), summary: `Сравнил исследования №${a} и №${b}` };
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
export const MODES = {
|
|
664
|
+
discuss: { label: 'Консультант', allowed: (t) => t.kind === 'read' },
|
|
665
|
+
advisor: { label: 'Консультант', allowed: () => true, confirm: (t) => t.kind !== 'read' },
|
|
666
|
+
autopilot: { label: 'Консультант', allowed: () => true, confirm: (t) => t.kind !== 'read' },
|
|
667
|
+
review: { label: 'Разбор', allowed: (t) => t.kind === 'read' },
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
export function toolSchemas(mode = 'advisor') {
|
|
671
|
+
const m = MODES[mode] || MODES.advisor;
|
|
672
|
+
return Object.entries(TOOLS)
|
|
673
|
+
.filter(([, t]) => m.allowed(t))
|
|
674
|
+
.map(([name, t]) => ({ type: 'function', function: { name, description: t.description, parameters: t.parameters } }));
|
|
675
|
+
}
|