From 8d10435e9945559b61cab9a48b5ae271faf2f8c9 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:44:27 +0000 Subject: [PATCH] feat: add bankroll growth tool for kelly criterion Adds the Kelly Criterion Bankroll Growth Tool to the math_explorer_gui. It simulates multiple iterations of a fractional Kelly strategy and visualizes the growth using `egui_plot`. The architecture adheres to the Strategy Pattern by introducing a `FinancialMathTab` and `FinancialMathTool`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- math_explorer_gui/src/app.rs | 8 +- .../src/tabs/ai/attention_maps.rs | 31 +-- .../battery_degradation/lifetime_estimator.rs | 4 +- .../tabs/financial_math/bankroll_growth.rs | 181 ++++++++++++++++++ .../src/tabs/financial_math/mod.rs | 58 ++++++ math_explorer_gui/src/tabs/mod.rs | 1 + todo_gui.md | 2 +- 7 files changed, 265 insertions(+), 20 deletions(-) create mode 100644 math_explorer_gui/src/tabs/financial_math/bankroll_growth.rs create mode 100644 math_explorer_gui/src/tabs/financial_math/mod.rs diff --git a/math_explorer_gui/src/app.rs b/math_explorer_gui/src/app.rs index 16122a74..3d4b241a 100644 --- a/math_explorer_gui/src/app.rs +++ b/math_explorer_gui/src/app.rs @@ -1,9 +1,10 @@ use crate::tabs::{ ai::AiTab, battery_degradation::BatteryDegradationTab, chaos::ChaosTab, clinical_trials::ClinicalTrialsTab, epidemiology::EpidemiologyTab, favoritism::FavoritismTab, - fluid_dynamics::FluidDynamicsTab, game_theory::GameTheoryTab, medical::MedicalTab, - morphogenesis::MorphogenesisTab, mri::MriTab, neuroscience::NeuroscienceTab, - number_theory::NumberTheoryTab, quantum::QuantumTab, solid_state::SolidStateTab, ExplorerTab, + financial_math::FinancialMathTab, fluid_dynamics::FluidDynamicsTab, game_theory::GameTheoryTab, + medical::MedicalTab, morphogenesis::MorphogenesisTab, mri::MriTab, + neuroscience::NeuroscienceTab, number_theory::NumberTheoryTab, quantum::QuantumTab, + solid_state::SolidStateTab, ExplorerTab, }; use eframe::egui; @@ -31,6 +32,7 @@ impl Default for MathExplorerApp { Box::new(BatteryDegradationTab::default()), Box::new(AiTab::default()), Box::new(FavoritismTab::default()), + Box::new(FinancialMathTab::default()), ], selected_tab: 0, } diff --git a/math_explorer_gui/src/tabs/ai/attention_maps.rs b/math_explorer_gui/src/tabs/ai/attention_maps.rs index e64210c1..9a5be3ef 100644 --- a/math_explorer_gui/src/tabs/ai/attention_maps.rs +++ b/math_explorer_gui/src/tabs/ai/attention_maps.rs @@ -29,12 +29,13 @@ impl Default for AttentionMapsTool { impl AttentionMapsTool { fn recalculate(&mut self) { - let (output, weights) = math_explorer::ai::transformer::attention::scaled_dot_product_attention( - &self.q_matrix, - &self.k_matrix, - &self.v_matrix, - None, - ); + let (output, weights) = + math_explorer::ai::transformer::attention::scaled_dot_product_attention( + &self.q_matrix, + &self.k_matrix, + &self.v_matrix, + None, + ); self.output_matrix = output; self.attention_weights = weights; } @@ -93,10 +94,8 @@ impl AttentionMapsTool { ((1.0 - normalized) * 200.0 + 55.0) as u8, ); - let (rect, _response) = ui.allocate_exact_size( - egui::vec2(40.0, 40.0), - egui::Sense::hover(), - ); + let (rect, _response) = + ui.allocate_exact_size(egui::vec2(40.0, 40.0), egui::Sense::hover()); ui.painter().rect_filled(rect, 2.0, color); @@ -135,13 +134,15 @@ impl AiTool for AttentionMapsTool { ui.vertical(|ui| { let mut changed = false; ui.group(|ui| { - changed |= Self::draw_matrix_input(ui, "Queries (Q)", &mut self.q_matrix); + changed |= + Self::draw_matrix_input(ui, "Queries (Q)", &mut self.q_matrix); }); ui.group(|ui| { changed |= Self::draw_matrix_input(ui, "Keys (K)", &mut self.k_matrix); }); ui.group(|ui| { - changed |= Self::draw_matrix_input(ui, "Values (V)", &mut self.v_matrix); + changed |= + Self::draw_matrix_input(ui, "Values (V)", &mut self.v_matrix); }); if changed { @@ -153,7 +154,11 @@ impl AiTool for AttentionMapsTool { ui.vertical(|ui| { ui.group(|ui| { - Self::draw_heatmap(ui, "Attention Weights (softmax(Q * K^T / sqrt(d_k)))", &self.attention_weights); + Self::draw_heatmap( + ui, + "Attention Weights (softmax(Q * K^T / sqrt(d_k)))", + &self.attention_weights, + ); }); ui.add_space(20.0); diff --git a/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs b/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs index 0583a6da..33f134ca 100644 --- a/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs +++ b/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs @@ -1,8 +1,6 @@ use super::BatteryDegradationTool; use eframe::egui; -use math_explorer::applied::battery_degradation::{ - Capacity, DepthOfDischarge, PowerLawModel, -}; +use math_explorer::applied::battery_degradation::{Capacity, DepthOfDischarge, PowerLawModel}; pub struct LifetimeEstimatorTool { target_capacity: f64, diff --git a/math_explorer_gui/src/tabs/financial_math/bankroll_growth.rs b/math_explorer_gui/src/tabs/financial_math/bankroll_growth.rs new file mode 100644 index 00000000..9bdd7e71 --- /dev/null +++ b/math_explorer_gui/src/tabs/financial_math/bankroll_growth.rs @@ -0,0 +1,181 @@ +use super::FinancialMathTool; +use eframe::egui; +use egui_plot::{Line, Plot, PlotPoints}; +use math_explorer::pure_math::statistics::kelly::{ + kelly_fraction, variants, BankrollFraction, EdgeProbability, Odds, +}; +use rand::Rng; + +pub struct BankrollGrowthTool { + initial_bankroll: f64, + probability: f64, + odds: f64, + num_bets: usize, + + // Cached plot data + full_kelly_points: Vec<[f64; 2]>, + half_kelly_points: Vec<[f64; 2]>, + quarter_kelly_points: Vec<[f64; 2]>, + + error_msg: Option, +} + +impl Default for BankrollGrowthTool { + fn default() -> Self { + let mut tool = Self { + initial_bankroll: 1000.0, + probability: 0.55, + odds: 2.0, // Decimal odds (net profit multiplier + 1, wait Kelly odds b is net profit multiplier. The Odds type in math_explorer: Odds::new(b). Let's check.) + num_bets: 100, + full_kelly_points: vec![], + half_kelly_points: vec![], + quarter_kelly_points: vec![], + error_msg: None, + }; + tool.recalculate(); + tool + } +} + +impl BankrollGrowthTool { + fn recalculate(&mut self) { + self.full_kelly_points.clear(); + self.half_kelly_points.clear(); + self.quarter_kelly_points.clear(); + self.error_msg = None; + + let prob_result = EdgeProbability::new(self.probability); + let odds_result = Odds::new(self.odds); + + match (prob_result, odds_result) { + (Ok(p), Ok(o)) => { + let full_kelly_res = kelly_fraction(&p, &o); + let half_kelly_res = variants::half_kelly(&p, &o); + let quarter_kelly_res = variants::quarter_kelly(&p, &o); + + match (full_kelly_res, half_kelly_res, quarter_kelly_res) { + (Ok(fk), Ok(hk), Ok(qk)) => { + let mut rng = rand::thread_rng(); + + let mut fk_bankroll = self.initial_bankroll; + let mut hk_bankroll = self.initial_bankroll; + let mut qk_bankroll = self.initial_bankroll; + + self.full_kelly_points.push([0.0, fk_bankroll]); + self.half_kelly_points.push([0.0, hk_bankroll]); + self.quarter_kelly_points.push([0.0, qk_bankroll]); + + for i in 1..=self.num_bets { + let win = rng.r#gen::() < p.value(); + + // Let's create a helper closure for bankroll update + let update_bankroll = + |bankroll: &mut f64, fraction: &BankrollFraction| { + let bet_amount = *bankroll * fraction.value(); + if win { + *bankroll += bet_amount * o.value(); + } else { + *bankroll -= bet_amount; + } + }; + + update_bankroll(&mut fk_bankroll, &fk); + update_bankroll(&mut hk_bankroll, &hk); + update_bankroll(&mut qk_bankroll, &qk); + + self.full_kelly_points.push([i as f64, fk_bankroll]); + self.half_kelly_points.push([i as f64, hk_bankroll]); + self.quarter_kelly_points.push([i as f64, qk_bankroll]); + } + } + (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => { + self.error_msg = Some(format!("Error: {:?}", e)); // e is an enum + } + } + } + (Err(e), _) => { + self.error_msg = Some(format!("Error: {:?}", e)); + } + (_, Err(e)) => { + self.error_msg = Some(format!("Error: {:?}", e)); + } + } + } +} + +impl FinancialMathTool for BankrollGrowthTool { + fn name(&self) -> &'static str { + "Bankroll Growth" + } + + fn show(&mut self, ui: &mut egui::Ui) { + ui.vertical(|ui| { + ui.heading("Parameters"); + let mut changed = false; + + changed |= ui + .add( + egui::Slider::new(&mut self.initial_bankroll, 100.0..=10000.0) + .text("Initial Bankroll"), + ) + .changed(); + changed |= ui + .add( + egui::Slider::new(&mut self.probability, 0.01..=0.99) + .text("Win Probability (p)"), + ) + .changed(); + changed |= ui + .add(egui::Slider::new(&mut self.odds, 0.1..=10.0).text("Net Odds (b)")) + .on_hover_text("Net profit multiplier. E.g., for +100 / even money, b = 1.0.") + .changed(); + changed |= ui + .add(egui::Slider::new(&mut self.num_bets, 10..=1000).text("Number of Bets")) + .changed(); + + if ui.button("Rerun Simulation").clicked() { + changed = true; + } + + if changed { + self.recalculate(); + } + + ui.separator(); + + if let Some(ref err) = self.error_msg { + ui.colored_label(egui::Color32::RED, err); + } else { + ui.heading("Bankroll Simulation"); + + let plot = Plot::new("bankroll_growth_plot") + .view_aspect(2.0) + .legend(egui_plot::Legend::default()); + + plot.show(ui, |plot_ui| { + plot_ui.line( + Line::new( + "Full Kelly", + PlotPoints::new(self.full_kelly_points.clone()), + ) + .color(egui::Color32::RED), + ); + plot_ui.line( + Line::new( + "Half Kelly", + PlotPoints::new(self.half_kelly_points.clone()), + ) + .color(egui::Color32::YELLOW), + ); + plot_ui.line( + Line::new( + "Quarter Kelly", + PlotPoints::new(self.quarter_kelly_points.clone()), + ) + .color(egui::Color32::GREEN), + ); + }); + } + }); + } +} diff --git a/math_explorer_gui/src/tabs/financial_math/mod.rs b/math_explorer_gui/src/tabs/financial_math/mod.rs new file mode 100644 index 00000000..3abca7a7 --- /dev/null +++ b/math_explorer_gui/src/tabs/financial_math/mod.rs @@ -0,0 +1,58 @@ +use crate::tabs::ExplorerTab; +use eframe::egui; + +pub mod bankroll_growth; + +/// A trait for sub-tools within the Financial Math tab. +pub trait FinancialMathTool { + /// Returns the name of the tool. + fn name(&self) -> &'static str; + + /// Renders the tool's UI. + fn show(&mut self, ui: &mut egui::Ui); +} + +pub struct FinancialMathTab { + tools: Vec>, + selected_tool_index: usize, +} + +impl Default for FinancialMathTab { + fn default() -> Self { + Self { + tools: vec![Box::new(bankroll_growth::BankrollGrowthTool::default())], + selected_tool_index: 0, + } + } +} + +impl ExplorerTab for FinancialMathTab { + fn name(&self) -> &'static str { + "Financial Math" + } + + fn show(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::SidePanel::left("financial_math_tool_selector").show(ctx, |ui| { + ui.heading("Tools"); + ui.separator(); + for (i, tool) in self.tools.iter().enumerate() { + if ui + .selectable_label(self.selected_tool_index == i, tool.name()) + .clicked() + { + self.selected_tool_index = i; + } + } + }); + + egui::CentralPanel::default().show(ctx, |ui| { + if let Some(tool) = self.tools.get_mut(self.selected_tool_index) { + tool.show(ui); + } else { + ui.centered_and_justified(|ui| { + ui.label("No tool selected"); + }); + } + }); + } +} diff --git a/math_explorer_gui/src/tabs/mod.rs b/math_explorer_gui/src/tabs/mod.rs index f6c6d2a0..cf1270b3 100644 --- a/math_explorer_gui/src/tabs/mod.rs +++ b/math_explorer_gui/src/tabs/mod.rs @@ -6,6 +6,7 @@ pub mod chaos; pub mod clinical_trials; pub mod epidemiology; pub mod favoritism; +pub mod financial_math; pub mod fluid_dynamics; pub mod game_theory; pub mod medical; diff --git a/todo_gui.md b/todo_gui.md index 122c3e52..8c709987 100644 --- a/todo_gui.md +++ b/todo_gui.md @@ -125,7 +125,7 @@ This document outlines the roadmap for integrating the various modules of the `m ### 4.4 Financial Math (Kelly Criterion) * **Module:** `pure_math::statistics::kelly` * **Features:** - * [ ] **Bankroll Growth:** Simulation of wealth over multiple bets using Kelly vs. fractional Kelly. + * [x] **Bankroll Growth:** Simulation of wealth over multiple bets using Kelly vs. fractional Kelly. * [ ] **Bet Size Calculator:** Input fields for odds and probability of winning. ---