Most machine learning models act like black boxes—you feed in indicators, and they spit out a prediction. But when the forecast is off, you’re left guessing: Was it the EMA? Or the ATR?
SHAP, short for SHapley Additive exPlanations, helps bring clarity. Instead of treating model decisions as opaque, SHAP explains precisely how each feature pushed a forecast up or down—both at a global level and for each individual bar. Think of it as a way to reverse-engineer the model’s reasoning, using math grounded in game theory.
This approach is especially useful for traders, where we need to not just build models, but also interpret, explain, and understand them
The Model: Predicting NIFTY-50 with CatBoost
We used CatBoost, a gradient boosting library that’s fast, robust, and relatively easy to tune, to forecast the daily close of the NIFTY-50 index. The dataset consisted of 10 years of historical OHLCV data pulled via yfinance, and the features were built entirely from price—nothing fundamental or macroeconomic.

Feature engineering included a mix of standard indicators from TA-Lib like ATR and EMA, along with custom Hull moving averages (HMA) computed using only TA-Lib’s EMA function. We ended up with eleven features: two ATRs, three EMAs, two return-based features, and three HMAs. These were chosen based on relevance to short-term momentum, trend strength, and volatility.
What We Did: NIFTY-50 Forecasting with CatBoost
We trained a CatBoostRegressor to forecast the daily close of NIFTY-50 using 10 years of price data and 11 features—ATR, EMAs, returns, and Hull-EMAs.
| Step | What we did |
|---|---|
| 1 | Pulled 10y of NIFTY data via yfinance |
| 2 | Engineered features using TA-Lib and custom Hull-EMAs |
| 3 | Trained on all but last 180 days |
| 4 | Did a recursive 30-day forecast |
| 5 | Ran SHAP analysis on the test and forecast window |
Install the Python Libraries
pip install yfinance catboost shap seaborn matplotlib scikit-learn pandas numpy
All libraries except ta-lib can be installed via pip directly. But TA-Lib requires a precompiled .whl (wheel) file on Windows.
Check Your Python Version and Windows Architecture
Open Command Prompt or PowerShell in your virtual environment and run:
python --version
python -c "import platform; print(platform.architecture())"
Example output:
Python 3.12.8
('64bit', 'WindowsPE')
Install TA-Lib (Special Handling for Windows)
TA-Lib depends on a C library that’s not available as a regular pip package on Windows. You need to install a precompiled wheel that matches your Python version.
Where to get .whl for Windows?
Get it from this trusted community build:
https://github.com/cgohlke/talib-build/releases
Install the TA-Lib .whl File
Assuming you downloaded ta_lib‑0.6.3‑cp312‑cp312‑win_amd64.whl into your current folder:
pip install .\ta_lib-0.6.3-cp312-cp312-win_amd64.whl
If successful, you’ll see:
Successfully installed ta-lib-0.6.3
Python Code for CatBoost Prediction
# ============================================================
# 1. Imports & warning filters
# ============================================================
import warnings, yfinance as yf, talib as ta, pandas as pd, numpy as np
from catboost import CatBoostRegressor, Pool
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import seaborn as sns, matplotlib.pyplot as plt, shap
warnings.filterwarnings("ignore")
# ============================================================
# 2. Hull-EMA helper (pure TA-Lib)
# ============================================================
def talib_hull_ema(price: pd.Series, n: int) -> pd.Series:
"""Hull-style EMA (uses only TA-Lib EMA calls)."""
x = price.values.astype("double")
e1 = ta.EMA(x, timeperiod=n)
e2 = ta.EMA(x, timeperiod=n//2)
diff = 2 * e2 - e1
h = ta.EMA(diff, timeperiod=int(np.sqrt(n)))
return pd.Series(h, index=price.index)
# ============================================================
# 3. Download 10 y of NIFTY-50 daily data
# ============================================================
df = yf.download("^NSEI", period="10y", interval="1d", auto_adjust=True)
df.columns = df.columns.map(lambda c: c[0].lower() if isinstance(c, tuple) else c)
df = df[["open", "high", "low", "close", "volume"]]
# ------------------------------------------------------------
# 3-B. Feature engineering (8 originals + 3 Hull-EMAs)
# ------------------------------------------------------------
df["atr5"] = ta.ATR(df["high"], df["low"], df["close"], 5)
df["atr10"] = ta.ATR(df["high"], df["low"], df["close"], 10)
df["ema5"] = ta.EMA(df["close"], 5)
df["ema10"] = ta.EMA(df["close"], 10)
df["ema20"] = ta.EMA(df["close"], 20)
df["ret1"] = df["close"].pct_change()
df["ret5"] = df["close"].pct_change(5)
df["hma7"] = talib_hull_ema(df["close"], 7)
df["hma10"] = talib_hull_ema(df["close"], 10)
df["hma14"] = talib_hull_ema(df["close"], 14)
df = df.dropna()
frozen_feats = ["atr5", "atr10"] # need high/low in future
rolling_feats = ["ema5", "ema10", "ema20",
"ret1", "ret5",
"hma7", "hma10", "hma14"]
features = frozen_feats + rolling_feats
target = "close"
# ============================================================
# 4. Train / test split & CatBoost fit
# ============================================================
train, test = df.iloc[:-180], df.iloc[-180:]
model = CatBoostRegressor(
loss_function="RMSE",
iterations=1200,
depth=7,
learning_rate=0.03,
subsample=0.8,
rsm=0.8,
l2_leaf_reg=3,
random_seed=42,
verbose=False
)
model.fit(Pool(train[features], train[target]),
eval_set=Pool(test[features], test[target]),
use_best_model=True)
def metrics(act, pred, tag):
mae = mean_absolute_error(act, pred)
rmse = np.sqrt(mean_squared_error(act, pred))
mape = np.mean(np.abs((act - pred) / act)) * 100
r2 = r2_score(act, pred)
print(f"{tag:5} MAE:{mae:8.2f} RMSE:{rmse:8.2f} MAPE:{mape:5.2f}% R²:{r2:6.3f}")
metrics(train[target], model.predict(train[features]), "Train")
metrics(test [target], model.predict(test [features]), "Test ")
# ============================================================
# 5. Recursive 30-day close forecast
# ============================================================
future_days = 30
hist_close = df["close"].copy()
# freeze ATRs for lack of future high/low
atr5_last, atr10_last = df["atr5"].iloc[-1], df["atr10"].iloc[-1]
future_rows = []
for ts in pd.bdate_range(df.index[-1] + pd.Timedelta(days=1),
periods=future_days, freq="C"):
# rolling-only indicators
ema5 = ta.EMA(hist_close.values, 5)[-1]
ema10 = ta.EMA(hist_close.values, 10)[-1]
ema20 = ta.EMA(hist_close.values, 20)[-1]
hma7 = talib_hull_ema(hist_close, 7).iloc[-1]
hma10 = talib_hull_ema(hist_close, 10).iloc[-1]
hma14 = talib_hull_ema(hist_close, 14).iloc[-1]
ret1 = hist_close.pct_change().iloc[-1]
ret5 = hist_close.pct_change(5).iloc[-1]
X = pd.DataFrame({
"atr5":[atr5_last], "atr10":[atr10_last],
"ema5":[ema5], "ema10":[ema10], "ema20":[ema20],
"ret1":[ret1], "ret5":[ret5],
"hma7":[hma7], "hma10":[hma10], "hma14":[hma14]
}, index=[ts])
pred_close = model.predict(X)[0]
X["close"] = pred_close
future_rows.append(X)
# roll forward
hist_close = pd.concat([hist_close, pd.Series(pred_close, index=[ts])])
future_df = pd.concat(future_rows)
# ============================================================
# 6. Headline forecasts
# ============================================================
print("\nHeadline forecasts (close)")
print(f"Next 1 d : {future_df['close'].iloc[0]:,.2f}")
print(f"Next 5 d : {future_df['close'].iloc[4]:,.2f}")
print(f"Next 30 d: {future_df['close'].iloc[-1]:,.2f}")
# ============================================================
# 7. Plot – last 3 months + 30-day forecast
# ============================================================
plot_df = pd.concat([df.tail(63)[["close"]], future_df[["close"]]])
plot_df["dataset"] = ["Actual"]*63 + ["Forecast"]*30
sns.set_style("whitegrid")
plt.figure(figsize=(12,6))
sns.lineplot(data=plot_df, x=plot_df.index, y="close", hue="dataset")
plt.title("NIFTY-50 – last 3 months & 30-day CatBoost close forecast")
plt.xticks(rotation=15); plt.ylabel("Close"); plt.tight_layout(); plt.show()
# ============================================================
# 8. SHAP explainability (optional)
# ============================================================
shap.initjs() # for interactive plots in Jupyter/Colab
# ---- build the explainer on the trained model ----
explainer = shap.TreeExplainer(model)
# ---- SHAP for the test window (hold-out history) ----
shap_values_test = explainer.shap_values(test[features])
# ---- SHAP for the 30-day forecast ----
shap_values_future = explainer.shap_values(future_df[features])
# 8-A Global importance – mean(|SHAP|)
shap.summary_plot(shap_values_test, test[features], show=False, plot_type="bar", max_display=10)
plt.title("Global feature importance – test window")
plt.show()
# 8-B Beeswarm for full distribution
shap.summary_plot(shap_values_test, test[features], show=False)
plt.title("SHAP beeswarm – test window")
plt.show()
The model was trained on the full dataset, leaving the last 180 business days as the test set. Once fitted, we used it to generate a 30-day recursive forecast, where each day’s prediction was fed back into the model as input for the next.
Despite being a single-output model (predicting just the close), CatBoost performed reasonably well: R² of ~0.79 on the test set.
What SHAP Revealed
To evaluate what was driving the model’s predictions, we used SHAP analysis. First, we generated global feature importance by averaging the absolute SHAP values across all test points. This ranked features by how much they influenced predictions, regardless of whether they pushed values up or down.

The global bar plot showed that short-term Hull-EMAs (particularly the 14- and 7-period variants) were by far the most influential. Traditional indicators like ATR and returns played a minimal role. This aligned with trading intuition—short-term trend continuation matters more than raw volatility or noise in daily returns.

Next, the SHAP beeswarm plot offered a per-row view. For each candle, it showed how high or low values of an indicator affected the forecast. For instance, in the case of hma14, higher values (representing strong upward momentum) consistently pushed the forecast up, while lower values pushed it down. On the other hand, daily returns (ret1) clustered around zero SHAP values—indicating that the model effectively ignored them, viewing day-to-day price changes as noise.
SHAP also helped detect feature overlap. EMA10 and HMA10, for example, share structural similarities. If their SHAP distributions overlapped significantly, we’d know we’re double-counting a concept—and could prune one without loss of information.
The Limits of CatBoost in Market Forecasting
While tree models like CatBoost are powerful, they aren’t without issues. First, they assume that the future will behave like the past. Any regime shift—say, a change in SEBI margin rules, macroeconomic policy, or global liquidity—can severely degrade performance.
Second, they don’t inherently understand time. Unless lags or rolling features are engineered into the data, the model treats every row as independent and identically distributed. There’s no notion of sequence or memory. In contrast, sequence models like LSTMs can learn patterns over time natively.
CatBoost also uses internal quantization to bucket continuous features, which may smooth out subtle patterns that matter to a trader. For instance, a 0.5% difference in EMA crossover might disappear inside a bucket unless the model is finely tuned.
SHAP doesn’t solve these problems, but it makes them visible. If your model is overly dependent on one feature, SHAP will show you. If regime drift makes a previously reliable signal unstable, the SHAP values will fluctuate erratically across time.
Why Traders Should Pay Attention
Traders often lean on gut feel or backtested P&L to decide whether a model is working. SHAP provides a more rigorous way to evaluate what the model is doing and why. It helps you separate meaningful signals from statistical coincidences.
It can also reduce overfitting during the feature selection phase. Instead of blindly adding indicators, SHAP helps test whether a feature adds unique, consistent explanatory power.
Perhaps most importantly, SHAP gives you a way to explain your strategy. If a compliance team or investor asks, “Why did the model go long on this expiry day?”, you can show the SHAP breakdown with exact contributions—something traditional feature-importance measures can’t do.
Final Thoughts
Machine learning models can be useful in trading—but only if you trust what they’re doing. SHAP doesn’t make your model more accurate, but it makes it more transparent. That transparency, in turn, allows for better strategy refinement, safer deployment, and clearer communication with stakeholders.
In a world where regulatory oversight is increasing, clients demand transparency, and execution decisions must be defensible, SHAP is not just a tool—it’s a necessity.
The full notebook, including model training, forecasting, and SHAP visualization, is shared below. You can plug it into Colab or VS Code and run your own experiments.
Let the model speak—and more importantly, learn to listen.
hi sir where is full code please share the link ,, by the way very great article and thanks a lot …
It is full code