agents.surf

Getting intoQuant Finance.

AgenKit is the harness that builds the trading bot from one prompt, even if you don't write Rust, including the low-latency path an HFT strategy needs. A spec and failing tests come first, then the Rust signal, a review, and paper trading.

The curriculum, as one map Open the guide

Hover a point, then click to open that lecture.

What Quant Trading Actually Is

Quant trading is mathematics applied to markets, repeated often enough that a small edge becomes a business.

The casino, not the gambler

A casino never tries to predict whether one roulette spin will win. It runs the game millions of times with a small, known mathematical edge (2.7% on European roulette) and lets the law of large numbers turn that edge into revenue. Quant firms operate the same way:

  1. Find an edge. A statistical relationship, pricing inefficiency, or structural feature of the market that gives positive expected value.
  2. Size it correctly. Big enough to matter, small enough to survive the variance.
  3. Repeat it thousands of times. Across instruments, across time, across strategies, so the average shows through the noise.

Edges exist because markets are complex systems run by humans and machines that make systematic, repeatable mistakes: overreacting to news, being forced to sell, paying for immediacy, ignoring small-probability events, or simply being slower.

0 +edge 1 bet win or lose ≈ a coin flip 10,000 bets average P&L per bet collapses onto the edge

The only equation that matters (at first)

Every trade is a bet with an expected value. If you win \(W\) with probability \(p\) and lose \(L\) with probability \(1-p\):

\[ \mathbb{E}[\text{P\&L}] = p\,W - (1-p)\,L - \text{costs} \]Expected value per trade, after costs. Positive EV is necessary but not sufficient: you also have to survive the variance.

A 52% win rate on even-money bets with 0.5% costs per round trip sounds like nothing. Do it 2,000 times a year with correct sizing and it compounds into a serious business. Do it once and it is a coin flip. One trial is noise. Two thousand trials, sized correctly, are a business.

The main families of strategies

FamilyWhere the edge comes fromTypical horizon
Market makingEarning the bid-ask spread for providing liquidity, while managing inventory and avoiding informed traders.µs – minutes
Statistical arbitrageMean reversion between related assets (pairs, baskets, factor residuals).minutes – weeks
Momentum / trendUnder-reaction and herding: winners keep winning for a while.days – months
Factor investingPersistent risk premia: value, quality, low volatility, carry.months – years
Volatility / optionsImplied volatility tends to exceed realised volatility; mispriced skew and term structure.days – months
Event-drivenSystematic reactions to earnings, index rebalances, macro releases, mergers.minutes – weeks
Latency arbitrage / HFTBeing first to react to information across venues.ns – ms
Prediction marketsImplied probabilities that deviate from what the data supports; cross-market arbitrage.hours – months

agents.surf catalogues 200 of these strategies, each with the math, a test plan, and a Rust signal layer. They are worked examples of the families above, from factor investing out to market making and latency arbitrage.

Discretionary vs. systematic

A discretionary trader decides each trade. A systematic (quant) trader decides the rules, tests them on history, and lets a machine execute. The quant's advantage is that every rule can be measured, falsified, and improved. The quant's danger is that a machine will execute a bad rule perfectly, thousands of times, at 3 AM. That is why most of this guide is about testing and risk, not about clever signals.

Once the rule exists, someone still has to turn it into a trading bot. The next lecture is that step: what has to be specified and tested before the code, including when the strategy is the fast kind.

From the Idea to a Trading Bot

You now know what an edge is, and you have 200 worked examples on this site. The remaining question is how an idea becomes a system: data, tests, a signal, and something that can run without you watching it.

Why a chat is not enough

A coding chat will write a trading script the moment you describe a strategy. It will also skip the tests, charge costs on the wrong bar, and forget the kill switch. That is a nuisance for a slow daily strategy. It is fatal for anything on the short end of the table you just read, where a market-making or HFT path lives or dies on latency, inventory, and whether the code did exactly what the spec said.

The fix is a harness: a written spec, tests that fail until the dangerous cases are handled, then the code, then a review of the code against the spec. You stay at the points where a decision is required. Nothing ships because a model was confident.

Where AgenKit fits

AgenKit is that harness, inside the coding tool you already use: Claude Code, Cursor, Codex, OpenCode, Antigravity, GitHub Copilot, and Kimi Code. You start it with /agenkit, or @agenkit in Copilot, and describe the strategy in plain language. You do not start by opening a Rust file.

The 200 strategies on agents.surf already use a Rust signal layer. AgenKit can build that layer from the spec, whether the strategy is a daily mean-reversion book or a low-latency execution path. You do not need to have been a Rust engineer first. You need to read what it claims and reject it when the claim is wrong.

  1. A spec. The signal, the data it is allowed to see, the costs, and the risk limits. You approve this before any code exists.
  2. Failing tests. Look-ahead, missing costs, and a broken risk limit fail on purpose. The build cannot go green by skipping them.
  3. The trading bot. Implementation follows the spec, including the Rust signal. A reviewer blocks code that compiles and still disagrees with the math.
  4. Paper trading. The first run is paper. Live capital, including anything that needs to be fast, is a later decision with the same code.

A plain prompt is enough to start: a daily mean-reversion strategy on liquid US stocks, costs on every fill, no look-ahead, and a stop if the drawdown hits the limit in the spec. The build walkthrough later in this guide shows the commands. The license key is on agenkit.xyz.

npx agenkit activate <license-key>
npx agenkit install engineering-kit

Node 18 or newer. After it installs, /agenkit is how you start.

The Roles Inside a Quant Firm

"Quant" is four or five different jobs. Preparation differs a lot between them, so pick a direction early.

The finder
Quant researcher

Finds patterns in huge datasets, builds predictive models, designs strategies. PhD-level math, stats and ML, or an exceptional undergraduate record in a hard quantitative field.

The decision-maker
Quant trader

Runs capital in real time using researchers' models. Fast probabilistic thinking, strong mental math, calm decisions under pressure. Highest pay variance in the industry.

The builder
Quant developer

Builds trading platforms, execution engines, real-time data pipelines and low-latency systems. Production C++, Rust and Python.

The guardian
Risk quant

Model validation, value at risk, stress testing, regulatory work. Most stable path, most predictable pay, lower ceiling.

The fastest-growing hybrid is the AI / ML quant: deep learning for signal generation, alternative data at scale, models deployed straight into live trading. It sits between quant research and ML engineering, and it is where much of the 2025–2026 hiring growth has been.

What it pays

Figures below are publicly reported ranges from 2025 compensation data and press coverage. Treat them as orders of magnitude, not offers.

LevelTop tier (Jane Street, Citadel, HRT)Mid tier (Two Sigma, DE Shaw, etc.)
New grad$300K – $500K+ (QR up to ~$650K)$250K – $350K
Mid career (3–7 yrs)$550K – $950K$350K – $625K
Senior (8+ yrs)$1M – $3M+$575K – $1.2M
Star trader / PM$3M – $30M+Highly variable

Jane Street's average compensation per employee was reported at roughly $1.4M for the first half of 2025. Interns at firms like IMC have earned the annualised equivalent of more than $240K. These numbers exist for one reason: the people who build and run edges generate far more than they cost.

Which role fits you

If you are…Aim forDouble down on
Strong in math / stats, enjoy open researchQuant researcherStatistics, ML, research projects you can defend
Fast, competitive, love games and bettingQuant traderProbability puzzles, mental math, market-making games
A software engineer who loves performanceQuant developerC++/Rust, systems, networking, data engineering
Careful, rigorous, like regulation and modelsRisk quantStatistics, derivatives pricing, stress testing
Building for yourself, not an employerIndependent systematic traderAll of the above at a smaller scale, plus engineering discipline
Myth to kill now

You do not need an Ivy League name or a finance degree. You need mathematical ability, programming skill, and proof you can turn both into something that works. The rest of this guide is how you build that proof.

What a Trading System Actually Is

Before the math, see the whole machine. Every quant system, from a weekend bot to a Citadel desk, has the same six stages.

Research · Python Production · Rust / C++ / Python Datacollect, clean,timestamp, store Signalfeatures, model,forecast Backtestcosts, walk-forward, stats Risksizing, limits,kill switch Executionorders, fills,latency MonitorPnL, drift,alerts research loop: live results become the next hypothesis AgenKit role Data engineer Conductor Test engineer Security Perf + Infra Frontend Backend architect designs the interfaces between every box. Code reviewer checks each one against the spec. Debugger traces bad fills back to the cause.

The same six boxes are also how you should split the work when an AI agent writes the code. An engineering harness like AgenKit assigns each stage to a specialist (the green labels above), so data, testing and risk each get an owner instead of being an afterthought in one long prompt.

The six stages in one paragraph each

1. Data. Prices, order books, fundamentals, alternative data. Collected from exchanges or vendors, cleaned, aligned to a single clock, stored so the past can be replayed exactly. Bad data is the most common silent killer of strategies.

2. Signal. A rule or model that turns data into a forecast: "this spread will narrow", "this stock will outperform its sector next week", "this contract is underpriced by 4 cents". This is where probability, statistics and ML live.

3. Backtest. Replay history as if you were trading it, with realistic costs, delays and constraints. The purpose is not to find a great curve. It is to try hard to kill the idea and see whether it survives.

4. Risk and sizing. How much to bet on each signal, the maximum loss you will tolerate, and the hard limits that stop the machine when something breaks.

5. Execution. Turning target positions into orders and fills: which venue, which order type, how fast, how to avoid moving the price against yourself.

6. Monitoring. Live PnL, slippage vs. backtest, data gaps, model drift, latency, alerts. The loop closes when what you learn live becomes the next research question.

Rule of thumb

Beginners spend 90% of their time on stage 2. Professionals spend most of theirs on stages 1, 3, 4 and 6. A mediocre signal with excellent data, testing and risk will outlive a brilliant signal with sloppy engineering.

Why Probability Comes First

Everything in quantitative finance comes down to one question: what are the odds, and are they in my favour?

Options pricing is a probability problem. Market making is a probability problem. Position sizing is a probability problem. Prediction markets are probability problems with a price tag. If you skip this layer, nothing above it will hold.

Conditional thinking

Most people think in absolutes. Quants think in conditionals: given what I know right now, how likely is this?

\[ P(A \mid B) = \frac{P(A \cap B)}{P(B)} \]The probability of A given B: both happening, divided by B happening.

