neuroalpha 0.2.0 → 0.3.12
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/LICENSE +21 -0
- package/README.md +7 -3
- package/bin/cli.js +69 -69
- package/config/seed-run.json +1 -0
- package/config/strategy.json +4 -10
- package/deploy/install.sh +128 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +103 -0
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -1
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +171 -0
- package/package.json +15 -5
- package/public/admin.html +426 -0
- package/public/app.js +741 -251
- package/public/index.html +45 -20
- package/public/landing.html +1004 -870
- package/public/legal.html +22 -0
- package/public/styles.css +49 -15
- package/server/api/routes.js +176 -23
- package/server/auth/index.js +544 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1224 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +53 -2
- package/server/hub.js +1 -0
- package/server/index.js +10 -2
- package/server/llm/client.js +1 -0
- package/server/llm/providers.js +50 -3
- package/server/model/dataset.js +362 -352
- package/server/model/forecast.js +15 -21
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +489 -486
- package/server/model/metrics.js +1 -0
- package/server/model/models.js +31 -21
- package/server/model/profiles.js +1 -0
- package/server/model/research.js +119 -172
- package/server/model/vocab.js +1 -0
- package/server/news/index.js +1 -0
- package/server/notify/index.js +304 -168
- package/server/params-meta.js +20 -25
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +1 -0
- package/server/strategist/index.js +6 -6
- package/server/strategist/tools.js +2 -1
- package/server/util.js +1 -0
- package/server/model/gpu-worker.html +0 -11
- package/server/model/gpu-worker.js +0 -60
- package/server/model/tf-node.js +0 -26
- package/server/model/transformer-core.js +0 -330
- package/server/model/transformer-gpu.js +0 -221
- package/server/model/transformer-pool.js +0 -200
- package/server/model/transformer-worker.js +0 -28
- package/server/model/transformer.js +0 -57
package/server/model/metrics.js
CHANGED
package/server/model/models.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import { eigenSym, ridgeSolve, matrix, spearman, kmeans } from './linalg.js';
|
|
2
3
|
import { BAR_INTERVAL } from './dataset.js';
|
|
3
4
|
|
|
@@ -19,11 +20,13 @@ for (const k of KINDS) for (const s of SCALES) for (const b of BETAS) HEADS.push
|
|
|
19
20
|
// trainStart..testStart — окно обучения; testStart..testEnd — месяц прогноза.
|
|
20
21
|
// Все массивы «по времени»: [u*N + i], u = t - lo.
|
|
21
22
|
// textWeight (0..1) — вес «текстовой» близости по профилям бизнеса; profiles — Map(тикер → 20 чисел), загружается один раз на исследование.
|
|
22
|
-
export function buildFold(ds, { trainStart, testStart, testEnd, H, embedDim, textWeight = 0, profiles = null }) {
|
|
23
|
+
export function buildFold(ds, { trainStart, testStart, testEnd, H, embedDim, textWeight = 0, profiles = null, multiHorizon = false }) {
|
|
23
24
|
const { tokens, mret, T } = ds;
|
|
24
25
|
const N = tokens.length;
|
|
26
|
+
const Hfast = Math.max(2, Math.round(H * 0.6));
|
|
27
|
+
const Hslow = Math.round(H * 1.4);
|
|
25
28
|
const lo = Math.max(1, trainStart - FEATURE_WARMUP);
|
|
26
|
-
const hi = Math.min(T, testEnd + H + 2);
|
|
29
|
+
const hi = Math.min(T, testEnd + (multiHorizon ? Hslow : H) + 2);
|
|
27
30
|
const W = hi - lo;
|
|
28
31
|
|
|
29
32
|
// чувствительность к рынку и обычный размах — только по окну обучения
|
|
@@ -80,20 +83,35 @@ export function buildFold(ds, { trainStart, testStart, testEnd, H, embedDim, tex
|
|
|
80
83
|
}
|
|
81
84
|
});
|
|
82
85
|
// цель: остаточное (без рынка) движение, которое реально можно получить: решение на закрытии свечи t,
|
|
83
|
-
// вход по цене открытия t+1, выход по цене открытия t+1+H
|
|
86
|
+
// вход по цене открытия t+1, выход по цене открытия t+1+H (в режиме multiHorizon — взвешенная траектория 0.6H / 1.0H / 1.4H)
|
|
84
87
|
const { mopen } = ds;
|
|
85
88
|
const yz = new Float64Array(W * N), yraw = new Float64Array(W * N), yok = new Uint8Array(W);
|
|
89
|
+
const invFast = 1 / Math.sqrt(Hfast), invMid = 1 / Math.sqrt(H), invSlow = 1 / Math.sqrt(Hslow);
|
|
86
90
|
for (let u = 0; u + 1 + H < W; u++) {
|
|
87
91
|
const t = lo + u;
|
|
88
92
|
const ma = mopen[t + 1], mb = mopen[t + 1 + H];
|
|
89
93
|
if (!(ma > 0 && mb > 0)) continue;
|
|
90
94
|
yok[u] = 1;
|
|
91
|
-
const
|
|
95
|
+
const mMid = Math.log(mb / ma);
|
|
96
|
+
const hasMulti = multiHorizon && (t + 1 + Hslow < T) && mopen[t + 1 + Hfast] > 0 && mopen[t + 1 + Hslow] > 0;
|
|
97
|
+
const mFast = hasMulti ? Math.log(mopen[t + 1 + Hfast] / ma) : 0;
|
|
98
|
+
const mSlow = hasMulti ? Math.log(mopen[t + 1 + Hslow] / ma) : 0;
|
|
92
99
|
for (let i = 0; i < N; i++) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const opn = tokens[i].open;
|
|
101
|
+
const a = opn[t + 1], b = opn[t + 1 + H];
|
|
102
|
+
const rMid = a > 0 && b > 0 ? Math.log(b / a) - beta[i] * mMid : 0;
|
|
103
|
+
yraw[u * N + i] = rMid;
|
|
104
|
+
const sig = sigma[i];
|
|
105
|
+
const zMid = Math.max(-8, Math.min(8, (rMid / sig) * invMid));
|
|
106
|
+
if (hasMulti && a > 0 && opn[t + 1 + Hfast] > 0 && opn[t + 1 + Hslow] > 0) {
|
|
107
|
+
const rFast = Math.log(opn[t + 1 + Hfast] / a) - beta[i] * mFast;
|
|
108
|
+
const rSlow = Math.log(opn[t + 1 + Hslow] / a) - beta[i] * mSlow;
|
|
109
|
+
const zFast = Math.max(-8, Math.min(8, (rFast / sig) * invFast));
|
|
110
|
+
const zSlow = Math.max(-8, Math.min(8, (rSlow / sig) * invSlow));
|
|
111
|
+
yz[u * N + i] = 0.25 * zFast + 0.50 * zMid + 0.25 * zSlow;
|
|
112
|
+
} else {
|
|
113
|
+
yz[u * N + i] = zMid;
|
|
114
|
+
}
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
|
|
@@ -196,7 +214,7 @@ export function buildFold(ds, { trainStart, testStart, testEnd, H, embedDim, tex
|
|
|
196
214
|
if (list.length) macro = { list };
|
|
197
215
|
}
|
|
198
216
|
|
|
199
|
-
return { N, lo, hi, W, trainStart, testStart, testEnd, H, beta, sigma, z, has, liq, X, yz, yraw, yok, emb, embN, cos, eigenvalues: eigVals, macro };
|
|
217
|
+
return { N, lo, hi, W, trainStart, testStart, testEnd, H, Hfast, Hslow, multiHorizon, beta, sigma, z, has, liq, X, yz, yraw, yok, emb, embN, cos, eigenvalues: eigVals, macro };
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
// Ликвидная акция: сделки хотя бы в 15 из последних 20 свечей
|
|
@@ -205,7 +223,8 @@ export const LIQ_MIN = 15;
|
|
|
205
223
|
// В обучение — только моменты, цель которых целиком лежит в окне обучения (без заглядывания в месяц прогноза)
|
|
206
224
|
export const trainUs = (f) => {
|
|
207
225
|
const us = [];
|
|
208
|
-
|
|
226
|
+
const maxH = f.multiHorizon ? f.Hslow : f.H;
|
|
227
|
+
for (let t = f.trainStart + FEATURE_WARMUP; t < f.testStart - maxH - 1; t++) { const u = t - f.lo; if (f.yok[u]) us.push(u); }
|
|
209
228
|
return us;
|
|
210
229
|
};
|
|
211
230
|
|
|
@@ -386,19 +405,10 @@ export const MODELS = {
|
|
|
386
405
|
inertia: { label: 'Своя инерция', fit: fitInertia, predict: predictInertia },
|
|
387
406
|
leadlag: { label: 'Кто за кем ходит', fit: fitLeadLag, predict: (m, f, u) => predictLin(m.W, f, u) },
|
|
388
407
|
attention: { label: 'Внимание через карту', fit: fitAttention, predict: predictAttention },
|
|
389
|
-
// ступень 3: нейросеть с вниманием между акциями (tfjs грузится только при первом обучении);
|
|
390
|
-
// fit асинхронный, после использования модель нужно освободить: model.dispose()
|
|
391
|
-
transformer: {
|
|
392
|
-
label: 'Трансформер',
|
|
393
|
-
fit: async (f, opts) => (await import('./transformer.js')).fitTransformer(f, opts),
|
|
394
|
-
predict: (m, f, u) => m.predictOne(m, f, u),
|
|
395
|
-
predictMany: (m, f, us) => m.predictMany(m, f, us),
|
|
396
|
-
heavy: true,
|
|
397
|
-
},
|
|
398
408
|
};
|
|
399
409
|
|
|
400
|
-
// Какие модели обучать: три базовые
|
|
401
|
-
export const modelNames = (
|
|
410
|
+
// Какие модели обучать: три базовые модели
|
|
411
|
+
export const modelNames = () => Object.keys(MODELS);
|
|
402
412
|
|
|
403
413
|
// Объяснение прогноза «внимания»: вклад каждого соседа в прогноз акции j
|
|
404
414
|
export function explainAttention(m, f, u, j, top = 3) {
|
package/server/model/profiles.js
CHANGED
package/server/model/research.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import { normalizeParams } from '../settings.js';
|
|
2
3
|
import { computeReport, downsample, buildPeriods } from './metrics.js';
|
|
3
4
|
import { round } from '../util.js';
|
|
@@ -18,9 +19,7 @@ const MAX_CONCURRENT_RESEARCH = 1;
|
|
|
18
19
|
const tick = () => new Promise((r) => setImmediate(r));
|
|
19
20
|
|
|
20
21
|
// Ключевые параметры для сравнения исследований (включая capital — он влияет на рубли)
|
|
21
|
-
|
|
22
|
-
const TF_KEYS = ['transformerEpochs', 'transformerStride', 'transformerGpu'];
|
|
23
|
-
const REUSE_KEYS = ['modelAutoAdapt', 'modelType', 'modelHorizon', 'modelMinEdge', 'modelExitRatio', 'modelMinHoldHours', 'modelTopK', 'modelAllocMode', 'modelDirection', 'modelMarketFilter', 'modelShortFeePct', 'modelTrainDays', 'modelHistoryDays', 'modelEmbedDim', 'modelUseMacro', 'modelUseDividends', 'modelTextWeight', 'modelUseTransformer', ...TF_KEYS, 'commissionPct', 'slippagePct', 'oosPct', 'capital'];
|
|
22
|
+
const REUSE_KEYS = ['modelAutoAdapt', 'modelType', 'modelHorizon', 'modelMinEdge', 'modelExitRatio', 'modelTopK', 'modelAllocMode', 'modelDirection', 'modelMarketFilter', 'modelShortFeePct', 'modelTrainDays', 'modelHistoryDays', 'modelEmbedDim', 'modelUseMacro', 'modelUseDividends', 'modelTextWeight', 'commissionPct', 'slippagePct', 'oosPct', 'capital'];
|
|
24
23
|
|
|
25
24
|
// Фильтр падающего рынка: индекс Мосбиржи в среднесрочном нисходящем тренде (ниже уровня 10 торговых дней назад)
|
|
26
25
|
// или в сильном локальном обвале (ниже уровня дня и 3 дней назад на >1%)
|
|
@@ -35,14 +34,13 @@ export function isMarketDown(ds, t) {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
// Версия обработки данных: при её смене прежние прогоны не считаются «такими же» (у старых её нет).
|
|
38
|
-
//
|
|
39
|
-
const DATA_VERSION =
|
|
37
|
+
// 7 — мульти-горизонтное обучение (0.6H / 1.0H / 1.4H) + взвешивание слота по силе сигнала (Conviction Sizing) в Авто-режиме
|
|
38
|
+
const DATA_VERSION = 7;
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
// настройки трансформера важны, только если он участвует
|
|
40
|
+
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
43
41
|
function reuseParamsKey(params) {
|
|
44
|
-
const
|
|
45
|
-
return JSON.stringify([params._data ?? 0, ...REUSE_KEYS.map((k) => (
|
|
42
|
+
const autoOn = (params.modelAutoAdapt ?? strategyDefaults.modelAutoAdapt) !== false;
|
|
43
|
+
return JSON.stringify([params._data ?? 0, ...REUSE_KEYS.map((k) => (autoOn && k in AUTO_LOCKED ? AUTO_LOCKED[k] : (params[k] ?? strategyDefaults[k])))]);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
function findReusableRun(params) {
|
|
@@ -143,7 +141,11 @@ const fmtDay = (ts) => new Date(ts + MSK).toISOString().slice(0, 10).split('-').
|
|
|
143
141
|
|
|
144
142
|
async function run(runId, p, state) {
|
|
145
143
|
const emit = (m) => hub.emit({ type: 'research:progress', runId, ...m });
|
|
146
|
-
const
|
|
144
|
+
const autoAdapt = p.modelAutoAdapt !== false;
|
|
145
|
+
const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
|
|
146
|
+
const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
|
|
147
|
+
const H = barsFromHours(effHorizon); // горизонт в свечах по 10 минут
|
|
148
|
+
const maxH = autoAdapt ? Math.round(H * 1.4) : H;
|
|
147
149
|
const tillTs = Date.now();
|
|
148
150
|
const researchFrom = tillTs - p.modelHistoryDays * DAY;
|
|
149
151
|
const fromTs = researchFrom - Math.ceil(p.modelTrainDays * 1.5 + 20) * DAY;
|
|
@@ -151,7 +153,7 @@ async function run(runId, p, state) {
|
|
|
151
153
|
emit({ stage: 'data', pct: 0, text: 'Загружаем словарь рынка (состав индекса широкого рынка)…' });
|
|
152
154
|
const ds = await loadDataset({
|
|
153
155
|
fromTs, tillTs, useMacro: p.modelUseMacro, useDividends: p.modelUseDividends, signal: state.ctrl.signal,
|
|
154
|
-
onProgress: (x) => emit({ stage: 'data', pct: Math.round((x.done / x.total) * 100), text: `Загружаем цены: ${x.done} из ${x.total}.
|
|
156
|
+
onProgress: (x) => emit({ stage: 'data', pct: Math.round((x.done / x.total) * 100), text: `Загружаем цены: ${x.done} из ${x.total} (${x.symbol})…`, symbol: x.symbol, done: x.done, total: x.total }),
|
|
155
157
|
});
|
|
156
158
|
const { tokens, T, timeline } = ds;
|
|
157
159
|
const N = tokens.length;
|
|
@@ -172,13 +174,8 @@ async function run(runId, p, state) {
|
|
|
172
174
|
});
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
// три базовые модели
|
|
177
|
+
// три базовые модели всегда сравниваются между собой
|
|
176
178
|
const names = modelNames(p);
|
|
177
|
-
const useTf = names.includes('transformer');
|
|
178
|
-
const tfMod = useTf ? await import('./transformer.js') : null;
|
|
179
|
-
const tfCfg = useTf ? tfMod.transformerCfg(p) : null;
|
|
180
|
-
const tfInfo = useTf ? { folds: [], device: 'процессор', threads: 1, note: '' } : null;
|
|
181
|
-
const fmtTfInfo = ({ backend, device, ...i }) => ({ ...i, valIc: i.valIc === null ? null : round(i.valIc, 4), scale: round(i.scale, 4) });
|
|
182
179
|
const preds = Object.fromEntries(names.map((n) => [n, new Map()])); // t -> Float64Array(N)
|
|
183
180
|
const yraw = new Map(); // t -> Float64Array(N)
|
|
184
181
|
const sigmaAt = new Map(); // t -> обычный размах акций в этом окне обучения
|
|
@@ -187,118 +184,75 @@ async function run(runId, p, state) {
|
|
|
187
184
|
const folds = Math.ceil((T - H - firstTest) / testLen);
|
|
188
185
|
let fi = 0;
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
const onAbort = () => tfRunner?.cancel();
|
|
202
|
-
state.ctrl.signal.addEventListener('abort', onAbort, { once: true });
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
while (testStart < T - H - 2) {
|
|
206
|
-
if (state.cancelled) throw new Error('Отменено');
|
|
207
|
-
const testEnd = Math.min(testStart + testLen, T - H - 2);
|
|
208
|
-
const trainStart = Math.max(FEATURE_WARMUP + H + 2, testStart - trainLen);
|
|
209
|
-
mainText = `Обучение ${fi + 1} из ${folds}: учимся до ${fmtDay(timeline[testStart])}, прогнозируем до ${fmtDay(timeline[testEnd - 1])}`;
|
|
210
|
-
if (tfRunner) progress();
|
|
211
|
-
else emit({ stage: 'sim', pct: Math.round((fi / folds) * 100), text: mainText });
|
|
187
|
+
while (testStart < T - maxH - 2) {
|
|
188
|
+
if (state.cancelled) throw new Error('Отменено');
|
|
189
|
+
const testEnd = Math.min(testStart + testLen, T - maxH - 2);
|
|
190
|
+
const trainStart = Math.max(FEATURE_WARMUP + maxH + 2, testStart - trainLen);
|
|
191
|
+
const mainText = `Обучение ${fi + 1} из ${folds}: учимся до ${fmtDay(timeline[testStart])}, прогнозируем до ${fmtDay(timeline[testEnd - 1])}`;
|
|
192
|
+
await tick();
|
|
193
|
+
const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
|
|
194
|
+
const fitted = {};
|
|
195
|
+
for (const n of names) {
|
|
196
|
+
fitted[n] = await MODELS[n].fit(f);
|
|
212
197
|
await tick();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
tfRunner = r.runner;
|
|
220
|
-
tfInfo.note = r.note;
|
|
221
|
-
if (r.note) emit({ stage: 'sim', pct: Math.round((fi / folds) * 100), text: `Трансформер: ${r.note}` });
|
|
222
|
-
if (tfRunner) { tfInfo.device = tfRunner.where; tfInfo.threads = tfRunner.threads; }
|
|
223
|
-
if (state.cancelled) throw new Error('Отменено');
|
|
224
|
-
}
|
|
225
|
-
const fitNames = tfRunner ? names.filter((n) => n !== 'transformer') : names;
|
|
226
|
-
const fitted = {};
|
|
227
|
-
const foldText = `Обучение ${fi + 1} из ${folds}`;
|
|
228
|
-
try {
|
|
229
|
-
for (const n of fitNames) {
|
|
230
|
-
// трансформер учится по эпохам: прогресс и отмена — между батчами
|
|
231
|
-
const opts = n === 'transformer' ? {
|
|
232
|
-
cfg: tfCfg,
|
|
233
|
-
isCancelled: () => state.cancelled,
|
|
234
|
-
onProgress: (x) => emit({ stage: 'sim', pct: Math.round(((fi + (x.epoch - 1) / x.epochs) / folds) * 100), text: `${foldText}: трансформер, эпоха ${x.epoch}…` }),
|
|
235
|
-
} : undefined;
|
|
236
|
-
fitted[n] = await MODELS[n].fit(f, opts);
|
|
237
|
-
await tick();
|
|
238
|
-
if (state.cancelled) throw new Error('Отменено');
|
|
239
|
-
}
|
|
240
|
-
const us = [];
|
|
241
|
-
for (let t = testStart; t < testEnd; t++) if (f.yok[t - f.lo]) us.push(t - f.lo);
|
|
242
|
-
// тяжёлые модели (трансформер) предсказывают сразу пачкой моментов
|
|
243
|
-
const many = Object.fromEntries(fitNames.filter((n) => MODELS[n].predictMany).map((n) => [n, MODELS[n].predictMany(fitted[n], f, us)]));
|
|
244
|
-
for (const [k, u] of us.entries()) {
|
|
245
|
-
if ((k & 511) === 0) {
|
|
246
|
-
if (state.cancelled) throw new Error('Отменено');
|
|
247
|
-
await tick();
|
|
248
|
-
}
|
|
249
|
-
const t = f.lo + u;
|
|
250
|
-
for (const n of fitNames) preds[n].set(t, many[n] ? many[n].get(u) : MODELS[n].predict(fitted[n], f, u));
|
|
251
|
-
yraw.set(t, Float64Array.from(f.yraw.subarray(u * N, u * N + N)));
|
|
252
|
-
sigmaAt.set(t, f.sigma);
|
|
253
|
-
}
|
|
254
|
-
if (tfRunner) {
|
|
255
|
-
// массивы окна уходят исполнителю без копирования: базовые модели этого окна уже посчитаны
|
|
256
|
-
const k = fi;
|
|
257
|
-
const pr = tfRunner.submit(tfMod.transformerInput(f), us, tfCfg, {
|
|
258
|
-
transfer: true,
|
|
259
|
-
onProgress: (x) => { tfFrac.set(k, (x.epoch - 1) / x.epochs); progress(); },
|
|
260
|
-
}).then((r) => { tfDone++; tfFrac.delete(k); progress(); return r; });
|
|
261
|
-
pr.catch((e) => { tfFail = tfFail || e; });
|
|
262
|
-
tfJobs.push({ lo: f.lo, pr });
|
|
263
|
-
}
|
|
264
|
-
} finally {
|
|
265
|
-
// веса нейросети освобождаем после каждого окна (и при отмене)
|
|
266
|
-
for (const n of fitNames) if (fitted[n]?.dispose) {
|
|
267
|
-
const m = fitted[n];
|
|
268
|
-
m.dispose();
|
|
269
|
-
if (tfInfo && n === 'transformer') tfInfo.folds.push(fmtTfInfo({ ...m.info, tensors: m.tf.memory().numTensors }));
|
|
270
|
-
delete fitted[n];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// снимок карты рынка на начало месяца прогноза
|
|
274
|
-
const snap = mapSnapshot(f);
|
|
275
|
-
const pts = alignTo(prevPts, snap.pts);
|
|
276
|
-
const cl = matchClusters(prevCl, snap.clusters, 8);
|
|
277
|
-
snapshots.push({ ts: timeline[testStart], pts: pts.map(([x, y]) => [round(x, 3), round(y, 3)]), cl });
|
|
278
|
-
prevPts = pts; prevCl = cl;
|
|
279
|
-
lastFold = f; lastModels = fitted;
|
|
280
|
-
testStart = testEnd;
|
|
281
|
-
fi++;
|
|
282
|
-
// не копим очередь окон (память): ждём, пока у исполнителя не освободится место
|
|
283
|
-
while (tfRunner && tfRunner.backlog > tfRunner.threads) {
|
|
198
|
+
if (state.cancelled) throw new Error('Отменено');
|
|
199
|
+
}
|
|
200
|
+
const us = [];
|
|
201
|
+
for (let t = testStart; t < testEnd; t++) if (f.yok[t - f.lo]) us.push(t - f.lo);
|
|
202
|
+
for (const [k, u] of us.entries()) {
|
|
203
|
+
if ((k & 511) === 0) {
|
|
284
204
|
if (state.cancelled) throw new Error('Отменено');
|
|
285
|
-
|
|
286
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
205
|
+
await tick();
|
|
287
206
|
}
|
|
207
|
+
const t = f.lo + u;
|
|
208
|
+
for (const n of names) preds[n].set(t, MODELS[n].predict(fitted[n], f, u));
|
|
209
|
+
yraw.set(t, Float64Array.from(f.yraw.subarray(u * N, u * N + N)));
|
|
210
|
+
sigmaAt.set(t, f.sigma);
|
|
288
211
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
212
|
+
// снимок карты рынка на начало месяца прогноза
|
|
213
|
+
const snap = mapSnapshot(f);
|
|
214
|
+
const pts = alignTo(prevPts, snap.pts);
|
|
215
|
+
const cl = matchClusters(prevCl, snap.clusters, 8);
|
|
216
|
+
const ptsRound = pts.map(([x, y]) => [round(x, 3), round(y, 3)]);
|
|
217
|
+
snapshots.push({ ts: timeline[testStart], pts: ptsRound, cl });
|
|
218
|
+
prevPts = pts; prevCl = cl;
|
|
219
|
+
lastFold = f; lastModels = fitted;
|
|
220
|
+
|
|
221
|
+
// живая телеметрия шага обучения для инфографики в интерфейсе
|
|
222
|
+
const attM = fitted.attention;
|
|
223
|
+
let foldIc = null;
|
|
224
|
+
if (preds.attention) {
|
|
225
|
+
let icS = 0, icK = 0;
|
|
226
|
+
for (let t = testStart; t < testEnd; t += Math.max(1, Math.floor(H / 2))) {
|
|
227
|
+
const pr = preds.attention.get(t), y = yraw.get(t);
|
|
228
|
+
if (!pr || !y) continue;
|
|
229
|
+
const ok = [...pr.keys()].filter((i) => tokens[i].liq[t] >= LIQ_MIN);
|
|
230
|
+
if (ok.length >= 10) { icS += spearman(ok.map((i) => pr[i]), ok.map((i) => y[i])); icK++; }
|
|
231
|
+
}
|
|
232
|
+
if (icK > 0) foldIc = round(icS / icK, 4);
|
|
297
233
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
234
|
+
const liveViz = {
|
|
235
|
+
fold: fi + 1,
|
|
236
|
+
folds,
|
|
237
|
+
tokens: N,
|
|
238
|
+
horizon: effHorizon,
|
|
239
|
+
trainFrom: timeline[trainStart],
|
|
240
|
+
testFrom: timeline[testStart],
|
|
241
|
+
testTill: timeline[testEnd - 1],
|
|
242
|
+
periodFrom: timeline[Math.max(0, firstTest - trainLen)],
|
|
243
|
+
periodTill: timeline[T - 1],
|
|
244
|
+
foldIc,
|
|
245
|
+
pts: ptsRound,
|
|
246
|
+
cl,
|
|
247
|
+
...(fi === 0 ? { symbols: tokens.map((tk) => tk.symbol) } : {}),
|
|
248
|
+
...(attM ? {
|
|
249
|
+
heads: HEADS.map((h, i) => ({ label: h.label, coef: round(attM.coef[i], 4) })),
|
|
250
|
+
self: [round(attM.coef[HEADS.length], 4), round(attM.coef[HEADS.length + 1], 4), round(attM.coef[HEADS.length + 2], 4)],
|
|
251
|
+
} : {}),
|
|
252
|
+
};
|
|
253
|
+
emit({ stage: 'sim', pct: Math.round(((fi + 1) / folds) * 100), text: mainText, viz: liveViz });
|
|
254
|
+
testStart = testEnd;
|
|
255
|
+
fi++;
|
|
302
256
|
}
|
|
303
257
|
|
|
304
258
|
emit({ stage: 'sim', pct: 100, text: 'Считаем результаты…' });
|
|
@@ -309,9 +263,7 @@ async function run(runId, p, state) {
|
|
|
309
263
|
// ---------- портфель из лучших прогнозов: покупки, продажи без покупки, пары «нейтрально к рынку» ----------
|
|
310
264
|
const K = p.modelTopK;
|
|
311
265
|
const dir = p.modelDirection;
|
|
312
|
-
const autoAdapt = p.modelAutoAdapt !== false;
|
|
313
266
|
const exitRatio = Number(p.modelExitRatio ?? 0.5);
|
|
314
|
-
const minHoldBars = p.modelMinHoldHours > 0 ? barsFromHours(p.modelMinHoldHours) : 0;
|
|
315
267
|
const mktFilter = p.modelMarketFilter === 'ma';
|
|
316
268
|
const invVol = p.modelAllocMode === 'invvol';
|
|
317
269
|
const realCost = (p.commissionPct + p.slippagePct) / 100;
|
|
@@ -359,9 +311,9 @@ async function run(runId, p, state) {
|
|
|
359
311
|
// Самообучение правил по прошлым закрытым сделкам (временный карантин «акций-убийц» с затуханием и авто-калибровка порога выхода)
|
|
360
312
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
361
313
|
const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
|
|
362
|
-
let curExitRatio = autoAdapt ? 0.
|
|
314
|
+
let curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
363
315
|
const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
|
|
364
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.
|
|
316
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.25, 1 / (2 * K)) : Math.min(0.35, 1.4 / (2 * K)));
|
|
365
317
|
|
|
366
318
|
const closePos = (i, h, t, end, why) => {
|
|
367
319
|
const exitPrice = px(i, t);
|
|
@@ -397,16 +349,15 @@ async function run(runId, p, state) {
|
|
|
397
349
|
let order, entries;
|
|
398
350
|
if (dir !== 'long') {
|
|
399
351
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
400
|
-
const need =
|
|
352
|
+
const need = effMinEdge * 2 * realCost;
|
|
401
353
|
const exitNeed = need * curExitRatio;
|
|
402
354
|
const earlyExitNeed = need * 0.35;
|
|
403
|
-
// держим позицию, пока ожидаемое движение выше порога выхода (
|
|
355
|
+
// держим позицию, пока ожидаемое движение выше порога выхода (первые два полушага — мягкий порог, чтобы не резать сделку от шума)
|
|
404
356
|
for (const [i, h] of [...held]) {
|
|
405
357
|
const e = edge(i);
|
|
406
358
|
const barsHeld = tIn - h.entryT;
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
if ((!lock && !(h.side === 'long' ? e > req : e < -req)) || !liquid(i, t)) closePos(i, h, tIn, false, 'Прогноз ослаб');
|
|
359
|
+
const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
|
|
360
|
+
if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn, false, 'Прогноз ослаб');
|
|
410
361
|
}
|
|
411
362
|
const needL = autoAdapt && dir === 'longshort' && bear ? need * 1.25 : need;
|
|
412
363
|
const needS = autoAdapt && dir === 'longshort' ? (bear ? need * 0.90 : need * 1.25) : need;
|
|
@@ -432,28 +383,25 @@ async function run(runId, p, state) {
|
|
|
432
383
|
...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]),
|
|
433
384
|
...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)]),
|
|
434
385
|
];
|
|
435
|
-
} else if (
|
|
386
|
+
} else if (effMinEdge > 0) {
|
|
436
387
|
// ожидаемое движение (в долях) = прогноз × обычный размах × √H; входим, только если оно окупает сделку
|
|
437
388
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
438
|
-
const need =
|
|
389
|
+
const need = effMinEdge * 2 * realCost;
|
|
439
390
|
const exitNeed = need * curExitRatio;
|
|
440
391
|
const earlyExitNeed = need * 0.35;
|
|
441
392
|
for (const [i, h] of [...held]) {
|
|
442
393
|
const e = edge(i);
|
|
443
394
|
const barsHeld = tIn - h.entryT;
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
if ((!lock && !(e > req)) || !liquid(i, t)) closePos(i, h, tIn, false);
|
|
395
|
+
const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
|
|
396
|
+
if (!(e > req) || !liquid(i, t)) closePos(i, h, tIn, false);
|
|
447
397
|
}
|
|
448
398
|
const free = K - held.size;
|
|
449
399
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
450
400
|
const cand = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, free));
|
|
451
401
|
order = [...held.keys(), ...cand];
|
|
452
402
|
} else {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
const cand = mktDown ? [] : [...pr.keys()].filter((i) => !lockedSet.has(i) && stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, K - keepLocked.length));
|
|
456
|
-
order = [...keepLocked, ...cand];
|
|
403
|
+
const cand = mktDown ? [] : [...pr.keys()].filter((i) => stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
|
|
404
|
+
order = cand;
|
|
457
405
|
const next = new Set(order);
|
|
458
406
|
for (const [i, h] of [...held]) if (!next.has(i)) closePos(i, h, tIn, false);
|
|
459
407
|
}
|
|
@@ -467,9 +415,12 @@ async function run(runId, p, state) {
|
|
|
467
415
|
}
|
|
468
416
|
const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
469
417
|
let avail = Math.max(0, equity * 0.99 - usedStart);
|
|
418
|
+
const needEdge = effMinEdge * 2 * realCost;
|
|
470
419
|
for (let idx = 0; idx < newEntries.length; idx++) {
|
|
471
420
|
const [i, side, edgeAt] = newEntries[idx];
|
|
472
|
-
const
|
|
421
|
+
const effEdge = edgeAt !== null ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
|
|
422
|
+
const conv = autoAdapt && needEdge > 0 && effEdge > 0 ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (effEdge / needEdge))) : 1;
|
|
423
|
+
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));
|
|
473
424
|
let alloc = baseAlloc;
|
|
474
425
|
if (invVol && avgInv > 0 && sg[i] > 0) {
|
|
475
426
|
const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
|
|
@@ -499,7 +450,7 @@ async function run(runId, p, state) {
|
|
|
499
450
|
function tradeRec(name, i, h, t, exitPrice, pnl, commission, borrow, end = false, why = '') {
|
|
500
451
|
const heldMin = (t - h.entryT) * BAR_INTERVAL; // минуты
|
|
501
452
|
const short = h.side === 'short';
|
|
502
|
-
const hz = round(
|
|
453
|
+
const hz = round(effHorizon, 2);
|
|
503
454
|
const label = MODELS[name].label;
|
|
504
455
|
const reasonEntry = dir === 'long' ? `Модель «${label}»: акция среди ${K} лучших прогнозов на ${hz} ч вперёд`
|
|
505
456
|
: short ? `Модель «${label}»: продажа: ожидаемо слабее рынка на ${Math.abs(h.expectedPct)}% за ${hz} ч`
|
|
@@ -638,9 +589,6 @@ async function run(runId, p, state) {
|
|
|
638
589
|
heads, self, macroHeads,
|
|
639
590
|
map: { snapshots, eigenvalues: lastFold.eigenvalues.map((v) => round(v, 3)) },
|
|
640
591
|
neighbors,
|
|
641
|
-
// трансформер: число весов и ход обучения по окнам (эпохи, совпадение на проверке)
|
|
642
|
-
// где шло обучение: «процессор» или «видеокарта: <название>»; потоков — сколько окон обучалось одновременно
|
|
643
|
-
...(tfInfo ? { transformer: { params: tfInfo.folds[0]?.params ?? null, epochs: tfCfg.epochs, stride: tfCfg.stride, device: tfInfo.device, threads: tfInfo.threads, ...(tfInfo.note ? { note: tfInfo.note } : {}), folds: tfInfo.folds } } : {}),
|
|
644
592
|
});
|
|
645
593
|
finishRun(runId, { metrics: report, equity: downsample(results[chosen].sim.curve) });
|
|
646
594
|
return report;
|
|
@@ -704,12 +652,12 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
704
652
|
const { tokens, T, timeline } = ds;
|
|
705
653
|
const N = tokens.length, bpd = ds.barsPerDay;
|
|
706
654
|
const trainLen = Math.round(p.modelTrainDays * bpd), testLen = Math.round(20 * bpd);
|
|
707
|
-
const chosen = p.modelType
|
|
655
|
+
const chosen = p.modelType in MODELS ? p.modelType : 'attention';
|
|
708
656
|
const realCost = (p.commissionPct + p.slippagePct) / 100;
|
|
709
657
|
const K = p.modelTopK, dir = p.modelDirection, slots = dir === 'long' ? K : 2 * K;
|
|
710
658
|
const autoAdapt = p.modelAutoAdapt !== false;
|
|
659
|
+
const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
|
|
711
660
|
const exitRatio = Number(p.modelExitRatio ?? 0.5);
|
|
712
|
-
const minHoldBars = p.modelMinHoldHours > 0 ? barsFromHours(p.modelMinHoldHours) : 0;
|
|
713
661
|
const mktFilter = p.modelMarketFilter === 'ma', invVol = p.modelAllocMode === 'invvol';
|
|
714
662
|
const liquid = (i, t) => tokens[i].liq[t] >= LIQ_MIN && tokens[i].open[t + 1] > 0;
|
|
715
663
|
const tsOf = (t) => timeline[Math.min(t, T - 1)];
|
|
@@ -730,17 +678,18 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
730
678
|
const histDays = Math.max(120, Math.min(1100, p.modelHistoryDays + sh));
|
|
731
679
|
const resFrom = periodTill - histDays * DAY;
|
|
732
680
|
let testStart = Math.max(timeline.findIndex((t) => t >= resFrom), trainLen + 10);
|
|
733
|
-
const
|
|
681
|
+
const maxH = autoAdapt ? Math.round(H * 1.4) : H;
|
|
682
|
+
const folds = Math.max(1, Math.ceil((T - maxH - testStart) / testLen));
|
|
734
683
|
let fi = 0;
|
|
735
684
|
const preds = new Map(), sigmaAt = new Map();
|
|
736
|
-
while (testStart < T -
|
|
685
|
+
while (testStart < T - maxH - 2) {
|
|
737
686
|
if (st.cancelled) throw new Error('Отменено');
|
|
738
|
-
const testEnd = Math.min(testStart + testLen, T -
|
|
739
|
-
const trainStart = Math.max(FEATURE_WARMUP +
|
|
687
|
+
const testEnd = Math.min(testStart + testLen, T - maxH - 2);
|
|
688
|
+
const trainStart = Math.max(FEATURE_WARMUP + maxH + 2, testStart - trainLen);
|
|
740
689
|
const pct = Math.min(99, 10 + Math.round(((si + fi / folds) / shifts.length) * 90));
|
|
741
690
|
emitStab({ pct, text: `Проверка устойчивости: сдвиг ${si + 1} из ${shifts.length} (${shLabel}), шаг ${fi + 1} из ${folds}…` });
|
|
742
691
|
await tick();
|
|
743
|
-
const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles });
|
|
692
|
+
const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
|
|
744
693
|
const m = await MODELS[chosen].fit(f);
|
|
745
694
|
for (let t = testStart; t < testEnd; t++) {
|
|
746
695
|
const u = t - f.lo;
|
|
@@ -762,9 +711,9 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
762
711
|
const countSide = (side) => [...held.values()].filter((h) => h.side === side).length;
|
|
763
712
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
764
713
|
const stockAllowed = (i) => !autoAdapt || !(stockCnt[i] >= 3 && stockPnl[i] < -0.012);
|
|
765
|
-
const curExitRatio = autoAdapt ? 0.
|
|
714
|
+
const curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
766
715
|
const stepBars = autoAdapt ? Math.max(2, Math.floor(H / 2)) : H;
|
|
767
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.
|
|
716
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.45, Math.max(1 / K, 1.35 / K)) : (dir === 'neutral' ? Math.min(0.25, 1 / (2 * K)) : Math.min(0.35, 1.4 / (2 * K)));
|
|
768
717
|
const closePos = (i, h, t) => {
|
|
769
718
|
const exitPrice = px(i, t);
|
|
770
719
|
const gross = h.side === 'short' ? h.alloc * (1 - exitPrice / h.entryPrice) : h.alloc * (exitPrice / h.entryPrice - 1);
|
|
@@ -788,14 +737,13 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
788
737
|
const bear = isMarketDown(ds, t);
|
|
789
738
|
const mktDown = mktFilter && dir !== 'neutral' && bear;
|
|
790
739
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
791
|
-
const need =
|
|
740
|
+
const need = effMinEdge * 2 * realCost, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
|
|
792
741
|
let order, entries;
|
|
793
742
|
if (dir !== 'long') {
|
|
794
743
|
for (const [i, h] of [...held]) {
|
|
795
744
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
if ((!lock && !(h.side === 'long' ? e > req : e < -req)) || !liquid(i, t)) closePos(i, h, tIn);
|
|
745
|
+
const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
|
|
746
|
+
if (!(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
799
747
|
}
|
|
800
748
|
const needL = autoAdapt && dir === 'longshort' && bear ? need * 1.25 : need;
|
|
801
749
|
const needS = autoAdapt && dir === 'longshort' ? (bear ? need * 0.90 : need * 1.25) : need;
|
|
@@ -812,26 +760,23 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
812
760
|
}
|
|
813
761
|
nL = Math.max(0, pairs - countSide('long')); nS = Math.max(0, pairs - countSide('short'));
|
|
814
762
|
}
|
|
815
|
-
entries = [...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long']), ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short'])];
|
|
816
|
-
} else if (
|
|
763
|
+
entries = [...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]), ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)])];
|
|
764
|
+
} else if (effMinEdge > 0) {
|
|
817
765
|
for (const [i, h] of [...held]) {
|
|
818
766
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
if ((!lock && !(e > req)) || !liquid(i, t)) closePos(i, h, tIn);
|
|
767
|
+
const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
|
|
768
|
+
if (!(e > req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
822
769
|
}
|
|
823
770
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
824
771
|
const cand = mktDown ? [] : [...pr.keys()].filter((i) => !held.has(i) && stockAllowed(i) && liquid(i, t) && edge(i) > needL).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, K - held.size));
|
|
825
772
|
order = [...held.keys(), ...cand];
|
|
826
773
|
} else {
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
const cand = mktDown ? [] : [...pr.keys()].filter((i) => !lockedSet.has(i) && stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, Math.max(0, K - keepLocked.length));
|
|
830
|
-
order = [...keepLocked, ...cand];
|
|
774
|
+
const cand = mktDown ? [] : [...pr.keys()].filter((i) => stockAllowed(i) && pr[i] > 0 && liquid(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
|
|
775
|
+
order = cand;
|
|
831
776
|
const next = new Set(order);
|
|
832
777
|
for (const [i, h] of [...held]) if (!next.has(i)) closePos(i, h, tIn);
|
|
833
778
|
}
|
|
834
|
-
if (!entries) entries = order.map((i) => [i, 'long']);
|
|
779
|
+
if (!entries) entries = order.map((i) => [i, 'long', null]);
|
|
835
780
|
const newEntries = entries.filter(([i]) => !held.has(i));
|
|
836
781
|
let avgInv = 0;
|
|
837
782
|
if (invVol && newEntries.length && sg) {
|
|
@@ -842,8 +787,10 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
842
787
|
const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
843
788
|
let avail = Math.max(0, equity * 0.99 - usedStart);
|
|
844
789
|
for (let idx = 0; idx < newEntries.length; idx++) {
|
|
845
|
-
const [i, side] = newEntries[idx];
|
|
846
|
-
const
|
|
790
|
+
const [i, side, edgeAt] = newEntries[idx];
|
|
791
|
+
const effEdge = edgeAt !== null && edgeAt !== undefined ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
|
|
792
|
+
const conv = autoAdapt && need > 0 && effEdge > 0 ? Math.min(1.20, Math.max(0.85, 0.80 + 0.20 * (effEdge / need))) : 1;
|
|
793
|
+
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));
|
|
847
794
|
let alloc = baseAlloc;
|
|
848
795
|
if (invVol && avgInv > 0 && sg[i] > 0) {
|
|
849
796
|
const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
|
package/server/model/vocab.js
CHANGED
package/server/news/index.js
CHANGED