neuroalpha 0.5.33 → 0.5.34

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.33",
3
+ "version": "0.5.34",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
@@ -524,6 +524,7 @@ export function saveBrokerAccount(input = {}) {
524
524
  entryTsBySymbol: acctChanged ? {} : (prev.entryTsBySymbol || {}),
525
525
  exitCooldownBySymbol: acctChanged ? {} : (prev.exitCooldownBySymbol || {}),
526
526
  entryFailCooldownBySymbol: acctChanged ? {} : (prev.entryFailCooldownBySymbol || {}),
527
+ entryFailReasonBySymbol: acctChanged ? {} : (prev.entryFailReasonBySymbol || {}),
527
528
  peakPriceBySymbol: acctChanged ? {} : (prev.peakPriceBySymbol || {}),
528
529
  baseRunId: nextBaseRunId,
529
530
  // жёсткие предохранители (недоступны для изменения ИИ-Стратегом)
@@ -1837,6 +1838,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
1837
1838
  const entryTsMap = acc.entryTsBySymbol || {};
1838
1839
  const exitCooldownMap = acc.exitCooldownBySymbol || {};
1839
1840
  const entryFailMap = acc.entryFailCooldownBySymbol || {};
1841
+ const entryFailReasonMap = acc.entryFailReasonBySymbol || {};
1840
1842
  // Спред-фильтр реального стакана по эшелонам ликвидности: для 2–3 эшелона порог выше в TIER2_EDGE_MULT раз.
1841
1843
  // Отдельного вечернего ужесточения нет: проверка 23.10.2026 (первые 70% и последние 30% истории) показала,
1842
1844
  // что порог ×1,25 после 19:00 МСК снижает итог на обеих частях — правила совпадают с бэктестом.
@@ -1846,10 +1848,9 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
1846
1848
  const minShortEdgePct = round(baseShortNeedPct, 3);
1847
1849
  const reqBuyEdgeOf = (sym) => rowMap.get(sym)?.buyThresholdPct ?? round(Math.max(minBuyEdgePct * symbolEdgeMult(sym), 0.30), 3);
1848
1850
  const reqShortEdgeOf = (sym) => rowMap.get(sym)?.shortThresholdPct ?? round(Math.max(minShortEdgePct * symbolEdgeMult(sym), 0.31), 3);
1849
- const isInCooldown = (sym) => Boolean(
1850
- (autoAdapt && exitCooldownMap[sym] && (nowTs - exitCooldownMap[sym] < 60 * 60000)) ||
1851
- (entryFailMap[sym] && (nowTs - entryFailMap[sym] < 5 * 60000)),
1852
- );
1851
+ const isExitCooldown = (sym) => Boolean(autoAdapt && exitCooldownMap[sym] && (nowTs - exitCooldownMap[sym] < 60 * 60000));
1852
+ const isEntryFailCooldown = (sym) => Boolean(entryFailMap[sym] && (nowTs - entryFailMap[sym] < 5 * 60000));
1853
+ const isInCooldown = (sym) => isExitCooldown(sym) || isEntryFailCooldown(sym);
1853
1854
  // Самообучение на истории реальных сделок счёта (адаптивный карантин убыточных бумаг, единый с research.js):
1854
1855
  // если за последние 15 торговых дней по бумаге накоплен убыток (< -1.2% за >= 2 сделки или одиночный пролив < -2.5%),
1855
1856
  // временно исключаем такую бумагу из новых входов!
@@ -1909,7 +1910,9 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
1909
1910
  const rowMap = new Map((fc.rows || []).map((r) => [r.symbol, r]));
1910
1911
  const nb = params.newsFilter ? blockingNews(6) : null;
1911
1912
 
1912
- const cooldownSkipped = fc.rows.filter((r) => ((r.signal && r.expectedPct >= reqBuyEdgeOf(r.symbol)) || (rawDir !== 'long' && r.shortSignal && r.expectedPct <= -reqShortEdgeOf(r.symbol))) && isInCooldown(r.symbol));
1913
+ const isRawSignalRow = (r) => (r.signal && r.expectedPct >= reqBuyEdgeOf(r.symbol)) || (rawDir !== 'long' && r.shortSignal && r.expectedPct <= -reqShortEdgeOf(r.symbol));
1914
+ const cooldownSkipped = fc.rows.filter((r) => isRawSignalRow(r) && isExitCooldown(r.symbol));
1915
+ const entryFailSkipped = fc.rows.filter((r) => isRawSignalRow(r) && !isExitCooldown(r.symbol) && isEntryFailCooldown(r.symbol));
1913
1916
  // Дивидендный предохранитель (общие правила с бэктестом, server/model/dividends.js)
1914
1917
  const divGuardOf = (sym, px) => liveDividendGuard(sym, Number(px) || specs[sym]?.lastPrice || 0, nowTs);
1915
1918
  const divBlocked = [];
@@ -2334,14 +2337,17 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2334
2337
  diagNote = `Сигнал ${deferredByImpulse.map((d) => `${d.symbol} (${d.side === 'short' ? 'шорт' : 'покупка'}, текущая ${d.curPx} ₽)`).join(', ')} взят на прицел: ждём затухания встречного минутного импульса для входа по лучшей цене (проверка каждые 20 сек).`;
2335
2338
  } else if (!newBuys.length && !newShorts.length) {
2336
2339
  const noShortNote = nonShortableSkipped.length ? ` (пропущены немаржинальные бумаги, недоступные для шорта у брокера: ${nonShortableSkipped.map((r) => `${r.symbol} ${r.expectedPct}%`).join(', ')})` : '';
2340
+ const failNote = entryFailSkipped.length
2341
+ ? ` Заявка по ${entryFailSkipped.map((r) => `${r.symbol} (${entryFailReasonMap[r.symbol] || 'отказ шлюза брокера'})`).join('; ')} временно отложена на 5 мин после ошибки приёма у брокера.`
2342
+ : '';
2337
2343
  const cdNote = cooldownSkipped.length ? ` Сигнал ${cooldownSkipped.map((r) => `${r.symbol} (${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}%)`).join(', ')} временно пропущен: действует пауза (30–60 мин) после недавнего выхода из позиции, чтобы не перезаходить в отыгранный импульс и не платить повторную комиссию.` : '';
2338
2344
  const buyThrT2 = round(minBuyEdgePct * symbolEdgeMult('__T2__'), 2);
2339
2345
  const shortThrT2 = round(minShortEdgePct * symbolEdgeMult('__T2__'), 2);
2340
2346
  const thrRangeStr = dir !== 'long'
2341
2347
  ? `+${round(minBuyEdgePct, 2)}% / +${buyThrT2}% в покупку, −${round(minShortEdgePct, 2)}% / −${shortThrT2}% в шорт`
2342
2348
  : `+${round(minBuyEdgePct, 2)}% (1-й эшелон) / +${buyThrT2}% (2–3-й эшелон)`;
2343
- diagNote = cdNote
2344
- ? `На свече ${fc.bar} новых входов нет.${cdNote}${noShortNote}`
2349
+ diagNote = (failNote || cdNote)
2350
+ ? `На свече ${fc.bar} новых входов нет.${failNote}${cdNote}${noShortNote}`
2345
2351
  : `На свече ${fc.bar} ни одна доступная бумага не превысила индивидуальный порог выгоды (${thrRangeStr})${noShortNote}.`;
2346
2352
  }
