Skip to content

Refactor architecture and integrate Smith-Wilson extrapolation - #120

Merged
sujitn merged 16 commits into
mainfrom
refactor-curve-architecture
Jun 12, 2026
Merged

Refactor architecture and integrate Smith-Wilson extrapolation#120
sujitn merged 16 commits into
mainfrom
refactor-curve-architecture

Conversation

@sujitn

@sujitn sujitn commented Jun 12, 2026

Copy link
Copy Markdown
Owner

This PR removes the convex-traits crate by consolidating ports into convex-engine/src/ports, and updates BuiltCurve to incorporate Smith-Wilson extrapolation and Monotone Convex interpolation natively within the pricing router.

Summary by CodeRabbit

  • New Features

    • Z-spread calculator supports configurable compounding conventions.
    • UFR-convergence extrapolation method added for curve tails.
  • Improvements

    • Asset-swap and proceeds calculations now use actual bond cash flows for more accurate results.
    • Curve building now caches/rebuilds internal representations for more reliable interpolation and faster repricing.
    • Ports/module surface reorganized for clearer engine/adapter integration.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92c15558-4fda-4686-9cee-721e7dc2a3bf

📥 Commits

Reviewing files that changed from the base of the PR and between e113ea0 and 0e48459.

📒 Files selected for processing (9)
  • crates/convex-analytics/src/spreads/asw/mod.rs
  • crates/convex-analytics/src/spreads/asw/par_par.rs
  • crates/convex-analytics/src/spreads/asw/proceeds.rs
  • crates/convex-analytics/src/spreads/zspread.rs
  • crates/convex-curves/src/curves/discrete.rs
  • crates/convex-engine/src/curve_builder.rs
  • crates/convex-math/src/extrapolation/mod.rs
  • crates/convex-math/src/lib.rs
  • crates/convex-ports/src/lib.rs
💤 Files with no reviewable changes (1)
  • crates/convex-math/src/extrapolation/mod.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • crates/convex-ports/src/lib.rs
  • crates/convex-math/src/lib.rs
  • crates/convex-analytics/src/spreads/asw/mod.rs
  • crates/convex-analytics/src/spreads/zspread.rs
  • crates/convex-analytics/src/spreads/asw/par_par.rs
  • crates/convex-analytics/src/spreads/asw/proceeds.rs
  • crates/convex-engine/src/curve_builder.rs

📝 Walkthrough

Walkthrough

Removes the convex-traits crate, adds convex-ports and convex-core IDs, rewires engine/adapters/tests to new ports/ID locations, adds UFR-convergence extrapolation and BuiltCurve inner caching, updates monotone-convex math, and refactors analytics to use bond cash flows and compounding-aware z-spreads.

Changes

Port and type migration

Layer / File(s) Summary
Workspace and crate manifest rewiring
Cargo.toml, crates/convex-engine/Cargo.toml, crates/convex-ext-file/Cargo.toml, crates/convex-ext-redb/Cargo.toml, crates/convex-server/Cargo.toml
Workspace removes convex-traits and adds convex-ports; engine, adapters, and server switch dependencies from convex-traits path to convex-ports workspace.
Core ID module exposure and Currency types
crates/convex-core/src/lib.rs, crates/convex-core/src/ids.rs
convex-core now exports public ids module; CurrencyPair uses crate::Currency references instead of convex_core::Currency.
Convex-ports crate structure
crates/convex-ports/Cargo.toml, crates/convex-ports/src/lib.rs, crates/convex-ports/src/market_data.rs, crates/convex-ports/src/output.rs, crates/convex-ports/src/reference_data.rs, crates/convex-ports/src/storage.rs
New convex-ports crate provides trait definitions for engine ports; all port modules import IDs from convex_core instead of local crate::ids.
Engine lib and port re-export
crates/convex-engine/src/lib.rs
Engine imports engine ports from crate::ports and re-exports convex_ports as ports for downstream access.
PricingEngine API migration
crates/convex-engine/src/lib.rs
All PricingEngine reactive handler and registration methods change ID parameter types from convex_traits::ids to convex_core::ids and config types to crate::ports::config.
Engine internal modules migration
crates/convex-engine/src/builder.rs, crates/convex-engine/src/error.rs, crates/convex-engine/src/cache.rs, crates/convex-engine/src/calc_graph.rs, crates/convex-engine/src/context.rs, crates/convex-engine/src/etf_pricing.rs, crates/convex-engine/src/portfolio_analytics.rs, crates/convex-engine/src/market_data_listener.rs
All modules switch imports from convex_traits to crate::ports for trait/config types and convex_core for IDs; market_data_listener introduces PublishResult type alias and refactors publish methods.
Pricing router port and ID migration
crates/convex-engine/src/pricing_router.rs
Router imports bond types from crate::ports, updates floating-rate index types to convex_core, refactors CUSIP extraction with as_deref(), and calls rebuild_inner() after curve mutations.
Reactive engine migration
crates/convex-engine/src/reactive.rs
Reactive engine switches bond cache and public APIs to use crate::ports reference data types; internal helpers and tests updated for new type paths.
External adapters and file extensions migration
crates/convex-ext-file/Cargo.toml, crates/convex-ext-file/src/lib.rs, crates/convex-ext-file/src/market_data.rs, crates/convex-ext-file/src/reference_data.rs, crates/convex-ext-redb/Cargo.toml, crates/convex-ext-redb/src/lib.rs
File and redb adapters switch from convex_traits to convex_ports trait dependencies and update ID imports to convex_core.
Server and test consumer migration
crates/convex-server/src/handlers.rs, crates/convex-server/src/main.rs, crates/convex-server/src/websocket.rs, crates/convex-server/tests/api_integration_tests.rs, crates/convex-server/tests/websocket_integration_tests.rs
Server modules and tests switch imports to convex_engine::ports and convex_core; server handler curves initialize with inner: None and call rebuild_inner().

Curve infrastructure with UFR convergence and cached internals

Layer / File(s) Summary
ExtrapolationMethod and UFR convergence model
crates/convex-curves/src/lib.rs, crates/convex-curves/src/curves/discrete.rs, crates/convex-math/src/extrapolation/mod.rs, crates/convex-math/src/extrapolation/linear.rs, crates/convex-math/src/extrapolation/ufr_convergence.rs
ExtrapolationMethod gains UfrConvergence variant; DiscreteCurve applies UFR extrapolation logic; new UfrConvergence struct implements Extrapolator trait with heuristic forward-rate convergence and unit tests.
MonotoneConvex quadratic Hagan-West formula
crates/convex-math/src/interpolation/monotone_convex.rs
MonotoneConvex removes g monotonicity field, replaces forward rate linear interpolation with quadratic formula, and updates interpolate to use exact integral-based zero-rate computation.
Math crate prelude and documentation
crates/convex-math/src/lib.rs
Math crate documentation and prelude updated to prefer UfrConvergence over SmithWilson.
BuiltCurve with cached DiscreteCurve
crates/convex-engine/src/curve_builder.rs
BuiltCurve adds inner field for optional cached DiscreteCurve, introduces rebuild_inner() method to construct/update cache from points, CurveBuilder adds extrapolation configuration, and all curve building paths initialize inner: None then rebuild.
Curve cache usage in benchmarks and server
crates/convex-engine/benches/pricing_benchmarks.rs, crates/convex-engine/src/pricing_router.rs, crates/convex-server/src/handlers.rs
Benchmarks and server handlers initialize BuiltCurve with inner: None and call rebuild_inner() before use; pricing router calls rebuild_inner() after point mutations.

