Hi there,
I found this while building historical ORE scenarios for VaR: the same historical date produced different ZeroSpread yield-curve discount factors when generated separately versus within the full historical run. Repeating the identical single-date configuration in fresh ORE processes confirmed that only YieldCurve/* outputs differed, while discount, index and FX curves matched. Using tenor-based quotes with true did not resolve it, so I reviewed buildZeroSpreadedCurve() and found that resolved dates and quote handles are passed to the curve constructor without being explicitly sorted by maturity.
Please find below for more details.
If you need any more information or any supporting documents, please let me know.
Thank you!
Description
When a yield curve is built from a ZeroSpread segment, repeated ORE runs with identical inputs can produce different curve discount factors. The differences are confined to YieldCurve/* factors; index curves, discount curves, FX and other factors remain identical.
Setting the Zero convention to <TenorBased>true</TenorBased> and using quote tenors such as 6M, 1Y, 2Y, etc. does not resolve the problem. TenorBased correctly converts each tenor into a date, but the resulting date/quote pairs are not ordered by resolved maturity before constructing PiecewiseZeroSpreadedTermStructure.
Environment
- ORE Python package:
open-source-risk-engine 1.8.16.0
- Python:
3.13.5
- OS: Windows 11, build 26100
- Evaluation date used in the reproduction:
2025-10-30
- The same construction is present on the current
master branch as checked on 2026-08-21.
Reproduction
- Define a yield curve containing a
ZeroSpread segment with multiple spread quotes and a reference yield curve.
- Use either explicit maturity dates with
TenorBased=false, or tenor quote identifiers with TenorBased=true.
- Run the same ORE base-scenario configuration twice in two fresh Python processes.
- Compare the two raw
scenario.csv files.
The test uses the same market data, curve configuration, conventions, valuation date and ORE configuration in both processes. No post-processing is applied to the raw ORE output.
Relevant source
The issue appears to be in YieldCurve::buildZeroSpreadedCurve().
The function:
- stores the loaded market data in
std::set<shared_ptr<MarketDatum>>;
- iterates over that set;
- appends each resolved maturity and quote handle to parallel vectors; and
- passes those vectors directly to
PiecewiseZeroSpreadedTermStructure.
The set is ordered by the smart pointers, not by the resolved quote maturities. The parallel dates and quoteHandles vectors are not explicitly sorted before curve construction. With tenor-based quotes, the maturity is resolved during step 3, but this still does not impose chronological order.
Expected result
- Each spread quote should remain paired with its resolved maturity.
- The nodes should be sorted chronologically by resolved maturity before the term structure is constructed.
- Repeated runs with identical inputs should produce identical zero-spread yield curves and identical raw scenario output.
TenorBased=true and TenorBased=false should both be deterministic.
Impact
This affects reproducibility of zero-spread yield curves and any downstream pricing or historical scenario generation that consumes their discount factors. In the observed case, it caused material differences in historical yield-curve scenarios between otherwise identical ORE runs.
Hi there,
I found this while building historical ORE scenarios for VaR: the same historical date produced different ZeroSpread yield-curve discount factors when generated separately versus within the full historical run. Repeating the identical single-date configuration in fresh ORE processes confirmed that only YieldCurve/* outputs differed, while discount, index and FX curves matched. Using tenor-based quotes with true did not resolve it, so I reviewed buildZeroSpreadedCurve() and found that resolved dates and quote handles are passed to the curve constructor without being explicitly sorted by maturity.
Please find below for more details.
If you need any more information or any supporting documents, please let me know.
Thank you!
Description
When a yield curve is built from a
ZeroSpreadsegment, repeated ORE runs with identical inputs can produce different curve discount factors. The differences are confined toYieldCurve/*factors; index curves, discount curves, FX and other factors remain identical.Setting the Zero convention to
<TenorBased>true</TenorBased>and using quote tenors such as6M,1Y,2Y, etc. does not resolve the problem.TenorBasedcorrectly converts each tenor into a date, but the resulting date/quote pairs are not ordered by resolved maturity before constructingPiecewiseZeroSpreadedTermStructure.Environment
open-source-risk-engine 1.8.16.03.13.52025-10-30masterbranch as checked on 2026-08-21.Reproduction
ZeroSpreadsegment with multiple spread quotes and a reference yield curve.TenorBased=false, or tenor quote identifiers withTenorBased=true.scenario.csvfiles.The test uses the same market data, curve configuration, conventions, valuation date and ORE configuration in both processes. No post-processing is applied to the raw ORE output.
Relevant source
The issue appears to be in
YieldCurve::buildZeroSpreadedCurve().The function:
std::set<shared_ptr<MarketDatum>>;PiecewiseZeroSpreadedTermStructure.The set is ordered by the smart pointers, not by the resolved quote maturities. The parallel
datesandquoteHandlesvectors are not explicitly sorted before curve construction. With tenor-based quotes, the maturity is resolved during step 3, but this still does not impose chronological order.Expected result
TenorBased=trueandTenorBased=falseshould both be deterministic.Impact
This affects reproducibility of zero-spread yield curves and any downstream pricing or historical scenario generation that consumes their discount factors. In the observed case, it caused material differences in historical yield-curve scenarios between otherwise identical ORE runs.