optionstratlib_visualization/visualization/
simulation.rs1use crate::visualization::{ColorScheme, Graph, GraphConfig, GraphData, Series2D, TraceMode};
10use optionstratlib_core::model::Positive;
11use optionstratlib_simulation::simulation::randomwalk::RandomWalk;
12use optionstratlib_simulation::simulation::simulator::Simulator;
13use rust_decimal::Decimal;
14use std::fmt::Display;
15use std::ops::AddAssign;
16
17const SIMULATION_CHART_WIDTH: u32 = 1600;
19const SIMULATION_CHART_HEIGHT: u32 = 900;
21const SIMULATION_LINE_WIDTH: f64 = 2.0;
23const RANDOM_WALK_LINE_COLOR: &str = "#1f77b4";
25
26impl<X, Y> Graph for RandomWalk<X, Y>
27where
28 X: Copy + TryInto<Positive> + AddAssign + Display,
29 Y: TryInto<Positive> + Display + Clone,
30{
31 fn graph_data(&self) -> GraphData {
32 let steps = self.get_steps();
33 let y: Vec<Decimal> = steps
34 .iter()
35 .map(|step| step.get_graph_y_value().unwrap_or(Positive::ZERO).to_dec())
36 .collect();
37 let x: Vec<Decimal> = steps
38 .iter()
39 .map(|step| -step.get_graph_x_in_days_left().to_dec())
40 .collect();
41
42 GraphData::Series(Series2D {
43 x,
44 y,
45 name: self.get_title().to_string(),
46 mode: TraceMode::Lines,
47 line_color: Some(RANDOM_WALK_LINE_COLOR.to_string()),
48 line_width: Some(SIMULATION_LINE_WIDTH),
49 })
50 }
51
52 fn graph_config(&self) -> GraphConfig {
53 GraphConfig {
54 title: self.get_title().to_string(),
55 x_label: Some("Date".to_string()),
56 y_label: Some("Price".to_string()),
57 z_label: None,
58 width: SIMULATION_CHART_WIDTH,
59 height: SIMULATION_CHART_HEIGHT,
60 show_legend: false,
61 color_scheme: ColorScheme::Default,
62 line_style: Default::default(),
63 legend: None,
64 }
65 }
66}
67
68impl<X, Y> Graph for Simulator<X, Y>
69where
70 X: Copy + TryInto<Positive> + AddAssign + Display,
71 Y: TryInto<Positive> + Display + Clone,
72{
73 fn graph_data(&self) -> GraphData {
74 let random_walks = self.get_steps();
75 let mut series: Vec<Series2D> = Vec::with_capacity(random_walks.len());
76 for (i, steps) in random_walks.iter().enumerate() {
77 let y: Vec<Decimal> = steps
78 .iter()
79 .map(|step| step.get_graph_y_value().unwrap_or(Positive::ZERO).to_dec())
80 .collect();
81 let x: Vec<Decimal> = steps
82 .iter()
83 .map(|step| -step.get_graph_x_in_days_left().to_dec())
84 .collect();
85 series.push(Series2D {
86 x,
87 y,
88 name: format!("Sim_{i}"),
89 mode: TraceMode::Lines,
90 line_color: None,
91 line_width: Some(SIMULATION_LINE_WIDTH),
92 });
93 }
94 GraphData::MultiSeries(series)
95 }
96
97 fn graph_config(&self) -> GraphConfig {
98 GraphConfig {
99 title: self.get_title().to_string(),
100 x_label: Some("Date".to_string()),
101 y_label: Some("Price".to_string()),
102 z_label: None,
103 width: SIMULATION_CHART_WIDTH,
104 height: SIMULATION_CHART_HEIGHT,
105 show_legend: true,
106 color_scheme: ColorScheme::HighContrast,
107 line_style: Default::default(),
108 legend: None,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use optionstratlib_core::model::ExpirationDate;
117 use optionstratlib_core::pos_or_panic;
118 use optionstratlib_core::utils::TimeFrame;
119 use optionstratlib_simulation::simulation::steps::Step;
120 use optionstratlib_simulation::simulation::{
121 WalkParams, WalkType, WalkTypeAble, generator_positive,
122 };
123 use rust_decimal_macros::dec;
124
125 #[derive(Clone)]
126 struct TestWalker;
127 impl WalkTypeAble<Positive, Positive> for TestWalker {}
128
129 fn params() -> WalkParams<Positive, Positive> {
130 WalkParams {
131 size: 3,
132 init_step: Step::new(
133 Positive::ONE,
134 TimeFrame::Day,
135 ExpirationDate::Days(pos_or_panic!(30.0)),
136 Positive::HUNDRED,
137 ),
138 walker: Box::new(TestWalker),
139 walk_type: WalkType::Historical {
140 timeframe: TimeFrame::Day,
141 prices: vec![
142 Positive::HUNDRED,
143 pos_or_panic!(101.0),
144 pos_or_panic!(102.0),
145 ],
146 symbol: None,
147 },
148 seed: None,
149 }
150 }
151
152 fn simulator(size: usize) -> Simulator<Positive, Positive> {
153 Simulator::new(
154 "Graph Test".to_string(),
155 size,
156 ¶ms(),
157 generator_positive,
158 )
159 .unwrap()
160 }
161
162 #[test]
163 fn test_random_walk_graph_data_is_one_series_of_prices() {
164 let sim = simulator(1);
165 let walk = sim.first().unwrap();
166 match walk.graph_data() {
167 GraphData::Series(series) => {
168 assert_eq!(series.name, "Graph Test_0");
169 assert_eq!(series.y, vec![dec!(100), dec!(101), dec!(102)]);
170 assert_eq!(series.x.len(), 3);
171 assert!(series.x.windows(2).all(|w| w[0] <= w[1]));
172 assert_eq!(series.line_color.as_deref(), Some(RANDOM_WALK_LINE_COLOR));
173 }
174 other => panic!("expected a single series, got {other:?}"),
175 }
176 let config = walk.graph_config();
177 assert_eq!(config.title, "Graph Test_0");
178 assert!(!config.show_legend);
179 assert_eq!(config.width, SIMULATION_CHART_WIDTH);
180 }
181
182 #[test]
183 fn test_simulator_graph_data_has_one_series_per_walk() {
184 let sim = simulator(3);
185 match sim.graph_data() {
186 GraphData::MultiSeries(series) => {
187 assert_eq!(series.len(), 3);
188 let names: Vec<&str> = series.iter().map(|s| s.name.as_str()).collect();
189 assert_eq!(names, vec!["Sim_0", "Sim_1", "Sim_2"]);
190 assert!(series.iter().all(|s| s.y.len() == 3));
191 }
192 other => panic!("expected multiple series, got {other:?}"),
193 }
194 let config = sim.graph_config();
195 assert_eq!(config.title, "Graph Test");
196 assert!(config.show_legend);
197 assert_eq!(config.height, SIMULATION_CHART_HEIGHT);
198 }
199
200 #[cfg(feature = "plotly")]
201 #[test]
202 fn test_simulator_write_html_round_trip() -> Result<(), Box<dyn std::error::Error>> {
203 use std::{fs, path::Path};
204
205 let sim = simulator(2);
206 let file_name = "Draws/Simulation/test_simulator.html".as_ref();
207 sim.write_html(file_name)?;
208 if Path::new(file_name).exists() {
209 fs::remove_file(file_name)?;
210 }
211 Ok(())
212 }
213}