Skip to content

perf(cftc): price FX prints from their peers without re-extracting the pair - #7750

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

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

Conversation

@artcashin

Copy link
Copy Markdown

Problem

fx_market prices each FX print from the pair's other prints, leaving the print itself out. It re-extracts every peer record, twice, for each print. The swap_trades forex priceability gate calls it for every print of the day, so one GET /cftc/swap_trades?asset_class=forex is 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 has the same shape: it re-extracts and re-implies every vanilla option of the pair for each option print.

Change

fx_market now prepares each pair once per call, with the same results.

  • One extraction per anchor. The extraction's only cross-record input is its median anchor. Leaving one print out of a sorted list leaves one of a few possible medians, so each anchor's oriented, carry-checked observations are built once.
  • Only the print's own medians are recomputed. A print removes its own observation and re-takes only the per-day and near-dated medians it touches. The curve interpolation and the CIP fallback are unchanged.
  • Option pricing is cached per market. Markets carry a key (pair, anchor, left-out prints, min_trades), and equal keys mean equal markets. Option prints never enter the forward curve, so all option prints of a pair share one market, and option_vol prices each family once per key.

Results unchanged

  • New test, test_fx_market_matches_the_leave_one_out_curve: on 61 synthetic KRW prints, including a shared identifier and prints outside the carry bound, every print's spot and forward at six tenors equals extract_fx_observations + ndf_forward_curve over its peers, exactly.
  • New test, test_option_vol_prices_a_family_once_per_market.
  • Real data: on the 2026-10-08 DTCC forex file, 889 sampled prints (506 forwards, 383 options across 43 pair/type groups) give field-for-field the same value_fx_trade output as before.

Speed

On the same file:

Before After
Valuing all 57,536 records about 21 min for the curves alone 5.1 s
The forex swap_trades endpoint over 60 min 55–76 s, most of it building the 40 MB response

Tests

Eight existing tests stubbed extract_fx_observations and ndf_forward_curve, which fx_market no longer calls. They now stub _peer_curve, the new seam, through the shared _patch_market helper, with their assertions unchanged. The own-print exclusion and memo tests now use real records. tests/test_fx_valuation.py passes 93/93, and the CFTC suite passes 1,227/1,227.

🤖 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
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

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

return _cip_market(peers, rates_records, trade, curve_date, min_trades, memo)
return _cip_market(curve["cip_near"], rates_records, trade, curve_date, min_trades, memo, key)

spot, forward = ndf_forward_curve(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The shared utility function was removed and replaced with an inline copy, making maintenence more difficult. For consistency, use the shared utility. While we are at it, can you please align the Vol Surface and Valuation to use the same NDF curve - with package legs?

return at(mid) if n % 2 else (at(mid - 1) + at(mid)) / 2


def _pair_view(fx_records: Iterable[dict], trade: dict, curve_date: dateType, memo: dict) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This, along with _anchored, re-implement the anchor, orient, and carry-check steps of extract_fx_observations. Let's remove re-implementations.

Comment on lines +283 to +284

def _median_without(ordered: list[float], removed: list[float]) -> float | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reimplements statistics.median, which is already part of extract_fx_observations. Let's remove the redundancies.

Comment on lines +317 to +329
def _pair_view(fx_records: Iterable[dict], trade: dict, curve_date: dateType, memo: dict) -> dict:
"""A pair's curve observations, prepared once per day for leave-one-out pricing.

fx_market prices each print from its peers (the pair's other prints).
Re-extracting every peer per print is quadratic in a busy pair's day. 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 each print only adjusts the
medians its own observation touches.
"""
from openbb_cftc.utils.fx import _fx_observation, resolve_pair

key = f"fx-view:{trade['pair']}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

fx-view:{pair} is keyed without curve_date, so the first call’s date fixes the tenors for the memo’s life.

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

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.

3 participants