{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"predicting-indian-stock-market-prices-using-machine-learning-and-lstm-8fz9g","url":"https://zyvop.com/predicting-indian-stock-market-prices-using-machine-learning-and-lstm-8fz9g","title":"Predicting Indian Stock Market Prices Using Machine Learning and LSTM","subtitle":"A practical guide to building an ML-powered stock price prediction model for NSE/BSE listed stocks","tldr":"Predict Indian stock prices (NSE/BSE) using Python &amp; LSTM — covers live data fetching with yfinance, RSI/MACD feature engineering, leak-free model training, and next-day INR price prediction. Includes a full paste-and-run script.","keywords":["machine learning","Python","StockMarket","DataScience","LSTM","Tutorial"],"entities":["Samod Alex","machine learning","Python","StockMarket","DataScience","LSTM","Tutorial","ZyVOP"],"keyTakeaways":["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."],"headings":["Introduction","The ML Stack at a Glance","Prerequisites","1. Check Your Python Version","2. Create a Virtual Environment","3. Install Required Libraries","Step 1 — Fetch Indian Stock Data","Step 2 — Exploratory Data Analysis","Step 3 — Feature Engineering","Define the feature columns","Step 4 — Prepare Sequences for LSTM","Step 5 — Build the LSTM Model","Step 6 — Train the Model","Step 7 — Evaluate and Visualise Predictions","Step 8 — Predict the Next Trading Day","Bugs Fixed Versus the Original","Limitations and Further Improvements","Conclusion","Full Runnable Script"],"outboundLinks":[],"contentText":"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 MinMaxScaler Normalises all features to [0, 1] so the neural network trains stably Model TensorFlow / Keras LSTM Deep learning model that learns temporal patterns in multivariate sequences Regularisation Keras Dropout ML technique to prevent the model from overfitting to training data Training control Keras EarlyStopping, ReduceLROnPlateau Stops training when the model stops improving; adjusts learning rate Evaluation scikit-learn MAE, RMSE 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=True and flattening the MultiIndex are essential fixes for compatibility with yfinance &gt;= 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: ReduceLROnPlateau is 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 df[[\"Close\"]] passed to scaler and sequences despite computing RSI, MACD, etc. All 15 features in FEATURE_COLS are scaled and fed into the LSTM Train/test split misalignment split applied to df index, but X/y are shorter than df by SEQUENCE_LENGTH rows split = int(len(X) * TRAIN_RATIO) applied to the sequence arrays after creation Inverse-transform for multivariate scaler scaler.inverse_transform called with a single-column array on a multi-column scaler inverse_transform_close() reconstructs a full-width zero array, places Close values in the correct column, then inverse-transforms Inconsistent date range Step-by-step used 2021–2024; runnable script used 2019–2024 Unified to 2019–2024 throughout MAPE on 2-D arrays y_true and y_pred were 2-D (n, 1) arrays Both are now 1-D via .flatten() inside inverse_transform_close 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=True and MultiIndex flattening for reliable NSE/BSE data 15-feature input — Close, OHLCV, RSI, MACD, Bollinger Bands, volatility, moving averages — all actually fed into the model scikit-learn MinMaxScaler fitted on training rows only, with a correct multivariate inverse-transform helper Stacked 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 &gt;= 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 TICKER variable at the top of the CONFIG section. All NSE-listed stocks follow the &lt;SYMBOL&gt;.NS format. BSE-listed stocks use &lt;SYMBOL&gt;.BO instead.","contentHash":"sha256:582e292a3296d2eebd7525d9082281cdfeab42827b2f35d83abc1b2d57a6c8c2","authorName":"Samod Alex","authorUrl":"https://zyvop.com/author/samod","authorSameAs":[],"category":"Tutorial","tags":["machine learning","Python","StockMarket","DataScience","LSTM"],"audience":"Developers, software engineers, and students learning Tutorial","tone":"Instructional, practical, code-first","readingTimeMinutes":15,"wordCount":3361,"faqs":null,"primaryTopic":"Tutorial","publishedAt":"2026-08-05T14:41:55.917Z","updatedAt":"2026-08-23T18:15:00.206Z","canonicalUrl":"https://zyvop.com/predicting-indian-stock-market-prices-using-machine-learning-and-lstm-8fz9g"}