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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 97 additions & 61 deletions openbb_platform/providers/cftc/openbb_cftc/utils/fx_valuation.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,38 @@ def _discount_for(
return memo[key]


def _pair_curve(
fx_records: Iterable[dict], trade: dict, curve_date: dateType, memo: dict
) -> dict:
"""Return the pair's forward curve for the day, built once and shared by its prints."""
from openbb_cftc.utils.fx import extract_fx_observations
from openbb_cftc.utils.fx_vol import ndf_forward_curve

key = f"fx-curve:{trade['pair']}:{curve_date.isoformat()}"

if key not in memo:
records = _pair_subset(fx_records, trade, memo)
observations = extract_fx_observations(
records,
pair=trade["pair"],
trade_date=curve_date,
min_notional=CURVE_MIN_NOTIONAL,
)
spot, forward = ndf_forward_curve(
records, trade["pair"], curve_date, min_notional=CURVE_MIN_NOTIONAL
)
memo[key] = {
"nodes": len({o["days"] for o in observations if o["days"] > 2}),
"near": [o["rate"] for o in observations if o["days"] <= 9],
"spot": spot,
"forward": forward,
}

return memo[key]


def _cip_market(
peers: list[dict],
near: list[float],
rates_records: Iterable[dict],
trade: dict,
curve_date: dateType,
Expand All @@ -292,19 +322,6 @@ def _cip_market(
"""Both legs' curves and a covered-interest-parity forward, when prints are sparse."""
from statistics import median

from openbb_cftc.utils.fx import extract_fx_observations

near = [
observation["rate"]
for observation in extract_fx_observations(
peers,
pair=trade["pair"],
trade_date=curve_date,
min_notional=CURVE_MIN_NOTIONAL,
)
if observation["days"] <= 9
]

if not near:
return None

Expand All @@ -327,46 +344,17 @@ def forward(years: float, _s=spot, _b=df_base, _q=df_quote) -> float:
return {"spot": spot, "forward": forward, "df_base": df_base, "df_quote": df_quote}


def fx_market(
fx_records: Iterable[dict],
def _curve_market(
spot: float,
forward: Callable[[float], float],
rates_records: Iterable[dict],
trade: dict,
curve_date: dateType,
min_trades: int = 1,
memo: dict | None = None,
min_trades: int,
memo: dict,
) -> dict | None:
"""Spot, forward interpolator and both legs' discount curves for a pair, or None."""
from openbb_cftc.utils.fx import extract_fx_observations
from openbb_cftc.utils.fx_vol import implied_df, ndf_forward_curve

memo = {} if memo is None else memo
peers = [
record
for record in _pair_subset(fx_records, trade, memo)
if (record.get("Dissemination Identifier") or "").strip()
!= trade["dissemination_identifier"]
and not is_package_leg(record)
]
nodes = {
observation["days"]
for observation in extract_fx_observations(
peers,
pair=trade["pair"],
trade_date=curve_date,
min_notional=CURVE_MIN_NOTIONAL,
)
if observation["days"] > 2
}

if len(nodes) < MIN_FORWARD_NODES:
return _cip_market(peers, rates_records, trade, curve_date, min_trades, memo)

spot, forward = ndf_forward_curve(
Comment thread
deeleeramone marked this conversation as resolved.
peers, trade["pair"], curve_date, min_notional=CURVE_MIN_NOTIONAL
)

if spot is None or forward is None or spot <= 0.0:
return _cip_market(peers, rates_records, trade, curve_date, min_trades, memo)
"""Both legs' discount curves around an observed forward curve."""
from openbb_cftc.utils.fx_vol import implied_df

df_base = _discount_for(rates_records, trade["base"], curve_date, min_trades, memo)

Expand All @@ -388,6 +376,46 @@ def _base_df(years: float, _quote=df_quote) -> float:
return {"spot": spot, "forward": forward, "df_base": df_base, "df_quote": df_quote}


def fx_market(
fx_records: Iterable[dict],
rates_records: Iterable[dict],
trade: dict,
curve_date: dateType,
min_trades: int = 1,
memo: dict | None = None,
) -> dict | None:
"""Spot, forward interpolator and both legs' discount curves for a pair, or None."""
memo = {} if memo is None else memo
key = f"fx-market:{trade['pair']}:{curve_date.isoformat()}:{min_trades}"

if key in memo:
return memo[key]

curve = _pair_curve(fx_records, trade, curve_date, memo)
spot, forward = curve["spot"], curve["forward"]

if (
curve["nodes"] < MIN_FORWARD_NODES
or spot is None
or forward is None
or spot <= 0.0
):
market = _cip_market(
curve["near"], rates_records, trade, curve_date, min_trades, memo
)
else:
market = _curve_market(
spot, forward, rates_records, trade, curve_date, min_trades, memo
)

if market is not None:
market["key"] = key

memo[key] = market

return market


def value_fx_forward(trade: dict, market: dict) -> dict:
"""Mark a forward to the pair's forward curve and discount to the curve date."""
years = trade["days"] / DAYS_PER_YEAR
Expand Down Expand Up @@ -422,17 +450,25 @@ def option_vol(
return None

memo = {} if memo is None else memo
options = extract_vanilla_options(
_pair_subset(fx_records, trade, memo),
trade["base"],
trade["quote"],
curve_date,
market["spot"],
family=family,
)
priced = price_options(
options, market["spot"], market["df_quote"], market["df_base"]
)
key = f"fx-options:{market['key']}:{family}" if market.get("key") else None

if key is not None and key in memo:
priced = memo[key]
else:
options = extract_vanilla_options(
_pair_subset(fx_records, trade, memo),
trade["base"],
trade["quote"],
curve_date,
market["spot"],
family=family,
)
priced = price_options(
options, market["spot"], market["df_quote"], market["df_base"]
)

if key is not None:
memo[key] = priced

if not priced:
return None
Expand Down
77 changes: 68 additions & 9 deletions openbb_platform/providers/cftc/tests/test_fx_valuation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import date
from datetime import date, timedelta

import pytest

Expand Down Expand Up @@ -438,18 +438,18 @@ def test_fx_market_cip_fallback_rejects_a_nonpositive_spot(monkeypatch):
assert fv.fx_market([], [], trade, DAY) is None


def test_fx_market_excludes_the_trades_own_print(monkeypatch):
def test_fx_market_uses_the_same_curve_as_the_vol_surface(monkeypatch):
seen = {}

def _observe(records, **kwargs):
def _curve(records, pair, as_of, **kwargs):
seen["ids"] = [r.get("Dissemination Identifier") for r in records]
return _observations([7, 38, 90])
return 1460.0, lambda years: 1460.0

monkeypatch.setattr("openbb_cftc.utils.fx.extract_fx_observations", _observe)
monkeypatch.setattr(
"openbb_cftc.utils.fx_vol.ndf_forward_curve",
lambda *a, **k: (1460.0, lambda years: 1460.0),
"openbb_cftc.utils.fx.extract_fx_observations",
lambda *a, **k: _observations([7, 38, 90]),
)
monkeypatch.setattr("openbb_cftc.utils.fx_vol.ndf_forward_curve", _curve)
monkeypatch.setattr(
"openbb_cftc.utils.fx_vol.rate_discount_factor",
lambda *a, **k: lambda years: 0.98**years,
Expand All @@ -459,10 +459,48 @@ def _observe(records, **kwargs):
)
own = _forward()
peer = _forward(**{"Dissemination Identifier": "4402190022000000201"})
leg = _forward(
**{
"Dissemination Identifier": "4402190022000000202",
"Package indicator": "TRUE",
}
)
trade = fv.extract_fx_trade(own, DAY)
fv.fx_market([own, peer], [], trade, DAY)
fv.fx_market([own, peer, leg], [], trade, DAY)

assert seen["ids"] == [
own["Dissemination Identifier"],
"4402190022000000201",
"4402190022000000202",
]


assert seen["ids"] == ["4402190022000000201"]
def test_fx_market_builds_the_curve_once_per_pair_and_day(monkeypatch):
_patch_market(monkeypatch, [7, 38, 90])
calls: list = []

def _curve(records, pair, as_of, **kwargs):
calls.append(as_of)
return 1460.0, lambda years: 1460.0

monkeypatch.setattr("openbb_cftc.utils.fx_vol.ndf_forward_curve", _curve)
first = _forward()
second = _forward(**{"Dissemination Identifier": "4402190022000000201"})
memo: dict = {}

for record in (first, second):
fv.fx_market(
[first, second], [], fv.extract_fx_trade(record, DAY), DAY, memo=memo
)

assert calls == [DAY]

later = DAY + timedelta(days=1)
fv.fx_market(
[first, second], [], fv.extract_fx_trade(first, later), later, memo=memo
)

assert calls == [DAY, later]


def test_fx_market_memo_builds_the_pair_subset_and_curves_once(monkeypatch):
Expand Down Expand Up @@ -528,6 +566,27 @@ def test_option_vol_returns_none_without_a_family():
assert fv.option_vol([], trade, _market(1.14), DAY) is None


def test_option_vol_prices_a_family_once_per_market(monkeypatch):
calls: list = []

def _price(*args, **kwargs):
calls.append(args)
return [{"tenor_days": 38, "implied_vol": 0.10}]

monkeypatch.setattr(
"openbb_cftc.utils.fx_vol.extract_vanilla_options", lambda *a, **k: [{}]
)
monkeypatch.setattr("openbb_cftc.utils.fx_vol.price_options", _price)
trade = fv.extract_fx_trade(_option(), DAY)
market = {**_market(1.14), "key": "fx-market:EURUSD:2026-07-24:1"}
memo: dict = {}

for _ in range(3):
assert fv.option_vol([], trade, market, DAY, memo) == pytest.approx(0.10)

assert len(calls) == 1


def test_value_fx_trade_prices_a_forward(monkeypatch):
_patch_market(monkeypatch, [7, 38, 90])
valued = fv.value_fx_trade(_forward(), [], [], DAY)
Expand Down