neuroalpha 0.3.13 → 0.3.15

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.
@@ -1,21 +1,127 @@
1
1
  // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import { normalizeParams } from '../settings.js';
3
- import { round, DAY, fmtBarMsk } from '../util.js';
3
+ import { round, DAY, MSK, fmtBarMsk } from '../util.js';
4
4
  import { loadDataset, barsFromHours, BAR_MS } from './dataset.js';
5
- import { buildFold, MODELS, HEADS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
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
8
  import { newsForSymbols } from '../news/index.js';
9
9
 
10
+ const trainedModelCache = new Map(); // fitKey -> { dayMsk, trainedAt, fTrain, fitted, snap }
10
11
  let baseState = null; // { key, trainedAt, ds, last, r, snap, prevExpMap, prevRankMap, lastTickResult, lastTickAt }
11
12
  const inflight = new Map();
12
13
 
14
+ // Быстрый расчёт признаков контекста в одной точке idx без переобучения матрицы внимания и весов (за ~1-2 мс)
15
+ function buildInferenceFoldAt(ds, idx, fTrain, fitted, liveSnap = null) {
16
+ const { tokens, mret, mclose } = ds;
17
+ const N = fTrain.N;
18
+ const { beta, sigma, cos } = fTrain;
19
+ const lo = Math.max(1, idx - FEATURE_WARMUP - 2);
20
+ const u = idx - lo;
21
+ const W = u + 1;
22
+
23
+ const z = new Float64Array(W * N);
24
+ const vr = new Float64Array(W * N);
25
+ for (let uk = 0; uk < W; uk++) {
26
+ const t = lo + uk;
27
+ const isLast = uk === u && liveSnap?.prices?.size > 0;
28
+ const mPrev = mclose?.[t - 1] || 0;
29
+ const mt = isLast && liveSnap.imoexPrice > 0 && mPrev > 0 ? Math.log(liveSnap.imoexPrice / mPrev) : mret[t];
30
+ for (let i = 0; i < N; i++) {
31
+ const tk = tokens[i];
32
+ let rt = tk.ret[t];
33
+ if (isLast) {
34
+ const prevPx = tk.close[t - 1];
35
+ const livePx = liveSnap.prices.get(tk.symbol);
36
+ if (livePx > 0 && prevPx > 0) rt = Math.log(livePx / prevPx);
37
+ }
38
+ const e = rt - beta[i] * mt;
39
+ z[uk * N + i] = Math.max(-8, Math.min(8, e / sigma[i]));
40
+ vr[uk * N + i] = Math.min(4, tk.valRatio[t]);
41
+ }
42
+ }
43
+
44
+ const X = KINDS.map(() => SCALES.map(() => new Float32Array(W * N)));
45
+ SCALES.forEach((sc, si) => {
46
+ const Xp = X[0][si], Xf = X[1][si];
47
+ const invP = 1 / Math.sqrt(sc.len), invF = invP / 1.5;
48
+ for (let i = 0; i < N; i++) {
49
+ let sp = 0, sf = 0;
50
+ for (let k = 0; k < sc.len; k++) {
51
+ const uk = u - sc.from - k;
52
+ if (uk < 0) break;
53
+ const zk = z[uk * N + i];
54
+ sp += zk;
55
+ sf += zk * vr[uk * N + i];
56
+ }
57
+ Xp[u * N + i] = sp * invP;
58
+ Xf[u * N + i] = sf * invF;
59
+ }
60
+ });
61
+
62
+ let macro = null;
63
+ if (fTrain.macro?.list?.length && ds.macro?.length) {
64
+ const list = fTrain.macro.list.map((mTrain) => {
65
+ const dm = ds.macro.find((x) => x.id === mTrain.id);
66
+ const zs = new Float32Array(SCALES.length * W);
67
+ if (dm) {
68
+ const msig = mTrain.msig || 1e-3;
69
+ SCALES.forEach((sc, si) => {
70
+ let sum = 0;
71
+ for (let k = 0; k < sc.len; k++) {
72
+ const t = idx - sc.from - k;
73
+ if (t >= 0) sum += Math.max(-8, Math.min(8, dm.ret[t] / msig));
74
+ }
75
+ zs[si * W + u] = sum / Math.sqrt(sc.len);
76
+ });
77
+ }
78
+ return { id: mTrain.id, label: mTrain.label, b: mTrain.b, zs };
79
+ });
80
+ macro = { list };
81
+ }
82
+
83
+ const f = { N, lo, hi: idx + 1, W, beta, sigma, z, X, cos, emb: fTrain.emb, embN: fTrain.embN, macro };
84
+ const att = fitted.attention;
85
+ let fittedForStep = fitted;
86
+ if (att?.As) {
87
+ const F = HEADS.map(() => new Float32Array(W * N));
88
+ const off = u * N;
89
+ for (let hi = 0; hi < HEADS.length; hi++) {
90
+ const h = HEADS[hi];
91
+ const bIdx = BETAS.findIndex((b) => b.v === h.beta);
92
+ const A = att.As[bIdx];
93
+ const Xh = X[h.kind === 'price' ? 0 : 1][h.scale];
94
+ const Fh = F[hi];
95
+ for (let j = 0; j < N; j++) {
96
+ let s = 0;
97
+ const row = j * N;
98
+ for (let i = 0; i < N; i++) s += A[row + i] * Xh[off + i];
99
+ Fh[off + j] = s;
100
+ }
101
+ }
102
+ fittedForStep = { ...fitted, attention: { ...att, F } };
103
+ }
104
+ return { f, u, fitted: fittedForStep };
105
+ }
106
+
13
107
  export const lastClosedIndex = (ds, now = Date.now()) => {
14
108
  let last = ds.T - 1;
15
109
  while (last > 0 && ds.timeline[last] + BAR_MS > now) last--;
16
110
  return last;
17
111
  };
18
112
 
113
+ export const isModelTrainedToday = (p, now = Date.now()) => {
114
+ const autoAdapt = p.modelAutoAdapt !== false;
115
+ const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
116
+ const chosen = p.modelType in MODELS ? p.modelType : 'attention';
117
+ const todayMsk = new Date(now + MSK).toISOString().slice(0, 10);
118
+ const prefix = JSON.stringify([autoAdapt, chosen, effHorizon, p.modelTrainDays, p.modelEmbedDim, p.modelUseMacro, p.modelUseDividends, p.modelTextWeight]).slice(0, -1);
119
+ for (const [k, v] of trainedModelCache) {
120
+ if (v.dayMsk === todayMsk && k.startsWith(prefix)) return true;
121
+ }
122
+ return false;
123
+ };
124
+
19
125
  // Быстрое получение живых цен Мосбиржи (TQBR + IMOEX) за 1 лёгкий запрос (~80 мс)
20
126
  async function fetchLiveMoexSnapshot() {
21
127
  try {
@@ -107,28 +213,63 @@ function applyLivePricesToFold(ds, f, u, fitted, liveSnap) {
107
213
  }
108
214
  }
109
215
 
110
- // Обучение на окне, заканчивающемся свечой idx, и прогноз по этой свече.
111
- // Возвращает строки по всем акциям: ожидаемое движение (относительно рынка), сигнал покупки, кто влияет.
112
- export async function predictAt(ds, p, idx, { explain = true, liveSnap = null } = {}) {
216
+ // Обучение ядра модели (карта рынка + веса внимания) выполняется не чаще 1 раза в торговый день для каждого набора параметров.
217
+ // На всех свечах внутри дня выполняется быстрый расчёт прогноза по уже обученным весам (buildInferenceFoldAt).
218
+ export async function predictAt(ds, p, idx, { explain = true, liveSnap = null, forceTrain = false } = {}) {
113
219
  const autoAdapt = p.modelAutoAdapt !== false;
114
220
  const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
115
221
  const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
116
222
  const H = barsFromHours(effHorizon);
117
223
  const maxH = autoAdapt ? Math.round(H * 1.4) : H;
224
+ const chosen = p.modelType in MODELS ? p.modelType : 'attention';
225
+ const fitKey = JSON.stringify([autoAdapt, chosen, effHorizon, p.modelTrainDays, p.modelEmbedDim, p.modelUseMacro, p.modelUseDividends, p.modelTextWeight, ds.tokens.length]);
226
+ const barDayMsk = new Date((ds.timeline[idx] || Date.now()) + MSK).toISOString().slice(0, 10);
227
+
228
+ const cached = trainedModelCache.get(fitKey);
229
+ if (!forceTrain && cached && cached.dayMsk === barDayMsk && cached.fTrain.N === ds.tokens.length) {
230
+ const inf = buildInferenceFoldAt(ds, idx, cached.fTrain, cached.fitted, liveSnap);
231
+ return {
232
+ ...buildPredictionRows(ds, p, idx, inf.f, inf.u, inf.fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap),
233
+ snap: cached.snap,
234
+ trainedAt: cached.trainedAt,
235
+ };
236
+ }
237
+
118
238
  const trainLen = Math.round(p.modelTrainDays * ds.barsPerDay);
119
239
  const trainStart = Math.max(FEATURE_WARMUP + maxH + 2, idx - trainLen);
120
- // «текстовая» часть карты: устойчивые описания бизнеса (профили готовятся отдельно, здесь только читаем)
121
240
  const profiles = p.modelTextWeight > 0 ? loadProfiles() : null;
122
241
  const f = buildFold(ds, { trainStart, testStart: idx, testEnd: idx + 1, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
123
242
  const u = idx - f.lo;
124
- const chosen = p.modelType in MODELS ? p.modelType : 'attention';
125
243
  const list = [...new Set(explain ? Object.keys(MODELS) : ['attention', chosen])];
126
244
  const fitted = {};
127
245
  for (const n of list) {
128
246
  fitted[n] = await MODELS[n].fit(f);
129
247
  }
248
+ const snap = mapSnapshot(f);
249
+ // Освобождаем тяжёлые полные 90-дневные буферы признаков из памяти, оставляя только структурное ядро модели на день
250
+ const fTrain = {
251
+ N: f.N,
252
+ beta: f.beta,
253
+ sigma: f.sigma,
254
+ cos: f.cos,
255
+ emb: f.emb,
256
+ embN: f.embN,
257
+ eigenvalues: f.eigenvalues,
258
+ macro: f.macro ? { list: f.macro.list.map((m) => ({ id: m.id, label: m.label, b: m.b })) } : null,
259
+ };
260
+ const lightFitted = {
261
+ ...fitted,
262
+ ...(fitted.attention ? { attention: { ...fitted.attention, F: null } } : {}),
263
+ };
264
+ const trainedAt = Date.now();
265
+ trainedModelCache.set(fitKey, { dayMsk: barDayMsk, trainedAt, fTrain, fitted: lightFitted, snap });
266
+
130
267
  if (liveSnap) applyLivePricesToFold(ds, f, u, fitted, liveSnap);
131
- return buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap);
268
+ return {
269
+ ...buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap),
270
+ snap,
271
+ trainedAt,
272
+ };
132
273
  }
133
274
 
134
275
  function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, autoAdapt, explain, liveSnap = null) {
@@ -210,6 +351,14 @@ function attachDeltas(rows, stateObj) {
210
351
  }
211
352
  }
212
353
 
354
+ const fmtLiveBarLabel = (barTs, liveSnapTs) => {
355
+ if (liveSnapTs && (!barTs || liveSnapTs > barTs + BAR_MS)) {
356
+ const liveTime = fmtBarMsk(liveSnapTs).slice(-5);
357
+ return `${fmtBarMsk(barTs || liveSnapTs)} · котировки ${liveTime}`;
358
+ }
359
+ return fmtBarMsk(barTs || liveSnapTs || Date.now());
360
+ };
361
+
213
362
  // Прогноз на ближайшие H часов с живым поминутным обновлением котировок (без переобучения тяжёлого окна каждую минуту)
214
363
  export async function forecastNow(input, { force = false, onProgress } = {}) {
215
364
  const p = normalizeParams(input);
@@ -223,18 +372,21 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
223
372
  if (!force && baseState && baseState.key === key && baseState.lastTickResult && now - baseState.lastTickAt < 15000) {
224
373
  return withNews(baseState.lastTickResult, p);
225
374
  }
226
- // 2) Если базовая модель обучена менее 10 минут назад и нет force — делаем быстрый живой пересчёт (~80 мс запрос + 2 мс математика)
227
- if (!force && baseState && baseState.key === key && now - baseState.trainedAt < 10 * 60000) {
375
+ // 2) Если срез свечей обновлялся менее 2 минут назад и нет force — делаем быстрый живой пересчёт (~80 мс запрос + 2 мс математика)
376
+ if (!force && baseState && baseState.key === key && now - baseState.dsLoadedAt < 2 * 60000) {
228
377
  const liveSnap = await fetchLiveMoexSnapshot();
229
378
  if (liveSnap) applyLivePricesToFold(baseState.ds, baseState.f, baseState.u, baseState.fitted, liveSnap);
230
379
  const H = barsFromHours(effHorizon);
231
380
  const r = buildPredictionRows(baseState.ds, p, baseState.last, baseState.f, baseState.u, baseState.fitted, baseState.chosen, H, effMinEdge, autoAdapt, true, liveSnap);
232
381
  const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: baseState.snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
233
382
  attachDeltas(sortedRows, baseState);
383
+ const liveTs = liveSnap?.ts || now;
384
+ const barTs = baseState.ds.timeline[baseState.last];
234
385
  const result = {
235
386
  ...baseState.lastTickResult,
236
387
  at: now,
237
- liveTs: liveSnap?.ts || now,
388
+ liveTs,
389
+ bar: fmtLiveBarLabel(barTs, liveSnap ? liveTs : null),
238
390
  rows: sortedRows,
239
391
  };
240
392
  baseState.lastTickResult = result;
@@ -244,21 +396,26 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
244
396
 
245
397
  if (!force && inflight.has(key)) return withNews(await inflight.get(key), p);
246
398
  const task = (async () => {
399
+ const trainedToday = !force && isModelTrainedToday(p, now);
400
+ const windowDays = trainedToday ? 7 : Math.ceil(p.modelTrainDays * 1.5 + 25);
247
401
  const [ds, liveSnap] = await Promise.all([
248
- loadDataset({ fromTs: now - Math.ceil(p.modelTrainDays * 1.5 + 25) * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress }),
402
+ loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, onProgress }),
249
403
  fetchLiveMoexSnapshot(),
250
404
  ]);
251
405
  const last = lastClosedIndex(ds, now);
252
- const r = await predictAt(ds, p, last, { explain: true, liveSnap });
253
- const snap = mapSnapshot(r.f);
406
+ const r = await predictAt(ds, p, last, { explain: true, liveSnap, forceTrain: Boolean(force) });
407
+ const snap = r.snap || mapSnapshot(r.f);
254
408
  const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
255
409
  const stateObj = baseState && baseState.key === key ? baseState : { prevExpMap: null, prevRankMap: null };
256
410
  attachDeltas(sortedRows, stateObj);
411
+ const liveTs = liveSnap?.ts || now;
412
+ const barTs = ds.timeline[last];
257
413
  const result = {
258
414
  at: now,
259
- liveTs: liveSnap?.ts || now,
260
- barTs: ds.timeline[last],
261
- bar: fmtBarMsk(ds.timeline[last]),
415
+ trainedAt: r.trainedAt || now,
416
+ liveTs,
417
+ barTs,
418
+ bar: fmtLiveBarLabel(barTs, liveSnap ? liveTs : null),
262
419
  horizon: effHorizon, direction: p.modelDirection, model: r.chosen, thresholdPct: round(r.threshold * 100, 3), marketDown: r.marketDown,
263
420
  rows: sortedRows,
264
421
  map: { symbols: ds.tokens.map((t) => t.symbol), names: ds.tokens.map((t) => t.name), pts: snap.pts.map(([x, y]) => [round(x, 3), round(y, 3)]), cl: snap.clusters },
@@ -266,7 +423,8 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
266
423
  };
267
424
  baseState = {
268
425
  key,
269
- trainedAt: now,
426
+ trainedAt: r.trainedAt || now,
427
+ dsLoadedAt: now,
270
428
  ds,
271
429
  last,
272
430
  f: r.f,
@@ -5,7 +5,7 @@ import { round, DAY, MSK, fmtBarMsk } from '../util.js';
5
5
  import { hub } from '../hub.js';
6
6
  import { latestBarTs } from '../data/moex.js';
7
7
  import { loadDataset, barsFromHours, BAR_MS, BAR_INTERVAL } from './dataset.js';
8
- import { predictAt, lastClosedIndex } from './forecast.js';
8
+ import { predictAt, lastClosedIndex, isModelTrainedToday } from './forecast.js';
9
9
  import { computeReport, downsample } from './metrics.js';
10
10
  import { MODELS } from './models.js';
11
11
  import { blockingNews } from '../news/index.js';
@@ -90,12 +90,32 @@ class LiveEngine {
90
90
  const status = returnPct < expPct - bandPct ? 'below' : returnPct > expPct + bandPct ? 'above' : 'inside';
91
91
  corridor = { runId: s.benchmark.runId, expectedPct: expPct, bandPct, minPct: round(expPct - bandPct, 1), maxPct: round(expPct + bandPct, 1), histReturnPct: s.benchmark.returnPct, histMaxDdPct: s.benchmark.maxDrawdownPct, status };
92
92
  }
93
+ const tradesList = [...(s.trades || [])].reverse().slice(0, 120).map((t) => ({
94
+ symbol: t.symbol,
95
+ side: t.side || 'long',
96
+ qty: t.qty || 0,
97
+ entryTs: t.entryTs,
98
+ exitTs: t.exitTs,
99
+ entryPrice: t.entryPrice || 0,
100
+ exitPrice: t.exitPrice || 0,
101
+ pnl: round(Number(t.pnl || 0), 2),
102
+ pnlPct: round(Number(t.pnlPct || 0), 2),
103
+ commission: round(Number(t.commission || 0), 2),
104
+ barsHeld: t.barsHeld || 0,
105
+ reasonEntry: t.reasonEntry || '',
106
+ reasonExit: t.reasonExit || '',
107
+ }));
108
+ const realizedPnl = round((s.trades || []).reduce((sum, t) => sum + (Number(t.pnl) || 0), 0), 2);
109
+ const totalCommission = round((s.trades || []).reduce((sum, t) => sum + (Number(t.commission) || 0), 0), 2);
93
110
  return {
94
111
  running: s.running, runId: s.runId, name: s.name || `Виртуальный счёт №${s.runId}`,
95
112
  baseRunId: s.baseRunId || null, benchmark: s.benchmark || null, corridor,
96
113
  params: s.params, startedAt: s.startedAt, stoppedAt: s.stoppedAt || null, lastBarTs: s.lastBarTs,
97
114
  error: s.error, busy: s.busy, snapshot: snap, returnPct, pnl,
98
- recent: s.recent.slice(0, 20), metrics: s.metrics,
115
+ realizedPnl, totalCommission,
116
+ trades: tradesList,
117
+ curve: downsample(s.curve || [], 120),
118
+ recent: s.recent.slice(0, 25), metrics: s.metrics,
99
119
  };
100
120
  }
101
121
 
@@ -179,7 +199,9 @@ class LiveEngine {
179
199
  if (!run || run.mode !== 'paper' || !st.params?.modelHorizon) continue;
180
200
  const isRunning = Boolean(st.running && run.status === 'running');
181
201
  const trades = getTrades(st.runId).map((t) => ({
182
- symbol: t.symbol, side: t.side, pnl: t.pnl, pnlPct: t.pnl_pct, entryTs: t.entry_ts, exitTs: t.exit_ts, commission: t.commission,
202
+ symbol: t.symbol, side: t.side, qty: t.qty, entryPrice: t.entry_price, exitPrice: t.exit_price,
203
+ pnl: t.pnl, pnlPct: t.pnl_pct, entryTs: t.entry_ts, exitTs: t.exit_ts, commission: t.commission,
204
+ reasonEntry: t.reason_entry, reasonExit: t.reason_exit,
183
205
  barsHeld: Math.round((t.exit_ts - t.entry_ts) / 600000) * BAR_INTERVAL,
184
206
  }));
185
207
  const curve = st.curve || [];
@@ -239,7 +261,9 @@ class LiveEngine {
239
261
  try {
240
262
  const p = s.params;
241
263
  const now = Date.now();
242
- const ds = await loadDataset({ fromTs: now - Math.ceil(p.modelTrainDays * 1.5 + 25) * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends !== false });
264
+ const trainedToday = isModelTrainedToday(p, now);
265
+ const windowDays = trainedToday ? 7 : Math.ceil(p.modelTrainDays * 1.5 + 25);
266
+ const ds = await loadDataset({ fromTs: now - windowDays * DAY, tillTs: now, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends !== false });
243
267
  s.lastSeenTs = latest;
244
268
  changed = true;
245
269
  const bySym = new Map(ds.tokens.map((t, i) => [t.symbol, i]));
@@ -308,7 +332,7 @@ class LiveEngine {
308
332
  const dir = this.#direction(s);
309
333
  const slots = dir === 'long' ? K : 2 * K;
310
334
  const autoAdapt = s.params.modelAutoAdapt !== false;
311
- const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (2 * K));
335
+ const maxSlotWeight = dir === 'long' ? Math.min(0.48, Math.max(1 / K, 1.4 / K)) : Math.min(0.48, 1.9 / (2 * K));
312
336
  const invVol = s.params.modelAllocMode === 'invvol';
313
337
  const effMinEdge = autoAdapt ? 2.5 : s.params.modelMinEdge;
314
338
  const needPct = effMinEdge * 2 * (s.params.commissionPct + s.params.slippagePct);
@@ -320,7 +344,7 @@ class LiveEngine {
320
344
  const side = o.action === 'short' ? 'short' : 'long';
321
345
  const px = price(o.symbol);
322
346
  if (s.held.has(o.symbol) || !(px > 0) || [...s.held.values()].filter((h) => h.side === side).length >= K) continue;
323
- const conv = autoAdapt && needPct > 0 && o.expectedPct ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (Math.abs(o.expectedPct) / needPct))) : 1;
347
+ const conv = autoAdapt && needPct > 0 && o.expectedPct ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (Math.abs(o.expectedPct) / needPct))) : 1;
324
348
  const baseAlloc = autoAdapt ? Math.min(s.equity * maxSlotWeight * conv, avail / Math.max(1, buyOrders.length - idx)) : Math.min(s.equity / slots, avail / Math.max(1, buyOrders.length - idx));
325
349
  let alloc = baseAlloc;
326
350
  if (invVol && o.weight > 0) {
@@ -356,7 +380,7 @@ class LiveEngine {
356
380
  const autoAdapt = p.modelAutoAdapt !== false;
357
381
  const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
358
382
  const H = barsFromHours(effHorizon);
359
- const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
383
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
360
384
  if (s.barsSinceRebalance >= stepBars) {
361
385
  await new Promise((r) => setImmediate(r));
362
386
  const r = await predictAt(ds, p, idx);
@@ -312,8 +312,8 @@ async function run(runId, p, state) {
312
312
  const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
313
313
  const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
314
314
  let curExitRatio = autoAdapt ? 0.95 : exitRatio;
315
- const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
316
- const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : Math.min(0.35, 1.4 / (2 * K));
315
+ const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
316
+ const maxSlotWeight = dir === 'long' ? Math.min(0.48, Math.max(1 / K, 1.4 / K)) : Math.min(0.48, 1.9 / (2 * K));
317
317
 
318
318
  const closePos = (i, h, t, end, why) => {
319
319
  const exitPrice = px(i, t);
@@ -405,7 +405,7 @@ async function run(runId, p, state) {
405
405
  for (let idx = 0; idx < newEntries.length; idx++) {
406
406
  const [i, side, edgeAt] = newEntries[idx];
407
407
  const effEdge = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
408
- const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (effEdge / needEdge))) : 1;
408
+ const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.25, Math.max(0.85, 0.80 + 0.25 * (effEdge / needEdge))) : 1;
409
409
  const baseAlloc = autoAdapt ? Math.min(equity * maxSlotWeight * conv, avail / Math.max(1, newEntries.length - idx)) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
410
410
  let alloc = baseAlloc;
411
411
  if (invVol && avgInv > 0 && sg[i] > 0) {