neuroalpha 0.5.21 → 0.5.24
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 +7 -1
- package/server/api/routes.js +28 -5
- package/server/brokers/index.js +137 -31
- package/server/data/moex.js +59 -28
- package/server/db.js +4 -0
- package/server/model/dataset.js +32 -12
- package/server/model/edge.js +39 -7
- package/server/model/forecast.js +83 -19
- package/server/model/live.js +29 -7
- package/server/model/models.js +27 -5
- package/server/model/research.js +114 -28
- package/server/strategist/tools.js +1 -0
package/server/model/research.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ensureProfiles } from './profiles.js';
|
|
|
9
9
|
import { spearman } from './linalg.js';
|
|
10
10
|
import { listRuns, getRun, createRun, finishRun, saveTrade, saveRunExtra, kvGet } from '../db.js';
|
|
11
11
|
import { strategyDefaults } from '../config.js';
|
|
12
|
-
import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight } from './edge.js';
|
|
12
|
+
import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight, isRegimeShocked } from './edge.js';
|
|
13
13
|
import { buildDividendCalendar, nextDividendAt, dividendRules, DIV_TAX } from './dividends.js';
|
|
14
14
|
|
|
15
15
|
const DAY = 86400000;
|
|
@@ -40,7 +40,9 @@ export function isMarketDown(ds, t) {
|
|
|
40
40
|
// 9: исторический состав индекса (без ошибки выжившего), дивиденды префов, правила v0.5.x
|
|
41
41
|
// 10: запрет переноса внутридневных сделок через ночь, период полураспада карантина 15 дн., строгий 1-й эшелон
|
|
42
42
|
// 11: индивидуальный расчёт порога каждой акции по её фактическому рублёвому обороту и остаточной волатильности
|
|
43
|
-
|
|
43
|
+
// 12: калибровка индивидуальных порогов 2–3 эшелона по ликвидности, защита от шорт-сквизов (≥20% за день) и обвалов (≥9,5% за 30м), отбор доступных шортов до Top-K
|
|
44
|
+
// 13: полная докачка левого края истории свечей (раньше при уже накопленных свечах 2022 год не догружался)
|
|
45
|
+
const DATA_VERSION = 13;
|
|
44
46
|
|
|
45
47
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
46
48
|
function reuseParamsKey(params) {
|
|
@@ -461,8 +463,8 @@ async function run(runId, p, state) {
|
|
|
461
463
|
}
|
|
462
464
|
const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
|
|
463
465
|
const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
|
|
464
|
-
const okLong = (i) => !divRule(i, t)?.blockLong;
|
|
465
|
-
const okShort = (i) => !divRule(i, t)?.blockShort;
|
|
466
|
+
const okLong = (i) => !divRule(i, t)?.blockLong && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
467
|
+
const okShort = (i) => !divRule(i, t)?.blockShort && (!shortable || shortable.has(tokens[i].symbol)) && (!needTrade || (tokens[i].has[tIn] && tokens[i].open[tIn] > 0)) && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
|
|
466
468
|
let order, entries;
|
|
467
469
|
if (dir !== 'long') {
|
|
468
470
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
@@ -474,12 +476,15 @@ async function run(runId, p, state) {
|
|
|
474
476
|
const e = edge(i);
|
|
475
477
|
const barsHeld = tIn - h.entryT;
|
|
476
478
|
const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
|
|
477
|
-
|
|
479
|
+
const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
|
|
480
|
+
if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) {
|
|
481
|
+
closePos(i, h, tIn, false, shocked ? 'Защита от экстремального сквиза/обвала' : 'Прогноз ослаб');
|
|
482
|
+
}
|
|
478
483
|
}
|
|
479
484
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
480
485
|
const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
|
|
481
|
-
const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) =>
|
|
482
|
-
const candS = !timeOk ? [] : [...pr.keys()].filter((i) =>
|
|
486
|
+
const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
|
|
487
|
+
const candS = !timeOk ? [] : [...pr.keys()].filter((i) => edge(i) < -needS * symMult[i] && canOpenAt(i) && okShort(i)).sort((a, b) => pr[a] - pr[b]);
|
|
483
488
|
// Вытеснение слабой/остывшей позиции новым мощным лидером (Opportunity Rotation):
|
|
484
489
|
// если капитал заперт в позиции с ослабевшим ожиданием (< 0.55 * exitNeed), а на текущей свече появился лидер в 2.4 раза сильнее порога,
|
|
485
490
|
// освобождаем слот под лидера, чтобы ни одна фаза не застревала в слабых бумагах
|
|
@@ -516,11 +521,27 @@ async function run(runId, p, state) {
|
|
|
516
521
|
const e = edge(i);
|
|
517
522
|
const barsHeld = tIn - h.entryT;
|
|
518
523
|
const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
|
|
519
|
-
|
|
524
|
+
const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
|
|
525
|
+
if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn, false, shocked ? 'Защита от экстремального обвала' : '');
|
|
520
526
|
}
|
|
521
|
-
const free = K - held.size;
|
|
522
527
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
523
|
-
const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) =>
|
|
528
|
+
const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
|
|
529
|
+
const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
530
|
+
if (autoAdapt && usedNow > equity * 0.65 && rawCand.length > 0) {
|
|
531
|
+
const bestNewEdge = edge(rawCand[0]);
|
|
532
|
+
if (bestNewEdge >= need * 2.2) {
|
|
533
|
+
let weakestIdx = -1, weakestE = Infinity;
|
|
534
|
+
for (const [i] of held) {
|
|
535
|
+
const curSignedE = edge(i);
|
|
536
|
+
if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
|
|
537
|
+
weakestE = curSignedE;
|
|
538
|
+
weakestIdx = i;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn, false, 'Ротация в более сильного лидера');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const free = K - held.size;
|
|
524
545
|
const cand = filterDynamicTopK(rawCand, edge, needL, Math.max(0, free));
|
|
525
546
|
order = [...held.keys(), ...cand];
|
|
526
547
|
} else {
|
|
@@ -912,7 +933,7 @@ function runStressTest({ simulate, chosen, testTs, tsOf, mcurve, p, H }) {
|
|
|
912
933
|
return round(((lo + hi) / 2) * 100, 3);
|
|
913
934
|
};
|
|
914
935
|
// Подбор порога входа под реальные издержки: живое исполнение, издержки 0,09% на сторону (комиссия 0,04% + проскальзывание ~0,05%)
|
|
915
|
-
const thresholdSweep = [1, 1.5, 1.65, 2, 2.5, 3].map((m) => {
|
|
936
|
+
const thresholdSweep = [1, 1.5, 1.6, 1.65, 2, 2.5, 3].map((m) => {
|
|
916
937
|
const sim = simulate(chosen, 0.0009, p.modelShortFeePct, 0, { ...LIVE, needMult: m });
|
|
917
938
|
const f = computeReport(sim.trades, sim.curve, p.capital, 0).full;
|
|
918
939
|
return { needMult: m, current: m === ENTRY_EDGE_MULT, entryEdgePct: round(2.5 * 2 * base * m * 100, 3), returnPct: f.returnPct, cagrPct: cagr(f.returnPct, yearsOf(sim.curve)), profitFactor: f.profitFactor, maxDrawdownPct: f.maxDrawdownPct, trades: f.trades, avgTradePct: f.avgTradePct, last12Pct: segment(sim, lastTs - 365 * 86400000).returnPct, last6Pct: segment(sim, lastTs - 182 * 86400000).returnPct };
|
|
@@ -924,7 +945,7 @@ function runStressTest({ simulate, chosen, testTs, tsOf, mcurve, p, H }) {
|
|
|
924
945
|
market: { full: mSeg(-Infinity), last12: mSeg(lastTs - 365 * 86400000), last6: mSeg(lastTs - 182 * 86400000) },
|
|
925
946
|
shortableKnown: Boolean(shortable),
|
|
926
947
|
caveats: [
|
|
927
|
-
'Состав рынка —
|
|
948
|
+
'Состав рынка — исторический состав индекса MOEXBMI на каждую дату плюс текущий состав; отсекаются только бумаги с покрытием данных менее 15% периода.',
|
|
928
949
|
'Константы стратегии подбирались на этой же истории, поэтому «последние 6 и 12 месяцев» не являются честно отложенной выборкой. Честная проверка — торговля после фиксации версии.',
|
|
929
950
|
'Список бумаг для шорта — текущий, а не на каждую дату.',
|
|
930
951
|
],
|
|
@@ -974,7 +995,9 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
974
995
|
await pause();
|
|
975
996
|
const p = normalizeParams(runRow.params || {});
|
|
976
997
|
const shifts = [-8, -4, 0, 4, 8];
|
|
977
|
-
const
|
|
998
|
+
const autoAdapt = p.modelAutoAdapt !== false;
|
|
999
|
+
const effHorizon = autoAdapt ? 2.5 : p.modelHorizon;
|
|
1000
|
+
const H = barsFromHours(effHorizon);
|
|
978
1001
|
const tillTs = Date.now();
|
|
979
1002
|
const todayStartMsk = Math.floor((tillTs + MSK) / DAY) * DAY - MSK;
|
|
980
1003
|
const fromTs = todayStartMsk - 1650 * DAY;
|
|
@@ -991,11 +1014,18 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
991
1014
|
const trainLen = Math.round(p.modelTrainDays * bpd), testLen = Math.round(20 * bpd);
|
|
992
1015
|
const chosen = p.modelType in MODELS ? p.modelType : 'attention';
|
|
993
1016
|
const realCost = (p.commissionPct + p.slippagePct) / 100;
|
|
994
|
-
const
|
|
995
|
-
const
|
|
1017
|
+
const rawTopK = Number(p.modelTopK ?? 2);
|
|
1018
|
+
const K = rawTopK > 0 ? rawTopK : 4;
|
|
1019
|
+
const dir = p.modelDirection, slots = dir === 'long' ? K : 2 * K;
|
|
996
1020
|
const effMinEdge = autoAdapt ? 2.5 : p.modelMinEdge;
|
|
997
1021
|
const exitRatio = Number(p.modelExitRatio ?? 0.5);
|
|
998
1022
|
const mktFilter = p.modelMarketFilter === 'ma', invVol = p.modelAllocMode === 'invvol';
|
|
1023
|
+
const baseSymMult = Float64Array.from(tokens, (tk) => symbolEdgeMult(tk.symbol));
|
|
1024
|
+
const divCal = buildDividendCalendar(ds);
|
|
1025
|
+
const divRule = (i, t) => {
|
|
1026
|
+
const nd = nextDividendAt(divCal, i, t);
|
|
1027
|
+
return nd ? dividendRules(nd.daysToEx, nd.y) : null;
|
|
1028
|
+
};
|
|
999
1029
|
const liquid = (i, t) => tokens[i].liq[t] >= LIQ_MIN && tokens[i].open[t + 1] > 0;
|
|
1000
1030
|
const canEnter = (i, t) => liquid(i, t) && (!tokens[i].inIndex || tokens[i].inIndex[t] === 1);
|
|
1001
1031
|
const tsOf = (t) => timeline[Math.min(t, T - 1)];
|
|
@@ -1019,7 +1049,7 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1019
1049
|
const maxH = autoAdapt ? Math.round(H * 1.4) : H;
|
|
1020
1050
|
const folds = Math.max(1, Math.ceil((T - maxH - testStart) / testLen));
|
|
1021
1051
|
let fi = 0;
|
|
1022
|
-
const preds = new Map(), sigmaAt = new Map();
|
|
1052
|
+
const preds = new Map(), sigmaAt = new Map(), multAt = new Map();
|
|
1023
1053
|
while (testStart < T - maxH - 2) {
|
|
1024
1054
|
if (st.cancelled) throw new Error('Отменено');
|
|
1025
1055
|
const testEnd = Math.min(testStart + testLen, T - maxH - 2);
|
|
@@ -1028,12 +1058,14 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1028
1058
|
emitStab({ pct, text: `Проверка устойчивости: сдвиг ${si + 1} из ${shifts.length} (${shLabel}), шаг ${fi + 1} из ${folds}…` });
|
|
1029
1059
|
await tick();
|
|
1030
1060
|
const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
|
|
1061
|
+
const foldEdgeMults = computeTokenEdgeMults(tokens, f.sigma, trainStart, testStart);
|
|
1031
1062
|
const m = await MODELS[chosen].fit(f);
|
|
1032
1063
|
for (let t = testStart; t < testEnd; t++) {
|
|
1033
1064
|
const u = t - f.lo;
|
|
1034
1065
|
if (!f.yok[u]) continue;
|
|
1035
1066
|
preds.set(t, MODELS[chosen].predict(m, f, u));
|
|
1036
1067
|
sigmaAt.set(t, f.sigma);
|
|
1068
|
+
multAt.set(t, foldEdgeMults);
|
|
1037
1069
|
}
|
|
1038
1070
|
testStart = testEnd;
|
|
1039
1071
|
fi++;
|
|
@@ -1051,7 +1083,7 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1051
1083
|
const stockAllowed = (i) => !autoAdapt || !isStockQuarantined(stockCnt[i], stockPnl[i]);
|
|
1052
1084
|
const curExitRatio = autoAdapt ? 0.95 : exitRatio;
|
|
1053
1085
|
const stepBars = autoAdapt ? Math.max(3, Math.floor(H / 5)) : H;
|
|
1054
|
-
const maxSlotWeight = dir === 'long' ? Math.min(0.
|
|
1086
|
+
const maxSlotWeight = dir === 'long' ? Math.min(0.58, Math.max(1 / K, 1.8 / K)) : Math.min(0.75, 3.0 / (2 * K));
|
|
1055
1087
|
let closedThisBar = new Set();
|
|
1056
1088
|
const closePos = (i, h, t) => {
|
|
1057
1089
|
const exitPrice = px(i, t);
|
|
@@ -1075,9 +1107,14 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1075
1107
|
for (let i = 0; i < N; i++) { stockPnl[i] *= QUARANTINE_DECAY; stockCnt[i] *= QUARANTINE_DECAY; }
|
|
1076
1108
|
}
|
|
1077
1109
|
const pr = preds.get(t), sg = sigmaAt.get(t);
|
|
1110
|
+
const symMult = multAt.get(t) || baseSymMult;
|
|
1078
1111
|
const bear = isMarketDown(ds, t);
|
|
1079
1112
|
const mktDown = mktFilter && bear;
|
|
1080
1113
|
const barTs = tsOf(t);
|
|
1114
|
+
for (const [i, h] of [...held]) {
|
|
1115
|
+
const r = divRule(i, t);
|
|
1116
|
+
if (r && (h.side === 'long' ? r.exitLong : r.exitShort)) closePos(i, h, tIn);
|
|
1117
|
+
}
|
|
1081
1118
|
if (autoAdapt) {
|
|
1082
1119
|
for (const [i, h] of [...held]) {
|
|
1083
1120
|
if (shouldExitBeforeNight(barTs, tsOf(h.entryT), stepBars, BAR_INTERVAL)) closePos(i, h, tIn);
|
|
@@ -1085,29 +1122,64 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1085
1122
|
}
|
|
1086
1123
|
const timeOk = !autoAdapt || canEnterIntraday(barTs, H, BAR_INTERVAL);
|
|
1087
1124
|
const canOpenAt = (i) => timeOk && !held.has(i) && !closedThisBar.has(i) && stockAllowed(i) && canEnter(i, t);
|
|
1125
|
+
const okLong = (i) => !divRule(i, t)?.blockLong && (!autoAdapt || !isRegimeShocked(ds, i, t, 'long'));
|
|
1126
|
+
const okShort = (i) => !divRule(i, t)?.blockShort && (!autoAdapt || !isRegimeShocked(ds, i, t, 'short'));
|
|
1088
1127
|
const edge = (i) => pr[i] * sg[i] * Math.sqrt(H);
|
|
1089
1128
|
const need = effMinEdge * 2 * realCost * ENTRY_EDGE_MULT, exitNeed = need * curExitRatio, earlyExitNeed = need * 0.35;
|
|
1090
1129
|
let order, entries;
|
|
1091
1130
|
if (dir !== 'long') {
|
|
1092
1131
|
for (const [i, h] of [...held]) {
|
|
1093
1132
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
1094
|
-
const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) *
|
|
1095
|
-
|
|
1133
|
+
const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
|
|
1134
|
+
const shocked = autoAdapt && isRegimeShocked(ds, i, t, h.side);
|
|
1135
|
+
if (shocked || !(h.side === 'long' ? e > req : e < -req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
1096
1136
|
}
|
|
1097
1137
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
1098
1138
|
const needS = autoAdapt ? (bear ? need * 0.90 : need * 1.25) : need;
|
|
1099
|
-
const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) =>
|
|
1100
|
-
const candS = !timeOk ? [] : [...pr.keys()].filter((i) =>
|
|
1139
|
+
const candL = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
|
|
1140
|
+
const candS = !timeOk ? [] : [...pr.keys()].filter((i) => edge(i) < -needS * symMult[i] && canOpenAt(i) && okShort(i)).sort((a, b) => pr[a] - pr[b]);
|
|
1141
|
+
const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
1142
|
+
if (autoAdapt && usedNow > equity * 0.65 && (candL.length > 0 || candS.length > 0)) {
|
|
1143
|
+
const bestNewEdge = Math.max(candL.length ? edge(candL[0]) : 0, candS.length ? Math.abs(edge(candS[0])) : 0);
|
|
1144
|
+
if (bestNewEdge >= need * 2.2) {
|
|
1145
|
+
let weakestIdx = -1, weakestE = Infinity;
|
|
1146
|
+
for (const [i, h] of held) {
|
|
1147
|
+
const curSignedE = h.side === 'long' ? edge(i) : -edge(i);
|
|
1148
|
+
if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
|
|
1149
|
+
weakestE = curSignedE;
|
|
1150
|
+
weakestIdx = i;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1101
1156
|
const nL = K - countSide('long'), nS = K - countSide('short');
|
|
1102
1157
|
entries = [...candL.slice(0, Math.max(0, nL)).map((i) => [i, 'long', edge(i)]), ...candS.slice(0, Math.max(0, nS)).map((i) => [i, 'short', edge(i)])];
|
|
1103
1158
|
} else if (effMinEdge > 0) {
|
|
1104
1159
|
for (const [i, h] of [...held]) {
|
|
1105
1160
|
const e = edge(i), barsHeld = tIn - h.entryT;
|
|
1106
|
-
const req = autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed;
|
|
1107
|
-
|
|
1161
|
+
const req = (autoAdapt && barsHeld <= 2 * stepBars ? earlyExitNeed : exitNeed) * symMult[i];
|
|
1162
|
+
const shocked = autoAdapt && isRegimeShocked(ds, i, t, 'long');
|
|
1163
|
+
if (shocked || !(e > req) || !liquid(i, t)) closePos(i, h, tIn);
|
|
1108
1164
|
}
|
|
1109
1165
|
const needL = autoAdapt && bear ? need * 1.25 : need;
|
|
1110
|
-
const
|
|
1166
|
+
const rawCand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => edge(i) > needL * symMult[i] && canOpenAt(i) && okLong(i)).sort((a, b) => pr[b] - pr[a]);
|
|
1167
|
+
const usedNow = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
1168
|
+
if (autoAdapt && usedNow > equity * 0.65 && rawCand.length > 0) {
|
|
1169
|
+
const bestNewEdge = edge(rawCand[0]);
|
|
1170
|
+
if (bestNewEdge >= need * 2.2) {
|
|
1171
|
+
let weakestIdx = -1, weakestE = Infinity;
|
|
1172
|
+
for (const [i] of held) {
|
|
1173
|
+
const curSignedE = edge(i);
|
|
1174
|
+
if (curSignedE < exitNeed * 0.60 && curSignedE < weakestE && bestNewEdge > Math.max(need, curSignedE) * 2.4) {
|
|
1175
|
+
weakestE = curSignedE;
|
|
1176
|
+
weakestIdx = i;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (weakestIdx >= 0) closePos(weakestIdx, held.get(weakestIdx), tIn);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const cand = rawCand.slice(0, Math.max(0, K - held.size));
|
|
1111
1183
|
order = [...held.keys(), ...cand];
|
|
1112
1184
|
} else {
|
|
1113
1185
|
const cand = (mktDown || !timeOk) ? [] : [...pr.keys()].filter((i) => !closedThisBar.has(i) && stockAllowed(i) && pr[i] > 0 && canEnter(i, t)).sort((a, b) => pr[b] - pr[a]).slice(0, K);
|
|
@@ -1125,11 +1197,25 @@ async function runStabilityLoop(targetId, runRow, st) {
|
|
|
1125
1197
|
}
|
|
1126
1198
|
const usedStart = [...held.values()].reduce((s, h) => s + h.alloc, 0);
|
|
1127
1199
|
let avail = Math.max(0, equity * 0.99 - usedStart);
|
|
1200
|
+
const activeTotal = Math.max(2, held.size + newEntries.length);
|
|
1201
|
+
const dynSlotWeight = autoAdapt
|
|
1202
|
+
? Math.min(dir === 'long' ? 0.58 : 0.75, Math.max(maxSlotWeight, (dir === 'long' ? 1.8 : 3.0) / Math.min(slots, activeTotal + 1)))
|
|
1203
|
+
: maxSlotWeight;
|
|
1204
|
+
if (autoAdapt && newEntries.length > 1) {
|
|
1205
|
+
newEntries.sort((a, b) => Math.abs(b[2] ?? 0) - Math.abs(a[2] ?? 0));
|
|
1206
|
+
}
|
|
1207
|
+
const edgeWeights = newEntries.map(([i, , edgeAt]) => {
|
|
1208
|
+
const effE = edgeAt !== null && edgeAt !== undefined ? Math.abs(edgeAt) : (sg ? Math.abs(pr[i] * sg[i] * Math.sqrt(H)) : 0);
|
|
1209
|
+
return Math.max(0.0001, effE);
|
|
1210
|
+
});
|
|
1211
|
+
let remWeightSum = edgeWeights.reduce((a, b) => a + b, 0);
|
|
1128
1212
|
for (let idx = 0; idx < newEntries.length; idx++) {
|
|
1129
|
-
const [i, side
|
|
1130
|
-
const effEdge =
|
|
1131
|
-
const conv = autoAdapt && need > 0 && effEdge > 0 ? Math.min(1.
|
|
1132
|
-
const
|
|
1213
|
+
const [i, side] = newEntries[idx];
|
|
1214
|
+
const effEdge = edgeWeights[idx];
|
|
1215
|
+
const conv = autoAdapt && need > 0 && effEdge > 0 ? Math.min(1.65, Math.max(0.85, 0.80 + 0.30 * (effEdge / need))) : 1;
|
|
1216
|
+
const propShare = autoAdapt && remWeightSum > 0 ? avail * (effEdge / remWeightSum) : avail / Math.max(1, newEntries.length - idx);
|
|
1217
|
+
remWeightSum = Math.max(0, remWeightSum - effEdge);
|
|
1218
|
+
const baseAlloc = autoAdapt ? Math.min(equity * dynSlotWeight * conv, propShare, avail) : Math.min(equity / slots, avail / Math.max(1, newEntries.length - idx));
|
|
1133
1219
|
let alloc = baseAlloc;
|
|
1134
1220
|
if (invVol && avgInv > 0 && sg[i] > 0) {
|
|
1135
1221
|
const w = Math.max(0.4, Math.min(1.8, (1 / sg[i]) / avgInv));
|
|
@@ -283,6 +283,7 @@ export const TOOLS = {
|
|
|
283
283
|
динамика_проц: x.deltaPct || 0,
|
|
284
284
|
сигнал_покупки: !!x.signal,
|
|
285
285
|
...(buyBlockedByDip ? { покупка_заблокирована: `сейчас бумага падает на текущей свече (${x.lastMovePct}%), сработала защита от ловли падающего ножа` } : {}),
|
|
286
|
+
...(x.buyBlockedByOverheat ? { покупка_заблокирована_перегревом: `бумага уже перегрета (+${x.buyBlockedByOverheat}% сверх рынка за 1–3 дня), сработала защита от покупки на вершине пампа` } : {}),
|
|
286
287
|
...(x.buyBlockedByDiv ? { покупка_заблокирована_дивидендами: x.buyBlockedByDiv } : {}),
|
|
287
288
|
...(x.buyBlockedByNews ? { покупка_заблокирована_новостями: x.buyBlockedByNews } : {}),
|
|
288
289
|
...(x.shortSignal !== undefined ? { сигнал_шорта: !!x.shortSignal } : {}),
|