neuroalpha 0.5.30 → 0.5.31

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.30",
3
+ "version": "0.5.31",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
@@ -1652,6 +1652,77 @@ function sessionEdgeWarning(now = Date.now()) {
1652
1652
  // ---------- формирование и исполнение пакета лимитных заявок ----------
1653
1653
  const ACCOUNT_LOCK_TTL_MS = 75000;
1654
1654
  const accountBusyLocks = new Map(); // id -> { since, token }
1655
+ const entryImpulseWatch = new Map(); // `${accId}:${symbol}:${side}` -> { firstTs, lastTs, lastPx, extremePx }
1656
+
1657
+ // Фильтр затухания минутного импульса перед входом (Impulse Exhaustion):
1658
+ // не даёт продавать в шорт посередине вертикально растущей минутной свечи (или покупать в полёт падающего ножа),
1659
+ // дожидаясь остановки разгона и первого микро-отката от пика/дна (с шагом проверки 20 секунд)
1660
+ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.now()) {
1661
+ if (!cands.length || acc.broker !== 'tbank' || !acc.token) return { ready: cands, deferred: [] };
1662
+ for (const [k, v] of entryImpulseWatch) {
1663
+ if (nowTs - v.lastTs > 15 * 60000) entryImpulseWatch.delete(k);
1664
+ }
1665
+ const ready = [];
1666
+ const deferred = [];
1667
+ for (const r of cands) {
1668
+ const sym = r.symbol;
1669
+ const isShort = side === 'short';
1670
+ const hasCounterImpulse = isShort
1671
+ ? ((r.lastMovePct || 0) > 0.15 || (r.selfPct || 0) < -0.10)
1672
+ : ((r.lastMovePct || 0) < -0.15 || (r.selfPct || 0) > 0.10);
1673
+ if (!hasCounterImpulse) {
1674
+ ready.push(r);
1675
+ continue;
1676
+ }
1677
+ const bars1m = await fetchTbankCandles(sym, nowTs - 8 * 60000, nowTs, { includeIncomplete: true, intervalMin: 1 }).catch(() => null);
1678
+ if (!bars1m?.length) {
1679
+ ready.push(r);
1680
+ continue;
1681
+ }
1682
+ const last1m = bars1m[bars1m.length - 1];
1683
+ const prev1m = bars1m.length >= 2 ? bars1m[bars1m.length - 2] : null;
1684
+ const curPx = Number(last1m?.close || r.price) || 0;
1685
+ const sp = specs[sym] || { minStep: 0.01 };
1686
+ const step = sp.minStep > 0 ? sp.minStep : 0.01;
1687
+ const wKey = `${acc.id}:${sym}:${side}`;
1688
+ const prevW = entryImpulseWatch.get(wKey) || null;
1689
+
1690
+ if (isShort) {
1691
+ const recentHigh = Math.max(prevW?.extremePx || 0, curPx, last1m?.high || 0, prev1m?.high || 0);
1692
+ const pullbackPct = recentHigh > 0 ? ((recentHigh - curPx) / recentHigh) * 100 : 0;
1693
+ const pullbackTicks = recentHigh > 0 ? (recentHigh - curPx) / step : 0;
1694
+ const barTurnedDown = Boolean(last1m && prev1m && last1m.close <= last1m.open && last1m.high <= prev1m.high + step * 0.5);
1695
+ const peakedAndPulledBack = pullbackPct >= 0.12 && pullbackTicks >= 2;
1696
+ const tickedDownFromPrev = Boolean(prevW && curPx <= prevW.lastPx - step && pullbackTicks >= 1.5);
1697
+ const waitTimeout = Boolean(prevW && nowTs - prevW.firstTs >= 180000);
1698
+ if (peakedAndPulledBack || barTurnedDown || tickedDownFromPrev || waitTimeout) {
1699
+ entryImpulseWatch.delete(wKey);
1700
+ r.price = curPx;
1701
+ ready.push(r);
1702
+ } else {
1703
+ entryImpulseWatch.set(wKey, { firstTs: prevW?.firstTs || nowTs, lastTs: nowTs, lastPx: curPx, extremePx: recentHigh });
1704
+ deferred.push({ symbol: sym, side, curPx, extremePx: recentHigh });
1705
+ }
1706
+ } else {
1707
+ const recentLow = Math.min(prevW?.extremePx || Infinity, curPx > 0 ? curPx : Infinity, last1m?.low > 0 ? last1m.low : Infinity, prev1m?.low > 0 ? prev1m.low : Infinity);
1708
+ const bouncePct = Number.isFinite(recentLow) && recentLow > 0 ? ((curPx - recentLow) / recentLow) * 100 : 0;
1709
+ const bounceTicks = Number.isFinite(recentLow) && recentLow > 0 ? (curPx - recentLow) / step : 0;
1710
+ const barTurnedUp = Boolean(last1m && prev1m && last1m.close >= last1m.open && last1m.low >= prev1m.low - step * 0.5);
1711
+ const bottomedAndBounced = bouncePct >= 0.12 && bounceTicks >= 2;
1712
+ const tickedUpFromPrev = Boolean(prevW && curPx >= prevW.lastPx + step && bounceTicks >= 1.5);
1713
+ const waitTimeout = Boolean(prevW && nowTs - prevW.firstTs >= 180000);
1714
+ if (bottomedAndBounced || barTurnedUp || tickedUpFromPrev || waitTimeout) {
1715
+ entryImpulseWatch.delete(wKey);
1716
+ r.price = curPx;
1717
+ ready.push(r);
1718
+ } else {
1719
+ entryImpulseWatch.set(wKey, { firstTs: prevW?.firstTs || nowTs, lastTs: nowTs, lastPx: curPx, extremePx: recentLow });
1720
+ deferred.push({ symbol: sym, side, curPx, extremePx: recentLow });
1721
+ }
1722
+ }
1723
+ }
1724
+ return { ready, deferred };
1725
+ }
1655
1726
 
