LynxCrypto Hands-On: Speedrun pandas in 6 Steps with Real Crypto Data

No syntax grinding. Pull real BTC market data with ccxt, process it in pandas, and backtest with vectorbt — 6 steps covering every core pandas skill in crypto quant, from loading data and resampling to hand-rolling RSI/MACD, multi-asset alignment, and feeding a backtest engine.

You can’t do crypto quant without pandas. Your market data is a table, your indicators are columns, and backtest engines eat DataFrames with a time index — vectorbt, Freqtrade, the strategy classes in your strategy/ directory all consume exactly this data structure.

But most people learn pandas the wrong way: they chew through syntax books and memorize APIs, then still can’t handle a single candlestick afterward.

This post is the pandas crash course I put together for myself, and it follows one principle: skip the syntax study; learn by driving real crypto data through it. In quant work, pandas only does four things — load data, organize it into time-series tables, compute technical indicators, and run statistical analysis. I’ve broken it into 6 steps, each with one set of concepts plus a code block that runs directly in JupyterLab. Finish it, and you’ll be able to run the full pipeline on your own: fetch data from ccxt → process in pandas → backtest in vectorbt.

Environment assumption: you have a Python environment with ccxt + pandas + vectorbt installed (mine is the .venv in the LynxCrypto project), and JupyterLab is running. If not, pip install ccxt pandas vectorbt jupyterlab covers everything.

If you’re starting from zero pandas experience, spend 30–60 minutes on one of these HTML tutorials first — all of them let you copy and run code directly:

  • Kaggle’s free Pandas coursehttps://www.kaggle.com/learn/pandas Run code right in the browser plus practice exercises, free. The smoothest way to get the concepts of Series / DataFrame / index / grouping.
  • The official “10 Minutes to pandas”https://pandas.pydata.org/docs/user_guide/10min.html The official quick tour, all code copyable. Builds a global picture of “what pandas can do.”
  • Python for Data Analysis (by pandas author Wes McKinney), chapters 5–9. There’s a free online version; it’s practical and works well as a reference book.

Come back and walk the 6 steps below after finishing any one of these — it’ll go much faster than grinding syntax cold.

Step 1 — Turn Market Data into a DataFrame (Loading + Structure)

Concepts: Series, DataFrame, index, columns, dtypes, head/tail, to_datetime

Task: Pull BTC/USDT candlesticks (OHLCV) from an exchange with ccxt and turn them into a time-indexed table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import ccxt
import pandas as pd

# Fetch BTC/USDT 1-minute candles from binance; public data needs no API key
# Can't connect? Make sure your proxy is running, or switch 'binance' to 'okx'/'gate'
ex = ccxt.binance()
ohlcv = ex.fetch_ohlcv('BTC/USDT', '1m', limit=500)  # 500 candles

# ccxt returns a list of lists: [timestamp, open, high, low, close, volume]
df = pd.DataFrame(ohlcv, columns=['ts', 'open', 'high', 'low', 'close', 'vol'])
df['ts'] = pd.to_datetime(df['ts'], unit='ms')   # millisecond timestamp → datetime
df = df.set_index('ts')                            # use time as the index
df.head()                                          # view first 5 rows
df.dtypes                                          # view each column's data type

Key insight: Each column in the table is a Series, the whole table is a DataFrame, and the leftmost time column is the index. The index is the foundation of every pandas time-series operation — all the resampling and slicing later depends on it.

Step 2 — Time-Series Organization and Slicing (Indexing, Slicing, Resampling)

Concepts: DatetimeIndex, loc/iloc slicing, resample, timezones

Task: Resample 1-minute candles into 4-hour candles and grab a recent window.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Slicing: grab a time range (loc + string time)
recent = df.loc['2026-09-05':]

# Resampling: 1m → 4h OHLC (the standard financial aggregation)
df_4h = df.resample('4h').agg({
    'open': 'first',   # period open = first candle's open
    'high': 'max',      # period high = max of all highs
    'low': 'min',
    'close': 'last',   # period close = last candle's close
    'vol': 'sum'       # period volume = sum
})
df_4h.head()

# Positional slicing (iloc, row numbers)
df.iloc[:10]      # first 10 rows
df.iloc[-5:]      # last 5 rows

Key insight: loc slices by label (time strings / index values); iloc slices by position (row number). resample is the core tool for compressing high-frequency data into lower frequencies — 1m→4h and 1h→1d in quant work all rely on it.

Step 3 — Hand-Rolling Technical Indicators (Column Math, rolling, shift, ewm)

Concepts: column operations, rolling (rolling window), shift (offset), ewm (exponentially weighted), apply/lambda

Task: Without using an indicator library, hand-compute MA, RSI, and MACD to understand how indicators actually work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Moving average MA(20): rolling mean over the past 20 candles
df['ma20'] = df['close'].rolling(20).mean()

# RSI(14): compute price changes first, then smooth with ewm
delta = df['close'].diff()                     # diff = current minus previous candle
gain = delta.clip(lower=0)                      # keep only the positive part (gains)
loss = -delta.clip(upper=0)                     # keep only the negative part (losses), take absolute value
avg_gain = gain.ewm(alpha=1/14, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/14, adjust=False).mean()
rs = avg_gain / avg_loss
df['rsi'] = 100 - 100 / (1 + rs)