Analytics calculation updates around bond cash flows

Layer / File(s) Summary
Coupon year fraction computation helper
crates/convex-analytics/src/spreads/asw/mod.rs
New coupon_year_fraction helper computes period year fractions from accrual dates or falls back to nominal frequency and parsed day-count convention.
Par-par asset swap cash flow refactor
crates/convex-analytics/src/spreads/asw/par_par.rs
Par-par asset swap derives annuity and coupon-vs-forward mismatch from bond.cash_flows(settlement) instead of synthetic dates; iterates coupon flows with per-flow tau via coupon_year_fraction; test MockBond emits coupon schedules.
Proceeds asset swap cash flow refactor
crates/convex-analytics/src/spreads/asw/proceeds.rs
Proceeds asset swap rewires annuity to pull coupon cash flows, discount each date, apply per-flow tau, and sum into annuity; test mock bond generates coupon schedules.
Z-spread compounding control
crates/convex-analytics/src/spreads/zspread.rs
ZSpreadCalculator adds compounding field with with_compounding setter, applies compounding-specific spread discount in solver, and z_spread wrapper derives convention from bond coupon frequency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sujitn/convex#112: Related work building on analytics DTOs and FFI/FFI-adjacent request shapes touching analytics.
  • sujitn/convex#82: Overlaps ASW par-par spread logic and coupon-vs-forward mismatch changes.

Poem

🐰 In ports we trust, traits rearranged,

Curves now whisper UFR, neatly ranged.
Cash flows hop, each coupon in parade,
Spreads and compounding freshly laid.
A rabbit cheered — the refactor made!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main architectural refactoring: removing convex-traits and integrating Smith-Wilson extrapolation into curve handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor-curve-architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/convex-math/src/extrapolation/smith_wilson.rs (1)

190-216: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

last_derivative is ignored, so this still fails CI and drops the LLP slope information.

The current implementation does two bad things at once: it hard-fails cargo doc/clippy because last_derivative is unused, and it discards the boundary slope that DiscreteCurve now threads through for the extrapolation handoff. That means the post-LLP curve is generally not tangent to the in-range curve even though the API suggests it should be. At minimum this needs to stop passing an unused parameter; the real fix is to make the extrapolated shape honor the boundary derivative.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-math/src/extrapolation/smith_wilson.rs` around lines 190 - 216,
The extrapolate method currently ignores the last_derivative parameter and thus
fails linting and loses slope continuity; update extrapolate to incorporate
last_derivative so the extrapolated zero-rate is tangent at last_t: keep
existing alpha, tau, convergence and ufr_implied calculations, but add a local
linear tangent target value = last_value + last_derivative * tau (or adjust for
log/zero-rate semantics if needed) and blend between that tangent target and the
ufr_implied value (using the same convergence factor) so at tau->0 you recover
last_value and slope, and as tau grows you converge to UFR; this both uses
last_derivative (removing the unused-variable error) and ensures the
extrapolated curve matches the boundary slope from DiscreteCurve.

Source: Pipeline failures

crates/convex-engine/src/pricing_router.rs (1)

1656-1675: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This helper no longer matches the flat-extrapolation assertion later in the test module.

Calling rebuild_inner() switches this test curve to MonotoneConvex + Smith-Wilson behavior, so extrapolation past 30Y will converge away from the last pillar instead of staying flat. test_built_curve_interpolation() still expects the old flat result on Line 2364, so this helper change leaves that test out of sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-engine/src/pricing_router.rs` around lines 1656 - 1675, The
helper constructs a BuiltCurve with explicit points but then calls
rebuild_inner(), which converts it to a MonotoneConvex/Smith-Wilson
representation and changes extrapolation behavior; revert this by removing the
call to BuiltCurve::rebuild_inner() (leave built.inner as None) so the
test_built_curve_interpolation() continues to see flat extrapolation past the
last 30Y pillar, or alternatively update the test expectations to match the new
monotone/Smith-Wilson extrapolation if that behavior is intended; locate the
helper that creates BuiltCurve (the block that sets curve_id, reference_date,
points, built_at, inputs_hash, inner) and remove the built.rebuild_inner()
invocation.
crates/convex-server/tests/websocket_integration_tests.rs (1)

70-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the fixed sleeps from the websocket tests.

These 50–100ms waits make the suite timing-dependent; on a slower CI runner the server may not be ready yet or the broadcast may arrive after the assertion window. Replace them with a readiness poll or a deterministic event from the server.

Also applies to: 469-470, 557-558, 659-660, 761-762

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-server/tests/websocket_integration_tests.rs` around lines 70 -
80, The fixed tokio::time::sleep calls (e.g., after TcpListener::bind / before
connecting to the test server) make the tests timing-dependent; remove those
sleeps and replace them with an explicit readiness check—either have the spawned
server send a ready signal via a oneshot channel (create a
tokio::sync::oneshot::channel and send after the axum server is ready) or
poll-connect in a loop (attempt TcpStream::connect to listener.local_addr() with
a small sleep/retry until success or timeout). Update the instances around
TcpListener::bind, tokio::spawn/axum::serve and the other similar locations
(lines referenced) to use the readiness signal/poll instead of
tokio::time::sleep.
🧹 Nitpick comments (1)
crates/convex-server/tests/websocket_integration_tests.rs (1)

26-47: Ports API types still match the struct-literal setup (MarketDataProvider/ReferenceDataProvider/EngineConfig).

MarketDataProvider and ReferenceDataProvider are still concrete pub structs (not traits) with Arc<dyn ...> fields, and EngineConfig remains a pub struct with impl Default, so this test’s struct-literal construction matches the ports API shape.

  • Replace fixed sleep-based timing in the websocket integration tests with a readiness/polling condition to reduce intermittent CI flakiness.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-server/tests/websocket_integration_tests.rs` around lines 26 -
