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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.5.10",
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
- if (document.body.classList.contains('chat-closed')) setChatClosed(false);
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> · Финрез.: <b class="${cls(totalPnlRub)}">${totalPnlRub >= 0 ? '+' : ''}${fmtNum(totalPnlRub, 0)} ₽</b></span>`
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 ? '<b class="pos">вкл. (шорт доступен)</b>' : '<b>выкл. у брокера (только в рост)</b>') : '<span class="muted">не проверено</span>'}</span>
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></div>
2445
- <div class="kpi"><div class="v ${ls.cashRub > 1000 ? 'pos' : 'muted'}">${fmtNum(ls.cashRub, 0)} ₽</div><div class="l">свободные рубли для покупок</div></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">
@@ -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),