Pump.fun OHLCV: 1-second candles for backtesting strategies
Pull pump.fun OHLCV candles at 1-second resolution and backtest memecoin strategies in Python: cohort building, a worked momentum test, fees and biases.
Pull pump.fun OHLCV candles at 1-second resolution and backtest memecoin strategies in Python: cohort building, a worked momentum test, fees and biases.
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.
/tokens/{mint}/candlesOne 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"
tf — 1s, 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
}
]
}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.
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.
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.
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.
Three failure modes account for most fake memecoin backtests:
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.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.
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.
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 key with 200,000 trial credits 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.
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.
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.
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.
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.
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 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
Free API key with 200,000 trial credits: full REST and WebSocket access to the same firehose that powers the Terminal. Connect a Solana wallet; no email, no card.