47, The tests use fixed sleep delays (e.g., tokio::time::sleep or
std::thread::sleep) in the websocket integration tests which causes flaky CI;
replace those sleeps with a readiness/polling loop that waits for a concrete
condition (socket connected, handshake completed, or specific message received)
with a bounded timeout. Locate the websocket test code and replace calls to
sleep with a loop that polls the actual readiness predicate (checking the
connection state, reading from the test socket, or awaiting a specific server
response) using tokio::time::interval or tokio::time::timeout and fail the test
if the timeout elapses; reference the existing test helpers and the
create_test_engine function to access the engine/socket and ensure the readiness
check ties to an observable state rather than elapsed time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/convex-analytics/src/spreads/asw/par_par.rs`:
- Around line 197-210: calculate_annuity() currently uses a hard-coded
year_fraction (1 / coupon_frequency) while calculate() and implied_price()
accumulate annuity using per-cash-flow accrual-aware tau values, causing
mismatch on stub/irregular schedules; refactor by extracting the per-cash-flow
tau computation into a shared helper (e.g., compute_tau_for_cashflow or
tau_for(cf, payments_per_year)) and replace the hard-coded year_fraction in
calculate_annuity() with calls to that helper so calculate(), implied_price(),
and annuity() all use the same tau logic when iterating cash_flows and computing
annuity.
- Around line 153-164: The computed first-period accrual fraction `tau` uses
cashflow accrual_start/accrual_end directly (see the `tau` match using
`cf.accrual_start`/`cf.accrual_end` and `payments_per_year`), causing the first
post-settlement term to be measured from the coupon period start instead of from
`settlement`; clamp the accrual window start to `settlement` when computing
`days` for the first accrual window so the first ASW period equals the traded
stub (i.e., replace `start` with `max(start, settlement)` or otherwise take `let
start = if start < settlement { settlement } else { start }` before
`start.days_between(&end)`), and apply the same change in both `par_par.rs` and
`proceeds.rs`.

In `@crates/convex-analytics/src/spreads/zspread.rs`:
- Around line 96-100: The docs currently claim Z-spread pricing uses exp(-Z × t)
but the builder's with_compounding method (and its compounding field of type
convex_core::types::Compounding) supports simple and periodic compounding too;
update the public documentation for the Z-spread type and the with_compounding()
method to list the exact discount adjustments for each Compounding variant
(e.g., continuous: exp(-Z * t), simple: 1 / (1 + Z * t), periodic (n
periods/year): (1 + Z/n)^(-n*t) or equivalent), so callers understand which
formula is applied for each compounding mode.
- Around line 171-179: The pricing paths (price_with_spread and
calculate_from_cash_flows) currently use hard-coded continuous discounting while
calculate() respects self.compounding; extract the compounding match into a
shared helper (e.g., a method or private fn like spread_df(&self, z: f64, dt:
f64) -> f64) that implements the match on convex_core::types::Compounding
(Continuous, Simple, others using periods_per_year()) and then replace the
inline matches in calculate(), price_with_spread(), and
calculate_from_cash_flows() to call this helper so all pricing and DV01 helpers
honor self.compounding consistently.

In `@crates/convex-core/src/types/currency.rs`:
- Around line 3-6: Remove the unused import USCalendar to fix the -D warnings
compilation failure: in crates/convex-core/src/types/currency.rs delete the
USCalendar entry from the use list so only actually used calendars (e.g.,
Calendar, JapanCalendar, SIFMACalendar, Target2Calendar, UKCalendar,
WeekendCalendar) remain; verify default_calendar (which uses SIFMACalendar for
USD) and any other references still compile cleanly after removing USCalendar.

In `@crates/convex-curves/src/curves/discrete.rs`:
- Around line 289-298: The Smith-Wilson branch in
ExtrapolationMethod::SmithWilson is blindly passing DiscreteCurve values
(last_value, last_derivative from interpolator.derivative at max_tenor) as if
they are continuously compounded zero rates; restrict or convert: either
validate that the DiscreteCurve is a zero-rate curve and return Err or panic
early when used on non-zero-rate representations, or convert the terminal
value/derivative to zero-rate form before calling sw.extrapolate and convert the
extrapolated result back to the curve's representation. To convert
discount-factor-like values use r = -ln(df)/T and dr/dT = -(d(df)/dT)/(df*T) (or
compute derivative from last_derivative accordingly) at llp (max_tenor), call
sw.extrapolate with r and dr/dT, then map the returned zero-rate back to
discount factor via df = exp(-r*T); implement this check/convert around the code
using DiscreteCurve, ExtrapolationMethod::SmithWilson, last_value,
last_derivative, interpolator.derivative, sw.extrapolate and max_tenor.

In `@crates/convex-engine/src/cache.rs`:
- Around line 9-10: Run rustfmt by executing cargo fmt --all to fix import
formatting and ordering issues in the file that imports InstrumentId and
RawQuote (the use statements referencing convex_core::ids::InstrumentId and
crate::ports::market_data::RawQuote); after formatting, commit the changes and
re-run CI to ensure the import formatting check passes.

In `@crates/convex-engine/src/calc_graph.rs`:
- Around line 39-41: Import ordering is not rustfmt-compliant causing CI
failures; run rustfmt (cargo fmt) on calc_graph.rs to reorder the use statements
so they follow rustfmt's canonical ordering (group and alphabetize imports),
ensuring the lines importing Date, crate::ports::config::{NodeConfig,
UpdateFrequency}, and convex_core::ids::* are reordered/formatted accordingly;
commit the formatted file to unblock the pipeline.

In `@crates/convex-engine/src/context.rs`:
- Around line 7-8: Imports in context.rs are failing CI formatting checks; run
the Rust formatter to reorder and format imports. Execute `cargo fmt --all` (or
run your IDE's Rustfmt) to apply the required import formatting so lines like
`use convex_core::ids::{CurveId, InstrumentId};` and `use
crate::ports::market_data::RawQuote;` match project style and pass CI.

In `@crates/convex-engine/src/curve_builder.rs`:
- Around line 40-43: interpolate_rate currently trusts self.inner if present but
rebuild_inner only replaces self.inner on success, so stale Arc<DiscreteCurve>
can persist after points change; modify rebuild_inner to set self.inner = None
at the start of the rebuild attempt (clear the cached Arc<DiscreteCurve> before
validating/constructing a new one) and change rebuild_inner's signature to
return a Result indicating success or failure so callers (and interpolate_rate)
can react to rebuild errors rather than silently using the old curve; update
caller sites of rebuild_inner and any logic in interpolate_rate to handle the
Result and avoid using self.inner when rebuild failed.

In `@crates/convex-math/src/interpolation/monotone_convex.rs`:
- Around line 208-214: forward_rate() clamps negative forward rates to zero but
interpolate() currently integrates the unclamped quadratic (using variables
f_discrete, f_lo, f_hi and x), allowing the integrated curve z(t) to be built
from negative area; update interpolate() (and the second occurrence around the
other block) to compute the quadratic value (e.g., f_raw = f_discrete + ... )
and then apply the same non-negative clamp (f = f_raw.max(0.0)) before returning
or using it so that interpolate() and forward_rate() describe the same
non-negative curve, preserving
InterpolationMethod::guarantees_positive_forwards(). Ensure both occurrences use
the clamped value.

---

Outside diff comments:
In `@crates/convex-engine/src/pricing_router.rs`:
- Around line 1656-1675: The helper constructs a BuiltCurve with explicit points
but then calls rebuild_inner(), which converts it to a
MonotoneConvex/Smith-Wilson representation and changes extrapolation behavior;
revert this by removing the call to BuiltCurve::rebuild_inner() (leave
built.inner as None) so the test_built_curve_interpolation() continues to see
flat extrapolation past the last 30Y pillar, or alternatively update the test
expectations to match the new monotone/Smith-Wilson extrapolation if that
behavior is intended; locate the helper that creates BuiltCurve (the block that
sets curve_id, reference_date, points, built_at, inputs_hash, inner) and remove
the built.rebuild_inner() invocation.

In `@crates/convex-math/src/extrapolation/smith_wilson.rs`:
- Around line 190-216: The extrapolate method currently ignores the
last_derivative parameter and thus fails linting and loses slope continuity;
update extrapolate to incorporate last_derivative so the extrapolated zero-rate
is tangent at last_t: keep existing alpha, tau, convergence and ufr_implied
calculations, but add a local linear tangent target value = last_value +
last_derivative * tau (or adjust for log/zero-rate semantics if needed) and
blend between that tangent target and the ufr_implied value (using the same
convergence factor) so at tau->0 you recover last_value and slope, and as tau
grows you converge to UFR; this both uses last_derivative (removing the
unused-variable error) and ensures the extrapolated curve matches the boundary
slope from DiscreteCurve.

In `@crates/convex-server/tests/websocket_integration_tests.rs`:
- Around line 70-80: The fixed tokio::time::sleep calls (e.g., after
TcpListener::bind / before connecting to the test server) make the tests
timing-dependent; remove those sleeps and replace them with an explicit
readiness check—either have the spawned server send a ready signal via a oneshot
channel (create a tokio::sync::oneshot::channel and send after the axum server
is ready) or poll-connect in a loop (attempt TcpStream::connect to
listener.local_addr() with a small sleep/retry until success or timeout). Update
the instances around TcpListener::bind, tokio::spawn/axum::serve and the other
similar locations (lines referenced) to use the readiness signal/poll instead of
tokio::time::sleep.

---

Nitpick comments:
In `@crates/convex-server/tests/websocket_integration_tests.rs`:
- Around line 26-47: The tests use fixed sleep delays (e.g., tokio::time::sleep
or std::thread::sleep) in the websocket integration tests which causes flaky CI;
replace those sleeps with a readiness/polling loop that waits for a concrete
condition (socket connected, handshake completed, or specific message received)
with a bounded timeout. Locate the websocket test code and replace calls to
sleep with a loop that polls the actual readiness predicate (checking the
connection state, reading from the test socket, or awaiting a specific server
response) using tokio::time::interval or tokio::time::timeout and fail the test
if the timeout elapses; reference the existing test helpers and the
create_test_engine function to access the engine/socket and ensure the readiness
check ties to an observable state rather than elapsed time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 809b2bd6-b5f1-4ba2-9ee4-138d6ecb8f3b

📥 Commits

Reviewing files that changed from the base of the PR and between 540a9ff and 52f8ee5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (50)
  • Cargo.toml
  • crates/convex-analytics/src/spreads/asw/par_par.rs
  • crates/convex-analytics/src/spreads/asw/proceeds.rs
  • crates/convex-analytics/src/spreads/zspread.rs
  • crates/convex-core/src/ids.rs
  • crates/convex-core/src/lib.rs
  • crates/convex-core/src/types/currency.rs
  • crates/convex-curves/src/curves/discrete.rs
  • crates/convex-curves/src/lib.rs
  • crates/convex-engine/Cargo.toml
  • crates/convex-engine/benches/pricing_benchmarks.rs
  • crates/convex-engine/src/builder.rs
  • crates/convex-engine/src/cache.rs
  • crates/convex-engine/src/calc_graph.rs
  • crates/convex-engine/src/context.rs
  • crates/convex-engine/src/curve_builder.rs
  • crates/convex-engine/src/error.rs
  • crates/convex-engine/src/etf_pricing.rs
  • crates/convex-engine/src/lib.rs
  • crates/convex-engine/src/market_data_listener.rs
  • crates/convex-engine/src/portfolio_analytics.rs
  • crates/convex-engine/src/ports/config.rs
  • crates/convex-engine/src/ports/error.rs
  • crates/convex-engine/src/ports/market_data.rs
  • crates/convex-engine/src/ports/mock.rs
  • crates/convex-engine/src/ports/mod.rs
  • crates/convex-engine/src/ports/output.rs
  • crates/convex-engine/src/ports/reference_data.rs
  • crates/convex-engine/src/ports/storage.rs
  • crates/convex-engine/src/pricing_router.rs
  • crates/convex-engine/src/reactive.rs
  • crates/convex-engine/src/scheduler.rs
  • crates/convex-ext-file/Cargo.toml
  • crates/convex-ext-file/src/lib.rs
  • crates/convex-ext-file/src/market_data.rs
  • crates/convex-ext-file/src/reference_data.rs
  • crates/convex-ext-redb/Cargo.toml
  • crates/convex-ext-redb/src/lib.rs
  • crates/convex-ffi/Cargo.toml
  • crates/convex-math/src/extrapolation/smith_wilson.rs
  • crates/convex-math/src/interpolation/monotone_convex.rs
  • crates/convex-mcp/Cargo.toml
  • crates/convex-server/Cargo.toml
  • crates/convex-server/src/handlers.rs
  • crates/convex-server/src/main.rs
  • crates/convex-server/src/websocket.rs
  • crates/convex-server/tests/api_integration_tests.rs
  • crates/convex-server/tests/websocket_integration_tests.rs
  • crates/convex-traits/Cargo.toml
  • crates/convex-traits/src/lib.rs
💤 Files with no reviewable changes (2)
  • crates/convex-traits/src/lib.rs
  • crates/convex-traits/Cargo.toml

Comment thread crates/convex-analytics/src/spreads/asw/par_par.rs Outdated
Comment thread crates/convex-analytics/src/spreads/asw/par_par.rs
Comment thread crates/convex-analytics/src/spreads/zspread.rs
Comment thread crates/convex-analytics/src/spreads/zspread.rs Outdated
Comment thread crates/convex-core/src/types/currency.rs Outdated
Comment thread crates/convex-engine/src/cache.rs Outdated
Comment thread crates/convex-engine/src/calc_graph.rs Outdated
Comment thread crates/convex-engine/src/context.rs Outdated
Comment thread crates/convex-engine/src/curve_builder.rs
Comment on lines +208 to 214
let f_discrete = self.discrete_forwards[i];

let f = f_discrete
+ (f_lo - f_discrete) * (1.0 - 4.0 * x + 3.0 * x * x)
+ (f_hi - f_discrete) * (-2.0 * x + 3.0 * x * x);

Ok(f.max(0.0)) // Ensure non-negative

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

forward_rate() and interpolate() can now describe different curves.

forward_rate() clamps negative values to zero, but interpolate() integrates the unclamped quadratic. Once this new unconstrained shape dips below zero, callers can observe non-negative forwards while z(t) is still built from negative forward area. That breaks the positive-forward contract exposed by InterpolationMethod::guarantees_positive_forwards() and makes downstream differentiation inconsistent.

Also applies to: 294-306

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-math/src/interpolation/monotone_convex.rs` around lines 208 -
214, forward_rate() clamps negative forward rates to zero but interpolate()
currently integrates the unclamped quadratic (using variables f_discrete, f_lo,
f_hi and x), allowing the integrated curve z(t) to be built from negative area;
update interpolate() (and the second occurrence around the other block) to
compute the quadratic value (e.g., f_raw = f_discrete + ... ) and then apply the
same non-negative clamp (f = f_raw.max(0.0)) before returning or using it so
that interpolate() and forward_rate() describe the same non-negative curve,
preserving InterpolationMethod::guarantees_positive_forwards(). Ensure both
occurrences use the clamped value.

