Google Trends
Google search interest over time for the full US equity universe by ticker, back two decades. Scores are 0-100 rescaled within each query's own time window, so filter to one window_key (and resolution) before comparing them: recent_daily is a rolling 30-day daily series, history_monthly two decades of monthly points.
google_trends — the dataset name to pass to the Obscura API.
What one row means
One Google Trends search-interest score for one ticker's search phrase (ticker + " stock"), captured from the TIMESERIES widget of the unofficial Trends web-app API and geo-scoped (default US). # The one thing to understand before using `score` A Trends score is NOT an absolute quantity. Every query is rescaled 0-100 *within its own time window*: the busiest point in that window is 100 and everything else is a fraction of it. So a `100` from a 30-day query ("the busiest day of the last month") and a `100` from a ten-year query ("the busiest month of the last decade") are different numbers, and averaging or concatenating them is a category error. This table therefore keys on the WINDOW as well as the ticker and the day. `window_key` names one normalization frame, `resolution` records the point spacing Google actually served for it, and `window_start`/`window_end` give the exact span that was scaled to 0-100. Filter to a single `window_key` before comparing scores. The collector runs several tiers over the same tickers — a rolling daily window for currency, and long windows for a decade of depth — and they coexist here as separate, individually-comparable series rather than being silently blended into one column. The stored `score` is always the RAW value Google returned for its own window. Nothing in this pipeline rescales or stitches windows together; a continuous cross-window series is a DERIVED product and must be built downstream, never by overwriting these source values.
One row per (ticker, window_key, resolution, trend_date) — one score per ticker per normalization window per point spacing per point; window_key + resolution together are what keep a daily series and a monthly series for the same ticker from colliding..
Point-in-time availability
Every Obscura dataset carries available_date: the calendar day the publisher made the row
available, day-of, with no session rounding. It is the one column a backtest filters on, and it means the
same thing on every dataset in the catalog.
For google_trends: snapshot: available_date = COALESCE(captured_date, synced_at::date) (STORED generated column) — the day this pipeline SCRAPED the window the point came from; Google Trends has no fixed publication schedule, it's a continuously-refreshed API, so the capture day is the true public-availability basis. trend_date can never serve: one scrape returns a whole window of past days at once.
Known limitation
Sampled and rescaled by Google per request; not a complete census of queries.
Refresh cadence
Obscura refreshes google_trends daily — the most frequent scheduled job that re-collects or re-exports it. This is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when a row became public is recorded per row in available_date.
Schema — 12 columns
The full public column list for google_trends, with the meaning of every field. The same
schema is served unauthenticated at https://api.obscura.trade/v1/catalog/google_trends.
| Column | Type | Description |
|---|---|---|
| ticker | text · not null | Equity ticker from the collector's configured universe — `all` (every ticker in the reference equities universe, ~21.9k including delisted), `live` (not delisted, ~6.3k), or `earnings` (tickers with an earnings release in the corporate-events reference data inside earnings_lookback_days). Part of the composite primary key with window_key and trend_date. The keyword actually queried is this ticker plus keyword_suffix (default " stock") to avoid collisions on word-like tickers (A, IT, ALL...). |
| window_key | text · not null | WHICH NORMALIZATION WINDOW this score belongs to — the discriminator that makes scores from different query windows safe to store in one table, and part of the composite primary key. Trends rescales every query 0-100 within that query's own window, so two scores are only comparable when this column matches. Values are the collector's tiers: `recent_daily` (rolling ~30-day daily window, refreshed every run), `history_monthly` (one ~10-year window per ticker, monthly points), `history_weekly` (5-year windows, weekly points) and `deep_daily:<window_start>` — the multi-window tiers carry the window's own start date in the key because each window was normalized independently and must not be pooled with its neighbours. ALWAYS filter or group by this column before comparing, averaging, or charting `score`. |
| trend_date | date · not null | The search-interest DAY the Trends widget attributes the score to (parsed in parse_timeline from timelineData[].time, a Unix-seconds string converted to a UTC date). Part of the composite primary key with ticker and window_key. Its MEANING depends on `resolution`: with DAY it is that calendar day, with WEEK it is the first day of the week the score covers, with MONTH the first day of the month, with YEAR the first day of the year — Google returns one timestamp per bucket, not per day, once the window is long enough to force coarser spacing. The ON CONFLICT upsert target is (ticker, window_key, resolution, trend_date), so re-fetching the same window overwrites that window's points in place; there is no history of revisions to a given point. |
| resolution | text · not null | Point spacing Google actually SERVED for this window, taken from the `resolution` field it echoes back in the TIMESERIES widget request — recorded as observed, never as assumed. One of `DAY`, `WEEK`, `MONTH`, `YEAR`. Google chooses it from the window LENGTH and ignores any client preference; measured against the live API on 2026-07-26, a 30-day and a 91-day window return DAY, a 273-day and a 5-year window return WEEK, a 10-year window returns MONTH, and `all` (2004-present) returns YEAR. This is what tells a consumer whether `trend_date` is a day or the first day of a longer bucket, and it is why a decade of DAILY history costs ~14 requests per ticker while a decade of monthly history costs one. Part of the composite primary key: the multi-window tiers anchor their windows to a fixed grid so the newest window is short while it fills, and Google serves it DAY points until it crosses ~268 days and then flips it to WEEK. A weekly bucket's timestamp is itself a real day, so without this column in the key that flip would overwrite the daily rows it lands on with differently-scaled weekly ones. |
| window_start | date · not null | First day of the span that was normalized to 0-100 to produce this row's `score`, i.e. the start of the query window (not of this point). Stored so the normalization frame is visible in the data rather than inferred from window_key, and so a rolling tier's frame is auditable after the fact — `recent_daily` re-queries a window that slides forward every day, and this records where it actually was for the run that wrote the row. |
| window_end | date · not null | Last day of the span that was normalized to 0-100 to produce this row's `score` — the end of the query window. With window_start it bounds the frame inside which `score = 100` means "the peak"; a point near either edge of a long window is the usual place to look when two tiers disagree about a ticker's shape. |
| keyword | text · not null | The literal search phrase sent to Trends for this row, i.e. `ticker + keyword_suffix` (e.g. "AAPL stock"). Stored per row (not just per ticker) so the phrase actually queried is auditable if keyword_suffix or geo changes between runs; updated on every upsert via the OnConflict update_columns list. |
| score | integer · not null | Google Trends interest-over-time index, 0-100, parsed VERBATIM from widgetdata/multiline's `value[0]` for this timeline point. This is the RAW per-window value: Google recomputes the 0-100 axis for every query, so 100 always means "the peak of THIS row's (ticker, window_key) window" and nothing more. It is comparable across points that share a ticker AND a window_key; it is NOT comparable across tickers, across window_keys, or across the independently-normalized windows of a multi-window tier. A 0 means negligible interest relative to that window's own peak, not a missing observation. Nothing in this pipeline rescales, splices, or overlap-corrects this number — a continuous series stitched across windows is a derived artefact and belongs in a downstream table, never on top of this column. |
| geo | text · not null | Two-letter (or empty for worldwide) geo scope of the query, from GoogleTrendsArgs.geo (default "US"). Passed verbatim into the Trends explore request's comparisonItem.geo and stored per row so a future multi-geo run is distinguishable, though the current default schedule only ever populates "US". |
| is_partial | boolean | Provisional/finality flag for this point's score, from the timeline point's raw `isPartial` key (`default.timelineData[].isPartial`). Google marks the most-recent trailing point(s) `isPartial = true` while it is still aggregating that bucket and will revise the `score` on a later fetch; settled buckets omit the key, captured here as `false`. Recorded at write time so a point-in-time backtest can distinguish a settled score from one that was still provisional when the row was written — a re-fetch of the same window flips it back to `false` (and corrects `score`) once the bucket settles. Note that on the long history tiers the trailing partial point is a whole partial MONTH or WEEK, not a partial day. |
| captured_date | date | The UTC calendar day the window this point came from was SCRAPED from Google — the day the score became knowable to us. `trend_date` cannot answer this: one `today 1-m` request returns 31 past days in a single scrape, so a point dated 30 days ago was still first observed today. Set from `Utc::now().date_naive()` on a live run and from `BronzeObject::captured_day()` on a bronze replay, so re-parsing an old capture keeps its original date. NULL on rows written before migration 000175 AND on rows written by a collector that does not yet set it, in which case `available_date` falls back to `synced_at::date` — the value it has always had. |
| available_date | date · not null | PUBLIC-availability date = `COALESCE(captured_date, (synced_at AT TIME ZONE 'UTC')::date)` (snapshot basis). DB-generated (STORED), read-only; the point-in-time column to filter/join on — NEVER the search-interest day (that's trend_date/occurred_on). |
Access google_trends
Two delivery paths, one identifier. Both require an Obscura account and an active subscription; the catalog entry and the schema above are public.
import obscura
client = obscura.Client("obs_live_…")
df = client.query(
dataset="google_trends",
symbols=["NVDA", "AAPL"],
start="2024-01-01",
)
Create a free account Browse all 95 datasets
Frequently asked questions
What is in the google_trends dataset?
Google search interest over time for the full US equity universe by ticker, back two decades. Scores are 0-100 rescaled within each query's own time window, so filter to one window_key (and resolution) before comparing them: recent_daily is a rolling 30-day daily series, history_monthly two decades of monthly points. One Google Trends search-interest score for one ticker's search phrase (ticker + " stock"), captured from the TIMESERIES widget of the unofficial Trends web-app API and geo-scoped (default US). # The one thing to understand before using `score` A Trends score is NOT an absolute quantity. Every query is rescaled 0-100 *within its own time window*: the busiest point in that window is 100 and everything else is a fraction of it. So a `100` from a 30-day query ("the busiest day of the last month") and a `100` from a ten-year query ("the busiest month of the last decade") are different numbers, and averaging or concatenating them is a category error. This table therefore keys on the WINDOW as well as the ticker and the day. `window_key` names one normalization frame, `resolution` records the point spacing Google actually served for it, and `window_start`/`window_end` give the exact span that was scaled to 0-100. Filter to a single `window_key` before comparing scores. The collector runs several tiers over the same tickers — a rolling daily window for currency, and long windows for a decade of depth — and they coexist here as separate, individually-comparable series rather than being silently blended into one column. The stored `score` is always the RAW value Google returned for its own window. Nothing in this pipeline rescales or stitches windows together; a continuous cross-window series is a DERIVED product and must be built downstream, never by overwriting these source values.
How do I avoid look-ahead bias with google_trends?
Filter on google_trends.available_date, the day the publisher made the row public. For this dataset that date is derived as follows — snapshot: available_date = COALESCE(captured_date, synced_at::date) (STORED generated column) — the day this pipeline SCRAPED the window the point came from; Google Trends has no fixed publication schedule, it's a continuously-refreshed API, so the capture day is the true public-availability basis. trend_date can never serve: one scrape returns a whole window of past days at once. A query of the form WHERE available_date <= '<as-of date>' never sees a row before it existed.
In what formats can I get google_trends?
As a Parquet bulk export (POST https://api.obscura.trade/v1/download) or as JSON from the typed query API (POST https://api.obscura.trade/v1/query), both with dataset="google_trends". The column schema is public at https://api.obscura.trade/v1/catalog/google_trends.
Can I filter google_trends by company or symbol?
Yes. google_trends carries ticker, the column the API's symbols filter resolves against.
Is google_trends complete?
Not yet, and the limitation is declared rather than hidden: Sampled and rescaled by Google per request; not a complete census of queries.
How often is google_trends updated?
Obscura refreshes google_trends on a daily schedule — that is the most frequent scheduled job that re-collects or re-exports the table. It is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when the publisher makes a row available is described by the availability rule above, and is recorded per row in available_date.