Investigation: The FX spot conventions against the market

Table of Contents

This page records an investigation made while the story Bring the FX spot conventions onto the market standard was open.

It is a snapshot, not a description of the system. Whatever it concludes that the code must keep doing belongs on the page that owns the subject; cite that page from code, recipes and models, not this one.

1. Context

A person opened a GBP/USD series in ores.web and saw the same rate for several ticks in a row:

Instant               Value   Source
2026-10-15 21:45:10Z  1.3586  synthetic.bloomberg.gbpusd
2026-10-15 21:45:08Z  1.3586  synthetic.bloomberg.gbpusd
2026-10-15 21:45:06Z  1.3586  synthetic.bloomberg.gbpusd
2026-10-15 21:45:04Z  1.3587  synthetic.bloomberg.gbpusd
2026-10-15 21:45:02Z  1.3587  synthetic.bloomberg.gbpusd
2026-10-15 21:45:00Z  1.3587  synthetic.bloomberg.gbpusd
2026-10-15 21:44:58Z  1.3587  synthetic.bloomberg.gbpusd

The reading was that the FX spot conventions are wrong. The investigation asked two things: do our FX spot conventions agree with the market, and is the spot data our synthetic feeds write realistic. The answer is that the conventions disagree with the market in four places, that one of those is what makes the series look frozen, and that the data behind it is sound.

2. Methodology

Measured the running environment, then read the model and the display path, then researched the market convention against primary sources.

  • The environment: ores_refdata_currency_pair_conventions_tbl, ores_refdata_currencies_tbl.spot_days, ores_synthetic_fx_spot_generation_configs_tbl and ores_marketdata_market_observations_tbl in database ores_dev_eager_maxwell.
  • The model and the seed: ores.refdata.currency_pair_convention.org, ores.refdata.currency.org, projects/ores.sql/populate/refdata/refdata_currency_pair_conventions_seed_populate.sql and projects/ores.sql/populate/synthetic/synthetic_feeds_bloomberg_populate.sql.
  • The display: projects/ores.web/packages/web/src/market/seriesView.ts.
  • The market: the ECB reference rates and their SDMX DECIMALS metadata, Deutsche Bank's FX4Cash Currency Guide, CLS's T+1 material, the ISDA FX Definitions, CME and CME/EBS market-structure material, the GFXC FX Global Code, the Federal Reserve H.10 release and the BIS Triennial Survey.
  • GBP/USD realised volatility was derived from ECB daily USD/EUR and GBP/EUR reference rates over 2024-01 to 2026-10.

Environment: worktree eager_maxwell, preset linux-clang-release-ninja, database ores_dev_eager_maxwell, provisioned with the ACME tenant.

3. Paths taken

  • The series read as a data fault. The stored values were read at full precision, not as displayed: 1.3590450087953925, with consecutive ticks about 0.00002 apart, near 0.24 pip. The generator is not flat. This path was set aside.
  • The display was the fault. seriesView.ts formats every rate through the convention's decimal_places, which the seed sets to 4 for a non-JPY pair. A 0.24-pip move is invisible at 4 decimal places, so several ticks round to the same number. This path held.
  • The convention fields were compared to the market, one by one, across the six pairs held: EUR/USD, GBP/USD, AUD/USD, USD/CAD, USD/CHF and USD/JPY. Four fields disagree, and the field-by-field result is under Conclusions.
  • The spot lag was looked for and found elsewhere. currency_pair_convention has no spot-lag column at all. currency.spot_days exists and currency_pair.spot_days is derived as max(base, quote); the seed gives USD 1 and CAD 2, so USD/CAD derives to T+2. This path found the largest error.

4. Conclusions

Our conventions are right where they describe the pip and wrong where they describe the quote, the value date and the increment.

Field Our value Verdict Market value, and the source that owns it
pip_factor non-JPY 0.0001 Accurate 1 pip = 0.0001
pip_factor USD/JPY 0.01 Accurate 1 pip = 0.01
decimal_places non-JPY 4 Accurate as the pip, incomplete as a quote A live major is quoted to 5 decimal places: the pipette is 0.1 pip. The ECB publishes GBP to 5 dp and USD/CAD/CHF/AUD to 4 dp on the same day, so precision is not uniform
decimal_places USD/JPY 2 Accurate as the pip A live JPY quote carries 3
tick_size 1 pip Wrong for spot Spot minimum increments are sub-pip and set per venue; the common finest is the pipette, 0.1 pip. Our own model generator default is 0.1, so the seed and the model disagree
business_day_convention ModifiedFollowing Accurate The market-consistent choice for a value date; the model generator default of Following is the outlier
spot_relative true Accurate An FX forward is quoted off the spot date
end_of_month false Likely wrong, and not uniform Standard forward practice preserves month-end; it must stay configurable
Spot lag Not a column; derived max(base, quote) Wrong USD/CAD is T+1; so are USD/TRY, USD/PHP and USD/RUB; every other pair is T+2

The largest error is the value date. The seed gives USD spot_days 1 and CAD 2, and the max rule makes USD/CAD settle T+2, where the market settles it T+1. The same rule mis-derives USD/TRY, USD/PHP and USD/RUB. A max-of-legs rule cannot express a pair-level exception, so the lag has to be a first-class per-pair attribute.

The visible error is the quote precision. The rate is stored correctly and moving about 0.24 pip per tick, but decimal_places = 4 prints it at pip precision, so a sub-pip move does not show. This is the whole of the frozen series; a 5-decimal feed changes on most ticks.

The data itself is sound. At 9.5% annual volatility, a GBP/USD 2-second tick has a standard deviation of about 0.32 pip, so roughly 1 pip of movement in 12 seconds is about one standard deviation and not too quiet. Two caveats: the two-component mixture inflates realised variance by 1.75x, so the feed realises about 12.6% rather than the 9.5% it states, and the model annualises on a 365-day clock where the market uses about 252 trading days, which offsets most of that inflation.

Two further divergences are worth confirming rather than fixing blind: the advance_calendar must be the union of the two holiday sets (the joint business-day calendar), and JPY detection keys off the quote currency only, which misses a JPY-base pair.

5. Recommendations

  1. State the spot lag per pair. Add it to currency_pair_convention or to currency_pair, seed USD/CAD, USD/TRY, USD/PHP and USD/RUB as T+1 and the rest as T+2, and stop deriving it as the maximum of the two legs.
  2. Separate the pip from the quote. Keep pip_factor as it is and let the display precision be the market's: five decimals for a non-JPY pair and three for a JPY pair. Update Pip, tick size, and pip factor, which states the pip convention as though it were the quote precision.
  3. Take the tick size down to a fraction of a pip, per venue, matching the model generator's 0.1 and removing the seed's 1.0.
  4. Decide end_of_month deliberately, rather than defaulting it to false.
  5. Say what the volatility model realises. Either drop the inflating tail component or state the realised volatility on the page that owns the feed.
  6. Confirm advance_calendar and the JPY detection before either is relied on.

The findings were checked against the market's own sources, listed under See also.

6. See also

Emacs 29.3 (Org mode 9.6.15)