use kestrel_chartkit::contract::{ContractSpec, Currency, InstrumentType};
use kestrel_chartkit::execution::ExecutionCosts;
use kestrel_chartkit::portfolio::{CashLedger, PositionSide, PositionSnapshot};
use kestrel_chartkit::stress::{
apply_portfolio_stress, multi_asset_block_bootstrap, simulate_equity_paths,
simulate_stop_gap_execution, PathSimulationSummary, StressScenario,
};
#[test]
fn test_golden_stress_neutral_scenario_reproduces_baseline() {
let ledger = CashLedger {
cash: 100_000.0,
..Default::default()
};
let spec = ContractSpec {
price_currency: Currency::eur(),
settlement_currency: Currency::eur(),
multiplier: 25.0,
quantity_step: 1.0,
min_quantity: 1.0,
instrument_type: InstrumentType::LinearFuture,
};
let pos = PositionSnapshot {
symbol: "FDAX".to_string(),
spec,
side: PositionSide::Long,
quantity: 2.0,
entry_price: 19_000.0,
current_price: 19_500.0,
stop_price: Some(18_800.0),
fx_to_account: 1.0,
};
let baseline = kestrel_chartkit::portfolio::evaluate_portfolio(
Currency::eur(),
&ledger,
std::slice::from_ref(&pos),
)
.unwrap();
let neutral_scenario = StressScenario::neutral();
let stressed =
apply_portfolio_stress(Currency::eur(), &ledger, &[pos], &neutral_scenario).unwrap();
assert_eq!(stressed.snapshot.equity, baseline.equity);
assert_eq!(stressed.snapshot.gross_exposure, baseline.gross_exposure);
assert_eq!(stressed.snapshot.net_exposure, baseline.net_exposure);
assert_eq!(stressed.snapshot.unrealized_pnl, baseline.unrealized_pnl);
assert_eq!(stressed.equity_change, 0.0);
assert_eq!(stressed.equity_change_pct, 0.0);
assert_eq!(stressed.gross_exposure_change, 0.0);
}
#[test]
fn test_golden_portfolio_stress_market_crash_and_fx_shock() {
let ledger = CashLedger {
cash: 50_000.0,
..Default::default()
};
let spec = ContractSpec {
price_currency: Currency::usd(),
settlement_currency: Currency::usd(),
multiplier: 50.0,
quantity_step: 1.0,
min_quantity: 1.0,
instrument_type: InstrumentType::LinearFuture,
};
let pos = PositionSnapshot {
symbol: "ES".to_string(),
spec,
side: PositionSide::Long,
quantity: 1.0,
entry_price: 5000.0,
current_price: 5200.0,
stop_price: Some(4800.0),
fx_to_account: 1.0,
};
let crash_scenario = StressScenario {
price_shock_pct: -0.10,
spread_multiplier: 2.0,
slippage_multiplier: 2.0,
fx_shock_pct: 0.0,
participation_cap_multiplier: 0.5,
};
let res = apply_portfolio_stress(Currency::usd(), &ledger, &[pos], &crash_scenario).unwrap();
assert_eq!(res.snapshot.equity, 34_000.0);
assert_eq!(res.snapshot.unrealized_pnl, -16_000.0);
assert_eq!(res.snapshot.gross_exposure, 234_000.0);
assert_eq!(res.equity_change, -26_000.0);
assert!((res.equity_change_pct - (-26_000.0 / 60_000.0)).abs() < 1e-12);
assert_eq!(res.gross_exposure_change, -26_000.0);
}
#[test]
fn test_golden_stop_gap_slippage_execution() {
let fill_normal = simulate_stop_gap_execution(100.0, 102.0, true);
assert_eq!(fill_normal, 100.0);
let fill_gap_down = simulate_stop_gap_execution(100.0, 92.0, true);
assert_eq!(fill_gap_down, 92.0);
let fill_short_normal = simulate_stop_gap_execution(100.0, 98.0, false);
assert_eq!(fill_short_normal, 100.0);
let fill_short_gap_up = simulate_stop_gap_execution(100.0, 108.0, false);
assert_eq!(fill_short_gap_up, 108.0);
}
#[test]
fn test_golden_deterministic_fee_and_spread_stress_scaling() {
let base_costs = ExecutionCosts {
fee_pct: 0.001, spread: 0.50, slippage_pct: 0.0005, };
let stress = StressScenario {
price_shock_pct: 0.0,
spread_multiplier: 3.0,
slippage_multiplier: 4.0,
fx_shock_pct: 0.0,
participation_cap_multiplier: 1.0,
};
let stressed_costs = stress.stressed_costs(base_costs);
assert_eq!(stressed_costs.fee_pct, 0.001);
assert_eq!(stressed_costs.spread, 1.50); assert_eq!(stressed_costs.slippage_pct, 0.0020); }
#[test]
fn test_golden_synchronized_multi_asset_block_bootstrap_preserves_pairs() {
let asset_a = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let asset_b = vec![100.0, 200.0, 300.0, 400.0, 500.0];
let bootstrapped = multi_asset_block_bootstrap(
&[asset_a, asset_b],
2, 10, 5, 12345, )
.unwrap();
assert_eq!(bootstrapped.len(), 5);
for path in &bootstrapped {
assert_eq!(path.len(), 2); let path_a = &path[0];
let path_b = &path[1];
assert_eq!(path_a.len(), 10);
assert_eq!(path_b.len(), 10);
for t in 0..10 {
assert!(
(path_b[t] - path_a[t] * 10.0).abs() < 1e-12,
"Contemporaneous pair relationship violated at step {t}: A={}, B={}",
path_a[t],
path_b[t]
);
}
}
}
#[test]
fn test_golden_path_simulation_seed_determinism() {
let returns = vec![0.01, -0.005, 0.02, -0.015, 0.008, 0.012, -0.02, 0.005];
let initial_equity = 10_000.0;
let seed = 987654321;
let run1 = simulate_equity_paths(initial_equity, &returns, 2, 20, 100, seed).unwrap();
let run2 = simulate_equity_paths(initial_equity, &returns, 2, 20, 100, seed).unwrap();
assert_eq!(
run1.terminal_equity_quantiles,
run2.terminal_equity_quantiles
);
assert_eq!(run1.max_drawdown_quantiles, run2.max_drawdown_quantiles);
assert_eq!(
run1.empirical_mean_terminal_equity,
run2.empirical_mean_terminal_equity
);
}
#[test]
fn test_golden_drawdown_exceedance_probability_calculation() {
let drawdowns = [0.05, 0.08, 0.10, 0.12, 0.15, 0.18, 0.20, 0.22, 0.25, 0.30];
let prob_20 = PathSimulationSummary::probability_drawdown_exceeds(&drawdowns, 0.20);
assert!((prob_20 - 0.40).abs() < 1e-12);
let prob_50 = PathSimulationSummary::probability_drawdown_exceeds(&drawdowns, 0.50);
assert_eq!(prob_50, 0.0);
}