WorkWingman — Stock Market Data Sources (Technical)¶
WorkWingman's equity-comp valuation branch and its news→realized-move event study both need occasional, low-volume US equity/ETF price lookups: (a) a current/EOD quote by ticker, and (b) a daily-close history series long enough to compute backward-looking historical volatility. No intraday data is needed. This document records the research behind picking a replacement default price source after Stooq's free endpoints stopped being usable, the comparison across candidates, the recommendation, and the exact adapter shape for whoever wires the winner in.
Why Stooq is no longer the default¶
Stooq's free CSV/quote endpoints (stooq.com/q/d/l, stooq.com/q/l) are now gated behind a
JavaScript proof-of-work bot-challenge before they serve data — confirmed independently by the
pandas-datareader project, which reports its Stooq reader breaking as of March 2026 because
Stooq now returns an HTML "request an API key" page instead of CSV
(pydata/pandas-datareader#1012).
A server-side .NET client cannot — and must not attempt to — solve a JS proof-of-work challenge
designed to gate a browser. WorkWingman does not implement bot-challenge circumvention.
Stooq is retired as the default source; the config gate should point at the replacement below.
Hard constraint applied to every candidate¶
Only sources with documented, officially sanctioned programmatic access were considered. Scraping, reverse-engineered endpoints, and ToS-violating bot-challenge bypasses were excluded regardless of how "widely used" they are. Where a popular option is unofficial (Yahoo Finance), that is called out explicitly rather than treated as acceptable because it's common.
Comparison table¶
All figures verified against vendor pricing/docs pages and corroborating GitHub issues in 2026; free tiers change without notice, so re-verify before relying on any exact number in production code.
| Source | Key required | Rate limit | Daily cap | Daily EOD history depth (free) | Official? | Commercial use on free tier | Notes |
|---|---|---|---|---|---|---|---|
| Financial Modeling Prep (FMP) | Yes (query param apikey) |
Not separately throttled per-minute in docs | 250 requests/day (rolling) | history endpoint documented but capped around 5 years on the free "Basic" plan (30+ years is a paid-tier feature) | Yes — documented REST API | Not explicitly restricted for the Basic plan in public docs (unlike Tiingo, which explicitly bars it) | ~150+ endpoints; quote + full historical EOD both present on free tier |
| Twelve Data | Yes (apikey query param or Authorization: apikey header) |
8 credits/minute | 800 credits/day | Real-time/EOD included; deep history and some instrument classes reserved for paid "Grow" tier — free tier history depth not guaranteed long | Yes — documented REST + OpenAPI/Swagger spec | Docs describe free tier as "internal non-display usage" — read the terms before any customer-facing display of the data | Good OpenAPI spec; batch quote support |
| Finnhub | Yes (token query param) |
60 calls/minute | No published daily cap | Disqualifying: /stock/candle (historical daily OHLC) returns 403 "no access to this resource" on free keys — moved to paid tiers (finnhubio/Finnhub-API#397); free tier now covers quote/profile/fundamentals/news only |
Yes — documented REST API | Free tier stated as personal/non-commercial | Excellent quote-only rate limit, but fails requirement (b) — no history — so it cannot be the default alone |
| Tiingo | Yes (token query param) |
50 requests/hour | 1,000 requests/day; 1 GB/month bandwidth; 500 unique symbols/month | 30+ years on free "Starter" tier — best history depth of any free tier evaluated | Yes — documented REST API | Free tier is explicitly "Internal Use Only" — ToS bars displaying/sharing the data with another person or organization; commercial/customer-facing use requires the paid "Power" tier ($30–50/mo) | Best history depth, but the ToS wording is the tightest — needs a read before treating "internal" as satisfied by a single-user desktop app |
| Marketstack | Yes (access_key query param) |
Not separately documented per-minute | 100 requests/month (free plan) | EOD-only on free plan | Yes — documented REST API (APILayer) | Requires a credit card even for the free plan | Cap is far too low (100/month) for realistic use even at "occasional" volume |
| Polygon.io | Yes (apiKey query param) |
5 requests/minute | Not separately published | Free tier historical window is limited ("a few years", exact cutoff undocumented publicly and has shifted over time) | Yes — documented REST API | Not restricted, but free tier is explicitly a trial/evaluation tier | Best-in-class paid tier, but free tier is the most rate-limited of the REST candidates and history depth is the least certain |
| Alpha Vantage (current baseline) | Yes (apikey query param) |
5 requests/minute | 25 requests/day | 20+ years on TIME_SERIES_DAILY free tier |
Yes — documented REST API | No explicit free-tier commercial restriction found | The 25/day cap is the tightest of any candidate with usable history depth; frequently rate-limited in practice (home-assistant/core#109153, TauricResearch/TradingAgents#305) |
Yahoo Finance chart endpoint (query1.finance.yahoo.com/v8/finance/chart) |
No key | Undocumented/unpublished; enforced ad hoc via IP blocks/CAPTCHA | None published | Full history available in practice | Unofficial. Yahoo shut down its official Finance API in 2017; this endpoint is a reverse-engineered leftover with no support contract, no SLA, and no documented terms of use for this specific path. Yahoo's general API ToS frames public endpoints as personal/non-commercial use, and unofficial/undocumented endpoint use for a commercial or quasi-commercial product is a ToS risk, not a green light. | Effectively none — Yahoo can rate-limit, CAPTCHA-gate, or kill the endpoint at any time with no notice, exactly as happened to Stooq | Excluded by the hard constraint in this task — it is the same category of risk (unofficial endpoint that can grow a bot-challenge overnight) that just took Stooq offline. Flagged here for completeness only; not recommended. |
SEC EDGAR (data.sec.gov) |
No key (User-Agent header required) | Reasonable-use, no hard published cap | N/A | N/A | Yes — official, documented, no key | N/A | Does not carry market/trading prices. EDGAR's companyfacts/submissions/companyconcept APIs expose XBRL-tagged financial-statement facts and filing metadata, not daily closing share prices. Useful for fundamentals elsewhere in WorkWingman, not a substitute for a price feed. |
| No-key free option | — | — | — | — | — | — | None qualifies. The only real no-key, no-signup option in 2026 is yfinance-style scraping of Yahoo's undocumented endpoints, which is unofficial and excluded by this task's hard constraint. There is no legitimate zero-key REST API for equity prices at the time of writing. |
Recommendation¶
Default: Financial Modeling Prep (FMP), free "Basic" tier.
Rationale against the two hard requirements (current quote + daily-close history) and the occasional/low-volume usage pattern:
- It is the only evaluated free tier that cleanly satisfies both requirements at once: a quote endpoint and a full daily EOD history endpoint, with 250 requests/day — far more headroom than Alpha Vantage's 25/day for a per-saved-job lookup pattern (one job could need a quote + a multi-year history call; at 25/day that's at most ~12 jobs/day before throttling, vs. ~125 jobs/day on FMP).
- 5 years of daily EOD history on the free tier is enough for a realized-volatility calculation and a news→move event study; it is shallower than Tiingo's 30+ years, but Tiingo's free tier explicitly forbids anything beyond internal/personal use in its terms, which is a worse fit for a distributed desktop app than FMP's plan (no equivalent public restriction found in FMP's Basic-tier terms as of this research — re-verify FMP's terms of service page directly at integration time, since vendor free-tier terms tighten without notice, exactly as Stooq's did).
- It fits WorkWingman's existing config-gate convention: free source enabled by default, keyed behind an environment variable, paid tiers/sources opt-in later if 250/day ever proves insufficient.
Ranked fallback order (if FMP's free tier is throttled, discontinued, or its terms change unfavorably):
- Twelve Data (free "Basic": 8 credits/min, 800/day) — good daily cap, official OpenAPI- documented REST API; verify current free-tier history depth and the "internal non-display usage" wording against WorkWingman's actual display use before promoting to default.
- Alpha Vantage (current baseline: 5/min, 25/day, 20+ years history) — deepest free history depth after Tiingo, but the 25/day cap is the binding constraint; keep as a documented fallback/secondary source rather than removing it outright, since it is already wired in.
- Tiingo (free "Starter": 50/hr, 1,000/day, 30+ years) — best rate limits and history depth of any evaluated source, but its free tier's "Internal Use Only" ToS wording needs explicit legal/product sign-off before use in a tool whose output the user sees and acts on; do not promote above FMP/Twelve Data without that review.
- Polygon.io (free: 5/min, limited history) — reputable but the most rate-limited and history-shallow of the REST candidates on its free tier; treat as a paid-tier candidate later, not a free default.
- Marketstack (100/month) — cap is too low for practical use; not recommended even as a fallback.
Not recommended at any tier:
- Finnhub — disqualified as a history source (free tier no longer serves
/stock/candle), despite an attractive 60/min quote rate limit. Could theoretically be paired with another source purely for the quote leg, but that doubles the integration surface for a single benefit; not worth it while FMP/Twelve Data serve both needs from one vendor. - Yahoo Finance chart endpoint — unofficial, undocumented, no ToS coverage for this specific endpoint, and exactly the class of risk (an endpoint that can sprout a bot-challenge with zero notice) that just disqualified Stooq. Excluded per this task's hard constraint.
- Any scraping-based "free" source (e.g.
yfinance) — same reasoning as above; excluded.
No qualifying no-key option exists. Every legitimate, ToS-clean, officially-documented source in this space requires an API key. This is worth stating plainly because the ideal "truly free, no signup" answer does not exist — the honest recommendation is a free-tier keyed source, gated the same way WorkWingman already gates other paid/keyed integrations.
Adapter design spec — FMP as the new config-gated default¶
For whoever wires this in on the feature-equity-comp and feature-news-price-context
branches. This section is a spec only; no code on those branches is touched by this research.
Config gate convention: follow the existing pattern — free source enabled by default behind
an environment variable holding the API key (e.g. WORKWINGMAN_FMP_API_KEY); if the variable is
unset, the price-data feature should degrade gracefully (no crash, feature reports "unavailable"),
consistent with how other optional integrations already behave in this codebase.
Auth: query-string parameter apikey=<key> appended to every request. No headers, no OAuth.
Endpoint 1 — current/EOD quote:
GET https://financialmodelingprep.com/stable/quote?symbol={TICKER}&apikey={KEY}
{TICKER}— a single US equity/ETF symbol (e.g.AAPL,SPY). Batch quoting for multiple symbols in one call exists as a separatebatch-quoteendpoint if the calling branch ever needs to look up more than one ticker at a time.- Response: JSON array containing one object (even for a single symbol) with at least:
symbol,price,changesPercentage,change,dayLow,dayHigh,yearLow,yearHigh,marketCap,priceAvg50,priceAvg200,exchange,volume,timestamp. Adapter should project onlysymbol,price, andtimestampunless a caller needs more.
Endpoint 2 — daily-close history:
GET https://financialmodelingprep.com/stable/historical-price-eod/full?symbol={TICKER}&apikey={KEY}
- Optional
from=YYYY-MM-DD/to=YYYY-MM-DDquery params to bound the range — use these to request only as much history as the volatility/event-study window actually needs, to conserve the 250/day cap (a bounded 2-year request instead of pulling full history every time). Verify current param names against FMP's live docs at integration time — vendor stable-API paths have moved before (legacy/api/v3/historical-price-full/{symbol}vs. current/stable/historical-price-eod/full?symbol=). - Response: JSON object with
symboland ahistoricalarray of daily bars, each with at leastdate,open,high,low,close,volume,adjClose(adjusted close — prefer this over rawclosefor volatility math so splits/dividends don't create false jumps).
Suggested C# shape (illustrative only — not implemented here):
public interface IStockPriceProvider
{
Task<StockQuote?> GetQuoteAsync(string symbol, CancellationToken ct);
Task<IReadOnlyList<DailyBar>> GetDailyHistoryAsync(
string symbol, DateOnly from, DateOnly to, CancellationToken ct);
}
public sealed record StockQuote(string Symbol, decimal Price, DateTimeOffset AsOf);
public sealed record DailyBar(DateOnly Date, decimal Open, decimal High, decimal Low,
decimal Close, decimal AdjClose, long Volume);
- Wrap both calls in the project's existing Flurl + Polly pattern (retry/backoff on 429/5xx,
matching how other HTTP integrations in
WorkWingman.Infrastructurealready behave). - 250 requests/day is shared across all WorkWingman features that end up calling FMP — if both the equity-comp branch and the news-price-context branch land and both call FMP, budget and possibly cache responses (e.g. per-symbol daily cache) so one busy day of job-saving doesn't exhaust the shared daily cap.
- Treat FMP as swappable behind
IStockPriceProviderfrom day one so promoting Twelve Data (or demoting FMP) later is a config change, not a rewrite.
Re-verification note¶
Every number in this document was checked against vendor pricing/docs pages and independent corroborating sources (GitHub issues, third-party comparison posts) in 2026. Free-tier terms, rate limits, and endpoint availability change without notice — Stooq itself went from "open CSV endpoint" to "bot-challenge gated" with no advance notice, and Finnhub silently moved historical candles behind a paywall. Re-check the live pricing/docs page for whichever source is wired in at integration time, rather than trusting this table indefinitely.