Stock News Timeline in Python: Merging Hourly Heat, Opinion Summaries and Media Hits into One Event Log (CATL Example)
When a stock suddenly gets talked about, the first useful question is not "why" but "in what order did things happen?" Which hour did discussion pick up? Did a media flash come before or after the first institutional comment? How broad was participation that day compared with the past month?
Answering that means building a stock news timeline: one table, sorted by time, where every row says which data feed produced it and what it counted. This tutorial builds one in Python from Lava's read-only heat endpoints, using CATL (300750.SZ) purely as the query example. The result is an auditable event log that a quant script or an AI agent can reproduce on demand. All readings shown are illustrative.
Why a Single Heat Number Is Not Enough
A heat score tells you how much a stock was discussed in a window. It does not tell you when inside that window, who was involved, or whether media coverage and institution/KOL commentary moved together. For quant research and institution/KOL heat monitoring, the ordering of events is often the more useful raw material: it lets you align attention with your own price or volume data later, on your own terms.
Lava splits that information across several feeds. None of them is a timeline on its own, but together they are.
The Five Feeds and Their Time Granularity
| Role in the timeline | MCP tool | HTTP path | Granularity |
|---|---|---|---|
| Skeleton: which hour heated up | get_stock_heat_windows |
/heat/windows |
six rolling windows plus hourly_24h |
| Anchors: what was being said | get_stock_news_heat |
/opinions |
per record (topic_time) |
| Media anchors: who named the stock | get_news_stock_hits |
/news/hits |
per news item (publish time) |
| Breadth: how many distinct sources | get_agency_coverage |
/coverage/stocks |
daily |
| Background: where today sits historically | get_stock_heat_panel |
/panel/daily |
calendar day |
All five accept the same identifier, either ts_code or stock_name. Use ts_code. Short names change and occasionally collide; codes join cleanly with your own market data.
Design Rule: Two Clocks, Never Mixed
The biggest source of broken timelines is mixing granularities. Keep two indexes:
- an hourly index for things that happen at a moment (opinion records, news hits, hourly buckets);
- a daily index for things that are only defined per day (coverage counts, daily panel rows).
Then normalize every timestamp to timezone-aware Asia/Shanghai:
meta.server_timein each response envelope carries+08:00;topic_timeon/opinionsrecords is Beijing time;/news/hitsrows have a publish time (check the exact column name in the OpenAPI schema);hourly_24hcovers the 24 hours before the call, with bucket boundaries as returned;- coverage and panel dates are Asia/Shanghai calendar days.
Do not spread a daily coverage number across hours. Attach it to the date and join it at read time.
Step-by-Step: Building the Stock News Timeline in Python
1. Shared request helper
# timeline.py - illustrative; base URL and token are placeholders
import os, json, requests, pandas as pd
BASE = os.environ.get("LAVA_BASE", "<YOUR_LAVA_BASE>/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "<YOUR_TOKEN>")}
CODE = "300750.SZ"
TZ = "Asia/Shanghai"
calls = [] # audit trail
def get(path, **params):
resp = requests.get(BASE + path, params=params, headers=HEADERS, timeout=60).json()
assert resp["ok"], (path, resp.get("code"))
calls.append({"path": path, "params": params,
"request_id": resp["meta"]["request_id"],
"server_time": resp["meta"]["server_time"]})
return resp["data"]
def rows(data):
return data.get("list", []) if isinstance(data, dict) else data
Every call appends its parameters, request_id and server_time to calls. That list is what makes the timeline auditable later.
2. The hourly skeleton
win = get("/heat/windows", ts_code=CODE, min_score=1050)
hourly = win.get("hourly_24h", []) # bucket column names: see OpenAPI
min_score=1050 only moves the threshold for the "high-score institutions/KOLs" column. Total mentions are unaffected.
3. Opinion records as anchors
ops = get("/opinions", ts_code=CODE, days=3, only_this_stock=0, limit=200)
records = rows(ops)
Each record carries opinion_id, topic_time, agency_id, agency_score, stocks, concepts, a summary (up to 120 characters) and detail (up to 600). only_this_stock=0 returns records where the stock is the first-mentioned name; =1 keeps records that mention only this stock. The two give different counts, so write down which one the timeline used.
4. Media hits
news = get("/news/hits", ts_code=CODE, hours=72, limit=200)
With a stock specified, hours defaults to 72 and goes up to 168; src pins a single outlet. Rows include news_id, publish time, outlet and matched stocks. There is no headline or body text.
5. Daily breadth and background
cov = get("/coverage/stocks", ts_code=CODE, days=30, min_score=1050)
panel = get("/panel/daily", type="stock", codes=CODE,
**{"from": "2026-08-17", "to": "2026-11-14"})
Coverage returns cov_7d, cov_30d, new_cov_cum and high_score_ratio_30d as counts only. The panel returns mentions, agencies, weighted_heat and is_first_mention_day, for ranges of at most 92 days.
6. Merge into one event log
def to_ts(value):
ts = pd.Timestamp(value)
return ts.tz_convert(TZ) if ts.tzinfo else ts.tz_localize(TZ)
events = []
for r in records:
events.append({"ts": to_ts(r["topic_time"]), "feed": "opinions",
"value": r.get("agency_score"), "ref": r.get("opinion_id")})
for n in rows(news):
# publish-time and outlet column names assumed; confirm in OpenAPI
events.append({"ts": to_ts(n.get("pub_time")), "feed": "news",
"value": n.get("src"), "ref": n.get("news_id")})
log = pd.DataFrame(events).sort_values("ts").reset_index(drop=True)
log["hour"] = log["ts"].dt.floor("h")
log.to_csv("catl_timeline.csv", index=False)
json.dump(calls, open("catl_timeline_calls.json", "w"), ensure_ascii=False, indent=2)
The to_ts helper avoids a classic bug: calling tz_localize on a string that already has +08:00 either raises or double-shifts. Left-join the hourly buckets on hour and each hour now lists its own records and media hits.
What the Output Looks Like
| Time (Asia/Shanghai) | Feed | Content |
|---|---|---|
| 11-13 09:00–10:00 | /heat/windows |
4 mentions in the hour |
| 11-13 10:12 | /news/hits |
Outlet A, news_id=<id> |
| 11-13 10:20 | /opinions |
agency_score=1080, summary: <summary> |
| 11-13 10:00–11:00 | /heat/windows |
17 mentions in the hour |
| 11-13 10:47 | /news/hits |
Outlet B |
| 11-13 (day) | /coverage/stocks |
cov_7d=46, cov_30d=131 |
The table answers "around the hour discussion rose, when did media hits and opinion records appear?" It deliberately does not answer "why" or "what next".
Boundaries to Respect
- No news text. The only prose in the timeline comes from the
/opinionssummaryfield. Do not backfill articles from elsewhere. - Hourly buckets cover only the last 24 hours. For older hours, count
/opinionsrecords bytopic_timeyourself and label that series separately; it is not the same definition ashourly_24h. - Panel weighting is not point-in-time.
weighted_heaton/panel/dailyuses current source scores. For a strict historical weighted view use/hot/weightedwithas_of_date.
Quality Checks Before You Save
- Timezone: spot-check three rows; all should end in
+08:00. - Truncation: if
/opinionsor/news/hitsreturned exactlylimitrows, shortendays/hours, split by date range, or split news bysrc. - Plausibility: an hour's
hourly_24hcount and that hour's record count should be the same order of magnitude; if not, note theonly_this_stocksetting. - Audit trail: five
request_ids saved. Five calls are billed five times at 0.005 yuan each; non-2xx responses are not billed.
Agent Prompt Version
Build a 72-hour news timeline for ts_code=<code>:
- call get_stock_heat_windows, get_stock_news_heat(days=3, limit=200),
get_news_stock_hits(hours=72), get_agency_coverage(days=30)
- convert every time to Asia/Shanghai; output a table: time / feed / count or id
- quote only the summary field; list news as outlet + time
- end with each call's request_id and server_time
- describe ordering only; no directional interpretation
FAQ
Why do I get fewer /opinions rows than data.heat.mentions?
The list is capped by limit (max 200) while the summary counts the whole range. If the row count equals the limit, narrow the window.
Does it matter whether the media hit or the opinion came first? Record it as a fact, but the two feeds are collected and matched differently. Order shows when each appeared, not causation.
Can I run this for a basket?
Yes, but calls grow linearly with the number of stocks. For the daily background, /panel/daily accepts up to 20 codes in one call.
Conclusion
A stock news timeline is five read-only feeds stitched together: hourly heat as the skeleton, opinion summaries and media hits as anchors, coverage and the daily panel as context. Normalize time first, keep hourly and daily data apart, log every request, and the result is a reproducible event log you can join with your own quant data. If you want to see what the monitored view of this stock looks like before writing code, open the CATL sentiment monitor.
中文简介:本文用 Python 把熔岩的逐小时热度(/heat/windows)、个股消息面摘要(/opinions)、新闻点名(/news/hits)、覆盖度与日频面板合并成一张按时间排序、可审计的单股消息面时间线,以宁德时代 300750.SZ 为查询示例,重点讲时区统一、小时/日两套索引、截断检查与调用元数据留存。熔岩是量化交易必备的机构/大V 热度监测工具。
Comments
Join the discussionNo comments yet. Be the first to comment.