
Introduction
The Indian stock market — comprising the National Stock Exchange (NSE) and the Bombay Stock Exchange (BSE) — is one of the largest and most dynamic markets in the world. With over 5,000 listed companies and millions of retail investors, there is enormous interest in leveraging Machine Learning (ML) to forecast price movements.
In this blog, we apply ML end-to-end: scikit-learn handles data preprocessing and evaluation, while TensorFlow/Keras powers a stacked LSTM (Long Short-Term Memory) neural network — a deep learning model specifically designed for time-series data like stock prices. LSTM belongs to the broader family of ML algorithms and is particularly well suited here because it learns patterns across sequences of past prices rather than treating each day in isolation.
We walk through a complete, working pipeline to:
Fetch real historical data for Indian stocks (Reliance, TCS, HDFC Bank, etc.)
Engineer meaningful technical indicators as ML features (RSI, MACD, Bollinger Bands)
Preprocess and scale data correctly using scikit-learn — with no data leakage
Train a stacked LSTM deep learning model using TensorFlow/Keras on all engineered features
Evaluate the ML model's predictions in real INR values and visualise results
Disclaimer: Stock price prediction is inherently uncertain. This guide is for educational purposes only and should not be treated as financial advice. A low MAPE does not imply the model has captured genuine market dynamics — LSTM models on price data often approximate a naïve "predict yesterday's price" baseline.
The ML Stack at a Glance
Before diving in, here is how Machine Learning is used at each stage of the pipeline:
Stage | Library | ML Role |
|---|---|---|
Data scaling | scikit-learn | Normalises all features to [0, 1] so the neural network trains stably |
Model | TensorFlow / Keras | Deep learning model that learns temporal patterns in multivariate sequences |
Regularisation | Keras | ML technique to prevent the model from overfitting to training data |
Training control | Keras | Stops training when the model stops improving; adjusts learning rate |
Evaluation | scikit-learn | Standard ML regression metrics to measure prediction accuracy |
LSTM is a type of Recurrent Neural Network (RNN) — a class of ML models that process inputs as ordered sequences. Unlike a standard regression model that sees one row at a time, LSTM remembers patterns across the previous 60 trading days to make each prediction. That memory is what makes it well suited for stock prices, where yesterday and last month both matter.
Prerequisites
1. Check Your Python Version
TensorFlow only supports Python 3.11 and 3.12. If you are on Python 3.13 or 3.14 (which is the default on many fresh Mac installs as of 2025–26), the pip install tensorflow step will fail with a "no matching distribution" error.
Check your version first:
python3 --version
If it says 3.13.x or 3.14.x, follow the Mac setup below before creating a virtual environment. If it says 3.11.x or 3.12.x, skip straight to Step 2.
Mac users — install Python 3.11 via pyenv:
# Install pyenv (skip if you already have it)
brew install pyenv
# Install Python 3.11
pyenv install 3.11.9
# Create the virtual environment using Python 3.11 explicitly
~/.pyenv/versions/3.11.9/bin/python -m venv stock-env
# Activate
source stock-env/bin/activate
# Confirm you're on the right version
python --version # should print Python 3.11.9
Why 3.11? TensorFlow on Apple Silicon (M1/M2/M3/M4) has the most stable wheel support on Python 3.11. Python 3.12 works too — 3.13 and above are not yet supported.
2. Create a Virtual Environment
Always use a virtual environment to keep this project's dependencies isolated from your system Python and other projects.
On macOS / Linux (Python 3.11 or 3.12 confirmed):
python3 -m venv stock-env
source stock-env/bin/activate
On Windows:
python -m venv stock-env
stock-env\Scripts\activate
Your terminal prompt will change to show (stock-env) — that means the environment is active.
To deactivate when you're done, simply run
deactivate.
3. Install Required Libraries
pip install yfinance pandas numpy matplotlib scikit-learn tensorflow
We use yfinance to pull NSE-listed stock data. NSE tickers always end with the .NS suffix — for example RELIANCE.NS, TCS.NS, HDFCBANK.NS.
Step 1 — Fetch Indian Stock Data
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# NSE-listed stocks use the .NS suffix
TICKER = "RELIANCE.NS" # Also try: TCS.NS, HDFCBANK.NS, INFY.NS, WIPRO.NS
START_DATE = "2019-01-01"
END_DATE = "2024-12-31"
df = yf.download(TICKER, start=START_DATE, end=END_DATE, auto_adjust=True)
# Flatten MultiIndex columns returned by recent yfinance versions
df.columns = df.columns.get_level_values(0)
# Keep OHLCV columns — we will use all of them for feature engineering
df = df[["Open", "High", "Low", "Close", "Volume"]].dropna()
print(df.head())
print(f"\nTotal trading days fetched : {len(df)}")
print(f"Close price range : ₹{df['Close'].min():.2f} – ₹{df['Close'].max():.2f}")
Sample Output:
Open High Low Close Volume
Date
2019-01-02 1152.699951 1163.300049 1139.050049 1157.199951 5765626.0
2019-01-03 1157.199951 1175.000000 1143.800049 1152.300049 6959592.0
...
Total trading days fetched : 1482
Close price range : ₹867.55 – ₹3,217.45
Note:
auto_adjust=Trueand flattening the MultiIndex are essential fixes for compatibility withyfinance >= 0.2.x. Always check the printed price range — if it doesn't match what the stock trades at today, adjust the date window.
Step 2 — Exploratory Data Analysis
# Compute moving averages for visualisation
ma50 = df["Close"].rolling(window=50).mean()
ma200 = df["Close"].rolling(window=200).mean()
plt.figure(figsize=(14, 5))
plt.plot(df["Close"], label="Close Price", linewidth=1.5, color="steelblue")
plt.plot(ma50, label="50-Day MA", linewidth=1.2, linestyle="--", color="orange")
plt.plot(ma200, label="200-Day MA", linewidth=1.2, linestyle=":", color="green")
plt.title(f"{TICKER} — Closing Price with Moving Averages")
plt.xlabel("Date")
plt.ylabel("Price (INR ₹)")
plt.legend()
plt.tight_layout()
plt.show()
Insight: When the 50-day MA crosses above the 200-day MA it forms a Golden Cross — a classic bullish signal followed closely by Indian institutional investors.
Step 3 — Feature Engineering
We engineer ten technical indicators from the raw OHLCV data. These become the actual input features fed into the LSTM — not just computed and discarded.
def add_features(df):
df = df.copy()
# Daily percentage return
df["Return"] = df["Close"].pct_change()
# 10-day rolling volatility (std dev of returns)
df["Volatility"] = df["Return"].rolling(10).std()
# Relative Strength Index — 14-day window
# Adding 1e-9 to the denominator prevents division-by-zero
delta = df["Close"].diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / (loss + 1e-9)
df["RSI"] = 100 - (100 / (1 + rs))
# Bollinger Bands — 20-day window
df["BB_Mid"] = df["Close"].rolling(20).mean()
df["BB_Upper"] = df["BB_Mid"] + 2 * df["Close"].rolling(20).std()
df["BB_Lower"] = df["BB_Mid"] - 2 * df["Close"].rolling(20).std()
# MACD and Signal line
ema12 = df["Close"].ewm(span=12, adjust=False).mean()
ema26 = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = ema12 - ema26
df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()
# Moving averages for trend context
df["MA50"] = df["Close"].rolling(50).mean()
df["MA200"] = df["Close"].rolling(200).mean()
# Drop rows with NaN (introduced by rolling windows)
return df.dropna()
df = add_features(df)
print(f"Shape after feature engineering: {df.shape}")
print(df.tail(3))
Define the feature columns
# All columns the LSTM will actually see as input
FEATURE_COLS = [
"Close", "Open", "High", "Low", "Volume",
"Return", "Volatility", "RSI",
"BB_Mid", "BB_Upper", "BB_Lower",
"MACD", "Signal", "MA50", "MA200"
]
N_FEATURES = len(FEATURE_COLS) # 15
# The column index of "Close" inside FEATURE_COLS — needed for inverse-transform
CLOSE_IDX = FEATURE_COLS.index("Close") # 0
Step 4 — Prepare Sequences for LSTM
LSTMs learn from sequences. We use a sliding window of the past 60 trading days (~3 months) to predict the next day's closing price. Every feature column is included in the window — this is a multivariate LSTM.
Critical: The scaler must be fitted only on training data to avoid data leakage.
Critical: The train/test split index must be applied to the sequence arrays X and y — not to df — because create_sequences consumes SEQUENCE_LENGTH rows to form the first window, making X shorter than df by exactly that amount. Splitting on df would give the wrong proportions.
from sklearn.preprocessing import MinMaxScaler
SEQUENCE_LENGTH = 60 # lookback window: 60 trading days ≈ 3 months
TRAIN_RATIO = 0.80 # 80 % train, 20 % test
# ── Step A: scale all features ────────────────────────────────────────────────
# Fit ONLY on the first 80 % of df rows to prevent leakage
n_total = len(df)
fit_end = int(n_total * TRAIN_RATIO) # row index for scaler fit boundary
scaler = MinMaxScaler()
scaler.fit(df[FEATURE_COLS].iloc[:fit_end]) # fit on train rows only
scaled_data = scaler.transform(df[FEATURE_COLS]) # transform entire dataset
# ── Step B: create (X, y) sequences ──────────────────────────────────────────
def create_sequences(data, seq_len, close_idx):
"""
data : 2-D scaled array, shape (n_rows, n_features)
seq_len : number of past timesteps used as input
close_idx : column index of Close inside `data`
Returns
-------
X : (n_samples, seq_len, n_features)
y : (n_samples,) — scaled Close price of the *next* day
"""
X, y = [], []
for i in range(seq_len, len(data)):
X.append(data[i - seq_len:i, :]) # all features, past seq_len days
y.append(data[i, close_idx]) # next-day Close only
return np.array(X), np.array(y)
X, y = create_sequences(scaled_data, SEQUENCE_LENGTH, CLOSE_IDX)
# ── Step C: train / test split on the SEQUENCE arrays ────────────────────────
# X has (n_total - SEQUENCE_LENGTH) rows — split must be applied here, not on df
split = int(len(X) * TRAIN_RATIO)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# Shape is already (samples, timesteps, features) — no reshape needed
print(f"X_train : {X_train.shape}") # e.g. (1137, 60, 15)
print(f"X_test : {X_test.shape}") # e.g. (285, 60, 15)
Step 5 — Build the LSTM Model
We use a stacked LSTM architecture with Dropout regularisation. The input shape is now (SEQUENCE_LENGTH, N_FEATURES) — one timestep per day, one channel per feature.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
model = Sequential([
# First LSTM layer — return_sequences=True passes output to next LSTM
LSTM(64, return_sequences=True, input_shape=(SEQUENCE_LENGTH, N_FEATURES)),
Dropout(0.2),
# Second LSTM layer — return_sequences=False collapses to a single vector
LSTM(64, return_sequences=False),
Dropout(0.2),
# Dense layers to map LSTM output to a single (scaled) Close price
Dense(32, activation="relu"),
Dense(1)
])
model.compile(optimizer="adam", loss="mean_squared_error")
model.summary()
Model Summary:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 60, 64) 20,480
dropout (Dropout) (None, 60, 64) 0
lstm_1 (LSTM) (None, 64) 33,024
dropout_1 (Dropout) (None, 64) 0
dense (Dense) (None, 32) 2,080
dense_1 (Dense) (None, 1) 33
=================================================================
Total params: 55,617
_________________________________________________________________
Note: Parameter count is slightly higher than the single-feature version because the first LSTM layer now receives 15 input channels instead of 1.
Step 6 — Train the Model
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
early_stop = EarlyStopping(
monitor="val_loss",
patience=10,
restore_best_weights=True,
verbose=1
)
reduce_lr = ReduceLROnPlateau(
monitor="val_loss",
factor=0.5,
patience=5,
min_lr=1e-6,
verbose=1
)
history = model.fit(
X_train, y_train,
epochs = 100,
batch_size = 32,
validation_split = 0.10,
callbacks = [early_stop, reduce_lr],
verbose = 1
)
plt.figure(figsize=(10, 4))
plt.plot(history.history["loss"], label="Train Loss")
plt.plot(history.history["val_loss"], label="Validation Loss")
plt.title("Model Loss Over Epochs")
plt.xlabel("Epoch")
plt.ylabel("MSE Loss")
plt.legend()
plt.tight_layout()
plt.show()
Tip:
ReduceLROnPlateauis especially helpful for volatile Indian mid-cap and small-cap stocks where gradients can be noisy.
Step 7 — Evaluate and Visualise Predictions
Because the scaler was fitted on all 15 features together, inverse-transforming only the Close column requires a small workaround: we reconstruct a full-width array of zeros, place the predicted values in the Close column, and then inverse-transform the whole array.
from sklearn.metrics import mean_absolute_error, mean_squared_error
def inverse_transform_close(scaler, values_1d, n_features, close_idx):
"""
Inverse-transform a 1-D array of scaled Close values back to INR.
Parameters
----------
scaler : fitted MinMaxScaler (n_features columns)
values_1d : 1-D numpy array of scaled predictions or actuals
n_features : total number of feature columns the scaler was fitted on
close_idx : column index of Close inside the scaler
Returns
-------
1-D numpy array in original INR scale
"""
dummy = np.zeros((len(values_1d), n_features))
dummy[:, close_idx] = values_1d
return scaler.inverse_transform(dummy)[:, close_idx]
# Generate scaled predictions
y_pred_scaled = model.predict(X_test).flatten()
# Inverse-transform to INR
y_pred = inverse_transform_close(scaler, y_pred_scaled, N_FEATURES, CLOSE_IDX)
y_true = inverse_transform_close(scaler, y_test, N_FEATURES, CLOSE_IDX)
# Evaluation metrics
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
print(f"MAE : ₹{mae:.2f}")
print(f"RMSE : ₹{rmse:.2f}")
print(f"MAPE : {mape:.2f}%")
plt.figure(figsize=(14, 5))
plt.plot(y_true, label="Actual Price (₹)", linewidth=1.5, color="steelblue")
plt.plot(y_pred, label="Predicted Price (₹)", linewidth=1.5, color="tomato", linestyle="--")
plt.title(f"{TICKER} — Actual vs Predicted Closing Price (Test Set)")
plt.xlabel("Trading Days (Test Period)")
plt.ylabel("Price (INR ₹)")
plt.legend()
plt.tight_layout()
plt.show()
Sample Metrics (Reliance Industries):
MAE : ₹21.00
RMSE : ₹27.00
MAPE : 0.66%
Interpret with caution: A low MAPE on this task does not necessarily mean the model has learned real predictive signals. LSTM models on stock prices can appear accurate simply by learning to predict a value close to the previous day's price (the naïve baseline). Always benchmark against a naïve model before drawing conclusions.
Step 8 — Predict the Next Trading Day
def predict_next_day(model, df, scaler, feature_cols, n_features, close_idx, seq_len=60):
"""
Returns tomorrow's predicted closing price in INR.
Uses the most recent `seq_len` trading days as input.
"""
recent_scaled = scaler.transform(df[feature_cols].iloc[-seq_len:])
X_input = recent_scaled.reshape(1, seq_len, n_features)
pred_scaled = model.predict(X_input).flatten()
pred_price = inverse_transform_close(scaler, pred_scaled, n_features, close_idx)[0]
return pred_price
next_day_price = predict_next_day(model, df, scaler, FEATURE_COLS, N_FEATURES, CLOSE_IDX)
last_price = float(df["Close"].iloc[-1])
change_pct = ((next_day_price - last_price) / last_price) * 100
print(f"Last Closing Price : ₹{last_price:.2f}")
print(f"Predicted Next Day : ₹{next_day_price:.2f}")
print(f"Expected Change : {change_pct:+.2f}%")
Bugs Fixed Versus the Original
Bug | Original Code | Fixed Code |
|---|---|---|
Features engineered but never used | Only | All 15 features in |
Train/test split misalignment |
|
|
Inverse-transform for multivariate scaler |
|
|
Inconsistent date range | Step-by-step used | Unified to |
MAPE on 2-D arrays |
| Both are now 1-D via |
MultiIndex columns | ✅ Already correct | Retained |
Data leakage | ✅ Already correct | Retained |
RSI division-by-zero guard | ✅ Already correct | Retained |
Limitations and Further Improvements
Limitation | Suggested Improvement |
|---|---|
No macro context | Include Nifty 50 index, USD/INR rate, RBI repo rate |
No news sentiment | Scrape NSE announcements or use FinBERT on headlines |
Only LSTM | Compare with Transformer, XGBoost on features, or Prophet |
No trading costs | Account for STT, brokerage, and slippage in any backtest |
No naïve baseline | Always compare MAPE against a model that predicts "tomorrow = today" |
Fixed sequence length | Use attention mechanisms to learn optimal lookback dynamically |
Conclusion
We built a corrected, end-to-end multivariate Machine Learning pipeline to predict Indian stock prices:
yfinance with
auto_adjust=Trueand MultiIndex flattening for reliable NSE/BSE data15-feature input — Close, OHLCV, RSI, MACD, Bollinger Bands, volatility, moving averages — all actually fed into the model
scikit-learn
MinMaxScalerfitted on training rows only, with a correct multivariate inverse-transform helperStacked LSTM with Dropout, EarlyStopping, and ReduceLROnPlateau
Train/test split applied correctly to the sequence arrays, not to the raw DataFrame
scikit-learn metrics (MAE, RMSE, MAPE) computed on properly flattened 1-D arrays
Happy coding and happy investing! 🇮🇳
Tested with: Python 3.11 · TensorFlow 2.x · yfinance 0.2.x · scikit-learn 1.x · pandas 2.x
Full Runnable Script
# ============================================================
# Indian Stock Market Price Prediction using Machine Learning
# ML stack: scikit-learn (preprocessing + metrics)
# TensorFlow/Keras LSTM (deep learning model)
# Works with: Python 3.11 | yfinance 0.2.x | TensorFlow 2.x
# Install: pip install yfinance pandas numpy matplotlib
# scikit-learn tensorflow
# ============================================================
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
# ── CONFIG ────────────────────────────────────────────────────────────────────
TICKER = "RELIANCE.NS" # NSE ticker — try TCS.NS, HDFCBANK.NS, INFY.NS
START_DATE = "2019-01-01"
END_DATE = "2024-12-31"
SEQUENCE_LENGTH = 60 # lookback window: 60 trading days ≈ 3 months
TRAIN_RATIO = 0.80 # 80 % train, 20 % test
EPOCHS = 100
BATCH_SIZE = 32
# All feature columns fed into the LSTM
FEATURE_COLS = [
"Close", "Open", "High", "Low", "Volume",
"Return", "Volatility", "RSI",
"BB_Mid", "BB_Upper", "BB_Lower",
"MACD", "Signal", "MA50", "MA200"
]
N_FEATURES = len(FEATURE_COLS) # 15
CLOSE_IDX = FEATURE_COLS.index("Close") # 0
# ── STEP 1: FETCH DATA ────────────────────────────────────────────────────────
print(f"\n[1/8] Downloading data for {TICKER} ...")
df = yf.download(TICKER, start=START_DATE, end=END_DATE, auto_adjust=True)
# Flatten MultiIndex columns (yfinance >= 0.2 returns nested columns)
df.columns = df.columns.get_level_values(0)
df = df[["Open", "High", "Low", "Close", "Volume"]].dropna()
print(f" {len(df)} trading days fetched ({START_DATE} → {END_DATE})")
# ── STEP 2: VISUALISE RAW PRICE ───────────────────────────────────────────────
print("\n[2/8] Plotting closing price with moving averages ...")
ma50 = df["Close"].rolling(50).mean()
ma200 = df["Close"].rolling(200).mean()
plt.figure(figsize=(14, 5))
plt.plot(df["Close"], label="Close Price", linewidth=1.5, color="steelblue")
plt.plot(ma50, label="50-Day MA", linewidth=1.2, linestyle="--", color="orange")
plt.plot(ma200, label="200-Day MA", linewidth=1.2, linestyle=":", color="green")
plt.title(f"{TICKER} — Closing Price with Moving Averages")
plt.xlabel("Date")
plt.ylabel("Price (INR ₹)")
plt.legend()
plt.tight_layout()
plt.show()
# ── STEP 3: FEATURE ENGINEERING ───────────────────────────────────────────────
print("\n[3/8] Engineering technical indicators ...")
def add_features(df):
df = df.copy()
df["Return"] = df["Close"].pct_change()
df["Volatility"] = df["Return"].rolling(10).std()
delta = df["Close"].diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / (loss + 1e-9)
df["RSI"] = 100 - (100 / (1 + rs))
df["BB_Mid"] = df["Close"].rolling(20).mean()
df["BB_Upper"] = df["BB_Mid"] + 2 * df["Close"].rolling(20).std()
df["BB_Lower"] = df["BB_Mid"] - 2 * df["Close"].rolling(20).std()
ema12 = df["Close"].ewm(span=12, adjust=False).mean()
ema26 = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = ema12 - ema26
df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()
df["MA50"] = df["Close"].rolling(50).mean()
df["MA200"] = df["Close"].rolling(200).mean()
return df.dropna()
df = add_features(df)
print(f" {len(df)} rows after feature engineering and dropna")
# ── STEP 4: SCALE + CREATE SEQUENCES ──────────────────────────────────────────
print("\n[4/8] Scaling data and creating sequences ...")
# Fit scaler only on the first TRAIN_RATIO portion of df to prevent leakage
fit_end = int(len(df) * TRAIN_RATIO)
scaler = MinMaxScaler()
scaler.fit(df[FEATURE_COLS].iloc[:fit_end])
scaled_data = scaler.transform(df[FEATURE_COLS])
def create_sequences(data, seq_len, close_idx):
"""
data : 2-D scaled array (n_rows, n_features)
seq_len : past timesteps used as input
close_idx : column index of Close — used as the prediction target
"""
X, y = [], []
for i in range(seq_len, len(data)):
X.append(data[i - seq_len:i, :]) # all features
y.append(data[i, close_idx]) # next-day Close
return np.array(X), np.array(y)
X, y = create_sequences(scaled_data, SEQUENCE_LENGTH, CLOSE_IDX)
# Split MUST be applied to X/y (not df) because create_sequences
# reduces row count by SEQUENCE_LENGTH
split = int(len(X) * TRAIN_RATIO)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# Shape: (samples, timesteps, features) — already correct, no reshape needed
print(f" Train: {X_train.shape} | Test: {X_test.shape}")
# ── STEP 5: BUILD MODEL ────────────────────────────────────────────────────────
print("\n[5/8] Building LSTM model ...")
model = Sequential([
LSTM(64, return_sequences=True, input_shape=(SEQUENCE_LENGTH, N_FEATURES)),
Dropout(0.2),
LSTM(64, return_sequences=False),
Dropout(0.2),
Dense(32, activation="relu"),
Dense(1)
])
model.compile(optimizer="adam", loss="mean_squared_error")
model.summary()
# ── STEP 6: TRAIN ─────────────────────────────────────────────────────────────
print("\n[6/8] Training model ...")
early_stop = EarlyStopping(
monitor="val_loss", patience=10,
restore_best_weights=True, verbose=1
)
reduce_lr = ReduceLROnPlateau(
monitor="val_loss", factor=0.5,
patience=5, min_lr=1e-6, verbose=1
)
history = model.fit(
X_train, y_train,
epochs = EPOCHS,
batch_size = BATCH_SIZE,
validation_split = 0.10,
callbacks = [early_stop, reduce_lr],
verbose = 1
)
plt.figure(figsize=(10, 4))
plt.plot(history.history["loss"], label="Train Loss")
plt.plot(history.history["val_loss"], label="Validation Loss")
plt.title("Model Loss Over Epochs")
plt.xlabel("Epoch")
plt.ylabel("MSE Loss")
plt.legend()
plt.tight_layout()
plt.show()
# ── STEP 7: EVALUATE ──────────────────────────────────────────────────────────
print("\n[7/8] Evaluating on test set ...")
def inverse_transform_close(scaler, values_1d, n_features, close_idx):
"""
Inverse-transform a 1-D array of scaled Close values back to INR.
Reconstructs a full-width zero array so the multi-column scaler
can invert correctly.
"""
dummy = np.zeros((len(values_1d), n_features))
dummy[:, close_idx] = values_1d
return scaler.inverse_transform(dummy)[:, close_idx]
y_pred_scaled = model.predict(X_test).flatten()
y_pred = inverse_transform_close(scaler, y_pred_scaled, N_FEATURES, CLOSE_IDX)
y_true = inverse_transform_close(scaler, y_test, N_FEATURES, CLOSE_IDX)
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
print(f"\n MAE : ₹{mae:.2f}")
print(f" RMSE : ₹{rmse:.2f}")
print(f" MAPE : {mape:.2f}%")
plt.figure(figsize=(14, 5))
plt.plot(y_true, label="Actual Price (₹)", linewidth=1.5, color="steelblue")
plt.plot(y_pred, label="Predicted Price (₹)", linewidth=1.5, color="tomato", linestyle="--")
plt.title(f"{TICKER} — Actual vs Predicted Closing Price (Test Set)")
plt.xlabel("Trading Days (Test Period)")
plt.ylabel("Price (INR ₹)")
plt.legend()
plt.tight_layout()
plt.show()
# ── STEP 8: PREDICT NEXT TRADING DAY ─────────────────────────────────────────
print("\n[8/8] Predicting next trading day ...")
def predict_next_day(model, df, scaler, feature_cols, n_features, close_idx, seq_len=60):
"""Returns the predicted closing price (INR) for the next trading day."""
recent_scaled = scaler.transform(df[feature_cols].iloc[-seq_len:])
X_input = recent_scaled.reshape(1, seq_len, n_features)
pred_scaled = model.predict(X_input).flatten()
return inverse_transform_close(scaler, pred_scaled, n_features, close_idx)[0]
next_day_price = predict_next_day(model, df, scaler, FEATURE_COLS, N_FEATURES, CLOSE_IDX)
last_price = float(df["Close"].iloc[-1])
change_pct = ((next_day_price - last_price) / last_price) * 100
print(f"\n Ticker : {TICKER}")
print(f" Last Closing Price : ₹{last_price:.2f}")
print(f" Predicted Next Day : ₹{next_day_price:.2f}")
print(f" Expected Change : {change_pct:+.2f}%")
print("\n✅ Done!")
To try a different stock, change the
TICKERvariable at the top of the CONFIG section. All NSE-listed stocks follow the<SYMBOL>.NSformat. BSE-listed stocks use<SYMBOL>.BOinstead.
Comments (0)
Login to post a comment.