@cubic-dev-ai cubic-dev-ai Bot 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.

14 issues found across 51 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/convex-curves/src/curves/discrete.rs">

<violation number="1" location="crates/convex-curves/src/curves/discrete.rs:294">
P1: The Smith-Wilson branch passes `last_value` directly to the extrapolator assuming it is a continuously compounded zero rate. However, `DiscreteCurve` can also represent `DiscountFactor` or other value types. For non-zero-rate curves, this produces numerically incorrect extrapolations. Either validate the `ValueType` up front (rejecting non-zero-rate) or convert through a zero-rate representation before calling Smith-Wilson.</violation>

<violation number="2" location="crates/convex-curves/src/curves/discrete.rs:296">
P2: Passing unvalidated `alpha` to `SmithWilson::new`, which panics if alpha ≤ 0. Since `ExtrapolationMethod::SmithWilson(ufr, alpha)` has no construction-time validation, a zero or negative alpha will cause a runtime panic on extrapolation rather than an error result.</violation>
</file>

<file name="crates/convex-analytics/src/spreads/zspread.rs">

<violation number="1" location="crates/convex-analytics/src/spreads/zspread.rs:171">
P1: Compounding is applied only in `calculate`, while repricing/risk paths still assume continuous discounting, causing inconsistent Z-spread and DV01 behavior when non-continuous compounding is selected.</violation>
</file>

