neuroalpha 0.6.10 → 0.6.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/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +9 -6
- 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 +27 -19
- package/package.json +1 -1
- package/public/app.js +14 -0
- package/public/index.html +4 -0
- package/server/brokers/index.js +180 -148
- package/server/model/edge.js +4 -3
- package/server/model/policy.js +190 -19
- package/server/model/research.js +5 -3
- package/server/model/simulator.js +151 -21
- package/server/strategist/index.js +24 -9
package/server/model/policy.js
CHANGED
|
@@ -19,11 +19,44 @@ export const STRATEGY_POLICY = Object.freeze({
|
|
|
19
19
|
profitLock: true,
|
|
20
20
|
// размер позиции: 'slot' — доля бюджета × сила сигнала
|
|
21
21
|
sizing: Object.freeze({ mode: 'slot' }),
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
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,
|
|
48
|
+
turnoverCapMult: 0.15,
|
|
49
|
+
tpRetainT2: 0.75,
|
|
50
|
+
tpTrailT2: 0.35,
|
|
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,
|
|
27
60
|
// капитал хранится в фонде денежного рынка LQDT как залог: внутридневные позиции открываются под обеспечение
|
|
28
61
|
// (без платы за перенос — позиции закрываются до ночи), LQDT не продаётся под входы
|
|
29
62
|
cashYield: true,
|
|
@@ -63,12 +96,15 @@ export function sessionEdgeWarning(ts = Date.now()) {
|
|
|
63
96
|
// ---------- фиксация прибыли ----------
|
|
64
97
|
// Уровни в процентах выигрыша от средней цены. peakGainPct — лучший выигрыш с момента входа.
|
|
65
98
|
// Возвращает { floorPct, minExitPct, kind } — выход, когда текущий выигрыш опустился до floorPct, но не ниже minExitPct; или null.
|
|
66
|
-
export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10) {
|
|
99
|
+
export function profitLockLevels(sym, peakGainPct, heldMin, roundTripFeePct = 0.10, policy = STRATEGY_POLICY) {
|
|
100
|
+
const isT1 = TIER1_LIQUID_SYMBOLS.has(sym);
|
|
67
101
|
const tierMult = symbolEdgeMult(sym);
|
|
68
102
|
const minNetFloorPct = Math.max(roundTripFeePct + 0.30, Math.round(0.38 * tierMult * 100) / 100);
|
|
69
103
|
const tpTriggerPct = (heldMin < 15 ? 1.55 : 1.20) * tierMult;
|
|
70
104
|
if (peakGainPct >= tpTriggerPct) {
|
|
71
|
-
|
|
105
|
+
const retain = !isT1 && policy?.tpRetainT2 ? policy.tpRetainT2 : 0.70;
|
|
106
|
+
const trail = !isT1 && policy?.tpTrailT2 ? policy.tpTrailT2 : 0.45;
|
|
107
|
+
return { floorPct: Math.max(peakGainPct * retain, peakGainPct - trail, minNetFloorPct), minExitPct: minNetFloorPct, kind: 'tp' };
|
|
72
108
|
}
|
|
73
109
|
if (heldMin >= 15 && peakGainPct >= 0.85 * tierMult) {
|
|
74
110
|
return { floorPct: Math.max(minNetFloorPct, peakGainPct * 0.45), minExitPct: roundTripFeePct + 0.22, kind: 'be' };
|
|
@@ -121,31 +157,166 @@ export function exitHoldRatio(heldMin, horizonMin, { sigmaBarPct = 0, realizedBa
|
|
|
121
157
|
return startRatio + (endRatio - startRatio) * ramp;
|
|
122
158
|
}
|
|
123
159
|
|
|
124
|
-
// ---------- плечо и
|
|
160
|
+
// ---------- профиль слотов по тарифу, плечо и предельный размер позиции ----------
|
|
125
161
|
export const MAX_LEVERAGE = 7;
|
|
126
|
-
|
|
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
|
+
// разгружает риск одиночной бумаги на высоких плечах и оставляет свободный маржинальный резерв под параллельные идеи.
|
|
222
|
+
export function tierSizeMult(symbol, leverage = 1, sideOrPolicy = 'long', policyArg = STRATEGY_POLICY) {
|
|
223
|
+
const isObj = sideOrPolicy && typeof sideOrPolicy === 'object';
|
|
224
|
+
const side = isObj ? 'long' : (sideOrPolicy === 'short' ? 'short' : 'long');
|
|
225
|
+
const policy = isObj ? sideOrPolicy : (policyArg || STRATEGY_POLICY);
|
|
127
226
|
const lev = Math.max(1, Math.min(MAX_LEVERAGE, Number(leverage) || 1));
|
|
128
|
-
|
|
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;
|
|
129
242
|
}
|
|
130
243
|
|
|
131
244
|
// ---------- предельный размер позиции на одну бумагу ----------
|
|
132
|
-
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0) {
|
|
245
|
+
export function symbolCapRub(symbol, side, budget, barTurnoverRub = 0, policy = STRATEGY_POLICY, profile = null) {
|
|
133
246
|
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
134
|
-
const
|
|
135
|
-
|
|
247
|
+
const isShort = side === 'short';
|
|
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
|
+
}
|
|
263
|
+
const rawCap = budget * capPct;
|
|
264
|
+
const turnMult = policy?.turnoverCapMult ?? 0.15;
|
|
265
|
+
return (!isT1 && barTurnoverRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnoverRub * turnMult)) : rawCap;
|
|
136
266
|
}
|
|
137
267
|
|
|
138
|
-
// Размер новой позиции в рублях до учёта свободного
|
|
139
|
-
// 'slot' — рабочий слот × сила сигнала (conv), не больше предела бумаги;
|
|
268
|
+
// Размер новой позиции в рублях до учёта свободного лимита и итеративного перелива (water-filling):
|
|
269
|
+
// 'slot' — рабочий слот × множитель эшелона × сила сигнала (conv), не больше предела бумаги;
|
|
140
270
|
// 'risk' — при срабатывании стопа счёт теряет не больше riskPct% бюджета.
|
|
141
|
-
export function targetPositionRub({ symbol, side, budget, slotRub, conv = 1, stopPct, barTurnoverRub = 0 }, policy = STRATEGY_POLICY) {
|
|
142
|
-
const cap = symbolCapRub(symbol, side, budget, barTurnoverRub);
|
|
143
|
-
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 || {};
|
|
144
274
|
if (sz.mode === 'risk' && stopPct > 0) {
|
|
145
275
|
const byRisk = (budget * (sz.riskPct / 100)) / (stopPct / 100);
|
|
146
276
|
return Math.min(cap, byRisk * (sz.useConv ? conv : 1), sz.maxSlotPct ? budget * sz.maxSlotPct : Infinity);
|
|
147
277
|
}
|
|
148
|
-
|
|
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;
|
|
149
320
|
}
|
|
150
321
|
|
|
151
322
|
// ---------- контекст торгового дня бумаги по свечам до момента решения ----------
|
package/server/model/research.js
CHANGED
|
@@ -40,7 +40,9 @@ export { isMarketDown };
|
|
|
40
40
|
// 16: главная оценка — симулятор с правилами живого контура (simulator.js), аварийный стоп 7%
|
|
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
|
+
// 20: разблокировка Conviction Sizing в T1 (до 92%/70% с сублинейным плечом 0,80), прямой риск-лимит T2, перелив water-filling и двухступенчатое потеснение
|
|
45
|
+
const DATA_VERSION = 20;
|
|
44
46
|
|
|
45
47
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
46
48
|
function reuseParamsKey(params) {
|
|
@@ -821,7 +823,7 @@ async function run(runId, p, state) {
|
|
|
821
823
|
const lqdtRows = await ensureLqdtHistory().catch(() => []);
|
|
822
824
|
const lqdt = lqdtRows.length ? alignSeries(timeline, lqdtRows) : null;
|
|
823
825
|
const liveRows = [];
|
|
824
|
-
for (const lev of [1, 3, 5]) {
|
|
826
|
+
for (const lev of [1, 2, 3, 5]) {
|
|
825
827
|
const lp = simulateStrategy({ ds, wf: wfLive, p, capital: p.capital, shortable: shortableSet(), divCal, lqdt, leverage: lev });
|
|
826
828
|
const yrs = Math.max(1 / 365, ((lp.curve.at(-1)?.ts || 0) - (lp.curve[0]?.ts || 0)) / (365.25 * 86400000));
|
|
827
829
|
const lastLpTs = lp.curve.at(-1)?.ts || 0;
|
|
@@ -836,7 +838,7 @@ async function run(runId, p, state) {
|
|
|
836
838
|
liveRows.push({
|
|
837
839
|
key: lev === 1 ? 'liveRules' : `liveRulesL${lev}`,
|
|
838
840
|
label: lev === 1 ? 'Правила живого контура, без плеча (главная оценка)' : `Правила живого контура, плечо ${lev}`,
|
|
839
|
-
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы,
|
|
841
|
+
note: `исполнение и сопровождение как у реального счёта: входы на любой свече, аварийный стоп и фиксация прибыли внутри свечи, окна сессий, планки, перегрев, паузы, итеративный перелив (water-filling), двухступенчатое потеснение и издержки по эшелонам${lqdt ? ', капитал в LQDT как залог' : ''}${lev > 1 ? `; сублинейный масштаб одиночной позиции при лимите портфеля ×${lev}` : ''}`,
|
|
840
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) },
|
|
841
843
|
costs: null,
|
|
842
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]);
|
|
@@ -189,7 +228,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
189
228
|
}
|
|
190
229
|
if (done) continue;
|
|
191
230
|
if (policy.profitLock) {
|
|
192
|
-
const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct);
|
|
231
|
+
const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart, roundTripFeePct, policy);
|
|
193
232
|
if (lv) {
|
|
194
233
|
const gO = gainOf(pos, o) * 100;
|
|
195
234
|
if (gO <= lv.floorPct && gO >= lv.minExitPct) { closePos(i, pos, t, o, 'Фиксация прибыли', 'lock'); stats.locks++; done = true; }
|
|
@@ -199,7 +238,7 @@ export function simulateStrategy({ ds, wf, p, policy = STRATEGY_POLICY, capital
|
|
|
199
238
|
if (done) continue;
|
|
200
239
|
pos.peakGain = Math.max(pos.peakGain, gainOf(pos, favorable));
|
|
201
240
|
if (policy.profitLock) {
|
|
202
|
-
const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct);
|
|
241
|
+
const lv = profitLockLevels(tk.symbol, pos.peakGain * 100, heldMinStart + BAR_INTERVAL, roundTripFeePct, policy);
|
|
203
242
|
const gC = gainOf(pos, c) * 100;
|
|
204
243
|
if (lv && gC <= lv.floorPct && gC >= lv.minExitPct) { closePos(i, pos, t, c, 'Фиксация прибыли', 'lock'); stats.locks++; }
|
|
205
244
|
}
|
|
@@ -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,27 +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
|
-
picks.sort((a, b) =>
|
|
329
|
-
|
|
330
|
-
|
|
444
|
+
picks.sort((a, b) => pickWeight(b[0]) - pickWeight(a[0]));
|
|
445
|
+
avail = Math.max(0, budget * maxGross - grossUsed(t));
|
|
446
|
+
const prepared = [];
|
|
331
447
|
for (const [i, side] of picks) {
|
|
332
448
|
const e = edge(i);
|
|
333
|
-
const w = Math.max(0.0001, Math.abs(e));
|
|
334
|
-
const propShare = remW > 0 ? avail * (w / remW) : avail;
|
|
335
|
-
remW -= w;
|
|
336
449
|
const sigmaBarPct = sg[i] * 100;
|
|
337
450
|
const ctx = dayContext(ds, i, t, sigmaBarPct);
|
|
338
451
|
if (counterTrendBlocked(side, ctx, policy)) { stats.blockedCT++; continue; }
|
|
339
452
|
const stopPct = protectiveStopPct(side, { sigmaBarPct, realizedBarPct: ctx.realizedBarPct, horizonBars: holdBars }, policy);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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);
|
|
345
475
|
if (!(alloc > 5000)) continue;
|
|
346
476
|
const tk = tokens[i];
|
|
347
477
|
let entryPx = fill === 'mid' ? (tk.open[t + 1] + tk.close[t + 1]) / 2 : tk.open[t + 1];
|
|
@@ -249,16 +249,31 @@ ${liveTradingSummary}
|
|
|
249
249
|
- Если данные инструментов расходятся с календарём или временем — **верь данным инструментов**: это фактические позиции и исполненные сделки.
|
|
250
250
|
|
|
251
251
|
## Как устроено автоматическое ядро платформы «НейроАльфа» (Рынок как язык)
|
|
252
|
-
- **Словарь:** ~
|
|
252
|
+
- **Словарь:** ~125 акций широкого рынка Мосбиржи (MOEXBMI) + индекс IMOEX. Свечи 10-минутные, дивидендные отсечки скорректированы со сверкой по реальному гэпу цены, объёмы нормированы по 24-часовому суточному профилю (valRatio по часам МСК).
|
|
253
253
|
- **Карта рынка и авто-эмбеддинг (modelEmbedDim = 0 Авто):** окно обучения фиксировано на 90 торговых днях. На каждом шаге размерность карты рынка автоматически определяется по спектральному порогу Марченко–Пастура (отсечение рыночного шума от истинных факторов).
|
|
254
|
-
- **12 голов внимания и мульти-горизонт (1,5–3,5 часа):** модель смотрит на 3 масштаба времени (1ч, 3ч, день), цену и денежный поток, узких и широких соседей по карте, предсказывая чистое остаточное движение каждой акции относительно индекса
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
-
|
|
261
|
-
-
|
|
254
|
+
- **12 голов внимания и мульти-горизонт обучения (2,5 часа = 15 свечей, диапазон 1,5–3,5 часа):** модель смотрит на 3 масштаба времени (1ч, 3ч, день), цену и денежный поток, узких и широких соседей по карте, предсказывая чистое остаточное движение каждой акции относительно индекса Мосбиржи (Edge_i = y_i × sigma_i × √15).
|
|
255
|
+
- **Эшелоны ликвидности и реальные издержки («на сторону» и «за полный круг»):**
|
|
256
|
+
- «Издержки на сторону» — это потери на одной операции (отдельно на входе и отдельно на выходе); за полный круг сделки («купил → продал») они удваиваются. Комиссия брокера (Т-Банк) одинаковая для всех бумаг (0,04% на сторону = 0,08% за круг), а потери в биржевом стакане (спред + проскальзывание) зависят от эшелона:
|
|
257
|
+
- **1-й эшелон (33 ликвидные акции с глубоким стаканом:** SBER, SBERP, LKOH, GAZP, T, YDEX, ROSN, NVTK, GMKN, PLZL, VTBR, TATN, TATNP, SNGS, SNGSP, ALRS, CHMF, NLMK, MAGN, MOEX, OZON, PHOR, AFLT, MTSS, IRAO, TRNFP, RUAL, FLOT, X5, MTLR, BSPB, RTKM, SVCB): потери в стакане ~0,025% + комиссия 0,04% = **0,065% на сторону (0,13% за круг)**.
|
|
258
|
+
- **2–3-й эшелон (остальные ~92 акции,** включая высоковолатильные бумаги с более широким спредом и хвостовым риском: SMLT, PIKK, POSI, AFKS, VKCO, SPBE, UGLD, HEAD, CBOM, LSRG, SFIN, FESH и др.): потери в стакане ~0,085% + комиссия 0,04% = **0,125% на сторону (0,25% за круг)**.
|
|
259
|
+
- **Пороги входа по эшелонам и тренду индекса:**
|
|
260
|
+
- Базовый порог выгоды на вход = 2,5 × 2 × (комиссия + проскальзывание) × 1,60. Для 1-го эшелона при настройке издержек 0,05% базовый порог составляет **+0,40%** (с тонкой поправкой symbolEdgeMult на оборот и волатильность бумаги в коридоре 0,88..1,45).
|
|
261
|
+
- Для 2–3-го эшелона порог входа повышен в **1,20× (TIER2_EDGE_MULT)** — до **~+0,48%**, чтобы ожидаемый импульс с запасом перекрывал более широкий спред стакана. В первые 20 минут утренней сессии (07:00–07:20 МСК) шорты 2-го эшелона блокируются из-за пустого утреннего стакана.
|
|
262
|
+
- Авто-балансировка по тренду индекса (isMarketDown): на растущем рынке покупки открываются от базового порога (+0,40% 1-й эшелон и +0,48% 2-й эшелон), а контртрендовые шорты ужесточаются в 1,25× (до −0,50% и −0,60%). На падающем рынке — наоборот: покупки ужесточаются в 1,25×, а шорты облегчаются в 0,90×. Всегда называй пользователю точный индивидуальный порог конкретной бумаги (\`порог_покупки_проц\` / \`порог_шорта_проц\`).
|
|
263
|
+
- **Размер позиций, сублинейное плечо, перелив капитала и динамическое потеснение (policy.js):**
|
|
264
|
+
- **Живой Conviction Sizing без холостого резерва:** робот держит до 4 лонгов и до 4 шортов, но поскольку 81,5% активного времени на рынке есть ровно 1 сигнал, стратегия не замораживает половину лимита «на всякий случай». В режиме владельца базовый слот 1-го эшелона составляет 56% бюджета и масштабируется на силу сигнала conv (от 0,85× до 1,92×), разгоняя сильные одиночные импульсы до **92% бюджета в лонг и до 70% в шорт** на счёте 1× (на VIP — до 75% / 56%, на Профи — до 52% / 40%, на Инвесторе — до 58% в лонг).
|
|
265
|
+
- **Сублинейное масштабирование одиночной позиции по плечу (tierSizeMult):**
|
|
266
|
+
1. В **1-м эшелоне** потолок одной бумаги растёт с плечом счёта сублинейно по формуле 1 + 0,80 × (плечо − 1) (на 1× → 1,00×, на 2× → 1,80×, на 3× → 2,60× при общем лимите портфеля 2×..3× капитала). Это удерживает риск одной бумаги под контролем, а разницу между лимитом бумаги и общим плечом счёта превращает в бесплатный маржинальный резерв под 2-ю и 3-ю параллельные сделки под залог LQDT.
|
|
267
|
+
2. Во **2-м эшелоне** действует прямой риск-потолок без двойного штрафа: до **17% бюджета в лонг** (масштаб плеча 1 + 0,72 × (плечо − 1)) и до **7% в шорт** (1 + 0,40 × (плечо − 1)), под жёстким отсечением по реальной ликвидности биржи — **не более 15% от медианного 10-минутного оборота свечи (barTurnoverRub)** (минимум 45 000 ₽).
|
|
268
|
+
- **Итеративный перелив капитала (waterFillAllocations) и чистая отдача (netPickSort):** очерёдность и веса между отобранными сделками определяются по чистой ожидаемой отдаче за вычетом круговых издержек (|expectedPct| − 0,13% для 1-го эшелона и |expectedPct| − 0,25% для 2-го). При одновременном входе бумаги с узким лимитом (2-й эшелон / тонкий стакан) забирают ровно свой потолок, а весь невыбранный остаток переливается флагманским лидерам 1-го эшелона.
|
|
269
|
+
- **Двухступенчатое динамическое потеснение (crowdOut):** если свободного лимита не хватает под новый сильный сигнал 1-го эшелона (не моложе 20 минут жизни уже открытых сделок), робот освобождает место по факту: (1) полностью ротирует остывшую позицию либо (2) частично подрезает позицию 2-го эшелона под флагман 1-го эшелона с сохранением времени входа entryTs и накопленного пика peakPrice для трала Profit Lock (на счетах auto работает и на быстрых 20-секундных входах, на confirm — на плановых 30-минутных шагах).
|
|
270
|
+
- **Капитал в фонде денежного рынка LQDT как залог:** свободный капитал работает в LQDT, а внутридневные позиции открываются под обеспечение и закрываются до ночи (без платы брокеру за перенос через ночь).
|
|
271
|
+
- **Глубина сопровождения позиций (130 минут), фиксация прибыли и защитные контуры:**
|
|
272
|
+
- **Глубина сопровождения (holdBars = 13 свечей = 130 минут = 2 ч 10 м):** плановая ревизия портфеля идёт каждые 20–30 минут (holdBars / 5), а вечернее окно новых внутридневных входов закрывается за 130 минут до конца торгов (после **21:30 МСК** новые входы не открываются, перед 23:40–23:50 МСК позиции закрываются перед ночью).
|
|
273
|
+
- **Плавная рампа удержания (exitHoldRatio):** первые **45 минут** после входа действует мягкий порог удержания (0,35× от порога входа), чтобы сделку не выбило шумом; затем с 45-й по **130-ю минуту** порог плавно поднимается к 0,95×, динамически подстраиваясь под сегодняшнюю волатильность бумаги (realizedBarPct / sigmaBarPct в коридоре [0,75..1,25]: в спокойный день держит дольше, в турбулентный фиксирует быстрее).
|
|
274
|
+
- **Скользящая фиксация прибыли (profitLockLevels):** включает безубыток от +0,85% × tierMult (после 15 мин) и скользящий Take-Profit от +1,20% × tierMult (+1,55% × tierMult в первые 15 мин). В 1-м эшелоне трал удерживает ≥70% пиковой прибыли (допуская откат до 0,45%), а во 2-м эшелоне работает **уплотнённый трал — удерживает ≥75% пика и допускает откат не более 0,35%**, забирая импульс до разворота спреда.
|
|
275
|
+
- **Аварийный стоп — 7% от средней цены входа** (выставляется реальной стоп-заявкой у брокера и контролируется внутри свечи).
|
|
276
|
+
- **Карантин убыточных бумаг (isStockQuarantined):** FIFO-учёт кругов сделок с периодом полураспада ~15 торговых дней (0,9985 на 30-минутном шаге); если бумага накопила серию убытков (≥2 сделок с суммарным итогом < −1,2% или < −2,5%), слабые сигналы по ней пропускаются, но **сильный сигнал (≥ 110% от индивидуального порога входа) пробивает карантин**.
|
|
262
277
|
|
|
263
278
|
## Брокерские счета и режимы работы
|
|
264
279
|
Полная поддержка — Т-Банк (T-Invest API, включая секцию TQBR и SPBRU для SPBE). Финам, БКС, Алор, Цифра брокер и ITI Capital — бета, только «Подтверждение заявок». СберИнвестиции, ВТБ и Альфа-Инвестиции сейчас не подключаются (нет открытого API для заявок).
|