# Finnhub Stock Market Data (`ryanclinton/finnhub-stock-data`) Actor

Pull real-time stock quotes, historical OHLCV candles, company profiles, financial metrics, earnings calendars, company news, and symbol lookups for any publicly traded stock, cryptocurrency, or forex pair -- all powered by the [Finnhub](https://finnhub.io/) financial data API.

- **URL**: https://apify.com/ryanclinton/finnhub-stock-data.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** AI, Developer tools
- **Stats:** 35 total users, 2 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 data fetcheds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Finnhub Stock Market Data

![Finnhub Stock Market Data — monitor change, not prices](https://apifyforge.com/readme-assets/ryanclinton-finnhub-stock-data/hero.png)

#### Stop monitoring prices. Start monitoring change.

**Market intelligence with memory, for stocks, crypto, and forex.** Stop pulling raw market data and building your own alert logic. This actor turns [Finnhub](https://finnhub.io/) data into deterministic market intelligence that answers the three questions a professional workflow actually asks:

- **What changed since last time?**
- **What needs attention first?**
- **Which trends are persistent versus noise?**

Most financial APIs tell you what happened. This actor tells you **what changed, what still matters, and what to look at first** — and remembers it across runs.

*Designed for scheduled monitoring, not one-off lookups.*

> **Descriptive, not advisory.** This actor classifies, monitors, and contextualises market data. It does **not** emit buy / sell / hold signals, price targets, or any quality/health/investment score, and it is not investment advice. See [What this does not do](#what-this-does-not-do).

#### Most APIs return this

```json
{ "c": 237.49, "pc": 234.37, "dp": 1.33 }
```

#### This actor returns this

```json
{
  "symbol": "AAPL",
  "marketState": { "trend": "advancing", "valuation": "elevated" },
  "newEvents": ["near-52-week-high"],
  "attentionLevel": "high",
  "stateVelocity": "improving",
  "stateDuration": { "trend": 12 }
}
```

***

### Why this exists

Most financial APIs answer one question: *what is the price?* Professional workflows need answers to different questions, the ones you currently write custom scripts and stitch alerts together to get:

- What changed since yesterday?
- Which symbols need attention?
- Which events are still active, and how long have they run?
- Which watchlists are improving and which are deteriorating?
- Is this classification persistent, or this-run noise?

This actor was built to answer those questions deterministically. No LLMs. No prompts. No subjective scoring. No buy/sell recommendations. Just stable market intelligence that automation platforms can trust.

***

### Who this is for

- **Portfolio monitoring** — track 20 to 500 symbols and receive only what changed, not 500 rows of raw data.
- **Automation builders** — route Dify, Make, Zapier, and n8n workflows on deterministic enums (`attentionLevel`, `transitions`, `eventSeverity`) instead of LLM prompts.
- **Research teams** — build historical datasets of state transitions, event persistence, and watchlist drift for backtesting.
- **Newsletter & content pipelines** — generate a daily "what changed" digest without manually reviewing dozens of tickers.

***

### Three ways to use it

Ten modes exist, but the product is really three:

| Mode | In three words | Answers | Returns |
|------|----------------|---------|---------|
| **Snapshot** (`snapshot`) | Understand the present | *What does this look like right now?* | One composite record per symbol: price + valuation + volatility + news tone + 52-week position + a `marketState`. |
| **Monitor** (`monitor`) | Detect change | *What changed since last time?* | State transitions, event lifecycle, attention, drift, and accumulated per-symbol memory, diffed against the last run. |
| **Command Center** (`command-center`) | Prioritise attention | *What do I look at first?* | One digest: `needsAttention` (symbols ranked), `topChanges`, `criticalEvents`, watchlist regime + health. |

The remaining modes (`quote`, `candles`, `profile`, `news`, `search`, `earnings`, `metrics`, `compare-watchlists`) are the building blocks and a cross-watchlist comparison. Everything else is implementation detail.

The pipeline that produces it:

```text
   Finnhub data
        ↓
   Classification     (valuation, volatility, sentiment, events)
        ↓
   Market state       (one composite per symbol)
        ↓
   Change detection   (transitions, new/resolved events, attention)
        ↓
   Market memory      (duration, history, stability, drift)
        ↓
   Command center     (what to look at first)
```

![How it works — Finnhub data through classification, change detection, market memory, to command center](https://apifyforge.com/readme-assets/ryanclinton-finnhub-stock-data/intelligence-layers.png)

***

### Try this in 5 minutes

Run this once:

```json
{
  "apiKey": "YOUR_FINNHUB_KEY",
  "mode": "monitor",
  "watchlistName": "demo",
  "symbols": ["AAPL", "MSFT", "NVDA"]
}
```

Then run the exact same input again tomorrow. Instead of getting the same data twice, the second run returns the things a plain data API never could:

- state `transitions` (what flipped since yesterday)
- `newEvents` / `resolvedEvents`
- `watchlistDrift` + `watchlistHealth`
- accumulated `stateDuration`, `stability`, and `stateVelocity`

That is the difference between a market-data API and a monitoring system.

***

![Change detection, market memory, command center, and automation-ready output](https://apifyforge.com/readme-assets/ryanclinton-finnhub-stock-data/feature-callouts.png)

### Market memory

Most actors forget everything between runs. This actor **remembers**. Name a watchlist, run it on a schedule, and it accumulates state across runs:

- **State duration** — how long the current trend / valuation / volatility has held (`stateDuration`), and a persistence confidence from it (`stateConfidence`).
- **Event history** — how long each event has stayed active and how many times it has fired (`activeEvents`, `eventHistory.timesTriggered`).
- **Transition frequency** — how often each classification flips (`stability`: stable / settling / volatile).
- **Long-term trajectory** — whether sentiment and trend are improving or deteriorating over the stored window (`history`, `stateVelocity`).
- **Watchlist drift** — how the whole watchlist is moving (`watchlistDrift`, `watchlistHealth`).

> **Unlike most financial APIs, this actor becomes more valuable over time.** Every run adds historical state, event history, drift analysis, and persistence tracking to your watchlists.

A competitor can reproduce the math in a weekend; they cannot reproduce months of accumulated watchlist state, event lifecycles, and drift history without rebuilding their architecture around persistent monitoring from day one. This is the moat: market memory, not just market data.

#### What happens after 30 days?

Most APIs only ever know today's market state. After a month of scheduled runs, this actor also knows:

- how long a trend has persisted
- how often a classification changes
- whether sentiment is improving or deteriorating
- which events repeatedly occur
- which watchlists are drifting

That knowledge exists only because the actor remembers. It is not in any single Finnhub response; it is built one run at a time.

***

### Why not just use Finnhub?

Finnhub tells you:

```json
{ "c": 237.49, "dp": 1.33 }
```

This actor tells you:

```json
{
  "marketState": { "trend": "advancing" },
  "transitions": [{ "field": "valuation", "from": "normal", "to": "elevated" }],
  "attentionLevel": "high"
}
```

Finnhub provides market data. This actor provides market intelligence: what changed, what still matters, and what to look at first. If you only need the price, use Finnhub directly. If you need to know what changed since yesterday across a watchlist, that is what this actor is for.

***

### A real monitoring run

Yesterday's run stored NVDA as quiet:

```json
{ "symbol": "NVDA", "state": { "trend": "advancing", "volatility": "normal" }, "attentionLevel": "low" }
```

Today's run diffs against it and surfaces only the change:

```json
{
  "symbol": "NVDA",
  "transitions": [{ "field": "volatility", "from": "normal", "to": "elevated" }],
  "newEvents": ["large-move-up"],
  "attentionLevel": "high",
  "stateVelocity": "rapidly-improving",
  "stateDuration": { "trend": 14 }
}
```

A normal API gives you today's price. This actor tells you what changed, how fast, and that the uptrend has now held for 14 runs.

***

### Compared to raw financial APIs

| Capability | Finnhub | Alpha Vantage | This actor |
|------------|:-------:|:-------------:|:----------:|
| Quotes / metrics / news | ✓ | ✓ | ✓ |
| **Remembers previous runs** | ✗ | ✗ | ✓ |
| **Explains what changed** | ✗ | ✗ | ✓ |
| **Prioritises your attention** | ✗ | ✗ | ✓ |
| Deterministic classification | ✗ | ✗ | ✓ |
| Event detection + severity | ✗ | ✗ | ✓ |
| Watchlist monitoring | ✗ | ✗ | ✓ |
| State transitions + lifecycle | ✗ | ✗ | ✓ |
| Historical state memory | ✗ | ✗ | ✓ |
| Command-center digest | ✗ | ✗ | ✓ |
| Cross-watchlist comparison | ✗ | ✗ | ✓ |
| Automation-ready enums | ✗ | ✗ | ✓ |

Your real alternatives are not other data APIs; they are TradingView alerts, Finviz screeners, Koyfin watchlists, and the custom monitoring scripts you would otherwise maintain yourself.

#### Compared to TradingView alerts

| Capability | TradingView alerts | This actor |
|------------|:------------------:|:----------:|
| Detects events | ✓ | ✓ |
| Remembers previous runs | ✗ | ✓ |
| Tracks event lifecycles | ✗ | ✓ |
| Watchlist drift | ✗ | ✓ |
| Portfolio intelligence (compare watchlists) | ✗ | ✓ |
| Historical state memory | ✗ | ✓ |
| Structured JSON for automation | ✗ | ✓ |

TradingView fires an alert and forgets it. This actor records it, ages it, and tells you whether it is still active and how the whole watchlist is drifting around it.

***

### Use cases

- **Daily watchlist monitoring** — schedule `monitor` (or `command-center`) every morning and receive state transitions, new/resolved events, and watchlist drift instead of raw quotes.
- **Portfolio oversight** — track trend persistence, volatility changes, sentiment deterioration, and active events across hundreds of symbols in one run.
- **Dify / Make / Zapier automation** — trigger workflows when `valuation` changes, an event appears, sentiment deteriorates, or `attentionLevel` becomes critical. No LLM classification required.
- **Research pipelines** — build historical datasets of market states, event lifecycles, transition frequency, and state velocity for backtesting and analysis.

***

### Key features

- **Ten data modes in one actor** -- quotes, candles, profiles, news, symbol search, earnings, metrics, a composite `snapshot`, a stateful `monitor`, and a `command-center` mode that returns one digest answering "what do I look at first?".
- **Cross-run change detection + memory (`monitor` mode)** -- name a watchlist, run it on a schedule, and get state transitions, event lifecycle (with age + times-triggered), an `attentionLevel`, watchlist drift, and accumulated per-symbol memory (`symbolContext`, `stability`, long-run `history`, `stateVelocity`). The accumulated state is what a stateless wrapper structurally cannot reproduce.
- **Command-center digest (`command-center` mode)** -- one record per run: `needsAttention` (symbols ranked), `topChanges`, `criticalEvents`, watchlist drift + regime. The anti-data-blindness surface: read three symbols, not two hundred rows.
- **Deterministic intelligence on every record** -- market-cap tiering, valuation context, 52-week range position, beta-based volatility class, dividend profile, news sentiment + headline-type classification, earnings beat/miss, deterministic `events[]` flags + a top-level `eventSeverity` + structured `triggers[]`, opt-in peer/sector benchmarking, and an opt-in symbol screener.
- **Technical indicators** -- in candles mode, SMA50/SMA200, RSI14, ATR14, and a trend-regime classification computed from the OHLCV bars (requires Finnhub candle access).
- **Plain-English `summary` on every row** -- a one-line, LLM-quotable description an agent or a spreadsheet can read without joining fields.
- **Run summary record + KV mirror** -- every run emits a `recordType: "summary"` digest (counts, distributions, top mover) and pins it to the `SUMMARY` key-value store record for one-object dashboard reads.
- **Streamed output** -- each symbol's rows appear in the dataset the moment they resolve, so results start showing within about a minute instead of after the whole run.
- **Multi-symbol batch processing** -- pass an entire watchlist in one run; results stream into a single dataset with a `recordType` discriminator per record.
- **Output profiles** -- choose `minimal` (summary + headline classification only), `standard` (data + intelligence), or `full` (everything, including the completeness confidence object).
- **Resilient by design** -- per-symbol error isolation (one failed ticker never wipes the batch), automatic retry with backoff on rate limits and transient errors, a circuit breaker that fails fast on a bad key, and a typed `failureType` on every error record.
- **Crypto and forex support** -- query pairs using exchange-prefixed symbols like `BINANCE:BTCUSDT` alongside traditional stock tickers.

***

### How to use Finnhub Stock Market Data

1. **Get a free Finnhub API key.** Visit [finnhub.io/register](https://finnhub.io/register), create an account, and copy the API key from your dashboard. The free tier allows 60 API calls per minute.
2. **Open the actor in Apify Console.** Go to [Finnhub Stock Market Data](https://apify.com/ryanclinton/finnhub-stock-data) and click "Start".
3. **Paste your API key.** Enter it into the "Finnhub API Key" field. The field is encrypted and sent to Finnhub as a request header, never exposed in logs or output URLs.
4. **Select a data mode.** Choose one of the seven modes.
5. **Enter your stock symbols.** Add one or more tickers (e.g. `AAPL`, `MSFT`, `TSLA`). For crypto, use the exchange-prefixed format like `BINANCE:BTCUSDT`. For Symbol Search mode, enter a search query instead.
6. **Adjust optional settings.** Set the candle resolution, the look-back window (`daysBack`, 1 to 365), and the output profile.
7. **Run and export.** Click Start, watch rows stream in, then download as JSON, CSV, or Excel, or connect the output to Zapier, Make, Dify, Google Sheets, or the Apify API.

***

### Input parameters

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `apiKey` | `string` (secret) | Yes | -- | Your API key from [finnhub.io](https://finnhub.io/register). Without it, the actor logs a setup message and exits cleanly (no data, no charge). |
| `mode` | `string` (select) | No | `quote` | `quote`, `candles`, `profile`, `news`, `search`, `earnings`, `metrics`, `snapshot`, `monitor`, `command-center`, or `compare-watchlists`. |
| `symbols` | `string[]` | Conditional | `["AAPL","MSFT","GOOGL"]` | Ticker symbols to query. Required for all modes except `search`. Up to 200 per run. |
| `watchlistName` | `string` | No | `default` | Monitor / command-center modes. Names the cross-run state store; each name is an independent watchlist that diffs against its own prior run. |
| `watchlists` | `string[]` | Conditional | -- | Required for `compare-watchlists`. Two or more previously-run watchlist names to rank by health. No Finnhub calls, no key needed. |
| `searchQuery` | `string` | Conditional | -- | Company name or keyword for symbol lookup. Required only in `search` mode. |
| `resolution` | `string` (select) | No | `D` (Daily) | Candle resolution: `1`, `5`, `15`, `30`, `60` (minutes), `D`, `W`, `M`. |
| `daysBack` | `integer` | No | `30` | Days of history for candles, news, and earnings. Range 1 to 365. |
| `outputProfile` | `string` (select) | No | `standard` | `minimal` (summary + headline only), `standard` (data + intelligence), or `full` (everything incl. confidence). |
| `enablePeerComparison` | `boolean` | No | `false` | Benchmark each symbol against its Finnhub peers (metrics / snapshot modes). Adds ~7 calls per symbol. |
| `screen` | `object` | No | -- | Tag your symbols against criteria (metrics / snapshot): `marketCapTier`, `peMax`, `peMin`, `betaMax`, `dividendMin`. |

#### Input example

```json
{
    "apiKey": "YOUR_FINNHUB_API_KEY",
    "mode": "metrics",
    "symbols": ["AAPL", "MSFT", "TSLA"],
    "outputProfile": "standard"
}
```

#### Tips for best results

- **Start with quote mode** to verify your API key works before moving to other modes.
- **Batch your symbols** into a single run rather than running once per ticker. Rows stream into one clean dataset with a `recordType` per record.
- **Read the `summary` field first.** Every record carries a one-line plain-English summary you can drop straight into a Slack message, a sheet cell, or an LLM prompt.
- **Use `outputProfile: "minimal"`** when feeding an agent or a dashboard that only needs the verdict, not the raw fields.
- **For crypto symbols**, use the exchange-prefixed format like `BINANCE:BTCUSDT`.

***

### The intelligence layer

Every classification is deterministic and documented. None of it requires an extra API call; it is computed from the data the mode already returns.

| Mode | Raw data | Added intelligence |
|------|----------|--------------------|
| `monitor` | quote + metrics + news per symbol, vs prior run + accumulated history | one `recordType:"monitor"` record per symbol with current `state`, `transitions`, event lifecycle (`activeEvents`, `eventHistory`), `attentionLevel`, plus accumulated memory: `symbolContext`, `stability`, long-run `history`, `stateVelocity`. Three calls per symbol. |
| `command-center` | same as monitor, one digest | one `recordType:"command-center"` record for the whole watchlist: `needsAttention` (ranked), `topChanges`, `criticalEvents`, drift + regime + health. Per-symbol records suppressed; charged once. |
| `compare-watchlists` | stored state only (no fetch, no key) | one `recordType:"watchlist-comparison"` record ranking previously-run watchlists by health: `strongest`, `improving[]`, `deteriorating[]`. |
| `snapshot` | quote + profile + metrics + recent news, fused | one `recordType:"snapshot"` record per symbol with a composite `marketState` (trend / valuation / volatility / news tone / 52-week position) plus `events[]`, `triggers[]`, `marketNarrative`, `newsMomentum`, and `signalAlignment`. The run summary adds watchlist analytics: `watchlistType` archetype, `watchlistTheme` (e.g. semiconductors-strength), `breadth`, `dispersion`, `leadership`, and `watchlistOutliers`. Makes four Finnhub calls per symbol. |
| `quote` | price, change, day high/low, prev close | `intraday` (direction, move magnitude, day-range position, gap), `events[]` (gap-up/down, large-move, near-day-high/low) |
| `profile` | name, market cap, industry, IPO date | `marketCapTier` (mega/large/mid/small/micro/nano), `companyAgeYears` since IPO |
| `metrics` | P/E, P/B, P/S, beta, 52w high/low, yield | `valuation` (flags + market-median note), `pricePosition52w` + band, `volatilityClass`, `dividend`, `events[]` (near-52-week-high/low). Two calls/symbol (`/stock/metric` + `/quote`). Opt-in `peerContext` and `screen`. |
| `news` | headline, summary, source, URL | `sentiment` + `sentimentScore` (deterministic lexicon) and `headlineType` (earnings / guidance / analyst-action / partnership / lawsuit / regulatory / acquisition / product-launch / management / dividend / other) |
| `earnings` | EPS actual vs estimate | `surprise` (beat/miss/inline + magnitude), `epsBeat` / `revenueBeat` booleans, `events[]` (earnings-beat/miss, large-eps-surprise) |
| `candles` | OHLCV bars | per-symbol `technicals` (SMA20/50/200, RSI14, ATR14, trend state, momentum state, trend classification, multi-timeframe short/medium/long-term alignment) + series stats + candle `events[]`, in the run summary. Requires candle access (Finnhub premium). |

Every data record also carries `recordType`, `schemaVersion`, a one-line `summary`, and (in `full` profile) a `confidence` object describing how complete the underlying data was.

**Opt-in add-ons (metrics + snapshot):**

- `enablePeerComparison: true` benchmarks each symbol against its Finnhub peer group (which is its sector cohort), returning `peerContext`: `marketCapRank`, `peRank`, `valuationRelative` (premium/inline/discount vs the peer-median P/E), `volatilityRelative` (above/below-average vs the peer-median beta), `sizeRelative`, `peerMedianPe`, `peerMedianBeta`. Adds up to ~7 extra calls per symbol.
- `screen: { marketCapTier, peMax, peMin, betaMax, dividendMin }` tags each of your symbols with `screen.match` + `screen.reasons[]`. This screens the symbols you supply; it does not discover new symbols from a universe. Non-matches stay in the output with `match: false`.

***

### Monitor mode (cross-run change detection)

`mode: "monitor"` turns the actor into a scheduled watchlist monitor. It computes each symbol's descriptive `state` (trend / valuation / volatility / news tone / 52-week position), stores it in a named state store keyed by `watchlistName`, and on the next run **diffs against the last run** to tell you what changed.

Each `monitor` record carries:

- **`state`** — the current descriptive state.
- **`transitions`** — `[{ field, from, to }]` for each state dimension that changed since the last run (e.g. `valuation: normal → elevated`).
- **`newEvents` / `resolvedEvents`** — event flags that appeared or cleared since the last run.
- **`changes[]`** — high-level change tags (`valuation-state-changed`, `event-added`, …).
- **`attentionLevel`** — an operational priority (none / low / medium / high / critical) that escalates by *how much the state moved*, plus `attentionReasons[]`. This is a "look at this because it changed" signal, **not** a quality score.
- **`attentionAge` / `attentionFreshness`** — how many runs the attention level has been held, and a fresh / aging / stale tag, so a symbol stuck at "high" for five runs stops nagging.
- **`activeEvents` / `eventHistory`** — event lifecycle: `[{ event, ageDays, lifecycle, timesTriggered }]` plus a cumulative `eventHistory` map (`firstSeen`, `timesTriggered`, `maxSeverity`). Tells you the 52-week-high is 8 days old and has fired 7 times, not just that it fired.
- **`newsMomentum`** — improving / deteriorating / stable, from the recent vs prior half of the news window.

It also **accumulates memory** across runs:

- **`symbolContext`** — runs spent in each notable state: `{ runsTracked, runsAdvancing, runsElevatedValuation, runsPositiveNews, runsHighVolatility, totalStateTransitions, totalEventTriggers }`. "Elevated valuation for 18 of 22 runs" is something a stateless wrapper can't tell you.
- **`stateDuration` / `stateConfidence`** — how many runs the current trend / valuation / volatility / news-tone value has held, and a `tentative` / `moderate` / `strong` confidence in the classification derived from that persistence. "Elevated" is a level; "elevated for 71 runs (strong)" is a fact only memory can give you.
- **`stability`** — how often each classification has flipped, plus a `stable` / `settling` / `volatile` label per dimension, so you know whether `valuation: elevated` is persistent or this-run noise.
- **`history`** — long-run trajectory from a bounded rolling window: `{ trend, sentiment (improving/deteriorating/stable), volatility (rising/falling/stable) }`. Builds once enough runs are stored.
- **`stateVelocity`** — `rapidly-improving` / `improving` / `stable` / `deteriorating` / `rapidly-deteriorating`, combining this run's churn with the trajectory. A level tells you where it is; velocity tells you how fast it's moving.

#### Command-center mode

`mode: "command-center"` runs the same per-symbol computation and state persistence as monitor, but emits **one** `recordType: "command-center"` digest instead of per-symbol records (and charges once). It answers "what do I look at first?": `needsAttention` (symbols ranked by attention level), `topChanges`, `criticalEvents`, `marketNarrative`, plus the watchlist `regime`, `watchlistDrift`, `watchlistHealth`, `breadth`, `dispersion`, and `leadership`. The anti-data-blindness surface for a 50-symbol watchlist.

#### Portfolio intelligence (compare watchlists)

`mode: "compare-watchlists"` with `watchlists: ["growth-tech", "dividend", "crypto"]` compares multiple watchlists and identifies the **strongest**, the **improving**, and the **deteriorating** — built entirely from accumulated watchlist history, with no market-data calls and no API key required. Each named watchlist must have been run in monitor or command-center mode first; any that hasn't is reported honestly as `no-data`. This is the portfolio-level view that only an actor with persistent memory can produce.

The run summary rolls these up into `watchlistChanges` (how many symbols changed, total new/resolved events), a majority-vote `watchlistState` with a descriptive `regime` (risk-on / risk-off / neutral), `watchlistDrift` (the watchlist's aggregate state vs its own prior run: `{ direction, magnitude, transitions }`), cohort analytics (`breadth`, `dispersion`, `leadership`), and `watchlistOutliers` (the most-extreme symbol per dimension).

**First run is a baseline:** with no prior state, every symbol is `isFirstRun: true` with empty transitions, and the summary reads "baseline captured". Transitions and resolved events appear from the second run onward. Schedule the same `watchlistName` (hourly, daily) and each run reports only the deltas. State is bounded (up to 1000 symbols per watchlist) and persists across migrations and aborts.

***

### Output

![Sample monitor output — symbol, trend, valuation, attention level, new events, state velocity](https://apifyforge.com/readme-assets/ryanclinton-finnhub-stock-data/output-table.png)

#### Output example

**Quote mode** (`mode: "quote"`, `outputProfile: "standard"`):

```json
{
    "recordType": "quote",
    "schemaVersion": "2.0.0",
    "symbol": "AAPL",
    "currentPrice": 237.49,
    "change": 3.12,
    "changePercent": 1.33,
    "highPrice": 238.91,
    "lowPrice": 233.76,
    "openPrice": 234.55,
    "previousClose": 234.37,
    "timestamp": "2026-02-07T21:00:00.000Z",
    "intraday": {
        "changeDirection": "up",
        "moveMagnitude": "normal",
        "dayRangePosition": 72.5,
        "gapPct": 0.08
    },
    "summary": "AAPL up +1.33% at 237.49 (normal move, 234.37 prev close)."
}
```

**Snapshot mode** (`mode: "snapshot"`, `enablePeerComparison: true`) — the composite cross-signal record:

```json
{
    "recordType": "snapshot",
    "symbol": "AAPL",
    "price": { "currentPrice": 301.54, "changePercent": -1.89, "dayHigh": 317.4, "dayLow": 301.17 },
    "profile": { "name": "Apple Inc", "industry": "Technology", "marketCapTier": "mega" },
    "valuation": { "flags": ["pe-above-market"], "note": "Trades at 37.1x trailing earnings, above the long-run market median of ~20x." },
    "volatilityClass": "market-like",
    "pricePosition52wBand": "near-high",
    "marketState": { "trend": "declining", "valuation": "elevated", "volatility": "normal", "newsTone": "mixed", "pricePosition": "near-high" },
    "events": ["near-52-week-high"],
    "peerContext": { "marketCapRank": 1, "peRank": 3, "valuationRelative": "premium", "sizeRelative": "largest", "peerMedianPe": 29.09 },
    "summary": "AAPL (Apple Inc): declining, valuation elevated, volatility normal, news mixed, near-high of 52w range."
}
```

**Metrics mode** (`mode: "metrics"`):

```json
{
    "recordType": "metrics",
    "schemaVersion": "2.0.0",
    "symbol": "MSFT",
    "peRatio": 34.2,
    "pbRatio": 12.1,
    "psRatio": 13.4,
    "dividendYield": 0.72,
    "beta": 0.91,
    "week52High": 468.35,
    "week52Low": 309.45,
    "valuation": {
        "peRatio": 34.2,
        "flags": ["pe-above-market", "pb-above-market", "ps-very-elevated"],
        "note": "Trades at 34.2x trailing earnings, above the long-run market median of ~20x."
    },
    "pricePosition52w": 78.1,
    "pricePosition52wBand": "upper-half",
    "volatilityClass": "market-like",
    "dividend": "low",
    "summary": "MSFT: Trades at 34.2x trailing earnings, above the long-run market median of ~20x. Volatility market-like (beta 0.91). low dividend."
}
```

**Summary record** (emitted once per run, also mirrored to the `SUMMARY` key-value store record):

```json
{
    "recordType": "summary",
    "schemaVersion": "2.0.0",
    "mode": "quote",
    "dataRecords": 3,
    "symbolsRequested": 3,
    "symbolsFailed": 0,
    "advancers": 2,
    "decliners": 1,
    "avgChangePercent": 0.74,
    "largestMove": { "symbol": "TSLA", "changePercent": -2.4 },
    "truncated": false
}
```

#### Record types

| `recordType` | When | Contents |
|--------------|------|----------|
| `quote` / `candle` / `profile` / `news` / `search` / `earnings` / `metrics` | per data row | the fetched data plus its mode-specific intelligence |
| `snapshot` | per symbol (snapshot mode) | composite record: price + profile + valuation + newsTone + `marketState` + `events[]` + `triggers[]` + `signalAlignment` (+ optional peerContext / screen) |
| `monitor` | per symbol (monitor mode) | current `state` + `transitions` + event lifecycle + `attentionLevel` + `symbolContext` + `stability` + `stateDuration` + `stateConfidence` + `history` + `stateVelocity` |
| `command-center` | one per run (command-center mode) | `needsAttention` + `topChanges` + `criticalEvents` + watchlist drift / regime / `watchlistHealth` |
| `watchlist-comparison` | one per run (compare-watchlists mode) | `strongest` + `improving[]` + `deteriorating[]` + per-watchlist health |
| `summary` | once per run | counts, distributions, and the headline digest; mirrored to the `SUMMARY` KV record |
| `error` | on failure | `failureType` (auth / rate\_limit / blocked / no-data / timeout / upstream-error / invalid-input), a `suggestedAction` (check-api-key / upgrade-finnhub-plan / reduce-symbol-count / …), a human-readable `message`, and (for partial runs) a `symbolErrors` list |

***

### Use in Dify

This actor returns **classified, enum-bearing JSON**, which is exactly the shape [Dify](https://dify.ai/) if/else nodes branch on. A competitor pointed at the same Finnhub endpoint returns raw numbers your workflow has to interpret; this returns `sentiment`, `surprise.result`, `marketCapTier`, `volatilityClass`, `pricePosition52wBand`, and a `recordType` discriminator your node can route on directly.

**Actor ID:** `tsl8SzioHu42ZEK6m`

Sample input for a news-sentiment watch:

```json
{
    "apiKey": "YOUR_FINNHUB_API_KEY",
    "mode": "news",
    "symbols": ["TSLA", "NVDA"],
    "daysBack": 3,
    "outputProfile": "minimal"
}
```

In a Dify workflow, an **if/else node** routes on the stable enums without parsing any prose:

- `sentiment == "negative"` -> branch to a Slack alert node ("negative coverage detected for {{symbol}}")
- `sentiment == "positive"` -> branch to a digest node
- else -> ignore

The same pattern works on other modes: route on `surprise.result == "beat"` (earnings), `pricePosition52wBand == "near-high"` (metrics), `intraday.moveMagnitude == "large"` (quotes), `eventSeverity == "high"` or `attentionLevel == "critical"` (any mode) to alert only on what matters, `recordType == "monitor"` + a non-empty `transitions` array to fire only when a watchlist symbol *changed state*, or `recordType == "error"` with `failureType == "blocked"` to fall back to a different source. Because every classification is deterministic, the same input always routes the same way, no flaky LLM calls in your branch logic. The one-line `summary` field is usable verbatim as the human-readable body of any notification, no LLM rewriting required.

***

### API & Integration

You can call this actor programmatically using the Apify API. The actor ID is `tsl8SzioHu42ZEK6m`.

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("tsl8SzioHu42ZEK6m").call(run_input={
    "apiKey": "YOUR_FINNHUB_API_KEY",
    "mode": "quote",
    "symbols": ["AAPL", "MSFT", "TSLA"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("recordType") == "quote":
        print(item["summary"])
```

#### JavaScript

```javascript
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "YOUR_APIFY_TOKEN" });

const run = await client.actor("tsl8SzioHu42ZEK6m").call({
    apiKey: "YOUR_FINNHUB_API_KEY",
    mode: "metrics",
    symbols: ["AAPL"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.recordType === "metrics").map((i) => i.summary));
```

#### Integrations

- **Zapier** -- trigger the actor and push stock data to Google Sheets, Slack, Airtable, or 5,000+ other apps using the [Apify Zapier integration](https://zapier.com/apps/apify/integrations).
- **Make (Integromat)** -- build workflows that branch on `sentiment`, `surprise.result`, or `pricePosition52wBand` and route alerts.
- **Dify** -- see [Use in Dify](#use-in-dify) above.
- **Google Sheets** -- export results directly for charting, pivot tables, or watchlist dashboards.
- **Webhooks** -- configure Apify webhooks to notify your own endpoint when a run completes.

***

### How it works

1. The actor reads your input and validates the selected mode.
2. It routes requests to the appropriate Finnhub API v1 endpoint, sending your key as a request header.
3. For symbol-based modes it iterates each ticker, retrying with backoff on rate limits and transient errors, and isolating per-symbol failures so one bad ticker never wipes the batch.
4. Each response is normalised (Unix timestamps to ISO 8601, standardised field names) and passed through the deterministic classification layer.
5. Each symbol's rows stream into the dataset as they resolve. A run summary record and `SUMMARY` KV mirror are written at the end.

***

### Performance & cost

This actor is billed pay-per-event at $0.002 per output record retrieved (the run summary record is not charged). The Finnhub free tier allows 60 API calls per minute, which comfortably supports batch queries of up to ~50 symbols per run; the actor retries with backoff if you approach the limit. Most runs use 256 MB of memory and finish in under 30 seconds.

| Scenario | Symbols | Records | Approx. PPE |
|----------|---------|---------|-------------|
| Daily quote check | 5 | 5 | $0.010 |
| Weekly metrics snapshot | 20 | 20 | $0.040 |
| News scan (3 days) | 10 | ~80 | ~$0.160 |

PPE charges are for output records only and exclude Apify platform compute. The actor never charges for empty or failed results.

***

### What this does not do

- **No buy / sell / hold signals and no price targets.** The classification layer is descriptive (it tells you a stock trades above the market-median P/E, or that news coverage skews negative); it does not tell you what to do about it. This is a deliberate design choice, not a missing feature.
- **No composite "health score" or stock-quality rating.** A single 0-100 score blending valuation, profitability, and stability reads as a buy signal no matter how it is labelled, so the actor deliberately does not emit one. It gives you the per-dimension descriptive classifications (each a fact) and stops there.
- **The screener tags your symbols; it does not discover them.** `screen` flags which of the symbols you supplied match your criteria. Finnhub's free tier has no "list every large-cap with P/E under 25" universe endpoint, so the actor does not pretend to scan the whole market.
- **Not investment advice.** Data is provided as-is from Finnhub. Verify critical data points against primary sources before any financial decision.
- **Not a real-time tick feed.** This actor makes point-in-time API calls; it does not hold a websocket connection. For continuous streaming, use Finnhub's websocket API directly.
- **Not a substitute for a paid Finnhub plan's coverage.** Some endpoints (notably intraday and historical stock candles) and some fundamentals are restricted on Finnhub's free tier and return a `403`; the actor reports this clearly on the error record rather than failing silently. Modes that work on free keys (quotes, profiles, metrics, news, search) return real data.

***

### FAQ

**Do I need a paid Finnhub plan?**
The free Finnhub key covers quotes, company profiles, financial metrics, company news, and symbol search. Some endpoints, notably historical stock candles, were moved to Finnhub's premium tiers and return a `403` on free keys; when that happens the actor reports it on the error record with a clear message. Sign up at [finnhub.io/register](https://finnhub.io/register).

**What happens if I run without an API key?**
The actor logs a setup message pointing you to finnhub.io/register and exits cleanly. No API calls, no errors, no charge.

**What is the `summary` field?**
A one-line, plain-English description of each record, built deterministically from the data. It is safe to paste into a Slack message, a sheet, or an LLM prompt without joining other fields.

**Is the classification investment advice?**
No. Every classification is descriptive and deterministic (for example, "trades above the market-median P/E" or "news sentiment negative"). The actor emits no buy/sell/hold signal. See [What this does not do](#what-this-does-not-do).

**Can I track crypto and stocks in the same run?**
Yes. Mix regular tickers (`AAPL`) with exchange-prefixed crypto symbols (`BINANCE:BTCUSDT`) in the same list.

**Why do some metrics or profile fields show null?**
Finnhub may not have a given metric for every company, particularly smaller-cap, international, or recently listed stocks. The actor sets the field to `null` and lowers the record's completeness `confidence` rather than inventing a value.

***

### Related actors

| Actor | Description |
|-------|-------------|
| [CoinGecko Crypto Data](https://apify.com/ryanclinton/coingecko-crypto-data) | Cryptocurrency prices, market cap, volume, and historical data covering 10,000+ coins. |
| [SEC EDGAR Filing Analyzer](https://apify.com/ryanclinton/sec-edgar-filing-analyzer) | Search and analyse SEC filings including 10-K, 10-Q, and 8-K reports. |
| [SEC Insider Trading](https://apify.com/ryanclinton/sec-insider-trading) | Track insider buying and selling from SEC Form 4 filings across all public companies. |
| [Congressional Stock Trade Tracker](https://apify.com/ryanclinton/congress-stock-tracker) | Monitor stock trades disclosed by US Congress members under the STOCK Act. |
| [BLS Economic Data](https://apify.com/ryanclinton/bls-economic-data) | US labor and price statistics with deterministic economic-signal classification. |

# Actor input Schema

## `apiKey` (type: `string`):

Free API key from finnhub.io (sign up at https://finnhub.io/register)

## `mode` (type: `string`):

What data to retrieve

## `symbols` (type: `array`):

Stock ticker symbols (e.g. AAPL, MSFT, TSLA). For crypto use format BINANCE:BTCUSDT

## `searchQuery` (type: `string`):

Search term for symbol lookup (search mode only)

## `resolution` (type: `string`):

Time resolution for candles

## `daysBack` (type: `integer`):

Number of days of historical data for candles/news

## `outputProfile` (type: `string`):

How much detail per record. 'standard' (default) returns all data + intelligence; 'full' also includes the confidence object and extraction timestamps; 'minimal' returns only the symbol, the one-line summary, and the headline classification for each record.

## `enablePeerComparison` (type: `boolean`):

Benchmark each symbol against its Finnhub peers: market-cap and P/E ranks plus a descriptive valuation-vs-peers position. Adds up to ~7 extra Finnhub calls per symbol (peers list + each peer's metrics), so it consumes more of your free-tier rate budget. Off by default.

## `screen` (type: `object`):

Tag each of YOUR symbols with whether it matches these criteria (this screens the symbols you supply; it does not discover new symbols from a universe). Keys: marketCapTier (mega/large/mid/small/micro/nano), peMax, peMin, betaMax, dividendMin. Non-matching symbols stay in the output with screen.match=false.

## `watchlistName` (type: `string`):

Names the cross-run state store for monitor and command-center modes. Each name is an independent watchlist: run the same name on a schedule and the actor diffs each run against the last, emitting state transitions, event lifecycle, attention, drift, and accumulated memory. The first run for a name captures a baseline (every symbol is NEW). Defaults to 'default'.

## `watchlists` (type: `array`):

Two or more watchlist names to rank by health. Each must have been run in monitor or command-center mode first (this mode reads stored state and makes no Finnhub calls). Returns the strongest watchlist plus improving/deteriorating lists.

## Actor input object example

```json
{
  "mode": "quote",
  "symbols": [
    "AAPL",
    "MSFT",
    "GOOGL"
  ],
  "resolution": "D",
  "daysBack": 30,
  "outputProfile": "standard",
  "enablePeerComparison": false,
  "screen": {
    "marketCapTier": "large",
    "peMax": 30,
    "dividendMin": 1
  },
  "watchlistName": "default",
  "watchlists": [
    "growth-tech",
    "dividend"
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "symbols": [
        "AAPL",
        "MSFT",
        "GOOGL"
    ],
    "screen": {
        "marketCapTier": "large",
        "peMax": 30,
        "dividendMin": 1
    },
    "watchlists": [
        "growth-tech",
        "dividend"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/finnhub-stock-data").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "symbols": [
        "AAPL",
        "MSFT",
        "GOOGL",
    ],
    "screen": {
        "marketCapTier": "large",
        "peMax": 30,
        "dividendMin": 1,
    },
    "watchlists": [
        "growth-tech",
        "dividend",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/finnhub-stock-data").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "symbols": [
    "AAPL",
    "MSFT",
    "GOOGL"
  ],
  "screen": {
    "marketCapTier": "large",
    "peMax": 30,
    "dividendMin": 1
  },
  "watchlists": [
    "growth-tech",
    "dividend"
  ]
}' |
apify call ryanclinton/finnhub-stock-data --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ryanclinton/finnhub-stock-data"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/tsl8SzioHu42ZEK6m/builds/kB1TfVKFvwMbXoEm0/openapi.json
