Building a Real-Time Crypto Pump Detector: How to Model Candle Data and Detect Anomalies in Go and Python

If you've spent any time in the cryptocurrency space—or even just watched it from a safe distance—you know it’s a wild west of sudden, violent market movements. "Pump and dump" schemes are as old as markets themselves, but in crypto, they happen at warp speed, often orchestrated by automated bots. For developers, this presents a fascinating engineering challenge: How do you ingest, process, and analyze financial candle data in real time to detect these anomalous spikes before they happen?

Today, a really cool open-source project caught my eye on Hacker News: a candle-only crypto pump detector looking for Machine Learning (ML) contributors. What makes this project particularly compelling is its constraint: candle-only. It doesn't rely on sentiment analysis from Twitter/X or Discord scrapers. Instead, it treats OHLCV (Open, High, Low, Close, Volume) candlestick data as a pure time-series anomaly detection problem.

As developers, building a tool like this touches on several disciplines we love: high-throughput data ingestion, stream processing, feature engineering, and real-time machine learning inference. In this post, we’re going to dive under the hood of how a candle-only pump detector works, build a working prototype in Go for data ingestion, and write a Python-based ML pipeline to detect these market anomalies.

Understanding the Math: What Does a "Pump" Look Like to an Algorithm?

To a human eye, a pump is obvious: a giant green candlestick shooting straight up. To a machine, we need to translate this visual intuition into mathematical features. Since we are restricted to candle-only data (OHLCV), we have five primary data points per time interval (e.g., 1-minute, 5-minute, or 1-hour candles):

  • Open ($O$): The price at the start of the interval.
  • High ($H$): The maximum price reached during the interval.
  • Low ($L$): The minimum price reached during the interval.
  • Close ($C$): The price at the end of the interval.
  • Volume ($V$): The total amount of the asset traded during the interval.

A typical pump exhibits two main characteristics: a massive price expansion and a massive volume surge relative to the historical baseline. Therefore, our detector cannot simply look at absolute values; it must look at relative changes. We can engineer several high-value features from raw candles:

1. Relative Volume (RVOL)

Volume is the fuel of a pump. If the current volume ($V_t$) is many standard deviations above the moving average volume over the last $N$ periods, it's highly anomalous.

RVOL = V_t / SMA(V, N)

2. Rate of Change (ROC) and Spread

We need to measure how fast the price is moving. The Rate of Change measures the difference between the current close and a past close, while the candlestick "spread" (the body size relative to the entire candle range) tells us if buyers are in absolute control.

Spread = (C_t - O_t) / (H_t - L_t)

Step 1: Building the Real-Time Ingest Engine in Go

Before we can run any machine learning models, we need a highly performant ingestion engine. Go (Golang) is the perfect tool for this because of its native concurrency model (goroutines and channels) and low memory footprint. We want to connect to a cryptocurrency exchange WebSocket (like Binance or Coinbase), stream live trades, aggregate them into 1-minute candles, and feed them into our detection pipeline.

Here is a simplified, robust Go implementation that connects to a public trade stream, aggregates candles, and prepares the data payload:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/url"
	"time"

	"github.com/gorilla/websocket"
)

// Trade represents the raw trade event from the exchange
type Trade struct {
	Price  float64 `json:"p,string"`
	Volume float64 `json:"q,string"`
	Time   int64   `json:"T"`
}

// Candle represents our aggregated OHLCV data
type Candle struct {
	OpenTime int64
	Open     float64
	High     float64
	Low      float64
	Close    float64
	Volume   float64
}

func main() {
	// We'll stream BTC/USDT trades from Binance's public WebSocket
	u := url.URL{Scheme: "wss", Host: "stream.binance.com:9443", Path: "/ws/btcusdt@trade"}
	log.Printf("Connecting to %s", u.String())

	c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		log.Fatal("dial:", err)
	}
	defer c.Close()

	var currentCandle *Candle
	candleDuration := 1 * time.Minute

	for {
		_, message, err := c.ReadMessage()
		if err != nil {
			log.Println("read:", err)
			return
		}

		var trade Trade
		if err := json.Unmarshal(message, &trade); err != nil {
			continue
		}

		tradeTime := time.Unix(0, trade.Time*int64(time.Millisecond))
		candleBucket := tradeTime.Truncate(candleDuration).Unix()

		if currentCandle == nil || currentCandle.OpenTime != candleBucket {
			if currentCandle != nil {
				// Broadcast the completed candle to our ML pipeline
				emitCandle(*currentCandle)
			}
			// Start a new candle
			currentCandle = &Candle{
				OpenTime: candleBucket,
				Open:     trade.Price,
				High:     trade.Price,
				Low:      trade.Price,
				Close:    trade.Price,
				Volume:   trade.Volume,
			}
		} else {
			// Update the current candle metrics
			currentCandle.High = math.Max(currentCandle.High, trade.Price)
			currentCandle.Low = math.Min(currentCandle.Low, trade.Price)
			currentCandle.Close = trade.Price
			currentCandle.Volume += trade.Volume
		}
	}
}

func emitCandle(candle Candle) {
	fmt.Printf("[CANDLE COMPLETED] Time: %d | O: %.2f | H: %.2f | L: %.2f | C: %.2f | V: %.4f\n",
		candle.OpenTime, candle.Open, candle.High, candle.Low, candle.Close, candle.Volume)
	// In production, you would stream this via gRPC or Kafka to your ML service
}

