neuroalpha 0.6.8 → 0.6.9

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.8",
3
+ "version": "0.6.9",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
@@ -2055,8 +2055,9 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2055
2055
  const nowTs = Date.now();
2056
2056
  const effHorizon = autoAdapt ? 2.5 : params.modelHorizon;
2057
2057
  const H = barsFromHours(effHorizon);
2058
- const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
2059
- const timeOk = !autoAdapt || canEnterIntraday(nowTs, H, BAR_INTERVAL);
2058
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
2059
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
2060
+ const timeOk = !autoAdapt || canEnterIntraday(nowTs, holdBars, BAR_INTERVAL);
2060
2061
  const entryTsMap = acc.entryTsBySymbol || {};
2061
2062
  const exitCooldownMap = acc.exitCooldownBySymbol || {};
2062
2063
  const entryFailMap = acc.entryFailCooldownBySymbol || {};
@@ -2143,7 +2144,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2143
2144
  const buyCands = !entryTimeOk ? [] : fc.rows.filter((r) => canBuyRow(r) && !isInCooldown(r.symbol) && !isDayBlocked(r.symbol, 'buy') && !isQuarantinedFor(r, 'buy') && !nb?.get(r.symbol)?.bad)
2144
2145
  .filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockLong) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
2145
2146
  .sort((a, b) => b.expectedPct - a.expectedPct).slice(0, K);
2146
- const allShortSignals = (rawDir !== 'long' && entryTimeOk) ? fc.rows.filter((r) => canShortRow(r) && (!autoAdapt || canEnterIntraday(nowTs, H, BAR_INTERVAL, 'short', r.symbol)) && !isInCooldown(r.symbol) && !isDayBlocked(r.symbol, 'short') && !isQuarantinedFor(r, 'short') && !nb?.get(r.symbol)?.good)
2147
+ const allShortSignals = (rawDir !== 'long' && entryTimeOk) ? fc.rows.filter((r) => canShortRow(r) && (!autoAdapt || canEnterIntraday(nowTs, holdBars, BAR_INTERVAL, 'short', r.symbol)) && !isInCooldown(r.symbol) && !isDayBlocked(r.symbol, 'short') && !isQuarantinedFor(r, 'short') && !nb?.get(r.symbol)?.good)
2147
2148
  .filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockShort) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
2148
2149
  .sort((a, b) => a.expectedPct - b.expectedPct) : [];
2149
2150
  const nonShortableSkipped = [];
@@ -2193,8 +2194,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2193
2194
  const effEntryTs = Math.max(entryTsMap[sym] || 0, lastHistEntryTs);
2194
2195
  const heldMs = effEntryTs > 0 ? Math.max(0, nowTs - effEntryTs) : 0;
2195
2196
  const heldMin = heldMs / 60000;
2196
- const horizonMin = Math.max(90, effHorizon * 60);
2197
- // Плавный переход порога удержания от мягкого (0.35 до 45 мин) к динамическому плотному (0.95 × clamp(realizedBarPct / sigmaBarPct, 0.75, 1.25) к концу горизонта 2.5 ч)
2197
+ const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
2198
+ // Плавный переход порога удержания от мягкого (0.35 до 45 мин) к динамическому плотному (0.95 × clamp(realizedBarPct / sigmaBarPct, 0.75, 1.25) к концу глубины сопровождения)
2198
2199
  const symMult = r.edgeMult || symbolEdgeMult(sym);
