Skip to main content

optionstratlib_simulation/simulation/steps/
step.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 23/3/25
5******************************************************************************/
6use crate::error::SimulationError;
7use crate::simulation::steps::{Xstep, Ystep};
8use num_traits::FromPrimitive;
9use optionstratlib_core::model::ExpirationDate;
10use optionstratlib_core::model::Positive;
11use optionstratlib_core::utils::TimeFrame;
12use rust_decimal::Decimal;
13use serde::ser::SerializeStruct;
14use serde::{Serialize, Serializer};
15use std::convert::TryInto;
16use std::fmt::{Display, Formatter};
17use std::ops::AddAssign;
18
19/// Represents a combined x-y step in a two-dimensional simulation or analysis.
20///
21/// This struct pairs an x-direction step (typically representing time) with a y-direction step
22/// (typically representing price or value). It's designed for financial simulations, data
23/// visualization, or any context requiring tracking of values across both a time dimension
24/// and a value dimension.
25///
26/// The generic parameters allow for flexibility in the types of values stored in both
27/// dimensions, while enforcing appropriate constraints to ensure mathematical operations
28/// can be performed safely.
29///
30/// # Type Parameters
31///
32/// * `X` - The type for x-axis values, which must implement `AddAssign` (allowing values to be
33///   accumulated), be convertible to `Positive`, and be `Copy`.
34///
35/// * `Y` - The type for y-axis values, which must be `Copy` and convertible to `Positive`.
36///
37/// # Fields
38///
39/// * `x` - An `Xstep<X>` instance representing the time dimension step with its associated
40///   temporal information and value.
41///
42/// * `y` - A `Ystep<Y>` instance representing the value dimension step with its associated index
43///   and numeric value.
44///
45/// # Usage
46///
47/// Typically used in financial modeling, time series analysis, and visualization contexts where
48/// coordinated progression along both time and value axes is needed.
49#[derive(Debug, Clone)]
50pub struct Step<X, Y>
51where
52    X: Copy + TryInto<Positive> + AddAssign + Display,
53    Y: TryInto<Positive> + Display + Clone,
54{
55    /// The x-axis step containing temporal information and an associated value
56    pub x: Xstep<X>,
57
58    /// The y-axis step containing an index and an associated positive value
59    pub y: Ystep<Y>,
60}
61
62/// Implementation of methods for the `Step` struct, which represents a point in a two-dimensional
63/// sequential space with both X and Y coordinates.
64///
65/// `Step<X, Y>` is designed to track position in simulations or financial models where both
66/// dimensions have meaningful values that must remain positive. Each step maintains its position
67/// in a sequence along with associated values.
68///
69/// # Type Parameters
70///
71/// * `X` - The x-axis value type that must implement `AddAssign`, be convertible to `Positive`, and be `Copy`.
72/// * `Y` - The y-axis value type that must be `Copy` and convertible to `Positive`.
73///
74impl<X, Y> Step<X, Y>
75where
76    X: Copy + TryInto<Positive> + AddAssign + Display,
77    Y: TryInto<Positive> + Display + Clone,
78{
79    /// Creates a new Step with the given X and Y coordinates
80    ///
81    /// # Parameters
82    ///
83    /// * `x_value` - The initial x-axis value
84    /// * `time_unit` - The time frame unit to use for this step
85    /// * `datetime` - The expiration date information
86    /// * `y_value` - The initial y-axis value
87    ///
88    /// # Returns
89    ///
90    /// A new `Step<X, Y>` instance with initialized x and y components
91    pub fn new(x_value: X, time_unit: TimeFrame, datetime: ExpirationDate, y_value: Y) -> Self {
92        Self {
93            x: Xstep::new(x_value, time_unit, datetime),
94            y: Ystep::new(0, y_value),
95        }
96    }
97
98    /// Move to the next step in the sequence
99    ///
100    /// Creates a new step with incremented x-position and index, using the provided y-value.
101    /// This is typically used to advance forward in a simulation or calculation sequence.
102    ///
103    /// # Parameters
104    ///
105    /// * `new_y_value` - The y-axis value to use for the next step
106    ///
107    /// # Returns
108    ///
109    /// A new `Step<X, Y>` instance that represents the next step in the sequence
110    ///
111    /// # Errors
112    ///
113    /// Returns [`SimulationError::ExpirationReached`] when the
114    /// current step is already at the end of the sequence, or
115    /// [`SimulationError::IndexConversion`] when the step index would
116    /// overflow `i32`.
117    pub fn next(&self, new_y_value: Y) -> Result<Self, SimulationError> {
118        let next_x = match self.x.next() {
119            Ok(x_step) => x_step,
120            Err(e) => {
121                return Err(SimulationError::step_error(&format!(
122                    "Cannot generate next step. Expiration date is already reached: {e}"
123                )));
124            }
125        };
126        let next_index = self.y.index().checked_add(1).ok_or_else(|| {
127            SimulationError::step_error("step index overflowed advancing the walk")
128        })?;
129        Ok(Self {
130            x: next_x,
131            y: Ystep::new(next_index, new_y_value),
132        })
133    }
134
135    /// Move to the previous step in the sequence
136    ///
137    /// Creates a new step with decremented x-position and index, using the provided y-value.
138    /// This is typically used to move backward in a simulation or calculation sequence.
139    ///
140    /// # Parameters
141    ///
142    /// * `new_y_value` - The y-axis value to use for the previous step
143    ///
144    /// # Returns
145    ///
146    /// A new `Step<X, Y>` instance that represents the previous step in the sequence
147    ///
148    /// # Errors
149    ///
150    /// Returns [`SimulationError::IndexConversion`] when the step
151    /// index would wrap below `i32::MIN`, or
152    /// [`SimulationError::ExpirationDate`] when the underlying
153    /// [`Xstep::previous`] datetime shift fails.
154    pub fn previous(&self, new_y_value: Y) -> Result<Self, SimulationError> {
155        let previous_x = match self.x.previous() {
156            Ok(x_step) => x_step,
157            Err(e) => {
158                return Err(SimulationError::step_error(&format!(
159                    "Cannot generate previous step. Expiration date is already reached: {e}"
160                )));
161            }
162        };
163        let previous_index = self.y.index().checked_sub(1).ok_or_else(|| {
164            SimulationError::step_error("step index underflowed rewinding the walk")
165        })?;
166        Ok(Self {
167            x: previous_x,
168            y: Ystep::new(previous_index, new_y_value),
169        })
170    }
171
172    /// Returns the x-value prepared for graphing operations
173    ///
174    /// Converts the current x-axis index to a `Positive` value that can be used
175    /// in graphing and visualization functions.
176    ///
177    /// # Returns
178    ///
179    /// A `Positive` representation of the x-axis index as a floating point value
180    ///
181    /// # Errors
182    ///
183    /// Returns [`SimulationError::IndexConversion`] when the step
184    /// index cannot be represented as a `Decimal` (practically
185    /// unreachable for `i32` inputs but retained for forward
186    /// compatibility).
187    pub fn get_graph_x_value(&self) -> Result<Decimal, SimulationError> {
188        match Decimal::from_i32(*self.x.index()) {
189            Some(x) => Ok(x),
190            None => Err(SimulationError::IndexConversion),
191        }
192    }
193
194    /// Returns the number of days left until expiration for the x-axis component of this step.
195    ///
196    /// This method provides a convenient way to access the time-to-expiry value from the
197    /// x-component of the step without having to handle the potential error result that
198    /// the underlying `days_left()` method returns. It's useful for graphing and visualization
199    /// contexts where the days remaining until expiration need to be accessed directly.
200    ///
201    /// # Returns
202    ///
203    /// * `Positive` - A guaranteed positive decimal value representing the number of days
204    ///   until expiration. This value is extracted from the step's x-component's expiration date.
205    ///
206    pub fn get_graph_x_in_days_left(&self) -> Positive {
207        // Used by visualization; on error we fall back to ZERO so the
208        // chart renders the step at the rightmost edge instead of
209        // panicking. Underlying error logged for diagnostics.
210        self.x.days_left().unwrap_or_else(|e| {
211            tracing::warn!("get_graph_x_in_days_left: days_left failed: {e}; using 0");
212            Positive::ZERO
213        })
214    }
215
216    /// Returns the y-value prepared for graphing operations
217    ///
218    /// Converts the current y-axis value to a `Positive` value that can be used
219    /// in graphing and visualization functions.
220    ///
221    /// # Returns
222    ///
223    /// A `Positive` representation of the y-axis value
224    ///
225    /// # Errors
226    ///
227    /// Propagates any [`SimulationError`] returned by
228    /// [`Ystep::positive`] (typically
229    /// [`SimulationError::PositiveError`] when the stored `y` value
230    /// violates the `Positive` invariant).
231    pub fn get_graph_y_value(&self) -> Result<Positive, SimulationError> {
232        self.y.positive()
233    }
234
235    /// Returns a reference to the `Ystep<Y>` instance associated with the current object.
236    ///
237    /// # Returns
238    ///
239    /// A reference to the `Ystep<Y>` contained within the struct.
240    ///
241    /// # Notes
242    ///
243    /// This method provides a non-mutable reference to the `Ystep<Y>`. If you need
244    /// to modify the `Ystep<Y>`, consider using a different method or accessing it mutably.
245    pub fn get_y_step(&self) -> &Ystep<Y> {
246        &self.y
247    }
248
249    ///
250    /// Returns a reference to the `Xstep<X>` held within the struct.
251    ///
252    /// # Return
253    ///
254    /// A reference of type `&Xstep<X>` representing the internal `x` field.
255    pub fn get_x_step(&self) -> &Xstep<X> {
256        &self.x
257    }
258
259    /// Returns a reference to the value of type `Y` held by the `self` instance.
260    ///
261    /// This method retrieves the value stored in the `y` field of the struct,
262    /// by calling the `value()` method on `y`, and returns a reference to it.
263    ///
264    /// # Returns
265    /// A reference to the value of type `Y`.
266    ///
267    pub fn get_value(&self) -> &Y {
268        self.y.value()
269    }
270
271    /// Retrieves the index value associated with the instance.
272    ///
273    /// # Returns
274    ///
275    /// A reference to an object of type `X`, obtained by calling the
276    /// `step_size_in_time` method on the field `x` of the instance.
277    ///
278    /// Note: The behavior of this method depends on the implementation of the
279    /// `step_size_in_time` method for the type of `x`.
280    pub fn get_index(&self) -> &X {
281        self.x.step_size_in_time()
282    }
283
284    /// Retrieves the positive value associated with the current instance.
285    ///
286    /// # Returns
287    ///
288    /// * `Positive` - A positive value derived from `self.y`.
289    ///
290    /// This function internally calls the `positive` method on `self.y`
291    /// and returns the resulting value.
292    ///
293    /// # Errors
294    ///
295    /// Propagates any [`SimulationError`] returned by
296    /// [`Ystep::positive`] (typically
297    /// [`SimulationError::PositiveError`] when the stored `y` value
298    /// violates the `Positive` invariant).
299    pub fn get_positive_value(&self) -> Result<Positive, SimulationError> {
300        self.y.positive()
301    }
302}
303
304impl<X, Y> Display for Step<X, Y>
305where
306    X: Copy + TryInto<Positive> + AddAssign + Display,
307    Y: TryInto<Positive> + Display + Clone,
308{
309    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
310        writeln!(f, "Step {{ x: {}, y: {} }}", self.x, self.y)
311    }
312}
313
314impl<X, Y> Serialize for Step<X, Y>
315where
316    X: Copy + TryInto<Positive> + AddAssign + Display + Serialize,
317    Y: TryInto<Positive> + Display + Serialize + Clone,
318{
319    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320    where
321        S: Serializer,
322    {
323        let mut state = serializer.serialize_struct("Step", 2)?;
324        state.serialize_field("x", &self.x)?;
325        state.serialize_field("y", &self.y)?;
326        state.end()
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use optionstratlib_core::pos_or_panic;
334
335    // Helper struct for testing
336    #[derive(Debug, Copy, Clone, PartialEq)]
337    struct TestValue(u32);
338
339    impl From<TestValue> for Positive {
340        fn from(val: TestValue) -> Self {
341            Positive::new(val.0 as f64).unwrap()
342        }
343    }
344
345    impl AddAssign for TestValue {
346        fn add_assign(&mut self, other: Self) {
347            self.0 += other.0;
348        }
349    }
350
351    impl Display for TestValue {
352        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
353            write!(f, "{}", self.0)
354        }
355    }
356
357    #[test]
358    fn test_step_new() {
359        // Test creation with valid parameters
360        let value = TestValue(5);
361        let time_unit = TimeFrame::Day;
362        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
363
364        let step = Xstep::new(value, time_unit, datetime);
365
366        assert_eq!(*step.index(), 0);
367        assert_eq!(step.step_size_in_time(), &TestValue(5));
368        assert_eq!(*step.time_unit(), TimeFrame::Day);
369        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
370    }
371
372    #[test]
373    fn test_step_new_with_datetime_is_normalized_to_days() {
374        // `Xstep::new` used to panic on `ExpirationDate::DateTime`. After
375        // issue #323 it normalizes the variant to `Days(N)` where `N` is
376        // the future days (or 0 if the conversion fails).
377        use chrono::{Duration, Utc};
378
379        let value = TestValue(5);
380        let time_unit = TimeFrame::Day;
381        let dt = Utc::now() + Duration::days(30);
382        let datetime = ExpirationDate::DateTime(dt);
383
384        let step = Xstep::new(value, time_unit, datetime);
385        match step.datetime() {
386            ExpirationDate::Days(_) => {}
387            other => panic!("expected ExpirationDate::Days, got {other:?}"),
388        }
389    }
390
391    #[test]
392    fn test_step_next() {
393        // Test days calculation for next step
394        // If we have a step with value 5 Day and 30 days expiration
395        // Next step should have 25 days expiration (30 - 5)
396        let value = TestValue(5);
397        let time_unit = TimeFrame::Day;
398        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
399
400        let step = Xstep::new(value, time_unit, datetime);
401        let next_step = step.next().unwrap();
402
403        assert_eq!(*step.index(), 0);
404        assert_eq!(step.step_size_in_time(), &TestValue(5));
405        assert_eq!(*step.time_unit(), TimeFrame::Day);
406        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
407
408        assert_eq!(*next_step.index(), 1);
409        assert_eq!(next_step.step_size_in_time(), &TestValue(5));
410        assert_eq!(*next_step.time_unit(), TimeFrame::Day);
411        assert_eq!(
412            *next_step.datetime(),
413            ExpirationDate::Days(pos_or_panic!(25.0))
414        );
415    }
416
417    #[test]
418    fn test_step_next_with_weeks() {
419        // Test time conversion for weeks
420        // If we have a step with value 2 Week and 30 days expiration
421        // Next step should have 16 days expiration (30 - (2 * 7))
422        let value = TestValue(2);
423        let time_unit = TimeFrame::Week;
424        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
425
426        let step = Xstep::new(value, time_unit, datetime);
427        let next_step = step.next().unwrap();
428
429        assert_eq!(*step.index(), 0);
430        assert_eq!(step.step_size_in_time(), &TestValue(2));
431        assert_eq!(*step.time_unit(), TimeFrame::Week);
432        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
433
434        assert_eq!(*next_step.index(), 1);
435        assert_eq!(next_step.step_size_in_time(), &TestValue(2));
436        assert_eq!(*next_step.time_unit(), TimeFrame::Week);
437        assert_eq!(
438            *next_step.datetime(),
439            ExpirationDate::Days(pos_or_panic!(16.0))
440        );
441    }
442
443    #[test]
444    fn test_step_previous() {
445        // Test days calculation for previous step
446        // If we have a step with value 5 Day and 30 days expiration
447        // Previous step should have 35 days expiration (30 + 5)
448        let value = TestValue(5);
449        let time_unit = TimeFrame::Day;
450        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
451
452        let step = Xstep::new(value, time_unit, datetime);
453        let prev_step = step.previous().unwrap();
454
455        assert_eq!(*step.index(), 0);
456        assert_eq!(step.step_size_in_time(), &TestValue(5));
457        assert_eq!(*step.time_unit(), TimeFrame::Day);
458        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
459
460        assert_eq!(*prev_step.index(), -1);
461        assert_eq!(prev_step.step_size_in_time(), &TestValue(5));
462        assert_eq!(*prev_step.time_unit(), TimeFrame::Day);
463        assert_eq!(
464            *prev_step.datetime(),
465            ExpirationDate::Days(pos_or_panic!(35.0))
466        );
467    }
468
469    #[test]
470    fn test_step_previouse_with_months() {
471        // Test time conversion for months
472        // If we have a step with value 1 Month and 30 days expiration
473        // Previous step should have 60 days expiration (30 + 30)
474        // Assuming convert_time_frame correctly converts 1 Month to 30 days
475        let value = TestValue(3);
476        let time_unit = TimeFrame::Month;
477        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
478
479        let step = Xstep::new(value, time_unit, datetime);
480        let prev_step = step.previous().unwrap();
481
482        assert_eq!(*step.index(), 0);
483        assert_eq!(step.step_size_in_time(), &TestValue(3));
484        assert_eq!(*step.time_unit(), TimeFrame::Month);
485        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
486
487        assert_eq!(*prev_step.index(), -1);
488        assert_eq!(prev_step.step_size_in_time(), &TestValue(3));
489        assert_eq!(*prev_step.time_unit(), TimeFrame::Month);
490        assert_eq!(
491            *prev_step.datetime(),
492            ExpirationDate::Days(pos_or_panic!(121.25))
493        );
494    }
495
496    #[test]
497    fn test_multiple_steps() {
498        // Test a sequence of steps
499        let value = TestValue(10);
500        let time_unit = TimeFrame::Day;
501        let datetime = ExpirationDate::Days(Positive::HUNDRED);
502
503        let step = Xstep::new(value, time_unit, datetime);
504        let step1 = step.next().unwrap();
505        let step2 = step1.next().unwrap();
506        let step3 = step2.next().unwrap();
507
508        assert_eq!(*step1.index(), 1);
509        assert_eq!(*step1.datetime(), ExpirationDate::Days(pos_or_panic!(90.0)));
510        assert_eq!(*step2.index(), 2);
511        assert_eq!(*step2.datetime(), ExpirationDate::Days(pos_or_panic!(80.0)));
512        assert_eq!(*step3.index(), 3);
513        assert_eq!(*step3.datetime(), ExpirationDate::Days(pos_or_panic!(70.0)));
514    }
515
516    #[test]
517    fn test_forward_and_backward() {
518        // Test going forward and then backward
519        let value = TestValue(5);
520        let time_unit = TimeFrame::Day;
521        let datetime = ExpirationDate::Days(pos_or_panic!(50.0));
522
523        let original = Xstep::new(value, time_unit, datetime);
524        let forward = original.next().unwrap();
525        let back_to_original = forward.previous().unwrap();
526
527        assert_eq!(*original.index(), 0);
528        assert_eq!(*forward.index(), 1);
529        assert_eq!(*back_to_original.index(), 0);
530        assert_eq!(
531            *original.datetime(),
532            ExpirationDate::Days(pos_or_panic!(50.0))
533        );
534        assert_eq!(
535            *forward.datetime(),
536            ExpirationDate::Days(pos_or_panic!(45.0))
537        );
538        assert_eq!(
539            *back_to_original.datetime(),
540            ExpirationDate::Days(pos_or_panic!(50.0))
541        );
542    }
543}
544
545#[cfg(test)]
546mod tests_positive {
547    use super::*;
548    use optionstratlib_core::pos_or_panic;
549
550    #[test]
551    fn test_step_new() {
552        // Test creation with valid parameters
553        let value = pos_or_panic!(5.0);
554        let time_unit = TimeFrame::Day;
555        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
556
557        let step = Xstep::new(value, time_unit, datetime);
558
559        assert_eq!(*step.index(), 0);
560        assert_eq!(*step.step_size_in_time(), pos_or_panic!(5.0));
561        assert_eq!(*step.time_unit(), TimeFrame::Day);
562        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
563    }
564
565    #[test]
566    fn test_step_new_with_datetime_is_normalized_to_days() {
567        // `Xstep::new` now normalizes `ExpirationDate::DateTime` into
568        // `Days(N)` rather than panicking (issue #323).
569        use chrono::{Duration, Utc};
570
571        let value = pos_or_panic!(5.0);
572        let time_unit = TimeFrame::Day;
573        let dt = Utc::now() + Duration::days(30);
574        let datetime = ExpirationDate::DateTime(dt);
575
576        let step = Xstep::new(value, time_unit, datetime);
577        match step.datetime() {
578            ExpirationDate::Days(_) => {}
579            other => panic!("expected ExpirationDate::Days, got {other:?}"),
580        }
581    }
582
583    #[test]
584    fn test_step_next() {
585        // Test days calculation for next step
586        // If we have a step with value 5 Day and 30 days expiration
587        // Next step should have 25 days expiration (30 - 5)
588        let value = pos_or_panic!(5.0);
589        let time_unit = TimeFrame::Day;
590        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
591
592        let step = Xstep::new(value, time_unit, datetime);
593        let next_step = step.next().unwrap();
594
595        assert_eq!(*next_step.index(), 1);
596        assert_eq!(*next_step.step_size_in_time(), pos_or_panic!(5.0));
597        assert_eq!(*next_step.time_unit(), TimeFrame::Day);
598        assert_eq!(
599            *next_step.datetime(),
600            ExpirationDate::Days(pos_or_panic!(25.0))
601        );
602    }
603
604    #[test]
605    fn test_step_next_with_weeks() {
606        // Test time conversion for weeks
607        // If we have a step with value 2 Week and 30 days expiration
608        // Next step should have 16 days expiration (30 - (2 * 7))
609        let value = Positive::TWO;
610        let time_unit = TimeFrame::Week;
611        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
612
613        let step = Xstep::new(value, time_unit, datetime);
614        let next_step = step.next().unwrap();
615
616        assert_eq!(*next_step.index(), 1);
617        assert_eq!(*next_step.step_size_in_time(), Positive::TWO);
618        assert_eq!(*next_step.time_unit(), TimeFrame::Week);
619        assert_eq!(
620            *next_step.datetime(),
621            ExpirationDate::Days(pos_or_panic!(16.0))
622        );
623    }
624
625    #[test]
626    fn test_step_previous() {
627        // Test days calculation for previous step
628        // If we have a step with value 5 Day and 30 days expiration
629        // Previous step should have 35 days expiration (30 + 5)
630        let value = pos_or_panic!(5.0);
631        let time_unit = TimeFrame::Day;
632        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
633
634        let step = Xstep::new(value, time_unit, datetime);
635        let prev_step = step.previous().unwrap();
636
637        assert_eq!(*prev_step.index(), -1);
638        assert_eq!(*prev_step.step_size_in_time(), pos_or_panic!(5.0));
639        assert_eq!(*prev_step.time_unit(), TimeFrame::Day);
640        assert_eq!(
641            *prev_step.datetime(),
642            ExpirationDate::Days(pos_or_panic!(35.0))
643        );
644    }
645
646    #[test]
647    fn test_step_previouse_with_months() {
648        // Test time conversion for months
649        // If we have a step with value 1 Month and 30 days expiration
650        // Previous step should have 60 days expiration (30 + 30)
651        // Assuming convert_time_frame correctly converts 1 Month to 30 days
652        let value = pos_or_panic!(3.0);
653        let time_unit = TimeFrame::Month;
654        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
655
656        let step = Xstep::new(value, time_unit, datetime);
657        let prev_step = step.previous().unwrap();
658
659        assert_eq!(*prev_step.index(), -1);
660        assert_eq!(*prev_step.step_size_in_time(), pos_or_panic!(3.0));
661        assert_eq!(*prev_step.time_unit(), TimeFrame::Month);
662        assert_eq!(
663            *prev_step.datetime(),
664            ExpirationDate::Days(pos_or_panic!(121.25))
665        );
666    }
667
668    #[test]
669    fn test_multiple_steps() {
670        // Test a sequence of steps
671        let value = Positive::TEN;
672        let time_unit = TimeFrame::Day;
673        let datetime = ExpirationDate::Days(Positive::HUNDRED);
674
675        let step = Xstep::new(value, time_unit, datetime);
676        let step1 = step.next().unwrap();
677        let step2 = step1.next().unwrap();
678        let step3 = step2.next().unwrap();
679
680        assert_eq!(*step1.index(), 1);
681        assert_eq!(*step1.datetime(), ExpirationDate::Days(pos_or_panic!(90.0)));
682        assert_eq!(*step2.index(), 2);
683        assert_eq!(*step2.datetime(), ExpirationDate::Days(pos_or_panic!(80.0)));
684        assert_eq!(*step3.index(), 3);
685        assert_eq!(*step3.datetime(), ExpirationDate::Days(pos_or_panic!(70.0)));
686    }
687
688    #[test]
689    fn test_forward_and_backward() {
690        // Test going forward and then backward
691        let value = pos_or_panic!(5.0);
692        let time_unit = TimeFrame::Day;
693        let datetime = ExpirationDate::Days(pos_or_panic!(50.0));
694
695        let original = Xstep::new(value, time_unit, datetime);
696        let forward = original.next().unwrap();
697        let back_to_original = forward.previous().unwrap();
698
699        assert_eq!(*original.index(), 0);
700        assert_eq!(*forward.index(), 1);
701        assert_eq!(*back_to_original.index(), 0);
702        assert_eq!(
703            *original.datetime(),
704            ExpirationDate::Days(pos_or_panic!(50.0))
705        );
706        assert_eq!(
707            *forward.datetime(),
708            ExpirationDate::Days(pos_or_panic!(45.0))
709        );
710        assert_eq!(
711            *back_to_original.datetime(),
712            ExpirationDate::Days(pos_or_panic!(50.0))
713        );
714    }
715}
716
717#[cfg(test)]
718mod tests_step {
719    use super::*;
720    use optionstratlib_core::pos_or_panic;
721
722    // Helper struct for testing
723    #[derive(Debug, Copy, Clone, PartialEq)]
724    struct TestValue(u32);
725
726    impl From<TestValue> for Positive {
727        fn from(val: TestValue) -> Self {
728            Positive::new(val.0 as f64).unwrap()
729        }
730    }
731
732    impl AddAssign for TestValue {
733        fn add_assign(&mut self, other: Self) {
734            self.0 += other.0;
735        }
736    }
737
738    impl Display for TestValue {
739        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
740            write!(f, "{}", self.0)
741        }
742    }
743
744    #[test]
745    fn test_step_new() {
746        // Test creation with valid parameters
747        let x_value = TestValue(5);
748        let time_unit = TimeFrame::Day;
749        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
750        let y_value = 42.5;
751
752        let step = Step::new(x_value, time_unit, datetime, y_value);
753
754        // Check X properties
755        assert_eq!(*step.x.index(), 0);
756        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
757        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
758        assert_eq!(
759            *step.x.datetime(),
760            ExpirationDate::Days(pos_or_panic!(30.0))
761        );
762
763        // Check Y properties
764        assert_eq!(*step.y.index(), 0);
765        assert_eq!(*step.y.value(), 42.5);
766
767        let result_next = step.next(100.0);
768        assert!(result_next.is_ok());
769        let next = result_next.unwrap();
770
771        assert!(
772            next.to_string()
773                .contains("Step { x: Xstep { index: 1, value: 5, time_unit: Day, datetime:")
774        );
775
776        let previous_next = step.previous(100.0);
777        assert!(previous_next.is_ok());
778        let previous = previous_next.unwrap();
779
780        assert!(
781            previous
782                .to_string()
783                .contains("Step { x: Xstep { index: -1, value: 5, time_unit: Day, datetime:")
784        );
785
786        assert_eq!(next.get_graph_x_value().unwrap(), Decimal::ONE);
787        assert_eq!(previous.get_graph_x_value().unwrap(), Decimal::NEGATIVE_ONE);
788    }
789
790    #[test]
791    fn test_step_next() {
792        // Setup initial step
793        let x_value = TestValue(5);
794        let time_unit = TimeFrame::Day;
795        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
796        let y_value = 42.5;
797
798        let step = Step::new(x_value, time_unit, datetime, y_value);
799
800        // Create next step with a new Y value
801        let new_y_value = 45.0;
802        let next_step = step.next(new_y_value).unwrap();
803
804        // Check X properties
805        assert_eq!(*step.x.index(), 0);
806        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
807        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
808        assert_eq!(
809            *step.x.datetime(),
810            ExpirationDate::Days(pos_or_panic!(30.0))
811        );
812
813        // Check Y properties
814        assert_eq!(*step.y.index(), 0);
815        assert_eq!(*step.y.value(), 42.5);
816
817        assert_eq!(*next_step.x.index(), 1);
818        assert_eq!(next_step.x.step_size_in_time(), &TestValue(5));
819        assert_eq!(*next_step.x.time_unit(), TimeFrame::Day);
820        assert_eq!(
821            *next_step.x.datetime(),
822            ExpirationDate::Days(pos_or_panic!(25.0))
823        );
824
825        // Check Y properties
826        assert_eq!(*next_step.y.index(), 1);
827        assert_eq!(*next_step.y.value(), 45.0);
828    }
829
830    #[test]
831    fn test_step_previous() {
832        // Setup initial step
833        let x_value = TestValue(5);
834        let time_unit = TimeFrame::Day;
835        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
836        let y_value = 42.5;
837
838        let step = Step::new(x_value, time_unit, datetime, y_value);
839
840        // Create previous step with a new Y value
841        let new_y_value = 38.0;
842        let prev_step = step.previous(new_y_value).unwrap();
843
844        // Check X properties
845        assert_eq!(*step.x.index(), 0);
846        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
847        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
848        assert_eq!(
849            *step.x.datetime(),
850            ExpirationDate::Days(pos_or_panic!(30.0))
851        );
852
853        // Check Y properties
854        assert_eq!(*prev_step.y.value(), 38.0);
855    }
856
857    #[test]
858    fn test_step_with_timeframe_conversion() {
859        // Test step creation and manipulation with time frame conversion
860        let x_value = TestValue(2);
861        let time_unit = TimeFrame::Week;
862        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
863        let y_value = 100.0;
864
865        let step = Step::new(x_value, time_unit, datetime, y_value);
866
867        // Next step should decrease days by 2 weeks (14 days)
868        let next_y = 105.0;
869        let next_step = step.next(next_y).unwrap();
870
871        assert_eq!(
872            *next_step.x.datetime(),
873            ExpirationDate::Days(pos_or_panic!(16.0))
874        );
875        assert_eq!(*next_step.y.value(), 105.0);
876
877        // Previous step from initial should increase days by 2 weeks
878        let prev_y = 95.0;
879        let prev_step = step.previous(prev_y).unwrap();
880
881        assert_eq!(
882            *prev_step.x.datetime(),
883            ExpirationDate::Days(pos_or_panic!(44.0))
884        );
885        assert_eq!(*prev_step.y.value(), 95.0);
886    }
887
888    #[test]
889    fn test_step_chain() {
890        // Test a chain of steps with different Y values
891        let x_value = TestValue(10);
892        let time_unit = TimeFrame::Day;
893        let datetime = ExpirationDate::Days(Positive::HUNDRED);
894        let y_value = 50.0;
895
896        let initial = Step::new(x_value, time_unit, datetime, y_value);
897
898        // Chain: initial -> step1 -> step2 -> step3
899        let step1 = initial.next(55.0).unwrap();
900        let step2 = step1.next(60.0).unwrap();
901        let step3 = step2.next(65.0).unwrap();
902
903        // Check progression of x and y values
904        assert_eq!(*step1.x.index(), 1);
905        assert_eq!(
906            *step1.x.datetime(),
907            ExpirationDate::Days(pos_or_panic!(90.0))
908        );
909        assert_eq!(*step1.y.index(), 1);
910        assert_eq!(*step1.y.value(), 55.0);
911        assert_eq!(*step2.x.index(), 2);
912        assert_eq!(
913            *step2.x.datetime(),
914            ExpirationDate::Days(pos_or_panic!(80.0))
915        );
916        assert_eq!(*step2.y.index(), 2);
917        assert_eq!(*step2.y.value(), 60.0);
918        assert_eq!(*step3.x.index(), 3);
919        assert_eq!(
920            *step3.x.datetime(),
921            ExpirationDate::Days(pos_or_panic!(70.0))
922        );
923        assert_eq!(*step3.y.index(), 3);
924        assert_eq!(*step3.y.value(), 65.0);
925    }
926
927    #[test]
928    fn test_with_positive_type() {
929        // Test using Positive as both X and Y types
930        let x_value = pos_or_panic!(5.0);
931        let time_unit = TimeFrame::Day;
932        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
933        let y_value = pos_or_panic!(50.0);
934
935        let step = Step::new(x_value, time_unit, datetime, y_value);
936
937        let next_step = step.next(pos_or_panic!(55.0)).unwrap();
938
939        assert_eq!(
940            *next_step.x.datetime(),
941            ExpirationDate::Days(pos_or_panic!(25.0))
942        );
943        assert_eq!(*next_step.y.value(), pos_or_panic!(55.0));
944    }
945
946    #[test]
947    fn test_with_zero_days() {
948        // Test using Positive as both X and Y types
949        let x_value = pos_or_panic!(5.0);
950        let time_unit = TimeFrame::Day;
951        let datetime = ExpirationDate::Days(Positive::ZERO);
952        let y_value = pos_or_panic!(50.0);
953        let step = Step::new(x_value, time_unit, datetime, y_value);
954        let result = step.next(pos_or_panic!(55.0));
955        assert!(result.is_err());
956        let step_x = step.get_x_step();
957        assert_eq!(*step_x.index(), 0);
958        assert_eq!(*step_x.step_size_in_time(), pos_or_panic!(5.0));
959        let result = step_x.next();
960        assert!(result.is_err());
961    }
962
963    #[test]
964    fn next_ok_increments_indices_and_builds_self() {
965        let x_value = pos_or_panic!(5.0);
966        let time_unit = TimeFrame::Day;
967        let datetime = ExpirationDate::Days(Positive::TWO);
968        let y_value = pos_or_panic!(50.0);
969        let step = Step::new(x_value, time_unit, datetime, y_value);
970
971        let new_y = pos_or_panic!(55.0);
972        let next = step.next(new_y).unwrap();
973
974        assert_eq!(next.get_value(), &new_y);
975        assert_eq!(*next.x.index(), 1);
976        assert_eq!(*next.y.index(), step.y.index() + 1);
977    }
978}
979
980#[cfg(test)]
981mod tests_step_serialization {
982    use super::*;
983
984    use chrono::{TimeZone, Utc};
985    use optionstratlib_core::pos_or_panic;
986    use serde_json::{self, Value};
987
988    // Helper function to create a test step with f64 values
989    fn create_test_step() -> Step<f64, f64> {
990        let x_value = 1.5;
991        let time_unit = TimeFrame::Day;
992        let expiration_date = ExpirationDate::Days(pos_or_panic!(30.0));
993        let y_value = 100.0;
994
995        Step::new(x_value, time_unit, expiration_date, y_value)
996    }
997
998    #[test]
999    fn test_step_serialization_with_days() {
1000        let step = create_test_step();
1001
1002        // Serialize to JSON string
1003        let serialized = serde_json::to_string(&step).unwrap();
1004
1005        // Parse the JSON string back to Value for easier assertions
1006        let json_value: Value = serde_json::from_str(&serialized).unwrap();
1007
1008        // Verify the structure is correct
1009        assert!(json_value.is_object());
1010        assert!(json_value.get("x").is_some());
1011        assert!(json_value.get("y").is_some());
1012
1013        // Verify x fields
1014        let x = &json_value["x"];
1015        assert_eq!(x["index"], 0);
1016        assert_eq!(x["step_size_in_time"], "1.5");
1017
1018        // Verify y fields
1019        let y = &json_value["y"];
1020        assert_eq!(y["index"], 0);
1021        assert_eq!(y["value"], "100");
1022    }
1023
1024    #[test]
1025    fn test_step_with_datetime_is_normalized() {
1026        // `Step::new` now normalizes `ExpirationDate::DateTime` into
1027        // `Days(N)` via `Xstep::new` (issue #323) instead of panicking.
1028        let x_value = 2.5;
1029        let time_unit = TimeFrame::Hour;
1030        let expiration_date =
1031            ExpirationDate::DateTime(Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap());
1032        let y_value = 200.0;
1033
1034        let step = Step::new(x_value, time_unit, expiration_date, y_value);
1035        match step.x.datetime() {
1036            ExpirationDate::Days(_) => {}
1037            other => panic!("expected ExpirationDate::Days, got {other:?}"),
1038        }
1039    }
1040
1041    #[test]
1042    fn test_step_serialize() {
1043        let x_value = pos_or_panic!(5.0);
1044        let time_unit = TimeFrame::Day;
1045        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
1046        let y_value = 42.5;
1047        let step = Step::new(x_value, time_unit, datetime, y_value);
1048
1049        // Serialize to JSON string
1050        let serialized = serde_json::to_string(&step).unwrap();
1051        assert_eq!(
1052            serialized,
1053            r#"{"x":{"index":0,"step_size_in_time":"5","time_unit":"Day","datetime":{"days":30.0}},"y":{"index":0,"value":"42.5"}}"#
1054        );
1055    }
1056
1057    #[test]
1058    fn test_step_pretty_serialization() {
1059        let step = create_test_step();
1060
1061        // Serialize to pretty JSON string
1062        let serialized = serde_json::to_string_pretty(&step).unwrap();
1063
1064        // Verify the serialized string contains expected formatting
1065        assert!(serialized.contains("{\n"));
1066        assert!(serialized.contains("  \"x\": {"));
1067        assert!(serialized.contains("  \"y\": {"));
1068
1069        // Make sure the content is still correct by deserializing
1070        let deserialized: Value = serde_json::from_str(&serialized).unwrap();
1071        assert_eq!(deserialized["x"]["step_size_in_time"], "1.5");
1072        assert_eq!(deserialized["y"]["value"], "100");
1073    }
1074}