σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 11 · Production systems & frontier models›Topic 11.13·~4 hrExpert

Capstone: an end-to-end pricing or strategy library

Skill positionModule 11 of 11 · Topic 13 of 13
11.13 Capstone: an end-to-end pricing or strategy library
—Lab score— / 100Total XP—Streak—

From exercises to a library

You have written a hundred small functions. A library is those functions arranged so that someone else (including you in six months) can use them with confidence: a clear public interface, each piece tested alone, the pieces composed into a pipeline, and the whole thing installable, documented and checked automatically. This capstone is the pattern in miniature. A strategy library is the easiest place to practise it: the signal turns prices into positions, the backtest turns positions into returns after costs, the metrics summarise the returns, and a runner wires them together. Each stage takes plain arrays and returns plain arrays, so each can be tested alone with numbers you can check by hand.

# The shape of a small library: a package, its tests, its metadata and its automated checks
tree = """\
quantlib/
├── pyproject.toml            name, version, dependencies (numpy)
├── src/quantlib/
│   ├── __init__.py           the public API: sma_positions, strategy_returns, performance_summary, run_strategy
│   ├── signals.py            prices -> positions
│   ├── backtest.py           positions -> returns after costs
│   └── metrics.py            returns -> total return, volatility, Sharpe, drawdown
├── tests/                    one test file per module, fixed seeds, hand-checked cases
└── .github/workflows/ci.yml  lint, types, tests on every push
"""
print(tree.splitlines()[0])                          # quantlib/
print(sum(1 for line in tree.splitlines() if ".py" in line))     # 4   the four module files

Signal and backtest: no look-ahead, costs included

The signal here is a moving-average crossover: be long when the fast average of prices is above the slow one, short when below, flat when they are equal or the slow average does not exist yet. The two rules that decide whether a backtest is honest are visible in the code. First, no look-ahead: the position decided at the close of day t may use prices up to day t only, and it earns the return of day t + 1, which is why the return of step t uses positions[t - 1]. You can prove it with a test: removing the last prices must not change earlier positions. Second, costs: every change of position pays cost * |change|, so a strategy that flips constantly can win before costs and lose after.

# The signal and the honest backtest: positions decided at the close are earned on the NEXT step, and trading costs money
import numpy as np

def sma_positions(prices, fast, slow):
    prices = np.asarray(prices, dtype=float)
    positions = np.zeros(len(prices), dtype=int)
    for t in range(slow - 1, len(prices)):
        f = prices[t - fast + 1 : t + 1].mean()
        s = prices[t - slow + 1 : t + 1].mean()
        positions[t] = 1 if f > s else -1 if f < s else 0
    return positions

def strategy_returns(prices, positions, cost):
    returns = []
    for t in range(1, len(prices)):
        before = positions[t - 2] if t >= 2 else 0
        returns.append(positions[t - 1] * (prices[t] / prices[t - 1] - 1) - cost * abs(positions[t - 1] - before))
    return np.array(returns)

prices = np.array([100, 101, 103, 102, 105, 108, 107, 110, 112, 111, 115, 118], dtype=float)
positions = sma_positions(prices, 2, 4)
print(positions)                                     # [0 0 0 1 1 1 1 1 1 1 1 1]   flat until the 4-day average exists, then long
print(np.allclose(sma_positions(prices[:8], 2, 4), positions[:8]))      # True   later prices never change earlier positions
print(np.round(strategy_returns(prices, positions, 0.0).sum() - strategy_returns(prices, positions, 0.001).sum(), 3))    # 0.001   one purchase at 0.1%

Performance numbers, and the runner

A return series is summarised by a few numbers. The total return compounds the periods, prod(1 + r) - 1; the annual return rescales it to a year, (1 + total)^(periods_per_year / n) - 1; the annual volatility is the sample standard deviation times sqrt(periods_per_year); the Sharpe ratio divides the mean return by that deviation, annualised, and is the headline measure of return per unit of risk (0 for a flat series rather than a division error); the maximum drawdown is the worst fall from a peak of the equity curve, the number that tells you how much pain the strategy asks you to bear. A runner function ties it together: positions, then returns, then the summary. It should only compose the other functions, so a bug can always be traced to one stage.

