neuroalpha 0.3.15 → 0.3.16
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 +80 -170
- package/config/strategy.json +2 -2
- package/docs//320/221/320/240/320/236/320/232/320/225/320/240/320/253_API.md +4 -3
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +10 -17
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +97 -156
- 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 +29 -45
- package/package.json +1 -1
- package/public/admin.html +9 -8
- package/public/app.js +444 -829
- package/public/index.html +141 -199
- package/public/landing.html +19 -33
- package/public/legal.html +3 -4
- package/public/styles.css +6 -2
- package/server/api/routes.js +10 -3
- package/server/auth/index.js +53 -34
- package/server/brokers/index.js +430 -104
- package/server/data/moex.js +74 -23
- package/server/db.js +3 -2
- package/server/model/dataset.js +10 -2
- package/server/model/forecast.js +56 -19
- package/server/model/live.js +30 -6
- package/server/model/models.js +113 -31
- package/server/model/research.js +100 -26
- package/server/params-meta.js +16 -16
- package/server/strategist/index.js +4 -0
- package/server/strategist/tools.js +229 -0
package/server/model/research.js
CHANGED
|
@@ -34,8 +34,8 @@ export function isMarketDown(ds, t) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Версия обработки данных: при её смене прежние прогоны не считаются «такими же» (у старых её нет).
|
|
37
|
-
//
|
|
38
|
-
const DATA_VERSION =
|
|
37
|
+
// 8 — скорректированный спектр Марченко–Пастура (sigmaBulk) для 0 — Авто + динамическая концентрация слота (Conviction Sizing)
|
|
38
|
+
const DATA_VERSION = 8;
|
|
39
39
|
|
|
40
40
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
41
41
|
function reuseParamsKey(params) {
|
|
@@ -147,8 +147,13 @@ async function run(runId, p, state) {
|
|
|
147
147
|
const H = barsFromHours(effHorizon); // горизонт в свечах по 10 минут
|
|
148
148
|
const maxH = autoAdapt ? Math.round(H * 1.4) : H;
|
|
149
149
|
const tillTs = Date.now();
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
// Выравниваем начало периода исследования по началу календарных суток МСК,
|
|
151
|
+
// а для чтения датасета из SQLite всегда берём единый широкий горизонт (fromTs = 0),
|
|
152
|
+
// чтобы смена «Окна обучения» (modelTrainDays) или «Периода исследования» (modelHistoryDays)
|
|
153
|
+
// НИКОГДА не сдвигала fromTs и не вызывала повторную загрузку котировок!
|
|
154
|
+
const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
|
|
155
|
+
const researchFrom = todayStartMsk - p.modelHistoryDays * DAY;
|
|
156
|
+
const fromTs = todayStartMsk - 1650 * DAY;
|
|
152
157
|
|
|
153
158
|
emit({ stage: 'data', pct: 0, text: 'Загружаем словарь рынка (состав индекса широкого рынка)…' });
|
|
154
159
|
const ds = await loadDataset({
|
|
@@ -250,18 +255,39 @@ async function run(runId, p, state) {
|
|
|
250
255
|
self: [round(attM.coef[HEADS.length], 4), round(attM.coef[HEADS.length + 1], 4), round(attM.coef[HEADS.length + 2], 4)],
|
|
251
256
|
} : {}),
|
|
252
257
|
};
|
|
253
|
-
emit({ stage: 'sim', pct: Math.round(((fi + 1) / folds) *
|
|
258
|
+
emit({ stage: 'sim', pct: Math.round(((fi + 1) / folds) * 80), text: mainText, viz: liveViz });
|
|
254
259
|
testStart = testEnd;
|
|
255
260
|
fi++;
|
|
256
261
|
}
|
|
257
262
|
|
|
258
|
-
emit({ stage: '
|
|
263
|
+
emit({ stage: 'calc', pct: 81, text: 'Финальный пересчёт · подготовка временного ряда прогнозов…' });
|
|
259
264
|
await tick();
|
|
260
265
|
const testTs = [...yraw.keys()].sort((a, b) => a - b);
|
|
261
266
|
const tsOf = (t) => timeline[t];
|
|
262
267
|
|
|
263
268
|
// ---------- портфель из лучших прогнозов: покупки, продажи без покупки, пары «нейтрально к рынку» ----------
|
|
264
|
-
const
|
|
269
|
+
const rawTopK = Number(p.modelTopK ?? 2);
|
|
270
|
+
const autoK = rawTopK === 0 || autoAdapt;
|
|
271
|
+
const K = rawTopK > 0 ? rawTopK : 4;
|
|
272
|
+
// Адаптивный отбор числа акций (Dynamic Top-K):
|
|
273
|
+
// лидер №1 берётся всегда при пробое порога; следующие бумаги (2…K) берутся только если их сигнал
|
|
274
|
+
// достаточно силён относительно лидера (>= 68% от силы №1) и уверенно перекрывает порог (>= 1.25 * need),
|
|
275
|
+
// чтобы не размывать капитал на слабые окраинные сигналы
|
|
276
|
+
const filterDynamicTopK = (cands, edgeFn, needVal, maxCount) => {
|
|
277
|
+
if (!cands.length || maxCount <= 0) return [];
|
|
278
|
+
const lim = Math.min(cands.length, maxCount);
|
|
279
|
+
if (!autoK || lim <= 1) return cands.slice(0, lim);
|
|
280
|
+
const bestE = Math.abs(edgeFn(cands[0]));
|
|
281
|
+
const out = [cands[0]];
|
|
282
|
+
for (let m = 1; m < lim; m++) {
|
|
283
|
+
const curE = Math.abs(edgeFn(cands[m]));
|
|
284
|
+
const relMin = m === 1 ? 0.62 : 0.78;
|
|
285
|
+
const absMin = m === 1 ? needVal * 1.15 : needVal * 1.45;
|
|
286
|
+
if (curE >= bestE * relMin && curE >= absMin) out.push(cands[m]);
|
|
287
|
+
else break;
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
};
|
|
265
291
|
const dir = p.modelDirection;
|
|
266
292
|
const exitRatio = Number(p.modelExitRatio ?? 0.5);
|
|
267
293
|
const mktFilter = p.modelMarketFilter === 'ma';
|
|
@@ -313,7 +339,7 @@ async function run(runId, p, state) {
|
|
|
313
339
|
const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
|
|
314
340
|
let curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
315
341
|
const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
|
|
316
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.
|
|
342
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
|
|
317
343
|
|
|
318
344
|
const closePos = (i, h, t, end, why) => {
|
|
319
345
|
const exitPrice = px(i, t);
|
|
@@ -365,9 +391,11 @@ async function run(runId, p, state) {
|
|
|
365
391
|
const candS = [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) < -needS).sort((a, b) => pr[a] - pr[b]);
|
|
366
392
|
const nL = K - countSide('long');
|
|
367
393
|
const nS = K - countSide('short');
|
|
394
|
+
const pickedL = filterDynamicTopK(candL, edge, needL, Math.max(0, nL));
|
|
395
|
+
const pickedS = filterDynamicTopK(candS, edge, needS, Math.max(0, nS));
|
|
368
396
|
entries = [
|
|
369
|
-
...
|
|
370
|
-
...
|
|
397
|
+
...pickedL.map((i) => [i, 'long', edge(i)]),
|
|
398
|
+
...pickedS.map((i) => [i, 'short', edge(i)]),
|
|
371
399
|
];
|
|
372
400
|
} else if (effMinEdge > 0) {
|
|
373
401
|
// ожидаемое движение (в долях) = прогноз × обычный размах × √H; входим, только если оно окупает сделку
|
|
@@ -383,7 +411,8 @@ async function run(runId, p, state) {
|
|
|
383
411
|
}
|
|
384
412
|
const free = K - held.size;
|
|
385
413
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
386
|
-
const
|
|
414
|
+
const rawCand = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]);
|
|
415
|
+
const cand = filterDynamicTopK(rawCand, edge, needL, Math.max(0, free));
|
|
387
416
|
order = [...held.keys(), ...cand];
|
|
388
417
|
} else {
|
|
389
418
|
const cand = mktDown ? [] : [...pr.keys()].filter((i) => stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
|
|
@@ -402,11 +431,29 @@ async function run(runId, p, state) {
|
|
|
402
431
|
const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
403
432
|
let avail = Math.max(0, equity * 0.99 - usedStart);
|
|
404
433
|
const needEdge = effMinEdge * 2 * realCost;
|
|
434
|
+
// Динамическая концентрация капитала под реально работающие позиции:
|
|
435
|
+
// если при K=5 на рынке есть всего 1–2 сильных сигнала, не замораживаем 60% денег в кэше под пустые слоты,
|
|
436
|
+
// а взвешиваем слот от фактического числа занятых + новых позиций и силы каждого сигнала (Conviction Sizing)
|
|
437
|
+
const activeTotal = Math.max(2, held.size + newEntries.length);
|
|
438
|
+
const dynSlotWeight = autoAdapt
|
|
439
|
+
? Math.min(dir === 'long' ? 0.58 : 0.75, Math.max(maxSlotWeight, (dir === 'long' ? 1.8 : 3.0) / Math.min(slots, activeTotal + 1)))
|
|
440
|
+
: maxSlotWeight;
|
|
441
|
+
// Сортируем новые входы по убыванию силы сигнала, чтобы лучшие идеи получали приоритетную долю капитала
|
|
442
|
+
if (autoAdapt && newEntries.length > 1) {
|
|
443
|
+
newEntries.sort((a, b) => Math.abs(b[2] ?? 0) - Math.abs(a[2] ?? 0));
|
|
444
|
+
}
|
|
445
|
+
const edgeWeights = newEntries.map(([i, , edgeAt]) => {
|
|
446
|
+
const effE = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
|
|
447
|
+
return Math.max(0.0001, effE);
|
|
448
|
+
});
|
|
449
|
+
let remWeightSum = edgeWeights.reduce((a, b) => a + b, 0);
|
|
405
450
|
for (let idx = 0; idx < newEntries.length; idx++) {
|
|
406
451
|
const [i, side, edgeAt] = newEntries[idx];
|
|
407
|
-
const effEdge =
|
|
408
|
-
const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.
|
|
409
|
-
const
|
|
452
|
+
const effEdge = edgeWeights[idx];
|
|
453
|
+
const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.65, Math.max(0.85, 0.80 + 0.30 * (effEdge / needEdge))) : 1;
|
|
454
|
+
const propShare = autoAdapt && remWeightSum > 0 ? avail * (effEdge / remWeightSum) : avail / Math.max(1, newEntries.length - idx);
|
|
455
|
+
remWeightSum = Math.max(0, remWeightSum - effEdge);
|
|
456
|
+
const baseAlloc = autoAdapt ? Math.min(equity * dynSlotWeight * conv, propShare, avail) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
|
|
410
457
|
let alloc = baseAlloc;
|
|
411
458
|
if (invVol && avgInv > 0 && sg[i] > 0) {
|
|
412
459
|
const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
|
|
@@ -465,9 +512,15 @@ async function run(runId, p, state) {
|
|
|
465
512
|
const cost = realCost;
|
|
466
513
|
const results = {};
|
|
467
514
|
let splitTs = null;
|
|
468
|
-
for (
|
|
515
|
+
for (let ni = 0; ni < names.length; ni++) {
|
|
516
|
+
const n = names[ni];
|
|
517
|
+
const lbl = MODELS[n]?.label || n;
|
|
518
|
+
emit({ stage: 'calc', pct: 82 + ni * 2, text: `Финальный пересчёт · симуляция портфеля (${ni + 1} из ${names.length}): «${lbl}»…` });
|
|
519
|
+
await tick();
|
|
520
|
+
if (state.cancelled) throw new Error('Отменено');
|
|
469
521
|
const sim = simulate(n, cost);
|
|
470
522
|
const report = computeReport(sim.trades, sim.curve, p.capital, p.oosPct);
|
|
523
|
+
await tick();
|
|
471
524
|
const gross = simulate(n, 0, 0);
|
|
472
525
|
const grossReport = computeReport(gross.trades, gross.curve, p.capital, p.oosPct);
|
|
473
526
|
splitTs = report.splitTs;
|
|
@@ -478,11 +531,16 @@ async function run(runId, p, state) {
|
|
|
478
531
|
return seg.length > 1 ? (seg[seg.length - 1].equity / seg[0].equity - 1) * 100 : 0;
|
|
479
532
|
};
|
|
480
533
|
const SEG = { full: [-Infinity, Infinity], inSample: [-Infinity, splitTs - 1], outOfSample: [splitTs, Infinity] };
|
|
481
|
-
const icStats = (n) => {
|
|
534
|
+
const icStats = async (n) => {
|
|
482
535
|
const out = {};
|
|
483
536
|
const weekly = new Map();
|
|
484
537
|
const all = [];
|
|
485
|
-
for (
|
|
538
|
+
for (let idx = 0; idx < testTs.length; idx++) {
|
|
539
|
+
if ((idx & 2047) === 0) {
|
|
540
|
+
await tick();
|
|
541
|
+
if (state.cancelled) throw new Error('Отменено');
|
|
542
|
+
}
|
|
543
|
+
const t = testTs[idx];
|
|
486
544
|
// совпадение прогноза с фактом — только по ликвидным акциям
|
|
487
545
|
const pr = preds[n].get(t), y = yraw.get(t);
|
|
488
546
|
const ok = [...pr.keys()].filter((i) => tokens[i].liq[t] >= LIQ_MIN);
|
|
@@ -508,8 +566,12 @@ async function run(runId, p, state) {
|
|
|
508
566
|
|
|
509
567
|
const comparison = {};
|
|
510
568
|
const weeklyIc = {};
|
|
511
|
-
for (
|
|
512
|
-
const
|
|
569
|
+
for (let ni = 0; ni < names.length; ni++) {
|
|
570
|
+
const n = names[ni];
|
|
571
|
+
const lbl = MODELS[n]?.label || n;
|
|
572
|
+
emit({ stage: 'calc', pct: 88 + ni * 2, text: `Финальный пересчёт · ранговая точность IC по неделям (${ni + 1} из ${names.length}): «${lbl}»…` });
|
|
573
|
+
await tick();
|
|
574
|
+
const ic = await icStats(n);
|
|
513
575
|
weeklyIc[n] = ic.weekly;
|
|
514
576
|
const rep = results[n].report;
|
|
515
577
|
comparison[n] = {};
|
|
@@ -529,7 +591,8 @@ async function run(runId, p, state) {
|
|
|
529
591
|
}
|
|
530
592
|
|
|
531
593
|
const chosen = names.includes(p.modelType) ? p.modelType : 'attention';
|
|
532
|
-
|
|
594
|
+
emit({ stage: 'calc', pct: 95, text: `Финальный пересчёт · проверка фазовой устойчивости (5 сдвигов точки входа)…` });
|
|
595
|
+
await tick();
|
|
533
596
|
const report = results[chosen].report;
|
|
534
597
|
report.period = { from: tsOf(testTs[0]), till: tsOf(testTs[testTs.length - 1]) };
|
|
535
598
|
report.bars = testTs.length;
|
|
@@ -546,18 +609,29 @@ async function run(runId, p, state) {
|
|
|
546
609
|
// Быстрая проверка устойчивости по сдвигам точки пересмотра и старта (на готовых прогнозах, без переобучения)
|
|
547
610
|
const bpdInt = Math.max(1, Math.round(bpd));
|
|
548
611
|
const offsets = [...new Set([0, Math.max(1, Math.floor(H / 3)), Math.max(2, Math.floor((2 * H) / 3)), bpdInt * 2, bpdInt * 4].filter((o) => o < Math.min(testTs.length / 4, bpdInt * 10)))];
|
|
549
|
-
const stabRuns =
|
|
612
|
+
const stabRuns = [];
|
|
613
|
+
for (let idx = 0; idx < offsets.length; idx++) {
|
|
614
|
+
const off = offsets[idx];
|
|
615
|
+
if (idx > 0) await tick();
|
|
550
616
|
const sm = idx === 0 ? results[chosen].sim : simulate(chosen, cost, p.modelShortFeePct, off);
|
|
551
617
|
const rep = idx === 0 ? results[chosen].report.full : computeReport(sm.trades, sm.curve, p.capital, p.oosPct).full;
|
|
552
|
-
|
|
618
|
+
stabRuns.push({
|
|
553
619
|
label: off === 0 ? 'база' : off < H ? `фаза +${off * BAR_INTERVAL}м` : `старт +${Math.round(off / bpd)}д`,
|
|
554
620
|
returnPct: rep?.returnPct ?? 0,
|
|
555
621
|
maxDrawdownPct: rep?.maxDrawdownPct ?? 0,
|
|
556
622
|
trades: rep?.trades ?? 0,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
559
625
|
const stability = summarizeStability(stabRuns, 'phase');
|
|
560
626
|
|
|
627
|
+
emit({ stage: 'calc', pct: 98, text: `Финальный пересчёт · запись ${results[chosen].sim.trades.length} сделок и кривых капитала в базу…` });
|
|
628
|
+
await tick();
|
|
629
|
+
const chosenTrades = results[chosen].sim.trades;
|
|
630
|
+
for (let ti = 0; ti < chosenTrades.length; ti++) {
|
|
631
|
+
saveTrade(runId, chosenTrades[ti]);
|
|
632
|
+
if ((ti & 511) === 511) await tick();
|
|
633
|
+
}
|
|
634
|
+
|
|
561
635
|
saveRunExtra(runId, {
|
|
562
636
|
kind: 'model',
|
|
563
637
|
symbols: tokens.map((t) => t.symbol),
|
|
@@ -625,8 +699,8 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
625
699
|
const shifts = [-8, -4, 0, 4, 8];
|
|
626
700
|
const H = barsFromHours(p.modelHorizon);
|
|
627
701
|
const tillTs = Date.now();
|
|
628
|
-
|
|
629
|
-
const fromTs =
|
|
702
|
+
const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
|
|
703
|
+
const fromTs = todayStartMsk - 1650 * DAY;
|
|
630
704
|
const periodTill = runRow.metrics?.period?.till || tillTs;
|
|
631
705
|
const emitStab = (m) => { if (!st.cancelled) hub.emit({ type: 'stability:progress', runId: targetId, ...m }); };
|
|
632
706
|
emitStab({ pct: 2, text: 'Проверка устойчивости: чтение локальных цен…' });
|
package/server/params-meta.js
CHANGED
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
// Описание всех настроек: для формы в интерфейсе, для ИИ-стратега и для проверки значений.
|
|
3
3
|
// hidden — не показывать в интерфейсе; ai:false — ИИ менять запрещено (честность проверки).
|
|
4
4
|
export const PARAMS = [
|
|
5
|
-
{ key: 'modelAutoAdapt', label: 'Самообучение правил торговли (Авто)', hint: 'Полный автомат: стратегия сама учится на мульти-горизонте (1,5–3,5 ч),
|
|
6
|
-
{ key: 'modelTopK', label: 'Сколько акций держать', hint: '
|
|
7
|
-
{ key: 'modelDirection', label: 'Направление торговли', hint: '
|
|
8
|
-
{ key: 'modelHorizon', label: 'Горизонт прогноза, часов (ручной режим)', hint: '
|
|
9
|
-
{ key: 'modelMinEdge', label: 'Порог выгоды на вход (ручной режим)', hint: '
|
|
10
|
-
{ key: 'modelExitRatio', label: 'Порог на выход (ручной режим)', hint: '
|
|
11
|
-
{ key: 'modelAllocMode', label: 'Распределение суммы между акциями', hint: '
|
|
12
|
-
{ key: 'modelMarketFilter', label: 'Фильтр падающего рынка', hint: '
|
|
13
|
-
{ key: 'modelTrainDays', label: 'Окно обучения, торговых дней', hint: '
|
|
14
|
-
{ key: 'modelHistoryDays', label: 'Период исследования, дней', hint: 'За сколько последних календарных дней проверять стратегию на истории.', min: 120, max: 1100 },
|
|
15
|
-
{ key: 'modelEmbedDim', label: 'Размерность карты рынка (
|
|
16
|
-
{ key: 'modelTextWeight', label: 'Вес бизнес-профилей компаний на карте', hint: '0 — карта рынка строится только по совместному поведению
|
|
17
|
-
{ key: 'modelUseMacro', label: 'Учитывать нефть, валюту, облигации', hint: 'Дополнительно учитывает чувствительность акций к Brent, юаню, доллару, золоту и RGBI.', type: 'checkbox', hidden: true },
|
|
5
|
+
{ key: 'modelAutoAdapt', label: 'Самообучение правил торговли (Авто)', hint: 'Полный автомат: стратегия сама учится на мульти-горизонте (1,5–3,5 ч), сама выбирает число акций (1–4) и размер слота по силе сигнала, управляет порогами входа и выхода и временно ставит на карантин убыточные акции.', type: 'checkbox', hidden: true, fixed: true },
|
|
6
|
+
{ key: 'modelTopK', label: 'Сколько акций держать (0 — Авто)', hint: '0 — система сама выбирает от 1 до 4 акций на каждом шаге по силе и концентрации сигналов; 1…20 — верхний предел числа позиций на сторону.', min: 0, max: 20, hidden: true, fixed: true },
|
|
7
|
+
{ key: 'modelDirection', label: 'Направление торговли', hint: 'Автоматически подстраивается под тариф и статус маржинальной торговли на брокерском счёте.', type: 'select', options: [['long', 'Только в рост (без шорта)'], ['longshort', 'В рост и в падение (с шортом)']], hidden: true, fixed: true },
|
|
8
|
+
{ key: 'modelHorizon', label: 'Горизонт прогноза, часов (ручной режим)', hint: 'Управляется автоматически (мульти-горизонт 1,5–3,5 ч).', min: 0.5, max: 8, hidden: true, fixed: true },
|
|
9
|
+
{ key: 'modelMinEdge', label: 'Порог выгоды на вход (ручной режим)', hint: 'Управляется автоматически (2,5× издержек).', min: 0, max: 10, hidden: true, fixed: true },
|
|
10
|
+
{ key: 'modelExitRatio', label: 'Порог на выход (ручной режим)', hint: 'Управляется автоматически (двухступенчатый выход 0,35 / 0,95).', min: 0, max: 1, step: 0.05, hidden: true, fixed: true },
|
|
11
|
+
{ key: 'modelAllocMode', label: 'Распределение суммы между акциями', hint: 'Управляется автоматически по силе сигнала (Conviction Sizing).', type: 'select', options: [['equal', 'Поровну'], ['invvol', 'Обратно размаху акции']], hidden: true, fixed: true },
|
|
12
|
+
{ key: 'modelMarketFilter', label: 'Фильтр падающего рынка', hint: 'Адаптивная балансировка порогов к тренду индекса включена автоматически.', type: 'select', options: [['off', 'Выключен'], ['ma', 'Не покупать при падении индекса']], hidden: true, fixed: true },
|
|
13
|
+
{ key: 'modelTrainDays', label: 'Окно обучения, торговых дней', hint: 'Фиксировано на эталонном окне 90 торговых дней.', min: 30, max: 400, hidden: true, fixed: true },
|
|
14
|
+
{ key: 'modelHistoryDays', label: 'Период исследования, дней', hint: 'За сколько последних календарных дней проверять стратегию на истории.', min: 120, max: 1100, hidden: true, fixed: true },
|
|
15
|
+
{ key: 'modelEmbedDim', label: 'Размерность карты рынка / эмбеддинг (0 — Авто)', hint: 'Авто-калибровка по спектру собственных чисел Марченко–Пастура.', min: 0, max: 1000, hidden: true, fixed: true },
|
|
16
|
+
{ key: 'modelTextWeight', label: 'Вес бизнес-профилей компаний на карте', hint: '0 — карта рынка строится только по совместному поведению цен.', min: 0, max: 1, step: 0.1, hidden: true, fixed: true },
|
|
17
|
+
{ key: 'modelUseMacro', label: 'Учитывать нефть, валюту, облигации', hint: 'Дополнительно учитывает чувствительность акций к Brent, юаню, доллару, золоту и RGBI.', type: 'checkbox', hidden: true, fixed: true },
|
|
18
18
|
{ key: 'modelType', label: 'Какая стратегия торгует', hint: 'Внимание через карту.', type: 'select', options: [['attention', 'Внимание через карту'], ['leadlag', 'Кто за кем ходит'], ['inertia', 'Своя инерция']], hidden: true, fixed: true },
|
|
19
19
|
{ key: 'modelUseDividends', label: 'Учитывать дивиденды', hint: 'Цены до даты отсечки уменьшаются на размер дивиденда со сверкой по реальному падению цены.', type: 'checkbox', hidden: true, fixed: true },
|
|
20
|
-
{ key: 'capital', label: 'Стартовая сумма, ₽', hint: 'С какой суммой начинает
|
|
20
|
+
{ key: 'capital', label: 'Стартовая сумма, ₽', hint: 'С какой суммой начинает счёт.', min: 10000, max: 1e9, hidden: true },
|
|
21
21
|
{ key: 'commissionPct', label: 'Комиссия брокера и биржи, % за сделку', hint: 'Сколько берут брокер и биржа за покупку и отдельно за продажу.', min: 0, max: 1, ai: false },
|
|
22
22
|
{ key: 'slippagePct', label: 'Проскальзывание при исполнении, %', hint: 'На сколько реальная цена сделки в стакане обычно хуже ожидаемой.', min: 0, max: 1, ai: false },
|
|
23
23
|
{ key: 'modelShortFeePct', label: 'Плата брокеру за заём акций (шорт), % годовых', hint: 'Ставка брокера за удержание коротких позиций (начисляется пропорционально времени удержания сделки).', min: 0, max: 100 },
|
|
24
|
-
{ key: 'autopilotBudget', label: 'Лимит исследований автопилота за задание', hint: 'Сколько экспериментов Стратег может провести подряд за одно
|
|
25
|
-
{ key: 'paperDailyReview', label: 'ИИ разбирает каждый торговый день', hint: 'После окончания торгового дня Стратег сам разбирает сделки
|
|
24
|
+
{ key: 'autopilotBudget', label: 'Лимит исследований автопилота за задание', hint: 'Сколько экспериментов Стратег может провести подряд за одно задание.', min: 1, max: 30, step: 1, int: true, ai: false, hidden: true },
|
|
25
|
+
{ key: 'paperDailyReview', label: 'ИИ разбирает каждый торговый день', hint: 'После окончания торгового дня Стратег сам разбирает сделки и пишет сводку в чат.', type: 'checkbox', hidden: true },
|
|
26
26
|
{ key: 'newsEnabled', label: 'Следить за новостями', hint: 'Раз в 10 минут собирает новости и раскрытия компаний.', type: 'checkbox', hidden: true, fixed: true },
|
|
27
27
|
{ key: 'newsFilter', label: 'Блокировать сделки против важных свежих новостей', hint: 'В живой торговле и брокерских заявках не покупать бумагу на сильном негативе и не шортить на сильном позитиве за последние 6 часов.', type: 'checkbox' },
|
|
28
28
|
{ key: 'oosPct', label: 'Контрольная часть, %', hint: 'Доля периода в конце, не используемая для подбора настроек.', min: 0, max: 60, ai: false, hidden: true },
|
|
@@ -94,6 +94,10 @@ get_news — свежие новости и раскрытия эмитенто
|
|
|
94
94
|
Настройка modelUseMacro добавляет «макро-головы»: чувствительность акций к движению нефти Brent, юаня, доллара, золота и индекса гособлигаций RGBI (бета считается только по окну обучения).
|
|
95
95
|
Настройка modelTextWeight (0…1) подмешивает к карте рынка «описания бизнеса»: заранее составленный ИИ-портрет каждой компании из 20 устойчивых признаков (экспортёр, банк, дивиденды, госдоля и т.д., без новостей и событий). 0 — карта только по поведению акций, 1 — только по описаниям, значения между — смесь. При первом запуске с весом > 0 составление описаний занимает пару минут.
|
|
96
96
|
Живая проверка (paper_start) торгует моделью на текущих ценах: раз в modelHorizon часов прогноз и пересмотр портфеля, заявки по цене открытия следующей свечи.
|
|
97
|
+
Ты также умеешь помогать пользователю открывать и подключать реальные брокерские счета (Т-Банк, Финам, БКС, Алор, Цифра брокер, ITI Capital, СберИнвестиции, ВТБ, Альфа-Инвестиции):
|
|
98
|
+
- вызывай broker_onboarding_guide, чтобы получить пошаговую инструкцию по выбранному брокеру и проверить уже подключённые счета;
|
|
99
|
+
- если пользователь прислал токен Т-Банка (t.…), вызывай discover_broker_accounts, чтобы показать все его субсчета, баланс, статус маржинальной торговли и убедиться, что на счёте нет автоследования;
|
|
100
|
+
- вызывай connect_broker_account, чтобы прямо из чата добавить/подключить брокерский счёт, синхронизировать портфель и рассчитать первые заявки.
|
|
97
101
|
|
|
98
102
|
## Настройки (точные названия для инструментов)
|
|
99
103
|
${params}
|
|
@@ -8,6 +8,7 @@ import { startModelResearch } from '../model/research.js';
|
|
|
8
8
|
import { forecastNow } from '../model/forecast.js';
|
|
9
9
|
import { live } from '../model/live.js';
|
|
10
10
|
import { listNews } from '../news/index.js';
|
|
11
|
+
import { BROKER_META, brokersState, discoverBrokerAccounts, saveBrokerAccount, syncBrokerAccount, prepareBrokerOrders } from '../brokers/index.js';
|
|
11
12
|
import { plural, fmtBarMsk } from '../util.js';
|
|
12
13
|
|
|
13
14
|
export const OOS_LIMIT = 3;
|
|
@@ -287,6 +288,234 @@ export const TOOLS = {
|
|
|
287
288
|
return { result: { ok: true, итог: compactMetrics(getRun(st.runId)?.metrics?.full) }, summary: `Живая проверка №${st.runId} остановлена` };
|
|
288
289
|
},
|
|
289
290
|
},
|
|
291
|
+
|
|
292
|
+
broker_onboarding_guide: {
|
|
293
|
+
kind: 'read',
|
|
294
|
+
label: 'Справка по открытию и подключению брокерского счёта',
|
|
295
|
+
description: 'Получить подробную инструкцию по открытию брокерского счёта, включению маржинальной торговли, выпуску API-токена и список уже подключённых счетов пользователя.',
|
|
296
|
+
parameters: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
broker: {
|
|
300
|
+
type: 'string',
|
|
301
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
|
|
302
|
+
description: 'код выбранного брокера',
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
async run({ broker = 'tbank' } = {}) {
|
|
307
|
+
const st = brokersState();
|
|
308
|
+
const meta = BROKER_META.find((b) => b.id === broker) || BROKER_META[0];
|
|
309
|
+
const guides = {
|
|
310
|
+
tbank: {
|
|
311
|
+
openAccountUrl: 'https://www.tbank.ru/invest/',
|
|
312
|
+
tokenUrl: 'https://www.tbank.ru/invest/settings/',
|
|
313
|
+
accountTypeAdvice: 'Открывайте обычный «Брокерский счёт» (не Инвесткопилку и не счёт автоследования; для шортов на тарифах Профи/VIP нужен обычный брокерский счёт с включённой маржинальной торговлей).',
|
|
314
|
+
stepsToGetToken: [
|
|
315
|
+
'1. Откройте веб-кабинет Т-Инвестиций: https://www.tbank.ru/invest/settings/ (раздел «Токены T-Invest API»).',
|
|
316
|
+
'2. Нажмите «Выпустить токен» → выберите «Полный доступ» (для выставления заявок) и «Все счета».',
|
|
317
|
+
'3. Скопируйте токен (начинается с t.…) и пришлите его сюда в чат или вставьте в форму — я сам найду все ваши субсчета через discover_broker_accounts и подключу нужный счёт.',
|
|
318
|
+
'4. Чтобы стратегия зарабатывала и на падении акций (шорт), в мобильном приложении Т-Инвестиций зайдите в настройки выбранного счёта (шестерёнка вверху справа) и включите «Маржинальная торговля».',
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
finam: {
|
|
322
|
+
openAccountUrl: 'https://www.finam.ru/',
|
|
323
|
+
tokenUrl: 'https://edox.finam.ru/',
|
|
324
|
+
accountTypeAdvice: 'Единый брокерский счёт Мосбиржи (КлФ / ЕДП).',
|
|
325
|
+
stepsToGetToken: [
|
|
326
|
+
'1. В кабинете клиента Финам (edox.finam.ru) откройте «Торговля → Токены доступа (Finam Trade API)».',
|
|
327
|
+
'2. Выпустите новый токен (начинается на CA…) и скопируйте ваш торговый код клиента ClientId (например 123456R).',
|
|
328
|
+
'3. Пришлите токен и ClientId или нажмите кнопку подтверждения — я подключу счёт через connect_broker_account.',
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
bcs: {
|
|
332
|
+
openAccountUrl: 'https://bcs.ru/',
|
|
333
|
+
tokenUrl: 'https://my.broker.ru/',
|
|
334
|
+
accountTypeAdvice: 'Обычный брокерский счёт БКС Мир инвестиций с доступом к Мосбирже TQBR.',
|
|
335
|
+
stepsToGetToken: [
|
|
336
|
+
'1. В веб-терминале «БКС Мир инвестиций» откройте Настройки профиля → «Торговый API (BCS Trade API)».',
|
|
337
|
+
'2. Выпустите Refresh-токен с правом «Чтение и торговля» (trade-api-write).',
|
|
338
|
+
'3. Передайте Refresh-токен — я создам и синхронизирую счёт.',
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
alor: {
|
|
342
|
+
openAccountUrl: 'https://www.alorbroker.ru/',
|
|
343
|
+
tokenUrl: 'https://alor.dev/',
|
|
344
|
+
accountTypeAdvice: 'Фондовый счёт Мосбиржи (портфель с литерой D, например D12345).',
|
|
345
|
+
stepsToGetToken: [
|
|
346
|
+
'1. Авторизуйтесь под брокерским аккаунтом на портале разработчика https://alor.dev (или в кабинете my.alor.ru → OpenAPI).',
|
|
347
|
+
'2. Выпустите Refresh Token для боевого контура и укажите номер портфеля D…',
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
cifra: {
|
|
351
|
+
openAccountUrl: 'https://cifra-broker.ru/',
|
|
352
|
+
tokenUrl: 'https://tradernet.ru/',
|
|
353
|
+
accountTypeAdvice: 'Брокерский счёт Tradernet (Мосбиржа).',
|
|
354
|
+
stepsToGetToken: [
|
|
355
|
+
'1. В кабинете tradernet.ru откройте Настройки профиля → «API ключи».',
|
|
356
|
+
'2. Сгенерируйте пару ключей PublicKey и SecretKey и передайте их через двоеточие: PublicKey:SecretKey.',
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
iticapital: {
|
|
360
|
+
openAccountUrl: 'https://iticapital.ru/',
|
|
361
|
+
tokenUrl: 'https://iticapital.ru/',
|
|
362
|
+
accountTypeAdvice: 'Фондовый портфель Мосбиржи вида BP12345.',
|
|
363
|
+
stepsToGetToken: [
|
|
364
|
+
'1. В личном кабинете ITI Capital откройте «Торговые системы → OpenAPI» и выпустите Refresh Token.',
|
|
365
|
+
'2. Укажите Refresh-токен и код портфеля BP…',
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
sber: {
|
|
369
|
+
openAccountUrl: 'https://www.sberbank.ru/ru/person/investments',
|
|
370
|
+
accountTypeAdvice: 'Обычный брокерский счёт СберИнвестиций.',
|
|
371
|
+
stepsToGetToken: [
|
|
372
|
+
'У СберИнвестиций нет облачного REST-токена для физлиц, поэтому счёт подключается БЕЗ токена в режиме «Подтверждение заявок» (или через адрес локального HTTP-шлюза QUIK).',
|
|
373
|
+
'Достаточно указать желаемый лимит суммы на стратегию (например 100 000 ₽) и номер счёта — я сразу создам счёт в платформе через connect_broker_account и рассчитаю первые заявки в лотах Мосбиржи!',
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
vtb: {
|
|
377
|
+
openAccountUrl: 'https://broker.vtb.ru/',
|
|
378
|
+
accountTypeAdvice: 'Обычный брокерский счёт ВТБ Мои Инвестиции.',
|
|
379
|
+
stepsToGetToken: [
|
|
380
|
+
'У ВТБ Мои Инвестиции нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» (с уведомлениями в Telegram/MAX и учётом портфеля) или через адрес моста QUIK.',
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
alfa: {
|
|
384
|
+
openAccountUrl: 'https://alfabank.ru/make-money/investments/',
|
|
385
|
+
accountTypeAdvice: 'Обычный брокерский счёт Альфа-Инвестиций.',
|
|
386
|
+
stepsToGetToken: [
|
|
387
|
+
'У Альфа-Инвестиций нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» или через адрес моста QUIK.',
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
return {
|
|
392
|
+
result: {
|
|
393
|
+
брокер: meta,
|
|
394
|
+
инструкция: guides[broker] || guides.tbank,
|
|
395
|
+
уже_подключённые_счета: st.accounts.map((a) => ({
|
|
396
|
+
id: a.id,
|
|
397
|
+
название: a.name,
|
|
398
|
+
брокер: a.broker,
|
|
399
|
+
субсчёт: a.accountId,
|
|
400
|
+
режим: a.mode,
|
|
401
|
+
лимит_руб: a.maxCapitalRub,
|
|
402
|
+
баланс_руб: a.lastSync?.totalRub ?? null,
|
|
403
|
+
маржиналка: a.lastSync?.marginEnabled ?? null,
|
|
404
|
+
})),
|
|
405
|
+
},
|
|
406
|
+
summary: `Загрузил инструкцию по подключению брокера «${meta.name}»`,
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
discover_broker_accounts: {
|
|
412
|
+
kind: 'read',
|
|
413
|
+
label: 'Ищет субсчета брокера по токену',
|
|
414
|
+
description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке (T-Invest API) по токену, показать их баланс, статус маржинальной торговли и проверить отсутствие автоследования.',
|
|
415
|
+
parameters: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
token: { type: 'string', description: 'Токен T-Invest API (начинается на t.…)' },
|
|
419
|
+
sandbox: { type: 'boolean', description: 'Контур песочницы (по умолчанию false)' },
|
|
420
|
+
},
|
|
421
|
+
required: ['token'],
|
|
422
|
+
},
|
|
423
|
+
async run({ token, sandbox = false }) {
|
|
424
|
+
const r = await discoverBrokerAccounts({ broker: 'tbank', token, sandbox });
|
|
425
|
+
return {
|
|
426
|
+
result: {
|
|
427
|
+
найдено_счетов: r.accounts.length,
|
|
428
|
+
счета: r.accounts.map((a) => ({
|
|
429
|
+
accountId: a.id,
|
|
430
|
+
название: a.name,
|
|
431
|
+
тип: a.type,
|
|
432
|
+
доступ: a.access,
|
|
433
|
+
баланс_руб: a.totalRub,
|
|
434
|
+
свободные_рубли: a.cashRub,
|
|
435
|
+
позиций: a.positionsCount,
|
|
436
|
+
маржинальная_торговля: a.marginEnabled ? 'включена (шорты доступны)' : 'выключена (только в рост)',
|
|
437
|
+
автоследование: a.autoFollowDetected ? 'ОБНАРУЖЕНО (подключать запрещено Офертой)' : 'нет (можно подключать)',
|
|
438
|
+
пригодность: a.eligibleForTrading ? 'подходит для торговли' : 'только просмотр / копилка',
|
|
439
|
+
})),
|
|
440
|
+
},
|
|
441
|
+
summary: `Найдено счетов в Т-Банке: ${r.accounts.length}`,
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
connect_broker_account: {
|
|
447
|
+
kind: 'write',
|
|
448
|
+
confirmAlways: true,
|
|
449
|
+
label: 'Подключает брокерский счёт',
|
|
450
|
+
description: 'Добавить или обновить брокерский счёт в платформе «НейроАльфа», сразу выполнить синхронизацию портфеля и рассчитать стартовый пакет заявок.',
|
|
451
|
+
parameters: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
id: { type: 'string', description: 'ID существующего счёта в платформе (если обновляем существующий счёт)' },
|
|
455
|
+
broker: {
|
|
456
|
+
type: 'string',
|
|
457
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
|
|
458
|
+
description: 'Код брокера',
|
|
459
|
+
},
|
|
460
|
+
name: { type: 'string', description: 'Понятное название счёта (например «Т-Банк основной»)' },
|
|
461
|
+
token: { type: 'string', description: 'API-токен / Refresh-токен (для Сбера/ВТБ/Альфы без QUIK оставьте пустым)' },
|
|
462
|
+
accountId: { type: 'string', description: 'ID субсчёта / ClientId / номер портфеля' },
|
|
463
|
+
mode: { type: 'string', enum: ['readonly', 'confirm', 'auto'], description: 'Режим работы: confirm (подтверждение заявок), auto (автомат) или readonly' },
|
|
464
|
+
legacyMode: { type: 'string', enum: ['isolate', 'smooth', 'takeover'], description: 'Защита старых бумаг: isolate (не трогать старые бумаги, по умолчанию), smooth или takeover' },
|
|
465
|
+
maxCapitalRub: { type: 'number', description: 'Лимит суммы на стратегию в рублях (по умолчанию 100000)' },
|
|
466
|
+
maxDailyDrawdownPct: { type: 'number', description: 'Дневной стоп по просадке в % (по умолчанию 3)' },
|
|
467
|
+
limitOffsetPct: { type: 'number', description: 'Защитный отступ лимитной заявки в % (по умолчанию 0.1)' },
|
|
468
|
+
},
|
|
469
|
+
required: ['broker'],
|
|
470
|
+
},
|
|
471
|
+
preview(args) {
|
|
472
|
+
const meta = BROKER_META.find((b) => b.id === args.broker);
|
|
473
|
+
const modeLbl = { readonly: 'Только чтение', confirm: 'Подтверждение заявок', auto: 'Автомат' }[args.mode || 'confirm'] || 'Подтверждение заявок';
|
|
474
|
+
return `Брокер: ${meta?.name || args.broker}${args.name ? ` («${args.name}»)` : ''}${args.accountId ? ` · счёт ${args.accountId}` : ''} · режим «${modeLbl}» · лимит ${args.maxCapitalRub || 100000} ₽`;
|
|
475
|
+
},
|
|
476
|
+
async run(args) {
|
|
477
|
+
const beforeIds = new Set(brokersState().accounts.map((a) => a.id));
|
|
478
|
+
const st = saveBrokerAccount({
|
|
479
|
+
id: args.id,
|
|
480
|
+
broker: args.broker,
|
|
481
|
+
name: args.name,
|
|
482
|
+
token: args.token,
|
|
483
|
+
accountId: args.accountId,
|
|
484
|
+
mode: args.mode || 'confirm',
|
|
485
|
+
legacyMode: args.legacyMode || 'isolate',
|
|
486
|
+
maxCapitalRub: args.maxCapitalRub || 100000,
|
|
487
|
+
maxDailyDrawdownPct: args.maxDailyDrawdownPct ?? 3,
|
|
488
|
+
limitOffsetPct: args.limitOffsetPct ?? 0.1,
|
|
489
|
+
});
|
|
490
|
+
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];
|
|
491
|
+
let syncRes = null;
|
|
492
|
+
if (created && (created.hasToken || ['sber', 'vtb', 'alfa'].includes(created.broker))) {
|
|
493
|
+
try {
|
|
494
|
+
const synced = await syncBrokerAccount(created.id);
|
|
495
|
+
await prepareBrokerOrders(created.id).catch(() => {});
|
|
496
|
+
const accNow = synced.accounts.find((a) => a.id === created.id);
|
|
497
|
+
syncRes = {
|
|
498
|
+
синхронизировано: Boolean(accNow?.lastSync?.ok),
|
|
499
|
+
ошибка_синхронизации: accNow?.lastSync?.error || null,
|
|
500
|
+
баланс_руб: accNow?.lastSync?.totalRub ?? null,
|
|
501
|
+
свободные_рубли: accNow?.lastSync?.cashRub ?? null,
|
|
502
|
+
маржинальная_торговля: accNow?.lastSync?.marginEnabled ?? null,
|
|
503
|
+
подготовлено_заявок: accNow?.pendingBatch?.orders?.length ?? 0,
|
|
504
|
+
};
|
|
505
|
+
} catch (e) {
|
|
506
|
+
syncRes = { синхронизировано: false, ошибка_синхронизации: e.message };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
result: {
|
|
511
|
+
ok: true,
|
|
512
|
+
счёт: created ? { id: created.id, название: created.name, брокер: created.broker, субсчёт: created.accountId, режим: created.mode, лимит_руб: created.maxCapitalRub } : null,
|
|
513
|
+
статус_подключения: syncRes,
|
|
514
|
+
},
|
|
515
|
+
summary: `Подключён брокерский счёт «${created?.name || args.broker}»${syncRes?.баланс_руб !== null && syncRes?.баланс_руб !== undefined ? ` (баланс ${syncRes.баланс_руб} ₽)` : ''}`,
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
},
|
|
290
519
|
};
|
|
291
520
|
|
|
292
521
|
export const MODES = {
|