Step 2: Designing the Machine Learning Detector in Python

Now that we have a stream of completed candles, we need a model to detect anomalies. Since true "pump" events are rare compared to normal market noise, this is a classic unsupervised anomaly detection task. Labeling data is tedious, so instead of supervised learning, we can use an Isolation Forest or a One-Class SVM.

Isolation Forest works by isolating anomalies instead of profiling normal data points. Because anomalies require fewer splits to isolate in a decision tree, they appear closer to the root of the tree.

Let's write a Python script using pandas and scikit-learn to calculate our engineered features and run them through an Isolation Forest model.

import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest

def engineer_features(df):
    """
    Accepts a pandas DataFrame with columns: ['open', 'high', 'low', 'close', 'volume']
    Returns the dataframe with engineered mathematical features for anomaly detection.
    """
    # 1. Price Rate of Change (ROC) over last 3 periods
    df['roc_3'] = df['close'].pct_change(periods=3)
    
    # 2. Candle Spread (relative body size)
    df['candle_range'] = df['high'] - df['low']
    # Prevent division by zero
    df['candle_range'] = df['candle_range'].replace(0, 1e-8)
    df['spread'] = (df['close'] - df['open']).abs() / df['candle_range']
    
    # 3. Relative Volume (RVOL) against a 20-period Simple Moving Average
    df['volume_sma_20'] = df['volume'].rolling(window=20).mean()
    df['rvol'] = df['volume'] / df['volume_sma_20']
    
    # Drop NaNs resulting from rolling averages and pct_changes
    df = df.dropna()
    return df

def train_and_detect_pumps(df):
    # Select features optimized for pump detection
    feature_cols = ['roc_3', 'spread', 'rvol']
    X = df[feature_cols]
    
    # Initialize Isolation Forest
    # contamination=0.01 means we expect roughly 1% of our data to be "anomalous" (pumps)
    model = IsolationForest(n_estimators=100, contamination=0.01, random_state=42)
    
    # Fit the model and predict
    # 1 = normal, -1 = anomaly (pump)
    df['anomaly_label'] = model.fit_predict(X)
    
    # Filter anomalies that have a positive price movement (to avoid dumps/crashes)
    pumps = df[(df['anomaly_label'] == -1) & (df['roc_3'] > 0)]
    
    return pumps, model

# Example Usage
if __name__ == "__main__":
    # Mocking some market data (100 normal candles, 1 massive pump at index 95)
    np.random.seed(42)
    data_length = 100
    
    prices = [100.0]
    for _ in range(data_length - 1):
        prices.append(prices[-1] * (1 + np.random.normal(0, 0.002))) # 0.2% volatility
        
    volumes = np.random.normal(10, 2, data_length).tolist()
    
    # Inject an obvious pump!
    prices[95] = prices[94] * 1.15  # 15% price spike in one candle
    volumes[95] = volumes[94] * 12   # 12x volume spike
    
    # Construct DataFrame
    mock_df = pd.DataFrame({
        'open': prices[:-1] + [prices[-1]], # simplified for mock
        'high': [p * 1.01 for p in prices],
        'low': [p * 0.99 for p in prices],
        'close': prices,
        'volume': volumes
    })
    
    processed_df = engineer_features(mock_df)
    detected_pumps, trained_model = train_and_detect_pumps(processed_df)
    
    print(f"Detected {len(detected_pumps)} potential pump events:")
    print(detected_pumps[['roc_3', 'spread', 'rvol']])

Production Considerations: Latency, False Positives, and Scaling

If you're looking to contribute to the open-source detector mentioned on Hacker News, or if you're building your own proprietary trading bot, raw code is only 20% of the battle. The remaining 80% lies in operationalizing this system under real-world conditions.

1. Reducing Latency with a Lambda Architecture

In crypto, a pump can be over in minutes. Waiting for a 1-minute candle to "close" before analyzing it means you're already too late. High-performance systems use a hybrid approach:

  • The Batch Layer: Trains the unsupervised model (like our Isolation Forest) every 24 hours on historical OHLCV data.
  • The Speed Layer: Uses sub-second sliding windows (e.g., a rolling 60-second window updated every 100 milliseconds) to generate "micro-candles" and runs fast inference using the pre-trained model weights.

2. Eliminating False Positives

Unsupervised models are highly sensitive. A sudden spike in volatility across the entire market (e.g., Bitcoin moving 2% in a minute) will cause the model to trigger pump alerts on hundreds of altcoins simultaneously. To prevent this, your feature engineering must normalize individual asset behavior against the broader market index. If the market is flat and one coin spikes, it's a pump. If the whole market is moving, it’s systemic volatility.

Conclusion: The Power of Open-Source Crypto Analysis

Building a candle-only pump detector is an incredible exercise in data engineering. By stripping away noisy external signals like social media sentiment and focusing purely on the mathematical realities of OHLCV data, developers can construct elegant, fast, and surprisingly accurate anomaly detection systems.

The open-source project featured on Hacker News is a great starting point if you want to get your hands dirty with real-world ML. Whether you contribute to their project or build your own pipeline from scratch using Go and Python, you'll be mastering the tools that power modern high-frequency finance.

What are your thoughts? Have you tried using ML models like LSTMs or Isolation Forests for real-time market data? Let’s chat in the comments below, and don't forget to subscribe to the "Coding with Alex" newsletter for weekly deep-dives into DevOps, security, and backend engineering!

Post a Comment

Previous Post Next Post