演習からライブラリへ
あなたは100ほどの小さな関数を書いてきました。ライブラリとは、それらの関数を、他の人(半年後の自分を含む)が安心して使えるように整理したものです。明確な公開インターフェース、個別にテストされた各部品、パイプラインとして組み合わされた部品、そして全体がインストールでき、文書化され、自動でチェックされること。この最終プロジェクトはそのパターンの縮図です。戦略ライブラリは練習に最適です。シグナルが価格をポジションに変え、バックテストがポジションをコスト控除後のリターンに変え、指標がリターンを要約し、ランナーがそれらをつなぎます。各段階は素朴な配列を受け取り素朴な配列を返すので、手計算で確認できる数値で個別にテストできます。
# 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シグナルとバックテスト:先読みなし、コスト込み
ここでのシグナルは移動平均のクロスです。価格の短期平均が長期平均より上ならロング、下ならショート、等しいか長期平均がまだ存在しないなら無ポジションです。バックテストが誠実かどうかを決める2つの規則は、コードに現れます。1つ目は先読みなしです。t 日の引けで決めるポジションは t 日までの価格しか使えず、t + 1 日のリターンを得ます。だからステップ t のリターンは positions[t - 1] を使います。テストで確かめられます。最後の価格を取り除いても、それ以前のポジションが変わってはいけません。2つ目はコストです。ポジションが変わるたびに cost * |変化| を払うので、頻繁に反転する戦略は、コスト前には勝ってもコスト後には負けることがあります。
# 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%パフォーマンス指標とランナー
リターン系列はいくつかの数値で要約します。累積リターンは期間を複利で合成した prod(1 + r) - 1。年率リターンはそれを1年に換算した (1 + total)^(年あたり期間数 / n) - 1。年率ボラティリティは標本標準偏差に sqrt(年あたり期間数) を掛けたもの。シャープレシオは平均リターンをその標準偏差で割って年率化したもので、リスク1単位あたりのリターンを示す代表的な指標です(平坦な系列では除算エラーではなく0)。最大ドローダウンは資産曲線のピークからの最悪の下落で、その戦略がどれだけの痛みを求めるかを示します。ランナー関数がすべてをつなぎます。ポジション、次にリターン、そして要約です。他の関数を組み合わせるだけにして、バグが常に1つの段階に追跡できるようにしましょう。
# 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あなたの最終プロジェクト
では自分のものを、自分のリポジトリで作りましょう。次のどちらか1つを選びます。価格評価ライブラリ:Black-Scholes とグリークス、対称変量法を使ったモンテカルロ評価、インプライド・ボラティリティのソルバー、気配の集合へのモデル較正。または戦略ライブラリ:複数のシグナル、コストとポジション制限付きのバックテスト、ウォークフォワード評価、レポート。どちらでも、同じものを納品します。明確な公開 API と型ヒントを備えたパッケージ。固定シードのテストと手計算で確認したケース。インストール方法と例の実行方法を示す pyproject.toml と README。lint・型・テストを走らせる CI ワークフロー。プロファイルしたボトルネックを1つ含むパフォーマンスメモ。前提と、モデルを壊すものについての短い文書。時間配分は、まずインターフェースを設計し、動く最小のパイプラインを作り、それから広げます。
- ライブラリとは、明確なインターフェースの背後で、テスト済みの小さな関数がパイプラインに組まれたもの。シグナル→ポジション→コスト後リターン→指標を、ランナーがつなぐ
- 誠実なバックテストは先読みをせず(t のポジションは t までの価格を使い、次のリターンを得る)、ポジションが変わるたびにコストを課す
- 累積・年率リターン、ボラティリティ、シャープ、最大ドローダウンで要約し、テスト、CI、README、誠実なレポート付きのパッケージを納品する
これでロードマップは完了です。あなたは今、現役のクオンツ開発者が持つ言語、数値ツール、市場データと執行の部品、モデル、エンジニアリングの習慣を手にしています。最終プロジェクトのリポジトリは大切に保管してください。ここで身につけたスキルの最良の証明になります。
ラボ:採点される内容
sma_positionsでトレンドをポジションにする25点strategy_returnsでコスト込みバックテストをする25点performance_summaryで結果を評価する25点run_strategyですべてをつなぐ25点