neuroalpha 0.6.1 → 0.6.3
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/public/app.js +21 -0
- package/server/brokers/index.js +23 -6
- package/server/model/forecast.js +86 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -1166,6 +1166,27 @@ async function loadForecast(force, silent = false) {
|
|
|
1166
1166
|
nearByDay.get(dk).push(ep);
|
|
1167
1167
|
}
|
|
1168
1168
|
const nearDays = [...nearByDay.keys()].sort((a, b) => b.localeCompare(a));
|
|
1169
|
+
const dayCountsStr = (arr) => `${arr.filter((x) => x.status === 'signal').length} сигн., ${arr.filter((x) => x.status === 'blocked').length} блок., ${arr.filter((x) => x.status === 'near').length} почти`;
|
|
1170
|
+
const nearEpRow = (ep) => {
|
|
1171
|
+
const tRange = ep.firstBar && ep.lastBar && ep.firstBar !== ep.lastBar ? `${ep.firstBar}–${ep.lastBar} (${ep.barsCount} св.)` : `${ep.peakBar || ep.lastBar || ''}`;
|
|
1172
|
+
const isSh = ep.side === 'short';
|
|
1173
|
+
const thrStr = `${isSh ? '−' : '+'}${fmtNum(ep.thresholdPct, 2)}%`;
|
|
1174
|
+
const stBadge = ep.status === 'signal'
|
|
1175
|
+
? `<span class="tag ok">${isSh ? '✓ сигнал шорт' : '✓ сигнал купить'}</span>`
|
|
1176
|
+
: ep.status === 'blocked'
|
|
1177
|
+
? '<span class="tag blocked">⛔ блок входа</span>'
|
|
1178
|
+
: `<span class="tag mid">почти (${ep.peakRatioPct}%)</span>`;
|
|
1179
|
+
const rowCls = ep.status === 'signal' ? ' class="best"' : ep.status === 'blocked' ? ' class="fc-blocked"' : '';
|
|
1180
|
+
return `<tr${rowCls}>
|
|
1181
|
+
<td class="muted small" style="white-space:nowrap">${esc(tRange)}</td>
|
|
1182
|
+
<td style="white-space:nowrap"><span class="sym-link" data-sym="${esc(ep.symbol)}" title="Открыть досье акции ${esc(ep.symbol)}"><b>${esc(ep.symbol)}</b> <span class="muted small">${esc(ep.name || '')}</span></span></td>
|
|
1183
|
+
<td class="${isSh ? 'neg' : 'pos'} small" style="white-space:nowrap">${isSh ? 'В шорт' : 'В покупку'}</td>
|
|
1184
|
+
<td class="num ${cls(ep.peakExpectedPct)}" style="white-space:nowrap"><b>${fmtPct(ep.peakExpectedPct, 2)}</b></td>
|
|
1185
|
+
<td class="num muted small" style="white-space:nowrap">${thrStr}</td>
|
|
1186
|
+
<td class="num small" style="white-space:nowrap"><b>${ep.peakRatioPct}%</b></td>
|
|
1187
|
+
<td class="small" style="white-space:normal">${stBadge} <span class="muted">${esc(ep.reasonText || '')}</span></td>
|
|
1188
|
+
</tr>`;
|
|
1189
|
+
};
|
|
1169
1190
|
const buildNearInnerHtml = () => {
|
|
1170
1191
|
if (nearDayFilter !== 'all' && !nearByDay.has(nearDayFilter)) nearDayFilter = '';
|
|
1171
1192
|
const activeNearDay = nearDayFilter || nearDays[0] || 'all';
|
package/server/brokers/index.js
CHANGED
|
@@ -1706,10 +1706,19 @@ const entryImpulseWatch = new Map(); // `${accId}:${symbol}:${side}` -> { firstT
|
|
|
1706
1706
|
// Фильтр затухания минутного импульса перед входом (Impulse Exhaustion):
|
|
1707
1707
|
// не даёт продавать в шорт посередине вертикально растущей минутной свечи (или покупать в полёт падающего ножа),
|
|
1708
1708
|
// дожидаясь остановки разгона и подтверждённого отката от пика/дна (с шагом проверки 20 секунд; без принудительного входа по таймауту)
|
|
1709
|
+
// Порог удержания уже взятого на прицел сигнала в entryImpulseWatch: пока ждём минутного отката,
|
|
1710
|
+
// само ожидание возврата к среднему неизбежно немного снижается — не теряем сигнал, пока он держится >= 90% порога (до 6 минут)
|
|
1711
|
+
const IMPULSE_WATCH_HOLD_RATIO = 0.90;
|
|
1712
|
+
const IMPULSE_WATCH_TTL_MS = 6 * 60000;
|
|
1713
|
+
const isOnImpulseWatch = (accId, sym, side, nowTs = Date.now()) => {
|
|
1714
|
+
const w = entryImpulseWatch.get(`${accId}:${sym}:${side}`);
|
|
1715
|
+
return Boolean(w && nowTs - w.firstTs <= IMPULSE_WATCH_TTL_MS && nowTs - w.lastTs <= IMPULSE_WATCH_TTL_MS);
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1709
1718
|
async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.now()) {
|
|
1710
1719
|
if (!cands.length || acc.broker !== 'tbank' || !acc.token) return { ready: cands, deferred: [] };
|
|
1711
1720
|
for (const [k, v] of entryImpulseWatch) {
|
|
1712
|
-
if (nowTs - v.lastTs >
|
|
1721
|
+
if (nowTs - v.lastTs > IMPULSE_WATCH_TTL_MS || nowTs - v.firstTs > IMPULSE_WATCH_TTL_MS) entryImpulseWatch.delete(k);
|
|
1713
1722
|
}
|
|
1714
1723
|
const ready = [];
|
|
1715
1724
|
const deferred = [];
|
|
@@ -1750,9 +1759,12 @@ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.n
|
|
|
1750
1759
|
const pullbackPct = recentHigh > 0 ? ((recentHigh - curPx) / recentHigh) * 100 : 0;
|
|
1751
1760
|
const pullbackTicks = recentHigh > 0 ? (recentHigh - curPx) / step : 0;
|
|
1752
1761
|
const barTurnedDown = Boolean(last1m && prev1m && last1m.close <= last1m.open && last1m.high <= prev1m.high + step * 0.5);
|
|
1762
|
+
// Если внутри самой минутной свечи цена уже взлетела до recentHigh и откатилась от пика на >= 1,4× порога отката (и >= 0,55%),
|
|
1763
|
+
// разворот с вершины уже состоялся — не ждём следующей минутной свечи!
|
|
1764
|
+
const intrabarReversalDown = Boolean(last1m && pullbackPct >= Math.max(0.55, minPullbackPct * 1.4) && pullbackTicks >= 4 && curPx <= last1m.high - step * 3);
|
|
1753
1765
|
const peakedAndPulledBack = pullbackPct >= minPullbackPct && pullbackTicks >= 2 && (!last1m || last1m.close <= last1m.open + step * 0.5);
|
|
1754
1766
|
const tickedDownFromPrev = Boolean(!isStrongSurge && prevW && curPx <= prevW.lastPx - step && pullbackTicks >= 2 && pullbackPct >= 0.12);
|
|
1755
|
-
const readyShort = isStrongSurge ? (peakedAndPulledBack && barTurnedDown) : (peakedAndPulledBack || barTurnedDown || tickedDownFromPrev);
|
|
1767
|
+
const readyShort = isStrongSurge ? (peakedAndPulledBack && (barTurnedDown || intrabarReversalDown)) : (peakedAndPulledBack || barTurnedDown || tickedDownFromPrev);
|
|
1756
1768
|
if (readyShort) {
|
|
1757
1769
|
entryImpulseWatch.delete(wKey);
|
|
1758
1770
|
r.price = curPx;
|
|
@@ -1765,10 +1777,11 @@ async function filterByImpulseExhaustion(acc, cands, side, specs, nowTs = Date.n
|
|
|
1765
1777
|
const recentLow = Math.min(prevW?.extremePx || Infinity, min1m);
|
|
1766
1778
|
const bouncePct = Number.isFinite(recentLow) && recentLow > 0 ? ((curPx - recentLow) / recentLow) * 100 : 0;
|
|
1767
1779
|
const bounceTicks = Number.isFinite(recentLow) && recentLow > 0 ? (curPx - recentLow) / step : 0;
|
|
1768
|
-
const barTurnedUp = Boolean(last1m && prev1m && last1m.
|
|
1780
|
+
const barTurnedUp = Boolean(last1m && prev1m && last1m.low >= prev1m.low - step * 0.5 && last1m.close >= last1m.open);
|
|
1781
|
+
const intrabarReversalUp = Boolean(last1m && bouncePct >= Math.max(0.55, minPullbackPct * 1.4) && bounceTicks >= 4 && curPx >= last1m.low + step * 3);
|
|
1769
1782
|
const bottomedAndBounced = bouncePct >= minPullbackPct && bounceTicks >= 2 && (!last1m || last1m.close >= last1m.open - step * 0.5);
|
|
1770
1783
|
const tickedUpFromPrev = Boolean(!isStrongSurge && prevW && curPx >= prevW.lastPx + step && bounceTicks >= 2 && bouncePct >= 0.12);
|
|
1771
|
-
const readyLong = isStrongSurge ? (bottomedAndBounced && barTurnedUp) : (bottomedAndBounced || barTurnedUp || tickedUpFromPrev);
|
|
1784
|
+
const readyLong = isStrongSurge ? (bottomedAndBounced && (barTurnedUp || intrabarReversalUp)) : (bottomedAndBounced || barTurnedUp || tickedUpFromPrev);
|
|
1772
1785
|
if (readyLong) {
|
|
1773
1786
|
entryImpulseWatch.delete(wKey);
|
|
1774
1787
|
r.price = curPx;
|
|
@@ -1972,10 +1985,14 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
1972
1985
|
const divGuardOf = (sym, px) => liveDividendGuard(sym, Number(px) || specs[sym]?.lastPrice || 0, nowTs);
|
|
1973
1986
|
const divBlocked = [];
|
|
1974
1987
|
const entryTimeOk = timeOk && !edgeWarn && !exitsOnly;
|
|
1975
|
-
const
|
|
1988
|
+
const canBuyRow = (r) => (r.signal && r.expectedPct >= reqBuyEdgeOf(r.symbol))
|
|
1989
|
+
|| (isOnImpulseWatch(acc.id, r.symbol, 'buy', nowTs) && r.liquid && r.inIndex !== false && !r.buyBlockedByDip && !r.buyBlockedByOverheat && !r.buyBlockedByDiv && !r.buyBlockedByNews && r.expectedPct >= reqBuyEdgeOf(r.symbol) * IMPULSE_WATCH_HOLD_RATIO);
|
|
1990
|
+
const canShortRow = (r) => (r.shortSignal && r.expectedPct <= -reqShortEdgeOf(r.symbol))
|
|
1991
|
+
|| (isOnImpulseWatch(acc.id, r.symbol, 'short', nowTs) && r.liquid && r.inIndex !== false && r.shortAllowed !== false && !r.shortBlockedBySqueeze && !r.shortBlockedByDiv && !r.shortBlockedByNews && r.expectedPct <= -reqShortEdgeOf(r.symbol) * IMPULSE_WATCH_HOLD_RATIO);
|
|
1992
|
+
const buyCands = !entryTimeOk ? [] : fc.rows.filter((r) => canBuyRow(r) && !isInCooldown(r.symbol) && !isDayBlocked(r.symbol, 'buy') && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.bad)
|
|
1976
1993
|
.filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockLong) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
|
|
1977
1994
|
.sort((a, b) => b.expectedPct - a.expectedPct).slice(0, K);
|
|
1978
|
-
const allShortSignals = (rawDir !== 'long' && entryTimeOk) ? fc.rows.filter((r) =>
|
|
1995
|
+
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') && !quarantined.has(r.symbol) && !nb?.get(r.symbol)?.good)
|
|
1979
1996
|
.filter((r) => { const g = divGuardOf(r.symbol, r.price); if (g?.blockShort) { divBlocked.push(`${r.symbol} (отсечка ${g.exDate}, ${g.yieldPct}%)`); return false; } return true; })
|
|
1980
1997
|
.sort((a, b) => a.expectedPct - b.expectedPct) : [];
|
|
1981
1998
|
const nonShortableSkipped = [];
|
package/server/model/forecast.js
CHANGED
|
@@ -19,17 +19,29 @@ const inflight = new Map();
|
|
|
19
19
|
const inflightStartedAt = new Map();
|
|
20
20
|
let nearSignalsCache = { key: null, scannedBarTs: 0, episodesMap: new Map(), list: [] };
|
|
21
21
|
|
|
22
|
-
// Синхронная живая доходность индекса относительно
|
|
23
|
-
// если
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// Синхронная живая доходность индекса относительно базовой закрытой свечи baseIdx:
|
|
23
|
+
// если последняя закрытая 10-минутная свеча idx уже закончилась (liveSnap.ts >= barTs + BAR_MS), живая цена относится к НОВОЙ
|
|
24
|
+
// формирующейся свече после idx — поэтому базой служит tk.close[idx], а не tk.close[idx - 1]. Иначе два подряд растущих бара
|
|
25
|
+
// складывались в одну точку и упирались в потолок 8σ одной свечи!
|
|
26
|
+
const liveBaseIndexOf = (ds, idx, liveSnap) => {
|
|
27
|
+
const barTs = ds.timeline?.[idx] || 0;
|
|
28
|
+
return (liveSnap?.ts && barTs > 0 && liveSnap.ts >= barTs + BAR_MS) ? idx : Math.max(0, idx - 1);
|
|
29
|
+
};
|
|
30
|
+
const liveElapsedBarsOf = (ds, idx, liveSnap) => {
|
|
31
|
+
const barTs = ds.timeline?.[idx] || 0;
|
|
32
|
+
if (!(liveSnap?.ts && barTs > 0)) return 1;
|
|
33
|
+
const startTs = liveSnap.ts >= barTs + BAR_MS ? barTs + BAR_MS : barTs;
|
|
34
|
+
return Math.max(1, (liveSnap.ts - startTs) / BAR_MS);
|
|
35
|
+
};
|
|
36
|
+
|
|
26
37
|
function computeLiveMarketReturn(ds, idx, liveSnap) {
|
|
27
38
|
if (idx < 1 || idx >= ds.T) return 0;
|
|
28
39
|
if (liveSnap?.prices?.size > 0) {
|
|
40
|
+
const baseIdx = liveBaseIndexOf(ds, idx, liveSnap);
|
|
29
41
|
let wSum = 0, rSum = 0, cnt = 0;
|
|
30
42
|
for (const tk of ds.tokens) {
|
|
31
43
|
if (tk.inIndex && !tk.inIndex[idx]) continue;
|
|
32
|
-
const prevPx = tk.close[
|
|
44
|
+
const prevPx = tk.close[baseIdx];
|
|
33
45
|
const livePx = liveSnap.prices.get(tk.symbol);
|
|
34
46
|
if (livePx > 0 && prevPx > 0) {
|
|
35
47
|
const w = tk.weight > 0 ? tk.weight : 1;
|
|
@@ -39,44 +51,53 @@ function computeLiveMarketReturn(ds, idx, liveSnap) {
|
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
if (cnt >= 15 && wSum > 0) return rSum / wSum;
|
|
42
|
-
const mPrev = ds.mclose?.[
|
|
54
|
+
const mPrev = ds.mclose?.[baseIdx] || 0;
|
|
43
55
|
if (liveSnap.imoexPrice > 0 && mPrev > 0) return Math.log(liveSnap.imoexPrice / mPrev);
|
|
44
56
|
}
|
|
45
57
|
return ds.mret[idx] || 0;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
// Быстрый расчёт признаков контекста в одной точке idx без переобучения матрицы внимания и весов (за ~1-2 мс)
|
|
60
|
+
// Быстрый расчёт признаков контекста в одной точке idx без переобучения матрицы внимания и весов (за ~1-2 мс).
|
|
61
|
+
// Выделяется дополнительный живой слот u+1, чтобы при формировании новой свечи (после закрытия idx) не затирать закрытую свечу idx!
|
|
49
62
|
function buildInferenceFoldAt(ds, idx, fTrain, fitted, liveSnap = null) {
|
|
50
63
|
const { tokens, mret } = ds;
|
|
51
64
|
const N = fTrain.N;
|
|
52
65
|
const { beta, sigma, cos } = fTrain;
|
|
53
66
|
const lo = Math.max(1, idx - FEATURE_WARMUP - 2);
|
|
54
|
-
const
|
|
55
|
-
const
|
|
67
|
+
const uClosed = idx - lo;
|
|
68
|
+
const barTs = ds.timeline[idx] || 0;
|
|
69
|
+
const hasNewLiveBar = Boolean(liveSnap?.prices?.size > 0 && liveSnap.ts && barTs > 0 && liveSnap.ts >= barTs + BAR_MS);
|
|
70
|
+
const u = hasNewLiveBar ? uClosed + 1 : uClosed;
|
|
71
|
+
const W = uClosed + 2;
|
|
56
72
|
|
|
57
73
|
const z = new Float64Array(W * N);
|
|
58
74
|
const vr = new Float64Array(W * N);
|
|
59
|
-
|
|
60
|
-
const elapsedBars = liveSnap?.ts && barTs > 0 ? Math.max(1, (liveSnap.ts - barTs) / BAR_MS) : 1;
|
|
61
|
-
const liveScale = 1 / Math.sqrt(elapsedBars);
|
|
62
|
-
const mLive = liveSnap?.prices?.size > 0 ? computeLiveMarketReturn(ds, idx, liveSnap) : (mret[idx] || 0);
|
|
63
|
-
for (let uk = 0; uk < W; uk++) {
|
|
75
|
+
for (let uk = 0; uk <= uClosed; uk++) {
|
|
64
76
|
const t = lo + uk;
|
|
65
|
-
const
|
|
66
|
-
const mt = isLast ? mLive : mret[t];
|
|
77
|
+
const mt = mret[t];
|
|
67
78
|
for (let i = 0; i < N; i++) {
|
|
68
79
|
const tk = tokens[i];
|
|
69
|
-
|
|
70
|
-
if (isLast) {
|
|
71
|
-
const prevPx = tk.close[t - 1];
|
|
72
|
-
const livePx = liveSnap.prices.get(tk.symbol);
|
|
73
|
-
if (livePx > 0 && prevPx > 0) rt = Math.log(livePx / prevPx);
|
|
74
|
-
}
|
|
75
|
-
const e = (rt - beta[i] * mt) * (isLast ? liveScale : 1);
|
|
80
|
+
const e = tk.ret[t] - beta[i] * mt;
|
|
76
81
|
z[uk * N + i] = Math.max(-8, Math.min(8, e / sigma[i]));
|
|
77
82
|
vr[uk * N + i] = Math.min(4, tk.valRatio[t]);
|
|
78
83
|
}
|
|
79
84
|
}
|
|
85
|
+
vr[(uClosed + 1) * N] = 1;
|
|
86
|
+
for (let i = 0; i < N; i++) vr[(uClosed + 1) * N + i] = Math.min(4, tokens[i].valRatio[idx]);
|
|
87
|
+
|
|
88
|
+
if (liveSnap?.prices?.size > 0) {
|
|
89
|
+
const baseIdx = liveBaseIndexOf(ds, idx, liveSnap);
|
|
90
|
+
const liveScale = 1 / Math.sqrt(liveElapsedBarsOf(ds, idx, liveSnap));
|
|
91
|
+
const mLive = computeLiveMarketReturn(ds, idx, liveSnap);
|
|
92
|
+
for (let i = 0; i < N; i++) {
|
|
93
|
+
const tk = tokens[i];
|
|
94
|
+
const prevPx = tk.close[baseIdx];
|
|
95
|
+
const livePx = liveSnap.prices.get(tk.symbol);
|
|
96
|
+
const rt = (livePx > 0 && prevPx > 0) ? Math.log(livePx / prevPx) : (hasNewLiveBar ? 0 : tk.ret[idx]);
|
|
97
|
+
const e = (rt - beta[i] * mLive) * liveScale;
|
|
98
|
+
z[u * N + i] = Math.max(-8, Math.min(8, e / sigma[i]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
80
101
|
|
|
81
102
|
const X = KINDS.map(() => SCALES.map(() => new Float32Array(W * N)));
|
|
82
103
|
SCALES.forEach((sc, si) => {
|
|
@@ -101,17 +122,19 @@ function buildInferenceFoldAt(ds, idx, fTrain, fitted, liveSnap = null) {
|
|
|
101
122
|
const list = fTrain.macro.list.map((mTrain) => {
|
|
102
123
|
const dm = ds.macro.find((x) => x.id === mTrain.id);
|
|
103
124
|
const zs = new Float32Array(SCALES.length * W);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
if (dm) {
|
|
126
|
+
const msig = mTrain.msig || 1e-3;
|
|
127
|
+
for (const uk of [uClosed, uClosed + 1]) {
|
|
128
|
+
SCALES.forEach((sc, si) => {
|
|
129
|
+
let sum = 0;
|
|
130
|
+
for (let k = 0; k < sc.len; k++) {
|
|
131
|
+
const t = Math.min(idx, lo + uk - sc.from - k);
|
|
132
|
+
if (t >= 0) sum += Math.max(-8, Math.min(8, dm.ret[t] / msig));
|
|
133
|
+
}
|
|
134
|
+
zs[si * W + uk] = sum / Math.sqrt(sc.len);
|
|
135
|
+
});
|
|
111
136
|
}
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
}
|
|
137
|
+
}
|
|
115
138
|
return { id: mTrain.id, label: mTrain.label, b: mTrain.b, zs };
|
|
116
139
|
});
|
|
117
140
|
macro = { list };
|
|
@@ -213,27 +236,33 @@ async function fetchLiveMoexSnapshot() {
|
|
|
213
236
|
}
|
|
214
237
|
}
|
|
215
238
|
|
|
216
|
-
// Мгновенное обновление
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
if (idx < 1 || idx >= ds.T) return;
|
|
239
|
+
// Мгновенное обновление живой точки контекста в уже обученной модели по живым котировкам (за ~2 мс).
|
|
240
|
+
// Возвращает индекс активной точки u (uClosed или uClosed+1, если после закрытия свечи idx уже идёт новая свеча).
|
|
241
|
+
function applyLivePricesToFold(ds, f, idx, fitted, liveSnap) {
|
|
242
|
+
const uClosed = idx - f.lo;
|
|
243
|
+
if (!liveSnap?.prices?.size || idx < 1 || idx >= ds.T) return uClosed;
|
|
221
244
|
const N = f.N;
|
|
222
|
-
const mLive = computeLiveMarketReturn(ds, idx, liveSnap);
|
|
223
245
|
const barTs = ds.timeline[idx] || 0;
|
|
224
|
-
const
|
|
225
|
-
const
|
|
246
|
+
const hasNewLiveBar = Boolean(liveSnap.ts && barTs > 0 && liveSnap.ts >= barTs + BAR_MS && uClosed + 1 < f.W);
|
|
247
|
+
const u = hasNewLiveBar ? uClosed + 1 : uClosed;
|
|
248
|
+
// восстанавливаем чистую закрытую свечу idx на случай, если на предыдущем тике она обновлялась внутри свечи
|
|
249
|
+
for (let i = 0; i < N; i++) {
|
|
250
|
+
const tk = ds.tokens[i];
|
|
251
|
+
const e0 = tk.ret[idx] - f.beta[i] * ds.mret[idx];
|
|
252
|
+
f.z[uClosed * N + i] = Math.max(-8, Math.min(8, e0 / f.sigma[i]));
|
|
253
|
+
}
|
|
254
|
+
const baseIdx = liveBaseIndexOf(ds, idx, liveSnap);
|
|
255
|
+
const mLive = computeLiveMarketReturn(ds, idx, liveSnap);
|
|
256
|
+
const liveScale = 1 / Math.sqrt(liveElapsedBarsOf(ds, idx, liveSnap));
|
|
226
257
|
|
|
227
|
-
// 1) пересчитываем z[u*N + i] по живой цене акции
|
|
258
|
+
// 1) пересчитываем z[u*N + i] по живой цене акции относительно закрытия базовой свечи
|
|
228
259
|
for (let i = 0; i < N; i++) {
|
|
229
260
|
const tk = ds.tokens[i];
|
|
230
|
-
const prevPx = tk.close[
|
|
261
|
+
const prevPx = tk.close[baseIdx];
|
|
231
262
|
const livePx = liveSnap.prices.get(tk.symbol);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
f.z[u * N + i] = Math.max(-8, Math.min(8, e / f.sigma[i]));
|
|
236
|
-
}
|
|
263
|
+
const ret = (livePx > 0 && prevPx > 0) ? Math.log(livePx / prevPx) : (hasNewLiveBar ? 0 : tk.ret[idx]);
|
|
264
|
+
const e = (ret - f.beta[i] * mLive) * liveScale;
|
|
265
|
+
f.z[u * N + i] = Math.max(-8, Math.min(8, e / f.sigma[i]));
|
|
237
266
|
}
|
|
238
267
|
|
|
239
268
|
// 2) пересчитываем все масштабы SCALES в точке u (как в buildInferenceFoldAt)
|
|
@@ -246,8 +275,9 @@ function applyLivePricesToFold(ds, f, u, fitted, liveSnap) {
|
|
|
246
275
|
const uk = u - sc.from - k;
|
|
247
276
|
if (uk < 0) break;
|
|
248
277
|
const zk = f.z[uk * N + i];
|
|
278
|
+
const t = Math.min(idx, f.lo + uk);
|
|
249
279
|
sp += zk;
|
|
250
|
-
sf += zk * Math.min(4, ds.tokens[i].valRatio[
|
|
280
|
+
sf += zk * Math.min(4, ds.tokens[i].valRatio[t]);
|
|
251
281
|
}
|
|
252
282
|
Xp[u * N + i] = sp * invP;
|
|
253
283
|
Xf[u * N + i] = sf * invF;
|
|
@@ -272,6 +302,7 @@ function applyLivePricesToFold(ds, f, u, fitted, liveSnap) {
|
|
|
272
302
|
}
|
|
273
303
|
}
|
|
274
304
|
}
|
|
305
|
+
return u;
|
|
275
306
|
}
|
|
276
307
|
|
|
277
308
|
// Обучение ядра модели (карта рынка + веса внимания) выполняется не чаще 1 раза в торговый день для каждого набора параметров.
|
|
@@ -357,7 +388,8 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
|
|
|
357
388
|
const idx30m = Math.max(0, idx - 3);
|
|
358
389
|
const idx1d = Math.max(0, idx - bpd);
|
|
359
390
|
const idx3d = Math.max(0, idx - 3 * bpd);
|
|
360
|
-
const
|
|
391
|
+
const baseIdx = liveBaseIndexOf(ds, idx, liveSnap);
|
|
392
|
+
const mPrevBar = idx > 0 ? (ds.mclose?.[baseIdx] || 0) : 0;
|
|
361
393
|
const mLiveRet = computeLiveMarketReturn(ds, idx, liveSnap);
|
|
362
394
|
const mNow = liveSnap?.prices?.size > 0 && mPrevBar > 0
|
|
363
395
|
? mPrevBar * Math.exp(mLiveRet)
|
|
@@ -368,7 +400,7 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
|
|
|
368
400
|
const rows = ds.tokens.map((tk, i) => {
|
|
369
401
|
let exp = preds[chosen][i] * f.sigma[i] * Math.sqrt(H);
|
|
370
402
|
const weight = avgInv > 0 && f.sigma[i] > 0 ? Math.max(0.4, Math.min(1.8, (1 / f.sigma[i]) / avgInv)) : 1;
|
|
371
|
-
const prevPx = idx > 0 ? (tk.close[
|
|
403
|
+
const prevPx = idx > 0 ? (tk.close[baseIdx] || 0) : 0;
|
|
372
404
|
const livePx = liveSnap?.prices?.get(tk.symbol) || tk.close[idx] || 0;
|
|
373
405
|
const rawBarRet = (livePx > 0 && prevPx > 0) ? Math.log(livePx / prevPx) : (tk.ret?.[idx] || 0);
|
|
374
406
|
const rawResPct = (rawBarRet - f.beta[i] * mLiveRet) * 100;
|
|
@@ -742,11 +774,12 @@ const fmtLiveBarLabel = (barTs, liveSnapTs) => {
|
|
|
742
774
|
// Мгновенный пересчёт последнего среза прогноза по живым секундным котировкам (без обращения к истории свечей)
|
|
743
775
|
async function tickLiveForecastState(p, now = Date.now()) {
|
|
744
776
|
const liveSnap = await fetchLiveMoexSnapshot();
|
|
745
|
-
|
|
777
|
+
const uActive = liveSnap ? applyLivePricesToFold(baseState.ds, baseState.f, baseState.last, baseState.fitted, liveSnap) : baseState.u;
|
|
778
|
+
baseState.u = uActive;
|
|
746
779
|
const effHorizon = p.modelAutoAdapt !== false ? 2.5 : p.modelHorizon;
|
|
747
780
|
const effMinEdge = p.modelAutoAdapt !== false ? 2.5 : p.modelMinEdge;
|
|
748
781
|
const H = barsFromHours(effHorizon);
|
|
749
|
-
const r = buildPredictionRows(baseState.ds, p, baseState.last, baseState.f,
|
|
782
|
+
const r = buildPredictionRows(baseState.ds, p, baseState.last, baseState.f, uActive, baseState.fitted, baseState.chosen, H, effMinEdge, p.modelAutoAdapt !== false, true, liveSnap);
|
|
750
783
|
const sortedRows = r.rows.map(({ i, expected, ...x }) => ({ ...x, cluster: baseState.snap.clusters[i] })).sort((a, b) => b.expectedPct - a.expectedPct);
|
|
751
784
|
attachDeltas(sortedRows, baseState);
|
|
752
785
|
const liveTs = liveSnap?.ts || now;
|