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