Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ jobs:
- name: Cache cargo registry
uses: Swatinem/rust-cache@v2

# `excel` profile = panic=unwind; see [profile.excel] in Cargo.toml.
- name: Build convex-ffi
run: cargo build --release -p convex-ffi
run: cargo build --profile excel -p convex-ffi

- name: Setup .NET
uses: actions/setup-dotnet@v4
Expand All @@ -115,8 +116,11 @@ jobs:
- name: Build Excel Add-in
run: dotnet build excel/Convex.Excel/Convex.Excel.csproj --configuration Release

- name: Test Excel Add-in (headless)
run: dotnet test excel/Convex.Excel.Tests/Convex.Excel.Tests.csproj --configuration Release

- name: Copy native DLL
run: copy target\release\convex_ffi.dll excel\Convex.Excel\bin\Release\net472\
run: copy target\excel\convex_ffi.dll excel\Convex.Excel\bin\Release\net472\

- name: Create Add-in Package
shell: pwsh
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/java-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ jobs:
sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"

# `excel` profile = panic=unwind; see [profile.excel] in Cargo.toml.
- name: Build cdylib
run: cargo build -p convex-ffi --release --target ${{ matrix.target }}
run: cargo build -p convex-ffi --profile excel --target ${{ matrix.target }}

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ matrix.classifier }}
path: target/${{ matrix.target }}/release/${{ matrix.lib }}
path: target/${{ matrix.target }}/excel/${{ matrix.lib }}
if-no-files-found: error

# Stage every cdylib into the jar resources and publish to Maven Central.
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ jobs:
- name: Cache cargo
uses: Swatinem/rust-cache@v2

# `excel` profile = panic=unwind; see [profile.excel] in Cargo.toml.
- name: Build convex-ffi cdylib
run: cargo build -p convex-ffi --release
run: cargo build -p convex-ffi --profile excel

- name: Stage native library
run: |
dest=bindings/java/src/main/resources/native/linux-x86_64
mkdir -p "$dest"
cp target/release/libconvex_ffi.so "$dest/"
cp target/excel/libconvex_ffi.so "$dest/"

- name: Set up JDK 22
uses: actions/setup-java@v4
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ jobs:
- name: Cache cargo registry
uses: Swatinem/rust-cache@v2

# `excel` profile = panic=unwind; see [profile.excel] in Cargo.toml.
- name: Build convex-ffi
run: cargo build --release -p convex-ffi
run: cargo build --profile excel -p convex-ffi

- name: Setup .NET
uses: actions/setup-dotnet@v4
Expand All @@ -97,7 +98,7 @@ jobs:
run: dotnet build excel/Convex.Excel/Convex.Excel.csproj --configuration Release

- name: Copy native DLL
run: copy target\release\convex_ffi.dll excel\Convex.Excel\bin\Release\net472\
run: copy target\excel\convex_ffi.dll excel\Convex.Excel\bin\Release\net472\

- name: Create Add-in Package
shell: pwsh
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ www/pkg/
npm-debug.log*
/excel/Convex.Excel/bin
/excel/Convex.Excel/obj
/excel/Convex.Excel.Tests/bin
/excel/Convex.Excel.Tests/obj
__pycache__/
*.pyc
/data
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ inherits = "release"
lto = "thin"
codegen-units = 4

# Host-embedded cdylib profile (Excel add-in, JVM bindings): panics must
# unwind so convex-ffi's catch_unwind guards can turn them into error
# envelopes instead of aborting the host process (Excel/JVM). Build with:
# cargo build --profile excel -p convex-ffi
[profile.excel]
inherits = "release"
panic = "unwind"

# Distribution profile for binaries
[profile.dist]
inherits = "release"
Expand Down
127 changes: 127 additions & 0 deletions crates/convex-analytics/src/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,133 @@ pub struct CurveQueryResponse {
pub value: f64,
}

// ---- YAS (one-call yield & spread analysis) --------------------------------

/// Bloomberg-YAS-style analysis in one request: every yield convention,
/// G/Z/benchmark/ASW spreads, risk metrics, and the settlement invoice.
/// Backed by `crate::yas::YASCalculator`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YasRequest {
pub bond: Handle,
pub settlement: Date,
pub mark: MarkInput,
/// Spot/discount curve (Z-spread; also G-spread unless `govt_curve` set).
pub curve: Handle,
/// Government curve for G-spread and the benchmark spread.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub govt_curve: Option<Handle>,
/// Swap curve for I-spread.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub swap_curve: Option<Handle>,
#[serde(default)]
pub quote_frequency: Frequency,
}