Suppose a market moves sharply after 40% of economic announcements. That is the base rate, and on its own it is not tradable. But on days when options implied volatility is elevated beforehand, a sharp move happens 68% of the time. That conditional probability is a usable signal. The 40% mixes signal and noise in a way you cannot separate without conditioning.

1,000 daysmacro announcements 250 high-IV daysP(high IV) = 25% 750 normal daysP(normal) = 75% 170 sharp movesP(sharp | high IV) = 68% 80 quiet32% 230 sharp movesP(sharp | normal) ≈ 31% 520 quiet69%

Bayes' theorem: updating beliefs

\[ \underbrace{P(H \mid D)}_{\text{posterior}} = \frac{\overbrace{P(D \mid H)}^{\text{likelihood}}\;\overbrace{P(H)}^{\text{prior}}}{\underbrace{\sum_i P(D \mid H_i)\,P(H_i)}_{\text{evidence}}} \]Your updated belief equals how well the hypothesis explains the data, times how much you believed it before, normalised over every hypothesis.

A model says a stock is worth $50. Earnings come out 3% above estimate. The posterior shifts up. The traders who update fastest and most accurately when new information arrives are the ones who get paid. In practice you rarely compute the denominator by hand: you use conjugate priors (below) or Monte Carlo sampling.

Random variables and distributions you must know

DistributionModelsWhere you will meet it
Bernoulli / BinomialWin/lose, number of wins in n trialsHit rates, prediction market outcomes
GeometricTrials until first successInterview puzzles, waiting times
PoissonCounts of rare events per intervalOrder arrivals, trade counts, jumps
ExponentialTime between Poisson eventsTime to next fill, queue modelling
NormalSums of many small effectsEverything, as a first approximation
Log-normalProducts of many small effectsAsset prices under GBM / Black-Scholes
Student-tNormal with fat tailsReal daily returns
BetaA probability that is itself uncertainBayesian win-rate estimates, prediction markets

Expected value and variance

\[ \mathbb{E}[X] = \sum_x x\,P(X=x) \qquad \operatorname{Var}(X) = \mathbb{E}\big[(X-\mathbb{E}[X])^2\big] = \mathbb{E}[X^2]-\mathbb{E}[X]^2 \]Expected value is your edge. Variance is your risk. You need both numbers for every decision.

If your strategy has positive expected value and you can survive the variance long enough, you will make money. If you size too large relative to the variance, you go broke before the expected value has time to show up. Chapter 12 makes this precise with the Kelly criterion.

Two theorems justify the whole business:

  • Law of large numbers: the sample average converges to the expected value as \(n \to \infty\).
  • Central limit theorem: the sample average is approximately normal with standard error \(\sigma/\sqrt{n}\). To halve your uncertainty you need four times the data.
Lab · law of large numbers

Code: simulate, then update like a Bayesian

import numpy as np
from scipy import stats

rng = np.random.default_rng(42)

# 1) Law of large numbers: running average converges to the true p
flips = rng.random(10_000) < 0.55
running = np.cumsum(flips) / np.arange(1, flips.size + 1)
print(f"after 100: {running[99]:.3f}  after 10,000: {running[-1]:.4f}  (true 0.55)")

# 2) Bayesian updater for a strategy's win rate (Beta-Binomial, conjugate prior)
#    Prior Beta(a, b): start sceptical, centred on 50% with the weight of 20 fake trades
a, b = 10, 10
trades = rng.random(60) < 0.57          # 60 live trades, true win rate 57%
wins, losses = trades.sum(), (~trades).sum()
post = stats.beta(a + wins, b + losses)

print(f"wins {wins}/{trades.size}")
print(f"posterior mean win rate: {post.mean():.3f}")
print(f"95% credible interval:   {post.ppf(0.025):.3f} – {post.ppf(0.975):.3f}")
print(f"P(true win rate > 50%):   {1 - post.cdf(0.5):.3f}")

Homework (3–4 weeks at 2 hours a day)

  • Read Blitzstein & Hwang, Introduction to Probability (free from Harvard). Work every problem in chapters 1–6. Watch the matching Stat 110 lectures.
  • Code: simulate 10,000 coin flips and plot the running average. Then a Bayesian updater that takes a prior and returns a posterior.
  • Puzzle: you flip a fair coin until you get two heads in a row. What is the expected number of flips? (Answer in chapter 19. Try it first.)

Why Most Signals Are Noise

Once you speak probability, you need to learn to listen to data. The first lesson: most of what looks like signal is noise.

Hypothesis testing

Your strategy backtests at 15% a year. Is it real? Set it up as a test:

  1. Null hypothesis \(H_0\): the strategy has zero true expected return.
  2. Compute a test statistic, e.g. the t-statistic of mean daily return: \(t = \bar r / (s/\sqrt{n})\).
  3. The p-value is the probability of seeing a result this strong if \(H_0\) were true.

A handy link: the t-statistic of daily returns is roughly the annualised Sharpe ratio times \(\sqrt{\text{years}}\). A Sharpe of 1 over 4 years gives \(t \approx 2\), which is only borderline significant.

The multiple comparisons trap

Test 1,000 random strategies and about 50 will show \(p < 0.05\) purely by chance. That is the single most common reason backtests look great and live results are terrible. Run the lab below: every strategy is pure noise, yet the best one looks like a hedge fund.

Lab · 1,000 strategies with zero edge
3 years of daily returns each

Fixes, from simplest to most rigorous:

  • Bonferroni: divide your significance threshold by the number of tests, \(\alpha/m\). Simple and very conservative.
  • Benjamini–Hochberg: controls the false discovery rate instead. Sort p-values and accept \(p_{(k)} \le \tfrac{k}{m}\alpha\).
  • Raise the bar: Harvey, Liu and Zhu argue a new factor needs \(t > 3\), not 2, given how many have been tried.
  • Deflated Sharpe ratio (chapter 11): adjusts the Sharpe for the number of trials, skew and fat tails.
  • Write down every trial. If you do not know how many things you tried, you cannot correct for them.
Hard truth

Your first ten strategies will almost all be noise. Accept that now and it will save you a lot of money.

Regression: decomposing returns

Linear regression \(y = X\beta + \varepsilon\) is the workhorse. In finance you regress strategy returns on known risk factors:

\[ r_t - r_f = \alpha + \beta_{\text{mkt}}(r_{m,t}-r_f) + \beta_{\text{smb}}\,\text{SMB}_t + \beta_{\text{hml}}\,\text{HML}_t + \varepsilon_t \qquad \hat\beta = (X^\top X)^{-1}X^\top y \]Fama–French 3-factor regression and the OLS estimator. The intercept α is the return no known factor explains.

If \(\alpha\) is zero after the factors, your "edge" is just disguised market, size or value exposure, which anyone can buy cheaply in an ETF. Financial returns are autocorrelated and heteroskedastic, so default OLS standard errors are wrong. Use Newey–West (HAC) standard errors.

Maximum likelihood estimation

\[ \hat\theta_{\text{MLE}} = \arg\max_\theta \sum_{i=1}^n \log f(x_i \mid \theta) \]Choose the parameters that make the observed data most likely. When a desk says "calibrate", it usually means MLE.

MLE fits GARCH volatility models, jump-diffusions, and option-pricing models to market quotes. It is asymptotically efficient: for large samples no consistent estimator has lower variance (the Cramér–Rao bound).

Fat tails: why the normal distribution lies

Real daily returns have far more extreme days than a normal distribution predicts. A "5-sigma" day should happen once every ~14,000 years under normality; equity markets produce several per decade. Model tails with a Student-t, and stress-test with history, never with a bell curve alone.

import numpy as np, pandas as pd, yfinance as yf, statsmodels.api as sm
from scipy import stats
from statsmodels.stats.multitest import multipletests
import pandas_datareader.data as web

# 1) Fat tails: fit a Student-t by MLE to real SPY returns
r = yf.download("SPY", start="2010-01-01", auto_adjust=True, progress=False)["Close"].squeeze().pct_change().dropna()
df_, loc, scale = stats.t.fit(r)
print(f"normality test p = {stats.normaltest(r).pvalue:.1e}  (reject: fat tails)")
print(f"Student-t degrees of freedom = {df_:.2f}  (normal would be infinite)")

# 2) Fama-French 3-factor regression with Newey-West standard errors
ff = web.DataReader("F-F_Research_Data_Factors_daily", "famafrench", start="2010-01-01")[0] / 100
aapl = yf.download("AAPL", start="2010-01-01", auto_adjust=True, progress=False)["Close"].squeeze().pct_change()
data = pd.concat([aapl.rename("r"), ff], axis=1, join="inner").dropna()
y = data["r"] - data["RF"]
X = sm.add_constant(data[["Mkt-RF", "SMB", "HML"]])
fit = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": 5})
print(fit.summary().tables[1])
print(f"annualised alpha: {fit.params['const'] * 252:.2%}")

# 3) Multiple testing: 200 random 'strategies', how many survive?
rng = np.random.default_rng(0)
pvals = [stats.ttest_1samp(rng.normal(0, 0.01, 756), 0).pvalue for _ in range(200)]
print("naive p<0.05:", sum(p < 0.05 for p in pvals))
print("after Benjamini-Hochberg:", multipletests(pvals, alpha=0.05, method="fdr_bh")[0].sum())

Homework (4–5 weeks)

  • Read Wasserman, All of Statistics, chapters 1–13.
  • Download real returns, test normality (it will fail), fit a t-distribution by MLE.
  • Run a Fama–French regression on a portfolio. Is there any alpha left?
  • Permutation test: shuffle your strategy's signal dates 10,000 times and see where the real result lands in the shuffled distribution.

Why Linear Algebra Runs Everything

Portfolio construction, PCA, factor models, covariance estimation, neural networks: all of it is matrices.

Thinking in matrices

A covariance matrix \(\Sigma\) captures how every asset moves with every other. For 500 stocks it is \(500 \times 500\) with 125,250 unique entries. Yet portfolio risk collapses to one expression:

\[ \sigma_p^2 = w^\top \Sigma\, w \qquad \mu_p = w^\top \mu \]Portfolio variance as a quadratic form. w is the weight vector. This one line underlies Markowitz, risk models and position limits.

