neuroalpha 0.5.10 → 0.5.11
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/package.json +1 -1
- package/public/app.js +15 -7
- package/server/brokers/index.js +49 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -1393,7 +1393,10 @@ async function sendChat(text, { newSession = false, title = null } = {}) {
|
|
|
1393
1393
|
if (speechRec && speechActive) stopVoiceInput(false);
|
|
1394
1394
|
text = (text ?? $('#chat-input').value).trim();
|
|
1395
1395
|
if (!text) return;
|
|
1396
|
-
|
|
1396
|
+
setChatClosed(false);
|
|
1397
|
+
if (newSession && window.innerWidth <= 920) {
|
|
1398
|
+
document.querySelector('section.chat')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1399
|
+
}
|
|
1397
1400
|
// новый запрос — лента снова «прилипает» к низу и показывает вопрос и потоковый ответ
|
|
1398
1401
|
toBottom(true);
|
|
1399
1402
|
try {
|
|
@@ -2407,7 +2410,7 @@ function renderBrokers() {
|
|
|
2407
2410
|
const isPaused = a.autoSchedule === false || a.mode === 'readonly';
|
|
2408
2411
|
const isEditingThis = editingBrokerId === a.id;
|
|
2409
2412
|
const collapsedMetricsHtml = !isOpen && ls?.ok
|
|
2410
|
-
? `<span class="small muted" style="margin-left:6px">Всего: <b style="color:var(--text)">${fmtNum(ls.totalRub, 0)} ₽</b> · Свободно: <b class="${ls.cashRub > 1000 ? 'pos' : ''}">${fmtNum(ls.cashRub, 0)} ₽</b
|
|
2413
|
+
? `<span class="small muted" style="margin-left:6px">Всего: <b style="color:var(--text)">${fmtNum(ls.totalRub, 0)} ₽</b> · Свободно: <b class="${ls.cashRub > 1000 ? 'pos' : ''}">${fmtNum(ls.cashRub, 0)} ₽</b>${ls.marginInfo ? ` · Марж. запас: <b class="pos">${fmtNum(ls.marginInfo.freeMarginRub, 0)} ₽</b>` : ''} · Финрез.: <b class="${cls(totalPnlRub)}">${totalPnlRub >= 0 ? '+' : ''}${fmtNum(totalPnlRub, 0)} ₽</b>${a.executionAudit?.periods ? ` · 30 дн.: <b class="${cls(a.executionAudit.periods.month.netRub)}">${a.executionAudit.periods.month.netRub >= 0 ? '+' : ''}${fmtNum(a.executionAudit.periods.month.netRub, 0)} ₽</b>` : ''}</span>`
|
|
2411
2414
|
: '';
|
|
2412
2415
|
|
|
2413
2416
|
return `<div class="live-card${isOpen ? '' : ' collapsed'}" data-bid="${esc(a.id)}">
|
|
@@ -2435,16 +2438,21 @@ function renderBrokers() {
|
|
|
2435
2438
|
<span class="k">Брокер и номер счёта:</span><span class="v"><b>${esc(BROKER_LABELS[a.broker] || a.broker)}</b> · <code>${esc(a.accountId || ls?.accountId || 'авто')}</code>${ls?.at ? ` <span class="muted">(обновлён ${fmtTs(ls.at)})</span>` : ''}</span>
|
|
2436
2439
|
<span class="k">Режим работы счёта:</span><span class="v"><span class="tag ${a.mode === 'auto' ? 'mid' : a.mode === 'confirm' ? 'ok' : ''}">${esc(BROKER_MODES[a.mode] || a.mode)}</span> · авто-торговля по расписанию: <b>${a.autoSchedule !== false ? '<span class="pos">вкл.</span>' : '<span class="muted">на паузе</span>'}</b></span>
|
|
2437
2440
|
<span class="k">Начальный портфель:</span><span class="v">${esc(LEGACY_MODES[lMode] || lMode)}</span>
|
|
2438
|
-
<span class="k">Маржинальная торговля:</span><span class="v">${ls ? (ls.marginEnabled ?
|
|
2441
|
+
<span class="k">Маржинальная торговля:</span><span class="v">${ls ? (ls.marginEnabled ? `<b class="pos">вкл. (шорт доступен)</b>${ls.marginInfo ? ` · ликвидный портфель <b>${fmtNum(ls.marginInfo.liquidPortfolioRub, 0)} ₽</b> · доступный запас <b class="pos">${fmtNum(ls.marginInfo.freeMarginRub, 0)} ₽</b> · нач. маржа <b>${fmtNum(ls.marginInfo.startingMarginRub, 0)} ₽</b> (мин. ${fmtNum(ls.marginInfo.minimalMarginRub, 0)} ₽)${ls.marginInfo.fundsSufficiencyLevel > 0 ? ` · достаточность <b>${fmtNum(ls.marginInfo.fundsSufficiencyLevel, 2)}×</b>` : ''}${ls.marginInfo.missingFundsRub > 0 ? ` · <b class="neg">нехватка ${fmtNum(ls.marginInfo.missingFundsRub, 0)} ₽</b>` : ''}` : ''}` : '<b>выкл. у брокера (только в рост)</b>') : '<span class="muted">не проверено</span>'}</span>
|
|
2442
|
+
${a.executionAudit?.periods ? (() => {
|
|
2443
|
+
const pr = a.executionAudit.periods;
|
|
2444
|
+
const fmtP = (lbl, x) => `${lbl}: <b class="${cls(x.netRub)}">${x.netRub > 0 ? '+' : ''}${fmtNum(x.netRub, 0)} ₽</b> <span class="muted small">(${x.pct > 0 ? '+' : ''}${fmtNum(x.pct, 2)}%)</span>`;
|
|
2445
|
+
return `<span class="k">Доход / убыток по периодам:</span><span class="v" title="Чистый результат счёта (закрытые сделки после комиссий + дивиденды/купоны; за сегодня — с учётом дневной переоценки портфеля)">${fmtP('сегодня', pr.day)} · ${fmtP('7 дн.', pr.week)} · ${fmtP('30 дн.', pr.month)} · ${fmtP('с начала года', pr.ytd)}</span>`;
|
|
2446
|
+
})() : ''}
|
|
2439
2447
|
<span class="k">Контроль автоследования:</span><span class="v">${ls ? (ls.autoFollowDetected ? '<b class="neg">обнаружено ✕ (торговля заблокирована)</b>' : '<b class="pos">не используется ✓ (счёт допущен)</b>') : '<span class="muted">не проверено</span>'}</span>
|
|
2440
2448
|
<span class="k">Предохранители и настройки:</span><span class="v">${capSpecHtml} · дневной стоп <b>−${fmtNum(a.maxDailyDrawdownPct, 1)}%</b> · отступ лимитки <b>${fmtNum(a.limitOffsetPct, 2)}%</b>${a.broker === 'tbank' ? ` · стоп-заявки у брокера: <b>${a.brokerStops ? `<span class="pos">вкл.</span> (${Object.keys(a.brokerStopBySymbol || {}).length} активн.)` : 'выкл.'}</b> · кэш в LQDT: <b>${a.cashSweep ? '<span class="pos">вкл.</span>' : 'выкл.'}</b>` : ''}${a.baseRunLabel ? ` · эксперимент <b>${esc(a.baseRunLabel)}</b>` : ''}</span>
|
|
2441
2449
|
${a.executionAudit ? `<span class="k">Реальные издержки (сверка):</span><span class="v" title="Сверка фактических сделок счёта (после отключения автоследования) с ориентиром стресс-теста">комиссия <b>${fmtNum(a.executionAudit.commPct, 3)}%</b>${a.executionAudit.slipAllPct !== null ? ` · проскальзывание к сигналу <b>${a.executionAudit.slipAllPct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipAllPct, 3)}%</b>${a.executionAudit.slipTier1Pct !== null ? ` (1-й эш. ${a.executionAudit.slipTier1Pct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipTier1Pct, 3)}%${a.executionAudit.slipTier2Pct !== null ? `, 2–3-й эш. ${a.executionAudit.slipTier2Pct > 0 ? '+' : ''}${fmtNum(a.executionAudit.slipTier2Pct, 3)}%` : ''})` : ''}` : ''} · итого <b class="${a.executionAudit.totalCostPerSidePct <= a.executionAudit.breakEvenTargetPct ? 'pos' : 'neg'}">${fmtNum(a.executionAudit.totalCostPerSidePct, 3)}%/сторона</b> (порог безубыточности стресс-теста ≤ ${fmtNum(a.executionAudit.breakEvenTargetPct, 3)}%, закрыто ${a.executionAudit.closedCount} поз.${a.executionAudit.profitFactorGross !== null ? `, PF до ком. ${fmtNum(a.executionAudit.profitFactorGross, 2)}` : ''})</span>` : ''}
|
|
2442
2450
|
</div>
|
|
2443
2451
|
${ls?.ok ? `<div class="kpis">
|
|
2444
|
-
<div class="kpi"><div class="v">${fmtNum(ls.totalRub, 0)} ₽</div><div class="l">всего на счёте (${ls.positions?.length || 0} поз.)</div
|
|
2445
|
-
<div class="kpi"><div class="v ${ls.cashRub > 1000 ? 'pos' : 'muted'}">${fmtNum(ls.cashRub, 0)} ₽</div><div class="l">свободные рубли для покупок</div
|
|
2446
|
-
<div class="kpi"><div class="v ${cls(totalPnlRub)}">${totalPnlRub >= 0 ? '+' : ''}${fmtNum(totalPnlRub, 0)} ₽</div><div class="l">финрезультат открытых позиций</div></div>
|
|
2447
|
-
<div class="kpi"><div class="v">${fmtNum(strategyRub, 0)} ₽ <span class="muted small">/ ${fmtNum(protectedRub, 0)} ₽</span></div><div class="l">в стратегии / защищено</div></div>
|
|
2452
|
+
<div class="kpi"><div class="v">${fmtNum(ls.totalRub, 0)} ₽</div><div class="l">всего на счёте (${ls.positions?.length || 0} поз.)</div>${ls.marginInfo ? `<div class="sub">марж. запас: ${fmtNum(ls.marginInfo.freeMarginRub, 0)} ₽</div>` : ''}</div>
|
|
2453
|
+
<div class="kpi"><div class="v ${ls.cashRub > 1000 ? 'pos' : 'muted'}">${fmtNum(ls.cashRub, 0)} ₽</div><div class="l">свободные рубли для покупок</div>${ls.marginInfo?.startingMarginRub > 0 ? `<div class="sub">занято маржи: ${fmtNum(ls.marginInfo.startingMarginRub, 0)} ₽</div>` : ''}</div>
|
|
2454
|
+
<div class="kpi"><div class="v ${cls(totalPnlRub)}">${totalPnlRub >= 0 ? '+' : ''}${fmtNum(totalPnlRub, 0)} ₽</div><div class="l">финрезультат открытых позиций</div>${a.executionAudit?.periods ? `<div class="sub">за 30 дн.: <b class="${cls(a.executionAudit.periods.month.netRub)}">${a.executionAudit.periods.month.netRub > 0 ? '+' : ''}${fmtNum(a.executionAudit.periods.month.netRub, 0)} ₽</b></div>` : ''}</div>
|
|
2455
|
+
<div class="kpi"><div class="v">${fmtNum(strategyRub, 0)} ₽ <span class="muted small">/ ${fmtNum(protectedRub, 0)} ₽</span></div><div class="l">в стратегии / защищено</div>${a.executionAudit?.periods ? `<div class="sub">сегодня: <b class="${cls(a.executionAudit.periods.day.netRub)}">${a.executionAudit.periods.day.netRub > 0 ? '+' : ''}${fmtNum(a.executionAudit.periods.day.netRub, 0)} ₽</b></div>` : ''}</div>
|
|
2448
2456
|
</div>` : ''}
|
|
2449
2457
|
${ls && !ls.ok ? `<p class="neg small">Ошибка связи с брокером: ${esc(ls.error)}</p>` : ''}
|
|
2450
2458
|
<div class="brk-subtabs">
|
package/server/brokers/index.js
CHANGED
|
@@ -287,7 +287,7 @@ const executionAuditCache = new Map(); // accountId -> { sig, val }
|
|
|
287
287
|
function computeExecutionAudit(a) {
|
|
288
288
|
const ops = Array.isArray(a.lastHistory?.operations) ? a.lastHistory.operations : [];
|
|
289
289
|
if (!ops.length) return null;
|
|
290
|
-
const sig = `${a.lastHistory?.at || 0}:${ops.length}:${ops[0]?.id || ''}:${ops[ops.length - 1]?.id || ''}`;
|
|
290
|
+
const sig = `${a.lastHistory?.at || 0}:${ops.length}:${ops[0]?.id || ''}:${ops[ops.length - 1]?.id || ''}:${Math.round(Number(a.lastSync?.dailyYieldRub || 0))}:${Math.round(Number(a.lastSync?.totalRub || 0))}`;
|
|
291
291
|
const hit = executionAuditCache.get(a.id);
|
|
292
292
|
if (hit && hit.sig === sig) return hit.val;
|
|
293
293
|
const sorted = [...ops].sort((x, y) => x.date - y.date);
|
|
@@ -320,8 +320,9 @@ function computeExecutionAudit(a) {
|
|
|
320
320
|
const slipTier2Pct = rubW2 > 0 ? round(slipW2 / rubW2, 3) : null;
|
|
321
321
|
const totalCostPerSidePct = slipAllPct !== null ? round(commPct + Math.max(0, slipAllPct), 3) : commPct;
|
|
322
322
|
|
|
323
|
-
// FIFO закрытых позиций (без старого портфеля baselineQty)
|
|
323
|
+
// FIFO закрытых позиций (без старого портфеля baselineQty) + разбивка финансового результата по периодам (день, 7 дн., 30 дн., с начала года)
|
|
324
324
|
const book = new Map();
|
|
325
|
+
const closedEvents = [];
|
|
325
326
|
let winRub = 0, lossRub = 0, closedCount = 0;
|
|
326
327
|
for (const t of trades) {
|
|
327
328
|
const q = t.kind === 'buy' ? t.qty : -t.qty;
|
|
@@ -332,6 +333,7 @@ function computeExecutionAudit(a) {
|
|
|
332
333
|
const take = Math.min(Math.abs(rem), Math.abs(lots[0].qty));
|
|
333
334
|
const pnl = lots[0].qty > 0 ? take * (t.price - lots[0].price) : take * (lots[0].price - t.price);
|
|
334
335
|
closedCount++;
|
|
336
|
+
closedEvents.push({ ts: t.date || 0, pnl });
|
|
335
337
|
if (pnl >= 0) winRub += pnl; else lossRub += -pnl;
|
|
336
338
|
lots[0].qty -= Math.sign(lots[0].qty) * take;
|
|
337
339
|
rem -= Math.sign(rem) * take;
|
|
@@ -344,6 +346,31 @@ function computeExecutionAudit(a) {
|
|
|
344
346
|
const netRub = round(grossRub - feesRub, 0);
|
|
345
347
|
const profitFactorGross = lossRub > 0 ? round(winRub / lossRub, 2) : (winRub > 0 ? 3 : null);
|
|
346
348
|
|
|
349
|
+
const nowTs = Date.now();
|
|
350
|
+
const nowMsk = new Date(nowTs + MSK);
|
|
351
|
+
const todayStartTs = Date.parse(`${nowMsk.toISOString().slice(0, 10)}T00:00:00+03:00`);
|
|
352
|
+
const yearStartTs = Date.parse(`${nowMsk.getUTCFullYear()}-01-01T00:00:00+03:00`);
|
|
353
|
+
const baseCap = Math.max(1, Number(a.lastSync?.totalRub || a.initialTotalRub || a.maxCapitalRub || 100000));
|
|
354
|
+
const calcPeriodPnl = (fromTs, includeUnrealizedToday = false) => {
|
|
355
|
+
const g = closedEvents.filter((c) => c.ts >= fromTs).reduce((s, c) => s + c.pnl, 0);
|
|
356
|
+
const f = algoOps.filter((o) => o.kind === 'fee' && (o.date || 0) >= fromTs).reduce((s, o) => s + Math.abs(o.paymentRub || 0), 0);
|
|
357
|
+
const d = sorted.filter((o) => o.kind === 'div' && (o.date || 0) >= fromTs).reduce((s, o) => s + (Number(o.paymentRub) || 0), 0);
|
|
358
|
+
const cCnt = closedEvents.filter((c) => c.ts >= fromTs).length;
|
|
359
|
+
let total = round(g - f + d, 0);
|
|
360
|
+
if (includeUnrealizedToday && cCnt === 0 && Math.abs(total) < 1 && a.lastSync?.dailyYieldRub) {
|
|
361
|
+
total = round(Number(a.lastSync.dailyYieldRub) || 0, 0);
|
|
362
|
+
} else if (includeUnrealizedToday && cCnt === 0 && Math.abs(total) < 1 && a.lastSync?.dayStartRub > 0 && a.lastSync?.totalRub > 0) {
|
|
363
|
+
total = round(a.lastSync.totalRub - a.lastSync.dayStartRub, 0);
|
|
364
|
+
}
|
|
365
|
+
return { netRub: total, pct: round((total / baseCap) * 100, 2), closed: cCnt };
|
|
366
|
+
};
|
|
367
|
+
const periods = {
|
|
368
|
+
day: calcPeriodPnl(todayStartTs, true),
|
|
369
|
+
week: calcPeriodPnl(nowTs - 7 * DAY, false),
|
|
370
|
+
month: calcPeriodPnl(nowTs - 30 * DAY, false),
|
|
371
|
+
ytd: calcPeriodPnl(yearStartTs, false),
|
|
372
|
+
};
|
|
373
|
+
|
|
347
374
|
const val = {
|
|
348
375
|
fromTs: algoOps[0]?.date || null,
|
|
349
376
|
tradesCount: trades.length,
|
|
@@ -359,6 +386,7 @@ function computeExecutionAudit(a) {
|
|
|
359
386
|
feesRub: round(feesRub, 0),
|
|
360
387
|
netRub,
|
|
361
388
|
profitFactorGross,
|
|
389
|
+
periods,
|
|
362
390
|
};
|
|
363
391
|
executionAuditCache.set(a.id, { sig, val });
|
|
364
392
|
return val;
|
|
@@ -856,10 +884,28 @@ async function syncTbank(acc) {
|
|
|
856
884
|
}
|
|
857
885
|
}
|
|
858
886
|
let marginEnabled = false;
|
|
887
|
+
let marginInfo = null;
|
|
859
888
|
if (accountType === 'ACCOUNT_TYPE_TINKOFF_IIS' || accountType === 'ACCOUNT_TYPE_INVEST_BOX') {
|
|
860
889
|
marginEnabled = false;
|
|
861
890
|
} else if (rMargin?.ok) {
|
|
862
891
|
marginEnabled = true;
|
|
892
|
+
const jMar = await rMargin.json().catch(() => null);
|
|
893
|
+
if (jMar) {
|
|
894
|
+
const liquidPortfolioRub = round(qVal(jMar.liquidPortfolio), 0);
|
|
895
|
+
const startingMarginRub = round(qVal(jMar.startingMargin), 0);
|
|
896
|
+
const minimalMarginRub = round(qVal(jMar.minimalMargin), 0);
|
|
897
|
+
const missingFundsRub = round(qVal(jMar.amountOfMissingFunds), 0);
|
|
898
|
+
const fundsSufficiencyLevel = round(qVal(jMar.fundsSufficiencyLevel), 2);
|
|
899
|
+
const freeMarginRub = round(Math.max(0, liquidPortfolioRub - startingMarginRub), 0);
|
|
900
|
+
marginInfo = {
|
|
901
|
+
liquidPortfolioRub,
|
|
902
|
+
startingMarginRub,
|
|
903
|
+
minimalMarginRub,
|
|
904
|
+
freeMarginRub,
|
|
905
|
+
missingFundsRub,
|
|
906
|
+
fundsSufficiencyLevel,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
863
909
|
} else if (rMargin) {
|
|
864
910
|
const mTxt = await rMargin.text().catch(() => '');
|
|
865
911
|
marginEnabled = !mTxt.includes('30051') && !/margin.*disabled/i.test(mTxt) ? false : false;
|
|
@@ -991,6 +1037,7 @@ async function syncTbank(acc) {
|
|
|
991
1037
|
accountName,
|
|
992
1038
|
accountType,
|
|
993
1039
|
marginEnabled,
|
|
1040
|
+
marginInfo,
|
|
994
1041
|
autoFollowDetected,
|
|
995
1042
|
totalRub: totalRub || cashRub,
|
|
996
1043
|
cashRub: round(cashRub, 2),
|