Pump.fun OHLCV: 1-second candles for backtesting strategies

· 8 min read

ohlcvcandlesbacktestingpythonstrategy

A pump.fun token's tradable life is measured in minutes, not days. If you want to backtest a Solana memecoin strategy on that market, the resolution of your data decides whether you are testing anything at all: on 1-minute bars most launches are two or three rows — open, wick, corpse. This guide is the full workflow on pump.fun OHLCV data: pulling candlestick data down to 1-second candles, assembling a multi-token panel in pandas, running a worked momentum backtest, and — the part most tutorials skip — reading the result honestly, using the Raiden pump.fun API.

The endpoint: /tokens/{mint}/candles

One call per token, five timeframes:

curl -H "X-API-Key: $RAIDEN_KEY" \
  "https://terminal.raiden.wtf/api/tokens/9xQm…pump/candles?tf=1s&from=2026-07-01T09:40:00Z&to=2026-07-01T10:40:00Z&limit=3600"
  • tf1s, 30s, 1m, 1h or 1d. The sub-minute series are computed on demand from raw ticks; 1m/1h/1d come from pre-aggregated series.
  • from / to — RFC3339 bounds. Without them you get the most recent candles (for a token that stopped trading, the window around its last activity) — so for launch studies always pass explicit bounds anchored to the token's created_at.
  • limit — caps the number of buckets returned; when the range holds more, you get the most recent ones. Dense 1s windows can hit the default cap, so size it to the window — 3,600 buckets per hour.

Each row is one bucket:

{
  "data": [
    {
      "time": "2026-06-21T09:41:14Z",
      "open": "0.00000042",
      "high": "0.00000051",
      "low": "0.00000039",
      "close": "0.00000048",
      "volume": "488888889",
      "trades": 137
    }
  ]
}
Units: open/high/low/close are decimal strings in SOL per token; volume is the SOL side of every fill in the bucket, summed, as a string in lamports (1 SOL = 10⁹); trades is an integer. Strings exist to protect precision — convert with pandas.to_numeric before doing math.

Two properties matter for backtesting. First, the series is continuous through graduation: the mint address never changes and every underlying fill carries its venue, so bonding-curve trading and PumpSwap trading form one price history — no splicing. Second, candles are trade buckets: a second with no fills produces no row. Before computing rolling statistics you must reindex to a full time grid, or your "trailing mean" will silently skip the quiet seconds.

Step 1 · Build the cohort from the screener

A backtest needs a defined universe, not cherry-picked charts. The screener enumerates it: GET /tokens filters by status (0 = on the bonding curve, 1 = graduated), bounds by creation date with from/to, and pages by keyset cursor (limit up to 500 per page — follow next_cursor until it is empty):

import requests
import pandas as pd

BASE = "https://terminal.raiden.wtf/api"
H = {"X-API-Key": "YOUR_KEY"}

def cohort(frm, to, status=None):
    out, cursor = [], None
    while True:
        p = {"sort": "newest", "limit": 500, "from": frm, "to": to}
        if status is not None: p["status"] = status
        if cursor: p["cursor"] = cursor
        page = requests.get(f"{BASE}/tokens", params=p, headers=H).json()
        out += page["data"]
        if not page.get("next_cursor"): break
        cursor = page["next_cursor"]
    return out

# everything created on July 1 that later graduated
tokens = cohort("2026-07-01T00:00:00Z", "2026-07-02T00:00:00Z", status=1)

status=1 gives the graduated cohort — convenient, liquid, and biased (more on that below). The graduated tokens guide covers what graduation means and what flips on the token record when it happens.

Step 2 · Pull candles into a panel

Per mint: fetch the launch window at 30-second resolution, convert the strings, and reindex onto a full grid so empty buckets exist as zero-volume rows:

def candles(mint, created_at, tf="30s", hours=2):
    t0 = pd.Timestamp(created_at)
    rows = requests.get(f"{BASE}/tokens/{mint}/candles",
        params={"tf": tf, "from": created_at,
                "to": (t0 + pd.Timedelta(hours=hours)).isoformat(),
                "limit": int(hours * 3600 / pd.Timedelta(tf).seconds)},
        headers=H).json()["data"]
    if not rows: return None
    df = pd.DataFrame(rows)
    df["time"] = pd.to_datetime(df["time"])
    for c in ("open", "high", "low", "close", "volume"):
        df[c] = pd.to_numeric(df[c])
    # trade buckets → full 30s grid (quiet buckets become volume=0 rows)
    df = df.set_index("time").resample("30s").agg(
        {"open": "first", "high": "max", "low": "min",
         "close": "last", "volume": "sum", "trades": "sum"})
    df["close"] = df["close"].ffill()
    df["mint"] = mint
    return df.reset_index()

frames = [candles(t["mint"], t["created_at"]) for t in tokens]
panel = pd.concat([f for f in frames if f is not None], ignore_index=True)

Why 30s here and not 1s? Match the resolution to the signal. A volume-spike trigger over a trailing few minutes reads cleanly on 30-second buckets; drop to 1s when the entry logic needs it — the download loop is identical, just thirty times the buckets; the function already sizes limit to the window, but at 1-second resolution also shorten hours= so a dense launch fits in one response.

Step 3 · A worked momentum backtest

The simplest strategy worth testing on this market: buy when 30-second volume spikes to 4× its trailing five-minute mean (10 buckets, shifted by one so the trigger never sees its own bucket), exit at +25% or −15%. One position per token, entry at the trigger bucket's close:

SPIKE, TP, SL, COST = 4.0, 0.25, 0.15, 0.03  # trigger, exits, round-trip cost — assumptions to tune

def run(df):
    base = df["volume"].rolling(10).mean().shift(1)  # trailing mean, no lookahead
    entry, out = None, []
    for i in range(len(df)):
        px = df["close"].iloc[i]
        if pd.isna(px): continue
        if entry is None:
            if df["volume"].iloc[i] >= SPIKE * base.iloc[i]:  # NaN warm-up → False
                entry = px  # optimistic: filled at the trigger bucket's close
        else:
            r = px / entry - 1
            if r >= TP or r <= -SL:
                out.append(r - COST)
                entry = None
    return out

rets = pd.Series([r for _, g in panel.groupby("mint") for r in run(g)])
print(f"trades={len(rets)}  hit={(rets > 0).mean():.0%}  "
      f"avg={rets.mean():+.2%}  sum={rets.sum():+.1%}")

Grid the three parameters, slice by hour of day, condition the entry on launch quality — the panel is a plain DataFrame, everything from here is ordinary pandas. The point of the exercise is not this toy strategy; it is that the loop above will look profitable for reasons that have nothing to do with edge. Which brings us to the part that matters.

Reading the result honestly

Three failure modes account for most fake memecoin backtests:

  • Survivorship bias. A status=1 cohort contains only winners — tokens that, by definition, went up enough to graduate. Almost any long strategy prints money on it. Rerun the same test on the full creation-window cohort (drop the status filter in cohort()) and let the strategy meet the sea of tokens that dumped minutes after launch — that number is the real one.
  • Unmodeled costs. COST = 0.03 above is a placeholder, not a fact. A real cost model has three parts: pump.fun's own fees (every raw swap row documents its actual fee, creator_fee and lp_fee — measure, don't guess), the landing spend (tip + priority fee; the landing-conditions guide shows how to read what actually clears from live landed-vs-failed data), and slippage against the bonding curve. During the exact volume spikes this strategy buys, all three are at their worst.
  • Optimistic fills. Filling at the trigger bucket's close assumes you saw the bucket complete, decided, landed on-chain and paid the close price — in the same instant. On a market where the move happens in seconds, add latency buckets between trigger and fill and watch the average return drop; if the edge does not survive a one-bucket delay at 1-second resolution, it does not exist.

And when candle granularity itself is the limit — same-slot entries, first-block launch dynamics, per-wallet behavior — drop to tick level: /tokens/{mint}/swaps returns every fill with price, amounts, every fee component, slot and block index, and failed=1 interleaves the reverted attempts that candles never show. The historical data guide covers that download path end to end; this article and that one are the two halves of the same pipeline.

How much history you get

Candles and ticks are kept on a 6-month rolling window — at the time of writing, the complete record since May 1, 2026. For memecoin backtesting that is more useful than it sounds: the pump.fun regime shifts roughly monthly (metas rotate, fee markets reprice, bot populations turn over), so a strategy averaged over years of history would be blended across markets that no longer exist. The record so far already spans multiple full regimes — test on monthly slices, walk forward, and treat any parameter that only works in one slice as noise.

Full request/response shapes for every endpoint are in the API reference, and the free invite-only beta is described on the pump.fun API page. From here: download tick-level history with Python, or build the graduated universe properly with the graduated tokens guide.

FAQ

Frequently asked questions

What timeframes does the pump.fun candles endpoint support?

GET /tokens/{mint}/candles accepts tf=1s, 30s, 1m, 1h or 1d, plus from/to bounds (RFC3339) and a limit. Each row carries time, open, high, low, close, volume and trades. The 1s and 30s series are computed on demand from raw ticks; 1m, 1h and 1d are served from pre-aggregated series.

What units are candle prices and volume in?

Open, high, low and close are decimal strings in SOL per token. Volume is the SOL side of every fill in the bucket, summed, as a string in lamports (1 SOL = 10^9 lamports). Trades is a plain integer. Strings avoid floating-point precision loss — convert with pandas.to_numeric before doing math.

Why do pump.fun backtests need 1-second candles?

Because a pump.fun token's tradable life is measured in minutes. A launch that pumps and round-trips inside three minutes is three rows on a 1-minute chart — there is nothing to test. At 1-second resolution the same window is 180 observations: enough to define an entry trigger, an exit and a stop.

What is survivorship bias in a memecoin backtest?

Testing only on tokens that graduated — the winners — makes almost any long strategy look profitable, because the cohort excludes every token that died on the bonding curve. Build the cohort from all tokens created in a time window (the screener's from/to filter, no status filter) and let the strategy meet the losers too.

How much pump.fun candle history is available for backtesting?

Six months of rolling retention — at the time of writing, the complete record since May 1, 2026. Since the memecoin regime shifts roughly monthly, that record already spans multiple full regimes: enough for walk-forward tests on monthly slices rather than one average over a market that no longer exists.

When should I use tick data instead of candles?

When the strategy depends on what happens inside a bucket: same-slot entries, first-block launch dynamics, individual wallet behavior, or fee and tip modeling. GET /tokens/{mint}/swaps returns every fill with price, amounts, every fee component, slot and block index — and failed=1 interleaves reverted attempts.

Build on the same data

Private beta — free invite-only keys, full REST + WebSocket access, and the same firehose that powers the Terminal.