Vocabulary to master: vectors and dot products, matrix multiplication, transpose and inverse, rank, positive (semi)definite matrices, projections, eigenvalues and eigenvectors, singular value decomposition (SVD), and Cholesky decomposition (used to simulate correlated returns).

Eigenvalues: what actually matters

\[ \Sigma = Q \Lambda Q^\top = \sum_{k} \lambda_k\, q_k q_k^\top \]Eigendecomposition. Each eigenvector q is an independent "factor"; its eigenvalue λ is how much variance it explains.

Decompose the covariance of a 500-stock universe and the first eigenvector is roughly "the market". The first five typically explain most of the variance. The long tail is mostly noise. That is principal component analysis (PCA), and it is the basis of factor investing and statistical arbitrage.

cumulative: 70% after 5 components PC1 ≈ "the market", 36% PC1PC5… noise Share of variance explained by each principal component
import numpy as np, yfinance as yf, cvxpy as cp

tickers = ["AAPL","MSFT","NVDA","AMZN","GOOGL","META","JPM","XOM","JNJ","PG","KO","PEP","WMT","HD","CVX"]
px = yf.download(tickers, start="2019-01-01", auto_adjust=True, progress=False)["Close"].dropna()
R = px.pct_change().dropna()
mu = R.mean().values * 252
cov = R.cov().values * 252

# PCA via eigendecomposition of the covariance matrix
vals, vecs = np.linalg.eigh(cov)
vals, vecs = vals[::-1], vecs[:, ::-1]
print("variance explained by PC1..PC5:", np.round(vals[:5] / vals.sum(), 3))

# Markowitz: minimise variance subject to a return target and limits
w = cp.Variable(len(tickers))
target = np.percentile(mu, 60)
prob = cp.Problem(
    cp.Minimize(cp.quad_form(w, cov)),
    [mu @ w >= target, cp.sum(w) == 1, w >= -0.05, w <= 0.25],
)
prob.solve()
ret, vol = mu @ w.value, np.sqrt(w.value @ cov @ w.value)
print(f"return {ret:.2%}  vol {vol:.2%}  Sharpe(rf=4%) {(ret - 0.04) / vol:.2f}")
print(dict(zip(tickers, np.round(w.value, 3))))
Estimation error

Unconstrained Markowitz with sample estimates is famously unstable: it piles into whatever had the luckiest past returns. Professionals shrink the covariance (Ledoit–Wolf), cap weights, or skip expected returns entirely and size by risk. The math is exact; your inputs are not.

Homework (4–6 weeks)

  • Watch all of Gilbert Strang's MIT 18.06. Not optional.
  • Work through Strang's Introduction to Linear Algebra. Optional and beautiful: 3Blue1Brown's Essence of Linear Algebra for intuition.
  • Run PCA on S&P 500 returns. Plot the eigenvalue spectrum. Name the top three components.
  • Implement Markowitz from scratch, then compare it with Ledoit–Wolf shrinkage (sklearn.covariance.LedoitWolf).

Calculus and Optimisation

Nearly every problem in quant finance is "maximise something subject to constraints". Calculus tells you how things change; optimisation tells you what to do about it.

Derivatives, gradients and Taylor expansion

Derivatives show up in every option Greek and every neural network's backpropagation. The tool that connects them is the Taylor expansion:

\[ f(x + \Delta x) \approx f(x) + f'(x)\,\Delta x + \tfrac{1}{2} f''(x)\,(\Delta x)^2 + \dots \]Delta hedging is the first-order term. Gamma hedging adds the second. Itô calculus exists because, for random processes, the second-order term does not vanish.

Gradient descent

\[ \theta_{k+1} = \theta_k - \eta\, \nabla_\theta L(\theta_k) \]Step against the gradient of the loss. This trains every ML model you will ever use. η is the learning rate.

Convex optimisation

A convex problem (convex objective, convex constraints) has one global minimum, and solvers find it fast and reliably. Most portfolio construction, risk budgeting and execution problems can be written as convex programs. Recognising convexity, and knowing when a relaxation of a non-convex problem is tight, is a core skill. Learn Lagrange multipliers and the KKT conditions: they explain why the optimiser chose what it chose.

volatility σexpected return μ individual assets min variance max-Sharpe (tangency) risk-free rate efficient frontier capital market line
import numpy as np, cvxpy as cp

# 1) Gradient descent from scratch on the Rosenbrock function (minimum at 1, 1)
def grad(x, y):
    return np.array([-2 * (1 - x) - 400 * x * (y - x**2), 200 * (y - x**2)])

p = np.array([-1.2, 1.0])
for _ in range(50_000):
    p -= 1e-3 * grad(*p)
print("gradient descent found:", np.round(p, 4))

# 2) Rebalance with transaction costs: a convex problem
rng = np.random.default_rng(1)
n = 8
mu = rng.uniform(0.03, 0.12, n)
A = rng.normal(size=(n, n)) * 0.1
cov = A @ A.T + np.eye(n) * 0.01
w_old = np.full(n, 1 / n)
w = cp.Variable(n)
risk_aversion, cost = 4.0, 0.002                   # 20 bps per unit traded
objective = cp.Maximize(mu @ w - risk_aversion * cp.quad_form(w, cov) - cost * cp.norm1(w - w_old))
cp.Problem(objective, [cp.sum(w) == 1, w >= 0, w <= 0.3]).solve()
print("new weights:", np.round(w.value, 3), " turnover:", round(np.abs(w.value - w_old).sum(), 3))

Homework (4–5 weeks)

  • Read Boyd & Vandenberghe, Convex Optimization (free), chapters 1–5. The Stanford EE364A lectures are excellent companions.
  • Implement gradient descent and Newton's method from scratch. Compare their convergence on Rosenbrock.
  • Solve a portfolio problem with transaction costs in cvxpy and watch how turnover falls as cost rises.

Stochastic Calculus: Where You Become a Quant

This is the layer where a derivative price is derived, rather than taken from a formula sheet.

Brownian motion

A Brownian motion (Wiener process) \(W_t\) is a continuous-time random walk with four properties:

  • \(W_0 = 0\).
  • Increments are normal: \(W_t - W_s \sim \mathcal N(0,\, t-s)\) for \(t > s\).
  • Non-overlapping increments are independent.
  • Paths are continuous but nowhere differentiable.

The one fact everything else depends on: \(dW_t\) has size \(\sqrt{dt}\), so \((dW_t)^2 = dt\). In ordinary calculus a squared small change is negligible. Here it is first order, and you cannot drop it.

Geometric Brownian motion: the standard stock model

\[ dS_t = \mu S_t\,dt + \sigma S_t\,dW_t \quad\Longrightarrow\quad S_T = S_0 \exp\!\Big(\big(\mu - \tfrac12\sigma^2\big)T + \sigma W_T\Big) \]Drift μ, volatility σ. Prices stay positive and log-returns are normal. Note the −σ²/2: that is Itô's lemma at work.
Lab · 60 GBM paths

Itô's lemma: the chain rule for randomness

\[ df(S_t,t) = \Big(\frac{\partial f}{\partial t} + \mu S\frac{\partial f}{\partial S} + \tfrac12\sigma^2 S^2 \frac{\partial^2 f}{\partial S^2}\Big)dt + \sigma S \frac{\partial f}{\partial S}\, dW_t \]The extra ½σ²S² ∂²f/∂S² term is what ordinary calculus misses. Apply it to f = ln S and you get the −σ²/2 drift above.

Deriving Black–Scholes in four steps

Follow along with pen and paper. Let \(V(S,t)\) be an option's price.

  1. Apply Itô to \(V\): you get a \(dt\) term and a \(dW_t\) term.
  2. Hedge. Build \(\Pi = V - \frac{\partial V}{\partial S} S\). The \(dW_t\) terms cancel exactly, so the portfolio is locally riskless.
  3. No arbitrage. A riskless portfolio must earn the risk-free rate: \(d\Pi = r\Pi\, dt\).
  4. Substitute and rearrange to get the PDE:
\[ \frac{\partial V}{\partial t} + \tfrac12\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + rS\frac{\partial V}{\partial S} - rV = 0 \]The Black–Scholes PDE. Look for μ: it is gone. The option price does not depend on where you think the stock is going, only on how much it moves.

Solving it for a European call with strike \(K\) and expiry \(T\):

\[ C = S\,N(d_1) - K e^{-rT} N(d_2), \qquad d_1 = \frac{\ln(S/K) + (r + \tfrac12\sigma^2)T}{\sigma\sqrt T}, \quad d_2 = d_1 - \sigma\sqrt T \]N is the standard normal CDF. Risk-neutral pricing: discount the expected payoff as if every investor were indifferent to risk.

The Greeks

GreekDefinitionWhat it tells a desk
Delta Δ\(\partial V/\partial S\)Hedge ratio: shares to hold per option
Gamma Γ\(\partial^2 V/\partial S^2\)How fast delta changes, so how often to re-hedge
Theta Θ\(\partial V/\partial t\)Time decay: the daily cost of holding long options
Vega ν\(\partial V/\partial \sigma\)Volatility exposure: the core of vol-trading desks
Rho ρ\(\partial V/\partial r\)Interest-rate sensitivity
import numpy as np
from scipy.stats import norm

def black_scholes(S, K, T, r, sigma, kind="call"):
    d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if kind == "call":
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

def monte_carlo(S0, K, T, r, sigma, n=500_000, seed=0):
    """Risk-neutral simulation: the drift is r, not mu."""
    Z = np.random.default_rng(seed).standard_normal(n)
    ST = S0 * np.exp((r - sigma**2 / 2) * T + sigma * np.sqrt(T) * Z)
    pay = np.exp(-r * T) * np.maximum(ST - K, 0)
    return pay.mean(), pay.std() / np.sqrt(n)

def greeks(S, K, T, r, sigma):
    d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return {
        "delta": norm.cdf(d1),
        "gamma": norm.pdf(d1) / (S * sigma * np.sqrt(T)),
        "theta": -S * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) - r * K * np.exp(-r * T) * norm.cdf(d2),
        "vega":  S * np.sqrt(T) * norm.pdf(d1),
        "rho":   K * T * np.exp(-r * T) * norm.cdf(d2),
    }

S, K, T, r, sigma = 100, 105, 1.0, 0.05, 0.2
mc, se = monte_carlo(S, K, T, r, sigma)
print(f"Black-Scholes {black_scholes(S, K, T, r, sigma):.4f}   Monte Carlo {mc:.4f} ± {se:.4f}")
for k, v in greeks(S, K, T, r, sigma).items():
    print(f"{k:>6}: {v: .5f}")

