Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backtesting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* [Multiple Time Frames](../examples/Multiple Time Frames.html)
* [**Parameter Heatmap & Optimization**](../examples/Parameter Heatmap & Optimization.html)
* [Trading with Machine Learning](../examples/Trading with Machine Learning.html)
* [FXMacroData Forex Data](../examples/FXMacroData Forex Data.html)

These tutorials are also available as live Jupyter notebooks:
[![Binder](https://mybinder.org/badge_logo.svg)][binder]
Expand Down
240 changes: 240 additions & 0 deletions doc/examples/FXMacroData Forex Data.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "db641788",
"metadata": {},
"source": [
"FXMacroData Forex Data\n",
"======================\n",
"\n",
"This example shows how to load daily FX spot rates from\n",
"[FXMacroData](https://fxmacrodata.com/) into the OHLCV\n",
"[`pandas.DataFrame`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html)\n",
"shape expected by _backtesting.py_.\n",
"\n",
"_Backtesting.py_ can run on a single instrument at a time, so the helper below\n",
"returns one currency pair as a daily data frame with `Open`, `High`, `Low`,\n",
"`Close`, and `Volume` columns. FXMacroData's spot endpoint provides one daily\n",
"reference rate per row, so this example maps that rate to all OHLC prices and\n",
"sets volume to zero."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ec58ffb9",
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations\n",
"\n",
"import json\n",
"import os\n",
"from typing import Iterable\n",
"from urllib.parse import urlencode\n",
"from urllib.request import Request, urlopen\n",
"\n",
"import pandas as pd\n",
"\n",
"\n",
"FXMACRODATA_API_BASE_URL = \"https://fxmacrodata.com/api/v1\"\n",
"\n",
"\n",
"def split_currency_pair(pair: str) -> tuple[str, str]:\n",
" \"\"\"Return normalized base and quote currency codes from EURUSD or EUR/USD.\"\"\"\n",
" normalized = pair.replace(\"/\", \"\").replace(\"-\", \"\").replace(\"_\", \"\").upper()\n",
" if len(normalized) != 6:\n",
" raise ValueError(\"currency pair must look like 'EURUSD' or 'EUR/USD'\")\n",
" return normalized[:3], normalized[3:]\n",
"\n",
"\n",
"def fxmacrodata_rows_to_ohlc(rows: Iterable[dict]) -> pd.DataFrame:\n",
" \"\"\"Convert FXMacroData forex rows to a Backtesting.py OHLCV data frame.\"\"\"\n",
" records = []\n",
" for row in rows:\n",
" date_value = (\n",
" row.get(\"date\")\n",
" or row.get(\"time\")\n",
" or row.get(\"timestamp\")\n",
" or row.get(\"datetime\")\n",
" )\n",
" rate = (\n",
" row.get(\"value\")\n",
" or row.get(\"val\")\n",
" or row.get(\"rate\")\n",
" or row.get(\"close\")\n",
" or row.get(\"fx_rate\")\n",
" )\n",
" if date_value is None or rate is None:\n",
" continue\n",
" records.append({\"Date\": pd.to_datetime(date_value), \"Rate\": float(rate)})\n",
"\n",
" if not records:\n",
" raise ValueError(\"FXMacroData response did not include dated rate rows\")\n",
"\n",
" data = pd.DataFrame.from_records(records)\n",
" data = data.drop_duplicates(subset=\"Date\").sort_values(\"Date\").set_index(\"Date\")\n",
" data.index.name = None\n",
" data[\"Open\"] = data[\"High\"] = data[\"Low\"] = data[\"Close\"] = data[\"Rate\"]\n",
" data[\"Volume\"] = 0\n",
" return data[[\"Open\", \"High\", \"Low\", \"Close\", \"Volume\"]]\n",
"\n",
"\n",
"def fetch_fxmacrodata_ohlc(\n",
" pair: str,\n",
" start_date: str,\n",
" end_date: str,\n",
" *,\n",
" api_key: str | None = None,\n",
" base_url: str = FXMACRODATA_API_BASE_URL,\n",
" timeout: float = 30,\n",
") -> pd.DataFrame:\n",
" \"\"\"Fetch daily FX spot rates and return Backtesting.py-compatible OHLCV data.\"\"\"\n",
" base_currency, quote_currency = split_currency_pair(pair)\n",
" params = {\"start_date\": start_date, \"end_date\": end_date}\n",
" headers = {}\n",
" api_key = api_key or os.getenv(\"FXMACRODATA_API_KEY\") or os.getenv(\"FXMD_API_KEY\")\n",
" if api_key:\n",
" headers[\"X-API-Key\"] = api_key\n",
"\n",
" request = Request(\n",
" f\"{base_url.rstrip('/')}/forex/{base_currency}/{quote_currency}?{urlencode(params)}\",\n",
" headers=headers,\n",
" )\n",
" with urlopen(request, timeout=timeout) as response:\n",
" payload = json.load(response)\n",
"\n",
" rows = payload.get(\"data\") if isinstance(payload, dict) else payload\n",
" return fxmacrodata_rows_to_ohlc(rows)"
]
},
{
"cell_type": "markdown",
"id": "9eef35f5",
"metadata": {},
"source": [
"Fetch a pair in your own notebook, then pass the resulting data frame to\n",
"`Backtest`. The API key is optional for public data and can be supplied through\n",
"`FXMACRODATA_API_KEY` or `FXMD_API_KEY`.\n",
"\n",
"```python\n",
"data = fetch_fxmacrodata_ohlc(\"EURUSD\", \"2024-01-01\", \"2024-12-31\")\n",
"```\n",
"\n",
"To keep this documentation example deterministic, the cells below use a small\n",
"sample shaped like the FXMacroData response."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6a704477",
"metadata": {},
"outputs": [],
"source": [
"sample_rows = [\n",
" {\"date\": \"2024-01-01\", \"value\": 1.1038},\n",
" {\"date\": \"2024-01-02\", \"value\": 1.0943},\n",
" {\"date\": \"2024-01-03\", \"value\": 1.0920},\n",
" {\"date\": \"2024-01-04\", \"value\": 1.0950},\n",
" {\"date\": \"2024-01-05\", \"value\": 1.0944},\n",
" {\"date\": \"2024-01-08\", \"value\": 1.0951},\n",
" {\"date\": \"2024-01-09\", \"value\": 1.0932},\n",
" {\"date\": \"2024-01-10\", \"value\": 1.0970},\n",
" {\"date\": \"2024-01-11\", \"value\": 1.0998},\n",
" {\"date\": \"2024-01-12\", \"value\": 1.0950},\n",
" {\"date\": \"2024-01-15\", \"value\": 1.0946},\n",
" {\"date\": \"2024-01-16\", \"value\": 1.0875},\n",
" {\"date\": \"2024-01-17\", \"value\": 1.0848},\n",
" {\"date\": \"2024-01-18\", \"value\": 1.0873},\n",
" {\"date\": \"2024-01-19\", \"value\": 1.0896},\n",
" {\"date\": \"2024-01-22\", \"value\": 1.0887},\n",
" {\"date\": \"2024-01-23\", \"value\": 1.0854},\n",
" {\"date\": \"2024-01-24\", \"value\": 1.0885},\n",
" {\"date\": \"2024-01-25\", \"value\": 1.0841},\n",
" {\"date\": \"2024-01-26\", \"value\": 1.0853},\n",
" {\"date\": \"2024-01-29\", \"value\": 1.0832},\n",
" {\"date\": \"2024-01-30\", \"value\": 1.0847},\n",
" {\"date\": \"2024-01-31\", \"value\": 1.0819},\n",
" {\"date\": \"2024-02-01\", \"value\": 1.0872},\n",
" {\"date\": \"2024-02-02\", \"value\": 1.0788},\n",
" {\"date\": \"2024-02-05\", \"value\": 1.0743},\n",
" {\"date\": \"2024-02-06\", \"value\": 1.0757},\n",
" {\"date\": \"2024-02-07\", \"value\": 1.0772},\n",
" {\"date\": \"2024-02-08\", \"value\": 1.0778},\n",
" {\"date\": \"2024-02-09\", \"value\": 1.0782},\n",
" {\"date\": \"2024-02-12\", \"value\": 1.0771},\n",
" {\"date\": \"2024-02-13\", \"value\": 1.0708},\n",
" {\"date\": \"2024-02-14\", \"value\": 1.0729},\n",
" {\"date\": \"2024-02-15\", \"value\": 1.0771},\n",
" {\"date\": \"2024-02-16\", \"value\": 1.0778},\n",
" {\"date\": \"2024-02-19\", \"value\": 1.0780},\n",
" {\"date\": \"2024-02-20\", \"value\": 1.0805},\n",
" {\"date\": \"2024-02-21\", \"value\": 1.0822},\n",
" {\"date\": \"2024-02-22\", \"value\": 1.0823},\n",
" {\"date\": \"2024-02-23\", \"value\": 1.0821},\n",
" {\"date\": \"2024-02-26\", \"value\": 1.0851},\n",
" {\"date\": \"2024-02-27\", \"value\": 1.0844},\n",
" {\"date\": \"2024-02-28\", \"value\": 1.0838},\n",
" {\"date\": \"2024-02-29\", \"value\": 1.0808},\n",
" {\"date\": \"2024-03-01\", \"value\": 1.0838},\n",
"]\n",
"\n",
"data = fxmacrodata_rows_to_ohlc(sample_rows)\n",
"data.tail()"
]
},
{
"cell_type": "markdown",
"id": "9ef6690c",
"metadata": {},
"source": [
"The returned frame can be used with any single-asset _backtesting.py_ strategy."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f470642b",
"metadata": {},
"outputs": [],
"source": [
"from backtesting import Backtest, Strategy\n",
"from backtesting.lib import crossover\n",
"from backtesting.test import SMA\n",
"\n",
"\n",
"class SmaCross(Strategy):\n",
" fast = 10\n",
" slow = 30\n",
"\n",
" def init(self):\n",
" self.sma_fast = self.I(SMA, self.data.Close, self.fast)\n",
" self.sma_slow = self.I(SMA, self.data.Close, self.slow)\n",
"\n",
" def next(self):\n",
" if crossover(self.sma_fast, self.sma_slow):\n",
" self.position.close()\n",
" self.buy()\n",
" elif crossover(self.sma_slow, self.sma_fast):\n",
" self.position.close()\n",
" self.sell()\n",
"\n",
"\n",
"bt = Backtest(data, SmaCross, cash=10_000, commission=0.0002, finalize_trades=True)\n",
"stats = bt.run()\n",
"stats"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading