Reference
CS2 Market Data API: Volume, Liquidity Score and 90-Day Averages for Quant Traders
Most CS2 price APIs return one number. The Skinstrack market data API returns volume, depth, a proprietary liquidity score, and 7 to 90 day aggregates across 34+ marketplaces in one response. This guide shows how to load it into pandas and build signals on it.
What Counts as Market Data Beyond Price
A single current price is the bare minimum. Useful market data has at least four dimensions: best bid and best ask, volume, liquidity, and a time-windowed reference price. The Skinstrack API returns all four in the same payload.
Best Bid and Best Ask
The lowest live ask across 34+ marketplaces is your effective buy price. The highest viable bid is your effective sell price. The gap is your raw spread.
Volume and Quantity
Per-marketplace volume tells you how often the item actually trades. Quantity tells you how much supply is sitting on shelves right now. They are different signals.
Liquidity Score
A 0-100 number that compresses recent volume, depth and price stability into one comparable metric. Cross-marketplace, normalized, and only Skinstrack publishes it.
Time-Window Aggregates
Pass avg=true and median=true to receive 7d, 14d, 30d, 60d and 90d means and medians. Enough to detect momentum, mean reversion, and price regime shifts.
Pulling Market Data in One Call
Hit GET /paid/items with avg=true and median=true. You receive every item the providers list, plus aggregates inline.
curl -H "X-API-KEY: $SKINSTRACK_KEY" \
"https://api.skinstrack.com/v1/paid/items?providers=csfloat,buff,skinport&avg=true&median=true"How 27 Marketplaces Become One Row
For each item, Skinstrack normalizes per-marketplace listings into the same shape: provider, price, count, volume, updated_at. That uniformity is the whole point: you write one function that works against every marketplace, instead of 27 adapters.
{
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"slug": "ak-47-redline-field-tested",
"liquidity": 85,
"quantity": 15000,
"prices": [
{ "provider": "csfloat", "price": 12.50, "count": 150, "volume": 500 },
{ "provider": "buff", "price": 11.92, "count": 482, "volume": 920 },
{ "provider": "skinport", "price": 13.10, "count": 44, "volume": 110 }
],
"average": { "7d": 12.35, "14d": 12.20, "30d": 12.10, "60d": 11.95, "90d": 11.85 },
"median": { "7d": 12.40, "14d": 12.25, "30d": 12.05, "60d": 11.95, "90d": 11.90 }
}Reading the Liquidity Score
The liquidity score is the most useful single field on the response for anyone running a strategy. It tells you whether the item you are modeling actually trades. The convention to start with:
80-100
Trades constantly. Tight spreads, fast fills. Safe for short-hold strategies and high-velocity arbitrage.
50-79
Trades regularly. Spreads widen on bad days. Hold time can stretch from hours to days.
20-49
Trades sporadically. Use limit logic only and expect to sit on inventory for days or weeks.
0-19
Trades rarely. Excellent for capturing illiquidity premia, terrible for unwinding fast. Often where mispricings persist longest.
The comparison tool uses the same score to filter cross-market opportunities. Eyeballing the tool with a liquidity slider is a fast way to calibrate where your strategy threshold belongs.
Loading the Universe Into pandas
For research, the right starting point is a flat dataframe with one row per item and columns for the fields you actually use. Everything else compounds from here.
import os, requests, pandas as pd
def load_universe(providers=('csfloat', 'buff', 'skinport')):
res = requests.get(
'https://api.skinstrack.com/v1/paid/items',
headers={'X-API-KEY': os.environ['SKINSTRACK_KEY']},
params={'providers': ','.join(providers), 'avg': 'true', 'median': 'true'},
)
res.raise_for_status()
rows = []
for item in res.json():
avg = item.get('average', {}) or {}
med = item.get('median', {}) or {}
best_bid = min((p['price'] for p in item['prices']), default=None)
best_ask = max((p['price'] for p in item['prices']), default=None)
rows.append({
'slug': item['slug'],
'name': item['market_hash_name'],
'rarity': item.get('rarity_name'),
'wear': item.get('wear_name'),
'liquidity': item.get('liquidity', 0),
'quantity': item.get('quantity', 0),
'best_bid': best_bid,
'best_ask': best_ask,
'spread': (best_ask - best_bid) if best_bid and best_ask else None,
'avg_7d': avg.get('7d'),
'avg_30d': avg.get('30d'),
'avg_90d': avg.get('90d'),
'med_7d': med.get('7d'),
'med_30d': med.get('30d'),
'med_90d': med.get('90d'),
})
return pd.DataFrame(rows)
df = load_universe()
print(df.describe())Three Quick Signal Sketches
These are not strategies. They are screens that produce candidate lists you then evaluate by hand. Tighten the thresholds before you put money behind them.
# Three quick signal sketches off the same dataframe.
# 1. Momentum: 7d average is meaningfully above 30d average,
# suggesting demand is accelerating.
momentum = df[
(df['avg_7d'].notna())
& (df['avg_30d'].notna())
& (df['avg_7d'] / df['avg_30d'] - 1 > 0.05)
& (df['liquidity'] >= 50)
]
# 2. Mean reversion: current best ask is far below the 30d average
# on a liquid item.
mean_reversion = df[
(df['best_ask'].notna())
& (df['avg_30d'].notna())
& (df['best_ask'] / df['avg_30d'] - 1 < -0.10)
& (df['liquidity'] >= 60)
]
# 3. Volume / quantity spike: current quantity is dramatically
# above what the price band would predict. Often a sell signal
# because supply is flooding in.
import numpy as np
df['price_bucket'] = pd.cut(df['avg_30d'].fillna(0), bins=[0,5,25,100,500,np.inf])
expected = df.groupby('price_bucket', observed=True)['quantity'].transform('median')
supply_spike = df[df['quantity'] > expected * 2.5]Building Your Own Time Series
The API ships 7d to 90d aggregates, which is enough for the signals above. For per-day or per-hour granularity, snapshot the API on a schedule and append.
# Incremental sync pattern: every 6 hours, fetch the snapshot
# and append a row per item with a load_ts. Build your own
# historical series from successive snapshots.
from datetime import datetime, timezone
import sqlite3
def sync(db_path='skins.db'):
df = load_universe()
df['load_ts'] = datetime.now(timezone.utc).isoformat()
with sqlite3.connect(db_path) as conn:
df.to_sql('skin_snapshots', conn, if_exists='append', index=False)Backtesting Caveats
Aggregates, not ticks
The API exposes 7d, 14d, 30d, 60d and 90d means and medians, plus the current snapshot. It does not stream individual trades. Plan for window-level signals, not tick-by-tick replay.
Build your own series
For per-day OHLC, store successive API snapshots yourself. A snapshot every few hours plus the load_ts column gives you a usable history within a week, and a serious one within a quarter.
Survivorship bias
Items that get delisted or retire silently disappear from the universe. If you backtest against today's universe, you are looking at the winners. Snapshot the full universe regularly so retired items stay in your archive.
Cross-market fees skew returns
Models that ignore the 12 percent Steam fee or the 2.5 percent Buff fee look great on paper and lose money in production. Always net out fees per execution venue.
Pull market data with one API key
Growth tier covers a full universe snapshot every few hours for a month of research.
Frequently Asked Questions
Does the CS2 market data API expose a full price history endpoint?
No raw tick stream. The API exposes the current cross-marketplace snapshot, plus 7d, 14d, 30d, 60d and 90d average and median price aggregates per item when you pass avg=true and median=true on /paid/items. For per-day or per-hour granularity, store successive snapshots on your side.
What does the Skinstrack liquidity score measure?
A 0-100 metric that combines recent trading volume, listing depth and price stability across the marketplaces an item is listed on. Higher means tighter spreads and faster fills. It is computed by Skinstrack and is not available through any single marketplace API.
Which marketplaces does the market data API cover?
27 marketplaces are normalized into one response, including Steam, Buff163, CSFloat, Skinport, DMarket, WaxPeer, Gamerpay, CSGOEmpire and BitSkins. The full list is in the API docs at /api-docs.
Can I use this data for algorithmic trading?
Yes for signal generation, alerting, and analytics. No for direct execution. The Skinstrack API does not place buy or sell orders. You execute on each marketplace through its own UI or its own API. See the CS2 arbitrage bot guide for a working architecture.
How do I backtest a strategy on CS2 skins?
Snapshot the universe on a fixed cadence (every 6 hours is a reasonable starting point), persist each snapshot with a load timestamp, and replay your signal logic over that history. Make sure to include items that have since delisted to avoid survivorship bias.
How fresh is the data?
Each price in the response carries its own updated_at timestamp from the marketplace it was pulled from. Most providers refresh within a few minutes. For signal logic, drop offers older than your chosen freshness window before computing best bid and best ask.
