Skip to document
← StockPredictorAbdul Moiz / Engineering studies Download PDF
Contents 12 chapters + figures

A technical study of daily equity forecasting under strict evaluation

Forecast what the evidence allows.

How a free-data pipeline, purged walk-forward testing, pre-declared gates, and an immutable live forecast log separated what could be predicted from what could not, and turned the result into a daily forecasting service.

  • Machine learning
  • Time-series evaluation
  • LightGBM / FinBERT
  • Python / FastAPI
  • MLOps

Part I / The question

01

Ask what can actually be forecast.

The project began with a familiar goal: predict whether a stock will rise or fall. Instead of tuning toward a headline accuracy number, every model was held to a baseline it had to beat on data it had never influenced. Across twenty-four trained models, no daily direction forecast, next-day or five-day, cleared that bar. Two other targets did: how far a stock is likely to move, and which stocks are likely to beat the others over the next week.

Those two results became the product. After every trading session the system forecasts the next session's move size with a calibrated range for fifty large caps, ranks one hundred large and mid caps for the following five sessions, writes both as immutable files, and scores them once the outcomes exist. The up/down probability is shown as the historical base rate, labeled as such.

Published outputUniverseEvidence status
Expected absolute move, 80% band, P(big move)50 large capsPassed its gate in 6 of 6 walk-forward years
Five-session cross-sectional ranking50 large caps + 50 S&P MidCap 400 membersPassed its gate; strongest among mid caps
Next-session up probability50 large capsBase rate only; no model beat it
StockPredictor overview showing the average expected move for the next session and model status
The overview leads with the average expected move and states which forecasts passed their gates.

Part I / The question

02

Heavy compute in the pipeline, light serving at the edge.

Collection, feature building, training, FinBERT scoring, and inference need LightGBM, a transformer model, and the full dataset, so they run on GitHub Actions runners rather than in request-time functions. Vercel serves the results: a read-only FastAPI function and a static React dashboard. The private S3-compatible archive is the immutable record; PostgreSQL holds a query copy for the API.

Heavy work runs in the pipeline; Vercel only serves resultsStockPredictor / System map
Sources & archiveDaily GitHub Actions pipelineServing
Free data sourcesAlpaca bars + news · SEC XBRL · ALFRED · GDELT
Content-addressed archiveraw responses by SHA-256 · manifests · checkpoints
Prepared datasetsclean bars · features · labels · purged blocks
Refresh & featuresextract · prepare · news · FinBERT scores
Forecast, rank & scoretwo-head bundle · 5-day ranker · scorecards
Weekly retrainingchampion / challenger on 13 unseen weeks
Neon PostgreSQLappend-only forecasts · rankings · model events
FastAPI on Vercelread-only role · CDN-cached responses
React dashboardstatic build · bundled snapshot fallback
Training and inference need LightGBM, FinBERT, and the full dataset, so they run on Actions runners. The private S3 archive is the immutable record; PostgreSQL is the query copy the API reads.
LayerImplementationBoundary
Research & pipelinePython 3.13, pandas/PyArrow, LightGBM, scikit-learn, statsmodels; uv-locked dependenciesRuns locally or on Actions with the same commands
ArchiveNeon object storage over the S3 API; content-addressed keys and manifestsImmutable objects; only checkpoints are mutable
SentimentProsusAI/finbert at a pinned revision in an isolated environmentWeights verified by SHA-256 before use
DatabaseNeon PostgreSQL 17 with separate migrator, writer, and runtime rolesForecasts and rankings are append-only
API & dashboardFastAPI with psycopg; React, Vite, Framer Motion, hand-built SVG chartsRead-only; dashboard falls back to a bundled snapshot

Part II / Data you can audit

03

Free sources, archived before they are trusted.

The pipeline uses only free data. Every HTTP response, including failures, is wrapped in an envelope with its request, status, selected headers, and body, then stored under the SHA-256 of its content before any parsing happens. Query credentials and any echoed secrets are redacted from the archived bytes. Collectors are rate limited, honor Retry-After, bound response sizes, and resume from checkpoints that only advance after the remote write is confirmed.

SourceUsed forMeasured collection
Alpaca historical SIP barsDaily OHLCV for 50 stocks and SPY138,210 bars, 2016-01-04 to 2026-10-13, 2,710 sessions
Alpaca (Benzinga) newsTimestamped headlines for news and sentiment features242,087 articles, 2015 to 2026, all 50 stocks
SEC Company Facts & filingsEarnings dates, earnings surprise, filing events964,419 facts; 9,836 filing records
FRED / ALFREDMacro series as they were known at each date31,328 observation rows across six series
GDELT, company IR pagesEvent and announcement candidatesPartial; blocked sources recorded as gaps