Homework (6–8 weeks, the hardest level)

  • Read Shreve, Stochastic Calculus for Finance I and II: the gold standard. A gentler alternative: Arguin, A First Course in Stochastic Calculus.
  • Derive: apply Itô to \(\ln S\) under GBM and get the \(-\sigma^2/2\).
  • Derive the full Black–Scholes PDE from the delta-hedging argument, without notes.
  • Code Black–Scholes and Monte Carlo and confirm they converge. Then price an Asian option, which has no closed form, by Monte Carlo.

Time Series and Signals

Markets are sequences. The bridge from math to money is learning which sequences are predictable and which only look predictable.

Returns, not prices

Prices wander and have no stable mean, so statistics computed on them are meaningless. Work with returns:

\[ r_t = \frac{P_t - P_{t-1}}{P_{t-1}} \qquad \ell_t = \ln\frac{P_t}{P_{t-1}} \qquad \sigma_{\text{annual}} \approx \sigma_{\text{daily}}\sqrt{252} \]Simple returns add across assets in a portfolio. Log returns add across time. Volatility scales with the square root of time.

Stationarity

A stationary series has a stable mean and variance, so its past says something about its future. Returns are roughly stationary; prices are not. The Augmented Dickey–Fuller (ADF) test checks for a unit root. Most features in a model should be stationary: z-scores, returns, spreads, ratios. López de Prado's fractional differentiation keeps as much memory as possible while still passing the test.

The core models

ModelEquationUsed for
AR(1)\(x_t = \phi x_{t-1} + \varepsilon_t\)Short-term autocorrelation, mean reversion
GARCH(1,1)\(\sigma_t^2 = \omega + \alpha\varepsilon_{t-1}^2 + \beta\sigma_{t-1}^2\)Volatility clustering: calm follows calm, storms follow storms
Ornstein–Uhlenbeck\(dX_t = \theta(\mu - X_t)dt + \sigma dW_t\)Mean-reverting spreads; half-life \(= \ln 2/\theta\)
Cointegration\(y_t - \beta x_t\) is stationaryPairs and basket trading
Kalman filterstate-space updateTime-varying hedge ratios and noisy signals

Two forces: momentum and mean reversion

Nearly every price-based signal is one of two bets. Momentum: what went up keeps going up for a while (trend following, cross-sectional momentum). Mean reversion: what stretched too far snaps back (pairs, statistical arbitrage, short-term reversal). They work at different horizons and often on the same asset. Knowing which one your signal bets on tells you when it will fail.

+2σ0−2σ z > +2: short the spread z back to 0: exit, take profit next entry
import numpy as np, pandas as pd, yfinance as yf, statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller, coint

px = yf.download(["KO", "PEP"], start="2015-01-01", auto_adjust=True, progress=False)["Close"].dropna()
y, x = np.log(px["KO"]), np.log(px["PEP"])

print("ADF p-value on KO price (should be high):", round(adfuller(y)[1], 3))
print("Engle-Granger cointegration p-value:   ", round(coint(y, x)[1], 3))

# Rolling hedge ratio and spread z-score. Only past data enters each estimate.
win = 252
beta = y.rolling(win).cov(x) / x.rolling(win).var()
spread = y - beta * x
z = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()

# Half-life of mean reversion from an AR(1) fit on the spread
s = spread.dropna()
phi = sm.OLS(s.diff().dropna(), sm.add_constant(s.shift(1).dropna())).fit().params.iloc[1]
print(f"half-life ≈ {np.log(2) / -phi:.1f} days")

# Positions: -1 short spread, +1 long spread, 0 flat. Hysteresis avoids flip-flopping.
pos = pd.Series(0.0, index=z.index)
for t in range(1, len(z)):
    prev, zt = pos.iloc[t - 1], z.iloc[t]
    if np.isnan(zt): continue
    if prev == 0 and zt > 2:    pos.iloc[t] = -1
    elif prev == 0 and zt < -2: pos.iloc[t] = 1
    elif prev != 0 and (abs(zt) < 0.5 or abs(zt) > 4): pos.iloc[t] = 0
    else: pos.iloc[t] = prev
print(pos.value_counts())

Why Data Is the Silent Killer

More strategies fail on the data than on the idea. Gaps, wrong timestamps and survivorship bias do not announce themselves.

The types of data

TypeWhat it isGood for
OHLCV barsOpen, high, low, close, volume per interval (1m, 1h, 1d)Learning, swing and daily strategies
Trades (tick)Every print: time, price, size, aggressor sideIntraday signals, volume profiles, realistic fills
Quotes / L1Best bid and ask with sizesSpread, slippage modelling
Order book L2 / L3Depth by price level (L2) or every individual order (L3)Market making, order-flow imbalance, HFT
FundamentalsEarnings, balance sheets, estimatesFactor and value strategies
Options chainsPrices, implied vols, Greeks across strikes and expiriesVolatility trading
Alternative dataNews, filings, satellite, web traffic, card spend, on-chainEdges others do not have yet

Where to get it

TierSourcesNotes
Freeyfinance, Finnhub, Alpha Vantage, FRED, Ken French library, exchange WebSockets (crypto)Fine for learning. yfinance is unofficial and can break or rate-limit.
Mid-rangePolygon, Databento, Tiingo, AlpacaReal-time feeds, tick history, cleaner corporate actions. Roughly $30–$300 a month.
InstitutionalBloomberg (~$32K/yr), Refinitiv/LSEG, FactSet, direct exchange feedsThe professional standard. Direct feeds need colocation.
Prediction / on-chainPolymarket API, Kalshi API, AlchemyPublic order books and on-chain history
Exchange feedsWebSocket · FIX · ITCH Vendor APIsPolygon · Databento · yf Alternative datanews · filings · on-chain Ingestasync · reconnect Validategaps · spikes StoreParquet · TSDB Researchbacktests Livefeatures Every record stores exchange_ts and received_ts. Raw data is append-only.Research and live read the same store with the same code.

The seven data sins

  1. Look-ahead bias. Using information that was not available at decision time: today's close to trade today's close, restated fundamentals, a revised economic number.
  2. Survivorship bias. Testing only on stocks that exist today ignores every company that went bankrupt or was delisted. Your backtest silently buys only winners.
  3. Corporate actions. Splits and dividends create fake jumps. Use adjusted prices for returns, raw prices for order sizing.
  4. Timezones and clocks. Store UTC. Record both the exchange timestamp and your receive timestamp. Daylight-saving bugs are real.
  5. Silent gaps. A WebSocket that drops for 40 seconds at 2 AM and reconnects without telling you. Detect sequence-number gaps and re-request.
  6. Bad ticks. Fat-finger prints and stale quotes. Filter with a robust rule such as rejecting returns beyond k × the median absolute deviation.
  7. Overwriting raw data. Clean into a new table. Never mutate the original. You will need to replay it.
close t−1close topen t+1close t+1 signal uses close t, fills at close t✕ fill ✓ signal from close t, earliest fill at t+1 In pandas this is one line: position = signal.shift(1).Forget it and your backtest trades on information it could not have had.
import polars as pl, yfinance as yf
from pathlib import Path

tickers = ["SPY", "QQQ", "IWM", "TLT", "GLD", "KO", "PEP"]
raw = yf.download(tickers, start="2005-01-01", auto_adjust=True, progress=False, group_by="ticker")

frames = []
for t in tickers:
    d = raw[t].dropna().reset_index()
    frames.append(pl.from_pandas(d).with_columns(pl.lit(t).alias("ticker")))
bars = pl.concat(frames).rename(str.lower)

# Validation: no duplicate (ticker, date), positive prices, flag suspicious jumps
bars = bars.unique(["ticker", "date"]).sort(["ticker", "date"])
assert bars.filter(pl.col("close") <= 0).height == 0
bars = bars.with_columns(pl.col("close").pct_change().over("ticker").alias("ret"))
mad = bars.group_by("ticker").agg((pl.col("ret") - pl.col("ret").median()).abs().median().alias("mad"))
flagged = bars.join(mad, on="ticker").filter(pl.col("ret").abs() > 15 * pl.col("mad"))
print(f"{flagged.height} suspicious bars to inspect by hand")

Path("data").mkdir(exist_ok=True)
bars.write_parquet("data/daily_bars.parquet")   # columnar, compressed, fast to scan
import asyncio, json, time, websockets   # pip install websockets

URL = "wss://stream.binance.com:9443/ws/btcusdt@bookTicker"   # public best bid/ask stream

async def stream():
    backoff = 1
    while True:
        try:
            async with websockets.connect(URL, ping_interval=20) as ws:
                backoff = 1
                last_id = None
                async for msg in ws:
                    recv_ns = time.time_ns()
                    m = json.loads(msg)
                    if last_id is not None and m["u"] <= last_id:
                        continue                      # stale or out-of-order update
                    last_id = m["u"]
                    bid, ask = float(m["b"]), float(m["a"])
                    bq, aq = float(m["B"]), float(m["A"])
                    micro = (bid * aq + ask * bq) / (aq + bq)
                    print(f"{recv_ns} bid {bid:.2f} ask {ask:.2f} spread {ask - bid:.2f} micro {micro:.2f}")
        except Exception as e:
            print(f"disconnected: {e!r}; reconnecting in {backoff}s")
            await asyncio.sleep(backoff)
            backoff = min(backoff * 2, 30)   # exponential backoff, capped

asyncio.run(stream())

This is exactly the kind of work worth handing to an agent harness rather than a single chat prompt. In AgenKit, the Data Engineer designs the schema, gap detection and causal timestamps before any code exists, so a 2 AM disconnect becomes a logged, re-requested gap instead of a silent hole in your history.

Storage that scales

  • Parquet + Polars/DuckDB: columnar files partitioned by date and symbol. Fast and free. Start here.
  • TimescaleDB / QuestDB / ClickHouse: time-series databases when you need live queries and many writers.
  • kdb+/q: the industry standard at banks and HFT firms. Very fast, steep learning curve, expensive.

Why Most Backtests Lie

A backtest is not a tool for finding strategies. It is a tool for killing them. The ones that survive deserve paper money.

Vectorised vs. event-driven

