dividends_and_borrow/
dividends_and_borrow.rs1mod 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}