2347
2353
  }
@@ -2422,6 +2428,9 @@ function formatTbankOrderError(status, rawText = '', ord = {}) {
2422
2428
  if (code.includes('30052') || /short.*disabled|short.*not/i.test(code)) {
2423
2429
  return 'По данной бумаге брокер временно запретил короткие продажи (шорт)';
2424
2430
  }
2431
+ if (code.includes('30099') || /outside the limits/i.test(code)) {
2432
+ return 'Цена бумаги вышла за границы дневного ценового коридора у брокера (код 30099: Мосбиржа расширила планку, шлюз брокера ещё обновляет лимиты)';
2433
+ }
2425
2434
  return `T-Invest PostOrder HTTP ${status}${msg ? `: ${msg}` : ''}${desc ? ` (код ${desc})` : ''}`;
2426
2435
  }
2427
2436
 
@@ -2773,6 +2782,7 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
2773
2782
  acc.entryTsBySymbol = acc.entryTsBySymbol || {};
2774
2783
  acc.exitCooldownBySymbol = acc.exitCooldownBySymbol || {};
2775
2784
  acc.entryFailCooldownBySymbol = acc.entryFailCooldownBySymbol || {};
2785
+ acc.entryFailReasonBySymbol = acc.entryFailReasonBySymbol || {};
2776
2786
  acc.peakPriceBySymbol = acc.peakPriceBySymbol || {};
2777
2787
  const execNowTs = Date.now();
