Case study · Quantitative research tooling · Team competition

RYM investment framework — equity screening for the UK Investment Challenge

Two command-line tools that turn a 500-stock universe into a short, ranked list with a documented reason for each name: a technical scanner for the first cut and an earnings-momentum scorer for conviction. It is a research-prioritisation tool. It has not been backtested, and this page does not present it as a strategy with known returns.

Result
3rd of 28 QMUL teams, UK Investment Challenge 2025 — a competition scored on portfolio performance and on the quality of the written rationale
My part
The framework in the repository: scanner, scorer, sentiment integration, output logging. Portfolio decisions were the team's
Modules
portfolio_scanner_final.py (S&P 500 technical screen) · earnings_ranker.py (weighted earnings-momentum score with FinBERT news sentiment)
Stack
Python, pandas, NumPy, yfinance, Finnhub API, Hugging Face Transformers (ProsusAI/finbert), matplotlib
Code
github.com/M-Rodani1/rym-investment-framework

The scanner narrows the universe; the scorer ranks candidates with upcoming earnings and writes down why. Whether the score predicts anything is an open question the repository lists as future work.

Workflow

  1. Screenscan all S&P 500 constituents on trend, momentum and risk-adjusted return
  2. Filterkeep names with earnings within 14 days ahead (or 7 days back, optionally)
  3. Scoresix weighted components → 0–10 composite and a rating
  4. Documentper-ticker breakdown and CSV log for the competition write-up
  5. Decideteam selects and sizes positions — outside the code

Technical scanner

For each constituent, 120 days of prices from yfinance, then: trend from the MA50 / MA200 relationship, RSI-14 with the conventional 30/70 bands, MACD (12, 26, 9) histogram sign, annualised volatility, and a Sharpe ratio with a zero risk-free rate. Requests are spaced 0.3 s apart to stay under the data provider's limits. The output is a table sorted by Sharpe, plus a two-panel chart (price with moving averages, RSI) for any single ticker.

Earnings-momentum scorer

The scorer is built around post-earnings-announcement drift: the tendency of prices to continue moving in the direction of an earnings surprise for days after the announcement. It does not measure surprise directly; it uses the average three-trading-day return after each of the last four announcements as a proxy for how the stock has recently behaved around earnings, and combines that with momentum, volatility, volume, risk-adjusted return and news sentiment.

Composite score components and weights, as coded
ComponentWeightInputs
Momentum0.2510-day return, RSI scaled, EMA 8/21 crossover
Volatility0.20ATR as a percentage of price (penalised), Bollinger bandwidth
Volume0.15Recent volume relative to its 50-day average
Sharpe0.1530-day risk-adjusted return
Earnings drift0.15Mean 3-day post-announcement return over the last four events
Sentiment0.10FinBERT (ProsusAI/finbert) on up to three recent headlines from a 7-day Finnhub window

Each component is a small hand-weighted combination of its inputs; the six are combined with the weights above and squashed onto 0–10 with a tanh mapping, then given a categorical rating. The earnings calendar comes from yfinance; by default, names that have already reported are filtered out so the list contains only upcoming events. An interactive mode keeps a session open for adding tickers and printing a comparison table.

Outputs

Two snapshots are committed as evidence of a full run: sp500_summary.csv (501 constituents with trend, RSI, MACD, volatility and Sharpe) and earnings_ranked.csv (50 scored tickers with every component and raw feature). Each scoring run appends a timestamped row per ticker to a log, which doubled as the audit trail the competition asked for.

What it does not establish

Unvalidated by design, so far

No historical backtest has been run. The repository lists one under future work, and until it exists there is no evidence that a higher composite score is followed by better returns. The weights are hand-set priors, not fitted.

The drift proxy uses four events per stock — too few to distinguish a persistent effect from noise — and is not conditioned on the sign or size of the earnings surprise, which is what the PEAD literature actually studies.

Sentiment is scored on at most three headlines, and the Sharpe ratios use short windows with no risk-free rate.

The 3rd-place result reflects a portfolio chosen by the team with this tool as one input, over one competition period; it is not a measurement of the tool.

What I would do next

  • Backtest the composite score with point-in-time constituents and earnings dates, reporting decile returns net of a market factor; fit or drop the weights on the result.
  • Replace the drift proxy with standardised unexpected earnings from actual surprise data.
  • Add sector neutralisation so the ranking is not a bet on whichever sector is trending.