neuroalpha 0.3.17 → 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.
Files changed (39) hide show
  1. package/config/certs/russian_trusted_root_ca.pem +33 -0
  2. package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
  3. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -4
  4. 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
  5. package/package.json +60 -53
  6. package/public/activate.html +36 -0
  7. package/public/activate.js +33 -0
  8. package/public/admin.html +126 -7
  9. package/public/app.js +573 -66
  10. package/public/index.html +48 -21
  11. package/public/landing.html +106 -37
  12. package/public/legal.html +4 -4
  13. package/public/styles.css +152 -36
  14. package/server/api/routes.js +308 -37
  15. package/server/auth/index.js +79 -15
  16. package/server/brokers/index.js +1014 -168
  17. package/server/build-info.js +7 -0
  18. package/server/config.js +13 -6
  19. package/server/data/moex.js +6 -4
  20. package/server/db.js +22 -18
  21. package/server/desktop-entry.js +103 -0
  22. package/server/hub.js +28 -14
  23. package/server/index.js +82 -12
  24. package/server/instance/agent.js +259 -0
  25. package/server/instance/central.js +491 -0
  26. package/server/instance/identity.js +79 -0
  27. package/server/instance/provision.js +166 -0
  28. package/server/instance/signing.js +71 -0
  29. package/server/model/edge.js +19 -0
  30. package/server/model/forecast.js +107 -57
  31. package/server/model/live.js +1 -1
  32. package/server/model/models.js +12 -3
  33. package/server/model/research.js +218 -30
  34. package/server/notify/index.js +51 -16
  35. package/server/role.js +44 -0
  36. package/server/security.js +40 -0
  37. package/server/strategist/index.js +86 -47
  38. package/server/strategist/tools.js +231 -42
  39. package/server/tls-ca.js +32 -0
@@ -5,8 +5,8 @@ import { getRun, listRuns } from '../db.js';
5
5
  import { analyzeRun, compareRuns, recentRuns } from './analytics.js';
6
6
  import { forecastNow } from '../model/forecast.js';
7
7
  import { listNews } from '../news/index.js';
8
- import { BROKER_META, brokersState, discoverBrokerAccounts, saveBrokerAccount, syncBrokerAccount, prepareBrokerOrders } from '../brokers/index.js';
9
- import { fmtBarMsk } from '../util.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
10
 
11
11
  export const OOS_LIMIT = 3;
12
12
  const AI_LOCKED = PARAMS.filter((p) => p.ai === false).map((p) => p.key);
