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 +1 -1
- package/server/brokers/index.js +52 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -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
|
|
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
|
-
:
|
|
2184
|
-
?
|
|
2185
|
-
:
|
|
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
|
|
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
|
-
:
|
|
2215
|
-
?
|
|
2216
|
-
:
|
|
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
|
-
|
|
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}` : ''}`);
|