use anyhow::Result;
use burn::module::Module;
use burn::tensor::backend::AutodiffBackend;
use burn::tensor::cast::ToElement;
use burn::tensor::Shape;
use burn::tensor::{backend::Backend, Tensor};
use num_traits::cast::NumCast;
use num_traits::Num;
use polars::datatypes::{DataType, TimeUnit};
use polars::prelude::*;
use polars::series::Series;
use std::ops::Add;
use std::path::Path;
use super::step_1_tensor_preparation::{
impute_missing_values, load_daily_csv, normalize_daily_features, DAILY_FEATURES,
};
use super::step_3_gru_model_arch::DailyGRUModel;
pub fn predict_with_model<B: Backend>(
model: &DailyGRUModel<B>,
input_data: Tensor<B, 3>,
) -> Tensor<B, 2> {
model.predict(input_data)
}
pub fn generate_forecast<B: Backend>(
model: &DailyGRUModel<B>,
mut df: DataFrame,
sequence_length: usize,
forecast_days: usize,
device: &B::Device,
) -> Result<Vec<f64>> {
if df.height() < sequence_length {
return Err(anyhow::anyhow!(
"Not enough data for the requested sequence length"
));
}
let original_df = df.clone();
normalize_daily_features(&mut df)?;
let feature_columns = &DAILY_FEATURES;
let num_features = feature_columns.len();
let mut forecasts = Vec::with_capacity(forecast_days);
let close_series = original_df.column("adjusted_close")?.f64()?;
let min_price = close_series.min().unwrap_or(0.0);
let max_price = close_series.max().unwrap_or(1.0);
for _day in 0..forecast_days {
let current_data = df.slice((df.height() - sequence_length) as i64, sequence_length);
let mut features_vec: Vec<Vec<f64>> = Vec::new();
for &col in feature_columns {
if !current_data.schema().contains(col) {
return Err(anyhow::anyhow!("Column '{}' not found in DataFrame", col));
}
let series = current_data.column(col)?.f64()?;
features_vec.push(
series
.into_iter()
.map(|v| v.unwrap_or(0.0))
.collect::<Vec<f64>>(),
);
}
let mut x_data = Vec::with_capacity(sequence_length * num_features);
for t in 0..sequence_length {
for f in 0..num_features {
x_data.push(features_vec[f][t]);
}
}
let x_tensor = Tensor::<B, 1>::from_data(x_data.as_slice(), device).reshape([
1,
sequence_length,
num_features,
]);
let prediction = model.predict(x_tensor);
let pred_value: f64 = prediction.into_scalar().to_f64();
let denormalized_pred = pred_value * (max_price - min_price) + min_price;
forecasts.push(denormalized_pred);
let mut new_row = df.clone().slice((df.height() - 1) as i64, 1);
let datetime_column_name = if new_row.schema().contains("datetime") {
"datetime"
} else if new_row.schema().contains("time") {
"time"
} else {
return Err(anyhow::anyhow!("Neither 'datetime' nor 'time' column found in DataFrame"));
};
let date_col = new_row.column(datetime_column_name)?;
let date_dtype = date_col.dtype();
match date_dtype {
DataType::Datetime(time_unit, tz) => {
let datetime_series = new_row.column(datetime_column_name)?.clone();
let datetime_int = datetime_series.cast(&DataType::Int64)?;
let datetime_values: Vec<i64> = datetime_int
.i64()?
.into_iter()
.map(|opt_val| opt_val.unwrap_or(0) + 86400000000) .collect();
let new_datetime = Series::new(datetime_column_name.into(), datetime_values)
.cast(&DataType::Datetime(*time_unit, tz.clone()))?;
new_row.replace(datetime_column_name, new_datetime)?;
},
DataType::String => {
let new_date = Series::new(datetime_column_name.into(), &["next_day"]);
new_row.replace(datetime_column_name, new_date)?;
},
_ => {
let new_date = Series::new(datetime_column_name.into(), &["next_day"]);
new_row.replace(datetime_column_name, new_date)?;
}
}
let normalized_pred_array = vec![pred_value];
new_row.replace(
"adjusted_close",
Series::new("adjusted_close".into(), normalized_pred_array.clone()),
)?;
new_row.replace(
"close",
Series::new("close".into(), normalized_pred_array.clone()),
)?;
new_row.replace(
"high",
Series::new("high".into(), normalized_pred_array.clone()),
)?;
new_row.replace(
"low",
Series::new("low".into(), normalized_pred_array.clone()),
)?;
new_row.replace("open", Series::new("open".into(), normalized_pred_array))?;
new_row.replace("returns", Series::new("returns".into(), vec![0.0f64]))?;
new_row.replace(
"price_range",
Series::new("price_range".into(), vec![0.0f64]),
)?;
df = df.vstack(&new_row)?;
}
Ok(forecasts)
}
pub fn predict_next_day<B: Backend>(
model: &DailyGRUModel<B>,
csv_path: &str,
sequence_length: usize,
device: &B::Device,
) -> Result<f64> {
let mut df = load_daily_csv(csv_path)?;
impute_missing_values(&mut df, "forward")?;
let forecasts = generate_forecast(model, df, sequence_length, 1, device)?;
Ok(forecasts[0])
}
pub fn evaluate_model<B: Backend>(
model: &DailyGRUModel<B>,
test_df: &DataFrame,
sequence_length: usize,
device: &B::Device,
) -> Result<(f64, f64)> {
let mut test_df = test_df.clone();
normalize_daily_features(&mut test_df)?;
let num_samples = test_df.height() - sequence_length;
let mut predictions_vec = Vec::with_capacity(num_samples);
let mut targets_vec = Vec::with_capacity(num_samples);
let target_col = test_df.column("adjusted_close")?.f64()?;
for i in 0..num_samples {
let sequence = test_df.slice(i as i64, sequence_length);
let open_col = sequence.column("open")?.f64()?;
let high_col = sequence.column("high")?.f64()?;
let low_col = sequence.column("low")?.f64()?;
let close_col = sequence.column("close")?.f64()?;
let volume_col = sequence.column("volume")?.f64()?;
let adj_close_col = sequence.column("adjusted_close")?.f64()?;
let num_features = 6; let mut x_data = Vec::with_capacity(sequence_length * num_features);
for j in 0..sequence_length {
x_data.push(open_col.get(j).unwrap_or(0.0) as f32);
x_data.push(high_col.get(j).unwrap_or(0.0) as f32);
x_data.push(low_col.get(j).unwrap_or(0.0) as f32);
x_data.push(close_col.get(j).unwrap_or(0.0) as f32);
x_data.push(volume_col.get(j).unwrap_or(0.0) as f32);
x_data.push(adj_close_col.get(j).unwrap_or(0.0) as f32);
}
let x_tensor = Tensor::<B, 1>::from_data(x_data.as_slice(), device).reshape([
1,
sequence_length,
num_features,
]);
let target = target_col.get(i + sequence_length).unwrap_or(0.0);
targets_vec.push(target as f32);
let pred = model.predict(x_tensor);
let pred_value = pred.into_scalar().to_f32();
predictions_vec.push(pred_value);
}
let predictions = Tensor::<B, 1>::from_data(predictions_vec.as_slice(), device);
let test_targets = Tensor::<B, 1>::from_data(targets_vec.as_slice(), device);
let two = Tensor::<B, 1>::ones_like(&predictions);
let squared_diff = (predictions.clone() - test_targets.clone()).powf(two);
let mse = squared_diff.mean().into_scalar().to_f64();
let abs_diff = (predictions - test_targets).abs();
let mae = abs_diff.mean().into_scalar().to_f64();
Ok((mse, mae))
}
pub fn forecast_daily_gru<B: Backend>(
model: &DailyGRUModel<B>,
csv_path: &str,
sequence_length: usize,
forecast_days: usize,
device: &B::Device,
) -> Result<Vec<f64>> {
let mut df = load_daily_csv(csv_path)?;
impute_missing_values(&mut df, "forward")?;
let price_col = df.column("adjusted_close")?;
let price_arr = price_col.f64()?;
let min_price = price_arr.min().unwrap_or(0.0);
let max_price = price_arr.max().unwrap_or(1.0);
normalize_daily_features(&mut df)?;
let mut forecasts = Vec::with_capacity(forecast_days);
for _day in 0..forecast_days {
let current_data = df.slice(
(df.height() as i64) - (sequence_length as i64),
sequence_length,
);
let open_col = current_data.column("open")?.f64()?;
let high_col = current_data.column("high")?.f64()?;
let low_col = current_data.column("low")?.f64()?;
let close_col = current_data.column("close")?.f64()?;
let volume_col = current_data.column("volume")?.f64()?;
let adj_close_col = current_data.column("adjusted_close")?.f64()?;
let num_features = 6; let mut x_data = Vec::with_capacity(sequence_length * num_features);
for i in 0..sequence_length {
x_data.push(open_col.get(i).unwrap_or(0.0) as f32);
x_data.push(high_col.get(i).unwrap_or(0.0) as f32);
x_data.push(low_col.get(i).unwrap_or(0.0) as f32);
x_data.push(close_col.get(i).unwrap_or(0.0) as f32);
x_data.push(volume_col.get(i).unwrap_or(0.0) as f32);
x_data.push(adj_close_col.get(i).unwrap_or(0.0) as f32);
}
let x_tensor = Tensor::<B, 1>::from_data(x_data.as_slice(), device).reshape([
1,
sequence_length,
num_features,
]);
let pred = model.predict(x_tensor);
let pred_value = pred.into_scalar().to_f64();
let denormalized_pred = pred_value * (max_price - min_price) + min_price;
forecasts.push(denormalized_pred);
let mut new_row = df.clone().slice((df.height() as i64) - 1, 1);
let datetime_column_name = if new_row.schema().contains("datetime") {
"datetime"
} else if new_row.schema().contains("time") {
"time"
} else {
return Err(anyhow::anyhow!("Neither 'datetime' nor 'time' column found in DataFrame"));
};
let date_col = new_row.column(datetime_column_name)?;
let date_dtype = date_col.dtype();
match date_dtype {
DataType::Datetime(time_unit, tz) => {
let datetime_series = new_row.column(datetime_column_name)?.clone();
let datetime_int = datetime_series.cast(&DataType::Int64)?;
let datetime_values: Vec<i64> = datetime_int
.i64()?
.into_iter()
.map(|opt_val| opt_val.unwrap_or(0) + 86400000000) .collect();
let new_datetime = Series::new(datetime_column_name.into(), datetime_values)
.cast(&DataType::Datetime(*time_unit, tz.clone()))?;
new_row.replace(datetime_column_name, new_datetime)?;
},
DataType::String => {
let new_date = Series::new(datetime_column_name.into(), &["next_day"]);
new_row.replace(datetime_column_name, new_date)?;
},
_ => {
let new_date = Series::new(datetime_column_name.into(), &["next_day"]);
new_row.replace(datetime_column_name, new_date)?;
}
}
let normalized_pred_array = vec![denormalized_pred];
new_row.replace(
"adjusted_close",
Series::new("adjusted_close".into(), &normalized_pred_array),
)?;
new_row.replace("close", Series::new("close".into(), &normalized_pred_array))?;
new_row.replace("high", Series::new("high".into(), &normalized_pred_array))?;
new_row.replace("low", Series::new("low".into(), &normalized_pred_array))?;
new_row.replace("open", Series::new("open".into(), &normalized_pred_array))?;
new_row.replace("returns", Series::new("returns".into(), &[0.0f64]))?;
new_row.replace("price_range", Series::new("price_range".into(), &[0.0f64]))?;
df = df.vstack(&new_row)?;
}
Ok(forecasts)
}
pub fn evaluate_daily_gru<B: Backend>(
model: &DailyGRUModel<B>,
test_data: &DataFrame,
sequence_length: usize,
device: &B::Device,
) -> Result<(f64, f64)> {
let mut test_df = test_data.clone();
normalize_daily_features(&mut test_df)?;
let num_samples = test_df.height() - sequence_length;
let mut predictions_vec = Vec::with_capacity(num_samples);
let mut targets_vec = Vec::with_capacity(num_samples);
let target_col = test_df.column("adjusted_close")?.f64()?;
for i in 0..num_samples {
let sequence = test_df.slice(i as i64, sequence_length);
let open_col = sequence.column("open")?.f64()?;
let high_col = sequence.column("high")?.f64()?;
let low_col = sequence.column("low")?.f64()?;
let close_col = sequence.column("close")?.f64()?;
let volume_col = sequence.column("volume")?.f64()?;
let adj_close_col = sequence.column("adjusted_close")?.f64()?;
let num_features = 6; let mut x_data = Vec::with_capacity(sequence_length * num_features);
for j in 0..sequence_length {
x_data.push(open_col.get(j).unwrap_or(0.0) as f32);
x_data.push(high_col.get(j).unwrap_or(0.0) as f32);
x_data.push(low_col.get(j).unwrap_or(0.0) as f32);
x_data.push(close_col.get(j).unwrap_or(0.0) as f32);
x_data.push(volume_col.get(j).unwrap_or(0.0) as f32);
x_data.push(adj_close_col.get(j).unwrap_or(0.0) as f32);
}
let x_tensor = Tensor::<B, 1>::from_data(x_data.as_slice(), device).reshape([
1,
sequence_length,
num_features,
]);
let target = target_col.get(i + sequence_length).unwrap_or(0.0);
targets_vec.push(target as f32);
let pred = model.predict(x_tensor);
let pred_value = pred.into_scalar().to_f32();
predictions_vec.push(pred_value);
}
let predictions = Tensor::<B, 1>::from_data(predictions_vec.as_slice(), device);
let test_targets = Tensor::<B, 1>::from_data(targets_vec.as_slice(), device);
let two = Tensor::<B, 1>::ones_like(&predictions);
let squared_diff = (predictions.clone() - test_targets.clone()).powf(two);
let mse = squared_diff.mean().into_scalar().to_f64();
let abs_diff = (predictions - test_targets).abs();
let mae = abs_diff.mean().into_scalar().to_f64();
Ok((mse, mae))
}