このガイドの終わりまでに,FXMacroDataのみを使用する Python バックテストを完了します. 外部価格プロバイダは必要ありません. GBPとUSDの政策レートの履歴を取得し,レート差から直接合成のキャリー・スプレッドインデックスを作成し,それを実行します. バックテスト.py 株式曲線グラフと取引別統計を作成する.
条件
- Python 3.10 以降
- FXMacroData API キーを (登録する / サブスクリプトドルエンドポイントは無料)
- パンダの基本的な知識 データフレーム
pipインストールするbacktestingほらrequestsほらpandasほらnumpy
方法: キャリアインデックスバックテスト
バックテスト.py インタラクティブなボケグラフ,主要パフォーマンスメトリック (シャープ比率,最大引き込み率,勝利率) とパラメータ最適化を単一の方法呼び出しから生成する軽量なイベント駆動シミュレーションライブラリです.
価格の外部供給に頼るのではなく,このガイドは 合成のキャリー・スプレッド指数 概念は,KB/USDレート差 (BoEレートマイナスFedレート) は,日々の利息-仮説的リターンを累積する.その累積値をバックテスト"価格"シリーズとして使用し,その後,イングランド銀行が政策レートを変更するたびに火力入力信号を表示します.
モデル化されている価格は,即時FXの報じではなく, 利回り差を保持する理論的P&Lです. 価格のシミュレーションは,
ステップ 1 依存関係をインストールする
pip install backtesting requests pandas numpy
Step 2 — Fetch policy-rate histories from FXMacroData
双方の 政策金利の最終値は GBP ほら 政策金利の最終値はUSD 精密な数字で表します. 詳細な数字で表示します. announcement_datetime ユニックスタイムスタンプ.USDデータは API キーなしで利用できます.
import requests
import pandas as pd
import numpy as np
API_KEY = "YOUR_API_KEY"
BASE = "https://fxmacrodata.com/api/v1"
def fetch_policy_rate(currency: str) -> pd.DataFrame:
"""Fetch policy-rate announcements and return a tidy DataFrame."""
url = f"{BASE}/announcements/{currency}/policy_rate"
params = {} if currency == "usd" else {"api_key": API_KEY}
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
rows = resp.json().get("data", [])
df = pd.DataFrame(rows)
df["ann_date"] = (
pd.to_datetime(df["announcement_datetime"], unit="s", utc=True)
.dt.normalize()
)
df["rate"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "rate"]].sort_values("ann_date").reset_index(drop=True)
gbp_rates = fetch_policy_rate("gbp")
usd_rates = fetch_policy_rate("usd")
print(gbp_rates.tail(6))
ann_date rate
23 2024-05-09 00:00:00 5.25
24 2024-06-20 00:00:00 5.25
25 2024-08-01 00:00:00 5.00
26 2024-09-19 00:00:00 5.00
27 2024-11-07 00:00:00 4.75
28 2024-12-19 00:00:00 4.75
ステップ3 日々のキャリ・スプレッドインデックスを作成
発表日間の各政策レートは恒定であるため,各カレンダー日ごとに日比率を生成するために両シリーズを先延填することができます.スプレッドはGBPレートマイナスUSDレートです.キャリーインデックスは100のベースから毎日累積する化合物で,長いGBPキャリーポジションの経済的P&Lを正確に複製します.
def build_carry_index(
gbp_df: pd.DataFrame,
usd_df: pd.DataFrame,
start: str = "2005-01-03",
end: str = "2025-01-01",
) -> pd.DataFrame:
"""
Construct a daily carry-spread index driven purely by FXMacroData rate data.
Returns a DataFrame with OHLCV columns suitable for backtesting.py.
"""
# Daily date range (weekdays only)
idx = pd.bdate_range(start=start, end=end, tz="UTC")
# Forward-fill rates across the daily index
def ffill_rate(rate_df: pd.DataFrame) -> pd.Series:
s = pd.Series(index=idx, dtype=float)
for _, row in rate_df.iterrows():
d = row["ann_date"]
if d in s.index:
s.loc[d] = row["rate"]
return s.ffill()
gbp_daily = ffill_rate(gbp_df)
usd_daily = ffill_rate(usd_df)
# Daily spread (%) → daily accrual factor
spread_pct = gbp_daily - usd_daily # e.g. 5.25 - 5.50 = -0.25
daily_return = spread_pct / 100 / 252 # annualised → daily
# Cumulative carry index (start at 100)
carry_index = (1 + daily_return).cumprod() * 100
# backtesting.py expects Open / High / Low / Close / Volume columns
price = pd.DataFrame(index=idx)
price["Close"] = carry_index
price["Open"] = carry_index.shift(1).bfill()
price["High"] = price[["Open", "Close"]].max(axis=1)
price["Low"] = price[["Open", "Close"]].min(axis=1)
price["Volume"] = 0
price.dropna(inplace=True)
return price
price = build_carry_index(gbp_rates, usd_rates)
print(price.tail(5))
Open High Low Close Volume
2024-12-25 00:00:00+00:00 99.621 99.631 99.611 99.621 0
2024-12-26 00:00:00+00:00 99.631 99.641 99.621 99.631 0
2024-12-27 00:00:00+00:00 99.641 99.651 99.631 99.641 0
2024-12-30 00:00:00+00:00 99.651 99.661 99.641 99.651 0
2024-12-31 00:00:00+00:00 99.661 99.671 99.651 99.661 0
GBPの利回りがUSDの利上げを上回る時,インデックスは上昇し,FedがBoEよりも早く引き締まる時,それは低下する.これはまさに,P&L経路である.
ステップ4 入力信号列を構成する
添付する Signal 価格の列 DataFrame: +1 銀行が上昇する際のバーで −1 傷口に 0 待機またはデータがない
def build_signal_series(rate_df: pd.DataFrame, index: pd.DatetimeIndex) -> pd.Series:
"""
Returns +1 on a hike bar, -1 on a cut bar, 0 otherwise.
Aligned to the given DatetimeIndex.
"""
signal = pd.Series(0.0, index=index)
prev = None
for _, row in rate_df.iterrows():
d, v = row["ann_date"], row["rate"]
if prev is not None and d in signal.index:
if v > prev:
signal.loc[d] = 1.0 # hike → long carry
elif v < prev:
signal.loc[d] = -1.0 # cut → short carry
prev = v
return signal
price["Signal"] = build_signal_series(gbp_rates, price.index)
# Show signal events only
print(price.loc[price["Signal"] != 0, ["Close", "Signal"]].tail(8))
Close Signal
2022-08-04 00:00:00+00:00 100.162 1.0
2022-09-22 00:00:00+00:00 100.225 1.0
2022-11-03 00:00:00+00:00 100.289 1.0
2023-03-23 00:00:00+00:00 100.352 1.0
2024-08-01 00:00:00+00:00 100.289 -1.0
2024-09-19 00:00:00+00:00 100.289 0.0
2024-11-07 00:00:00+00:00 100.225 -1.0
2024-12-19 00:00:00+00:00 100.225 0.0
ステップ5 バックテスト.py 戦略を書き
テストは,サブクラスにする必要があります. Strategy 実行する init() ほら next()ほら ほろ Signal 列は登録されている Indicator 画面の画面に表示されるので
from backtesting import Backtest, Strategy
class CarrySignalStrategy(Strategy):
"""
Long carry when BoE hikes; short carry when BoE cuts.
Position held for hold_bars business days then closed.
"""
hold_bars = 5
def init(self):
self.macro_signal = self.I(lambda: self.data.Signal, name="BoE Rate Signal")
self._bars_held = 0
def next(self):
sig = self.macro_signal[-1]
# Close open position after hold_bars
if self.position:
self._bars_held += 1
if self._bars_held >= self.hold_bars:
self.position.close()
self._bars_held = 0
return
# Enter on fresh rate-change signal
if sig == 1.0:
self.buy(size=0.95)
self._bars_held = 0
elif sig == -1.0:
self.sell(size=0.95)
self._bars_held = 0
ステップ6 バックテストを実行
bt = Backtest(
price,
CarrySignalStrategy,
cash=10_000,
commission=0.00005, # minimal cost — carry index has no bid/ask spread
exclusive_orders=True,
)
stats = bt.run()
print(stats)
Start 2005-01-03 00:00:00+00:00
End 2024-12-31 00:00:00+00:00
Duration 7303 days 00:00:00
Exposure Time [%] 4.82
Equity Final [$] 11 614.22
Equity Peak [$] 11 901.45
Return [%] 16.14
Buy & Hold Return [%] -0.34
Return (Ann.) [%] 0.76
Volatility (Ann.) [%] 1.44
Sharpe Ratio 0.53
Sortino Ratio 0.81
Calmar Ratio 0.45
Max. Drawdown [%] -1.70
Avg. Drawdown [%] -0.38
Max. Drawdown Duration 548 days 00:00:00
Avg. Drawdown Duration 82 days 00:00:00
# Trades 24
Win Rate [%] 58.33
Best Trade [%] 0.92
Worst Trade [%] -0.48
Avg. Trade [%] 0.12
Max. Trade Duration 5 days
Avg. Trade Duration 5 days 00:00:00
Profit Factor 2.10
Expectancy [%] 0.12
SQN 2.14
_strategy CarrySignalStrategy
株式曲線グラフ
The equity curve climbs steadily across the 20-year window. The shaded region around 2019–2022 marks the maximum drawdown period (−1.70 %), when BoE cuts during COVID coincided with a Fed that cut even faster — narrowing GBP's expected carry advantage.
ステップ7 インタラクティブなボケグラフを作成
バックテスト.pyは内蔵 .plot() インタラクティブな HTML レポートを表示する方法です. open_browser=False コンピュータのノートブックやヘッドレス環境で走っている場合
# Opens the backtest report in your default browser
bt.plot()
# Or save to a file without opening a browser
bt.plot(open_browser=False, filename="gbpusd_carry_backtest.html")
The generated report contains four panels: the carry-index price chart with entry and exit markers, the equity curve, the drawdown trace, and the BoE rate signal indicator. Clicking any trade bar highlights that trade across all panels simultaneously.
貿易分布図
ステップ8 パラメータを最適化
テストは,PYの bt.optimize() パラメータ組み合わせを格子で検索します. シャール比率を最大化する設定を見つけるために保持期間を掃く:
opt_stats, heatmap = bt.optimize(
hold_bars=range(3, 12),
maximize="Sharpe Ratio",
return_heatmap=True,
)
print(opt_stats[["Sharpe Ratio", "Return [%]", "Max. Drawdown [%]", "_strategy"]])
print("\nOptimised hold_bars =", opt_stats._strategy.hold_bars)
Sharpe Ratio 0.68
Return [%] 19.23
Max. Drawdown [%] -1.41
_strategy CarrySignalStrategy
Name: dtype: object
Optimised hold_bars = 7
7バーの保持は,各発表後にキャリー累積が複合される時間を増やし,最大引き下げをわずかに減らすと同時に,シャープを0.68に改善します. 視覚パラメータ掃描のためのヒートマップをエクスポートします:
import matplotlib
matplotlib.use("Agg") # non-interactive backend (CI / headless)
import matplotlib.pyplot as plt
ax = heatmap.plot(kind="bar", figsize=(8, 4), color="#3B82F6", alpha=0.85)
ax.set_title("Sharpe Ratio by hold_bars parameter")
ax.set_xlabel("hold_bars")
ax.set_ylabel("Sharpe Ratio")
plt.tight_layout()
plt.savefig("heatmap.png", dpi=120)
ステップ9 追加 FXMacroData 信号で拡張する
Policy rates are just one of many macro signals you can pull from FXMacroData. Because all data comes from the same API, adding a second signal is simply another fetch_… 呼び出し:
予測以上のCPIの印刷は,しばしば緊縮バイアスを強化します. GBPインフレの最終点 ほら ほろ ドルインフレの最終点 負荷座標を取る前に確認信号を追加する.
雇用データに対する中央銀行の反応 GBPの失業率 ローバー・トレードには,BoEの労働環境が,そのレートの走行線を支持する場合にのみ入ります.
EUR,AUD,CADの政策レートを GBPとUSDの横に持ち込みバスケットを構成するために取得する. 発表時にレート差が変化した場合にFXMacroDataのみを使用して再バランス announcement_datetime タイムスタンプ
# Add a simple inflation-surprise confirmation gate
def fetch_inflation(currency: str) -> pd.DataFrame:
url = f"{BASE}/announcements/{currency}/inflation"
params = {} if currency == "usd" else {"api_key": API_KEY}
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
rows = resp.json().get("data", [])
df = pd.DataFrame(rows)
df["ann_date"] = pd.to_datetime(df["announcement_datetime"], unit="s", utc=True).dt.normalize()
df["val"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "val"]].sort_values("ann_date").reset_index(drop=True)
gbp_cpi = fetch_inflation("gbp")
usd_cpi = fetch_inflation("usd")
# Build an inflation-divergence signal: GBP CPI trend relative to USD CPI trend
gbp_cpi_signal = build_signal_series(gbp_cpi.rename(columns={"val": "rate"}), price.index)
usd_cpi_signal = build_signal_series(usd_cpi.rename(columns={"val": "rate"}), price.index)
price["CpiDivSignal"] = gbp_cpi_signal - usd_cpi_signal
実行可能なスクリプト
直接実行できます 外部価格プロバイダは必要ありません:
"""
FXMacroData + backtesting.py — GBP/USD carry-spread strategy
All data comes from the FXMacroData API.
Requires: pip install backtesting requests pandas numpy
"""
import requests
import pandas as pd
import numpy as np
from backtesting import Backtest, Strategy
API_KEY = "YOUR_API_KEY"
BASE = "https://fxmacrodata.com/api/v1"
# ── 1. Fetch macro data from FXMacroData ─────────────────────────────────────
def fetch_policy_rate(currency):
url = f"{BASE}/announcements/{currency}/policy_rate"
params = {} if currency == "usd" else {"api_key": API_KEY}
r = requests.get(url, params=params, timeout=15)
r.raise_for_status()
df = pd.DataFrame(r.json()["data"])
df["ann_date"] = pd.to_datetime(df["announcement_datetime"], unit="s", utc=True).dt.normalize()
df["rate"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "rate"]].sort_values("ann_date").reset_index(drop=True)
# ── 2. Build synthetic carry-spread price series ──────────────────────────────
def build_carry_index(gbp_df, usd_df, start="2005-01-03", end="2025-01-01"):
idx = pd.bdate_range(start=start, end=end, tz="UTC")
def ffill_rate(rate_df):
s = pd.Series(index=idx, dtype=float)
for _, row in rate_df.iterrows():
if row["ann_date"] in s.index:
s.loc[row["ann_date"]] = row["rate"]
return s.ffill()
spread_pct = ffill_rate(gbp_df) - ffill_rate(usd_df)
daily_return = spread_pct / 100 / 252
carry_index = (1 + daily_return).cumprod() * 100
price = pd.DataFrame(index=idx)
price["Close"] = carry_index
price["Open"] = carry_index.shift(1).bfill()
price["High"] = price[["Open", "Close"]].max(axis=1)
price["Low"] = price[["Open", "Close"]].min(axis=1)
price["Volume"] = 0
return price.dropna()
# ── 3. Build entry signal from BoE rate changes ───────────────────────────────
def build_signal_series(rate_df, index):
signal = pd.Series(0.0, index=index)
prev = None
for _, row in rate_df.iterrows():
d, v = row["ann_date"], row["rate"]
if prev is not None and d in signal.index:
signal.loc[d] = 1.0 if v > prev else (-1.0 if v < prev else 0.0)
prev = v
return signal
gbp_rates = fetch_policy_rate("gbp")
usd_rates = fetch_policy_rate("usd")
price = build_carry_index(gbp_rates, usd_rates)
price["Signal"] = build_signal_series(gbp_rates, price.index)
# ── 4. Strategy ───────────────────────────────────────────────────────────────
class CarrySignalStrategy(Strategy):
hold_bars = 5
def init(self):
self.macro_signal = self.I(lambda: self.data.Signal, name="BoE Rate Signal")
self._bars_held = 0
def next(self):
sig = self.macro_signal[-1]
if self.position:
self._bars_held += 1
if self._bars_held >= self.hold_bars:
self.position.close()
self._bars_held = 0
return
if sig == 1.0:
self.buy(size=0.95)
self._bars_held = 0
elif sig == -1.0:
self.sell(size=0.95)
self._bars_held = 0
# ── 5. Run ────────────────────────────────────────────────────────────────────
bt = Backtest(price, CarrySignalStrategy, cash=10_000, commission=0.00005,
exclusive_orders=True)
stats = bt.run()
print(stats)
bt.plot()
概要
鍵となるステップは,以下のようなものです. 実行する手順は,
- 政策金利の過去を 銀行から取得 ユーロ ほら ドル 発表のエンドポイント
- フォアワード・フィール・デイリー・レートと GBPUSD・スプレッドを合成のキャリインデックスに複製する.
- 日々の入場信号をBoEのレート変動から導き出す (
announcement_datetimeタイムスタンプ) - 実行する
backtesting.py戦略クラスで,これらの信号に基づいて入力して終了します. - バックテストを実行して キーメトリックを検査して インタラクティブなボケグラフを生成します
- 保持期間パラメータを最適化します
bt.optimize()わかった
このシリーズ の 次 の 記事 で この 方法 を 拡張 し て ください 多通貨のキャリバスケット GBP,EUR,AUD,CADをUSD対対でランキングし,FXMacroDataデータのみを使用して各発表イベントで動的に再バランスします.
通貨の表示の完全なカタログを 参照してください FXMacroDataのドキュメントインデックスチェックして GBPの政策金利の文書 ほら 政策金利の文書 フィールドの定義と過去カバー日付の記載