PythonでAPIキー不要の再現可能バックテスト
APIキーなしでPythonのバックテストを再現する方法を解説します。固定インストールと決定論的なサンプルデータを使い、単一の損益曲線だけでは見えない限界も検証します。
Pythonで再現可能なバックテストとは、第三者がアカウントやAPI keyなしに、環境を構築しただけのマシンで再実行し、まったく同じ数値を得られるものです。多くのチュートリアルは、最初の一行でこの条件を満たさなくなります。ライブダウンロードにより、次の読者が取得する価格履歴が、執筆者の取得時とわずかに異なるためです。
この解説では、オープンソースのエンジンを一つに固定し、quantjourney-bt version 0.12.4で、認証情報を一切使わずに同梱のサンプルを実行します。その後、実際の市場データを使い、単一のクリーンな実行だけではなお判断できないことを示します。
再現可能なバックテストとは
ここでいう再現可能性には、狭く検証可能な意味があります。別の人がクリーンな環境で一つのコマンドを実行し、あなたの数値を小数点以下まで再現できることです。これを壊す要因は、主に二つあります。
一つ目はコードです。バージョン0.xのライブラリには、マイナーリリース間の互換性が保証されていません。名称変更、デフォルト値の変更、列順の変更などがあると、スクリプトは実行し続けても、別の内容を静かに報告することがあります。
二つ目はデータです。冒頭でライブデータをダウンロードするチュートリアルは、再現可能ではありません。データベンダーは過去データを改訂し、株式分割を調整し、欠損を補完します。そのため、同じスクリプトでも一カ月後には異なる数値が出力されます。後からコードの変更とデータの変更を切り分けることはできません。
このプロジェクトから参考にすべき構成は、固定したエンジンのバージョンと、パッケージ自体に同梱された小規模なデータセットを組み合わせることです。
インストールを固定する:pip install quantjourney-bt==0.12.4
quantjourney-btはApache License 2.0の下で公開されているQuantJourneyのバックテスターで、Python 3.11以降が必要です。バージョン0.12.4は2026年7月21日に公開されました。2026年8月時点で、以下のコマンドはすべてこのバージョンを参照します。
分離環境内で作業します。python3 -m venv .venvで環境を作成し、source .venv/bin/activateで環境に入り、python -m pip install -U pipで環境内のインストーラーを更新します。その後、正確なバージョンを指定します。pip install quantjourney-bt==0.12.4です。
プロジェクトでは、バージョンを固定しない形式としてpip install quantjourney-btが記載されています。==0.12.4の指定は利用者の責任です。0.x系では、依存関係を固定することが重要です。次に作業する人が確認できる場所に固定内容を記録します。pip freeze > requirements.txtを使うと、明示的に指定していない依存関係も含め、解決済みの依存関係をすべて保存できます。
オプションの追加機能は二つあります。pip install "quantjourney-bt[wf]"では、ウォークフォワード分析と最適化の例に使用するOptunaが追加されます。pip install "quantjourney-bt[data]"では、ベンチマークに使用するyfinanceのフォールバック機能が追加されます。
Apache-2.0は緩やかな条件のライセンスです。コードを商用利用・改変できます。再配布する場合は、ライセンスファイルと通知ファイルを保持する必要があります。また、貢献者は特許権を明示的に許諾します。
APIキーなしで同梱SMAサンプルを実行する方法
リポジトリには、50本の実行可能なサンプル戦略とともにランチャースクリプトが収録されています。サンプルは、ウェイトベースの方式と注文ベースの方式に分かれており、その中には5本のウォークフォワード・ワークフローも含まれます。./strategy.sh --listで一覧を表示できます。./strategy.sh example_weights_01_sma_daily --checkは単一の戦略をインポートするだけで、データには一切アクセスしません。インストールが正常かを最も簡単に確認できる方法です。
デモの実行は一行です。./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
要点は--sample-dataフラグにあります。プロジェクトでは、このフラグが指定するデータセットを次のように説明しています。
サンプルデータセットは、意図的に小規模かつ再現可能な構成になっています。アカウントを作成せずに、インストールの確認、レポート生成、エンジンの処理フローの確認を行う用途に適しています。
出典:quantjourney-bt README、バージョン0.12.4、2026年8月6日閲覧。
実行結果は、コンソール上の判定ではなくディレクトリに書き出されます。summary.txtとsummary.json、metrics.csv、そのequity_curve.pngと同じ場所に置かれるequity_curve.csv、dashboard.html、plots/フォルダ、そして実行時の設定を記録したrun_metadata.jsonが生成されます。最後のファイルは、多くの人が見落とします。しかし、1年後に結果を監査可能にするのはこのファイルです。
生成された指標は、額面どおりに受け取らないでください。同梱データセットは小規模で説明用のため、summary.txtに表示されるシャープレシオと最大ドローダウンは、サンプルファイルの内容を示すにすぎません。これらは戦略の有効性を示す証拠ではありません。これを実績とみなすことが、最初に犯し得る誤りです。
この実行で確認できる点には意味があります。インストールが機能し、シグナルから目標ウェイト、再構築されたポートフォリオ価値に至るエンジン全体の処理フローが、認証情報なしでお使いのマシン上に成果物を生成します。実際の履歴データを使ったバックテスト向けに、プロジェクト独自のデータサービスへ認証して接続する方法もあります。その手順は文書化されています。このガイドは、誰からも何も受け取る必要がない範囲、つまりここで終わります。
インサンプルの一つのエクイティカーブでは分からないこと
サンプル実行では、一つのエクイティカーブが描かれます。しかし、そのカーブだけでは分からないことがあります。デモ用ファイルではなく、実際の市場データで確認します。
下のパネルでは、サンプル戦略と同じ考え方を使います。20セッション移動平均線が50セッション移動平均線を上抜け・下抜けするルールをSPYに適用し、2017年から2025年までの各暦年について個別に結果を示しています。各セッションのポジションは前セッションの終値で決まります。そのため、このルールがまだ取得していない数値で売買することはありません。
各数値の背後にある正確なSQL
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY year変更していない一つのルールを、9回に分けて測定しています。まず、二つのパーセント列を上から確認してください。2017では、このルールの年間成績は16%となりました。同じ期間にSPYを保有した場合は19.4%です。2025では、同じ二列がそれぞれ10.4%と16.4%になります。両方の行でコードは同一です。変わったのは期間だけです。
クロスオーバー列は、基礎となるデータがいかに少ないかを示しています。4のポジション変更が2025にわたって発生するだけでは、一年分のエクイティカーブは少数の判断に依存します。これほど小さいサンプルを結果と呼ぶのは適切ではありません。
他の銘柄でも同じルールは同じように機能するのでしょうか?
日付の期間を変えることは、単一のカーブを検証する方法の一つです。もう一つは、対象ユニバースを変えることです。以下のパネルではパラメーターを固定し、同一のルールを、2021年から2025年までの五暦年にわたって、流動性の高い五銘柄に適用しています。
各数値の背後にある正確なSQL
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQはパネル上部の40.2%に位置し、最下段のKOは3.8%となっています。days_long_pct列は、各バージョンが期間中に何らかのポジションを保有していた時間の割合を示しており、最上段では67%です。一つのパラメーター設定を五つのユニバースで検証すると、結果には十分な差が生じます。しかし、事後的に勝者を選んでも、まだ実行していない運用の結果については何も示しません。
これは、クロスオーバー取引を推奨するものではありません。クロスオーバーはバックテストを測定するための物差しであり、私たちが測定しているのはそのバックテストです。
ウェイトのバックテストに先読みバイアスが入り込む場所
ウェイトベースのエンジンは、シグナルを目標ウェイトに変換し、そのウェイトに基づいて約定をシミュレーションし、結果として得られたポジションからポートフォリオ価値を再構築します。問題は、シグナルとウェイトを結合する部分に潜みます。当日のウェイトを当日の終値から算出し、そのウェイトで当日のリターンを得ると、注文を出す時点では存在しなかった情報に基づいて売買したことになります。これが先読みバイアスです。エラーは発生しません。ただ、すべてが実際より良く見えるだけです。
以下のパネルでは、同じSPYの時系列を使い、1つのルールについて2つのバージョンを実行しています。
各数値の背後にある正確なSQL
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY year2017では、前セッション版が16%を示したのに対し、同一セッション版は17.1%を示しました。差は1.1パーセントポイントです。2025では、両者の差は1.6パーセントポイントでした。セッションの終値をあらかじめ知っていなかった機械が生成できるのは、そのうち一方の列だけです。両者の差は会計上の処理だけで生じたものであり、投資アイデアもスキルも、実際の取引も関係していません。
このエンジンは、タイミングに関する自らの立場を明示しています。単なる約束よりも、明文化された説明の方が重要です。
始値で約定する場合、レンジに依存するスリッページは直前に完了したバーだけを参照し、約定可能な出来高は遅行観測値から予測します。エンジンは、その日の後続の高値、安値、終値、または終日出来高を使用しません。
出所:quantjourney-bt README、バージョン0.12.4、2026年8月6日閲覧。
文書化された前提は、すでにインストールしたソースコードと照合できます。文書化されていない前提は、推測にすぎません。
ウォークフォワード用の追加機能が存在する理由
ウォークフォワードの例であるWF01からWF05には、[wf]追加機能と、その依存関係にあるOptunaが含まれています。ウォークフォワードでは、過去データの一つの区間でパラメーターを適合させ、その直後の区間で検証します。その後、この二つの区間を前方へ移動して同じ処理を繰り返します。ローリング方式と拡張方式の違いは、前方へ進む際に適合期間から最も古いデータを外すかどうかです。別の例では、各境界にパージとエンバーゴを設け、接合部に最も近い観測値を除外します。これにより、適合に使った区間の情報が、検証対象の区間へ漏洩するのを防ぎます。
ただし、これによって弱いアイデアが機能するようになるわけではありません。一つの数値を、検証可能な数値の分布に置き換えることが、この手法の本質的な改善点です。その次の段階も、まだ実資金を使った取引ではありません。実資金投入前のペーパートレードでは、バックテストでは構造上確認できない点を測定します。まず、注文がシミュレーターの想定価格にどの程度近い水準で約定するかを確認します。また、取引ボット向けサーキットブレーカーでは、コードが想定外の挙動を示した日に何が起きるかを管理します。これらを支える統計については、オープンソースのクオンツ取引書に関する解説で、さらに詳しく説明しています。
FAQ
APIキーなしで戦略をバックテストできますか?
はい。quantjourney-btには、--sample-dataフラグで利用できるサンプルデータセットが同梱されています。サンプル戦略は、アカウントや認証情報なしでこのデータセットに対して実行できます。データセットは小規模で説明用のため、この実行結果は戦略の有効性を示す証拠ではなく、インストールと処理パイプラインの確認として扱ってください。
Pythonのバックテスト・パッケージのバージョンを固定する理由は何ですか?
0.xのパッケージには、マイナーリリース間の互換性が保証されていません。デフォルト設定の変更や指標名の変更があっても、通知されないことがあります。pip install quantjourney-bt==0.12.4でバージョンを固定し、requirementsファイルに環境を記録すれば、今日作成した結果を、来年もその結果を生成したエンジン上で再構築できます。
quantjourney-btのライセンスは何ですか?
Apache License 2.0です。商用利用と改変が認められています。再配布する場合は、ライセンスファイルと通知ファイルを保持する必要があります。また、貢献者による明示的な特許許諾も含まれています。バージョン0.12.4は2026年7月21日に公開され、Python 3.11以降が必要です。
バックテストの結果が良好なら、その戦略は機能すると考えてよいですか?
いいえ。バックテストは、単一の期間と単一の銘柄ユニバースに対する、一つの測定結果にすぎません。上のパネルでは、変更していない同じルールでも、単一のtickerにおける年次結果が大きく異なり、五銘柄間でも結果が大きく異なることを示しています。こうした差を明らかにするために、ウォークフォワード検証とアウトオブサンプル・テストが用いられます。
上のパネルの計算方法
日次終値には、各日付について通常取引時間の最後の1分間に成立したprintを使用しています。時刻はニューヨーク時間で、午前9時30分から午後4時までの範囲です。これにより、取引時間が短縮される日も、セッション時間を固定値として設定せずに正しく処理できます。短期移動平均は20セッション、長期移動平均は50セッションで、いずれも単純移動平均です。各系列の最初の49セッションはウォームアップ期間で、ポジションを保有しません。年次結果では、ルールがロングだったセッションについて、各セッションの終値から次の終値までの変動率を複利計算しています。比較用の保有列では、同じ年の全セッションを複利計算しています。クロスセクションの五銘柄は、対象期間内に分割がなく、連続した履歴がある銘柄を選んでいます。そのため、終値系列の調整は不要です。対象期間は過去の固定された期間であるため、これらのパネルは再生成するたびに同じ数値を返します。
ここにある各パネルには、その直下に完全なSQLを掲載しています。そのため、このページの数値は、バージョンを固定したインストール環境と同じように再実行できます。バックテストのコードを書く前に、自分の期間でルールを測定するには、Strasmore terminalで質問を平易な英語で入力してください。