SkillAgentSearch skills...

Volume Anomaly

Statistical volume anomaly detection for trade streams - Hawkes process, CUSUM, and Bayesian Online Changepoint Detection (BOCPD). Zero dependencies. TypeScript.

Install / Use

npx skills add tripolskypetr/volume-anomaly

Installs into whichever agent you are using.

README

<p align="center"> <img src="https://github.com/tripolskypetr/volume-anomaly/raw/master/assets/logo.png" height="115px" alt="garch" /> </p> <p align="center"> <strong>Volume anomaly detection for trade streams</strong><br> Hawkes Process · CUSUM · Bayesian Online Changepoint Detection<br> TypeScript. Zero dependencies. </p>

Installation

npm install volume-anomaly

Quickstart (no math required)

Feed 15–30 minutes of trades (oldest first) into one call and read the answer in plain language — scan() splits baseline/recent by time itself, so there is nothing to slice and nothing to get wrong:

import { scan, explain } from 'volume-anomaly';

const result = scan(trades);       // trades: last 15–30+ min, oldest first

if (result.anomaly) {
  console.log(explain(result));
  // Volume anomaly detected (severity: extreme) — confidence 0.97 vs alert threshold 0.75.
  // Strongest signal: volume ran ~81 robust sigma above the recent typical level at the 5s scale.
  // Peak at 2025-03-01T07:06:10.323Z.
  // Order flow at the peak: 74% sell-side.
  // Follow-through ranking (moveScore): 0.92 — top-tier; historically precedes real price movement...
}

switch (result.severity) {        // 'none' | 'notable' | 'strong' | 'extreme'
  case 'extreme': /* page someone */ break;
  case 'strong':  /* alert */        break;
}

Runnable on real data committed to this repo: node examples/quickstart.mjs.

Field glossary — what the numbers mean in plain words:

| Field | Plain meaning | |-------|---------------| | confidence | How unusual this moment is, 0–1. Alert when ≥ your threshold (default 0.75) | | severity | The same as an enum: none / notable / strong / extreme | | moveScore | How strongly moments like this have historically preceded actual price movement. Use to rank/prioritize alerts, not as the alert itself | | direction | Which side drove the burst (long = buyers). Describes the event; measured on real data it does not predict the next move's direction | | burstImbalance | Buy/sell balance inside the burst itself, −1 (all sells) … +1 (all buys) | | peakTs | When the anomaly peaked (Unix ms) | | stats.zVol / zRate | How many "sigmas" volume / trade-arrival rate ran above what was typical recently, per time scale | | hawkesLambda | Instantaneous trade-arrival intensity under the fitted model | | runLength / cusumStat | How long since the order-flow regime last changed / accumulated drift evidence |

detector.calibrationReport tells you (in words) whether the detector could adapt to your data or is running on universal defaults, and what to feed it to fix that.

Overview

The library detects abnormal surges in trade flow — sudden acceleration of arrivals and volume waves — from a raw stream of aggregated trades. The direction of the trade must come from your own analysis (fundamental, technical). This library answers a narrower question: is right now a statistically unusual moment in market microstructure?

The primary statistic is a self-calibrated robust z-score of trade rate and volume rate: the detector measures "trades per 5 s / per 30 s" and "quantity per 5 s / per 30 s" in the detection window and scores the peaks against the median/MAD of the same statistic over the training window — how many robust σ above the recent typical level. Thresholds are calibrated on a full day of real BTCUSDT aggTrades (1.49 M trades, see test/eval.test.ts): at the default confidence = 0.75 the detector catches ≈ 92 % of locally-strong volume anomalies (robust z ≥ 8 vs the trailing hour) and ≈ 95 % of anomaly events, with ≈ 2.5 % false alarms on normal 30-second windows.

Two secondary detectors (CUSUM and BOCPD on order-flow imbalance) are computed and reported in scores/signals, but by default carry zero weight in the combined confidence: on real data they track flow-regime shifts — a different phenomenon — and any additive weight on them measurably dilutes recall. Re-weight via scoreWeights if your use case targets flow shifts specifically.


API

detect(historical, recent, confidence?)

One-shot convenience function. Trains on historical data, then evaluates the recent window. Returns a DetectionResult.

import { detect } from 'volume-anomaly';
import type { IAggregatedTradeData } from 'volume-anomaly';

// historical: 15–30 MINUTES of trades (time span matters more than count —
// the baseline must cover enough market time to define "typical")
const historical: IAggregatedTradeData[] = await getAggregatedTrades('BTCUSDT', 10_000);
const recent:     IAggregatedTradeData[] = await getAggregatedTrades('BTCUSDT', 300);

