neuroalpha 0.5.32 → 0.5.33
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/server/brokers/index.js +33 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -1612,7 +1612,7 @@ export async function freezeBrokerBaseline(id) {
|
|
|
1612
1612
|
}
|
|
1613
1613
|
|
|
1614
1614
|
// Оценка срабатывания скользящего Take-Profit и защиты заработанной прибыли от отката в минус (по лучшей цене с момента входа peakPx)
|
|
1615
|
-
function evaluateProfitLock(sym, side, avgPx, curPx, peakPx, roundTripFeePct = 0.10) {
|
|
1615
|
+
function evaluateProfitLock(sym, side, avgPx, curPx, peakPx, roundTripFeePct = 0.10, heldMin = 999) {
|
|
1616
1616
|
if (!(avgPx > 0 && curPx > 0)) return null;
|
|
1617
1617
|
const isLong = side === 'long';
|
|
1618
1618
|
const curGainPct = isLong ? ((curPx - avgPx) / avgPx) * 100 : ((avgPx - curPx) / avgPx) * 100;
|
|
@@ -1621,19 +1621,22 @@ function evaluateProfitLock(sym, side, avgPx, curPx, peakPx, roundTripFeePct = 0
|
|
|
1621
1621
|
: (isLong ? Math.max(avgPx, curPx) : Math.min(avgPx, curPx));
|
|
1622
1622
|
const peakGainPct = isLong ? ((effPeakPx - avgPx) / avgPx) * 100 : ((avgPx - effPeakPx) / avgPx) * 100;
|
|
1623
1623
|
const tierMult = symbolEdgeMult(sym);
|
|
1624
|
-
//
|
|
1624
|
+
// Минимальный чистый порог фиксации с учётом спреда стакана и двойной комиссии (чтобы выход не закрывался в 0.00% после исполнения лимитки)
|
|
1625
|
+
const minNetFloorPct = Math.max(roundTripFeePct + 0.30, round(0.38 * tierMult, 2));
|
|
1626
|
+
// 1) Скользящий Take-Profit при сильном перевыполнении цели (в первые 15 мин требуем более сильный импульс 1.55×tierMult, далее 1.20×tierMult):
|
|
1625
1627
|
// фиксируем не менее 70% набранного хода (или при откате на 0.45% с вершины)
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1628
|
+
const tpTriggerPct = (heldMin < 15 ? 1.55 : 1.20) * tierMult;
|
|
1629
|
+
if (peakGainPct >= tpTriggerPct) {
|
|
1630
|
+
const trailFloorPct = Math.max(peakGainPct * 0.70, peakGainPct - 0.45, minNetFloorPct);
|
|
1631
|
+
if (curGainPct <= trailFloorPct && curGainPct >= minNetFloorPct) {
|
|
1629
1632
|
return `Фиксация прибыли (Take-Profit: пик +${round(peakGainPct, 2)}% → +${round(curGainPct, 2)}%)`;
|
|
1630
1633
|
}
|
|
1631
1634
|
}
|
|
1632
|
-
// 2) Защита заработанного плюса / безубытка (от +0.
|
|
1633
|
-
// не позволяем плюсовой сделке развернуться обратно в
|
|
1634
|
-
if (peakGainPct >= 0.
|
|
1635
|
-
const breakevenFloorPct = Math.max(
|
|
1636
|
-
if (curGainPct <= breakevenFloorPct) {
|
|
1635
|
+
// 2) Защита заработанного плюса / безубытка (после первых 15 минут удержания, от +0.85% на 1-м эшелоне и от +1.02% на 2-м эшелоне):
|
|
1636
|
+
// не позволяем зрелой плюсовой сделке развернуться обратно в убыток, но и не режем позицию стартовым шумом стакана в первые минуты
|
|
1637
|
+
if (heldMin >= 15 && peakGainPct >= 0.85 * tierMult) {
|
|
1638
|
+
const breakevenFloorPct = Math.max(minNetFloorPct, peakGainPct * 0.45);
|
|
1639
|
+
if (curGainPct <= breakevenFloorPct && curGainPct >= roundTripFeePct + 0.22) {
|
|
1637
1640
|
return `Защита прибыли от отката (пик +${round(peakGainPct, 2)}% → фиксация +${round(curGainPct, 2)}%)`;
|
|
1638
1641
|
}
|
|
1639
1642
|
}
|
|
@@ -1977,7 +1980,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
1977
1980
|
if (aQty > 0) {
|
|
1978
1981
|
const lots = Math.floor(aQty / sp.lotSize);
|
|
1979
1982
|
const curGainPct = avgPx > 0 ? ((curPx - avgPx) / avgPx) * 100 : 0;
|
|
1980
|
-
const profitLockReason = autoAdapt ? evaluateProfitLock(sym, 'long', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct) : null;
|
|
1983
|
+
const profitLockReason = autoAdapt ? evaluateProfitLock(sym, 'long', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct, heldMin) : null;
|
|
1981
1984
|
const hitHardStopLoss = curGainPct <= -5.0 || Boolean(autoAdapt && r.regimeCrash);
|
|
1982
1985
|
// Если горизонт 2.5 ч ещё не вышел и прогноз смотрит в сторону лонга (> softExitThr), не режем позицию на локальном минутном откате в мелкий минус
|
|
1983
1986
|
const holdThroughMinorDip = autoAdapt && heldMin < horizonMin && r.liquid && r.expectedPct > softExitThr && curGainPct >= -1.0 && curGainPct < roundTripFeePct + 0.12;
|
|
@@ -2007,7 +2010,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2007
2010
|
} else if (aQty < 0) {
|
|
2008
2011
|
const lots = Math.floor(Math.abs(aQty) / sp.lotSize);
|
|
2009
2012
|
const curGainPct = avgPx > 0 ? ((avgPx - curPx) / avgPx) * 100 : 0;
|
|
2010
|
-
const profitLockReason = autoAdapt ? evaluateProfitLock(sym, 'short', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct) : null;
|
|
2013
|
+
const profitLockReason = autoAdapt ? evaluateProfitLock(sym, 'short', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct, heldMin) : null;
|
|
2011
2014
|
const hitHardStopLoss = curGainPct <= -4.5 || Boolean(autoAdapt && r.regimeSqueeze);
|
|
2012
2015
|
// Если горизонт 2.5 ч ещё не вышел и прогноз смотрит в сторону шорта (< -softExitThr), не закрываем шорт на минутном шипе в мелкий минус
|
|
2013
2016
|
const holdThroughMinorSpike = autoAdapt && heldMin < horizonMin && dir !== 'long' && r.liquid && r.expectedPct < -softExitThr && curGainPct >= -1.0 && curGainPct < roundTripFeePct + 0.12;
|
|
@@ -2331,7 +2334,7 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2331
2334
|
diagNote = `Сигнал ${deferredByImpulse.map((d) => `${d.symbol} (${d.side === 'short' ? 'шорт' : 'покупка'}, текущая ${d.curPx} ₽)`).join(', ')} взят на прицел: ждём затухания встречного минутного импульса для входа по лучшей цене (проверка каждые 20 сек).`;
|
|
2332
2335
|
} else if (!newBuys.length && !newShorts.length) {
|
|
2333
2336
|
const noShortNote = nonShortableSkipped.length ? ` (пропущены немаржинальные бумаги, недоступные для шорта у брокера: ${nonShortableSkipped.map((r) => `${r.symbol} ${r.expectedPct}%`).join(', ')})` : '';
|
|
2334
|
-
const cdNote = cooldownSkipped.length ? ` Сигнал ${cooldownSkipped.map((r) => `${r.symbol} (${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}%)`).join(', ')} временно пропущен: действует 60
|
|
2337
|
+
const cdNote = cooldownSkipped.length ? ` Сигнал ${cooldownSkipped.map((r) => `${r.symbol} (${r.expectedPct > 0 ? '+' : ''}${r.expectedPct}%)`).join(', ')} временно пропущен: действует пауза (30–60 мин) после недавнего выхода из позиции, чтобы не перезаходить в отыгранный импульс и не платить повторную комиссию.` : '';
|
|
2335
2338
|
const buyThrT2 = round(minBuyEdgePct * symbolEdgeMult('__T2__'), 2);
|
|
2336
2339
|
const shortThrT2 = round(minShortEdgePct * symbolEdgeMult('__T2__'), 2);
|
|
2337
2340
|
const thrRangeStr = dir !== 'long'
|
|
@@ -2816,7 +2819,9 @@ export async function executeBrokerOrders(id, { _lockHeld = false } = {}) {
|
|
|
2816
2819
|
hadExits = true;
|
|
2817
2820
|
delete acc.entryTsBySymbol[ord.symbol];
|
|
2818
2821
|
delete acc.peakPriceBySymbol[ord.symbol];
|
|
2819
|
-
if (ord.
|
|
2822
|
+
if (ord.symbol !== 'LQDT') {
|
|
2823
|
+
acc.exitCooldownBySymbol[ord.symbol] = ord.isStopLoss ? execNowTs : (execNowTs - 30 * 60000);
|
|
2824
|
+
}
|
|
2820
2825
|
}
|
|
2821
2826
|
} catch (e) {
|
|
2822
2827
|
// Брокер явно отклонил заявку — при повторе нужен новый ID. Сетевая ошибка или таймаут — ID сохраняется:
|
|
@@ -3232,19 +3237,24 @@ async function reconcileBrokerStopsLocked(list, acc) {
|
|
|
3232
3237
|
const aQty = algoQtyFor(acc, sym, Number(pos.qty) || 0);
|
|
3233
3238
|
if (!aQty) continue;
|
|
3234
3239
|
const wantCloseDir = aQty > 0 ? 'sell' : 'buy';
|
|
3240
|
+
const wantOpenDir = aQty > 0 ? 'buy' : 'sell';
|
|
3235
3241
|
if (activeOrders.some((o) => o.symbol === sym && o.direction === wantCloseDir)) continue;
|
|
3242
|
+
const activeOpenOrd = activeOrders.find((o) => o.symbol === sym && o.direction === wantOpenDir);
|
|
3243
|
+
const ordAgeMs = activeOpenOrd ? (Date.now() - (Date.parse(activeOpenOrd.orderDate || 0) || Number(acc.entryTsBySymbol?.[sym]) || 0)) : Infinity;
|
|
3244
|
+
const isFillingNow = Boolean(activeOpenOrd && ordAgeMs < 60000);
|
|
3236
3245
|
const lots = Math.floor(Math.abs(aQty) / (sp.lotSize || 1));
|
|
3237
3246
|
const avgPx = Number(pos.avgPrice) || 0;
|
|
3238
3247
|
if (lots < 1 || avgPx <= 0) continue;
|
|
3239
3248
|
const side = aQty > 0 ? 'long' : 'short';
|
|
3240
3249
|
const stopPx = roundToStep(side === 'long' ? avgPx * (1 - BROKER_STOP_LONG_PCT / 100) : avgPx * (1 + BROKER_STOP_SHORT_PCT / 100), sp.minStep, sp.decimals);
|
|
3241
|
-
want.set(sym, { lots, side, stopPx, sp });
|
|
3250
|
+
want.set(sym, { lots, side, stopPx, sp, filling: isFillingNow });
|
|
3242
3251
|
}
|
|
3243
3252
|
let changed = false;
|
|
3244
3253
|
// 1) снять свои стоп-заявки без позиции, сработавшие (их нет среди активных) или с другим объёмом / ценой / стороной
|
|
3245
3254
|
for (const [sym, t] of Object.entries(acc.brokerStopBySymbol)) {
|
|
3246
3255
|
const w = want.get(sym);
|
|
3247
3256
|
const alive = t?.stopOrderId && active.has(t.stopOrderId);
|
|
3257
|
+
if (alive && w?.filling && w.side === t.side) continue;
|
|
3248
3258
|
const same = alive && w && w.lots === t.lots && w.side === t.side && Math.abs(w.stopPx - t.stopPx) < (w.sp.minStep || 0.01) / 2;
|
|
3249
3259
|
if (same) continue;
|
|
3250
3260
|
if (alive) {
|
|
@@ -3265,7 +3275,7 @@ async function reconcileBrokerStopsLocked(list, acc) {
|
|
|
3265
3275
|
}
|
|
3266
3276
|
// 2) выставить недостающие
|
|
3267
3277
|
for (const [sym, w] of want) {
|
|
3268
|
-
if (acc.brokerStopBySymbol[sym]) continue;
|
|
3278
|
+
if (acc.brokerStopBySymbol[sym] || w.filling) continue;
|
|
3269
3279
|
const cd = acc.brokerStopCooldown[sym];
|
|
3270
3280
|
if (cd && (Date.now() - cd < 120000 || Number(acc.lastSync?.at || 0) < cd + 30000)) continue;
|
|
3271
3281
|
if (cd) { delete acc.brokerStopCooldown[sym]; changed = true; }
|
|
@@ -3349,12 +3359,14 @@ async function checkIntradayProfitLock(accId) {
|
|
|
3349
3359
|
const lastHistEntryOp = histOps.find((op) => op.symbol === sym && op.kind === wantOpenKind && op.date > 0);
|
|
3350
3360
|
const avgPx = Number(pos.avgPrice) > 0 ? Number(pos.avgPrice) : (Number(lastHistEntryOp?.price) || curPx);
|
|
3351
3361
|
if (avgPx <= 0) continue;
|
|
3362
|
+
const effEntryTs = Math.max(Number(acc.entryTsBySymbol?.[sym]) || 0, Number(lastHistEntryOp?.date) || 0);
|
|
3363
|
+
const heldMin = effEntryTs > 0 ? Math.max(0, (Date.now() - effEntryTs) / 60000) : 999;
|
|
3352
3364
|
|
|
3353
3365
|
if (aQty > 0) {
|
|
3354
3366
|
const lots = Math.floor(aQty / sp.lotSize);
|
|
3355
3367
|
if (lots < 1) continue;
|
|
3356
3368
|
const curGainPct = ((curPx - avgPx) / avgPx) * 100;
|
|
3357
|
-
const profitLockReason = evaluateProfitLock(sym, 'long', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct);
|
|
3369
|
+
const profitLockReason = evaluateProfitLock(sym, 'long', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct, heldMin);
|
|
3358
3370
|
const hitHardStopLoss = !hasBrokerHardStop && curGainPct <= -BROKER_STOP_LONG_PCT;
|
|
3359
3371
|
if (hitHardStopLoss || profitLockReason) {
|
|
3360
3372
|
const limitPrice = sniperLimitPrice(curPx, -1, acc.limitOffsetPct, sp);
|
|
@@ -3369,7 +3381,7 @@ async function checkIntradayProfitLock(accId) {
|
|
|
3369
3381
|
const lots = Math.floor(Math.abs(aQty) / sp.lotSize);
|
|
3370
3382
|
if (lots < 1) continue;
|
|
3371
3383
|
const curGainPct = ((avgPx - curPx) / avgPx) * 100;
|
|
3372
|
-
const profitLockReason = evaluateProfitLock(sym, 'short', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct);
|
|
3384
|
+
const profitLockReason = evaluateProfitLock(sym, 'short', avgPx, curPx, Number(peakMap[sym]) || 0, roundTripFeePct, heldMin);
|
|
3373
3385
|
const hitHardStopLoss = !hasBrokerHardStop && curGainPct <= -BROKER_STOP_SHORT_PCT;
|
|
3374
3386
|
if (hitHardStopLoss || profitLockReason) {
|
|
3375
3387
|
const limitPrice = sniperLimitPrice(curPx, 1, acc.limitOffsetPct, sp);
|
|
@@ -3414,7 +3426,9 @@ async function checkIntradayProfitLock(accId) {
|
|
|
3414
3426
|
sentLockOrders.push(ord);
|
|
3415
3427
|
delete acc.entryTsBySymbol[ord.symbol];
|
|
3416
3428
|
delete acc.peakPriceBySymbol[ord.symbol];
|
|
3417
|
-
if (ord.
|
|
3429
|
+
if (ord.symbol !== 'LQDT') {
|
|
3430
|
+
acc.exitCooldownBySymbol[ord.symbol] = ord.isStopLoss ? execNowTs : (execNowTs - 30 * 60000);
|
|
3431
|
+
}
|
|
3418
3432
|
} catch (e) {
|
|
3419
3433
|
results.push({ symbol: ord.symbol, ok: false, error: e.message });
|
|
3420
3434
|
logBrokerEvent(acc, 'protective_stop_error', `${ord.symbol}: ${e.message}`);
|