From 7ded086724d6259505ebe0f8c23dd305246d4279 Mon Sep 17 00:00:00 2001 From: sujitn Date: Sun, 28 Jun 2026 12:14:13 +0100 Subject: [PATCH] Fix crates.io publishing for the current workspace layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library is published to crates.io (convex-core, convex-math, convex-curves, convex-bonds, convex-analytics at 0.11.1), but the README, release workflow, and workspace manifest all claimed it was not, and the config had drifted so that `cargo publish` no longer worked. - Cargo.toml: give the five published crates' internal deps a version alongside the path so `cargo publish` succeeds (path-only deps are rejected at publish time). - Mark the facade, FFI, WASM, MCP and pricing-engine/server crates publish = false so a release can't push applications or bindings. The `convex` facade additionally can't publish — the name belongs to an unrelated project on crates.io. - release.yml: add a publish-crates job that publishes the five library crates in dependency order, gated on the release tag, skipping any version already on crates.io so re-runs are safe. - README: correct the installation and architecture sections to the current 15-crate layout (published vs internal), fix the language bindings status, and replace the outdated code samples with snippets verified by compile-checked examples. --- .github/workflows/release.yml | 61 +++++++- Cargo.toml | 21 ++- README.md | 144 ++++++++++-------- .../examples/readme_quickstart.rs | 27 ++++ .../examples/readme_zspread.rs | 36 +++++ crates/convex-engine/Cargo.toml | 1 + crates/convex-ext-file/Cargo.toml | 1 + crates/convex-ext-redb/Cargo.toml | 1 + crates/convex-ffi/Cargo.toml | 1 + crates/convex-mcp/Cargo.toml | 1 + crates/convex-portfolio/Cargo.toml | 1 + crates/convex-ports/Cargo.toml | 1 + crates/convex-server/Cargo.toml | 1 + crates/convex-wasm/Cargo.toml | 1 + crates/convex/Cargo.toml | 2 + 15 files changed, 224 insertions(+), 76 deletions(-) create mode 100644 crates/convex-analytics/examples/readme_quickstart.rs create mode 100644 crates/convex-analytics/examples/readme_zspread.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f3a2e58..99c78513 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,10 +119,56 @@ jobs: path: convex-excel-addin-v*.zip retention-days: 5 + publish-crates: + name: Publish to crates.io + runs-on: ubuntu-latest + needs: validate + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@v2 + + # Publish the five library crates in dependency order. cargo publish + # waits for each version to land in the index before returning, so the + # next crate can resolve it. The remaining workspace crates are + # publish = false and are skipped automatically. + # + # crates.io versions are immutable, so a crate already at this version is + # skipped (HTTP 200 from the per-version API) rather than erroring. This + # makes the job safe to re-run after a mid-list failure. + - name: Publish library crates + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + VERSION: ${{ needs.validate.outputs.version }} + run: | + set -euo pipefail + ua="convex-release (https://github.com/sujitn/convex)" + for crate in convex-core convex-math convex-curves convex-bonds convex-analytics; do + status=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "User-Agent: $ua" \ + "https://crates.io/api/v1/crates/$crate/$VERSION") + if [ "$status" = "200" ]; then + echo "✓ $crate@$VERSION already on crates.io — skipping" + continue + elif [ "$status" != "404" ]; then + echo "Unexpected HTTP $status checking $crate@$VERSION on crates.io" >&2 + exit 1 + fi + echo "::group::cargo publish -p $crate ($VERSION)" + cargo publish -p "$crate" + echo "::endgroup::" + done + github-release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [validate, build-excel-addin] + needs: [validate, build-excel-addin, publish-crates] permissions: contents: write steps: @@ -145,8 +191,17 @@ jobs: body: | ## Installation - Internal workspace — not published to crates.io. Clone the repo - and depend on the crates via path. + The core library crates are published to crates.io. Depend on the + unified analytics crate (it pulls in core, math, curves and bonds): + + ```toml + convex-analytics = "${{ needs.validate.outputs.version }}" + ``` + + Individually published crates: `convex-core`, `convex-math`, + `convex-curves`, `convex-bonds`, `convex-analytics`. The `convex` + umbrella plus the FFI / WASM / MCP / server crates are internal — + consume them via a git or path dependency. ### Excel Add-in Download the `convex-excel-addin` zip from assets below, extract, diff --git a/Cargo.toml b/Cargo.toml index 9e6d9abb..825e2506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,13 +38,22 @@ keywords = ["finance", "bonds", "fixed-income", "pricing", "quantitative"] categories = ["finance", "mathematics", "science"] [workspace.dependencies] -# Internal crates — path-only; this workspace is not published to crates.io. +# Internal crates. +# +# The five library crates (core, math, curves, bonds, analytics) are published +# to crates.io, so they carry both `path` and `version`: cargo uses the path for +# local builds and rewrites it to the version on `cargo publish`. Keep `version` +# in sync with `workspace.package.version` above. +# +# Everything else is `publish = false` (set in each crate's Cargo.toml) and stays +# path-only — the facade `convex` name is taken on crates.io by an unrelated +# project, and the engine/server/bindings crates are applications, not libraries. convex = { path = "crates/convex" } -convex-core = { path = "crates/convex-core" } -convex-math = { path = "crates/convex-math" } -convex-curves = { path = "crates/convex-curves" } -convex-bonds = { path = "crates/convex-bonds" } -convex-analytics = { path = "crates/convex-analytics" } +convex-core = { path = "crates/convex-core", version = "0.13.0" } +convex-math = { path = "crates/convex-math", version = "0.13.0" } +convex-curves = { path = "crates/convex-curves", version = "0.13.0" } +convex-bonds = { path = "crates/convex-bonds", version = "0.13.0" } +convex-analytics = { path = "crates/convex-analytics", version = "0.13.0" } convex-portfolio = { path = "crates/convex-portfolio" } convex-ffi = { path = "crates/convex-ffi" } convex-wasm = { path = "crates/convex-wasm" } diff --git a/README.md b/README.md index 07d6dc37..cfba8805 100644 --- a/README.md +++ b/README.md @@ -22,69 +22,61 @@ Convex is a production-grade fixed income analytics library providing comprehens - **High Performance**: Microsecond-level pricing - **Type Safety**: Leverage Rust's type system to prevent errors - **WebAssembly**: Full browser support via wasm-pack ([Live Demo](https://convex-demo.pages.dev/) | [WASM Demo](https://sujitn.github.io/convex/)) -- **Language Bindings**: C FFI bindings available; Python, Java, C# (coming soon) +- **Language Bindings**: C FFI and Java (published to Maven Central) available; Python and C# planned ## Quick Start ### Installation -Convex is an internal workspace — it is not published to crates.io. Depend on the -crate(s) you need via a git dependency: +The core library crates are published to [crates.io](https://crates.io). Most +users only need `convex-analytics`, which re-exports the lower layers (core, +math, curves, bonds): ```toml [dependencies] -convex-bonds = { git = "https://github.com/sujitn/convex.git" } -convex-analytics = { git = "https://github.com/sujitn/convex.git" } -convex-curves = { git = "https://github.com/sujitn/convex.git" } +convex-analytics = "0.13" ``` -or clone the repo and use a path dependency. +The full set of published crates: `convex-core`, `convex-math`, +`convex-curves`, `convex-bonds`, `convex-analytics`. -### Example: Pricing a US Treasury Bond +> **Note:** the `convex` umbrella crate is *not* on crates.io — that name +> belongs to an unrelated project ([convex.dev](https://convex.dev)). The +> umbrella facade, along with the FFI, WASM, MCP and pricing-server crates, +> ships in-repo only. Depend on them via git or a path dependency: +> +> ```toml +> convex = { git = "https://github.com/sujitn/convex.git" } +> ``` + +> The two snippets below are mirrored by compile-checked examples — run them +> with `cargo run -p convex-analytics --example readme_quickstart` and +> `--example readme_zspread`. + +### Example: Build a Bond and Compute Yield ```rust -use convex::prelude::*; +use convex_analytics::functions::yield_to_maturity; +use convex_bonds::instruments::FixedRateBond; +use convex_core::types::{Date, Frequency}; use rust_decimal_macros::dec; fn main() -> Result<(), Box> { - // Create a US Treasury bond - let bond = Bond::builder() - .isin("US912828Z229") - .coupon_rate(dec!(2.5)) - .maturity(Date::from_ymd(2030, 5, 15)) + // A 5% semi-annual bond. Coupon is a decimal (0.05 == 5%); day count and + // calendar default to 30/360 US and SIFMA. + let bond = FixedRateBond::builder() + .cusip_unchecked("912828Z29") + .coupon_rate(dec!(0.05)) + .issue_date(Date::from_ymd(2020, 5, 15)?) + .maturity(Date::from_ymd(2030, 5, 15)?) .frequency(Frequency::SemiAnnual) - .day_count(DayCount::ActAct) - .build()?; - - // Build a yield curve from market data - let curve = YieldCurve::bootstrap() - .add_deposit(dec!(0.015), Period::Months(3)) - .add_deposit(dec!(0.018), Period::Months(6)) - .add_bond(dec!(98.50), Date::from_ymd(2027, 5, 15)) - .add_bond(dec!(97.25), Date::from_ymd(2032, 5, 15)) - .interpolation(Interpolation::CubicSpline) .build()?; - // Price the bond - let settlement = Date::today(); - let price = bond.price(&curve, settlement)?; - - println!("Clean Price: {:.4}", price.clean()); - println!("Dirty Price: {:.4}", price.dirty()); - println!("Accrued Interest: {:.4}", price.accrued()); + // Yield to maturity from a clean price of 98.50 (per 100 face). + let settlement = Date::from_ymd(2025, 5, 15)?; + let ytm = yield_to_maturity(&bond, settlement, dec!(98.50), Frequency::SemiAnnual)?; - // Calculate yield - let ytm = bond.yield_to_maturity(price.clean(), settlement)?; - println!("Yield to Maturity: {:.4}%", ytm.as_percentage()); - - // Calculate risk metrics - let duration = bond.modified_duration(&curve, settlement)?; - let convexity = bond.convexity(&curve, settlement)?; - let dv01 = bond.dv01(&curve, settlement)?; - - println!("Modified Duration: {:.4}", duration); - println!("Convexity: {:.4}", convexity); - println!("DV01: ${:.2}", dv01); + println!("Yield to Maturity: {:.4}%", ytm.yield_percent()); Ok(()) } @@ -93,28 +85,36 @@ fn main() -> Result<(), Box> { ### Example: Z-Spread Calculation ```rust -use convex::prelude::*; +use convex_analytics::spreads::z_spread; +use convex_bonds::instruments::FixedRateBond; +use convex_core::types::{Date, Frequency}; +use convex_curves::curves::DiscountCurveBuilder; use rust_decimal_macros::dec; fn main() -> Result<(), Box> { - let bond = Bond::builder() - .isin("US459200KJ18") // IBM Corporate Bond - .coupon_rate(dec!(3.75)) - .maturity(Date::from_ymd(2028, 11, 15)) + // 3.75% semi-annual corporate bond. + let bond = FixedRateBond::builder() + .cusip_unchecked("459200KJ1") + .coupon_rate(dec!(0.0375)) + .issue_date(Date::from_ymd(2018, 11, 15)?) + .maturity(Date::from_ymd(2028, 11, 15)?) .frequency(Frequency::SemiAnnual) - .day_count(DayCount::Thirty360) .build()?; - // Government curve for discounting - let gov_curve = YieldCurve::from_market_data(/* ... */)?; - - // Market price - let market_price = Price::new(dec!(102.50), Currency::USD); - - // Calculate Z-spread - let z_spread = bond.z_spread(market_price, &gov_curve, Date::today())?; - - println!("Z-Spread: {:.2} bps", z_spread.as_basis_points()); + // A flat 4% continuously-compounded discount curve to spread against. + let settlement = Date::from_ymd(2025, 1, 15)?; + let rate = 0.04_f64; + let curve = DiscountCurveBuilder::new(settlement) + .add_pillar(1.0, (-rate * 1.0).exp()) + .add_pillar(2.0, (-rate * 2.0).exp()) + .add_pillar(5.0, (-rate * 5.0).exp()) + .add_pillar(10.0, (-rate * 10.0).exp()) + .with_extrapolation() + .build()?; + + // Z-spread that reprices the bond to a dirty price of 102.50. + let spread = z_spread(&bond, dec!(102.50), &curve, settlement)?; + println!("Z-Spread: {:.2} bps", spread.as_bps()); Ok(()) } @@ -182,17 +182,27 @@ spread DV01. ## Architecture -Convex is organized into several crates for modularity: +Convex is organized into focused crates. The five **library** crates are +published to crates.io; the rest are internal (the facade, language bindings, +and the pricing-engine/server stack) and ship in-repo only. ``` convex/ -├── convex-core # Core types (Date, Price, Yield, etc.) -├── convex-math # Mathematical utilities and solvers -├── convex-curves # Yield curve construction and interpolation -├── convex-bonds # Bond instruments and definitions -├── convex-analytics # Unified analytics (yields, spreads, risk) -├── convex-wasm # WebAssembly bindings -└── convex-ffi # Foreign Function Interface for language bindings +├── convex-core # Core types (Date, Price, Yield, calendars, day counts) [published] +├── convex-math # Solvers and interpolators (Brent, Newton, LM) [published] +├── convex-curves # Yield/credit curves, bootstrapping, multi-curve [published] +├── convex-bonds # Bond instruments (fixed, FRN, callable, zero, sinker) [published] +├── convex-analytics # Unified analytics: pricing, yields, spreads, risk [published] +├── convex # Single-import facade re-exporting the public API (internal) +├── convex-portfolio # Portfolio and ETF analytics (internal) +├── convex-ffi # C-ABI FFI for language bindings (Java, Excel) (internal) +├── convex-wasm # WebAssembly bindings for the browser demo (internal) +├── convex-mcp # MCP server for tool/agent integration (internal) +├── convex-ports # Hexagonal port traits (market/reference data, storage) (internal) +├── convex-engine # Reactive pricing engine with a calculation graph (internal) +├── convex-ext-file # File-backed market/reference data adapter (internal) +├── convex-ext-redb # redb embedded-storage adapter (internal) +└── convex-server # REST + WebSocket pricing server (deployed to Fly.io) (internal) ``` ## Performance diff --git a/crates/convex-analytics/examples/readme_quickstart.rs b/crates/convex-analytics/examples/readme_quickstart.rs new file mode 100644 index 00000000..8e8a5885 --- /dev/null +++ b/crates/convex-analytics/examples/readme_quickstart.rs @@ -0,0 +1,27 @@ +//! Mirrors the Quick Start snippet in the repository README so it stays +//! compile-checked. Run with: `cargo run -p convex-analytics --example readme_quickstart`. + +use convex_analytics::functions::yield_to_maturity; +use convex_bonds::instruments::FixedRateBond; +use convex_core::types::{Date, Frequency}; +use rust_decimal_macros::dec; + +fn main() -> Result<(), Box> { + // A 5% semi-annual bond. Coupon is a decimal (0.05 == 5%); day count and + // calendar default to 30/360 US and SIFMA. + let bond = FixedRateBond::builder() + .cusip_unchecked("912828Z29") + .coupon_rate(dec!(0.05)) + .issue_date(Date::from_ymd(2020, 5, 15)?) + .maturity(Date::from_ymd(2030, 5, 15)?) + .frequency(Frequency::SemiAnnual) + .build()?; + + // Yield to maturity from a clean price of 98.50 (per 100 face). + let settlement = Date::from_ymd(2025, 5, 15)?; + let ytm = yield_to_maturity(&bond, settlement, dec!(98.50), Frequency::SemiAnnual)?; + + println!("Yield to Maturity: {:.4}%", ytm.yield_percent()); + + Ok(()) +} diff --git a/crates/convex-analytics/examples/readme_zspread.rs b/crates/convex-analytics/examples/readme_zspread.rs new file mode 100644 index 00000000..9d22bc8f --- /dev/null +++ b/crates/convex-analytics/examples/readme_zspread.rs @@ -0,0 +1,36 @@ +//! Mirrors the Z-Spread snippet in the repository README so it stays +//! compile-checked. Run with: `cargo run -p convex-analytics --example readme_zspread`. + +use convex_analytics::spreads::z_spread; +use convex_bonds::instruments::FixedRateBond; +use convex_core::types::{Date, Frequency}; +use convex_curves::curves::DiscountCurveBuilder; +use rust_decimal_macros::dec; + +fn main() -> Result<(), Box> { + // 3.75% semi-annual corporate bond. + let bond = FixedRateBond::builder() + .cusip_unchecked("459200KJ1") + .coupon_rate(dec!(0.0375)) + .issue_date(Date::from_ymd(2018, 11, 15)?) + .maturity(Date::from_ymd(2028, 11, 15)?) + .frequency(Frequency::SemiAnnual) + .build()?; + + // A flat 4% continuously-compounded discount curve to spread against. + let settlement = Date::from_ymd(2025, 1, 15)?; + let rate = 0.04_f64; + let curve = DiscountCurveBuilder::new(settlement) + .add_pillar(1.0, (-rate * 1.0).exp()) + .add_pillar(2.0, (-rate * 2.0).exp()) + .add_pillar(5.0, (-rate * 5.0).exp()) + .add_pillar(10.0, (-rate * 10.0).exp()) + .with_extrapolation() + .build()?; + + // Z-spread that reprices the bond to a dirty price of 102.50. + let spread = z_spread(&bond, dec!(102.50), &curve, settlement)?; + println!("Z-Spread: {:.2} bps", spread.as_bps()); + + Ok(()) +} diff --git a/crates/convex-engine/Cargo.toml b/crates/convex-engine/Cargo.toml index bfc29799..968c15f6 100644 --- a/crates/convex-engine/Cargo.toml +++ b/crates/convex-engine/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-engine" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-ext-file/Cargo.toml b/crates/convex-ext-file/Cargo.toml index 6fe32bae..a0a74d54 100644 --- a/crates/convex-ext-file/Cargo.toml +++ b/crates/convex-ext-file/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-ext-file" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-ext-redb/Cargo.toml b/crates/convex-ext-redb/Cargo.toml index 9f10aa1c..10b8a79d 100644 --- a/crates/convex-ext-redb/Cargo.toml +++ b/crates/convex-ext-redb/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-ext-redb" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-ffi/Cargo.toml b/crates/convex-ffi/Cargo.toml index 7fc3552b..dc481ebc 100644 --- a/crates/convex-ffi/Cargo.toml +++ b/crates/convex-ffi/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-ffi" +publish = false # internal; not published to crates.io description = "C FFI bindings for the Convex fixed income analytics library" version.workspace = true edition.workspace = true diff --git a/crates/convex-mcp/Cargo.toml b/crates/convex-mcp/Cargo.toml index e991fc10..20a820c9 100644 --- a/crates/convex-mcp/Cargo.toml +++ b/crates/convex-mcp/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-mcp" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-portfolio/Cargo.toml b/crates/convex-portfolio/Cargo.toml index 19b2fd62..63e31109 100644 --- a/crates/convex-portfolio/Cargo.toml +++ b/crates/convex-portfolio/Cargo.toml @@ -12,6 +12,7 @@ [package] name = "convex-portfolio" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-ports/Cargo.toml b/crates/convex-ports/Cargo.toml index 83965e95..f6e08205 100644 --- a/crates/convex-ports/Cargo.toml +++ b/crates/convex-ports/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-ports" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-server/Cargo.toml b/crates/convex-server/Cargo.toml index 3478cfbb..c22f4036 100644 --- a/crates/convex-server/Cargo.toml +++ b/crates/convex-server/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-server" +publish = false # internal; not published to crates.io version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/convex-wasm/Cargo.toml b/crates/convex-wasm/Cargo.toml index 868cd56f..3b01dad7 100644 --- a/crates/convex-wasm/Cargo.toml +++ b/crates/convex-wasm/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "convex-wasm" +publish = false # internal; not published to crates.io version = "0.1.0" edition = "2021" description = "WebAssembly bindings for Convex fixed income analytics" diff --git a/crates/convex/Cargo.toml b/crates/convex/Cargo.toml index 2eb6eb7c..3e8e5787 100644 --- a/crates/convex/Cargo.toml +++ b/crates/convex/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "convex" description = "Convex fixed income analytics — single-import facade." +# Not published: the `convex` name on crates.io belongs to an unrelated project. +publish = false version.workspace = true edition.workspace = true authors.workspace = true