Task: Review stochastic process component tools; implement Ornstein-Uhlenbeck

Table of Contents

This page documents a task in the GMM improvements: tidy up synthetic data generation loose ends story. It captures the goal, current status, acceptance, and any notes or results.

Goal

The synthetic market data generator design (Synthetic market data generators, FX spot synthetic data PoC: architecture) catalogues several stochastic process types as component tools — GBM, Brownian/Wiener, jump, regime-mix, Ornstein-Uhlenbeck (synthetic_ou, mean-reverting) among them. Ornstein-Uhlenbeck in particular is documented but not implemented anywhere under projects/ (confirmed via grep and compass search; no separate open capture/task exists for it — this task is its home). Review which catalogued process types are implemented vs designed-only, and close the gaps, prioritising Ornstein-Uhlenbeck.

Status

Field Value
State DONE
Parent story GMM improvements: tidy up synthetic data generation loose ends
Now Done.
Waiting on Nothing.
Next Close the story (last open task).
Last touched 2026-07-03

Acceptance

  • Inventory of catalogued vs implemented stochastic process types is documented.
  • Ornstein-Uhlenbeck is implemented as a selectable process type with parameters (kappa, theta/mean level, sigma) exposed in the config UI, or its deferral is explicitly recorded with rationale.
  • Any other implementation gaps found are implemented or explicitly scoped out with rationale.

Plan

