import asyncio
import io
import json
import zipfile
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_cftc.utils import fixings
class _Response:
def __init__(self, body, status=200):
self._body = body
self.status = status
def raise_for_status(self):
if self.status >= 400:
raise OpenBBError(f"status {self.status}")
async def text(self):
return self._body if isinstance(self._body, str) else self._body.decode()
async def read(self):
return self._body if isinstance(self._body, bytes) else self._body.encode()
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
class _Session:
def __init__(self, replies, seen):
self._replies = replies
self._seen = seen
def _reply(self, url):
self._seen.append(url)
for fragment, body in self._replies:
if fragment in url:
return body if isinstance(body, _Response) else _Response(body)
return _Response("", status=404)
def get(self, url, **kwargs):
return self._reply(url)
def post(self, url, **kwargs):
self._seen.append(kwargs.get("json") or kwargs.get("data"))
return self._reply(url)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
def _patch(monkeypatch, replies):
import aiohttp
seen: list = []
monkeypatch.setattr(
aiohttp, "ClientSession", lambda *a, **k: _Session(replies, seen)
)
monkeypatch.setattr(aiohttp, "TCPConnector", lambda *a, **k: None)
return seen
TAIBOR_SHEET = (
"中華民國銀行公會金融業拆款中心,,,,,,,,\n"
"台北金融業拆款定盤利率,,,,,,,,\n"
"日期,一星期,二星期,一個月,二個月,三個月,六個月,九個月,一年期\n"
"27JUL26,1.38078,1.47022,1.60033,1.62644,1.67944,1.71100,1.78133,1.89067\n"
"24JUL26,1.38078,1.47022,1.60033,1.62600,1.67944,1.71100,1.78133,1.89067\n"
"======,,,,,,,,\n"
)
def _taibor_zip() -> bytes:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as bundle:
bundle.writestr("JUL2026.csv", TAIBOR_SHEET.encode("big5"))
bundle.writestr("notes.txt", b"ignored")
return buffer.getvalue()
TAIBOR_INDEX = (
"
| 2026/07/27 | "
"x |
"
"| 2020/01/01 | "
"old |
"
"| no date here |
"
)
def test_taibor_rows_parses_dated_lines():
rows = fixings._taibor_rows(TAIBOR_SHEET, 5, "2026-01-01", "2026-12-31")
assert rows == {"2026-07-27": 0.0167944, "2026-07-24": 0.0167944}
def test_taibor_rows_skips_rows_outside_the_window():
assert fixings._taibor_rows(TAIBOR_SHEET, 5, "2027-01-01", "2027-12-31") == {}
def test_taibor_rows_skips_short_and_unparseable_rows():
text = "27JUL26,1.0\n27JUL26,1,2,3,4,bad\nnotadate,1,2,3,4,5.0\n"
assert fixings._taibor_rows(text, 5, "2000-01-01", "2030-01-01") == {}
def test_fetch_taibor_merges_archives(monkeypatch):
seen = _patch(
monkeypatch,
[("taiborDaily/index", TAIBOR_INDEX), ("DownloadAll", _taibor_zip())],
)
rates = asyncio.run(fixings._fetch_taibor("5", "2026-01-01", "2026-12-31"))
assert rates["2026-07-27"] == pytest.approx(0.0167944)
assert any("id=3235" in str(x) for x in seen)
assert not any("id=1" in str(x) for x in seen)
def test_fetch_taibor_skips_a_bad_archive(monkeypatch):
_patch(
monkeypatch,
[("taiborDaily/index", TAIBOR_INDEX), ("DownloadAll", b"not-a-zip")],
)
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_taibor("5", "2026-01-01", "2026-12-31"))
def test_fetch_taibor_skips_a_failed_download(monkeypatch):
_patch(
monkeypatch,
[
("taiborDaily/index", TAIBOR_INDEX),
("DownloadAll", _Response("", status=500)),
],
)
with pytest.raises(OpenBBError):
asyncio.run(fixings._fetch_taibor("5", "2026-01-01", "2026-12-31"))
CFETS_CSV = (
"2026-07-27,,,,,,1.44,1.44,1.43\n"
"2026-07-24,,,,,,1.4,1.41,1.43\n"
"2020-01-02,,,,,,2.0,2.1,2.2\n"
"header,row,not,a,date,x,y,z,w\n"
"2026-07-23,,,,,,1.39,bad,1.45\n"
)
def test_fetch_cfets_reads_the_named_column(monkeypatch):
_patch(monkeypatch, [("frr-chrt.csv", CFETS_CSV)])
rates = asyncio.run(fixings._fetch_cfets("7", "2026-01-01", "2026-12-31"))
assert rates == {
"2026-07-27": pytest.approx(0.0144),
"2026-07-24": pytest.approx(0.0141),
}
def test_fetch_cfets_raises_when_nothing_is_in_range(monkeypatch):
_patch(monkeypatch, [("frr-chrt.csv", CFETS_CSV)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_cfets("7", "2030-01-01", "2030-12-31"))
SHIBOR_BODY = json.dumps(
{
"head": {"rep_code": "200"},
"data": {
"columns": [
"date",
"open",
"high",
"low",
"close",
"volume",
"O/N",
"1W",
"2W",
"1M",
"3M",
"6M",
"9M",
"1Y",
],
"csv": (
"\r\n2026-07-29,,,,,,1.438,1.51,1.481,1.421,1.431,1.4505,1.471,1.481"
"\r\n2026-07-28,,,,,,1.415,1.46,1.45,1.42,1.43,1.4505,1.47,1.48"
"\r\n2020-01-02,,,,,,2.0,2.1,2.2,2.3,2.4,2.5,2.6,2.7"
"\r\nnot a date,,,,,,x,y,z,a,b,c,d,e"
"\r\n2026-07-27,,,,,,bad,1.43,1.42,1.42,1.43,1.45,1.47,1.48"
"\r\n"
),
},
}
)
def test_fetch_shibor_reads_the_tenor_column(monkeypatch):
_patch(monkeypatch, [("ShiborChrt", SHIBOR_BODY)])
rates = asyncio.run(fixings._fetch_shibor("O/N", "2026-01-01", "2026-12-31"))
assert rates == {
"2026-07-29": pytest.approx(0.01438),
"2026-07-28": pytest.approx(0.01415),
}
def test_fetch_shibor_reads_a_term_tenor(monkeypatch):
_patch(monkeypatch, [("ShiborChrt", SHIBOR_BODY)])
rates = asyncio.run(fixings._fetch_shibor("3M", "2026-07-29", "2026-07-29"))
assert rates == {"2026-07-29": pytest.approx(0.01431)}
def test_fetch_shibor_raises_when_nothing_is_in_range(monkeypatch):
_patch(monkeypatch, [("ShiborChrt", SHIBOR_BODY)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_shibor("O/N", "2030-01-01", "2030-12-31"))
def test_fetch_shibor_raises_on_an_unknown_tenor(monkeypatch):
_patch(monkeypatch, [("ShiborChrt", SHIBOR_BODY)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_shibor("5Y", "2026-01-01", "2026-12-31"))
FINWIRE_BODY = json.dumps(
{
"data": {
"indicator": "wibor-3m",
"points": [
{"date": "2026-07-24", "value": 3.79},
{"date": "2026-07-23", "value": 3.82},
{"date": "2020-01-02", "value": 1.71},
{"date": "bad", "value": 1.0},
{"date": "2026-07-22", "value": None},
],
}
}
)
def test_fetch_finwire_reads_points(monkeypatch):
seen = _patch(monkeypatch, [("interest-rate", FINWIRE_BODY)])
rates = asyncio.run(fixings._fetch_finwire("wibor-3m", "2026-07-01", "2026-07-24"))
assert rates == {
"2026-07-24": pytest.approx(0.0379),
"2026-07-23": pytest.approx(0.0382),
}
assert any("days=" in str(u) for u in seen)
def test_fetch_finwire_raises_on_an_empty_series(monkeypatch):
_patch(monkeypatch, [("interest-rate", json.dumps({"data": {"points": []}}))])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_finwire("wibor-3m", "2026-07-01", "2026-07-24"))
GPW_TABLE = (
"| Index Date | 1M |
"
"| 2026-07-17 | 3.599 | 3.59886 |
"
"| 2026-07-16 | 3.641 | 3.59812 |
"
"| 2026-07-20 | - | 3.59868 |
"
"| 2020-01-02 | 1.500 | 1.5 |
"
)
def test_fetch_gpw_reads_the_column(monkeypatch):
_patch(monkeypatch, [("delayed_data", GPW_TABLE)])
rates = asyncio.run(fixings._fetch_gpw("1", "2026-07-01", "2026-07-31"))
assert rates == {
"2026-07-17": pytest.approx(0.03599),
"2026-07-16": pytest.approx(0.03641),
}
def test_fetch_gpw_retries_then_succeeds(monkeypatch):
import aiohttp
monkeypatch.setattr(fixings, "GPW_RETRY_SECONDS", 0.0)
attempts = {"n": 0}
real = _Session([("delayed_data", GPW_TABLE)], [])
class _Flaky:
def get(self, url, **kwargs):
attempts["n"] += 1
if attempts["n"] < 3:
raise aiohttp.ClientError("reset")
return real.get(url)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
monkeypatch.setattr(aiohttp, "ClientSession", lambda *a, **k: _Flaky())
monkeypatch.setattr(aiohttp, "TCPConnector", lambda *a, **k: None)
rates = asyncio.run(fixings._fetch_gpw("1", "2026-07-01", "2026-07-31"))
assert attempts["n"] == 3
assert rates["2026-07-17"] == pytest.approx(0.03599)
def test_fetch_gpw_gives_up_after_the_last_attempt(monkeypatch):
import aiohttp
monkeypatch.setattr(fixings, "GPW_RETRY_SECONDS", 0.0)
monkeypatch.setattr(fixings, "GPW_MAX_ATTEMPTS", 2)
class _Dead:
def get(self, url, **kwargs):
raise aiohttp.ClientError("reset")
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
monkeypatch.setattr(aiohttp, "ClientSession", lambda *a, **k: _Dead())
monkeypatch.setattr(aiohttp, "TCPConnector", lambda *a, **k: None)
with pytest.raises(aiohttp.ClientError):
asyncio.run(fixings._fetch_gpw("1", "2026-07-01", "2026-07-31"))
def test_fetch_gpw_raises_when_no_rows_match(monkeypatch):
_patch(monkeypatch, [("delayed_data", GPW_TABLE)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_gpw("1", "2030-01-01", "2030-12-31"))
BOK_CONTENT = json.dumps(
[
{
"Selection of Items": "CD (91-day)",
"20260727": 2.92,
"20260724": 2.91,
"20200102": 1.3,
"Unit": "Percent Per Annum",
"20260723": "",
}
]
)
def test_fetch_bok_reads_date_keyed_columns(monkeypatch):
seen = _patch(
monkeypatch,
[("httpService", json.dumps({"data": {"jsonCtnt": BOK_CONTENT}}))],
)
rates = asyncio.run(
fixings._fetch_bok("817Y002/010502000", "2026-07-01", "2026-07-27")
)
assert rates["2026-07-27"] == pytest.approx(0.0292)
assert rates["2026-07-24"] == pytest.approx(0.0291)
assert rates["2020-01-02"] == pytest.approx(0.013)
body = next(x for x in seen if isinstance(x, dict))
assert body["data"]["statSrchDsList"][0]["dsItmVal1"] == "010502000"
assert body["header"]["trxCd"] == fixings.BOK_ECOS_TRX
def test_fetch_bok_raises_without_content(monkeypatch):
_patch(monkeypatch, [("httpService", json.dumps({"data": {}}))])
with pytest.raises(OpenBBError, match="ECOS"):
asyncio.run(fixings._fetch_bok("817Y002/010502000", "2026-07-01", "2026-07-27"))
def test_fetch_bok_raises_when_no_dates_parse(monkeypatch):
body = json.dumps({"data": {"jsonCtnt": json.dumps([{"Unit": "x"}])}})
_patch(monkeypatch, [("httpService", body)])
with pytest.raises(OpenBBError, match="ECOS"):
asyncio.run(fixings._fetch_bok("817Y002/010502000", "2026-07-01", "2026-07-27"))
SARB_ROWS = json.dumps(
[
{"Period": "2026-07-24T00:00:00", "Value": 7.0},
{"Period": "2026-07-23T00:00:00", "Value": 7.0},
{"Period": "2020-01-02T00:00:00", "Value": 6.5},
{"Period": "", "Value": 1.0},
{"Period": "2026-07-22T00:00:00", "Value": None},
]
)
def test_fetch_sarb_series_reads_observations(monkeypatch):
_patch(monkeypatch, [("GetTimeseriesObservations", SARB_ROWS)])
rates = asyncio.run(
fixings._fetch_sarb_series("MMRD403A", "2026-07-01", "2026-07-31")
)
assert rates == {
"2026-07-24": pytest.approx(0.07),
"2026-07-23": pytest.approx(0.07),
}
def test_fetch_sarb_series_rejects_a_non_list(monkeypatch):
_patch(monkeypatch, [("GetTimeseriesObservations", json.dumps({"x": 1}))])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_sarb_series("MMRD403A", "2026-07-01", "2026-07-31"))
def test_fetch_sarb_series_rejects_an_empty_window(monkeypatch):
_patch(monkeypatch, [("GetTimeseriesObservations", SARB_ROWS)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_sarb_series("MMRD403A", "2030-01-01", "2030-12-31"))
def _hkma_page(records):
return json.dumps({"result": {"records": records}})
def test_fetch_hkma_pages_until_exhausted(monkeypatch):
monkeypatch.setattr(fixings, "HKMA_PAGE_SIZE", 2)
pages = [
_hkma_page(
[
{"end_of_date": "2026-07-24", "hibor_fixing_1m": 2.72},
{"end_of_date": "2026-07-23", "hibor_fixing_1m": 2.71},
]
),
_hkma_page([{"end_of_date": "2026-07-22", "hibor_fixing_1m": 2.70}]),
]
calls = {"n": 0}
class _Paged(_Session):
def get(self, url, **kwargs):
body = pages[min(calls["n"], len(pages) - 1)]
calls["n"] += 1
return _Response(body)
import aiohttp
monkeypatch.setattr(aiohttp, "ClientSession", lambda *a, **k: _Paged([], []))
monkeypatch.setattr(aiohttp, "TCPConnector", lambda *a, **k: None)
rates = asyncio.run(
fixings._fetch_hkma("hibor_fixing_1m", "2026-07-01", "2026-07-31")
)
assert len(rates) == 3
assert rates["2026-07-22"] == pytest.approx(0.027)
def test_fetch_hkma_raises_when_empty(monkeypatch):
_patch(monkeypatch, [("hkma", _hkma_page([]))])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_hkma("hibor_fixing_1m", "2026-07-01", "2026-07-31"))
BNM_TABLE = (
"| Ref | Pub | Rate |
"
"| 24/07/2026 | 27/07/2026 | 2.75 |
"
"| 23/07/2026 | 24/07/2026 | 2.74 |
"
"| 02/01/2020 | 03/01/2020 | 3.00 |
"
"| notadate | x | 1.0 |
"
"| 22/07/2026 | x | bad |
"
)
def test_fetch_bnm_reads_the_column(monkeypatch):
seen = _patch(monkeypatch, [("data-download", BNM_TABLE)])
rates = asyncio.run(
fixings._fetch_bnm("data-download-myor?x=1|2", "2026-01-01", "2026-12-31")
)
assert rates["2026-07-24"] == pytest.approx(0.0275)
assert rates["2026-07-23"] == pytest.approx(0.0274)
assert any("data-download-myor" in str(u) for u in seen)
def test_fetch_bnm_raises_when_nothing_parses(monkeypatch):
_patch(monkeypatch, [("data-download", BNM_TABLE)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(
fixings._fetch_bnm("data-download-myor|2", "2030-01-01", "2030-12-31")
)
CNB_YEAR = (
"|1 day||1 week||2 weeks||1 month||2 months||3 months|\n"
"Date|PRIBID|PRIBOR|PRIBID|PRIBOR|PRIBID|PRIBOR|PRIBID|PRIBOR|PRIBID|PRIBOR|PRIBID|PRIBOR\n"
"23 Jul 2026||3.75||3.77||3.79||3.80||||3.85\n"
"22 Jul 2026||3.75||3.77||3.78||3.80||||3.85\n"
"bad row||1||2||3||4||||5\n"
)
def test_fetch_cnb_reads_the_tenor_column(monkeypatch):
seen = _patch(monkeypatch, [("year.txt", CNB_YEAR)])
rates = asyncio.run(fixings._fetch_cnb("12", "2026-01-01", "2026-12-31"))
assert rates == {
"2026-07-23": pytest.approx(0.0385),
"2026-07-22": pytest.approx(0.0385),
}
assert any("year=2026" in str(u) for u in seen)
def test_fetch_cnb_clamps_the_year_span(monkeypatch):
seen = _patch(monkeypatch, [("year.txt", CNB_YEAR)])
asyncio.run(fixings._fetch_cnb("12", "1990-01-01", "2026-12-31"))
years = {str(u).split("year=")[-1] for u in seen if "year=" in str(u)}
assert len(years) == fixings.CNB_MAX_YEARS + 1
def test_fetch_cnb_skips_a_failed_year(monkeypatch):
_patch(monkeypatch, [("year.txt", _Response("", status=500))])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_cnb("12", "2026-01-01", "2026-12-31"))
BBK_CSV = (
'"",BBIG1.D.SERIES,FLAGS\n'
'"",Money market rates,\n'
"Decimals,3,\n"
"2026-07-24,2.488,\n"
"2026-07-23,2.472,\n"
"2020-01-02,-0.400,\n"
"2026-07-22,,\n"
"notadate,1.0,\n"
)
def test_fetch_bbk_reads_daily_rows(monkeypatch):
_patch(monkeypatch, [("rest/download", BBK_CSV)])
rates = asyncio.run(
fixings._fetch_bbk("BBIG1/D.SERIES", "2026-07-01", "2026-07-31")
)
assert rates == {
"2026-07-24": pytest.approx(0.02488),
"2026-07-23": pytest.approx(0.02472),
}
def test_fetch_bbk_rejects_a_non_csv_body(monkeypatch):
_patch(monkeypatch, [("rest/download", "no separators here")])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_bbk("BBIG1/D.SERIES", "2026-07-01", "2026-07-31"))
def test_fetch_bbk_rejects_an_empty_window(monkeypatch):
_patch(monkeypatch, [("rest/download", BBK_CSV)])
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_bbk("BBIG1/D.SERIES", "2030-01-01", "2030-12-31"))
class _Sheet:
def __init__(self, rows, ncols):
self._rows = rows
self.nrows = len(rows)
self.ncols = ncols
def cell_value(self, row, col):
cells = self._rows[row]
return cells[col] if col < len(cells) else ""
class _Book:
def __init__(self, sheets):
self._sheets = sheets
self.datemode = 0
def sheet_names(self):
return list(self._sheets)
def sheet_by_name(self, name):
return self._sheets[name]
def _patch_mnb(monkeypatch, book):
import xlrd
_patch(monkeypatch, [("bubor", b"payload")])
monkeypatch.setattr(xlrd, "open_workbook", lambda **kwargs: book)
monkeypatch.setattr(
xlrd, "xldate_as_tuple", lambda value, mode: (2026, 7, int(value), 0, 0, 0)
)
def test_fetch_mnb_reads_year_sheets(monkeypatch):
rows = [
["h"] * 8,
["h"] * 8,
[24.0, 5.75, 5.75, 5.75, 5.75, 5.65, 5.60, 5.56],
[23.0, 5.75, 5.75, 5.75, 5.75, 5.66, 5.61, 5.57],
["", "", "", "", "", "", "", ""],
]
_patch_mnb(monkeypatch, _Book({"2026": _Sheet(rows, 8), "comment": _Sheet([], 0)}))
rates = asyncio.run(fixings._fetch_mnb("6", "2026-01-01", "2026-12-31"))
assert rates == {
"2026-07-24": pytest.approx(0.0560),
"2026-07-23": pytest.approx(0.0561),
}
def test_fetch_mnb_skips_short_rows_and_bad_values(monkeypatch):
rows = [["h"], ["h"], [24.0], ["notafloat", 1, 2, 3, 4, 5, 6, 7]]
_patch_mnb(monkeypatch, _Book({"2026": _Sheet(rows, 8)}))
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_mnb("6", "2026-01-01", "2026-12-31"))
def test_fetch_mnb_ignores_years_outside_the_window(monkeypatch):
rows = [["h"] * 8, ["h"] * 8, [24.0, 1, 2, 3, 4, 5, 5.60, 7]]
_patch_mnb(monkeypatch, _Book({"2019": _Sheet(rows, 8)}))
with pytest.raises(OpenBBError, match="Unexpected fixings response"):
asyncio.run(fixings._fetch_mnb("6", "2026-01-01", "2026-12-31"))
@pytest.mark.parametrize(
("index", "source"),
[
("TAIBOR", "_fetch_taibor"),
("FR007", "_fetch_cfets"),
("SHIBOR", "_fetch_shibor"),
("WIBOR", "_fetch_finwire"),
("POLSTR", "_fetch_gpw"),
("BUBOR", "_fetch_mnb"),
("KRWCD", "_fetch_bok"),
("JIBAR", "_fetch_sarb_series"),
("HIBOR", "_fetch_hkma"),
("MYOR", "_fetch_bnm"),
("PRIBOR", "_fetch_cnb"),
("EURIBOR", "_fetch_bbk"),
],
)
def test_get_fixings_dispatches_to_each_source(monkeypatch, index, source):
from datetime import date
called: list = []
async def _fetch(path, start, end):
called.append(path)
return {"2026-07-24": 0.05}
monkeypatch.setattr(fixings, source, _fetch)
rates = asyncio.run(
fixings.get_fixings(
index, date(2026, 7, 24), date(2026, 7, 24), use_cache=False
)
)
assert called
assert rates[date(2026, 7, 24)] == pytest.approx(0.05)