VectorisedEvent-driven
HowWhole arrays at once in pandas/numpyReplay events one by one through the same code that trades live
SpeedVery fast: thousands of parameter sets in secondsSlower, but realistic
RealismEasy to sneak in look-ahead; crude fillsOrder queues, partial fills, latency, rejects
Use it forResearch and screening ideasValidating finalists before going live
Toolsvectorbt, pandas, polarsNautilusTrader, backtrader, LEAN/QuantConnect

Costs: the edge-killer

Every trade pays commission, half the bid-ask spread, slippage, and market impact that grows with size. A common impact model is the square-root law:

\[ \text{cost per share} \approx \tfrac12\,\text{spread} + c\,\sigma_{\text{daily}}\sqrt{\frac{Q}{V_{\text{daily}}}} \]Impact rises with volatility and with the square root of your order size Q as a share of daily volume V. c is typically of order 1.

Rule of thumb: if your strategy's edge per trade is not at least 2–3× its estimated round-trip cost, it will not survive contact with the market.

Metrics that matter

MetricFormulaRough bar
Sharpe ratio\(\sqrt{252}\,\bar r / \sigma_r\)> 1 decent, > 2 excellent (daily strategies, after costs)
Sortino ratioSharpe using only downside deviationHigher than Sharpe if losses are rare
Max drawdown\(\max_t (1 - E_t / \max_{s\le t} E_s)\)Ask: could I sit through it?
CalmarCAGR / max drawdown> 0.5 is respectable
Hit rate & payoff% winners, average win / average lossRead them together, never alone
TurnoverAverage fraction of the book traded per dayDrives cost sensitivity
CapacityCapital before impact eats the edgeKnow it before you scale
import numpy as np, pandas as pd, yfinance as yf

px = yf.download("SPY", start="2000-01-01", auto_adjust=True, progress=False)["Close"].squeeze()
ret = px.pct_change().fillna(0)

def run(fast=50, slow=200, cost_bps=2.0):
    signal = (px.rolling(fast).mean() > px.rolling(slow).mean()).astype(float)
    pos = signal.shift(1).fillna(0)                 # decide on close t, hold from t+1
    turnover = pos.diff().abs().fillna(0)
    return pos * ret - turnover * cost_bps / 1e4

def report(r, name):
    eq = (1 + r).cumprod()
    yrs = len(r) / 252
    cagr = eq.iloc[-1] ** (1 / yrs) - 1
    vol = r.std() * np.sqrt(252)
    sharpe = r.mean() / r.std() * np.sqrt(252)
    downside = r[r < 0].std() * np.sqrt(252)
    mdd = (eq / eq.cummax() - 1).min()
    print(f"{name:<14} CAGR {cagr:6.2%}  vol {vol:6.2%}  Sharpe {sharpe:5.2f}  "
          f"Sortino {r.mean() * 252 / downside:5.2f}  maxDD {mdd:7.2%}  Calmar {cagr / -mdd:5.2f}")

report(ret, "buy & hold")
report(run(), "50/200 trend")
report(run(cost_bps=20), "trend @20bps")

Walk-forward testing and purging

Never tune parameters on the same data you evaluate them on. Walk-forward analysis re-fits on a rolling training window and tests only on the period that follows. If labels look ahead (say, the next 5 days' return), purge training samples whose label window overlaps the test set, and leave an embargo gap after it. Otherwise information leaks across the boundary.

fold 1 fold 2 fold 3 fold 4 fold 5 OOS train (fit params)purge / embargo gapstitched out-of-sample equity curve

Deflate your Sharpe

If you tried \(N\) variations, the best one's Sharpe is inflated. Bailey and López de Prado's probabilistic Sharpe ratio asks how likely the true Sharpe exceeds a benchmark \(SR^*\), given track length \(T\), skew \(\gamma_3\) and kurtosis \(\gamma_4\):

\[ \widehat{PSR}(SR^*) = \Phi\!\left(\frac{(\widehat{SR} - SR^*)\sqrt{T-1}}{\sqrt{1 - \gamma_3\widehat{SR} + \frac{\gamma_4 - 1}{4}\widehat{SR}^2}}\right) \]For the deflated Sharpe ratio, set SR* to the Sharpe you would expect from the best of N pure-noise trials. Use per-period (not annualised) Sharpe here.

The pre-live checklist

  • Signal uses only data available at decision time (shift(1), point-in-time fundamentals, survivorship-free universe).
  • Realistic costs and slippage, and the result survives doubling them.
  • Out-of-sample and walk-forward results are in the same ballpark as in-sample.
  • Works across nearby parameters (a plateau, not a spike) and across several assets or regimes.
  • You know why it should work: who is on the other side, and why they keep losing.
  • You logged how many variants you tried and deflated the Sharpe accordingly.
  • Paper trading for weeks matches the backtest's fills and slippage.

If you build with an AI coding agent, turn this checklist into tests, not intentions. A harness like AgenKit writes a failing test for each item first (no look-ahead, costs on every fill) and its reviewer blocks code whose math disagrees with the spec, even when it compiles.

Why Sizing Decides Survival

The signal decides direction. Sizing decides whether you are still in business next year.

The Kelly criterion

Kelly gives the bet size that maximises the long-run growth rate of wealth. For a bet paying \(b\) to 1 with win probability \(p\):

\[ f^* = p - \frac{1-p}{b} \qquad\text{even money: } f^* = 2p - 1 \qquad\text{continuous: } f^* = \frac{\mu - r}{\sigma^2} \]Growth per bet is g(f) = p·ln(1+bf) + (1−p)·ln(1−f). It peaks at f*, falls back to zero at about 2f*, and is negative beyond that.

Full Kelly is mathematically optimal and practically brutal: drawdowns of 50% are routine, and it assumes you know \(p\) exactly. You never do. Most professionals bet quarter to half Kelly, which keeps most of the growth with far less pain. Try it:

Lab · Kelly sizing on 40 bettors

Push the slider past 2× and the typical bettor loses money while holding a winning edge. At 3× most of them are wiped out. Overbetting a good strategy is one of the most reliable ways to lose money.

Volatility targeting

\[ w_t = \frac{\sigma_{\text{target}}}{\hat\sigma_t} \cdot \text{signal}_t, \qquad \hat\sigma_t^2 = \lambda\,\hat\sigma_{t-1}^2 + (1-\lambda)\,r_{t-1}^2 \]Scale exposure so the strategy runs at constant risk. The EWMA volatility estimate (λ ≈ 0.94) reacts quickly to regime changes.

Measuring tail risk

  • Value at Risk (VaR 99%): the loss exceeded on only 1% of days. Easy to report, silent about how bad the worst 1% is.
  • Expected Shortfall / CVaR: the average loss within that worst 1%. Better, and convex, so you can optimise it.
  • Stress tests: replay 2008, March 2020, the 2010 flash crash, or a 25% overnight gap. Ask what breaks.

Hard limits: the kill switch

Pre-trade
Before every order
  • Max order size and notional
  • Price collar vs. last trade
  • Max position per symbol
  • Max gross and net exposure
  • Order rate limit
Real-time
While running
  • Daily loss limit
  • Max drawdown from peak
  • Stale data detector
  • Position vs. broker reconciliation
Kill switch
When tripped
  • Cancel all open orders
  • Flatten or freeze positions
  • Page a human
  • Require manual restart
Why this matters

In 2012 Knight Capital lost about $440M in 45 minutes because of a deployment error that reactivated old code, with no effective kill switch. Risk checks are not paperwork. They are the brakes.

import numpy as np

class RiskGate:
    """Every order passes through check(). Any breach trips the kill switch."""
    def __init__(self, max_pos, max_notional, daily_loss_limit, max_dd):
        self.max_pos, self.max_notional = max_pos, max_notional
        self.daily_loss_limit, self.max_dd = daily_loss_limit, max_dd
        self.peak_equity, self.killed = None, False

    def update_equity(self, equity, day_start_equity):
        self.peak_equity = max(self.peak_equity or equity, equity)
        dd = 1 - equity / self.peak_equity
        if equity - day_start_equity < -self.daily_loss_limit or dd > self.max_dd:
            self.killed = True                      # sticky until a human resets it

    def check(self, symbol, qty, price, position):
        if self.killed:
            return False, "kill switch active"
        if abs(position + qty) > self.max_pos:
            return False, "position limit"
        if abs(qty * price) > self.max_notional:
            return False, "notional limit"
        return True, "ok"

def kelly_fraction(returns, rf=0.0, fraction=0.5, cap=2.0):
    mu, var = returns.mean() * 252 - rf, returns.var() * 252
    return float(np.clip(fraction * mu / var, -cap, cap))

def vol_target_weight(returns, target=0.10, lam=0.94):
    var = returns[:20].var()
    for r in returns[20:]:
        var = lam * var + (1 - lam) * r * r
    return target / np.sqrt(var * 252)

def historical_var_cvar(returns, level=0.99):
    q = np.quantile(returns, 1 - level)
    return -q, -returns[returns <= q].mean()

Write these limits into the spec before the first line of strategy code. When an agent harness such as AgenKit builds the bot, the kill switch and key handling are a security gate the build cannot pass without, and its debugger can trace any rejected order back to the rule that stopped it.

Machine Learning Without Fooling Yourself

Machine learning combines weak signals. Without a test designed to reject noise, it will fit the noise.

Why finance is hard for ML

  • Low signal-to-noise. A model that predicts next-day direction 52% of the time can be world-class.
  • Non-stationary. The data-generating process changes: regimes, regulations, competitors, your own impact.
  • Small effective sample. 20 years of daily data is only about 5,000 points, heavily autocorrelated.
  • Adversarial. Once an edge is found and traded, it decays.

The ML workflow for trading

  1. FeaturesStationary, causal inputs: returns over several horizons, volatility, z-scores, order-flow imbalance, carry, fundamentals ratios, text sentiment. Rank-transform across assets to remove scale.
  2. LabelsWhat you predict. Fixed-horizon returns are naive. The triple-barrier method labels each event by which comes first: take-profit, stop-loss, or time-out.
  3. ModelStart with gradient-boosted trees (LightGBM, XGBoost, CatBoost) on tabular data. They beat deep learning on most tabular finance problems and are easy to inspect.
  4. ValidationPurged, embargoed walk-forward cross-validation (chapter 11). Never shuffle time series.
  5. Meta-labellingA second model decides whether to act on a primary signal and how big to bet. Often worth more than a better primary model.
  6. Sizing and backtestMap probabilities to positions (for example, bet size ∝ 2p − 1), then run the full cost-aware backtest.
take profit stop loss time-out (vertical barrier) entry upper hit first → label +1 Barriers are set as multiples of current volatility, so labels adapt to the regime.
import numpy as np, pandas as pd, yfinance as yf, lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import roc_auc_score

px = yf.download("SPY", start="2005-01-01", auto_adjust=True, progress=False)["Close"].squeeze()
r = px.pct_change()
vol = r.ewm(span=50).std()

def triple_barrier(px, vol, horizon=10, k=1.5):
    out = pd.Series(np.nan, index=px.index)
    for i in range(len(px) - horizon):
        p0, up, dn = px.iloc[i], k * vol.iloc[i], -k * vol.iloc[i]
        path = px.iloc[i + 1 : i + 1 + horizon] / p0 - 1
        hit_up = path[path >= up].index.min()
        hit_dn = path[path <= dn].index.min()
        if pd.isna(hit_up) and pd.isna(hit_dn): out.iloc[i] = 0
        elif pd.isna(hit_dn) or (not pd.isna(hit_up) and hit_up < hit_dn): out.iloc[i] = 1
        else: out.iloc[i] = -1
    return out

X = pd.DataFrame({f"ret_{h}": px.pct_change(h) for h in (1, 5, 20, 60)})
X["vol_20"] = r.rolling(20).std()
X["z_20"] = (px - px.rolling(20).mean()) / px.rolling(20).std()
X["dist_200"] = px / px.rolling(200).mean() - 1
y = (triple_barrier(px, vol) == 1).astype(int)
data = pd.concat([X, y.rename("y")], axis=1).dropna().iloc[:-10]

aucs = []
for train, test in TimeSeriesSplit(n_splits=5, gap=10).split(data):   # gap = label horizon (purge)
    tr, te = data.iloc[train], data.iloc[test]
    model = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.02, num_leaves=15,
                               subsample=0.8, subsample_freq=1, colsample_bytree=0.8, verbose=-1)
    model.fit(tr.drop(columns="y"), tr["y"])
    aucs.append(roc_auc_score(te["y"], model.predict_proba(te.drop(columns="y"))[:, 1]))
