use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OptionType {
Defer,
Expand,
Contract,
Abandon,
Switch,
Compound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ValuationMethod {
BlackScholes,
#[default]
Binomial,
MonteCarlo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionDefinition {
#[serde(rename = "type")]
pub option_type: OptionType,
pub name: String,
#[serde(default)]
pub exercise_cost: f64,
#[serde(default)]
pub salvage_value: f64,
#[serde(default)]
pub max_deferral: f64,
#[serde(default = "default_expansion_factor")]
pub expansion_factor: f64,
#[serde(default = "default_contraction_factor")]
pub contraction_factor: f64,
}
const fn default_expansion_factor() -> f64 {
1.5
}
const fn default_contraction_factor() -> f64 {
0.5
}
impl OptionDefinition {
#[must_use]
pub fn defer(name: &str, max_deferral: f64, exercise_cost: f64) -> Self {
Self {
option_type: OptionType::Defer,
name: name.to_string(),
exercise_cost,
salvage_value: 0.0,
max_deferral,
expansion_factor: 1.0,
contraction_factor: 1.0,
}
}
#[must_use]
pub fn expand(name: &str, expansion_factor: f64, exercise_cost: f64) -> Self {
Self {
option_type: OptionType::Expand,
name: name.to_string(),
exercise_cost,
salvage_value: 0.0,
max_deferral: 0.0,
expansion_factor,
contraction_factor: 1.0,
}
}
#[must_use]
pub fn abandon(name: &str, salvage_value: f64) -> Self {
Self {
option_type: OptionType::Abandon,
name: name.to_string(),
exercise_cost: 0.0,
salvage_value,
max_deferral: 0.0,
expansion_factor: 1.0,
contraction_factor: 1.0,
}
}
#[must_use]
pub fn contract(name: &str, contraction_factor: f64, cost_savings: f64) -> Self {
Self {
option_type: OptionType::Contract,
name: name.to_string(),
exercise_cost: -cost_savings, salvage_value: 0.0,
max_deferral: 0.0,
expansion_factor: 1.0,
contraction_factor,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnderlyingConfig {
pub current_value: f64,
pub volatility: f64,
pub risk_free_rate: f64,
pub time_horizon: f64,
#[serde(default)]
pub dividend_yield: f64,
}
impl UnderlyingConfig {
#[must_use]
pub const fn new(
current_value: f64,
volatility: f64,
risk_free_rate: f64,
time_horizon: f64,
) -> Self {
Self {
current_value,
volatility,
risk_free_rate,
time_horizon,
dividend_yield: 0.0,
}
}
#[must_use]
pub const fn with_dividend_yield(mut self, yield_rate: f64) -> Self {
self.dividend_yield = yield_rate;
self
}
pub fn validate(&self) -> Result<(), String> {
if self.current_value <= 0.0 {
return Err("Current value must be positive".to_string());
}
if self.volatility <= 0.0 || self.volatility > 2.0 {
return Err("Volatility must be between 0 and 200%".to_string());
}
if self.risk_free_rate < 0.0 || self.risk_free_rate > 1.0 {
return Err("Risk-free rate must be between 0% and 100%".to_string());
}
if self.time_horizon <= 0.0 {
return Err("Time horizon must be positive".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealOptionsConfig {
#[serde(default)]
pub name: String,
#[serde(default)]
pub method: ValuationMethod,
pub underlying: UnderlyingConfig,
#[serde(default)]
pub options: Vec<OptionDefinition>,
#[serde(default = "default_binomial_steps")]
pub binomial_steps: usize,
#[serde(default = "default_mc_iterations")]
pub monte_carlo_iterations: usize,
pub seed: Option<u64>,
}
const fn default_binomial_steps() -> usize {
100
}
const fn default_mc_iterations() -> usize {
10000
}
impl RealOptionsConfig {
#[must_use]
pub fn new(name: &str, underlying: UnderlyingConfig) -> Self {
Self {
name: name.to_string(),
method: ValuationMethod::Binomial,
underlying,
options: Vec::new(),
binomial_steps: 100,
monte_carlo_iterations: 10000,
seed: None,
}
}
#[must_use]
pub const fn with_method(mut self, method: ValuationMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub fn with_option(mut self, option: OptionDefinition) -> Self {
self.options.push(option);
self
}
#[must_use]
pub const fn with_binomial_steps(mut self, steps: usize) -> Self {
self.binomial_steps = steps;
self
}
#[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn validate(&self) -> Result<(), String> {
self.underlying.validate()?;
if self.options.is_empty() {
return Err("At least one option must be defined".to_string());
}
if self.binomial_steps == 0 {
return Err("Binomial steps must be positive".to_string());
}
if self.monte_carlo_iterations == 0 {
return Err("Monte Carlo iterations must be positive".to_string());
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod config_tests {
use super::*;
#[test]
fn test_underlying_validation() {
let underlying = UnderlyingConfig::new(10_000_000.0, 0.30, 0.05, 3.0);
assert!(underlying.validate().is_ok());
let bad_value = UnderlyingConfig::new(-100.0, 0.30, 0.05, 3.0);
assert!(bad_value.validate().is_err());
let bad_vol = UnderlyingConfig::new(100.0, -0.1, 0.05, 3.0);
assert!(bad_vol.validate().is_err());
}
#[test]
fn test_config_builder() {
let config = RealOptionsConfig::new(
"Factory Investment",
UnderlyingConfig::new(10_000_000.0, 0.30, 0.05, 3.0),
)
.with_method(ValuationMethod::Binomial)
.with_option(OptionDefinition::defer("Wait 2 years", 2.0, 8_000_000.0))
.with_option(OptionDefinition::abandon("Sell assets", 3_000_000.0))
.with_binomial_steps(50);
assert!(config.validate().is_ok());
assert_eq!(config.options.len(), 2);
}
#[test]
fn test_option_types() {
let defer = OptionDefinition::defer("Wait", 2.0, 1_000_000.0);
assert_eq!(defer.option_type, OptionType::Defer);
let expand = OptionDefinition::expand("Scale up", 1.5, 500_000.0);
assert_eq!(expand.option_type, OptionType::Expand);
assert_eq!(expand.expansion_factor, 1.5);
let abandon = OptionDefinition::abandon("Exit", 200_000.0);
assert_eq!(abandon.option_type, OptionType::Abandon);
assert_eq!(abandon.salvage_value, 200_000.0);
}
}