neuroalpha 0.6.8 → 0.6.10
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 +10 -8
- package/server/index.js +16 -3
- package/server/model/dataset.js +4 -7
- package/server/model/forecast.js +10 -4
- package/server/model/live.js +5 -3
- package/server/model/policy.js +6 -0
- package/server/model/research.js +23 -15
- package/server/model/simulator.js +6 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -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
|
|
2059
|
-
const
|
|
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,
|
|
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,
|
|
2197
|
-
// Плавный переход порога удержания от мягкого (0.35 до 45 мин) к динамическому плотному (0.95 × clamp(realizedBarPct / sigmaBarPct, 0.75, 1.25) к концу
|
|
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} вечернее окно новых входов закрыто: до конца сессии осталось меньше
|
|
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
|
-
|
|
3487
|
-
|
|
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')
|
|
152
|
-
|
|
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 (
|
|
176
|
+
if (shouldOpenBrowser) openBrowser(`http://localhost:${PORT}`);
|
|
164
177
|
});
|
|
165
178
|
|
|
166
179
|
function openBrowser(url) {
|
package/server/model/dataset.js
CHANGED
|
@@ -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 <
|
|
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 <
|
|
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
|
-
|
|
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 {
|
package/server/model/forecast.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
});
|
package/server/model/live.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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);
|
package/server/model/policy.js
CHANGED
|
@@ -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
|
// ---------- окна начала и конца сессий (широкий спред, гэпы, аукционы) ----------
|
package/server/model/research.js
CHANGED
|
@@ -12,6 +12,7 @@ import { strategyDefaults } from '../config.js';
|
|
|
12
12
|
import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isRegimeShocked, isMarketDown } from './edge.js';
|
|
13
13
|
import { buildDividendCalendar, nextDividendAt, dividendRules, DIV_TAX } from './dividends.js';
|
|
14
14
|
import { simulateStrategy, alignSeries } from './simulator.js';
|
|
15
|
+
import { STRATEGY_POLICY, exitHoldRatio } from './policy.js';
|
|
15
16
|
import { ensureLqdtHistory } from '../data/lqdt.js';
|
|
16
17
|
|
|
17
18
|
const DAY = 86400000;
|
|
@@ -38,7 +39,8 @@ export { isMarketDown };
|
|
|
38
39
|
// 15: защита от биржевых планок (high === low) и блокировка утренних шортов 2-го эшелона в первые 20 минут сессии (07:00–07:20 МСК)
|
|
39
40
|
// 16: главная оценка — симулятор с правилами живого контура (simulator.js), аварийный стоп 7%
|
|
40
41
|
// 17: капитал в LQDT как залог, половинный размер 2-го эшелона, строки с плечом 3 и 5
|
|
41
|
-
|
|
42
|
+
// 18: глубина сопровождения позиции и вечернего окна входов holdBars=13 (130 мин) во всех контурах симуляции
|
|
43
|
+
const DATA_VERSION = 18;
|
|
42
44
|
|
|
43
45
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
44
46
|
function reuseParamsKey(params) {
|
|
@@ -395,7 +397,9 @@ async function run(runId, p, state) {
|
|
|
395
397
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
396
398
|
const stockAllowed = (i, edgeRatio = 0) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i], edgeRatio);
|
|
397
399
|
let curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
398
|
-
const
|
|
400
|
+
const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
|
|
401
|
+
const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
|
|
402
|
+
const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
|
|
399
403
|
const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
|
|
400
404
|
let closedThisBar = new Set();
|
|
401
405
|
|
|
@@ -458,21 +462,21 @@ async function run(runId, p, state) {
|
|
|
458
462
|
}
|
|
459
463
|
}
|
|
460
464
|
}
|
|
461
|
-
const timeOk = !autoAdapt || canEnterIntraday(barTs,
|
|
465
|
+
const timeOk = !autoAdapt || canEnterIntraday(barTs, holdBars, BAR_INTERVAL);
|
|
462
466
|
const canOpenAt = (i, edgeRatio = 0) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i, edgeRatio) && canEnter(i, t);
|
|
463
467
|
const okLong = (i) => !divRule(i, t)?.blockLong && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
464
|
-
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,
|
|
468
|
+
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, holdBars, BAR_INTERVAL, 'short', tokens[i].symbol)));
|
|
465
469
|
let order, entries;
|
|
466
470
|
if (dir !== 'long') {
|
|
467
471
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
468
472
|
const need = effMinEdge * 2 * realCost * needMult;
|
|
469
473
|
const exitNeed = need * curExitRatio;
|
|
470
|
-
|
|
471
|
-
// держим позицию, пока ожидаемое движение выше порога выхода (первые два полушага — мягкий порог, чтобы не резать сделку от шума)
|
|
474
|
+
// держим позицию по единой рампе удержания exitHoldRatio (до 45 мин — 0.35, затем плавный подъём к 0.95 к концу глубины сопровождения horizonMin)
|
|
472
475
|
for (const [i, h] of [...held]) {
|
|
473
476
|
const e = edge(i);
|
|
474
477
|
const barsHeld = tIn - h.entryT;
|
|
475
|
-
const
|
|
478
|
+
const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
|
|
479
|
+
const req = need * holdRatio * symMult[i];
|
|
476
480
|
const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
|
|
477
481
|
if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) {
|
|
478
482
|
closePos(i, h, tIn, false, shocked ? 'Защита от экстремального сквиза/обвала' : 'Прогноз ослаб');
|
|
@@ -513,11 +517,11 @@ async function run(runId, p, state) {
|
|
|
513
517
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
514
518
|
const need = effMinEdge * 2 * realCost * needMult;
|
|
515
519
|
const exitNeed = need * curExitRatio;
|
|
516
|
-
const earlyExitNeed = need * 0.35;
|
|
517
520
|
for (const [i, h] of [...held]) {
|
|
518
521
|
const e = edge(i);
|
|
519
522
|
const barsHeld = tIn - h.entryT;
|
|
520
|
-
const
|
|
523
|
+
const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
|
|
524
|
+
const req = need * holdRatio * symMult[i];
|
|
521
525
|
const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
|
|
522
526
|
if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn, false, shocked ? 'Защита от экстремального обвала' : '');
|
|
523
527
|
}
|
|
@@ -1120,7 +1124,9 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1120
1124
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
1121
1125
|
const stockAllowed = (i, edgeRatio = 0) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i], edgeRatio);
|
|
1122
1126
|
const curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
1123
|
-
const
|
|
1127
|
+
const holdBars = autoAdapt ? (STRATEGY_POLICY.holdBars ?? H) : H;
|
|
1128
|
+
const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
|
|
1129
|
+
const stepBars = autoAdapt ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
|
|
1124
1130
|
const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
|
|
1125
1131
|
let closedThisBar = new Set();
|
|
1126
1132
|
const closePos = (i, h, t) => {
|
|
@@ -1158,17 +1164,18 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1158
1164
|
if (shouldExitBeforeNight(barTs, tsOf(h.entryT), stepBars, BAR_INTERVAL)) closePos(i, h, tIn);
|
|
1159
1165
|
}
|
|
1160
1166
|
}
|
|
1161
|
-
const timeOk = !autoAdapt || canEnterIntraday(barTs,
|
|
1167
|
+
const timeOk = !autoAdapt || canEnterIntraday(barTs, holdBars, BAR_INTERVAL);
|
|
1162
1168
|
const canOpenAt = (i, edgeRatio = 0) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i, edgeRatio) && canEnter(i, t);
|
|
1163
1169
|
const okLong = (i) => !divRule(i, t)?.blockLong && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
1164
|
-
const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs,
|
|
1170
|
+
const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || (!isRegimeShocked(ds, i, t, 'short') && canEnterIntraday(barTs, holdBars, BAR_INTERVAL, 'short', tokens[i].symbol)));
|
|
1165
1171
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
1166
|
-
const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio
|
|
1172
|
+
const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio;
|
|
1167
1173
|
let order, entries;
|
|
1168
1174
|
if (dir !== 'long') {
|
|
1169
1175
|
for (const [i, h] of [...held]) {
|
|
1170
1176
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
1171
|
-
const
|
|
1177
|
+
const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
|
|
1178
|
+
const req = need * holdRatio * symMult[i];
|
|
1172
1179
|
const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
|
|
1173
1180
|
if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
1174
1181
|
}
|
|
@@ -1196,7 +1203,8 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1196
1203
|
} else if (effMinEdge > 0) {
|
|
1197
1204
|
for (const [i, h] of [...held]) {
|
|
1198
1205
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
1199
|
-
const
|
|
1206
|
+
const holdRatio = exitHoldRatio(barsHeld * BAR_INTERVAL, horizonMin, { baseEndRatio: curExitRatio, autoAdapt }, STRATEGY_POLICY);
|
|
1207
|
+
const req = need * holdRatio * symMult[i];
|
|
1200
1208
|
const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
|
|
1201
1209
|
if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
1202
1210
|
}
|
|
@@ -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(
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
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;
|