The previous two posts on this site covered who actually makes the money in quant (Inside Quadrature Capital and Radix Trading) and why their edge persists (Why Top Quant Desks Outperform). This post is the runnable companion. A specific GitHub stack you can install in an afternoon, a concrete backtest you can run today, and the published academic results that tell you what kind of returns are realistic before you put real money on the table.
The frame is honest. Retail does not beat Citadel. Retail does not need to. Retail needs to beat the median retail trader, and the median retail trader is not running honest backtests, not subtracting realistic fees, and not respecting research discipline. If you do those three things, you are above the curve.
The stack, one tool per job
| Layer | Library | What it does | License |
|---|---|---|---|
| Data | ccxt | Unified API across 100+ exchanges, including Coinbase Advanced | MIT |
| Backtesting (research) | vectorbt | Vectorized, fast, parameter-sweep oriented | Apache 2.0 |
| Backtesting (deployment) | Backtesting.py | Simple API, good for single-strategy work | AGPL-3.0 |
| Live execution (crypto) | freqtrade | Full bot framework with paper, dry-run, and live modes | GPL-3.0 |
| Native Coinbase access | coinbase/coinbase-advanced-py | Official Python SDK, REST and WebSocket | Apache 2.0 |
| Data analysis | pandas, polars, NumPy | The default for time-series work | various |
| Plotting | plotly, matplotlib | Self-explanatory | various |
Two notes worth flagging up front. ccxt is the right place to start for cross-exchange research because it normalizes naming, market metadata, and historical OHLCV calls across venues. The official Coinbase Advanced SDK is the right place to be when you go live, because it handles authentication, rate limits, and websocket reconnection without you reinventing those wheels. (Coinbase Advanced API Python SDK)
The fee math you cannot skip
Coinbase Advanced charges 0.40 percent maker and 0.60 percent taker for accounts under $10,000 of monthly volume. Volume tiers reduce that, with the lowest published taker fee at 0.05 percent for traders above $250 million in monthly volume. Stablecoin pairs (USDC pairings other than USDT/USDC and USDT/USD as of May 2025) carry their own pricing. (Coinbase Advanced fee schedule, Coinbase exchange fees, help center)
For backtesting, the relevant numbers if you are starting out are 0.6 percent taker, 0.4 percent maker. A reasonable default if you do not split execution between maker and taker is to model 0.5 percent round-trip per rebalance, which approximates a mix of the two. That number alone is enough to wipe out the edge in many strategies you read about. The first job of an honest backtest is to surface that.
What the academic literature says before you start
Before writing any code, set realistic expectations.
Han, Kang, and Ryu (2023) ran cryptocurrency time-series and cross-sectional momentum strategies under realistic assumptions including transaction costs, finding that evidence of time-series momentum is strong while cross-sectional momentum is weak once costs are included. Many published cross-sectional momentum portfolios stop being statistically significant once realistic execution costs are subtracted. (SSRN 4675565)
Huang, Sangiorgi, and Urquhart published volume-weighted time-series momentum results showing strategy performance up to 0.94 percent per day with an annualized Sharpe ratio of 2.17 in their tested window, on volume-weighted market returns rather than naïve simple-average baskets. (SSRN 4825389)
Zarattini, Pagani, and Barbon presented a tactical momentum model on a rotational portfolio of the top 20 most liquid coins, reporting a Sharpe ratio above 1.5 net of fees and an annualized alpha of 10.8 percent versus Bitcoin in their test window. (SSRN 5209907)
Read these as a range, not a guarantee. They were published in different windows, on different universes, with different cost assumptions. The honest reading is: time-series momentum on a top-N liquid universe, properly costed, has historically produced Sharpe ratios in the 1 to 2 range, with material drawdowns. That is a real edge if you can capture it. It is not the 24.8 percent monthly story you read on Medium.
Install
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install ccxt pandas numpy vectorbt plotly
That is enough for the backtest below. To go further:
pip install coinbase-advanced-py # native Coinbase
pip install freqtrade # full live-trading bot
pip install quantstats # tearsheet reporting
A working time-series momentum backtest
Save this as tsmom_backtest.py. It pulls daily OHLCV from Coinbase Advanced via ccxt, computes a simple time-series momentum signal on each pair, equal-weights the long pairs, and applies a realistic taker fee. Run it as python tsmom_backtest.py. The print at the end is your backtest, on your universe, in your window. Do not trust any number on a website unless you have rerun it.
"""
Time-series momentum backtest on Coinbase USD pairs.
Educational. Not investment advice. Costs and outcomes vary.
"""
import time
import ccxt
import numpy as np
import pandas as pd
EXCHANGE = ccxt.coinbase()
LOOKBACK_DAYS = 540 # ~18 months of daily data
TS_LOOKBACK = 30 # 30-day momentum signal
MIN_24H_QUOTE_VOL_USD = 5_000_000
TAKER_FEE = 0.006 # 0.6% Coinbase Advanced default
SLIPPAGE = 0.0005 # 5 bps per fill assumption
def liquid_usd_pairs(min_vol):
EXCHANGE.load_markets()
pairs = []
for sym, m in EXCHANGE.markets.items():
if m.get("quote") == "USD" and m.get("active") and "/" in sym:
try:
t = EXCHANGE.fetch_ticker(sym)
except Exception:
continue
qv = t.get("quoteVolume") or 0
if qv >= min_vol:
pairs.append(sym)
time.sleep(EXCHANGE.rateLimit / 1000)
return pairs
def daily_close(symbol, days):
since = EXCHANGE.parse8601(
(pd.Timestamp.utcnow() - pd.Timedelta(days=days)).strftime("%Y-%m-%dT00:00:00Z")
)
rows, cursor = [], since
while True:
batch = EXCHANGE.fetch_ohlcv(symbol, timeframe="1d", since=cursor, limit=300)
if not batch:
break
rows.extend(batch)
cursor = batch[-1][0] + 86_400_000
time.sleep(EXCHANGE.rateLimit / 1000)
if len(batch) < 300:
break
df = pd.DataFrame(rows, columns=["ts", "o", "h", "l", "c", "v"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
return df.set_index("ts").sort_index()["c"].rename(symbol)
def build_panel(symbols, days):
series = []
for s in symbols:
try:
series.append(daily_close(s, days))
except Exception as e:
print(f"skip {s}: {e}")
return pd.concat(series, axis=1).sort_index().ffill(limit=2)
def tsmom_weights(panel, lookback):
momentum_positive = (panel.pct_change(lookback) > 0).astype(float)
n_active = momentum_positive.sum(axis=1).replace(0, np.nan)
weights = momentum_positive.div(n_active, axis=0).fillna(0)
return weights
def backtest(panel, weights, fee, slippage):
daily_returns = panel.pct_change().fillna(0)
gross = (weights.shift(1) * daily_returns).sum(axis=1)
turnover = (weights - weights.shift(1)).abs().sum(axis=1)
cost = turnover * (fee + slippage)
net = gross - cost
equity = (1 + net).cumprod()
return net, equity, turnover
def stats(net, equity):
daily_mu, daily_sd = net.mean(), net.std()
ann_ret = daily_mu * 365
ann_vol = daily_sd * np.sqrt(365)
sharpe = ann_ret / ann_vol if ann_vol > 0 else float("nan")
drawdown = (equity / equity.cummax() - 1).min()
return {
"annualized_return": round(ann_ret, 4),
"annualized_vol": round(ann_vol, 4),
"sharpe": round(sharpe, 3),
"max_drawdown": round(drawdown, 4),
"n_observations": len(net),
}
if __name__ == "__main__":
print("loading Coinbase markets…")
pairs = liquid_usd_pairs(MIN_24H_QUOTE_VOL_USD)
print(f"liquid USD pairs: {len(pairs)}")
panel = build_panel(pairs, LOOKBACK_DAYS)
weights = tsmom_weights(panel, TS_LOOKBACK)
net, equity, turnover = backtest(panel, weights, TAKER_FEE, SLIPPAGE)
print("\nbacktest summary")
for k, v in stats(net, equity).items():
print(f" {k:>22}: {v}")
print(f"\naverage daily turnover: {turnover.mean():.3f}")
equity.to_csv("tsmom_equity.csv")
print("\nwrote tsmom_equity.csv (cumulative net equity curve)")
A few things to notice in the code, because they matter more than they look.
The signal is shifted. weights.shift(1) * daily_returns is critical. If you do not lag the weights by one bar, you have look-ahead bias and the backtest is fiction. This is the single most common bug in retail backtests.
Fees and slippage are charged on turnover, not on every position. A pair that stays in the basket through a rebalance does not incur a fee. Many retail backtests double-count fees here.
The universe is pulled from current liquid pairs. This is survivorship bias and you should know it. A more honest backtest would pull the historical liquid set on each rebalance date. For a first pass to set expectations, the current snapshot is fine; for capital allocation, it is not.
The output is a CSV, not a plot. Treat your equity curve as data, not a story. Open it in your tool of choice, look for regime breaks, and ask yourself which window is doing the work.
The mature enhancements anyone can add
Each of these turns the toy backtest above into something closer to a research-quality system. None of them require new libraries.
1. Volatility targeting. Scale each pair's weight by the inverse of its rolling realized volatility, then renormalize. The intuition is that a basket of equal-weighted assets is dominated in risk terms by the most volatile asset. Vol targeting flattens that.
def vol_targeted_weights(panel, base_weights, vol_lookback=30, target_vol=0.50):
realized_vol = panel.pct_change().rolling(vol_lookback).std() * np.sqrt(365)
scaler = (target_vol / realized_vol).clip(upper=2.0).fillna(0)
raw = base_weights * scaler
return raw.div(raw.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
2. Regime gating. Crypto momentum is asymmetric. Long-only momentum pays in expansionary regimes and gets shredded in 2018-style or 2022-style drawdowns. Add a Bitcoin regime filter that turns the strategy off when BTC sits below its 200-day moving average.
def regime_filter(btc_close, ma_window=200):
return (btc_close > btc_close.rolling(ma_window).mean()).astype(float)
# usage
btc = panel["BTC/USD"]
regime = regime_filter(btc).reindex(weights.index, method="ffill").fillna(0)
weights = weights.mul(regime, axis=0)
3. Daily turnover cap. Limit how much the basket can churn per rebalance. Caps the cost drag and forces the strategy to make conviction-based changes rather than noise-based ones.
def cap_turnover(weights, max_daily_turnover=0.30):
out = weights.copy()
for i in range(1, len(out)):
prev = out.iloc[i - 1]
target = out.iloc[i]
delta = target - prev
if delta.abs().sum() > max_daily_turnover:
scale = max_daily_turnover / delta.abs().sum()
out.iloc[i] = prev + delta * scale
return out
4. Kelly-fraction sizing on the strategy itself. The output of the backtest above is unleveraged. Real allocation involves choosing a fraction of bankroll. Use a quarter-Kelly cap on the overall strategy: never more than 0.25 × (mean_return / variance) of bankroll deployed. For small accounts this is dominated by personal risk tolerance, but the math keeps you from over-betting on a noisy edge.
5. Walk-forward, not in-sample. Run the same backtest above for three windows: train on 2020-2022, validate on 2023, test on 2024. If the parameters that work in train are different from those that work in validate and test, your model has fit to noise. The Han, Kang, Ryu (2023) paper applies a similar discipline; it is the right discipline. (SSRN 4675565)
6. Polymarket sharp-wallet overlay. A genuine retail edge that does not show up in any institutional research. Pull Polymarket Subgraph data via Dune, identify wallets with lifetime PnL above $100,000, win rate above 60 percent, and at least 50 trades in the last 90 days. A cluster of three or more such wallets entering the same market in the same direction within 24 hours is a tradeable signal. The decay profile is short, on the order of 4 to 12 hours. None of the elite firms cited in this series compete here.
Going from research to live
The same backtest logic does not become a live bot just because you wrap it in a while True loop. The serious move is to translate the strategy into freqtrade, which gives you paper trading (dry-run), historical replay, hyperparameter optimization (Hyperopt), and live execution against Coinbase, Binance, Kraken, and others, all from one repo. (freqtrade GitHub, freqtrade docs on Coinbase Pro / Advanced)
For pure Coinbase work, the official coinbase-advanced-py SDK gives you direct REST and WebSocket access for order placement, account state, and live market data. Stub out the strategy logic from the backtest above, route it through the SDK, and start with dry-run for at least 30 days before any real money moves. (Coinbase Advanced Python SDK)
The single biggest upgrade you can make in the live transition is logging. Every signal, every fill, every fee, every rejection, every reconnect. Live trading reveals dozens of edge cases that backtests cannot simulate. The traders who survive the transition are the ones who keep enough state to debug after the fact.
How to use the whale framework with this stack
For people who already work from the Whale Intelligence Polymarket integration, the natural pairing is:
- Run the time-series momentum stack above on Coinbase top-20 USD pairs as the always-on base allocation.
- Add a discretionary sleeve sized at no more than 20 percent of bankroll, driven by the whale framework's CRITICAL-tier signals: cross-system insider plus options sweep plus Polymarket sharp wallet alignment.
- Use the same Section 1256 and Section 475(f) considerations from the prior post when you choose between trading SPY options versus SPX options versus ES futures for any equity-side hedge.
- Track expected versus realized for both sleeves separately. The strategy and the discretionary book diverge, so do not pool them in your evaluation.
That setup is replicable. It is also still a small fraction of what an institutional desk does. The difference is that this fraction is honest, costed, and yours.
Related reading
- Inside Quadrature Capital and Radix Trading for the institutional context
- Why Top Quant Desks Outperform And How U.S. Tax Code Multiplies The Edge for the structural reasons behind cited returns
- A 24.8 Percent AI Crypto Portfolio, Back-Checked for the cost-and-tax adjustment template applied to a real article
- What Actually Fixed My Claude Code Sessions for the research-process foundation that turns trading work into shippable systems
Fact-check notes and sources
- Coinbase Advanced fee schedule: Coinbase Advanced product page, Coinbase exchange fees help, Coinbase blog on volume tier upgrade
- Official Coinbase Python SDK: coinbase/coinbase-advanced-py on GitHub, docs.cdp.coinbase.com
- ccxt: github.com/ccxt/ccxt
- vectorbt: github.com/polakowo/vectorbt, vectorbt.dev
- Backtesting.py: github.com/kernc/backtesting.py
- freqtrade: github.com/freqtrade/freqtrade
- Awesome systematic trading list: github.com/wangzhe3224/awesome-systematic-trading
- Han, Kang, Ryu (2023) on cryptocurrency time-series and cross-sectional momentum under realistic assumptions: SSRN 4675565
- Huang, Sangiorgi, Urquhart on volume-weighted time-series momentum: SSRN 4825389
- Zarattini, Pagani, Barbon on tactical momentum, top-20 coin universe: SSRN 5209907
- Coverage on AI tools for quant research, Nov 2025: alphacorp.ai blog
- IRS material on trader tax topics referenced from the companion post: IRS Topic 429
This post is informational and educational, not investment, tax, or legal advice. The code shown is for illustration; running it produces a result on your machine, in your window, with your assumptions, and that result is not a forecast. Cryptocurrency trading involves risk of total loss. Past performance of any cited academic strategy is not indicative of future results. Consult a qualified financial advisor and CPA before deploying capital.