neuroalpha 0.3.17 → 0.5.2
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/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 +1 -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
- 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 +589 -66
- package/public/index.html +48 -21
- 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 +1095 -168
- 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/role.js +44 -0
- package/server/security.js +40 -0
- package/server/strategist/index.js +86 -47
- package/server/strategist/tools.js +231 -42
- 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
|
-
|
|
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: {
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
порог_покупки_проц:
|
|
108
|
-
порог_шорта_проц:
|
|
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:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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'
|
|
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: 'Найти все открытые брокерские субсчета пользователя в Т-Банке (
|
|
523
|
+
label: 'Ищет субсчета брокера по сохранённому токену',
|
|
524
|
+
description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке по токену УЖЕ СОХРАНЁННОГО в платформе счёта (id из get_brokers_status): баланс, маржинальная торговля, отсутствие автоследования. Токен никогда не передаётся через чат: если счёта с токеном ещё нет, попроси пользователя вставить токен в форму «+ Добавить счёт» (поле «Токен») и сохранить счёт.',
|
|
334
525
|
parameters: {
|
|
335
526
|
type: 'object',
|
|
336
527
|
properties: {
|
|
337
|
-
|
|
528
|
+
id: { type: 'string', description: 'ID сохранённого счёта Т-Банка в платформе (brk_…)' },
|
|
338
529
|
sandbox: { type: 'boolean', description: 'Контур песочницы (по умолчанию false)' },
|
|
339
530
|
},
|
|
340
|
-
required: ['
|
|
531
|
+
required: ['id'],
|
|
341
532
|
},
|
|
342
|
-
async run({
|
|
343
|
-
const r = await discoverBrokerAccounts({ broker: 'tbank',
|
|
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'
|
|
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 (
|
|
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 &&
|
|
603
|
+
if (created && created.hasToken) {
|
|
415
604
|
try {
|
|
416
605
|
const synced = await syncBrokerAccount(created.id);
|
|
417
606
|
await prepareBrokerOrders(created.id).catch(() => {});
|
package/server/tls-ca.js
ADDED
|
@@ -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;
|