print("out-of-sample AUC per fold:", np.round(aucs, 3))   # ~0.50–0.55 is normal. Be suspicious of 0.7.

Deep learning, reinforcement learning, and LLMs

Deep learning
Sequences and order books

LSTMs, temporal CNNs and transformers on limit-order-book data (DeepLOB) and long return histories. Most useful with very large datasets: high-frequency data, many assets, or text.

Reinforcement learning
Execution and market making

RL suits sequential decisions with clear rewards: optimal execution, inventory management. FinRL is a starting point. Simulators are the hard part.

LLMs
Text as data

Sentiment and event extraction from news, filings and earnings calls, turned into numeric features. Watch for look-ahead: models trained on data after your backtest date already "know" the future.

AI agents
Agents as your research team

The biggest 2026 shift is AI coding agents that write, test and review the research and trading code itself. Used with discipline (specs, tests, review), they compress months of engineering into weeks. That is what agent harnesses such as AgenKit exist for.

Also worth exploring: Microsoft Qlib, an AI-oriented quant research platform with data handling, models and backtesting in one place.

Inside the Order Book

Prices move because orders arrive, rest, cancel and match. Short-horizon research starts in the limit order book.

The limit order book

Buyers post bids, sellers post asks. The highest bid and lowest ask form the spread. A limit order rests in the book and provides liquidity. A market order crosses the spread and takes liquidity. Most exchanges match by price-time priority: best price first, then first in the queue.

Bids (buyers)Asks (sellers) 100.05900 100.04600 100.03750 100.02400 100.01220 · best ask spread 1 tick 100.00best bid · 480 99.99650 99.98800 99.97520 99.961000 mid = 100.005 microprice ≈ 100.0069 imbalance = 480 / 700 = 0.69 more size bids → price leans up
\[ P_{\text{micro}} = \frac{P_b\,Q_a + P_a\,Q_b}{Q_a + Q_b} \qquad I = \frac{Q_b}{Q_b + Q_a} \qquad \text{OFI}_t = \Delta Q_b^{\text{(at best)}} - \Delta Q_a^{\text{(at best)}} \]Microprice weights the mid towards the side more likely to be hit. Order-flow imbalance (Cont, Kukanov, Stoikov) explains a large share of short-term price changes.

