pump-anomaly 0.1.0
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/LICENSE +21 -0
- package/README.md +678 -0
- package/build/index.cjs +2755 -0
- package/build/index.mjs +2680 -0
- package/package.json +58 -0
- package/types.d.ts +1346 -0
package/types.d.ts
ADDED
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Контракты pump-matrix.
|
|
3
|
+
*
|
|
4
|
+
* ParserItem — совместим со схемой parser-items из backtest-ollama-crontab
|
|
5
|
+
* (поля direction/entry/targets/stoploss присутствуют в источнике, но детектору
|
|
6
|
+
* нужны только channel/symbol/direction/ts — остальное игнорируется).
|
|
7
|
+
*/
|
|
8
|
+
type Direction = "long" | "short";
|
|
9
|
+
/** Режим отбора входов. */
|
|
10
|
+
type DetectorMode = "auto" | "matrix" | "single";
|
|
11
|
+
/** Пороги жизнеспособности матрицы авторства (строгий критерий для auto-режима). */
|
|
12
|
+
interface ViabilityConfig {
|
|
13
|
+
minSharedEvents: number;
|
|
14
|
+
minPeakShare: number;
|
|
15
|
+
minStrongEdges: number;
|
|
16
|
+
minStructure: number;
|
|
17
|
+
}
|
|
18
|
+
/** Отчёт о жизнеспособности матрицы — почему auto выбрал matrix или single. */
|
|
19
|
+
interface ViabilityReport {
|
|
20
|
+
viable: boolean;
|
|
21
|
+
channels: number;
|
|
22
|
+
maxSharedEvents: number;
|
|
23
|
+
strongEdges: number;
|
|
24
|
+
multiChannelClusters: number;
|
|
25
|
+
clusterCount: number;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
/** Строка из коллекции parser-items (вход публичного API). */
|
|
29
|
+
interface ParserItem {
|
|
30
|
+
channel: string;
|
|
31
|
+
symbol: string;
|
|
32
|
+
direction: Direction;
|
|
33
|
+
/** unix-время публикации, мс. */
|
|
34
|
+
ts: number;
|
|
35
|
+
/** нижняя граница зоны входа (один пост уже двигает цену). */
|
|
36
|
+
entryFromPrice?: number;
|
|
37
|
+
/** верхняя граница зоны входа. */
|
|
38
|
+
entryToPrice?: number;
|
|
39
|
+
[extra: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** Нормализованное событие, с которым работают внутренние слои. */
|
|
42
|
+
interface SignalEvent {
|
|
43
|
+
channel: string;
|
|
44
|
+
symbol: string;
|
|
45
|
+
direction: Direction;
|
|
46
|
+
ts: number;
|
|
47
|
+
entryFromPrice?: number;
|
|
48
|
+
entryToPrice?: number;
|
|
49
|
+
/** идентификатор исходного parser-item — для сопоставления результата теста с парсингом */
|
|
50
|
+
id?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Вердикт по одному (symbol, direction). */
|
|
53
|
+
interface PumpVerdict {
|
|
54
|
+
symbol: string;
|
|
55
|
+
action: "open" | "skip";
|
|
56
|
+
direction: Direction | null;
|
|
57
|
+
ts: number;
|
|
58
|
+
independentClusters: number;
|
|
59
|
+
totalChannels: number;
|
|
60
|
+
confidence: number;
|
|
61
|
+
reason: string;
|
|
62
|
+
/** каким режимом получен сигнал: matrix (корреляция) или single (fallback) */
|
|
63
|
+
source: "matrix" | "single";
|
|
64
|
+
/** канал-источник (для single — конкретный пост; для matrix — null, межканальный) */
|
|
65
|
+
channel: string | null;
|
|
66
|
+
/** id якорного parser-item (для сопоставления live-сигнала с парсингом) */
|
|
67
|
+
id?: string;
|
|
68
|
+
/** id всех parser-item, вошедших в сигнал */
|
|
69
|
+
ids?: string[];
|
|
70
|
+
}
|
|
71
|
+
/** Карта авторства: канал → id кластера-автора. */
|
|
72
|
+
type AuthorMap = Map<string, number>;
|
|
73
|
+
/** Полный результат предсказания. */
|
|
74
|
+
interface PredictionResult {
|
|
75
|
+
/** Только action="open", отсортированы по confidence убыв. — то, ради чего всё. */
|
|
76
|
+
signals: PumpVerdict[];
|
|
77
|
+
/** Все вердикты, включая skip. */
|
|
78
|
+
verdicts: PumpVerdict[];
|
|
79
|
+
/** Карта склеенных каналов одного автора. */
|
|
80
|
+
authors: AuthorMap;
|
|
81
|
+
/** Сколько независимых авторов выявлено. */
|
|
82
|
+
authorCount: number;
|
|
83
|
+
/** Самооценённый характерный лаг между братскими каналами, мс. */
|
|
84
|
+
tauMs: number;
|
|
85
|
+
/** Итоговое окно синхронности всплеска, мс. */
|
|
86
|
+
windowMs: number;
|
|
87
|
+
/** Каким режимом фактически отработал детектор. */
|
|
88
|
+
usedMode: "matrix" | "single";
|
|
89
|
+
/** Оценка жизнеспособности матрицы (почему выбран режим в auto). */
|
|
90
|
+
viability: ViabilityReport;
|
|
91
|
+
}
|
|
92
|
+
interface DetectorConfig {
|
|
93
|
+
windowK: number;
|
|
94
|
+
minClusters: number;
|
|
95
|
+
jaccardThreshold: number;
|
|
96
|
+
lagPeakThreshold: number;
|
|
97
|
+
maxBurstWindowMs: number;
|
|
98
|
+
/** режим отбора входов: auto (по жизнеспособности матрицы) | matrix | single */
|
|
99
|
+
mode: DetectorMode;
|
|
100
|
+
/** переопределение порогов жизнеспособности матрицы (auto-режим) */
|
|
101
|
+
viability?: Partial<ViabilityConfig>;
|
|
102
|
+
/**
|
|
103
|
+
* Окно стационарности, мс: статистики (τ, author-матрица) считаются по локальному
|
|
104
|
+
* окну, а не по всей истории — защита от дрейфа режима на длинном горизонте.
|
|
105
|
+
* Infinity (по умолчанию) = вся история.
|
|
106
|
+
*/
|
|
107
|
+
stationarityWindowMs: number;
|
|
108
|
+
}
|
|
109
|
+
declare const DEFAULT_CONFIG: DetectorConfig;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Контракт источника свечей. Совместим с getCandles из backtest-kit.
|
|
113
|
+
* Тренировка идёт в прошлом (не realtime), поэтому look-ahead-ограничения сняты:
|
|
114
|
+
* свечи можно брать по обе стороны от события.
|
|
115
|
+
*/
|
|
116
|
+
type CandleInterval = "1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "1d";
|
|
117
|
+
interface ICandleData {
|
|
118
|
+
/** Unix ms, момент ОТКРЫТИЯ свечи. */
|
|
119
|
+
timestamp: number;
|
|
120
|
+
open: number;
|
|
121
|
+
high: number;
|
|
122
|
+
low: number;
|
|
123
|
+
close: number;
|
|
124
|
+
volume: number;
|
|
125
|
+
}
|
|
126
|
+
/** Длительность одного шага интервала в мс. */
|
|
127
|
+
declare const STEP_MS: Record<CandleInterval, number>;
|
|
128
|
+
/** Выравнивание timestamp вниз к границе свечи интервала. */
|
|
129
|
+
declare const alignTs: (t: number, interval: CandleInterval) => number;
|
|
130
|
+
/**
|
|
131
|
+
* Первая ПОЛНОСТЬЮ сформированная свеча, торгуемая БЕЗ look-ahead: если сигнал
|
|
132
|
+
* пришёл внутри минуты (ts > границы), свеча, СОДЕРЖАЩАЯ сигнал, ещё формируется —
|
|
133
|
+
* её close/high/low станут известны только в КОНЦЕ минуты, ПОСЛЕ сигнала. Входить
|
|
134
|
+
* в неё = заглядывать вперёд. Поэтому старт входа = следующая граница. Если сигнал
|
|
135
|
+
* ровно на границе (ts === aligned) — эта свеча открывается одновременно с сигналом
|
|
136
|
+
* и торгуема честно, не пропускаем.
|
|
137
|
+
*/
|
|
138
|
+
declare const entryStartTs: (t: number, interval: CandleInterval) => number;
|
|
139
|
+
/**
|
|
140
|
+
* Источник свечей. Семантика диапазонов (sDate inclusive, eDate exclusive):
|
|
141
|
+
* (limit) → [alignedWhen − limit·step, alignedWhen)
|
|
142
|
+
* (limit, sDate) → [align(sDate), align(sDate) + limit·step)
|
|
143
|
+
* (limit, _, eDate) → [align(eDate) − limit·step, eDate)
|
|
144
|
+
* (_, sDate, eDate) → [align(sDate), eDate), limit из диапазона
|
|
145
|
+
* (limit, sDate, eDate) → [align(sDate), …), ровно limit свечей
|
|
146
|
+
*/
|
|
147
|
+
type GetCandles = (symbol: string, interval: CandleInterval, limit?: number, sDate?: number, eDate?: number) => Promise<ICandleData[]>;
|
|
148
|
+
|
|
149
|
+
type Key = string;
|
|
150
|
+
interface EventTable {
|
|
151
|
+
/** все события, отсортированы по ts */
|
|
152
|
+
events: SignalEvent[];
|
|
153
|
+
/** события по (symbol,direction), каждая группа отсортирована по ts */
|
|
154
|
+
byKey: Map<Key, SignalEvent[]>;
|
|
155
|
+
/** `${channel}|${key}` → отсортированные ts */
|
|
156
|
+
byChannelKey: Map<string, number[]>;
|
|
157
|
+
/** список уникальных каналов */
|
|
158
|
+
channels: string[];
|
|
159
|
+
}
|
|
160
|
+
/** Нормализует сырой поток событий в индексированную таблицу. */
|
|
161
|
+
declare function buildTable(raw: SignalEvent[]): EventTable;
|
|
162
|
+
/**
|
|
163
|
+
* Окно стационарности. Статистики (τ, author-матрица, Jaccard) на длинном горизонте
|
|
164
|
+
* корраптятся: они агрегируются по ВСЕЙ истории, а за 5 месяцев режим дрейфует —
|
|
165
|
+
* каналы появляются/замолкают, «братские» пары распадаются, τ плывёт. Один глобальный
|
|
166
|
+
* набор усредняет несопоставимые периоды.
|
|
167
|
+
*
|
|
168
|
+
* Решение без новой математики: считать статистики только по локальному окну,
|
|
169
|
+
* заканчивающемуся в момент anchorTs. windowMs=Infinity → вся история (старое
|
|
170
|
+
* поведение, для коротких данных). Размер окна перебирается grid'ом в train.
|
|
171
|
+
*/
|
|
172
|
+
declare function windowEvents(events: SignalEvent[], anchorTs: number, windowMs: number): SignalEvent[];
|
|
173
|
+
/** Таблица, построенная по окну стационарности до anchorTs. */
|
|
174
|
+
declare function buildWindowedTable(events: SignalEvent[], anchorTs: number, windowMs: number): EventTable;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Слой 1 — самооценка характерного лага τ.
|
|
178
|
+
*
|
|
179
|
+
* Строит гистограмму всех попарных положительных задержек между РАЗНЫМИ каналами
|
|
180
|
+
* по совпадающим (symbol,direction). У случайных пар распределение ≈ плоское,
|
|
181
|
+
* у «братских» каналов — острый пик у малого лага. Модальный лог-бин даёт τ.
|
|
182
|
+
*
|
|
183
|
+
* Возвращает τ в мс, зажатый в [30с, 60мин]. Если данных мало — дефолт 15 мин.
|
|
184
|
+
*/
|
|
185
|
+
declare function selfTuneLag(tbl: EventTable): number;
|
|
186
|
+
|
|
187
|
+
interface Edge {
|
|
188
|
+
a: string;
|
|
189
|
+
b: string;
|
|
190
|
+
jaccard: number;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Близость двух каналов по скользящему окну (сырой ts, без бакетизации).
|
|
194
|
+
* Доля событий по общим (symbol,direction), у которых нашёлся партнёр у другого
|
|
195
|
+
* канала в пределах |Δ| ≤ window. Симметризованный Jaccard.
|
|
196
|
+
*/
|
|
197
|
+
declare function jaccardPair(tbl: EventTable, a: string, b: string, window: number): number;
|
|
198
|
+
/** Слой 2 — грубое сито: все пары каналов с Jaccard ≥ threshold. */
|
|
199
|
+
declare function jaccardScreen(tbl: EventTable, window: number, threshold: number): Edge[];
|
|
200
|
+
|
|
201
|
+
interface DirectedEdge extends Edge {
|
|
202
|
+
/** модальная |задержка|, мс */
|
|
203
|
+
lag: number;
|
|
204
|
+
/** доля задержек в окне остроты пика (0..1) */
|
|
205
|
+
peakShare: number;
|
|
206
|
+
/** инициатор */
|
|
207
|
+
leader: string;
|
|
208
|
+
/** ведомый */
|
|
209
|
+
follower: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Слой 3 — лаговая кросс-корреляция точечных процессов.
|
|
213
|
+
*
|
|
214
|
+
* Для каждой пары-кандидата собирает знаковые задержки Δ = t_b − t_a между
|
|
215
|
+
* ближайшими событиями по общим (symbol,direction). Узкий смещённый пик ⇒
|
|
216
|
+
* братские каналы одного автора; размазанный фон ⇒ совпадение, ребро отбрасывается.
|
|
217
|
+
*
|
|
218
|
+
* Острота пика меряется по peakWindow (= windowK·τ, окно сита), НЕ по голому τ:
|
|
219
|
+
* иначе брат с лагом чуть больше τ ложно выпадает и пара рвётся.
|
|
220
|
+
*/
|
|
221
|
+
declare function lagXCorr(tbl: EventTable, edges: Edge[], peakThreshold: number, peakWindow: number): DirectedEdge[];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Слой 4 — кластеризация каналов в авторов (union-find / connected components).
|
|
225
|
+
* Каждое направленное ребро «братства» сливает два канала в один кластер.
|
|
226
|
+
* Возвращает карту channel → целочисленный id кластера.
|
|
227
|
+
*/
|
|
228
|
+
declare function clusterAuthors(channels: string[], edges: DirectedEdge[]): AuthorMap;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Слой 5 — early-warning по НЕЗАВИСИМЫМ кластерам-авторам.
|
|
232
|
+
*
|
|
233
|
+
* Для каждого (symbol,direction) скользящим окном считает плотность не каналов,
|
|
234
|
+
* а РАЗНЫХ кластеров. Всплеск из N каналов одного автора → 1 кластер → skip.
|
|
235
|
+
* Всплеск из ≥ minClusters независимых кластеров → open.
|
|
236
|
+
*
|
|
237
|
+
* confidence = dedup × fill, где
|
|
238
|
+
* dedup = clusters/channels (1 = все источники независимы, <1 = есть дубли автора)
|
|
239
|
+
* fill = насыщенность окна относительно minClusters·2 (растёт с числом источников)
|
|
240
|
+
*/
|
|
241
|
+
declare function earlyWarning(tbl: EventTable, clusterOf: AuthorMap, cfg: DetectorConfig, tau: number): PumpVerdict[];
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Single-channel fallback. Когда корреляция недоступна (один канал / mode="single"),
|
|
245
|
+
* матрица авторства пуста и earlyWarning молчит. Но даже один пост двигает рынок:
|
|
246
|
+
* аудитория входит, возникает краткосрочный импульс. Поэтому здесь КАЖДЫЙ пост =
|
|
247
|
+
* сигнал к входу, а вся ответственность за результат — на обученном exit
|
|
248
|
+
* (trailing take / hard stop / staleness / импакт-горизонт), который уже доказал,
|
|
249
|
+
* что отделяет памп от stop hunt.
|
|
250
|
+
*
|
|
251
|
+
* Дедупликация: несколько постов по одному (symbol,direction) в пределах окна
|
|
252
|
+
* схлопываются в один вход (повторный пост в активную позицию — не новый вход).
|
|
253
|
+
*/
|
|
254
|
+
declare function singleChannelSignals(tbl: EventTable, cfg: DetectorConfig, tau: number): PumpVerdict[];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Жизнеспособность матрицы авторства. Отвечает на вопрос «достаточно ли в данных
|
|
258
|
+
* структуры, чтобы доверять корреляции», а НЕ «выдала ли матрица хоть что-то».
|
|
259
|
+
*
|
|
260
|
+
* Без этого auto оставался бы в matrix даже на двух каналах со ШУМОВЫМ совпадением
|
|
261
|
+
* (Jaccard случайно перевалил порог на 1-2 событиях) и выдавал бы ложный сигнал.
|
|
262
|
+
* Строгий критерий: матрица годна только при ЯВНЫХ кластерах И достаточном
|
|
263
|
+
* событийном перекрытии; иначе — откат в single.
|
|
264
|
+
*/
|
|
265
|
+
declare const DEFAULT_VIABILITY: ViabilityConfig;
|
|
266
|
+
declare function assessViability(tbl: EventTable, directed: DirectedEdge[], authors: AuthorMap, cfg?: ViabilityConfig): ViabilityReport;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Объёмная математика детектора каскада ликвидаций. ПОЛНОСТЬЮ СИММЕТРИЧНА по
|
|
270
|
+
* направлению — long-trap и short-trap это зеркала одного механизма:
|
|
271
|
+
*
|
|
272
|
+
* short-squeeze: толпа шортит на плече → стена ликвидаций СВЕРХУ → каскад
|
|
273
|
+
* форсированных buy толкает вверх (против short).
|
|
274
|
+
* long-cascade: толпа лонгует на плече → стена ликвидаций СНИЗУ → каскад
|
|
275
|
+
* форсированных sell толкает вниз (против long).
|
|
276
|
+
*
|
|
277
|
+
* Отличить ловушку от честного движения: при каскаде объём растёт на свечах,
|
|
278
|
+
* где цена идёт ПРОТИВ позиции (ликвидации — форсированные сделки против толпы).
|
|
279
|
+
* При честном движении объём растёт В СТОРОНУ позиции. Знак «против» определяется
|
|
280
|
+
* через направление, поэтому формула одна на оба случая.
|
|
281
|
+
*/
|
|
282
|
+
interface VolumeFeatures {
|
|
283
|
+
/** z-score объёма входной свечи против базлайна до входа: накопление плечевого топлива */
|
|
284
|
+
volZ: number;
|
|
285
|
+
/** доля объёма на движениях ПРОТИВ позиции в окне после входа (0..1): сигнатура каскада */
|
|
286
|
+
squeezePressure: number;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* volZ: насколько объём входной свечи аномален против скользящего окна ДО входа.
|
|
290
|
+
* Высокий volZ = синхронный заход толпы в плечо (та самая «синяя свеча» из 1028592).
|
|
291
|
+
* baselineWindow — сколько свечей до входа берём за норму.
|
|
292
|
+
*/
|
|
293
|
+
declare function volumeZScore(candles: ICandleData[], entryIdx: number, baselineWindow: number): number;
|
|
294
|
+
/**
|
|
295
|
+
* squeezePressure: доля объёма в окне после входа, пришедшегося на свечи, где цена
|
|
296
|
+
* двигалась ПРОТИВ позиции. Симметрично: для long «против» = свеча закрылась ниже
|
|
297
|
+
* открытия (давление вниз, каскад sell); для short «против» = выше (каскад buy).
|
|
298
|
+
*
|
|
299
|
+
* Высокое значение → движение питается ликвидациями толпы, а не честным потоком →
|
|
300
|
+
* это ловушка (stop hunt / squeeze), входить опасно либо выходить раньше.
|
|
301
|
+
*/
|
|
302
|
+
declare function squeezePressure(candles: ICandleData[], entryIdx: number, dir: Direction, horizon: number): number;
|
|
303
|
+
/** Считает оба признака разом для входа на entryIdx. */
|
|
304
|
+
declare function volumeFeatures(candles: ICandleData[], entryIdx: number, dir: Direction, baselineWindow: number, horizon: number): VolumeFeatures;
|
|
305
|
+
/** Режим объёма по порогу volZ: спокойный или аномальный (топливо накоплено). */
|
|
306
|
+
type VolRegime = "calm" | "anomalous";
|
|
307
|
+
declare const volRegimeOf: (volZ: number, threshold: number) => VolRegime;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Точная симуляция prod-выхода по минутным свечам (listenActivePing на закрытии
|
|
311
|
+
* каждой 1m-свечи). Метка обучения = то, что реально снимет твой выход, а не
|
|
312
|
+
* close-to-close. Так stop hunting отсекается: прокол не дотягивает до trailingTake,
|
|
313
|
+
* а откат бьёт hard stop → отрицательная метка, даже если close[t+H] положительный.
|
|
314
|
+
*
|
|
315
|
+
* moonbag (long) — hard stop НИЖЕ входа.
|
|
316
|
+
* gravebag (short) — hard stop ВЫШЕ входа.
|
|
317
|
+
*/
|
|
318
|
+
interface ExitParams {
|
|
319
|
+
/** trailing take: откат от пикового PnL%, при currentProfit ≥ 0 → выход */
|
|
320
|
+
trailingTake: number;
|
|
321
|
+
/** hard stop: фикса % от входа против позиции */
|
|
322
|
+
hardStop: number;
|
|
323
|
+
/** peak staleness: пик должен достичь этого PnL%, чтобы таймер протухания включился */
|
|
324
|
+
stalenessSinceProfit: number;
|
|
325
|
+
/** peak staleness: минут без нового пика → выход */
|
|
326
|
+
stalenessSinceMinutes: number;
|
|
327
|
+
/** потолок жизни позиции в минутных свечах (эмпирически подбираемый импакт-горизонт) */
|
|
328
|
+
staleMinutes: number;
|
|
329
|
+
/** baseline-окно для volZ (свечей до входа); если не задано — volZ не считается */
|
|
330
|
+
volBaselineWindow?: number;
|
|
331
|
+
/** порог volZ для разметки режима calm/anomalous */
|
|
332
|
+
volZThreshold?: number;
|
|
333
|
+
/** политика реакции на каскад: tighten (туже trailing) | veto (не входить) | none */
|
|
334
|
+
squeezePolicy?: "none" | "tighten" | "veto" | "invert" | "ignore";
|
|
335
|
+
/** порог squeezePressure, выше которого срабатывает policy */
|
|
336
|
+
squeezeThreshold?: number;
|
|
337
|
+
/** множитель ужатия trailing при policy="tighten" (0.5 = вдвое туже) */
|
|
338
|
+
tightenFactor?: number;
|
|
339
|
+
/**
|
|
340
|
+
* Окно детекции каскада ликвидаций в минутах — НЕЗАВИСИМО от staleMinutes.
|
|
341
|
+
* Сквиз/каскад это БЫСТРОЕ событие (минуты), его нельзя мерить на 24ч-горизонте
|
|
342
|
+
* удержания: длинное окно размывает резкий разворот. Раньше брался staleMinutes,
|
|
343
|
+
* что связывало два несвязанных концерна (жизнь позиции и окно детектора).
|
|
344
|
+
* Если не задано — fallback на staleMinutes (обратная совместимость).
|
|
345
|
+
*/
|
|
346
|
+
cascadeWindowMinutes?: number;
|
|
347
|
+
}
|
|
348
|
+
type ExitReason = "trailing-take" | "hard-stop" | "peak-staleness" | "life-cap" | "cascade-veto" | "invert" | "no-entry";
|
|
349
|
+
interface ReplayResult {
|
|
350
|
+
/** реализованный PnL% (в долях: 0.05 = +5%). При hard-stop = -hardStop% (честный убыток). */
|
|
351
|
+
pnl: number;
|
|
352
|
+
reason: ExitReason;
|
|
353
|
+
/** пиковый PnL% за жизнь позиции */
|
|
354
|
+
peak: number;
|
|
355
|
+
/** минут от входа до выхода */
|
|
356
|
+
heldMinutes: number;
|
|
357
|
+
entered: boolean;
|
|
358
|
+
/** цена входа (close в зоне либо clamp midpoint). 0 если не вошли. */
|
|
359
|
+
entryPrice: number;
|
|
360
|
+
/** цена выхода, по которой реализован pnl. 0 если не вошли. */
|
|
361
|
+
exitPrice: number;
|
|
362
|
+
/** z-score объёма входной свечи (накопление плечевого топлива) */
|
|
363
|
+
volZ: number;
|
|
364
|
+
/** доля объёма против позиции (сигнатура каскада ликвидаций) */
|
|
365
|
+
squeezePressure: number;
|
|
366
|
+
/** режим объёма на входе: calm | anomalous */
|
|
367
|
+
volRegime: VolRegime;
|
|
368
|
+
/** была ли позиция инвертирована (policy=invert сработал) */
|
|
369
|
+
inverted: boolean;
|
|
370
|
+
/** замер горизонта неполный: после входа не хватило свечей на полный life-cap */
|
|
371
|
+
truncated: boolean;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Прогоняет 1m-свечи через prod-выход. candles должны быть отсортированы по ts
|
|
375
|
+
* и покрывать окно от события вперёд (минимум до staleMinutes).
|
|
376
|
+
*
|
|
377
|
+
* entryFrom/entryTo — зона входа: вход на первой свече, чей хвост пересекает зону.
|
|
378
|
+
* entryPrice = close, если он попал в зону, иначе clamp midpoint к [low,high].
|
|
379
|
+
* Цена входа = кламп середины зоны в диапазон свечи (консервативно — фактическое касание).
|
|
380
|
+
*/
|
|
381
|
+
declare function replayExit(candles: ICandleData[], dir: Direction, entryFrom: number, entryTo: number, p: ExitParams): ReplayResult;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Tensor exit-параметров: mode → channel → symbol → direction → volRegime → ExitParams.
|
|
385
|
+
*
|
|
386
|
+
* Математика выхода НЕ смешивается между источниками: каждая ячейка обучается
|
|
387
|
+
* только на своих replay-результатах. Каскад ликвидаций симметричен, но long-trap
|
|
388
|
+
* и short-trap получают РАЗНЫЕ ячейки (разная динамика разворота), и режим объёма
|
|
389
|
+
* (calm/anomalous) тоже разделён — short в аномальном объёме это накопленное
|
|
390
|
+
* топливо для сквиза, exit там должен быть туже.
|
|
391
|
+
*
|
|
392
|
+
* Иерархический fallback при пустой ячейке:
|
|
393
|
+
* [mode][channel][symbol][direction][volRegime]
|
|
394
|
+
* → схлопнуть volRegime: [mode][channel][symbol][direction]
|
|
395
|
+
* → схлопнуть direction: [mode][channel][symbol]
|
|
396
|
+
* → [mode] → global
|
|
397
|
+
*/
|
|
398
|
+
type RegimeCell = Partial<Record<VolRegime, ExitParams>>;
|
|
399
|
+
type DirCell = Partial<Record<Direction, RegimeCell>>;
|
|
400
|
+
type SymbolCell = Record<string, DirCell>;
|
|
401
|
+
type ChannelCell = Record<string, SymbolCell>;
|
|
402
|
+
interface ExitTensor {
|
|
403
|
+
cells: {
|
|
404
|
+
matrix: ChannelCell;
|
|
405
|
+
single: ChannelCell;
|
|
406
|
+
};
|
|
407
|
+
/** уровень символа+направления (схлопнут volRegime) */
|
|
408
|
+
bySymbolDir: {
|
|
409
|
+
matrix: Record<string, Partial<Record<Direction, ExitParams>>>;
|
|
410
|
+
single: Record<string, Partial<Record<Direction, ExitParams>>>;
|
|
411
|
+
};
|
|
412
|
+
/** уровень режима (схлопнуты канал/символ/направление) */
|
|
413
|
+
byMode: {
|
|
414
|
+
matrix: ExitParams;
|
|
415
|
+
single: ExitParams;
|
|
416
|
+
};
|
|
417
|
+
/** корень дерева */
|
|
418
|
+
global: ExitParams;
|
|
419
|
+
}
|
|
420
|
+
type ResolveSource = "cell" | "symbol-dir" | "mode" | "global";
|
|
421
|
+
interface ResolvedExit {
|
|
422
|
+
exit: ExitParams;
|
|
423
|
+
source: ResolveSource;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Иерархический резолвер. Возвращает exit + уровень, с которого он разрешён,
|
|
427
|
+
* чтобы прод видел, обучен ли он персонально под (канал,символ,направление,режим)
|
|
428
|
+
* или это fallback.
|
|
429
|
+
*/
|
|
430
|
+
declare function resolveExit(tensor: ExitTensor, mode: "matrix" | "single", channel: string, symbol: string, direction: Direction, volRegime: VolRegime): ResolvedExit;
|
|
431
|
+
/**
|
|
432
|
+
* Резолв БЕЗ volRegime (свечей нет): пропускаем cell-уровень (требует режима),
|
|
433
|
+
* начинаем с symbol-dir → mode → global.
|
|
434
|
+
*/
|
|
435
|
+
declare function resolveExitNoRegime(tensor: ExitTensor, mode: "matrix" | "single", symbol: string, direction: Direction): ResolvedExit;
|
|
436
|
+
|
|
437
|
+
/** Кандидат-всплеск без применённого порога minClusters — для переиспользования в grid. */
|
|
438
|
+
interface CandidateBurst {
|
|
439
|
+
symbol: string;
|
|
440
|
+
direction: Direction;
|
|
441
|
+
ts: number;
|
|
442
|
+
independentClusters: number;
|
|
443
|
+
totalChannels: number;
|
|
444
|
+
confidence: number;
|
|
445
|
+
/** id якорного (последнего в окне) события — для сопоставления с парсингом */
|
|
446
|
+
id?: string;
|
|
447
|
+
/** id ВСЕХ событий, вошедших во всплеск (в matrix может быть несколько) */
|
|
448
|
+
ids?: string[];
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Перечисляет ВСЕ всплески при заданных (windowK, jaccardThreshold, lagPeakThreshold),
|
|
452
|
+
* НЕ отсекая по minClusters — это делает grid дёшево поверх готового списка.
|
|
453
|
+
* Кластеризация зависит от jaccard/lag/windowK, поэтому пересчитывается на эти оси grid;
|
|
454
|
+
* а minClusters — пост-фильтр, его перебор бесплатный.
|
|
455
|
+
*/
|
|
456
|
+
declare function enumerateBursts(items: ParserItem[] | SignalEvent[], windowK: number, jaccardThreshold: number, lagPeakThreshold: number, maxBurstWindowMs: number, stationarityWindowMs?: number): CandidateBurst[];
|
|
457
|
+
/**
|
|
458
|
+
* Перечисляет КАЖДЫЙ пост как кандидата (single-channel fallback), схлопывая
|
|
459
|
+
* близкие посты по одному (symbol,direction) в пределах окна в один вход.
|
|
460
|
+
* independentClusters=1 всегда — фильтра качества нет, исход решает exit.
|
|
461
|
+
*/
|
|
462
|
+
declare function enumeratePosts(items: ParserItem[] | SignalEvent[], windowK: number, maxBurstWindowMs: number): CandidateBurst[];
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Размеченный всплеск: реализованный PnL по prod-выходу для каждого набора
|
|
466
|
+
* exit-параметров. Метку ставит симуляция твоего trailing/hard-stop по 1m-свечам,
|
|
467
|
+
* а не close-to-close — поэтому stop hunting получает отрицательную метку.
|
|
468
|
+
*/
|
|
469
|
+
interface LabeledBurst {
|
|
470
|
+
symbol: string;
|
|
471
|
+
direction: Direction;
|
|
472
|
+
ts: number;
|
|
473
|
+
/** ключ exit-набора → результат replay */
|
|
474
|
+
byExit: Map<string, ReplayResult>;
|
|
475
|
+
}
|
|
476
|
+
/** Стабильный строковый ключ exit-набора для кэша/grid. */
|
|
477
|
+
declare const exitKey: (p: ExitParams) => string;
|
|
478
|
+
/**
|
|
479
|
+
* Достаёт 1m-свечи от события вперёд на покрытие максимального life-cap и
|
|
480
|
+
* прогоняет каждый exit-набор через replay. Зона входа берётся из события;
|
|
481
|
+
* если не задана — точка entryFrom=entryTo=open первой свечи.
|
|
482
|
+
*/
|
|
483
|
+
declare function labelBurst(getCandles: GetCandles, symbol: string, direction: Direction, ts: number, exitSets: ExitParams[], entryFromPrice?: number, entryToPrice?: number): Promise<LabeledBurst | null>;
|
|
484
|
+
|
|
485
|
+
/** Максимум свечей в одном чанке (как CC_MAX_CANDLES_PER_REQUEST в проде). */
|
|
486
|
+
declare const MAX_CANDLES_PER_CHUNK = 500;
|
|
487
|
+
/**
|
|
488
|
+
* Chunked-загрузчик свечей. Дублирует логику пагинации из prod-адаптера: если
|
|
489
|
+
* запрошено больше MAX_CANDLES_PER_CHUNK, бьёт на чанки, двигая since вперёд на
|
|
490
|
+
* chunkLimit·step, и склеивает с дедупликацией по timestamp.
|
|
491
|
+
*
|
|
492
|
+
* Зачем внутри либы: labelBurst под длинный импакт-горизонт (staleMinutes до 1440)
|
|
493
|
+
* просит staleMinutes·2+5 ≈ 2885 свечей. Если адаптер пагинацию НЕ делает сам и
|
|
494
|
+
* упирается в лимит биржи, либа должна разрулить это сама, а не зависеть от того,
|
|
495
|
+
* как реализован чужой getCandles.
|
|
496
|
+
*
|
|
497
|
+
* Семантика — forward от since (case sDate+limit): возвращает ровно столько свечей,
|
|
498
|
+
* сколько доступно, начиная с align(since). Если адаптер на каком-то чанке вернул
|
|
499
|
+
* пусто (край истории / дыра) — останавливаемся и отдаём, что собрали.
|
|
500
|
+
*/
|
|
501
|
+
declare function fetchCandlesChunked(getCandles: GetCandles, symbol: string, interval: CandleInterval, limit: number, since: number, chunkSize?: number): Promise<ICandleData[]>;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Objective для подбора порогов: shrinkage-expectancy.
|
|
505
|
+
*
|
|
506
|
+
* score = mean(returns) · N/(N+k)
|
|
507
|
+
*
|
|
508
|
+
* Средний forward-return отобранных всплесков, усаженный к нулю при малой выборке.
|
|
509
|
+
* Без усадки grid выбрал бы вырожденный порог, ловящий 1 жирный всплеск и
|
|
510
|
+
* рапортующий «идеальный эдж» — ровно ловушка winrate-68%-с-чёрным-лебедем.
|
|
511
|
+
* k — сила усадки (по умолчанию 5): при N=k вклад режется вдвое.
|
|
512
|
+
*/
|
|
513
|
+
declare function shrinkageExpectancy(returns: number[], k?: number): number;
|
|
514
|
+
/** Доля положительных (winrate) — для отчёта, не для оптимизации. */
|
|
515
|
+
declare function winrate(returns: number[]): number;
|
|
516
|
+
/**
|
|
517
|
+
* Стандартная ошибка среднего по фолдам: SE = std(foldScores) / sqrt(n).
|
|
518
|
+
* std — выборочное (делитель n-1). При n<2 SE=0 (разброс не оценить).
|
|
519
|
+
*/
|
|
520
|
+
declare function standardError(foldScores: number[]): number;
|
|
521
|
+
/**
|
|
522
|
+
* One-standard-error rule (Breiman 1984) — против winner's curse при grid-search.
|
|
523
|
+
*
|
|
524
|
+
* Проблема: argmax по CV-score из N конфигураций систематически завышен — максимум
|
|
525
|
+
* шумных оценок смещён вверх на ~sigma·sqrt(2·ln N) даже при истинном edge=0. Чем
|
|
526
|
+
* больше grid, тем сильнее переобучение на шум выборки.
|
|
527
|
+
*
|
|
528
|
+
* Правило: берём НЕ максимум, а самую КОНСЕРВАТИВНУЮ конфигурацию среди тех, чей
|
|
529
|
+
* score в пределах 1 SE от максимума. Разница внутри 1 SE статистически незначима
|
|
530
|
+
* (внутри шума), поэтому вместо счастливого выброса выбираем робастную конфигурацию.
|
|
531
|
+
*
|
|
532
|
+
* @param entries кандидаты
|
|
533
|
+
* @param scoreOf извлечь CV-score кандидата
|
|
534
|
+
* @param foldsOf извлечь fold-scores кандидата (для SE максимума)
|
|
535
|
+
* @param isSimpler компаратор «a консервативнее b» (true → предпочесть a)
|
|
536
|
+
*/
|
|
537
|
+
declare function oneStandardErrorSelect<T>(entries: T[], scoreOf: (e: T) => number, foldsOf: (e: T) => number[], isSimpler: (a: T, b: T) => boolean, seMultiplier?: number): T | null;
|
|
538
|
+
/**
|
|
539
|
+
* Перцентиль p (0..1) по выборке методом линейной интерполяции (type-7, как в numpy).
|
|
540
|
+
* percentile([...], 0.95) = P95. Пустая выборка → 0.
|
|
541
|
+
*/
|
|
542
|
+
declare function percentile(xs: number[], p: number): number;
|
|
543
|
+
/** Статистика risk-reward по набору сделок. */
|
|
544
|
+
interface RiskRewardStats {
|
|
545
|
+
/** среднее RR */
|
|
546
|
+
mean: number;
|
|
547
|
+
/** P95 RR (хвост в плюс) */
|
|
548
|
+
p95: number;
|
|
549
|
+
/** P99 RR */
|
|
550
|
+
p99: number;
|
|
551
|
+
/** число сделок в выборке */
|
|
552
|
+
n: number;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* RR на сделку = pnl / hardStop (реализованный в единицах риска — сколько R сняли).
|
|
556
|
+
* Считает mean / P95 / P99 по парам (pnl, hardStop). Сделки с hardStop ≤ 0
|
|
557
|
+
* пропускаются (деление на ноль). Главный исследовательский выход бэктеста.
|
|
558
|
+
*/
|
|
559
|
+
declare function riskRewardStats(trades: Array<{
|
|
560
|
+
pnl: number;
|
|
561
|
+
hardStop: number;
|
|
562
|
+
}>): RiskRewardStats;
|
|
563
|
+
/**
|
|
564
|
+
* Устойчивая к выбросам статистика реализованного PnL системы (в долях).
|
|
565
|
+
* Дополняет mean процентилями и медианой, чтобы ОДНА плохая (или одна жирная)
|
|
566
|
+
* сделка не определяла оценку выигрыша:
|
|
567
|
+
* - median — робастный центр, полностью иммунный к выбросам (50-й перцентиль);
|
|
568
|
+
* - p5 — нижний хвост (насколько плохи худшие 5% сделок);
|
|
569
|
+
* - p95/p99— верхний хвост (вклад редких крупных выигрышей).
|
|
570
|
+
* mean остаётся для сравнения, но median/перцентили показывают систему без
|
|
571
|
+
* искажения единичными экстремумами. NaN/Infinity отбрасываются.
|
|
572
|
+
*/
|
|
573
|
+
interface PnlStats {
|
|
574
|
+
/** среднее PnL (чувствительно к выбросам — для сравнения) */
|
|
575
|
+
mean: number;
|
|
576
|
+
/** медиана PnL (робастный центр, иммунный к выбросам) */
|
|
577
|
+
median: number;
|
|
578
|
+
/** P5 — нижний хвост (худшие сделки) */
|
|
579
|
+
p5: number;
|
|
580
|
+
/** P95 — верхний хвост */
|
|
581
|
+
p95: number;
|
|
582
|
+
/** P99 — крайний верхний хвост */
|
|
583
|
+
p99: number;
|
|
584
|
+
/** число сделок в выборке */
|
|
585
|
+
n: number;
|
|
586
|
+
}
|
|
587
|
+
declare function pnlStats(pnls: number[]): PnlStats;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Достоверность обучения. Отвечает на вопрос «можно ли доверять подобранным
|
|
591
|
+
* порогам», а НЕ «велик ли эдж». На малой выборке confidence низкий и
|
|
592
|
+
* reliable=false (либа работает, но честно предупреждает); по мере роста
|
|
593
|
+
* данных все три оси растут → confidence→1, reliable переключается сам.
|
|
594
|
+
*
|
|
595
|
+
* confidence = support × stability × significance (каждое в [0,1])
|
|
596
|
+
*
|
|
597
|
+
* Менять код при росте выборки не нужно — формула пересчитывает доверие.
|
|
598
|
+
*/
|
|
599
|
+
interface ReliabilityInput {
|
|
600
|
+
/** per-fold средние forward-return на валидации */
|
|
601
|
+
foldMeans: number[];
|
|
602
|
+
/** per-fold размеры валидационных выборок */
|
|
603
|
+
foldSizes: number[];
|
|
604
|
+
/** все валидационные ретёрны (для значимости против нуля) */
|
|
605
|
+
allReturns: number[];
|
|
606
|
+
}
|
|
607
|
+
interface Reliability {
|
|
608
|
+
confidence: number;
|
|
609
|
+
reliable: boolean;
|
|
610
|
+
support: number;
|
|
611
|
+
stability: number;
|
|
612
|
+
significance: number;
|
|
613
|
+
totalN: number;
|
|
614
|
+
}
|
|
615
|
+
interface ReliabilityConfig {
|
|
616
|
+
/** при N=supportK вклад объёма ≈ 0.5 */
|
|
617
|
+
supportK: number;
|
|
618
|
+
/** порог confidence для reliable=true */
|
|
619
|
+
confidenceThreshold: number;
|
|
620
|
+
/** минимум суммарных сделок для reliable=true */
|
|
621
|
+
minN: number;
|
|
622
|
+
}
|
|
623
|
+
declare const DEFAULT_RELIABILITY: ReliabilityConfig;
|
|
624
|
+
declare function computeReliability(input: ReliabilityInput, cfg?: ReliabilityConfig): Reliability;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Прогрессбар обучения. Train делает вложенные циклы: фаза разметки (медленная,
|
|
628
|
+
* каждый кандидат = await getCandles по 1m-свечам) и фаза grid-скоринга (быстрая,
|
|
629
|
+
* чистый CPU по кэшу). Бар отражает РЕАЛЬНУЮ работу — тики разметки, где идёт IO.
|
|
630
|
+
*
|
|
631
|
+
* Передаётся в train как опция onProgress; по умолчанию пишет в stdout в стиле,
|
|
632
|
+
* заданном пользователем. В тестах подменяется на no-op или сборщик, чтобы не
|
|
633
|
+
* засорять вывод.
|
|
634
|
+
*/
|
|
635
|
+
interface ProgressEvent {
|
|
636
|
+
/** сколько единиц обработано */
|
|
637
|
+
done: number;
|
|
638
|
+
/** всего единиц в текущей фазе */
|
|
639
|
+
total: number;
|
|
640
|
+
/** метка фазы: "label" (разметка свечами) | "score" (grid-скоринг) */
|
|
641
|
+
phase: "label" | "score" | "nested";
|
|
642
|
+
/** что сейчас обрабатывается (символ/ключ кластеризации) — для контекста */
|
|
643
|
+
label: string;
|
|
644
|
+
}
|
|
645
|
+
type ProgressFn = (e: ProgressEvent) => void;
|
|
646
|
+
/** Дефолтный stdout-бар в стиле пользователя. */
|
|
647
|
+
declare const stdoutProgress: ProgressFn;
|
|
648
|
+
/** No-op для тестов/тихого режима. */
|
|
649
|
+
declare const silentProgress: ProgressFn;
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* ЕДИНЫЙ СТАБИЛЬНЫЙ КОНТРАКТ ВЫВОДА.
|
|
653
|
+
*
|
|
654
|
+
* Один сигнал = одно исполняемое решение. Никаких optional-флагов, размазывающих
|
|
655
|
+
* состояние по объекту (было: inverted + originalDirection + recommendation —
|
|
656
|
+
* три поля про одно решение). Прод читает плоскую исполняемую часть и не думает.
|
|
657
|
+
*
|
|
658
|
+
* Что исполнять — всегда валидно (symbol, direction, exit). Происхождение — в
|
|
659
|
+
* одном вложенном `origin`, для аудита, не для ветвления в прикладном коде.
|
|
660
|
+
*/
|
|
661
|
+
/** Что это за исход — единственный дискриминатор. */
|
|
662
|
+
type SignalAction = "enter" | "invert" | "tighten";
|
|
663
|
+
/** Плоский исполняемый exit-план. Готов к передаче в openPosition без доработки. */
|
|
664
|
+
interface ExitPlan {
|
|
665
|
+
/** trailing take %, откат от пика PnL (уже ужат, если action="tighten") */
|
|
666
|
+
trailingTake: number;
|
|
667
|
+
/** hard stop %, фикса от входа */
|
|
668
|
+
hardStop: number;
|
|
669
|
+
/** через сколько минут пост теряет импакт (эмпирический потолок жизни) */
|
|
670
|
+
impactHorizonMinutes: number;
|
|
671
|
+
/** пик-протухание: порог прибыли % */
|
|
672
|
+
stalenessSinceProfit: number;
|
|
673
|
+
/** пик-протухание: минут без нового пика */
|
|
674
|
+
stalenessSinceMinutes: number;
|
|
675
|
+
}
|
|
676
|
+
/** Происхождение сигнала — единый вложенный объект, не флаги. Для аудита. */
|
|
677
|
+
interface SignalOrigin {
|
|
678
|
+
/** режим детектора: matrix (корреляция авторов) | single (fallback на пост) */
|
|
679
|
+
detector: "matrix" | "single";
|
|
680
|
+
/** канал-источник (single) или null (matrix — межканальный) */
|
|
681
|
+
channel: string | null;
|
|
682
|
+
/**
|
|
683
|
+
* Если сигнал инвертирован — исходное направление поста (а direction уже развёрнут).
|
|
684
|
+
* null = инверсии не было. Это НЕ дублирует direction: direction = что исполнять,
|
|
685
|
+
* invertedFrom = что говорил канал. Чтение не обязательно для исполнения.
|
|
686
|
+
*/
|
|
687
|
+
invertedFrom: Direction | null;
|
|
688
|
+
/** с какого уровня тензора разрешён exit: cell | symbol-dir | mode | global */
|
|
689
|
+
exitSource: "cell" | "symbol-dir" | "mode" | "global";
|
|
690
|
+
/** режим объёма на входе (если считался из свечей): calm | anomalous | null */
|
|
691
|
+
volRegime: "calm" | "anomalous" | null;
|
|
692
|
+
/** острота всплеска (из детектора) */
|
|
693
|
+
confidence: number;
|
|
694
|
+
/** число независимых кластеров-авторов */
|
|
695
|
+
independentClusters: number;
|
|
696
|
+
/** доверие к модели на момент обучения (0..1) */
|
|
697
|
+
modelConfidence: number;
|
|
698
|
+
/** надёжна ли модель (хватило ли данных) */
|
|
699
|
+
modelReliable: boolean;
|
|
700
|
+
/** id якорного parser-item — для сопоставления live-сигнала с парсингом */
|
|
701
|
+
id?: string;
|
|
702
|
+
/** id всех parser-item, вошедших в сигнал */
|
|
703
|
+
ids?: string[];
|
|
704
|
+
}
|
|
705
|
+
/** Единый исполняемый сигнал. Прод читает плоскую часть, origin — для аудита. */
|
|
706
|
+
interface TradeSignal {
|
|
707
|
+
symbol: string;
|
|
708
|
+
/** ИТОГОВОЕ направление к исполнению (при инверсии — уже развёрнутое против поста) */
|
|
709
|
+
direction: Direction;
|
|
710
|
+
/** что это за исход */
|
|
711
|
+
action: SignalAction;
|
|
712
|
+
/** unix-время сигнала, мс */
|
|
713
|
+
ts: number;
|
|
714
|
+
/** готовый exit-план */
|
|
715
|
+
exit: ExitPlan;
|
|
716
|
+
/** происхождение (аудит), не для ветвления */
|
|
717
|
+
origin: SignalOrigin;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Политика разрешённых исходов (allow-список).
|
|
721
|
+
*
|
|
722
|
+
* Сериализуема: фиксируется в момент fit, попадает в params (model.json).
|
|
723
|
+
* В исполнении READONLY: второй аргумент signals() может СУЗИТЬ allow для одного
|
|
724
|
+
* вызова, но не расширить — исполнение не разрешает то, что обучение запретило.
|
|
725
|
+
*
|
|
726
|
+
* veto в allow быть НЕ может: veto это «не входить», т.е. отсутствие сигнала.
|
|
727
|
+
* Запрет veto = удаление сигнала из выдачи (фильтр внутри signals).
|
|
728
|
+
*/
|
|
729
|
+
type AllowAction = "enter" | "invert" | "tighten";
|
|
730
|
+
interface SignalPolicy {
|
|
731
|
+
/** какие исходы попадают в выдачу. По умолчанию все три. */
|
|
732
|
+
allow: AllowAction[];
|
|
733
|
+
/**
|
|
734
|
+
* Минимальный risk-reward символа для допуска сигнала (readonly-фильтр).
|
|
735
|
+
* Режет символы, у которых backtest-RR ниже порога. Какую метрику сравнивать —
|
|
736
|
+
* задаёт rrMetric. undefined = без RR-фильтра.
|
|
737
|
+
*/
|
|
738
|
+
minRiskReward?: number;
|
|
739
|
+
/** какую RR-метрику символа сравнивать с minRiskReward. По умолчанию "mean". */
|
|
740
|
+
rrMetric?: "mean" | "p95" | "p99";
|
|
741
|
+
}
|
|
742
|
+
declare const DEFAULT_POLICY: SignalPolicy;
|
|
743
|
+
/**
|
|
744
|
+
* Пересечение политик: эффективный allow = trained ∩ requested.
|
|
745
|
+
* Реализует readonly-инвариант — запрос не может разрешить то, чего нет в обученной.
|
|
746
|
+
* RR-фильтр (minRiskReward/rrMetric) — чисто рантаймовый: запрос может его ужесточить,
|
|
747
|
+
* обученная политика дефолта не несёт (RR-статистика отдельно в params.riskReward).
|
|
748
|
+
*/
|
|
749
|
+
declare function intersectPolicy(trained: SignalPolicy, requested?: Partial<SignalPolicy>): SignalPolicy;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Математический аппарат для отличия РЕАЛЬНОГО эджа от ВЫБРОСА/оверфита.
|
|
753
|
+
*
|
|
754
|
+
* Брутфорс-grid (argmax по CV из N конфигов) систематически выдаёт ложный эдж:
|
|
755
|
+
* максимум N шумных оценок смещён вверх на ≈ σ·√(2·ln N) даже при истинном эдже 0.
|
|
756
|
+
* Эти функции дают СТАТИСТИЧЕСКИЙ СЕРТИФИКАТ, а не «score повыше».
|
|
757
|
+
*
|
|
758
|
+
* Ссылки: López de Prado (Deflated Sharpe 2014, PBO 2015, minTRL),
|
|
759
|
+
* White (Reality Check 2000), Hansen (SPA 2005), Politis-Romano (stationary
|
|
760
|
+
* bootstrap 1994), Breiman (1-SE 1984).
|
|
761
|
+
*
|
|
762
|
+
* Все функции — чистые над массивами ретёрнов сделок. Без внешних зависимостей.
|
|
763
|
+
*/
|
|
764
|
+
declare function mean(a: number[]): number;
|
|
765
|
+
declare function variance(a: number[]): number;
|
|
766
|
+
declare function stdev(a: number[]): number;
|
|
767
|
+
/** Выборочный коэффициент асимметрии (Fisher-Pearson). */
|
|
768
|
+
declare function skewness(a: number[]): number;
|
|
769
|
+
/** Выборочный куртозис (НЕ excess: нормаль = 3). */
|
|
770
|
+
declare function kurtosis(a: number[]): number;
|
|
771
|
+
/** Sharpe ratio по ряду ретёрнов (без аннуализации; per-trade). */
|
|
772
|
+
declare function sharpe(returns: number[]): number;
|
|
773
|
+
/** CDF стандартной нормали через erf-приближение Abramowitz-Stegun 7.1.26. */
|
|
774
|
+
declare function normalCdf(z: number): number;
|
|
775
|
+
/** Обратная нормаль (quantile) — Acklam 2003. Точность ~1e-9 в [1e-15, 1-1e-15]. */
|
|
776
|
+
declare function normalInv(p: number): number;
|
|
777
|
+
/**
|
|
778
|
+
* Ожидаемый МАКСИМАЛЬНЫЙ Sharpe при истинном эдже 0, если перебрано N независимых
|
|
779
|
+
* конфигураций с дисперсией SR-оценок varSR. Это «планка случайности»: насколько
|
|
780
|
+
* высокий Sharpe выскочит из чистого шума просто потому, что мы выбрали лучший из N.
|
|
781
|
+
*
|
|
782
|
+
* E[max] ≈ √varSR · [(1−γ)·Z(1−1/N) + γ·Z(1−1/(N·e))] (López de Prado 2014)
|
|
783
|
+
*/
|
|
784
|
+
declare function expectedMaxSharpe(varSR: number, nTrials: number): number;
|
|
785
|
+
/**
|
|
786
|
+
* Deflated Sharpe Ratio: вероятность, что ИСТИННЫЙ Sharpe > порога случайности,
|
|
787
|
+
* с поправкой на (а) число испытаний N, (б) асимметрию/куртозис ряда, (в) длину T.
|
|
788
|
+
*
|
|
789
|
+
* DSR = Φ( (SR − SR0)·√(T−1) / √(1 − skew·SR + (kurt−1)/4·SR²) )
|
|
790
|
+
*
|
|
791
|
+
* SR — наблюдаемый Sharpe лучшей стратегии; SR0 — expectedMaxSharpe(varSR, N).
|
|
792
|
+
* Возвращает p ∈ [0,1]. p ≥ 0.95 → эдж РЕАЛЕН с учётом перебора. На малой выборке
|
|
793
|
+
* или огромном N → p ≈ 0 (честный отказ вместо ложного «reliable»).
|
|
794
|
+
*/
|
|
795
|
+
declare function deflatedSharpe(returns: number[], nTrials: number, varSRAcrossTrials: number): number;
|
|
796
|
+
/**
|
|
797
|
+
* Минимальная длина ряда (число сделок), при которой наблюдаемый Sharpe значим на
|
|
798
|
+
* уровне α (по умолчанию 0.05). Если фактическое N < minTRL — выборки физически НЕ
|
|
799
|
+
* хватает, любой вывод преждевременен. Это «сколько сделок до доверия».
|
|
800
|
+
*
|
|
801
|
+
* minTRL = 1 + [1 − skew·SR + (kurt−1)/4·SR²]·(Z_α / SR)² (López de Prado)
|
|
802
|
+
*/
|
|
803
|
+
declare function minTrackRecordLength(returns: number[], alpha?: number): number;
|
|
804
|
+
/**
|
|
805
|
+
* Probability of Backtest Overfitting через Combinatorially-Symmetric CV (CSCV).
|
|
806
|
+
*
|
|
807
|
+
* Матрица M[config][fold] (perf каждого конфига на каждом фолде). Делим S фолдов
|
|
808
|
+
* на все C(S, S/2) комбинаций IS/OOS. На каждой: выбираем лучший конфиг по IS,
|
|
809
|
+
* смотрим его РАНГ на OOS. Если IS-лучший систематически плох на OOS — это оверфит.
|
|
810
|
+
*
|
|
811
|
+
* PBO = доля разбиений, где IS-лучший попал в нижнюю половину OOS (logit < 0).
|
|
812
|
+
* PBO → 0.5 = чистый оверфит; PBO → 0 = эдж переносится OOS.
|
|
813
|
+
*
|
|
814
|
+
* @param perf perf[c][f] — метрика конфига c на фолде f (больше = лучше)
|
|
815
|
+
*/
|
|
816
|
+
declare function probabilityOfBacktestOverfitting(perf: number[][]): number;
|
|
817
|
+
/**
|
|
818
|
+
* Stationary bootstrap (Politis-Romano 1994): ресэмпл ряда блоками случайной
|
|
819
|
+
* геометрической длины (средняя 1/p), сохраняя автокорреляцию. Для зависимых рядов
|
|
820
|
+
* сделок обычный i.i.d. бутстрэп даёт оптимистичный результат — блочность чинит это.
|
|
821
|
+
*/
|
|
822
|
+
declare function stationaryBootstrapResample(returns: number[], pBlock: number, rng: () => number): number[];
|
|
823
|
+
/** Детерминированный ГПСЧ (mulberry32) — воспроизводимые бутстрэп-прогоны в тестах. */
|
|
824
|
+
declare function mulberry32(seed: number): () => number;
|
|
825
|
+
/**
|
|
826
|
+
* White's Reality Check / Hansen SPA через stationary bootstrap.
|
|
827
|
+
* H0: лучшая из N стратегий НЕ лучше бенчмарка 0 (весь эдж — data-snooping).
|
|
828
|
+
*
|
|
829
|
+
* Статистика V = max_k √T · mean(returns_k). Бутстрэпим центрированные ряды,
|
|
830
|
+
* считаем распределение макс-статистики при H0, p-value = доля бутстрэп-V,
|
|
831
|
+
* превысивших наблюдаемый V. p ≤ 0.05 → отвергаем H0 (эдж не объясним перебором).
|
|
832
|
+
*
|
|
833
|
+
* @param strategiesReturns массив рядов (по одному на конфиг-кандидат)
|
|
834
|
+
*/
|
|
835
|
+
declare function realityCheckPValue(strategiesReturns: number[][], opts?: {
|
|
836
|
+
bootstraps?: number;
|
|
837
|
+
pBlock?: number;
|
|
838
|
+
seed?: number;
|
|
839
|
+
}): number;
|
|
840
|
+
/**
|
|
841
|
+
* Итоговый сертификат: пять барьеров López de Prado / White / Hansen.
|
|
842
|
+
* certified=true ТОЛЬКО если эдж переживает поправку на N испытаний, не оверфит
|
|
843
|
+
* по CSCV, не объясним data-snooping, и выборки достаточно.
|
|
844
|
+
*/
|
|
845
|
+
interface CertificationInput {
|
|
846
|
+
/** ретёрны ВЫБРАННОЙ стратегии (по сделкам) */
|
|
847
|
+
selectedReturns: number[];
|
|
848
|
+
/** число перебранных конфигураций (N испытаний) */
|
|
849
|
+
nTrials: number;
|
|
850
|
+
/** дисперсия Sharpe-оценок ПО испытаниям (для DSR planка) */
|
|
851
|
+
varSRAcrossTrials: number;
|
|
852
|
+
/** perf[config][fold] для PBO (CSCV) */
|
|
853
|
+
perfMatrix: number[][];
|
|
854
|
+
/** ретёрны всех конфигов-кандидатов для SPA */
|
|
855
|
+
candidateReturns: number[][];
|
|
856
|
+
/** несмещённый nested-CV OOS score (null если не считался) */
|
|
857
|
+
nestedScore: number | null;
|
|
858
|
+
}
|
|
859
|
+
interface Certification {
|
|
860
|
+
certified: boolean;
|
|
861
|
+
dsr: number;
|
|
862
|
+
pbo: number;
|
|
863
|
+
spaPValue: number;
|
|
864
|
+
minTRL: number;
|
|
865
|
+
actualN: number;
|
|
866
|
+
nestedScore: number | null;
|
|
867
|
+
reasons: string[];
|
|
868
|
+
}
|
|
869
|
+
declare function certifyStrategy(inp: CertificationInput, thresholds?: {
|
|
870
|
+
dsr?: number;
|
|
871
|
+
pbo?: number;
|
|
872
|
+
spa?: number;
|
|
873
|
+
}): Certification;
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Мета-учёт переобучений — против МЕТА-winner's-curse.
|
|
877
|
+
*
|
|
878
|
+
* Проблема (Tripolsky): DSR штрафует N конфигов ВНУТРИ одного fit, но если гонять
|
|
879
|
+
* fit 720 раз за месяц (ежечасно) и торговать только когда выпал certified=true —
|
|
880
|
+
* это повторный перебор УЖЕ ПО ВРЕМЕНИ. Каждый «сертифицированный» прогон может быть
|
|
881
|
+
* тем самым выбросом среди 720 попыток. Сертификат на отдельном fit слеп к цепочке.
|
|
882
|
+
*
|
|
883
|
+
* Лечение (двухчастное):
|
|
884
|
+
* 1) CADENCE GUARD: запрет частого переобучения. fit разрешён не чаще minRefitMs
|
|
885
|
+
* (дни/недели, не часы). Частые refit = размножение испытаний.
|
|
886
|
+
* 2) FAMILY-WISE коррекция: эффективное число испытаний = N_внутри × число_fit_попыток.
|
|
887
|
+
* ВСЕ попытки логируются (не только certified), иначе знаменатель занижен и
|
|
888
|
+
* поправка лжёт. DSR на эффективном N нейтрализует мета-curse (доказано тестом:
|
|
889
|
+
* 720 fit на шуме → наивно 2 ложных, мета 0).
|
|
890
|
+
*/
|
|
891
|
+
interface FitAttempt {
|
|
892
|
+
/** когда запущен fit (ms epoch) */
|
|
893
|
+
ts: number;
|
|
894
|
+
/** число конфигов в гриде этого fit (внутренние испытания) */
|
|
895
|
+
innerTrials: number;
|
|
896
|
+
/** сертифицирован ли ЭТОТ fit по собственному (наивному) критерию */
|
|
897
|
+
certifiedNaive: boolean;
|
|
898
|
+
}
|
|
899
|
+
interface MetaLedgerState {
|
|
900
|
+
/** ВСЕ попытки fit, не только успешные — иначе знаменатель занижен */
|
|
901
|
+
attempts: FitAttempt[];
|
|
902
|
+
}
|
|
903
|
+
interface MetaPolicy {
|
|
904
|
+
/** минимальный интервал между fit (ms). По умолчанию 7 дней. */
|
|
905
|
+
minRefitMs: number;
|
|
906
|
+
}
|
|
907
|
+
declare const DEFAULT_META_POLICY: MetaPolicy;
|
|
908
|
+
/** Пустой реестр. */
|
|
909
|
+
declare function emptyLedger(): MetaLedgerState;
|
|
910
|
+
/**
|
|
911
|
+
* Разрешён ли новый fit сейчас по cadence-политике. Возвращает {allowed, reason,
|
|
912
|
+
* nextAllowedTs}. Частое переобучение размножает испытания → запрещаем.
|
|
913
|
+
*/
|
|
914
|
+
declare function canRefit(ledger: MetaLedgerState, now: number, policy?: MetaPolicy): {
|
|
915
|
+
allowed: boolean;
|
|
916
|
+
reason: string;
|
|
917
|
+
nextAllowedTs: number;
|
|
918
|
+
};
|
|
919
|
+
/** Регистрирует попытку fit (ЛЮБУЮ — и certified, и нет). Возвращает новый реестр. */
|
|
920
|
+
declare function recordAttempt(ledger: MetaLedgerState, attempt: FitAttempt): MetaLedgerState;
|
|
921
|
+
/**
|
|
922
|
+
* Эффективное число испытаний для family-wise коррекции DSR: суммарно по ВСЕМ
|
|
923
|
+
* fit-попыткам, не только текущей. Если за месяц было M fit-ов с N конфигов каждый —
|
|
924
|
+
* эффективно перебрано до Σ Nᵢ гипотез. Это и есть честный знаменатель для DSR.
|
|
925
|
+
*
|
|
926
|
+
* Используется как nTrials в deflatedSharpe вместо одного board.length. Так
|
|
927
|
+
* сертификат учитывает, что ты гонял fit многократно и выбираешь успешные.
|
|
928
|
+
*/
|
|
929
|
+
declare function effectiveTrials(ledger: MetaLedgerState, currentInnerTrials: number): number;
|
|
930
|
+
/**
|
|
931
|
+
* Сколько РАЗ был запущен fit (длина цепочки попыток + текущая). Для отчёта и для
|
|
932
|
+
* грубой Bonferroni-поправки порога значимости при желании.
|
|
933
|
+
*/
|
|
934
|
+
declare function fitAttemptCount(ledger: MetaLedgerState): number;
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Параметры выбора конфигурации и валидации. Вынесены в одно место, чтобы в логике
|
|
938
|
+
* train не было магических литералов — каждое число здесь именовано и объяснено.
|
|
939
|
+
*/
|
|
940
|
+
interface SelectionConfig {
|
|
941
|
+
/** множитель SE для коридора one-standard-error (1 = классический Breiman) */
|
|
942
|
+
seMultiplier: number;
|
|
943
|
+
/** число внешних фолдов nested-CV для несмещённой оценки (0 = не делать nested) */
|
|
944
|
+
nestedOuterFolds: number;
|
|
945
|
+
}
|
|
946
|
+
declare const DEFAULT_SELECTION: SelectionConfig;
|
|
947
|
+
/**
|
|
948
|
+
* Порядок агрессии реакции на каскад: чем выше, тем агрессивнее вмешательство.
|
|
949
|
+
* ignore (вход вопреки каскаду) ≈ none (просто вход) < tighten (ужать) <
|
|
950
|
+
* veto (не входить) < invert (развернуться).
|
|
951
|
+
* Используется как ось консервативности: при near-tie выбираем менее агрессивную.
|
|
952
|
+
* ignore намеренно НЕ реагирует на каскад → наименее консервативная реакция (0).
|
|
953
|
+
*/
|
|
954
|
+
declare const CASCADE_AGGRESSION: Record<string, number>;
|
|
955
|
+
declare const cascadeAggressionOf: (policy: string | undefined) => number;
|
|
956
|
+
/**
|
|
957
|
+
* Ключ консервативности exit-конфигурации для one-standard-error tie-break.
|
|
958
|
+
* Лексикографический порядок (меньше = консервативнее):
|
|
959
|
+
* 1) hardStop — меньший риск на сделку
|
|
960
|
+
* 2) staleMinutes — короче экспозиция
|
|
961
|
+
* 3) cascade aggression— мягче вмешательство в каскад
|
|
962
|
+
* 4) -cvScore — при полном равенстве выше score (детерминизм)
|
|
963
|
+
*
|
|
964
|
+
* `score` передаётся отдельно, т.к. ExitParams его не содержит.
|
|
965
|
+
*/
|
|
966
|
+
declare function conservatismKey(exit: ExitParams, cvScore: number): number[];
|
|
967
|
+
/** Сравнение «a консервативнее b» по лексикографическому ключу (true → предпочесть a). */
|
|
968
|
+
declare function isMoreConservative(a: {
|
|
969
|
+
exit: ExitParams;
|
|
970
|
+
cvScore: number;
|
|
971
|
+
}, b: {
|
|
972
|
+
exit: ExitParams;
|
|
973
|
+
cvScore: number;
|
|
974
|
+
}): boolean;
|
|
975
|
+
|
|
976
|
+
interface TrainGrid {
|
|
977
|
+
windowK: number[];
|
|
978
|
+
minClusters: number[];
|
|
979
|
+
jaccardThreshold: number[];
|
|
980
|
+
lagPeakThreshold: number[];
|
|
981
|
+
trailingTake: number[];
|
|
982
|
+
hardStop: number[];
|
|
983
|
+
stalenessSinceProfit: number[];
|
|
984
|
+
stalenessSinceMinutes: number[];
|
|
985
|
+
/** life-cap в минутных свечах — ЭМПИРИЧЕСКИЙ импакт-горизонт поста */
|
|
986
|
+
staleMinutes: number[];
|
|
987
|
+
/** порог volZ для разметки calm/anomalous — эмпирически */
|
|
988
|
+
volZThreshold: number[];
|
|
989
|
+
/** политика реакции на каскад: train выберет по CV (или зафиксируй параметром) */
|
|
990
|
+
squeezePolicy: Array<"none" | "tighten" | "veto" | "invert" | "ignore">;
|
|
991
|
+
/** порог squeezePressure для срабатывания policy */
|
|
992
|
+
squeezeThreshold: number[];
|
|
993
|
+
/** baseline-окно для volZ (свечей до входа) */
|
|
994
|
+
volBaselineWindow: number[];
|
|
995
|
+
/**
|
|
996
|
+
* Окно детекции каскада в минутах — НЕЗАВИСИМО от staleMinutes. Сквиз быстрый,
|
|
997
|
+
* окно должно быть коротким (минуты). Перебирается отдельно от горизонта удержания.
|
|
998
|
+
*/
|
|
999
|
+
cascadeWindowMinutes: number[];
|
|
1000
|
+
/**
|
|
1001
|
+
* Окно стационарности, мс: на длинном горизонте статистики (τ, author-матрица)
|
|
1002
|
+
* считаются по локальному окну, а не по всей истории. Infinity = вся история.
|
|
1003
|
+
* train перебирает варианты и выбирает по CV.
|
|
1004
|
+
*/
|
|
1005
|
+
stationarityWindowMs: number[];
|
|
1006
|
+
}
|
|
1007
|
+
declare const DEFAULT_GRID: TrainGrid;
|
|
1008
|
+
interface TrainOptions {
|
|
1009
|
+
grid?: Partial<TrainGrid>;
|
|
1010
|
+
/** число фолдов time-series K-fold (расширяющееся окно) */
|
|
1011
|
+
folds?: number;
|
|
1012
|
+
/** сила усадки objective */
|
|
1013
|
+
shrinkageK?: number;
|
|
1014
|
+
/** жёсткий потолок окна всплеска, мс */
|
|
1015
|
+
maxBurstWindowMs?: number;
|
|
1016
|
+
/** настройка порогов доверия */
|
|
1017
|
+
reliability?: Partial<ReliabilityConfig>;
|
|
1018
|
+
/** режим отбора входов для обучения: auto | matrix | single */
|
|
1019
|
+
mode?: "auto" | "matrix" | "single";
|
|
1020
|
+
/** переопределение порогов жизнеспособности матрицы (auto-режим) */
|
|
1021
|
+
viability?: Partial<ViabilityConfig>;
|
|
1022
|
+
/** колбэк прогресса обучения (по умолчанию stdout-бар; передай silentProgress чтобы заглушить) */
|
|
1023
|
+
onProgress?: ProgressFn;
|
|
1024
|
+
/**
|
|
1025
|
+
* Политика разрешённых исходов, вшиваемая в обученную модель (сериализуется).
|
|
1026
|
+
* По умолчанию все: enter, invert, tighten. В исполнении её можно только сузить.
|
|
1027
|
+
*/
|
|
1028
|
+
policy?: SignalPolicy;
|
|
1029
|
+
/** настройка выбора конфигурации: SE-коридор + nested-CV (см. selection.ts) */
|
|
1030
|
+
selection?: Partial<SelectionConfig>;
|
|
1031
|
+
/**
|
|
1032
|
+
* Мета-реестр прошлых fit-попыток (против МЕТА-winner's-curse). Если передан,
|
|
1033
|
+
* DSR использует эффективное число испытаний = Σ конфигов по ВСЕМ fit-ам, а не
|
|
1034
|
+
* только текущему. Так сертификат учитывает, что fit гоняли многократно.
|
|
1035
|
+
* Без него (undefined) — поправки нет (одиночный fit, наивный N).
|
|
1036
|
+
*/
|
|
1037
|
+
metaLedger?: MetaLedgerState;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Запись истории одного сигнала для внешней аналитики (dump()).
|
|
1041
|
+
* Все цены абсолютные; pnl/peak в долях (0.05 = +5%); ts в мс.
|
|
1042
|
+
*/
|
|
1043
|
+
interface SignalRecord {
|
|
1044
|
+
/** id якорного parser-item — для сопоставления результата теста с парсингом */
|
|
1045
|
+
id?: string;
|
|
1046
|
+
/** id всех parser-item, вошедших в сигнал (в matrix может быть несколько) */
|
|
1047
|
+
ids?: string[];
|
|
1048
|
+
symbol: string;
|
|
1049
|
+
direction: "long" | "short";
|
|
1050
|
+
channel: string;
|
|
1051
|
+
/** время сигнала (ts всплеска), мс */
|
|
1052
|
+
ts: number;
|
|
1053
|
+
/** вошли ли в позицию (false для no-entry / cascade-veto) */
|
|
1054
|
+
entered: boolean;
|
|
1055
|
+
entryPrice: number;
|
|
1056
|
+
exitPrice: number;
|
|
1057
|
+
/** реализованный PnL в долях */
|
|
1058
|
+
pnl: number;
|
|
1059
|
+
/** пиковый PnL за жизнь позиции, доли */
|
|
1060
|
+
peak: number;
|
|
1061
|
+
reason: string;
|
|
1062
|
+
heldMinutes: number;
|
|
1063
|
+
/** была ли позиция инвертирована (policy=invert) */
|
|
1064
|
+
inverted: boolean;
|
|
1065
|
+
volRegime: VolRegime;
|
|
1066
|
+
/** число независимых кластеров авторства на всплеске (1 в single-режиме) */
|
|
1067
|
+
independentClusters: number;
|
|
1068
|
+
}
|
|
1069
|
+
interface TrainedParams {
|
|
1070
|
+
version: 3;
|
|
1071
|
+
config: DetectorConfig;
|
|
1072
|
+
/** prod-выход: tensor3d [mode][channel][symbol] + иерархический fallback */
|
|
1073
|
+
exit: ExitTensor;
|
|
1074
|
+
/**
|
|
1075
|
+
* Политика разрешённых исходов, ЗАФИКСИРОВАННАЯ на обучении и сериализуемая.
|
|
1076
|
+
* В исполнении readonly — signals() может только сузить её, не расширить.
|
|
1077
|
+
*/
|
|
1078
|
+
policy: SignalPolicy;
|
|
1079
|
+
/**
|
|
1080
|
+
* Risk-reward (pnl/hardStop) по бэктесту: per-symbol (для runtime-фильтра по
|
|
1081
|
+
* символам) + global (отчёт). Главный исследовательский выход наряду с
|
|
1082
|
+
* impactHorizonMinutes. Сериализуем, в исполнении readonly.
|
|
1083
|
+
*/
|
|
1084
|
+
riskReward: {
|
|
1085
|
+
bySymbol: Record<string, RiskRewardStats>;
|
|
1086
|
+
global: RiskRewardStats;
|
|
1087
|
+
};
|
|
1088
|
+
/**
|
|
1089
|
+
* Устойчивая к выбросам статистика реализованного PnL (median + перцентили),
|
|
1090
|
+
* чтобы одна плохая/жирная сделка не определяла оценку выигрыша системы.
|
|
1091
|
+
* Per-symbol + global. Сериализуется, в исполнении readonly.
|
|
1092
|
+
*/
|
|
1093
|
+
pnl: {
|
|
1094
|
+
bySymbol: Record<string, PnlStats>;
|
|
1095
|
+
global: PnlStats;
|
|
1096
|
+
};
|
|
1097
|
+
/**
|
|
1098
|
+
* История сигналов выбранной конфигурации (для аналитики сторонним скриптом).
|
|
1099
|
+
* Каждая запись — один кандидат-всплеск, размеченный ВЫБРАННЫМ global-exit:
|
|
1100
|
+
* цена входа/выхода, реализованный pnl, причина и длительность. Сериализуется в
|
|
1101
|
+
* save()/load(); удобнее получать через dump() (плоский JSON-массив).
|
|
1102
|
+
*/
|
|
1103
|
+
history?: SignalRecord[];
|
|
1104
|
+
meta: {
|
|
1105
|
+
trainedAt: number;
|
|
1106
|
+
folds: number;
|
|
1107
|
+
shrinkageK: number;
|
|
1108
|
+
cvScore: number;
|
|
1109
|
+
/** несмещённая out-of-sample оценка через nested CV (null если не считалась) */
|
|
1110
|
+
nestedScore: number | null;
|
|
1111
|
+
cvWinrate: number;
|
|
1112
|
+
cvSupport: number;
|
|
1113
|
+
gridSize: number;
|
|
1114
|
+
/** эффективный режим обучения: matrix | single */
|
|
1115
|
+
mode: "matrix" | "single";
|
|
1116
|
+
/** честная диагностика: ПОЧЕМУ выбран этот режим (auto-критерий или явный) */
|
|
1117
|
+
modeReason: string;
|
|
1118
|
+
impactHorizonMinutes: number;
|
|
1119
|
+
confidence: number;
|
|
1120
|
+
reliable: boolean;
|
|
1121
|
+
support: number;
|
|
1122
|
+
stability: number;
|
|
1123
|
+
significance: number;
|
|
1124
|
+
totalSamples: number;
|
|
1125
|
+
/** статистический сертификат (DSR/PBO/SPA/minTRL) */
|
|
1126
|
+
certification: Certification;
|
|
1127
|
+
/** эффективное число испытаний с family-wise поправкой на цепочку fit (мета-curse) */
|
|
1128
|
+
effectiveTrials: number;
|
|
1129
|
+
/** число конфигов в гриде текущего fit */
|
|
1130
|
+
innerTrials: number;
|
|
1131
|
+
/** сколько раз всего запускался fit (для прозрачности мета-перебора) */
|
|
1132
|
+
fitAttempts: number;
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
interface TrainResult {
|
|
1136
|
+
predict: (items: ParserItem[]) => PredictionResult;
|
|
1137
|
+
params: TrainedParams;
|
|
1138
|
+
reliability: Reliability;
|
|
1139
|
+
leaderboard: Array<{
|
|
1140
|
+
config: DetectorConfig;
|
|
1141
|
+
exit: ExitParams;
|
|
1142
|
+
cvScore: number;
|
|
1143
|
+
cvWinrate: number;
|
|
1144
|
+
cvSupport: number;
|
|
1145
|
+
}>;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Обучает пороги детектора И параметры prod-выхода на исторических данных.
|
|
1149
|
+
* Метку ставит симуляция твоего trailing/hard-stop по 1m-свечам (replay),
|
|
1150
|
+
* поэтому stop hunting размечается как убыток. Объектив — shrinkage-expectancy
|
|
1151
|
+
* под time-series K-fold. Эмпирически выбирает импакт-горизонт (staleMinutes).
|
|
1152
|
+
*/
|
|
1153
|
+
declare function train(items: ParserItem[], getCandles: GetCandles, opts?: TrainOptions): Promise<TrainResult>;
|
|
1154
|
+
declare function loadPredict(params: TrainedParams): (items: ParserItem[]) => PredictionResult;
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Casual-фасад с ЕДИНЫМ стабильным контрактом ввода-вывода.
|
|
1158
|
+
*
|
|
1159
|
+
* const model = await PumpMatrix.fit(history, getCandles); // обучить
|
|
1160
|
+
* const json = model.save(); // сохранить (string)
|
|
1161
|
+
* const model = PumpMatrix.load(json); // в проде, без обучения
|
|
1162
|
+
*
|
|
1163
|
+
* for (const s of model.signals(liveItems)) // УЖЕ отфильтровано
|
|
1164
|
+
* openPosition(s.symbol, s.direction, s.exit); // прод не думает
|
|
1165
|
+
*
|
|
1166
|
+
* signals() возвращает ТОЛЬКО исполняемое: veto (каскад ликвидаций) не попадает в
|
|
1167
|
+
* выдачу вообще — фильтр внутри. Разрешённые исходы задаются вторым аргументом
|
|
1168
|
+
* (allow-список), но не шире, чем зашито в обученную модель (readonly-инвариант).
|
|
1169
|
+
*/
|
|
1170
|
+
declare class PumpMatrix {
|
|
1171
|
+
private readonly params;
|
|
1172
|
+
private readonly _predict;
|
|
1173
|
+
private constructor();
|
|
1174
|
+
/** Обучить модель на истории сигналов. */
|
|
1175
|
+
static fit(history: ParserItem[], getCandles: GetCandles, opts?: TrainOptions): Promise<PumpMatrix>;
|
|
1176
|
+
/** Восстановить модель из сохранённого JSON (в проде, без обучения). */
|
|
1177
|
+
static load(json: string | TrainedParams): PumpMatrix;
|
|
1178
|
+
/** Сериализовать модель в JSON-строку (включая policy). */
|
|
1179
|
+
save(): string;
|
|
1180
|
+
/**
|
|
1181
|
+
* Экспорт истории сигналов выбранной конфигурации для внешней аналитики.
|
|
1182
|
+
* Возвращает плоский массив записей (цена входа/выхода, pnl, причина выхода,
|
|
1183
|
+
* длительность и т.д.) — посчитать метрики можно отдельным скриптом.
|
|
1184
|
+
*
|
|
1185
|
+
* Включает и НЕ вошедшие сигналы (no-entry / cascade-veto) с entered=false,
|
|
1186
|
+
* чтобы аналитика видела пропуски, а не только реализованные сделки.
|
|
1187
|
+
* Доступно после fit() и сохраняется в save()/load().
|
|
1188
|
+
*
|
|
1189
|
+
* @param asString true → JSON-строка; иначе массив объектов (по умолчанию массив)
|
|
1190
|
+
*/
|
|
1191
|
+
dump(asString: true): string;
|
|
1192
|
+
dump(asString?: false): SignalRecord[];
|
|
1193
|
+
/** Число записей в истории сигналов (0 если модель загружена без истории). */
|
|
1194
|
+
get historySize(): number;
|
|
1195
|
+
/** Полный exit-tensor (для аудита). */
|
|
1196
|
+
get exit(): ExitTensor;
|
|
1197
|
+
/** Политика разрешённых исходов, зашитая в модель (readonly-копия). */
|
|
1198
|
+
get policy(): SignalPolicy;
|
|
1199
|
+
/** Надёжна ли модель (хватило ли данных при обучении). */
|
|
1200
|
+
get reliable(): boolean;
|
|
1201
|
+
/** Доверие к модели 0..1. */
|
|
1202
|
+
get confidence(): number;
|
|
1203
|
+
/**
|
|
1204
|
+
* Эффективное число испытаний с family-wise поправкой на цепочку fit (мета-curse).
|
|
1205
|
+
* Если fit гнали многократно — это Σ конфигов по всем попыткам, а не текущий грид.
|
|
1206
|
+
*/
|
|
1207
|
+
get effectiveTrials(): number;
|
|
1208
|
+
/** Число конфигов в гриде текущего fit (внутренние испытания). */
|
|
1209
|
+
get innerTrials(): number;
|
|
1210
|
+
/** Сколько раз всего запускался fit (прозрачность мета-перебора). */
|
|
1211
|
+
get fitAttempts(): number;
|
|
1212
|
+
/**
|
|
1213
|
+
* Статистический сертификат: прошёл ли эдж пять барьеров (DSR ≥ 0.95, PBO ≤ 0.10,
|
|
1214
|
+
* SPA p ≤ 0.05, N ≥ minTRL, nested OOS > 0). certified=false с reasons, если эдж
|
|
1215
|
+
* не доказан — тогда модель торговать НЕ должна.
|
|
1216
|
+
*/
|
|
1217
|
+
get certification(): Certification;
|
|
1218
|
+
/** Эмпирический импакт-горизонт поста в минутах (global-уровень). */
|
|
1219
|
+
get impactHorizonMinutes(): number;
|
|
1220
|
+
/**
|
|
1221
|
+
* Сколько минут истории СВЕЧЕЙ ДО сигнала нужно live-вызову plan() для каждого
|
|
1222
|
+
* сигнала: max(volBaselineWindow, cascadeWindowMinutes) + запас 5 свечей. Столько
|
|
1223
|
+
* 1m-свечей plan() запрашивает у getCandles (строго в прошлое, без look-ahead).
|
|
1224
|
+
* В проде держи доступной историю минимум на это окно для каждого свежего сигнала.
|
|
1225
|
+
*/
|
|
1226
|
+
get lookbackMinutes(): number;
|
|
1227
|
+
/**
|
|
1228
|
+
* Минимальное число НЕЗАВИСИМЫХ кластеров авторства, которые должны сойтись на
|
|
1229
|
+
* тикере, чтобы matrix-всплеск считался сигналом. Из config (по умолчанию 2).
|
|
1230
|
+
* В single-режиме не применяется (там всегда 1 кластер).
|
|
1231
|
+
*/
|
|
1232
|
+
get minClusters(): number;
|
|
1233
|
+
/**
|
|
1234
|
+
* Минимальное число ОБЩИХ событий между каналами, при котором author-матрица
|
|
1235
|
+
* считается жизнеспособной (не шумовое совпадение) — порог перекрытия для
|
|
1236
|
+
* auto-режима. Из config.viability (по умолчанию DEFAULT_VIABILITY.minSharedEvents).
|
|
1237
|
+
* Грубо: сколько раз кластеры должны совпасть, чтобы их связь была не случайной.
|
|
1238
|
+
*/
|
|
1239
|
+
get minSharedEvents(): number;
|
|
1240
|
+
/** Режим, которым обучена модель: matrix (корреляция) | single (fallback). */
|
|
1241
|
+
get mode(): "matrix" | "single";
|
|
1242
|
+
/** Честная диагностика: ПОЧЕМУ выбран этот режим (auto-критерий или явный выбор). */
|
|
1243
|
+
get modeReason(): string;
|
|
1244
|
+
/**
|
|
1245
|
+
* Risk-reward по бэктесту: per-symbol + global. Главный исследовательский выход.
|
|
1246
|
+
* RR = pnl/hardStop в единицах риска (сколько R снято). bySymbol используется
|
|
1247
|
+
* runtime-фильтром minRiskReward.
|
|
1248
|
+
*/
|
|
1249
|
+
get riskReward(): {
|
|
1250
|
+
bySymbol: Record<string, RiskRewardStats>;
|
|
1251
|
+
global: RiskRewardStats;
|
|
1252
|
+
};
|
|
1253
|
+
/**
|
|
1254
|
+
* Устойчивая к выбросам статистика реализованного PnL: median + перцентили
|
|
1255
|
+
* (p5/p95/p99) per-symbol и global. median/перцентили показывают выигрыш
|
|
1256
|
+
* системы без искажения единичной плохой или жирной сделкой.
|
|
1257
|
+
*/
|
|
1258
|
+
get pnl(): {
|
|
1259
|
+
bySymbol: Record<string, PnlStats>;
|
|
1260
|
+
global: PnlStats;
|
|
1261
|
+
};
|
|
1262
|
+
/**
|
|
1263
|
+
* Главный prod-вызов БЕЗ свечей. Возвращает ТОЛЬКО исполняемые сигналы — veto
|
|
1264
|
+
* уже отфильтрован. Без свечей каскад не оценивается → все исходы "enter".
|
|
1265
|
+
* Второй аргумент — allow-список, сужающий разрешённые исходы (не шире обученной).
|
|
1266
|
+
*/
|
|
1267
|
+
signals(items: ParserItem[], policy?: Partial<SignalPolicy>): TradeSignal[];
|
|
1268
|
+
/**
|
|
1269
|
+
* LIVE-решение об открытии позиции — БЕЗ look-ahead. Возвращает только
|
|
1270
|
+
* исполняемые сигналы (veto/инверс-запрет отфильтрованы). Использует свечи
|
|
1271
|
+
* СТРОГО ДО сигнала: volZ-режим по базлайну до входа и каскад-давление по
|
|
1272
|
+
* прошлым свечам (squeezePressureBefore). НИКОГДА не тянет свечи из будущего —
|
|
1273
|
+
* в live их не существует. Это решение «входить ли сейчас и с какими exit».
|
|
1274
|
+
*
|
|
1275
|
+
* Источник свечей:
|
|
1276
|
+
* 1) getCandles — та же, что в fit(): подгружает историю ДО сигнала. Async.
|
|
1277
|
+
* Бросок по символу (дыра в данных) → сигнал без свечей (как signals()),
|
|
1278
|
+
* не роняя весь вызов.
|
|
1279
|
+
* 2) candlesBySymbol — словарь предзагруженной истории ДО сигнала. Sync.
|
|
1280
|
+
*
|
|
1281
|
+
* Для бэктеста (replay вперёд + реализованный pnl) используй backtest().
|
|
1282
|
+
*/
|
|
1283
|
+
plan(items: ParserItem[], getCandles: GetCandles, policy?: Partial<SignalPolicy>): Promise<TradeSignal[]>;
|
|
1284
|
+
plan(items: ParserItem[], candlesBySymbol: Record<string, ICandleData[]>, policy?: Partial<SignalPolicy>): TradeSignal[];
|
|
1285
|
+
private planLiveViaGetCandles;
|
|
1286
|
+
/**
|
|
1287
|
+
* БЭКТЕСТ — replay вперёд по истории + реализованный pnl/каскад. Тянет свечи
|
|
1288
|
+
* ПОСЛЕ сигнала (life-cap горизонт), прогоняет полный replay. ТОЛЬКО для анализа
|
|
1289
|
+
* завершённого прошлого: в live свечей вперёд нет. Look-ahead отсутствует, т.к.
|
|
1290
|
+
* мы в настоящем смотрим на уже закрытые свечи прошлого.
|
|
1291
|
+
*
|
|
1292
|
+
* Источник свечей — getCandles (async) или словарь {symbol: candles} (sync).
|
|
1293
|
+
*/
|
|
1294
|
+
backtest(items: ParserItem[], getCandles: GetCandles, policy?: Partial<SignalPolicy>): Promise<TradeSignal[]>;
|
|
1295
|
+
backtest(items: ParserItem[], candlesBySymbol: Record<string, ICandleData[]>, policy?: Partial<SignalPolicy>): TradeSignal[];
|
|
1296
|
+
private backtestViaGetCandles;
|
|
1297
|
+
/** Точечно под ОДНУ позицию в LIVE (вход = последняя свеча, каскад по прошлому). */
|
|
1298
|
+
planFor(symbol: string, direction: Direction, channel: string | null, candles: ICandleData[], policy?: Partial<SignalPolicy>): TradeSignal | null;
|
|
1299
|
+
/** Бэктест под ОДНУ позицию с явным entryTs (replay вперёд, каскад по будущему). */
|
|
1300
|
+
planForAt(symbol: string, direction: Direction, channel: string | null, candles: ICandleData[], entryTs: number, policy?: Partial<SignalPolicy>): TradeSignal | null;
|
|
1301
|
+
/** Полный отчёт (все вердикты + карта авторства) — для разбора. */
|
|
1302
|
+
explain(items: ParserItem[]): PredictionResult;
|
|
1303
|
+
private collect;
|
|
1304
|
+
private flatExit;
|
|
1305
|
+
/**
|
|
1306
|
+
* BACKTEST-сборка сигнала: каскад по свечам ПОСЛЕ входа (forward squeezePressure),
|
|
1307
|
+
* допустимо только на истории. Делегирует в общее ядро с mode="backtest".
|
|
1308
|
+
*/
|
|
1309
|
+
private buildSignal;
|
|
1310
|
+
/**
|
|
1311
|
+
* LIVE-сборка сигнала: каскад по свечам ДО входа (backward squeezePressureBefore),
|
|
1312
|
+
* БЕЗ look-ahead. Делегирует в общее ядро с mode="live".
|
|
1313
|
+
*/
|
|
1314
|
+
private buildSignalLive;
|
|
1315
|
+
/**
|
|
1316
|
+
* Строит ЕДИНЫЙ TradeSignal из вердикта. Возвращает null, если исполнять нечего:
|
|
1317
|
+
* каскад дал veto ИЛИ получившийся action не в allow-списке. Инверсия здесь же
|
|
1318
|
+
* разворачивает direction и тянет exit из инверс-ячейки — наружу уходит готовое
|
|
1319
|
+
* направление, без флагов.
|
|
1320
|
+
*
|
|
1321
|
+
* mode="live": каскад меряется по свечам ДО входа (squeezePressureBefore) — в live
|
|
1322
|
+
* свечей после входа нет, look-ahead запрещён.
|
|
1323
|
+
* mode="backtest": каскад по свечам ПОСЛЕ входа (squeezePressure) — допустимо на
|
|
1324
|
+
* завершённой истории.
|
|
1325
|
+
*/
|
|
1326
|
+
private buildSignalCore;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Чёрная коробка. Единственная точка входа.
|
|
1331
|
+
*
|
|
1332
|
+
* predict(parserItems) -> PredictionResult
|
|
1333
|
+
*
|
|
1334
|
+
* Два режима отбора входов (config.mode):
|
|
1335
|
+
* - "matrix": вход = синхронный всплеск независимых кластеров-авторов.
|
|
1336
|
+
* - "single": fallback — каждый пост = вход, исход решает обученный exit.
|
|
1337
|
+
* - "auto": матрица только если корреляция жизнеспособна, иначе single.
|
|
1338
|
+
*
|
|
1339
|
+
* Exit НЕ единый: подбирается отдельно под каждую ячейку тензора
|
|
1340
|
+
* [mode][channel][symbol][direction][volRegime] — математика разных источников
|
|
1341
|
+
* не смешивается (matrix/single, long/short, calm/anomalous — свои критерии).
|
|
1342
|
+
*/
|
|
1343
|
+
declare function predict(parserItems: ParserItem[], config?: Partial<DetectorConfig>): PredictionResult;
|
|
1344
|
+
|
|
1345
|
+
export { CASCADE_AGGRESSION, DEFAULT_CONFIG, DEFAULT_GRID, DEFAULT_META_POLICY, DEFAULT_POLICY, DEFAULT_RELIABILITY, DEFAULT_SELECTION, DEFAULT_VIABILITY, MAX_CANDLES_PER_CHUNK, PumpMatrix, STEP_MS, alignTs, assessViability, buildTable, buildWindowedTable, canRefit, cascadeAggressionOf, certifyStrategy, clusterAuthors, computeReliability, conservatismKey, deflatedSharpe, earlyWarning, effectiveTrials, emptyLedger, entryStartTs, enumerateBursts, enumeratePosts, exitKey, expectedMaxSharpe, fetchCandlesChunked, fitAttemptCount, intersectPolicy, isMoreConservative, jaccardPair, jaccardScreen, kurtosis, labelBurst, lagXCorr, loadPredict, mean, minTrackRecordLength, mulberry32, normalCdf, normalInv, oneStandardErrorSelect, percentile, pnlStats, predict, probabilityOfBacktestOverfitting, realityCheckPValue, recordAttempt, replayExit, resolveExit, resolveExitNoRegime, riskRewardStats, selfTuneLag, sharpe, shrinkageExpectancy, silentProgress, singleChannelSignals, skewness, squeezePressure, standardError, stationaryBootstrapResample, stdev, stdoutProgress, train, variance, volRegimeOf, volumeFeatures, volumeZScore, windowEvents, winrate };
|
|
1346
|
+
export type { AuthorMap, CandleInterval, Certification, CertificationInput, DetectorConfig, DetectorMode, Direction, ExitParams, ExitPlan, ExitReason, ExitTensor, FitAttempt, GetCandles, ICandleData, LabeledBurst, MetaLedgerState, MetaPolicy, ParserItem, PnlStats, PredictionResult, ProgressEvent, ProgressFn, PumpVerdict, Reliability, ReliabilityConfig, ReliabilityInput, ReplayResult, ResolveSource, ResolvedExit, RiskRewardStats, SelectionConfig, SignalAction, SignalEvent, SignalOrigin, SignalPolicy, SignalRecord, TradeSignal, TrainGrid, TrainOptions, TrainResult, TrainedParams, ViabilityConfig, ViabilityReport, VolRegime, VolumeFeatures };
|