const result = detect(historical, recent, 0.75);
// {
//   anomaly:      true,
//   confidence:   0.98,          // combined score (volume channel by default)
//   scores:       { hawkes: 0.98, cusum: 0.61, bocpd: 0.00 },  // raw sub-scores
//   stats:        { zRate: 22.3, zVol: 104.1, zRateSlow: 25.3, zVolSlow: 87.2, lambdaRatio: 3.1 },
//   imbalance:    0.72,          // buy-side dominance
//   hawkesLambda: 61.8,          // peak intensity (trades/sec)
//   cusumStat:    3.1,           // CUSUM accumulator
//   runLength:    2,             // periods since last changepoint
//   signals: [
//     { kind: 'volume_spike',    score: 0.98, meta: { lambda: 61.8, zRate: 22.3, zVol: 104.1, ... } },
//     { kind: 'imbalance_shift', score: 0.72, meta: { imbalance: 0.72, absImbalance: 0.72 } },
//   ]
// }

Parameters:

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | historical | IAggregatedTradeData[] | required | Baseline window for training. Must span 15–30 minutes of market time (code minimum: 50 trades). A count-based baseline ("last 500 trades") adapts its duration to market pace and masks the very burst being detected | | recent | IAggregatedTradeData[] | required | Window to evaluate. Typically 100–500 trades; must span at least rateHorizonSec (5 s) for the fast channel to engage | | confidence | number | 0.75 | Threshold in (0, 1). result.anomaly = result.confidence >= confidence |

Returns: DetectionResult

interface DetectionResult {
  anomaly:      boolean;       // confidence >= threshold
  confidence:   number;        // composite score [0,1]
  scores:       { hawkes: number; cusum: number; bocpd: number }; // raw sub-scores
  stats:        { zRate: number; zVol: number;                    // robust z of peak
                  zRateSlow: number; zVolSlow: number;            // rolling rates
                  lambdaRatio: number };                          // peak λ vs training
  signals:      AnomalySignal[]; // which sub-detectors fired
  imbalance:    number;        // buy/sell balance [-1, +1]
  hawkesLambda: number;        // conditional intensity at last trade (trades/sec)
  cusumStat:    number;        // max(S⁺, S⁻) — CUSUM accumulator
  runLength:    number;        // MAP run length — periods since last changepoint
}

predict(historical, recent, confidence?, imbalanceThreshold?)

One-shot convenience function. Wraps detect() and adds a directional signal derived from the burst-local order flow.

import { predict } from 'volume-anomaly';

const result = predict(historical, recent, 0.75, 0.3);
// {
//   anomaly:        true,
//   confidence:     0.81,
//   direction:      'long',    // 'long' | 'short' | 'neutral'
//   imbalance:      0.42,      // full-window flow balance
//   burstImbalance: 0.72,      // flow balance INSIDE the peak burst window
//   moveScore:      0.68,      // predictive ranking score for forward price response
// }

Parameters:

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | historical | IAggregatedTradeData[] | required | Baseline window for training (≥ 50 trades) | | recent | IAggregatedTradeData[] | required | Window to evaluate | | confidence | number | 0.75 | Anomaly threshold [0,1] | | imbalanceThreshold | number | (trained) | Override the trained directional threshold. Omit to use the value derived automatically from training data (p75 of the rolling signed imbalance series) |

Direction logic:

thr = max(0, imbalanceThreshold)  (if provided explicitly)
    = max(0, detector.trainedModels.imbalanceThreshold)  (otherwise — p75 from training)

direction = 'long'    if anomaly && burstImbalance >  +thr
direction = 'short'   if anomaly && burstImbalance < −thr
direction = 'neutral' otherwise (no anomaly, or balanced flow at the burst)

Direction reads the burst-local imbalance — the order flow inside the rolling window that actually produced the anomaly score — not the full-window average, which dilutes a burst's onset with surrounding two-way flow (measured on real data: a pure sell burst reads −0.9 at the burst but only −0.42 over the full bucket). The burst imbalance is additionally shrunk toward neutral by effective sample size (Kish n_eff over qty), so a three-trade or single-whale "burst" cannot fake directional conviction.

On a neutral/balanced market thr will be near 0 (most windows have close-to-zero imbalance, p75 ≈ 0.1–0.2). On a trending market the p75 shifts upward with the trend, so the bar for direction=long rises accordingly — preventing chronic false long signals during a bull run where sustained buy imbalance is normal, not anomalous. The threshold is clamped at zero, so 'long' always implies buy-side burst flow and 'short' sell-side.

⚠️ direction is descriptive, not predictive. It identifies which side drove the burst — essential for interpreting the event — but measured on the full-day benchmark, no flow-direction statistic (hard-window imbalance, exponentially-weighted flow, or a full bivariate buy/sell Hawkes excitation model — all evaluated) predicted the sign of the

Related Skills

View on GitHub
GitHub Stars7
CategoryDevelopment
Updated6d ago
Forks3

Languages

TypeScript

Security Score

75/100

Audited on Aug 1, 2026

No findings