# Performance statistics of a return series, and the runner that wires the three stages together
import math
import numpy as np

def performance_summary(returns, periods_per_year):
    returns = np.asarray(returns, dtype=float)
    n = len(returns)
    equity = np.concatenate(([1.0], np.cumprod(1 + returns)))
    total = float(equity[-1] - 1)
    deviation = float(np.std(returns, ddof=1))
    peak = np.maximum.accumulate(equity)
    return {
        "total_return": total,
        "annual_return": float((1 + total) ** (periods_per_year / n) - 1),
        "annual_vol": deviation * math.sqrt(periods_per_year),
        "sharpe": float(returns.mean() / deviation * math.sqrt(periods_per_year)) if deviation > 0 else 0.0,
        "max_drawdown": float(np.max((peak - equity) / peak)),
    }

summary = performance_summary([0.10, -0.20, 0.05], 252)
print(round(summary["total_return"], 4), round(summary["max_drawdown"], 4))     # -0.076 0.2   equity 1 -> 1.1 -> 0.88 -> 0.924: a 20% fall from the peak
print(performance_summary([0.0, 0.0, 0.0], 252)["sharpe"])                       # 0.0   no variation: a Sharpe of 0, not an error
# The runner only composes: if a number looks wrong you can test each stage on its own
def run_strategy(prices, fast, slow, cost, periods_per_year=252):
    positions = sma_positions(prices, fast, slow)
    returns = strategy_returns(prices, positions, cost)
    return performance_summary(returns, periods_per_year)

# (sma_positions, strategy_returns and performance_summary are the functions you wrote in this lab)
steps = ["sma_positions", "strategy_returns", "performance_summary"]
print(" -> ".join(steps))                           # sma_positions -> strategy_returns -> performance_summary
print(len(steps), "stages, one result: a dict of five numbers")        # 3 stages, one result: a dict of five numbers

Your capstone project

Now build your own, in your own repository. Pick one of two directions. A pricing library: Black-Scholes and Greeks, a Monte Carlo pricer with antithetic variates, an implied-volatility solver, and a calibration of one model to a quote set. Or a strategy library: several signals, a backtest with costs and position limits, walk-forward evaluation and a report. Either way, deliver the same things: a package with a clear public API and type hints; tests with fixed seeds and hand-checked cases; a pyproject.toml and a README that shows how to install and run an example; a CI workflow running lint, types and tests; a performance note with one profiled bottleneck; and a short write-up of the assumptions and what would break the model. Budget your time: design the interface first, build the smallest working pipeline, then widen it.

A good result is boring to reproduceSomeone else should be able to clone your repository, run one command and get exactly your numbers. Fix seeds, pin versions, keep the data snapshot or a script that fetches it, and show the equity curve next to a simple benchmark. Report what the strategy costs in drawdown and turnover as well as what it earns, and say plainly which parts you did not test. That honesty is what makes a quant's work trusted.
  • A library is small tested functions composed into a pipeline behind a clear interface: signal -> positions -> returns after costs -> metrics, wired by a runner.
  • An honest backtest has no look-ahead (the position at t uses prices up to t and earns the next return) and charges costs for every change of position.
  • Summarise with total and annual return, volatility, Sharpe and maximum drawdown; then ship a package with tests, CI, a README and an honest write-up.

That completes the skill. You now have the language, the numerical tools, the market-data and execution pieces, the models and the engineering habits of a working quant developer. Keep your capstone repository: it is the best proof of the skills you built here.

Lab · what's graded

  • Turn a trend into positions with sma_positions25 pts
  • Backtest with costs in strategy_returns25 pts
  • Judge the result with performance_summary25 pts
  • Wire it all together in run_strategy25 pts
Explorer · lab-142-1
▾ lab-142-1
…
bash · in-browser
learner@quantcraft:~/lab-142-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-142-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub