Files
OpenBB/openbb_platform/providers/cftc/tests/test_fx_models.py
Danglewood 869cfed125 [Release] openbb-v5.0.0 (#7741)
* merge branch develop with main

* bump desktop version

* fix the merge

* sync Cargo.lock to desktop 2.0.0

* fix cli conftest.py

---------

Co-authored-by: deeleeramone <>
2026-09-29 10:58:44 -07:00

250 lines
9.1 KiB
Python

import asyncio
from datetime import date, timedelta
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_cftc.models.fx_forward_points import (
CftcFxForwardPointsData,
CftcFxForwardPointsFetcher,
CftcFxForwardPointsQueryParams,
)
from openbb_cftc.models.fx_implied_vol import (
CftcFxImpliedVolData,
CftcFxImpliedVolFetcher,
CftcFxImpliedVolQueryParams,
)
from openbb_cftc.utils.fx_theory import garman_kohlhagen
AS_OF = date(2026, 7, 15)
SPOT = 1.10
def _forward(effective_days, quote_per_base):
return {
"UPI FISN": "NA/Fwd EUR USD",
"Action type": "NEWT",
"Effective Date": (AS_OF + timedelta(days=effective_days)).isoformat(),
"Notional amount-Leg 1": "10,000,000",
"Notional currency-Leg 1": "EUR",
"Notional amount-Leg 2": f"{10_000_000 * quote_per_base}",
"Notional currency-Leg 2": "USD",
}
def _option(strike, tenor_days, vol, is_call):
per_unit = garman_kohlhagen(
SPOT, strike, tenor_days / 365.0, 0.04, 0.02, vol, is_call
)
return {
"UPI FISN": f"NA/O Van {'Call' if is_call else 'Put'} EUR USD",
"Action type": "NEWT",
"Event type": "TRAD",
"Execution Timestamp": f"{AS_OF.isoformat()}T10:00:00Z",
"Strike Price": f"{strike}",
"Strike price currency/currency pair": "EUR/USD",
"Call currency": "EUR" if is_call else "USD",
"Call amount": f"{10_000_000 if is_call else 10_000_000 * strike}",
"Put currency": "USD" if is_call else "EUR",
"Put amount": f"{10_000_000 * strike if is_call else 10_000_000}",
"Option Premium Amount": f"{per_unit * 10_000_000}",
"Option Premium Currency": "USD",
"Expiration Date": (AS_OF + timedelta(days=tenor_days)).isoformat(),
"Notional amount-Leg 1": "10,000,000",
"Notional currency-Leg 1": "EUR",
"Notional amount-Leg 2": f"{10_000_000 * SPOT}",
"Notional currency-Leg 2": "USD",
}
def _forex_dataset():
records = [_forward(1, 1.10), _forward(2, 1.101)]
strikes = [1.03, 1.06, 1.09, 1.105, 1.12, 1.15, 1.18]
for tenor in (90, 182):
for strike in strikes:
records.append(_option(strike, tenor, 0.10, True))
records.append(_option(strike, tenor, 0.10, False))
return records
def test_fx_implied_vol_surface(slice_records):
query = CftcFxImpliedVolQueryParams(pair="EURUSD", min_trades=5)
result = CftcFxImpliedVolFetcher.transform_data(
query, (_forex_dataset(), slice_records, AS_OF.isoformat(), AS_OF.isoformat())
)
assert result.metadata["spot"] == pytest.approx(1.1005, abs=1e-3)
assert result.metadata["basis"] == "empirical"
assert all(isinstance(r, CftcFxImpliedVolData) for r in result.result)
assert all(1.0 < r.strike < 1.25 for r in result.result)
populated = [
v for r in result.result for v in (r.vol_3m, r.vol_6m) if v is not None
]
assert len(populated) > 2
assert all(v == pytest.approx(10.0, abs=1.0) for v in populated)
dumped = [r.model_dump(exclude_unset=True) for r in result.result]
assert all("vol_1w" not in d for d in dumped)
assert any("vol_3m" in d for d in dumped)
def test_fx_implied_vol_theoretical_basis(slice_records):
query = CftcFxImpliedVolQueryParams(
pair="EURUSD", basis="theoretical", min_trades=5
)
result = CftcFxImpliedVolFetcher.transform_data(
query, (_forex_dataset(), slice_records, AS_OF.isoformat(), AS_OF.isoformat())
)
assert result.metadata["basis"] == "theoretical"
assert result.metadata["sabr_fits"]
spot_row = next(r for r in result.result if r.strike_offset == 0.0)
assert spot_row.strike == pytest.approx(result.metadata["spot"], abs=1e-4)
assert spot_row.vol_3m == pytest.approx(10.0, abs=1.5)
strikes = [r.strike for r in result.result]
assert min(strikes) < result.metadata["spot"] < max(strikes)
assert spot_row.num_options > 0
def test_fx_implied_vol_theoretical_raises_when_unfittable(slice_records):
one_sided = [
o
for o in _forex_dataset()
if not o["UPI FISN"].startswith("NA/O") or float(o["Strike Price"]) < 1.10
]
with pytest.raises(OpenBBError, match="theoretical"):
CftcFxImpliedVolFetcher.transform_data(
CftcFxImpliedVolQueryParams(
pair="EURUSD", basis="theoretical", min_trades=5
),
(one_sided, slice_records, AS_OF.isoformat(), AS_OF.isoformat()),
)
def test_fx_implied_vol_raises_without_spot(slice_records):
options = [o for o in _forex_dataset() if o["UPI FISN"].startswith("NA/O")]
with pytest.raises(OpenBBError, match="spot"):
CftcFxImpliedVolFetcher.transform_data(
CftcFxImpliedVolQueryParams(pair="EURUSD"),
(options, slice_records, AS_OF.isoformat(), AS_OF.isoformat()),
)
def test_fx_implied_vol_raises_without_options(slice_records):
spot_only = [_forward(1, 1.10), _forward(2, 1.101)]
with pytest.raises(OpenBBError, match="volatility"):
CftcFxImpliedVolFetcher.transform_data(
CftcFxImpliedVolQueryParams(pair="EURUSD"),
(spot_only, slice_records, AS_OF.isoformat(), AS_OF.isoformat()),
)
def test_fx_implied_vol_query_accepts_lower_case():
assert CftcFxImpliedVolQueryParams(pair="usdjpy").pair == "USDJPY"
def test_fx_implied_vol_aextract(monkeypatch, slice_records):
async def _dates(asset_class):
return ["2026-07-13", "2026-07-14", "2026-07-15"]
async def _slice(asset_class, report_date, use_cache=True):
return _forex_dataset() if asset_class == "forex" else slice_records
monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates)
monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice)
_, rates, report_date, rates_date = asyncio.run(
CftcFxImpliedVolFetcher.aextract_data(
CftcFxImpliedVolQueryParams(pair="EURUSD"), None
)
)
assert report_date == "2026-07-15"
assert rates_date == "2026-07-15"
assert rates is slice_records
_, _, hist_date, hist_rates_date = asyncio.run(
CftcFxImpliedVolFetcher.aextract_data(
CftcFxImpliedVolQueryParams(pair="EURUSD", date=date(2026, 7, 14)), None
)
)
assert hist_date == "2026-07-14"
assert hist_rates_date == "2026-07-14"
def test_fx_implied_vol_walks_back_to_a_day_with_spot(monkeypatch, slice_records):
options_only = [
_option(strike, tenor, 0.10, is_call)
for tenor in (90, 182)
for strike in (1.06, 1.10, 1.14)
for is_call in (True, False)
]
async def _dates(asset_class):
return ["2026-07-15", "2026-07-18", "2026-07-19"]
async def _slice(asset_class, report_date, use_cache=True):
if asset_class == "rates":
return slice_records
if report_date in ("2026-07-18", "2026-07-19"):
return options_only
return _forex_dataset()
monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates)
monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice)
_, _, report_date, _ = asyncio.run(
CftcFxImpliedVolFetcher.aextract_data(
CftcFxImpliedVolQueryParams(pair="EURUSD"), None
)
)
assert report_date == "2026-07-15"
def test_fx_forward_points_theoretical(fx_records, slice_records):
query = CftcFxForwardPointsQueryParams(pair="EURUSD", source="slice")
result = CftcFxForwardPointsFetcher.transform_data(
query, (fx_records, slice_records, AS_OF.isoformat(), AS_OF.isoformat())
)
assert all(isinstance(r, CftcFxForwardPointsData) for r in result.result)
tenor = next(r for r in result.result if r.theoretical_points is not None)
assert tenor.quotation in ("premium", "discount")
assert tenor.basis_points == pytest.approx(
tenor.forward_points - tenor.theoretical_points, abs=1e-9
)
assert tenor.carry == ("earn" if tenor.forward_points < 0 else "pay")
assert (tenor.quotation == "premium") == (tenor.forward_points > 0)
def test_fx_forward_points_no_theoretical_without_two_ois_legs(
fx_records, slice_records
):
query = CftcFxForwardPointsQueryParams(pair="AUDUSD", source="slice")
result = CftcFxForwardPointsFetcher.transform_data(
query, (fx_records, slice_records, AS_OF.isoformat(), AS_OF.isoformat())
)
assert all(r.theoretical_points is None for r in result.result)
priced = [r for r in result.result if r.forward_points]
assert priced
assert all(r.quotation in ("premium", "discount") for r in priced)
assert all(r.carry in ("earn", "pay") for r in priced)
def test_fx_forward_points_tolerates_an_unbuildable_ois_curve(fx_records):
query = CftcFxForwardPointsQueryParams(pair="EURUSD", source="slice")
result = CftcFxForwardPointsFetcher.transform_data(
query, (fx_records, [], AS_OF.isoformat(), AS_OF.isoformat())
)
assert all(r.theoretical_points is None for r in result.result)
assert "theoretical_points" not in result.result[0].model_dump(exclude_unset=True)
priced = [r for r in result.result if r.forward_points]
assert priced and all(r.carry in ("earn", "pay") for r in priced)