Top Gainers & Losers API
Rank the day's biggest movers across the entire US market and get gainers and losers back in a single call, each row carrying the price, absolute and percentage change, volume, market cap, sector, industry, P/E, dividend yield, 1-week, 1-month and year-to-date performance, and the 52-week range. Rows are ranked strictly inside one trading session, and the quote behind each row is chosen by timestamp rather than by the wall clock, so pre-open, mid-session, after-hours, weekends and market holidays all resolve correctly without a market-calendar table. The API is free to use.
Three bucket modes available:
https://data.businessquant.com/trending?bucket={bucket}&count={count}&api_key={api_key}
Related endpoints: Need a quote for named tickers rather than a ranked list? Use the Stock Quotes API. Stream prices as trades happen with the Real-Time Quotes API. Rank on fundamentals instead of today's move with the Stock Screener API. Chart a mover's history using the Historical Metrics API.
1. Request Parameters
Only api_key is required — every other parameter has a default, and calling the endpoint bare returns the top 10 gainers and top 10 losers among US equities. The five filter parameters are not cosmetic: without them the raw table cannot be ranked at all, for the reasons set out in section 4.
| Parameter | Description |
|---|---|
| api_key |
Required
Your unique API key for authentication.
|
| bucket |
Optional
Which side of the market to return. One of gainers, losers, or both. Defaults to both. Case insensitive. Any other value returns 400. The two buckets are ranked independently, so both costs the same single query as either side alone.
Format:
bucket=gainers |
| count |
Optional
Rows per bucket, not per response. Defaults to 10, minimum 1, maximum 250. With bucket=both the response therefore holds up to twice this number — count=250 returns 500 rows. Out-of-range values are rejected by validation with 422.
Format:
count=25 |
| security_types |
Optional
Comma separated list of security types to rank. Valid values are Equity, ETF, and Fund, matched case sensitively. Defaults to Equity. A list that is empty after parsing returns 400. Read the coverage note below the table before requesting ETF or Fund.
Format:
security_types=Equity,ETF |
| min_price |
Optional
Price floor, applied to the previous settled close. Defaults to 5.0. Its job is to keep sub-penny shells out of a ranking sorted by percentage change, where a half-cent tick is a double-digit move. Set to 0 to disable.
Format:
min_price=1 |
| min_marketcap |
Optional
Market capitalization floor in dollars. Defaults to 2000000000 — two billion, i.e. mid cap and above. Lower it to surface small caps, which is where the largest percentage moves usually are.
Format:
min_marketcap=300000000 |
| max_abs_move |
Optional
Rejects any row whose absolute percentage change exceeds this value. Defaults to 100.0, must be greater than 0. This parameter is also the non-finite guard: because abs(x) <= n is false for both infinity and NaN, it is what removes rows whose stored daily performance is literally infinite. Raising it far above 100 re-admits those rows, so raise it deliberately.
Format:
max_abs_move=25 |
| min_volume |
Optional
Share volume floor. Defaults to 0, which disables the filter entirely. Applied to the same resolved volume that appears in the response, so it honours the daily-bar fallback described in section 3 rather than the raw intraday counter. A liquidity floor is worth setting on any list shown to end users, since a large percentage move on a few hundred shares is noise.
Format:
min_volume=1000000 |
| max_staleness_sessions |
Optional
How many sessions before the anchor session a quote may come from and still be ranked. Defaults to 0, meaning the anchor session only. Range 0–30. Raising it fills out a thin list at the cost of mixing sessions, so a name's "today" move may actually be yesterday's. See section 4 for when this is genuinely needed.
Format:
max_staleness_sessions=1 |
Coverage note on ETFs and funds. The market-capitalization floor is applied before anything else, and a row whose market cap is unknown does not pass a numeric floor — in SQL, NULL >= 0 is not true. In this dataset market cap is populated for 7,518 of 8,730 equities but only 2 of 4,040 ETFs and 5 of 22,099 funds, because a fund's size is expressed as net assets rather than market cap.
The practical consequence is that security_types=ETF and security_types=Fund return only a handful of rows today, and lowering min_marketcap does not change that. Treat this endpoint as an equities ranking for now. For fund and ETF size and flows, use the Fund Overview API and the Fund Flows API.
curl -X GET "https://data.businessquant.com/trending?api_key=YOUR_API_KEY"
curl -X GET "https://data.businessquant.com/trending?bucket=gainers&count=25&min_volume=1000000&api_key=YOUR_API_KEY"
curl -X GET "https://data.businessquant.com/trending?min_marketcap=300000000&min_price=1&count=20&api_key=YOUR_API_KEY"
2. Sample Response
A default call — top 10 gainers and top 10 losers among US equities. The response is a metadata envelope followed by a single flat data array holding both buckets together, each row tagged with a bucket field. Rows arrive gainers first, then losers, each side already in rank order, so a client can either render the array as-is or split it on bucket in one pass. Note that metadata.count echoes what you requested per bucket, while metadata.total_records counts the rows actually returned. Switch to JSON to inspect the raw structure.
Gainers and Losers Response
Bucket: both •
Session: 2026-08-27 •
Source: intraday •
20 rows
| Bucket | Ticker | Name | Price | Change | Change % | Volume | Market Cap | Sector | Source |
|---|---|---|---|---|---|---|---|---|---|
| gainer | OKTA | Okta | 171.12 | 36.70 | +27.30% | 19.08M | 22.54B | Technology | intraday |
| gainer | CRM | Salesforce | 248.22 | 42.60 | +20.72% | 67.93M | 168.71B | Technology | intraday |
| gainer | VEEV | Veeva Systems | 287.56 | 42.65 | +17.41% | 6.11M | 39.87B | Healthcare | intraday |
| gainer | CRWD | CrowdStrike Holdings | 221.07 | 31.89 | +16.86% | 27.59M | 192.63B | Technology | intraday |
| gainer | SAIL | SailPoint | 20.74 | 2.52 | +13.80% | 6.18M | 10.33B | Technology | intraday |
| gainer | MSTR | Strategy | 138.60 | 15.41 | +12.51% | 49.42M | 43.36B | Technology | intraday |
| gainer | PANW | Palo Alto Networks | 378.89 | 39.58 | +11.66% | 6.49M | 275.86B | Technology | intraday |
| gainer | TENB | Tenable Holdings | 37.50 | 3.84 | +11.41% | 7.03M | 3.71B | Technology | intraday |
| gainer | FROG | JFrog | 103.12 | 10.32 | +11.12% | 4.04M | 11.42B | Technology | intraday |
| gainer | SNPS | Synopsys | 454.40 | 44.40 | +10.83% | 4.12M | 78.56B | Technology | intraday |
| loser | HQY | Healthequity | 92.90 | -11.52 | -11.03% | 2.64M | 8.76B | Healthcare | intraday |
| loser | REAX | Real Brokerage | 23.85 | -2.74 | -10.30% | 1.66M | 5.60B | Real Estate | intraday |
| loser | NEXA | Nexa Resources | 14.02 | -1.56 | -10.04% | 2.15M | 2.06B | Basic Materials | intraday |
| loser | HRL | Hormel Foods | 21.41 | -2.30 | -9.70% | 17.06M | 13.04B | Consumer Defensive | intraday |
| loser | PAY | Paymentus Holdings | 36.00 | -3.43 | -8.70% | 2.05M | 4.97B | Technology | intraday |
| loser | P | Everpure | 99.44 | -9.46 | -8.69% | 8.51M | 36.16B | Technology | intraday |
| loser | TIGR | UP Fintech Holding | 5.06 | -0.40 | -7.33% | 4.21M | 14.09B | Financial Services | intraday |
| loser | VERA | Vera Therapeutics | 34.44 | -2.38 | -6.48% | 2.87M | 2.65B | Healthcare | intraday |
| loser | SOGP | Sound | 12.60 | -0.86 | -6.39% | 39.44K | 5.62B | Communication Services | intraday |
| loser | HMY | Harmony Gold Mining | 21.02 | -1.41 | -6.29% | 11.37M | 14.24B | Basic Materials | intraday |
3. Response Field Reference
The response has exactly two top level keys: metadata, which echoes the request and describes the session that was ranked, and data, a flat array holding both buckets.
Metadata Fields
| Field | Type | Description |
|---|---|---|
| bucket | string | The bucket mode that was applied, lowercased. Echoes the request. |
| count | number | Rows requested per bucket. This is the request parameter echoed back, not a row count — use total_records for that. |
| gainers | number | How many gainer rows are in data. Zero when bucket=losers. |
| losers | number | How many loser rows are in data. Zero when bucket=gainers. |
| total_records | number | Length of the data array, i.e. gainers + losers. May be lower than count if too few names cleared the filters. |
| session_date | string | The trading session every row was ranked within, as YYYY-MM-DD. Derived from the newest timestamp in the data, never from a clock — see section 4. |
| timezone | string | Always America/New_York. The timezone that session_date and every as_of should be read in. |
| sources | object | Row counts keyed by quote source, for example {"intraday": 20} or {"eod": 14, "intraday": 6}. A mixed object is normal and expected, not an error — the source is decided per ticker. |
| max_staleness_sessions | number | The staleness allowance that was applied. Echoes the request. |
| universe | object | The five filters that produced this ranking — security_types, min_price, min_marketcap, max_abs_move, and min_volume. Echoed so a cached or logged response is self-describing. |
Data Row Fields
| Field | Type | Description |
|---|---|---|
| bucket | string | gainer or loser. Split the array on this field to get two ranked lists. Note the singular — the metadata keys are plural, the row values are not. |
| ticker | string | Ticker symbol. |
| name | string|null | Full registered company name, for example Okta, Inc. Null when the ticker has no profile record. |
| name_short | string|null | Abbreviated display name, for example Okta. Prefer this in a table or tile where width is constrained. |
| exchange | string|null | Listing exchange, for example NASDAQ. |
| price | number | The quote this row was ranked on. Its meaning depends on source: the latest tick for intraday, the settled close for eod. |
| pricechange | number | Absolute change against the previous close, in dollars. Subtracting it from price gives that previous close. |
| pricechange_pct | number | Percentage change against the previous close. This is the ranking key — descending for gainers, ascending for losers. Always positive in the gainer bucket and negative in the loser bucket; rows with a change of exactly zero appear in neither. |
| marketcap | number|null | Market capitalization in dollars, taken from the end-of-day snapshot. It is therefore struck at the previous close and does not move with an intraday price — a name up 27% intraday still reports yesterday's cap. Also the tiebreaker when two rows share a percentage change. |
| volume | number|null | Share volume, with a fallback: the intraday counter is used when non-zero, otherwise the most recent completed daily bar. See the note below this table. |
| sector | string|null | Sector classification, for example Technology. |
| industry | string|null | Industry classification, for example Software - Infrastructure. |
| pe | number|null | Price to earnings ratio from the end-of-day snapshot. Null for companies with no positive earnings. |
| divyield | number|null | Dividend yield as a percentage. 0 for non payers. |
| perf_1w | number|null | One week performance percentage, from the end-of-day snapshot. Useful for separating a one-day spike from a sustained run. |
| perf_1m | number|null | One month performance percentage, from the end-of-day snapshot. |
| perf_ytd | number|null | Year to date performance percentage, from the end-of-day snapshot. |
| high_52w | number|null | 52-week high, from the end-of-day snapshot. Because it excludes the current session, an intraday breakout legitimately reports a price above this value — do not treat that as a data error. |
| low_52w | number|null | 52-week low, from the end-of-day snapshot, with the same caveat in reverse. |
| source | string | intraday or eod — which table this row's quote came from. Decided per ticker, so one response can legitimately mix both. |
| as_of | string | ISO 8601 timestamp of the quote itself. For eod rows this is anchored to 16:00 on the settlement date rather than being a real tick time. |
Why volume has a fallback. The intraday snapshot resets its volume counter to 0 — not to null — once ticks stop for the day. A plain null-coalesce would keep that zero, and every row would read "no volume" after the close. The endpoint therefore treats zero as absent and falls back to the most recent completed daily bar, which is also what min_volume filters on. A genuine null here means neither source had a figure.
4. How Rows Are Selected
Four rules decide what appears in a ranking. They matter mostly when a result surprises you — a name you expected is missing, two share classes collapsed into one, or a list came back shorter than count.
1. The quote is chosen by timestamp, not by the clock
Every ticker offers at most two candidate quotes, one intraday and one end-of-day, and each carries its own timestamp. The newer one wins. Nothing asks whether the market is currently open, which is what makes pre-open, mid-session, after-hours, weekends and market holidays all resolve correctly without a market-calendar table. When a tick is stamped exactly 16:00:00 on the same date as the settled close, the tie breaks toward the close, because that is the settled value. This is why source is a per-row field rather than one global label — a thinly traded name can legitimately report eod in the same response where a liquid one reports intraday.
2. Ranking is confined to a single session
The anchor session is simply the most recent session present in the data after your filters are applied — derived from the rows themselves, not from a calendar. Only quotes from that session are ranked. Without this floor, "newest candidate wins" would still rank a delisted name frozen months ago, because that stale row genuinely is the newest one it has.
The anchor is global to your filtered universe, and this is the usual cause of a short list. If even a handful of securities carry a fresher timestamp than the rest, they set the anchor for everything, and every name still on the previous session is excluded. This is invisible for equities, where thousands of tickers move together. It bites on mixed or fund-heavy requests: a small number of funds quoting today can pull the anchor to today while the bulk of the universe is still settled on yesterday. If a list comes back far shorter than count, raise max_staleness_sessions to 1 and compare — a large jump in row count confirms an anchor split, and metadata.sources will usually shift to a mix of eod and intraday.
3. Share classes are collapsed to one row per company
Two share classes of one company are one company as far as a movers list is concerned, and letting both occupy slots wastes a slot rather than adding information. Equities are therefore deduplicated on the SEC registrant (CIK), and the surviving row is the class with the larger market cap, with the ticker as a deterministic tiebreak so the choice is stable between calls. Deduplication happens before ranking, so the top count is count distinct companies. Funds and ETFs are deliberately excluded from this rule: for a fund the SEC registrant is the issuer rather than the product, and a single issuer files hundreds of ETFs, so keying on CIK would collapse an entire fund range into one row. Anything that is not an equity, and any equity with no CIK on file, falls back to its own ticker and is effectively not deduplicated.
4. The filter defaults are load-bearing
Sorting the raw table by daily performance with no floors does not produce a usable list. It returns rows whose stored performance is literally infinite, followed by a run of sub-penny shells showing five-figure percentage gains. The price and market-cap floors exist so that everything in the list is a name a reader could actually trade, and max_abs_move doubles as the non-finite guard because abs(x) <= n is false for infinity and NaN alike. Every default is overridable — just be aware of what each one is holding back before lowering it.
Caching
Responses are cached server side for 60 seconds, keyed on the full filter set, so two different filter combinations never share a cached payload. The window is deliberately short because rankings move minute to minute while the market is open. Polling faster than once a minute returns the same payload and gains nothing.
5. Error Responses
Every error is returned as a JSON object with a single detail key.
| Status | Meaning | When it fires |
|---|---|---|
400 | Invalid bucket | bucket was something other than gainers, losers, or both. The message names the three valid values. |
400 | No security type | security_types was supplied but parsed to an empty list — for example a bare comma. Omit the parameter entirely to accept the default instead. |
404 | No trending data found | The filters excluded everything. Most often the market-cap floor against a non-equity request, or a filter combination narrow enough that nothing in the anchor session survives. Widen the filters or raise max_staleness_sessions. |
422 | Parameter out of range | Request validation rejected a value before the handler ran — count outside 1–250, max_staleness_sessions outside 0–30, a negative floor, or max_abs_move at exactly 0. |
500 | Server error | An unexpected failure while building the ranking. Retry once; if it persists the payload names the underlying cause. |
Trending Stocks Request Examples
Copy any request below directly into your application.
https://data.businessquant.com/trending?api_key=YOUR_API_KEY
https://data.businessquant.com/trending?bucket=gainers&count=25&min_volume=1000000&api_key=YOUR_API_KEY
https://data.businessquant.com/trending?bucket=losers&count=50&min_marketcap=10000000000&api_key=YOUR_API_KEY
https://data.businessquant.com/trending?min_marketcap=300000000&min_price=1&count=20&api_key=YOUR_API_KEY
https://data.businessquant.com/trending?max_abs_move=5&count=15&api_key=YOUR_API_KEY
https://data.businessquant.com/trending?max_staleness_sessions=1&count=20&api_key=YOUR_API_KEY
Frequently Asked Questions
Does count limit the whole response or each bucket?
Each bucket. With the default bucket=both, count=10 returns up to 20 rows — 10 gainers and 10 losers — and count=250, the maximum, returns up to 500. The metadata.count field echoes what you asked for per bucket, while metadata.total_records reports how many rows actually came back.
Why do some rows say intraday and others say eod in the same response?
Because the source is decided per ticker by comparing timestamps, not once per request by checking whether the market is open. Each ticker offers at most two candidate quotes and the newer one wins, so a liquid name with a fresh tick reports intraday while a thinly traded one whose last tick predates its settled close reports eod. A mixed metadata.sources object is normal. This design is what makes pre-open, after-hours, weekends and holidays resolve correctly without a market-calendar table.
Why did I get fewer rows than I asked for?
Usually the session anchor. Ranking is confined to the most recent session present in your filtered universe, so if a small number of securities carry a fresher timestamp than the rest, they set the anchor and everything still on the previous session is excluded. Raise max_staleness_sessions to 1 and compare row counts — a large jump confirms it. The other common causes are a market-cap floor applied to a non-equity request, and filter combinations narrow enough that few names qualify.
Can I get trending ETFs or mutual funds?
Only partially today. security_types accepts ETF and Fund, but the market-capitalization floor is applied first and a row with an unknown market cap does not pass a numeric floor. Market cap is populated for the large majority of equities and for almost no funds, since a fund's size is reported as net assets instead. Lowering min_marketcap does not work around it. Treat this endpoint as an equities ranking, and use the Fund Overview and Fund Flows APIs for fund size and flow data.
Why is price higher than high_52w on some rows?
Because they come from different places. price can be a live intraday tick, while high_52w and low_52w are taken from the end-of-day snapshot and do not include the current session. A stock breaking out to a new high intraday will therefore legitimately show a price above its 52-week high until that session settles. The same applies to marketcap, pe, divyield and the three performance fields — all are struck at the previous close, so a name up 27% intraday still reports yesterday's market cap.
Why do I only see one share class of a company?
Equities are deduplicated on the SEC registrant before ranking, so two classes of the same company cannot occupy two slots in the top ten. The surviving row is the class with the larger market cap, with the ticker as a stable tiebreak. ETFs and mutual funds are deliberately exempt, because for a fund the registrant is the issuer rather than the product and hundreds of ETFs share one filer.
How often should I poll this endpoint?
No more than once a minute. Responses are cached server side for 60 seconds keyed on the full filter set, so a faster poll returns the identical payload. If you need tick-level movement on specific tickers rather than a ranked list, use the Real-Time Quotes WebSocket API instead.
Is the Top Gainers & Losers API free to use?
Yes, the Top Gainers & Losers API is free to use and available on every plan. Sign up for an API key to start pulling gainers and losers immediately.