Pada akhir panduan ini Anda akan memiliki backtest Python yang bekerja yang hanya menggunakan FXMacroData tidak perlu penyedia harga eksternal. Anda akan mengambil sejarah suku bunga kebijakan GBP dan USD, membangun indeks carry-spread sintetis langsung dari perbedaan suku bunga, dan menjalankan itu melalui backtesting.py Untuk menghasilkan grafik kurva ekuitas dan statistik per perdagangan.
Persyaratan
- Python 3.10 atau lebih baru
- Kunci API FXMacroData (daftar di /langganan; titik akhir USD bebas)
- Keterampilan dasar dengan panda DataFrames
pipakses untuk menginstalbacktestingAku akan pergi.requestsAku akan pergi.pandas, dannumpy
Pendekatan: backtesting carry-index
backtesting.py adalah perpustakaan simulasi berbasis peristiwa ringan yang menghasilkan plot Bokeh interaktif, metrik kinerja utama (ratio Sharpe, penarikan maksimum, tingkat kemenangan), dan optimasi parameter dari panggilan metode tunggal.
Alih-alih mengandalkan umpan harga eksternal, panduan ini membangun indeks carry-spread sintetis ide ini adalah representasi yang setia dari perdagangan carry: perbedaan suku bunga GBP/USD (Boe rate minus Fed rate) mengumpulkan pengembalian bunga-notional harian. kami menggunakan nilai kumulatif dari pengemasan itu sebagai seri "harga" backtest kami, kemudian sinyal masuk api setiap kali Bank of England mengubah suku bunga kebijakan.
Ini adalah bagaimana strategi carry institusional sebenarnya dimodelkan harga yang Anda simulasi adalah P & L teoritis memegang diferensial imbal hasil, bukan penawaran FX spot.
Langkah 1 Menginstal ketergantungan
pip install backtesting requests pandas numpy
Langkah 2 Dapatkan riwayat suku bunga kebijakan dari FXMacroData
Keduanya. Poin akhir suku bunga kebijakan GBP dan Nilai akhir suku bunga kebijakan USD kembali setiap keputusan bank sentral sejak seri dimulai, masing-masing dengan tepat announcement_datetime Unix time stamp. Data USD tersedia tanpa kunci 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
Langkah 3 Membangun indeks carry-spread harian
Antara tanggal pengumuman setiap suku bunga kebijakan adalah konstan, sehingga kita dapat mengisi kedua seri ke depan untuk menghasilkan suku bunga harian untuk setiap hari kalender. Spread adalah GBP rate dikurangi USD rate. Indeks carry senyawa yang harian akumulasi dari basis 100, persis mereplikasi ekonomi P & L dari posisi GBP carry panjang.
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
Ketika hasil GBP melebihi hasil USD, indeks bergeser ke atas; ketika Fed mengencangkan lebih cepat dari BoE, itu bergesers ke bawah.
Langkah 4 Membangun kolom sinyal masuk
Tambahkan Signal kolom ke harga DataFrame: +1 di bar ketika BoE naik, −1 pada luka, 0 menunggu atau tidak ada data.
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
Langkah 5 Tulis strategi backtesting.py
backtesting.py mengharuskan Anda untuk subclass Strategy dan menerapkan init() Dan next()- Itu... Signal kolom terdaftar sebagai Indicator jadi muncul sebagai panel sendiri dalam output Bokeh.
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
Langkah 6 Lakukan backtest
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
Grafik kurva ekuitas
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.
Langkah 7 Membuat grafik Bokeh interaktif
backtesting.py mengirimkan built-in .plot() metode yang membuat laporan HTML interaktif. open_browser=False jika Anda berjalan di sebuah notebook atau lingkungan tanpa kepala.
# 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")
Laporan yang dihasilkan berisi empat panel: grafik harga indeks carry dengan penanda masuk dan keluar, kurva ekuitas, jejak penarikan, dan indikator sinyal suku bunga BoE. Mengklik setiap bar perdagangan menyoroti perdagangan di semua panel secara bersamaan.
Bagan distribusi perdagangan
Langkah 8 Optimalkan parameter
backtesting.py's bt.optimize() menjalankan pencarian grid di seluruh kombinasi parameter. menyapu periode ditahan untuk menemukan konfigurasi yang memaksimalkan rasio Sharpe:
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
Penundaan 7-bar memberikan waktu lebih banyak untuk mengkomposisi akumulasi carry setelah setiap pengumuman, meningkatkan Sharpe menjadi 0,68 sementara juga mengurangi penarikan maksimum sedikit.
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)
Langkah 9 Perluas dengan sinyal FXMacroData tambahan
Karena semua data berasal dari API yang sama, menambahkan sinyal kedua hanyalah sinyal lain. fetch_… panggilan:
Sebuah cetakan IPK di atas perkiraan sering memperkuat bias pengetatan. Endpoint inflasi GBP dengan Endpoint inflasi USD untuk menambahkan sinyal konfirmasi sebelum mengambil posisi carry.
Bank sentral bereaksi terhadap data ketenagakerjaan. Pengangguran GBP hanya masuk ke perdagangan carry ketika latar belakang tenaga kerja BoE mendukung jalur suku bunga yang dinyatakan.
Membawa suku bunga kebijakan untuk EUR, AUD, dan CAD bersama dengan GBP dan USD untuk membangun keranjang bawa peringkat. Rebalancing ketika perbedaan suku bunga bergeser pada saat pengumuman hanya menggunakan FXMacroData announcement_datetime Stempel waktu.
# 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
Skripsi yang dapat dijalankan lengkap
Menggabungkan semuanya dalam satu file Anda dapat menjalankan langsung tidak ada penyedia harga eksternal diperlukan:
"""
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()
Ringkasan
Anda sekarang telah membangun backtest membawa FX yang didorong makro lengkap menggunakan hanya FXMacroData dan framework backtesting.py tidak ada penyedia harga eksternal yang diperlukan.
- Dapatkan riwayat suku bunga kebijakan GBP dan USD dari GBP Dan USD titik akhir pengumuman.
- Tentukan nilai transfer harian dan perkalikan spread GBPUSD menjadi indeks carry sintetis.
- Menghasilkan sinyal entri harian dari peristiwa perubahan suku bunga BoE (
announcement_datetimeStempel waktu). - Melakukan
backtesting.pyKelas strategi yang masuk dan keluar berdasarkan sinyal tersebut. - Jalankan backtest, memeriksa metrik kunci, dan menghasilkan grafik Bokeh interaktif.
- Optimalkan parameter periode penyimpanan dengan
bt.optimize()Aku tidak tahu.
Artikel berikutnya dalam seri ini memperluas pendekatan ini ke Keranjang bawa multi-mata uang peringkat GBP, EUR, AUD, dan CAD terhadap USD berdasarkan perbedaan nilai tukar dan rebalancing secara dinamis pada setiap acara pengumuman hanya menggunakan data FXMacroData.
Jelajahi katalog indikator lengkap untuk setiap mata uang di Indeks dokumentasi FXMacroData, dan periksa Docs suku bunga kebijakan GBP Dan Dokumen suku bunga kebijakan USD untuk definisi bidang dan tanggal cakupan historis.