How to backtest a CS2 skin trading strategy
Backtest a CS2 skin trading strategy on point-in-time bid and ask history, with fees, gaps, and no lookahead bias. Runnable data-pull code included.
On this page
A backtest replays a trading rule over past prices to estimate how it would have done. It is only as honest as its assumptions. The three that most often turn a losing CS2 strategy into a paper winner are buying at an average price instead of the ask, deciding on data that was not available yet, and ignoring fees and the time an item cannot be moved after you buy it.
SkinPricer's history endpoints give you both sides of the book per time bucket, per market or across the market, which is what an honest backtest needs. This guide shows how to pull the right series, how to line up decisions and fills in time, and how to model costs and gaps. The strategy code is a deliberately simple example; the structure around it is the part to reuse.
Choose the series that matches your execution#
| You need | Use |
|---|---|
| The price you could buy at | bestAskCents from price history. Not avgPrice or medianPrice: you cannot buy at the average. |
| The price you could sell into immediately | bestBidCents from the same buckets. In the aggregate series it is the best cash-executable bid that does not exceed the bucket's ask. |
| Which venue you would actually trade on | History by market, one series per venue, filtered with markets. |
| Evidence that trades happened | listingType=SALE_HISTORY, whose volume is a trailing-window sale count (see the warning below). |
| A float band or Doppler phase | Attribute price history, with open, high, low, and close asks per bucket. |
Step 1: pull point-in-time history#
Pin the bucket size with interval so every item in your test uses the same clock. Hourly buckets are a good default for skins: fine enough to model entries and exits, coarse enough that 90 days of one item is about 2,160 points. History requires the Pro plan, and a single request is capped at 10,000 points, counting one series per market on the per-market endpoint.
export SKINPRICER_API_KEY="sp_live_•••" # from your account's API keys page
curl -s "https://pricing.skinpricer.com/v1/pricing/AK-47%20%7C%20Redline%20(Field-Tested)/history?interval=HOUR_1&from=2026-06-27T00:00:00Z" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY"import os
from datetime import datetime, timedelta, timezone
from urllib.parse import quote
import requests
API = "https://pricing.skinpricer.com/v1"
HEADERS = {"Authorization": f"ApiKey {os.environ['SKINPRICER_API_KEY']}"}
def hourly_history(name: str, days: int = 90) -> list[dict]:
since = datetime.now(timezone.utc) - timedelta(days=days)
res = requests.get(
f"{API}/pricing/{quote(name, safe='')}/history",
params={"interval": "HOUR_1", "from": since.isoformat()},
headers=HEADERS,
timeout=10,
)
res.raise_for_status()
data = res.json()
if data.get("effectiveInterval") != "1h":
raise ValueError(f"unexpected bucket size: {data.get('effectiveInterval')}")
return data["history"]
buckets = hourly_history("AK-47 | Redline (Field-Tested)")// npm i @skinpricer/sdk
import { SkinpricerClient } from "@skinpricer/sdk";
const client = new SkinpricerClient({
apiKey: process.env.SKINPRICER_API_KEY!,
authScheme: "ApiKey",
});
// One series per venue you could actually trade on.
const perVenue = await client.history.byMarket("AK-47 | Redline (Field-Tested)", {
markets: ["buff163", "csfloat", "skinport"],
interval: "HOUR_1",
from: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
});
for (const { market, history } of perVenue.series) {
console.log(market, history.length, "hourly buckets");
}Step 2: line up decisions and fills in time#
Each bucket's timestamp is the start of its window. The bucket only describes the market once the window has closed, so a rule evaluated on bucket i can only trade in bucket i + 1 at the earliest, and at that bucket's prices, not bucket i's. This one rule removes most accidental lookahead.
def backtest(buckets, lookback=168, buy_fee=0.0, sell_fee=0.05):
"""Toy mean reversion: buy 10% under the trailing mean ask, sell once
the bid covers cost plus 5%. Decide on bucket i, fill on bucket i + 1."""
pnl, entry = [], None
for i in range(lookback, len(buckets) - 1):
now, nxt = buckets[i], buckets[i + 1]
past = [b["bestAskCents"] for b in buckets[i - lookback : i + 1]
if b["bestAskCents"] is not None]
if len(past) < lookback // 2:
continue # too many gaps to trust the mean
mean_ask = sum(past) / len(past)
if entry is None:
signal = now["bestAskCents"] is not None and now["bestAskCents"] < 0.9 * mean_ask
if signal and nxt["bestAskCents"] is not None:
entry = nxt["bestAskCents"] * (1 + buy_fee) # pay the next ask
else:
bid = now["bestBidCents"]
if bid is not None and bid * (1 - sell_fee) >= entry * 1.05 and nxt["bestBidCents"] is not None:
pnl.append(nxt["bestBidCents"] * (1 - sell_fee) - entry) # hit the next bid
entry = None
return pnl
trades = backtest(buckets)
print(len(trades), "round trips, net", sum(trades) / 100, "USD")Step 3: model the costs you would really pay#
- Buy at the ask and sell at the bid. A strategy that only works on
medianPricehas not found an edge; it has found the spread. - Apply each venue's buyer fee, seller commission, and any withdrawal cost separately. Percentage fees and fixed fees behave very differently on cheap items.
- Model the holding period. Many items cannot be moved or resold for a while after purchase, so an exit signal during that window cannot be acted on.
- Steam Community Market prices settle in Steam wallet credit, not cash. Keep them out of cash P&L unless you model the conversion; the Steam vs cash-market guide explains why.
- Size matters. A best ask is often a single listing; if your rule trades more than one unit, check depth rather than assuming the whole order fills at the top of book.
Step 4: handle gaps and nulls explicitly#
Real market history has holes. A missing bucket means nothing was observed in that window, and a null side means no quote existed on that side. Neither is a price. Do not trade on a bucket with a missing side, and do not forward-fill an old ask across a long gap as if it were still available. Log how many decisions were skipped for missing data; if it is a large share of your signals, the result says more about coverage than about the strategy.
Testing float-band and phase strategies#
Strategies on low floats or Doppler phases need band-level data. Attribute price history returns, per bucket id, openAskCents, highAskCents, lowAskCents, closeAskCents, and closeBidCents at 10m, 1h, or 1d spacing, with the same point-in-time caveats. Use closeAskCents of bucket i + 1 as your fill, not the lowAskCents of bucket i, which you could only have known in hindsight. Windows are capped at 10,000 buckets, so run long tests at 1d.
Watch for survivorship bias too. If you choose today's popular items and test them over the past quarter, you are testing items that already turned out to be liquid. Pick your universe with rules you could have applied at the start of the test.
When to reach for a different endpoint#
To explore a series visually before you test it, start with the price history guide. To turn a signal into an estimate of what an item is worth right now, see estimating fair value. And if your strategy trades float or phase premiums, BUFF163 prices and history covers the bucketed series.
Endpoints used in this guide
Plans start at $50/month on the Developer tier. Want to try first? Ask in our Discord for a free 2-day Developer key.
Related guides
- History & backtestingBeginner
How to get CS2 skin price history at any interval
Fetch CS2 skin price history in 10-minute, hourly, or daily buckets, across all markets or split per market, ready for charts, alerts, and analysis.
GET /v1/pricing/:marketHashName/historyGET /v1/pricing/:marketHashName/history/markets6 min read - Prices & quotesIntermediate
How to estimate what a CS2 skin is really worth
Estimate what a CS2 skin is really worth: combine NBBO, recent history, and a suggested list price with confidence scores instead of one market's number.
GET /v1/recommendations/:marketHashNameGET /v1/nbbo6 min read - History & backtestingIntermediate
How to get BUFF163 prices, float buckets, and history in USD
Get BUFF163 prices, native float buckets, and price history for CS2 items in USD cents, without scraping or converting currency. Runnable API code included.
GET /v1/markets/buff163/latestGET /v1/markets/buff163/history6 min read