Key concepts

  • Adverse selection. When your resting order fills, it is often because someone knew more than you. Market makers win on the spread and lose to informed flow.
  • Queue position. At the same price, being first in the queue is worth real money. This is a big reason firms race on latency.
  • Market impact. Large orders move prices, temporarily and permanently (Kyle's lambda, the square-root law).
  • Maker/taker fees and rebates. Exchanges pay some liquidity providers. Many market-making strategies depend on rebates.
  • Tick size. The minimum price increment shapes queue dynamics: large-tick assets reward queue position, small-tick assets reward speed.

Market making: Avellaneda–Stoikov

The classic model of an optimal market maker quotes around a reservation price that is skewed against current inventory \(q\):

\[ r = s - q\,\gamma\,\sigma^2 (T - t) \qquad \delta^a + \delta^b = \gamma\sigma^2 (T-t) + \frac{2}{\gamma}\ln\!\Big(1 + \frac{\gamma}{k}\Big) \]s mid price, γ risk aversion, σ volatility, k order-arrival decay. Long inventory pushes both quotes down to attract sellers and repel buyers.
import numpy as np

def avellaneda_stoikov(mid, inventory, sigma, gamma=0.1, k=1.5, t_left=1.0, tick=0.01):
    reservation = mid - inventory * gamma * sigma**2 * t_left
    spread = gamma * sigma**2 * t_left + (2 / gamma) * np.log(1 + gamma / k)
    bid = np.floor((reservation - spread / 2) / tick) * tick
    ask = np.ceil((reservation + spread / 2) / tick) * tick
    return round(bid, 2), round(ask, 2)

for inv in (-5, 0, 5):
    print(f"inventory {inv:+d}: quotes", avellaneda_stoikov(mid=100.0, inventory=inv, sigma=2.0))

Read further

  • Cartea, Jaimungal & Penalva, Algorithmic and High-Frequency Trading: the modern textbook on optimal execution and market making.
  • Bouchaud, Bonart, Donier & Gould, Trades, Quotes and Prices: the physics view of order books.
  • Larry Harris, Trading and Exchanges: the best plain-English overview of who trades and why.

Execution and Low Latency

Execution is where a paper edge becomes real money, or leaks away one tick at a time.

Execution algorithms

AlgoIdeaWhen
TWAPSlice evenly across timeSimple, predictable, low urgency
VWAPSlice in proportion to expected volumeBenchmark-driven institutional orders
POVTrade a fixed percentage of market volumeAdapts to liquidity
Implementation shortfallTrade off impact against the risk of waitingAlpha-driven orders where delay is costly
Passive / peggedRest at the bid or ask, earn the spreadWhen you are not in a hurry

The Almgren–Chriss model gives the optimal schedule for liquidating \(X\) shares over time \(T\) when impact and price risk compete:

\[ x_j = X\,\frac{\sinh\!\big(\kappa (T - t_j)\big)}{\sinh(\kappa T)}, \qquad \kappa \approx \sqrt{\frac{\lambda \sigma^2}{\eta}} \]x_j shares remaining at time t_j. λ risk aversion, σ volatility, η temporary-impact coefficient. As risk aversion → 0 you get TWAP; higher urgency front-loads the trades.

What "low latency" means, and what it means for you

1 ns1 µs1 ms100 ms L1 cache read~1 ns DRAM read~100 ns FPGA tick-to-trade< 1 µs Kernel-bypass software~2 µs tick-to-trade Linux kernel network stack~10–20 µs Python handling one tick~50–500 µs Same-region cloud round trip~0.5 ms Chicago → New Jersey (microwave)~4 ms Retail broker REST order~50 ms+
Honest advice

For an individual, "low latency" should mean robust and predictable: a few milliseconds, no garbage-collection pauses at the wrong time, no dropped messages, clean reconnects. Pick strategies where the edge lasts seconds to days, not microseconds: stat-arb, trend, volatility, prediction markets. The engineering below still matters, because it makes your system reliable.

Architecture of a fast trading engine

Exchangematchingengine NIC rxkernel bypass Feed handlerdecode ITCH/SBE Book builderL2/L3 state Strategysignal → intent Risk gatelimits, ~100 ns Order gatewayencode OUCH/FIX NIC txkernel bypass Journalasync tick-to-trade Each stage runs on a pinned CPU core, connected by lock-free single-producer/single-consumerring buffers. Nothing allocates on the hot path.

The low-latency engineering playbook

TechniqueWhy
Pre-allocate everythingHeap allocation and garbage collection cause unpredictable pauses
Fixed-point integer pricesFloats are inexact and slower to compare; store prices as integer ticks
Lock-free SPSC ring buffersPass messages between threads without mutexes
Pin threads to isolated cores, busy-spinNo context switches, no scheduler jitter, warm caches
Cache-friendly data layoutArrays of structs sized to cache lines; avoid pointer chasing and false sharing
Kernel bypass (DPDK, Solarflare/AMD Onload, ef_vi)Skip the operating system's network stack
Binary protocols (SBE, ITCH/OUCH)Decoding JSON is very slow by comparison
Colocation and FPGAsPut the server in the exchange's data centre; move logic into hardware
Measure tail latencyAverage latency hides the problems. Track p99 and p99.9 with hardware timestamps.

Why Rust (and C++)

C++ has dominated HFT for decades: control over memory layout, no GC, and decades of tooling (QuantLib, Eigen, Boost). Rust gives the same performance with memory safety checked at compile time, removing whole classes of crashes. The pattern spreading through new trading infrastructure is a Rust core with a Python API: NautilusTrader works this way, and every strategy on agents.surf ships a Rust signal layer for the same reason.

// Cargo.toml: rtrb = "0.3", core_affinity = "0.8"
use rtrb::RingBuffer;

/// Prices as integer ticks: exact, fast to compare, no float surprises.
#[derive(Clone, Copy, Debug)]
struct Tick { ts_ns: u64, bid: i64, ask: i64, bid_sz: u32, ask_sz: u32 }

#[derive(Clone, Copy, Debug)]
enum Intent { Buy { px: i64, qty: u32 }, Sell { px: i64, qty: u32 } }

fn main() {
    let (mut tick_tx, mut tick_rx) = RingBuffer::<Tick>::new(1 << 16);
    let (mut order_tx, mut order_rx) = RingBuffer::<Intent>::new(1 << 12);

    // Feed thread, pinned to core 2: decode packets, push ticks. Never allocates.
    std::thread::spawn(move || {
        core_affinity::set_for_current(core_affinity::CoreId { id: 2 });
        loop {
            let t = read_next_tick();
            while tick_tx.push(t).is_err() { std::hint::spin_loop(); }
        }
    });

    // Gateway thread, pinned to core 4: risk-check and send.
    std::thread::spawn(move || {
        core_affinity::set_for_current(core_affinity::CoreId { id: 4 });
        loop {
            match order_rx.pop() {
                Ok(intent) => send_if_within_limits(intent),
                Err(_) => std::hint::spin_loop(),
            }
        }
    });

    // Strategy thread (this one), pinned to core 3: microprice imbalance signal.
    core_affinity::set_for_current(core_affinity::CoreId { id: 3 });
    loop {
        let Ok(t) = tick_rx.pop() else { std::hint::spin_loop(); continue };
        let (b, a) = (t.bid_sz as i64, t.ask_sz as i64);
        // imbalance in basis points of total top-of-book size, integer math only
        let imb_bps = (b - a) * 10_000 / (b + a).max(1);
        let intent = if imb_bps > 6_000 { Some(Intent::Buy { px: t.bid, qty: 1 }) }
                     else if imb_bps < -6_000 { Some(Intent::Sell { px: t.ask, qty: 1 }) }
                     else { None };
        if let Some(i) = intent { let _ = order_tx.push(i); }
    }
}

fn read_next_tick() -> Tick { /* kernel-bypass socket + binary decoder */ unimplemented!() }
fn send_if_within_limits(_i: Intent) { /* pre-trade risk, then encode + send */ }
import numpy as np

def almgren_chriss_schedule(X, n_slices, T=1.0, risk_aversion=1e-6, sigma=0.3, eta=2.5e-6):
    """Shares remaining after each slice. risk_aversion -> 0 gives TWAP."""
    kappa = np.sqrt(risk_aversion * sigma**2 / eta)
    t = np.linspace(0, T, n_slices + 1)
    if kappa < 1e-9:
        return X * (1 - t / T)
    return X * np.sinh(kappa * (T - t)) / np.sinh(kappa * T)

for ra in (0.0, 1e-6, 1e-5):
    remaining = almgren_chriss_schedule(100_000, 10, risk_aversion=ra)
    print(f"risk aversion {ra:g}: trades per slice", np.round(-np.diff(remaining)).astype(int))

Most of this work is unglamorous: latency budgets, p99 alerts, idempotent order IDs, reconnects, broker reconciliation. It is also exactly what an AI agent skips unless something forces it. In AgenKit the performance and infrastructure specialists own it, with paper and live as separate deploy targets and automated rollback.

Prediction Markets: Probability With Prices

Elections, rates, sports, geopolitics. A prediction market contract pays $1 if an event happens, so its price is an implied probability.

That makes prediction markets the cleanest application of everything in this guide. A systematic quant does not try to guess who wins an election. They look for markets where the implied probability differs measurably from what the data supports, bet on the gap with correct sizing, and repeat across hundreds of events. Institutional quants now run the same frameworks here as in equities: probability models, signal combination, Kelly sizing, risk limits.

The edge, in one line

\[ \text{EV per contract} = p_{\text{model}} - \text{price} - \text{fees}, \qquad f^*_{\text{Kelly}} = \frac{p_{\text{model}} - \text{price}}{1 - \text{price}} \]Buy YES at price c when your probability p beats it. The Kelly fraction for a binary contract follows from b = (1 − c)/c.

How automated market makers price beliefs: LMSR

Robin Hanson's Logarithmic Market Scoring Rule powers many automated prediction markets. With \(q_i\) outstanding shares of outcome \(i\) and liquidity \(b\):

\[ C(\mathbf q) = b \ln \sum_{j=1}^{n} e^{q_j / b} \qquad p_i = \frac{e^{q_i/b}}{\sum_j e^{q_j/b}} \]The price is a softmax, the same function at the end of every neural-network classifier. Prices sum to 1, always exist, and the market maker's worst-case loss is bounded by b·ln(n).

Polymarket itself runs a central limit order book, so chapter 14's microstructure applies directly. Kalshi is a CFTC-regulated exchange for US event contracts.

Where the edges are

  • Mispriced probabilities: models built from polls, odds, base rates or live data that beat the crowd on specific market types.
  • Structural arbitrage: mutually exclusive outcomes whose YES prices sum to less than $1 (or more), or the same event priced differently on two venues.
  • Combinatorial arbitrage: logically linked markets ("Party wins" vs. the state-by-state markets) that disagree. Finding these at scale is an integer program, which is where solvers like Gurobi and OR-Tools come in.
  • Market making: providing liquidity in thin books, with inventory risk managed as in Avellaneda–Stoikov.
  • Speed on resolution-relevant news: reacting to official data releases faster than manual traders.
import numpy as np

def lmsr_cost(q, b):
    return b * np.log(np.exp(np.asarray(q) / b).sum())

def lmsr_prices(q, b):
    e = np.exp(np.asarray(q) / b - np.max(q) / b)    # stable softmax
    return e / e.sum()

b, q = 100.0, np.array([0.0, 0.0, 0.0])
print("start prices:", lmsr_prices(q, b))
buy = np.array([50.0, 0, 0])
print(f"cost of 50 YES on outcome 0: ${lmsr_cost(q + buy, b) - lmsr_cost(q, b):.2f}")
print("new prices:", np.round(lmsr_prices(q + buy, b), 3))
print(f"max market-maker loss: ${b * np.log(len(q)):.2f}")

# Structural arbitrage check on mutually exclusive outcomes (best asks, $ per share)
asks = {"A": 0.41, "B": 0.33, "C": 0.22}
total = sum(asks.values())
fee = 0.01 * len(asks)
if total + fee < 1:
    print(f"buy every outcome for ${total:.2f}, guaranteed $1 payout, edge ${1 - total - fee:.2f}")
Read the rules

Check legal eligibility in your jurisdiction, resolution criteria (the exact wording decides payouts), fees, and liquidity before trading any prediction market. Thin books make backtested fills unrealistic.

Building Your First Bot With an Agent Harness

You have the math, the data, the backtest, the risk rules and the architecture. The last step is engineering it the way a desk would.

A single AI chat will happily write you a trading script. It will also leave out the reconnect logic, apply costs on the wrong bar, skip the kill switch, and give you no tests. An agent harness fixes that by wrapping the model in process: a written spec, specialist roles, tests before code, review against the spec, and approval gates where nothing moves without you. That is how a professional desk ships research. We use AgenKit for the walkthrough below because it runs that process inside the coding tool you already use. The method is what matters; the commands are just how you run it.

01 orientContextreads your repo 02 brainstormConductoridea → spec 03 designArchitectfailure design 04 buildTest engineerred → green 05 reviewCode reviewerspec ✓ quality ✓ 06 shipInfra / DevOpspaper → live approval gates: you read the spec, the architecture, the plan, the review /agenkit · one command, whole engineering lifecycle

Step 1 · Set up the harness

Get a license key from agenkit.xyz. You need Node 18 or newer. Run these in your project folder; the installer detects your harness automatically.

npx agenkit activate <license-key>
npx agenkit install engineering-kit

Works in Claude Code, Cursor, Codex CLI, OpenCode, Antigravity, GitHub Copilot and Kimi Code. Use /agenkit in most tools, @agenkit in Copilot, and invoke the AgenKit skill by name in Codex.

Step 2 · Write the brief as a spec

The better your spec, the better the system. Notice how every line below maps to a chapter of this guide:

/agenkit build a production-grade trading bot: Python research layer, Rust execution core.

Strategy (ch. 9): KO/PEP pairs mean reversion. Hedge ratio from 252-day rolling OLS on log prices.
  Enter when |z| > 2, exit when |z| < 0.5, stop out when |z| > 4. Daily decisions on the close.
Data (ch. 10): yfinance daily bars for research, Alpaca WebSocket for live. Parquet storage,
  gap detection, UTC timestamps, exchange_ts and received_ts on every record.
Backtest (ch. 11): event-driven, positions take effect on the next bar, 2 bps cost + 1 bp slippage,
  walk-forward 3y train / 6m test with a 10-day embargo. Report Sharpe, Sortino, max DD,
  turnover and deflated Sharpe. Fail the build if any test detects look-ahead.
Risk (ch. 12): 10% annualised vol target, half-Kelly cap, max 20% gross per leg,
  daily loss limit 2%, 15% drawdown kill switch that cancels orders and requires manual reset.
Execution (ch. 15): Alpaca paper account first. Idempotent client order IDs, reconnect with
  backoff, reconcile positions against the broker every minute.
Monitoring: dashboard with positions, z-score, PnL vs. backtest, slippage; Telegram alerts.

Or start smaller and let the Conductor ask questions one at a time: /agenkit build a production-grade Polymarket bot with a Markov-chain signal.

Step 3 · Stay at the gates

  1. SpecThe Conductor asks about universe, lookback, thresholds and sizing. Answer in plain English, then read the written spec. Does the math match what you learned here? Approve it.
  2. ArchitectureThe Backend Architect lays out modules, interfaces, data model, retries and failure modes. Check that research and live share the same signal code. Approve.
  3. Plan and testsThe Test Engineer writes a failing test for each rule: no look-ahead, costs on every fill, kill switch trips at 15%. Then the minimum code to pass.
  4. ReviewThe Code Reviewer checks against the spec first, then code quality. It blocks if the z-score is computed with future data, even when everything compiles.
  5. Ship to paperInfra/DevOps deploys to the paper target with CI and rollback. Run for weeks. Compare live slippage and PnL with the backtest.
  6. Go live smallOnly when the paper book is clean. Then scale slowly, and keep the kill switch armed.

Who owns what on a trading build

RoleWhat it does on your bot
ConductorTurns your prompt into a written spec, one question at a time, and runs the whole pipeline
Backend architectModules, interfaces, retries, reconnects, idempotent orders. Failure design before any code.
Test engineerA failing test for every rule in the spec, then the minimum code to pass it
Code reviewerBlocks merges where the math does not match the spec
Data engineerPrice feeds, schemas, gap detection, causal timestamps, backfills
DebuggerTraces a bad fill or a broken z-score back to its root cause
Frontend engineerThe dashboard where you watch inventory, signals and PnL
Infra / DevOpsCI, deploys, rollback, with paper and live as separate targets
Perf & observabilityLatency budgets, p99 alerts, metrics and logs while it runs
SecurityAPI keys, position limits and the kill switch, in place before the first order

Then iterate

Not sure what to build first? Every one of the 200 strategies on agents.surf comes with its math, test plan, a Rust signal layer and a ready-made brief you can paste into the same pipeline.

Every change after that is another prompt through the same pipeline: a tighter threshold, a new universe, a Telegram alert, a Kalman-filter hedge ratio. The tests grow with the book. Run npx agenkit update to pull the latest kit; your code stays yours.

The Complete Toolbox

The libraries are public. The work is deciding which ones a given system actually needs, and using them correctly.

Two kinds of programming

Research programming: clean Python to analyse data, build models and backtest. Used by researchers and analysts every day. Production systems programming: C++ or Rust that handles live data and orders at microsecond latency without missing a message. Used by quant developers and HFT engineers. Know which one your target role needs.

Python stack

JobLibraries
Data framespandas, polars (often 10–50× faster on big data), duckdb
Numericsnumpy, scipy, numba (JIT for hot loops)
Statisticsstatsmodels, arch (GARCH), scikit-learn
ML, tabularlightgbm, xgboost, catboost
Deep learningpytorch
Optimisationcvxpy, Gurobi (free academic licence), Google OR-Tools, PuLP/Pyomo
DerivativesQuantLib (C++ core, Python bindings)
BacktestingNautilusTrader (production), vectorbt and backtrader (learning), LEAN/QuantConnect
Research platformsMicrosoft Qlib, FinRL
Brokers / APIsAlpaca, Interactive Brokers, ccxt (crypto exchanges), Polymarket and Kalshi APIs

Systems stack

LanguageLibraries and tools
C++QuantLib, Eigen (linear algebra), Boost, Abseil, Google Benchmark, perf
RustRustQuant, NautilusTrader core, tokio, rtrb / crossbeam, criterion benchmarks
InfraLinux tuning (isolcpus, hugepages), Docker, Grafana + Prometheus, kdb+/q or ClickHouse

Decoding the Interview Gauntlet

Quant interviews are more structured and more demanding than most candidates expect, and very learnable.

Resume screenprojects, research, competitions Online assessmentZetamac, logic, coding Phone screensprobability, betting Superday3–6 × 45 min Offer

What each stage tests

  • Mental math: speed and accuracy under pressure. Practise on Zetamac until you score 50+ consistently.
  • Probability and expected value: conditional probability, Markov chains, martingales, optimal stopping, dice and card games.
  • Betting and market-making games: "make me a market on the sum of three dice". Width, skew and updating on information all matter.
  • Coding: data structures and algorithms. LeetCode Blind 75, with dynamic programming as the most common final-round failure point.
  • Systems (dev roles): C++/Rust internals, memory, concurrency, networking, design of a matching engine or feed handler.
  • Research (QR roles): a hypothesis you formed, tested and can defend, including what failed and why.
  • Behavioural: practise out loud. Every final round has a human judgement layer that weighs as much as the technical rounds.
The meta-skill

Think out loud. A candidate who narrates their reasoning, checks edge cases and keeps going under uncertainty beats one who goes silent and then produces a correct answer.

Practice problems (try before opening)

1. You flip a fair coin until you get two heads in a row. What is the expected number of flips?

Show solution

Define states by progress. \(E_0\): expected flips from scratch. \(E_1\): expected flips when the last flip was heads.

\[ \begin{aligned} E_0 &= 1 + \tfrac12 E_1 + \tfrac12 E_0 \\ E_1 &= 1 + \tfrac12 \cdot 0 + \tfrac12 E_0 \end{aligned} \quad\Longrightarrow\quad E_0 = 6 \]

The method matters more than the number: define states, write one equation per state, solve the linear system. The same approach handles any pattern, such as HTH (10) or HHH (14).

2. You roll a fair die and are paid its face value in dollars. If you do not like the roll, you may re-roll once and must take the second result. What is the game worth?

Show solution

A re-roll is worth 3.5, so keep 4, 5 or 6 and re-roll 1, 2 or 3. Value \(= \tfrac12 \cdot 5 + \tfrac12 \cdot 3.5 = 4.25\). This is optimal stopping: compare the value of stopping with the expected value of continuing.

3. A test for a condition is 99% accurate, and 1 in 1,000 people have it. You test positive. What is the probability you have it?

Show solution

Bayes: \(P = \dfrac{0.99 \times 0.001}{0.99 \times 0.001 + 0.01 \times 0.999} \approx 9\%\). Base rates dominate. The same logic says most backtests showing "99% significance" are false positives if you tested enough ideas.

The preparation stack

Competitions that work as recruiting pipelines

  • WorldQuant BRAIN: submit alpha signals, get paid for good ones, and get noticed.
  • Jane Street Kaggle competitions: real market-data forecasting with serious prize pools.
  • Citadel / Correlation One datathons: strong performers are fast-tracked to interviews.
  • IMC Prosperity and similar trading challenges: simulated markets, algorithmic and manual rounds.

The 18-Month Staircase

Run the math track and the coding track in parallel. Do not wait for the math to be perfect before you start building.

0369121518 Probability Statistics Linear algebra Optimisation Stochastic calculus Python + data Backtester + strategy Microstructure + ML Paper trading bot Interviews + comps months
  1. Build the foundation in orderProbability → statistics → linear algebra → optimisation → stochastic calculus. Code every concept as you learn it.
  2. Ship one real project before applying anywhereA documented backtest on real data with every assumption written down. A model submitted to WorldQuant BRAIN or Kaggle. A paper-trading bot on Alpaca, built and tested through AgenKit. Projects prove you can turn math into something that works.
  3. Get your first institutional credentialCold-email PhD students and ask to help with their research. TA a quantitative course. Take a research-assistant role. The title matters much less than having real technical work to talk about.
  4. Use each credential to reach the nextResearch lab → startup → mid-tier firm → elite fund. Each step is evidence you can do the work of the next one.
  5. Apply before you feel ready, and track everythingKeep a spreadsheet of every application, assessment, interview, and question you could not answer cleanly. Study that exact thing before the next one.
  6. Compete in publicFirms watch competition leaderboards. Strong results have led to offers for people with no prior connection to the firm.
Where you are

Name the lecture you are on, the one that comes next, and a dated task for the next seven days: a chapter of problems, a backtest, or a set of interview questions.

Three constraints that do not go away

Estimation error is the real enemy. Full Kelly, unconstrained Markowitz, and ML models with too many features all fail for the same reason: they fit noise in parameter estimates. The math is exact with true parameters, and you never have true parameters.

Libraries are not an edge. QuantLib, a market-data vendor and PyTorch are available to everyone. Edge is data, a model, or execution that someone else does not have.

The mathematics is the part you cannot skip. Code can be generated. You still have to know why Itô's lemma carries an extra term, and when a convex relaxation is tight. A result you cannot derive is a result you cannot trust.

Reading List and Resources

Read them in this order. Free material is marked.

Mathematics

Quantitative finance and trading

  • Hull, Options, Futures, and Other DerivativesderivativesThe standard reference.
  • Natenberg, Option Volatility and Pricingvol tradingHow option traders actually think.
  • López de Prado, Advances in Financial Machine LearningMLTriple barrier, meta-labelling, purged CV, deflated Sharpe.
  • Ernest Chan, Quantitative Trading and Algorithmic TradingpracticalThe best start for independent systematic traders.
  • Grinold & Kahn, Active Portfolio ManagementportfolioThe fundamental law of active management, and alpha in a risk-model world.
  • Cartea, Jaimungal & Penalva, Algorithmic and High-Frequency TradingHFTOptimal execution and market making, rigorously.
  • Larry Harris, Trading and ExchangesmicrostructureWho trades, why, and how markets are built.
  • Zuckerman, The Man Who Solved the Market · Thorp, A Man for All MarketsstoryRenaissance and the original quant. Motivation for the hard chapters.

Papers worth reading

  • Black & Scholes (1973), The Pricing of Options and Corporate LiabilitiesclassicWhere it all started.
  • Kelly (1956), A New Interpretation of Information RatesizingThe original Kelly criterion.
  • Almgren & Chriss (2000), Optimal Execution of Portfolio TransactionsexecutionThe impact-vs-risk trade-off.
  • Avellaneda & Stoikov (2008), High-frequency trading in a limit order bookmarket makingReservation price and optimal spread.
  • Bailey & López de Prado (2014), The Deflated Sharpe RatiobacktestingCorrecting for selection bias and non-normality.
  • Harvey, Liu & Zhu (2016), …and the Cross-Section of Expected Returnsmultiple testingWhy the bar should be t > 3.
  • Cont, Kukanov & Stoikov (2014), The Price Impact of Order Book EventsOFIOrder-flow imbalance explains short-term moves.
  • Moskowitz, Ooi & Pedersen (2012), Time Series MomentumtrendTrend following across asset classes.

Platforms, data and code

  • NautilusTraderopen sourceProduction-grade, Rust core, Python API. Backtest and live with the same code.
  • Microsoft Qlib · FinRLopen sourceAI-oriented research platform; reinforcement learning for trading.
  • vectorbt · backtraderlearningSimpler backtesters to learn the concepts.
  • AlpacabrokerFree paper trading with a clean API. The default first live target.
  • Polymarket docs · Kalshi docsprediction marketsOrder book and trading APIs.
  • agents.surfthis site200 strategies with math, tests, Rust code and AgenKit prompts.
  • AgenKitbuildTurn any strategy into a tested, reviewed, deployable bot with /agenkit.
Educational content only. Not financial, investment, legal or tax advice. Trading involves substantial risk of loss, and past or backtested performance does not guarantee future results. Check the laws in your jurisdiction before trading any product, including prediction markets.
© agents.surf