Files
2026-07-13 13:26:28 +08:00

855 lines
37 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "0e6cf5e5",
"metadata": {
"papermill": {
"duration": 0.001987,
"end_time": "2026-06-13T02:33:42.350184+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:42.348197+00:00",
"status": "completed"
}
},
"source": [
"# FX Pairs — Exploratory Data Analysis\n",
"\n",
"**Docker image**: `ml4t`\n",
"\n",
"## Purpose\n",
"Profile the OANDA 20-pair, 4-hour FX dataset that anchors the FX case study.\n",
"FX is OTC: there is no central tape, so quotes and reported volumes are\n",
"venue-specific. The notebook surveys coverage, quote conventions, OHLC\n",
"integrity, and the 4h→daily aggregation used by downstream chapters.\n",
"\n",
"## Learning Objectives\n",
"- Load and inspect the 4-hour OHLC + indicative-volume panel for 20 pairs.\n",
"- Distinguish direct (USD-quoted), indirect (USD-base), and cross pairs.\n",
"- Read FX volume as an OANDA indicator, not an authoritative tape.\n",
"- Aggregate 4h bars to UTC-day daily bars and read the gap-pattern signal.\n",
"\n",
"## Book reference\n",
"Chapter 2, §2.2 (asset-class market data — foreign exchange). The FX case\n",
"study built on this dataset lives in `case_studies/fx_pairs/`.\n",
"\n",
"## Prerequisites\n",
"- OANDA 4h FX parquet files materialized under `ML4T_DATA_PATH`.\n",
"- Loader `data.load_fx_pairs`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "75a4c694",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:42.353826Z",
"iopub.status.busy": "2026-06-13T02:33:42.353718Z",
"iopub.status.idle": "2026-06-13T02:33:43.581552Z",
"shell.execute_reply": "2026-06-13T02:33:43.581002Z"
},
"papermill": {
"duration": 1.230464,
"end_time": "2026-06-13T02:33:43.582246+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:42.351782+00:00",
"status": "completed"
}
},
"outputs": [],
"source": [
"\"\"\"FX Pairs — Exploratory data analysis of OANDA currency pair data.\"\"\"\n",
"\n",
"import polars as pl\n",
"\n",
"from data import load_fx_pairs\n",
"from utils.data_quality import check_ohlc_invariants, per_asset_stats"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "26b9a4ca",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.586308Z",
"iopub.status.busy": "2026-06-13T02:33:43.586220Z",
"iopub.status.idle": "2026-06-13T02:33:43.588185Z",
"shell.execute_reply": "2026-06-13T02:33:43.587686Z"
},
"papermill": {
"duration": 0.004652,
"end_time": "2026-06-13T02:33:43.588663+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.584011+00:00",
"status": "completed"
},
"tags": [
"parameters"
]
},
"outputs": [],
"source": [
"# Production defaults — Papermill injects overrides for CI\n",
"# (No tunable knobs: this notebook EDAs the full 12-pair universe via the\n",
"# canonical load_fx_pairs() API; there is no MAX_SYMBOLS / START_DATE knob to expose.)"
]
},
{
"cell_type": "markdown",
"id": "d3ace9f3",
"metadata": {
"papermill": {
"duration": 0.000828,
"end_time": "2026-06-13T02:33:43.590412+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.589584+00:00",
"status": "completed"
}
},
"source": [
"## 1. Load and Inspect"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e092607c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.592603Z",
"iopub.status.busy": "2026-06-13T02:33:43.592534Z",
"iopub.status.idle": "2026-06-13T02:33:43.645036Z",
"shell.execute_reply": "2026-06-13T02:33:43.644439Z"
},
"papermill": {
"duration": 0.054124,
"end_time": "2026-06-13T02:33:43.645352+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.591228+00:00",
"status": "completed"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== FX Dataset ===\n",
"Shape: (478640, 7)\n",
"Columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume']\n",
"Date range: 2011-01-02 14:00:00 to 2025-12-31 18:00:00\n"
]
}
],
"source": [
"fx_4h = load_fx_pairs(frequency=\"4h\")\n",
"\n",
"print(\"=== FX Dataset ===\")\n",
"print(f\"Shape: {fx_4h.shape}\")\n",
"print(f\"Columns: {fx_4h.columns}\")\n",
"print(f\"Date range: {fx_4h['timestamp'].min()} to {fx_4h['timestamp'].max()}\")"
]
},
{
"cell_type": "markdown",
"id": "226404df",
"metadata": {
"papermill": {
"duration": 0.000883,
"end_time": "2026-06-13T02:33:43.647282+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.646399+00:00",
"status": "completed"
}
},
"source": [
"### Volume Disclaimer\n",
"\n",
"**Important**: FX is an OTC market. Volume figures are indicative estimates from OANDA,\n",
"not authoritative exchange data. Do not interpret FX volume the same way as equity volume."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "582f10dd",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.649708Z",
"iopub.status.busy": "2026-06-13T02:33:43.649581Z",
"iopub.status.idle": "2026-06-13T02:33:43.653412Z",
"shell.execute_reply": "2026-06-13T02:33:43.652994Z"
},
"papermill": {
"duration": 0.00548,
"end_time": "2026-06-13T02:33:43.653645+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.648165+00:00",
"status": "completed"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div><style>\n",
".dataframe > thead > tr,\n",
".dataframe > tbody > tr {\n",
" text-align: right;\n",
" white-space: pre-wrap;\n",
"}\n",
"</style>\n",
"<small>shape: (5, 7)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>timestamp</th><th>symbol</th><th>open</th><th>high</th><th>low</th><th>close</th><th>volume</th></tr><tr><td>datetime[ms]</td><td>str</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>i64</td></tr></thead><tbody><tr><td>2011-01-02 18:00:00</td><td>&quot;AUD_JPY&quot;</td><td>83.0</td><td>83.332</td><td>82.8</td><td>82.875</td><td>986</td></tr><tr><td>2011-01-02 22:00:00</td><td>&quot;AUD_JPY&quot;</td><td>82.877</td><td>83.015</td><td>82.698</td><td>82.796</td><td>4378</td></tr><tr><td>2011-01-03 02:00:00</td><td>&quot;AUD_JPY&quot;</td><td>82.8</td><td>82.98</td><td>82.782</td><td>82.924</td><td>1934</td></tr><tr><td>2011-01-03 06:00:00</td><td>&quot;AUD_JPY&quot;</td><td>82.926</td><td>83.03</td><td>82.826</td><td>83.03</td><td>3949</td></tr><tr><td>2011-01-03 10:00:00</td><td>&quot;AUD_JPY&quot;</td><td>83.032</td><td>83.158</td><td>82.986</td><td>83.01</td><td>5296</td></tr></tbody></table></div>"
],
"text/plain": [
"shape: (5, 7)\n",
"┌─────────────────────┬─────────┬────────┬────────┬────────┬────────┬────────┐\n",
"│ timestamp ┆ symbol ┆ open ┆ high ┆ low ┆ close ┆ volume │\n",
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
"│ datetime[ms] ┆ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 │\n",
"╞═════════════════════╪═════════╪════════╪════════╪════════╪════════╪════════╡\n",
"│ 2011-01-02 18:00:00 ┆ AUD_JPY ┆ 83.0 ┆ 83.332 ┆ 82.8 ┆ 82.875 ┆ 986 │\n",
"│ 2011-01-02 22:00:00 ┆ AUD_JPY ┆ 82.877 ┆ 83.015 ┆ 82.698 ┆ 82.796 ┆ 4378 │\n",
"│ 2011-01-03 02:00:00 ┆ AUD_JPY ┆ 82.8 ┆ 82.98 ┆ 82.782 ┆ 82.924 ┆ 1934 │\n",
"│ 2011-01-03 06:00:00 ┆ AUD_JPY ┆ 82.926 ┆ 83.03 ┆ 82.826 ┆ 83.03 ┆ 3949 │\n",
"│ 2011-01-03 10:00:00 ┆ AUD_JPY ┆ 83.032 ┆ 83.158 ┆ 82.986 ┆ 83.01 ┆ 5296 │\n",
"└─────────────────────┴─────────┴────────┴────────┴────────┴────────┴────────┘"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fx_4h.head()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "97391cf2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.656035Z",
"iopub.status.busy": "2026-06-13T02:33:43.655974Z",
"iopub.status.idle": "2026-06-13T02:33:43.661039Z",
"shell.execute_reply": "2026-06-13T02:33:43.660623Z"
},
"papermill": {
"duration": 0.006656,
"end_time": "2026-06-13T02:33:43.661267+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.654611+00:00",
"status": "completed"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Currency pairs (20):\n",
" AUD_JPY\n",
" AUD_NZD\n",
" AUD_USD\n",
" CAD_JPY\n",
" CHF_JPY\n",
" EUR_AUD\n",
" EUR_CAD\n",
" EUR_CHF\n",
" EUR_GBP\n",
" EUR_JPY\n",
" EUR_USD\n",
" GBP_AUD\n",
" GBP_CHF\n",
" GBP_JPY\n",
" GBP_USD\n",
" NZD_JPY\n",
" NZD_USD\n",
" USD_CAD\n",
" USD_CHF\n",
" USD_JPY\n"
]
}
],
"source": [
"# Available pairs\n",
"pairs = fx_4h[\"symbol\"].unique().sort().to_list()\n",
"print(f\"\\nCurrency pairs ({len(pairs)}):\")\n",
"for pair in pairs:\n",
" print(f\" {pair}\")"
]
},
{
"cell_type": "markdown",
"id": "95116a76",
"metadata": {
"papermill": {
"duration": 0.000895,
"end_time": "2026-06-13T02:33:43.663133+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.662238+00:00",
"status": "completed"
}
},
"source": [
"### Symbol Normalization\n",
"\n",
"The data uses underscore format (`EUR_USD`). The canonical format for this dataset is\n",
"concatenated (`EURUSD`). Here's how to convert:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "aea13dc3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.665439Z",
"iopub.status.busy": "2026-06-13T02:33:43.665377Z",
"iopub.status.idle": "2026-06-13T02:33:43.677943Z",
"shell.execute_reply": "2026-06-13T02:33:43.677539Z"
},
"papermill": {
"duration": 0.014182,
"end_time": "2026-06-13T02:33:43.678196+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.664014+00:00",
"status": "completed"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Symbol normalization example:\n",
" Raw format: EUR_USD, USD_JPY, GBP_USD\n",
" Canonical: EURUSD, USDJPY, GBPUSD\n",
"\n",
"Normalized pairs: ['AUDJPY', 'AUDNZD', 'AUDUSD', 'CADJPY', 'CHFJPY', 'EURAUD'] ...\n"
]
}
],
"source": [
"# Normalize symbols: EUR_USD → EURUSD (canonical format)\n",
"# The raw file uses underscores; we normalize to concatenated format for downstream joins\n",
"fx = fx_4h.with_columns(pl.col(\"symbol\").str.replace(\"_\", \"\").alias(\"symbol\"))\n",
"\n",
"print(\"Symbol normalization example:\")\n",
"print(\" Raw format: EUR_USD, USD_JPY, GBP_USD\")\n",
"print(\" Canonical: EURUSD, USDJPY, GBPUSD\")\n",
"print(f\"\\nNormalized pairs: {fx['symbol'].unique().sort().to_list()[:6]} ...\")"
]
},
{
"cell_type": "markdown",
"id": "a1437f5f",
"metadata": {
"papermill": {
"duration": 0.000954,
"end_time": "2026-06-13T02:33:43.680241+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.679287+00:00",
"status": "completed"
}
},
"source": [
"## 2. Coverage Summary"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "bb1c9769",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.682634Z",
"iopub.status.busy": "2026-06-13T02:33:43.682558Z",
"iopub.status.idle": "2026-06-13T02:33:43.693125Z",
"shell.execute_reply": "2026-06-13T02:33:43.692663Z"
},
"papermill": {
"duration": 0.012245,
"end_time": "2026-06-13T02:33:43.693424+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.681179+00:00",
"status": "completed"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div><style>\n",
".dataframe > thead > tr,\n",
".dataframe > tbody > tr {\n",
" text-align: right;\n",
" white-space: pre-wrap;\n",
"}\n",
"</style>\n",
"<small>shape: (20, 6)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>rows</th><th>start</th><th>end</th><th>avg_price</th><th>avg_volume</th></tr><tr><td>str</td><td>u32</td><td>datetime[ms]</td><td>datetime[ms]</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>&quot;GBPAUD&quot;</td><td>23945</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>1.795271</td><td>27719.096262</td></tr><tr><td>&quot;GBPJPY&quot;</td><td>23924</td><td>2011-01-02 18:00:00</td><td>2025-12-31 18:00:00</td><td>156.656959</td><td>22954.817673</td></tr><tr><td>&quot;EURCAD&quot;</td><td>23921</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>1.44887</td><td>21140.860081</td></tr><tr><td>&quot;GBPCHF&quot;</td><td>23924</td><td>2011-01-02 18:00:00</td><td>2025-12-31 18:00:00</td><td>1.302496</td><td>18534.499415</td></tr><tr><td>&quot;CHFJPY&quot;</td><td>23919</td><td>2011-01-02 18:00:00</td><td>2025-12-31 18:00:00</td><td>122.492732</td><td>18296.437686</td></tr><tr><td>&hellip;</td><td>&hellip;</td><td>&hellip;</td><td>&hellip;</td><td>&hellip;</td><td>&hellip;</td></tr><tr><td>&quot;NZDUSD&quot;</td><td>23943</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>0.702921</td><td>7312.247797</td></tr><tr><td>&quot;EURCHF&quot;</td><td>23925</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>1.099216</td><td>6110.44698</td></tr><tr><td>&quot;EURGBP&quot;</td><td>23922</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>0.846666</td><td>5989.497952</td></tr><tr><td>&quot;AUDUSD&quot;</td><td>23941</td><td>2011-01-02 18:00:00</td><td>2025-12-31 18:00:00</td><td>0.789923</td><td>5563.880832</td></tr><tr><td>&quot;USDCHF&quot;</td><td>23927</td><td>2011-01-02 14:00:00</td><td>2025-12-31 18:00:00</td><td>0.93156</td><td>5375.534835</td></tr></tbody></table></div>"
],
"text/plain": [
"shape: (20, 6)\n",
"┌────────┬───────┬─────────────────────┬─────────────────────┬────────────┬──────────────┐\n",
"│ symbol ┆ rows ┆ start ┆ end ┆ avg_price ┆ avg_volume │\n",
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
"│ str ┆ u32 ┆ datetime[ms] ┆ datetime[ms] ┆ f64 ┆ f64 │\n",
"╞════════╪═══════╪═════════════════════╪═════════════════════╪════════════╪══════════════╡\n",
"│ GBPAUD ┆ 23945 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 1.795271 ┆ 27719.096262 │\n",
"│ GBPJPY ┆ 23924 ┆ 2011-01-02 18:00:00 ┆ 2025-12-31 18:00:00 ┆ 156.656959 ┆ 22954.817673 │\n",
"│ EURCAD ┆ 23921 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 1.44887 ┆ 21140.860081 │\n",
"│ GBPCHF ┆ 23924 ┆ 2011-01-02 18:00:00 ┆ 2025-12-31 18:00:00 ┆ 1.302496 ┆ 18534.499415 │\n",
"│ CHFJPY ┆ 23919 ┆ 2011-01-02 18:00:00 ┆ 2025-12-31 18:00:00 ┆ 122.492732 ┆ 18296.437686 │\n",
"│ … ┆ … ┆ … ┆ … ┆ … ┆ … │\n",
"│ NZDUSD ┆ 23943 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 0.702921 ┆ 7312.247797 │\n",
"│ EURCHF ┆ 23925 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 1.099216 ┆ 6110.44698 │\n",
"│ EURGBP ┆ 23922 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 0.846666 ┆ 5989.497952 │\n",
"│ AUDUSD ┆ 23941 ┆ 2011-01-02 18:00:00 ┆ 2025-12-31 18:00:00 ┆ 0.789923 ┆ 5563.880832 │\n",
"│ USDCHF ┆ 23927 ┆ 2011-01-02 14:00:00 ┆ 2025-12-31 18:00:00 ┆ 0.93156 ┆ 5375.534835 │\n",
"└────────┴───────┴─────────────────────┴─────────────────────┴────────────┴──────────────┘"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Per-pair statistics (using normalized symbols)\n",
"pairs = fx[\"symbol\"].unique().sort().to_list()\n",
"\n",
"pair_stats = per_asset_stats(\n",
" fx,\n",
" time_col=\"timestamp\",\n",
" asset_col=\"symbol\",\n",
" price_col=\"close\",\n",
" volume_col=\"volume\",\n",
")\n",
"\n",
"pair_stats.sort(\"avg_volume\", descending=True)"
]
},
{
"cell_type": "markdown",
"id": "736a507b",
"metadata": {
"papermill": {
"duration": 0.001009,
"end_time": "2026-06-13T02:33:43.695620+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.694611+00:00",
"status": "completed"
}
},
"source": [
"## 3. Quote Conventions\n",
"\n",
"FX pairs follow **BASE/QUOTE** convention:\n",
"\n",
"| Pair | Interpretation | USD Strength |\n",
"|------|----------------|--------------|\n",
"| EUR/USD = 1.10 | 1 EUR costs 1.10 USD | Down = USD stronger |\n",
"| USD/JPY = 150 | 1 USD costs 150 JPY | Up = USD stronger |\n",
"| EUR/GBP = 0.86 | 1 EUR costs 0.86 GBP | Cross rate (no USD) |\n",
"\n",
"To create a consistent \"USD index\", you must **invert** EUR/USD and GBP/USD."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "1248d371",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.698207Z",
"iopub.status.busy": "2026-06-13T02:33:43.698121Z",
"iopub.status.idle": "2026-06-13T02:33:43.701090Z",
"shell.execute_reply": "2026-06-13T02:33:43.700792Z"
},
"papermill": {
"duration": 0.004731,
"end_time": "2026-06-13T02:33:43.701341+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.696610+00:00",
"status": "completed"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div><style>\n",
".dataframe > thead > tr,\n",
".dataframe > tbody > tr {\n",
" text-align: right;\n",
" white-space: pre-wrap;\n",
"}\n",
"</style>\n",
"<small>shape: (9, 4)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>convention</th><th>meaning</th><th>usd_strength</th></tr><tr><td>str</td><td>str</td><td>str</td><td>str</td></tr></thead><tbody><tr><td>&quot;EURUSD&quot;</td><td>&quot;Direct&quot;</td><td>&quot;USD per EUR&quot;</td><td>&quot;invert for USD strength&quot;</td></tr><tr><td>&quot;GBPUSD&quot;</td><td>&quot;Direct&quot;</td><td>&quot;USD per GBP&quot;</td><td>&quot;invert for USD strength&quot;</td></tr><tr><td>&quot;AUDUSD&quot;</td><td>&quot;Direct&quot;</td><td>&quot;USD per AUD&quot;</td><td>&quot;invert for USD strength&quot;</td></tr><tr><td>&quot;NZDUSD&quot;</td><td>&quot;Direct&quot;</td><td>&quot;USD per NZD&quot;</td><td>&quot;invert for USD strength&quot;</td></tr><tr><td>&quot;USDJPY&quot;</td><td>&quot;Indirect&quot;</td><td>&quot;JPY per USD&quot;</td><td>&quot;direct USD strength&quot;</td></tr><tr><td>&quot;USDCHF&quot;</td><td>&quot;Indirect&quot;</td><td>&quot;CHF per USD&quot;</td><td>&quot;direct USD strength&quot;</td></tr><tr><td>&quot;USDCAD&quot;</td><td>&quot;Indirect&quot;</td><td>&quot;CAD per USD&quot;</td><td>&quot;direct USD strength&quot;</td></tr><tr><td>&quot;EURGBP&quot;</td><td>&quot;Cross&quot;</td><td>&quot;GBP per EUR&quot;</td><td>&quot;no USD&quot;</td></tr><tr><td>&quot;EURJPY&quot;</td><td>&quot;Cross&quot;</td><td>&quot;JPY per EUR&quot;</td><td>&quot;no USD&quot;</td></tr></tbody></table></div>"
],
"text/plain": [
"shape: (9, 4)\n",
"┌────────┬────────────┬─────────────┬─────────────────────────┐\n",
"│ symbol ┆ convention ┆ meaning ┆ usd_strength │\n",
"│ --- ┆ --- ┆ --- ┆ --- │\n",
"│ str ┆ str ┆ str ┆ str │\n",
"╞════════╪════════════╪═════════════╪═════════════════════════╡\n",
"│ EURUSD ┆ Direct ┆ USD per EUR ┆ invert for USD strength │\n",
"│ GBPUSD ┆ Direct ┆ USD per GBP ┆ invert for USD strength │\n",
"│ AUDUSD ┆ Direct ┆ USD per AUD ┆ invert for USD strength │\n",
"│ NZDUSD ┆ Direct ┆ USD per NZD ┆ invert for USD strength │\n",
"│ USDJPY ┆ Indirect ┆ JPY per USD ┆ direct USD strength │\n",
"│ USDCHF ┆ Indirect ┆ CHF per USD ┆ direct USD strength │\n",
"│ USDCAD ┆ Indirect ┆ CAD per USD ┆ direct USD strength │\n",
"│ EURGBP ┆ Cross ┆ GBP per EUR ┆ no USD │\n",
"│ EURJPY ┆ Cross ┆ JPY per EUR ┆ no USD │\n",
"└────────┴────────────┴─────────────┴─────────────────────────┘"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Quote convention classification (using canonical symbols).\n",
"QUOTE_CONVENTIONS = {\n",
" \"EURUSD\": (\"Direct\", \"USD per EUR\", \"invert for USD strength\"),\n",
" \"GBPUSD\": (\"Direct\", \"USD per GBP\", \"invert for USD strength\"),\n",
" \"AUDUSD\": (\"Direct\", \"USD per AUD\", \"invert for USD strength\"),\n",
" \"NZDUSD\": (\"Direct\", \"USD per NZD\", \"invert for USD strength\"),\n",
" \"USDJPY\": (\"Indirect\", \"JPY per USD\", \"direct USD strength\"),\n",
" \"USDCHF\": (\"Indirect\", \"CHF per USD\", \"direct USD strength\"),\n",
" \"USDCAD\": (\"Indirect\", \"CAD per USD\", \"direct USD strength\"),\n",
" \"EURGBP\": (\"Cross\", \"GBP per EUR\", \"no USD\"),\n",
" \"EURJPY\": (\"Cross\", \"JPY per EUR\", \"no USD\"),\n",
"}\n",
"\n",
"quote_conventions = pl.DataFrame(\n",
" [\n",
" {\"symbol\": p, \"convention\": c, \"meaning\": m, \"usd_strength\": n}\n",
" for p, (c, m, n) in QUOTE_CONVENTIONS.items()\n",
" if p in pairs\n",
" ]\n",
")\n",
"quote_conventions"
]
},
{
"cell_type": "markdown",
"id": "f6e41bbb",
"metadata": {
"papermill": {
"duration": 0.0011,
"end_time": "2026-06-13T02:33:43.703547+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.702447+00:00",
"status": "completed"
}
},
"source": [
"## 4. Data Quality"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "4476f44a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.706165Z",
"iopub.status.busy": "2026-06-13T02:33:43.706091Z",
"iopub.status.idle": "2026-06-13T02:33:43.713408Z",
"shell.execute_reply": "2026-06-13T02:33:43.712933Z"
},
"papermill": {
"duration": 0.009113,
"end_time": "2026-06-13T02:33:43.713719+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.704606+00:00",
"status": "completed"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div><style>\n",
".dataframe > thead > tr,\n",
".dataframe > tbody > tr {\n",
" text-align: right;\n",
" white-space: pre-wrap;\n",
"}\n",
"</style>\n",
"<small>shape: (6, 4)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>check</th><th>valid_pct</th><th>applicable_rows</th><th>total_rows</th></tr><tr><td>str</td><td>f64</td><td>i64</td><td>i64</td></tr></thead><tbody><tr><td>&quot;high_gte_low&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr><tr><td>&quot;high_gte_open&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr><tr><td>&quot;high_gte_close&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr><tr><td>&quot;low_lte_open&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr><tr><td>&quot;low_lte_close&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr><tr><td>&quot;volume_non_negative&quot;</td><td>100.0</td><td>478640</td><td>478640</td></tr></tbody></table></div>"
],
"text/plain": [
"shape: (6, 4)\n",
"┌─────────────────────┬───────────┬─────────────────┬────────────┐\n",
"│ check ┆ valid_pct ┆ applicable_rows ┆ total_rows │\n",
"│ --- ┆ --- ┆ --- ┆ --- │\n",
"│ str ┆ f64 ┆ i64 ┆ i64 │\n",
"╞═════════════════════╪═══════════╪═════════════════╪════════════╡\n",
"│ high_gte_low ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"│ high_gte_open ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"│ high_gte_close ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"│ low_lte_open ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"│ low_lte_close ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"│ volume_non_negative ┆ 100.0 ┆ 478640 ┆ 478640 │\n",
"└─────────────────────┴───────────┴─────────────────┴────────────┘"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# OHLC invariants\n",
"invariants = check_ohlc_invariants(fx)\n",
"invariants"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "9c27cc3c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.717828Z",
"iopub.status.busy": "2026-06-13T02:33:43.717708Z",
"iopub.status.idle": "2026-06-13T02:33:43.724040Z",
"shell.execute_reply": "2026-06-13T02:33:43.723475Z"
},
"papermill": {
"duration": 0.009097,
"end_time": "2026-06-13T02:33:43.724412+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.715315+00:00",
"status": "completed"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Gaps > 24 hours (EURUSD): 747 (should be weekends only)\n"
]
}
],
"source": [
"# Check for weekend gaps (expected in FX, which trades 24/5)\n",
"eurusd = fx.filter(pl.col(\"symbol\") == \"EURUSD\").sort(\"timestamp\")\n",
"\n",
"eurusd_gaps = eurusd.with_columns(\n",
" pl.col(\"timestamp\").diff().dt.total_hours().alias(\"hours_since_prev\")\n",
")\n",
"\n",
"large_gaps = eurusd_gaps.filter(pl.col(\"hours_since_prev\") > 24)\n",
"print(f\"\\nGaps > 24 hours (EURUSD): {len(large_gaps)} (should be weekends only)\")"
]
},
{
"cell_type": "markdown",
"id": "b6e1c1bb",
"metadata": {
"papermill": {
"duration": 0.001464,
"end_time": "2026-06-13T02:33:43.727484+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.726020+00:00",
"status": "completed"
}
},
"source": [
"## 5. Daily Aggregation\n",
"\n",
"Aggregate 4-hour bars to daily for consistency with other datasets.\n",
"\n",
"**Note**: This uses UTC midnight boundaries. FX daily bars are conventionally defined\n",
"by a session cutoff (often 5pm New York). For production, align to your broker's\n",
"convention. This simple calendar-day aggregation is sufficient for exploration."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "1c719ab9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-13T02:33:43.730960Z",
"iopub.status.busy": "2026-06-13T02:33:43.730762Z",
"iopub.status.idle": "2026-06-13T02:33:43.773847Z",
"shell.execute_reply": "2026-06-13T02:33:43.773341Z"
},
"papermill": {
"duration": 0.045315,
"end_time": "2026-06-13T02:33:43.774176+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.728861+00:00",
"status": "completed"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Daily aggregation (UTC boundaries) — shape: (94642, 7)\n"
]
},
{
"data": {
"text/html": [
"<div><style>\n",
".dataframe > thead > tr,\n",
".dataframe > tbody > tr {\n",
" text-align: right;\n",
" white-space: pre-wrap;\n",
"}\n",
"</style>\n",
"<small>shape: (5, 7)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>timestamp</th><th>open</th><th>high</th><th>low</th><th>close</th><th>volume</th></tr><tr><td>str</td><td>datetime[ms]</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>i64</td></tr></thead><tbody><tr><td>&quot;EURUSD&quot;</td><td>2025-12-26 00:00:00</td><td>1.17873</td><td>1.17968</td><td>1.17616</td><td>1.1771</td><td>99014</td></tr><tr><td>&quot;EURUSD&quot;</td><td>2025-12-28 00:00:00</td><td>1.1768</td><td>1.17864</td><td>1.17654</td><td>1.17842</td><td>10811</td></tr><tr><td>&quot;EURUSD&quot;</td><td>2025-12-29 00:00:00</td><td>1.1784</td><td>1.17891</td><td>1.17494</td><td>1.17751</td><td>110397</td></tr><tr><td>&quot;EURUSD&quot;</td><td>2025-12-30 00:00:00</td><td>1.1775</td><td>1.17798</td><td>1.17419</td><td>1.17426</td><td>83060</td></tr><tr><td>&quot;EURUSD&quot;</td><td>2025-12-31 00:00:00</td><td>1.17424</td><td>1.17595</td><td>1.172</td><td>1.17459</td><td>92505</td></tr></tbody></table></div>"
],
"text/plain": [
"shape: (5, 7)\n",
"┌────────┬─────────────────────┬─────────┬─────────┬─────────┬─────────┬────────┐\n",
"│ symbol ┆ timestamp ┆ open ┆ high ┆ low ┆ close ┆ volume │\n",
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
"│ str ┆ datetime[ms] ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 │\n",
"╞════════╪═════════════════════╪═════════╪═════════╪═════════╪═════════╪════════╡\n",
"│ EURUSD ┆ 2025-12-26 00:00:00 ┆ 1.17873 ┆ 1.17968 ┆ 1.17616 ┆ 1.1771 ┆ 99014 │\n",
"│ EURUSD ┆ 2025-12-28 00:00:00 ┆ 1.1768 ┆ 1.17864 ┆ 1.17654 ┆ 1.17842 ┆ 10811 │\n",
"│ EURUSD ┆ 2025-12-29 00:00:00 ┆ 1.1784 ┆ 1.17891 ┆ 1.17494 ┆ 1.17751 ┆ 110397 │\n",
"│ EURUSD ┆ 2025-12-30 00:00:00 ┆ 1.1775 ┆ 1.17798 ┆ 1.17419 ┆ 1.17426 ┆ 83060 │\n",
"│ EURUSD ┆ 2025-12-31 00:00:00 ┆ 1.17424 ┆ 1.17595 ┆ 1.172 ┆ 1.17459 ┆ 92505 │\n",
"└────────┴─────────────────────┴─────────┴─────────┴─────────┴─────────┴────────┘"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Daily aggregation (must be sorted for group_by_dynamic)\n",
"fx_daily = (\n",
" fx.sort(\"symbol\", \"timestamp\")\n",
" .group_by_dynamic(\"timestamp\", every=\"1d\", group_by=\"symbol\")\n",
" .agg(\n",
" [\n",
" pl.col(\"open\").first(),\n",
" pl.col(\"high\").max(),\n",
" pl.col(\"low\").min(),\n",
" pl.col(\"close\").last(),\n",
" pl.col(\"volume\").sum(),\n",
" ]\n",
" )\n",
")\n",
"\n",
"print(f\"Daily aggregation (UTC boundaries) — shape: {fx_daily.shape}\")\n",
"fx_daily.filter(pl.col(\"symbol\") == \"EURUSD\").tail(5)"
]
},
{
"cell_type": "markdown",
"id": "28013e00",
"metadata": {
"papermill": {
"duration": 0.001196,
"end_time": "2026-06-13T02:33:43.776700+00:00",
"exception": false,
"start_time": "2026-06-13T02:33:43.775504+00:00",
"status": "completed"
}
},
"source": [
"## Key Takeaways\n",
"\n",
"Profile of the OANDA 4h FX panel that anchors the FX case study.\n",
"\n",
"### Quantitative Findings\n",
"- **Panel scale**: 478,640 4h observations across 20 currency pairs spanning\n",
" 2011-01-02 → 2025-12-31. Each pair has ~23,92023,945 4h bars.\n",
"- **Liquidity tiers (by indicative OANDA volume)**: GBPAUD, GBPJPY, EURCAD,\n",
" GBPCHF and CHFJPY top the table at 18k28k contracts/4h; the bottom of\n",
" the universe (NZDUSD, EURCHF, EURGBP, AUDUSD, USDCHF) sits at 5k7k.\n",
" These rankings are *OANDA-specific* — interbank-market liquidity for\n",
" EURUSD/USDJPY is the largest globally but is not visible to a single\n",
" retail venue.\n",
"- **OHLC integrity**: 100% of 4h bars satisfy all six invariants\n",
" (high ≥ low/open/close, low ≤ open/close, volume ≥ 0).\n",
"- **Session gaps**: 747 EURUSD inter-bar gaps exceed 24 h, matching the\n",
" ~770 weekend closes over 14 years — confirming the 24/5 calendar.\n",
"- **Daily roll-up**: UTC-boundary aggregation produces 94,642 daily rows\n",
" across the panel (≈4,732 trading days × 20 pairs).\n",
"\n",
"### Implications for Practitioners\n",
"- **Volume**: Treat as a relative liquidity indicator across pairs on this\n",
" venue, not as an interbank tape.\n",
"- **Quote inversion**: USD strength composites must invert direct pairs\n",
" (EUR/USD, GBP/USD, AUD/USD, NZD/USD); USD-base and cross pairs do not\n",
" need inversion.\n",
"- **Daily session convention**: UTC-day aggregation is convenient for joins\n",
" with the equity/crypto panels but is *not* a tradable session boundary;\n",
" broker-specific 5pm-NY cutoffs are wired in `case_studies/fx_pairs/`\n",
" downstream.\n",
"\n",
"**Next**: `13_data_quality_framework` profiles the cross-asset DQ checks\n",
"that consume this panel and the others built up so far."
]
}
],
"metadata": {
"jupytext": {
"cell_metadata_filter": "tags,-all"
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.3"
},
"papermill": {
"default_parameters": {},
"duration": 2.888138,
"end_time": "2026-06-13T02:33:44.593918+00:00",
"environment_variables": {},
"exception": null,
"input_path": "02_financial_data_universe/12_fx_pairs_eda.ipynb",
"output_path": "02_financial_data_universe/12_fx_pairs_eda.ipynb",
"parameters": {},
"start_time": "2026-06-13T02:33:41.705780+00:00",
"version": "2.7.0"
},
"ml4t_provenance": {
"source_py_blob": "8f3afbd31633079f8ac78d86f3bc5a9cf4b326ad",
"executed_at": "2026-06-13T02:33:44.819485+00:00",
"executor": "cpu-local",
"production": true,
"parameters": {},
"notes": "executed-state finalization batch (2026-06-13 run)"
}
},
"nbformat": 4,
"nbformat_minor": 5
}