neuroalpha 0.3.16 → 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 +0 -1
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -2
- package/package.json +1 -1
- package/public/app.js +35 -116
- package/public/index.html +10 -27
- package/server/brokers/index.js +14 -10
- package/server/params-meta.js +3 -3
- package/server/strategist/index.js +423 -488
- package/server/strategist/tools.js +486 -532
package/README.md
CHANGED
|
@@ -56,7 +56,6 @@ MOEX ISS: ~100 акций MOEXBMI + IMOEX, 10-минутные свечи + жи
|
|
|
56
56
|
- **Новости рынка** — лента новостей и корпоративных раскрытий с ИИ-оценкой тональности и влияния на конкретные тикеры.
|
|
57
57
|
3. **Управление и настройки:**
|
|
58
58
|
- **Эксперименты** *(доступен эксклюзивно в режиме «Хозяин»)* — Walk-Forward проверка ядра и управление витриной.
|
|
59
|
-
- **Издержки и новости** — настройка комиссии брокера, проскальзывания, ставки за шорт и новостного фильтра.
|
|
60
59
|
- **Подписка и уведомления** — управление тарифом, привязка ботов Telegram и MAX для подтверждения заявок в 1 клик.
|
|
61
60
|
- **Модели ИИ** — подключение провайдеров ИИ для Стратега (OpenAI-совместимые, GigaChat с сертификатом НУЦ Минцифры, Ollama).
|
|
62
61
|
|
package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md
CHANGED
|
@@ -29,10 +29,9 @@
|
|
|
29
29
|
- **Брокерские счета** — управление всеми подключёнными счетами (боевыми и тестовыми).
|
|
30
30
|
2. **Аналитика рынка:**
|
|
31
31
|
- **Прогноз сейчас** — живой радар ожиданий по 100 акциям Мосбиржи.
|
|
32
|
-
- **Новости рынка** — свежие новости и раскрытия компаний с оценкой влияния
|
|
32
|
+
- **Новости рынка** — свежие новости и раскрытия компаний с оценкой влияния ИИ и переключателем «Блокировать сделки против важных свежих новостей».
|
|
33
33
|
3. **Управление и настройки:**
|
|
34
34
|
- **Эксперименты** *(виден и доступен эксклюзивно в режиме «Хозяин»)* — исторические Walk-Forward исследования модели.
|
|
35
|
-
- **Издержки и новости** — настройка брокерской комиссии, проскальзывания, ставки за шорт и новостного фильтра.
|
|
36
35
|
- **Подписка и уведомления** — выбор тарифа и привязка ботов **Telegram / MAX**.
|
|
37
36
|
- **Модели ИИ** — подключение и выбор языковых моделей для Стратега.
|
|
38
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -165,8 +165,7 @@ function updateTabBadges() {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function showMainSec(sec) {
|
|
168
|
-
if (sec === 'now') sec = 'brokers';
|
|
169
|
-
if (sec === 'settings' || sec === 'g1') sec = 'g0';
|
|
168
|
+
if (sec === 'now' || sec === 'settings' || sec === 'g0' || sec === 'g1') sec = 'brokers';
|
|
170
169
|
if (sec === 'overview' && !isOwnerUser()) sec = 'brokers';
|
|
171
170
|
document.querySelectorAll('#main-nav [data-sec]').forEach((b) => b.classList.toggle('active', b.dataset.sec === sec));
|
|
172
171
|
document.querySelectorAll('#main-workspace .ssec').forEach((el) => { el.hidden = el.dataset.sec !== sec; });
|
|
@@ -391,35 +390,8 @@ function syncUiAvailability() {
|
|
|
391
390
|
}
|
|
392
391
|
|
|
393
392
|
function syncSettingsDependencies() {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const s = state.settings;
|
|
397
|
-
const autoOn = s.modelAutoAdapt !== false;
|
|
398
|
-
const dir = s.modelDirection || 'long';
|
|
399
|
-
|
|
400
|
-
const setRowEnabled = (key, enabled, whyDisabled = '') => {
|
|
401
|
-
const row = form.querySelector(`.srow[data-key="${key}"]`);
|
|
402
|
-
if (!row) return;
|
|
403
|
-
row.style.opacity = enabled ? '1' : '0.45';
|
|
404
|
-
const ctrl = form.elements[key];
|
|
405
|
-
if (ctrl) {
|
|
406
|
-
ctrl.disabled = !enabled;
|
|
407
|
-
ctrl.title = enabled ? '' : whyDisabled;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
setRowEnabled('modelHorizon', !autoOn, 'Управляется автоматически при включённом «Самообучении» (мульти-горизонт 1,5–3,5 ч)');
|
|
412
|
-
setRowEnabled('modelMinEdge', !autoOn, 'Управляется автоматически при включённом «Самообучении»');
|
|
413
|
-
setRowEnabled('modelExitRatio', !autoOn, 'Управляется автоматически при включённом «Самообучении»');
|
|
414
|
-
setRowEnabled('modelShortFeePct', dir !== 'long', 'Действует только когда включены короткие продажи («В рост и в падение (с шортом)»)');
|
|
415
|
-
setRowEnabled('modelMarketFilter', true, '');
|
|
416
|
-
|
|
417
|
-
const doneCount = (state.runs || []).filter((r) => r.status !== 'running').length;
|
|
418
|
-
if ($('#btn-clean')) $('#btn-clean').disabled = doneCount < 1;
|
|
419
|
-
if ($('#btn-emb-auto')) {
|
|
420
|
-
const isAutoEmb = Number(s.modelEmbedDim ?? 0) === 0;
|
|
421
|
-
$('#btn-emb-auto').classList.toggle('primary', isAutoEmb);
|
|
422
|
-
$('#btn-emb-auto').textContent = isAutoEmb ? '✓ Авто (0)' : '⚡ Авто (0)';
|
|
393
|
+
if ($('#chk-news-filter') && state.settings) {
|
|
394
|
+
$('#chk-news-filter').checked = Boolean(state.settings.newsFilter);
|
|
423
395
|
}
|
|
424
396
|
}
|
|
425
397
|
function syncShowcaseBtn() {
|
|
@@ -962,7 +934,7 @@ async function loadForecast(force, silent = false) {
|
|
|
962
934
|
};
|
|
963
935
|
const row = (r) => `<tr>
|
|
964
936
|
<td><b>${r.symbol}</b> <span class="muted small">${esc(r.name)}</span>${rankBadge(r)}${dot(r.news)}</td>
|
|
965
|
-
<td class="num" style="white-space:nowrap">${r.price ? `${fmtNum(r.price, 2)} ₽` : '—'}</td>
|
|
937
|
+
<td class="num" style="white-space:nowrap">${r.price ? `${fmtNum(r.price, r.price < 1 ? 5 : r.price < 10 ? 3 : 2)} ₽` : '—'}</td>
|
|
966
938
|
<td class="num ${cls(r.expectedPct)}" style="white-space:nowrap"><b>${fmtPct(r.expectedPct, 2)}</b></td>
|
|
967
939
|
<td class="num" style="white-space:nowrap">${arrowOf(r)}</td>
|
|
968
940
|
<td style="text-align:center;white-space:nowrap">${sigBadge(r)}</td>
|
|
@@ -1010,43 +982,14 @@ async function loadNews() {
|
|
|
1010
982
|
}
|
|
1011
983
|
|
|
1012
984
|
// ======================================================================
|
|
1013
|
-
//
|
|
985
|
+
// НАСТРОЙКА ФИЛЬТРА НОВОСТЕЙ
|
|
1014
986
|
// ======================================================================
|
|
1015
|
-
|
|
1016
|
-
['Издержки брокера и новости', ['commissionPct', 'slippagePct', 'modelShortFeePct', 'newsFilter']],
|
|
1017
|
-
];
|
|
1018
|
-
function buildForm() {
|
|
1019
|
-
const field = (p) => {
|
|
1020
|
-
let input;
|
|
1021
|
-
if (p.type === 'select') input = `<select name="${p.key}" class="sctrl">${p.options.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join('')}</select>`;
|
|
1022
|
-
else if (p.type === 'checkbox') input = `<input type="checkbox" name="${p.key}" class="switch">`;
|
|
1023
|
-
else input = `<input type="number" step="${p.step ?? 'any'}" name="${p.key}" min="${p.min}" max="${p.max}" class="sctrl">`;
|
|
1024
|
-
return `<div class="srow" data-key="${p.key}"><div class="stxt"><div class="slbl">${esc(p.label)}<button type="button" class="mark" data-def="${p.key}" hidden title="Отличается от умолчания — нажмите, чтобы вернуть">изменено</button></div><div class="hint">${esc(p.hint)}</div></div>${input}</div>`;
|
|
1025
|
-
};
|
|
1026
|
-
const shown = state.config.params.filter((p) => !p.hidden);
|
|
1027
|
-
const curSec = document.querySelector('#main-nav [data-sec].active')?.dataset.sec || 'brokers';
|
|
1028
|
-
$('#params').innerHTML = GROUPS.map(([name, keys], i) => {
|
|
1029
|
-
const items = keys.map((k) => shown.find((p) => p.key === k)).filter(Boolean);
|
|
1030
|
-
const secKey = `g${i}`;
|
|
1031
|
-
return items.length ? `<section class="ssec" data-sec="${secKey}"${curSec === secKey ? '' : ' hidden'}><div class="stitle">${name}</div><div class="scard">${items.map(field).join('')}</div></section>` : '';
|
|
1032
|
-
}).join('');
|
|
1033
|
-
}
|
|
987
|
+
function buildForm() {}
|
|
1034
988
|
function fillForm(s) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
else el.value = s[el.name];
|
|
1039
|
-
}
|
|
1040
|
-
markDefaults(s);
|
|
1041
|
-
}
|
|
1042
|
-
// Метка «изменено» у параметров, отличающихся от умолчания
|
|
1043
|
-
function markDefaults(s) {
|
|
1044
|
-
const d = state.config?.defaults || {};
|
|
1045
|
-
$('#params').querySelectorAll('[data-def]').forEach((b) => {
|
|
1046
|
-
b.hidden = JSON.stringify(s[b.dataset.def]) === JSON.stringify(d[b.dataset.def]);
|
|
1047
|
-
});
|
|
989
|
+
if ($('#chk-news-filter') && s) {
|
|
990
|
+
$('#chk-news-filter').checked = Boolean(s.newsFilter);
|
|
991
|
+
}
|
|
1048
992
|
syncApplyBtn();
|
|
1049
|
-
syncSettingsDependencies();
|
|
1050
993
|
updateTabBadges();
|
|
1051
994
|
}
|
|
1052
995
|
const saveSettings = async (body) => {
|
|
@@ -1054,28 +997,30 @@ const saveSettings = async (body) => {
|
|
|
1054
997
|
const r = await api('/api/settings', { method: 'PUT', body });
|
|
1055
998
|
if (r.errors?.length) toast(r.errors.join('; '), 'error');
|
|
1056
999
|
state.settings = r.settings;
|
|
1057
|
-
|
|
1000
|
+
fillForm(state.settings);
|
|
1058
1001
|
} catch (e) { toast(e.message, 'error'); }
|
|
1059
1002
|
};
|
|
1060
1003
|
const saveSetting = (k, v) => saveSettings({ [k]: v });
|
|
1061
|
-
$('#
|
|
1062
|
-
|
|
1063
|
-
$('#params').addEventListener('click', async (e) => {
|
|
1064
|
-
const b = e.target.closest('[data-def]');
|
|
1065
|
-
if (!b) return;
|
|
1066
|
-
await saveSetting(b.dataset.def, state.config.defaults[b.dataset.def]);
|
|
1067
|
-
fillForm(state.settings);
|
|
1004
|
+
$('#chk-news-filter')?.addEventListener('change', (e) => {
|
|
1005
|
+
saveSetting('newsFilter', e.target.checked);
|
|
1068
1006
|
});
|
|
1069
1007
|
|
|
1070
1008
|
// ======================================================================
|
|
1071
1009
|
// ЧАТ СО СТРАТЕГОМ
|
|
1072
1010
|
// ======================================================================
|
|
1073
1011
|
const TOOL_LABELS = {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1012
|
+
get_brokers_status: 'Смотрит брокерские счета и портфель',
|
|
1013
|
+
get_settings: 'Смотрит настройки издержек',
|
|
1014
|
+
list_runs: 'Смотрит список исследований',
|
|
1015
|
+
analyze_run: 'Разбирает исследование',
|
|
1016
|
+
compare_runs: 'Сравнивает исследования',
|
|
1017
|
+
model_forecast: 'Смотрит живой прогноз',
|
|
1018
|
+
get_news: 'Смотрит свежие новости',
|
|
1019
|
+
market_neighbors: 'Смотрит соседей на карте',
|
|
1020
|
+
set_settings: 'Изменение настроек',
|
|
1021
|
+
broker_onboarding_guide: 'Смотрит инструкцию по брокеру',
|
|
1022
|
+
discover_broker_accounts: 'Ищет субсчета по токену',
|
|
1023
|
+
connect_broker_account: 'Подключение брокерского счёта',
|
|
1079
1024
|
};
|
|
1080
1025
|
const STATUS = { pending: 'ждёт вашего решения', approved: 'подтверждено', running: 'выполняется…', done: 'готово', error: 'не получилось', rejected: 'отклонено' };
|
|
1081
1026
|
const feed = $('#feed');
|
|
@@ -1252,34 +1197,28 @@ function renderChatState() {
|
|
|
1252
1197
|
const s = state.chat.state;
|
|
1253
1198
|
if (!s) return;
|
|
1254
1199
|
const noTools = s.tools === false;
|
|
1255
|
-
const curMode = noTools ? 'discuss' : (s.mode in MODE_META ? s.mode : 'advisor');
|
|
1256
|
-
const mm = MODE_META[curMode] || MODE_META.advisor;
|
|
1257
|
-
if ($('#chat-mode-ico')) $('#chat-mode-ico').innerHTML = mm.svg;
|
|
1258
|
-
if ($('#chat-mode-lbl')) $('#chat-mode-lbl').textContent = mm.label;
|
|
1259
|
-
if ($('#chat-mode-btn')) $('#chat-mode-btn').title = noTools ? 'Выбранная модель не умеет пользоваться инструментами — Стратег только беседует' : MODE_TITLE;
|
|
1260
|
-
document.querySelectorAll('#mode-menu [data-mode-val]').forEach((b) => {
|
|
1261
|
-
const v = b.dataset.modeVal;
|
|
1262
|
-
b.disabled = noTools && v !== 'discuss';
|
|
1263
|
-
b.classList.toggle('active', v === curMode);
|
|
1264
|
-
});
|
|
1265
1200
|
$('#chat-stop').hidden = !s.busy && !s.waiting;
|
|
1266
1201
|
syncActionBtn();
|
|
1267
1202
|
let st = '';
|
|
1268
|
-
if (!s.llm) st = 'ИИ не подключён: выберите модель и ключ (
|
|
1269
|
-
else if (s.waiting) st = 'Стратег ждёт вашего
|
|
1203
|
+
if (!s.llm) st = 'ИИ не подключён: выберите модель и ключ (Модели ИИ)';
|
|
1204
|
+
else if (s.waiting) st = 'Стратег ждёт вашего подтверждения (или напишите ответ)';
|
|
1270
1205
|
else if (s.busy) {
|
|
1271
1206
|
const m = [...state.chat.msgs.values()].reverse().find((x) => x.tool_calls && Object.values(x.data?.tools || {}).some((t) => t.status === 'running'));
|
|
1272
1207
|
const c = m?.tool_calls.find((x) => m.data.tools[x.id]?.status === 'running');
|
|
1273
1208
|
st = c ? `${TOOL_LABELS[c.function.name] || 'Работает'}…` : 'Думает…';
|
|
1274
|
-
|
|
1275
|
-
} else if (noTools) st = 'У этой модели нет инструментов: Стратег только беседует';
|
|
1209
|
+
} else if (noTools) st = 'У этой модели нет инструментов: Стратег отвечает по контексту';
|
|
1276
1210
|
$('#chat-status').textContent = st;
|
|
1277
1211
|
for (const m of state.chat.msgs.values()) if (m.tool_calls) upsertMsg(m);
|
|
1278
1212
|
renderSuggest();
|
|
1279
1213
|
syncUiAvailability();
|
|
1280
1214
|
}
|
|
1281
1215
|
|
|
1282
|
-
const SUGGEST = [
|
|
1216
|
+
const SUGGEST = [
|
|
1217
|
+
'Помоги подключить брокерский счёт',
|
|
1218
|
+
'Что стратегия ждёт в ближайшие часы?',
|
|
1219
|
+
'Разбери мой портфель и текущие заявки',
|
|
1220
|
+
'Какие сейчас важные новости по рынку?',
|
|
1221
|
+
];
|
|
1283
1222
|
function renderSuggest() {
|
|
1284
1223
|
const has = [...state.chat.msgs.values()].some((m) => m.kind === 'user');
|
|
1285
1224
|
$('#suggest').innerHTML = has || state.chat.state?.busy ? '' : SUGGEST.map((s) => `<button>${esc(s)}</button>`).join('');
|
|
@@ -1312,31 +1251,12 @@ function closeModeMenu() {
|
|
|
1312
1251
|
if (m) m.hidden = true;
|
|
1313
1252
|
}
|
|
1314
1253
|
|
|
1315
|
-
$('#chat-mode-btn')?.addEventListener('click', (e) => {
|
|
1316
|
-
e.stopPropagation();
|
|
1317
|
-
closeModelMenu();
|
|
1318
|
-
const m = $('#mode-menu');
|
|
1319
|
-
if (m) m.hidden = !m.hidden;
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
$('#mode-menu')?.addEventListener('click', async (e) => {
|
|
1323
|
-
const b = e.target.closest('button[data-mode-val]');
|
|
1324
|
-
if (!b || b.disabled) return;
|
|
1325
|
-
closeModeMenu();
|
|
1326
|
-
try {
|
|
1327
|
-
state.chat.state = await post('/api/chat/mode', { mode: b.dataset.modeVal });
|
|
1328
|
-
renderChatState();
|
|
1329
|
-
} catch (err) { toast(err.message, 'error'); }
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
1254
|
async function sendChat(text) {
|
|
1333
1255
|
if (speechRec && speechActive) stopVoiceInput(false);
|
|
1334
1256
|
text = (text ?? $('#chat-input').value).trim();
|
|
1335
1257
|
if (!text) return;
|
|
1336
1258
|
try {
|
|
1337
|
-
|
|
1338
|
-
const mode = s?.tools === false ? 'discuss' : (s?.mode || 'advisor');
|
|
1339
|
-
await post('/api/chat/send', { text, mode });
|
|
1259
|
+
await post('/api/chat/send', { text, mode: 'advisor' });
|
|
1340
1260
|
$('#chat-input').value = '';
|
|
1341
1261
|
resizeChatInput();
|
|
1342
1262
|
toBottom();
|
|
@@ -2815,7 +2735,6 @@ function connectWs() {
|
|
|
2815
2735
|
state.settings = m.settings;
|
|
2816
2736
|
fillForm(m.settings);
|
|
2817
2737
|
if (m.source === 'ai') {
|
|
2818
|
-
for (const k of Object.keys(m.changed || {})) { const l = $('#params').elements[k]?.closest('.srow, label'); if (l) { l.classList.remove('flash'); void l.offsetWidth; l.classList.add('flash'); } }
|
|
2819
2738
|
toast(`Стратег изменил настройки: ${Object.entries(m.changed).map(([k, v]) => `${labelOf(k)} ${v.было} → ${v.стало}`).join('; ')}`);
|
|
2820
2739
|
}
|
|
2821
2740
|
break;
|
|
@@ -3204,7 +3123,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
3204
3123
|
}
|
|
3205
3124
|
if (urlParams.get('run') && isOwnerUser()) {
|
|
3206
3125
|
showMainSec('overview');
|
|
3207
|
-
} else if (savedNav.mainSec && ['brokers', 'forecast', 'news', 'overview', '
|
|
3126
|
+
} else if (savedNav.mainSec && ['brokers', 'forecast', 'news', 'overview', 'cloud', 'llm'].includes(savedNav.mainSec)) {
|
|
3208
3127
|
showMainSec(savedNav.mainSec);
|
|
3209
3128
|
} else {
|
|
3210
3129
|
showMainSec('brokers');
|
package/public/index.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<b>Стратег</b>
|
|
31
31
|
<span id="chat-status" class="muted small"></span>
|
|
32
32
|
<span class="grow"></span>
|
|
33
|
-
<button id="chat-new" class="icon small" title="Начать новый
|
|
33
|
+
<button id="chat-new" class="icon small" title="Начать новый диалог со Стратегом">+ Новый диалог</button>
|
|
34
34
|
<button id="chat-hide" class="icon small" title="Свернуть панель чата со Стратегом">◀</button>
|
|
35
35
|
</div>
|
|
36
36
|
<div class="feed-wrap">
|
|
@@ -40,30 +40,10 @@
|
|
|
40
40
|
<div class="composer">
|
|
41
41
|
<div id="suggest" class="suggest"></div>
|
|
42
42
|
<div class="prompt-box" id="prompt-box">
|
|
43
|
-
<textarea id="chat-input" rows="2" placeholder="
|
|
43
|
+
<textarea id="chat-input" rows="2" placeholder="Спросите Стратега о прогнозе, портфеле или настройках счёта…"></textarea>
|
|
44
44
|
<div class="prompt-bar">
|
|
45
45
|
<div class="prompt-left">
|
|
46
|
-
<
|
|
47
|
-
<button type="button" id="chat-mode-btn" class="ghost-select" title="Режим работы Стратега">
|
|
48
|
-
<span id="chat-mode-ico" class="g-ico"></span>
|
|
49
|
-
<span id="chat-mode-lbl">Авто</span>
|
|
50
|
-
<svg class="caret-svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5L6 7.5L9 4.5"/></svg>
|
|
51
|
-
</button>
|
|
52
|
-
<div id="mode-menu" class="pop-menu mode-menu" hidden>
|
|
53
|
-
<button type="button" class="model-opt" data-mode-val="autopilot">
|
|
54
|
-
<span class="m-name">⚡ Авто</span>
|
|
55
|
-
<span class="muted small">сам ставит эксперименты</span>
|
|
56
|
-
</button>
|
|
57
|
-
<button type="button" class="model-opt" data-mode-val="advisor">
|
|
58
|
-
<span class="m-name">✋ С согласия</span>
|
|
59
|
-
<span class="muted small">спрашивает перед запуском</span>
|
|
60
|
-
</button>
|
|
61
|
-
<button type="button" class="model-opt" data-mode-val="discuss">
|
|
62
|
-
<span class="m-name">💬 Обсуждение</span>
|
|
63
|
-
<span class="muted small">только анализ и беседа</span>
|
|
64
|
-
</button>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
46
|
+
<span class="muted small" style="padding:0 6px;user-select:none">🤖 ИИ-консультант и помощник по счетам</span>
|
|
67
47
|
</div>
|
|
68
48
|
<div class="prompt-right">
|
|
69
49
|
<div class="model-picker" id="model-picker">
|
|
@@ -109,7 +89,6 @@
|
|
|
109
89
|
<button type="button" data-sec="news" id="nnav-news">Новости рынка</button>
|
|
110
90
|
<div class="ssep"></div>
|
|
111
91
|
<button type="button" data-sec="overview" id="nnav-overview" hidden>Эксперименты</button>
|
|
112
|
-
<button type="button" data-sec="g0" id="snav-costs">Издержки и новости</button>
|
|
113
92
|
<button type="button" data-sec="cloud" id="snav-cloud">Подписка и уведомления</button>
|
|
114
93
|
<button type="button" data-sec="llm" id="snav-llm">Модели ИИ</button>
|
|
115
94
|
</aside>
|
|
@@ -199,7 +178,13 @@
|
|
|
199
178
|
</section>
|
|
200
179
|
|
|
201
180
|
<section class="ssec" data-sec="news" id="news-box" hidden>
|
|
202
|
-
<div class="stitle"
|
|
181
|
+
<div class="stitle bar">
|
|
182
|
+
<span>Новости и раскрытия компаний</span>
|
|
183
|
+
<label class="row-gap small" style="cursor:pointer;font-weight:500;color:var(--text)" title="Не покупать бумагу на сильном негативе и не шортить на сильном позитиве за последние 6 часов">
|
|
184
|
+
<span>Блокировать сделки против важных свежих новостей</span>
|
|
185
|
+
<input id="chk-news-filter" type="checkbox" class="switch">
|
|
186
|
+
</label>
|
|
187
|
+
</div>
|
|
203
188
|
<div class="scard spadded">
|
|
204
189
|
<div id="news-list" class="muted">Свежих размеченных новостей пока нет.</div>
|
|
205
190
|
</div>
|
|
@@ -255,8 +240,6 @@
|
|
|
255
240
|
</div>
|
|
256
241
|
</section>
|
|
257
242
|
|
|
258
|
-
<form id="params"></form>
|
|
259
|
-
|
|
260
243
|
<section class="ssec" data-sec="cloud" id="cloud-box" hidden>
|
|
261
244
|
<div class="stitle">Подписка, лицензия и уведомления (Telegram / MAX)</div>
|
|
262
245
|
<div class="scard spadded" id="cloud-pane"></div>
|
package/server/brokers/index.js
CHANGED
|
@@ -382,7 +382,7 @@ async function resolveTbankMetaMap(base, headers, keys = []) {
|
|
|
382
382
|
async function ensureTbankShortableMap(acc) {
|
|
383
383
|
if (!acc || acc.broker !== 'tbank' || !acc.token) return null;
|
|
384
384
|
const cached = kvGet('tbankShortableMap');
|
|
385
|
-
if (cached?.v ===
|
|
385
|
+
if (cached?.v === 4 && cached?.map && Date.now() - cached.at < 12 * 3600000) {
|
|
386
386
|
if (cached.uidMap) {
|
|
387
387
|
for (const [sym, uid] of Object.entries(cached.uidMap)) tbankUidCache.set(sym, uid);
|
|
388
388
|
}
|
|
@@ -413,9 +413,7 @@ async function ensureTbankShortableMap(acc) {
|
|
|
413
413
|
}
|
|
414
414
|
const minStep = qVal(inst.minPriceIncrement);
|
|
415
415
|
if (minStep > 0) {
|
|
416
|
-
const
|
|
417
|
-
const dotIdx = stepStr.indexOf('.');
|
|
418
|
-
const decimals = dotIdx >= 0 ? Math.min(6, stepStr.length - dotIdx - 1) : 0;
|
|
416
|
+
const decimals = Math.max(0, Math.min(6, Math.ceil(-Math.log10(minStep) - 1e-9)));
|
|
419
417
|
specMap[inst.ticker] = {
|
|
420
418
|
lotSize: Math.max(1, Number(inst.lot) || 1),
|
|
421
419
|
minStep,
|
|
@@ -424,7 +422,7 @@ async function ensureTbankShortableMap(acc) {
|
|
|
424
422
|
}
|
|
425
423
|
}
|
|
426
424
|
}
|
|
427
|
-
kvSet('tbankShortableMap', { v:
|
|
425
|
+
kvSet('tbankShortableMap', { v: 4, at: Date.now(), map, uidMap, specMap });
|
|
428
426
|
return map;
|
|
429
427
|
} catch {
|
|
430
428
|
return cached?.map || null;
|
|
@@ -1171,10 +1169,10 @@ export async function prepareBrokerOrders(id) {
|
|
|
1171
1169
|
const isOwnerPlan = Boolean(plan.isOwner || plan.id === 'owner' || plan.maxAnnualReturnPct === 0);
|
|
1172
1170
|
let slotRub;
|
|
1173
1171
|
if (isOwnerPlan && autoAdapt) {
|
|
1174
|
-
const maxSlotWeight =
|
|
1172
|
+
const maxSlotWeight = Math.min(0.45, Math.max(1 / K, 1.8 / K));
|
|
1175
1173
|
slotRub = budget * maxSlotWeight;
|
|
1176
1174
|
} else if (vipBoost && autoAdapt) {
|
|
1177
|
-
const maxSlotWeight =
|
|
1175
|
+
const maxSlotWeight = Math.min(0.40, Math.max(1 / K, 1.4 / K));
|
|
1178
1176
|
slotRub = budget * maxSlotWeight;
|
|
1179
1177
|
} else if (plan.id === 'pro') {
|
|
1180
1178
|
slotRub = budget * Math.min(0.28, 1.15 / Math.max(2, slots));
|
|
@@ -1314,9 +1312,9 @@ export async function prepareBrokerOrders(id) {
|
|
|
1314
1312
|
const totalNew = newBuys.length + newShorts.length;
|
|
1315
1313
|
const activeTotal = Math.max(2, activeLongs + activeShorts + totalNew);
|
|
1316
1314
|
const dynSlotRub = (isOwnerPlan && autoAdapt)
|
|
1317
|
-
? budget * Math.min(
|
|
1315
|
+
? budget * Math.min(0.45, Math.max(slotRub / Math.max(1, budget), 1.8 / Math.min(slots, activeTotal + 1)))
|
|
1318
1316
|
: (vipBoost && autoAdapt)
|
|
1319
|
-
? budget * Math.min(0.
|
|
1317
|
+
? budget * Math.min(0.40, Math.max(slotRub / Math.max(1, budget), 1.5 / Math.min(slots, activeTotal + 1)))
|
|
1320
1318
|
: slotRub;
|
|
1321
1319
|
|
|
1322
1320
|
// В режиме «Плавный переход» (smooth):
|
|
@@ -1371,12 +1369,18 @@ export async function prepareBrokerOrders(id) {
|
|
|
1371
1369
|
}
|
|
1372
1370
|
let remWeightSum = combinedNew.reduce((s, x) => s + Math.max(0.01, Math.abs(x.r.expectedPct || 0)), 0);
|
|
1373
1371
|
|
|
1372
|
+
// Жёсткий предохранитель концентрации на одну бумагу в реальном стакане брокера:
|
|
1373
|
+
// максимум до 45% счёта на один лонг и максимум до 35% счёта на один шорт (включая множитель силы сигнала convOf)
|
|
1374
|
+
const maxLongPerStockRub = budget * 0.45;
|
|
1375
|
+
const maxShortPerStockRub = budget * 0.35;
|
|
1376
|
+
|
|
1374
1377
|
for (const { r, side } of combinedNew) {
|
|
1375
1378
|
const wCur = Math.max(0.01, Math.abs(r.expectedPct || 0));
|
|
1376
1379
|
const propShare = autoAdapt && remWeightSum > 0 ? availRub * (wCur / remWeightSum) : availRub / Math.max(1, remNew);
|
|
1377
1380
|
remWeightSum = Math.max(0, remWeightSum - wCur);
|
|
1378
1381
|
remNew--;
|
|
1379
|
-
const
|
|
1382
|
+
const sideCapRub = side === 'short' ? maxShortPerStockRub : maxLongPerStockRub;
|
|
1383
|
+
const rawSum = Math.min(sideCapRub, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r));
|
|
1380
1384
|
const sum = Math.min(rawSum, propShare, availRub);
|
|
1381
1385
|
const refPx = r.price || specs[r.symbol]?.lastPrice || 0;
|
|
1382
1386
|
const ord = calcLotOrder(r.symbol, sum, refPx, side, acc.limitOffsetPct, specs);
|
package/server/params-meta.js
CHANGED
|
@@ -18,9 +18,9 @@ export const PARAMS = [
|
|
|
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
20
|
{ key: 'capital', label: 'Стартовая сумма, ₽', hint: 'С какой суммой начинает счёт.', min: 10000, max: 1e9, hidden: true },
|
|
21
|
-
{ key: 'commissionPct', label: 'Комиссия брокера и биржи, % за сделку', hint: 'Сколько берут брокер и биржа за покупку и отдельно за продажу.', min: 0, max: 1, ai: false },
|
|
22
|
-
{ key: 'slippagePct', label: 'Проскальзывание при исполнении, %', hint: 'На сколько реальная цена сделки в стакане обычно хуже ожидаемой.', min: 0, max: 1, ai: false },
|
|
23
|
-
{ key: 'modelShortFeePct', label: 'Плата брокеру за заём акций (шорт), % годовых', hint: 'Ставка брокера за удержание коротких позиций (начисляется пропорционально времени удержания сделки).', min: 0, max: 100 },
|
|
21
|
+
{ key: 'commissionPct', label: 'Комиссия брокера и биржи, % за сделку', hint: 'Сколько берут брокер и биржа за покупку и отдельно за продажу.', min: 0, max: 1, ai: false, hidden: true, fixed: true },
|
|
22
|
+
{ key: 'slippagePct', label: 'Проскальзывание при исполнении, %', hint: 'На сколько реальная цена сделки в стакане обычно хуже ожидаемой.', min: 0, max: 1, ai: false, hidden: true, fixed: true },
|
|
23
|
+
{ key: 'modelShortFeePct', label: 'Плата брокеру за заём акций (шорт), % годовых', hint: 'Ставка брокера за удержание коротких позиций (начисляется пропорционально времени удержания сделки).', min: 0, max: 100, hidden: true, fixed: true },
|
|
24
24
|
{ key: 'autopilotBudget', label: 'Лимит исследований автопилота за задание', hint: 'Сколько экспериментов Стратег может провести подряд за одно задание.', min: 1, max: 30, step: 1, int: true, ai: false, hidden: true },
|
|
25
25
|
{ key: 'paperDailyReview', label: 'ИИ разбирает каждый торговый день', hint: 'После окончания торгового дня Стратег сам разбирает сделки и пишет сводку в чат.', type: 'checkbox', hidden: true },
|
|
26
26
|
{ key: 'newsEnabled', label: 'Следить за новостями', hint: 'Раз в 10 минут собирает новости и раскрытия компаний.', type: 'checkbox', hidden: true, fixed: true },
|