<file name="crates/convex-analytics/src/spreads/asw/par_par.rs">

<violation number="1" location="crates/convex-analytics/src/spreads/asw/par_par.rs:150">
P2: `calculate_annuity()` hard-codes `year_fraction = 1/payments_per_year` for every period, but `annuity_and_mismatch_pct()` now uses accrual-aware `tau` values. On stub or irregular schedules the public `annuity()` result will disagree with the annuity used in `calculate()` and `implied_price()`. Extract the tau computation into a shared helper and use it in both paths.</violation>
</file>

<file name="crates/convex-core/src/types/currency.rs">

<violation number="1" location="crates/convex-core/src/types/currency.rs:214">
P2: Several G10 currencies (CHF, CAD, AUD, NZD, SEK, NOK) fall back to `WeekendCalendar`, which only excludes weekends — no national holidays. In a pricing context this can produce incorrect business-day rolling, settlement dates, and curve interpolation for these currencies. Consider adding specific calendars or using `JointCalendar`/`CustomCalendarBuilder` for at least the G10 currencies.</violation>
</file>

<file name="crates/convex-math/src/interpolation/monotone_convex.rs">

<violation number="1" location="crates/convex-math/src/interpolation/monotone_convex.rs:300">
P1: The new quadratic forward interpolation can become negative without monotonicity limiting, and `interpolate()` integrates those negative values while `forward_rate()` clamps them, creating inconsistent curve behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/convex-curves/src/curves/discrete.rs
Comment thread crates/convex-analytics/src/spreads/zspread.rs Outdated
Comment thread crates/convex-engine/src/curve_builder.rs Outdated
Ok(z_final)

// Exact integral of the Hagan-West quadratic forward rate
let integral_x = (x - 2.0 * x * x + x * x * x) * f_lo

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The new quadratic forward interpolation can become negative without monotonicity limiting, and interpolate() integrates those negative values while forward_rate() clamps them, creating inconsistent curve behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/convex-math/src/interpolation/monotone_convex.rs, line 300:

<comment>The new quadratic forward interpolation can become negative without monotonicity limiting, and `interpolate()` integrates those negative values while `forward_rate()` clamps them, creating inconsistent curve behavior.</comment>

