Case study, 2025. Kyndryl, open source.
A recommendation service for the Stock Trader demo
Stock Trader is a demo trading application. I built the service that answers “what’s the sentiment on this stock, and why?” from recent market news, and took it from a prototype to something the team used.
- What
- A FastAPI service and Streamlit dashboard that collect market news, retrieve the most relevant articles, and have Azure OpenAI explain the sentiment with sources.
- My part
- Designed and built the service at Kyndryl. It is published under Apache 2.0.
- When
- 2025, as a software engineer co‑op at Kyndryl
- Stack
- Python, FastAPI, Azure OpenAI, Azure AI Search, Redis, Streamlit
- Results
- A 95% hit rate on the Redis cache.
- Cached queries answer in under 100 ms at p99.
- Went from prototype to a service the team used in the demo.
Context
The question is simple: given a ticker, what is the market saying about it, and why? Answering it well means pulling fresh articles from several sources, finding the ones that actually matter, and asking a model to reason over them. Done naively, every request pays for every upstream call and every model call, and is only as reliable as the slowest data source.
The goal was an answer that is grounded in sources, fast when it can be, and still works when a source is down.
Design
News comes from yfinance, Alpha Vantage and Finnhub. Articles are embedded and stored in Azure AI Search. A query runs two searches at once: a semantic vector search, and a keyword search over titles and summaries that catches exact ticker and company names the vector search can miss.
The two rankings are merged with reciprocal rank fusion. Each article scores 1 / (60 + rank) in every list it appears in, and the scores add up. An article that both searches rank highly rises to the top without having to compare a cosine similarity with a keyword score directly. The top three fused articles, above a minimum fused score, go to Azure OpenAI as context for the answer.
Redis sits in front of the expensive steps, with a different lifetime for each kind of data: prices for an hour, news for two hours, sentiment for a day, and stored article embeddings for a week. External calls go through a circuit breaker that opens after five failures and retries after a minute, so a failing data source degrades one panel instead of the whole response.
Decisions
| Option | For | Against | Chosen |
|---|---|---|---|
| Vector search only | Simple semantic matching | Misses exact tickers and names | No |
| Hybrid search with RRF | Balances meaning and exact matches | One more ranking step to tune | Yes |
| One cache lifetime for everything | Easy to reason about | Prices go stale; embeddings get recomputed | No |
| A lifetime per kind of data | Matches how fast each one changes | Needs a clear cache policy | Yes |
Results
The Redis cache ran at a 95% hit rate, and cached queries answer in under 100 ms at p99, because a repeat query never reaches retrieval or the model. The service went from prototype to a part of the Stock Trader demo the team used, and the code is public under Apache 2.0.
I don’t publish traffic numbers for it. The demo’s usage isn’t a meaningful production figure, and I’d rather show the design than a number I can’t stand behind.
What I’d change
I’d build an evaluation set before tuning the cache: a fast answer from the wrong articles is still wrong. With that in place I’d track groundedness, warm and cold p99, and model calls avoided, and use them to tune the fusion constant and the retrieval threshold instead of choosing them by hand.