2199
2200
  const holdRatio = exitHoldRatio(heldMin, horizonMin, {
2200
2201
  sigmaBarPct: Number(r.day?.sigmaBarPct || r.sigmaPct) || 0,
@@ -2584,7 +2585,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
2584
2585
  } else if (portfolioStale) {
2585
2586
  diagNote = `Сервис портфеля брокера временно отвечает с задержкой (${acc.lastSync?.staleReason || 'таймаут'}) — открытие новых позиций отложено до подтверждения остатков, текущие позиции и заявки под контролем.`;
2586
2587
  } else if (!timeOk) {
2587
- diagNote = `На свече ${fc.bar} вечернее окно новых входов закрыто: до конца сессии осталось меньше горизонта прогноза (${effHorizon} ч) — внутридневные сделки не переносятся через ночь.`;
2588
+ diagNote = `На свече ${fc.bar} вечернее окно новых входов закрыто: до конца сессии осталось меньше глубины сопровождения (${round((holdBars * BAR_INTERVAL) / 60, 2)} ч) — внутридневные сделки не переносятся через ночь.`;
2588
2589
  } else if (edgeWarn) {
2589
2590
  diagNote = `На свече ${fc.bar} новые входы временно отложены: ${edgeWarn.toLowerCase()}.`;
2590
2591
  } else if ((newBuys.length || newShorts.length) && availRub < 1500) {
@@ -3483,8 +3484,9 @@ async function tickBrokerSchedule() {
3483
3484
  const autoAdapt = params.modelAutoAdapt !== false;
3484
3485
  const effHorizon = autoAdapt ? 2.5 : params.modelHorizon;
3485
3486
  const H = barsFromHours(effHorizon);
3486
- // Внутричасовой перехват H/5 (каждые 30 мин) активен в режимах VIP и Хозяин; на тарифах Инвестор и Профи шаг равен полному горизонту H
3487
- const stepBars = ((plan.allowVipBoost || plan.isOwner) && autoAdapt) ? Math.max(3, Math.floor(H / 5)) : H;
3487
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
3488
+ // Внутричасовой перехват holdBars/5 (каждые 30 мин) активен в режимах VIP и Хозяин; на тарифах Инвестор и Профи шаг равен полному окну holdBars
3489
+ const stepBars = ((plan.allowVipBoost || plan.isOwner) && autoAdapt) ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
3488
3490
  const minStepMs = stepBars * BAR_MS;
3489
3491
  const rawLastFullWallTs = acc.lastFullStepWallTs || 0;
3490
3492
  const batchCreatedTs = acc.pendingBatch?.createdAt || 0;
package/server/index.js CHANGED
@@ -146,10 +146,23 @@ function lanUrls() {
146
146
  .map((a) => `http://${a.address}:${PORT}`);
147
147
  }
148
148
 
149
+ const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
150
+ || (process.env.OPEN_BROWSER !== '0'
151
+ && process.env.NO_BROWSER !== '1'
152
+ && !process.argv.includes('--no-browser')
153
+ && !process.execArgv.includes('--watch')
154
+ && !IS_CENTRAL
155
+ && (!IS_INSTANCE || IS_DESKTOP)
156
+ && (process.platform === 'win32' || process.platform === 'darwin' || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)));
157
+
149
158
  wss.on('error', () => {});
150
159
  server.on('error', (e) => {
151
- if (e.code === 'EADDRINUSE') console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент: http://localhost:${PORT}`);
152
- else console.error(e);
160
+ if (e.code === 'EADDRINUSE') {
161
+ console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент: http://localhost:${PORT}`);
162
+ if (shouldOpenBrowser) openBrowser(`http://localhost:${PORT}`);
163
+ } else {
164
+ console.error(e);
165
+ }
153
166
  process.exit(1);
154
167
  });
155
168
 
@@ -160,7 +173,7 @@ server.listen(PORT, LISTEN_HOST, () => {
160
173
  console.log(`ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (нет модели или ключа: Настройки → Модели ИИ)'}`);
161
174
  if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В локальной сети: ${url}`);
162
175
  console.log(`Клиент: http://localhost:${PORT}`);
163
- if (process.env.OPEN_BROWSER === '1') openBrowser(`http://localhost:${PORT}`);
176
+ if (shouldOpenBrowser) openBrowser(`http://localhost:${PORT}`);
164
177
  });
165
178
 
166
179
  function openBrowser(url) {
@@ -239,13 +239,14 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
239
239
  }
240
240
  return lastClosedBar >= latestClosedSlotTs;
241
241
  };
242
+ const cacheTtlMs = isTradingNow ? 4 * 60000 : 30 * 60000;
242
243
  const key = cacheKey(interval, fromTs, tillTs, useMacro, useDividends, localOnly);
243
244
  const hit = datasetCache.get(key);
244
- if (hit && now - hit.at < 4 * 60000 && isCacheFreshForSlot(hit)) return hit.ds;
245
+ if (hit && now - hit.at < cacheTtlMs && isCacheFreshForSlot(hit)) return hit.ds;
245
246
  // Если в памяти уже есть более широкий датасет (не localOnly), который полностью покрывает запрошенный диапазон [fromTs … tillTs], возвращаем его
246
247
  const modeKey = JSON.stringify([interval, useMacro ? 1 : 0, useDividends ? (process.env.DIVIDENDS_MODE || 1) : 0]);
247
248
  for (const v of datasetCache.values()) {
248
- if (v.modeKey === modeKey && (!v.localOnly || localOnly) && now - v.at < 4 * 60000 && Math.abs(v.fromTs - fromTs) < 2 * DAY && Math.abs(v.tillTs - tillTs) < 10 * 60000 && isCacheFreshForSlot(v)) {
249
+ if (v.modeKey === modeKey && (!v.localOnly || localOnly) && now - v.at < cacheTtlMs && Math.abs(v.fromTs - fromTs) < 2 * DAY && Math.abs(v.tillTs - tillTs) < 10 * 60000 && isCacheFreshForSlot(v)) {
249
250
  return v.ds;
250
251
  }
251
252
  }
@@ -263,7 +264,6 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
263
264
  const symbols = vocab.map((v) => v.symbol);
264
265
  const raw = {};
265
266
  let done = 0;
266
- const divStat = { applied: 0, skipped: 0 };
267
267
  const macroIds = useMacro ? MACRO.filter((m) => m.id !== 'BR').map((m) => m.id) : [];
268
268
  const all = [...symbols, 'IMOEX', ...macroIds];
269
269
  // акция дольше 2 минут — пропускаем и останавливаем её загрузку (скачанное останется в кэше)
@@ -284,9 +284,7 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
284
284
  raw[s] = compact(rows);
285
285
  if (divs?.length) {
286
286
  // DIVIDENDS_MODE=raw — прежняя корректировка без сверки с ценой (только для сравнения результатов)
287
- const st = adjustDividends(raw[s], divs, { verify: process.env.DIVIDENDS_MODE !== 'raw' });
288
- divStat.applied += st.applied;
289
- divStat.skipped += st.skipped;
287
+ adjustDividends(raw[s], divs, { verify: process.env.DIVIDENDS_MODE !== 'raw' });
290
288
  }
291
289
  })(),