1656
1727
  function isAccountBusy(id) {
1657
1728
  const cur = accountBusyLocks.get(id);
@@ -2052,8 +2123,15 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2052
2123
  if (bQ > 0 && !closing.has(r.symbol)) return false;
2053
2124
  return !(aQ < 0 && !closing.has(r.symbol));
2054
2125
  });
2055
- const newBuys = filterBrokerDynamicTopK(rawNewBuys, minBuyEdgePct, Math.max(0, K - activeLongs));
2056
- const newShorts = filterBrokerDynamicTopK(rawNewShorts, minShortEdgePct, Math.max(0, K - activeShorts));
2126
+ const candidateBuys = filterBrokerDynamicTopK(rawNewBuys, minBuyEdgePct, Math.max(0, K - activeLongs));
2127
+ const candidateShorts = filterBrokerDynamicTopK(rawNewShorts, minShortEdgePct, Math.max(0, K - activeShorts));
2128
+ const [buyExhaustion, shortExhaustion] = await Promise.all([
2129
+ filterByImpulseExhaustion(acc, candidateBuys, 'buy', specs, nowTs),
2130
+ filterByImpulseExhaustion(acc, candidateShorts, 'short', specs, nowTs),
2131
+ ]);
2132
+ const newBuys = buyExhaustion.ready;
2133
+ const newShorts = shortExhaustion.ready;
2134
+ const deferredByImpulse = [...buyExhaustion.deferred, ...shortExhaustion.deferred];
2057
2135
  const totalNew = newBuys.length + newShorts.length;
2058
2136
  const activeTotal = Math.max(2, activeLongs + activeShorts + totalNew);
2059
2137
  const dynSlotRub = (isOwnerPlan && autoAdapt)