# MACD: the difference between two EMAs
ema12 = df['close'].ewm(span=12, adjust=False).mean()
ema26 = df['close'].ewm(span=26, adjust=False).mean()
df['macd'] = ema12 - ema26
df['signal'] = df['macd'].ewm(span=9, adjust=False).mean()
df['hist'] = df['macd'] - df['signal']

df[['close', 'ma20', 'rsi']].tail()             # view the most recent rows

Key insight: rolling(20).mean() means “open a 20-element sliding window and take the mean” — nearly every trend indicator derives from this. shift(1) moves data back one row, commonly used for “today’s value vs. yesterday’s value.” ewm is a mean with decaying weights; both RSI and MACD use it. Hand-roll these once, and you’ll understand what the ready-made functions in ft-pandas-ta are doing under the hood.

Step 4 — Multi-Asset Alignment (concat, merge, Correlation)

Concepts: concat, merge/join, multi-asset time alignment, fillna, corr (correlation)

Task: Align BTC and ETH closing prices into one table and compute their correlation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# First fetch ETH's close series the same way
eth = ex.fetch_ohlcv('ETH/USDT', '1m', limit=500)
eth_df = pd.DataFrame(eth, columns=['ts','o','h','l','c','v'])
eth_df['ts'] = pd.to_datetime(eth_df['ts'], unit='ms')
eth_close = eth_df.set_index('ts')['c']

# Horizontal concat: combine BTC close and ETH close into two columns (axis=1 = by column)
both = pd.concat({'BTC': df['close'], 'ETH': eth_close}, axis=1)
both = both.dropna()            # drop rows that didn't align

both.corr()                     # correlation matrix
both['spread'] = both['BTC'] - both['ETH']   # new column = difference of two columns
both.plot()                     # plot directly (built into pandas)

Key insight: The core of multi-asset strategies (pairs trading, cross-coin arbitrage) is “time alignment” — when two coins’ timestamps don’t line up, you fillna or dropna; concat(axis=1) is the workhorse for horizontal merging. The high correlation between BTC and ETH is a basic fact of the crypto market, and both pairs trading and statistical arbitrage are built on top of it.

Step 5 — Statistics and Visualization (describe, groupby, Plotting)

Concepts: describe, pct_change (returns), groupby, agg (aggregation), plot

Task: Compute the return distribution, group by day/hour to look for patterns, and plot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
df['ret'] = df['close'].pct_change()          # return = (today - yesterday) / yesterday
df['ret'].describe()                          # quick look at mean / std / quantiles

# Group by hour: which hour is the most volatile?
df['hour'] = df.index.hour
df.groupby('hour')['ret'].agg(['mean', 'std', 'count'])

# Aggregate returns by day
df['day'] = df.index.date
daily_ret = df.groupby('day')['ret'].sum()

# Plotting (rendered inline in the notebook)
df[['close','ma20']].plot(title='BTC close + MA20')
df['ret'].hist(bins=50)                        # histogram of the return distribution

Key insight: describe() is the fastest health check on your data. groupby means “group by a column, then compute per group” — finding patterns by hour / day of week / month in quant work relies on it entirely. The shape of the return distribution (normal or not, fat tails or not) determines your strategy’s risk assumptions. Crypto returns have obvious fat tails, which is exactly why martingale loses in the long run (see the liquidation math in my previous post).

Step 6 — Wiring into vectorbt (Feeding the DataFrame to a Backtest Engine)

Concepts: No new pandas this step — it’s about understanding “how research output enters a backtest”: a pandas DataFrame is vectorbt’s standard input.

Task: Build a crossover signal from the MA20 computed in Step 3 and feed it to vectorbt for backtesting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import vectorbt as vbt

# Signal: buy when close crosses above MA20, sell on cross below (boolean Series)
entries = df['close'] > df['ma20']
exits   = df['close'] <= df['ma20']

# vectorbt takes the close (Series) + boolean entry/exit signals
pf = vbt.Portfolio.from_signals(df['close'], entries, exits, init_cash=10000)
pf.stats()                  # backtest stats: total return / win rate / max drawdown, etc.
pf.plot()                   # backtest equity curve

Key insight: At this point pandas output is “finished goods” — vectorbt and the strategy classes in your strategy/ directory consume exactly this kind of time-indexed DataFrame. You explore ideas in the notebook during the research phase (these steps), then distill the settled logic back into .py modules under strategy/. That’s the division of labor between Jupyter and engineering code: the notebook is scratch paper; .py is the deliverable.

What You Can Do After This

  • You’ve run pandas’ 6 core quant actions (load / slice / compute indicators / align / statistics / feed backtest) on real crypto data
  • You can independently run the full pipeline: ccxt data → pandas processing → vectorbt backtest
  • Next: take these hand-rolled indicators and signals and compare them against the strategies already in your strategy/ directory (I compared against MacdCross) to understand the correspondence between the “notebook exploration version” and the “engineering module version”

Four Anti-Patterns (I Stepped on These So You Don’t Have To)

  • Don’t cram ten ideas into one notebook — one notebook, one research question, or three months later even you won’t be able to read it.
  • Write the assumptions and conclusions at the top of the notebook — when you come back, read those two lines first, then the code.
  • Distill settled logic back into .py — keep only the research process in the notebook; don’t let it become a stew pot.
  • Don’t just read — run it — type out every code block yourself, tweak the parameters, and watch what changes in the output. You can’t learn by looking alone.

This post is the hands-on entry of the LynxCrypto series. Other posts in the series: Research Blueprint · From Grid Martingale to Alpha (Part 1) · The Falsification Post · WFO Final Verdict · Tooling Roadmap