Repository: ANeji-ARezgui-RRachdi-NCherni/Stock-Price-Predictions-Model
Pull request #21: Add training script
Author: login: anasneji2002
Created: 2025-05-11T11:48:05Z | Merged: 2025-05-13T21:45:16Z

# This PR includes:
 - Adding a script to train the models to be used in a cron job later
 - Add some utilities functions / modules to be used later / in the script
 - updated the .env.example and the train pipeline accordingly


This script and the solution in general was designed in a way that it would solely depend on the environment (only the script does, the utility methods don't). i.e if I for some reason want to no longer use LSTM and use another model instead (as long as the model is implemented and uses the same data as the LSTM) we would only need to change the env var MODEL_NAME and delete the azure ml model and run the pipeline manually : WE WONT NEED TO DO ANY CODE CHANGES WHATSOEVER (the same applies to the window size)
 
# What does the script do exactly (for each stock):
 - We fetch the csv file if existing
 - We get the model / scaler (for now we just instantiate them and later on we will integrate azure ML)
 - Get the model's last trained date (a new property added to the IModel abstract class) , this date corresponds to the date of the latest row on which the model was trained
 - if the date exists we check for rows with later dates and only use them for training, else we use the whole dataset to train the model and fit the scaler
 - after training we save the model and scaler (not implemented yet)
 
# What's left:
 - Implement the Azure ML integration part cc @ramirachdi 
 - potentially add mechanisms to check for regressions in model accuracy and send alerts


Diff:
diff --git a/.env.example b/.env.example
index adebf04..a315241 100644
--- a/.env.example
+++ b/.env.example
@@ -15,7 +15,11 @@ BACKEND_URL= xxx
 FRONTEND_URL= xxx
 
 # frontend cache
-FRONTEND_CACHE_EXPIRATION_TIME= xx
+FRONTEND_CACHE_EXPIRATION_TIME= xxx
+
+# model training
+MODEL_NAME= xxx
+WINDOW_SIZE= xxx
 
 # Langsmith API key
 LANGSMITH_API_KEY= xx
@@ -36,4 +40,5 @@ USER_AGENT= DefaultLangchainUserAgent # Default user agent for the RAG web searc
 
 # Pinecone API key and index name
 PINECONE_API_KEY= xx
-INDEX_NAME=  xx
\ No newline at end of file
+INDEX_NAME=  xx
+
diff --git a/.github/workflows/train.yml b/.github/workflows/train.yml
index 27422bf..d8379c5 100644
--- a/.github/workflows/train.yml
+++ b/.github/workflows/train.yml
@@ -6,6 +6,12 @@ on:
       - data/processed/**  # Trigger when processed data is updated
     branches:
       - main
+  workflow_dispatch:
+
+env:
+  KERAS_BACKEND: ${{ vars.KERAS_BACKEND }}
+  MODEL_NAME: ${{ vars.MODEL_NAME }}
+  WINDOW_SIZE: ${{ vars.WINDOW_SIZE }}
 
 jobs:
   train-model:
@@ -60,4 +66,5 @@ jobs:
           echo "PYTHONPATH: $PYTHONPATH"
 
           # Run the training script
-          echo "Training model on Azure ML (To be implemented)"
+          echo "Training model on Azure ML"
+          python -m scripts.train_model
diff --git a/scripts/train_model.py b/scripts/train_model.py
new file mode 100644
index 0000000..a56c9ff
--- /dev/null
+++ b/scripts/train_model.py
@@ -0,0 +1,72 @@
+import os
+import subprocess
+import pandas as pd
+from dotenv import load_dotenv
+import logging
+
+import sys
+from pathlib import Path
+sys.path.insert(0, str(Path(os.getcwd()) / '..'))
+from src.handlers.model_handler import get_or_create_model, save_model
+from src.handlers.scaler_handler import get_or_create_scaler, save_scaler
+from utils.train_test_utils import train_model
+
+# Configure logging
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+def train():
+    DATA_DIR = os.path.join(os.path.dirname(__file__), "../data/processed")
+    DATA_DIR = os.path.abspath(DATA_DIR)
+
+    load_dotenv()
+
+    WINDOW_SIZE = int(os.environ.get("WINDOW_SIZE")) 
+    MODEL_NAME = os.environ.get("MODEL_NAME")
+
+    stocks_list = [f.replace(".csv.dvc", "") for f in os.listdir(DATA_DIR) if f.endswith(".csv.dvc")]
+    logger.info(f"found {len(stocks_list)} stocks")
+    for stock in stocks_list:
+        logger.info(f"training model: {MODEL_NAME} for stock: {stock}")
+        dvc_file = os.path.join(DATA_DIR, f"{stock}.csv.dvc")
+        csv_file = dvc_file.replace(".dvc", "")
+        if not os.path.exists(csv_file):
+            logger.info("pulling csv files")
+            subprocess.run(["dvc", "pull", dvc_file], check=True, capture_output=True, text=True)
+        if not os.path.exists(csv_file):
+            logger.warning(f"{stock}.csv not found")
+            continue
+        df = pd.read_csv(csv_file, sep=";")
+        model = get_or_create_model(stock, MODEL_NAME)
+        scaler = get_or_create_scaler(stock)
+        last_trained_date = model.get_last_trained_date()
+        logger.info(f"model's old last trained date: {last_trained_date if last_trained_date else 'Never trained before'}")
+        df["date"] = pd.to_datetime(df["date"])
+        if last_trained_date:
+            num_train_rows = len(df[df['date'] > last_trained_date])
+        else:
+            num_train_rows = len(df["date"]) - WINDOW_SIZE
+        if num_train_rows == 0:
+            logger.warning("No new rows to train ...")
+            continue
+        logger.info(f"Found {num_train_rows} rows to train the model with")
+        new_last_trained_date = max(df["date"])
+        logger.info(f"model's new last trained date: {new_last_trained_date}")
+        if last_trained_date:
+            train_df = df.tail(WINDOW_SIZE + num_train_rows).copy()
+        else:
+            train_df = df.copy()
+        train_df = train_df["cloture"].values.reshape(-1,1)
+        full_model_name = f"{MODEL_NAME} {stock}"
+        scaler = train_model(model, full_model_name, train_df, scaler, WINDOW_SIZE, new_last_trained_date)
+        logger.info(f"Training {stock} model done, saving model and scaler")
+        save_model(model)
+        save_scaler(scaler)
+    logger.info("Training script completed successfully")
+
+if __name__ == "__main__":
+    # Run the processing pipeline
+    train()
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
index 28768ae..86db74e 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1,2 +1,4 @@
 from .web import CacheService
-from .prediction_model import IModel, ARIMAModel, LSTMModel, GRUModel
\ No newline at end of file
+from .prediction_model import IModel, ARIMAModel, LSTMModel, GRUModel
+from .factories import create_model
+from .handlers import get_or_create_scaler, get_or_create_model, save_scaler, save_model
\ No newline at end of file
diff --git a/src/factories/__init__.py b/src/factories/__init__.py
new file mode 100644
index 0000000..d0c3bf7
--- /dev/null
+++ b/src/factories/__init__.py
@@ -0,0 +1 @@
+from .model_factory import create_model
\ No newline at end of file
diff --git a/src/factories/model_factory.py b/src/factories/model_factory.py
new file mode 100644
index 0000000..4d5e502
--- /dev/null
+++ b/src/factories/model_factory.py
@@ -0,0 +1,17 @@
+from src.prediction_model.models import IModel, ARIMAModel, LSTMModel, GRUModel
+from dotenv import load_dotenv
+
+load_dotenv()
+
+def create_model(stock: str, model_name: str) -> IModel:
+    if model_name is None:
+        raise KeyError("The model name is not specified in the environment file!")
+    match model_name:
+        case "ARIMA":
+            return ARIMAModel(stock)
+        case "GRU":
+            return GRUModel(stock)
+        case "LSTM":
+            return LSTMModel(stock)
+        case _:
+            return NotImplementedError("The given model name is not implemented yet, please choose another one!")
\ No newline at end of file
diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py
new file mode 100644
index 0000000..d218249
--- /dev/null
+++ b/src/handlers/__init__.py
@@ -0,0 +1,2 @@
+from .model_handler import get_or_create_model, save_model
+from .scaler_handler import get_or_create_scaler, save_scaler
\ No newline at end of file
diff --git a/src/handlers/model_handler.py b/src/handlers/model_handler.py
new file mode 100644
index 0000000..8146cab
--- /dev/null
+++ b/src/handlers/model_handler.py
@@ -0,0 +1,9 @@
+from src.prediction_model.models import IModel
+from src.factories.model_factory import create_model
+
+def get_or_create_model(stock: str, model_name: str) -> IModel:
+    # TODO: Check first if the model exists already in azure ML, if so bring it
+    return create_model(stock, model_name)
+
+def save_model(model: IModel):
+    raise NotImplementedError("Method not implemented!")
\ No newline at end of file
diff --git a/src/handlers/scaler_handler.py b/src/handlers/scaler_handler.py
new file mode 100644
index 0000000..7716fd3
--- /dev/null
+++ b/src/handlers/scaler_handler.py
@@ -0,0 +1,8 @@
+from sklearn.preprocessing import MinMaxScaler
+
+def get_or_create_scaler(stock: str):
+    # TODO: Check first if the scaler exists already in azure ML, if so bring it
+    return MinMaxScaler()
+
+def save_scaler(scaler: MinMaxScaler):
+    raise NotImplementedError("This method isn't implemented!")
\ No newline at end of file
diff --git a/src/prediction_model/models/ARIMAModel.py b/src/prediction_model/models/ARIMAModel.py
index eaf8564..a382f86 100644
--- a/src/prediction_model/models/ARIMAModel.py
+++ b/src/prediction_model/models/ARIMAModel.py
@@ -3,16 +3,18 @@
 from .IModel import IModel
 
 class ARIMAModel(IModel):
-    def __init__(self):
+    def __init__(self, stock_name: str = None):
+        super().__init__(stock_name)
         pass
     
-    def train(self, features, targets):
+    def train(self, features, targets, last_trained_date):
         # for ARIMA model there isn't a separation between features and targets so we pass all the data in the features param
         features = features.flatten()
         print(f"features shape: {features.shape}")
         model = ARIMA(features, order=(2,1,5)) # the order was found when running auto_arima method which generated the best orders
         model_fit = model.fit()
         self.model = model_fit
+        self.__last_trained_date = max(last_trained_date, self.__last_trained_date)
     
     def predict(self, input_data):
         numberOfDays = len(input_data)
diff --git a/src/prediction_model/models/GRUModel.py b/src/prediction_model/models/GRUModel.py
index 9f0d55d..835aa8e 100644
--- a/src/prediction_model/models/GRUModel.py
+++ b/src/prediction_model/models/GRUModel.py
@@ -1,9 +1,9 @@
 from .IModel import IModel
 from keras.src.models import Sequential
 from keras.src.layers import GRU, Dense, Dropout
-
 class GRUModel(IModel):
-    def __init__(self):
+    def __init__(self, stock_name: str = None):
+        super().__init__(stock_name)
         model = Sequential()
         model.add(GRU(50))
         model.add(Dropout(0.2))
@@ -12,10 +12,11 @@ def __init__(self):
         model.compile(optimizer='adam', loss='mean_squared_error')
         self.model = model
 
-    def train(self, features, targets):
+    def train(self, features, targets, last_trained_date):
         print(f"features shape: {features.shape}")
         print(f"targets shape: {targets.shape}")
         self.model.fit(x=features, y=targets, batch_size=32, epochs=40, verbose=1)
+        self.__last_trained_date = max(last_trained_date, self.__last_trained_date)
     
     def predict(self, input_data):
         print(f"input shape: {input_data.shape}")
diff --git a/src/prediction_model/models/IModel.py b/src/prediction_model/models/IModel.py
index 1a43f99..291a2e5 100644
--- a/src/prediction_model/models/IModel.py
+++ b/src/prediction_model/models/IModel.py
@@ -1,16 +1,20 @@
 from abc import ABC, abstractmethod
 import numpy as np
 from enums import ValidationMetricEnum
-from sklearn.preprocessing import MinMaxScaler
+from datetime import datetime
 
 class IModel(ABC):
     """
     Abstract base class for a prediction model.
     This class defines the interface that all prediction models must implement.
     """
+    @abstractmethod
+    def __init__(self, stock_name: str = None):
+        self.__last_trained_date = None
+        self.__stock_name = stock_name
 
     @abstractmethod
-    def train(self, features: np.ndarray, targets: np.ndarray):
+    def train(self, features: np.ndarray, targets: np.ndarray, last_trained_date: datetime):
         """
         Train the model using the provided data.
 
@@ -67,3 +71,9 @@ def evaluate(self, x_test: np.ndarray, y_test: np.ndarray, metric: ValidationMet
                 return mape
             case _:
                 raise NotImplementedError("This metric isn't implemented yet, if you want to use it please add its implementation")
+    
+    def get_last_trained_date(self) -> datetime:
+        return self.__last_trained_date
+
+    def get_stock_name(self) -> str:
+        return self.__stock_name
diff --git a/src/prediction_model/models/LSTMModel.py b/src/prediction_model/models/LSTMModel.py
index 4b00c60..8e27f6e 100644
--- a/src/prediction_model/models/LSTMModel.py
+++ b/src/prediction_model/models/LSTMModel.py
@@ -3,7 +3,8 @@
 from keras.src.layers import LSTM, Dense, Dropout
 
 class LSTMModel(IModel):
-    def __init__(self):
+    def __init__(self, stock_name: str = None):
+        super().__init__(stock_name)
         model = Sequential()
         model.add(LSTM(50, return_sequences=True))
         model.add(Dropout(0.2))
@@ -14,10 +15,11 @@ def __init__(self):
         model.compile(optimizer='adam', loss='mean_squared_error')
         self.model = model
 
-    def train(self, features, targets):
+    def train(self, features, targets, last_trained_date):
         print(f"features shape: {features.shape}")
         print(f"targets shape: {targets.shape}")
         self.model.fit(x=features, y=targets, batch_size=32, epochs=40, verbose=1)
+        self.__last_trained_date = max(last_trained_date, self.__last_trained_date)
     
     def predict(self, input_data):
         print(f"input shape: {input_data.shape}")
diff --git a/tests/scripts/test_train_model.py b/tests/scripts/test_train_model.py
new file mode 100644
index 0000000..2416844
--- /dev/null
+++ b/tests/scripts/test_train_model.py
@@ -0,0 +1,57 @@
+from unittest.mock import patch, MagicMock, mock_open
+import os
+import pandas as pd
+import numpy as np
+
+from pathlib import Path
+import sys
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+
+@patch("builtins.open", new_callable=mock_open)
+@patch("os.path.exists")
+@patch("os.listdir")
+@patch("os.path.join", side_effect=lambda *args: "/".join(args))
+@patch("subprocess.run")
+@patch("pandas.read_csv")
+@patch("src.handlers.model_handler.get_or_create_model")
+@patch("src.handlers.model_handler.save_model")
+@patch("src.handlers.scaler_handler.get_or_create_scaler")
+@patch("src.handlers.scaler_handler.save_scaler")
+@patch("utils.train_test_utils.train_model")
+@patch("dotenv.load_dotenv")
+@patch.dict(os.environ, {"WINDOW_SIZE": "5", "MODEL_NAME": "LSTM"})
+def test_train_function(
+    mock_dotenv, mock_train_model, mock_save_scaler, mock_get_scaler,
+    mock_save_model, mock_get_model, mock_read_csv, mock_subprocess_run,
+    mock_join, mock_listdir, mock_exists, mock_open_file
+):
+    from scripts.train_model import train
+
+    mock_listdir.return_value = ["AAPL.csv.dvc"]
+    mock_exists.side_effect = lambda path: not path.endswith(".dvc")
+
+    df = pd.DataFrame({
+        "date": pd.date_range(start="2020-01-01", periods=20),
+        "cloture": np.random.rand(20)
+    })
+    mock_read_csv.return_value = df
+
+    dummy_model = MagicMock()
+    dummy_model.get_last_trained_date.return_value = pd.to_datetime("2020-01-10")
+    mock_get_model.return_value = dummy_model
+
+    dummy_scaler = MagicMock()
+    mock_get_scaler.return_value = dummy_scaler
+    mock_train_model.return_value = dummy_scaler
+
+    # --- Call the function ---
+    train()
+
+    # --- Assertions ---
+    mock_dotenv.assert_called()
+    mock_read_csv.assert_called_once()
+    mock_get_model.assert_called_once_with("AAPL", "LSTM")
+    mock_train_model.assert_called_once()
+    mock_save_model.assert_called_once()
+    mock_save_scaler.assert_called_once()
diff --git a/tests/utils/test_train_test_utils.py b/tests/utils/test_train_test_utils.py
index f05a724..451ef64 100644
--- a/tests/utils/test_train_test_utils.py
+++ b/tests/utils/test_train_test_utils.py
@@ -5,13 +5,14 @@
 import pytest
 import matplotlib.pyplot as plt
 from sklearn.preprocessing import MinMaxScaler
+from datetime import datetime
 
 # we need to change directory to import modules
 import sys,os
 from pathlib import Path
 sys.path.insert(0, str(Path(os.getcwd()) / '..' / '..'))
-from src import IModel, ARIMAModel
-from utils import split_dataset, get_features_target_from_dataset, plot_stock_graph, train, evaluate, plot_evaluation_result
+from src import IModel
+from utils import split_dataset, get_features_target_from_dataset, plot_stock_graph, train, evaluate, plot_evaluation_result, train_model, predict
 from enums import ValidationMetricEnum
 
 def test_split_dataset():
@@ -110,7 +111,36 @@ def test_plot_stock_graph_calls_plotting(mock_plt):
         mock_title.assert_called_once()
         mock_legend.assert_called_once()
         mock_show.assert_called_once()
-    
 
+def test_predict_sequence_generation():
+    mock_model = Mock(spec=IModel)
+    mock_model.predict.side_effect = lambda x: np.array([[0.5]])
+
+    dataset = np.array([[i] for i in range(10)])
+    scaler = MinMaxScaler()
+    scaler.fit(dataset)
+
+    predictions = predict(mock_model, dataset, scaler, num_days=3, window_size=2)
+
+    # Check correct number of predictions
+    assert predictions.shape[0] == 3
+    assert mock_model.predict.call_count == 3
+
+@patch("utils.train_test_utils.train")
+def test_train_model_trains_and_returns_scaler(mock_train_func):
+    mock_model = Mock(spec=IModel)
+    model_name = "LSTM"
+    dataset = np.array([[i] for i in range(20)])
+    old_scaler = MinMaxScaler().fit(dataset)
+    window_size = 5
+    last_trained_date = datetime.now()
+
+    mock_train_func.return_value = {model_name: old_scaler}
+    result_scaler = train_model(mock_model, model_name, dataset, old_scaler, window_size, last_trained_date)
+
+    assert isinstance(result_scaler, MinMaxScaler)
+    mock_train_func.assert_called_once()
+
+    
 if __name__ == "__main__":
     pytest.main()
diff --git a/utils/__init__.py b/utils/__init__.py
index 905f9b5..fd177c9 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -1,5 +1,3 @@
-from .constants import HEADERS, SYMBOLS, RAW_DATA_DOWNLOAD_BASELINK, NEWS_BASE_URL, PAGE_URL
-from .train_test_utils import split_dataset, get_features_target_from_dataset, train, evaluate, plot_evaluation_result, plot_stock_graph
-from .pinecone_vector_store import get_pinecone_vector_store
-
-
+from .constants import HEADERS, SYMBOLS, RAW_DATA_DOWNLOAD_BASELINK
+from .train_test_utils import split_dataset, get_features_target_from_dataset, train, evaluate, plot_evaluation_result, plot_stock_graph, train_model, predict
+from .pinecone_vector_store import get_pinecone_vector_store
\ No newline at end of file
diff --git a/utils/train_test_utils.py b/utils/train_test_utils.py
index 529fd14..c4bbb43 100644
--- a/utils/train_test_utils.py
+++ b/utils/train_test_utils.py
@@ -4,6 +4,7 @@
 import matplotlib.pyplot as plt
 from sklearn.preprocessing import MinMaxScaler 
 from enums import ValidationMetricEnum
+from datetime import datetime
 
 # we need to change directory to import IModel interface
 import sys,os
@@ -46,11 +47,23 @@ def get_features_target_from_dataset(dataset: np.ndarray, ShouldOnlyKeepCloseCol
         Y.append(y_to_append)
     return np.array(X), np.array(Y)
 
-def train(models_dictionary: Dict[str, tuple[IModel, pd.DataFrame, pd.DataFrame]]) -> Dict[str, MinMaxScaler]:
-    scaler_dict = {}
+def train_model(model: IModel, model_name: str, dataset: pd.DataFrame | np.ndarray, old_scaler: MinMaxScaler, window_size: int, last_trained_date: datetime) -> MinMaxScaler:
+    model_dict = {model_name: (model, dataset, pd.DataFrame([]))} # we don't care about the test dataset
+    if old_scaler:
+        scaler_dict = {model_name: old_scaler}
+    else:
+        scaler_dict = None
+    scaler = train(model_dict, window_size, scaler_dict=scaler_dict, last_trained_date=last_trained_date)[model_name]
+    return scaler
+
+def train(models_dictionary: Dict[str, tuple[IModel, pd.DataFrame | np.ndarray, pd.DataFrame | np.ndarray]], window_size: int = 60, scaler_dict: Dict[str, MinMaxScaler] = None, last_trained_date: datetime = datetime.today()) -> Dict[str, MinMaxScaler]:
+    new_scaler_dict = {}
     for name, tuple in models_dictionary.items():
         print(f"training {name} model ...")
-        scaler = MinMaxScaler()
+        if scaler_dict == None or (name not in scaler_dict) or scaler_dict.get(name) == None:
+            scaler = MinMaxScaler()
+        else :
+            scaler = scaler_dict.get(name)
         model = tuple[0]
         df = tuple[1]
         ShouldOnlyKeepCloseCol = df.shape[1] > 1 # if the dataframe has more than one column we need to keep cloture only as target
@@ -64,10 +77,21 @@ def train(models_dictionary: Dict[str, tuple[IModel, pd.DataFrame, pd.DataFrame]
         else :
             df_transformed = scaler.fit_transform(df)
             closeColIdx = 0
-        x_train, y_train = get_features_target_from_dataset(df_transformed, ShouldOnlyKeepCloseCol, closeColIdx=closeColIdx, isArimaContext=isArimaContext)
-        model.train(x_train, y_train)
-        scaler_dict[name] = scaler
-    return scaler_dict
+        x_train, y_train = get_features_target_from_dataset(df_transformed, ShouldOnlyKeepCloseCol, closeColIdx=closeColIdx, isArimaContext=isArimaContext, window=window_size)
+        model.train(x_train, y_train, last_trained_date)
+        new_scaler_dict[name] = scaler
+    return new_scaler_dict
+
+def predict(model: IModel, dataset: np.ndarray, scaler: MinMaxScaler, num_days: int, window_size: int) -> np.ndarray:
+    last_n_days = scaler.transform(dataset[:-window_size])
+    output = np.array([])
+    for i in range(num_days):
+        predicted_value = model.predict(last_n_days)
+        transformed_predicted_value = scaler.inverse_transform(predicted_value)
+        last_n_days = np.delete(last_n_days, 0)
+        last_n_days = np.append(last_n_days, predicted_value)
+        output = np.append(output, transformed_predicted_value)
+    return output
 
 def evaluate(models_dictionary: Dict[str, tuple[IModel, pd.DataFrame, pd.DataFrame, pd.DataFrame]], metric: ValidationMetricEnum, scaler_dict: Dict[str, MinMaxScaler]) -> Dict[str, float]:
     result = {}


Reviews:
user: login: ahmedrezgui
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T20:19:34Z
user: login: neirezcher
body: ellerine sağlık! çok iyi bir iş çıkardın 😉
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T21:33:59Z
user: login: anasneji2002
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T21:36:23Z

Review comments:
diff_hunk: @@ -3,16 +3,18 @@
 from .IModel import IModel
 
 class ARIMAModel(IModel):
-    def __init__(self):
+    def __init__(self, stock_name: str = None):
+        super().__init__(stock_name)
         pass
     
-    def train(self, features, targets):
+    def train(self, features, targets, last_trained_date):
         # for ARIMA model there isn't a separation between features and targets so we pass all the data in the features param
         features = features.flatten()
         print(f"features shape: {features.shape}")
         model = ARIMA(features, order=(2,1,5)) # the order was found when running auto_arima method which generated the best orders
         model_fit = model.fit()
path: src/prediction_model/models/ARIMAModel.py
user: login: neirezcher
body: Why don't you assign a new value to the attribute directly, like self.model = model.fit()? Is there a specific reason for avoiding that?
created_at: 2025-05-13T21:09:20Z
author_association: COLLABORATOR
diff_hunk: @@ -3,16 +3,18 @@
 from .IModel import IModel
 
 class ARIMAModel(IModel):
-    def __init__(self):
+    def __init__(self, stock_name: str = None):
+        super().__init__(stock_name)
         pass
     
-    def train(self, features, targets):
+    def train(self, features, targets, last_trained_date):
         # for ARIMA model there isn't a separation between features and targets so we pass all the data in the features param
         features = features.flatten()
         print(f"features shape: {features.shape}")
         model = ARIMA(features, order=(2,1,5)) # the order was found when running auto_arima method which generated the best orders
         model_fit = model.fit()
path: src/prediction_model/models/ARIMAModel.py
user: login: anasneji2002
body: this is old code (not related to the PR, but i don't know : maybe for readibility but not sure tbh)
will make sure to fix it in another pr
created_at: 2025-05-13T21:36:23Z
author_association: COLLABORATOR

PR comments:
user: login: anasneji2002
created_at: 2025-05-11T12:05:04Z
body: the failing test is related to web/backend module (unrelated to my changes), @ramirachdi can you check please ?
author_association: COLLABORATOR
user: login: anasneji2002
created_at: 2025-05-13T21:44:56Z
body: merging the PR (will check the test afterwards since the tests pass locally)
author_association: COLLABORATOR