import asyncio from datetime import date, timedelta import pytest from openbb_core.app.model.abstract.error import OpenBBError from openbb_core.provider.utils.errors import EmptyDataError from openbb_cftc.models.cds_index_trades import ( CftcCdsIndexTradesFetcher, CftcCdsIndexTradesQueryParams, ) from openbb_cftc.models.cot import CftcCotFetcher, CftcCotQueryParams from openbb_cftc.models.cot_search import CftcCotSearchFetcher, CftcCotSearchQueryParams 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.models.ois_curve import ( CftcOisCurveData, CftcOisCurveFetcher, CftcOisCurveQueryParams, ) from openbb_cftc.models.ois_curve_history import ( CftcOisCurveHistoryFetcher, CftcOisCurveHistoryQueryParams, ) from openbb_cftc.models.ois_forward_curve import ( CftcOisForwardCurveData, CftcOisForwardCurveFetcher, CftcOisForwardCurveQueryParams, ) from openbb_cftc.models.swap_trades import ( CftcSwapTradesData, CftcSwapTradesFetcher, CftcSwapTradesQueryParams, ) TRADE_DATE = date(2026, 7, 15) @pytest.fixture(name="patch_source") def patch_source_fixture(monkeypatch, slice_records, fx_records): async def _dates(asset_class): return ["2026-07-14", "2026-07-15"] async def _slice(asset_class, report_date, use_cache=True): return fx_records 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) @pytest.fixture(name="patch_search") def patch_search_fixture(monkeypatch, chf_search_records): calls: list = [] async def _search(asset_class, **kwargs): calls.append({"asset_class": asset_class, **kwargs}) return chf_search_records monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) return calls @pytest.mark.parametrize( ("fetcher", "query_cls", "params"), [ (CftcCdsIndexTradesFetcher, CftcCdsIndexTradesQueryParams, {}), (CftcCotFetcher, CftcCotQueryParams, {"code": "088691"}), (CftcCotSearchFetcher, CftcCotSearchQueryParams, {"query": "gold"}), (CftcSwapTradesFetcher, CftcSwapTradesQueryParams, {}), (CftcOisCurveFetcher, CftcOisCurveQueryParams, {}), (CftcOisCurveHistoryFetcher, CftcOisCurveHistoryQueryParams, {}), (CftcOisForwardCurveFetcher, CftcOisForwardCurveQueryParams, {}), (CftcFxForwardPointsFetcher, CftcFxForwardPointsQueryParams, {}), (CftcFxImpliedVolFetcher, CftcFxImpliedVolQueryParams, {}), ], ) def test_transform_query(fetcher, query_cls, params): assert isinstance(fetcher.transform_query(params), query_cls) @pytest.mark.parametrize( "model", [ CftcOisCurveData, CftcOisForwardCurveData, CftcFxForwardPointsData, CftcFxImpliedVolData, CftcSwapTradesData, ], ) def test_widget_config_survives_the_serialization_schema(model): schema = model.model_json_schema(mode="serialization") props = schema.get("properties", {}) assert props, f"{model.__name__} exposes no serialization-schema properties" assert any("x-widget_config" in prop for prop in props.values()), ( f"{model.__name__} lost its x-widget_config in the serialization schema" ) def test_cot_search_query_defaults_are_none(): query = CftcCotSearchQueryParams() assert query.query is None assert query.code is None def test_currency_and_pair_params_accept_any_case(): assert CftcOisCurveQueryParams(currency="usd").currency == "USD" assert CftcOisCurveHistoryQueryParams(currency="chf").currency == "CHF" assert CftcFxForwardPointsQueryParams(pair="eurUSD").pair == "EURUSD" assert CftcSwapTradesQueryParams(asset_class="RATES").asset_class == "rates" def test_swap_trades_data_normalizes_values(): model = CftcSwapTradesData.model_validate( { "Dissemination Identifier": "4217413061000000101", "Notional amount-Leg 1": "79,501,573,807+", "Fixed rate-Leg 1": "0.04", "Product name": "", "Effective Date": "2026-07-17", } ) assert model.notional_amount_leg_1 == 79501573807.0 assert model.is_capped is True assert model.product_name is None assert model.effective_date == date(2026, 7, 17) def test_swap_trades_data_passes_through_non_str(): model = CftcSwapTradesData.model_validate( {"Notional amount-Leg 1": 100.0, "Asset Class": "IR", "Cleared": None} ) assert model.notional_amount_leg_1 == 100.0 assert model.asset_class == "IR" assert model.cleared is None assert model.is_capped is False def test_swap_trades_data_passes_non_dict_payloads_through(): from pydantic import ValidationError with pytest.raises(ValidationError, match="valid dictionary"): CftcSwapTradesData.model_validate(["not", "a", "mapping"]) def test_swap_trades_data_drops_a_bare_cap_marker(): model = CftcSwapTradesData.model_validate({"Notional amount-Leg 1": "+"}) assert model.notional_amount_leg_1 is None assert model.is_capped is True def test_swap_trades_data_qualifies_the_identifier_out_of_double_range(): model = CftcSwapTradesData.model_validate( {"Dissemination Identifier": "4402189634000000101", "Asset Class": "IR"} ) assert model.trade_key == "IR:4402189634000000101" assert not model.trade_key.isdigit() bare = model.trade_key.split(":", 1)[1] assert float(bare) != int(bare) def test_swap_trades_data_qualifies_an_identifier_without_an_asset_class(): model = CftcSwapTradesData.model_validate( {"Dissemination Identifier": "4402189634000000101"} ) assert model.trade_key == "NA:4402189634000000101" def test_swap_trades_data_has_no_trade_key_without_an_identifier(): assert CftcSwapTradesData.model_validate({"Asset Class": "IR"}).trade_key is None @pytest.mark.parametrize( ("given", "expected"), [ ("IR:4402189634000000101", "4402189634000000101"), ("FX:4402190022000000101", "4402190022000000101"), ("4402189634000000101", "4402189634000000101"), (" 4402189634000000101 ", "4402189634000000101"), (None, None), ], ) def test_swap_valuation_accepts_a_qualified_trade_key(given, expected): from openbb_cftc.models.swap_valuation import CftcSwapValuationQueryParams query = CftcSwapValuationQueryParams(dissemination_identifier=given) assert query.dissemination_identifier == expected def test_swap_trades_fetcher_filters(patch_source): records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( date=TRADE_DATE, currency="USD", underlier="SOFR", upi_fisn="Swap OIS", action_type="NEWT", ), None, ) ) assert records assert all(r["Notional currency-Leg 1"] == "USD" for r in records) assert all("SOFR" in r["UPI Underlier Name"].upper() for r in records) def test_swap_trades_fetcher_notional_bounds_and_limit(patch_source): records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( date=TRADE_DATE, min_notional=1e7, max_notional=5e7, limit=5 ), None, ) ) assert len(records) == 5 def _priceable(**overrides): record = { "Action type": "NEWT", "Event type": "TRAD", "UPI FISN": "NA/Swap OIS USD", "Notional currency-Leg 1": "USD", "Fixed rate-Leg 1": "0.04", "Effective Date": "2020-01-01", "Expiration Date": "2099-01-01", } record.update(overrides) return record async def _passthrough_priceable(filtered, day_records, day, query): return filtered def test_swap_trades_fetcher_skips_records_without_a_notional(monkeypatch): priced = _priceable(**{"Notional amount-Leg 1": "20,000,000"}) unpriced = {**priced, "Notional amount-Leg 1": ""} async def _dates(*args, **kwargs): return ["2026-07-15"] async def _slice(*args, **kwargs): return [unpriced, priced] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(min_notional=1e6), None ) ) assert records == [{**priced, "trade_type": "spot"}] def _searched_print(day, **overrides): record = _priceable( **{ "Unique Product Identifier": "QZXQ4R16245X", "Execution Timestamp": f"{day}T14:00:00Z", "Dissemination Timestamp": f"{day}T14:00:01Z", } ) record.update(overrides) return record def test_swap_trades_ticker_queries_the_search_api_across_the_window(monkeypatch): seen: list = [] async def _search(asset_class, start_date, end_date, **kwargs): seen.append((asset_class, start_date, end_date, kwargs)) return [ _searched_print("2026-07-14", **{"Cleared": "Y"}), _searched_print("2026-07-15", **{"Cleared": "N"}), ] async def _forbidden_slice(*args, **kwargs): raise AssertionError("get_slice ran for a ticker-routed query") async def _rates(report_date, currencies, use_cache=True): return [], "2026-07-15" monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _forbidden_slice) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_rates_slice_for", _rates) monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( ticker="qzxq4r16245x", currency="USD", cleared=True, start_date=date(2026, 7, 14), end_date=date(2026, 7, 15), ), None, ) ) assert [r["Cleared"] for r in records] == ["Y"] assert seen == [ ( "rates", date(2026, 7, 14), date(2026, 7, 15), {"currency": "USD", "upi": "QZXQ4R16245X", "use_cache": True}, ) ] def test_swap_trades_ticker_without_dates_keeps_the_latest_disseminated_day( monkeypatch, ): async def _search(asset_class, start_date, end_date, **kwargs): return [ _searched_print("2026-07-14"), _searched_print("2026-07-15"), _searched_print("2026-07-15"), ] async def _rates(report_date, currencies, use_cache=True): return [], "2026-07-15" monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_rates_slice_for", _rates) monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(ticker="QZXQ4R16245X"), None ) ) assert len(records) == 2 assert all(r["Dissemination Timestamp"].startswith("2026-07-15") for r in records) def test_swap_trades_ticker_raises_when_the_search_matches_nothing(monkeypatch): async def _search(*args, **kwargs): return [] monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) with pytest.raises(EmptyDataError, match="No rates transactions matched"): asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(ticker="QZXQ4R16245X"), None ) ) def test_swap_trades_ticker_routes_forex_through_search(monkeypatch): seen: list = [] async def _search(asset_class, start_date, end_date, **kwargs): seen.append((asset_class, kwargs.get("upi"))) return [ { "Action type": "NEWT", "UPI FISN": "NA/Fwd EUR USD", "Unique Product Identifier": "QZFWDAAAAAAA", "Dissemination Timestamp": "2026-07-15T10:00:00Z", } ] async def _forbidden_slice(*args, **kwargs): raise AssertionError("get_slice ran for a ticker-routed forex query") monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _forbidden_slice) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(asset_class="forex", ticker="qzfwdaaaaaaa"), None, ) ) assert seen == [("forex", "QZFWDAAAAAAA")] assert [r["Unique Product Identifier"] for r in records] == ["QZFWDAAAAAAA"] def test_apply_filters_drops_a_ticker_the_search_did_not_match_exactly(): from openbb_cftc.models import swap_trades as st wanted = { "Action type": "NEWT", "UPI FISN": "NA/Fwd EUR USD", "Unique Product Identifier": "QZFWDAAAAAAA", } other = {**wanted, "Unique Product Identifier": "QZFWDBBBBBBB"} query = CftcSwapTradesQueryParams(asset_class="forex", ticker="qzfwdaaaaaaa") assert st._apply_filters([wanted, other], query) == [wanted] def test_swap_trades_rejects_an_unpriceable_asset_class(): from pydantic import ValidationError for asset_class in ("credits", "equities", "commodities"): with pytest.raises(ValidationError): CftcSwapTradesQueryParams(asset_class=asset_class) assert CftcSwapTradesQueryParams(asset_class="forex").asset_class == "forex" def test_swap_trades_ticker_rejects_a_backwards_window(): with pytest.raises(OpenBBError, match="ends before it starts"): asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( ticker="QZXQ4R16245X", start_date=date(2026, 7, 15), end_date=date(2026, 7, 14), ), None, ) ) def test_swap_trades_ticker_route_applies_the_priceability_gate(monkeypatch): async def _search(asset_class, start_date, end_date, **kwargs): return [ _searched_print("2026-07-15", **{"Notional amount-Leg 1": "20,000,000"}) ] async def _rates(report_date, currencies, use_cache=True): return [], "2026-07-15" monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_rates_slice_for", _rates) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(ticker="QZXQ4R16245X"), None ) ) assert records == [] def test_swap_trades_fetcher_cleared_filter(monkeypatch): records = [ _priceable(**{"Cleared": "Y"}), _priceable(**{"Cleared": "I"}), _priceable(**{"Cleared": "N"}), ] async def _dates(*args, **kwargs): return ["2026-07-15"] async def _slice(*args, **kwargs): return records monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) cleared = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(cleared=True), None ) ) assert [r["Cleared"] for r in cleared] == ["Y", "I"] bilateral = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(cleared=False), None ) ) assert [r["Cleared"] for r in bilateral] == ["N"] def test_classify_rates_swap_rejects_a_non_trad_event(): from openbb_cftc.models.swap_trades import _classify_rates_swap record = _priceable(**{"Event type": "COMP"}) assert _classify_rates_swap(record, date(2026, 7, 20)) is None def test_classify_rates_swap_rejects_an_unpriced_leg_with_no_second_currency(): from openbb_cftc.models.swap_trades import _classify_rates_swap record = _priceable(**{"Fixed rate-Leg 1": "", "Fixed rate-Leg 2": ""}) assert _classify_rates_swap(record, date(2026, 7, 20)) is None def test_classify_rates_swap_splits_spot_from_forward(): from openbb_cftc.models.swap_trades import _classify_rates_swap today = date(2026, 7, 20) spot = _priceable(**{"Effective Date": "2020-01-01"}) forward = _priceable(**{"Effective Date": "2030-01-01"}) assert _classify_rates_swap(spot, today) == "spot" assert _classify_rates_swap(forward, today) == "forward" def test_classify_rates_swap_rejects_a_fixed_vs_float_swap_with_no_effective_date(): from openbb_cftc.models.swap_trades import _classify_rates_swap record = _priceable(**{"Effective Date": ""}) assert _classify_rates_swap(record, date(2026, 7, 20)) is None def test_classify_rates_swap_identifies_basis_and_cross_currency(): from openbb_cftc.models.swap_trades import _classify_rates_swap today = date(2026, 7, 20) basis = _priceable( **{ "UPI FISN": "NA/Swap Flt Flt OIS USD", "Fixed rate-Leg 1": "", "Notional currency-Leg 2": "USD", } ) cross_currency = _priceable( **{ "UPI FISN": "NA/Swap Flt Flt EUR USD", "Notional currency-Leg 1": "EUR", "Fixed rate-Leg 1": "", "Notional currency-Leg 2": "USD", } ) assert _classify_rates_swap(basis, today) == "basis" assert _classify_rates_swap(cross_currency, today) == "cross_currency" def test_classify_rates_swap_rejects_a_swaption(): from openbb_cftc.models.swap_trades import _classify_rates_swap record = _priceable( **{ "UPI FISN": "NA/O P Epn Fxd Flt USD", "Fixed rate-Leg 1": "", "Notional currency-Leg 2": "USD", } ) assert _classify_rates_swap(record, date(2026, 7, 20)) is None def test_swap_trades_trade_type_filter_narrows_the_table(monkeypatch): today = date(2026, 7, 20) spot = _priceable(**{"Effective Date": "2020-01-01"}) forward = _priceable(**{"Effective Date": "2030-01-01"}) basis = _priceable( **{ "UPI FISN": "NA/Swap Flt Flt OIS USD", "Fixed rate-Leg 1": "", "Notional currency-Leg 2": "USD", } ) cross_currency = _priceable( **{ "UPI FISN": "NA/Swap Flt Flt EUR USD", "Notional currency-Leg 1": "EUR", "Fixed rate-Leg 1": "", "Notional currency-Leg 2": "USD", } ) async def _dates(*args, **kwargs): return [today.isoformat()] async def _slice(*args, **kwargs): return [spot, forward, basis, cross_currency] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(end_date=today, trade_type="cross_currency"), None, ) ) assert len(records) == 1 assert records[0]["trade_type"] == "cross_currency" assert records[0]["UPI FISN"] == "NA/Swap Flt Flt EUR USD" all_records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(end_date=today), None ) ) assert {r["trade_type"] for r in all_records} == { "spot", "forward", "basis", "cross_currency", } def test_swap_trades_action_type_filter_applies_outside_rates(monkeypatch): forex_newt = {"Action type": "NEWT", "UPI FISN": "NA/Fwd EUR USD"} forex_modi = {"Action type": "MODI", "UPI FISN": "NA/Fwd EUR USD"} async def _dates(*args, **kwargs): return ["2026-07-15"] async def _slice(*args, **kwargs): return [forex_newt, forex_modi] async def _keep(filtered, day_records, day, query): return filtered monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) monkeypatch.setattr("openbb_cftc.models.swap_trades._keep_priceable_forex", _keep) records = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(asset_class="forex", action_type="NEWT"), None ) ) assert records == [forex_newt] def test_swap_trades_fetcher_defaults_to_the_latest_date(patch_source): assert asyncio.run( CftcSwapTradesFetcher.aextract_data(CftcSwapTradesQueryParams(), None) ) def test_swap_trades_fetcher_raises_when_nothing_matches(patch_source): with pytest.raises(EmptyDataError, match="No rates transactions matched"): asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(end_date=TRADE_DATE, currency="ZWL"), None ) ) def test_swap_trades_window_concatenates_days_and_limits(patch_source, monkeypatch): monkeypatch.setattr( "openbb_cftc.models.swap_trades._keep_priceable", _passthrough_priceable ) single = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams(start_date=TRADE_DATE, end_date=TRADE_DATE), None ) ) window = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( start_date=TRADE_DATE - timedelta(days=2), end_date=TRADE_DATE ), None, ) ) limited = asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( start_date=TRADE_DATE - timedelta(days=2), end_date=TRADE_DATE, limit=5, ), None, ) ) assert len(window) == 3 * len(single) assert len(limited) == 5 def test_keep_priceable_drops_rows_the_valuation_cannot_price( slice_records, fx_records, monkeypatch ): from openbb_cftc.models import swap_trades as st from openbb_cftc.utils.swap import extract_spread_trade, parse_trade_record async def _fx(asset_class, report_date, use_cache=True): assert asset_class == "forex" return fx_records monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _fx) no_curve = _priceable( **{ "UPI FISN": "NA/Swap Fxd Flt ZWL", "Notional currency-Leg 1": "ZWL", "Notional amount-Leg 1": "20,000,000", } ) query = CftcSwapTradesQueryParams() filtered = st._apply_filters([*slice_records, no_curve], query) assert any(r["Notional currency-Leg 1"] == "ZWL" for r in filtered) kept = asyncio.run(st._keep_priceable(filtered, slice_records, "2026-07-15", query)) assert kept assert all(r["Notional currency-Leg 1"] != "ZWL" for r in kept) today = date(2026, 7, 15) for row in kept: if row["trade_type"] in ("basis", "cross_currency"): assert extract_spread_trade(row, today) is not None elif row["trade_type"] == "inflation": from openbb_cftc.utils.swap import extract_inflation_trade assert extract_inflation_trade(row, today) is not None else: assert parse_trade_record(row, today) is not None def test_keep_priceable_drops_forex_rows_the_valuation_cannot_price(monkeypatch): from openbb_cftc.models import swap_trades as st async def _slice(*args, **kwargs): return [] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcSwapTradesQueryParams(asset_class="forex") rows = [{"UPI FISN": "NA/Fwd EUR USD"}] assert asyncio.run(st._keep_priceable(rows, rows, "2026-07-15", query)) == [] def test_keep_priceable_forex_returns_nothing_without_a_rates_slice(monkeypatch): from openbb_core.app.model.abstract.error import OpenBBError from openbb_cftc.models import swap_trades as st async def _slice(*args, **kwargs): raise OpenBBError("no rates slice") monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcSwapTradesQueryParams(asset_class="forex") rows = [{"UPI FISN": "NA/Fwd EUR USD"}] assert asyncio.run(st._keep_priceable(rows, rows, "2026-07-15", query)) == [] def test_keep_priceable_drops_cross_currency_when_forex_is_unavailable( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st fetches: list = [] async def _broken(*args, **kwargs): fetches.append(args) raise OpenBBError("forex source unavailable") monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _broken) cross = [{"trade_type": "cross_currency", "UPI FISN": "NA/Swap Flt Flt EUR USD"}] kept = asyncio.run( st._keep_priceable( cross, slice_records, "2026-07-15", CftcSwapTradesQueryParams() ) ) assert len(fetches) == 1 assert kept == [] spot_query = CftcSwapTradesQueryParams(trade_type="spot", currency="USD") spot = st._apply_filters(slice_records, spot_query) assert spot kept_spot = asyncio.run( st._keep_priceable(spot, slice_records, "2026-07-15", spot_query) ) assert kept_spot assert len(fetches) == 1 def test_curve_ok_persists_a_closed_day_verdict(slice_records, monkeypatch): from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import fx_vol builds: list = [] real_build = fx_vol.rate_discount_factor def _counted(*args, **kwargs): builds.append(1) return real_build(*args, **kwargs) monkeypatch.setattr(fx_vol, "rate_discount_factor", _counted) assert st._curve_ok(slice_records, "USD", "2026-07-15", {}) is True assert builds == [1] assert st._curve_ok(slice_records, "USD", "2026-07-15", {}) is True assert builds == [1] memo: dict = {} assert st._curve_ok(slice_records, "ZWL", "2026-07-15", memo) is False assert st._curve_ok(slice_records, "ZWL", "2026-07-15", memo) is False def test_curve_ok_does_not_persist_todays_verdict(slice_records): from datetime import datetime, timezone from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import store today = datetime.now(timezone.utc).date().isoformat() assert st._curve_ok(slice_records, "ZWL", today, {}) is False assert store.get_curve(f"priceable-curve:{today}:ZWL") is None def test_spot_ok_requires_forex_prints(fx_records): from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import store assert st._spot_ok(None, "EUR", "USD", "2026-07-15", {}) is False assert store.get_curve("priceable-spot:2026-07-15:EUR/USD") is None assert st._spot_ok(fx_records, "EUR", "USD", "2026-07-15", {}) is True assert st._spot_ok(None, "EUR", "USD", "2026-07-15", {}) is True memo: dict = {} assert st._spot_ok(fx_records, "ZWL", "USD", "2026-07-15", memo) is False assert st._spot_ok(fx_records, "ZWL", "USD", "2026-07-15", memo) is False def test_spot_ok_does_not_persist_todays_verdict(fx_records): from datetime import datetime, timezone from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import store today = datetime.now(timezone.utc).date().isoformat() assert st._spot_ok(fx_records, "ZWL", "USD", today, {}) is False assert store.get_curve(f"priceable-spot:{today}:ZWL/USD") is None def test_priceable_rejects_a_spot_print_missing_terms(): from openbb_cftc.models import swap_trades as st record = { "trade_type": "spot", "UPI FISN": "NA/Swap OIS USD", "Notional currency-Leg 1": "USD", } assert st._priceable(record, [], "2026-07-15", date(2026, 7, 25), None, {}) is False def test_priceable_rejects_a_spread_print_without_priceable_legs(): from openbb_cftc.models import swap_trades as st record = {"trade_type": "basis", "UPI FISN": "NA/Swap Flt Flt OIS USD"} assert st._priceable(record, [], "2026-07-15", date(2026, 7, 25), None, {}) is False def test_priceable_rejects_a_basis_print_without_same_day_references( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import swap monkeypatch.setattr( swap, "extract_spread_trade", lambda record, day, min_notional=0.0: { "currency": "USD", "other_currency": None, "underlier": "NO SUCH INDEX", "trade_type": "basis", "spread_leg": 1, }, ) record = {"trade_type": "basis"} assert ( st._priceable(record, slice_records, "2026-07-15", date(2026, 7, 15), None, {}) is False ) def test_priceable_gates_a_cross_currency_print_on_curves_and_spot( slice_records, fx_records, monkeypatch ): from openbb_cftc.models import swap_trades as st from openbb_cftc.utils import swap trade = { "currency": "USD", "other_currency": "EUR", "underlier": "EUR USD", "trade_type": "cross_currency", "spread_leg": 1, } monkeypatch.setattr( swap, "extract_spread_trade", lambda record, day, min_notional=0.0: trade ) record = {"trade_type": "cross_currency"} assert ( st._priceable( record, slice_records, "2026-07-15", date(2026, 7, 15), fx_records, {} ) is True ) trade["other_currency"] = "ZWL" assert ( st._priceable( record, slice_records, "2026-07-15", date(2026, 7, 15), fx_records, {} ) is False ) def test_swap_trades_window_skips_an_unpublished_day(monkeypatch, slice_records): from openbb_cftc.models.swap_trades import _apply_filters async def _slice(asset_class, report_date, use_cache=True): if report_date == "2026-07-14": raise EmptyDataError("No PPD file published.") return slice_records monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcSwapTradesQueryParams( start_date=TRADE_DATE - timedelta(days=1), end_date=TRADE_DATE ) records = asyncio.run(CftcSwapTradesFetcher.aextract_data(query, None)) assert len(records) == len(_apply_filters(slice_records, query)) def test_swap_trades_rejects_an_inverted_window(patch_source): with pytest.raises(OpenBBError, match="ends before it starts"): asyncio.run( CftcSwapTradesFetcher.aextract_data( CftcSwapTradesQueryParams( start_date=TRADE_DATE, end_date=TRADE_DATE - timedelta(days=3) ), None, ) ) def test_swap_trades_transform_data(patch_source, slice_records): result = CftcSwapTradesFetcher.transform_data( CftcSwapTradesQueryParams(), slice_records[:3] ) assert len(result) == 3 assert all(isinstance(r, CftcSwapTradesData) for r in result) def test_ois_curve_fetcher_usd(patch_source): query = CftcOisCurveQueryParams(date=TRADE_DATE, source="slice", min_trades=5) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert result.metadata["source"] == "slice" assert result.metadata["benchmark"] == "SOFR" assert result.metadata["central_bank"] == "Federal Reserve" assert result.metadata["curve_date"] == "2026-07-15" assert result.metadata["lookback_days"] is None assert all(isinstance(n, CftcOisCurveData) for n in result.result) ten_year = next(n for n in result.result if n.tenor == "10Y") assert ten_year.currency == "USD" assert ten_year.benchmark == "SOFR" assert ten_year.index == "10Y" assert ten_year.curve_type == "par" assert ten_year.rate == pytest.approx(4.13782, abs=5e-5) assert ten_year.discount_factor == pytest.approx(0.6616279, abs=5e-7) assert all(n.as_of_date == TRADE_DATE for n in result.result) assert all(n.staleness_days == 0 for n in result.result) def test_ois_curve_slice_walks_back_to_a_priceable_day(monkeypatch, slice_records): from openbb_cftc.utils.constants import SOFR_OIS_FISN forward_only = [ { "UPI FISN": SOFR_OIS_FISN, "Action type": "NEWT", "Event type": "TRAD", "Notional currency-Leg 1": "USD", "Fixed rate-Leg 1": "0.04", "Effective Date": "2026-08-19", "Expiration Date": "2027-08-19", "Notional amount-Leg 1": "50,000,000", } ] async def _dates(asset_class): return ["2026-07-16", "2026-07-17", "2026-07-18", "2026-07-19"] async def _slice(asset_class, report_date, use_cache=True): if report_date == "2026-07-19": return forward_only if report_date == "2026-07-18": return [] return slice_records monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcOisCurveQueryParams( source="slice", date=date(2026, 7, 19), lookback_days=7, min_trades=5 ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert data[1] == "2026-07-17" assert result.metadata["curve_date"] == "2026-07-17" assert result.result def test_ois_curve_slice_raises_when_nothing_priceable_in_window(monkeypatch): from openbb_core.app.model.abstract.error import OpenBBError async def _dates(asset_class): return ["2026-07-17", "2026-07-18", "2026-07-19"] async def _slice(asset_class, report_date, use_cache=True): return [] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcOisCurveQueryParams(source="slice", lookback_days=3) with pytest.raises(OpenBBError, match="held usable data"): asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) def test_ois_curve_type_selects_the_variant_in_rate(patch_source): common = {"date": TRADE_DATE, "source": "slice", "min_trades": 5} def curve(curve_type): query = CftcOisCurveQueryParams(**common, curve_type=curve_type) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) return {n.tenor: n for n in result.result}, result.metadata par, par_meta = curve("par") zero, _ = curve("zero") assert par_meta["curve_type"] == "par" assert all(n.curve_type == "par" for n in par.values()) assert par["10Y"].rate == pytest.approx(4.13782, abs=5e-5) assert zero["10Y"].curve_type == "zero" assert zero["10Y"].rate > par["10Y"].rate assert not hasattr(par["10Y"], "par_rate") assert set(par["10Y"].model_dump()) == set(zero["10Y"].model_dump()) def test_ois_curve_fetcher_honors_log_cubic_interpolation(patch_source): common = {"date": TRADE_DATE, "source": "slice", "min_trades": 5} linear = CftcOisCurveFetcher.transform_data( CftcOisCurveQueryParams(**common), asyncio.run( CftcOisCurveFetcher.aextract_data(CftcOisCurveQueryParams(**common), None) ), ) cubic_query = CftcOisCurveQueryParams(**common, interpolation="log_cubic") cubic = CftcOisCurveFetcher.transform_data( cubic_query, asyncio.run(CftcOisCurveFetcher.aextract_data(cubic_query, None)), ) linear_by = {n.tenor: n for n in linear.result} cubic_by = {n.tenor: n for n in cubic.result} assert cubic_query.interpolation == "log_cubic" assert {n.tenor: n.rate for n in cubic.result} == { n.tenor: n.rate for n in linear.result } assert cubic_by["1Y"].discount_factor == pytest.approx( linear_by["1Y"].discount_factor, abs=1e-12 ) assert cubic_by["10Y"].discount_factor == pytest.approx( linear_by["10Y"].discount_factor, abs=5e-4 ) assert all( n.discount_factor is None or 0.0 < n.discount_factor <= 1.0 for n in cubic.result ) def test_tenor_to_years_maps_labels_and_rejects_unknown(): from openbb_core.app.model.abstract.error import OpenBBError from openbb_cftc.utils.constants import tenor_to_years assert tenor_to_years("1Y") == pytest.approx(1.0) assert tenor_to_years("6m") == pytest.approx(182 / 365) assert tenor_to_years("3M") == pytest.approx(91 / 365) with pytest.raises(OpenBBError, match="Invalid tenor"): tenor_to_years("7M") def test_ois_curve_observed_index_is_the_maturity_date(patch_source): observed = CftcOisCurveQueryParams( date=TRADE_DATE, source="slice", granularity="observed", min_trades=1 ) obs_result = CftcOisCurveFetcher.transform_data( observed, asyncio.run(CftcOisCurveFetcher.aextract_data(observed, None)) ) assert all(n.index == n.maturity_date.isoformat() for n in obs_result.result) benchmark = CftcOisCurveQueryParams(date=TRADE_DATE, source="slice", min_trades=5) bench_result = CftcOisCurveFetcher.transform_data( benchmark, asyncio.run(CftcOisCurveFetcher.aextract_data(benchmark, None)) ) assert all(n.index == n.tenor for n in bench_result.result) def test_ois_curve_cleared_filter_drops_uncleared(patch_source): common = {"date": TRADE_DATE, "source": "slice", "min_trades": 5} everything = CftcOisCurveFetcher.transform_data( CftcOisCurveQueryParams(**common), asyncio.run( CftcOisCurveFetcher.aextract_data(CftcOisCurveQueryParams(**common), None) ), ) cleared_query = CftcOisCurveQueryParams(**common, cleared=True) cleared = CftcOisCurveFetcher.transform_data( cleared_query, asyncio.run(CftcOisCurveFetcher.aextract_data(cleared_query, None)), ) assert everything.metadata["cleared_only"] is False assert cleared.metadata["cleared_only"] is True assert cleared.metadata["trades"] < everything.metadata["trades"] def test_ois_curve_fetcher_search_is_the_default(patch_search): query = CftcOisCurveQueryParams( currency="CHF", date=date(2026, 7, 16), min_trades=1 ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert query.source == "search" assert result.metadata["source"] == "search" assert result.metadata["lookback_days"] == 7 assert result.metadata["benchmark"] == "SARON" assert result.metadata["curve_date"] == "2026-07-16" assert result.metadata["freshest_node"] == "2026-07-16" assert result.metadata["stalest_node"] < result.metadata["freshest_node"] assert {n.staleness_days for n in result.result} != {0} assert min(n.rate for n in result.result) < 0 def test_ois_curve_search_window_is_the_lookback(patch_search): query = CftcOisCurveQueryParams( currency="CHF", date=date(2026, 7, 16), lookback_days=30 ) asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) assert patch_search[0]["start_date"] == date(2026, 6, 17) assert patch_search[0]["end_date"] == date(2026, 7, 16) assert patch_search[0]["currency"] == "CHF" assert patch_search[0]["upi_short_name"] == "NA/Swap OIS CHF" assert patch_search[0]["asset_class"] == "rates" def test_ois_curve_search_defaults_the_window_to_today(patch_search): from datetime import datetime, timezone asyncio.run( CftcOisCurveFetcher.aextract_data(CftcOisCurveQueryParams(currency="CHF"), None) ) assert patch_search[0]["end_date"] == datetime.now(timezone.utc).date() def test_ois_curve_search_caps_staleness(patch_search): base = CftcOisCurveQueryParams(currency="CHF") full = CftcOisCurveFetcher.transform_data( base, asyncio.run(CftcOisCurveFetcher.aextract_data(base, None)) ) capped_query = CftcOisCurveQueryParams(currency="CHF", max_staleness_days=0) capped = CftcOisCurveFetcher.transform_data( capped_query, asyncio.run(CftcOisCurveFetcher.aextract_data(capped_query, None)), ) assert capped.result assert all(n.staleness_days == 0 for n in capped.result) assert len(capped.result) < len(full.result) @pytest.mark.parametrize( ("currency", "index"), [ ("EUR", "ESTR"), ("GBP", "SONIA"), ("JPY", "TONA"), ("CAD", "CORRA"), ("CHF", "SARON"), ("MXN", "TIIE"), ("SGD", "SORA"), ("INR", "MIBOR"), ("COP", "IBR"), ("ZAR", "ZARONIA"), ("CLP", "ICP"), ], ) def test_ois_curve_every_central_bank(patch_source, currency, index): query = CftcOisCurveQueryParams(date=TRADE_DATE, currency=currency, source="slice") data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert result.metadata["benchmark"] == index assert result.result factors = [ n.discount_factor for n in result.result if n.discount_factor is not None ] assert all(a > b for a, b in zip(factors, factors[1:])) assert all(n.benchmark == index for n in result.result) def test_ois_currency_literal_matches_the_index_registry(): from typing import get_args from openbb_cftc.utils.constants import ( FIXED_FLOAT_CURVE_SPECS, OIS_INDICES, day_count_basis, ) registry = set(OIS_INDICES) fixed_float = set(FIXED_FLOAT_CURVE_SPECS) thin = {"AUD", "NZD"} offered_by_class = { CftcOisCurveQueryParams: (registry - thin) | fixed_float, CftcOisForwardCurveQueryParams: registry - thin, CftcOisCurveHistoryQueryParams: registry | fixed_float, } for query_cls, expected in offered_by_class.items(): offered = set(get_args(query_cls.model_fields["currency"].annotation)) assert offered == expected, query_cls.__name__ for currency in ("THB", "ILS", "BRL", "AUD", "NZD"): entry = OIS_INDICES[currency] assert entry["index"] and entry["central_bank"] assert day_count_basis(entry["day_count"], "A004") in (360.0, 365.0) def test_ois_curve_slice_defaults_to_the_latest_published_date(patch_source): _, report_date, anchor = asyncio.run( CftcOisCurveFetcher.aextract_data(CftcOisCurveQueryParams(source="slice"), None) ) assert report_date == "2026-07-15" assert anchor is None @pytest.mark.parametrize("lookback", [0, 366]) def test_ois_curve_rejects_a_lookback_outside_the_horizon(lookback): from pydantic import ValidationError with pytest.raises(ValidationError): CftcOisCurveQueryParams(lookback_days=lookback) def test_ois_curve_accepts_a_full_horizon_lookback(): assert CftcOisCurveQueryParams(lookback_days=365).lookback_days == 365 def test_ois_curve_history_pivot(patch_source): query = CftcOisCurveHistoryQueryParams( start_date=date(2026, 7, 14), end_date=TRADE_DATE, min_trades=5 ) nodes = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) result = CftcOisCurveHistoryFetcher.transform_data(query, nodes) assert result.metadata["pivot"] is True assert result.metadata["index"] == "SOFR" assert len(result.result) == 2 assert result.result[0].tenor_10y == pytest.approx(0.041378, abs=5e-7) assert result.result[0].tenor is None def test_ois_curve_history_flat_and_tenor_filter(patch_source): query = CftcOisCurveHistoryQueryParams( start_date=date(2026, 7, 14), end_date=TRADE_DATE, tenor="2y, 10Y", pivot=False, measure="zero_rate", min_trades=5, ) nodes = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) result = CftcOisCurveHistoryFetcher.transform_data(query, nodes) assert {r.tenor for r in result.result} == {"2Y", "10Y"} assert all(r.value is not None for r in result.result) assert result.metadata["measure"] == "zero_rate" def test_ois_curve_history_tenor_filter_in_pivot_mode(patch_source): query = CftcOisCurveHistoryQueryParams( start_date=date(2026, 7, 14), end_date=TRADE_DATE, tenor="10Y", min_trades=5 ) nodes = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) result = CftcOisCurveHistoryFetcher.transform_data(query, nodes) assert result.result[0].tenor_10y is not None assert result.result[0].tenor_2y is None def test_ois_curve_history_other_currency(patch_source): query = CftcOisCurveHistoryQueryParams( currency="EUR", start_date=date(2026, 7, 14), end_date=TRADE_DATE ) nodes = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) result = CftcOisCurveHistoryFetcher.transform_data(query, nodes) assert result.metadata["index"] == "ESTR" assert result.result def test_ois_curve_history_defaults_the_window(patch_source): nodes = asyncio.run( CftcOisCurveHistoryFetcher.aextract_data( CftcOisCurveHistoryQueryParams(min_trades=5), None ) ) assert {n["date"] for n in nodes} == {date(2026, 7, 14), date(2026, 7, 15)} def test_ois_curve_history_unmatched_tenor_raises(patch_source): query = CftcOisCurveHistoryQueryParams( start_date=date(2026, 7, 14), end_date=TRADE_DATE, tenor="99Y", min_trades=5 ) nodes = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) with pytest.raises(EmptyDataError, match="No curve nodes matched"): CftcOisCurveHistoryFetcher.transform_data(query, nodes) def test_ois_curve_history_skips_dates_without_a_curve(monkeypatch, slice_records): async def _dates(*args, **kwargs): return ["2026-07-14", "2026-07-15"] async def _slice(asset_class, report_date, use_cache=True): return slice_records if report_date == "2026-07-15" else [] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) nodes = asyncio.run( CftcOisCurveHistoryFetcher.aextract_data( CftcOisCurveHistoryQueryParams( start_date=date(2026, 7, 14), end_date=TRADE_DATE, min_trades=5 ), None, ) ) assert {n["date"] for n in nodes} == {date(2026, 7, 15)} def test_ois_curve_history_raises_without_any_curve(monkeypatch): async def _dates(*args, **kwargs): return ["2026-07-15"] async def _slice(*args, **kwargs): return [] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) with pytest.raises(EmptyDataError, match="could be built"): asyncio.run( CftcOisCurveHistoryFetcher.aextract_data( CftcOisCurveHistoryQueryParams(), None ) ) def test_ois_curve_history_raises_without_published_files(monkeypatch): async def _none(*args, **kwargs): return [] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _none) with pytest.raises(EmptyDataError, match="No PPD interest-rate files"): asyncio.run( CftcOisCurveHistoryFetcher.aextract_data( CftcOisCurveHistoryQueryParams(), None ) ) def test_ois_curve_history_raises_when_window_has_no_files(monkeypatch): async def _dates(*args, **kwargs): return ["2026-07-15"] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) with pytest.raises(EmptyDataError, match="No PPD files were published"): asyncio.run( CftcOisCurveHistoryFetcher.aextract_data( CftcOisCurveHistoryQueryParams( start_date=date(2020, 1, 1), end_date=date(2020, 2, 1) ), None, ) ) def test_ois_forward_curve_slice_prices_a_strip(patch_source): query = CftcOisForwardCurveQueryParams( source="slice", forward_tenor="1Y", forward_step="1Y", min_trades=5 ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert data[1] == "2026-07-15" assert result.metadata["index"] == "SOFR" assert result.metadata["forward_tenor"] == "1Y" assert all(isinstance(r, CftcOisForwardCurveData) for r in result.result) assert all(r.forward_rate is not None for r in result.result) starts = [round(r.start_years, 6) for r in result.result] assert starts == sorted(starts) assert starts[0] == 0.0 five_year = next(r for r in result.result if r.start_years == pytest.approx(5.0)) assert 3.0 < five_year.forward_rate < 5.0 def test_ois_forward_curve_observed_method(patch_source, monkeypatch): def _grid( records, fisn, currency, curve_date, *, forward_tenor_days, step_years, count, min_trades, spot_rate, ): assert spot_rate > 0.0 return [ { "tenor_years": 0.0, "tenor_days": 0, "forward_rate": 0.04, "forward_extrapolated": False, "num_trades": 7, }, { "tenor_years": 0.25, "tenor_days": 91, "forward_rate": 0.041, "forward_extrapolated": False, "num_trades": 7, }, ] monkeypatch.setattr("openbb_cftc.utils.policy.observed_forward_grid", _grid) query = CftcOisForwardCurveQueryParams( source="slice", method="observed", forward_tenor="1Y", forward_step="3M" ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert result.metadata["method"] == "observed" assert [round(r.start_years, 2) for r in result.result] == [0.0, 0.25] assert result.result[0].forward_rate == pytest.approx(4.0) def test_ois_forward_curve_slice_walks_back_over_an_empty_weekend( monkeypatch, slice_records ): async def _dates(asset_class): return ["2026-07-16", "2026-07-17", "2026-07-18", "2026-07-19"] async def _slice(asset_class, report_date, use_cache=True): return [] if report_date in ("2026-07-18", "2026-07-19") else slice_records monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) query = CftcOisForwardCurveQueryParams( source="slice", date=date(2026, 7, 19), forward_step="1Y", min_trades=5 ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert data[1] == "2026-07-17" assert result.result[0].date == date(2026, 7, 17) def test_ois_forward_curve_forward_tenor_accepts_labels(patch_source): query = CftcOisForwardCurveQueryParams( source="slice", date=TRADE_DATE, forward_tenor="6m", forward_step="6m" ) assert query.forward_tenor == "6M" assert query.forward_step == "6M" result = CftcOisForwardCurveFetcher.transform_data( query, asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) ) assert result.metadata["forward_tenor"] == "6M" assert all("6M @" in r.tenor for r in result.result) def test_ois_forward_curve_count_caps_the_strip(patch_source): query = CftcOisForwardCurveQueryParams( source="slice", date=TRADE_DATE, forward_step="1Y", forward_count=3, min_trades=5, ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert len(result.result) == 3 assert [round(r.start_years, 6) for r in result.result] == [0.0, 1.0, 2.0] def test_ois_forward_curve_search_defaults_report_date_none(patch_search): query = CftcOisForwardCurveQueryParams( currency="chf", source="search", forward_tenor="1Y", forward_step="1Y" ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert data[1] is None assert result.metadata["index"] == "SARON" assert result.result def test_ois_forward_curve_flags_an_extrapolated_tail(patch_source): query = CftcOisForwardCurveQueryParams( source="slice", date=TRADE_DATE, forward_tenor="5Y", forward_step="1Y", min_trades=3, ) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) result = CftcOisForwardCurveFetcher.transform_data(query, data) assert all(r.forward_rate is not None for r in result.result) assert result.result[0].extrapolated is False assert result.result[-1].extrapolated is True def test_ois_forward_curve_raises_on_empty_strip(patch_source, monkeypatch): from openbb_core.app.model.abstract.error import OpenBBError monkeypatch.setattr( "openbb_cftc.utils.curve.forward_curve_grid", lambda *a, **k: [] ) query = CftcOisForwardCurveQueryParams(source="slice", date=TRADE_DATE) data = asyncio.run(CftcOisForwardCurveFetcher.aextract_data(query, None)) with pytest.raises(OpenBBError, match="forward curve could be built"): CftcOisForwardCurveFetcher.transform_data(query, data) def test_fx_forward_points_fetcher_slice(patch_source): query = CftcFxForwardPointsQueryParams( pair="USDJPY", date=TRADE_DATE, source="slice" ) data = asyncio.run(CftcFxForwardPointsFetcher.aextract_data(query, None)) result = CftcFxForwardPointsFetcher.transform_data(query, data) assert result.metadata["source"] == "slice" assert result.metadata["pair"] == "USDJPY" assert result.metadata["spot_rate"] > 100 assert result.metadata["curve_date"] == "2026-07-15" assert result.metadata["lookback_days"] is None assert all(isinstance(p, CftcFxForwardPointsData) for p in result.result) assert all(p.staleness_days == 0 for p in result.result) series = [p.forward_points for p in result.result] assert all(a >= b for a, b in zip(series, series[1:])) def test_fx_forward_points_slice_defaults_to_the_latest_published_date(patch_source): _, _, report_date, _ = asyncio.run( CftcFxForwardPointsFetcher.aextract_data( CftcFxForwardPointsQueryParams(source="slice"), None ) ) assert report_date == "2026-07-15" def test_fx_forward_points_search_queries_both_fisns(monkeypatch, fx_records): from datetime import ( date as dateType, timedelta, ) calls: list = [] async def _search(asset_class, **kwargs): calls.append(kwargs) return [ {**r, "Dissemination Timestamp": "2026-07-15T10:00:00Z"} for r in fx_records ] monkeypatch.setattr("openbb_cftc.utils.search.search_trades", _search) query = CftcFxForwardPointsQueryParams(pair="USDJPY", date=dateType(2026, 7, 15)) data = asyncio.run(CftcFxForwardPointsFetcher.aextract_data(query, None)) result = CftcFxForwardPointsFetcher.transform_data(query, data) assert [c["upi_short_name"] for c in calls] == [ "NA/Fwd JPY USD", "NA/Swaps JPY USD", ] assert all(c.get("currency") is None for c in calls) assert calls[0]["start_date"] == dateType(2026, 7, 15) - timedelta(days=29) assert result.metadata["source"] == "search" assert result.metadata["lookback_days"] == 30 assert result.metadata["freshest_tenor"] == "2026-07-15" series = [p.forward_points for p in result.result] assert all(a >= b for a, b in zip(series, series[1:])) assert series[-1] < 0 def _infl_row(**overrides): record = { "Dissemination Identifier": "INF1", "Action type": "NEWT", "Event type": "TRAD", "UPI FISN": "NA/Swap Infl Idx GBP", "UPI Underlier Name": "UK-RPI", "Notional currency-Leg 1": "GBP", "Fixed rate-Leg 1": "0.03", "Effective Date": "2020-01-01", "Expiration Date": "2036-07-15", "Notional amount-Leg 1": "10,000,000", "trade_type": "inflation", } record.update(overrides) return record def test_priceable_drops_a_seasoned_inflation_print_without_its_index( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st monkeypatch.setattr(st, "_curve_ok", lambda *a, **k: True) monkeypatch.setattr( "openbb_cftc.utils.inflation.build_breakeven_curve", lambda *a, **k: [(9.0, 0.03)], ) row = _infl_row() today = date(2026, 7, 15) assert st._priceable(row, slice_records, "2026-07-15", today, None, {}, {}) is False assert ( st._priceable( row, slice_records, "2026-07-15", today, None, {}, {"UKRPI": {date(2019, 11, 1): 100.0, date(2026, 5, 1): 120.0}}, ) is True ) def test_priceable_keeps_an_unstarted_inflation_print_without_any_index( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st monkeypatch.setattr(st, "_curve_ok", lambda *a, **k: True) monkeypatch.setattr( "openbb_cftc.utils.inflation.build_breakeven_curve", lambda *a, **k: [(9.0, 0.03)], ) row = _infl_row(**{"Effective Date": "2026-08-01"}) assert ( st._priceable(row, slice_records, "2026-07-15", date(2026, 7, 15), None, {}, {}) is True ) def test_priceable_drops_an_inflation_print_that_will_not_extract(slice_records): from openbb_cftc.models import swap_trades as st unpriceable = _infl_row(**{"Notional amount-Leg 1": ""}) assert ( st._priceable( unpriceable, slice_records, "2026-07-15", date(2026, 7, 15), None, {}, {} ) is False ) def test_priceable_drops_an_inflation_print_without_a_rate_curve( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st monkeypatch.setattr(st, "_curve_ok", lambda *a, **k: False) assert ( st._priceable( _infl_row(), slice_records, "2026-07-15", date(2026, 7, 15), None, {}, {} ) is False ) def test_priceable_drops_an_inflation_print_with_no_breakeven_curve( slice_records, monkeypatch ): from openbb_cftc.models import swap_trades as st monkeypatch.setattr(st, "_curve_ok", lambda *a, **k: True) monkeypatch.setattr( "openbb_cftc.utils.inflation.build_breakeven_curve", lambda *a, **k: [] ) assert ( st._priceable( _infl_row(), slice_records, "2026-07-15", date(2026, 7, 15), None, {}, {} ) is False ) def test_inflation_levels_fetches_each_index_once_and_survives_an_outage(monkeypatch): from openbb_core.app.model.abstract.error import OpenBBError from openbb_cftc.models import swap_trades as st calls: list = [] async def _index(index, use_cache=True): calls.append(index) if index == "USCPI": raise OpenBBError("publisher down") return {date(2026, 1, 1): 100.0} monkeypatch.setattr("openbb_cftc.utils.inflation.get_inflation_index", _index) rows = [ _infl_row(), _infl_row(), _infl_row(**{"UPI Underlier Name": "USA-CPI-U"}), {"trade_type": "spot", "UPI Underlier Name": "USD-SOFR-COMPOUND"}, ] levels = asyncio.run(st._inflation_levels(rows, True)) assert sorted(calls) == ["UKRPI", "USCPI"] assert sorted(levels) == ["UKRPI"] def test_ois_curve_overnight_anchor(patch_source, monkeypatch): seen: list = [] async def _fixings(index, start_date, end_date, use_cache=True): seen.append((index, start_date, end_date)) return {date(2026, 7, 14): 0.0435} monkeypatch.setattr("openbb_cftc.utils.fixings.get_fixings", _fixings) query = CftcOisCurveQueryParams( date=TRADE_DATE, source="slice", min_trades=5, overnight_anchor=True ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) first = result.result[0] assert seen[0][0] == "SOFR" assert first.tenor == "1D" assert first.num_trades == 0 assert first.as_of_date == date(2026, 7, 14) assert first.staleness_days == 1 assert first.rate == pytest.approx(4.35) assert result.metadata["overnight_anchor"] == { "date": "2026-07-14", "rate": pytest.approx(4.35), } def test_ois_curve_overnight_anchor_search_source(patch_search, monkeypatch): async def _fixings(index, start_date, end_date, use_cache=True): return {end_date: 0.011} monkeypatch.setattr("openbb_cftc.utils.fixings.get_fixings", _fixings) query = CftcOisCurveQueryParams(currency="CHF", min_trades=1, overnight_anchor=True) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert result.result[0].tenor == "1D" assert result.metadata["overnight_anchor"]["rate"] == pytest.approx(1.1) def test_ois_curve_anchor_unavailable_on_a_fixings_outage(patch_source, monkeypatch): async def _broken(index, start_date, end_date, use_cache=True): raise OpenBBError("source down") monkeypatch.setattr("openbb_cftc.utils.fixings.get_fixings", _broken) query = CftcOisCurveQueryParams( date=TRADE_DATE, source="slice", min_trades=5, overnight_anchor=True ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert all(n.tenor != "1D" for n in result.result) assert result.metadata["overnight_anchor"] == "unavailable" def test_ois_curve_anchor_metadata_defaults_off(patch_source): query = CftcOisCurveQueryParams(date=TRADE_DATE, source="slice", min_trades=5) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert result.metadata["overnight_anchor"] is False def test_latest_fixing_skips_days_after_the_cutoff(monkeypatch): from openbb_cftc.models.ois_curve import _latest_fixing async def _fixings(index, start_date, end_date, use_cache=True): return {date(2026, 8, 1): 0.05} monkeypatch.setattr("openbb_cftc.utils.fixings.get_fixings", _fixings) assert asyncio.run(_latest_fixing("USD", date(2026, 7, 15), True)) is None def test_keep_priceable_forex_injects_the_cfets_curve_for_cny(monkeypatch): from openbb_cftc.models.swap_trades import ( CftcSwapTradesQueryParams, _keep_priceable_forex, ) marker = {"UPI FISN": "NA/Swap Fxd Flt CNY", "synthetic": "1"} captured: list = [] async def _rates(asset_class, report_date, use_cache=True): return [{"UPI FISN": "NA/Swap OIS USD"}] async def _cny(day, use_cache=True): return [marker] def _value(record, fx_records, rates_records, curve_date, **kwargs): captured.append(list(rates_records)) return {"npv": 1.0} monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _rates) monkeypatch.setattr("openbb_cftc.utils.cfets.cny_curve_records", _cny) monkeypatch.setattr("openbb_cftc.utils.fx_valuation.value_fx_trade", _value) filtered = [{"UPI FISN": "NA/Fwd NDF CNY USD", "UPI Underlier Name": "CNY USD"}] query = CftcSwapTradesQueryParams(asset_class="forex") kept = asyncio.run(_keep_priceable_forex(filtered, [], "2026-07-24", query)) assert kept == filtered assert marker in captured[0] def test_keep_priceable_forex_skips_cfets_without_cny(monkeypatch): from openbb_cftc.models.swap_trades import ( CftcSwapTradesQueryParams, _keep_priceable_forex, ) async def _rates(asset_class, report_date, use_cache=True): return [{"UPI FISN": "NA/Swap OIS USD"}] def _forbidden(*args, **kwargs): raise AssertionError("no CFETS fetch for a CNY-free day") def _value(record, fx_records, rates_records, curve_date, **kwargs): return {"npv": 1.0} monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _rates) monkeypatch.setattr("openbb_cftc.utils.cfets.cny_curve_records", _forbidden) monkeypatch.setattr("openbb_cftc.utils.fx_valuation.value_fx_trade", _value) filtered = [{"UPI FISN": "NA/Fwd EUR USD", "UPI Underlier Name": "EUR USD"}] query = CftcSwapTradesQueryParams(asset_class="forex") kept = asyncio.run(_keep_priceable_forex(filtered, [], "2026-07-24", query)) assert kept == filtered def test_swap_summary_fx_injects_the_cfets_curve_for_cny(monkeypatch): from openbb_cftc.models.swap_summary import ( CftcSwapSummaryQueryParams, _fx_metadata, ) marker = {"UPI FISN": "NA/Swap Fxd Flt CNY", "synthetic": "1"} captured: list = [] async def _slice(asset_class, report_date, use_cache=True): return [{"UPI FISN": "NA/Swap OIS USD"}] async def _cny(day, use_cache=True): return [marker] def _value(record, fx_records, rates_records, curve_date, **kwargs): captured.append(list(rates_records)) return {"npv": 1.0} monkeypatch.setattr("openbb_cftc.utils.dtcc.get_slice", _slice) monkeypatch.setattr("openbb_cftc.utils.cfets.cny_curve_records", _cny) monkeypatch.setattr("openbb_cftc.utils.fx_valuation.value_fx_trade", _value) record = { "UPI FISN": "NA/Fwd NDF CNY USD", "UPI Underlier Name": "CNY USD", "Dissemination Timestamp": "2026-07-24T12:00:00Z", } query = CftcSwapSummaryQueryParams(dissemination_identifier="4400000000000000101") valued = asyncio.run(_fx_metadata(query, record)) assert valued["npv"] == 1.0 assert valued["trade_date"] == "2026-07-24" assert marker in captured[0] def _patch_cfets_bulletin(monkeypatch): from tests.test_cfets import BULLETIN async def _bulletin(search_date, use_cache=True): return BULLETIN monkeypatch.setattr("openbb_cftc.utils.cfets.fetch_irs_bulletin", _bulletin) def test_ois_curve_cny_slice_builds_from_the_cfets_backstop(patch_source, monkeypatch): _patch_cfets_bulletin(monkeypatch) query = CftcOisCurveQueryParams( currency="CNY", source="slice", date=TRADE_DATE, min_trades=1 ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) tenors = {row.tenor for row in result.result} assert result.metadata["benchmark"] == "FR007" assert result.metadata["central_bank"] == "People's Bank of China" assert result.metadata["curve_date"] == "2026-07-15" assert {"1M", "1Y", "5Y"} <= tenors assert all(row.currency == "CNY" for row in result.result) factors = [ row.discount_factor for row in result.result if row.discount_factor is not None ] assert all(a > b for a, b in zip(factors, factors[1:])) def test_ois_curve_cny_slice_without_the_backstop_uses_the_tape( patch_source, monkeypatch ): async def _empty(day, use_cache=True): return [] monkeypatch.setattr("openbb_cftc.utils.cfets.cny_curve_records", _empty) query = CftcOisCurveQueryParams( currency="CNY", source="slice", date=TRADE_DATE, min_trades=1 ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert len(result.result) > 0 assert all(row.currency == "CNY" for row in result.result) def test_ois_curve_cny_slice_rejects_an_empty_window(patch_source, monkeypatch): from openbb_core.app.model.abstract.error import OpenBBError as CoreError async def _dates(asset_class): return ["2026-07-14", "2026-07-15"] monkeypatch.setattr("openbb_cftc.utils.dtcc.get_available_dates", _dates) query = CftcOisCurveQueryParams( currency="CNY", source="slice", date=date(2020, 1, 1) ) with pytest.raises(CoreError, match="No PPD rates files"): asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) def test_ois_curve_cny_search_source(patch_search, monkeypatch): _patch_cfets_bulletin(monkeypatch) query = CftcOisCurveQueryParams(currency="CNY", min_trades=1) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert result.metadata["benchmark"] == "FR007" assert len(result.result) >= 9 def test_ois_curve_cny_overnight_anchor_uses_shibor(patch_source, monkeypatch): _patch_cfets_bulletin(monkeypatch) seen: list = [] async def _fixings(index, start_date, end_date, use_cache=True): seen.append(index) return {date(2026, 7, 14): 0.0142} monkeypatch.setattr("openbb_cftc.utils.fixings.get_fixings", _fixings) query = CftcOisCurveQueryParams( currency="CNY", source="slice", date=TRADE_DATE, min_trades=1, overnight_anchor=True, ) data = asyncio.run(CftcOisCurveFetcher.aextract_data(query, None)) result = CftcOisCurveFetcher.transform_data(query, data) assert seen == ["SHIBOR"] assert result.result[0].tenor == "1D" assert result.result[0].rate == pytest.approx(1.42) def test_ois_curve_history_cny(patch_source, monkeypatch): _patch_cfets_bulletin(monkeypatch) query = CftcOisCurveHistoryQueryParams( currency="CNY", start_date=date(2026, 7, 14), end_date=TRADE_DATE, min_trades=1, ) data = asyncio.run(CftcOisCurveHistoryFetcher.aextract_data(query, None)) result = CftcOisCurveHistoryFetcher.transform_data(query, data) assert result.metadata["index"] == "FR007" assert len(result.result) > 0