mollendorff_forge/real_options/
config.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum OptionType {
11 Defer,
13 Expand,
15 Contract,
17 Abandon,
19 Switch,
21 Compound,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
27#[serde(rename_all = "snake_case")]
28pub enum ValuationMethod {
29 BlackScholes,
31 #[default]
33 Binomial,
34 MonteCarlo,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct OptionDefinition {
41 #[serde(rename = "type")]
43 pub option_type: OptionType,
44 pub name: String,
46 #[serde(default)]
48 pub exercise_cost: f64,
49 #[serde(default)]
51 pub salvage_value: f64,
52 #[serde(default)]
54 pub max_deferral: f64,
55 #[serde(default = "default_expansion_factor")]
57 pub expansion_factor: f64,
58 #[serde(default = "default_contraction_factor")]
60 pub contraction_factor: f64,
61}
62
63const fn default_expansion_factor() -> f64 {
64 1.5
65}
66
67const fn default_contraction_factor() -> f64 {
68 0.5
69}
70
71impl OptionDefinition {
72 #[must_use]
74 pub fn defer(name: &str, max_deferral: f64, exercise_cost: f64) -> Self {
75 Self {
76 option_type: OptionType::Defer,
77 name: name.to_string(),
78 exercise_cost,
79 salvage_value: 0.0,
80 max_deferral,
81 expansion_factor: 1.0,
82 contraction_factor: 1.0,
83 }
84 }
85
86 #[must_use]
88 pub fn expand(name: &str, expansion_factor: f64, exercise_cost: f64) -> Self {
89 Self {
90 option_type: OptionType::Expand,
91 name: name.to_string(),
92 exercise_cost,
93 salvage_value: 0.0,
94 max_deferral: 0.0,
95 expansion_factor,
96 contraction_factor: 1.0,
97 }
98 }
99
100 #[must_use]
102 pub fn abandon(name: &str, salvage_value: f64) -> Self {
103 Self {
104 option_type: OptionType::Abandon,
105 name: name.to_string(),
106 exercise_cost: 0.0,
107 salvage_value,
108 max_deferral: 0.0,
109 expansion_factor: 1.0,
110 contraction_factor: 1.0,
111 }
112 }
113
114 #[must_use]
116 pub fn contract(name: &str, contraction_factor: f64, cost_savings: f64) -> Self {
117 Self {
118 option_type: OptionType::Contract,
119 name: name.to_string(),
120 exercise_cost: -cost_savings, salvage_value: 0.0,
122 max_deferral: 0.0,
123 expansion_factor: 1.0,
124 contraction_factor,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct UnderlyingConfig {
132 pub current_value: f64,
134 pub volatility: f64,
136 pub risk_free_rate: f64,
138 pub time_horizon: f64,
140 #[serde(default)]
142 pub dividend_yield: f64,
143}
144
145impl UnderlyingConfig {
146 #[must_use]
148 pub const fn new(
149 current_value: f64,
150 volatility: f64,
151 risk_free_rate: f64,
152 time_horizon: f64,
153 ) -> Self {
154 Self {
155 current_value,
156 volatility,
157 risk_free_rate,
158 time_horizon,
159 dividend_yield: 0.0,
160 }
161 }
162
163 #[must_use]
165 pub const fn with_dividend_yield(mut self, yield_rate: f64) -> Self {
166 self.dividend_yield = yield_rate;
167 self
168 }
169
170 pub fn validate(&self) -> Result<(), String> {
177 if self.current_value <= 0.0 {
178 return Err("Current value must be positive".to_string());
179 }
180 if self.volatility <= 0.0 || self.volatility > 2.0 {
181 return Err("Volatility must be between 0 and 200%".to_string());
182 }
183 if self.risk_free_rate < 0.0 || self.risk_free_rate > 1.0 {
184 return Err("Risk-free rate must be between 0% and 100%".to_string());
185 }
186 if self.time_horizon <= 0.0 {
187 return Err("Time horizon must be positive".to_string());
188 }
189 Ok(())
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct RealOptionsConfig {
196 #[serde(default)]
198 pub name: String,
199 #[serde(default)]
201 pub method: ValuationMethod,
202 pub underlying: UnderlyingConfig,
204 #[serde(default)]
206 pub options: Vec<OptionDefinition>,
207 #[serde(default = "default_binomial_steps")]
209 pub binomial_steps: usize,
210 #[serde(default = "default_mc_iterations")]
212 pub monte_carlo_iterations: usize,
213 pub seed: Option<u64>,
215}
216
217const fn default_binomial_steps() -> usize {
218 100
219}
220
221const fn default_mc_iterations() -> usize {
222 10000
223}
224
225impl RealOptionsConfig {
226 #[must_use]
228 pub fn new(name: &str, underlying: UnderlyingConfig) -> Self {
229 Self {
230 name: name.to_string(),
231 method: ValuationMethod::Binomial,
232 underlying,
233 options: Vec::new(),
234 binomial_steps: 100,
235 monte_carlo_iterations: 10000,
236 seed: None,
237 }
238 }
239
240 #[must_use]
242 pub const fn with_method(mut self, method: ValuationMethod) -> Self {
243 self.method = method;
244 self
245 }
246
247 #[must_use]
249 pub fn with_option(mut self, option: OptionDefinition) -> Self {
250 self.options.push(option);
251 self
252 }
253
254 #[must_use]
256 pub const fn with_binomial_steps(mut self, steps: usize) -> Self {
257 self.binomial_steps = steps;
258 self
259 }
260
261 #[must_use]
263 pub const fn with_seed(mut self, seed: u64) -> Self {
264 self.seed = Some(seed);
265 self
266 }
267
268 pub fn validate(&self) -> Result<(), String> {
275 self.underlying.validate()?;
276
277 if self.options.is_empty() {
278 return Err("At least one option must be defined".to_string());
279 }
280
281 if self.binomial_steps == 0 {
282 return Err("Binomial steps must be positive".to_string());
283 }
284
285 if self.monte_carlo_iterations == 0 {
286 return Err("Monte Carlo iterations must be positive".to_string());
287 }
288
289 Ok(())
290 }
291}
292
293#[cfg(test)]
294#[allow(clippy::float_cmp)]
296mod config_tests {
297 use super::*;
298
299 #[test]
300 fn test_underlying_validation() {
301 let underlying = UnderlyingConfig::new(10_000_000.0, 0.30, 0.05, 3.0);
302 assert!(underlying.validate().is_ok());
303
304 let bad_value = UnderlyingConfig::new(-100.0, 0.30, 0.05, 3.0);
305 assert!(bad_value.validate().is_err());
306
307 let bad_vol = UnderlyingConfig::new(100.0, -0.1, 0.05, 3.0);
308 assert!(bad_vol.validate().is_err());
309 }
310
311 #[test]
312 fn test_config_builder() {
313 let config = RealOptionsConfig::new(
314 "Factory Investment",
315 UnderlyingConfig::new(10_000_000.0, 0.30, 0.05, 3.0),
316 )
317 .with_method(ValuationMethod::Binomial)
318 .with_option(OptionDefinition::defer("Wait 2 years", 2.0, 8_000_000.0))
319 .with_option(OptionDefinition::abandon("Sell assets", 3_000_000.0))
320 .with_binomial_steps(50);
321
322 assert!(config.validate().is_ok());
323 assert_eq!(config.options.len(), 2);
324 }
325
326 #[test]
327 fn test_option_types() {
328 let defer = OptionDefinition::defer("Wait", 2.0, 1_000_000.0);
329 assert_eq!(defer.option_type, OptionType::Defer);
330
331 let expand = OptionDefinition::expand("Scale up", 1.5, 500_000.0);
332 assert_eq!(expand.option_type, OptionType::Expand);
333 assert_eq!(expand.expansion_factor, 1.5);
334
335 let abandon = OptionDefinition::abandon("Exit", 200_000.0);
336 assert_eq!(abandon.option_type, OptionType::Abandon);
337 assert_eq!(abandon.salvage_value, 200_000.0);
338 }
339}