use crate::compat::Instant;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use crate::error::CorpFinanceError;
use crate::types::{with_metadata, ComputationOutput, Money, Rate};
use crate::CorpFinanceResult;
const NEWTON_SQRT_ITERATIONS: u32 = 20;
const DAYS_PER_YEAR: Decimal = dec!(365);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DefiAnalysisType {
YieldFarm,
ImpermanentLoss,
Staking,
LiquidityPool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefiYieldInput {
pub protocol_name: String,
pub analysis_type: DefiAnalysisType,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_apr: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reward_apr: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compounding_frequency: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gas_cost_per_compound: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub principal: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub holding_period_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_price_a: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_price_b: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub final_price_a: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub final_price_b: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_deposit_value: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_fee_apr: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub staked_amount: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annual_reward_rate: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validator_commission: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slashing_probability: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slashing_penalty: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unbonding_period_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compounding: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_tvl: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_deposit: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub daily_volume: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_fee_rate: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_a_weight: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_change_pct: Option<Rate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefiYieldOutput {
pub analysis_type: String,
pub effective_apy: Rate,
#[serde(skip_serializing_if = "Option::is_none")]
pub net_apy: Option<Rate>,
pub total_return: Money,
#[serde(skip_serializing_if = "Option::is_none")]
pub impermanent_loss_pct: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub impermanent_loss_amount: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub il_vs_fees: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub staking_effective_yield: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub staking_expected_annual_reward: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_share: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_fee_income: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk_adjusted_apy: Option<Rate>,
pub warnings: Vec<String>,
}
pub fn analyze_defi(
input: &DefiYieldInput,
) -> CorpFinanceResult<ComputationOutput<DefiYieldOutput>> {
let start = Instant::now();
let output = match &input.analysis_type {
DefiAnalysisType::YieldFarm => analyze_yield_farm(input)?,
DefiAnalysisType::ImpermanentLoss => analyze_impermanent_loss(input)?,
DefiAnalysisType::Staking => analyze_staking(input)?,
DefiAnalysisType::LiquidityPool => analyze_liquidity_pool(input)?,
};
let elapsed = start.elapsed().as_micros() as u64;
Ok(with_metadata(
&format!(
"DeFi Analysis — {:?} for {}",
input.analysis_type, input.protocol_name
),
&serde_json::json!({
"protocol": input.protocol_name,
"analysis_type": format!("{:?}", input.analysis_type),
"math": "rust_decimal (iterative compound, Newton sqrt)",
}),
output.warnings.clone(),
elapsed,
output,
))
}
fn analyze_yield_farm(input: &DefiYieldInput) -> CorpFinanceResult<DefiYieldOutput> {
let base_apr = require_field(input.base_apr, "base_apr")?;
let compounding_frequency =
require_field(input.compounding_frequency, "compounding_frequency")?;
let principal = require_field(input.principal, "principal")?;
let holding_period_days = require_field(input.holding_period_days, "holding_period_days")?;
let mut warnings: Vec<String> = Vec::new();
if compounding_frequency == 0 {
return Err(CorpFinanceError::InvalidInput {
field: "compounding_frequency".into(),
reason: "Compounding frequency must be > 0".into(),
});
}
if principal <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "principal".into(),
reason: "Principal must be positive".into(),
});
}
if base_apr < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "base_apr".into(),
reason: "Base APR cannot be negative".into(),
});
}
let total_apr = base_apr + input.reward_apr.unwrap_or(Decimal::ZERO);
if total_apr > dec!(10) {
warnings.push("APR exceeds 1000% — verify this is correct".into());
}
let n = compounding_frequency;
let period_rate = total_apr / Decimal::from(n);
let effective_apy = compound_iterative(period_rate, n) - Decimal::ONE;
let annualized_gas = match input.gas_cost_per_compound {
Some(gas) => gas * Decimal::from(n),
None => Decimal::ZERO,
};
let gas_drag = if principal > Decimal::ZERO {
annualized_gas / principal
} else {
Decimal::ZERO
};
let net_apy = effective_apy - gas_drag;
if net_apy < Decimal::ZERO {
warnings.push("Gas costs exceed yield — position is net negative".into());
}
let holding_fraction = Decimal::from(holding_period_days) / DAYS_PER_YEAR;
let total_return = principal * net_apy * holding_fraction;
Ok(DefiYieldOutput {
analysis_type: "YieldFarm".to_string(),
effective_apy,
net_apy: Some(net_apy),
total_return,
impermanent_loss_pct: None,
impermanent_loss_amount: None,
il_vs_fees: None,
staking_effective_yield: None,
staking_expected_annual_reward: None,
pool_share: None,
pool_fee_income: None,
risk_adjusted_apy: None,
warnings,
})
}
fn analyze_impermanent_loss(input: &DefiYieldInput) -> CorpFinanceResult<DefiYieldOutput> {
let initial_price_a = require_field(input.initial_price_a, "initial_price_a")?;
let initial_price_b = require_field(input.initial_price_b, "initial_price_b")?;
let final_price_a = require_field(input.final_price_a, "final_price_a")?;
let final_price_b = require_field(input.final_price_b, "final_price_b")?;
let initial_deposit_value =
require_field(input.initial_deposit_value, "initial_deposit_value")?;
let mut warnings: Vec<String> = Vec::new();
if initial_price_a <= Decimal::ZERO || initial_price_b <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "initial_price".into(),
reason: "Initial prices must be positive".into(),
});
}
if final_price_a <= Decimal::ZERO || final_price_b <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "final_price".into(),
reason: "Final prices must be positive".into(),
});
}
if initial_deposit_value <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "initial_deposit_value".into(),
reason: "Initial deposit value must be positive".into(),
});
}
let ratio_a = final_price_a / initial_price_a;
let ratio_b = final_price_b / initial_price_b;
let price_ratio = ratio_a / ratio_b;
let sqrt_ratio = newton_sqrt(price_ratio)?;
let il_pct = dec!(2) * sqrt_ratio / (Decimal::ONE + price_ratio) - Decimal::ONE;
let il_amount = initial_deposit_value * il_pct.abs();
let hold_value =
initial_deposit_value / dec!(2) * ratio_a + initial_deposit_value / dec!(2) * ratio_b;
let lp_value = hold_value + hold_value * il_pct;
let _ = lp_value;
let holding_days = input.holding_period_days.unwrap_or(365);
let holding_fraction = Decimal::from(holding_days) / DAYS_PER_YEAR;
let fee_income = match input.pool_fee_apr {
Some(fee_apr) => initial_deposit_value * fee_apr * holding_fraction,
None => Decimal::ZERO,
};
let il_vs_fees = if fee_income > Decimal::ZERO {
if fee_income >= il_amount {
Some("Fees exceed IL".to_string())
} else {
Some("IL exceeds fees".to_string())
}
} else {
None
};
let net_gain = fee_income - il_amount;
let effective_apy = if initial_deposit_value > Decimal::ZERO && holding_fraction > Decimal::ZERO
{
net_gain / initial_deposit_value / holding_fraction
} else {
Decimal::ZERO
};
if il_pct.abs() > dec!(0.10) {
warnings.push(format!(
"Impermanent loss is significant: {:.2}%",
il_pct * dec!(100)
));
}
Ok(DefiYieldOutput {
analysis_type: "ImpermanentLoss".to_string(),
effective_apy,
net_apy: None,
total_return: net_gain,
impermanent_loss_pct: Some(il_pct),
impermanent_loss_amount: Some(il_amount),
il_vs_fees,
staking_effective_yield: None,
staking_expected_annual_reward: None,
pool_share: None,
pool_fee_income: Some(fee_income),
risk_adjusted_apy: None,
warnings,
})
}
fn analyze_staking(input: &DefiYieldInput) -> CorpFinanceResult<DefiYieldOutput> {
let staked_amount = require_field(input.staked_amount, "staked_amount")?;
let annual_reward_rate = require_field(input.annual_reward_rate, "annual_reward_rate")?;
let validator_commission = require_field(input.validator_commission, "validator_commission")?;
let slashing_probability = require_field(input.slashing_probability, "slashing_probability")?;
let slashing_penalty = require_field(input.slashing_penalty, "slashing_penalty")?;
let _unbonding_period_days =
require_field(input.unbonding_period_days, "unbonding_period_days")?;
let compounding = input.compounding.unwrap_or(false);
let mut warnings: Vec<String> = Vec::new();
if staked_amount <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "staked_amount".into(),
reason: "Staked amount must be positive".into(),
});
}
if annual_reward_rate < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "annual_reward_rate".into(),
reason: "Annual reward rate cannot be negative".into(),
});
}
if validator_commission < Decimal::ZERO || validator_commission > Decimal::ONE {
return Err(CorpFinanceError::InvalidInput {
field: "validator_commission".into(),
reason: "Validator commission must be between 0 and 1".into(),
});
}
if slashing_probability < Decimal::ZERO || slashing_probability > Decimal::ONE {
return Err(CorpFinanceError::InvalidInput {
field: "slashing_probability".into(),
reason: "Slashing probability must be between 0 and 1".into(),
});
}
if slashing_penalty < Decimal::ZERO || slashing_penalty > Decimal::ONE {
return Err(CorpFinanceError::InvalidInput {
field: "slashing_penalty".into(),
reason: "Slashing penalty must be between 0 and 1".into(),
});
}
let net_yield = annual_reward_rate * (Decimal::ONE - validator_commission);
let effective_apy = if compounding {
let daily_rate = net_yield / DAYS_PER_YEAR;
compound_iterative(daily_rate, 365) - Decimal::ONE
} else {
net_yield
};
let expected_slashing_cost = slashing_probability * slashing_penalty;
let risk_adjusted_apy = effective_apy - expected_slashing_cost;
let expected_annual_reward = staked_amount * effective_apy;
let total_return = staked_amount * risk_adjusted_apy;
if risk_adjusted_apy < Decimal::ZERO {
warnings.push("Risk-adjusted yield is negative — slashing risk exceeds rewards".into());
}
if slashing_probability > dec!(0.05) {
warnings.push("Slashing probability exceeds 5% — high-risk validator".into());
}
if let Some(unbonding) = input.unbonding_period_days {
if unbonding > 28 {
warnings.push(format!(
"Unbonding period is {} days — capital locked for extended period",
unbonding
));
}
}
Ok(DefiYieldOutput {
analysis_type: "Staking".to_string(),
effective_apy,
net_apy: None,
total_return,
impermanent_loss_pct: None,
impermanent_loss_amount: None,
il_vs_fees: None,
staking_effective_yield: Some(net_yield),
staking_expected_annual_reward: Some(expected_annual_reward),
pool_share: None,
pool_fee_income: None,
risk_adjusted_apy: Some(risk_adjusted_apy),
warnings,
})
}
fn analyze_liquidity_pool(input: &DefiYieldInput) -> CorpFinanceResult<DefiYieldOutput> {
let pool_tvl = require_field(input.pool_tvl, "pool_tvl")?;
let user_deposit = require_field(input.user_deposit, "user_deposit")?;
let daily_volume = require_field(input.daily_volume, "daily_volume")?;
let pool_fee_rate = require_field(input.pool_fee_rate, "pool_fee_rate")?;
let _token_a_weight = require_field(input.token_a_weight, "token_a_weight")?;
let price_change_pct = require_field(input.price_change_pct, "price_change_pct")?;
let mut warnings: Vec<String> = Vec::new();
if pool_tvl <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "pool_tvl".into(),
reason: "Pool TVL must be positive".into(),
});
}
if user_deposit <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "user_deposit".into(),
reason: "User deposit must be positive".into(),
});
}
if user_deposit > pool_tvl {
warnings.push("User deposit exceeds pool TVL — check inputs".into());
}
if pool_fee_rate < Decimal::ZERO || pool_fee_rate > Decimal::ONE {
return Err(CorpFinanceError::InvalidInput {
field: "pool_fee_rate".into(),
reason: "Pool fee rate must be between 0 and 1".into(),
});
}
let pool_share = user_deposit / pool_tvl;
let daily_fee_income = daily_volume * pool_fee_rate * pool_share;
let annual_fee_income = daily_fee_income * DAYS_PER_YEAR;
let fee_apy = if user_deposit > Decimal::ZERO {
annual_fee_income / user_deposit
} else {
Decimal::ZERO
};
let price_ratio = Decimal::ONE + price_change_pct;
let (il_pct, il_amount) = if price_ratio > Decimal::ZERO {
let sqrt_ratio = newton_sqrt(price_ratio)?;
let il = dec!(2) * sqrt_ratio / (Decimal::ONE + price_ratio) - Decimal::ONE;
(il, user_deposit * il.abs())
} else {
warnings.push("Price ratio non-positive after change — IL undefined".into());
(Decimal::ZERO, Decimal::ZERO)
};
let il_vs_fees = if annual_fee_income >= il_amount {
Some("Fees exceed IL".to_string())
} else {
Some("IL exceeds fees".to_string())
};
let effective_apy = fee_apy + il_pct;
let total_return = annual_fee_income - il_amount;
if effective_apy < Decimal::ZERO {
warnings.push("Net APY is negative — impermanent loss exceeds fee income".into());
}
Ok(DefiYieldOutput {
analysis_type: "LiquidityPool".to_string(),
effective_apy,
net_apy: None,
total_return,
impermanent_loss_pct: Some(il_pct),
impermanent_loss_amount: Some(il_amount),
il_vs_fees,
staking_effective_yield: None,
staking_expected_annual_reward: None,
pool_share: Some(pool_share),
pool_fee_income: Some(annual_fee_income),
risk_adjusted_apy: None,
warnings,
})
}
fn compound_iterative(rate: Decimal, n: u32) -> Decimal {
let mut result = Decimal::ONE;
let factor = Decimal::ONE + rate;
for _ in 0..n {
result *= factor;
}
result
}
fn newton_sqrt(value: Decimal) -> CorpFinanceResult<Decimal> {
if value < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "sqrt_input".into(),
reason: "Cannot take square root of a negative number".into(),
});
}
if value.is_zero() {
return Ok(Decimal::ZERO);
}
if value == Decimal::ONE {
return Ok(Decimal::ONE);
}
let mut guess = if value > Decimal::ONE {
value / dec!(2)
} else {
value
};
for _ in 0..NEWTON_SQRT_ITERATIONS {
if guess.is_zero() {
break;
}
guess = (guess + value / guess) / dec!(2);
}
Ok(guess)
}
fn require_field<T>(opt: Option<T>, field_name: &str) -> CorpFinanceResult<T> {
opt.ok_or_else(|| CorpFinanceError::InvalidInput {
field: field_name.to_string(),
reason: format!("{} is required for this analysis type", field_name),
})
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn yield_farm_input() -> DefiYieldInput {
DefiYieldInput {
protocol_name: "TestFarm".to_string(),
analysis_type: DefiAnalysisType::YieldFarm,
base_apr: Some(dec!(0.12)),
reward_apr: None,
compounding_frequency: Some(365),
gas_cost_per_compound: None,
principal: Some(dec!(10000)),
holding_period_days: Some(365),
initial_price_a: None,
initial_price_b: None,
final_price_a: None,
final_price_b: None,
initial_deposit_value: None,
pool_fee_apr: None,
staked_amount: None,
annual_reward_rate: None,
validator_commission: None,
slashing_probability: None,
slashing_penalty: None,
unbonding_period_days: None,
compounding: None,
pool_tvl: None,
user_deposit: None,
daily_volume: None,
pool_fee_rate: None,
token_a_weight: None,
price_change_pct: None,
}
}
fn il_input() -> DefiYieldInput {
DefiYieldInput {
protocol_name: "UniswapV2".to_string(),
analysis_type: DefiAnalysisType::ImpermanentLoss,
base_apr: None,
reward_apr: None,
compounding_frequency: None,
gas_cost_per_compound: None,
principal: None,
holding_period_days: Some(365),
initial_price_a: Some(dec!(1000)),
initial_price_b: Some(dec!(1)),
final_price_a: Some(dec!(2000)),
final_price_b: Some(dec!(1)),
initial_deposit_value: Some(dec!(10000)),
pool_fee_apr: None,
staked_amount: None,
annual_reward_rate: None,
validator_commission: None,
slashing_probability: None,
slashing_penalty: None,
unbonding_period_days: None,
compounding: None,
pool_tvl: None,
user_deposit: None,
daily_volume: None,
pool_fee_rate: None,
token_a_weight: None,
price_change_pct: None,
}
}
fn staking_input() -> DefiYieldInput {
DefiYieldInput {
protocol_name: "EthStaking".to_string(),
analysis_type: DefiAnalysisType::Staking,
base_apr: None,
reward_apr: None,
compounding_frequency: None,
gas_cost_per_compound: None,
principal: None,
holding_period_days: None,
initial_price_a: None,
initial_price_b: None,
final_price_a: None,
final_price_b: None,
initial_deposit_value: None,
pool_fee_apr: None,
staked_amount: Some(dec!(32)),
annual_reward_rate: Some(dec!(0.05)),
validator_commission: Some(dec!(0.10)),
slashing_probability: Some(dec!(0.01)),
slashing_penalty: Some(dec!(0.05)),
unbonding_period_days: Some(14),
compounding: Some(false),
pool_tvl: None,
user_deposit: None,
daily_volume: None,
pool_fee_rate: None,
token_a_weight: None,
price_change_pct: None,
}
}
fn lp_input() -> DefiYieldInput {
DefiYieldInput {
protocol_name: "UniswapV3".to_string(),
analysis_type: DefiAnalysisType::LiquidityPool,
base_apr: None,
reward_apr: None,
compounding_frequency: None,
gas_cost_per_compound: None,
principal: None,
holding_period_days: None,
initial_price_a: None,
initial_price_b: None,
final_price_a: None,
final_price_b: None,
initial_deposit_value: None,
pool_fee_apr: None,
staked_amount: None,
annual_reward_rate: None,
validator_commission: None,
slashing_probability: None,
slashing_penalty: None,
unbonding_period_days: None,
compounding: None,
pool_tvl: Some(dec!(10_000_000)),
user_deposit: Some(dec!(100_000)),
daily_volume: Some(dec!(5_000_000)),
pool_fee_rate: Some(dec!(0.003)),
token_a_weight: Some(dec!(0.5)),
price_change_pct: Some(dec!(0.50)),
}
}
#[test]
fn test_apr_to_apy_daily_compounding() {
let input = yield_farm_input();
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(out.analysis_type, "YieldFarm");
assert!(
out.effective_apy > dec!(0.1274) && out.effective_apy < dec!(0.1276),
"Daily compound APY of 12% APR should be ~12.75%, got {}",
out.effective_apy
);
}
#[test]
fn test_net_apy_after_gas_costs() {
let mut input = yield_farm_input();
input.gas_cost_per_compound = Some(dec!(0.50)); input.principal = Some(dec!(1000));
let result = analyze_defi(&input).unwrap();
let out = &result.result;
let net = out.net_apy.unwrap();
assert!(
net < Decimal::ZERO,
"With high gas on small principal, net APY should be negative, got {}",
net
);
assert!(
out.warnings.iter().any(|w| w.contains("net negative")),
"Should warn about negative net yield"
);
}
#[test]
fn test_impermanent_loss_2x_price_increase() {
let input = il_input(); let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(out.analysis_type, "ImpermanentLoss");
let il = out.impermanent_loss_pct.unwrap();
assert!(
il < dec!(-0.056) && il > dec!(-0.058),
"IL for 2x price change should be ~-5.72%, got {}",
il
);
let il_amt = out.impermanent_loss_amount.unwrap();
assert!(
il_amt > dec!(560) && il_amt < dec!(580),
"IL amount should be ~572, got {}",
il_amt
);
}
#[test]
fn test_impermanent_loss_symmetric_change() {
let mut input = il_input();
input.initial_price_a = Some(dec!(100));
input.initial_price_b = Some(dec!(100));
input.final_price_a = Some(dec!(200));
input.final_price_b = Some(dec!(200));
let result = analyze_defi(&input).unwrap();
let out = &result.result;
let il = out.impermanent_loss_pct.unwrap();
assert!(
il.abs() < dec!(0.0001),
"Symmetric price change should yield ~0 IL, got {}",
il
);
}
#[test]
fn test_il_vs_fees_comparison() {
let mut input = il_input();
input.pool_fee_apr = Some(dec!(0.20)); input.holding_period_days = Some(365);
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(
out.il_vs_fees.as_deref(),
Some("Fees exceed IL"),
"With 20% fee APR, fees should exceed IL"
);
let mut input2 = il_input();
input2.pool_fee_apr = Some(dec!(0.01)); input2.holding_period_days = Some(365);
let result2 = analyze_defi(&input2).unwrap();
let out2 = &result2.result;
assert_eq!(
out2.il_vs_fees.as_deref(),
Some("IL exceeds fees"),
"With 1% fee APR, IL should exceed fees"
);
}
#[test]
fn test_staking_yield_after_commission() {
let input = staking_input();
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(out.analysis_type, "Staking");
let staking_yield = out.staking_effective_yield.unwrap();
assert_eq!(
staking_yield,
dec!(0.045),
"Staking yield after 10% commission on 5% rate should be 4.5%, got {}",
staking_yield
);
assert_eq!(
out.effective_apy,
dec!(0.045),
"Without compounding, APY = net yield"
);
}
#[test]
fn test_staking_risk_adjusted_yield() {
let input = staking_input();
let result = analyze_defi(&input).unwrap();
let out = &result.result;
let risk_adj = out.risk_adjusted_apy.unwrap();
assert_eq!(
risk_adj,
dec!(0.0445),
"Risk-adjusted yield should be 4.45%, got {}",
risk_adj
);
let expected_return = dec!(32) * dec!(0.0445);
assert_eq!(
out.total_return, expected_return,
"Total return should be {}, got {}",
expected_return, out.total_return
);
}
#[test]
fn test_liquidity_pool_share_and_fees() {
let input = lp_input();
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(out.analysis_type, "LiquidityPool");
let share = out.pool_share.unwrap();
assert_eq!(share, dec!(0.01), "Pool share should be 1%, got {}", share);
let fee_income = out.pool_fee_income.unwrap();
assert_eq!(
fee_income,
dec!(54750),
"Annual fee income should be 54750, got {}",
fee_income
);
}
#[test]
fn test_yield_farm_with_reward_apr() {
let mut input = yield_farm_input();
input.base_apr = Some(dec!(0.08)); input.reward_apr = Some(dec!(0.04)); input.compounding_frequency = Some(1);
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert_eq!(
out.effective_apy,
dec!(0.12),
"With annual compounding, APY should equal APR of 12%, got {}",
out.effective_apy
);
}
#[test]
fn test_staking_with_compounding() {
let mut input = staking_input();
input.compounding = Some(true);
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert!(
out.effective_apy > dec!(0.0460) && out.effective_apy < dec!(0.0461),
"Compounded staking APY should be ~4.60%, got {}",
out.effective_apy
);
let simple_yield = out.staking_effective_yield.unwrap();
assert!(
out.effective_apy > simple_yield,
"Compounded APY ({}) should exceed simple yield ({})",
out.effective_apy,
simple_yield
);
}
#[test]
fn test_liquidity_pool_il_from_price_change() {
let input = lp_input(); let result = analyze_defi(&input).unwrap();
let out = &result.result;
let il = out.impermanent_loss_pct.unwrap();
assert!(
il < dec!(-0.019) && il > dec!(-0.021),
"IL for 50% price change should be ~-2.02%, got {}",
il
);
}
#[test]
fn test_yield_farm_total_return() {
let mut input = yield_farm_input();
input.holding_period_days = Some(182);
let result = analyze_defi(&input).unwrap();
let out = &result.result;
let holding_fraction = dec!(182) / dec!(365);
let expected_approx = dec!(10000) * out.effective_apy * holding_fraction;
let diff = (out.total_return - expected_approx).abs();
assert!(
diff < dec!(0.01),
"Total return should be ~{}, got {}",
expected_approx,
out.total_return
);
}
#[test]
fn test_validation_zero_compounding_frequency() {
let mut input = yield_farm_input();
input.compounding_frequency = Some(0);
let result = analyze_defi(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "compounding_frequency");
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_validation_negative_principal() {
let mut input = yield_farm_input();
input.principal = Some(dec!(-1000));
let result = analyze_defi(&input);
assert!(result.is_err());
}
#[test]
fn test_validation_missing_staking_field() {
let mut input = staking_input();
input.staked_amount = None;
let result = analyze_defi(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "staked_amount");
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_il_no_price_change() {
let mut input = il_input();
input.final_price_a = input.initial_price_a; input.final_price_b = input.initial_price_b;
let result = analyze_defi(&input).unwrap();
let out = &result.result;
let il = out.impermanent_loss_pct.unwrap();
assert!(
il.abs() < dec!(0.0001),
"No price change should yield ~0 IL, got {}",
il
);
}
#[test]
fn test_newton_sqrt_known_values() {
let s4 = newton_sqrt(dec!(4)).unwrap();
let diff4 = (s4 - dec!(2)).abs();
assert!(diff4 < dec!(0.0000001), "sqrt(4) should be 2, got {}", s4);
let s2 = newton_sqrt(dec!(2)).unwrap();
let diff2 = (s2 - dec!(1.41421356)).abs();
assert!(
diff2 < dec!(0.00001),
"sqrt(2) should be ~1.41421, got {}",
s2
);
let s1 = newton_sqrt(dec!(1)).unwrap();
assert_eq!(s1, dec!(1));
let s0 = newton_sqrt(dec!(0)).unwrap();
assert_eq!(s0, dec!(0));
}
#[test]
fn test_metadata_populated() {
let input = yield_farm_input();
let result = analyze_defi(&input).unwrap();
assert!(!result.methodology.is_empty());
assert!(result.methodology.contains("DeFi"));
assert_eq!(result.metadata.precision, "rust_decimal_128bit");
assert!(!result.metadata.version.is_empty());
}
#[test]
fn test_high_apr_warning() {
let mut input = yield_farm_input();
input.base_apr = Some(dec!(15.0));
let result = analyze_defi(&input).unwrap();
assert!(
result.result.warnings.iter().any(|w| w.contains("1000%")),
"Should warn about APR exceeding 1000%"
);
}
#[test]
fn test_lp_il_exceeds_fees_negative_return() {
let mut input = lp_input();
input.price_change_pct = Some(dec!(3.0)); input.daily_volume = Some(dec!(100));
let result = analyze_defi(&input).unwrap();
let out = &result.result;
assert!(
out.total_return < Decimal::ZERO,
"With severe IL and low fees, total return should be negative, got {}",
out.total_return
);
assert_eq!(out.il_vs_fees.as_deref(), Some("IL exceeds fees"),);
}
}