use serde::Serialize;
use crate::deflated_sharpe::sharpe_ratio;
use crate::stats::std_dev;
#[derive(Clone, Debug)]
pub struct DominanceChoice {
pub options: Vec<f64>,
pub chosen: usize,
}
impl DominanceChoice {
pub fn is_dominated(&self) -> bool {
match self.options.get(self.chosen) {
Some(&picked) => self.options.iter().any(|&v| v > picked),
None => false,
}
}
}
pub fn rationality_score(choices: &[DominanceChoice]) -> f64 {
if choices.is_empty() {
return 1.0;
}
let bad = choices.iter().filter(|c| c.is_dominated()).count();
1.0 - bad as f64 / choices.len() as f64
}
pub fn has_money_pump(prefs: &[(usize, usize)], n_items: usize) -> bool {
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n_items];
for &(a, b) in prefs {
if a < n_items && b < n_items {
adj[a].push(b);
}
}
fn visit(u: usize, adj: &[Vec<usize>], color: &mut [u8]) -> bool {
color[u] = 1;
for &v in &adj[u] {
if color[v] == 1 || (color[v] == 0 && visit(v, adj, color)) {
return true;
}
}
color[u] = 2;
false
}
let mut color = vec![0u8; n_items];
(0..n_items).any(|u| color[u] == 0 && visit(u, &adj, &mut color))
}
pub fn elicit_revealed_selection(
candidates: &[Vec<f64>],
submitted: &[f64],
) -> Option<DominanceChoice> {
fn comparable_sharpe(returns: &[f64]) -> Option<f64> {
if returns.len() < 2 || std_dev(returns) == 0.0 {
return None;
}
let s = sharpe_ratio(returns);
s.is_finite().then_some(s)
}
let submitted_sharpe = comparable_sharpe(submitted)?;
let mut options: Vec<f64> = candidates
.iter()
.filter_map(|c| comparable_sharpe(c))
.collect();
if options.is_empty() {
return None;
}
options.push(submitted_sharpe);
let chosen = options.len() - 1;
Some(DominanceChoice { options, chosen })
}
#[derive(Clone, Debug, Serialize)]
pub struct EconRationalityReport {
pub rationality_score: f64,
pub dominance_violations: usize,
pub has_money_pump: bool,
}
pub fn assess_rationality(
choices: &[DominanceChoice],
prefs: &[(usize, usize)],
n_items: usize,
) -> EconRationalityReport {
EconRationalityReport {
rationality_score: rationality_score(choices),
dominance_violations: choices.iter().filter(|c| c.is_dominated()).count(),
has_money_pump: has_money_pump(prefs, n_items),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn choice(options: &[f64], chosen: usize) -> DominanceChoice {
DominanceChoice {
options: options.to_vec(),
chosen,
}
}
#[test]
fn always_picking_the_best_is_rational() {
let choices = vec![choice(&[0.1, 0.3, 0.2], 1), choice(&[0.5, 0.4], 0)];
assert_eq!(rationality_score(&choices), 1.0);
assert!(!choices[0].is_dominated());
}
#[test]
fn leaving_value_on_the_table_is_a_violation() {
let choices = vec![
choice(&[0.1, 0.3, 0.2], 0), choice(&[0.5, 0.4], 0), ];
assert_eq!(rationality_score(&choices), 0.5);
assert!(choices[0].is_dominated());
}
#[test]
fn intransitive_preferences_are_a_money_pump() {
assert!(has_money_pump(&[(0, 1), (1, 2), (2, 0)], 3));
}
#[test]
fn transitive_preferences_have_no_pump() {
assert!(!has_money_pump(&[(0, 1), (1, 2), (0, 2)], 3));
}
fn stream(mean_ret: f64, amp: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| mean_ret + amp * (i as f64 * 0.7).sin())
.collect()
}
#[test]
fn elicitor_is_deterministic() {
let candidates = vec![stream(0.001, 0.002, 60), stream(0.004, 0.002, 60)];
let submitted = stream(0.002, 0.002, 60);
let a = elicit_revealed_selection(&candidates, &submitted).expect("elicitable");
let b = elicit_revealed_selection(&candidates, &submitted).expect("elicitable");
assert_eq!(a.options.len(), b.options.len());
for (x, y) in a.options.iter().zip(&b.options) {
assert_eq!(x.to_bits(), y.to_bits());
}
assert_eq!(a.chosen, b.chosen);
}
#[test]
fn submitting_below_a_declared_candidate_is_a_dominance_violation() {
let candidates = vec![stream(0.004, 0.002, 60)];
let submitted = stream(0.001, 0.002, 60);
let choice = elicit_revealed_selection(&candidates, &submitted).expect("elicitable");
assert_eq!(choice.chosen, choice.options.len() - 1);
assert!(choice.is_dominated());
assert_eq!(rationality_score(&[choice]), 0.0);
let best =
elicit_revealed_selection(&candidates, &stream(0.006, 0.002, 60)).expect("elicitable");
assert!(!best.is_dominated());
}
#[test]
fn elicitor_declines_when_nothing_is_recorded() {
let submitted = stream(0.002, 0.002, 60);
assert!(elicit_revealed_selection(&[], &submitted).is_none());
assert!(elicit_revealed_selection(&[vec![0.5; 60]], &submitted).is_none());
assert!(elicit_revealed_selection(std::slice::from_ref(&submitted), &[0.5; 60]).is_none());
}
#[test]
fn combined_report() {
let choices = vec![choice(&[0.2, 0.1], 1)]; let r = assess_rationality(&choices, &[(0, 1), (1, 0)], 2);
assert_eq!(r.dominance_violations, 1);
assert!(r.has_money_pump);
assert_eq!(r.rationality_score, 0.0);
}
}