2778
2788
  const exitOrders = batch.orders.filter((o) => o.action === 'sell' || o.action === 'cover');
@@ -2815,6 +2825,7 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
2815
2825
  acc.peakPriceBySymbol[ord.symbol] = Number(ord.limitPrice || ord.refPrice) || 0;
2816
2826
  delete acc.exitCooldownBySymbol[ord.symbol];
2817
2827
  delete acc.entryFailCooldownBySymbol[ord.symbol];
2828
+ delete acc.entryFailReasonBySymbol[ord.symbol];
2818
2829
  } else if (ord.action === 'sell' || ord.action === 'cover') {
2819
2830
  hadExits = true;
2820
2831
  delete acc.entryTsBySymbol[ord.symbol];
@@ -2831,6 +2842,7 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
2831
2842
  else ord.uncertainSend = true;
2832
2843
  if (ord.action === 'buy' || ord.action === 'short') {
2833
2844
  acc.entryFailCooldownBySymbol[ord.symbol] = execNowTs;
2845
+ acc.entryFailReasonBySymbol[ord.symbol] = e.message;
2834
2846
  }
2835
2847
  results.push({ symbol: ord.symbol, ok: false, error: e.message, uncertain: !e.definitive });
2836
2848
  logBrokerEvent(acc, 'order_error', `${ord.action} ${ord.symbol} ${ord.lots} л.: ${e.message}`);
@@ -281,8 +281,9 @@ export function newsForSymbols(hours = 24) {
281
281
  // Для живой проверки с newsFilter: самые важные свежие новости (важность ≥ 0.6, |тон| ≥ 0.4)
282
282
  // по тикерам: symbol → { bad: { gist, imp, tone, ts }, good: {…} }
283
283
  export function blockingNews(hours = 6, atTs = Date.now()) {
284
+ const sinceTs = atTs - hours * 3600000;
284
285
  const rows = db.prepare(`SELECT ts, title, gist, imp, tags FROM news
285
- WHERE marked = 1 AND imp >= 0.6 AND tags IS NOT NULL AND tags != '[]' AND ts >= ? AND ts <= ?`).all(atTs - hours * 3600000, atTs);
286
+ WHERE marked = 1 AND imp >= 0.6 AND tags IS NOT NULL AND tags != '[]' AND ts >= ? AND ts <= ?`).all(sinceTs, atTs);
286
287
  const m = new Map();
287
288
  for (const r of rows) {
288
289
  for (const [sym, tone] of parseTags(r)) {
@@ -293,6 +294,29 @@ export function blockingNews(hours = 6, atTs = Date.now()) {
293
294
  m.set(sym, e);
294
295
  }
295
296
  }
297
+ // Официальные сообщения Мосбиржи о расширении верхней/нижней границы ценового коридора акции (планки) —
298
+ // блокируют вход против пробоя планки даже до разметки ИИ (и защищают от отказа шлюза брокера с кодом 30099)
299
+ const corridorRows = db.prepare(`SELECT ts, title, tags FROM news
300
+ WHERE ts >= ? AND ts <= ? AND title LIKE '%границы ценового коридора%ценной бумаги%' AND title NOT LIKE '%коридора РЕПО%'`).all(sinceTs, atTs);
301
+ for (const r of corridorRows) {
302
+ const t = String(r.title || '');
303
+ if (/коридора\s+репо/i.test(t)) continue;
304
+ const isUpper = /верхней границы ценового коридора/i.test(t);
305
+ const isLower = /нижней границы ценового коридора/i.test(t);
306
+ if (!isUpper && !isLower) continue;
307
+ const syms = new Set(parseTags(r).map(([s]) => s).filter(Boolean));
308
+ const matchSym = /ценной бумаги\s+([A-Z0-9]{2,8})\b/i.exec(t);
309
+ if (matchSym?.[1]) syms.add(matchSym[1].toUpperCase());
310
+ for (const sym of syms) {
311
+ const e = m.get(sym) || {};
312
+ const side = isUpper ? 'good' : 'bad';
313
+ const tone = isUpper ? 0.9 : -0.9;
314
+ const imp = 0.95;
315
+ const gist = isUpper ? `Мосбиржа расширила верхнюю планку коридора ${sym}` : `Мосбиржа расширила нижнюю планку коридора ${sym}`;
316
+ if (!e[side] || imp >= e[side].imp) e[side] = { tone, imp, gist, ts: r.ts };
317
+ m.set(sym, e);
318
+ }
319
+ }
296
320
  return m;
297
321
  }
298
322