Several sources failed in practice: GDELT article search returned HTTP 429 and tripped a circuit breaker, and nineteen investor-relations pages returned HTTP 403. Those failures are retained as missing coverage. They are never converted into zero-news features, because a source that failed is different from a day with no news.

Part II / Data you can audit

04

Outcomes never leak into inputs.

Cleaning works on the exchange calendar, not on calendar days. Of 138,210 bars, 138,202 are valid canonical rows with no missing sessions. Seven unsupported corporate-action or identity boundaries and one unexplained price jump are quarantined: their future-label intervals are excluded and feature history restarts after them. Range features also cap erroneous provider wicks, such as a pre-split print, at four times the stock's previous 22-session volatility.

Features use only information available by the session close plus thirty minutes. Labels are generated by separate code from future prices. Blocks are split on whole session dates so every stock for a day stays together, and examples whose label would reach into the next block are purged at each boundary.

BlockExamplesForecast sessionsPurged boundary rows
Train84,3632016-12-30 to 2023-10-06245
Validation12,0592023-10-16 to 2024-10-04245
Calibration12,1332024-10-14 to 2025-10-06250
Test11,9892025-10-14 to 2026-10-060

The first dataset carries 34 core features; the next-day work extends them to 191 price factors in the style of Alpha158. Later studies add volatility, earnings-event, earnings-surprise, news-volume, sentiment, cross-sectional, and slow-momentum groups, each built with the same cutoff rule and checked by tests that change only future rows and confirm features are unchanged.

Part III / What the models found

05

Direction did not move.

Direction was attacked from every reasonable angle: tree ensembles, linear and ARIMA baselines, recurrent and convolutional networks, a residual MLP, a 209M-parameter time-series foundation model, macro vintages, SEC fundamentals, historical headlines, FinBERT sentiment, and FinBERT fine-tuned on the headlines themselves. Each was compared on identical rows with a fixed selection rule.

StudyModel resultBaseline on the same rows
Five-session UP / FLAT / DOWNLightGBM 40.73% accuracyLogistic 41.49%; prior 40.28%
Next-session UP / FLAT / DOWNLightGBM 42.83%; LSTM 42.72%Always FLAT 43.51%
Daily CNN-LSTM43.39%, but 0% DOWN recallAlways FLAT 43.51%
Timestral-209M, zero-shot43.20% accuracyAlways FLAT 43.51%
Two-head direction, six yearsPooled AUC 0.498Training prior in 0 of 6 years
+ news, sentiment, earnings surprisePooled AUC 0.494Beat the prior in 1 of 6 years
FinBERT fine-tuned on headlinesDirection AUC 0.500Never beat the base rate

A diagnostic explains why this was expected. Even with perfect knowledge of the next day's market return, three-class accuracy on the 2025-2026 block would only reach 48.3%. Public headlines available at the close carry no measurable next-day direction signal for these large caps; the market has already priced them.

Part III / What the models found

06

Move size is forecastable.

The two-head design separates what is predictable from what is not. The magnitude head forecasts the expected absolute next-session return with LightGBM, an 80% quantile band with split-conformal scaling, and a calibrated big-move probability. The direction head chooses between the training prior, logistic regression, and LightGBM by inner-validation log loss. Three-class probabilities are derived from both heads for display only.

Test yearQLIKE (model)QLIKE (HAR)Rank correlation80% coverage
2020-21−7.326−7.2070.32586.1%
2021-22−6.829−6.6720.31378.8%
2022-23−7.349−7.1780.32982.0%
2023-24−7.501−7.1980.29678.8%
2024-25−6.986−6.8010.29780.6%
2025-26−6.986−6.7450.31278.7%
Pooled−7.162−6.9660.33180.8%

Lower QLIKE is better, and the model beats the HAR volatility baseline in every year. Sorting forecasts into quintiles gives realized average moves of 0.80%, 1.00%, 1.18%, 1.46%, and 2.31%, so the top quintile moves 2.9 times as much as the bottom. Earnings-date features account for most of the gain; earnings surprise, news volume, and FinBERT sentiment were each promoted only after improving on price-only in at least five of six years.

The production configuration keeps price plus the promoted groups, sets direction to the training prior, and was refit on all labeled history: QLIKE −7.155 against −6.966 for HAR, 80.6% band coverage, and a big-move AUC of 0.591. It scores marginally below the all-groups model because the promotion rule dropped a group that only helps in combination. The rule was not overridden after seeing those test years.

Part III / What the models found

07

Rank the week, not the day.

Ranking reframes direction as a relative question: which stocks will beat the others? LightGBM is trained on each row's percentile rank of forward return within its session, over 445 inputs that are rank-normalized per session. Evaluation uses daily Spearman rank IC with t-statistics on non-overlapping sessions, and a long-short portfolio of the top and bottom quintiles, net of costs.

HorizonMean rank ICYears IC > 0t (non-overlapping)Net Sharpe at 5 bpsGate
1 day+0.0226 of 64.620.04Passed, economically nil
5 days+0.0235 of 62.660.97 (0.51-1.01 by start day)Passed
20 days+0.0224 of 60.680.43Failed

At five sessions the model beats 12-1 month momentum, five-day reversal, and low-volatility baselines. Most of the edge comes from the mid-cap cohort, which also carries the most survivorship bias. The reported Sharpe of 0.97 came from the most favorable rebalance day; averaged across all five start days it is 0.73, so the honest reading is roughly 0.5 to 0.75, with rank IC as the more stable measure.

A GPU comparison later trained eight rankers, including XGBoost, CatBoost, LambdaRank, an MLP, and an ensemble, with the winner chosen in advance by inner-year IC. The pre-selected ensemble failed its gate, and the model with the best test IC had ranked second-last on the inner years. Promoting it would have meant choosing on test results, so the live ranker stayed unchanged.

Part IV / Evaluation discipline

08

Gates are set before the test is opened.

Every study uses expanding yearly walk-forward folds: six test years, each preceded by an inner-validation year used for early stopping, calibration, conformal scaling, and model choice. Training labels end before the inner year, inner labels end before the test year, and rows whose outcome would fall in the reserved prospective block are never touched.

Every model faces a frozen, purged evaluationStockPredictor / System map
Frozen inputsChronologyPre-declared gateOutcome
Checksummed datasetmanifest · recipe hash · code fingerprint
Purged chronologyfixed blocks or yearly folds · tuning before test
Gate vs baseline5 of 6 years and pooled · set before testing
Live or documentedpromoted bundle, or a failure report
Test years never choose features, settings, or thresholds. A model that only wins after looking at its test results is not promoted.
Twenty-four models were trained. Seventeen failed their baseline or gate, three are reference benchmarks, one was superseded, one passed in research, and two serve live forecasts.
GateRule
MagnitudeQLIKE below HAR in at least 5 of 6 folds and pooled; pooled 80% coverage within ±3 points
DirectionLog loss below the training prior in 5 of 6 folds and pooled AUC ≥ 0.52
Feature groupAdding the group improves on price-only in 5 of 6 folds and pooled
RankingPositive IC in 5 of 6 years, pooled t ≥ 2, and positive net Sharpe

The first trained bundle bound its final test archive to the exact recipe that selected it, so a new recipe could not reuse that holdout for another selection. Recipe hashes cover configuration, input checksums, and source files; an identical command reuses completed folds instead of refitting. A unit test scrambles every test-block label and confirms that predictions do not change.

Part V / The live system

09

One forecast per session, never revised.

A scheduled workflow runs at 22:45 UTC on weekdays, more than thirty minutes after the close in both EST and EDT. It pulls live state from the archive, refreshes bars, SEC filings, and news, scores only headlines that have no FinBERT score yet, rebuilds every feature group with the exact study code, and forecasts the newest session with the production bundle.

One immutable forecast per session, scored when the outcome existsStockPredictor / System map
After the closeBefore the openOutcome maturesEach week
Refresh inputsbars · SEC filings · news · new FinBERT scores
Forecast & logwrite <origin>.json once · archive collision check
Score matured rowsband coverage · rank IC · weekly portfolio
Challenger gatepromote only if it beats the comparator
The champion stays frozen between retrains, so every logged forecast is a clean test of the model that made it. Every evaluation, promoted or not, is appended to the model registry.
The daily run starts at 22:45 UTC on weekdays, more than 30 minutes after the close in both EST and EDT. A logged forecast can never be revised; a repeated run with different values fails.
StepGuarantee
Load the production bundleEvery LightGBM text model and parameter file is checked against its SHA-256 before loading
Write the forecast logOne file per origin session; a second run with different values fails instead of overwriting
Archive the logRemote keys are immutable; a changed forecast for an existing origin is rejected
Score matured rowsBand coverage, rank correlation, direction log loss and AUC, per origin and per model
Rank five sessions aheadOfficial weekly portfolio forms on each week's first session so results never overlap

Each forecast row records the expected absolute move, the 80% band, P(big move), P(up), derived class probabilities, origin and target sessions, the origin close, and the model recipe that produced it. A parity check rebuilt the 191 price features through the live path and matched the frozen dataset on all 4,700 overlapping rows.

StockPredictor move-size table with expected moves and 80% bands
The move-size view shows each stock's expected move, its 80% band, and the base-rate up probability.

Part V / The live system

10

A challenger has to earn production.

Both live models retrain weekly through a champion/challenger check. The champion stays frozen between retrains so each logged forecast tests the model that made it. A retrain becomes due once five new sessions of outcomes have matured after the champion's training data ends; the trigger is data-driven, so a missed day only delays it.

PolicyMove-size modelFive-session ranker
Cadence5 new matured sessions5 new matured sessions
HoldoutNewest 63 labeled sessions (about 13 weeks)Newest 63 labeled sessions
ChallengerChampion's exact configuration and featuresChampion's exact configuration and inputs
Comparator if champion never saw the holdoutChampion's QLIKEChampion's mean daily rank IC
Comparator if it didHAR baseline QLIKE, coverage within ±3 pointsMean rank IC at or above 0

A passing challenger is refit on all history including the new week, saved as a new checksummed bundle, and the production pointer moves to it. Every evaluation, promoted or not, is appended to a JSON Lines registry, published to the model events table, and shown in the dashboard's track record. A week adds about 250 move-size rows to roughly 120,000, so retraining changes the model gradually; the gate exists to stop a bad data week or a broken recipe from reaching production.

Part V / The live system

11

Serve the record without touching it.

After the daily jobs, the workflow exports a dashboard snapshot, publishes forecasts, rankings, scorecards, and model events to PostgreSQL, builds the static dashboard, deploys it to Vercel, and smoke-tests the API. Publishing steps run even when an earlier step fails, so a partial day still leaves an inspectable record and logs retained for ninety days.

Database rolePermissions
schema_migratorApplies versioned SQL migrations
pipeline_writerInserts forecasts, rankings, and model events; replaces scorecards; prunes old snapshots
app_runtimeSELECT only, used by the API through a pooled connection

The API exposes health, the full dashboard snapshot, latest and per-symbol forecasts and rankings, scorecards, and model events. Responses are CDN-cached for five minutes with stale-while-revalidate, symbols are validated, and the health check reports an unreachable database as a generic 503 without connection details. The dashboard loads the newer of the API response and a snapshot bundled at build time, and says which one it is showing.

StockPredictor track record with scorecards and model history
The track record fills only with prospective results and every weekly retraining decision.

Part VI / Verification & tradeoffs

12

Verify the claim, and say what it cannot prove.

The repository carries tests for causality, lagged volume, exchange-calendar gaps, corporate-action reconciliation, boundary purging, holdout access order, scrambled-label invariance, artifact corruption, cached-run reuse, and interrupted-training resume. Metrics in each report were recomputed independently from saved predictions, and reloaded native-text bundles reproduced in-memory predictions. FinBERT scores computed on a Colab GPU matched a CPU re-score of a random sample exactly.

LimitConsequence
SurvivorshipToday's index members flatter results, most visibly for mid-cap reversal bets
Reconstructed availabilityHistorical bars and actions come from later provider revisions
Inspected test yearsWalk-forward results are retrospective; prospective logs are the clean test
Small portfoliosTwenty stocks per side leave Sharpe estimates uncertain by roughly ±0.4
CostsFive basis points per side is modeled; shorting costs and borrow limits are not

The scorecard is the only evidence free of retrospective selection, and its samples start small: one session gives fifty rows exposed to the same market day. Band coverage and rank correlation become meaningful after a few weeks; ranking results need months.

  • Decide the gate before opening the test, and keep the decision when it is inconvenient.
  • Report the failures; they explain why the published forecasts are shaped the way they are.
  • Archive raw bytes and pin every input so any number can be traced and rerun.
  • Keep heavy compute in a scheduled pipeline and serve its results read-only.
  • Log forecasts before outcomes exist; it is the only test that cannot be fitted after the fact.