Pump.fun OHLCV: 1-second candles for backtesting strategies
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"
tf—1s,30s,1m,1hor1d. The sub-minute series are computed on demand from raw ticks;1m/1h/1dcome 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'screated_at.limit— caps the number of buckets returned; when the range holds more, you get the most recent ones. Dense1swindows 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.
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=1cohort 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 thestatusfilter incohort()) and let the strategy meet the sea of tokens that dumped minutes after launch — that number is the real one. - Unmodeled costs.
COST = 0.03above is a placeholder, not a fact. A real cost model has three parts: pump.fun's own fees (every raw swap row documents its actualfee,creator_feeandlp_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.
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.