292
290
  new Promise((_, rej) => {
@@ -306,7 +304,6 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
306
304
  onProgress({ symbol: s, done: ++done, total: all.length });
307
305
  }
308
306
  });
309
- if (useDividends) console.log(`[dataset] дивиденды: учтено ${divStat.applied}, пропущено (нет подтверждающего падения цены) ${divStat.skipped}`);
310
307
  // нефть Brent: склейка фьючерсов (много контрактов — отдельно от общего пула)
311
308
  if (useMacro) {
312
309
  try {
@@ -7,7 +7,7 @@ import { loadProfiles } from './profiles.js';
7
7
  import { ENTRY_EDGE_MULT, TIER2_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, REGIME_SHOCK_1D, REGIME_CRASH_30M, isStockQuarantined, canEnterIntraday, isMarketDown } from './edge.js';
8
8
  export { TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults };
9
9
  import { newsForSymbols, blockingNews } from '../news/index.js';
10
- import { dayContext } from './policy.js';
10
+ import { dayContext, STRATEGY_POLICY } from './policy.js';
11
11
  import { liveDividendGuard } from './dividends.js';
12
12
  import { fetchTbankRealtimePrices, getActiveAccountSignalFilters } from '../brokers/index.js';
13
13
  import { kvGet, kvSet } from '../db.js';
@@ -447,7 +447,8 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
447
447
  const isLimitUpLocked = isFlatLimitBar && excess1d > 0.05;
448
448
  const isLimitDownLocked = isFlatLimitBar && excess1d < -0.05;
449
449
  const barDecisionTs = ds.timeline[idx] || Date.now();
450
- const isMorningOpenShortBlocked = autoAdapt && !canEnterIntraday(barDecisionTs, H, 10, 'short', tk.symbol);
450
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
451
+ const isMorningOpenShortBlocked = autoAdapt && !canEnterIntraday(barDecisionTs, holdBars, 10, 'short', tk.symbol);
451
452
  const isRegimeCrash = excess1d < -REGIME_SHOCK_1D || excess30m < -REGIME_CRASH_30M || isLimitDownLocked;
452
453
  const isRegimeSqueeze = excess1d > REGIME_SHOCK_1D || isLimitUpLocked;
453
454
  const isExtremeDip = Boolean(isRegimeCrash || (liveSnap && (lastMovePct < -EXTREME_BAR_MOVE_PCT || rawPriceMovePct < -EXTREME_BAR_MOVE_PCT)));
@@ -939,10 +940,15 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
939
940
  inflight.delete(key);
940
941
  inflightStartedAt.delete(key);
941
942
  }
943
+ const mskDate = new Date(now + MSK);
944
+ const mskDow = mskDate.getUTCDay();
945
+ const mskMins = mskDate.getUTCHours() * 60 + mskDate.getUTCMinutes();
946
+ const isTradingWindow = mskDow >= 1 && mskDow <= 5 && mskMins >= 7 * 60 && mskMins <= 23 * 60 + 55;
942
947
  const latestClosedSlotTs = Math.floor((now - BAR_MS) / BAR_MS) * BAR_MS;
943
948
  const curBarTs = baseState.ds?.timeline?.[baseState.last] || 0;
944
- const barStale = curBarTs > 0 && curBarTs < latestClosedSlotTs && (now - baseState.dsLoadedAt >= 60000);
945
- if ((now - baseState.dsLoadedAt >= 4 * 60000 || barStale) && !inflight.has(key)) {
949
+ const barStale = isTradingWindow && curBarTs > 0 && curBarTs < latestClosedSlotTs && (now - baseState.dsLoadedAt >= 60000);
950
+ const dsTtlMs = isTradingWindow ? 4 * 60000 : 30 * 60000;
951
+ if ((now - baseState.dsLoadedAt >= dsTtlMs || barStale) && !inflight.has(key)) {
946
952
  setImmediate(() => {
947
953
  refreshDatasetStateInBackground(p, key).catch(() => {});
948
954
  });
@@ -7,6 +7,7 @@ import { latestBarTs } from '../data/moex.js';
7
7
  import { loadDataset, barsFromHours, BAR_MS, BAR_INTERVAL } from './dataset.js';
8
8
  import { predictAt, lastClosedIndex } from './forecast.js';
9
9
  import { ENTRY_EDGE_MULT, symbolEdgeMult, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isRegimeShocked } from './edge.js';
10
+ import { STRATEGY_POLICY } from './policy.js';
10
11
  import { computeReport, downsample } from './metrics.js';
11
12
  import { MODELS } from './models.js';
12
13
  import { blockingNews } from '../news/index.js';
@@ -395,7 +396,8 @@ class LiveEngine {
395
396
  const autoAdapt = p.modelAutoAdapt !== false;
396
397
  const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
397
398
  const H = barsFromHours(effHorizon);
398
- const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
399
+ const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
400
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
399
401
  if (s.barsSinceRebalance >= stepBars) {
400
402
  await new Promise((r) => setImmediate(r));
401
403
  const r = await predictAt(ds, p, idx);
@@ -442,7 +444,7 @@ class LiveEngine {
442
444
  const req = r.threshold * (autoAdapt && heldBars <= 2 * stepBars ? 0.35 : exitRatio) * (x?.edgeMult || symbolEdgeMult(sym));
443
445
  if (!(x && x.liquid && (h.side === 'short' ? x.expected < -req : x.expected > req))) closePos(sym);
444
446
  }
445
- const timeOk = !autoAdapt || canEnterIntraday(ts, H, BAR_INTERVAL);
447
+ const timeOk = !autoAdapt || canEnterIntraday(ts, holdBars, BAR_INTERVAL);
446
448
  const sideHeld = (side) => [...s.held].filter(([sym, h]) => h.side === side && !closing.has(sym));
447
449
  const nb = p.newsFilter ? blockingNews(6) : null;
448
450
  const take = (list, side, n) => {
@@ -462,7 +464,7 @@ class LiveEngine {
462
464
  };
463
465
  const open = (x) => !s.held.has(x.symbol) && !closing.has(x.symbol);
464
466
  const candL = (mktDown || !timeOk) ? [] : r.rows.filter((x) => x.signal && open(x)).sort((a, b) => b.expected - a.expected);
465
- 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);
467
+ const candS = (dir === 'long' || !timeOk) ? [] : r.rows.filter((x) => x.shortSignal && open(x) && (!autoAdapt || canEnterIntraday(ts, holdBars, BAR_INTERVAL, 'short', x.symbol))).sort((a, b) => a.expected - b.expected);
466
468
  const usedNow = [...s.held.entries()].filter(([sym]) => !closing.has(sym)).reduce((sum, [, h]) => sum + h.alloc, 0);
467
469
  if (autoAdapt && usedNow > s.equity * 0.65 && (candL.length > 0 || candS.length > 0)) {
468
470
  const bestNewEdge = Math.max(candL.length ? candL[0].expected : 0, candS.length ? Math.abs(candS[0].expected) : 0);
@@ -38,6 +38,12 @@ export const STRATEGY_POLICY = Object.freeze({
38
38
  // в спокойный день (множитель до 0.75×) не вытряхивает позицию раньше времени, в турбулентный день (до 1.25×) быстрее фиксирует выход при затухании прогноза.
39
39
  // Проверка 09.11.2026 (06.2024–11.2026, плечо 3×): CAGR 58,6%/52,1% по половинам (весь 55,1%, PF 1,97, DD 6,4%) против 57,5%/48,1% (весь 52,6%, PF 1,89, DD 6,8%).
40
40
  exitVolClamp: Object.freeze([0.75, 1.25]),
41
+ // глубина сопровождения позиции и вечерней отсечки входов в 10-минутных свечах (13 свечей = 130 минут = 2 ч 10 м),
42
+ // при сохранении базового мульти-горизонта обучения модели (H = 15 свечей = 2,5 ч) и калибровки порога входа √H.
43
+ // Проверка 12.11.2026 (06.2024–11.2026, 59 051 свеча): улучшает итог и снижает риск на обеих половинах истории при любом плече
44
+ // (при плече 2×: CAGR 54,5%/40,6% по половинам, весь 47,2%, PF 1,87, DD 3,4%, худшая сделка −3,54% против 51,9%/40,3%, весь 45,8%, PF 1,82, DD 3,6%, худшая −4,39%;
45
+ // при плече 3×: CAGR 65,9%/55,2% по половинам, весь 60,3%, PF 2,03, DD 6,0% против 61,9%/54,6%, весь 58,0%, PF 1,97, DD 6,3%).
46
+ holdBars: 13,
41
47
  });
42
48
 
43
49
  // ---------- окна начала и конца сессий (широкий спред, гэпы, аукционы) ----------
@@ -36,8 +36,9 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
36
36
  const N = tokens.length;
37
37
  const H = wf.H;
38
38
  const sqrtH = Math.sqrt(H);
39
+ const holdBars = policy.holdBars ?? H;
39
40
  const bpd = Math.max(1, Math.round(ds.barsPerDay || 94));
40
- const stepBars = plan === 'owner' || plan === 'vip' ? Math.max(3, Math.floor(H / 5)) : H;
41
+ const stepBars = plan === 'owner' || plan === 'vip' ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
41
42
  const K = policy.K ?? 4;
42
43
  const dir = p.modelDirection === 'long' ? 'long' : 'longshort';
43
44
  const realCost = (p.commissionPct + p.slippagePct) / 100;
@@ -48,7 +49,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
48
49
  const commission = p.commissionPct / 100;
49
50
  const cooldownBars = Math.round((policy.cooldownMin ?? 30) / BAR_INTERVAL);
50
51
  const stopCooldownBars = Math.round((policy.stopCooldownMin ?? 60) / BAR_INTERVAL);
51
- const horizonMin = Math.max(90, (H * BAR_INTERVAL));
52
+ const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
52
53
  const slotW = policy.slotPct ?? (plan === 'owner' ? 0.45 : plan === 'vip' ? 0.40 : plan === 'pro' ? Math.min(0.28, 1.15 / 8) : Math.min(0.24, 1.05 / 5));
53
54
  const convMax = plan === 'owner' ? 1.65 : plan === 'vip' ? 1.30 : 1;
54
55
  const convK = plan === 'owner' ? 0.30 : 0.25;
@@ -275,7 +276,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
275
276
  dayDdBreachCount = 0;
276
277
  }
277
278
  }
278
- const timeOk = !dayHalted && canEnterIntraday(decTs, H, BAR_INTERVAL) && !sessionEdgeWarning(decTs) && !nightNow && (policy.entryEveryBar !== false || isStep);
279
+ const timeOk = !dayHalted && canEnterIntraday(decTs, holdBars, BAR_INTERVAL) && !sessionEdgeWarning(decTs) && !nightNow && (policy.entryEveryBar !== false || isStep);
279
280
  if (timeOk) {
280
281
  const baseOk = (i, edgeRatio = 0) => !held.has(i) && cooldownUntil[i] <= t && liquid(i) && (!tokens[i].inIndex || tokens[i].inIndex[t] === 1) && !isStockQuarantined(stockCnt[i], stockPnl[i], edgeRatio) && (policy.tier2 !== false || isT1(i));
281
282
  const candL = [];
@@ -292,7 +293,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
292
293
  } else if (dir !== 'long' && e < -thrS) {
293
294
  if (!baseOk(i, (-e) / thrS)) continue;
294
295
  if (shortable && !shortable.has(tokens[i].symbol)) continue;
295
- if (!canEnterIntraday(decTs, H, BAR_INTERVAL, 'short', tokens[i].symbol)) continue;
296
+ if (!canEnterIntraday(decTs, holdBars, BAR_INTERVAL, 'short', tokens[i].symbol)) continue;
296
297
  const fl = flags(i);
297
298
  if (fl.spikeBlock || divRule(i, t)?.blockShort) continue;
298
299
  candS.push(i);
@@ -335,7 +336,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
335
336
  const sigmaBarPct = sg[i] * 100;
336
337
  const ctx = dayContext(ds, i, t, sigmaBarPct);
337
338
  if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
338
- const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: H }, policy);
339
+ const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
339
340
  const conv = Math.min(convMax, Math.max(0.85, 0.80 + convK * (Math.abs(e) / need)));
340
341
  const tm = isT1(i) ? (policy.sizeMultT1 ?? lev) : (policy.sizeMultT2 ?? 1);
341
342
  if (!(tm > 0)) continue;