<file context>
@@ -312,25 +288,22 @@ impl Interpolator for MonotoneConvex {
-        Ok(z_final)
+        
+        // Exact integral of the Hagan-West quadratic forward rate
+        let integral_x = (x - 2.0 * x * x + x * x * x) * f_lo
+                       + (-x * x + x * x * x) * f_hi
+                       + (3.0 * x * x - 2.0 * x * x * x) * f_discrete;
</file context>

Comment thread crates/convex-mcp/Cargo.toml Outdated
Comment thread crates/convex-analytics/src/spreads/asw/proceeds.rs Outdated
Comment thread crates/convex-analytics/src/spreads/asw/par_par.rs
Comment thread crates/convex-engine/src/calc_graph.rs
Comment thread crates/convex-ext-file/Cargo.toml Outdated
Comment thread crates/convex-ext-redb/Cargo.toml Outdated
sujitn and others added 7 commits June 12, 2026 08:00
default_calendar() had zero call sites and was the only reason currency.rs
pulled in the calendar types (one of which, USCalendar, was already unused).
Remove the speculative method and its imports; it can be reintroduced where a
calendar is actually selected if needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot the engine

The previous refactor folded the hexagonal port traits into convex-engine
(crate::ports). That inverted the dependency direction: adapters
(convex-ext-file, convex-ext-redb) had to depend on the whole engine just to
implement a trait, and convex-ffi / convex-mcp were given an engine dependency
they never used. It also created a dev-dependency cycle
(convex-engine ->dev-> convex-ext-file -> convex-engine), which pulled two
copies of the engine into the test graph and forced a 246-line duplicate set of
empty mocks (ports/mock.rs) because the engine's own tests could not use the
adapter empties.

Extract a thin convex-ports crate (trait definitions only, no runtime deps) that
both the engine and the adapters depend down on:

- New crate convex-ports holding market_data, reference_data, storage, output,
  config, error (moved verbatim; crate::ports:: -> crate:: internally).
- convex-engine re-exports it as `pub use convex_ports as ports`, so existing
  convex_engine::ports::* paths (server, benches) keep working unchanged.
- convex-ext-file / convex-ext-redb now depend on convex-ports, not convex-engine.
- convex-ffi / convex-mcp drop the unused convex-engine dependency.
- Delete the duplicate ports/mock.rs and point the reactive test back at the
  convex-ext-file empties (now possible: the cycle is gone). Drop the unused
  mockall dev-dependency and the now-unused async-trait dependency in the engine.

cargo check --workspace --all-targets passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both par-par and proceeds ASW reconstructed the period year fraction with a
copy-pasted heuristic: actual-days/365 when the period length differed from
365/freq by more than a magic 15 days, else the nominal 1/freq. That ignored the
instrument's own day count and mispriced stubs and non-Act/365 conventions.

Replace it with a single shared `coupon_year_fraction` helper that uses the
bond's day-count convention over the cash flow's accrual boundaries, falling
back to the nominal 1/freq when accrual dates are absent. Also drop the dead
`frequency_to_months` helpers (left unused after the cash-flow refactor) and the
unused `tau` / `maturity` bindings in par_par.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ence

The `SmithWilson` extrapolator was not Smith-Wilson: it was an ad-hoc
exponential blend of zero rates that ignored its `last_derivative` argument
(an unused-variable warning), so it was not even continuous in the forward at
the last liquid point, while its docs claimed EIOPA / Solvency II regulatory
compliance and C-infinity continuity. True Smith-Wilson fits a Wilson-kernel
curve to all input instruments and cannot be expressed as a pointwise tail
extrapolator, so the name was actively misleading.

Rename it to `UfrConvergence` and implement it honestly: the instantaneous
forward decays exponentially from its observed value at the LLP towards the UFR,
and the zero rate is the exact integral of that forward. This now uses
`last_derivative` (continuous in level and forward at the LLP) and is documented
as a heuristic, explicitly not EIOPA Smith-Wilson. Drops the dead Wilson-kernel
and convergence-weight helpers and the unused EIOPA preset constructors.

Renames the `ExtrapolationMethod::SmithWilson` variants (convex-math and
convex-curves) to `UfrConvergence { ufr, alpha }` accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… drop

rebuild_inner() baked EUR insurance parameters
(UfrConvergence { ufr: 0.042, alpha: 0.1 }) into every curve regardless of
currency, and deduplicated pillars by dropping any tenor not strictly greater
than the previous one -- silently discarding unsorted or out-of-order input
points.

- Add an `extrapolation` field to BuiltCurve and a configurable default on
  CurveBuilder (`with_extrapolation`, defaulting to flat-forward instead of a
  hard-coded UFR). Bumped-curve clones used for KR01/CS01 sensitivities now
  preserve the original curve's extrapolation automatically.
- rebuild_inner now sorts pillars by tenor and collapses only exact-duplicate
  tenors (keeping the latest rate); distinct points are never dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctor)

