neuroalpha 0.5.10 → 0.5.12

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.12",
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 {
@@ -2013,6 +2016,12 @@ function toggleBrokerHistFilter(id, flt) {
2013
2016
  }
2014
2017
  let openBrokerId = loadSavedNav().openBrokerId ?? null;
2015
2018
  let openBrokerCfgId = loadSavedNav().openBrokerCfgId ?? null;
2019
+ let hideBrokerSpecsById = new Set(Array.isArray(loadSavedNav().hideBrokerSpecs) ? loadSavedNav().hideBrokerSpecs : []);
2020
+ function toggleBrokerSpecs(id) {
2021
+ if (hideBrokerSpecsById.has(id)) hideBrokerSpecsById.delete(id);
2022
+ else hideBrokerSpecsById.add(id);
2023
+ saveNavPatch({ hideBrokerSpecs: [...hideBrokerSpecsById] });
2024
+ }
2016
2025
  let editingBrokerId = null;
2017
2026
  let brokerPollBusy = false;
2018
2027
  let lastBrokerPollTs = 0;
@@ -2406,8 +2415,9 @@ function renderBrokers() {
2406
2415
 
2407
2416
  const isPaused = a.autoSchedule === false || a.mode === 'readonly';
2408
2417
  const isEditingThis = editingBrokerId === a.id;
2418
+ const isSpecsHidden = hideBrokerSpecsById.has(a.id);
2409
2419
  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>`
2420
+ ? `<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
2421
  : '';
2412
2422
 
2413
2423
  return `<div class="live-card${isOpen ? '' : ' collapsed'}" data-bid="${esc(a.id)}">
@@ -2424,6 +2434,7 @@ function renderBrokers() {
2424
2434
  <div class="row-gap" style="align-items:center">
2425
2435
  <button type="button" class="brk-toggle ${isPaused ? 'on' : ''}" data-btrade="pause" data-bid="${esc(a.id)}" title="${isPaused ? 'Авто-торговля на паузе. Нажмите, чтобы возобновить торговлю по расписанию' : 'Нажмите, чтобы поставить авто-торговлю на этом счёте на паузу'}">${isPaused ? '▶ Запустить' : '⏸ Пауза'}</button>
2426
2436
  ${isOpen && ls?.positions?.length ? `<button type="button" class="brk-toggle ${lMode === 'isolate' && Object.keys(baseMap).length ? 'on' : ''}" data-btrade="freeze" data-bid="${esc(a.id)}" title="${lMode === 'isolate' && Object.keys(baseMap).length ? 'Жёсткая защита включена (режим «Изолированный карман»): старые бумаги на счёте неприкосновенны. Нажмите, чтобы переключить в «Плавный переход».' : 'Нажмите, чтобы зафиксировать все текущие бумаги на счёте как неприкосновенные (включить режим «Изолированный карман»).'}">${lMode === 'isolate' && Object.keys(baseMap).length ? '🔒 Защита бумаг: ВКЛ' : '🔓 Защита бумаг: ВЫКЛ'}</button>` : ''}
2437
+ ${isOpen ? `<button type="button" class="brk-icon-btn${!isSpecsHidden ? ' primary' : ''}" data-btrade="specs" data-bid="${esc(a.id)}" title="${isSpecsHidden ? 'Показать служебную информацию по счёту' : 'Скрыть служебную информацию по счёту'}" aria-label="Служебная информация">ℹ</button>` : ''}
2427
2438
  ${isOpen ? `<button type="button" class="brk-icon-btn" data-btrade="events" data-bid="${esc(a.id)}" title="Журнал исполнения: отправленные заявки, ошибки брокера, стопы, ручные действия" aria-label="Журнал исполнения">📋</button>` : ''}
2428
2439
  ${isOpen ? `<button type="button" class="brk-icon-btn${isEditingThis ? ' primary' : ''}" data-btrade="cfg" data-bid="${esc(a.id)}" title="${isEditingThis ? 'Закрыть настройки и вернуться к торговле' : 'Настроить счёт и предохранители'}">⚙</button>` : ''}
2429
2440
  </div>
@@ -2431,20 +2442,25 @@ function renderBrokers() {
2431
2442
  <div ${isOpen ? '' : 'hidden'}>
2432
2443
  <div class="brk-cfg-mount" data-cfg-mount="${esc(a.id)}" ${isEditingThis ? '' : 'hidden'} style="margin-top:12px"></div>
2433
2444
  <div class="brk-trade-view" ${isEditingThis ? 'hidden' : ''}>
2434
- <div class="brk-specs">
2445
+ <div class="brk-specs" ${isSpecsHidden ? 'hidden' : ''}>
2435
2446
  <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
2447
  <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
2448
  <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>
2449
+ <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>
2450
+ ${a.executionAudit?.periods ? (() => {
2451
+ const pr = a.executionAudit.periods;
2452
+ 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>`;
2453
+ return `<span class="k">Доход / убыток по периодам:</span><span class="v" title="Чистый результат счёта (закрытые сделки после комиссий + дивиденды/купоны; за сегодня — с учётом дневной переоценки портфеля)">${fmtP('сегодня', pr.day)} · ${fmtP('7 дн.', pr.week)} · ${fmtP('30 дн.', pr.month)} · ${fmtP('с начала года', pr.ytd)}</span>`;
2454
+ })() : ''}
2439
2455
  <span class="k">Контроль автоследования:</span><span class="v">${ls ? (ls.autoFollowDetected ? '<b class="neg">обнаружено ✕ (торговля заблокирована)</b>' : '<b class="pos">не используется ✓ (счёт допущен)</b>') : '<span class="muted">не проверено</span>'}</span>
2440
2456
  <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
2457
  ${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
2458
  </div>
2443
2459
  ${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>
2460
+ <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>
2461
+ <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>
2462
+ <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>
2463
+ <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
2464
  </div>` : ''}
2449
2465
  ${ls && !ls.ok ? `<p class="neg small">Ошибка связи с брокером: ${esc(ls.error)}</p>` : ''}
2450
2466
  <div class="brk-subtabs">
@@ -2887,6 +2903,11 @@ $('#brokers-trade').addEventListener('click', async (e) => {
2887
2903
  }
2888
2904
  return;
2889
2905
  }
2906
+ if (act === 'specs') {
2907
+ toggleBrokerSpecs(id);
2908
+ renderBrokers();
2909
+ return;
2910
+ }
2890
2911
  if (act === 'events') {
2891
2912
  openBrokerEvents(id);
2892
2913
  return;
package/public/styles.css CHANGED
@@ -414,6 +414,7 @@ table.cmp td { padding: 3px 8px; }
414
414
  .brk-alloc-legend span { display: inline-flex; align-items: center; gap: 5px; }
415
415
  .brk-alloc-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
416
416
  .brk-specs { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 4px 14px; font-size: 13px; margin: 10px 0; line-height: 1.45; }
417
+ .brk-specs[hidden] { display: none; }
417
418
  .brk-specs .k { color: var(--muted); }
418
419
  .brk-specs .v { color: var(--text); }
419
420
  .brk-toolbar { display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; padding: 8px 0 10px; margin-bottom: 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-x: auto; }
@@ -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),