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    /// A chart position, not a figure to compute with: where the days cannot be read
207    /// it logs a warning and gives zero. That includes a `DateTime` step in a build
208    /// with neither the `clock` feature nor a reference datetime on this thread. Use
209    /// [`Xstep::days_left`] for the error.
210    ///
211    pub fn get_graph_x_in_days_left(&self) -> Positive {
212        // Used by visualization; on error we fall back to ZERO so the
213        // chart renders the step at the rightmost edge instead of
214        // panicking. Underlying error logged for diagnostics.
215        self.x.days_left().unwrap_or_else(|e| {
216            tracing::warn!("get_graph_x_in_days_left: days_left failed: {e}; using 0");
217            Positive::ZERO
218        })
219    }
220
221    /// Returns the y-value prepared for graphing operations
222    ///
223    /// Converts the current y-axis value to a `Positive` value that can be used
224    /// in graphing and visualization functions.
225    ///
226    /// # Returns
227    ///
228    /// A `Positive` representation of the y-axis value
229    ///
230    /// # Errors
231    ///
232    /// Propagates any [`SimulationError`] returned by
233    /// [`Ystep::positive`] (typically
234    /// [`SimulationError::PositiveError`] when the stored `y` value
235    /// violates the `Positive` invariant).
236    pub fn get_graph_y_value(&self) -> Result<Positive, SimulationError> {
237        self.y.positive()
238    }
239
240    /// Returns a reference to the `Ystep<Y>` instance associated with the current object.
241    ///
242    /// # Returns
243    ///
244    /// A reference to the `Ystep<Y>` contained within the struct.
245    ///
246    /// # Notes
247    ///
248    /// This method provides a non-mutable reference to the `Ystep<Y>`. If you need
249    /// to modify the `Ystep<Y>`, consider using a different method or accessing it mutably.
250    pub fn get_y_step(&self) -> &Ystep<Y> {
251        &self.y
252    }
253
254    ///
255    /// Returns a reference to the `Xstep<X>` held within the struct.
256    ///
257    /// # Return
258    ///
259    /// A reference of type `&Xstep<X>` representing the internal `x` field.
260    pub fn get_x_step(&self) -> &Xstep<X> {
261        &self.x
262    }
263
264    /// Returns a reference to the value of type `Y` held by the `self` instance.
265    ///
266    /// This method retrieves the value stored in the `y` field of the struct,
267    /// by calling the `value()` method on `y`, and returns a reference to it.
268    ///
269    /// # Returns
270    /// A reference to the value of type `Y`.
271    ///
272    pub fn get_value(&self) -> &Y {
273        self.y.value()
274    }
275
276    /// Retrieves the index value associated with the instance.
277    ///
278    /// # Returns
279    ///
280    /// A reference to an object of type `X`, obtained by calling the
281    /// `step_size_in_time` method on the field `x` of the instance.
282    ///
283    /// Note: The behavior of this method depends on the implementation of the
284    /// `step_size_in_time` method for the type of `x`.
285    pub fn get_index(&self) -> &X {
286        self.x.step_size_in_time()
287    }
288
289    /// Retrieves the positive value associated with the current instance.
290    ///
291    /// # Returns
292    ///
293    /// * `Positive` - A positive value derived from `self.y`.
294    ///
295    /// This function internally calls the `positive` method on `self.y`
296    /// and returns the resulting value.
297    ///
298    /// # Errors
299    ///
300    /// Propagates any [`SimulationError`] returned by
301    /// [`Ystep::positive`] (typically
302    /// [`SimulationError::PositiveError`] when the stored `y` value
303    /// violates the `Positive` invariant).
304    pub fn get_positive_value(&self) -> Result<Positive, SimulationError> {
305        self.y.positive()
306    }
307}
308
309impl<X, Y> Display for Step<X, Y>
310where
311    X: Copy + TryInto<Positive> + AddAssign + Display,
312    Y: TryInto<Positive> + Display + Clone,
313{
314    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
315        writeln!(f, "Step {{ x: {}, y: {} }}", self.x, self.y)
316    }
317}
318
319impl<X, Y> Serialize for Step<X, Y>
320where
321    X: Copy + TryInto<Positive> + AddAssign + Display + Serialize,
322    Y: TryInto<Positive> + Display + Serialize + Clone,
323{
324    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
325    where
326        S: Serializer,
327    {
328        let mut state = serializer.serialize_struct("Step", 2)?;
329        state.serialize_field("x", &self.x)?;
330        state.serialize_field("y", &self.y)?;
331        state.end()
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use optionstratlib_core::pos_or_panic;
339
340    // Helper struct for testing
341    #[derive(Debug, Copy, Clone, PartialEq)]
342    struct TestValue(u32);
343
344    impl From<TestValue> for Positive {
345        fn from(val: TestValue) -> Self {
346            Positive::new(val.0 as f64).unwrap()
347        }
348    }
349
350    impl AddAssign for TestValue {
351        fn add_assign(&mut self, other: Self) {
352            self.0 += other.0;
353        }
354    }
355
356    impl Display for TestValue {
357        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358            write!(f, "{}", self.0)
359        }
360    }
361
362    #[test]
363    fn test_step_new() {
364        // Test creation with valid parameters
365        let value = TestValue(5);
366        let time_unit = TimeFrame::Day;
367        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
368
369        let step = Xstep::new(value, time_unit, datetime);
370
371        assert_eq!(*step.index(), 0);
372        assert_eq!(step.step_size_in_time(), &TestValue(5));
373        assert_eq!(*step.time_unit(), TimeFrame::Day);
374        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
375    }
376
377    #[cfg(feature = "clock")]
378    #[test]
379    fn test_step_new_with_datetime_is_normalized_to_days() {
380        // `Xstep::new` used to panic on `ExpirationDate::DateTime`. After
381        // issue #323 it normalizes the variant to `Days(N)` where `N` is
382        // the future days (or 0 if the conversion fails).
383        use chrono::{Duration, Utc};
384
385        let value = TestValue(5);
386        let time_unit = TimeFrame::Day;
387        let dt = Utc::now() + Duration::days(30);
388        let datetime = ExpirationDate::DateTime(dt);
389
390        let step = Xstep::new(value, time_unit, datetime);
391        match step.datetime() {
392            ExpirationDate::Days(_) => {}
393            other => panic!("expected ExpirationDate::Days, got {other:?}"),
394        }
395    }
396
397    #[test]
398    fn test_step_next() {
399        // Test days calculation for next step
400        // If we have a step with value 5 Day and 30 days expiration
401        // Next step should have 25 days expiration (30 - 5)
402        let value = TestValue(5);
403        let time_unit = TimeFrame::Day;
404        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
405
406        let step = Xstep::new(value, time_unit, datetime);
407        let next_step = step.next().unwrap();
408
409        assert_eq!(*step.index(), 0);
410        assert_eq!(step.step_size_in_time(), &TestValue(5));
411        assert_eq!(*step.time_unit(), TimeFrame::Day);
412        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
413
414        assert_eq!(*next_step.index(), 1);
415        assert_eq!(next_step.step_size_in_time(), &TestValue(5));
416        assert_eq!(*next_step.time_unit(), TimeFrame::Day);
417        assert_eq!(
418            *next_step.datetime(),
419            ExpirationDate::Days(pos_or_panic!(25.0))
420        );
421    }
422
423    #[test]
424    fn test_step_next_with_weeks() {
425        // Test time conversion for weeks
426        // If we have a step with value 2 Week and 30 days expiration
427        // Next step should have 16 days expiration (30 - (2 * 7))
428        let value = TestValue(2);
429        let time_unit = TimeFrame::Week;
430        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
431
432        let step = Xstep::new(value, time_unit, datetime);
433        let next_step = step.next().unwrap();
434
435        assert_eq!(*step.index(), 0);
436        assert_eq!(step.step_size_in_time(), &TestValue(2));
437        assert_eq!(*step.time_unit(), TimeFrame::Week);
438        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
439
440        assert_eq!(*next_step.index(), 1);
441        assert_eq!(next_step.step_size_in_time(), &TestValue(2));
442        assert_eq!(*next_step.time_unit(), TimeFrame::Week);
443        assert_eq!(
444            *next_step.datetime(),
445            ExpirationDate::Days(pos_or_panic!(16.0))
446        );
447    }
448
449    #[test]
450    fn test_step_previous() {
451        // Test days calculation for previous step
452        // If we have a step with value 5 Day and 30 days expiration
453        // Previous step should have 35 days expiration (30 + 5)
454        let value = TestValue(5);
455        let time_unit = TimeFrame::Day;
456        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
457
458        let step = Xstep::new(value, time_unit, datetime);
459        let prev_step = step.previous().unwrap();
460
461        assert_eq!(*step.index(), 0);
462        assert_eq!(step.step_size_in_time(), &TestValue(5));
463        assert_eq!(*step.time_unit(), TimeFrame::Day);
464        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
465
466        assert_eq!(*prev_step.index(), -1);
467        assert_eq!(prev_step.step_size_in_time(), &TestValue(5));
468        assert_eq!(*prev_step.time_unit(), TimeFrame::Day);
469        assert_eq!(
470            *prev_step.datetime(),
471            ExpirationDate::Days(pos_or_panic!(35.0))
472        );
473    }
474
475    #[test]
476    fn test_step_previouse_with_months() {
477        // Test time conversion for months
478        // If we have a step with value 1 Month and 30 days expiration
479        // Previous step should have 60 days expiration (30 + 30)
480        // Assuming convert_time_frame correctly converts 1 Month to 30 days
481        let value = TestValue(3);
482        let time_unit = TimeFrame::Month;
483        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
484
485        let step = Xstep::new(value, time_unit, datetime);
486        let prev_step = step.previous().unwrap();
487
488        assert_eq!(*step.index(), 0);
489        assert_eq!(step.step_size_in_time(), &TestValue(3));
490        assert_eq!(*step.time_unit(), TimeFrame::Month);
491        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
492
493        assert_eq!(*prev_step.index(), -1);
494        assert_eq!(prev_step.step_size_in_time(), &TestValue(3));
495        assert_eq!(*prev_step.time_unit(), TimeFrame::Month);
496        assert_eq!(
497            *prev_step.datetime(),
498            ExpirationDate::Days(pos_or_panic!(121.25))
499        );
500    }
501
502    #[test]
503    fn test_multiple_steps() {
504        // Test a sequence of steps
505        let value = TestValue(10);
506        let time_unit = TimeFrame::Day;
507        let datetime = ExpirationDate::Days(Positive::HUNDRED);
508
509        let step = Xstep::new(value, time_unit, datetime);
510        let step1 = step.next().unwrap();
511        let step2 = step1.next().unwrap();
512        let step3 = step2.next().unwrap();
513
514        assert_eq!(*step1.index(), 1);
515        assert_eq!(*step1.datetime(), ExpirationDate::Days(pos_or_panic!(90.0)));
516        assert_eq!(*step2.index(), 2);
517        assert_eq!(*step2.datetime(), ExpirationDate::Days(pos_or_panic!(80.0)));
518        assert_eq!(*step3.index(), 3);
519        assert_eq!(*step3.datetime(), ExpirationDate::Days(pos_or_panic!(70.0)));
520    }
521
522    #[test]
523    fn test_forward_and_backward() {
524        // Test going forward and then backward
525        let value = TestValue(5);
526        let time_unit = TimeFrame::Day;
527        let datetime = ExpirationDate::Days(pos_or_panic!(50.0));
528
529        let original = Xstep::new(value, time_unit, datetime);
530        let forward = original.next().unwrap();
531        let back_to_original = forward.previous().unwrap();
532
533        assert_eq!(*original.index(), 0);
534        assert_eq!(*forward.index(), 1);
535        assert_eq!(*back_to_original.index(), 0);
536        assert_eq!(
537            *original.datetime(),
538            ExpirationDate::Days(pos_or_panic!(50.0))
539        );
540        assert_eq!(
541            *forward.datetime(),
542            ExpirationDate::Days(pos_or_panic!(45.0))
543        );
544        assert_eq!(
545            *back_to_original.datetime(),
546            ExpirationDate::Days(pos_or_panic!(50.0))
547        );
548    }
549}
550
551#[cfg(test)]
552mod tests_positive {
553    use super::*;
554    use optionstratlib_core::pos_or_panic;
555
556    #[test]
557    fn test_step_new() {
558        // Test creation with valid parameters
559        let value = pos_or_panic!(5.0);
560        let time_unit = TimeFrame::Day;
561        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
562
563        let step = Xstep::new(value, time_unit, datetime);
564
565        assert_eq!(*step.index(), 0);
566        assert_eq!(*step.step_size_in_time(), pos_or_panic!(5.0));
567        assert_eq!(*step.time_unit(), TimeFrame::Day);
568        assert_eq!(*step.datetime(), ExpirationDate::Days(pos_or_panic!(30.0)));
569    }
570
571    #[cfg(feature = "clock")]
572    #[test]
573    fn test_step_new_with_datetime_is_normalized_to_days() {
574        // `Xstep::new` now normalizes `ExpirationDate::DateTime` into
575        // `Days(N)` rather than panicking (issue #323).
576        use chrono::{Duration, Utc};
577
578        let value = pos_or_panic!(5.0);
579        let time_unit = TimeFrame::Day;
580        let dt = Utc::now() + Duration::days(30);
581        let datetime = ExpirationDate::DateTime(dt);
582
583        let step = Xstep::new(value, time_unit, datetime);
584        match step.datetime() {
585            ExpirationDate::Days(_) => {}
586            other => panic!("expected ExpirationDate::Days, got {other:?}"),
587        }
588    }
589
590    #[test]
591    fn test_step_next() {
592        // Test days calculation for next step
593        // If we have a step with value 5 Day and 30 days expiration
594        // Next step should have 25 days expiration (30 - 5)
595        let value = pos_or_panic!(5.0);
596        let time_unit = TimeFrame::Day;
597        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
598
599        let step = Xstep::new(value, time_unit, datetime);
600        let next_step = step.next().unwrap();
601
602        assert_eq!(*next_step.index(), 1);
603        assert_eq!(*next_step.step_size_in_time(), pos_or_panic!(5.0));
604        assert_eq!(*next_step.time_unit(), TimeFrame::Day);
605        assert_eq!(
606            *next_step.datetime(),
607            ExpirationDate::Days(pos_or_panic!(25.0))
608        );
609    }
610
611    #[test]
612    fn test_step_next_with_weeks() {
613        // Test time conversion for weeks
614        // If we have a step with value 2 Week and 30 days expiration
615        // Next step should have 16 days expiration (30 - (2 * 7))
616        let value = Positive::TWO;
617        let time_unit = TimeFrame::Week;
618        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
619
620        let step = Xstep::new(value, time_unit, datetime);
621        let next_step = step.next().unwrap();
622
623        assert_eq!(*next_step.index(), 1);
624        assert_eq!(*next_step.step_size_in_time(), Positive::TWO);
625        assert_eq!(*next_step.time_unit(), TimeFrame::Week);
626        assert_eq!(
627            *next_step.datetime(),
628            ExpirationDate::Days(pos_or_panic!(16.0))
629        );
630    }
631
632    #[test]
633    fn test_step_previous() {
634        // Test days calculation for previous step
635        // If we have a step with value 5 Day and 30 days expiration
636        // Previous step should have 35 days expiration (30 + 5)
637        let value = pos_or_panic!(5.0);
638        let time_unit = TimeFrame::Day;
639        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
640
641        let step = Xstep::new(value, time_unit, datetime);
642        let prev_step = step.previous().unwrap();
643
644        assert_eq!(*prev_step.index(), -1);
645        assert_eq!(*prev_step.step_size_in_time(), pos_or_panic!(5.0));
646        assert_eq!(*prev_step.time_unit(), TimeFrame::Day);
647        assert_eq!(
648            *prev_step.datetime(),
649            ExpirationDate::Days(pos_or_panic!(35.0))
650        );
651    }
652
653    #[test]
654    fn test_step_previouse_with_months() {
655        // Test time conversion for months
656        // If we have a step with value 1 Month and 30 days expiration
657        // Previous step should have 60 days expiration (30 + 30)
658        // Assuming convert_time_frame correctly converts 1 Month to 30 days
659        let value = pos_or_panic!(3.0);
660        let time_unit = TimeFrame::Month;
661        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
662
663        let step = Xstep::new(value, time_unit, datetime);
664        let prev_step = step.previous().unwrap();
665
666        assert_eq!(*prev_step.index(), -1);
667        assert_eq!(*prev_step.step_size_in_time(), pos_or_panic!(3.0));
668        assert_eq!(*prev_step.time_unit(), TimeFrame::Month);
669        assert_eq!(
670            *prev_step.datetime(),
671            ExpirationDate::Days(pos_or_panic!(121.25))
672        );
673    }
674
675    #[test]
676    fn test_multiple_steps() {
677        // Test a sequence of steps
678        let value = Positive::TEN;
679        let time_unit = TimeFrame::Day;
680        let datetime = ExpirationDate::Days(Positive::HUNDRED);
681
682        let step = Xstep::new(value, time_unit, datetime);
683        let step1 = step.next().unwrap();
684        let step2 = step1.next().unwrap();
685        let step3 = step2.next().unwrap();
686
687        assert_eq!(*step1.index(), 1);
688        assert_eq!(*step1.datetime(), ExpirationDate::Days(pos_or_panic!(90.0)));
689        assert_eq!(*step2.index(), 2);
690        assert_eq!(*step2.datetime(), ExpirationDate::Days(pos_or_panic!(80.0)));
691        assert_eq!(*step3.index(), 3);
692        assert_eq!(*step3.datetime(), ExpirationDate::Days(pos_or_panic!(70.0)));
693    }
694
695    #[test]
696    fn test_forward_and_backward() {
697        // Test going forward and then backward
698        let value = pos_or_panic!(5.0);
699        let time_unit = TimeFrame::Day;
700        let datetime = ExpirationDate::Days(pos_or_panic!(50.0));
701
702        let original = Xstep::new(value, time_unit, datetime);
703        let forward = original.next().unwrap();
704        let back_to_original = forward.previous().unwrap();
705
706        assert_eq!(*original.index(), 0);
707        assert_eq!(*forward.index(), 1);
708        assert_eq!(*back_to_original.index(), 0);
709        assert_eq!(
710            *original.datetime(),
711            ExpirationDate::Days(pos_or_panic!(50.0))
712        );
713        assert_eq!(
714            *forward.datetime(),
715            ExpirationDate::Days(pos_or_panic!(45.0))
716        );
717        assert_eq!(
718            *back_to_original.datetime(),
719            ExpirationDate::Days(pos_or_panic!(50.0))
720        );
721    }
722}
723
724#[cfg(test)]
725mod tests_step {
726    use super::*;
727    use optionstratlib_core::pos_or_panic;
728
729    // Helper struct for testing
730    #[derive(Debug, Copy, Clone, PartialEq)]
731    struct TestValue(u32);
732
733    impl From<TestValue> for Positive {
734        fn from(val: TestValue) -> Self {
735            Positive::new(val.0 as f64).unwrap()
736        }
737    }
738
739    impl AddAssign for TestValue {
740        fn add_assign(&mut self, other: Self) {
741            self.0 += other.0;
742        }
743    }
744
745    impl Display for TestValue {
746        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
747            write!(f, "{}", self.0)
748        }
749    }
750
751    #[test]
752    fn test_step_new() {
753        // Test creation with valid parameters
754        let x_value = TestValue(5);
755        let time_unit = TimeFrame::Day;
756        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
757        let y_value = 42.5;
758
759        let step = Step::new(x_value, time_unit, datetime, y_value);
760
761        // Check X properties
762        assert_eq!(*step.x.index(), 0);
763        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
764        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
765        assert_eq!(
766            *step.x.datetime(),
767            ExpirationDate::Days(pos_or_panic!(30.0))
768        );
769
770        // Check Y properties
771        assert_eq!(*step.y.index(), 0);
772        assert_eq!(*step.y.value(), 42.5);
773
774        let result_next = step.next(100.0);
775        assert!(result_next.is_ok());
776        let next = result_next.unwrap();
777
778        assert!(
779            next.to_string()
780                .contains("Step { x: Xstep { index: 1, value: 5, time_unit: Day, datetime:")
781        );
782
783        let previous_next = step.previous(100.0);
784        assert!(previous_next.is_ok());
785        let previous = previous_next.unwrap();
786
787        assert!(
788            previous
789                .to_string()
790                .contains("Step { x: Xstep { index: -1, value: 5, time_unit: Day, datetime:")
791        );
792
793        assert_eq!(next.get_graph_x_value().unwrap(), Decimal::ONE);
794        assert_eq!(previous.get_graph_x_value().unwrap(), Decimal::NEGATIVE_ONE);
795    }
796
797    #[test]
798    fn test_step_next() {
799        // Setup initial step
800        let x_value = TestValue(5);
801        let time_unit = TimeFrame::Day;
802        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
803        let y_value = 42.5;
804
805        let step = Step::new(x_value, time_unit, datetime, y_value);
806
807        // Create next step with a new Y value
808        let new_y_value = 45.0;
809        let next_step = step.next(new_y_value).unwrap();
810
811        // Check X properties
812        assert_eq!(*step.x.index(), 0);
813        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
814        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
815        assert_eq!(
816            *step.x.datetime(),
817            ExpirationDate::Days(pos_or_panic!(30.0))
818        );
819
820        // Check Y properties
821        assert_eq!(*step.y.index(), 0);
822        assert_eq!(*step.y.value(), 42.5);
823
824        assert_eq!(*next_step.x.index(), 1);
825        assert_eq!(next_step.x.step_size_in_time(), &TestValue(5));
826        assert_eq!(*next_step.x.time_unit(), TimeFrame::Day);
827        assert_eq!(
828            *next_step.x.datetime(),
829            ExpirationDate::Days(pos_or_panic!(25.0))
830        );
831
832        // Check Y properties
833        assert_eq!(*next_step.y.index(), 1);
834        assert_eq!(*next_step.y.value(), 45.0);
835    }
836
837    #[test]
838    fn test_step_previous() {
839        // Setup initial step
840        let x_value = TestValue(5);
841        let time_unit = TimeFrame::Day;
842        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
843        let y_value = 42.5;
844
845        let step = Step::new(x_value, time_unit, datetime, y_value);
846
847        // Create previous step with a new Y value
848        let new_y_value = 38.0;
849        let prev_step = step.previous(new_y_value).unwrap();
850
851        // Check X properties
852        assert_eq!(*step.x.index(), 0);
853        assert_eq!(step.x.step_size_in_time(), &TestValue(5));
854        assert_eq!(*step.x.time_unit(), TimeFrame::Day);
855        assert_eq!(
856            *step.x.datetime(),
857            ExpirationDate::Days(pos_or_panic!(30.0))
858        );
859
860        // Check Y properties
861        assert_eq!(*prev_step.y.value(), 38.0);
862    }
863
864    #[test]
865    fn test_step_with_timeframe_conversion() {
866        // Test step creation and manipulation with time frame conversion
867        let x_value = TestValue(2);
868        let time_unit = TimeFrame::Week;
869        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
870        let y_value = 100.0;
871
872        let step = Step::new(x_value, time_unit, datetime, y_value);
873
874        // Next step should decrease days by 2 weeks (14 days)
875        let next_y = 105.0;
876        let next_step = step.next(next_y).unwrap();
877
878        assert_eq!(
879            *next_step.x.datetime(),
880            ExpirationDate::Days(pos_or_panic!(16.0))
881        );
882        assert_eq!(*next_step.y.value(), 105.0);
883
884        // Previous step from initial should increase days by 2 weeks
885        let prev_y = 95.0;
886        let prev_step = step.previous(prev_y).unwrap();
887
888        assert_eq!(
889            *prev_step.x.datetime(),
890            ExpirationDate::Days(pos_or_panic!(44.0))
891        );
892        assert_eq!(*prev_step.y.value(), 95.0);
893    }
894
895    #[test]
896    fn test_step_chain() {
897        // Test a chain of steps with different Y values
898        let x_value = TestValue(10);
899        let time_unit = TimeFrame::Day;
900        let datetime = ExpirationDate::Days(Positive::HUNDRED);
901        let y_value = 50.0;
902
903        let initial = Step::new(x_value, time_unit, datetime, y_value);
904
905        // Chain: initial -> step1 -> step2 -> step3
906        let step1 = initial.next(55.0).unwrap();
907        let step2 = step1.next(60.0).unwrap();
908        let step3 = step2.next(65.0).unwrap();
909
910        // Check progression of x and y values
911        assert_eq!(*step1.x.index(), 1);
912        assert_eq!(
913            *step1.x.datetime(),
914            ExpirationDate::Days(pos_or_panic!(90.0))
915        );
916        assert_eq!(*step1.y.index(), 1);
917        assert_eq!(*step1.y.value(), 55.0);
918        assert_eq!(*step2.x.index(), 2);
919        assert_eq!(
920            *step2.x.datetime(),
921            ExpirationDate::Days(pos_or_panic!(80.0))
922        );
923        assert_eq!(*step2.y.index(), 2);
924        assert_eq!(*step2.y.value(), 60.0);
925        assert_eq!(*step3.x.index(), 3);
926        assert_eq!(
927            *step3.x.datetime(),
928            ExpirationDate::Days(pos_or_panic!(70.0))
929        );
930        assert_eq!(*step3.y.index(), 3);
931        assert_eq!(*step3.y.value(), 65.0);
932    }
933
934    #[test]
935    fn test_with_positive_type() {
936        // Test using Positive as both X and Y types
937        let x_value = pos_or_panic!(5.0);
938        let time_unit = TimeFrame::Day;
939        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
940        let y_value = pos_or_panic!(50.0);
941
942        let step = Step::new(x_value, time_unit, datetime, y_value);
943
944        let next_step = step.next(pos_or_panic!(55.0)).unwrap();
945
946        assert_eq!(
947            *next_step.x.datetime(),
948            ExpirationDate::Days(pos_or_panic!(25.0))
949        );
950        assert_eq!(*next_step.y.value(), pos_or_panic!(55.0));
951    }
952
953    #[test]
954    fn test_with_zero_days() {
955        // Test using Positive as both X and Y types
956        let x_value = pos_or_panic!(5.0);
957        let time_unit = TimeFrame::Day;
958        let datetime = ExpirationDate::Days(Positive::ZERO);
959        let y_value = pos_or_panic!(50.0);
960        let step = Step::new(x_value, time_unit, datetime, y_value);
961        let result = step.next(pos_or_panic!(55.0));
962        assert!(result.is_err());
963        let step_x = step.get_x_step();
964        assert_eq!(*step_x.index(), 0);
965        assert_eq!(*step_x.step_size_in_time(), pos_or_panic!(5.0));
966        let result = step_x.next();
967        assert!(result.is_err());
968    }
969
970    #[test]
971    fn next_ok_increments_indices_and_builds_self() {
972        let x_value = pos_or_panic!(5.0);
973        let time_unit = TimeFrame::Day;
974        let datetime = ExpirationDate::Days(Positive::TWO);
975        let y_value = pos_or_panic!(50.0);
976        let step = Step::new(x_value, time_unit, datetime, y_value);
977
978        let new_y = pos_or_panic!(55.0);
979        let next = step.next(new_y).unwrap();
980
981        assert_eq!(next.get_value(), &new_y);
982        assert_eq!(*next.x.index(), 1);
983        assert_eq!(*next.y.index(), step.y.index() + 1);
984    }
985}
986
987#[cfg(test)]
988mod tests_step_serialization {
989    use super::*;
990
991    #[cfg(feature = "clock")]
992    use chrono::{TimeZone, Utc};
993    use optionstratlib_core::pos_or_panic;
994    use serde_json::{self, Value};
995
996    // Helper function to create a test step with f64 values
997    fn create_test_step() -> Step<f64, f64> {
998        let x_value = 1.5;
999        let time_unit = TimeFrame::Day;
1000        let expiration_date = ExpirationDate::Days(pos_or_panic!(30.0));
1001        let y_value = 100.0;
1002
1003        Step::new(x_value, time_unit, expiration_date, y_value)
1004    }
1005
1006    #[test]
1007    fn test_step_serialization_with_days() {
1008        let step = create_test_step();
1009
1010        // Serialize to JSON string
1011        let serialized = serde_json::to_string(&step).unwrap();
1012
1013        // Parse the JSON string back to Value for easier assertions
1014        let json_value: Value = serde_json::from_str(&serialized).unwrap();
1015
1016        // Verify the structure is correct
1017        assert!(json_value.is_object());
1018        assert!(json_value.get("x").is_some());
1019        assert!(json_value.get("y").is_some());
1020
1021        // Verify x fields
1022        let x = &json_value["x"];
1023        assert_eq!(x["index"], 0);
1024        assert_eq!(x["step_size_in_time"], "1.5");
1025
1026        // Verify y fields
1027        let y = &json_value["y"];
1028        assert_eq!(y["index"], 0);
1029        assert_eq!(y["value"], "100");
1030    }
1031
1032    #[cfg(feature = "clock")]
1033    #[test]
1034    fn test_step_with_datetime_is_normalized() {
1035        // `Step::new` now normalizes `ExpirationDate::DateTime` into
1036        // `Days(N)` via `Xstep::new` (issue #323) instead of panicking.
1037        let x_value = 2.5;
1038        let time_unit = TimeFrame::Hour;
1039        let expiration_date =
1040            ExpirationDate::DateTime(Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap());
1041        let y_value = 200.0;
1042
1043        let step = Step::new(x_value, time_unit, expiration_date, y_value);
1044        match step.x.datetime() {
1045            ExpirationDate::Days(_) => {}
1046            other => panic!("expected ExpirationDate::Days, got {other:?}"),
1047        }
1048    }
1049
1050    #[test]
1051    fn test_step_serialize() {
1052        let x_value = pos_or_panic!(5.0);
1053        let time_unit = TimeFrame::Day;
1054        let datetime = ExpirationDate::Days(pos_or_panic!(30.0));
1055        let y_value = 42.5;
1056        let step = Step::new(x_value, time_unit, datetime, y_value);
1057
1058        // Serialize to JSON string
1059        let serialized = serde_json::to_string(&step).unwrap();
1060        assert_eq!(
1061            serialized,
1062            r#"{"x":{"index":0,"step_size_in_time":"5","time_unit":"Day","datetime":{"days":30.0}},"y":{"index":0,"value":"42.5"}}"#
1063        );
1064    }
1065
1066    #[test]
1067    fn test_step_pretty_serialization() {
1068        let step = create_test_step();
1069
1070        // Serialize to pretty JSON string
1071        let serialized = serde_json::to_string_pretty(&step).unwrap();
1072
1073        // Verify the serialized string contains expected formatting
1074        assert!(serialized.contains("{\n"));
1075        assert!(serialized.contains("  \"x\": {"));
1076        assert!(serialized.contains("  \"y\": {"));
1077
1078        // Make sure the content is still correct by deserializing
1079        let deserialized: Value = serde_json::from_str(&serialized).unwrap();
1080        assert_eq!(deserialized["x"]["step_size_in_time"], "1.5");
1081        assert_eq!(deserialized["y"]["value"], "100");
1082    }
1083}