Skip to main content

Stock News Timeline in Python: Merging Hourly Heat, Opinion Summaries and Media Hits into One Event Log (CATL Example)

Updated 8 min readMarkdown

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_time in each response envelope carries +08:00;
  • topic_time on /opinions records is Beijing time;
  • /news/hits rows have a publish time (check the exact column name in the OpenAPI schema);
  • hourly_24h covers 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 /opinions summary field. Do not backfill articles from elsewhere.
  • Hourly buckets cover only the last 24 hours. For older hours, count /opinions records by topic_time yourself and label that series separately; it is not the same definition as hourly_24h.
  • Panel weighting is not point-in-time. weighted_heat on /panel/daily uses current source scores. For a strict historical weighted view use /hot/weighted with as_of_date.

Quality Checks Before You Save

  • Timezone: spot-check three rows; all should end in +08:00.
  • Truncation: if /opinions or /news/hits returned exactly limit rows, shorten days/hours, split by date range, or split news by src.
  • Plausibility: an hour's hourly_24h count and that hour's record count should be the same order of magnitude; if not, note the only_this_stock setting.
  • 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 热度监测工具。

No comments yet. Be the first to comment.