Skip to content

perf(cftc): build each FX pair's forward curve once per day, shared with the vol surface - #7750

Open
artcashin wants to merge 3 commits into
openbq-org:developfrom
artcashin:perf/cftc-fx-leave-one-out
Open

artcashin wants to merge 3 commits into
openbq-org:developfrom
artcashin:perf/cftc-fx-leave-one-out

Conversation

@artcashin

@artcashin artcashin commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

fx_market built a forward curve for every FX print from the pair's other prints, re-extracting every peer record each time. The swap_trades forex priceability gate calls it for every print of the day, so one GET /cftc/swap_trades?asset_class=forex was quadratic in a pair's prints. On a day of about 43,000 FX prints (USDKRW alone has 12,231), it ran for over an hour at 100% CPU. option_vol re-extracted and re-priced every vanilla option of the pair for each option print.

Change

  • One curve per pair and curve date. _pair_curve calls the shared extract_fx_observations and ndf_forward_curve once, over all of the pair's prints, package legs included, and memoizes the result. Every print of the pair is valued against that curve. Valuation and the vol surface therefore use the same NDF curve.
  • No re-implementations. The first version's leave-one-out helpers are gone: the per-anchor extraction, the median helper and the inline forward interpolator. The CIP fallback receives the near-dated rates from the same extraction.
  • Memo keys include the curve date: fx-curve:{pair}:{date}, fx-market:{pair}:{date}:{min_trades}, and option_vol's fx-options:{market key}:{family}, which prices each option family once per market.

Behaviour change

A print is now valued against the pair's full curve, which includes the print itself and package legs. Before, a curve left out the print and package legs. The test test_fx_market_excludes_the_trades_own_print is replaced by test_fx_market_uses_the_same_curve_as_the_vol_surface and test_fx_market_builds_the_curve_once_per_pair_and_day.

On the 2026-10-08 DTCC forex file, the endpoint's output is 42,762 rows, against 42,760 with the earlier leave-one-out version. Three more forwards become priceable (two CHF/EUR, one NZD/USD), and one USD/JPY NDO put no longer is. That is the effect of the shared curve near the minimum-node threshold.

Speed

On the same file, measured end to end through GET /api/v1/cftc/swap_trades?asset_class=forex&start_date=2026-10-08&end_date=2026-10-08:

develop this PR
forex swap_trades over 60 min 21–24 s (40 MB response)

The earlier leave-one-out version of this PR took 24 s warm on the same request, so the shared-curve refactor keeps the speed-up.

Tests

tests/test_fx_valuation.py passes 93/93, and the CFTC suite passes 1,227/1,227 (develop: 91 and 1,225).

🤖 Generated with Claude Code

…e pair

fx_market prices each FX print from the pair's other prints (leave-one-out)
and re-extracted every peer record, twice, for each print; the swap_trades
forex priceability gate does that for every print of the day, so a forex
swap_trades call was quadratic in a pair's prints: over an hour of CPU on a
day of ~43k FX prints, holding the event loop the whole time. option_vol
likewise re-extracted and re-implied every vanilla option of the pair for
each option print.

fx_market now prepares each pair once per call. The extraction's only
cross-record input is its median anchor, and leaving out one print leaves
one of a few possible anchors, so each anchor's oriented, carry-checked
observations are built once and a print only re-takes the medians its own
observation touches. Markets carry a key (pair, anchor, left-out prints,
min_trades); equal keys mean equal markets, and option_vol prices a family
once per key.

Results are unchanged. A new test checks every print's spot and forward
against extract_fx_observations + ndf_forward_curve over its peers; on a real
2026-10-08 DTCC file, 889 sampled prints (506 forwards, 383 options) gave
field-for-field the same value_fx_trade output, and all 57,536 records valued
in 5.1 s. Tests that stubbed the extraction helpers now stub _peer_curve,
the new seam, with unchanged assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Oct 9, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@artcashin

Copy link
Copy Markdown
Contributor Author

Could a maintainer approve the workflow runs for this PR? They're waiting for approval because it comes from a fork (action_required on openbb-cftc, CodeQL and the PR checks). Locally, tests/test_fx_valuation.py passes 93/93 and the openbb-cftc suite passes 1,227/1,227. Thanks!

@dlee-bq dlee-bq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @artcashin, thanks for the PR. I've left some comments in the code regarding code duplication through reimplementations. Please have a look and refactor accordingly.

Thanks

Comment thread openbb_platform/providers/cftc/openbb_cftc/utils/fx_valuation.py
Comment thread openbb_platform/providers/cftc/openbb_cftc/utils/fx_valuation.py Outdated
Comment thread openbb_platform/providers/cftc/openbb_cftc/utils/fx_valuation.py Outdated
Comment thread openbb_platform/providers/cftc/openbb_cftc/utils/fx_valuation.py Outdated
Address review: drop the leave-one-out reimplementation (_pair_view,
_anchored, _median_without and the inline forward interpolator).
fx_market now calls extract_fx_observations and ndf_forward_curve once per
pair and curve date over all of the pair's prints, package legs included,
so valuation and the vol surface use the same NDF curve. The curve, market
and option-pricing memo keys include the curve date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEna2ixXYhEGEAQdJM2D3G
@artcashin artcashin changed the title perf(cftc): price FX prints from their peers without re-extracting the pair perf(cftc): build each FX pair's forward curve once per day, shared with the vol surface Oct 12, 2026
@artcashin

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed in 70c3257:

  • Shared utility: fx_market now calls extract_fx_observations and ndf_forward_curve directly, once per pair and curve date (_pair_curve), instead of an inline copy.
  • Anchor/orient/carry re-implementation: _pair_view and _anchored are removed.
  • Median re-implementation: _median_without is removed. The two remaining median calls are the original ones in _cip_market and option_vol.
  • Vol surface and valuation on one curve with package legs: both now use ndf_forward_curve over all of the pair's prints, package legs included.
  • fx-view key without curve_date: gone. The new keys (fx-curve, fx-market, fx-options) all include the curve date. The memo is also created per day in swap_trades.

One behaviour change to confirm: since valuation now shares the vol surface's curve, a print is valued against a curve that includes itself, so the old leave-one-out exclusion, and its test test_fx_market_excludes_the_trades_own_print, are gone. On the 2026-10-08 forex file this changes 4 of 42,760 rows (details in the updated description). The endpoint takes about 24 s either way, against over an hour on develop. If you'd rather keep the own-print exclusion, I can add it back on top of the shared curve.

@dlee-bq

dlee-bq commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Hi @artcashin, sorry for the delay getting back to you on this. These functions are fiddly by nature, and the raw data is noisy to begin with. They command a higher level of scrutiny vs other provider endpoints simply returning cleaned data.

Can we please restore the own-print exclusion? It can force flat extrapolation and marks at zero.

The new memo keys carry the date, but fx-pair and fx-df do not. Let's align them all to carry the date.

As an enhacement, let's see if we can build the market in one shared function that both fx_implied_vol and fx_market call, owning the notional floor, min_trades, forward-aware discounting and the spot rule. This removes the three constants that differ today (floor 0 vs 1,000, min_trades 3 vs 1, forward-aware True vs False) and the duplicated days > 2 / days <= 9 cut-offs. Keep the $1,000 floor in both: the surface already works with it on the NDF pairs, and it protects against the single-print node problem.

Another small enhancement we can look at is to make the valued option read its vol the way the surface does.

Thanks, and let me know if you need more info or help.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants