I Rebuilt My Weekly Stock Picker with LightGBM and Yahoo Finance
A reproducible guide to training LightGBM on Yahoo Finance data, ranking stocks before each week, buying the first session, and exiting the last.
Approximately 11 min read
A while ago I built a small quantitative-trading experiment that I later stopped using. I remembered the architecture much more clearly than the original code: historical Yahoo Finance data, a model whose name I remembered as something like “BGM,” and a simple weekly trading rule — generate the picks before the week starts, buy on Monday, and sell on Friday.
The model name was almost certainly LightGBM, usually used through the LGBMRegressor or LGBMClassifier Python classes.
I could not recover the exact original source file, ticker universe, feature list, hyperparameters, or historical performance numbers. Rather than invent those details, I rebuilt the tool from the architecture I actually remembered and fixed several things that matter in a financial backtest, especially look-ahead bias and time-series validation.
The result is a compact weekly stock-ranking system that is useful as a machine-learning exercise even if you never trade it with real money.
This article documents that reconstruction.
This is a research and educational project, not investment advice. The code does not connect to a broker or place orders.
What the reconstructed tool does
The workflow is:
Yahoo Finance daily OHLCV data
↓
features known before the week begins
↓
LightGBM regression model
↓
predict next week's return for each stock
↓
rank the stock universe
↓
buy the top-ranked names at the first session's open
↓
hold for the week
↓
exit at the last session's close
“Monday to Friday” is the easy way to describe the idea, but the code uses the first and last trading sessions of each calendar week. That matters during holiday weeks when Monday or Friday is not a trading day.
The prediction target is:
weekly return = last session Close / first session Open - 1
For a normal week, that is approximately Monday open to Friday close.
The most important rule: features must come from the previous week
It is very easy to create an impressive-looking trading model accidentally.
Suppose I want to buy at Monday’s open. If I calculate a feature using Monday’s closing price, Monday’s volume, or anything else that did not exist at the time of entry, the backtest is already contaminated.
So each training row follows this timeline:
Previous Friday close
│
├── calculate features
│
▼
Next week's first session open
│
├── simulated entry
│
▼
Next week's last session close
│
└── training target
The model never gets the entry week’s price action as an input feature.
That one detail is more important than squeezing another decimal point out of model accuracy.
Why I used LightGBM
This problem is ordinary tabular machine learning rather than a language-model problem.
For every stock-week pair I have a row of numeric features such as recent returns, volatility, moving-average distance, RSI, volume behavior, and broad-market conditions. LightGBM is a natural fit for this kind of dataset because gradient-boosted decision trees can model nonlinear interactions without requiring a huge dataset or a GPU-heavy neural network.
I reconstructed the project as a regression problem:
X = features available before the week
Y = return from first-session open to last-session close
The model predicts an expected weekly return. I then rank the stocks by that prediction.
A classifier version would also be reasonable:
Y = 1 if next week's return > 0
Y = 0 otherwise
But regression is more useful for ranking because a predicted return of +3% should normally rank above +0.2%.
Install the environment
A minimal environment is enough:
python -m venv .venv
source .venv/bin/activate
pip install yfinance pandas numpy lightgbm
The current yfinance API supports adjusted daily OHLC data. In the reconstruction I explicitly set:
auto_adjust=True
rather than relying on whichever default a particular package version happens to use.
The yfinance project also states that it is an independent open-source tool using Yahoo’s public interfaces and is intended for research and educational use. I would not treat it as an institutional execution-quality market-data feed.
Pick a stock universe
For a demo I use ten familiar US stocks:
TICKERS = [
"AAPL",
"MSFT",
"NVDA",
"AMZN",
"GOOGL",
"META",
"JPM",
"XOM",
"COST",
"UNH",
]
This is not claimed to be my original universe. That part of the old project was not recovered.
It also creates an important research problem: survivorship bias.
If I choose today’s successful companies and then backtest them ten years into the past, I am giving the model information that a historical investor did not have — namely, which companies survived and remained important enough to be in my current list.
For serious research, the universe itself should be point-in-time data.
For a tutorial, a fixed list keeps the mechanics understandable.
Download daily data
I download one ticker at a time and keep adjusted OHLCV bars:
import yfinance as yf
import pandas as pd
START = "2015-01-01"
def download_daily(ticker):
df = yf.Ticker(ticker).history(
start=START,
interval="1d",
auto_adjust=True,
actions=False,
)
df = df[["Open", "High", "Low", "Close", "Volume"]].dropna().copy()
df.index = pd.to_datetime(df.index).tz_localize(None)
return df.sort_index()
For a research pipeline I prefer storing the downloaded raw data locally rather than repeatedly requesting the same history while changing model code. That makes an experiment easier to reproduce later.
Create the features
The reconstructed feature set is intentionally ordinary:
1-day return
5-day return
20-day return
20-day realized volatility
price vs. 10-day moving average
price vs. 20-day moving average
volume / 20-day average volume
14-day RSI
SPY 5-day return
SPY 20-day return
SPY 20-day volatility
I do not claim these were the exact features in the original tool. They are a conservative reconstruction of the kind of features I was using for a weekly model.
The code looks like this:
def add_features(df):
out = df.copy()
close = out["Close"]
out["ret_1d"] = close.pct_change(1)
out["ret_5d"] = close.pct_change(5)
out["ret_20d"] = close.pct_change(20)
out["vol_20d"] = out["ret_1d"].rolling(20).std()
sma10 = close.rolling(10).mean()
sma20 = close.rolling(20).mean()
out["price_vs_sma10"] = close / sma10 - 1
out["price_vs_sma20"] = close / sma20 - 1
out["volume_ratio"] = out["Volume"] / out["Volume"].rolling(20).mean()
out["rsi_14"] = rsi(close, 14)
return out
I also add SPY features so the model can distinguish, for example, a stock rising during a strong market from a stock rising while the broad market is weak.
Turn daily prices into weekly supervised-learning rows
This was the part I cared most about getting right during the reconstruction.
For every ticker and every completed calendar week:
- Find the first trading session of the week.
- Find the last trading session of the week.
- Find the final available feature row before the first session.
- Use that previous row as
X. - Compute the week’s first-open-to-last-close return as
Y.
Conceptually the resulting table looks like this:
| ticker | signal date | entry | exit | ret_5d | RSI | SPY ret | target |
|---|---|---|---|---|---|---|---|
| AAPL | prior Friday | Monday | Friday | … | … | … | future weekly return |
| MSFT | prior Friday | Monday | Friday | … | … | … | future weekly return |
| NVDA | prior Friday | Monday | Friday | … | … | … | future weekly return |
The critical line in the code is effectively:
prior = df.loc[df.index < entry_date]
feature_row = prior.iloc[-1]
The model sees only information that existed before the simulated trade.
Train the LightGBM model
The reconstructed model is deliberately modest:
import lightgbm as lgb
def make_model():
return lgb.LGBMRegressor(
objective="regression",
n_estimators=300,
learning_rate=0.03,
num_leaves=31,
min_child_samples=30,
subsample=0.8,
subsample_freq=1,
colsample_bytree=0.8,
reg_lambda=1.0,
random_state=42,
n_jobs=-1,
verbosity=-1,
)
These are not magic parameters and they are not presented as an optimized trading model.
A common mistake in quantitative ML is to spend enormous effort optimizing hyperparameters against one historical backtest. That can simply turn the backtest itself into the training set.
I would rather begin with a reasonable model and spend my effort making the validation procedure harder to fool.
Do not randomly split financial time series
For ordinary machine learning it is common to call a random train_test_split.
I do not do that here.
If a row from 2025 is used for training while a row from 2022 is treated as a test observation, the experiment no longer matches the way the model would have been deployed.
Instead I use an expanding walk-forward test:
train through week 156 → predict week 157
train through week 157 → predict week 158
train through week 158 → predict week 159
...
The implementation is straightforward:
for test_week in weeks[MIN_TRAIN_WEEKS:]:
train = samples[samples["week"] < test_week]
test = samples[samples["week"] == test_week].copy()
model = make_model()
model.fit(train[FEATURES], train["target_return"])
test["predicted_return"] = model.predict(test[FEATURES])
Every reported prediction is therefore made by a model that was trained only on earlier weeks.
That does not eliminate overfitting, but it removes one of the easiest ways to fool myself.
Convert predictions into the Monday-to-Friday strategy
After the model predicts every stock in a test week, I rank them:
picks = group.sort_values(
"predicted_return",
ascending=False,
).head(TOP_N)
The reconstruction uses three rules:
choose the top 3 predictions
only buy names with predicted return > 0
equal-weight the selected names
If no stock has a positive prediction, the simulated portfolio holds cash for that week.
I also subtract an example round-trip trading cost:
ROUND_TRIP_COST = 0.002
That represents 20 basis points for the complete entry-and-exit cycle. It is only a configurable research assumption, not a statement about anyone’s actual brokerage costs.
A backtest with zero friction is especially misleading for a strategy that turns over its entire portfolio every week.
What I measure
The script reports:
number of out-of-sample weeks
final equity multiple
CAGR
annualized Sharpe ratio
maximum drawdown
weekly hit rate
I intentionally do not publish a claimed return for this reconstructed version in this guide.
Why?
Because a number is meaningful only after fixing the exact universe, data snapshot, transaction-cost assumptions, training window, delisting treatment, benchmark, and model version. Changing those can materially change the result.
The guide is about building a defensible experiment, not advertising a profitable backtest.
Generate the next week’s ranking
After the walk-forward evaluation, the script can train on all completed historical samples and rank the current demo universe:
Ticker Signal date Predicted weekly return
------ ----------- -----------------------
... ... ...
The intended operating schedule is:
Friday after market close / weekend
↓
refresh completed daily data
↓
calculate features
↓
fit model on completed historical weeks
↓
rank stocks
↓
first session of next week: simulated entry
↓
last session of week: simulated exit
I would not run this code Monday afternoon and pretend the newest Monday data had been available before Monday’s open.
Timing is part of the model.
Complete reconstructed script
I put the full single-file reconstruction here:
https://ramgpt.org/code/weekly-lightgbm-stock-picker.py
The script includes data download, feature engineering, weekly label construction, LightGBM training, expanding-window testing, portfolio ranking, transaction-cost adjustment, performance metrics, and next-week ranking.
The important point is that it is a reconstruction, not a claim that I recovered the exact old file byte-for-byte.
What I can confidently recover from the original idea is the architecture:
Yahoo Finance data
+
LightGBM
+
weekly prediction
+
Monday-to-Friday holding period
+
model-generated stock ranking
The exact original feature set and tuning are no longer something I can verify, so I would rather document that uncertainty than fabricate precision.
What I would improve before taking the backtest seriously
The reconstructed tool is enough to learn from, but several issues remain before I would treat its results as evidence of a real trading edge.
Point-in-time universe
A current ticker list creates survivorship bias. A stronger experiment needs the constituents that were actually investable at each historical date, including companies that later disappeared.
Better transaction-cost model
A fixed 20 bps assumption is convenient, but real friction depends on spread, liquidity, volatility, order size, broker fees, and execution method.
Benchmarking
The strategy should be compared against at least a broad-market benchmark and a simpler non-ML rule. If LightGBM cannot beat a simple momentum or equal-weight baseline after costs, the model complexity may not be earning its keep.
Feature stability
Feature importance should be checked across different training periods. A feature that looks powerful only in one market regime is not necessarily a durable signal.
Parameter discipline
Repeatedly changing the ticker list, target, cost assumption, and model parameters until the equity curve looks good is another form of training on the test set.
I would keep a final untouched evaluation period or move to paper trading before drawing conclusions.
Data quality
yfinance is excellent for experimentation, but research code should still validate missing bars, adjusted prices, corporate actions, delistings, and suspicious values rather than assuming every downloaded row is perfect.
What I learned rebuilding it
The interesting part of this project was not LightGBM itself.
The model is one line compared with all the decisions around it.
The real system is:
when is information available?
what exactly is the target?
what universe existed at that time?
how is the test period isolated?
what does a simulated trade cost?
when is the model retrained?
what benchmark must it beat?
That is why a quantitative-trading project is a useful machine-learning exercise even when it never becomes a live strategy.
A sophisticated model with a contaminated backtest is less useful than a simple model with a clean timeline.
The reconstructed version keeps the original idea I remember — train on Yahoo Finance history, use LightGBM to rank stocks, enter at the beginning of the week, and exit at the end — but puts the time boundaries front and center.
That is the version I would want to rebuild today.