@@ -2237,6 +2315,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2237
2315
  } else if (!newBuys.length && !marginAllowed && rawShortCands.length > 0) {
2238
2316
  const skippedShorts = rawShortCands.map((r) => `${r.symbol} (${r.expectedPct}%)`).join(', ');
2239
2317
  diagNote = `Счёт автоматически работает в режиме «Только в рост (без шорта)», так как на нём в Т-Инвестициях отключена маржинальная торговля. Сигналы на падение (${skippedShorts}) пропущены; новых сигналов на покупку на свече ${fc.bar} нет. Если хотите зарабатывать и на падении (шортах), включите «Маржинальную торговлю» в настройках этого счёта в приложении Т-Инвестиций.`;
2318
+ } else if (!newBuys.length && !newShorts.length && deferredByImpulse.length > 0) {
2319
+ diagNote = `Сигнал ${deferredByImpulse.map((d) => `${d.symbol} (${d.side === 'short' ? 'шорт' : 'покупка'}, текущая ${d.curPx} ₽)`).join(', ')} взят на прицел: ждём затухания встречного минутного импульса для входа по лучшей цене (проверка каждые 20 сек).`;
2240
2320
  } else if (!newBuys.length && !newShorts.length) {
2241
2321
  const noShortNote = nonShortableSkipped.length ? ` (пропущены немаржинальные бумаги, недоступные для шорта у брокера: ${nonShortableSkipped.map((r) => `${r.symbol} ${r.expectedPct}%`).join(', ')})` : '';
2242
2322
  const cdNote = cooldownSkipped.length ? ` Сигнал ${cooldownSkipped.map((r) => `${r.symbol} (${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}%)`).join(', ')} временно пропущен: действует 60-минутная пауза после недавнего выхода из позиции, чтобы не платить повторную комиссию.` : '';
@@ -2392,16 +2472,20 @@ async function sendOrderToBroker(acc, ord) {
2392
2472
  }
2393
2473
  } else {
2394
2474
  // Входящие заявки (buy / short):
2395
- // • при узком спреде (<= 0,06% или <= 2 шагов цены — голубые фишки 1-го эшелона) берём по лучшей встречной цене в пределах защитного лимита finalLimitPx, чтобы не упускать движение;
2475
+ // • при узком спреде (<= 0,06% или <= 2 шагов цены — голубые фишки 1-го эшелона) берём по лучшей встречной цене в пределах защитного лимита finalLimitPx,
2476
+ // КРОМЕ случая, когда в первых 5 уровнях стакана встречный объём давит в >= 2,2 раза сильнее нашей стороны (тогда встаём пассивно на своей стороне спреда, чтобы остаток импульса налил нам по лучшей цене!);
2396
2477
  // • при более широком спреде (2–3 эшелон) встаём на середине спреда (или на 1 шаг внутри спреда), не переплачивая маркетмейкеру!
2397
- const tightSpread = spreadPct <= 0.06 || (bestAsk - bestBid) <= 2.1 * step;
2478
+ const bidTop5 = (jBook?.bids || []).slice(0, 5).reduce((s, lv) => s + (Number(lv.quantity) || 0), 0);
2479
+ const askTop5 = (jBook?.asks || []).slice(0, 5).reduce((s, lv) => s + (Number(lv.quantity) || 0), 0);
2480
+ const opposingBookPressure = isBuy ? (askTop5 > bidTop5 * 2.2) : (bidTop5 > askTop5 * 2.2);
2481
+ const tightSpread = (spreadPct <= 0.06 || (bestAsk - bestBid) <= 2.1 * step) && !opposingBookPressure;
2398
2482
  if (tightSpread) {
2399
2483
  const activePx = roundToStep(isBuy ? bestAsk : bestBid, step, sp.decimals);
2400
2484
  if (activePx > 0) finalLimitPx = isBuy ? Math.min(finalLimitPx, activePx) : Math.max(finalLimitPx, activePx);
2401
2485
  } else {
2402
2486
  const passivePx = isBuy
2403
- ? Math.min(finalLimitPx, roundToStep(Math.min(midPx, bestBid + step), step, sp.decimals))
2404
- : Math.max(finalLimitPx, roundToStep(Math.max(midPx, bestAsk - step), step, sp.decimals));
2487
+ ? Math.min(finalLimitPx, roundToStep(opposingBookPressure ? bestBid : Math.min(midPx, bestBid + step), step, sp.decimals))
2488
+ : Math.max(finalLimitPx, roundToStep(opposingBookPressure ? bestAsk : Math.max(midPx, bestAsk - step), step, sp.decimals));
2405
2489
  if (passivePx > 0) finalLimitPx = passivePx;
2406
2490
  }
2407
2491
  }