Put-Call Parity Test: Property-Based Tests vs Unit Tests in optionstratlib

Property-based tests verify the put-call parity invariant across thousands of randomly generated market scenarios, while unit tests validate the same mathematical relationship using fixed, hand-picked inputs.

The joaquinbejar/optionstratlib repository implements a rigorous dual-testing strategy for its Black-Scholes pricing engine, employing both property-based and unit testing approaches to enforce the put-call parity relationship $C - P = S - K e^{-rT}$. Understanding the distinction between these methodologies reveals how the library ensures mathematical correctness across both predictable edge cases and the entire domain of possible market inputs.

How the Tests Differ in Scope and Implementation

Goals and Coverage

Property-based tests aim to validate that the put-call parity equation holds invariant across the entire statistical domain of possible inputs. The test harness generates random values for spot price (50–500), strike price (50–500), volatility (0.1–0.8), days to expiration (7–365), and risk-free rate (0.01–0.10), ensuring the Black-Scholes implementation behaves correctly for combinations developers might not manually consider.

Unit tests verify the parity relationship using a single, deterministic scenario. Located directly in src/pricing/black_scholes_model.rs, these tests use the create_base_option helper to instantiate specific call and put options, providing immediate feedback on whether the core arithmetic logic remains intact after code changes.

Tooling and Execution

The property-based suite relies on the proptest crate configured with #[proptest_config(ProptestConfig::with_cases(100))], automatically executing the parity check one hundred times per test run with different random seeds. This non-deterministic approach can expose edge cases such as extreme volatility or near-zero expiries that static tests miss.

Unit tests use Rust’s standard test harness with #[test] attributes. They call black_scholes(&Options) -> Result<Decimal, Error> with predetermined parameters and assert equality between the left-hand side (call price minus put price) and right-hand side (discounted strike differential) using assert_decimal_eq! with a tolerance of dec!(0.01).

Property-Based Test Architecture

The property tests reside in tests/property/put_call_parity_test.rs, separate from the implementation source. This file defines factory functions create_call_option and create_put_option to construct option instances from generated parameters.

// tests/property/put_call_parity_test.rs
proptest! {
    #[proptest_config(ProptestConfig::with_cases(100))]
    fn test_put_call_parity(
        spot in 50.0f64..500.0,
        strike in 50.0f64..500.0,
        volatility in 0.1f64..0.8,
        days in 7u32..365,
        rate in 0.01f64..0.10,
    ) {
        // Build call and put options from random inputs
        let call = create_call_option(
            Positive::new(spot).unwrap(),
            Positive::new(strike).unwrap(),
            Positive::new(volatility).unwrap(),
            days,
            Decimal::from_f64_retain(rate).unwrap(),
        );
        let put = create_put_option(/* identical parameters */);

        // Compute prices – skip the case where pricing fails
        let call_price = black_scholes(&call)?;
        let put_price  = black_scholes(&put)?;
        // … compute parity and assert diff < 0.01
    }
}

This test skips cases where pricing returns an error, focusing strictly on the parity invariant rather than input validation.

Unit Test Implementation

The unit test lives inside a #[cfg(test)] mod tests block within src/pricing/black_scholes_model.rs, adjacent to the black_scholes function it validates. This co-location ensures developers see parity verification alongside the implementation logic.

// src/pricing/black_scholes_model.rs
#[test]
fn test_put_call_parity() {
    let call = create_base_option(Side::Long, OptionStyle::Call);
    let put  = create_base_option(Side::Long, OptionStyle::Put);

    let call_price = black_scholes(&call).unwrap();
    let put_price  = black_scholes(&put).unwrap();

    let discount = (-call.risk_free_rate
        * call.expiration_date.get_years().unwrap().to_dec()).exp();

    let lhs = call_price - put_price;
    let rhs = call.underlying_price.to_dec()
        - (call.strike_price.to_dec() * discount);

    assert_decimal_eq!(lhs, rhs, dec!(0.01));
}

Unlike the property-based version, this test uses the same fixed inputs on every execution, making it ideal for continuous integration pipelines that require deterministic outcomes.

Complementary Safety for Financial Models

Together, these approaches provide broad statistical assurance and precise regression safety. The property-based tests in tests/property/put_call_parity_test.rs act as a fuzzing layer, discovering subtle floating-point errors or boundary condition violations across the mathematical surface. The unit tests in src/pricing/black_scholes_model.rs serve as executable documentation, demonstrating exactly how the parity calculation should behave for a known valid scenario.

Summary

  • Property-based tests in tests/property/put_call_parity_test.rs use the proptest crate to validate put-call parity across 100+ random input combinations per run, covering spot prices from 50 to 500, volatilities between 0.1 and 0.8, and expiries from 7 to 365 days.
  • Unit tests embedded in src/pricing/black_scholes_model.rs execute deterministic parity checks using static option parameters via create_base_option, failing immediately on regression.
  • Both methodologies enforce the $C - P = S - K e^{-rT}$ invariant with a numerical tolerance of dec!(0.01).
  • Property tests skip pathological inputs that cause pricing errors, while unit tests assert directly and fail fast.
  • The dual strategy ensures the optionstratlib pricing engine remains mathematically sound across both anticipated and unanticipated market scenarios.

Frequently Asked Questions

What is put-call parity in options pricing?

Put-call parity is a fundamental no-arbitrage relationship stating that the difference between a call option price ($C$) and a put option price ($P$) with identical strike ($K$) and expiration ($T$) equals the spot price ($S$) minus the discounted strike price ($K e^{-rT}$). The optionstratlib codebase enforces this invariant to ensure the Black-Scholes implementation produces consistent pricing across both option styles.

Why does optionstratlib use proptest for property-based testing?

The proptest crate enables generative testing that explores the input space statistically rather than exhaustively. According to the source code in tests/property/put_call_parity_test.rs, this approach reveals edge cases—such as extremely short expiries or high volatility regimes—that developers might not anticipate when writing manual unit tests, providing higher confidence in the mathematical correctness of the pricing models.

Can property-based tests completely replace unit tests?

No. While property-based tests in tests/property/ provide broad domain coverage, they are non-deterministic and may miss specific business-critical scenarios. The unit tests in src/pricing/black_scholes_model.rs serve as regression guards and executable specifications for exact expected values, offering immediate, deterministic feedback during development that randomized testing cannot guarantee.

How does the library handle numerical precision in parity tests?

Both testing strategies use a tolerance of dec!(0.01) (one cent) when comparing the left-hand side and right-hand side of the parity equation. The unit tests use assert_decimal_eq! with this epsilon, while property tests use prop_assert! with the same threshold, accounting for floating-point arithmetic variations inherent in financial calculations without allowing material pricing errors.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →