use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use crate::error::CorpFinanceError;
use crate::CorpFinanceResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HModelInput {
pub d0: Decimal,
pub r: Decimal,
pub g_short: Decimal,
pub g_long: Decimal,
pub half_life: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HModelOutput {
pub intrinsic_value: Decimal,
pub stable_value: Decimal,
pub growth_premium: Decimal,
pub growth_premium_pct: Decimal,
pub implied_growth: Decimal,
}
pub fn calculate_h_model(input: &HModelInput) -> CorpFinanceResult<HModelOutput> {
validate_input(input)?;
let d0 = input.d0;
let r = input.r;
let g_l = input.g_long;
let g_s = input.g_short;
let h = input.half_life;
let denom = r - g_l;
let stable_value = d0 * (Decimal::ONE + g_l) / denom;
let growth_premium = d0 * h * (g_s - g_l) / denom;
let intrinsic_value = stable_value + growth_premium;
let growth_premium_pct = if intrinsic_value == Decimal::ZERO {
Decimal::ZERO
} else {
growth_premium / intrinsic_value * dec!(100)
};
let implied_growth = if intrinsic_value + d0 == Decimal::ZERO {
Decimal::ZERO
} else {
(intrinsic_value * r - d0) / (intrinsic_value + d0)
};
Ok(HModelOutput {
intrinsic_value,
stable_value,
growth_premium,
growth_premium_pct,
implied_growth,
})
}
fn validate_input(input: &HModelInput) -> CorpFinanceResult<()> {
if input.d0 < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "d0".into(),
reason: "Current dividend must be non-negative.".into(),
});
}
if input.r <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "r".into(),
reason: "Required rate of return must be positive.".into(),
});
}
if input.r <= input.g_long {
return Err(CorpFinanceError::FinancialImpossibility(
"Required return (r) must exceed long-term growth (g_long) for convergent valuation."
.into(),
));
}
if input.half_life < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "half_life".into(),
reason: "Half-life must be non-negative.".into(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn approx_eq(a: Decimal, b: Decimal, eps: Decimal) -> bool {
(a - b).abs() < eps
}
fn base_input() -> HModelInput {
HModelInput {
d0: dec!(2.00),
r: dec!(0.10),
g_short: dec!(0.15),
g_long: dec!(0.04),
half_life: dec!(5),
}
}
#[test]
fn test_basic_h_model() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.stable_value, dec!(34.6667), dec!(0.01)));
assert!(approx_eq(out.growth_premium, dec!(18.3333), dec!(0.01)));
assert!(approx_eq(out.intrinsic_value, dec!(53.0), dec!(0.01)));
}
#[test]
fn test_pure_gordon_when_h_zero() {
let input = HModelInput {
d0: dec!(2.00),
r: dec!(0.10),
g_short: dec!(0.15),
g_long: dec!(0.04),
half_life: Decimal::ZERO,
};
let out = calculate_h_model(&input).unwrap();
assert_eq!(out.growth_premium, Decimal::ZERO);
assert!(approx_eq(out.stable_value, dec!(34.6667), dec!(0.01)));
assert!(approx_eq(
out.intrinsic_value,
out.stable_value,
dec!(0.0001)
));
}
#[test]
fn test_equal_growth_rates() {
let input = HModelInput {
d0: dec!(3.00),
r: dec!(0.12),
g_short: dec!(0.05),
g_long: dec!(0.05),
half_life: dec!(10),
};
let out = calculate_h_model(&input).unwrap();
assert_eq!(out.growth_premium, Decimal::ZERO);
assert!(approx_eq(out.stable_value, dec!(45.0), dec!(0.01)));
}
#[test]
fn test_growth_premium_pct() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
let expected_pct = out.growth_premium / out.intrinsic_value * dec!(100);
assert!(approx_eq(out.growth_premium_pct, expected_pct, dec!(0.01)));
}
#[test]
fn test_growth_premium_pct_h_zero() {
let input = HModelInput {
half_life: Decimal::ZERO,
..base_input()
};
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.growth_premium_pct, Decimal::ZERO, dec!(0.01)));
}
#[test]
fn test_high_growth_premium() {
let input = HModelInput {
d0: dec!(1.00),
r: dec!(0.12),
g_short: dec!(0.30),
g_long: dec!(0.03),
half_life: dec!(10),
};
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.growth_premium, dec!(30.0), dec!(0.01)));
assert!(approx_eq(out.stable_value, dec!(11.4444), dec!(0.01)));
}
#[test]
fn test_negative_growth_long_term() {
let input = HModelInput {
d0: dec!(5.00),
r: dec!(0.08),
g_short: dec!(0.02),
g_long: dec!(-0.02),
half_life: dec!(3),
};
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.stable_value, dec!(49.0), dec!(0.01)));
assert!(approx_eq(out.growth_premium, dec!(6.0), dec!(0.01)));
}
#[test]
fn test_negative_g_short_below_g_long() {
let input = HModelInput {
d0: dec!(2.00),
r: dec!(0.10),
g_short: dec!(0.02),
g_long: dec!(0.04),
half_life: dec!(5),
};
let out = calculate_h_model(&input).unwrap();
assert!(out.growth_premium < Decimal::ZERO);
}
#[test]
fn test_large_half_life() {
let input = HModelInput {
d0: dec!(1.00),
r: dec!(0.10),
g_short: dec!(0.20),
g_long: dec!(0.03),
half_life: dec!(50),
};
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.growth_premium, dec!(121.4286), dec!(0.01)));
}
#[test]
fn test_zero_dividend() {
let input = HModelInput {
d0: Decimal::ZERO,
r: dec!(0.10),
g_short: dec!(0.15),
g_long: dec!(0.04),
half_life: dec!(5),
};
let out = calculate_h_model(&input).unwrap();
assert_eq!(out.intrinsic_value, Decimal::ZERO);
assert_eq!(out.stable_value, Decimal::ZERO);
assert_eq!(out.growth_premium, Decimal::ZERO);
}
#[test]
fn test_reject_negative_dividend() {
let input = HModelInput {
d0: dec!(-1),
..base_input()
};
assert!(calculate_h_model(&input).is_err());
}
#[test]
fn test_reject_r_equal_g_long() {
let input = HModelInput {
r: dec!(0.04),
g_long: dec!(0.04),
..base_input()
};
assert!(calculate_h_model(&input).is_err());
}
#[test]
fn test_reject_r_less_than_g_long() {
let input = HModelInput {
r: dec!(0.03),
g_long: dec!(0.04),
..base_input()
};
assert!(calculate_h_model(&input).is_err());
}
#[test]
fn test_reject_negative_half_life() {
let input = HModelInput {
half_life: dec!(-1),
..base_input()
};
assert!(calculate_h_model(&input).is_err());
}
#[test]
fn test_reject_zero_r() {
let input = HModelInput {
r: Decimal::ZERO,
g_long: dec!(-0.02),
..base_input()
};
assert!(calculate_h_model(&input).is_err());
}
#[test]
fn test_implied_growth_between_g_long_and_g_short() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
assert!(out.implied_growth >= input.g_long);
assert!(out.implied_growth <= input.g_short);
}
#[test]
fn test_implied_growth_equals_g_long_when_h_zero() {
let input = HModelInput {
half_life: Decimal::ZERO,
..base_input()
};
let out = calculate_h_model(&input).unwrap();
assert!(approx_eq(out.implied_growth, input.g_long, dec!(0.001)));
}
#[test]
fn test_stable_value_always_positive() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
assert!(out.stable_value > Decimal::ZERO);
}
#[test]
fn test_serialization_roundtrip() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
let json = serde_json::to_string(&out).unwrap();
let _: HModelOutput = serde_json::from_str(&json).unwrap();
}
#[test]
fn test_small_spread_r_g() {
let input = HModelInput {
d0: dec!(1.00),
r: dec!(0.051),
g_short: dec!(0.10),
g_long: dec!(0.05),
half_life: dec!(5),
};
let out = calculate_h_model(&input).unwrap();
assert!(out.intrinsic_value > dec!(1000));
}
#[test]
fn test_intrinsic_equals_sum_of_components() {
let input = base_input();
let out = calculate_h_model(&input).unwrap();
let sum = out.stable_value + out.growth_premium;
assert!(approx_eq(out.intrinsic_value, sum, dec!(0.0001)));
}
}