pump-anomaly 0.1.0 → 1.0.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/README.md +114 -9
- package/build/index.cjs +147 -27
- package/build/index.mjs +147 -27
- package/package.json +12 -2
- package/types.d.ts +101 -7
package/README.md
CHANGED
|
@@ -55,7 +55,8 @@ const trades = model.signals(liveItems);
|
|
|
55
55
|
const trades = await model.plan(liveItems, getCandles);
|
|
56
56
|
|
|
57
57
|
for (const s of trades) {
|
|
58
|
-
|
|
58
|
+
// direction is already inverted if needed; exit is ready; entry zone for the live order
|
|
59
|
+
openPosition(s.symbol, s.direction, { from: s.entryFromPrice, to: s.entryToPrice }, s.exit);
|
|
59
60
|
}
|
|
60
61
|
```
|
|
61
62
|
|
|
@@ -79,7 +80,6 @@ Tuned `TrainGrid`s per asset live in [`config/`](config/) — one `*-grid.mjs` e
|
|
|
79
80
|
|
|
80
81
|
| Asset | Pump speed | `staleMinutes` | `hardStop` % | `trailingTake` % | `stalenessSinceProfit` % | Noise | Matrix strictness |
|
|
81
82
|
|---|---|---|---|---|---|---|---|
|
|
82
|
-
| [Fartcoin](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/fartcoin-grid.mjs) | Very fast | 25m – 4h | 0.65–2.0 | 0.5–2.4 | 0.3–1.0 | Very high | Low |
|
|
83
83
|
| [HYPE](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/hype-grid.mjs) | Very fast | 30m – 4h | 0.7–2.0 | 0.5–2.5 | 0.3–1.0 | High | Low |
|
|
84
84
|
| [Solana](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/solana-grid.mjs) | Fast | 45m – 8h | 0.8–2.5 | 0.6–2.2 | 0.4–1.3 | High | Low–Med |
|
|
85
85
|
| [TRX](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/tron-grid.mjs) | Medium | 1.5h – 15h | 1.0–3.0 | 0.7–3.5 | 0.5–1.4 | Medium | Medium |
|
|
@@ -87,6 +87,7 @@ Tuned `TrainGrid`s per asset live in [`config/`](config/) — one `*-grid.mjs` e
|
|
|
87
87
|
| [DOGE](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/doge-grid.mjs) | Medium | 1.5h – 16h | 1.1–3.2 | 0.8–4.0 | 0.5–1.5 | Medium+ | Medium+ |
|
|
88
88
|
| [BNB](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/bnb-grid.mjs) | Medium | 3h – 24h | 1.2–3.5 | 0.9–4.5 | 0.6–1.6 | Medium | Medium+ |
|
|
89
89
|
| [Ethereum](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/ethereum-grid.mjs) | Slow | 2h – 24h | 1.2–3.5 | 0.5–2.5 | 0.3–1.0 | Low | High |
|
|
90
|
+
| [Fartcoin](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/fartcoin-grid.mjs) | Very fast | 25m – 4h | 0.65–2.0 | 0.5–2.4 | 0.3–1.0 | Very high | Low |
|
|
90
91
|
| [Ripple (XRP)](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/ripple-grid.mjs) | Medium-slow | 3h – 24h | 1.3–4.0 | 0.9–5.0 | 0.6–1.7 | Low–Med | High |
|
|
91
92
|
| [Litecoin (LTC)](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/litecoin-grid.mjs) | Medium-slow | 4h – 30h | 1.3–3.8 | 0.9–5.0 | 0.7–1.8 | Low–Med | High |
|
|
92
93
|
| [Zcash (ZEC)](https://github.com/tripolskypetr/pump-anomaly/blob/master/config/zec-grid.mjs) | Medium-slow | 4h – 28h | 1.4–4.2 | 0.9–5.5 | 0.6–1.7 | Low–Med | High |
|
|
@@ -400,7 +401,8 @@ The calm/anomalous threshold (`volZThreshold`) and the firing threshold (`squeez
|
|
|
400
401
|
|
|
401
402
|
```ts
|
|
402
403
|
for (const s of model.signals(liveItems)) {
|
|
403
|
-
|
|
404
|
+
// direction is already flipped if inverted; entry zone + exit are ready for the order
|
|
405
|
+
openPosition(s.symbol, s.direction, { from: s.entryFromPrice, to: s.entryToPrice }, s.exit);
|
|
404
406
|
}
|
|
405
407
|
```
|
|
406
408
|
|
|
@@ -412,6 +414,8 @@ interface TradeSignal {
|
|
|
412
414
|
direction: "long" | "short"; // FINAL (inversion already applied)
|
|
413
415
|
action: "enter" | "invert" | "tighten";
|
|
414
416
|
ts: number;
|
|
417
|
+
entryFromPrice?: number; // entry zone from the parser-item (for opening the live position)
|
|
418
|
+
entryToPrice?: number; // undefined → enter at market
|
|
415
419
|
exit: { // flat, ready for openPosition
|
|
416
420
|
trailingTake: number; // tightened if action="tighten"
|
|
417
421
|
hardStop: number;
|
|
@@ -446,18 +450,38 @@ await model.plan(items, getCandles, policy?) // Promise<TradeSig
|
|
|
446
450
|
model.plan(items, { SOLUSDT: candles }, policy?) // TradeSignal[]
|
|
447
451
|
|
|
448
452
|
// BACKTEST — replay forward over closed history, source = getCandles | map:
|
|
449
|
-
await model.backtest(items, getCandles, policy?) // Promise<
|
|
450
|
-
model.backtest(items, { SOLUSDT: candles }, policy?) //
|
|
453
|
+
await model.backtest(items, getCandles, policy?) // Promise<BacktestSignal[]>
|
|
454
|
+
model.backtest(items, { SOLUSDT: candles }, policy?) // BacktestSignal[]
|
|
451
455
|
|
|
452
456
|
// single-position helpers:
|
|
453
|
-
model.planFor(symbol, dir, channel, candles, policy?) // live, null on veto
|
|
454
|
-
model.planForAt(symbol, dir, channel, candles, ts, policy?) // backtest, null on veto
|
|
457
|
+
model.planFor(symbol, dir, channel, candles, policy?) // live → TradeSignal, null on veto
|
|
458
|
+
model.planForAt(symbol, dir, channel, candles, ts, policy?) // backtest → BacktestSignal, null on veto
|
|
455
459
|
|
|
456
460
|
// full report (all verdicts + author map) for debugging:
|
|
457
461
|
model.explain(items)
|
|
458
462
|
```
|
|
459
463
|
|
|
460
|
-
`plan`
|
|
464
|
+
`plan` and `backtest` differ in two ways. **(1) Which candles they see:** `plan` measures the cascade from candles *before* the entry (live-safe, no look-ahead); `backtest` from candles *after* the entry (forward replay over already-closed history). **(2) What they return:** `plan` returns a `TradeSignal` (a decision — the position isn't closed yet, so there's no pnl); `backtest` returns a **`BacktestSignal`** — the same signal plus a `result` that *replays the exit plan forward* and reports the realized pnl. That replayed `result` is the whole point of `backtest`:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
interface BacktestResult { // present ONLY on BacktestSignal (backtest / planForAt)
|
|
468
|
+
entered: boolean; // false → entry zone never touched on the candle window
|
|
469
|
+
pnl: number; // realized, fraction (hard-stop = honest -hardStop%)
|
|
470
|
+
peak: number; // peak pnl over the position's life
|
|
471
|
+
reason: string; // hard-stop | trailing-take | peak-staleness | life-cap | …
|
|
472
|
+
heldMinutes: number;
|
|
473
|
+
entryPrice: number; // 0 if not entered
|
|
474
|
+
exitPrice: number; // 0 if not entered
|
|
475
|
+
truncated: boolean; // not enough candles after entry for the full life-cap
|
|
476
|
+
}
|
|
477
|
+
interface BacktestSignal extends TradeSignal { result: BacktestResult }
|
|
478
|
+
|
|
479
|
+
for (const s of model.backtest(items, getCandles)) {
|
|
480
|
+
console.log(s.symbol, s.direction, s.result.pnl, s.result.reason); // realized, no join with dump()
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The replay uses the signal's FINAL direction (inversion already applied) and its resolved exit plan; the entry zone comes from the parser-item (`entryFromPrice`/`entryToPrice`), falling back to the first candle's open. `dump()` still holds the training-time history; `backtest` is the same machinery applied to whatever items/candles you pass now.
|
|
461
485
|
|
|
462
486
|
### Permissions — allow-list, serialized at training time, readonly at runtime
|
|
463
487
|
|
|
@@ -482,6 +506,7 @@ model.certification; // five-barrier edge certificate (DSR/PBO/SPA/minTR
|
|
|
482
506
|
model.effectiveTrials; // family-wise meta-trial count (Σ configs over all fit attempts)
|
|
483
507
|
model.innerTrials; // grid size of this fit
|
|
484
508
|
model.fitAttempts; // how many times fit has run in the chain
|
|
509
|
+
model.labeling; // labeling diagnostics — WHY a fit came out empty
|
|
485
510
|
model.impactHorizonMinutes; // empirical post impact horizon (global level)
|
|
486
511
|
model.mode; // "matrix" | "single" — how the model was trained
|
|
487
512
|
model.modeReason; // honest diagnostics: WHY this mode was chosen
|
|
@@ -494,6 +519,26 @@ model.policy; // the baked-in allow-list (readonly copy)
|
|
|
494
519
|
|
|
495
520
|
`lookbackMinutes` = `max(volBaselineWindow, cascadeWindowMinutes) + 5` — the amount of pre-signal 1m history `plan()` pulls per signal (strictly in the past, no look-ahead). In prod, keep at least this much history available for every fresh signal.
|
|
496
521
|
|
|
522
|
+
### Troubleshoot
|
|
523
|
+
|
|
524
|
+
A `fit` that produces `totalSamples: 0` is otherwise mute — "no data" and "no entries" look identical. `model.labeling` makes it speak: per **unique** candidate burst, what its labeling outcome was (and the raw `getCandles` exception text, deduped):
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
model.labeling;
|
|
528
|
+
// {
|
|
529
|
+
// candidates: number; // unique bursts seen
|
|
530
|
+
// outcomes: { // only non-zero outcomes present
|
|
531
|
+
// ok?: number; // labeled, has an entry
|
|
532
|
+
// "adapter-error"?: number; // getCandles threw (look-ahead guard / gap / symbol)
|
|
533
|
+
// "no-candles"?: number; // getCandles returned empty (symbol/range gave nothing)
|
|
534
|
+
// "no-entry"?: number; // candles exist, but no exit-set entered the zone
|
|
535
|
+
// };
|
|
536
|
+
// errors: Record<string, number>; // unique getCandles exception messages → count
|
|
537
|
+
// }
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
So when a trained model is empty, `labeling.outcomes` tells you whether to fix `getCandles` (`adapter-error`), the symbol/range (`no-candles`), or accept there were no entries — and `labeling.errors` carries the exact thrown message (e.g. `{ "ccxt: symbol not found": 32 }`) instead of swallowing it.
|
|
541
|
+
|
|
497
542
|
---
|
|
498
543
|
|
|
499
544
|
## Signal history (`dump`) — for external analytics
|
|
@@ -610,9 +655,67 @@ All five are computed over the stationarity window. In single mode the matrix is
|
|
|
610
655
|
|
|
611
656
|
---
|
|
612
657
|
|
|
658
|
+
## Integration with backtest-kit
|
|
659
|
+
|
|
660
|
+
`PumpMatrix` needs one thing from your data layer: a `getCandles` that serves 1m candles by range. [`backtest-kit`](https://www.npmjs.com/package/backtest-kit) already provides exactly that contract via `Exchange.getRawCandles(symbol, interval, { exchangeName }, limit, sDate, eDate)` — the argument order matches `GetCandles` one-to-one, so the adapter is a thin pass-through. Register an exchange schema once, then wire the three phases (train → live → backtest).
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
import { addExchangeSchema, Exchange, roundTicks } from "backtest-kit";
|
|
664
|
+
import { singleshot } from "functools-kit";
|
|
665
|
+
import * as pump from "pump-anomaly";
|
|
666
|
+
import ccxt from "ccxt";
|
|
667
|
+
|
|
668
|
+
import signals from "./assets/parser-items.json" with { type: "json" };
|
|
669
|
+
import weights from "./assets/model-weights.json" with { type: "json" };
|
|
670
|
+
|
|
671
|
+
const getExchange = singleshot(async () => {
|
|
672
|
+
const exchange = new ccxt.binance({ enableRateLimit: true, options: { defaultType: "spot" } });
|
|
673
|
+
await exchange.loadMarkets();
|
|
674
|
+
return exchange;
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Register the exchange once. getCandles here is backtest-kit's OHLCV fetch;
|
|
678
|
+
// formatPrice/formatQuantity/getOrderBook/getAggregatedTrades omitted for brevity.
|
|
679
|
+
addExchangeSchema({
|
|
680
|
+
exchangeName: "ccxt-exchange",
|
|
681
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
682
|
+
const exchange = await getExchange();
|
|
683
|
+
const rows = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
|
|
684
|
+
return rows.map(([timestamp, open, high, low, close, volume]) => ({ timestamp, open, high, low, close, volume }));
|
|
685
|
+
},
|
|
686
|
+
// ...formatPrice, formatQuantity, getOrderBook, getAggregatedTrades
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Adapter: backtest-kit's getRawCandles → pump-anomaly's GetCandles (same arg order).
|
|
690
|
+
const getCandles = (symbol, interval, limit, sDate, eDate) =>
|
|
691
|
+
Exchange.getRawCandles(symbol, interval, { exchangeName: "ccxt-exchange" }, limit, sDate, eDate);
|
|
692
|
+
|
|
693
|
+
// 1) TRAIN once on history → serialize weights (slow: labels replay 1m candles).
|
|
694
|
+
async function trainWeights() {
|
|
695
|
+
const model = await pump.PumpMatrix.fit(signals, getCandles);
|
|
696
|
+
return model.save(); // → write to assets/model-weights.json
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 2) LIVE — load weights (no retraining), get ready-to-execute signals.
|
|
700
|
+
async function planLive() {
|
|
701
|
+
const model = pump.PumpMatrix.load(weights);
|
|
702
|
+
return model.plan(signals, getCandles); // TradeSignal[] — direction/entry/exit ready
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 3) BACKTEST — same weights, replay forward, realized pnl in result.
|
|
706
|
+
async function runBacktest() {
|
|
707
|
+
const model = pump.PumpMatrix.load(weights);
|
|
708
|
+
return model.backtest(signals, getCandles); // BacktestSignal[] — each has result.pnl
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
`parser-items.json` is your channel history (`ParserItem[]`), `model-weights.json` is `model.save()` output. The same `getCandles` adapter serves all three phases — `fit` pulls 1m candles forward from each signal to label it, `plan` pulls them strictly before the signal (live, no look-ahead), `backtest` after (forward replay). Other anomaly libraries plug into the same `Exchange` schema independently: [`volume-anomaly`](https://www.npmjs.com/package/volume-anomaly) consumes `Exchange.getAggregatedTrades` for entry timing, [`garch`](https://www.npmjs.com/package/garch) consumes `Exchange.getCandles` for TP/SL sizing — pump-anomaly answers *which post to trade and how to exit it*, and they compose without touching each other.
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
613
716
|
## Tests
|
|
614
717
|
|
|
615
|
-
**
|
|
718
|
+
**538 tests** across **52 test files**. All passing.
|
|
616
719
|
|
|
617
720
|
| File | Tests | What is covered |
|
|
618
721
|
|------|-------|-----------------|
|
|
@@ -666,6 +769,8 @@ All five are computed over the stationarity window. In single mode the matrix is
|
|
|
666
769
|
| `meta-ledger.test.ts` | 9 | Meta-overfitting guard: cadence guard blocks too-frequent refits, `effectiveTrials` sums ALL fit attempts (not only certified ones), family-wise DSR drops false certificates from 720 noise refits to 0 while a strong edge survives the correction |
|
|
667
770
|
| `staleness-and-id.test.ts` | 7 | `stalenessSinceProfit`/`stalenessSinceMinutes` are searched in `DEFAULT_GRID` (not pinned); a parser-item `id` threads through to every `dump()` record (numeric→string, matches the source post by `ts`, survives save/load, `undefined` without an id) |
|
|
668
771
|
| `id-threading-attack.test.ts` | 6 | `id` threading is leak-proof: time-separated bursts on one symbol both survive (no best-per-symbol loss), collapsed posts keep their `id` in `ids` (`enumeratePosts` + `singleChannelSignals`), and `id`/`ids` reach the LIVE `plan` signal's `origin` (not only `dump`) |
|
|
772
|
+
| `labeling-diagnostics.test.ts` | 8 | `model.labeling` makes an empty `fit` speak: outcomes per unique burst (ok / adapter-error / no-candles / no-entry), counts not inflated by grid size, sum of outcomes = candidates, and the raw `getCandles` exception text is captured in `errors` (incl. non-`Error` throws) |
|
|
773
|
+
| `backtest-result.test.ts` | 5 | `backtest()` returns `BacktestSignal` with a replayed `result` (realized pnl/reason/prices); no candles → `entered:false` not a crash; `planForAt` carries `result` too; `plan()`/`signals()` do NOT carry `result` |
|
|
669
774
|
|
|
670
775
|
```bash
|
|
671
776
|
npm test
|
package/build/index.cjs
CHANGED
|
@@ -293,6 +293,8 @@ function earlyWarning(tbl, clusterOf, cfg, tau) {
|
|
|
293
293
|
`в окне ${(window / 60000).toFixed(0)}м (каналов: ${channels.size})`,
|
|
294
294
|
source: "matrix",
|
|
295
295
|
channel: null,
|
|
296
|
+
entryFromPrice: evs[hi].entryFromPrice,
|
|
297
|
+
entryToPrice: evs[hi].entryToPrice,
|
|
296
298
|
};
|
|
297
299
|
if (!best || cand.confidence > best.confidence)
|
|
298
300
|
best = cand;
|
|
@@ -359,6 +361,8 @@ function singleChannelSignals(tbl, cfg, tau) {
|
|
|
359
361
|
channel: e.channel,
|
|
360
362
|
id,
|
|
361
363
|
ids: id != null ? [id] : [],
|
|
364
|
+
entryFromPrice: e.entryFromPrice,
|
|
365
|
+
entryToPrice: e.entryToPrice,
|
|
362
366
|
};
|
|
363
367
|
verdicts.push(current);
|
|
364
368
|
}
|
|
@@ -655,10 +659,19 @@ async function fetchCandlesChunked(getCandles, symbol, interval, limit, since, c
|
|
|
655
659
|
const chunk = await getCandles(symbol, interval, chunkLimit, currentSince);
|
|
656
660
|
if (!chunk || chunk.length === 0)
|
|
657
661
|
break; // край истории / дыра — отдаём собранное
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
+
for (const c of chunk)
|
|
663
|
+
all.push(c); // НЕ спред: при большом limit чанк переполнит стек
|
|
664
|
+
// ЧАСТИЧНЫЙ чанк (биржа недодала: вернула < chunkLimit, но не пусто) — двигаем
|
|
665
|
+
// since от ФАКТИЧЕСКИ последней свечи (+step), а remaining уменьшаем на реально
|
|
666
|
+
// полученное (chunk.length). Иначе since прыгает на полный chunkLimit·step, минуя
|
|
667
|
+
// недополученный хвост → дыра в склеенном ряду + недосчёт. Свечи могут прийти
|
|
668
|
+
// неотсортированными — берём max(ts), а не последний элемент.
|
|
669
|
+
let maxTs = currentSince;
|
|
670
|
+
for (const c of chunk)
|
|
671
|
+
if (c.timestamp > maxTs)
|
|
672
|
+
maxTs = c.timestamp;
|
|
673
|
+
remaining -= chunk.length;
|
|
674
|
+
currentSince = maxTs + step;
|
|
662
675
|
}
|
|
663
676
|
// дедуп по timestamp (на стыках чанков адаптер может вернуть пограничную свечу
|
|
664
677
|
// дважды). Оставляем ПЕРВОЕ вхождение: при forward-пагинации первая свеча с данным
|
|
@@ -704,10 +717,14 @@ function volumeZScore(candles, entryIdx, baselineWindow) {
|
|
|
704
717
|
* это ловушка (stop hunt / squeeze), входить опасно либо выходить раньше.
|
|
705
718
|
*/
|
|
706
719
|
function squeezePressure(candles, entryIdx, dir, horizon) {
|
|
720
|
+
// КЛАМП нижней границы (симметрично squeezePressureBefore): при отрицательном
|
|
721
|
+
// entryIdx (findIndex вернул -1, битый вызов) старт стал бы < 0 и цикл прочитал
|
|
722
|
+
// candles[-1] = undefined → краш на c.close. max(0, ...) этого не допускает.
|
|
723
|
+
const start = Math.max(0, entryIdx + 1);
|
|
707
724
|
const end = Math.min(candles.length, entryIdx + horizon + 1);
|
|
708
725
|
let againstVol = 0;
|
|
709
726
|
let totalVol = 0;
|
|
710
|
-
for (let i =
|
|
727
|
+
for (let i = start; i < end; i++) {
|
|
711
728
|
const c = candles[i];
|
|
712
729
|
const delta = c.close - c.open; // знак внутрисвечного движения
|
|
713
730
|
// «против позиции»: long не любит падение (delta<0), short не любит рост (delta>0)
|
|
@@ -932,7 +949,11 @@ const exitKey = (p) => `tt${p.trailingTake}|hs${p.hardStop}|sp${p.stalenessSince
|
|
|
932
949
|
* если не задана — точка entryFrom=entryTo=open первой свечи.
|
|
933
950
|
*/
|
|
934
951
|
async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFromPrice, entryToPrice) {
|
|
935
|
-
|
|
952
|
+
// НЕ Math.max(...arr.map()): spread-в-аргументы переполняет стек на большом наборе.
|
|
953
|
+
let maxLife = 0;
|
|
954
|
+
for (const e of exitSets)
|
|
955
|
+
if (e.staleMinutes > maxLife)
|
|
956
|
+
maxLife = e.staleMinutes;
|
|
936
957
|
// старт = первая полностью сформированная свеча ПОСЛЕ сигнала (без look-ahead):
|
|
937
958
|
// свеча, содержащая сигнал, ещё формируется — её OHLC известны только в конце минуты.
|
|
938
959
|
const since = entryStartTs(ts, "1m");
|
|
@@ -945,11 +966,15 @@ async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFrom
|
|
|
945
966
|
try {
|
|
946
967
|
candles = await fetchCandlesChunked(getCandles, symbol, "1m", limit, since);
|
|
947
968
|
}
|
|
948
|
-
catch {
|
|
949
|
-
|
|
969
|
+
catch (e) {
|
|
970
|
+
// НЕ глотаем текст: 32 одинаковых adapter-error немы без него. Сообщение
|
|
971
|
+
// (или String(e) для не-Error) уходит в meta.labeling для диагностики.
|
|
972
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
973
|
+
return { outcome: "adapter-error", burst: null, error };
|
|
974
|
+
}
|
|
975
|
+
if (!candles || candles.length === 0) {
|
|
976
|
+
return { outcome: "no-candles", burst: null };
|
|
950
977
|
}
|
|
951
|
-
if (!candles || candles.length === 0)
|
|
952
|
-
return null;
|
|
953
978
|
const from = entryFromPrice ?? candles[0].open;
|
|
954
979
|
const to = entryToPrice ?? candles[0].open;
|
|
955
980
|
const byExit = new Map();
|
|
@@ -965,9 +990,10 @@ async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFrom
|
|
|
965
990
|
byExit.set(exitKey(ex), r);
|
|
966
991
|
}
|
|
967
992
|
const anyEntered = [...byExit.values()].some((r) => r.entered);
|
|
968
|
-
if (byExit.size === 0 || !anyEntered)
|
|
969
|
-
return null;
|
|
970
|
-
|
|
993
|
+
if (byExit.size === 0 || !anyEntered) {
|
|
994
|
+
return { outcome: "no-entry", burst: null };
|
|
995
|
+
}
|
|
996
|
+
return { outcome: "ok", burst: { symbol, direction, ts, byExit } };
|
|
971
997
|
}
|
|
972
998
|
|
|
973
999
|
/**
|
|
@@ -1200,6 +1226,8 @@ function computeReliability(input, cfg = DEFAULT_RELIABILITY) {
|
|
|
1200
1226
|
const BAR_LENGTH = 30;
|
|
1201
1227
|
const BAR_FILLED_CHAR = "\u2588";
|
|
1202
1228
|
const BAR_EMPTY_CHAR = "\u2591";
|
|
1229
|
+
/** \u0424\u0438\u043a\u0441. \u0448\u0438\u0440\u0438\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430: \u0431\u0430\u0440(30) + \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u044b/\u0441\u0447\u0451\u0442\u0447\u0438\u043a/\u0444\u0430\u0437\u0430 + \u043c\u0435\u0442\u043a\u0430-\u0442\u0438\u043a\u0435\u0440. */
|
|
1230
|
+
const LINE_WIDTH = 80;
|
|
1203
1231
|
/** Дефолтный stdout-бар в стиле пользователя. */
|
|
1204
1232
|
const stdoutProgress = (e) => {
|
|
1205
1233
|
if (e.total <= 0)
|
|
@@ -1209,7 +1237,10 @@ const stdoutProgress = (e) => {
|
|
|
1209
1237
|
const filled = Math.round(ratio * BAR_LENGTH);
|
|
1210
1238
|
const empty = BAR_LENGTH - filled;
|
|
1211
1239
|
const bar = BAR_FILLED_CHAR.repeat(filled) + BAR_EMPTY_CHAR.repeat(empty);
|
|
1212
|
-
|
|
1240
|
+
// фикс. ширина: pad пробелами + slice. Иначе при более короткой новой метке
|
|
1241
|
+
// (SOLUSDT после FARTCOINUSDT) \r не стирает хвост → "BTCUSDTTUSDT".
|
|
1242
|
+
const line = `[${bar}] ${percent}% (${e.done}/${e.total}) ${e.phase} ${e.label}`;
|
|
1243
|
+
process.stdout.write("\r" + line.padEnd(LINE_WIDTH).slice(0, LINE_WIDTH));
|
|
1213
1244
|
if (e.done >= e.total)
|
|
1214
1245
|
process.stdout.write("\n");
|
|
1215
1246
|
};
|
|
@@ -1315,7 +1346,13 @@ function sharpe(returns) {
|
|
|
1315
1346
|
// (масштаб данных × machine epsilon), а НЕ относительно mean. Прошлый порог
|
|
1316
1347
|
// |mean|·1e-9 ошибочно убивал ВЫСОКИЙ Sharpe (малый std при большом mean — это и
|
|
1317
1348
|
// есть высокий Sharpe, не пыль). Масштаб = max|x|.
|
|
1318
|
-
|
|
1349
|
+
// НЕ Math.max(...returns): на длинном ряде сделок spread-в-аргументы переполняет стек.
|
|
1350
|
+
let scale = Math.abs(m);
|
|
1351
|
+
for (const x of returns) {
|
|
1352
|
+
const a = Math.abs(x);
|
|
1353
|
+
if (a > scale)
|
|
1354
|
+
scale = a;
|
|
1355
|
+
}
|
|
1319
1356
|
const dustFloor = scale * 1e-13; // ~500× machine epsilon (2.2e-16) от масштаба данных
|
|
1320
1357
|
if (s <= dustFloor)
|
|
1321
1358
|
return 0;
|
|
@@ -1798,19 +1835,34 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1798
1835
|
});
|
|
1799
1836
|
const labeledCache = new Map();
|
|
1800
1837
|
const seenCluster = new Set();
|
|
1838
|
+
// диагностика разметки: исход каждого УНИКАЛЬНОГО всплеска. dedup по (symbol|dir|ts):
|
|
1839
|
+
// один всплеск перечисляется в нескольких проходах грида — считаем исход раз, иначе
|
|
1840
|
+
// счётчики раздуты числом конфигов. Пустой fit перестаёт быть немым: тэлли скажет,
|
|
1841
|
+
// adapter-error / no-candles / no-entry это или реально ok.
|
|
1842
|
+
const outcomeTally = new Map();
|
|
1843
|
+
// уникальные тексты adapter-error → сколько раз встретились (32 одинаковых схлопнутся).
|
|
1844
|
+
const errorTally = new Map();
|
|
1845
|
+
const diagSeen = new Set();
|
|
1801
1846
|
const labelCandidates = async (cands, onTick) => {
|
|
1802
1847
|
const labeled = [];
|
|
1803
1848
|
for (const b of cands) {
|
|
1804
1849
|
const src = entryIndex.get(`${b.symbol}|${b.direction}|${b.ts}`);
|
|
1805
|
-
const
|
|
1850
|
+
const { outcome, burst, error } = await labelBurst(getCandles, b.symbol, b.direction, b.ts, exitSets, src?.entryFromPrice, src?.entryToPrice);
|
|
1806
1851
|
onTick?.(b.symbol);
|
|
1807
|
-
|
|
1852
|
+
const diagKey = `${b.symbol}|${b.direction}|${b.ts}`;
|
|
1853
|
+
if (!diagSeen.has(diagKey)) {
|
|
1854
|
+
diagSeen.add(diagKey);
|
|
1855
|
+
outcomeTally.set(outcome, (outcomeTally.get(outcome) ?? 0) + 1);
|
|
1856
|
+
if (error)
|
|
1857
|
+
errorTally.set(error, (errorTally.get(error) ?? 0) + 1);
|
|
1858
|
+
}
|
|
1859
|
+
if (!burst)
|
|
1808
1860
|
continue;
|
|
1809
1861
|
const byExit = new Map();
|
|
1810
1862
|
// veto-вход (entered=false, reason=cascade-veto) тоже несёт сигнал: его pnl=0,
|
|
1811
1863
|
// и он ДОЛЖЕН учитываться как «не вошли и не потеряли», иначе policy=veto нечестно
|
|
1812
1864
|
// сравнивать с policy=none. Поэтому храним и не-entered, помечая флагом.
|
|
1813
|
-
for (const [k, r] of
|
|
1865
|
+
for (const [k, r] of burst.byExit) {
|
|
1814
1866
|
byExit.set(k, {
|
|
1815
1867
|
pnl: r.pnl, volRegime: r.volRegime, entered: r.entered,
|
|
1816
1868
|
entryPrice: r.entryPrice, exitPrice: r.exitPrice, reason: r.reason,
|
|
@@ -1920,7 +1972,8 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1920
1972
|
foldMeans.push(valRet.length ? valRet.reduce((s, x) => s + x, 0) / valRet.length : 0);
|
|
1921
1973
|
foldWins.push(winrate(valRet));
|
|
1922
1974
|
foldSupp.push(valRet.length);
|
|
1923
|
-
|
|
1975
|
+
for (const r of valRet)
|
|
1976
|
+
allRet.push(r);
|
|
1924
1977
|
}
|
|
1925
1978
|
const avg = (a) => (a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0);
|
|
1926
1979
|
out.push({
|
|
@@ -1937,10 +1990,14 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1937
1990
|
};
|
|
1938
1991
|
// основной board — по всем данным, с прогрессом фазы score
|
|
1939
1992
|
let scoreDone = 0;
|
|
1940
|
-
board.push(...
|
|
1993
|
+
// НЕ спред (board.push(...arr)): на полном гриде arr — десятки тысяч элементов,
|
|
1994
|
+
// и spread-в-аргументы переполняет стек вызовов (Maximum call stack size exceeded).
|
|
1995
|
+
const mainBoard = buildBoard(() => true, (label) => {
|
|
1941
1996
|
scoreDone++;
|
|
1942
1997
|
progress({ done: scoreDone, total: scoreTotal, phase: "score", label });
|
|
1943
|
-
})
|
|
1998
|
+
});
|
|
1999
|
+
for (const entry of mainBoard)
|
|
2000
|
+
board.push(entry);
|
|
1944
2001
|
// ── выбор победителя: one-standard-error rule (против winner's curse) ──
|
|
1945
2002
|
// вместо argmax по cvScore берём самую КОНСЕРВАТИВНУЮ конфигурацию среди тех,
|
|
1946
2003
|
// чей score в пределах SE от максимума. Это убирает переобучение на шум grid:
|
|
@@ -2191,6 +2248,11 @@ async function train(items, getCandles, opts = {}) {
|
|
|
2191
2248
|
effectiveTrials: nTrialsEff,
|
|
2192
2249
|
innerTrials,
|
|
2193
2250
|
fitAttempts: opts.metaLedger ? fitAttemptCount(opts.metaLedger) + 1 : 1,
|
|
2251
|
+
labeling: {
|
|
2252
|
+
candidates: diagSeen.size,
|
|
2253
|
+
outcomes: Object.fromEntries(outcomeTally),
|
|
2254
|
+
errors: Object.fromEntries(errorTally),
|
|
2255
|
+
},
|
|
2194
2256
|
},
|
|
2195
2257
|
};
|
|
2196
2258
|
const leaderboard = board.slice(0, 20).map(({ config, exit, cvScore, cvWinrate, cvSupport }) => ({ config, exit, cvScore, cvWinrate, cvSupport }));
|
|
@@ -2285,6 +2347,16 @@ class PumpMatrix {
|
|
|
2285
2347
|
get fitAttempts() {
|
|
2286
2348
|
return this.params.meta.fitAttempts;
|
|
2287
2349
|
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Диагностика фазы разметки: { candidates, outcomes, errors }. Если модель пустая
|
|
2352
|
+
* (totalSamples=0), причина в outcomes по LabelOutcome: "adapter-error" (getCandles
|
|
2353
|
+
* бросает), "no-candles" (вернул пусто — символ/диапазон), "no-entry" (свечи есть,
|
|
2354
|
+
* входов в зону нет), "ok" (размечено). errors — уникальные тексты исключений
|
|
2355
|
+
* getCandles со счётчиком (чтобы adapter-error не был немым).
|
|
2356
|
+
*/
|
|
2357
|
+
get labeling() {
|
|
2358
|
+
return this.params.meta.labeling;
|
|
2359
|
+
}
|
|
2288
2360
|
/**
|
|
2289
2361
|
* Статистический сертификат: прошёл ли эдж пять барьеров (DSR ≥ 0.95, PBO ≤ 0.10,
|
|
2290
2362
|
* SPA p ≤ 0.05, N ≥ minTRL, nested OOS > 0). certified=false с reasons, если эдж
|
|
@@ -2401,7 +2473,15 @@ class PumpMatrix {
|
|
|
2401
2473
|
if (typeof source === "function") {
|
|
2402
2474
|
return this.backtestViaGetCandles(items, source, policy);
|
|
2403
2475
|
}
|
|
2404
|
-
|
|
2476
|
+
const eff = intersectPolicy(this.params.policy, policy);
|
|
2477
|
+
const out = [];
|
|
2478
|
+
for (const v of this._predict(items).signals) {
|
|
2479
|
+
// зона входа уже в вердикте (протянута из parser-item) → buildSignal её прокинет
|
|
2480
|
+
const s = this.buildSignal(v, source[v.symbol] ?? null, eff);
|
|
2481
|
+
if (s)
|
|
2482
|
+
out.push(s);
|
|
2483
|
+
}
|
|
2484
|
+
return out;
|
|
2405
2485
|
}
|
|
2406
2486
|
async backtestViaGetCandles(items, getCandles, policy) {
|
|
2407
2487
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
@@ -2436,7 +2516,11 @@ class PumpMatrix {
|
|
|
2436
2516
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
2437
2517
|
return this.buildSignalLive(v, candles, eff);
|
|
2438
2518
|
}
|
|
2439
|
-
/**
|
|
2519
|
+
/**
|
|
2520
|
+
* Бэктест под ОДНУ позицию с явным entryTs (replay вперёд, каскад по будущему).
|
|
2521
|
+
* Возвращает BacktestSignal с реализованным result. Зона входа не задаётся —
|
|
2522
|
+
* replay берёт точку = open первой свечи (как при отсутствии зоны в обучении).
|
|
2523
|
+
*/
|
|
2440
2524
|
planForAt(symbol, direction, channel, candles, entryTs, policy) {
|
|
2441
2525
|
const v = {
|
|
2442
2526
|
symbol, direction, action: "open", ts: entryTs,
|
|
@@ -2451,11 +2535,13 @@ class PumpMatrix {
|
|
|
2451
2535
|
return this._predict(items);
|
|
2452
2536
|
}
|
|
2453
2537
|
// ── общий сборщик: predict → buildSignal → отсев null (veto/не разрешено) ──
|
|
2538
|
+
// signals() — без свечей. Используем LIVE-сборку: каскад не оценивается (нет свечей),
|
|
2539
|
+
// и result не доклеивается — signals() отдаёт чистый TradeSignal, не BacktestSignal.
|
|
2454
2540
|
collect(items, candlesOf, policy) {
|
|
2455
2541
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
2456
2542
|
const out = [];
|
|
2457
2543
|
for (const v of this._predict(items).signals) {
|
|
2458
|
-
const s = this.
|
|
2544
|
+
const s = this.buildSignalLive(v, candlesOf(v), eff);
|
|
2459
2545
|
if (s)
|
|
2460
2546
|
out.push(s); // null = veto или исход не в allow → не отдаём
|
|
2461
2547
|
}
|
|
@@ -2472,10 +2558,15 @@ class PumpMatrix {
|
|
|
2472
2558
|
}
|
|
2473
2559
|
/**
|
|
2474
2560
|
* BACKTEST-сборка сигнала: каскад по свечам ПОСЛЕ входа (forward squeezePressure),
|
|
2475
|
-
* допустимо только на истории.
|
|
2561
|
+
* допустимо только на истории. Возвращает BacktestSignal с реализованным result
|
|
2562
|
+
* (replay exit-плана вперёд) — главное отличие backtest от plan. entryFrom/entryTo
|
|
2563
|
+
* — зона входа для replay (из parser-item); без свечей result.entered=false.
|
|
2476
2564
|
*/
|
|
2477
2565
|
buildSignal(v, candles, policy) {
|
|
2478
|
-
|
|
2566
|
+
const sig = this.buildSignalCore(v, candles, policy, "backtest");
|
|
2567
|
+
if (!sig)
|
|
2568
|
+
return null;
|
|
2569
|
+
return { ...sig, result: this.replayResult(sig, candles) };
|
|
2479
2570
|
}
|
|
2480
2571
|
/**
|
|
2481
2572
|
* LIVE-сборка сигнала: каскад по свечам ДО входа (backward squeezePressureBefore),
|
|
@@ -2484,6 +2575,31 @@ class PumpMatrix {
|
|
|
2484
2575
|
buildSignalLive(v, candles, policy) {
|
|
2485
2576
|
return this.buildSignalCore(v, candles, policy, "live");
|
|
2486
2577
|
}
|
|
2578
|
+
/**
|
|
2579
|
+
* Реализованный результат для backtest: replay ИТОГОВОГО (с учётом инверсии)
|
|
2580
|
+
* направления и exit-плана сигнала по свечам после входа. Зона входа из parser-item;
|
|
2581
|
+
* если не задана — точка = open первой свечи (как в обучении). Нет свечей → не вошли.
|
|
2582
|
+
*/
|
|
2583
|
+
replayResult(sig, candles) {
|
|
2584
|
+
if (!candles || candles.length === 0) {
|
|
2585
|
+
return { entered: false, pnl: 0, peak: 0, reason: "no-candles", heldMinutes: 0, entryPrice: 0, exitPrice: 0, truncated: false };
|
|
2586
|
+
}
|
|
2587
|
+
// зона входа теперь в самом сигнале (протянута из parser-item); нет → open первой свечи.
|
|
2588
|
+
const from = sig.entryFromPrice ?? candles[0].open;
|
|
2589
|
+
const to = sig.entryToPrice ?? candles[0].open;
|
|
2590
|
+
const r = replayExit(candles, sig.direction, from, to, {
|
|
2591
|
+
trailingTake: sig.exit.trailingTake,
|
|
2592
|
+
hardStop: sig.exit.hardStop,
|
|
2593
|
+
stalenessSinceProfit: sig.exit.stalenessSinceProfit,
|
|
2594
|
+
stalenessSinceMinutes: sig.exit.stalenessSinceMinutes,
|
|
2595
|
+
staleMinutes: sig.exit.impactHorizonMinutes,
|
|
2596
|
+
});
|
|
2597
|
+
return {
|
|
2598
|
+
entered: r.entered, pnl: r.pnl, peak: r.peak, reason: r.reason,
|
|
2599
|
+
heldMinutes: r.heldMinutes, entryPrice: r.entryPrice, exitPrice: r.exitPrice,
|
|
2600
|
+
truncated: r.truncated,
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2487
2603
|
/**
|
|
2488
2604
|
* Строит ЕДИНЫЙ TradeSignal из вердикта. Возвращает null, если исполнять нечего:
|
|
2489
2605
|
* каскад дал veto ИЛИ получившийся action не в allow-списке. Инверсия здесь же
|
|
@@ -2583,7 +2699,11 @@ class PumpMatrix {
|
|
|
2583
2699
|
id: v.id,
|
|
2584
2700
|
ids: v.ids,
|
|
2585
2701
|
};
|
|
2586
|
-
return {
|
|
2702
|
+
return {
|
|
2703
|
+
symbol: v.symbol, direction: finalDir, action, ts: v.ts,
|
|
2704
|
+
entryFromPrice: v.entryFromPrice, entryToPrice: v.entryToPrice,
|
|
2705
|
+
exit: plan, origin,
|
|
2706
|
+
};
|
|
2587
2707
|
}
|
|
2588
2708
|
}
|
|
2589
2709
|
|
package/build/index.mjs
CHANGED
|
@@ -291,6 +291,8 @@ function earlyWarning(tbl, clusterOf, cfg, tau) {
|
|
|
291
291
|
`в окне ${(window / 60000).toFixed(0)}м (каналов: ${channels.size})`,
|
|
292
292
|
source: "matrix",
|
|
293
293
|
channel: null,
|
|
294
|
+
entryFromPrice: evs[hi].entryFromPrice,
|
|
295
|
+
entryToPrice: evs[hi].entryToPrice,
|
|
294
296
|
};
|
|
295
297
|
if (!best || cand.confidence > best.confidence)
|
|
296
298
|
best = cand;
|
|
@@ -357,6 +359,8 @@ function singleChannelSignals(tbl, cfg, tau) {
|
|
|
357
359
|
channel: e.channel,
|
|
358
360
|
id,
|
|
359
361
|
ids: id != null ? [id] : [],
|
|
362
|
+
entryFromPrice: e.entryFromPrice,
|
|
363
|
+
entryToPrice: e.entryToPrice,
|
|
360
364
|
};
|
|
361
365
|
verdicts.push(current);
|
|
362
366
|
}
|
|
@@ -653,10 +657,19 @@ async function fetchCandlesChunked(getCandles, symbol, interval, limit, since, c
|
|
|
653
657
|
const chunk = await getCandles(symbol, interval, chunkLimit, currentSince);
|
|
654
658
|
if (!chunk || chunk.length === 0)
|
|
655
659
|
break; // край истории / дыра — отдаём собранное
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
+
for (const c of chunk)
|
|
661
|
+
all.push(c); // НЕ спред: при большом limit чанк переполнит стек
|
|
662
|
+
// ЧАСТИЧНЫЙ чанк (биржа недодала: вернула < chunkLimit, но не пусто) — двигаем
|
|
663
|
+
// since от ФАКТИЧЕСКИ последней свечи (+step), а remaining уменьшаем на реально
|
|
664
|
+
// полученное (chunk.length). Иначе since прыгает на полный chunkLimit·step, минуя
|
|
665
|
+
// недополученный хвост → дыра в склеенном ряду + недосчёт. Свечи могут прийти
|
|
666
|
+
// неотсортированными — берём max(ts), а не последний элемент.
|
|
667
|
+
let maxTs = currentSince;
|
|
668
|
+
for (const c of chunk)
|
|
669
|
+
if (c.timestamp > maxTs)
|
|
670
|
+
maxTs = c.timestamp;
|
|
671
|
+
remaining -= chunk.length;
|
|
672
|
+
currentSince = maxTs + step;
|
|
660
673
|
}
|
|
661
674
|
// дедуп по timestamp (на стыках чанков адаптер может вернуть пограничную свечу
|
|
662
675
|
// дважды). Оставляем ПЕРВОЕ вхождение: при forward-пагинации первая свеча с данным
|
|
@@ -702,10 +715,14 @@ function volumeZScore(candles, entryIdx, baselineWindow) {
|
|
|
702
715
|
* это ловушка (stop hunt / squeeze), входить опасно либо выходить раньше.
|
|
703
716
|
*/
|
|
704
717
|
function squeezePressure(candles, entryIdx, dir, horizon) {
|
|
718
|
+
// КЛАМП нижней границы (симметрично squeezePressureBefore): при отрицательном
|
|
719
|
+
// entryIdx (findIndex вернул -1, битый вызов) старт стал бы < 0 и цикл прочитал
|
|
720
|
+
// candles[-1] = undefined → краш на c.close. max(0, ...) этого не допускает.
|
|
721
|
+
const start = Math.max(0, entryIdx + 1);
|
|
705
722
|
const end = Math.min(candles.length, entryIdx + horizon + 1);
|
|
706
723
|
let againstVol = 0;
|
|
707
724
|
let totalVol = 0;
|
|
708
|
-
for (let i =
|
|
725
|
+
for (let i = start; i < end; i++) {
|
|
709
726
|
const c = candles[i];
|
|
710
727
|
const delta = c.close - c.open; // знак внутрисвечного движения
|
|
711
728
|
// «против позиции»: long не любит падение (delta<0), short не любит рост (delta>0)
|
|
@@ -930,7 +947,11 @@ const exitKey = (p) => `tt${p.trailingTake}|hs${p.hardStop}|sp${p.stalenessSince
|
|
|
930
947
|
* если не задана — точка entryFrom=entryTo=open первой свечи.
|
|
931
948
|
*/
|
|
932
949
|
async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFromPrice, entryToPrice) {
|
|
933
|
-
|
|
950
|
+
// НЕ Math.max(...arr.map()): spread-в-аргументы переполняет стек на большом наборе.
|
|
951
|
+
let maxLife = 0;
|
|
952
|
+
for (const e of exitSets)
|
|
953
|
+
if (e.staleMinutes > maxLife)
|
|
954
|
+
maxLife = e.staleMinutes;
|
|
934
955
|
// старт = первая полностью сформированная свеча ПОСЛЕ сигнала (без look-ahead):
|
|
935
956
|
// свеча, содержащая сигнал, ещё формируется — её OHLC известны только в конце минуты.
|
|
936
957
|
const since = entryStartTs(ts, "1m");
|
|
@@ -943,11 +964,15 @@ async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFrom
|
|
|
943
964
|
try {
|
|
944
965
|
candles = await fetchCandlesChunked(getCandles, symbol, "1m", limit, since);
|
|
945
966
|
}
|
|
946
|
-
catch {
|
|
947
|
-
|
|
967
|
+
catch (e) {
|
|
968
|
+
// НЕ глотаем текст: 32 одинаковых adapter-error немы без него. Сообщение
|
|
969
|
+
// (или String(e) для не-Error) уходит в meta.labeling для диагностики.
|
|
970
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
971
|
+
return { outcome: "adapter-error", burst: null, error };
|
|
972
|
+
}
|
|
973
|
+
if (!candles || candles.length === 0) {
|
|
974
|
+
return { outcome: "no-candles", burst: null };
|
|
948
975
|
}
|
|
949
|
-
if (!candles || candles.length === 0)
|
|
950
|
-
return null;
|
|
951
976
|
const from = entryFromPrice ?? candles[0].open;
|
|
952
977
|
const to = entryToPrice ?? candles[0].open;
|
|
953
978
|
const byExit = new Map();
|
|
@@ -963,9 +988,10 @@ async function labelBurst(getCandles, symbol, direction, ts, exitSets, entryFrom
|
|
|
963
988
|
byExit.set(exitKey(ex), r);
|
|
964
989
|
}
|
|
965
990
|
const anyEntered = [...byExit.values()].some((r) => r.entered);
|
|
966
|
-
if (byExit.size === 0 || !anyEntered)
|
|
967
|
-
return null;
|
|
968
|
-
|
|
991
|
+
if (byExit.size === 0 || !anyEntered) {
|
|
992
|
+
return { outcome: "no-entry", burst: null };
|
|
993
|
+
}
|
|
994
|
+
return { outcome: "ok", burst: { symbol, direction, ts, byExit } };
|
|
969
995
|
}
|
|
970
996
|
|
|
971
997
|
/**
|
|
@@ -1198,6 +1224,8 @@ function computeReliability(input, cfg = DEFAULT_RELIABILITY) {
|
|
|
1198
1224
|
const BAR_LENGTH = 30;
|
|
1199
1225
|
const BAR_FILLED_CHAR = "\u2588";
|
|
1200
1226
|
const BAR_EMPTY_CHAR = "\u2591";
|
|
1227
|
+
/** \u0424\u0438\u043a\u0441. \u0448\u0438\u0440\u0438\u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430: \u0431\u0430\u0440(30) + \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u044b/\u0441\u0447\u0451\u0442\u0447\u0438\u043a/\u0444\u0430\u0437\u0430 + \u043c\u0435\u0442\u043a\u0430-\u0442\u0438\u043a\u0435\u0440. */
|
|
1228
|
+
const LINE_WIDTH = 80;
|
|
1201
1229
|
/** Дефолтный stdout-бар в стиле пользователя. */
|
|
1202
1230
|
const stdoutProgress = (e) => {
|
|
1203
1231
|
if (e.total <= 0)
|
|
@@ -1207,7 +1235,10 @@ const stdoutProgress = (e) => {
|
|
|
1207
1235
|
const filled = Math.round(ratio * BAR_LENGTH);
|
|
1208
1236
|
const empty = BAR_LENGTH - filled;
|
|
1209
1237
|
const bar = BAR_FILLED_CHAR.repeat(filled) + BAR_EMPTY_CHAR.repeat(empty);
|
|
1210
|
-
|
|
1238
|
+
// фикс. ширина: pad пробелами + slice. Иначе при более короткой новой метке
|
|
1239
|
+
// (SOLUSDT после FARTCOINUSDT) \r не стирает хвост → "BTCUSDTTUSDT".
|
|
1240
|
+
const line = `[${bar}] ${percent}% (${e.done}/${e.total}) ${e.phase} ${e.label}`;
|
|
1241
|
+
process.stdout.write("\r" + line.padEnd(LINE_WIDTH).slice(0, LINE_WIDTH));
|
|
1211
1242
|
if (e.done >= e.total)
|
|
1212
1243
|
process.stdout.write("\n");
|
|
1213
1244
|
};
|
|
@@ -1313,7 +1344,13 @@ function sharpe(returns) {
|
|
|
1313
1344
|
// (масштаб данных × machine epsilon), а НЕ относительно mean. Прошлый порог
|
|
1314
1345
|
// |mean|·1e-9 ошибочно убивал ВЫСОКИЙ Sharpe (малый std при большом mean — это и
|
|
1315
1346
|
// есть высокий Sharpe, не пыль). Масштаб = max|x|.
|
|
1316
|
-
|
|
1347
|
+
// НЕ Math.max(...returns): на длинном ряде сделок spread-в-аргументы переполняет стек.
|
|
1348
|
+
let scale = Math.abs(m);
|
|
1349
|
+
for (const x of returns) {
|
|
1350
|
+
const a = Math.abs(x);
|
|
1351
|
+
if (a > scale)
|
|
1352
|
+
scale = a;
|
|
1353
|
+
}
|
|
1317
1354
|
const dustFloor = scale * 1e-13; // ~500× machine epsilon (2.2e-16) от масштаба данных
|
|
1318
1355
|
if (s <= dustFloor)
|
|
1319
1356
|
return 0;
|
|
@@ -1796,19 +1833,34 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1796
1833
|
});
|
|
1797
1834
|
const labeledCache = new Map();
|
|
1798
1835
|
const seenCluster = new Set();
|
|
1836
|
+
// диагностика разметки: исход каждого УНИКАЛЬНОГО всплеска. dedup по (symbol|dir|ts):
|
|
1837
|
+
// один всплеск перечисляется в нескольких проходах грида — считаем исход раз, иначе
|
|
1838
|
+
// счётчики раздуты числом конфигов. Пустой fit перестаёт быть немым: тэлли скажет,
|
|
1839
|
+
// adapter-error / no-candles / no-entry это или реально ok.
|
|
1840
|
+
const outcomeTally = new Map();
|
|
1841
|
+
// уникальные тексты adapter-error → сколько раз встретились (32 одинаковых схлопнутся).
|
|
1842
|
+
const errorTally = new Map();
|
|
1843
|
+
const diagSeen = new Set();
|
|
1799
1844
|
const labelCandidates = async (cands, onTick) => {
|
|
1800
1845
|
const labeled = [];
|
|
1801
1846
|
for (const b of cands) {
|
|
1802
1847
|
const src = entryIndex.get(`${b.symbol}|${b.direction}|${b.ts}`);
|
|
1803
|
-
const
|
|
1848
|
+
const { outcome, burst, error } = await labelBurst(getCandles, b.symbol, b.direction, b.ts, exitSets, src?.entryFromPrice, src?.entryToPrice);
|
|
1804
1849
|
onTick?.(b.symbol);
|
|
1805
|
-
|
|
1850
|
+
const diagKey = `${b.symbol}|${b.direction}|${b.ts}`;
|
|
1851
|
+
if (!diagSeen.has(diagKey)) {
|
|
1852
|
+
diagSeen.add(diagKey);
|
|
1853
|
+
outcomeTally.set(outcome, (outcomeTally.get(outcome) ?? 0) + 1);
|
|
1854
|
+
if (error)
|
|
1855
|
+
errorTally.set(error, (errorTally.get(error) ?? 0) + 1);
|
|
1856
|
+
}
|
|
1857
|
+
if (!burst)
|
|
1806
1858
|
continue;
|
|
1807
1859
|
const byExit = new Map();
|
|
1808
1860
|
// veto-вход (entered=false, reason=cascade-veto) тоже несёт сигнал: его pnl=0,
|
|
1809
1861
|
// и он ДОЛЖЕН учитываться как «не вошли и не потеряли», иначе policy=veto нечестно
|
|
1810
1862
|
// сравнивать с policy=none. Поэтому храним и не-entered, помечая флагом.
|
|
1811
|
-
for (const [k, r] of
|
|
1863
|
+
for (const [k, r] of burst.byExit) {
|
|
1812
1864
|
byExit.set(k, {
|
|
1813
1865
|
pnl: r.pnl, volRegime: r.volRegime, entered: r.entered,
|
|
1814
1866
|
entryPrice: r.entryPrice, exitPrice: r.exitPrice, reason: r.reason,
|
|
@@ -1918,7 +1970,8 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1918
1970
|
foldMeans.push(valRet.length ? valRet.reduce((s, x) => s + x, 0) / valRet.length : 0);
|
|
1919
1971
|
foldWins.push(winrate(valRet));
|
|
1920
1972
|
foldSupp.push(valRet.length);
|
|
1921
|
-
|
|
1973
|
+
for (const r of valRet)
|
|
1974
|
+
allRet.push(r);
|
|
1922
1975
|
}
|
|
1923
1976
|
const avg = (a) => (a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0);
|
|
1924
1977
|
out.push({
|
|
@@ -1935,10 +1988,14 @@ async function train(items, getCandles, opts = {}) {
|
|
|
1935
1988
|
};
|
|
1936
1989
|
// основной board — по всем данным, с прогрессом фазы score
|
|
1937
1990
|
let scoreDone = 0;
|
|
1938
|
-
board.push(...
|
|
1991
|
+
// НЕ спред (board.push(...arr)): на полном гриде arr — десятки тысяч элементов,
|
|
1992
|
+
// и spread-в-аргументы переполняет стек вызовов (Maximum call stack size exceeded).
|
|
1993
|
+
const mainBoard = buildBoard(() => true, (label) => {
|
|
1939
1994
|
scoreDone++;
|
|
1940
1995
|
progress({ done: scoreDone, total: scoreTotal, phase: "score", label });
|
|
1941
|
-
})
|
|
1996
|
+
});
|
|
1997
|
+
for (const entry of mainBoard)
|
|
1998
|
+
board.push(entry);
|
|
1942
1999
|
// ── выбор победителя: one-standard-error rule (против winner's curse) ──
|
|
1943
2000
|
// вместо argmax по cvScore берём самую КОНСЕРВАТИВНУЮ конфигурацию среди тех,
|
|
1944
2001
|
// чей score в пределах SE от максимума. Это убирает переобучение на шум grid:
|
|
@@ -2189,6 +2246,11 @@ async function train(items, getCandles, opts = {}) {
|
|
|
2189
2246
|
effectiveTrials: nTrialsEff,
|
|
2190
2247
|
innerTrials,
|
|
2191
2248
|
fitAttempts: opts.metaLedger ? fitAttemptCount(opts.metaLedger) + 1 : 1,
|
|
2249
|
+
labeling: {
|
|
2250
|
+
candidates: diagSeen.size,
|
|
2251
|
+
outcomes: Object.fromEntries(outcomeTally),
|
|
2252
|
+
errors: Object.fromEntries(errorTally),
|
|
2253
|
+
},
|
|
2192
2254
|
},
|
|
2193
2255
|
};
|
|
2194
2256
|
const leaderboard = board.slice(0, 20).map(({ config, exit, cvScore, cvWinrate, cvSupport }) => ({ config, exit, cvScore, cvWinrate, cvSupport }));
|
|
@@ -2283,6 +2345,16 @@ class PumpMatrix {
|
|
|
2283
2345
|
get fitAttempts() {
|
|
2284
2346
|
return this.params.meta.fitAttempts;
|
|
2285
2347
|
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Диагностика фазы разметки: { candidates, outcomes, errors }. Если модель пустая
|
|
2350
|
+
* (totalSamples=0), причина в outcomes по LabelOutcome: "adapter-error" (getCandles
|
|
2351
|
+
* бросает), "no-candles" (вернул пусто — символ/диапазон), "no-entry" (свечи есть,
|
|
2352
|
+
* входов в зону нет), "ok" (размечено). errors — уникальные тексты исключений
|
|
2353
|
+
* getCandles со счётчиком (чтобы adapter-error не был немым).
|
|
2354
|
+
*/
|
|
2355
|
+
get labeling() {
|
|
2356
|
+
return this.params.meta.labeling;
|
|
2357
|
+
}
|
|
2286
2358
|
/**
|
|
2287
2359
|
* Статистический сертификат: прошёл ли эдж пять барьеров (DSR ≥ 0.95, PBO ≤ 0.10,
|
|
2288
2360
|
* SPA p ≤ 0.05, N ≥ minTRL, nested OOS > 0). certified=false с reasons, если эдж
|
|
@@ -2399,7 +2471,15 @@ class PumpMatrix {
|
|
|
2399
2471
|
if (typeof source === "function") {
|
|
2400
2472
|
return this.backtestViaGetCandles(items, source, policy);
|
|
2401
2473
|
}
|
|
2402
|
-
|
|
2474
|
+
const eff = intersectPolicy(this.params.policy, policy);
|
|
2475
|
+
const out = [];
|
|
2476
|
+
for (const v of this._predict(items).signals) {
|
|
2477
|
+
// зона входа уже в вердикте (протянута из parser-item) → buildSignal её прокинет
|
|
2478
|
+
const s = this.buildSignal(v, source[v.symbol] ?? null, eff);
|
|
2479
|
+
if (s)
|
|
2480
|
+
out.push(s);
|
|
2481
|
+
}
|
|
2482
|
+
return out;
|
|
2403
2483
|
}
|
|
2404
2484
|
async backtestViaGetCandles(items, getCandles, policy) {
|
|
2405
2485
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
@@ -2434,7 +2514,11 @@ class PumpMatrix {
|
|
|
2434
2514
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
2435
2515
|
return this.buildSignalLive(v, candles, eff);
|
|
2436
2516
|
}
|
|
2437
|
-
/**
|
|
2517
|
+
/**
|
|
2518
|
+
* Бэктест под ОДНУ позицию с явным entryTs (replay вперёд, каскад по будущему).
|
|
2519
|
+
* Возвращает BacktestSignal с реализованным result. Зона входа не задаётся —
|
|
2520
|
+
* replay берёт точку = open первой свечи (как при отсутствии зоны в обучении).
|
|
2521
|
+
*/
|
|
2438
2522
|
planForAt(symbol, direction, channel, candles, entryTs, policy) {
|
|
2439
2523
|
const v = {
|
|
2440
2524
|
symbol, direction, action: "open", ts: entryTs,
|
|
@@ -2449,11 +2533,13 @@ class PumpMatrix {
|
|
|
2449
2533
|
return this._predict(items);
|
|
2450
2534
|
}
|
|
2451
2535
|
// ── общий сборщик: predict → buildSignal → отсев null (veto/не разрешено) ──
|
|
2536
|
+
// signals() — без свечей. Используем LIVE-сборку: каскад не оценивается (нет свечей),
|
|
2537
|
+
// и result не доклеивается — signals() отдаёт чистый TradeSignal, не BacktestSignal.
|
|
2452
2538
|
collect(items, candlesOf, policy) {
|
|
2453
2539
|
const eff = intersectPolicy(this.params.policy, policy);
|
|
2454
2540
|
const out = [];
|
|
2455
2541
|
for (const v of this._predict(items).signals) {
|
|
2456
|
-
const s = this.
|
|
2542
|
+
const s = this.buildSignalLive(v, candlesOf(v), eff);
|
|
2457
2543
|
if (s)
|
|
2458
2544
|
out.push(s); // null = veto или исход не в allow → не отдаём
|
|
2459
2545
|
}
|
|
@@ -2470,10 +2556,15 @@ class PumpMatrix {
|
|
|
2470
2556
|
}
|
|
2471
2557
|
/**
|
|
2472
2558
|
* BACKTEST-сборка сигнала: каскад по свечам ПОСЛЕ входа (forward squeezePressure),
|
|
2473
|
-
* допустимо только на истории.
|
|
2559
|
+
* допустимо только на истории. Возвращает BacktestSignal с реализованным result
|
|
2560
|
+
* (replay exit-плана вперёд) — главное отличие backtest от plan. entryFrom/entryTo
|
|
2561
|
+
* — зона входа для replay (из parser-item); без свечей result.entered=false.
|
|
2474
2562
|
*/
|
|
2475
2563
|
buildSignal(v, candles, policy) {
|
|
2476
|
-
|
|
2564
|
+
const sig = this.buildSignalCore(v, candles, policy, "backtest");
|
|
2565
|
+
if (!sig)
|
|
2566
|
+
return null;
|
|
2567
|
+
return { ...sig, result: this.replayResult(sig, candles) };
|
|
2477
2568
|
}
|
|
2478
2569
|
/**
|
|
2479
2570
|
* LIVE-сборка сигнала: каскад по свечам ДО входа (backward squeezePressureBefore),
|
|
@@ -2482,6 +2573,31 @@ class PumpMatrix {
|
|
|
2482
2573
|
buildSignalLive(v, candles, policy) {
|
|
2483
2574
|
return this.buildSignalCore(v, candles, policy, "live");
|
|
2484
2575
|
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Реализованный результат для backtest: replay ИТОГОВОГО (с учётом инверсии)
|
|
2578
|
+
* направления и exit-плана сигнала по свечам после входа. Зона входа из parser-item;
|
|
2579
|
+
* если не задана — точка = open первой свечи (как в обучении). Нет свечей → не вошли.
|
|
2580
|
+
*/
|
|
2581
|
+
replayResult(sig, candles) {
|
|
2582
|
+
if (!candles || candles.length === 0) {
|
|
2583
|
+
return { entered: false, pnl: 0, peak: 0, reason: "no-candles", heldMinutes: 0, entryPrice: 0, exitPrice: 0, truncated: false };
|
|
2584
|
+
}
|
|
2585
|
+
// зона входа теперь в самом сигнале (протянута из parser-item); нет → open первой свечи.
|
|
2586
|
+
const from = sig.entryFromPrice ?? candles[0].open;
|
|
2587
|
+
const to = sig.entryToPrice ?? candles[0].open;
|
|
2588
|
+
const r = replayExit(candles, sig.direction, from, to, {
|
|
2589
|
+
trailingTake: sig.exit.trailingTake,
|
|
2590
|
+
hardStop: sig.exit.hardStop,
|
|
2591
|
+
stalenessSinceProfit: sig.exit.stalenessSinceProfit,
|
|
2592
|
+
stalenessSinceMinutes: sig.exit.stalenessSinceMinutes,
|
|
2593
|
+
staleMinutes: sig.exit.impactHorizonMinutes,
|
|
2594
|
+
});
|
|
2595
|
+
return {
|
|
2596
|
+
entered: r.entered, pnl: r.pnl, peak: r.peak, reason: r.reason,
|
|
2597
|
+
heldMinutes: r.heldMinutes, entryPrice: r.entryPrice, exitPrice: r.exitPrice,
|
|
2598
|
+
truncated: r.truncated,
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2485
2601
|
/**
|
|
2486
2602
|
* Строит ЕДИНЫЙ TradeSignal из вердикта. Возвращает null, если исполнять нечего:
|
|
2487
2603
|
* каскад дал veto ИЛИ получившийся action не в allow-списке. Инверсия здесь же
|
|
@@ -2581,7 +2697,11 @@ class PumpMatrix {
|
|
|
2581
2697
|
id: v.id,
|
|
2582
2698
|
ids: v.ids,
|
|
2583
2699
|
};
|
|
2584
|
-
return {
|
|
2700
|
+
return {
|
|
2701
|
+
symbol: v.symbol, direction: finalDir, action, ts: v.ts,
|
|
2702
|
+
entryFromPrice: v.entryFromPrice, entryToPrice: v.entryToPrice,
|
|
2703
|
+
exit: plan, origin,
|
|
2704
|
+
};
|
|
2585
2705
|
}
|
|
2586
2706
|
}
|
|
2587
2707
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pump-anomaly",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Detector of synchronized trading pump signals via author-cluster deduplication, with path-aware exit replay and liquidation-cascade detection.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Petr Tripolsky",
|
|
7
|
+
"email": "tripolskypetr@gmail.com",
|
|
8
|
+
"url": "https://github.com/tripolskypetr"
|
|
9
|
+
},
|
|
10
|
+
"funding": {
|
|
11
|
+
"type": "individual",
|
|
12
|
+
"url": "http://paypal.me/tripolskypetr"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
|
|
5
16
|
"keywords": [
|
|
6
17
|
"trading",
|
|
7
18
|
"pump",
|
|
@@ -19,7 +30,6 @@
|
|
|
19
30
|
"typescript"
|
|
20
31
|
],
|
|
21
32
|
"type": "module",
|
|
22
|
-
"license": "MIT",
|
|
23
33
|
"main": "./build/index.cjs",
|
|
24
34
|
"module": "./build/index.mjs",
|
|
25
35
|
"types": "./types.d.ts",
|
package/types.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ interface ParserItem {
|
|
|
36
36
|
entryFromPrice?: number;
|
|
37
37
|
/** верхняя граница зоны входа. */
|
|
38
38
|
entryToPrice?: number;
|
|
39
|
+
/** идентификатор исходного поста — протягивается до dump() и origin live-сигнала для сопоставления с парсингом. */
|
|
40
|
+
id?: string | number;
|
|
39
41
|
[extra: string]: unknown;
|
|
40
42
|
}
|
|
41
43
|
/** Нормализованное событие, с которым работают внутренние слои. */
|
|
@@ -67,6 +69,9 @@ interface PumpVerdict {
|
|
|
67
69
|
id?: string;
|
|
68
70
|
/** id всех parser-item, вошедших в сигнал */
|
|
69
71
|
ids?: string[];
|
|
72
|
+
/** зона входа из parser-item — нужна для открытия live-позиции */
|
|
73
|
+
entryFromPrice?: number;
|
|
74
|
+
entryToPrice?: number;
|
|
70
75
|
}
|
|
71
76
|
/** Карта авторства: канал → id кластера-автора. */
|
|
72
77
|
type AuthorMap = Map<string, number>;
|
|
@@ -473,6 +478,23 @@ interface LabeledBurst {
|
|
|
473
478
|
/** ключ exit-набора → результат replay */
|
|
474
479
|
byExit: Map<string, ReplayResult>;
|
|
475
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Исход разметки одного кандидата. Диагностика «немых» пустых fit: пустой результат
|
|
483
|
+
* выглядит одинаково для «нет данных» и «нет входов», а это РАЗНЫЕ проблемы (битый
|
|
484
|
+
* getCandles vs реально не было входов в зону).
|
|
485
|
+
* - ok — размечен, есть вход (burst != null);
|
|
486
|
+
* - adapter-error — getCandles бросил (look-ahead guard / дыра / count-mismatch);
|
|
487
|
+
* - no-candles — getCandles вернул пусто (символ/диапазон не дали свечей);
|
|
488
|
+
* - no-entry — свечи есть, но ни один exit-набор не вошёл в зону (или все truncated).
|
|
489
|
+
*/
|
|
490
|
+
type LabelOutcome = "ok" | "adapter-error" | "no-candles" | "no-entry";
|
|
491
|
+
/** Результат labelBurst: типизированный исход + сам размеченный всплеск (null кроме ok). */
|
|
492
|
+
interface LabelResult {
|
|
493
|
+
outcome: LabelOutcome;
|
|
494
|
+
burst: LabeledBurst | null;
|
|
495
|
+
/** текст брошенного getCandles исключения (только при outcome="adapter-error"). */
|
|
496
|
+
error?: string;
|
|
497
|
+
}
|
|
476
498
|
/** Стабильный строковый ключ exit-набора для кэша/grid. */
|
|
477
499
|
declare const exitKey: (p: ExitParams) => string;
|
|
478
500
|
/**
|
|
@@ -480,7 +502,7 @@ declare const exitKey: (p: ExitParams) => string;
|
|
|
480
502
|
* прогоняет каждый exit-набор через replay. Зона входа берётся из события;
|
|
481
503
|
* если не задана — точка entryFrom=entryTo=open первой свечи.
|
|
482
504
|
*/
|
|
483
|
-
declare function labelBurst(getCandles: GetCandles, symbol: string, direction: Direction, ts: number, exitSets: ExitParams[], entryFromPrice?: number, entryToPrice?: number): Promise<
|
|
505
|
+
declare function labelBurst(getCandles: GetCandles, symbol: string, direction: Direction, ts: number, exitSets: ExitParams[], entryFromPrice?: number, entryToPrice?: number): Promise<LabelResult>;
|
|
484
506
|
|
|
485
507
|
/** Максимум свечей в одном чанке (как CC_MAX_CANDLES_PER_REQUEST в проде). */
|
|
486
508
|
declare const MAX_CANDLES_PER_CHUNK = 500;
|
|
@@ -711,11 +733,46 @@ interface TradeSignal {
|
|
|
711
733
|
action: SignalAction;
|
|
712
734
|
/** unix-время сигнала, мс */
|
|
713
735
|
ts: number;
|
|
736
|
+
/** нижняя граница зоны входа из parser-item (для открытия live-позиции; undefined = вход по рынку) */
|
|
737
|
+
entryFromPrice?: number;
|
|
738
|
+
/** верхняя граница зоны входа из parser-item */
|
|
739
|
+
entryToPrice?: number;
|
|
714
740
|
/** готовый exit-план */
|
|
715
741
|
exit: ExitPlan;
|
|
716
742
|
/** происхождение (аудит), не для ветвления */
|
|
717
743
|
origin: SignalOrigin;
|
|
718
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Реализованный результат сделки — РЕПЛЕЙ exit-плана по свечам ПОСЛЕ входа.
|
|
747
|
+
* Существует только в backtest (forward-replay по закрытой истории); plan/signals
|
|
748
|
+
* его НЕ дают (там позиция ещё не закрыта). pnl/peak в долях (0.05 = +5%).
|
|
749
|
+
*/
|
|
750
|
+
interface BacktestResult {
|
|
751
|
+
/** вошли ли в позицию (false → зона входа не задета на окне свечей) */
|
|
752
|
+
entered: boolean;
|
|
753
|
+
/** реализованный pnl, доля (при hard-stop = честный -hardStop%) */
|
|
754
|
+
pnl: number;
|
|
755
|
+
/** пиковый pnl за жизнь позиции, доля */
|
|
756
|
+
peak: number;
|
|
757
|
+
/** причина выхода (hard-stop / trailing-take / peak-staleness / life-cap / …) */
|
|
758
|
+
reason: string;
|
|
759
|
+
/** минут от входа до выхода */
|
|
760
|
+
heldMinutes: number;
|
|
761
|
+
/** цена входа (0 если не вошли) */
|
|
762
|
+
entryPrice: number;
|
|
763
|
+
/** цена выхода (0 если не вошли) */
|
|
764
|
+
exitPrice: number;
|
|
765
|
+
/** замер неполный: после входа не хватило свечей на полный life-cap */
|
|
766
|
+
truncated: boolean;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Сигнал backtest = TradeSignal + реализованный result. Тип-потомок: главное
|
|
770
|
+
* отличие backtest от plan — он РЕПЛЕИТ позицию вперёд и возвращает realized pnl.
|
|
771
|
+
* Сигнатура backtest() возвращает именно его, поэтому pnl виден без джойна с dump().
|
|
772
|
+
*/
|
|
773
|
+
interface BacktestSignal extends TradeSignal {
|
|
774
|
+
result: BacktestResult;
|
|
775
|
+
}
|
|
719
776
|
/**
|
|
720
777
|
* Политика разрешённых исходов (allow-список).
|
|
721
778
|
*
|
|
@@ -1130,6 +1187,19 @@ interface TrainedParams {
|
|
|
1130
1187
|
innerTrials: number;
|
|
1131
1188
|
/** сколько раз всего запускался fit (для прозрачности мета-перебора) */
|
|
1132
1189
|
fitAttempts: number;
|
|
1190
|
+
/**
|
|
1191
|
+
* Диагностика фазы разметки: сколько УНИКАЛЬНЫХ кандидатов-всплесков и во что они
|
|
1192
|
+
* вылились (outcomes по LabelOutcome — присутствуют только ненулевые исходы).
|
|
1193
|
+
* totalSamples=0 при candidates>0 указывает причину: "adapter-error" — getCandles
|
|
1194
|
+
* бросает (look-ahead/дыра/символ); "no-candles" — пусто (символ/диапазон);
|
|
1195
|
+
* "no-entry" — свечи есть, но входов в зону нет. Без этого пустой fit немой.
|
|
1196
|
+
*/
|
|
1197
|
+
labeling: {
|
|
1198
|
+
candidates: number;
|
|
1199
|
+
outcomes: Partial<Record<LabelOutcome, number>>;
|
|
1200
|
+
/** уникальные тексты getCandles-исключений → счётчик (для adapter-error). */
|
|
1201
|
+
errors: Record<string, number>;
|
|
1202
|
+
};
|
|
1133
1203
|
};
|
|
1134
1204
|
}
|
|
1135
1205
|
interface TrainResult {
|
|
@@ -1209,6 +1279,18 @@ declare class PumpMatrix {
|
|
|
1209
1279
|
get innerTrials(): number;
|
|
1210
1280
|
/** Сколько раз всего запускался fit (прозрачность мета-перебора). */
|
|
1211
1281
|
get fitAttempts(): number;
|
|
1282
|
+
/**
|
|
1283
|
+
* Диагностика фазы разметки: { candidates, outcomes, errors }. Если модель пустая
|
|
1284
|
+
* (totalSamples=0), причина в outcomes по LabelOutcome: "adapter-error" (getCandles
|
|
1285
|
+
* бросает), "no-candles" (вернул пусто — символ/диапазон), "no-entry" (свечи есть,
|
|
1286
|
+
* входов в зону нет), "ok" (размечено). errors — уникальные тексты исключений
|
|
1287
|
+
* getCandles со счётчиком (чтобы adapter-error не был немым).
|
|
1288
|
+
*/
|
|
1289
|
+
get labeling(): {
|
|
1290
|
+
candidates: number;
|
|
1291
|
+
outcomes: Partial<Record<LabelOutcome, number>>;
|
|
1292
|
+
errors: Record<string, number>;
|
|
1293
|
+
};
|
|
1212
1294
|
/**
|
|
1213
1295
|
* Статистический сертификат: прошёл ли эдж пять барьеров (DSR ≥ 0.95, PBO ≤ 0.10,
|
|
1214
1296
|
* SPA p ≤ 0.05, N ≥ minTRL, nested OOS > 0). certified=false с reasons, если эдж
|
|
@@ -1291,20 +1373,26 @@ declare class PumpMatrix {
|
|
|
1291
1373
|
*
|
|
1292
1374
|
* Источник свечей — getCandles (async) или словарь {symbol: candles} (sync).
|
|
1293
1375
|
*/
|
|
1294
|
-
backtest(items: ParserItem[], getCandles: GetCandles, policy?: Partial<SignalPolicy>): Promise<
|
|
1295
|
-
backtest(items: ParserItem[], candlesBySymbol: Record<string, ICandleData[]>, policy?: Partial<SignalPolicy>):
|
|
1376
|
+
backtest(items: ParserItem[], getCandles: GetCandles, policy?: Partial<SignalPolicy>): Promise<BacktestSignal[]>;
|
|
1377
|
+
backtest(items: ParserItem[], candlesBySymbol: Record<string, ICandleData[]>, policy?: Partial<SignalPolicy>): BacktestSignal[];
|
|
1296
1378
|
private backtestViaGetCandles;
|
|
1297
1379
|
/** Точечно под ОДНУ позицию в LIVE (вход = последняя свеча, каскад по прошлому). */
|
|
1298
1380
|
planFor(symbol: string, direction: Direction, channel: string | null, candles: ICandleData[], policy?: Partial<SignalPolicy>): TradeSignal | null;
|
|
1299
|
-
/**
|
|
1300
|
-
|
|
1381
|
+
/**
|
|
1382
|
+
* Бэктест под ОДНУ позицию с явным entryTs (replay вперёд, каскад по будущему).
|
|
1383
|
+
* Возвращает BacktestSignal с реализованным result. Зона входа не задаётся —
|
|
1384
|
+
* replay берёт точку = open первой свечи (как при отсутствии зоны в обучении).
|
|
1385
|
+
*/
|
|
1386
|
+
planForAt(symbol: string, direction: Direction, channel: string | null, candles: ICandleData[], entryTs: number, policy?: Partial<SignalPolicy>): BacktestSignal | null;
|
|
1301
1387
|
/** Полный отчёт (все вердикты + карта авторства) — для разбора. */
|
|
1302
1388
|
explain(items: ParserItem[]): PredictionResult;
|
|
1303
1389
|
private collect;
|
|
1304
1390
|
private flatExit;
|
|
1305
1391
|
/**
|
|
1306
1392
|
* BACKTEST-сборка сигнала: каскад по свечам ПОСЛЕ входа (forward squeezePressure),
|
|
1307
|
-
* допустимо только на истории.
|
|
1393
|
+
* допустимо только на истории. Возвращает BacktestSignal с реализованным result
|
|
1394
|
+
* (replay exit-плана вперёд) — главное отличие backtest от plan. entryFrom/entryTo
|
|
1395
|
+
* — зона входа для replay (из parser-item); без свечей result.entered=false.
|
|
1308
1396
|
*/
|
|
1309
1397
|
private buildSignal;
|
|
1310
1398
|
/**
|
|
@@ -1312,6 +1400,12 @@ declare class PumpMatrix {
|
|
|
1312
1400
|
* БЕЗ look-ahead. Делегирует в общее ядро с mode="live".
|
|
1313
1401
|
*/
|
|
1314
1402
|
private buildSignalLive;
|
|
1403
|
+
/**
|
|
1404
|
+
* Реализованный результат для backtest: replay ИТОГОВОГО (с учётом инверсии)
|
|
1405
|
+
* направления и exit-плана сигнала по свечам после входа. Зона входа из parser-item;
|
|
1406
|
+
* если не задана — точка = open первой свечи (как в обучении). Нет свечей → не вошли.
|
|
1407
|
+
*/
|
|
1408
|
+
private replayResult;
|
|
1315
1409
|
/**
|
|
1316
1410
|
* Строит ЕДИНЫЙ TradeSignal из вердикта. Возвращает null, если исполнять нечего:
|
|
1317
1411
|
* каскад дал veto ИЛИ получившийся action не в allow-списке. Инверсия здесь же
|
|
@@ -1343,4 +1437,4 @@ declare class PumpMatrix {
|
|
|
1343
1437
|
declare function predict(parserItems: ParserItem[], config?: Partial<DetectorConfig>): PredictionResult;
|
|
1344
1438
|
|
|
1345
1439
|
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 };
|
|
1440
|
+
export type { AuthorMap, BacktestResult, BacktestSignal, 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 };
|