neuroalpha 0.6.6 → 0.6.7

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.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
@@ -2171,18 +2171,25 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2171
2171
  const keepByForecast = onlyEntries ? (r.liquid && r.expectedPct > -needPct) : (r.liquid && (r.expectedPct > curExitThr || holdThroughMinorDip));
2172
2172
  const dg = divGuardOf(sym, curPx);
2173
2173
  const divExit = Boolean(dg?.exitLong);
2174
- const keep = keepByForecast && !profitLockReason && !hitHardStopLoss && !divExit && !nightExit;
2174
+ const badNews = nb?.get(sym)?.bad;
2175
+ const newsExit = Boolean(autoAdapt && badNews && badNews.imp >= 0.80 && badNews.tone <= -0.60 && effEntryTs > 0 && badNews.ts > effEntryTs);
2176
+ const keep = keepByForecast && !profitLockReason && !hitHardStopLoss && !divExit && !newsExit && !nightExit;
2175
2177
  if (lots >= 1 && !keep) {
2176
2178
  const limitPrice = sniperLimitPrice(curPx, -1, acc.limitOffsetPct, sp);
2179
+ if (newsExit && !hitHardStopLoss && !profitLockReason && !divExit) {
2180
+ logBrokerEvent(acc, 'news_exit', `${sym}: закрытие покупки по свежей новости (важность ${badNews.imp}, тон ${badNews.tone}) — ${badNews.gist}`);
2181
+ }
2177
2182
  const exitReason = hitHardStopLoss
2178
2183
  ? `Защитный стоп-лосс позиции (${round(curGainPct, 2)}%)`
2179
2184
  : profitLockReason
2180
2185
  ? profitLockReason
2181
2186
  : divExit
2182
2187
  ? `Дивидендная отсечка ${dg.exDate} (дивиденд ${dg.yieldPct}%): закрытие покупки в последний день с правом, чтобы не терять 13% налога с дивиденда`
2183
- : nightExit
2184
- ? 'Закрытие внутридневной позиции перед ночным клирингом (защита от утреннего гэпа)'
2185
- : `Выход из покупки: ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% ниже порога удержания +${curExitThr}%`;
2188
+ : newsExit
2189
+ ? `Выход из покупки по важной негативной новости: ${badNews.gist}`
2190
+ : nightExit
2191
+ ? 'Закрытие внутридневной позиции перед ночным клирингом (защита от утреннего гэпа)'
2192
+ : `Выход из покупки: ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% ниже порога удержания +${curExitThr}%`;
2186
2193
  orders.push({
2187
2194
  symbol: sym, action: 'sell', lotSize: sp.lotSize, lots, shares: lots * sp.lotSize,
2188
2195
  refPrice: curPx, limitPrice, amountRub: round(lots * sp.lotSize * limitPrice, 2),
@@ -2202,18 +2209,25 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2202
2209
  const keepByForecast = onlyEntries ? (dir !== 'long' && r.liquid && r.expectedPct < needPct) : (dir !== 'long' && r.liquid && (r.expectedPct < -curExitThr || holdThroughMinorSpike));
2203
2210
  const dg = divGuardOf(sym, curPx);
2204
2211
  const divExit = Boolean(dg?.exitShort);
2205
- const keep = keepByForecast && !profitLockReason && !hitHardStopLoss && !divExit && !nightExit;
2212
+ const goodNews = nb?.get(sym)?.good;
2213
+ const newsExit = Boolean(autoAdapt && goodNews && goodNews.imp >= 0.80 && goodNews.tone >= 0.60 && effEntryTs > 0 && goodNews.ts > effEntryTs);
2214
+ const keep = keepByForecast && !profitLockReason && !hitHardStopLoss && !divExit && !newsExit && !nightExit;
2206
2215
  if (lots >= 1 && !keep) {
2207
2216
  const limitPrice = sniperLimitPrice(curPx, 1, acc.limitOffsetPct, sp);
2217
+ if (newsExit && !hitHardStopLoss && !profitLockReason && !divExit) {
2218
+ logBrokerEvent(acc, 'news_exit', `${sym}: закрытие шорта по свежей новости (важность ${goodNews.imp}, тон +${goodNews.tone}) — ${goodNews.gist}`);
2219
+ }
2208
2220
  const exitReason = hitHardStopLoss
2209
2221
  ? `Защитный стоп-лосс шорта от выноса вверх (${round(curGainPct, 2)}%)`
2210
2222
  : profitLockReason
2211
2223
  ? profitLockReason
2212
2224
  : divExit
2213
2225
  ? `Дивидендная отсечка ${dg.exDate} (дивиденд ${dg.yieldPct}%): закрытие шорта в последний день с правом, чтобы брокер не списал компенсацию дивиденда`
2214
- : nightExit
2215
- ? 'Закрытие короткой позиции перед ночным клирингом (защита от утреннего гэпа и платы за перенос)'
2216
- : `Закрытие шорта: ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% выше порога удержания −${curExitThr}%`;
2226
+ : newsExit
2227
+ ? `Закрытие шорта по важной позитивной новости: ${goodNews.gist}`
2228
+ : nightExit
2229
+ ? 'Закрытие короткой позиции перед ночным клирингом (защита от утреннего гэпа и платы за перенос)'
2230
+ : `Закрытие шорта: ожидание ${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}% выше порога удержания −${curExitThr}%`;
2217
2231
  orders.push({
2218
2232
  symbol: sym, action: 'cover', lotSize: sp.lotSize, lots, shares: lots * sp.lotSize,
2219
2233
  refPrice: curPx, limitPrice, amountRub: round(lots * sp.lotSize * limitPrice, 2),
@@ -2634,7 +2648,8 @@ function formatTbankOrderError(status, rawText = '', ord = {}) {
2634
2648
  return 'По данной бумаге брокер временно запретил короткие продажи (шорт)';
2635
2649
  }
2636
2650
  if (code.includes('30099') || /outside the limits/i.test(code)) {
2637
- return 'Цена бумаги вышла за границы дневного ценового коридора у брокера (код 30099: Мосбиржа расширила планку, шлюз брокера ещё обновляет лимиты)';
2651
+ const corr = (ord.limitDown > 0 || ord.limitUp > 0) ? `, заявка ${ord.limitPrice || ''} ₽ при коридоре ${ord.limitDown || '—'}..${ord.limitUp || '—'} ₽` : '';
2652
+ return `Цена бумаги вышла за границы дневного ценового коридора у брокера (код 30099${corr}: Мосбиржа расширила планку, шлюз брокера ещё обновляет лимиты)`;
2638
2653
  }
2639
2654
  return `T-Invest PostOrder HTTP ${status}${msg ? `: ${msg}` : ''}${desc ? ` (код ${desc})` : ''}`;
2640
2655
  }
@@ -2652,6 +2667,7 @@ async function sendOrderToBroker(acc, ord) {
2652
2667
  // • если спред в стакане в момент входа разъехался шире половины ожидаемого хода сигнала (или >0,35%) — отклоняем вход в тонкий стакан!
2653
2668
  let finalLimitPx = Number(ord.limitPrice) || 0;
2654
2669
  let lotsToOrder = Number(ord.lots) || 0;
2670
+ let limitUp = 0, limitDown = 0;
2655
2671
  const sp = kvGet('tbankShortableMap')?.specMap?.[ord.symbol] || { minStep: 0.01, decimals: 2 };
2656
2672
  const rBook = await brokerFetch(`${base}/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook`, {
2657
2673
  method: 'POST', headers, body: JSON.stringify({ instrumentId, depth: 10 }), timeoutMs: 3500,
@@ -2660,6 +2676,16 @@ async function sendOrderToBroker(acc, ord) {
2660
2676
  const jBook = await rBook.json().catch(() => null);
2661
2677
  const bestBid = qVal(jBook?.bids?.[0]?.price);
2662
2678
  const bestAsk = qVal(jBook?.asks?.[0]?.price);
2679
+ limitUp = qVal(jBook?.limitUp);
2680
+ limitDown = qVal(jBook?.limitDown);
2681
+ if (isEntry && ord.symbol !== 'LQDT') {
2682
+ if (!isBuy && limitUp > 0 && bestBid >= limitUp) {
2683
+ throw Object.assign(new Error(`Цена бумаги ${ord.symbol} (${bestBid} ₽) у верхней границы дневного ценового коридора у брокера (limitUp ${limitUp} ₽, код 30099)`), { definitive: true });
2684
+ }
2685
+ if (isBuy && limitDown > 0 && bestAsk > 0 && bestAsk <= limitDown) {
2686
+ throw Object.assign(new Error(`Цена бумаги ${ord.symbol} (${bestAsk} ₽) у нижней границы дневного ценового коридора у брокера (limitDown ${limitDown} ₽, код 30099)`), { definitive: true });
2687
+ }
2688
+ }
2663
2689
  if (isEntry && ord.symbol !== 'LQDT' && (!(bestBid > 0) || !(bestAsk > 0))) {
2664
2690
  logBrokerEvent(acc, 'spread_skip', `${ord.symbol}: пустая сторона стакана (bid ${bestBid || 0} / ask ${bestAsk || 0}, планка или аукцион)`);
2665
2691
  throw Object.assign(new Error(`В стакане ${ord.symbol} отсутствует ${!(bestAsk > 0) ? 'предложение (верхняя планка / аукцион)' : 'спрос (нижняя планка / аукцион)'} — вход пропущен`), { definitive: true });
@@ -2723,6 +2749,22 @@ async function sendOrderToBroker(acc, ord) {
2723
2749
  }
2724
2750
  }
2725
2751
  }
2752
+ // Защита от выхода лимитной цены за границы дневного коридора Т-Банка (limitDown..limitUp из GetOrderBook):
2753
+ // если сама рыночная цена внутри коридора, а за границу вылез только защитный запас limitOffsetPct — прижимаем цену к границе коридора;
2754
+ // если же при входе сама цена уже за коридором против позиции — отклоняем с кодом 30099 до вызова PostOrder.
2755
+ const step = sp.minStep || 0.01;
2756
+ if (limitUp > 0 && finalLimitPx > limitUp) {
2757
+ if (isEntry && !isBuy) {
2758
+ throw Object.assign(new Error(`Цена заявки ${ord.symbol} (${finalLimitPx} ₽) выше верхней границы ценового коридора у брокера (limitUp ${limitUp} ₽, код 30099)`), { definitive: true });
2759
+ }
2760
+ if (!(limitDown > limitUp)) finalLimitPx = roundToStep(limitUp, step, sp.decimals);
2761
+ }
2762
+ if (limitDown > 0 && finalLimitPx < limitDown) {
2763
+ if (isEntry && isBuy) {
2764
+ throw Object.assign(new Error(`Цена заявки ${ord.symbol} (${finalLimitPx} ₽) ниже нижней границы ценового коридора у брокера (limitDown ${limitDown} ₽, код 30099)`), { definitive: true });
2765
+ }
2766
+ if (!(limitUp > 0 && limitDown > limitUp)) finalLimitPx = roundToStep(limitDown, step, sp.decimals);
2767
+ }
2726
2768
  }
2727
2769
  const units = Math.trunc(finalLimitPx);
2728
2770
  const nano = Math.round((finalLimitPx - units) * 1e9);
@@ -2798,7 +2840,7 @@ async function sendOrderToBroker(acc, ord) {
2798
2840
  if (!r.ok) {
2799
2841
  const errText = await r.text().catch(() => '');
2800
2842
  // 5xx — исход не известен (заявка могла быть принята), 4xx — брокер точно отклонил
2801
- throw Object.assign(new Error(formatTbankOrderError(r.status, errText, ord)), { definitive: r.status < 500 });
2843
+ throw Object.assign(new Error(formatTbankOrderError(r.status, errText, { ...ord, limitPrice: finalLimitPx, limitUp, limitDown })), { definitive: r.status < 500 });
2802
2844
  }
2803
2845
  const j = await r.json();
2804
2846
  logBrokerEvent(acc, 'order_sent', `${ord.action} ${ord.symbol} ${lotsToOrder} л. по ${finalLimitPx} ₽ (сигнал ${ord.refPrice || finalLimitPx} ₽) · ${body.orderId}${ord.reason ? ` · ${ord.reason}` : ''}`);