neuroalpha 0.5.19 → 0.5.21
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/admin.html +2 -1
- package/public/app.js +39 -15
- package/public/landing.html +1207 -1499
- package/public/legal.html +6 -5
- package/server/api/routes.js +15 -3
- package/server/auth/index.js +23 -9
- package/server/brokers/index.js +4 -3
- package/server/model/edge.js +34 -1
- package/server/model/forecast.js +9 -6
- package/server/model/research.js +9 -4
- package/server/security.js +4 -1
package/public/legal.html
CHANGED
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
<h1>Правовые документы сервиса «НейроАльфа»</h1>
|
|
38
38
|
|
|
39
39
|
<section class="doc" id="offer">
|
|
40
|
-
<h2>1.
|
|
41
|
-
<p>Настоящий документ является официальным предложением (публичной офертой в соответствии со ст. 435
|
|
40
|
+
<h2>1. Лицензионная публичная оферта (Договор предоставления права использования ПО и аренды инфраструктуры PaaS)</h2>
|
|
41
|
+
<p>Настоящий документ является официальным предложением (публичной офертой в соответствии со ст. 435, ч. 2 ст. 437 и ст. 1286 Гражданского кодекса РФ) Правообладателя и Оператора платформы «НейроАльфа» (далее — Лицензиар / Исполнитель) любому дееспособному физическому или юридическому лицу (далее — Лицензиат / Пользователь) заключить договор на предоставление простой (неисключительной) лицензии на право использования программного обеспечения для ЭВМ «НейроАльфа» и аренду выделенной виртуальной машины PaaS.</p>
|
|
42
42
|
|
|
43
|
-
<h3>1.1. Предмет договора и правовой статус
|
|
43
|
+
<h3>1.1. Предмет договора и правовой статус программного обеспечения</h3>
|
|
44
44
|
<ul>
|
|
45
|
-
<li
|
|
46
|
-
<li
|
|
45
|
+
<li>Лицензиар предоставляет Пользователю за вознаграждение простую (неисключительную) лицензию на право дистанционного использования программно-вычислительного комплекса «НейроАльфа» (модель PaaS или локальный дистрибутив на тарифе VIP) в соответствии с выбранным тарифным планом («Инвестор», «Профи», «VIP»).</li>
|
|
46
|
+
<li>Оплата по настоящему Договору взимается <b>исключительно за предоставление права использования программного обеспечения (лицензии)</b> и выделенные вычислительные ресурсы PaaS.</li>
|
|
47
|
+
<li>Программный комплекс «НейроАльфа» <b>не является сервисом по зарабатыванию денег</b>, а представляет собой исключительно техническое программное обеспечение для ЭВМ (статистический калькулятор котировок и технический шлюз автоматизации обмена командами с API брокеров по параметрам и предохранителям, заданным самим Пользователем).</li>
|
|
47
48
|
<li><b>Исполнитель не осуществляет деятельность по инвестиционному консультированию</b> (в смысле ст. 6.1 Федерального закона № 39-ФЗ «О рынке ценных бумаг»), не составляет индивидуальных инвестиционных профилей и не даёт индивидуальных инвестиционных рекомендаций (ИИР).</li>
|
|
48
49
|
<li><b>Исполнитель не осуществляет доверительное управление</b> или брокерскую деятельность, не принимает денежные средства или ценные бумаги Пользователей на свои счета. Все торговые операции совершаются Пользователем самостоятельно на его личном счёте у лицензированного брокера.</li>
|
|
49
50
|
</ul>
|
package/server/api/routes.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ROOT, strategyDefaults } from '../config.js';
|
|
|
6
6
|
import { activeModelInfo } from '../llm/client.js';
|
|
7
7
|
import { listProviders, getSelected, addProvider, updateProvider, deleteProvider, fetchRemoteModels, testProvider, selectModel, resolveUpstreamForProxy, providerFetch } from '../llm/providers.js';
|
|
8
8
|
import { listRuns, allRuns, getRun, getTrades, deleteRun, updateRunTitle, setRunShowcase, listShowcaseRuns, countUserRunsToday, countUserChatToday, listJournal, deleteJournal, addJournal, listSettingsHistory, kvGet, kvSet, db, loadCandles } from '../db.js';
|
|
9
|
-
import { PLANS, effectivePlan, publicUser, getUserById, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createOrUpdateAdminUser, createPromoCode, listPromoCodes, redeemPromoCode, acceptUserOffer, createVipRequest, listVipRequests, parseCookies, publicOAuthConfig, saveOAuthConfig, startOAuthRedirect, completeOAuthCallback, verifyTelegramLoginWidget } from '../auth/index.js';
|
|
9
|
+
import { PLANS, effectivePlan, publicUser, getUserById, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createOrUpdateAdminUser, createPromoCode, listPromoCodes, redeemPromoCode, acceptUserOffer, createVipRequest, listVipRequests, parseCookies, parseAllCookieValues, publicOAuthConfig, saveOAuthConfig, startOAuthRedirect, completeOAuthCallback, verifyTelegramLoginWidget } from '../auth/index.js';
|
|
10
10
|
import { publicBillingConfig, saveBillingConfig, createYookassaPayment, verifyYookassaPayment, verifyUserLatestPayment, listAllPayments, listUserPayments } from '../billing/yookassa.js';
|
|
11
11
|
import { publicNotifyConfig, getNotifyConfig, saveNotifyConfig, notifyUser, createTelegramLoginChallenge, checkTelegramLoginChallenge, handleMaxWebhook } from '../notify/index.js';
|
|
12
12
|
import { PARAMS } from '../params-meta.js';
|
|
@@ -73,6 +73,14 @@ export function createRouter() {
|
|
|
73
73
|
const secure = isHttpsRequest(res.req) ? '; Secure' : '';
|
|
74
74
|
res.setHeader('Set-Cookie', `sid=${encodeURIComponent(sid)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSec}${secure}`);
|
|
75
75
|
};
|
|
76
|
+
const isRuSegmentRequest = (req) => {
|
|
77
|
+
if (process.env.ALLOW_FOREIGN_AUTH === '1') return false;
|
|
78
|
+
const country = String(req.headers?.['cf-ipcountry'] || req.headers?.['x-country-code'] || req.headers?.['x-geo-country'] || '').trim().toUpperCase();
|
|
79
|
+
if (country) return country === 'RU';
|
|
80
|
+
const lang = String(req.headers?.['accept-language'] || '').toLowerCase();
|
|
81
|
+
if (lang && !lang.includes('ru')) return false;
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
76
84
|
r.get('/auth/me', (req, res) => res.json({
|
|
77
85
|
user: req.user ? { ...publicUser(req.user), instances: IS_CENTRAL ? cabinetInstances(req.user.id) : [], payments: listUserPayments(req.user.id, 15) } : null,
|
|
78
86
|
downloads: IS_CENTRAL ? {
|
|
@@ -83,6 +91,7 @@ export function createRouter() {
|
|
|
83
91
|
role: IS_INSTANCE ? 'instance' : IS_CENTRAL ? 'central' : 'dev',
|
|
84
92
|
centralUrl: IS_INSTANCE ? CENTRAL_URL : '',
|
|
85
93
|
cloudMode: process.env.CLOUD_MODE === '1',
|
|
94
|
+
isRuSegment: isRuSegmentRequest(req),
|
|
86
95
|
plans: PLANS,
|
|
87
96
|
notify: publicNotifyConfig(),
|
|
88
97
|
oauth: publicOAuthConfig(),
|
|
@@ -154,8 +163,9 @@ export function createRouter() {
|
|
|
154
163
|
res.json({ user: publicUser(u), token });
|
|
155
164
|
}));
|
|
156
165
|
r.post('/auth/logout', (req, res) => {
|
|
157
|
-
const
|
|
158
|
-
|
|
166
|
+
for (const tok of parseAllCookieValues(req.headers?.cookie, 'sid')) {
|
|
167
|
+
if (tok && tok !== 'logged_out') deleteSession(tok);
|
|
168
|
+
}
|
|
159
169
|
setCookie(res, 'logged_out', 365 * 86400);
|
|
160
170
|
res.json({ ok: true, ...(IS_INSTANCE ? { redirect: CENTRAL_URL } : {}) });
|
|
161
171
|
});
|
|
@@ -748,6 +758,8 @@ export function createRouter() {
|
|
|
748
758
|
yearChangePct,
|
|
749
759
|
tier: isTier1 ? 1 : 2,
|
|
750
760
|
tierLabel: isTier1 ? '1-й эшелон (высокая ликвидность)' : '2-й эшелон',
|
|
761
|
+
edgeMult: fcRow?.edgeMult ?? round(tierMult, 3),
|
|
762
|
+
sigmaPct: fcRow?.sigmaPct ?? null,
|
|
751
763
|
buyThresholdPct: fcRow?.buyThresholdPct ?? round(Math.max((fc?.buyThresholdPct || 0.413) * tierMult, 0.30), 3),
|
|
752
764
|
shortThresholdPct: fcRow?.shortThresholdPct ?? round(Math.max((fc?.shortThresholdPct || 0.516) * tierMult, 0.31), 3),
|
|
753
765
|
shortAllowedByBroker: shortableMap[sym] !== undefined ? Boolean(shortableMap[sym]) : null,
|
package/server/auth/index.js
CHANGED
|
@@ -360,8 +360,20 @@ export function parseCookies(cookieHeader = '') {
|
|
|
360
360
|
return out;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
export function parseAllCookieValues(cookieHeader = '', key = 'sid') {
|
|
364
|
+
const vals = [];
|
|
365
|
+
for (const part of String(cookieHeader || '').split(';')) {
|
|
366
|
+
const i = part.indexOf('=');
|
|
367
|
+
if (i > 0 && part.slice(0, i).trim() === key) {
|
|
368
|
+
vals.push(decodeURIComponent(part.slice(i + 1).trim()));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return vals;
|
|
372
|
+
}
|
|
373
|
+
|
|
363
374
|
export function userFromRequest(req) {
|
|
364
375
|
const cookies = parseCookies(req.headers?.cookie);
|
|
376
|
+
const allSids = parseAllCookieValues(req.headers?.cookie, 'sid');
|
|
365
377
|
const authHeader = String(req.headers?.authorization || '');
|
|
366
378
|
const apiKeyHeader = String(req.headers?.['x-api-key'] || '').trim();
|
|
367
379
|
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : '';
|
|
@@ -370,8 +382,8 @@ export function userFromRequest(req) {
|
|
|
370
382
|
const byKey = getUserByApiKey(apiCandidate);
|
|
371
383
|
if (byKey) return byKey;
|
|
372
384
|
}
|
|
373
|
-
const
|
|
374
|
-
|
|
385
|
+
const candidates = [...allSids.filter((s) => s && s !== 'logged_out'), ...(bearer ? [bearer] : [])];
|
|
386
|
+
for (const token of candidates) {
|
|
375
387
|
const sess = db.prepare('SELECT * FROM sessions WHERE token = ? AND expires_at > ?').get(token, Date.now());
|
|
376
388
|
if (sess) {
|
|
377
389
|
const u = getUserById(sess.user_id);
|
|
@@ -381,7 +393,8 @@ export function userFromRequest(req) {
|
|
|
381
393
|
// В локальном режиме (без CLOUD_MODE=1) владелец (id = 1) открывается без входа только с этого компьютера
|
|
382
394
|
// Установка на компьютер клиента после активации — так же, без пароля только с этого компьютера
|
|
383
395
|
const localTrusted = !IS_INSTANCE || (IS_DESKTOP && isActivated());
|
|
384
|
-
|
|
396
|
+
const explicitlyLoggedOut = cookies.sid === 'logged_out' || allSids.includes('logged_out');
|
|
397
|
+
if (process.env.CLOUD_MODE !== '1' && localTrusted && !explicitlyLoggedOut && isLoopbackRequest(req)) {
|
|
385
398
|
return getUserById(1);
|
|
386
399
|
}
|
|
387
400
|
return null;
|
|
@@ -549,11 +562,11 @@ export function authMiddleware(req, res, next) {
|
|
|
549
562
|
|
|
550
563
|
// ---------- OAuth 2.0 / Социальный вход: Яндекс, Google, Сбер ID, Т-Банк ID, VK ID ----------
|
|
551
564
|
export const OAUTH_PROVIDERS = [
|
|
552
|
-
{ id: 'yandex', name: 'Яндекс ID', authUrl: 'https://oauth.yandex.ru/authorize', tokenUrl: 'https://oauth.yandex.ru/token', userUrl: 'https://login.yandex.ru/info?format=json', scope: 'login:email login:info' },
|
|
553
|
-
{ id: 'google', name: 'Google', authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', userUrl: 'https://www.googleapis.com/oauth2/v3/userinfo', scope: 'openid email profile' },
|
|
554
|
-
{ id: 'sber', name: 'Сбер ID', authUrl: 'https://oauth.sber.ru/ru/prod/tokens/v2/oauth', tokenUrl: 'https://oauth.sber.ru/ru/prod/tokens/v2/oidc', userUrl: 'https://oauth.sber.ru/ru/prod/sberbankid/v2.1/userinfo', scope: 'openid name email' },
|
|
555
|
-
{ id: 'tbank', name: 'T-ID (Т-Банк)', authUrl: 'https://id.tbank.ru/auth/authorize', tokenUrl: 'https://id.tbank.ru/auth/token', userUrl: 'https://id.tbank.ru/userinfo/userinfo', scope: 'openid profile email' },
|
|
556
|
-
{ id: 'vk', name: 'VK ID', authUrl: 'https://oauth.vk.com/authorize', tokenUrl: 'https://oauth.vk.com/access_token', userUrl: 'https://api.vk.com/method/users.get?v=5.199', scope: 'email' },
|
|
565
|
+
{ id: 'yandex', name: 'Яндекс ID', foreign: false, authUrl: 'https://oauth.yandex.ru/authorize', tokenUrl: 'https://oauth.yandex.ru/token', userUrl: 'https://login.yandex.ru/info?format=json', scope: 'login:email login:info' },
|
|
566
|
+
{ id: 'google', name: 'Google', foreign: true, authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', userUrl: 'https://www.googleapis.com/oauth2/v3/userinfo', scope: 'openid email profile' },
|
|
567
|
+
{ id: 'sber', name: 'Сбер ID', foreign: false, authUrl: 'https://oauth.sber.ru/ru/prod/tokens/v2/oauth', tokenUrl: 'https://oauth.sber.ru/ru/prod/tokens/v2/oidc', userUrl: 'https://oauth.sber.ru/ru/prod/sberbankid/v2.1/userinfo', scope: 'openid name email' },
|
|
568
|
+
{ id: 'tbank', name: 'T-ID (Т-Банк)', foreign: false, authUrl: 'https://id.tbank.ru/auth/authorize', tokenUrl: 'https://id.tbank.ru/auth/token', userUrl: 'https://id.tbank.ru/userinfo/userinfo', scope: 'openid profile email' },
|
|
569
|
+
{ id: 'vk', name: 'VK ID', foreign: false, authUrl: 'https://oauth.vk.com/authorize', tokenUrl: 'https://oauth.vk.com/access_token', userUrl: 'https://api.vk.com/method/users.get?v=5.199', scope: 'email' },
|
|
557
570
|
];
|
|
558
571
|
|
|
559
572
|
export function getOAuthConfig() {
|
|
@@ -564,7 +577,7 @@ export function getOAuthConfig() {
|
|
|
564
577
|
const cur = saved[p.id] || {};
|
|
565
578
|
const clientId = String(cur.clientId || process.env[`${envPrefix}CLIENT_ID`] || '').trim();
|
|
566
579
|
const clientSecret = decryptSecret(cur.clientSecret || '') || String(process.env[`${envPrefix}CLIENT_SECRET`] || '').trim();
|
|
567
|
-
out[p.id] = { id: p.id, name: p.name, clientId, clientSecret, enabled: Boolean(clientId && clientSecret) };
|
|
580
|
+
out[p.id] = { id: p.id, name: p.name, foreign: Boolean(p.foreign), clientId, clientSecret, enabled: Boolean(clientId && clientSecret) };
|
|
568
581
|
}
|
|
569
582
|
return out;
|
|
570
583
|
}
|
|
@@ -574,6 +587,7 @@ export function publicOAuthConfig() {
|
|
|
574
587
|
return Object.fromEntries(OAUTH_PROVIDERS.map((p) => [p.id, {
|
|
575
588
|
id: p.id,
|
|
576
589
|
name: p.name,
|
|
590
|
+
foreign: Boolean(p.foreign),
|
|
577
591
|
clientId: cfg[p.id].clientId,
|
|
578
592
|
hasSecret: Boolean(cfg[p.id].clientSecret),
|
|
579
593
|
enabled: cfg[p.id].enabled,
|
package/server/brokers/index.js
CHANGED
|
@@ -1575,8 +1575,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false } = {}) {
|
|
|
1575
1575
|
const baseShortNeedPct = Number(fc.shortThresholdPct ?? fc.thresholdPct ?? 0.50);
|
|
1576
1576
|
const minBuyEdgePct = round(baseBuyNeedPct, 3);
|
|
1577
1577
|
const minShortEdgePct = round(baseShortNeedPct, 3);
|
|
1578
|
-
const reqBuyEdgeOf = (sym) => round(Math.max(minBuyEdgePct * symbolEdgeMult(sym), 0.30), 3);
|
|
1579
|
-
const reqShortEdgeOf = (sym) => round(Math.max(minShortEdgePct * symbolEdgeMult(sym), 0.31), 3);
|
|
1578
|
+
const reqBuyEdgeOf = (sym) => rowMap.get(sym)?.buyThresholdPct ?? round(Math.max(minBuyEdgePct * symbolEdgeMult(sym), 0.30), 3);
|
|
1579
|
+
const reqShortEdgeOf = (sym) => rowMap.get(sym)?.shortThresholdPct ?? round(Math.max(minShortEdgePct * symbolEdgeMult(sym), 0.31), 3);
|
|
1580
1580
|
const isInCooldown = (sym) => autoAdapt && exitCooldownMap[sym] && (nowTs - exitCooldownMap[sym] < 60 * 60000);
|
|
1581
1581
|
// Самообучение на истории реальных сделок счёта (адаптивный карантин убыточных бумаг, единый с research.js):
|
|
1582
1582
|
// если за последние 15 торговых дней по бумаге накоплен убыток (< -1.2% за >= 2 сделки или одиночный пролив < -2.5%),
|
|
@@ -1694,7 +1694,8 @@ export async function prepareBrokerOrders(id, { onlyEntries = false } = {}) {
|
|
|
1694
1694
|
const heldMs = effEntryTs > 0 ? Math.max(0, nowTs - effEntryTs) : 0;
|
|
1695
1695
|
// Как в эталонном симуляторе (research.js): первые 65 минут после открытия позиции действует мягкий порог удержания (0.35),
|
|
1696
1696
|
// чтобы не резать сделку от рыночного шума каждые 30 минут; затем включается плотный порог фиксации (0.95) с учётом эшелона
|
|
1697
|
-
const
|
|
1697
|
+
const symMult = r.edgeMult || symbolEdgeMult(sym);
|
|
1698
|
+
const curExitThr = round(((autoAdapt && heldMs <= 65 * 60000) ? earlyExitThresholdPct : exitThresholdPct) * symMult, 3);
|
|
1698
1699
|
const nightExit = !onlyEntries && autoAdapt && shouldExitBeforeNight(nowTs, effEntryTs, stepBars, BAR_INTERVAL);
|
|
1699
1700
|
const curPx = Number(r.price || pos.price) || 0;
|
|
1700
1701
|
const avgPx = Number(pos.avgPrice) > 0 ? Number(pos.avgPrice) : (Number(lastHistEntryOp?.price) || curPx);
|
package/server/model/edge.js
CHANGED
|
@@ -16,7 +16,40 @@ export const TIER1_LIQUID_SYMBOLS = new Set([
|
|
|
16
16
|
'OZON', 'HEAD', 'PHOR', 'AFLT', 'MTSS', 'IRAO', 'TRNFP', 'RUAL', 'FLOT', 'X5',
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
-
export
|
|
19
|
+
export function symbolEdgeMult(symbol, { sigma = 0, avgSigma = 0, avgBarValRub = 0 } = {}) {
|
|
20
|
+
const isT1 = TIER1_LIQUID_SYMBOLS.has(symbol);
|
|
21
|
+
const base = isT1 ? 1.0 : TIER2_EDGE_MULT;
|
|
22
|
+
if (!(sigma > 0 || avgBarValRub > 0)) return base;
|
|
23
|
+
const refVal = isT1 ? 15e6 : 1.5e6;
|
|
24
|
+
const liqAdj = avgBarValRub > 0
|
|
25
|
+
? Math.max(-0.07, Math.min(0.10, -0.05 * Math.log10(Math.max(5e4, avgBarValRub) / refVal)))
|
|
26
|
+
: 0;
|
|
27
|
+
const sigRatio = (sigma > 0 && avgSigma > 0) ? (sigma / avgSigma) : 1;
|
|
28
|
+
const volAdj = Math.max(-0.05, Math.min(0.10, 0.12 * (sigRatio - 1)));
|
|
29
|
+
return Math.max(0.88, Math.min(1.45, base * (1 + liqAdj + volAdj)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeTokenEdgeMults(tokens, sigma, fromIdx = 0, tillIdx = 0) {
|
|
33
|
+
const N = tokens.length;
|
|
34
|
+
let sigSum = 0, sigCnt = 0;
|
|
35
|
+
if (sigma) {
|
|
36
|
+
for (let i = 0; i < N; i++) if (sigma[i] > 0) { sigSum += sigma[i]; sigCnt++; }
|
|
37
|
+
}
|
|
38
|
+
const avgSigma = sigCnt > 0 ? sigSum / sigCnt : 0;
|
|
39
|
+
const out = new Float64Array(N);
|
|
40
|
+
for (let i = 0; i < N; i++) {
|
|
41
|
+
const tk = tokens[i];
|
|
42
|
+
const lo = Math.max(0, fromIdx);
|
|
43
|
+
const hi = tillIdx > lo ? Math.min(tk.value.length, tillIdx) : tk.value.length;
|
|
44
|
+
let vSum = 0, vCnt = 0;
|
|
45
|
+
for (let t = lo; t < hi; t++) {
|
|
46
|
+
if (tk.value[t] > 0) { vSum += tk.value[t]; vCnt++; }
|
|
47
|
+
}
|
|
48
|
+
const avgBarValRub = vCnt > 0 ? vSum / vCnt : 0;
|
|
49
|
+
out[i] = symbolEdgeMult(tk.symbol, { sigma: sigma ? sigma[i] : 0, avgSigma, avgBarValRub });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
20
53
|
|
|
21
54
|
// Затухание штрафных счётчиков карантина на 30-минутном шаге (период полураспада ~15 торговых дней: 0,9985^(15×33) ≈ 0,48)
|
|
22
55
|
export const QUARANTINE_DECAY = 0.9985;
|
package/server/model/forecast.js
CHANGED
|
@@ -5,8 +5,8 @@ import { loadDataset, barsFromHours, BAR_MS } from './dataset.js';
|
|
|
5
5
|
import { buildFold, MODELS, HEADS, KINDS, SCALES, BETAS, LIQ_MIN, FEATURE_WARMUP, explainAttention, mapSnapshot } from './models.js';
|
|
6
6
|
import { loadProfiles } from './profiles.js';
|
|
7
7
|
import { isMarketDown } from './research.js';
|
|
8
|
-
import { ENTRY_EDGE_MULT, TIER2_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult } from './edge.js';
|
|
9
|
-
export { TIER1_LIQUID_SYMBOLS, symbolEdgeMult };
|
|
8
|
+
import { ENTRY_EDGE_MULT, TIER2_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults } from './edge.js';
|
|
9
|
+
export { TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults };
|
|
10
10
|
import { newsForSymbols, blockingNews } from '../news/index.js';
|
|
11
11
|
import { liveDividendGuard } from './dividends.js';
|
|
12
12
|
import { fetchTbankRealtimePrices } from '../brokers/index.js';
|
|
@@ -313,14 +313,15 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
|
|
|
313
313
|
let sInv = 0, nInv = 0;
|
|
314
314
|
for (let i = 0; i < ds.tokens.length; i++) if (f.sigma[i] > 0 && ds.tokens[i].liq[idx] >= LIQ_MIN) { sInv += 1 / f.sigma[i]; nInv++; }
|
|
315
315
|
const avgInv = nInv ? sInv / nInv : 0;
|
|
316
|
+
const trainSpan = Math.round(90 * (ds.barsPerDay || 54));
|
|
317
|
+
const edgeMults = computeTokenEdgeMults(ds.tokens, f.sigma, Math.max(0, idx - trainSpan), idx);
|
|
316
318
|
const rows = ds.tokens.map((tk, i) => {
|
|
317
319
|
const exp = preds[chosen][i] * f.sigma[i] * Math.sqrt(H);
|
|
318
320
|
const weight = avgInv > 0 && f.sigma[i] > 0 ? Math.max(0.4, Math.min(1.8, (1 / f.sigma[i]) / avgInv)) : 1;
|
|
319
321
|
const livePx = liveSnap?.prices?.get(tk.symbol) || tk.close[idx] || 0;
|
|
320
322
|
const lastMovePct = round(f.z[u * f.N + i] * f.sigma[i] * 100, 3);
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
const tierMult = symbolEdgeMult(tk.symbol);
|
|
323
|
+
// Индивидуальный множитель порога каждой акции по её фактическому обороту в рублях и волатильности свечи:
|
|
324
|
+
const tierMult = edgeMults[i] || symbolEdgeMult(tk.symbol);
|
|
324
325
|
const symNeedL = Math.max(needL * tierMult, 0.0030);
|
|
325
326
|
const symNeedS = Math.max(needS * tierMult, 0.0031);
|
|
326
327
|
// Аварийный предохранитель экстремальных свечей (±4,5% за одну 10-минутную свечу — планка / остановка торгов / обвал):
|
|
@@ -337,7 +338,9 @@ function buildPredictionRows(ds, p, idx, f, u, fitted, chosen, H, effMinEdge, au
|
|
|
337
338
|
const buyBlockedByDip = Boolean(liveSnap && exp > 0 && lastMovePct < -EXTREME_BAR_MOVE_PCT);
|
|
338
339
|
const row = {
|
|
339
340
|
i, symbol: tk.symbol, name: tk.name,
|
|
340
|
-
tier:
|
|
341
|
+
tier: TIER1_LIQUID_SYMBOLS.has(tk.symbol) ? 1 : 2,
|
|
342
|
+
edgeMult: round(tierMult, 3),
|
|
343
|
+
sigmaPct: round(f.sigma[i] * 100, 3),
|
|
341
344
|
price: round(livePx, 4),
|
|
342
345
|
liquid: tk.liq[idx] >= LIQ_MIN,
|
|
343
346
|
expected: exp,
|
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, symbolEdgeMult, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight } from './edge.js';
|
|
12
|
+
import { ENTRY_EDGE_MULT, TIER1_LIQUID_SYMBOLS, symbolEdgeMult, computeTokenEdgeMults, QUARANTINE_DECAY, isStockQuarantined, canEnterIntraday, shouldExitBeforeNight } from './edge.js';
|
|
13
13
|
import { buildDividendCalendar, nextDividendAt, dividendRules, DIV_TAX } from './dividends.js';
|
|
14
14
|
|
|
15
15
|
const DAY = 86400000;
|
|
@@ -39,7 +39,8 @@ export function isMarketDown(ds, t) {
|
|
|
39
39
|
// 8 — скорректированный спектр Марченко–Пастура (sigmaBulk) для 0 — Авто + динамическая концентрация слота (Conviction Sizing)
|
|
40
40
|
// 9: исторический состав индекса (без ошибки выжившего), дивиденды префов, правила v0.5.x
|
|
41
41
|
// 10: запрет переноса внутридневных сделок через ночь, период полураспада карантина 15 дн., строгий 1-й эшелон
|
|
42
|
-
|
|
42
|
+
// 11: индивидуальный расчёт порога каждой акции по её фактическому рублёвому обороту и остаточной волатильности
|
|
43
|
+
const DATA_VERSION = 11;
|
|
43
44
|
|
|
44
45
|
const AUTO_LOCKED = { modelHorizon: 2.5, modelMinEdge: 2.5, modelExitRatio: 0.95 };
|
|
45
46
|
function reuseParamsKey(params) {
|
|
@@ -195,6 +196,7 @@ async function run(runId, p, state) {
|
|
|
195
196
|
const yraw = new Map(); // t -> Float64Array(N)
|
|
196
197
|
const sigmaAt = new Map(); // t -> обычный размах акций в этом окне обучения
|
|
197
198
|
const betaAt = new Map(); // t -> бета акций к индексу в этом окне обучения (для защиты от движения рынка)
|
|
199
|
+
const multAt = new Map(); // t -> индивидуальные множители порога каждой акции в этом окне обучения
|
|
198
200
|
const snapshots = [];
|
|
199
201
|
let prevPts = null, prevCl = null, lastFold = null, lastModels = null;
|
|
200
202
|
const folds = Math.ceil((T - H - firstTest) / testLen);
|
|
@@ -207,6 +209,7 @@ async function run(runId, p, state) {
|
|
|
207
209
|
const mainText = `Обучение ${fi + 1} из ${folds}: учимся до ${fmtDay(timeline[testStart])}, прогнозируем до ${fmtDay(timeline[testEnd - 1])}`;
|
|
208
210
|
await tick();
|
|
209
211
|
const f = buildFold(ds, { trainStart, testStart, testEnd, H, embedDim: p.modelEmbedDim, textWeight: p.modelTextWeight, profiles, multiHorizon: autoAdapt });
|
|
212
|
+
const foldEdgeMults = computeTokenEdgeMults(tokens, f.sigma, trainStart, testStart);
|
|
210
213
|
const fitted = {};
|
|
211
214
|
for (const n of names) {
|
|
212
215
|
fitted[n] = await MODELS[n].fit(f);
|
|
@@ -225,6 +228,7 @@ async function run(runId, p, state) {
|
|
|
225
228
|
yraw.set(t, Float64Array.from(f.yraw.subarray(u * N, u * N + N)));
|
|
226
229
|
sigmaAt.set(t, f.sigma);
|
|
227
230
|
betaAt.set(t, f.beta);
|
|
231
|
+
multAt.set(t, foldEdgeMults);
|
|
228
232
|
}
|
|
229
233
|
// снимок карты рынка на начало месяца прогноза
|
|
230
234
|
const snap = mapSnapshot(f);
|
|
@@ -291,7 +295,7 @@ async function run(runId, p, state) {
|
|
|
291
295
|
const mktFilter = p.modelMarketFilter === 'ma';
|
|
292
296
|
const invVol = p.modelAllocMode === 'invvol';
|
|
293
297
|
const realCost = (p.commissionPct + p.slippagePct) / 100;
|
|
294
|
-
const
|
|
298
|
+
const baseSymMult = Float64Array.from(tokens, (tk) => symbolEdgeMult(tk.symbol));
|
|
295
299
|
const divCal = buildDividendCalendar(ds);
|
|
296
300
|
// реальные цены вечного фьючерса на индекс МосБиржи (IMOEXF) — для честной оценки защиты фьючерсом
|
|
297
301
|
let futOpen = null;
|
|
@@ -351,7 +355,7 @@ async function run(runId, p, state) {
|
|
|
351
355
|
// множитель порога входа и выхода (1 — как в стратегии): проверка «торговать реже, но с запасом над издержками»
|
|
352
356
|
const needMult = Number(ex.needMult) > 0 ? Number(ex.needMult) : ENTRY_EDGE_MULT;
|
|
353
357
|
const cashRateAnnual = Number(ex.cashRateAnnual) > 0 ? Number(ex.cashRateAnnual) : 0;
|
|
354
|
-
const costOf = (i) => (ex.tierCost ? (
|
|
358
|
+
const costOf = (i) => (ex.tierCost ? (TIER1_LIQUID_SYMBOLS.has(tokens[i].symbol) ? 0.00065 : 0.00125) : cost);
|
|
355
359
|
// дивидендный предохранитель (правило стратегии, как в живом контуре; выключается только для сравнения)
|
|
356
360
|
const divGuard = ex.divGuard !== false;
|
|
357
361
|
// налог 13% с дивиденда по покупкам, удержанным через отсечку (цены в бэктесте скорректированы на полный дивиденд)
|
|
@@ -436,6 +440,7 @@ async function run(runId, p, state) {
|
|
|
436
440
|
}
|
|
437
441
|
const pr = preds[name].get(t);
|
|
438
442
|
const sg = sigmaAt.get(t);
|
|
443
|
+
const symMult = multAt.get(t) || baseSymMult;
|
|
439
444
|
const bear = isMarketDown(ds, t);
|
|
440
445
|
const mktDown = mktFilter && bear;
|
|
441
446
|
const barTs = tsOf(t);
|
package/server/security.js
CHANGED
|
@@ -33,7 +33,10 @@ export function isCrossOrigin(req) {
|
|
|
33
33
|
const origin = req.headers?.origin;
|
|
34
34
|
if (!origin || origin === 'null') return Boolean(origin);
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
const origHost = new URL(origin).host.toLowerCase();
|
|
37
|
+
const fwdHost = String(req.headers?.['x-forwarded-host'] || '').split(',')[0].trim().toLowerCase();
|
|
38
|
+
const host = String(req.headers?.host || '').toLowerCase();
|
|
39
|
+
return origHost !== host && (!fwdHost || origHost !== fwdHost);
|
|
37
40
|
} catch {
|
|
38
41
|
return true;
|
|
39
42
|
}
|