@@ -34,7 +34,7 @@ export const TOOLS = {
34
34
  get_brokers_status: {
35
35
  kind: 'read',
36
36
  label: 'Смотрит брокерские счета и портфель',
37
- description: 'Текущее состояние подключённых брокерских счетов пользователя: баланс, свободные рубли, открытые позиции, доход, активные заявки в стакане, подготовленный пакет заявок по прогнозу и настройки предохранителей.',
37
+ description: 'Текущий снимок подключённых брокерских счетов ПРЯМО СЕЙЧАС: баланс, свободные рубли, открытые позиции в портфеле, активные заявки в стакане и подготовленный пакет заявок. ВНИМАНИЕ: чтобы посмотреть историю прошлых сделок (как прошли торги вчера, сегодня или за неделю), вызывай get_broker_operations!',
38
38
  parameters: { type: 'object', properties: {} },
39
39
  async run() {
40
40
  const st = brokersState();
@@ -50,10 +50,11 @@ export const TOOLS = {
50
50
  дневной_стоп_проц: a.maxDailyDrawdownPct,
51
51
  отступ_лимитки_проц: a.limitOffsetPct,
52
52
  пауза_причина: a.haltedReason || null,
53
+ время_снимка_портфеля_мск: a.lastSync?.at ? fmtBarMsk(a.lastSync.at) : null,
53
54
  баланс_руб: a.lastSync?.totalRub ?? null,
54
55
  свободные_рубли: a.lastSync?.cashRub ?? null,
55
56
  маржинальная_торговля: a.lastSync?.marginEnabled ?? null,
56
- позиции: (a.lastSync?.positions || []).map((p) => ({
57
+ текущие_позиции_в_портфеле: (a.lastSync?.positions || []).map((p) => ({
57
58
  тикер: p.symbol,
58
59
  название: p.name,
59
60
  кол_во_шт: p.qty,
@@ -61,6 +62,7 @@ export const TOOLS = {
61
62
  текущая_цена: p.price,
62
63
  стоимость_руб: p.valueRub,
63
64
  доход_руб: p.pnlRub,
65
+ ...(a.entryTsBySymbol?.[p.symbol] ? { время_входа_робота_мск: fmtBarMsk(a.entryTsBySymbol[p.symbol]) } : {}),
64
66
  })),
65
67
  активные_заявки_в_стакане: a.lastSync?.activeOrders || [],
66
68
  подготовленный_пакет_заявок: a.pendingBatch ? {
@@ -75,37 +77,213 @@ export const TOOLS = {
75
77
  } : null,
76
78
  }));
77
79
  return {
78
- result: { подключено_счетов: accs.length, счета: accs },
80
+ result: {
81
+ текущее_время_мск: fmtBarMsk(Date.now()),
82
+ подключено_счетов: accs.length,
83
+ счета: accs,
84
+ },
79
85
  summary: accs.length ? `Проверил брокерские счета (${accs.length})` : 'Брокерские счета пока не подключены',
80
86
  };
81
87
  },
82
88
  },
83
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
+
84
252
  model_forecast: {
85
253
  kind: 'read',
86
254
  label: 'Смотрит прогноз на ближайшие часы',
87
- description: 'Живой прогноз модели по ~100 акциям Мосбиржи: какие акции ожидаются сильнее или слабее рынка в ближайшие 1,5–3,5 часа, пороги входа в лонг и шорт, а также цепочка влияния соседей по карте рынка.',
255
+ description: 'Живой прогноз модели по ~100 акциям Мосбиржи: какие акции ожидаются сильнее или слабее рынка в ближайшие 1,5–3,5 часа, пороги входа в лонг и шорт, живой импульс текущей свечи и цепочка влияния соседей по карте рынка.',
88
256
  parameters: { type: 'object', properties: { top: { type: 'number', description: 'сколько акций показать, по умолчанию 10' } } },
89
257
  async run({ top = 10 }) {
90
258
  const r = await forecastNow(getSettings());
91
- const pick = (x) => ({
92
- акция: x.symbol,
93
- название: x.name,
94
- цена_руб: x.price,
95
- ожидание_проц: x.expectedPct,
96
- динамика_проц: x.deltaPct || 0,
97
- сигнал_покупки: !!x.signal,
98
- ...(x.shortSignal !== undefined ? { сигнал_шорта: !!x.shortSignal } : {}),
99
- ...(x.shortBlockedByBroker ? { шорт_запрещён_брокером: true } : {}),
100
- ликвидна: x.liquid,
101
- влияют_соседи: (x.influences || []).map((n) => `${n.symbol} (${n.contribPct > 0 ? '+' : ''}${n.contribPct}%, близость ${n.closeness})`),
102
- });
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
+ };
103
280
  return {
104
281
  result: {
282
+ текущее_время_мск: fmtBarMsk(Date.now()),
105
283
  свеча: r.bar,
106
284
  горизонт_часов: r.horizon,
107
- порог_покупки_проц: r.buyThresholdPct ?? r.thresholdPct,
108
- порог_шорта_проц: r.shortThresholdPct ?? r.thresholdPct,
285
+ порог_покупки_проц: buyThr,
286
+ порог_шорта_проц: shortThr,
109
287
  тренд_индекса: r.marketDown ? 'падающий (облегчён шорт, ужесточён лонг)' : 'растущий (стандартный лонг, ужесточён шорт)',
110
288
  сильнее_рынка: r.rows.slice(0, top).map(pick),
111
289
  слабее_рынка: r.rows.slice(-Math.min(6, top)).reverse().map(pick),
@@ -129,15 +307,28 @@ export const TOOLS = {
129
307
  async run({ symbol, hours = 24 }) {
130
308
  const items = listNews({ symbol, hours, limit: 25 });
131
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);
132
313
  return {
133
- result: items.map((n) => ({
134
- время: fmtBarMsk(n.ts),
135
- источник: n.src,
136
- ...(n.symbols.length ? { акции: n.symbols.map((s) => `${s.symbol} (тон ${s.tone ?? '—'})`) } : {}),
137
- ...(n.imp !== null ? { важность: n.imp } : {}),
138
- суть: n.gist || n.title,
139
- ссылка: n.link,
140
- })),
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
+ },
141
332
  summary: `Новости за ${hours} ч: ${items.length}${symbol ? ` по ${symbol}` : ''}`,
142
333
  };
143
334
  },
@@ -216,7 +407,7 @@ export const TOOLS = {
216
407
  properties: {
217
408
  broker: {
218
409
  type: 'string',
219
- enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
410
+ enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital'],
220
411
  description: 'код выбранного брокера',
221
412
  },
222
413
  },
@@ -329,18 +520,18 @@ export const TOOLS = {
329
520
 
330
521
  discover_broker_accounts: {
331
522
  kind: 'read',
332
- label: 'Ищет субсчета брокера по токену',
333
- description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке (T-Invest API) по токену, показать их баланс, статус маржинальной торговли и проверить отсутствие автоследования.',
523
+ label: 'Ищет субсчета брокера по сохранённому токену',
524
+ description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке по токену УЖЕ СОХРАНЁННОГО в платформе счёта (id из get_brokers_status): баланс, маржинальная торговля, отсутствие автоследования. Токен никогда не передаётся через чат: если счёта с токеном ещё нет, попроси пользователя вставить токен в форму «+ Добавить счёт» (поле «Токен») и сохранить счёт.',
334
525
  parameters: {
335
526
  type: 'object',
336
527
  properties: {
337
- token: { type: 'string', description: 'Токен T-Invest API (начинается на t.…)' },
528
+ id: { type: 'string', description: 'ID сохранённого счёта Т-Банка в платформе (brk_…)' },
338
529
  sandbox: { type: 'boolean', description: 'Контур песочницы (по умолчанию false)' },
339
530
  },
340
- required: ['token'],
531
+ required: ['id'],
341
532
  },
342
- async run({ token, sandbox = false }) {
343
- const r = await discoverBrokerAccounts({ broker: 'tbank', token, sandbox });
533
+ async run({ id, sandbox = false }) {
534
+ const r = await discoverBrokerAccounts({ broker: 'tbank', id, sandbox });
344
535
  return {
345
536
  result: {
346
537
  найдено_счетов: r.accounts.length,
@@ -366,20 +557,19 @@ export const TOOLS = {
366
557
  kind: 'write',
367
558
  confirmAlways: true,
368
559
  label: 'Подключает или настраивает брокерский счёт',
369
- description: 'Добавить или обновить настройки брокерского счёта в платформе «НейроАльфа», сразу выполнить синхронизацию портфеля и рассчитать пакет заявок.',
560
+ description: 'Добавить или обновить настройки брокерского счёта в платформе «НейроАльфа», сразу выполнить синхронизацию портфеля и рассчитать пакет заявок. Токен брокера этим инструментом НЕ задаётся и в чат не запрашивается — пользователь вставляет его сам в форму счёта. Режим «auto» доступен только для Т-Банка.',
370
561
  parameters: {
371
562
  type: 'object',
372
563
  properties: {
373
564
  id: { type: 'string', description: 'ID существующего счёта в платформе (если обновляем существующий счёт)' },
374
565
  broker: {
375
566
  type: 'string',
376
- enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
377
- description: 'Код брокера',
567
+ enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital'],
568
+ description: 'Код брокера (полная поддержка — tbank; остальные в бете, только с подтверждением заявок)',
378
569
  },
379
570
  name: { type: 'string', description: 'Понятное название счёта (например «Т-Банк основной»)' },
380
- token: { type: 'string', description: 'API-токен / Refresh-токен (для Сбера/ВТБ/Альфы без QUIK оставьте пустым)' },
381
571
  accountId: { type: 'string', description: 'ID субсчёта / ClientId / номер портфеля' },
382
- mode: { type: 'string', enum: ['readonly', 'confirm', 'auto'], description: 'Режим работы: confirm (подтверждение заявок), auto (автомат) или readonly' },
572
+ mode: { type: 'string', enum: ['readonly', 'confirm', 'auto'], description: 'Режим работы: confirm (подтверждение заявок), auto (автомат, только Т-Банк) или readonly' },
383
573
  legacyMode: { type: 'string', enum: ['isolate', 'smooth', 'takeover'], description: 'Режим для старых бумаг на счёте: isolate (не трогать старые бумаги), smooth (плавный переход) или takeover (полное управление)' },
384
574
  maxCapitalRub: { type: 'number', description: 'Лимит суммы на стратегию в рублях (например 100000 или 1000000)' },
385
575
  maxDailyDrawdownPct: { type: 'number', description: 'Дневной стоп по просадке в % (по умолчанию 3)' },
@@ -400,7 +590,6 @@ export const TOOLS = {
400
590
  id: args.id,
401
591
  broker: args.broker,
402
592
  name: args.name,
403
- token: args.token,
404
593
  accountId: args.accountId,
405
594
  mode: args.mode || 'confirm',
406
595
  legacyMode: args.legacyMode || 'isolate',
@@ -411,7 +600,7 @@ export const TOOLS = {
411
600
  });
412
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];
413
602
  let syncRes = null;
414
- if (created && (created.hasToken || ['sber', 'vtb', 'alfa'].includes(created.broker))) {
603
+ if (created && created.hasToken) {
415
604
  try {
416
605
  const synced = await syncBrokerAccount(created.id);
417
606
  await prepareBrokerOrders(created.id).catch(() => {});
@@ -0,0 +1,32 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
+ // Доверенные корневые сертификаты для HTTPS к российским сервисам (брокеры, GigaChat, MAX):
3
+ // системные корни Node.js + корневой сертификат НУЦ Минцифры РФ (Russian Trusted Root CA).
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import tls from 'node:tls';
7
+ import { ROOT } from './config.js';
8
+
9
+ const CANDIDATES = [
10
+ process.env.BROKER_CA_FILE,
11
+ process.env.GIGACHAT_CA_FILE,
12
+ path.join(ROOT, 'config', 'certs', 'russian_trusted_root_ca.pem'),
13
+ path.resolve(ROOT, '../work/#system/certs/russian_trusted_root_ca.pem'),
14
+ ].filter(Boolean);
15
+
16
+ let russianRootPem = '';
17
+ let russianRootPath = '';
18
+ for (const p of CANDIDATES) {
19
+ try {
20
+ if (fs.existsSync(p)) {
21
+ russianRootPem = fs.readFileSync(p, 'utf8');
22
+ russianRootPath = p;
23
+ break;
24
+ }
25
+ } catch { /* ignore */ }
26
+ }
27
+ if (!russianRootPem) {
28
+ console.warn('⚠ Не найден сертификат НУЦ Минцифры (russian_trusted_root_ca.pem): HTTPS к Т-Банку будет отклонён. Укажите путь в BROKER_CA_FILE.');
29
+ }
30
+
31
+ export const TRUSTED_CA = russianRootPem ? [...tls.rootCertificates, russianRootPem] : [...tls.rootCertificates];
32
+ export const RUSSIAN_ROOT_CA_PATH = russianRootPath;