use std::cmp::Reverse;
use serde::Serialize;
use crate::optimize::rank_key;
use crate::params::TaxTables;
use crate::plan::{Dollars, Plan, push_issue};
use crate::project::{MarketPath, Projection, deflate, project_on};
use super::{History, draw, start_in};
use crate::search::{Progress, RunError, run_all};
pub const BAND_PERCENTILES: [u8; 5] = [10, 25, 50, 75, 90];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunName {
Planned,
Trial(u32),
Start(i16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Run {
pub name: RunName,
pub ending: Dollars,
pub unfunded: Dollars,
pub first_short: Option<i16>,
pub net_worth: Vec<Dollars>,
pub is_success: bool,
rank: (Dollars, Reverse<Dollars>),
}
impl Run {
fn of(name: RunName, projection: &Projection, leave_at_least: Option<Dollars>) -> Self {
let summary = projection.summary(true);
let first_short = summary.first_unfunded_year;
let ending = summary.final_net_worth;
Self {
name,
ending,
unfunded: summary.lifetime_unfunded,
first_short,
net_worth: projection
.years
.iter()
.map(|row| deflate(row.net_worth, row.deflator))
.collect(),
is_success: first_short.is_none() && leave_at_least.is_none_or(|floor| ending >= floor),
rank: rank_key(projection),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Band {
pub year: i16,
pub net_worth: [Dollars; BAND_PERCENTILES.len()],
pub funded: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Runs {
pub planned: Run,
pub runs: Vec<Run>,
pub successes: usize,
pub bands: Vec<Band>,
}
impl Runs {
fn of(planned: Run, runs: Vec<Run>, start_year: i16) -> Self {
let years = planned.net_worth.len();
let bands = (0..years)
.map(|at| {
let year = start_year + i16::try_from(at).unwrap_or(i16::MAX);
let mut worths: Vec<Dollars> = runs.iter().map(|run| run.net_worth[at]).collect();
worths.sort_unstable();
let funded = runs
.iter()
.filter(|run| run.first_short.is_none_or(|short| short > year))
.count();
Band {
year,
net_worth: BAND_PERCENTILES
.map(|percentile| at_percentile(&worths, percentile).unwrap_or_default()),
funded: funded as f64 / runs.len().max(1) as f64,
}
})
.collect();
Self {
successes: runs.iter().filter(|run| run.is_success).count(),
planned,
runs,
bands,
}
}
#[must_use]
pub fn success_rate(&self) -> f64 {
self.successes as f64 / self.runs.len().max(1) as f64
}
#[must_use]
pub fn worst_first(&self) -> Vec<usize> {
let mut order: Vec<usize> = (0..self.runs.len()).collect();
order.sort_by_key(|&at| (Reverse(self.runs[at].rank), at));
order
}
}
fn at_percentile<T: Copy>(sorted: &[T], percentile: u8) -> Option<T> {
let last = sorted.len().checked_sub(1)?;
let at = (f64::from(percentile) / 100.0 * last as f64).round() as usize;
Some(sorted[at.min(last)])
}
#[derive(Debug, Clone, PartialEq)]
pub struct MonteCarlo {
pub runs: Runs,
pub singled_out: Vec<usize>,
}
fn planned(plan: &Plan, tables: &TaxTables) -> Run {
let projection = project_on(plan, tables, &MarketPath::expected(plan));
Run::of(
RunName::Planned,
&projection,
plan.market().leave_at_least(),
)
}
pub fn monte_carlo(
plan: &Plan,
tables: &TaxTables,
history: &History,
progress: &Progress,
) -> Result<MonteCarlo, RunError> {
let market = plan.market();
let floor = market.leave_at_least();
let count = usize::try_from(market.trials()).unwrap_or_default();
let runs = run_all(count, progress, |at| {
let trial = u32::try_from(at).unwrap_or(u32::MAX);
let projection = project_on(plan, tables, &draw(plan, history, trial));
Some(Run::of(RunName::Trial(trial), &projection, floor))
})?;
let runs = Runs::of(planned(plan, tables), runs, plan.plan.start_year);
let worst_first = runs.worst_first();
let mut singled_out: Vec<usize> = BAND_PERCENTILES
.iter()
.rev()
.filter_map(|&percentile| at_percentile(&worst_first, percentile))
.collect();
singled_out.extend(worst_first.first());
Ok(MonteCarlo { runs, singled_out })
}
pub fn historical(
plan: &Plan,
tables: &TaxTables,
history: &History,
progress: &Progress,
) -> Result<Runs, RunError> {
let market = plan.market();
let (from, to) = (market.from(), market.to());
let covered = format!(
"the record covers {} to {}",
history.first_year(),
history.last_year()
);
let mut issues = Vec::new();
if from < history.first_year() {
push_issue(&mut issues, "market.historical.from", &covered);
}
if to > history.last_year() {
push_issue(&mut issues, "market.historical.to", &covered);
}
if !issues.is_empty() {
return Err(RunError::Refused(issues));
}
let floor = market.leave_at_least();
let count = usize::try_from(to - from + 1).unwrap_or_default();
let runs = run_all(count, progress, |at| {
let start = from + i16::try_from(at).unwrap_or(i16::MAX);
let path = start_in(plan, history, start, market.wrap())?;
Some(Run::of(
RunName::Start(start),
&project_on(plan, tables, &path),
floor,
))
})?;
if runs.is_empty() {
push_issue(
&mut issues,
"market.historical.wrap",
"no start year has a whole horizon of history; wrap, or start earlier",
);
return Err(RunError::Refused(issues));
}
Ok(Runs::of(planned(plan, tables), runs, plan.plan.start_year))
}
#[must_use]
pub fn replay(
plan: &Plan,
tables: &TaxTables,
history: &History,
name: RunName,
) -> Option<Projection> {
let path = match name {
RunName::Planned => MarketPath::expected(plan),
RunName::Trial(trial) => draw(plan, history, trial),
RunName::Start(start) => start_in(plan, history, start, plan.market().wrap())?,
};
Some(project_on(plan, tables, &path))
}