neuroalpha 0.5.29 → 0.5.30
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 +55 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.30",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/server/brokers/index.js
CHANGED
|
@@ -297,8 +297,26 @@ export function brokerAccountsSummary(uid = 1) {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
const candlePrevStmt = db.prepare('SELECT ts, open, close FROM candles WHERE symbol = ? AND interval = 10 AND ts <= ? ORDER BY ts DESC LIMIT 3');
|
|
300
|
+
const recentBarValStmt = db.prepare('SELECT value FROM candles WHERE symbol = ? AND interval = 10 AND value > 0 ORDER BY ts DESC LIMIT 48');
|
|
300
301
|
const executionAuditCache = new Map(); // accountId -> { sig, val }
|
|
301
302
|
|
|
303
|
+
// Медианный/средний 10-минутный биржевой оборот акции (в рублях) за последние ~8 торговых часов —
|
|
304
|
+
// ограничивает размер заявки во 2-м эшелоне, чтобы не ставить в тонкий стакан крупную «плиту», заметную роботам-маркетмейкерам
|
|
305
|
+
function recentBarTurnoverRub(symbol) {
|
|
306
|
+
try {
|
|
307
|
+
const rows = recentBarValStmt.all(symbol);
|
|
308
|
+
if (!rows.length) return 0;
|
|
309
|
+
const vals = rows.map((r) => Number(r.value) || 0).filter((v) => v > 0).sort((a, b) => a - b);
|
|
310
|
+
if (!vals.length) return 0;
|
|
311
|
+
const mid = Math.floor(vals.length / 2);
|
|
312
|
+
const medianVal = vals.length % 2 ? vals[mid] : (vals[mid - 1] + vals[mid]) / 2;
|
|
313
|
+
const avgVal = vals.reduce((s, v) => s + v, 0) / vals.length;
|
|
314
|
+
return Math.round((medianVal * 0.6 + avgVal * 0.4));
|
|
315
|
+
} catch {
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
302
320
|
// Телеметрия качества исполнения и сверка реальных сделок счёта с ориентиром стресс-теста (с кешированием по срезу истории)
|
|
303
321
|
function computeExecutionAudit(a) {
|
|
304
322
|
const ops = Array.isArray(a.lastHistory?.operations) ? a.lastHistory.operations : [];
|
|
@@ -2101,8 +2119,10 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2101
2119
|
const lqdtLots = Math.floor(lqdtAlgoQty / lqdtSp.lotSize);
|
|
2102
2120
|
const neededForNewRub = combinedNew.reduce((s, { r, side }) => {
|
|
2103
2121
|
const isT1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2122
|
+
const rawCap = budget * (side === 'short' ? (isT1 ? 0.35 : 0.18) : (isT1 ? 0.45 : 0.22));
|
|
2123
|
+
const barTurnRub = !isT1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2124
|
+
const liqCap = (!isT1 && barTurnRub > 0) ? Math.max(45000, Math.min(rawCap, barTurnRub * 0.12)) : rawCap;
|
|
2125
|
+
return s + Math.min(liqCap, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r));
|
|
2106
2126
|
}, 0);
|
|
2107
2127
|
const needFromLqdtRub = Math.min(Math.max(0, budget * 0.99 - usedRub), neededForNewRub) - availRub;
|
|
2108
2128
|
if (lqdtLots >= 1 && needFromLqdtRub > 1500) {
|
|
@@ -2140,9 +2160,15 @@ export async function prepareBrokerOrders(id, { onlyEntries = false, scheduledBa
|
|
|
2140
2160
|
remWeightSum = Math.max(0, remWeightSum - wCur);
|
|
2141
2161
|
remNew--;
|
|
2142
2162
|
const isTier1 = TIER1_LIQUID_SYMBOLS.has(r.symbol);
|
|
2143
|
-
const
|
|
2163
|
+
const rawSideCapRub = side === 'short'
|
|
2144
2164
|
? (isTier1 ? maxShortTier1Rub : maxShortTier2Rub)
|
|
2145
2165
|
: (isTier1 ? maxLongTier1Rub : maxLongTier2Rub);
|
|
2166
|
+
// Лимит от реального 10-минутного оборота стакана (Liquidity Cap): во 2-м эшелоне не входим объёмом больше 12% от 10-минутного оборота биржи,
|
|
2167
|
+
// чтобы заявка не становилась «стенкой» для стаканных роботов и исполнялась целиком без проскальзывания
|
|
2168
|
+
const barTurnoverRub = !isTier1 ? recentBarTurnoverRub(r.symbol) : 0;
|
|
2169
|
+
const sideCapRub = (!isTier1 && barTurnoverRub > 0)
|
|
2170
|
+
? Math.max(45000, Math.min(rawSideCapRub, barTurnoverRub * 0.12))
|
|
2171
|
+
: rawSideCapRub;
|
|
2146
2172
|
const rawSum = Math.min(sideCapRub, (params.modelAllocMode === 'invvol' && r.weight ? dynSlotRub * r.weight : dynSlotRub) * convOf(r));
|
|
2147
2173
|
const sum = Math.min(rawSum, propShare, availRub);
|
|
2148
2174
|
const refPx = r.price || specs[r.symbol]?.lastPrice || 0;
|
|
@@ -2316,9 +2342,10 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2316
2342
|
// • для входа (buy/short) и для LQDT ставим лимитку по лучшей цене своей стороны или середине спреда (не переплачиваем за пересечение спреда!);
|
|
2317
2343
|
// • если спред в стакане в момент входа разъехался шире половины ожидаемого хода сигнала (или >0,35%) — отклоняем вход в тонкий стакан!
|
|
2318
2344
|
let finalLimitPx = Number(ord.limitPrice) || 0;
|
|
2345
|
+
let lotsToOrder = Number(ord.lots) || 0;
|
|
2319
2346
|
const sp = kvGet('tbankShortableMap')?.specMap?.[ord.symbol] || { minStep: 0.01, decimals: 2 };
|
|
2320
2347
|
const rBook = await brokerFetch(`${base}/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook`, {
|
|
2321
|
-
method: 'POST', headers, body: JSON.stringify({ instrumentId, depth:
|
|
2348
|
+
method: 'POST', headers, body: JSON.stringify({ instrumentId, depth: 10 }), timeoutMs: 3500,
|
|
2322
2349
|
}).catch(() => null);
|
|
2323
2350
|
if (rBook?.ok) {
|
|
2324
2351
|
const jBook = await rBook.json().catch(() => null);
|
|
@@ -2333,14 +2360,35 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2333
2360
|
logBrokerEvent(acc, 'spread_skip', `${ord.symbol}: спред ${round(spreadPct, 2)}% > порога ${round(maxSpreadPct, 2)}% (ожидание ${ord.expectedPct || 0}%)`);
|
|
2334
2361
|
throw Object.assign(new Error(`Широкий спред в стакане ${ord.symbol} (${round(spreadPct, 2)}% > ${round(maxSpreadPct, 2)}%) — вход пропущен, чтобы не потерять на спреде`), { definitive: true });
|
|
2335
2362
|
}
|
|
2363
|
+
// Контроль видимой глубины стакана при входе во 2-й эшелон: не выставляем заявку крупнее 65% суммарного объёма 5 лучших уровней стакана,
|
|
2364
|
+
// чтобы не светить крупную «плиту» перед стаканными роботами
|
|
2365
|
+
if (isEntry && ord.symbol !== 'LQDT' && !TIER1_LIQUID_SYMBOLS.has(ord.symbol) && lotsToOrder > 1) {
|
|
2366
|
+
const topBookSide = (isBuy ? jBook?.asks : jBook?.bids) || [];
|
|
2367
|
+
const top5Lots = topBookSide.slice(0, 5).reduce((s, lv) => s + (Number(lv.quantity) || 0), 0);
|
|
2368
|
+
const minViableLots = Math.max(1, Math.ceil(30000 / Math.max(1, midPx * (ord.lotSize || 1))));
|
|
2369
|
+
if (top5Lots >= minViableLots) {
|
|
2370
|
+
const depthCapLots = Math.max(minViableLots, Math.floor(top5Lots * 0.65));
|
|
2371
|
+
if (lotsToOrder > depthCapLots) lotsToOrder = depthCapLots;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2336
2374
|
if (ord.symbol === 'LQDT') {
|
|
2337
2375
|
finalLimitPx = roundToStep(isBuy ? bestAsk : bestBid, sp.minStep || 0.001, sp.decimals ?? 3);
|
|
2338
2376
|
} else if (!isEntry) {
|
|
2339
|
-
// Закрывающие заявки (sell / cover):
|
|
2340
|
-
//
|
|
2341
|
-
|
|
2377
|
+
// Закрывающие заявки (sell / cover):
|
|
2378
|
+
// • при экстренном стоп-лоссе (isStopLoss) закрываемся гарантированно в пределах защитного лимита;
|
|
2379
|
+
// • при штатном выходе (Take-Profit или остывание прогноза) НЕ пробиваем несколько уровней тонкого стакана против себя:
|
|
2380
|
+
// при узком спреде (<= 0,10% или <= 2 шагов) ставим ровно в лучшую встречную цену (bestAsk / bestBid),
|
|
2381
|
+
// а при широком спреде встаём на середине спреда (midPx), не переплачивая стаканным роботам!
|
|
2382
|
+
const tightExitSpread = spreadPct <= 0.10 || (bestAsk - bestBid) <= 2.1 * step;
|
|
2383
|
+
if (ord.isStopLoss && spreadPct <= 0.80) {
|
|
2342
2384
|
const mktExitPx = roundToStep(isBuy ? bestAsk : bestBid, step, sp.decimals);
|
|
2343
2385
|
if (mktExitPx > 0) finalLimitPx = isBuy ? Math.max(finalLimitPx, mktExitPx) : Math.min(finalLimitPx, mktExitPx);
|
|
2386
|
+
} else if (tightExitSpread) {
|
|
2387
|
+
const cleanExitPx = roundToStep(isBuy ? bestAsk : bestBid, step, sp.decimals);
|
|
2388
|
+
if (cleanExitPx > 0) finalLimitPx = cleanExitPx;
|
|
2389
|
+
} else if (spreadPct <= 0.80) {
|
|
2390
|
+
const midExitPx = roundToStep(midPx, step, sp.decimals);
|
|
2391
|
+
if (midExitPx > 0) finalLimitPx = isBuy ? Math.min(bestAsk, midExitPx) : Math.max(bestBid, midExitPx);
|
|
2344
2392
|
}
|
|
2345
2393
|
} else {
|
|
2346
2394
|
// Входящие заявки (buy / short):
|
|
@@ -2361,7 +2409,6 @@ async function sendOrderToBroker(acc, ord) {
|
|
|
2361
2409
|
}
|
|
2362
2410
|
const units = Math.trunc(finalLimitPx);
|
|
2363
2411
|
const nano = Math.round((finalLimitPx - units) * 1e9);
|
|
2364
|
-
let lotsToOrder = Number(ord.lots) || 0;
|
|
2365
2412
|
|
|
2366
2413
|
// Перед выставлением новой лимитной заявки автоматически снимаем старые неисполненные заявки по этой же бумаге и направлению,
|
|
2367
2414
|
// чтобы в стакане никогда не копились дублирующиеся заявки и не блокировали рублёвый лимит счёта!
|