Inventory. Process types catalogued in Synthetic market data generators / FX spot synthetic data PoC: architecture vs implemented under projects/ores.synthetic/service/src/processes/: GBM/geometric (gmm_process) — implemented; arithmetic Brownian (arithmetic_gmm_process) — implemented; Ornstein-Uhlenbeck (synthetic_ou) — designed, not implemented (confirmed by grep — this task's reason for existing). Jump/Poisson processes and cross-rate correlation matrices are also designed-only, already tracked as "planned, not wired to anything" in the FxSpotRateEditor "Component Tools" disclosure panel — left as-is (out of scope; not silently dropped, already visibly flagged to users).

Implementation — Ornstein-Uhlenbeck. Added ou_process (IStochasticProcess) implementing the exact per-tick OU discretisation: X_{t+1} = θ + (X_t - θ)e^{-κ} + σ√((1-e^{-2κ})/(2κ))·Z, with κ≤0 degenerating to a driftless random walk. Wired a new "ou" branch in process_factory::make_process().

OU is a single-regime process (3 scalars: κ, θ, σ), not a K-component mixture, so rather than adding new persisted columns/protocol fields it reuses the existing GMM-component channels: weights[0] = κ, stdevs[0] = σ, and initial_price doubles as θ (reverts toward its own starting level by default). This is the same pattern already established for the "arithmetic" engine, which reuses the %-suffixed μ/σ fields for absolute price increments via a warning banner rather than relabelling fields.

UI (FxSpotRateEditor): added "Ornstein-Uhlenbeck (mean-reverting)" to the engine combo; added updateEngineUi() to swap the inline warning banner text and disable "Add process" (OU has one regime, not a mixture) while selected; switching to OU collapses the Advanced table to its first row; the weight-sum label becomes a "κ = …" echo instead of "Weight Sum = …" for OU. refreshCharts() skips normalisation for OU (κ/σ are scalars, not mixture shares) and clears the Return Distribution Preview (the increment-mixture PDF doesn't apply to a mean-reverting level process) — Sample Price Paths still renders correctly since it plots whatever the server returns regardless of engine. onSaveClicked() skips the "all weights zero" guard (κ=0 is a valid OU parameter) and the weight-normalise-to-1 step for OU, so κ persists as entered rather than being silently overwritten to 1.0.

SQL: extended the process_type check constraint (ores.synthetic.fx_spot_generation_config.org model) to allow 'ou'. Regenerating via compass codegen generate dropped unrelated hand-patched validation-function logic (template/output drift, unrelated to this change) — reverted the full regen and hand-edited just the one constraint line; filed a capture for the drift itself.

Notes

Known, deliberate limitation: the Volatility (σ) field keeps its "%" suffix and the value is still divided by 100 internally for OU, same as the pre-existing "arithmetic" engine's reuse of that field for absolute (non-%) units — cosmetically imprecise but functionally consistent with the established pattern, not fixed here to avoid widening this task's footprint.

No dedicated unit test was added for ou_process, matching the existing gmm_process=/=arithmetic_gmm_process (neither has one either) — no established Catch2 convention exists yet for this directory.

PRs

PR Title
   

Review

# Comment summary File Decision Notes
1 OU silently invalid when Simple mode stays selected — kappa derived from unrelated Drift/Vol/Jump sliders, jump component silently dropped FxSpotRateEditor.cpp Fixed 1d07dae74 — onEngineChanged forces Advanced mode + locks Simple toggle for non-mixing engines
2 UI hardcodes engine-specific isOuEngine() checks throughout (raised directly by user, not the bot review) FxSpotRateEditor.cpp Fixed 1d07dae74 — data-driven EngineInfo table (code/label/supportsMixing) replaces scattered checks
3 Switching to OU silently truncates mixture components FxSpotRateEditor.cpp Fixed (as a side effect of #1) Advanced mode is now forced open when the collapse happens, so it's visible/editable, not silent
4 Duplicated chart theme colour constants across 5 call sites ReturnDistributionChart.cpp, SamplePricePathsChart.cpp Declined (follow-up) Captured: 4EE11531-E6AB-4094-8297-F0E7DB12EC39
5 Weight/Volatility column headers and tooltips still say generic text for OU FxSpotRateEditor.cpp Fixed (superseded) c314e99cd — see #9; headers shortened and tooltips now update per engine
6 Per-component QLineSeries::setName() calls are effectively dead code (legend hidden) ReturnDistributionChart.cpp Fixed 1d07dae74 — one-line comment clarifying the swatch column is the intended indicator
7 No unit tests for ou_process ou_process.hpp/.cpp Declined Matches untested gmm_process/arithmetic_gmm_process; no Catch2 convention yet for this directory
8 Validation logic ("are these parameters good?") hardcoded in the UI instead of the engine (raised directly by user, not the bot review) FxSpotRateEditor.cpp, process_factory.cpp Fixed c314e99cd — new ores.synthetic.api::domain::validate_process_parameters(), shared by process_factory and the Qt client; server-side enforcement at save/feed-start time not yet wired up, captured: FDEAC2F6-A87F-4281-82CC-437B06D5B02F
9 Advanced component table headers too verbose, dialog congested (raised directly by user, not the bot review) FxSpotRateEditor.cpp Fixed c314e99cd — headers shortened to symbols (μ/σ/w); explanations moved to per-header tooltips, updated per engine by updateEngineUi()
10 Opening an existing "ou" record defaults to Simple mode (construction-time gap: engineCombo_->setCurrentIndex() runs before the currentIndexChanged connection, so onEngineChanged() never fires); Save without touching anything silently overwrites the persisted kappa with 1.0. Reported across three consecutive re-review rounds after #1's fix only covered the live combo-switch path FxSpotRateEditor.cpp Fixed 247eccda3 — moved the forcing logic from onEngineChanged() into updateEngineUi(), which runs both on every engine change and once at construction time

Result

Ornstein-Uhlenbeck is implemented end-to-end: ou_process (core numerics), process_factory dispatch, SQL constraint, and UI (engine selector, reused parameter fields, adjusted validation/save/preview paths for its single-regime shape). Other designed-only process types (jump/Poisson, correlation matrices) reviewed and left explicitly scoped out — already flagged to users in the existing "Component Tools" panel, not silently dropped. Built ores.qt.synthetic.lib, ores.synthetic.service.lib and ores.marketdata.service.lib clean (clang debug/make). Also fixed, on user report during this task: hardcoded change-reason-code string literals replaced with ores.dq.api constants across FeedDialog.cpp, MarketSimulatorWindow.cpp, FxSpotRateEditor.cpp; and synthetic market observations were missing source=/=point_id (tenor) — feed_ingest_loop.cpp now captures source_name and sets point_id = "SPOT" for scalar FX spot series.

PR #1409 review round 1 addressed: forced Advanced mode for non-mixing engines (closing a real correctness gap — Simple mode could previously save a meaningless κ and silently drop jump components); replaced scattered isOuEngine() checks with a data-driven EngineInfo table so future single-regime engines (rate processes) are a table row, not more special-cased UI logic; moved parameter validation out of the UI into a shared ores.synthetic.api::domain::validate_process_parameters() used by both process_factory and the Qt client; and shortened the Advanced table's headers to symbols with per-engine tooltips instead of permanent verbose text, addressing dialog congestion.

Emacs 29.3 (Org mode 9.6.15)