The architecture refactor was applied with mechanical scripts that left large
swaths of non-rustfmt-compliant code (one-line if/return bodies, trailing
whitespace, hand-wrapped argument lists), which would fail the CI `cargo fmt
--all -- --check` gate. Reformat the affected files; only files the branch had
made non-compliant are touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- discrete.rs: the new UFR-convergence match arm had a `use` after statements
  (clippy::items_after_statements under convex-curves' `#![warn(pedantic)]`);
  call the trait method via its fully-qualified path instead.
- convex-ports lib docs: `[`module`]: text` list items parse as markdown link
  reference definitions (clippy "link reference defined in list item"); use an
  em dash separator.

Pre-existing `result_large_err` warnings in convex-engine's market_data_listener
are unchanged from main and left out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot 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.

3 issues found across 49 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/convex-curves/src/curves/discrete.rs">

<violation number="1" location="crates/convex-curves/src/curves/discrete.rs:296">
P2: Passing unvalidated `alpha` to `SmithWilson::new`, which panics if alpha ≤ 0. Since `ExtrapolationMethod::SmithWilson(ufr, alpha)` has no construction-time validation, a zero or negative alpha will cause a runtime panic on extrapolation rather than an error result.</violation>
</file>

<file name="crates/convex-core/src/types/currency.rs">

<violation number="1" location="crates/convex-core/src/types/currency.rs:214">
P2: Several G10 currencies (CHF, CAD, AUD, NZD, SEK, NOK) fall back to `WeekendCalendar`, which only excludes weekends — no national holidays. In a pricing context this can produce incorrect business-day rolling, settlement dates, and curve interpolation for these currencies. Consider adding specific calendars or using `JointCalendar`/`CustomCalendarBuilder` for at least the G10 currencies.</violation>
</file>

<file name="crates/convex-math/src/interpolation/monotone_convex.rs">

<violation number="1" location="crates/convex-math/src/interpolation/monotone_convex.rs:300">
P1: The new quadratic forward interpolation can become negative without monotonicity limiting, and `interpolate()` integrates those negative values while `forward_rate()` clamps them, creating inconsistent curve behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/convex-engine/src/curve_builder.rs Outdated
Comment thread crates/convex-ports/src/lib.rs Outdated
Comment thread crates/convex-analytics/src/spreads/asw/mod.rs Outdated
`MarketDataPublisher`'s seven `publish_*` methods returned
`Result<(), broadcast::error::SendError<MarketDataUpdate>>`. The error carries
the un-delivered update (~152 bytes), tripping clippy's `result_large_err` and
bloating the success path of every publish.

Box the error behind a `PublishResult` alias and route all seven methods through
a single private `publish()` helper (which also removes the duplicated
`send(...).map(|_| ())` boilerplate). This was pre-existing debt on main, not
introduced by the curve refactor, but it was the last thing keeping
`cargo clippy --workspace -- -D warnings` from passing on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/convex-math/src/extrapolation/mod.rs (1)

60-76: Clarify the relationship between the two ExtrapolationMethod enums

  • convex_math::extrapolation::ExtrapolationMethod = { None, #[default] Flat, Linear, UfrConvergence{..} }, while convex_curves::ExtrapolationMethod = { #[default] None, Flat, Linear, FlatForward, UfrConvergence{..} }.
  • In convex_curves discrete curves, UfrConvergence delegates into convex_math::extrapolation, but None/Flat/Linear/FlatForward are handled directly (with FlatForward currently effectively falling back to flat behavior).
  • Add/expand docs (or a conversion layer) making it explicit that these enums are not interchangeable and that Default differs (convex_mathFlat, convex_curvesNone), even though CurveBuilder sets its own default_extrapolation (e.g., Flat for rate/credit curves).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-math/src/extrapolation/mod.rs` around lines 60 - 76, Document
and/or add an explicit conversion layer clarifying that the two enums
convex_math::extrapolation::ExtrapolationMethod and
convex_curves::ExtrapolationMethod are distinct and not interchangeable: state
that convex_math::extrapolation::ExtrapolationMethod defaults to Flat while
convex_curves::ExtrapolationMethod defaults to None, enumerate how each variant
maps (e.g., convex_curves::UfrConvergence -> convex_math::UfrConvergence, while
None/Flat/Linear/FlatForward are handled locally with FlatForward currently
falling back to Flat), and either add conversion functions (e.g.,
try_from_convex_curves_to_math and from_math_to_curves) or expand the docs in
the enum definitions and CurveBuilder/default_extrapolation to explicitly
describe expected behavior and defaulting rules so callers are not surprised.
crates/convex-analytics/src/spreads/asw/proceeds.rs (1)

279-290: 💤 Low value

Consider adding test coverage for accrual-aware year fraction calculation.

The mock sets accrual_start: None and accrual_end: None for all generated cash flows, which means coupon_year_fraction will always use the nominal fallback (1 / payments_per_year). This exercises the fallback path but not the day-count-aware calculation introduced in mod.rs.

Consider adding a test case where cash flows include accrual boundaries to verify the day-count logic works correctly for stub periods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-analytics/src/spreads/asw/proceeds.rs` around lines 279 - 290,
The generated mock cash flows in proceeds.rs set accrual_start and accrual_end
to None, so tests only cover the nominal fallback in coupon_year_fraction; add a
new unit test that constructs BondCashFlow entries with explicit accrual_start
and accrual_end spanning a stub/short period (e.g., first or last coupon) and
use those in the same payment_dates -> BondCashFlow mapping used in the proceeds
tests to exercise the day-count-aware code in mod.rs; specifically, create flows
with non-None accrual_start/accrual_end and assert coupon_year_fraction (or the
public function that consumes it) returns the expected fractional year according
to the day count convention implemented in mod.rs.
crates/convex-analytics/src/spreads/asw/mod.rs (1)

27-42: Day-count parsing accepts "ACT/ACT"; nominal fallback in ASW tests is due to missing accrual dates

  • DayCountConvention::from_str in crates/convex-core/src/daycounts/mod.rs accepts "ACT/ACT" (whitespace/case tolerant) and maps it to ActActIsda, so parsing won’t fail silently.
  • In the ASW test mocks in crates/convex-analytics/src/spreads/asw/proceeds.rs / par_par.rs, the BondCashFlow literals set accrual_start: None and accrual_end: None, so coupon_year_fraction always falls back to nominal regardless of day_count.
  • If you want coverage for the accrual-aware year-fraction path, add tests where BondCashFlow provides Some(accrual_start) and Some(accrual_end).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/convex-analytics/src/spreads/asw/mod.rs` around lines 27 - 42, The ASW
tests are always hitting the nominal fallback because coupon_year_fraction sees
BondCashFlow.accrual_start and accrual_end as None; update the test fixtures in
crates/convex-analytics/src/spreads/asw/proceeds.rs and par_par.rs to provide
Some(accrual_start) and Some(accrual_end) dates for at least one BondCashFlow so
coupon_year_fraction("ACT/ACT", cf, ...) exercises the accrual-aware branch
(function coupon_year_fraction) and verifies year_fraction is computed using
DayCountConvention parsing rather than using the nominal fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/convex-curves/src/curves/discrete.rs`:
- Around line 289-305: The UFR-convergence branch in
DiscreteCurve.with_extrapolation() passes last_value and last_derivative into
convex_math::extrapolation::UfrConvergence assuming continuously compounded
zero-rate semantics; update with_extrapolation() so that when
ExtrapolationMethod::UfrConvergence is selected it either (A) validates that
self.value_type is ValueType::ZeroRate with Compounding::Continuous and returns
an Err if not, or (B) converts the tail basis to continuous zero rates before
computing last_value = z(LLP) and last_derivative = z'(LLP) and then converts
results back to the curve’s original ValueType; change the UfrConvergence match
arm (the block constructing ext and calling Extrapolator::extrapolate) to use
the validated/converted zero-rate values (referencing DiscreteCurve,
with_extrapolation, ExtrapolationMethod::UfrConvergence, ValueType::ZeroRate,
and Compounding::Continuous).

---

Nitpick comments:
In `@crates/convex-analytics/src/spreads/asw/mod.rs`:
- Around line 27-42: The ASW tests are always hitting the nominal fallback
because coupon_year_fraction sees BondCashFlow.accrual_start and accrual_end as
None; update the test fixtures in
crates/convex-analytics/src/spreads/asw/proceeds.rs and par_par.rs to provide
Some(accrual_start) and Some(accrual_end) dates for at least one BondCashFlow so
coupon_year_fraction("ACT/ACT", cf, ...) exercises the accrual-aware branch
(function coupon_year_fraction) and verifies year_fraction is computed using
DayCountConvention parsing rather than using the nominal fallback.

In `@crates/convex-analytics/src/spreads/asw/proceeds.rs`:
- Around line 279-290: The generated mock cash flows in proceeds.rs set
accrual_start and accrual_end to None, so tests only cover the nominal fallback
in coupon_year_fraction; add a new unit test that constructs BondCashFlow
entries with explicit accrual_start and accrual_end spanning a stub/short period
(e.g., first or last coupon) and use those in the same payment_dates ->
BondCashFlow mapping used in the proceeds tests to exercise the day-count-aware
code in mod.rs; specifically, create flows with non-None
accrual_start/accrual_end and assert coupon_year_fraction (or the public
function that consumes it) returns the expected fractional year according to the
day count convention implemented in mod.rs.

In `@crates/convex-math/src/extrapolation/mod.rs`:
- Around line 60-76: Document and/or add an explicit conversion layer clarifying
that the two enums convex_math::extrapolation::ExtrapolationMethod and
convex_curves::ExtrapolationMethod are distinct and not interchangeable: state
that convex_math::extrapolation::ExtrapolationMethod defaults to Flat while
convex_curves::ExtrapolationMethod defaults to None, enumerate how each variant
maps (e.g., convex_curves::UfrConvergence -> convex_math::UfrConvergence, while
None/Flat/Linear/FlatForward are handled locally with FlatForward currently
falling back to Flat), and either add conversion functions (e.g.,
try_from_convex_curves_to_math and from_math_to_curves) or expand the docs in
the enum definitions and CurveBuilder/default_extrapolation to explicitly
describe expected behavior and defaulting rules so callers are not surprised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f90f7151-438f-4a5e-a216-3f485db05c13

📥 Commits

Reviewing files that changed from the base of the PR and between 52f8ee5 and e113ea0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (43)
  • Cargo.toml
  • crates/convex-analytics/src/spreads/asw/mod.rs
  • crates/convex-analytics/src/spreads/asw/par_par.rs
  • crates/convex-analytics/src/spreads/asw/proceeds.rs
  • crates/convex-analytics/src/spreads/zspread.rs
  • crates/convex-curves/src/curves/discrete.rs
  • crates/convex-curves/src/lib.rs
  • crates/convex-engine/Cargo.toml
  • crates/convex-engine/benches/pricing_benchmarks.rs
  • crates/convex-engine/src/cache.rs
  • crates/convex-engine/src/calc_graph.rs
  • crates/convex-engine/src/context.rs
  • crates/convex-engine/src/curve_builder.rs
  • crates/convex-engine/src/etf_pricing.rs
  • crates/convex-engine/src/lib.rs
  • crates/convex-engine/src/market_data_listener.rs
  • crates/convex-engine/src/portfolio_analytics.rs
  • crates/convex-engine/src/pricing_router.rs
  • crates/convex-engine/src/reactive.rs
  • crates/convex-ext-file/Cargo.toml
  • crates/convex-ext-file/src/lib.rs
  • crates/convex-ext-file/src/market_data.rs
  • crates/convex-ext-file/src/reference_data.rs
  • crates/convex-ext-redb/Cargo.toml
  • crates/convex-ext-redb/src/lib.rs
  • crates/convex-math/src/extrapolation/linear.rs
  • crates/convex-math/src/extrapolation/mod.rs
  • crates/convex-math/src/extrapolation/smith_wilson.rs
  • crates/convex-math/src/extrapolation/ufr_convergence.rs
  • crates/convex-math/src/interpolation/monotone_convex.rs
  • crates/convex-math/src/lib.rs
  • crates/convex-ports/Cargo.toml
  • crates/convex-ports/src/config.rs
  • crates/convex-ports/src/error.rs
  • crates/convex-ports/src/lib.rs
  • crates/convex-ports/src/market_data.rs
  • crates/convex-ports/src/output.rs
  • crates/convex-ports/src/reference_data.rs
  • crates/convex-ports/src/storage.rs
  • crates/convex-server/src/handlers.rs
  • crates/convex-server/src/main.rs
  • crates/convex-server/tests/api_integration_tests.rs
  • crates/convex-server/tests/websocket_integration_tests.rs
💤 Files with no reviewable changes (1)
  • crates/convex-math/src/extrapolation/smith_wilson.rs
✅ Files skipped from review due to trivial changes (8)
  • crates/convex-ports/src/market_data.rs
  • crates/convex-engine/src/calc_graph.rs
  • crates/convex-ports/src/output.rs
  • crates/convex-server/tests/websocket_integration_tests.rs
  • crates/convex-math/src/extrapolation/linear.rs
  • crates/convex-ext-file/src/lib.rs
  • crates/convex-ext-file/src/reference_data.rs
  • crates/convex-engine/src/context.rs
🚧 Files skipped from review as they are similar to previous changes (15)
  • crates/convex-engine/src/etf_pricing.rs
  • crates/convex-ext-redb/src/lib.rs
  • crates/convex-ext-file/src/market_data.rs
  • crates/convex-engine/src/cache.rs
  • crates/convex-server/src/main.rs
  • Cargo.toml
  • crates/convex-server/tests/api_integration_tests.rs
  • crates/convex-engine/benches/pricing_benchmarks.rs
  • crates/convex-server/src/handlers.rs
  • crates/convex-engine/src/portfolio_analytics.rs
  • crates/convex-engine/src/curve_builder.rs
  • crates/convex-analytics/src/spreads/asw/par_par.rs
  • crates/convex-engine/src/reactive.rs
  • crates/convex-analytics/src/spreads/zspread.rs
  • crates/convex-math/src/interpolation/monotone_convex.rs

Comment thread crates/convex-curves/src/curves/discrete.rs
sujitn and others added 7 commits June 12, 2026 09:33
The refactor that added sort/dedup to rebuild_inner also changed it to
unconditionally assign `self.inner` (build result `.ok()`), whereas the original
only assigned on success. As a result a failed reconstruction now wipes a
working `inner` to `None`, silently dropping the curve to the linear
`interpolate_rate` fallback. This is reachable on bumped curves during KRD/PV01
(pricing_router clones the curve, mutates rates, then rebuilds), corrupting the
sensitivity.

Build only when >= 2 distinct pillars remain; on a build error keep the
previously-built curve and log a warning instead of discarding it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
coupon_year_fraction re-parsed the bond's day-count string and rebuilt the
boxed day counter on every cash flow in the coupon loop. Add a `day_counter`
helper that parses once into a reusable `Box<dyn DayCount>`; the year-fraction
helper now takes the pre-built `&dyn DayCount`, so par_par and proceeds parse a
single time before the loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The crate doc claimed "ONLY trait definitions and their plain-data types, with
no runtime dependencies", but convex-ports depends on tokio (for the
sync::broadcast receiver types) and ships concrete helpers (receiver wrappers,
bond-filter matching, config constructors). Reword to state accurately what it
contains and that its only non-core dependency is tokio's sync feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate()

calculate() discounted the spread using self.compounding, but price_with_spread,
spread_dv01 (via price_with_spread), and calculate_from_cash_flows hard-coded
continuous discounting. With a non-continuous calculator (e.g. the z_spread()
helper derives SemiAnnual from the coupon frequency) the solve and the pricing
paths used different conventions, so price_with_spread/calculate did not
round-trip and DV01 was computed under the wrong convention.

Extract the compounding match into a private spread_df(z, dt) helper and route
calculate(), price_with_spread(), and calculate_from_cash_flows() through it.
Behavior is unchanged for the continuous default; add a semi-annual round-trip
test to guard the consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ro curves

The UFR-convergence tail reads the last value/derivative as a continuously
compounded zero rate (instantaneous forward f_LLP = z + t*z'), which only holds
when the curve stores continuously-compounded zero rates. with_extrapolation()
applied it regardless of ValueType, so selecting it on e.g. a discount-factor
curve produced silently wrong rates. Validate at construction and return an
error otherwise (the sole production caller already uses continuous zero rates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
convex_math::extrapolation::ExtrapolationMethod was never constructed or matched
anywhere -- all curve extrapolation selection goes through the separate
convex_curves::ExtrapolationMethod enum. The dead duplicate only invited
confusion about which enum is authoritative; delete it and its prelude
re-export rather than document a mapping between a live and a dead type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ASW mock fixtures emit cash flows with accrual_start/end = None, so the
existing tests only exercised the nominal 1/frequency fallback. Add a focused
unit test that builds a cash flow with explicit accrual boundaries and asserts
coupon_year_fraction uses the parsed day-count convention (ACT/360 = days/360),
not the nominal fraction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sujitn
sujitn merged commit d0dea02 into main Jun 12, 2026
10 checks passed
@sujitn
sujitn deleted the refactor-curve-architecture branch June 12, 2026 14:56
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.

1 participant