/// Yields in percent, spreads in basis points, prices/amounts per 100 face.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YasResponse {
pub clean_price: f64,
pub dirty_price: f64,
pub accrued: f64,
pub accrued_days: i32,
pub ytm_pct: f64,
pub current_yield_pct: f64,
pub simple_yield_pct: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub money_market_yield_pct: Option<f64>,
pub g_spread_bps: f64,
pub z_spread_bps: f64,
pub benchmark_spread_bps: f64,
pub benchmark_tenor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asw_spread_bps: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oas_bps: Option<f64>,
pub modified_duration: f64,
pub macaulay_duration: f64,
pub convexity: f64,
pub dv01_per_100: f64,
pub principal_amount: f64,
pub accrued_amount: f64,
pub settlement_amount: f64,
}

// ---- Scenario analysis ------------------------------------------------------

/// One bump component. Tagged JSON: `{ "kind": "parallel" | "steepener" |
/// "flattener" | "key_rate" | "credit_spread", ... }`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ScenarioBumpSpec {
Parallel {
shift_bps: f64,
},
Steepener {
short_shift_bps: f64,
long_shift_bps: f64,
#[serde(default = "default_pivot_tenor")]
pivot_tenor: f64,
},
Flattener {
short_shift_bps: f64,
long_shift_bps: f64,
#[serde(default = "default_pivot_tenor")]
pivot_tenor: f64,
},
KeyRate {
tenor: f64,
shift_bps: f64,
},
CreditSpread {
shift_bps: f64,
},
}

fn default_pivot_tenor() -> f64 {
5.0
}

/// One named scenario = a list of bumps applied together.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub bumps: Vec<ScenarioBumpSpec>,
}

/// Run N curve scenarios against one bond in a single call. Methodology:
/// the mark implies a Z-spread over the base curve; each scenario bumps the
/// curve and reprices holding that Z-spread fixed.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioRequest {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Schema consumers cannot discover or validate convex_scenario: convex_schema("ScenarioRequest") fails because these DTOs were not added to the curated schema registry. Add request/response schema definitions and lookup arms alongside this public wire contract.

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

<comment>Schema consumers cannot discover or validate `convex_scenario`: `convex_schema("ScenarioRequest")` fails because these DTOs were not added to the curated schema registry. Add request/response schema definitions and lookup arms alongside this public wire contract.</comment>

<file context>
@@ -522,6 +522,133 @@ pub struct CurveQueryResponse {
+/// the mark implies a Z-spread over the base curve; each scenario bumps the
+/// curve and reprices holding that Z-spread fixed.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ScenarioRequest {
+    pub bond: Handle,
+    pub curve: Handle,
</file context>

pub bond: Handle,
pub curve: Handle,
pub settlement: Date,
pub mark: MarkInput,
pub scenarios: Vec<ScenarioSpec>,
#[serde(default)]
pub quote_frequency: Frequency,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioRow {
pub name: String,
pub clean_price: f64,
pub dirty_price: f64,
/// Clean price change vs the base mark.
pub delta_clean: f64,
/// YTM at the scenario price, decimal.
pub ytm_decimal: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioResponse {
pub base_clean: f64,
pub base_ytm_decimal: f64,
pub z_spread_bps: f64,
pub rows: Vec<ScenarioRow>,
}

// ---- Risk profile / hedge advisor -----------------------------------------
//
// `convex_risk_profile` returns a `RiskProfile`; being a serializable value it
Expand Down
20 changes: 14 additions & 6 deletions crates/convex-analytics/src/spreads/zspread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,14 @@ impl<'a> ZSpreadCalculator<'a> {
}
})?;

// Convert to basis points
let z_spread_bps = (result.root * 10_000.0).round();
// 1e-4 bp rounding: suppresses solver float noise without the
// DV01-scale round-trip price gap that whole-bp rounding caused.
let z_spread_bps = (result.root * 10_000.0 * 10_000.0).round() / 10_000.0;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// from_f64 (not from_f64_retain): the division is not exactly
// representable in binary, and retain would carry that noise into
// the Decimal (51.2346000000000001…).
Ok(Spread::new(
Decimal::from_f64_retain(z_spread_bps).unwrap_or_default(),
Decimal::from_f64(z_spread_bps).unwrap_or_default(),
SpreadType::ZSpread,
))
}
Expand Down Expand Up @@ -318,10 +322,14 @@ impl<'a> ZSpreadCalculator<'a> {
}
})?;

// Convert to basis points
let z_spread_bps = (result.root * 10_000.0).round();
// 1e-4 bp rounding: suppresses solver float noise without the
// DV01-scale round-trip price gap that whole-bp rounding caused.
let z_spread_bps = (result.root * 10_000.0 * 10_000.0).round() / 10_000.0;
// from_f64 (not from_f64_retain): the division is not exactly
// representable in binary, and retain would carry that noise into
// the Decimal (51.2346000000000001…).
Ok(Spread::new(
Decimal::from_f64_retain(z_spread_bps).unwrap_or_default(),
Decimal::from_f64(z_spread_bps).unwrap_or_default(),
SpreadType::ZSpread,
))
}
Expand Down
Loading
Loading