neuroalpha 0.6.11 → 0.6.13
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/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +5 -4
- 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 +12 -9
- package/package.json +1 -1
- package/server/brokers/index.js +179 -146
- package/server/index.js +33 -8
- package/server/model/dataset.js +2 -4
- package/server/model/forecast.js +1 -3
- package/server/model/policy.js +173 -35
- package/server/model/research.js +4 -3
- package/server/model/simulator.js +148 -19
- package/server/news/index.js +2 -3
- package/server/strategist/index.js +7 -6
package/server/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
2
|
import http from 'node:http';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import { spawn } from 'node:child_process';
|
|
5
6
|
import path from 'node:path';
|
|
@@ -141,11 +142,26 @@ app.use('/docs', express.static(path.join(ROOT, 'docs')));
|
|
|
141
142
|
app.use(express.static(path.join(ROOT, 'public')));
|
|
142
143
|
|
|
143
144
|
function lanUrls() {
|
|
144
|
-
return Object.
|
|
145
|
+
return Object.entries(os.networkInterfaces())
|
|
146
|
+
.filter(([name]) => !/vethernet|wsl|docker|hyper-v|virtualbox|vmware/i.test(name))
|
|
147
|
+
.flatMap(([, list]) => list || [])
|
|
145
148
|
.filter((a) => a && a.family === 'IPv4' && !a.internal)
|
|
146
149
|
.map((a) => `http://${a.address}:${PORT}`);
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
function fmtClickableUrl(url, out = process.stdout) {
|
|
153
|
+
if (!out?.isTTY) return url;
|
|
154
|
+
const osc8 = Boolean(
|
|
155
|
+
process.env.WT_SESSION
|
|
156
|
+
|| process.env.TERM_PROGRAM
|
|
157
|
+
|| process.env.COLORTERM
|
|
158
|
+
|| process.env.FORCE_HYPERLINK === '1'
|
|
159
|
+
|| (process.platform !== 'win32' && process.env.TERM && process.env.TERM !== 'dumb'),
|
|
160
|
+
);
|
|
161
|
+
const styled = `\x1b[4;96m${url}\x1b[0m`;
|
|
162
|
+
return osc8 ? `\x1b]8;;${url}\x1b\\${styled}\x1b]8;;\x1b\\` : styled;
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
|
|
150
166
|
|| (process.env.OPEN_BROWSER !== '0'
|
|
151
167
|
&& process.env.NO_BROWSER !== '1'
|
|
@@ -157,9 +173,10 @@ const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
|
|
|
157
173
|
|
|
158
174
|
wss.on('error', () => {});
|
|
159
175
|
server.on('error', (e) => {
|
|
176
|
+
const clientUrl = `http://localhost:${PORT}`;
|
|
160
177
|
if (e.code === 'EADDRINUSE') {
|
|
161
|
-
console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент:
|
|
162
|
-
if (shouldOpenBrowser) openBrowser(
|
|
178
|
+
console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент: ${fmtClickableUrl(clientUrl, process.stderr)}`);
|
|
179
|
+
if (shouldOpenBrowser) openBrowser(clientUrl);
|
|
163
180
|
} else {
|
|
164
181
|
console.error(e);
|
|
165
182
|
}
|
|
@@ -168,12 +185,20 @@ server.on('error', (e) => {
|
|
|
168
185
|
|
|
169
186
|
server.listen(PORT, LISTEN_HOST, () => {
|
|
170
187
|
const ident = IS_INSTANCE ? getIdentity() : null;
|
|
171
|
-
|
|
188
|
+
let ver = '';
|
|
189
|
+
try { ver = ` v${JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version}`; } catch { /* ignore */ }
|
|
190
|
+
console.log(`◈ НейроАльфа${ver} запущена${IS_INSTANCE ? ` (${INSTANCE_KIND}) · машина ${ident.id || 'не активирована'} · центр ${ident.centralUrl}` : (IS_CENTRAL ? ' · центр' : '')}`);
|
|
172
191
|
const llm = activeModelInfo();
|
|
173
|
-
console.log(
|
|
174
|
-
if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
console.log(` ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (Настройки → Модели ИИ)'}`);
|
|
193
|
+
if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В сети: ${fmtClickableUrl(url)}`);
|
|
194
|
+
const clientUrl = `http://localhost:${PORT}`;
|
|
195
|
+
const enterHint = process.stdin?.isTTY && !IS_CENTRAL ? ' (Enter — открыть в браузере)' : '';
|
|
196
|
+
console.log(` Клиент: ${fmtClickableUrl(clientUrl)}${enterHint}`);
|
|
197
|
+
if (shouldOpenBrowser) openBrowser(clientUrl);
|
|
198
|
+
if (process.stdin?.isTTY && !IS_CENTRAL) {
|
|
199
|
+
process.stdin.on('data', () => openBrowser(clientUrl));
|
|
200
|
+
process.stdin.unref?.();
|
|
201
|
+
}
|
|
177
202
|
});
|
|
178
203
|
|
|
179
204
|
function openBrowser(url) {
|
package/server/model/dataset.js
CHANGED
|
@@ -295,10 +295,9 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
|
|
|
295
295
|
killer.unref?.();
|
|
296
296
|
}),
|
|
297
297
|
]);
|
|
298
|
-
} catch
|
|
298
|
+
} catch {
|
|
299
299
|
if (signal?.aborted) throw new Error('Отменено');
|
|
300
300
|
raw[s] = EMPTY();
|
|
301
|
-
console.warn(`[dataset] ${s}: ${e.message} — акция пропущена`);
|
|
302
301
|
} finally {
|
|
303
302
|
clearTimeout(killer);
|
|
304
303
|
onProgress({ symbol: s, done: ++done, total: all.length });
|
|
@@ -309,10 +308,9 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
|
|
|
309
308
|
try {
|
|
310
309
|
onProgress({ symbol: 'BR', done: done, total: all.length });
|
|
311
310
|
raw.BR = await loadBrent(fromTs, tillTs, signal, { localOnly });
|
|
312
|
-
} catch
|
|
311
|
+
} catch {
|
|
313
312
|
if (signal?.aborted) throw new Error('Отменено');
|
|
314
313
|
raw.BR = EMPTY();
|
|
315
|
-
console.warn(`[dataset] BR: ${e.message} — нефть пропущена`);
|
|
316
314
|
}
|
|
317
315
|
}
|
|
318
316
|
if (!raw.IMOEX?.n) throw new Error('Нет данных индекса МосБиржи');
|
package/server/model/forecast.js
CHANGED
|
@@ -966,9 +966,7 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
|
|
|
966
966
|
if (saved?.result && saved.key === key && now - (saved.result.at || 0) < 4 * 3600000) {
|
|
967
967
|
if (!inflight.has(key)) {
|
|
968
968
|
setImmediate(() => {
|
|
969
|
-
refreshDatasetStateInBackground(p, key, { force: false }).catch((
|
|
970
|
-
console.warn('[forecast] background warm-up error:', err?.message || err);
|
|
971
|
-
});
|
|
969
|
+
refreshDatasetStateInBackground(p, key, { force: false }).catch(() => {});
|
|
972
970
|
});
|
|
973
971
|
}
|
|
974
972
|
const liveSnap = await fetchLiveMoexSnapshot().catch(() => null);
|
package/server/model/policy.js
CHANGED
|
@@ -19,28 +19,44 @@ export const STRATEGY_POLICY = Object.freeze({
|
|
|
19
19
|
profitLock: true,
|
|
20
20
|
// размер позиции: 'slot' — доля бюджета × сила сигнала
|
|
21
21
|
sizing: Object.freeze({ mode: 'slot' }),
|
|
22
|
-
//
|
|
23
|
-
// • 1-й эшелон (33 ликвидные акции,
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// •
|
|
30
|
-
// •
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
// Адаптивное распределение размера позиции, сублинейное плечо и перелив капитала (water-filling + потеснение):
|
|
23
|
+
// • 1-й эшелон (33 ликвидные акции, 0 стопов на 59 051 свече): живой Conviction Sizing (базовый слот 0,56 × conv до 1,92×)
|
|
24
|
+
// разгоняет одиночные сильные импульсы до потолка 92% бюджета в лонг и 70% в шорт на 1× (убирая холостой простой кэша,
|
|
25
|
+
// т.к. 81,5% активного времени открыта 1 позиция). С ростом плеча счёта потолок одной бумаги растёт сублинейно
|
|
26
|
+
// (1 + 0,80 × (lev − 1): до 166% на 2× и 239% на 3×), оставляя остаток бесплатного плеча под залог LQDT свободным резервом;
|
|
27
|
+
// • 2-й эшелон: единый прямой лимит без двойного перемножения — до 17% бюджета в лонг (масштаб плеча 0,72) и до 7% в шорт (0,40),
|
|
28
|
+
// под жёстким отсечением по реальной ёмкости стакана (15% от медианного 10-минутного оборота свечи barTurnoverRub);
|
|
29
|
+
// • waterFill: при пакетном входе мелкие по лимиту сделки 2-го эшелона не «замораживают» квоту крупных лидеров 1-го эшелона;
|
|
30
|
+
// • crowdOut: двухступенчатое потеснение (ротация остывшей позиции или частичное подрезание без потери peakPrice/entryTs)
|
|
31
|
+
// при нехватке лимита под сильный сигнал 1-го эшелона (повышение качества T2 → T1 либо перекладка из остывшей T1).
|
|
32
|
+
// Проверка 13.11.2026 (06.2024–11.2026, 59 051 свеча): улучшает итог и PF на обеих половинах истории при любом плече и тарифе
|
|
33
|
+
// • плечо 1×: CAGR 59,9%/38,7% по половинам (весь 48,7%, PF 1,90, DD 3,4%) против 49,2%/30,7% (весь 39,5%, PF 1,85);
|
|
34
|
+
// • плечо 2×: CAGR 95,0%/62,4% по половинам (весь 77,6%, 6 мес. 165,1%, PF 2,05, DD 5,6%) против 73,8%/47,4% (весь 59,8%, PF 2,03);
|
|
35
|
+
// • плечо 3×: CAGR 133,7%/90,1% по половинам (весь 110,3%, 6 мес. 292,3%, PF 2,18, DD 7,1%) против 104,6%/65,6% (весь 83,7%, PF 2,07).
|
|
36
|
+
slotOwner: 0.56,
|
|
37
|
+
convMaxOwner: 1.92,
|
|
38
|
+
convKOwner: 0.38,
|
|
39
|
+
capT1Long: 0.92,
|
|
40
|
+
capT1Short: 0.70,
|
|
41
|
+
capT2Long: 0.17,
|
|
42
|
+
capT2Short: 0.070,
|
|
43
|
+
slotT2LongMult: 0.25,
|
|
44
|
+
slotT2ShortMult: 0.10,
|
|
45
|
+
levSlopeT1: 0.80,
|
|
46
|
+
levSlopeT2Long: 0.72,
|
|
47
|
+
levSlopeT2Short: 0.40,
|
|
40
48
|
turnoverCapMult: 0.15,
|
|
41
49
|
tpRetainT2: 0.75,
|
|
42
50
|
tpTrailT2: 0.35,
|
|
43
51
|
netPickSort: true,
|
|
52
|
+
waterFill: true,
|
|
53
|
+
crowdOut: true,
|
|
54
|
+
crowdMinMin: 20,
|
|
55
|
+
crowdMinPct: 0.06,
|
|
56
|
+
crowdRotBufPct: 0.08,
|
|
57
|
+
crowdTrimBufPct: 0.12,
|
|
58
|
+
crowdCoolRatioT1: 0.45,
|
|
59
|
+
crowdCoolRatioT2: 0.80,
|
|
44
60
|
// капитал хранится в фонде денежного рынка LQDT как залог: внутридневные позиции открываются под обеспечение
|
|
45
61
|
// (без платы за перенос — позиции закрываются до ночи), LQDT не продаётся под входы
|
|
46
62
|
cashYield: true,
|
|
@@ -141,44 +157,166 @@ export function exitHoldRatio(heldMin, horizonMin, { sigmaBarPct = 0, realizedBa
|
|
|
141
157
|
return startRatio + (endRatio - startRatio) * ramp;
|
|
142
158
|
}
|
|
143
159
|
|
|
144
|
-
// ---------- плечо и
|
|
160
|
+
// ---------- профиль слотов по тарифу, плечо и предельный размер позиции ----------
|
|
145
161
|
export const MAX_LEVERAGE = 7;
|
|
162
|
+
|
|
163
|
+
export function planSizingProfile(plan = 'owner', dir = 'longshort', policy = STRATEGY_POLICY) {
|
|
164
|
+
const id = typeof plan === 'object' && plan ? (plan.isOwner ? 'owner' : (plan.id || 'investor')) : String(plan || 'owner');
|
|
165
|
+
if (id === 'owner') {
|
|
166
|
+
return {
|
|
167
|
+
id: 'owner',
|
|
168
|
+
slotW: policy?.slotPct ?? policy?.slotOwner ?? 0.56,
|
|
169
|
+
convMax: policy?.convMaxOwner ?? 1.92,
|
|
170
|
+
convK: policy?.convKOwner ?? 0.38,
|
|
171
|
+
capT1Long: policy?.capT1Long ?? 0.92,
|
|
172
|
+
capT1Short: policy?.capT1Short ?? 0.70,
|
|
173
|
+
capT2Long: policy?.capT2Long ?? 0.17,
|
|
174
|
+
capT2Short: policy?.capT2Short ?? 0.070,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (id === 'vip') {
|
|
178
|
+
return {
|
|
179
|
+
id: 'vip',
|
|
180
|
+
slotW: policy?.slotPct ?? 0.46,
|
|
181
|
+
convMax: 1.50,
|
|
182
|
+
convK: 0.28,
|
|
183
|
+
capT1Long: 0.75,
|
|
184
|
+
capT1Short: 0.56,
|
|
185
|
+
capT2Long: 0.14,
|
|
186
|
+
capT2Short: 0.065,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (id === 'pro') {
|
|
190
|
+
return {
|
|
191
|
+
id: 'pro',
|
|
192
|
+
slotW: policy?.slotPct ?? (dir === 'long' ? 0.36 : 0.34),
|
|
193
|
+
convMax: 1.0,
|
|
194
|
+
convK: 0,
|
|
195
|
+
capT1Long: 0.52,
|
|
196
|
+
capT1Short: 0.40,
|
|
197
|
+
capT2Long: 0.12,
|
|
198
|
+
capT2Short: 0.060,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
id: 'investor',
|
|
203
|
+
slotW: policy?.slotPct ?? 0.42,
|
|
204
|
+
convMax: 1.0,
|
|
205
|
+
convK: 0,
|
|
206
|
+
capT1Long: 0.58,
|
|
207
|
+
capT1Short: 0,
|
|
208
|
+
capT2Long: 0.14,
|
|
209
|
+
capT2Short: 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function convictionMult(edgeRatio = 1, profile = null) {
|
|
214
|
+
const convMax = profile?.convMax ?? 1;
|
|
215
|
+
const convK = profile?.convK ?? 0;
|
|
216
|
+
if (!(convMax > 1) || !(edgeRatio > 0)) return 1;
|
|
217
|
+
return Math.min(convMax, Math.max(0.85, 0.80 + convK * edgeRatio));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Сублинейный множитель масштабирования одиночной позиции от плеча счёта (на плече 1× равен 1,00):
|
|
221
|
+
// разгружает риск одиночной бумаги на высоких плечах и оставляет свободный маржинальный резерв под параллельные идеи.
|
|
146
222
|
export function tierSizeMult(symbol, leverage = 1, sideOrPolicy = 'long', policyArg = STRATEGY_POLICY) {
|
|
147
223
|
const isObj = sideOrPolicy && typeof sideOrPolicy === 'object';
|
|
148
224
|
const side = isObj ? 'long' : (sideOrPolicy === 'short' ? 'short' : 'long');
|
|
149
225
|
const policy = isObj ? sideOrPolicy : (policyArg || STRATEGY_POLICY);
|
|
150
226
|
const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
227
|
+
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
228
|
+
// Обратная совместимость со старыми вариантами лаборатории (sizeMultT2 / sizeMultT2LongBase)
|
|
229
|
+
if (policy?.sizeMultT2 !== undefined) return isT1 ? (policy.sizeMultT1 ?? lev) : policy.sizeMultT2;
|
|
230
|
+
if (policy?.sizeMultT2LongBase !== undefined) {
|
|
231
|
+
if (isT1) return policy.sizeMultT1 ?? lev;
|
|
232
|
+
const rawOld = side === 'short'
|
|
233
|
+
? Math.max(policy.sizeMultT2ShortBase ?? 0.35, (policy.sizeMultT2ShortLev ?? 0.22) * lev)
|
|
234
|
+
: Math.max(policy.sizeMultT2LongBase ?? 0.50, (policy.sizeMultT2LongLev ?? 0.38) * lev);
|
|
235
|
+
return Math.round(rawOld * 1000) / 1000;
|
|
236
|
+
}
|
|
237
|
+
if (isT1 && policy?.sizeMultT1 !== undefined) return policy.sizeMultT1;
|
|
238
|
+
const slope = isT1
|
|
239
|
+
? (policy?.levSlopeT1 ?? 0.80)
|
|
240
|
+
: (side === 'short' ? (policy?.levSlopeT2Short ?? 0.40) : (policy?.levSlopeT2Long ?? 0.72));
|
|
241
|
+
return Math.round((1 + slope * (lev - 1)) * 1000) / 1000;
|
|
157
242
|
}
|
|
158
243
|
|
|
159
244
|
// ---------- предельный размер позиции на одну бумагу ----------
|
|
160
|
-
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY) {
|
|
245
|
+
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY, profile = null) {
|
|
161
246
|
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
162
247
|
const isShort = side === 'short';
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
248
|
+
const legacyMode = policy?.sizeMultT2 !== undefined || policy?.sizeMultT2LongBase !== undefined;
|
|
249
|
+
let capPct;
|
|
250
|
+
if (legacyMode) {
|
|
251
|
+
capPct = isShort
|
|
252
|
+
? (isT1 ? (policy?.capT1Short ?? 0.38) : (policy?.capT2Short ?? 0.18))
|
|
253
|
+
: (isT1 ? (policy?.capT1Long ?? 0.50) : (policy?.capT2Long ?? 0.22));
|
|
254
|
+
} else if (profile) {
|
|
255
|
+
capPct = isShort
|
|
256
|
+
? (isT1 ? profile.capT1Short : profile.capT2Short)
|
|
257
|
+
: (isT1 ? profile.capT1Long : profile.capT2Long);
|
|
258
|
+
} else {
|
|
259
|
+
capPct = isShort
|
|
260
|
+
? (isT1 ? (policy?.capT1Short ?? 0.70) : (policy?.capT2Short ?? 0.070))
|
|
261
|
+
: (isT1 ? (policy?.capT1Long ?? 0.92) : (policy?.capT2Long ?? 0.17));
|
|
262
|
+
}
|
|
166
263
|
const rawCap = budget * capPct;
|
|
167
264
|
const turnMult = policy?.turnoverCapMult ?? 0.15;
|
|
168
265
|
return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * turnMult)) : rawCap;
|
|
169
266
|
}
|
|
170
267
|
|
|
171
|
-
// Размер новой позиции в рублях до учёта свободного
|
|
172
|
-
// 'slot' — рабочий слот × сила сигнала (conv), не больше предела бумаги;
|
|
268
|
+
// Размер новой позиции в рублях до учёта свободного лимита и итеративного перелива (water-filling):
|
|
269
|
+
// 'slot' — рабочий слот × множитель эшелона × сила сигнала (conv), не больше предела бумаги;
|
|
173
270
|
// 'risk' — при срабатывании стопа счёт теряет не больше riskPct% бюджета.
|
|
174
|
-
export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0 }, policy = STRATEGY_POLICY) {
|
|
175
|
-
const cap = symbolCapRub(symbol, side, budget, barTurnoverRub, policy);
|
|
176
|
-
const sz = policy
|
|
271
|
+
export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0, profile = null }, policy = STRATEGY_POLICY) {
|
|
272
|
+
const cap = symbolCapRub(symbol, side, budget, barTurnoverRub, policy, profile);
|
|
273
|
+
const sz = policy?.sizing || {};
|
|
177
274
|
if (sz.mode === 'risk' && stopPct > 0) {
|
|
178
275
|
const byRisk = (budget * (sz.riskPct / 100)) / (stopPct / 100);
|
|
179
276
|
return Math.min(cap, byRisk * (sz.useConv ? conv : 1), sz.maxSlotPct ? budget * sz.maxSlotPct : Infinity);
|
|
180
277
|
}
|
|
181
|
-
|
|
278
|
+
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
279
|
+
const legacyMode = policy?.sizeMultT2 !== undefined || policy?.sizeMultT2LongBase !== undefined;
|
|
280
|
+
const slotTierMult = (isT1 || legacyMode)
|
|
281
|
+
? 1.0
|
|
282
|
+
: (side === 'short' ? (policy?.slotT2ShortMult ?? 0.10) : (policy?.slotT2LongMult ?? 0.25));
|
|
283
|
+
return Math.min(cap, slotRub * slotTierMult * conv);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Итеративное распределение доступного лимита методом перелива (water-filling):
|
|
287
|
+
// бумаги с низким индивидуальным потолком (2-й эшелон / лимит оборота стакана) забирают ровно свой потолок,
|
|
288
|
+
// а весь невыбранный остаток капитала перераспределяется между флагманскими лидерами пропорционально чистой выгоде (weight).
|
|
289
|
+
export function waterFillAllocations(items, availRub) {
|
|
290
|
+
const n = Array.isArray(items) ? items.length : 0;
|
|
291
|
+
const out = new Float64Array(n);
|
|
292
|
+
if (!(availRub > 0) || !n) return out;
|
|
293
|
+
const totalDesired = items.reduce((s, x) => s + Math.max(0, Number(x.targetRub) || 0), 0);
|
|
294
|
+
if (totalDesired <= availRub) {
|
|
295
|
+
for (let k = 0; k < n; k++) out[k] = Math.max(0, Number(items[k].targetRub) || 0);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
let remAvail = availRub;
|
|
299
|
+
const open = new Set(items.map((_, idx) => idx));
|
|
300
|
+
while (open.size > 0 && remAvail > 0) {
|
|
301
|
+
let wSum = 0;
|
|
302
|
+
for (const idx of open) wSum += Math.max(1e-6, Number(items[idx].weight) || 0);
|
|
303
|
+
let clampedAny = false;
|
|
304
|
+
for (const idx of [...open]) {
|
|
305
|
+
const share = remAvail * (Math.max(1e-6, Number(items[idx].weight) || 0) / wSum);
|
|
306
|
+
const cap = Math.max(0, Number(items[idx].targetRub) || 0);
|
|
307
|
+
if (cap <= share + 1e-6) {
|
|
308
|
+
out[idx] = cap;
|
|
309
|
+
remAvail = Math.max(0, remAvail - cap);
|
|
310
|
+
open.delete(idx);
|
|
311
|
+
clampedAny = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!clampedAny) {
|
|
315
|
+
for (const idx of open) out[idx] = remAvail * (Math.max(1e-6, Number(items[idx].weight) || 0) / wSum);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
182
320
|
}
|
|
183
321
|
|
|
184
322
|
// ---------- контекст торгового дня бумаги по свечам до момента решения ----------
|
package/server/model/research.js
CHANGED
|
@@ -41,7 +41,8 @@ export { isMarketDown };
|
|
|
41
41
|
// 17: капитал в LQDT как залог, половинный размер 2-го эшелона, строки с плечом 3 и 5
|
|
42
42
|
// 18: глубина сопровождения позиции и вечернего окна входов holdBars=13 (130 мин) во всех контурах симуляции
|
|
43
43
|
// 19: ревизия правил 1-го и 2-го эшелонов (MTLR/BSPB/RTKM/SVCB в T1, асимметричный масштаб T2 от плеча, лимит 50%/38% T1 и плотный трал T2)
|
|
44
|
-
|
|
44
|
+
// 20: разблокировка Conviction Sizing в T1 (до 92%/70% с сублинейным плечом 0,80), прямой риск-лимит T2, перелив water-filling и двухступенчатое потеснение
|
|
45
|
+
const DATA_VERSION = 20;
|
|
45
46
|
|
|
46
47
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
47
48
|
function reuseParamsKey(params) {
|
|
@@ -822,7 +823,7 @@ async function run(runId, p, state) {
|
|
|
822
823
|
const lqdtRows = await ensureLqdtHistory().catch(() => []);
|
|
823
824
|
const lqdt = lqdtRows.length ? alignSeries(timeline, lqdtRows) : null;
|
|
824
825
|
const liveRows = [];
|
|
825
|
-
for (const lev of [1, 3, 5]) {
|
|
826
|
+
for (const lev of [1, 2, 3, 5]) {
|
|
826
827
|
const lp = simulateStrategy({ ds, wf: wfLive, p, capital: p.capital, shortable: shortableSet(), divCal, lqdt, leverage: lev });
|
|
827
828
|
const yrs = Math.max(1 / 365, ((lp.curve.at(-1)?.ts || 0) - (lp.curve[0]?.ts || 0)) / (365.25 * 86400000));
|
|
828
829
|
const lastLpTs = lp.curve.at(-1)?.ts || 0;
|
|
@@ -837,7 +838,7 @@ async function run(runId, p, state) {
|
|
|
837
838
|
liveRows.push({
|
|
838
839
|
key: lev === 1 ? 'liveRules' : `liveRulesL${lev}`,
|
|
839
840
|
label: lev === 1 ? 'Правила живого контура, без плеча (главная оценка)' : `Правила живого контура, плечо ${lev}`,
|
|
840
|
-
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы,
|
|
841
|
+
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы, итеративный перелив (water-filling), двухступенчатое потеснение и издержки по эшелонам${lqdt ? ', капитал в LQDT как залог' : ''}${lev > 1 ? `; сублинейный масштаб одиночной позиции при лимите портфеля ×${lev}` : ''}`,
|
|
841
842
|
full: { returnPct: full.returnPct, cagrPct, profitFactor: full.profitFactor, maxDrawdownPct: full.maxDrawdownPct, trades: full.trades, winRate: full.winRate, avgTradePct: full.avgTradePct, longPnl: round(lp.trades.filter((t) => t.side === 'long').reduce((a, t) => a + t.pnl, 0), 0), shortPnl: round(lp.trades.filter((t) => t.side === 'short').reduce((a, t) => a + t.pnl, 0), 0), afterTaxCagrPct: cagrPct === null ? null : round(cagrPct > 0 ? cagrPct * 0.87 : cagrPct, 1) },
|
|
842
843
|
costs: null,
|
|
843
844
|
last12: segOf(lastLpTs - 365 * 86400000),
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isMarketDown,
|
|
18
18
|
} from './edge.js';
|
|
19
19
|
import { nextDividendAt, dividendRules } from './dividends.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
STRATEGY_POLICY, MAX_LEVERAGE, sessionEdgeWarning, profitLockLevels, protectiveStopPct, exitHoldRatio,
|
|
22
|
+
planSizingProfile, convictionMult, tierSizeMult, targetPositionRub, waterFillAllocations,
|
|
23
|
+
counterTrendBlocked, thesisStopPx, dayContext,
|
|
24
|
+
} from './policy.js';
|
|
21
25
|
|
|
22
26
|
// Выравнивание внешнего ряда (свечи IMOEXF, дневные цены LQDT) по шкале датасета с переносом последней цены
|
|
23
27
|
export function alignSeries(timeline, rows) {
|
|
@@ -40,7 +44,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
40
44
|
const bpd = Math.max(1, Math.round(ds.barsPerDay || 94));
|
|
41
45
|
const stepBars = plan === 'owner' || plan === 'vip' ? Math.max(3, Math.floor(holdBars / 5)) : holdBars;
|
|
42
46
|
const K = policy.K ?? 4;
|
|
43
|
-
const dir = p.modelDirection === 'long' ? 'long' : 'longshort';
|
|
47
|
+
const dir = plan === 'investor' || p.modelDirection === 'long' ? 'long' : 'longshort';
|
|
44
48
|
const realCost = (p.commissionPct + p.slippagePct) / 100;
|
|
45
49
|
const need = 2.5 * 2 * realCost * (policy.entryEdgeMult ?? ENTRY_EDGE_MULT);
|
|
46
50
|
const roundTripFeePct = 2 * (p.commissionPct + p.slippagePct);
|
|
@@ -50,9 +54,15 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
50
54
|
const cooldownBars = Math.round((policy.cooldownMin ?? 30) / BAR_INTERVAL);
|
|
51
55
|
const stopCooldownBars = Math.round((policy.stopCooldownMin ?? 60) / BAR_INTERVAL);
|
|
52
56
|
const horizonMin = Math.max(90, holdBars * BAR_INTERVAL);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
57
|
+
const legacySizing = policy.sizeMultT2 !== undefined || policy.sizeMultT2LongBase !== undefined;
|
|
58
|
+
const profile = legacySizing
|
|
59
|
+
? {
|
|
60
|
+
slotW: policy.slotPct ?? (plan === 'owner' ? 0.50 : plan === 'vip' ? 0.40 : plan === 'pro' ? Math.min(0.28, 1.15 / 8) : Math.min(0.24, 1.05 / 5)),
|
|
61
|
+
convMax: plan === 'owner' ? 1.65 : plan === 'vip' ? 1.30 : 1,
|
|
62
|
+
convK: plan === 'owner' ? 0.30 : 0.25,
|
|
63
|
+
}
|
|
64
|
+
: planSizingProfile(plan, dir, policy);
|
|
65
|
+
const slotW = profile.slotW;
|
|
56
66
|
const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
|
|
57
67
|
const maxGross = policy.maxGross ?? 0.99 * lev;
|
|
58
68
|
const capScale = policy.capScale ?? 1;
|
|
@@ -69,7 +79,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
69
79
|
const curve = [];
|
|
70
80
|
const cooldownUntil = new Int32Array(N).fill(-1);
|
|
71
81
|
const stockPnl = new Float64Array(N), stockCnt = new Float64Array(N);
|
|
72
|
-
const stats = { stops: 0, thesis: 0, locks: 0, signalExits: 0, timeExits: 0, night: 0, rotations: 0, regime: 0, div: 0, blockedCT: 0, entries: 0, missedPassive: 0, hedgePnl: 0, hedgeCost: 0, cashYield: 0, exposureSum: 0, exposureN: 0 };
|
|
82
|
+
const stats = { stops: 0, thesis: 0, locks: 0, signalExits: 0, timeExits: 0, night: 0, rotations: 0, trims: 0, regime: 0, div: 0, blockedCT: 0, entries: 0, missedPassive: 0, hedgePnl: 0, hedgeCost: 0, cashYield: 0, exposureSum: 0, exposureN: 0 };
|
|
73
83
|
let hedgeRub = 0;
|
|
74
84
|
|
|
75
85
|
const divRule = (i, t) => {
|
|
@@ -110,6 +120,35 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
110
120
|
stockPnl[i] = stockPnl[i] * 0.90 + (g - 2 * c);
|
|
111
121
|
stockCnt[i] += 1;
|
|
112
122
|
};
|
|
123
|
+
const trimPos = (i, pos, exitT, exitPx, trimAllocRub, why) => {
|
|
124
|
+
const frac = Math.min(1, Math.max(0, trimAllocRub / pos.alloc));
|
|
125
|
+
if (frac >= 0.999) {
|
|
126
|
+
closePos(i, pos, exitT, exitPx, why, 'rotation');
|
|
127
|
+
stats.rotations++;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const c = costOf(i);
|
|
131
|
+
const g = gainOf(pos, exitPx);
|
|
132
|
+
const sliceAlloc = pos.alloc * frac;
|
|
133
|
+
const sliceEntryFee = pos.entryFee * frac;
|
|
134
|
+
const gross = sliceAlloc * g;
|
|
135
|
+
const fee = (sliceAlloc + (pos.side === 'long' ? gross : -gross)) * c;
|
|
136
|
+
equity += gross - fee;
|
|
137
|
+
if (collectTrades) {
|
|
138
|
+
trades.push({
|
|
139
|
+
symbol: tokens[i].symbol, side: pos.side, entryTs: timeline[pos.entryT], exitTs: timeline[Math.min(exitT, T - 1)],
|
|
140
|
+
entryPrice: pos.entryPx, exitPrice: exitPx, alloc: sliceAlloc,
|
|
141
|
+
pnl: gross - fee - sliceEntryFee, pnlPct: ((gross - fee - sliceEntryFee) / sliceAlloc) * 100,
|
|
142
|
+
commission: fee + sliceEntryFee, barsHeld: (exitT - pos.entryT) * BAR_INTERVAL,
|
|
143
|
+
expectedPct: pos.expectedPct, stopPct: pos.stopPct, reasonExit: why, exitKind: 'trim',
|
|
144
|
+
dayMovePct: pos.dayMovePct, counterTrend: pos.counterTrend, tier: isT1(i) ? 1 : 2,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
pos.alloc -= sliceAlloc;
|
|
148
|
+
pos.entryFee -= sliceEntryFee;
|
|
149
|
+
stats.trims++;
|
|
150
|
+
return false;
|
|
151
|
+
};
|
|
113
152
|
const mtmUnreal = (t) => {
|
|
114
153
|
let s = 0;
|
|
115
154
|
for (const q of held.values()) s += q.alloc * gainOf(q, tokens[q.i].close[t]);
|
|
@@ -304,8 +343,84 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
304
343
|
let nL = 0, nS = 0;
|
|
305
344
|
for (const q of held.values()) { if (q.side === 'long') nL++; else nS++; }
|
|
306
345
|
const budget = equity + mtmUnreal(t);
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
const pickWeight = (i) => (policy.netPickSort !== false ? Math.max(0.0001, Math.abs(edge(i)) - 2 * costOf(i)) : Math.max(0.0001, Math.abs(edge(i))));
|
|
347
|
+
const calcDesiredRub = (i, side, e, stopPct) => {
|
|
348
|
+
const tm = tierSizeMult(tokens[i].symbol, lev, side, policy);
|
|
349
|
+
if (!(tm > 0)) return 0;
|
|
350
|
+
const conv = convictionMult(Math.abs(e) / need, profile);
|
|
351
|
+
return targetPositionRub({
|
|
352
|
+
symbol: tokens[i].symbol, side,
|
|
353
|
+
budget: budget * capScale * tm,
|
|
354
|
+
slotRub: budget * slotW * tm,
|
|
355
|
+
conv, stopPct,
|
|
356
|
+
barTurnoverRub: isT1(i) ? 0 : barTurnover(i, t),
|
|
357
|
+
profile: legacySizing ? null : profile,
|
|
358
|
+
}, policy);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Двухступенчатое динамическое потеснение (ротация остывшей позиции или частичное подрезание T2 под флагман T1)
|
|
362
|
+
let avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
363
|
+
if (!legacySizing && policy.crowdOut !== false && held.size > 0 && (candL.length || candS.length)) {
|
|
364
|
+
const rawPicks = [
|
|
365
|
+
...candL.slice(0, Math.max(0, K - nL)).map((i) => [i, 'long']),
|
|
366
|
+
...candS.slice(0, Math.max(0, K - nS)).map((i) => [i, 'short']),
|
|
367
|
+
];
|
|
368
|
+
rawPicks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
|
|
369
|
+
const minAgeBars = Math.round((policy.crowdMinMin ?? 20) / BAR_INTERVAL);
|
|
370
|
+
const minChunkRub = Math.max(45000, budget * (policy.crowdMinPct ?? 0.06));
|
|
371
|
+
const rotBuf = (policy.crowdRotBufPct ?? 0.08) / 100;
|
|
372
|
+
const trimBuf = (policy.crowdTrimBufPct ?? 0.12) / 100;
|
|
373
|
+
const desiredItems = [];
|
|
374
|
+
for (const [i, side] of rawPicks) {
|
|
375
|
+
const e = edge(i);
|
|
376
|
+
const sigmaBarPct = sg[i] * 100;
|
|
377
|
+
const ctx = dayContext(ds, i, t, sigmaBarPct);
|
|
378
|
+
if (counterTrendBlocked(side, ctx, policy)) continue;
|
|
379
|
+
const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
|
|
380
|
+
desiredItems.push({ i, side, e, netEdge: pickWeight(i), targetRub: calcDesiredRub(i, side, e, stopPct) });
|
|
381
|
+
}
|
|
382
|
+
const totalDesired = desiredItems.reduce((s, x) => s + x.targetRub, 0);
|
|
383
|
+
let shortfall = totalDesired - avail;
|
|
384
|
+
const bestT1 = desiredItems.find((x) => isT1(x.i));
|
|
385
|
+
if (shortfall >= minChunkRub && bestT1) {
|
|
386
|
+
const elig = [];
|
|
387
|
+
for (const [hi, q] of held) {
|
|
388
|
+
if (t + 1 - q.entryT < minAgeBars) continue;
|
|
389
|
+
if (!(tokens[hi].open[t + 1] > 0)) continue;
|
|
390
|
+
const curSigned = q.side === 'long' ? edge(hi) : -edge(hi);
|
|
391
|
+
const thrEntry = q.side === 'long' ? Math.max(needL * mult[hi] * tierScale(hi), minEdgeL) : Math.max(needS * mult[hi] * tierScale(hi), minEdgeS);
|
|
392
|
+
elig.push({ hi, q, curSigned, thrEntry, tierRank: isT1(hi) ? 1 : 0 });
|
|
393
|
+
}
|
|
394
|
+
elig.sort((a, b) => (a.tierRank - b.tierRank) || (a.curSigned - b.curSigned));
|
|
395
|
+
for (const el of elig) {
|
|
396
|
+
if (shortfall < minChunkRub * 0.5) break;
|
|
397
|
+
const { hi, q, curSigned, thrEntry } = el;
|
|
398
|
+
const pxExit = fill === 'mid' ? (tokens[hi].open[t + 1] + tokens[hi].close[t + 1]) / 2 : tokens[hi].open[t + 1];
|
|
399
|
+
const heldIsT1 = isT1(hi);
|
|
400
|
+
const cooled = curSigned < thrEntry * (heldIsT1 ? (policy.crowdCoolRatioT1 ?? 0.45) : (policy.crowdCoolRatioT2 ?? 0.80));
|
|
401
|
+
const netAdv = bestT1.netEdge - Math.max(0, curSigned);
|
|
402
|
+
if (cooled && netAdv >= rotBuf) {
|
|
403
|
+
if (!heldIsT1 || curSigned < thrEntry * 0.35 || q.alloc - shortfall < minChunkRub) {
|
|
404
|
+
closePos(hi, q, t + 1, pxExit, 'Ротация в сильный сигнал', 'rotation');
|
|
405
|
+
stats.rotations++;
|
|
406
|
+
if (q.side === 'long') nL--; else nS--;
|
|
407
|
+
} else {
|
|
408
|
+
const trimAmt = Math.min(shortfall, q.alloc - minChunkRub);
|
|
409
|
+
if (trimAmt >= minChunkRub) trimPos(hi, q, t + 1, pxExit, trimAmt, 'Частичное потеснение под сильный сигнал');
|
|
410
|
+
}
|
|
411
|
+
avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
412
|
+
shortfall = totalDesired - avail;
|
|
413
|
+
} else if (!cooled && !heldIsT1 && netAdv >= trimBuf && q.alloc >= minChunkRub * 1.5) {
|
|
414
|
+
const trimAmt = Math.min(shortfall, q.alloc - minChunkRub);
|
|
415
|
+
if (trimAmt >= minChunkRub) {
|
|
416
|
+
trimPos(hi, q, t + 1, pxExit, trimAmt, 'Частичное потеснение под сильный сигнал');
|
|
417
|
+
avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
418
|
+
shortfall = totalDesired - avail;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if (legacySizing && isStep && (candL.length || candS.length) && grossUsed(t) > budget * maxGross * 0.65) {
|
|
309
424
|
const best = Math.max(candL.length ? edge(candL[0]) : 0, candS.length ? -edge(candS[0]) : 0);
|
|
310
425
|
if (best >= need * 2.2) {
|
|
311
426
|
let wi = -1, we = Infinity;
|
|
@@ -321,28 +436,42 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
321
436
|
}
|
|
322
437
|
}
|
|
323
438
|
}
|
|
439
|
+
|
|
324
440
|
const picks = [
|
|
325
441
|
...candL.slice(0, Math.max(0, K - nL)).map((i) => [i, 'long']),
|
|
326
442
|
...candS.slice(0, Math.max(0, K - nS)).map((i) => [i, 'short']),
|
|
327
443
|
];
|
|
328
|
-
const pickWeight = (i) => (policy.netPickSort !== false ? Math.max(0.0001, Math.abs(edge(i)) - 2 * costOf(i)) : Math.max(0.0001, Math.abs(edge(i))));
|
|
329
444
|
picks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
|
|
330
|
-
|
|
331
|
-
|
|
445
|
+
avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
446
|
+
const prepared = [];
|
|
332
447
|
for (const [i, side] of picks) {
|
|
333
448
|
const e = edge(i);
|
|
334
|
-
const w = pickWeight(i);
|
|
335
|
-
const propShare = remW > 0 ? avail * (w / remW) : avail;
|
|
336
|
-
remW -= w;
|
|
337
449
|
const sigmaBarPct = sg[i] * 100;
|
|
338
450
|
const ctx = dayContext(ds, i, t, sigmaBarPct);
|
|
339
451
|
if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
|
|
340
452
|
const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
453
|
+
const targetRub = calcDesiredRub(i, side, e, stopPct);
|
|
454
|
+
if (!(targetRub > 0)) continue;
|
|
455
|
+
prepared.push({ i, side, e, weight: pickWeight(i), targetRub, stopPct, ctx });
|
|
456
|
+
}
|
|
457
|
+
const allocs = (!legacySizing && policy.waterFill !== false)
|
|
458
|
+
? waterFillAllocations(prepared, avail)
|
|
459
|
+
: (() => {
|
|
460
|
+
const out = new Float64Array(prepared.length);
|
|
461
|
+
let a = avail;
|
|
462
|
+
let remW = prepared.reduce((s, x) => s + x.weight, 0);
|
|
463
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
464
|
+
const propShare = remW > 0 ? a * (prepared[idx].weight / remW) : a;
|
|
465
|
+
remW -= prepared[idx].weight;
|
|
466
|
+
const al = Math.min(prepared[idx].targetRub, propShare, a);
|
|
467
|
+
out[idx] = al;
|
|
468
|
+
a -= al;
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
})();
|
|
472
|
+
for (let idx = 0; idx < prepared.length; idx++) {
|
|
473
|
+
const { i, side, e, stopPct, ctx } = prepared[idx];
|
|
474
|
+
const alloc = Math.min(allocs[idx], avail);
|
|
346
475
|
if (!(alloc > 5000)) continue;
|
|
347
476
|
const tk = tokens[i];
|
|
348
477
|
let entryPx = fill === 'mid' ? (tk.open[t + 1] + tk.close[t + 1]) / 2 : tk.open[t + 1];
|