neuroalpha 0.3.15 → 0.3.17
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 +79 -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 +96 -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 +464 -930
- package/public/index.html +148 -223
- 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 +434 -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 +19 -19
- package/server/strategist/index.js +423 -484
- package/server/strategist/tools.js +486 -303
|
@@ -1,303 +1,486 @@
|
|
|
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
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const locked
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const
|
|
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
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
})),
|
|
183
|
-
summary:
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
kind: '
|
|
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
|
-
const
|
|
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
|
-
|
|
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 } from '../brokers/index.js';
|
|
9
|
+
import { fmtBarMsk } 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: 'Текущее состояние подключённых брокерских счетов пользователя: баланс, свободные рубли, открытые позиции, доход, активные заявки в стакане, подготовленный пакет заявок по прогнозу и настройки предохранителей.',
|
|
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?.totalRub ?? null,
|
|
54
|
+
свободные_рубли: a.lastSync?.cashRub ?? null,
|
|
55
|
+
маржинальная_торговля: a.lastSync?.marginEnabled ?? null,
|
|
56
|
+
позиции: (a.lastSync?.positions || []).map((p) => ({
|
|
57
|
+
тикер: p.symbol,
|
|
58
|
+
название: p.name,
|
|
59
|
+
кол_во_шт: p.qty,
|
|
60
|
+
средняя_цена: p.avgPrice,
|
|
61
|
+
текущая_цена: p.price,
|
|
62
|
+
стоимость_руб: p.valueRub,
|
|
63
|
+
доход_руб: p.pnlRub,
|
|
64
|
+
})),
|
|
65
|
+
активные_заявки_в_стакане: a.lastSync?.activeOrders || [],
|
|
66
|
+
подготовленный_пакет_заявок: a.pendingBatch ? {
|
|
67
|
+
свеча: a.pendingBatch.bar,
|
|
68
|
+
статус: a.pendingBatch.status,
|
|
69
|
+
рабочий_бюджет_руб: a.pendingBatch.budgetRub,
|
|
70
|
+
свободно_под_вход_руб: a.pendingBatch.availRub,
|
|
71
|
+
направление: a.pendingBatch.effectiveDirection,
|
|
72
|
+
диагностика: a.pendingBatch.diagNote || null,
|
|
73
|
+
тарифная_заметка: a.pendingBatch.planThrottleNote || null,
|
|
74
|
+
заявки: a.pendingBatch.orders || [],
|
|
75
|
+
} : null,
|
|
76
|
+
}));
|
|
77
|
+
return {
|
|
78
|
+
result: { подключено_счетов: accs.length, счета: accs },
|
|
79
|
+
summary: accs.length ? `Проверил брокерские счета (${accs.length})` : 'Брокерские счета пока не подключены',
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
model_forecast: {
|
|
85
|
+
kind: 'read',
|
|
86
|
+
label: 'Смотрит прогноз на ближайшие часы',
|
|
87
|
+
description: 'Живой прогноз модели по ~100 акциям Мосбиржи: какие акции ожидаются сильнее или слабее рынка в ближайшие 1,5–3,5 часа, пороги входа в лонг и шорт, а также цепочка влияния соседей по карте рынка.',
|
|
88
|
+
parameters: { type: 'object', properties: { top: { type: 'number', description: 'сколько акций показать, по умолчанию 10' } } },
|
|
89
|
+
async run({ top = 10 }) {
|
|
90
|
+
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
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
result: {
|
|
105
|
+
свеча: r.bar,
|
|
106
|
+
горизонт_часов: r.horizon,
|
|
107
|
+
порог_покупки_проц: r.buyThresholdPct ?? r.thresholdPct,
|
|
108
|
+
порог_шорта_проц: r.shortThresholdPct ?? r.thresholdPct,
|
|
109
|
+
тренд_индекса: r.marketDown ? 'падающий (облегчён шорт, ужесточён лонг)' : 'растущий (стандартный лонг, ужесточён шорт)',
|
|
110
|
+
сильнее_рынка: r.rows.slice(0, top).map(pick),
|
|
111
|
+
слабее_рынка: r.rows.slice(-Math.min(6, top)).reverse().map(pick),
|
|
112
|
+
},
|
|
113
|
+
summary: `Прогноз на свече ${r.bar}: сигналов покупки ${r.rows.filter((x) => x.signal).length}, шорта ${r.rows.filter((x) => x.shortSignal).length}`,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
get_news: {
|
|
119
|
+
kind: 'read',
|
|
120
|
+
label: 'Смотрит свежие новости',
|
|
121
|
+
description: 'Свежие новости и раскрытия эмитентов Мосбиржи: каких акций касаются, тон −1…+1 (негатив/позитив), важность 0…1 и краткая суть.',
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
symbol: { type: 'string', description: 'тикер, например SBER; без него — все важные новости по рынку' },
|
|
126
|
+
hours: { type: 'number', description: 'за сколько часов смотреть, по умолчанию 24' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
async run({ symbol, hours = 24 }) {
|
|
130
|
+
const items = listNews({ symbol, hours, limit: 25 });
|
|
131
|
+
if (!items.length) return { result: { новостей: 'нет' }, summary: `Новостей ${symbol ? `по ${symbol}` : ''} за последние ${hours} ч нет` };
|
|
132
|
+
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
|
+
})),
|
|
141
|
+
summary: `Новости за ${hours} ч: ${items.length}${symbol ? ` по ${symbol}` : ''}`,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
market_neighbors: {
|
|
147
|
+
kind: 'read',
|
|
148
|
+
label: 'Смотрит соседей акции на карте рынка',
|
|
149
|
+
description: 'Ближайшие соседи акции на спектральной карте рынка (какие бумаги движутся синхронно и передают импульс).',
|
|
150
|
+
parameters: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] },
|
|
151
|
+
async run({ symbol }) {
|
|
152
|
+
const symUp = String(symbol || '').toUpperCase().trim();
|
|
153
|
+
const fc = await forecastNow(getSettings()).catch(() => null);
|
|
154
|
+
const row = fc?.rows?.find((r) => r.symbol === symUp);
|
|
155
|
+
if (row?.influences?.length) {
|
|
156
|
+
return {
|
|
157
|
+
result: { акция: symUp, название: row.name, цена_руб: row.price, ожидание_проц: row.expectedPct, ближайшие_влияющие_соседи: row.influences },
|
|
158
|
+
summary: `Соседи ${symUp}: ${row.influences.slice(0, 4).map((x) => x.symbol).join(', ')}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const run = listRuns().find((r) => r.mode === 'model' && r.status === 'done');
|
|
162
|
+
const ex = run && getRun(run.id)?.extra;
|
|
163
|
+
if (!ex) return { result: { ошибка: 'нет данных по соседям' }, summary: 'Нет данных по соседям', ok: false };
|
|
164
|
+
const i = ex.symbols.indexOf(symUp);
|
|
165
|
+
if (i < 0) return { result: { ошибка: `${symUp} нет в словаре модели` }, summary: `${symUp} нет в словаре`, ok: false };
|
|
166
|
+
return {
|
|
167
|
+
result: { акция: ex.symbols[i], соседи: ex.neighbors[i].map(([k, c]) => ({ акция: ex.symbols[k], близость: c })) },
|
|
168
|
+
summary: `Соседи ${ex.symbols[i]}: ${ex.neighbors[i].slice(0, 4).map(([k]) => ex.symbols[k]).join(', ')}`,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
get_settings: {
|
|
174
|
+
kind: 'read',
|
|
175
|
+
label: 'Смотрит настройки издержек и фильтра новостей',
|
|
176
|
+
description: 'Текущие настройки комиссий брокера, проскальзывания, ставки за шорт и новостного фильтра.',
|
|
177
|
+
parameters: { type: 'object', properties: {} },
|
|
178
|
+
async run() {
|
|
179
|
+
const s = getSettings();
|
|
180
|
+
const visible = PARAMS.filter((p) => !p.hidden);
|
|
181
|
+
return {
|
|
182
|
+
result: Object.fromEntries(visible.map((p) => [p.key, { значение: s[p.key], что_это: p.label, пояснение: p.hint }])),
|
|
183
|
+
summary: 'Прочитал настройки издержек и новостей',
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
set_settings: {
|
|
189
|
+
kind: 'write',
|
|
190
|
+
confirmAlways: true,
|
|
191
|
+
label: 'Меняет настройки',
|
|
192
|
+
description: 'Предложить изменение настроек в разделе «Издержки и новости» (с подтверждением пользователя кнопкой).',
|
|
193
|
+
parameters: { type: 'object', properties: { changes: changesSchema, reason: { type: 'string', description: 'пояснение для пользователя' } }, required: ['changes', 'reason'] },
|
|
194
|
+
preview(args) {
|
|
195
|
+
const { values, errors } = checkChanges(args.changes);
|
|
196
|
+
return `${args.reason || ''}\nИзменения: ${describeChanges(values)}${errors.length ? `\nОшибки: ${errors.join('; ')}` : ''}`;
|
|
197
|
+
},
|
|
198
|
+
async run(args) {
|
|
199
|
+
const { values, errors } = checkChanges(args.changes);
|
|
200
|
+
const r = updateSettings(values, { source: 'ai', reason: args.reason });
|
|
201
|
+
const all = [...errors, ...r.errors];
|
|
202
|
+
return {
|
|
203
|
+
result: { изменено: r.changed, ошибки: all.length ? all : undefined },
|
|
204
|
+
summary: Object.keys(r.changed).length ? `Настройки изменены: ${describeChanges(Object.fromEntries(Object.entries(r.changed).map(([k, v]) => [k, v.стало])))}` : `Настройки не изменились${all.length ? `: ${all.join('; ')}` : ''}`,
|
|
205
|
+
ok: !all.length,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
broker_onboarding_guide: {
|
|
211
|
+
kind: 'read',
|
|
212
|
+
label: 'Справка по открытию и подключению брокерского счёта',
|
|
213
|
+
description: 'Получить подробную пошаговую инструкцию по открытию брокерского счёта, включению маржинальной торговли, выпуску API-токена и список уже подключённых счетов пользователя.',
|
|
214
|
+
parameters: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
broker: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
|
|
220
|
+
description: 'код выбранного брокера',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
async run({ broker = 'tbank' } = {}) {
|
|
225
|
+
const st = brokersState();
|
|
226
|
+
const meta = BROKER_META.find((b) => b.id === broker) || BROKER_META[0];
|
|
227
|
+
const guides = {
|
|
228
|
+
tbank: {
|
|
229
|
+
openAccountUrl: 'https://www.tbank.ru/invest/',
|
|
230
|
+
tokenUrl: 'https://www.tbank.ru/invest/settings/',
|
|
231
|
+
accountTypeAdvice: 'Открывайте обычный «Брокерский счёт» (не Инвесткопилку и не счёт автоследования; для шортов на тарифах Профи/VIP нужен обычный брокерский счёт с включённой маржинальной торговлей).',
|
|
232
|
+
stepsToGetToken: [
|
|
233
|
+
'1. Откройте веб-кабинет Т-Инвестиций: https://www.tbank.ru/invest/settings/ (раздел «Токены T-Invest API»).',
|
|
234
|
+
'2. Нажмите «Выпустить токен» → выберите «Полный доступ» (для выставления заявок) и «Все счета».',
|
|
235
|
+
'3. Скопируйте токен (начинается с t.…) и пришлите его сюда в чат или вставьте в форму — я сам найду все ваши субсчета через discover_broker_accounts и подключу нужный счёт.',
|
|
236
|
+
'4. Чтобы стратегия зарабатывала и на падении акций (шорт), в мобильном приложении Т-Инвестиций зайдите в настройки выбранного счёта (шестерёнка вверху справа) и включите «Маржинальная торговля».',
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
finam: {
|
|
240
|
+
openAccountUrl: 'https://www.finam.ru/',
|
|
241
|
+
tokenUrl: 'https://edox.finam.ru/',
|
|
242
|
+
accountTypeAdvice: 'Единый брокерский счёт Мосбиржи (КлФ / ЕДП).',
|
|
243
|
+
stepsToGetToken: [
|
|
244
|
+
'1. В кабинете клиента Финам (edox.finam.ru) откройте «Торговля → Токены доступа (Finam Trade API)».',
|
|
245
|
+
'2. Выпустите новый токен (начинается на CA…) и скопируйте ваш торговый код клиента ClientId (например 123456R).',
|
|
246
|
+
'3. Пришлите токен и ClientId или нажмите кнопку подтверждения — я подключу счёт через connect_broker_account.',
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
bcs: {
|
|
250
|
+
openAccountUrl: 'https://bcs.ru/',
|
|
251
|
+
tokenUrl: 'https://my.broker.ru/',
|
|
252
|
+
accountTypeAdvice: 'Обычный брокерский счёт БКС Мир инвестиций с доступом к Мосбирже TQBR.',
|
|
253
|
+
stepsToGetToken: [
|
|
254
|
+
'1. В веб-терминале «БКС Мир инвестиций» откройте Настройки профиля → «Торговый API (BCS Trade API)».',
|
|
255
|
+
'2. Выпустите Refresh-токен с правом «Чтение и торговля» (trade-api-write).',
|
|
256
|
+
'3. Передайте Refresh-токен — я создам и синхронизирую счёт.',
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
alor: {
|
|
260
|
+
openAccountUrl: 'https://www.alorbroker.ru/',
|
|
261
|
+
tokenUrl: 'https://alor.dev/',
|
|
262
|
+
accountTypeAdvice: 'Фондовый счёт Мосбиржи (портфель с литерой D, например D12345).',
|
|
263
|
+
stepsToGetToken: [
|
|
264
|
+
'1. Авторизуйтесь под брокерским аккаунтом на портале разработчика https://alor.dev (или в кабинете my.alor.ru → OpenAPI).',
|
|
265
|
+
'2. Выпустите Refresh Token для боевого контура и укажите номер портфеля D…',
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
cifra: {
|
|
269
|
+
openAccountUrl: 'https://cifra-broker.ru/',
|
|
270
|
+
tokenUrl: 'https://tradernet.ru/',
|
|
271
|
+
accountTypeAdvice: 'Брокерский счёт Tradernet (Мосбиржа).',
|
|
272
|
+
stepsToGetToken: [
|
|
273
|
+
'1. В кабинете tradernet.ru откройте Настройки профиля → «API ключи».',
|
|
274
|
+
'2. Сгенерируйте пару ключей PublicKey и SecretKey и передайте их через двоеточие: PublicKey:SecretKey.',
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
iticapital: {
|
|
278
|
+
openAccountUrl: 'https://iticapital.ru/',
|
|
279
|
+
tokenUrl: 'https://iticapital.ru/',
|
|
280
|
+
accountTypeAdvice: 'Фондовый портфель Мосбиржи вида BP12345.',
|
|
281
|
+
stepsToGetToken: [
|
|
282
|
+
'1. В личном кабинете ITI Capital откройте «Торговые системы → OpenAPI» и выпустите Refresh Token.',
|
|
283
|
+
'2. Укажите Refresh-токен и код портфеля BP…',
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
sber: {
|
|
287
|
+
openAccountUrl: 'https://www.sberbank.ru/ru/person/investments',
|
|
288
|
+
accountTypeAdvice: 'Обычный брокерский счёт СберИнвестиций.',
|
|
289
|
+
stepsToGetToken: [
|
|
290
|
+
'У СберИнвестиций нет облачного REST-токена для физлиц, поэтому счёт подключается БЕЗ токена в режиме «Подтверждение заявок» (или через адрес локального HTTP-шлюза QUIK).',
|
|
291
|
+
'Достаточно указать желаемый лимит суммы на стратегию (например 100 000 ₽) и номер счёта — я сразу создам счёт в платформе через connect_broker_account и рассчитаю первые заявки в лотах Мосбиржи!',
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
vtb: {
|
|
295
|
+
openAccountUrl: 'https://broker.vtb.ru/',
|
|
296
|
+
accountTypeAdvice: 'Обычный брокерский счёт ВТБ Мои Инвестиции.',
|
|
297
|
+
stepsToGetToken: [
|
|
298
|
+
'У ВТБ Мои Инвестиции нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» (с уведомлениями в Telegram/MAX и учётом портфеля) или через адрес моста QUIK.',
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
alfa: {
|
|
302
|
+
openAccountUrl: 'https://alfabank.ru/make-money/investments/',
|
|
303
|
+
accountTypeAdvice: 'Обычный брокерский счёт Альфа-Инвестиций.',
|
|
304
|
+
stepsToGetToken: [
|
|
305
|
+
'У Альфа-Инвестиций нет облачного REST-токена для физлиц — счёт подключается без токена в режиме «Подтверждение заявок» или через адрес моста QUIK.',
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
result: {
|
|
311
|
+
брокер: meta,
|
|
312
|
+
инструкция: guides[broker] || guides.tbank,
|
|
313
|
+
уже_подключённые_счета: st.accounts.map((a) => ({
|
|
314
|
+
id: a.id,
|
|
315
|
+
название: a.name,
|
|
316
|
+
брокер: a.broker,
|
|
317
|
+
субсчёт: a.accountId,
|
|
318
|
+
режим: a.mode,
|
|
319
|
+
режим_старого_портфеля: a.legacyMode || 'isolate',
|
|
320
|
+
лимит_руб: a.maxCapitalRub,
|
|
321
|
+
баланс_руб: a.lastSync?.totalRub ?? null,
|
|
322
|
+
маржиналка: a.lastSync?.marginEnabled ?? null,
|
|
323
|
+
})),
|
|
324
|
+
},
|
|
325
|
+
summary: `Загрузил инструкцию по подключению брокера «${meta.name}»`,
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
discover_broker_accounts: {
|
|
331
|
+
kind: 'read',
|
|
332
|
+
label: 'Ищет субсчета брокера по токену',
|
|
333
|
+
description: 'Найти все открытые брокерские субсчета пользователя в Т-Банке (T-Invest API) по токену, показать их баланс, статус маржинальной торговли и проверить отсутствие автоследования.',
|
|
334
|
+
parameters: {
|
|
335
|
+
type: 'object',
|
|
336
|
+
properties: {
|
|
337
|
+
token: { type: 'string', description: 'Токен T-Invest API (начинается на t.…)' },
|
|
338
|
+
sandbox: { type: 'boolean', description: 'Контур песочницы (по умолчанию false)' },
|
|
339
|
+
},
|
|
340
|
+
required: ['token'],
|
|
341
|
+
},
|
|
342
|
+
async run({ token, sandbox = false }) {
|
|
343
|
+
const r = await discoverBrokerAccounts({ broker: 'tbank', token, sandbox });
|
|
344
|
+
return {
|
|
345
|
+
result: {
|
|
346
|
+
найдено_счетов: r.accounts.length,
|
|
347
|
+
счета: r.accounts.map((a) => ({
|
|
348
|
+
accountId: a.id,
|
|
349
|
+
название: a.name,
|
|
350
|
+
тип: a.type,
|
|
351
|
+
доступ: a.access,
|
|
352
|
+
баланс_руб: a.totalRub,
|
|
353
|
+
свободные_рубли: a.cashRub,
|
|
354
|
+
позиций: a.positionsCount,
|
|
355
|
+
маржинальная_торговля: a.marginEnabled ? 'включена (шорты доступны)' : 'выключена (только в рост)',
|
|
356
|
+
автоследование: a.autoFollowDetected ? 'ОБНАРУЖЕНО (подключать запрещено Офертой)' : 'нет (можно подключать)',
|
|
357
|
+
пригодность: a.eligibleForTrading ? 'подходит для торговли' : 'только просмотр / копилка',
|
|
358
|
+
})),
|
|
359
|
+
},
|
|
360
|
+
summary: `Найдено счетов в Т-Банке: ${r.accounts.length}`,
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
connect_broker_account: {
|
|
366
|
+
kind: 'write',
|
|
367
|
+
confirmAlways: true,
|
|
368
|
+
label: 'Подключает или настраивает брокерский счёт',
|
|
369
|
+
description: 'Добавить или обновить настройки брокерского счёта в платформе «НейроАльфа», сразу выполнить синхронизацию портфеля и рассчитать пакет заявок.',
|
|
370
|
+
parameters: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
id: { type: 'string', description: 'ID существующего счёта в платформе (если обновляем существующий счёт)' },
|
|
374
|
+
broker: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
enum: ['tbank', 'finam', 'bcs', 'alor', 'cifra', 'iticapital', 'sber', 'vtb', 'alfa'],
|
|
377
|
+
description: 'Код брокера',
|
|
378
|
+
},
|
|
379
|
+
name: { type: 'string', description: 'Понятное название счёта (например «Т-Банк основной»)' },
|
|
380
|
+
token: { type: 'string', description: 'API-токен / Refresh-токен (для Сбера/ВТБ/Альфы без QUIK оставьте пустым)' },
|
|
381
|
+
accountId: { type: 'string', description: 'ID субсчёта / ClientId / номер портфеля' },
|
|
382
|
+
mode: { type: 'string', enum: ['readonly', 'confirm', 'auto'], description: 'Режим работы: confirm (подтверждение заявок), auto (автомат) или readonly' },
|
|
383
|
+
legacyMode: { type: 'string', enum: ['isolate', 'smooth', 'takeover'], description: 'Режим для старых бумаг на счёте: isolate (не трогать старые бумаги), smooth (плавный переход) или takeover (полное управление)' },
|
|
384
|
+
maxCapitalRub: { type: 'number', description: 'Лимит суммы на стратегию в рублях (например 100000 или 1000000)' },
|
|
385
|
+
maxDailyDrawdownPct: { type: 'number', description: 'Дневной стоп по просадке в % (по умолчанию 3)' },
|
|
386
|
+
limitOffsetPct: { type: 'number', description: 'Защитный отступ лимитной заявки в % (по умолчанию 0.1)' },
|
|
387
|
+
sandbox: { type: 'boolean', description: 'Тестовый режим — песочница брокера (по умолчанию false)' },
|
|
388
|
+
},
|
|
389
|
+
required: ['broker'],
|
|
390
|
+
},
|
|
391
|
+
preview(args) {
|
|
392
|
+
const meta = BROKER_META.find((b) => b.id === args.broker);
|
|
393
|
+
const modeLbl = { readonly: 'Только чтение', confirm: 'Подтверждение заявок', auto: 'Автомат' }[args.mode || 'confirm'] || 'Подтверждение заявок';
|
|
394
|
+
const legLbl = { isolate: 'Изолированный карман', smooth: 'Плавный переход', takeover: 'Полное управление' }[args.legacyMode || 'isolate'] || 'Изолированный карман';
|
|
395
|
+
return `Брокер: ${meta?.name || args.broker}${args.name ? ` («${args.name}»)` : ''}${args.accountId ? ` · счёт ${args.accountId}` : ''} · режим «${modeLbl}» · старый портфель: «${legLbl}» · лимит ${args.maxCapitalRub || 100000} ₽`;
|
|
396
|
+
},
|
|
397
|
+
async run(args) {
|
|
398
|
+
const beforeIds = new Set(brokersState().accounts.map((a) => a.id));
|
|
399
|
+
const st = saveBrokerAccount({
|
|
400
|
+
id: args.id,
|
|
401
|
+
broker: args.broker,
|
|
402
|
+
name: args.name,
|
|
403
|
+
token: args.token,
|
|
404
|
+
accountId: args.accountId,
|
|
405
|
+
mode: args.mode || 'confirm',
|
|
406
|
+
legacyMode: args.legacyMode || 'isolate',
|
|
407
|
+
maxCapitalRub: args.maxCapitalRub || 100000,
|
|
408
|
+
maxDailyDrawdownPct: args.maxDailyDrawdownPct ?? 3,
|
|
409
|
+
limitOffsetPct: args.limitOffsetPct ?? 0.1,
|
|
410
|
+
sandbox: Boolean(args.sandbox),
|
|
411
|
+
});
|
|
412
|
+
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
|
+
let syncRes = null;
|
|
414
|
+
if (created && (created.hasToken || ['sber', 'vtb', 'alfa'].includes(created.broker))) {
|
|
415
|
+
try {
|
|
416
|
+
const synced = await syncBrokerAccount(created.id);
|
|
417
|
+
await prepareBrokerOrders(created.id).catch(() => {});
|
|
418
|
+
const accNow = synced.accounts.find((a) => a.id === created.id);
|
|
419
|
+
syncRes = {
|
|
420
|
+
синхронизировано: Boolean(accNow?.lastSync?.ok),
|
|
421
|
+
ошибка_синхронизации: accNow?.lastSync?.error || null,
|
|
422
|
+
баланс_руб: accNow?.lastSync?.totalRub ?? null,
|
|
423
|
+
свободные_рубли: accNow?.lastSync?.cashRub ?? null,
|
|
424
|
+
маржинальная_торговля: accNow?.lastSync?.marginEnabled ?? null,
|
|
425
|
+
подготовлено_заявок: accNow?.pendingBatch?.orders?.length ?? 0,
|
|
426
|
+
};
|
|
427
|
+
} catch (e) {
|
|
428
|
+
syncRes = { синхронизировано: false, ошибка_синхронизации: e.message };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
result: {
|
|
433
|
+
ok: true,
|
|
434
|
+
счёт: created ? { id: created.id, название: created.name, брокер: created.broker, субсчёт: created.accountId, режим: created.mode, лимит_руб: created.maxCapitalRub } : null,
|
|
435
|
+
статус_подключения: syncRes,
|
|
436
|
+
},
|
|
437
|
+
summary: `Подключён брокерский счёт «${created?.name || args.broker}»${syncRes?.баланс_руб !== null && syncRes?.баланс_руб !== undefined ? ` (баланс ${syncRes.баланс_руб} ₽)` : ''}`,
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
list_runs: {
|
|
443
|
+
kind: 'read',
|
|
444
|
+
label: 'Смотрит список проведённых исследований',
|
|
445
|
+
description: 'Посмотреть список завершённых исторических исследований модели.',
|
|
446
|
+
parameters: { type: 'object', properties: { limit: { type: 'number', description: 'сколько последних, по умолчанию 6' } } },
|
|
447
|
+
async run({ limit = 6 }) {
|
|
448
|
+
return { result: recentRuns(Math.min(20, limit)), summary: 'Просмотрел список исследований' };
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
analyze_run: {
|
|
453
|
+
kind: 'read',
|
|
454
|
+
label: 'Разбирает результаты исследования',
|
|
455
|
+
description: 'Подробный разбор завершённого исследования по его номеру: вклад акций, часов входа, сроков удержания и голов внимания.',
|
|
456
|
+
parameters: { type: 'object', properties: { run_id: { type: 'number', description: 'номер исследования' } }, required: ['run_id'] },
|
|
457
|
+
async run({ run_id }) {
|
|
458
|
+
const r = analyzeRun(Number(run_id));
|
|
459
|
+
return { result: r, summary: r.ошибка || `Разобрал исследование №${run_id}` };
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
compare_runs: {
|
|
464
|
+
kind: 'read',
|
|
465
|
+
label: 'Сравнивает два исследования',
|
|
466
|
+
description: 'Сравнить два готовых исследования по их номерам.',
|
|
467
|
+
parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },
|
|
468
|
+
async run({ a, b }) {
|
|
469
|
+
return { result: compareRuns(Number(a), Number(b)), summary: `Сравнил исследования №${a} и №${b}` };
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export const MODES = {
|
|
475
|
+
discuss: { label: 'Консультант', allowed: (t) => t.kind === 'read' },
|
|
476
|
+
advisor: { label: 'Консультант', allowed: () => true, confirm: (t) => t.kind !== 'read' },
|
|
477
|
+
autopilot: { label: 'Консультант', allowed: () => true, confirm: (t) => t.kind !== 'read' },
|
|
478
|
+
review: { label: 'Разбор', allowed: (t) => t.kind === 'read' },
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export function toolSchemas(mode = 'advisor') {
|
|
482
|
+
const m = MODES[mode] || MODES.advisor;
|
|
483
|
+
return Object.entries(TOOLS)
|
|
484
|
+
.filter(([, t]) => m.allowed(t))
|
|
485
|
+
.map(([name, t]) => ({ type: 'function', function: { name, description: t.description, parameters: t.parameters } }));
|
|
486
|
+
}
|