use anyhow::{Context, Result};
use burn::tensor::backend::Backend;
use polars::prelude::*;
use super::step_3_gru_model_arch::TimeSeriesGru;
use crate::constants::{EXTENDED_INDICATORS, TECHNICAL_INDICATORS};
use crate::minute::lstm::step_1_tensor_preparation::{dataframe_to_tensors, normalize_features};
pub fn predict_next_step<B: Backend>(
model: &TimeSeriesGru<B>,
df: DataFrame,
device: &B::Device,
use_extended_features: bool,
) -> Result<f64> {
let feature_columns = if use_extended_features {
&EXTENDED_INDICATORS[..]
} else {
&TECHNICAL_INDICATORS[..]
};
if !df.is_empty() {
for col in feature_columns {
if !df.schema().contains(col) {
return Err(anyhow::anyhow!("Missing required column: {}", col));
}
}
}
let prediction_df = df.clone();
let (features, _) = dataframe_to_tensors::<B>(
&prediction_df,
crate::constants::SEQUENCE_LENGTH,
1,
device,
use_extended_features,
None,
)
.context("Tensor creation failed for prediction")?;
let seq_count = features.dims()[0];
let seq = features.clone().narrow(0, seq_count - 1, 1);
let pred_tensor = model.forward(seq);
let data = pred_tensor.to_data().convert::<f32>();
let slice = data.as_slice::<f32>().unwrap();
let value = slice[0];
Ok(value as f64)
}
pub fn predict_multiple_steps<B: Backend>(
model: &TimeSeriesGru<B>,
df: DataFrame,
horizon: usize,
device: &B::Device,
use_extended_features: bool,
) -> Result<Vec<f64>> {
if horizon == 0 {
return Ok(Vec::new());
}
let mut prediction_df = df.clone();
normalize_features(
&mut prediction_df,
&["close", "open", "high", "low"],
use_extended_features,
false,
)?;
let mut predictions = Vec::with_capacity(horizon);
for _ in 0..horizon {
let next_value =
predict_next_step(model, prediction_df.clone(), device, use_extended_features)?;
predictions.push(next_value);
let column_names: Vec<String> = prediction_df
.get_column_names()
.iter()
.map(|&s| s.to_string())
.collect();
let mut next_row_values = Vec::new();
for col_name in &column_names {
let col = prediction_df.column(col_name)?;
let height = col.len();
if col_name == "close" {
next_row_values.push(Series::new(col_name.into(), vec![next_value]).into());
} else if height > 0 {
if let Ok(f64_series) = col.f64() {
let last_val = f64_series.get(height - 1).unwrap_or(0.0);
next_row_values.push(Series::new(col_name.into(), vec![last_val]).into());
} else {
let last_val = col.get(height - 1);
match last_val {
Ok(AnyValue::Int32(v)) => {
next_row_values.push(Series::new(col_name.into(), vec![v]).into())
}
Ok(AnyValue::Int64(v)) => {
next_row_values.push(Series::new(col_name.into(), vec![v]).into())
}
Ok(AnyValue::Float32(v)) => {
next_row_values.push(Series::new(col_name.into(), vec![v]).into())
}
Ok(AnyValue::Float64(v)) => {
next_row_values.push(Series::new(col_name.into(), vec![v]).into())
}
Ok(AnyValue::String(v)) => {
next_row_values.push(Series::new(col_name.into(), vec![v]).into())
}
_ => next_row_values.push(Series::new(col_name.into(), vec![0.0]).into()),
};
}
} else {
next_row_values.push(Series::new(col_name.into(), vec![0.0]).into());
}
}
if !next_row_values.is_empty() {
let next_row = DataFrame::new(next_row_values)?;
if next_row.width() != prediction_df.width() {
return Err(anyhow::anyhow!(
"Error: lengths don't match: unable to append to a DataFrame of width {} with a DataFrame of width {}",
prediction_df.width(), next_row.width()
));
}
prediction_df = prediction_df.vstack(&next_row)?;
}
}
Ok(predictions)
}
pub fn compare_with_lstm<B: Backend>(
gru_model: &TimeSeriesGru<B>,
lstm_model: &crate::minute::lstm::step_3_lstm_model_arch::TimeSeriesLstm<B>,
df: DataFrame,
horizon: usize,
device: &B::Device,
) -> Result<(Vec<f64>, Vec<f64>)> {
let mut normalized_df = df.clone();
normalize_features(
&mut normalized_df,
&["close", "open", "high", "low"],
false,
false,
)?;
let gru_predictions =
predict_multiple_steps(gru_model, normalized_df.clone(), horizon, device, false)?;
let lstm_predictions =
crate::minute::lstm::step_5_prediction::generate_forecast_with_correction(
lstm_model,
normalized_df,
horizon,
device,
false,
0.5, )?;
Ok((gru_predictions, lstm_predictions))
}