ta_lib_in_rust/indicators/math/
mod.rs

1use polars::prelude::*;
2
3/// Vector arithmetic addition
4///
5/// # Arguments
6///
7/// * `df` - DataFrame containing the data
8/// * `col1` - First column name
9/// * `col2` - Second column name
10///
11/// # Returns
12///
13/// Returns a PolarsResult containing the addition Series
14pub fn calculate_add(df: &DataFrame, col1: &str, col2: &str) -> PolarsResult<Series> {
15    if !df.schema().contains(col1) || !df.schema().contains(col2) {
16        return Err(PolarsError::ComputeError(
17            format!("Addition requires both {col1} and {col2} columns").into(),
18        ));
19    }
20
21    let series1 = df.column(col1)?.f64()?;
22    let series2 = df.column(col2)?.f64()?;
23
24    let result = series1 + series2;
25
26    Ok(result.with_name(format!("{col1}_add_{col2}").into()).into())
27}
28
29/// Vector arithmetic subtraction
30///
31/// # Arguments
32///
33/// * `df` - DataFrame containing the data
34/// * `col1` - First column name (minuend)
35/// * `col2` - Second column name (subtrahend)
36///
37/// # Returns
38///
39/// Returns a PolarsResult containing the subtraction Series
40pub fn calculate_sub(df: &DataFrame, col1: &str, col2: &str) -> PolarsResult<Series> {
41    if !df.schema().contains(col1) || !df.schema().contains(col2) {
42        return Err(PolarsError::ComputeError(
43            format!("Subtraction requires both {col1} and {col2} columns").into(),
44        ));
45    }
46
47    let series1 = df.column(col1)?.f64()?;
48    let series2 = df.column(col2)?.f64()?;
49
50    let result = series1 - series2;
51
52    Ok(result.with_name(format!("{col1}_sub_{col2}").into()).into())
53}
54
55/// Vector arithmetic multiplication
56///
57/// # Arguments
58///
59/// * `df` - DataFrame containing the data
60/// * `col1` - First column name
61/// * `col2` - Second column name
62///
63/// # Returns
64///
65/// Returns a PolarsResult containing the multiplication Series
66pub fn calculate_mult(df: &DataFrame, col1: &str, col2: &str) -> PolarsResult<Series> {
67    if !df.schema().contains(col1) || !df.schema().contains(col2) {
68        return Err(PolarsError::ComputeError(
69            format!("Multiplication requires both {col1} and {col2} columns").into(),
70        ));
71    }
72
73    let series1 = df.column(col1)?.f64()?;
74    let series2 = df.column(col2)?.f64()?;
75
76    let result = series1 * series2;
77
78    Ok(result
79        .with_name(format!("{col1}_mult_{col2}").into())
80        .into())
81}
82
83/// Vector arithmetic division
84///
85/// # Arguments
86///
87/// * `df` - DataFrame containing the data
88/// * `col1` - First column name (numerator)
89/// * `col2` - Second column name (denominator)
90///
91/// # Returns
92///
93/// Returns a PolarsResult containing the division Series
94pub fn calculate_div(df: &DataFrame, col1: &str, col2: &str) -> PolarsResult<Series> {
95    if !df.schema().contains(col1) || !df.schema().contains(col2) {
96        return Err(PolarsError::ComputeError(
97            format!("Division requires both {col1} and {col2} columns").into(),
98        ));
99    }
100
101    let series1 = df.column(col1)?.f64()?;
102    let series2 = df.column(col2)?.f64()?;
103
104    // Replace zeros with NaN to avoid division by zero
105    let mut div_values = Vec::with_capacity(df.height());
106
107    for i in 0..df.height() {
108        let num = series1.get(i).unwrap_or(f64::NAN);
109        let denom = series2.get(i).unwrap_or(f64::NAN);
110
111        if denom != 0.0 && !denom.is_nan() && !num.is_nan() {
112            div_values.push(num / denom);
113        } else {
114            div_values.push(f64::NAN);
115        }
116    }
117
118    Ok(Series::new(format!("{col1}_div_{col2}").into(), div_values))
119}
120
121/// Find maximum value over a specified window
122///
123/// # Arguments
124///
125/// * `df` - DataFrame containing the data
126/// * `column` - Column name to calculate on
127/// * `window` - Window size for the calculation
128///
129/// # Returns
130///
131/// Returns a PolarsResult containing the MAX Series
132pub fn calculate_max(df: &DataFrame, column: &str, window: usize) -> PolarsResult<Series> {
133    if !df.schema().contains(column) {
134        return Err(PolarsError::ComputeError(
135            format!("MAX calculation requires {column} column").into(),
136        ));
137    }
138
139    let series = df.column(column)?.f64()?;
140
141    let mut max_values = Vec::with_capacity(df.height());
142
143    // Fill initial values with NaN
144    for _i in 0..window - 1 {
145        max_values.push(f64::NAN);
146    }
147
148    // Calculate max for each window
149    for i in window - 1..df.height() {
150        let mut max_val = f64::NEG_INFINITY;
151        let mut all_nan = true;
152
153        for j in 0..window {
154            let val = series.get(i - j).unwrap_or(f64::NAN);
155            if !val.is_nan() {
156                max_val = max_val.max(val);
157                all_nan = false;
158            }
159        }
160
161        if all_nan {
162            max_values.push(f64::NAN);
163        } else {
164            max_values.push(max_val);
165        }
166    }
167
168    Ok(Series::new(
169        format!("{column}_max_{window}").into(),
170        max_values,
171    ))
172}
173
174/// Find minimum value over a specified window
175///
176/// # Arguments
177///
178/// * `df` - DataFrame containing the data
179/// * `column` - Column name to calculate on
180/// * `window` - Window size for the calculation
181///
182/// # Returns
183///
184/// Returns a PolarsResult containing the MIN Series
185pub fn calculate_min(df: &DataFrame, column: &str, window: usize) -> PolarsResult<Series> {
186    if !df.schema().contains(column) {
187        return Err(PolarsError::ComputeError(
188            format!("MIN calculation requires {column} column").into(),
189        ));
190    }
191
192    let series = df.column(column)?.f64()?;
193
194    let mut min_values = Vec::with_capacity(df.height());
195
196    // Fill initial values with NaN
197    for _i in 0..window - 1 {
198        min_values.push(f64::NAN);
199    }
200
201    // Calculate min for each window
202    for i in window - 1..df.height() {
203        let mut min_val = f64::INFINITY;
204        let mut all_nan = true;
205
206        for j in 0..window {
207            let val = series.get(i - j).unwrap_or(f64::NAN);
208            if !val.is_nan() {
209                min_val = min_val.min(val);
210                all_nan = false;
211            }
212        }
213
214        if all_nan {
215            min_values.push(f64::NAN);
216        } else {
217            min_values.push(min_val);
218        }
219    }
220
221    Ok(Series::new(
222        format!("{column}_min_{window}").into(),
223        min_values,
224    ))
225}
226
227/// Calculate sum over a specified window
228///
229/// # Arguments
230///
231/// * `df` - DataFrame containing the data
232/// * `column` - Column name to calculate on
233/// * `window` - Window size for the calculation
234///
235/// # Returns
236///
237/// Returns a PolarsResult containing the SUM Series
238pub fn calculate_sum(df: &DataFrame, column: &str, window: usize) -> PolarsResult<Series> {
239    if !df.schema().contains(column) {
240        return Err(PolarsError::ComputeError(
241            format!("SUM calculation requires {column} column").into(),
242        ));
243    }
244
245    let series = df.column(column)?.f64()?;
246
247    let mut sum_values = Vec::with_capacity(df.height());
248
249    // Fill initial values with NaN
250    for _i in 0..window - 1 {
251        sum_values.push(f64::NAN);
252    }
253
254    // Calculate sum for each window
255    for i in window - 1..df.height() {
256        let mut sum = 0.0;
257        let mut all_nan = true;
258
259        for j in 0..window {
260            let val = series.get(i - j).unwrap_or(f64::NAN);
261            if !val.is_nan() {
262                sum += val;
263                all_nan = false;
264            }
265        }
266
267        if all_nan {
268            sum_values.push(f64::NAN);
269        } else {
270            sum_values.push(sum);
271        }
272    }
273
274    Ok(Series::new(
275        format!("{column}_sum_{window}").into(),
276        sum_values,
277    ))
278}
279
280/// Calculate the rolling sum of a column in a DataFrame
281///
282/// # Arguments
283///
284/// * `df` - DataFrame containing the column
285/// * `column_name` - Name of the column to sum
286/// * `window` - Window size for the rolling sum
287///
288/// # Returns
289///
290/// Returns a PolarsResult containing the rolling sum Series
291pub fn calculate_rolling_sum(
292    df: &DataFrame,
293    column_name: &str,
294    window: usize,
295) -> PolarsResult<Series> {
296    // Get the column
297    let column = df.column(column_name)?.f64()?;
298    let n = column.len();
299
300    // Initialize a new vector for the results
301    let mut result = Vec::with_capacity(n);
302
303    // Calculate the first window-1 values which are null
304    for _i in 0..window - 1 {
305        result.push(f64::NAN);
306    }
307
308    // Calculate the remaining values
309    for i in window - 1..n {
310        let mut sum = 0.0;
311        for j in 0..window {
312            sum += column.get(i - j).unwrap_or(0.0);
313        }
314        result.push(sum);
315    }
316
317    // Return the result as a Series
318    Ok(Series::new(
319        format!("{}_sum{}", column_name, window).into(),
320        result,
321    ))
322}
323
324/// Calculate the rolling average of a column in a DataFrame
325///
326/// # Arguments
327///
328/// * `df` - DataFrame containing the column
329/// * `column_name` - Name of the column to average
330/// * `window` - Window size for the rolling average
331///
332/// # Returns
333///
334/// Returns a PolarsResult containing the rolling average Series
335pub fn calculate_rolling_avg(
336    df: &DataFrame,
337    column_name: &str,
338    window: usize,
339) -> PolarsResult<Series> {
340    // Get the column
341    let column = df.column(column_name)?.f64()?;
342    let n = column.len();
343
344    // Initialize a new vector for the results
345    let mut result = Vec::with_capacity(n);
346
347    // Calculate the first window-1 values which are null
348    for _i in 0..window - 1 {
349        result.push(f64::NAN);
350    }
351
352    // Calculate the remaining values
353    for i in window - 1..n {
354        let mut sum = 0.0;
355        for j in 0..window {
356            sum += column.get(i - j).unwrap_or(0.0);
357        }
358        result.push(sum / window as f64);
359    }
360
361    // Return the result as a Series
362    Ok(Series::new(
363        format!("{}_avg{}", column_name, window).into(),
364        result,
365    ))
366}
367
368/// Calculate the rolling standard deviation of a column in a DataFrame
369///
370/// # Arguments
371///
372/// * `df` - DataFrame containing the column
373/// * `column_name` - Name of the column to calculate the standard deviation for
374/// * `window` - Window size for the rolling standard deviation
375///
376/// # Returns
377///
378/// Returns a PolarsResult containing the rolling standard deviation Series
379pub fn calculate_rolling_std(
380    df: &DataFrame,
381    column_name: &str,
382    window: usize,
383) -> PolarsResult<Series> {
384    // Get the column
385    let column = df.column(column_name)?.f64()?;
386    let n = column.len();
387
388    // Initialize a new vector for the results
389    let mut result = Vec::with_capacity(n);
390
391    // Calculate the first window-1 values which are null
392    for _i in 0..window - 1 {
393        result.push(f64::NAN);
394    }
395
396    // Calculate the remaining values
397    for i in window - 1..n {
398        let mut sum = 0.0;
399        let mut sum_sq = 0.0;
400
401        for j in 0..window {
402            let value = column.get(i - j).unwrap_or(0.0);
403            sum += value;
404            sum_sq += value * value;
405        }
406
407        let avg = sum / window as f64;
408        let variance = if window > 1 {
409            (sum_sq - sum * avg) / (window as f64 - 1.0)
410        } else {
411            0.0
412        };
413
414        if variance < 0.0 {
415            // Due to floating point errors, variance can be slightly negative
416            // when it should be zero. In this case, just return 0.0.
417            result.push(0.0);
418        } else {
419            result.push(variance.sqrt());
420        }
421    }
422
423    // Return the result as a Series
424    Ok(Series::new(
425        format!("{}_std{}", column_name, window).into(),
426        result,
427    ))
428}
429
430/// Calculate the rate of change (ROC) of a column in a DataFrame
431///
432/// # Arguments
433///
434/// * `df` - DataFrame containing the column
435/// * `column_name` - Name of the column to calculate ROC for
436/// * `period` - Period for the ROC calculation
437///
438/// # Returns
439///
440/// Returns a PolarsResult containing the ROC Series
441pub fn calculate_rate_of_change(
442    df: &DataFrame,
443    column_name: &str,
444    period: usize,
445) -> PolarsResult<Series> {
446    // Get the column
447    let column = df.column(column_name)?.f64()?;
448    let n = column.len();
449
450    // Initialize a new vector for the results
451    let mut result = Vec::with_capacity(n);
452
453    // Calculate the first period values which are null
454    for _ in 0..period {
455        result.push(f64::NAN);
456    }
457
458    // Calculate the remaining values
459    for i in period..n {
460        let current_value = column.get(i).unwrap_or(0.0);
461        let previous_value = column.get(i - period).unwrap_or(1.0); // Avoid division by zero
462
463        if previous_value == 0.0 {
464            result.push(0.0); // Handle division by zero
465        } else {
466            let roc = ((current_value - previous_value) / previous_value) * 100.0;
467            result.push(roc);
468        }
469    }
470
471    // Return the result as a Series
472    Ok(Series::new(
473        format!("{}_roc{}", column_name, period).into(),
474        result,
475    ))
476}