Skip to main content

dividends_and_borrow/
dividends_and_borrow.rs

1//! Carry inputs: continuous dividend yield, discrete cash dividends and
2//! stock borrow cost, and how each engine treats them.
3//!
4//! Run with:  cargo run --release --example dividends_and_borrow
5
6mod common;
7
8use chrono::NaiveDate;
9use rustyqlib::core::trade::PutOrCall;
10use rustyqlib::core::traits::Instrument;
11use rustyqlib::equity::barrier::{BarrierDirection, KnockType};
12use rustyqlib::equity::blackscholes::bs_price;
13use rustyqlib::equity::builder::EquityOptionBuilder;
14use rustyqlib::equity::utils::Engine;
15
16const SPOT: f64 = 100.0;
17const STRIKE: f64 = 100.0;
18const VOL: f64 = 0.30;
19const RATE: f64 = 0.05;
20
21fn asof() -> NaiveDate {
22    NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
23}
24fn expiry() -> NaiveDate {
25    NaiveDate::from_ymd_opt(2027, 1, 1).unwrap()
26}
27
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
38
39fn main() {
40    common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42    common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43    common::table_header();
44    common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45    common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46    common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47    common::row(
48        "q = 1% + borrow = 3%",
49        &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50    );
51    common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53    let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54    let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55    common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57    common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58    common::table_header();
59    for b in [0.0, 0.02, 0.05, 0.15] {
60        let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61        common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62    }
63    let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64    println!(
65        "  forward with 15% borrow: {:.4} (vs spot {SPOT})",
66        hard.base.forward_price()
67    );
68
69    common::section("Discrete cash dividends: 2 x 1.50 over the year");
70    let with_divs = |b: EquityOptionBuilder| {
71        b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72            .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73    };
74    let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75    println!(
76        "  spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77        analytic.base.pv_cash_dividends(),
78        analytic.base.effective_spot()
79    );
80    common::table_header();
81    common::row("Analytical (escrowed model)", &analytic);
82    common::row(
83        "Binomial (escrowed)",
84        &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85    );
86    common::row(
87        "Finite difference (jump model)",
88        &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89    );
90    common::row(
91        "Monte Carlo terminal (escrowed)",
92        &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93    );
94    common::row(
95        "Monte Carlo path-wise (jump model)",
96        &with_divs(base())
97            .vanilla(PutOrCall::Call)
98            .engine(Engine::MonteCarlo)
99            .mc_time_steps(200)
100            .paths(50_000)
101            .build(),
102    );
103    common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104    common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106    common::check(
107        "escrowed analytic == BS on the escrowed spot",
108        analytic.npv(),
109        bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110        1e-10,
111    );
112
113    common::section("Where the jump model matters: American exercise and barriers");
114    common::table_header();
115    common::row(
116        "American put, FD (jumps)",
117        &with_divs(base())
118            .american()
119            .vanilla(PutOrCall::Put)
120            .engine(Engine::FiniteDifference)
121            .build(),
122    );
123    common::row(
124        "American put, no dividends",
125        &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126    );
127    common::row(
128        "Down-and-out call H=85, MC (jumps)",
129        &with_divs(base())
130            .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131            .engine(Engine::MonteCarlo)
132            .paths(50_000)
133            .build(),
134    );
135    common::row(
136        "Down-and-out call H=85, no dividends",
137        &base()
138            .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139            .engine(Engine::MonteCarlo)
140            .paths(50_000)
141            .build(),
142    );
143    common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145    common::section("Put-call parity with full carry");
146    let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147    let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148    let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149        - STRIKE * (-RATE * 1.0_f64).exp();
150    common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151    println!();
152}