neuroalpha 0.5.31 → 0.5.32
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 +44 -21
- package/server/model/edge.js +14 -2
- package/server/model/forecast.js +10 -5
- package/server/model/live.js +1 -1
- package/server/model/research.js +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -1642,9 +1642,9 @@ function evaluateProfitLock(sym, side, avgPx, curPx, peakPx, roundTripFeePct = 0
|
|
|
1642
1642
|
function sessionEdgeWarning(now = Date.now()) {
|
|
1643
1643
|
const d = new Date(now + MSK);
|
|
1644
1644
|
const mins = d.getUTCHours() * 60 + d.getUTCMinutes();
|
|
1645
|
-
// открытие утренней сессии 06:50–07:
|
|
1646
|
-
if ((mins >= 6 * 60 + 50 && mins <= 7 * 60 +
|
|
1647
|
-
return 'Первые
|
|
1645
|
+
// открытие утренней сессии 06:50–07:25 (отыгрыш ночных гэпов и формирование стакана), открытие основной сессии 09:50–10:10 и закрытие дневной 18:35–18:55 МСК
|
|
1646
|
+
if ((mins >= 6 * 60 + 50 && mins <= 7 * 60 + 25) || (mins >= 9 * 60 + 50 && mins <= 10 * 60 + 10) || (mins >= 18 * 60 + 35 && mins <= 18 * 60 + 55)) {
|
|
1647
|
+
return 'Первые минуты утренней/основной сессии или закрытие дневной (широкий спред и утренние гэпы в стакане)';
|
|
1648
1648
|
}
|
|
1649
1649
|
return null;
|
|
1650
1650
|
}
|
|
@@ -1656,7 +1656,7 @@ const entryImpulseWatch = new Map(); // `${accId}:${symbol}:${side}` -> { firstT
|
|
|
1656
1656
|
|
|
1657
1657
|
// Фильтр затухания минутного импульса перед входом (Impulse Exhaustion):
|
|
1658
1658
|
// не даёт продавать в шорт посередине вертикально растущей минутной свечи (или покупать в полёт падающего ножа),
|
|
1659
|
-
// дожидаясь остановки разгона и
|
|
1659
|
+
// дожидаясь остановки разгона и подтверждённого отката от пика/дна (с шагом проверки 20 секунд; без принудительного входа по таймауту)
|
|
1660
1660
|
async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.now()) {
|
|
1661
1661
|
if (!cands.length || acc.broker !== 'tbank' || !acc.token) return { ready: cands, deferred: [] };
|
|
1662
1662
|
for (const [k, v] of entryImpulseWatch) {
|
|
@@ -1674,7 +1674,7 @@ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.n
|
|
|
1674
1674
|
ready.push(r);
|
|
1675
1675
|
continue;
|
|
1676
1676
|
}
|
|
1677
|
-
const bars1m = await fetchTbankCandles(sym, nowTs -
|
|
1677
|
+
const bars1m = await fetchTbankCandles(sym, nowTs - 15 * 60000, nowTs, { includeIncomplete: true, intervalMin: 1 }).catch(() => null);
|
|
1678
1678
|
if (!bars1m?.length) {
|
|
1679
1679
|
ready.push(r);
|
|
1680
1680
|
continue;
|
|
@@ -1686,16 +1686,25 @@ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.n
|
|
|
1686
1686
|
const step = sp.minStep > 0 ? sp.minStep : 0.01;
|
|
1687
1687
|
const wKey = `${acc.id}:${sym}:${side}`;
|
|
1688
1688
|
const prevW = entryImpulseWatch.get(wKey) || null;
|
|
1689
|
+
let max1m = curPx, min1m = curPx > 0 ? curPx : Infinity;
|
|
1690
|
+
for (const b of bars1m) {
|
|
1691
|
+
if (b.high > max1m) max1m = b.high;
|
|
1692
|
+
if (b.low > 0 && b.low < min1m) min1m = b.low;
|
|
1693
|
+
}
|
|
1694
|
+
const windowRangePct = (Number.isFinite(min1m) && min1m > 0 && max1m > min1m) ? ((max1m - min1m) / min1m) * 100 : 0;
|
|
1695
|
+
const impulseMagPct = Math.max(Math.abs(Number(r.lastMovePct) || 0), windowRangePct);
|
|
1696
|
+
const minPullbackPct = Math.max(0.15, Math.min(0.55, impulseMagPct * 0.15));
|
|
1697
|
+
const isStrongSurge = impulseMagPct >= 1.5;
|
|
1689
1698
|
|
|
1690
1699
|
if (isShort) {
|
|
1691
|
-
const recentHigh = Math.max(prevW?.extremePx || 0, curPx,
|
|
1700
|
+
const recentHigh = Math.max(prevW?.extremePx || 0, curPx, max1m);
|
|
1692
1701
|
const pullbackPct = recentHigh > 0 ? ((recentHigh - curPx) / recentHigh) * 100 : 0;
|
|
1693
1702
|
const pullbackTicks = recentHigh > 0 ? (recentHigh - curPx) / step : 0;
|
|
1694
1703
|
const barTurnedDown = Boolean(last1m && prev1m && last1m.close <= last1m.open && last1m.high <= prev1m.high + step * 0.5);
|
|
1695
|
-
const peakedAndPulledBack = pullbackPct >=
|
|
1696
|
-
const tickedDownFromPrev = Boolean(prevW && curPx <= prevW.lastPx - step && pullbackTicks >=
|
|
1697
|
-
const
|
|
1698
|
-
if (
|
|
1704
|
+
const peakedAndPulledBack = pullbackPct >= minPullbackPct && pullbackTicks >= 2 && (!last1m || last1m.close <= last1m.open + step * 0.5);
|
|
1705
|
+
const tickedDownFromPrev = Boolean(!isStrongSurge && prevW && curPx <= prevW.lastPx - step && pullbackTicks >= 2 && pullbackPct >= 0.12);
|
|
1706
|
+
const readyShort = isStrongSurge ? (peakedAndPulledBack && barTurnedDown) : (peakedAndPulledBack || barTurnedDown || tickedDownFromPrev);
|
|
1707
|
+
if (readyShort) {
|
|
1699
1708
|
entryImpulseWatch.delete(wKey);
|
|
1700
1709
|
r.price = curPx;
|
|
1701
1710
|
ready.push(r);
|
|
@@ -1704,14 +1713,14 @@ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.n
|
|
|
1704
1713
|
deferred.push({ symbol: sym, side, curPx, extremePx: recentHigh });
|
|
1705
1714
|
}
|
|
1706
1715
|
} else {
|
|
1707
|
-
const recentLow = Math.min(prevW?.extremePx || Infinity,
|
|
1716
|
+
const recentLow = Math.min(prevW?.extremePx || Infinity, min1m);
|
|
1708
1717
|
const bouncePct = Number.isFinite(recentLow) && recentLow > 0 ? ((curPx - recentLow) / recentLow) * 100 : 0;
|
|
1709
1718
|
const bounceTicks = Number.isFinite(recentLow) && recentLow > 0 ? (curPx - recentLow) / step : 0;
|
|
1710
1719
|
const barTurnedUp = Boolean(last1m && prev1m && last1m.close >= last1m.open && last1m.low >= prev1m.low - step * 0.5);
|
|
1711
|
-
const bottomedAndBounced = bouncePct >=
|
|
1712
|
-
const tickedUpFromPrev = Boolean(prevW && curPx >= prevW.lastPx + step && bounceTicks >=
|
|
1713
|
-
const
|
|
1714
|
-
if (
|
|
1720
|
+
const bottomedAndBounced = bouncePct >= minPullbackPct && bounceTicks >= 2 && (!last1m || last1m.close >= last1m.open - step * 0.5);
|
|
1721
|
+
const tickedUpFromPrev = Boolean(!isStrongSurge && prevW && curPx >= prevW.lastPx + step && bounceTicks >= 2 && bouncePct >= 0.12);
|
|
1722
|
+
const readyLong = isStrongSurge ? (bottomedAndBounced && barTurnedUp) : (bottomedAndBounced || barTurnedUp || tickedUpFromPrev);
|
|
1723
|
+
if (readyLong) {
|
|
1715
1724
|
entryImpulseWatch.delete(wKey);
|
|
1716
1725
|
r.price = curPx;
|
|
1717
1726
|
ready.push(r);
|
|
@@ -1901,10 +1910,11 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
1901
1910
|
// Дивидендный предохранитель (общие правила с бэктестом, server/model/dividends.js)
|
|
1902
1911
|
const divGuardOf = (sym, px) => liveDividendGuard(sym, Number(px) || specs[sym]?.lastPrice || 0, nowTs);
|
|
1903
1912
|
const divBlocked = [];
|
|
1904
|
-
const
|
|
1913
|
+
const entryTimeOk = timeOk && !edgeWarn;
|
|
1914
|
+
const buyCands = !entryTimeOk ? [] : fc.rows.filter((r) => r.signal && r.expectedPct >= reqBuyEdgeOf(r.symbol) && !isInCooldown(r.symbol) && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.bad)
|
|
1905
1915
|
.filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockLong) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
|
|
1906
1916
|
.sort((a, b) => b.expectedPct - a.expectedPct).slice(0, K);
|
|
1907
|
-
const allShortSignals = (rawDir !== 'long' &&
|
|
1917
|
+
const allShortSignals = (rawDir !== 'long' && entryTimeOk) ? fc.rows.filter((r) => r.shortSignal && r.expectedPct <= -reqShortEdgeOf(r.symbol) && (!autoAdapt || canEnterIntraday(nowTs, H, BAR_INTERVAL, 'short', r.symbol)) && !isInCooldown(r.symbol) && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.good)
|
|
1908
1918
|
.filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockShort) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
|
|
1909
1919
|
.sort((a, b) => a.expectedPct - b.expectedPct) : [];
|
|
1910
1920
|
const nonShortableSkipped = [];
|
|
@@ -2305,6 +2315,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2305
2315
|
if (!orders.length) {
|
|
2306
2316
|
if (!timeOk) {
|
|
2307
2317
|
diagNote = `На свече ${fc.bar} вечернее окно новых входов закрыто: до конца сессии осталось меньше горизонта прогноза (${effHorizon} ч) — внутридневные сделки не переносятся через ночь.`;
|
|
2318
|
+
} else if (edgeWarn) {
|
|
2319
|
+
diagNote = `На свече ${fc.bar} новые входы временно отложены: ${edgeWarn.toLowerCase()}.`;
|
|
2308
2320
|
} else if ((newBuys.length || newShorts.length) && availRub < 1500) {
|
|
2309
2321
|
const sigList = [...newBuys.map((r) => `${r.symbol} (+${r.expectedPct}%)`), ...newShorts.map((r) => `${r.symbol} (${r.expectedPct}%)`)].join(', ');
|
|
2310
2322
|
const cashNow = round(acc.lastSync?.cashRub || 0, 0);
|
|
@@ -2370,7 +2382,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2370
2382
|
results: [],
|
|
2371
2383
|
};
|
|
2372
2384
|
saveRawAccounts(list);
|
|
2373
|
-
if (acc.mode === 'auto' &&
|
|
2385
|
+
if (acc.mode === 'auto' && acc.token && orders.length && (!edgeWarn || orders.every((o) => o.action === 'sell' || o.action === 'cover'))) {
|
|
2374
2386
|
return await executeBrokerOrders(id, { _lockHeld: true });
|
|
2375
2387
|
}
|
|
2376
2388
|
if (acc.mode === 'confirm' && orders.length && !isDuplicatePending) {
|
|
@@ -2431,6 +2443,10 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2431
2443
|
const jBook = await rBook.json().catch(() => null);
|
|
2432
2444
|
const bestBid = qVal(jBook?.bids?.[0]?.price);
|
|
2433
2445
|
const bestAsk = qVal(jBook?.asks?.[0]?.price);
|
|
2446
|
+
if (isEntry && ord.symbol !== 'LQDT' && (!(bestBid > 0) || !(bestAsk > 0))) {
|
|
2447
|
+
logBrokerEvent(acc, 'spread_skip', `${ord.symbol}: пустая сторона стакана (bid ${bestBid || 0} / ask ${bestAsk || 0}, планка или аукцион)`);
|
|
2448
|
+
throw Object.assign(new Error(`В стакане ${ord.symbol} отсутствует ${!(bestAsk > 0) ? 'предложение (верхняя планка / аукцион)' : 'спрос (нижняя планка / аукцион)'} — вход пропущен`), { definitive: true });
|
|
2449
|
+
}
|
|
2434
2450
|
if (bestBid > 0 && bestAsk >= bestBid) {
|
|
2435
2451
|
const midPx = (bestBid + bestAsk) / 2;
|
|
2436
2452
|
const spreadPct = ((bestAsk - bestBid) / midPx) * 100;
|
|
@@ -3234,9 +3250,16 @@ async function reconcileBrokerStopsLocked(list, acc) {
|
|
|
3234
3250
|
if (alive) {
|
|
3235
3251
|
try { await tbankCancelStop(acc, t.stopOrderId); } catch (e) { logBrokerEvent(acc, 'broker_stop_error', `${sym}: ${e.message}`); continue; }
|
|
3236
3252
|
}
|
|
3237
|
-
logBrokerEvent(acc, alive ? 'broker_stop_cancel' : 'broker_stop_gone', `${sym}: ${!
|
|
3238
|
-
// стоп
|
|
3239
|
-
if (!alive)
|
|
3253
|
+
logBrokerEvent(acc, alive ? 'broker_stop_cancel' : 'broker_stop_gone', `${sym}: ${!alive ? 'стоп-заявка сработала или снята у брокера' : !w ? 'позиции стратегии нет' : 'объём или цена позиции изменились'}`);
|
|
3254
|
+
// стоп сработал у брокера: включаем 60-минутную паузу повторного входа и блокируем перевыставление стопа до обновления портфеля
|
|
3255
|
+
if (!alive) {
|
|
3256
|
+
const nowStopTs = Date.now();
|
|
3257
|
+
acc.brokerStopCooldown[sym] = nowStopTs;
|
|
3258
|
+
acc.exitCooldownBySymbol = acc.exitCooldownBySymbol || {};
|
|
3259
|
+
acc.exitCooldownBySymbol[sym] = nowStopTs;
|
|
3260
|
+
if (acc.entryTsBySymbol) delete acc.entryTsBySymbol[sym];
|
|
3261
|
+
if (acc.peakPriceBySymbol) delete acc.peakPriceBySymbol[sym];
|
|
3262
|
+
}
|
|
3240
3263
|
delete acc.brokerStopBySymbol[sym];
|
|
3241
3264
|
changed = true;
|
|
3242
3265
|
}
|
package/server/model/edge.js
CHANGED
|
@@ -49,6 +49,12 @@ export function isRegimeShocked(ds, i, t, side = 'long') {
|
|
|
49
49
|
if (p0 > 0 && p1 > 0 && m0 > 0 && m1 > 0) {
|
|
50
50
|
const ex1d = (p1 / p0 - 1) - (m1 / m0 - 1);
|
|
51
51
|
if (side === 'short' ? ex1d > REGIME_SHOCK_1D : ex1d < -REGIME_SHOCK_1D) return true;
|
|
52
|
+
// Защита от биржевой планки (верхней/нижней границы ценового коридора Мосбиржи):
|
|
53
|
+
// если при миллионном обороте за 10-минутную свечу все сделки идут по одной предельной цене (high === low) после сильного сдвига (>5% к индексу),
|
|
54
|
+
// встречная сторона в стакане отсутствует
|
|
55
|
+
if (tk.high?.[t] > 0 && tk.high[t] === tk.low[t] && tk.value?.[t] > 1e6) {
|
|
56
|
+
if (side === 'short' ? ex1d > 0.05 : ex1d < -0.05) return true;
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
59
|
if (side === 'long' && t >= 3) {
|
|
54
60
|
const t3 = t - 3;
|
|
@@ -93,9 +99,15 @@ const MSK = 3 * 3600000;
|
|
|
93
99
|
const DAY = 86400000;
|
|
94
100
|
export const SESSION_END_MINS_MSK = 23 * 60 + 40;
|
|
95
101
|
|
|
96
|
-
export const canEnterIntraday = (ts, horizonBars, barInterval = 10) => {
|
|
102
|
+
export const canEnterIntraday = (ts, horizonBars, barInterval = 10, side = null, symbol = null) => {
|
|
97
103
|
const minsMsk = Math.floor(((ts + MSK) % DAY) / 60000);
|
|
98
|
-
|
|
104
|
+
if (minsMsk > SESSION_END_MINS_MSK - horizonBars * barInterval) return false;
|
|
105
|
+
// В первые 20 минут утренней сессии (свечи 07:00 и 07:10 МСК) в бумагах 2-го эшелона стакан ещё пустой,
|
|
106
|
+
// а ночные корпоративные новости и утренние гэпы искажают часовой остаток z — шорты 2-го эшелона в это окно блокируются
|
|
107
|
+
if (side === 'short' && minsMsk >= 7 * 60 && minsMsk < 7 * 60 + 20 && (!symbol || !TIER1_LIQUID_SYMBOLS.has(symbol))) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
99
111
|
};
|
|
100
112
|
|
|
101
113
|
export const shouldExitBeforeNight = (ts, entryTs = 0, stepBars = 3, barInterval = 10) => {
|
package/server/model/forecast.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadDataset, barsFromHours, BAR_MS } from './dataset.js';
|
|
|
5
5
|
import { buildFold, MODELS, HEADS, KINDS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
|
|
6
6
|
import { loadProfiles } from './profiles.js';
|
|
7
7
|
import { isMarketDown } from './research.js';
|
|
8
|
-
import { ENTRY_EDGE_MULT, TIER2_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, REGIME_SHOCK_1D, REGIME_CRASH_30M } from './edge.js';
|
|
8
|
+
import { ENTRY_EDGE_MULT, TIER2_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, REGIME_SHOCK_1D, REGIME_CRASH_30M, canEnterIntraday } from './edge.js';
|
|
9
9
|
export { TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults };
|
|
10
10
|
import { newsForSymbols, blockingNews } from '../news/index.js';
|
|
11
11
|
import { liveDividendGuard } from './dividends.js';
|
|
@@ -403,13 +403,18 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
|
|
|
403
403
|
const tierMult = edgeMults[i] || symbolEdgeMult(tk.symbol);
|
|
404
404
|
const symNeedL = Math.max(needL * tierMult, 0.0030);
|
|
405
405
|
const symNeedS = Math.max(needS * tierMult, 0.0031);
|
|
406
|
-
// Аварийный предохранитель экстремальных свечей (±4,5% за одну 10-минутную свечу) и структурного слома режима (≥20% за день или обвал ≥9,5% за 30м):
|
|
406
|
+
// Аварийный предохранитель экстремальных свечей (±4,5% за одну 10-минутную свечу), биржевых планок и структурного слома режима (≥20% за день или обвал ≥9,5% за 30м):
|
|
407
407
|
// обычные рабочие импульсы 1,5–3,5% НЕ блокируются, так как именно на возврате к среднему после локального выброса модель формирует сигнал!
|
|
408
408
|
const EXTREME_BAR_MOVE_PCT = 4.5;
|
|
409
|
-
const
|
|
410
|
-
const
|
|
409
|
+
const isFlatLimitBar = tk.high?.[idx] > 0 && tk.high[idx] === tk.low[idx] && tk.value?.[idx] > 1e6;
|
|
410
|
+
const isLimitUpLocked = isFlatLimitBar && excess1d > 0.05;
|
|
411
|
+
const isLimitDownLocked = isFlatLimitBar && excess1d < -0.05;
|
|
412
|
+
const barDecisionTs = ds.timeline[idx] || Date.now();
|
|
413
|
+
const isMorningOpenShortBlocked = autoAdapt && !canEnterIntraday(barDecisionTs, H, 10, 'short', tk.symbol);
|
|
414
|
+
const isRegimeCrash = excess1d < -REGIME_SHOCK_1D || excess30m < -REGIME_CRASH_30M || isLimitDownLocked;
|
|
415
|
+
const isRegimeSqueeze = excess1d > REGIME_SHOCK_1D || isLimitUpLocked;
|
|
411
416
|
const isExtremeDip = Boolean(isRegimeCrash || (liveSnap && (lastMovePct < -EXTREME_BAR_MOVE_PCT || rawPriceMovePct < -EXTREME_BAR_MOVE_PCT)));
|
|
412
|
-
const isExtremeSpike = Boolean(isRegimeSqueeze || (liveSnap && (lastMovePct > EXTREME_BAR_MOVE_PCT || rawPriceMovePct > EXTREME_BAR_MOVE_PCT)));
|
|
417
|
+
const isExtremeSpike = Boolean(isRegimeSqueeze || isMorningOpenShortBlocked || (liveSnap && (lastMovePct > EXTREME_BAR_MOVE_PCT || rawPriceMovePct > EXTREME_BAR_MOVE_PCT)));
|
|
413
418
|
const liveBuyOk = !isExtremeDip && !buyBlockedByOverheat;
|
|
414
419
|
const liveShortOk = !isExtremeSpike;
|
|
415
420
|
// сигналы — только по бумагам из текущего состава индекса (исторические бумаги остаются в матрице рынка как контекст)
|
package/server/model/live.js
CHANGED
|
@@ -461,7 +461,7 @@ class LiveEngine {
|
|
|
461
461
|
};
|
|
462
462
|
const open = (x) => !s.held.has(x.symbol) && !closing.has(x.symbol);
|
|
463
463
|
const candL = (mktDown || !timeOk) ? [] : r.rows.filter((x) => x.signal && open(x)).sort((a, b) => b.expected - a.expected);
|
|
464
|
-
const candS = (dir === 'long' || !timeOk) ? [] : r.rows.filter((x) => x.shortSignal && open(x)).sort((a, b) => a.expected - b.expected);
|
|
464
|
+
const candS = (dir === 'long' || !timeOk) ? [] : r.rows.filter((x) => x.shortSignal && open(x) && (!autoAdapt || canEnterIntraday(ts, H, BAR_INTERVAL, 'short', x.symbol))).sort((a, b) => a.expected - b.expected);
|
|
465
465
|
const usedNow = [...s.held.entries()].filter(([sym]) => !closing.has(sym)).reduce((sum, [, h]) => sum + h.alloc, 0);
|
|
466
466
|
if (autoAdapt && usedNow > s.equity * 0.65 && (candL.length > 0 || candS.length > 0)) {
|
|
467
467
|
const bestNewEdge = Math.max(candL.length ? candL[0].expected : 0, candS.length ? Math.abs(candS[0].expected) : 0);
|
package/server/model/research.js
CHANGED
|
@@ -43,7 +43,8 @@ export function isMarketDown(ds, t) {
|
|
|
43
43
|
// 12: калибровка индивидуальных порогов 2–3 эшелона по ликвидности, защита от шорт-сквизов (≥20% за день) и обвалов (≥9,5% за 30м), отбор доступных шортов до Top-K
|
|
44
44
|
// 13: полная докачка левого края истории свечей (раньше при уже накопленных свечах 2022 год не догружался)
|
|
45
45
|
// 14: фиксация начала датасета современного 17-часового режима Мосбиржи (18.01.2024) и якоря фолдов после редомициляции (13.06.2024)
|
|
46
|
-
|
|
46
|
+
// 15: защита от биржевых планок (high === low) и блокировка утренних шортов 2-го эшелона в первые 20 минут сессии (07:00–07:20 МСК)
|
|
47
|
+
const DATA_VERSION = 15;
|
|
47
48
|
|
|
48
49
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
49
50
|
function reuseParamsKey(params) {
|
|
@@ -466,7 +467,7 @@ async function run(runId, p, state) {
|
|
|
466
467
|
const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
|
|
467
468
|
const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
|
|
468
469
|
const okLong = (i) => !divRule(i, t)?.blockLong && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
469
|
-
const okShort = (i) => !divRule(i, t)?.blockShort && (!shortable || shortable.has(tokens[i].symbol)) && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
|
|
470
|
+
const okShort = (i) => !divRule(i, t)?.blockShort && (!shortable || shortable.has(tokens[i].symbol)) && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs, H, BAR_INTERVAL, 'short', tokens[i].symbol)));
|
|
470
471
|
let order, entries;
|
|
471
472
|
if (dir !== 'long') {
|
|
472
473
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
@@ -1124,7 +1125,7 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1124
1125
|
const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
|
|
1125
1126
|
const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
|
|
1126
1127
|
const okLong = (i) => !divRule(i, t)?.blockLong && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
1127
|
-
const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
|
|
1128
|
+
const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs, H, BAR_INTERVAL, 'short', tokens[i].symbol)));
|
|
1128
1129
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
1129
1130
|
const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
|
|
1130
1131
|
let order, entries;
|