<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dustin Liang's Blog]]></title><description><![CDATA[Dustin Liang's Blog]]></description><link>https://dustinliang.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 15 Nov 2026 20:11:27 GMT</lastBuildDate><atom:link href="https://dustinliang.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Old Label or New Narrative? Scoring Stock Concept Tag Freshness with First and Last Mention Times]]></title><description><![CDATA[Ask a heat dataset "which themes is this stock associated with?" and you get a ranked list. The top entry looks like the stock's main story. Often it is…]]></description><link>https://dustinliang.hashnode.dev/stock-concept-tag-freshness</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-concept-tag-freshness</guid><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 16:31:44 GMT</pubDate><content:encoded><![CDATA[<p>Ask a heat dataset "which themes is this stock associated with?" and you get a ranked list. The top entry looks like the stock's main story. Often it is not. It may be a label that has been attached to the company for a year and is simply accumulating mentions, while the genuinely new narrative sits at rank 40 with a handful of mentions from the last few days.</p>
<p>The difference is visible only if you look at <em>when</em> each association was first and last mentioned. This article shows how to turn those two timestamps into a stock concept tag freshness score using Lava's stock-to-concept endpoint, how to avoid the lookback trap that makes old tags look new, and how to track tag changes over time. All concept names and readings are illustrative, and every label produced here is descriptive.</p>
<h2>The Endpoint: Stock to Concept</h2>
<p>Lava covers both directions of the stock–concept relationship:</p>
<ul>
<li>concept → stocks: <code>get_concept_stock_heat</code> (<code>/concepts/stocks</code>)</li>
<li>stock → concepts: <code>get_stock_concept_heat</code> (<code>GET /concepts/by-stock</code>)</li>
</ul>
<p>This post uses the second one.</p>
<table>
<thead>
<tr>
<th>Item</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Identifier</td>
<td><code>ts_code</code> (e.g. <code>300750.SZ</code>) or <code>stock_name</code></td>
</tr>
<tr>
<td><code>days</code></td>
<td>lookback, default 90, range 1–365</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>rows, default 30, max 200</td>
</tr>
<tr>
<td>Per row</td>
<td>concept name, mention count, first-mention time, last-mention time, number of institutions/KOLs</td>
</tr>
<tr>
<td>Cost</td>
<td>0.005 yuan per call; non-2xx not billed</td>
</tr>
</tbody></table>
<p>Exact column names come from the OpenAPI schema. Below they are written as <code>concept</code>, <code>mentions</code>, <code>agencies</code>, <code>first_time</code> and <code>last_time</code>.</p>
<h2>Why Ranking by Mentions Misleads</h2>
<p>Mentions are cumulative. A concept discussed steadily for three months will outrank one that appeared last Tuesday, even if the new one is where the current conversation is. For quant research that uses theme exposure as a feature, or for institution/KOL heat monitoring that wants to catch narrative shifts, you need a second axis: age.</p>
<p>Two derived numbers give you that axis:</p>
<ul>
<li><code>first_age</code> = days between <code>first_time</code> and the request time;</li>
<li><code>last_age</code> = days between <code>last_time</code> and the request time.</li>
</ul>
<p>Use <code>meta.server_time</code> (which carries <code>+08:00</code>) as the request time rather than your local clock.</p>
<h2>The Lookback Trap: First Mention Is Window-Relative</h2>
<p><code>first_time</code> means the first mention <strong>inside the lookback</strong>. With the default <code>days=90</code>, a label that has existed for a year will show a first mention near the start of the 90-day window. Shorten <code>days</code> to 7 and every surviving concept looks "new".</p>
<p>The fix is a second pull with a long lookback, then a comparison. A concept is genuinely recent only if its first mention is recent in <em>both</em> the 90-day and 365-day pulls. Two calls, two charges, and a lot of false positives removed.</p>
<p>A related pitfall is <code>limit</code>. New associations start with few mentions, so they rank low. At the default <code>limit=30</code> they are often cut off. For freshness work, use <code>limit=200</code>.</p>
<h2>A Freshness Labeling Function</h2>
<p>Example thresholds (tune them for your research):</p>
<table>
<thead>
<tr>
<th>Label</th>
<th>Rule</th>
</tr>
</thead>
<tbody><tr>
<td><code>emerging</code></td>
<td><code>first_age &lt;= 7</code> in the 90-day pull and <code>first_age_365 &lt;= 7</code></td>
</tr>
<tr>
<td><code>persistent</code></td>
<td><code>first_age &gt; 30</code> and <code>last_age &lt;= 3</code></td>
</tr>
<tr>
<td><code>dormant</code></td>
<td><code>last_age &gt; 30</code></td>
</tr>
<tr>
<td><code>intermediate</code></td>
<td>anything else (do not force a category)</td>
</tr>
<tr>
<td><code>thin</code> flag</td>
<td><code>agencies &lt; 5</code>, added to any label</td>
</tr>
</tbody></table>
<pre><code class="language-python"># concept_freshness.py - illustrative; base URL and token are placeholders
import os, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
TZ = "Asia/Shanghai"

def by_stock(code, days):
    resp = requests.get(BASE + "/concepts/by-stock",
                        params={"ts_code": code, "days": days, "limit": 200},
                        headers=HEADERS, timeout=60).json()
    assert resp["ok"], resp.get("code")
    data = resp["data"]
    rows = data if isinstance(data, list) else data.get("list", [])
    return pd.DataFrame(rows), pd.Timestamp(resp["meta"]["server_time"])

def age_days(col, now):
    ts = pd.to_datetime(col)
    ts = ts.dt.tz_convert(TZ) if ts.dt.tz is not None else ts.dt.tz_localize(TZ)
    return (now - ts).dt.days

def label(r):
    if r.first_age &lt;= 7 and r.first_age_365 &lt;= 7:
        tag = "emerging"
    elif r.first_age &gt; 30 and r.last_age &lt;= 3:
        tag = "persistent"
    elif r.last_age &gt; 30:
        tag = "dormant"
    else:
        tag = "intermediate"
    return tag + ("+thin" if r.agencies &lt; 5 else "")

d90, now = by_stock("&lt;CODE&gt;", 90)
d365, _ = by_stock("&lt;CODE&gt;", 365)
m = d90.merge(d365[["concept", "first_time"]], on="concept",
              suffixes=("", "_365"), how="left")
m["first_age"] = age_days(m["first_time"], now)
m["last_age"] = age_days(m["last_time"], now)
m["first_age_365"] = age_days(m["first_time_365"], now)
m["freshness"] = m.apply(label, axis=1)
print(m[["concept", "mentions", "agencies", "first_age", "last_age", "freshness"]])
</code></pre>
<p>Illustrative output:</p>
<table>
<thead>
<tr>
<th>concept</th>
<th>mentions</th>
<th>agencies</th>
<th>first_age</th>
<th>last_age</th>
<th>freshness</th>
</tr>
</thead>
<tbody><tr>
<td>Concept A</td>
<td>412</td>
<td>96</td>
<td>88</td>
<td>0</td>
<td>persistent</td>
</tr>
<tr>
<td>Concept B</td>
<td>37</td>
<td>14</td>
<td>4</td>
<td>0</td>
<td>emerging</td>
</tr>
<tr>
<td>Concept C</td>
<td>65</td>
<td>22</td>
<td>85</td>
<td>41</td>
<td>dormant</td>
</tr>
<tr>
<td>Concept D</td>
<td>9</td>
<td>3</td>
<td>2</td>
<td>1</td>
<td>emerging+thin</td>
</tr>
</tbody></table>
<p>Concept A tops the mention ranking, but Concept B is the association that did not exist a week ago.</p>
<h2>Three Different "Firsts" You Should Not Mix</h2>
<p>Lava has three time markers that all sound like "first mention":</p>
<ol>
<li><strong><code>first_time</code> on <code>/concepts/by-stock</code></strong>: first time a <em>stock–concept pair</em> appeared in the lookback. Describes whether a label was newly attached.</li>
<li><strong><code>/hot/first-mentions</code></strong> (<code>get_first_mention_stocks</code>): a <em>stock</em> not mentioned at all in the previous 7 days that was first mentioned within the last <code>hours</code> (default 24, max 72). Returns <code>first_time</code>, the first source's <code>agency_id</code> and score. Describes whether the stock itself newly surfaced.</li>
<li><strong><code>is_first_mention_day</code></strong> on <code>/panel/daily</code>: a stock-level flag per calendar day.</li>
</ol>
<p>Keep them in separate columns with explicit names. Merging them produces statements nobody can verify.</p>
<h2>Reading Context: Sample the Records Behind a New Tag</h2>
<p>Counts and timestamps tell you a tag is new, not what it is about. Pull recent records for the same stock from <code>get_stock_news_heat</code> (<code>/opinions</code>) and filter locally for the concept name:</p>
<pre><code class="language-python">resp = requests.get(BASE + "/opinions",
                    params={"ts_code": "&lt;CODE&gt;", "days": 7, "limit": 200},
                    headers=HEADERS, timeout=60).json()
assert resp["ok"], resp.get("code")
data = resp["data"]
recs = data.get("list", []) if isinstance(data, dict) else data
hits = [r for r in recs if "Concept B" in (r.get("concepts") or [])]
print(len(hits), len({r["agency_id"] for r in hits}))
for r in sorted(hits, key=lambda r: r["topic_time"])[:5]:
    print(r["topic_time"], r["summary"])
</code></pre>
<p>Two things to check: whether the new association comes from many anonymous <code>agency_id</code>s or a few repeating themselves, and whether records actually exist near <code>first_time</code>. <code>/opinions</code> accepts <code>days</code> up to 90 and truncates at <code>limit</code>; if you get exactly 200 rows, narrow the range.</p>
<h2>Connecting Fresh Tags to the Rest of the Workflow</h2>
<ul>
<li><strong>Is the concept hot market-wide?</strong> Check <code>get_market_hot_concepts</code> (<code>/hot/concepts</code>).</li>
<li><strong>Who else carries it?</strong> Call <code>get_concept_stock_heat</code> (<code>/concepts/stocks</code>) with the canonical name (exact match, up to 30 characters). If only one stock is discussed under it, the narrative is probably company-specific.</li>
<li><strong>Need history?</strong> <code>/concepts/by-stock</code> has no <code>as_of_date</code>, so it cannot reconstruct a past profile. Use <code>/panel/daily</code> with <code>type=concept</code> and <code>names</code> for calendar-day series, and label the definition difference.</li>
</ul>
<p>Concept names returned by <code>/concepts/by-stock</code> are canonical, so pass them straight into the other endpoints instead of typing aliases.</p>
<h2>Snapshots and Change Tracking</h2>
<p>Store per run: <code>ts_code</code>, <code>days</code>, <code>limit</code>, <code>meta.server_time</code>, <code>meta.request_id</code>, <code>meta.billing.charged_yuan</code>, every row, and the computed ages and labels. A weekly snapshot of a fixed watchlist is enough; concept profiles are long-lookback statistics and barely change intraday. Align two snapshots on concept name and count additions, removals and label transitions (for example <code>emerging</code> → <code>persistent</code>). That diff describes changes in discussion co-occurrence, not changes in a company's business or official classification.</p>
<h2>FAQ</h2>
<p><strong>Why does <code>/concepts/by-stock</code> default to 90 days when <code>/concepts/stocks</code> defaults to 30?</strong>
They answer different questions. A stock's concept profile needs a longer view to be complete. Pass <code>days</code> explicitly either way.</p>
<p><strong>Does a missing concept at <code>limit=30</code> mean it is not associated?</strong>
No, it just ranked outside the top 30. Re-check with <code>limit=200</code>.</p>
<p><strong>Does <code>last_age = 41</code> mean the association is over?</strong>
It means no new mention in the lookback for 41 days. Say exactly that.</p>
<h2>Conclusion</h2>
<p>Stock concept tag freshness comes down to two timestamps and one discipline: confirm "new" with a long lookback. Compute ages from <code>meta.server_time</code>, flag thin samples, sample the underlying records, and snapshot weekly. You will separate persistent labels from emerging narratives without guessing. To look up a stock's concept profile interactively first, try the <a href="https://lava.ai-dolphin.com/tools/stock-concepts">stock concepts lookup tool</a>.</p>
<hr />
<p>中文简介：同样排在第一的概念，可能是老标签，也可能是新叙事。本文基于熔岩「个股 → 概念」接口 get_stock_concept_heat（/concepts/by-stock）的首提与最近提及时间，给出概念标签新鲜度打分函数（新近出现 / 持续活跃 / 已沉寂 / 样本小），讲清 days=90 与 365 双回看复核、limit=200、三种「首次提及」口径的区别，以及用 /opinions 抽样核对和周度快照对比。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[The Lookback Window Problem in Concept-to-Stock Mention Rankings: Getting the days Parameter Right]]></title><description><![CDATA["Which stocks are carrying the discussion around this theme right now?" is one of the most common questions people ask a heat monitoring dataset. In Lava…]]></description><link>https://dustinliang.hashnode.dev/concept-stock-ranking-lookback-window</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/concept-stock-ranking-lookback-window</guid><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 16:28:55 GMT</pubDate><content:encoded><![CDATA[<p>"Which stocks are carrying the discussion around this theme right now?" is one of the most common questions people ask a heat monitoring dataset. In Lava, the answer comes from the concept member ranking: <code>GET /concepts/stocks</code>, exposed to agents as the MCP tool <code>get_concept_stock_heat</code>. It returns the stocks most mentioned by institutions and influencers under a given concept, along with how many distinct sources mentioned each one.</p>
<p>The endpoint has one innocent-looking parameter, <code>days</code>, which defaults to 30 and accepts 1 to 365. Combined with a hard <code>limit</code>, a roughly ten-minute refresh cycle, and exact-match concept names, it is responsible for most "why don't our two queries agree?" conversations. This article explains the mechanics and the fixes. Concept names like "Concept Alpha" and all numbers are illustrative.</p>
<h2>Where the Concept Ranking Fits</h2>
<p>Lava exposes three read-only paths around concepts. They look similar but are parameterized differently:</p>
<table>
<thead>
<tr>
<th>MCP tool</th>
<th>HTTP path</th>
<th>Time parameter</th>
<th>Rows</th>
<th>Refresh</th>
</tr>
</thead>
<tbody><tr>
<td><code>get_market_hot_concepts</code></td>
<td><code>/hot/concepts</code></td>
<td><code>window</code>: <code>1h</code>, <code>4h</code>, <code>18h</code>, <code>24h</code>, <code>3d</code> (default <code>24h</code>, no <code>7d</code>)</td>
<td>default 50, max 200</td>
<td>about 2 min</td>
</tr>
<tr>
<td><code>get_concept_stock_heat</code></td>
<td><code>/concepts/stocks</code></td>
<td><code>days</code>: default 30, range 1–365</td>
<td>default 50, max 100</td>
<td>about 10 min</td>
</tr>
<tr>
<td><code>get_stock_concept_heat</code></td>
<td><code>/concepts/by-stock</code></td>
<td><code>days</code>: default 90, range 1–365</td>
<td>default 30, max 200</td>
<td>per response</td>
</tr>
</tbody></table>
<p><code>concept</code> is required on <code>/concepts/stocks</code>, matched exactly, and limited to 30 characters.</p>
<h2>It Is a Mention Ranking, Not an Index Constituent List</h2>
<p>The most frequent misunderstanding is treating the output as an official membership list. It is a ranking of stocks that were <strong>mentioned</strong> in connection with the concept during the lookback. A stock commonly classified under the theme but not discussed in the window simply will not appear.</p>
<p>Name the column accordingly in your research tables, for example <code>concept_mention_members_d30</code>, and source official constituents from your own reference data.</p>
<h2>How <code>days</code> Shapes the Answer</h2>
<h3>Unstated lookbacks create phantom disagreements</h3>
<p>One analyst says the top name under Concept Alpha is A; another gets B. Usually one used the default <code>days=30</code> and the other passed <code>days=7</code>. A single burst early in the month can dominate the 30-day ranking while the 7-day ranking reflects this week. Always cite the lookback: "Concept Alpha, <code>days=7</code>, rank 1".</p>
<h3>Long lookbacks freeze old events at the top</h3>
<p>At <code>days=90</code> or longer, the leader can stay unchanged for weeks. A stock that collected 300 mentions from one event sixty days ago and single digits since can still rank first. Long lookbacks answer "who has carried this theme over time"; short ones answer "who is carrying it lately". To see change, read several lookbacks together.</p>
<h3>Short lookbacks are thin</h3>
<p>At <code>days=1</code>, the ranking reshuffles on every refresh because positions 5 and 30 may differ by one or two mentions. Read only the top few and always look at the counts. For intraday structure on a specific name, <code>/heat/windows</code> gives six rolling windows plus <code>hourly_24h</code>.</p>
<h3><code>days</code> is not <code>window</code></h3>
<p>Comparing <code>/hot/concepts?window=3d</code> with <code>/concepts/stocks?days=3</code> produces numbers that do not reconcile, because one is a rolling hourly window over the whole concept and the other is a day-based lookback per member stock. Reconcile within one endpoint; across endpoints compare direction and coverage only.</p>
<h2>Operational Limits That Bite</h2>
<p><strong>Row cap.</strong> <code>limit</code> defaults to 50 and tops out at 100, with no documented offset parameter. If your definition is "every mentioned member", label the table "top 100" instead. For very broad themes, shorten <code>days</code> or work from the stock side with <code>/concepts/by-stock</code>.</p>
<p><strong>Refresh cadence.</strong> The ranking updates about every ten minutes. Polling every minute mostly returns duplicates while each successful call is still billed. Poll at ten-minute intervals or slower and hash the payload, storing a snapshot only when the content changes. Four hours at one-minute intervals is 240 calls; at ten minutes it is 24.</p>
<p><strong>Exact names.</strong> "Concept Alpha" may resolve while "Concept Alpha theme" or a variant with different spacing or full-width characters returns nothing. Copy canonical names from <code>/hot/concepts</code> or <code>/concepts/by-stock</code> responses and keep a local dictionary. Record an empty result as "no match for these parameters" rather than retrying with near-synonyms.</p>
<p><strong>Moving snapshots.</strong> Two <code>days=30</code> pulls taken in the morning and afternoon are not the same cross-section: the window slid and the data refreshed in between. Save <code>meta.server_time</code> and <code>meta.request_id</code> with every snapshot and fix a daily capture time if you track membership changes.</p>
<h2>Do Not Backtest on a Lookback</h2>
<p>The endpoint has no <code>as_of_date</code>; the lookback always ends at request time. A <code>days=30</code> ranking pulled today includes discussion that happened after the date you might want to study. For history:</p>
<ul>
<li><code>/panel/daily</code> (<code>get_stock_heat_panel</code>) with <code>type=stock</code> and up to 20 <code>codes</code>, or <code>type=concept</code> with <code>names</code>, for calendar-day series of up to 92 days;</li>
<li><code>/hot/weighted</code> with <code>as_of_date</code> (messages through 15:00 that day) or <code>/agencies/pit</code> for point-in-time, score-weighted cross-sections.</li>
</ul>
<h2>A Three-Rung Lookback Ladder</h2>
<p>The practical fix for most of the above is to pull several lookbacks at once and compare membership:</p>
<pre><code class="language-python"># concept_days_ladder.py - illustrative; base URL and token are placeholders
import hashlib
import json
import os
import requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
CONCEPT = "Concept Alpha"   # copy the canonical name from /hot/concepts

def members(days):
    resp = requests.get(
        BASE + "/concepts/stocks",
        params={"concept": CONCEPT, "days": days, "limit": 100},
        headers=HEADERS, timeout=60,
    ).json()
    if not resp.get("ok"):
        return None
    data = resp["data"]
    rows = data if isinstance(data, list) else data.get("list", [])
    digest = hashlib.sha256(json.dumps(rows, sort_keys=True).encode()).hexdigest()[:12]
    # Identifier column names should be checked against the OpenAPI schema.
    codes = {r.get("ts_code") or r.get("stock_name") for r in rows}
    return {"codes": codes, "server_time": resp["meta"]["server_time"], "hash": digest}

ladder = {d: members(d) for d in (7, 30, 90)}
s7, s30, s90 = (ladder[d]["codes"] if ladder[d] else set() for d in (7, 30, 90))
print("in all three:", len(s7 &amp; s30 &amp; s90))
print("7-day only:", len(s7 - s30 - s90))
print("90-day only:", len(s90 - s30 - s7))
</code></pre>
<p>Read the output as structure, not as a signal. Names present in all three rungs are the long-term carriers of the theme. Names that appear only at seven days were pulled into the conversation recently. Names only at ninety days are fading from it.</p>
<h2>Cross-Checking from the Stock Side</h2>
<p>Concept-to-stock and stock-to-concept are two directions of one relationship, but their defaults differ: 30 days versus 90. If you do not align them, you get the false contradiction "the stock ranks high under the concept, but the concept ranks low for the stock".</p>
<p>A clean cross-check: pull the top ten members with <code>days=30</code>, call <code>/concepts/by-stock</code> for each with an explicit <code>days=30</code>, and note where Concept Alpha ranks among each stock's concepts. A low position means that stock's discussion is mostly carried by other themes. That is eleven calls in total, so it is cheap to schedule daily.</p>
<h2>FAQ</h2>
<p><strong>Does <code>/concepts/stocks</code> accept <code>min_score</code>?</strong>
Check the OpenAPI schema. For a high-score source view, apply <code>min_score</code> on <code>/hot/concepts</code> or inspect members individually through <code>/heat/windows</code>.</p>
<p><strong><code>/hot/concepts</code> has no <code>7d</code>. How do I get a weekly view?</strong>
Use <code>/concepts/stocks</code> with <code>days=7</code>, or <code>/panel/daily</code> with <code>type=concept</code> for a daily series. Do not pass unlisted window values.</p>
<p><strong>Is <code>days=365</code> useful?</strong>
As background, yes: it shows which names have carried a theme over a year. It barely reflects recent change and the 100-row cap trims the tail, so read it alongside the 7- and 30-day rungs.</p>
<h2>Summary</h2>
<p><code>get_concept_stock_heat</code> is a mention ranking whose meaning depends almost entirely on <code>days</code>. Remember the defaults (30, range 1–365), the 100-row cap, the ten-minute refresh, exact concept names, and the missing <code>as_of_date</code>. Pull a 7/30/90 ladder, store parameters and server time with every snapshot, and most inconsistencies disappear.</p>
<p>To explore concept members interactively before writing code, open the <a href="https://lava.ai-dolphin.com/tools/concept-stocks">concept stocks tool</a>.</p>
<hr />
<p>中文简介：熔岩概念成分提及排行（get_concept_stock_heat）的读数高度依赖 days 回看参数，长回看会把旧事件「冻」在榜首，短回看样本又太薄。本文梳理 limit 上限、10 分钟更新、概念名精确匹配与不可回测等坑，并给出 7/30/90 三档并列的 Python 示例。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[Stock News Timeline in Python: Merging Hourly Heat, Opinion Summaries and Media Hits into One Event Log (CATL Example)]]></title><description><![CDATA[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?…]]></description><link>https://dustinliang.hashnode.dev/stock-news-timeline-python-catl</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-news-timeline-python-catl</guid><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 16:26:59 GMT</pubDate><content:encoded><![CDATA[<p>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?</p>
<p>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 (<code>300750.SZ</code>) 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.</p>
<h2>Why a Single Heat Number Is Not Enough</h2>
<p>A heat score tells you <em>how much</em> a stock was discussed in a window. It does not tell you <em>when</em> inside that window, <em>who</em> 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.</p>
<p>Lava splits that information across several feeds. None of them is a timeline on its own, but together they are.</p>
<h2>The Five Feeds and Their Time Granularity</h2>
<table>
<thead>
<tr>
<th>Role in the timeline</th>
<th>MCP tool</th>
<th>HTTP path</th>
<th>Granularity</th>
</tr>
</thead>
<tbody><tr>
<td>Skeleton: which hour heated up</td>
<td><code>get_stock_heat_windows</code></td>
<td><code>/heat/windows</code></td>
<td>six rolling windows plus <code>hourly_24h</code></td>
</tr>
<tr>
<td>Anchors: what was being said</td>
<td><code>get_stock_news_heat</code></td>
<td><code>/opinions</code></td>
<td>per record (<code>topic_time</code>)</td>
</tr>
<tr>
<td>Media anchors: who named the stock</td>
<td><code>get_news_stock_hits</code></td>
<td><code>/news/hits</code></td>
<td>per news item (publish time)</td>
</tr>
<tr>
<td>Breadth: how many distinct sources</td>
<td><code>get_agency_coverage</code></td>
<td><code>/coverage/stocks</code></td>
<td>daily</td>
</tr>
<tr>
<td>Background: where today sits historically</td>
<td><code>get_stock_heat_panel</code></td>
<td><code>/panel/daily</code></td>
<td>calendar day</td>
</tr>
</tbody></table>
<p>All five accept the same identifier, either <code>ts_code</code> or <code>stock_name</code>. Use <code>ts_code</code>. Short names change and occasionally collide; codes join cleanly with your own market data.</p>
<h2>Design Rule: Two Clocks, Never Mixed</h2>
<p>The biggest source of broken timelines is mixing granularities. Keep two indexes:</p>
<ul>
<li>an <strong>hourly index</strong> for things that happen at a moment (opinion records, news hits, hourly buckets);</li>
<li>a <strong>daily index</strong> for things that are only defined per day (coverage counts, daily panel rows).</li>
</ul>
<p>Then normalize every timestamp to timezone-aware <code>Asia/Shanghai</code>:</p>
<ul>
<li><code>meta.server_time</code> in each response envelope carries <code>+08:00</code>;</li>
<li><code>topic_time</code> on <code>/opinions</code> records is Beijing time;</li>
<li><code>/news/hits</code> rows have a publish time (check the exact column name in the OpenAPI schema);</li>
<li><code>hourly_24h</code> covers the 24 hours before the call, with bucket boundaries as returned;</li>
<li>coverage and panel dates are Asia/Shanghai calendar days.</li>
</ul>
<p>Do not spread a daily coverage number across hours. Attach it to the date and join it at read time.</p>
<h2>Step-by-Step: Building the Stock News Timeline in Python</h2>
<h3>1. Shared request helper</h3>
<pre><code class="language-python"># timeline.py - illustrative; base URL and token are placeholders
import os, json, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
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
</code></pre>
<p>Every call appends its parameters, <code>request_id</code> and <code>server_time</code> to <code>calls</code>. That list is what makes the timeline auditable later.</p>
<h3>2. The hourly skeleton</h3>
<pre><code class="language-python">win = get("/heat/windows", ts_code=CODE, min_score=1050)
hourly = win.get("hourly_24h", [])  # bucket column names: see OpenAPI
</code></pre>
<p><code>min_score=1050</code> only moves the threshold for the "high-score institutions/KOLs" column. Total mentions are unaffected.</p>
<h3>3. Opinion records as anchors</h3>
<pre><code class="language-python">ops = get("/opinions", ts_code=CODE, days=3, only_this_stock=0, limit=200)
records = rows(ops)
</code></pre>
<p>Each record carries <code>opinion_id</code>, <code>topic_time</code>, <code>agency_id</code>, <code>agency_score</code>, <code>stocks</code>, <code>concepts</code>, a <code>summary</code> (up to 120 characters) and <code>detail</code> (up to 600). <code>only_this_stock=0</code> returns records where the stock is the first-mentioned name; <code>=1</code> keeps records that mention only this stock. The two give different counts, so write down which one the timeline used.</p>
<h3>4. Media hits</h3>
<pre><code class="language-python">news = get("/news/hits", ts_code=CODE, hours=72, limit=200)
</code></pre>
<p>With a stock specified, <code>hours</code> defaults to 72 and goes up to 168; <code>src</code> pins a single outlet. Rows include <code>news_id</code>, publish time, outlet and matched stocks. There is no headline or body text.</p>
<h3>5. Daily breadth and background</h3>
<pre><code class="language-python">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"})
</code></pre>
<p>Coverage returns <code>cov_7d</code>, <code>cov_30d</code>, <code>new_cov_cum</code> and <code>high_score_ratio_30d</code> as counts only. The panel returns <code>mentions</code>, <code>agencies</code>, <code>weighted_heat</code> and <code>is_first_mention_day</code>, for ranges of at most 92 days.</p>
<h3>6. Merge into one event log</h3>
<pre><code class="language-python">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)
</code></pre>
<p>The <code>to_ts</code> helper avoids a classic bug: calling <code>tz_localize</code> on a string that already has <code>+08:00</code> either raises or double-shifts. Left-join the hourly buckets on <code>hour</code> and each hour now lists its own records and media hits.</p>
<h2>What the Output Looks Like</h2>
<table>
<thead>
<tr>
<th>Time (Asia/Shanghai)</th>
<th>Feed</th>
<th>Content</th>
</tr>
</thead>
<tbody><tr>
<td>11-13 09:00–10:00</td>
<td><code>/heat/windows</code></td>
<td>4 mentions in the hour</td>
</tr>
<tr>
<td>11-13 10:12</td>
<td><code>/news/hits</code></td>
<td>Outlet A, <code>news_id=&lt;id&gt;</code></td>
</tr>
<tr>
<td>11-13 10:20</td>
<td><code>/opinions</code></td>
<td><code>agency_score=1080</code>, summary: <code>&lt;summary&gt;</code></td>
</tr>
<tr>
<td>11-13 10:00–11:00</td>
<td><code>/heat/windows</code></td>
<td>17 mentions in the hour</td>
</tr>
<tr>
<td>11-13 10:47</td>
<td><code>/news/hits</code></td>
<td>Outlet B</td>
</tr>
<tr>
<td>11-13 (day)</td>
<td><code>/coverage/stocks</code></td>
<td><code>cov_7d=46</code>, <code>cov_30d=131</code></td>
</tr>
</tbody></table>
<p>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".</p>
<h2>Boundaries to Respect</h2>
<ul>
<li><strong>No news text.</strong> The only prose in the timeline comes from the <code>/opinions</code> <code>summary</code> field. Do not backfill articles from elsewhere.</li>
<li><strong>Hourly buckets cover only the last 24 hours.</strong> For older hours, count <code>/opinions</code> records by <code>topic_time</code> yourself and label that series separately; it is not the same definition as <code>hourly_24h</code>.</li>
<li><strong>Panel weighting is not point-in-time.</strong> <code>weighted_heat</code> on <code>/panel/daily</code> uses current source scores. For a strict historical weighted view use <code>/hot/weighted</code> with <code>as_of_date</code>.</li>
</ul>
<h2>Quality Checks Before You Save</h2>
<ul>
<li><strong>Timezone:</strong> spot-check three rows; all should end in <code>+08:00</code>.</li>
<li><strong>Truncation:</strong> if <code>/opinions</code> or <code>/news/hits</code> returned exactly <code>limit</code> rows, shorten <code>days</code>/<code>hours</code>, split by date range, or split news by <code>src</code>.</li>
<li><strong>Plausibility:</strong> an hour's <code>hourly_24h</code> count and that hour's record count should be the same order of magnitude; if not, note the <code>only_this_stock</code> setting.</li>
<li><strong>Audit trail:</strong> five <code>request_id</code>s saved. Five calls are billed five times at 0.005 yuan each; non-2xx responses are not billed.</li>
</ul>
<h2>Agent Prompt Version</h2>
<pre><code class="language-text">Build a 72-hour news timeline for ts_code=&lt;code&gt;:
- 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
</code></pre>
<h2>FAQ</h2>
<p><strong>Why do I get fewer <code>/opinions</code> rows than <code>data.heat.mentions</code>?</strong>
The list is capped by <code>limit</code> (max 200) while the summary counts the whole range. If the row count equals the limit, narrow the window.</p>
<p><strong>Does it matter whether the media hit or the opinion came first?</strong>
Record it as a fact, but the two feeds are collected and matched differently. Order shows when each appeared, not causation.</p>
<p><strong>Can I run this for a basket?</strong>
Yes, but calls grow linearly with the number of stocks. For the daily background, <code>/panel/daily</code> accepts up to 20 codes in one call.</p>
<h2>Conclusion</h2>
<p>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 <a href="https://lava.ai-dolphin.com/monitor/catl-sentiment">CATL sentiment monitor</a>.</p>
<hr />
<p>中文简介：本文用 Python 把熔岩的逐小时热度（/heat/windows）、个股消息面摘要（/opinions）、新闻点名（/news/hits）、覆盖度与日频面板合并成一张按时间排序、可审计的单股消息面时间线，以宁德时代 300750.SZ 为查询示例，重点讲时区统一、小时/日两套索引、截断检查与调用元数据留存。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[Mentions per Agency: A Simple Ratio for Measuring Discussion Concentration in Stock Heat Data]]></title><description><![CDATA[Two stocks can sit side by side on a heat ranking with exactly the same mention count and still have very different stories behind them. One might be…]]></description><link>https://dustinliang.hashnode.dev/mentions-per-agency-a-simple-ratio-for-measuring-discussion-concentration-in-stock-heat-data</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/mentions-per-agency-a-simple-ratio-for-measuring-discussion-concentration-in-stock-heat-data</guid><category><![CDATA[Python]]></category><category><![CDATA[Quant]]></category><category><![CDATA[fintech]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 04:28:22 GMT</pubDate><content:encoded><![CDATA[<p>Two stocks can sit side by side on a heat ranking with exactly the same mention count and still have very different stories behind them. One might be mentioned once each by forty different institutions and influencers. The other might be mentioned ten times each by four. A leaderboard sorted by <code>mentions</code> cannot tell them apart.</p>
<p>This post walks through a small derived metric that can: <strong>mentions per agency</strong> (<code>mpa</code>). It works on any heat dataset that reports both a raw mention count and a distinct-source count, and it is especially useful when you build quant research features on top of an institution and KOL heat monitor such as Lava. All numbers below are illustrative.</p>
<h2>The Two Counters Behind Every Heat Row</h2>
<p>Most rows in Lava's heat endpoints carry two integers measured over the same interval:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>What it counts</th>
<th>Deduplication</th>
</tr>
</thead>
<tbody><tr>
<td><code>mentions</code></td>
<td>Number of times the stock was mentioned</td>
<td>None. Repeated mentions by one source are counted each time</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>Number of distinct institutions / influencers that mentioned it</td>
<td>Deduplicated by anonymous <code>agency_id</code></td>
</tr>
</tbody></table>
<p>The interval depends on the endpoint. Market-wide rankings (<code>/hot/stocks</code>) use a rolling <code>window</code> such as <code>24h</code> or <code>7d</code>. Concept-level views (<code>/concepts/stocks</code>, <code>/concepts/by-stock</code>) use a <code>days</code> lookback. The daily panel (<code>/panel/daily</code>) slices by calendar day. The ratio is only meaningful when both counters come from the same row, so never mix a <code>mentions</code> value from one endpoint with an <code>agencies</code> value from another.</p>
<h2>Defining the Ratio and Its Edge Cases</h2>
<p>The definition is deliberately boring:</p>
<pre><code class="language-text">mpa = mentions / agencies     if agencies &gt;= 1
mpa = null                    if agencies == 0
</code></pre>
<p>The edge cases are where most pipelines go wrong:</p>
<ol>
<li><strong>The floor is 1.</strong> Every source contributes at least one mention, so <code>mpa &gt;= 1</code>. A value of exactly 1 means each source spoke once, which is the most dispersed structure possible.</li>
<li><strong>A zero denominator is null, not zero.</strong> Rows with <code>mentions &gt; 0</code> and <code>agencies == 0</code> can occur when a mention record is not linked to a source. Keep them as null and count them separately. Folding them into either bucket distorts the distribution.</li>
<li><strong>Small denominators are noise.</strong> With <code>agencies</code> at 1 or 2, the ratio swings wildly. Set a minimum (we use 5 in the examples) and only read <code>mpa</code> above it.</li>
<li><strong>Intervals are not interchangeable.</strong> A source has more chances to repeat itself over seven days than over twenty-four hours, so long-window ratios drift upward. Fix the window before you compare anything.</li>
<li><strong>Use the log for cross-sections.</strong> <code>log(mpa)</code> compresses outliers, and zero maps neatly to "one mention per source".</li>
</ol>
<h2>Four Discussion Structures</h2>
<p>Combine the cross-sectional percentile of <code>mentions</code> with <code>mpa</code>, and every row falls into one of four quadrants:</p>
<table>
<thead>
<tr>
<th>Mentions percentile</th>
<th><code>mpa</code></th>
<th>Structure</th>
<th>What to look at next</th>
</tr>
</thead>
<tbody><tr>
<td>High</td>
<td>Low (about 1–2)</td>
<td>Broad: many sources, each mentioning once</td>
<td>Concept links via <code>/concepts/by-stock</code></td>
</tr>
<tr>
<td>High</td>
<td>High</td>
<td>Concentrated: a few sources repeating themselves</td>
<td>Sample <code>agency_id</code> distribution in <code>/opinions</code></td>
</tr>
<tr>
<td>Low</td>
<td>High</td>
<td>Niche: small volume from a small group</td>
<td>Usually logged, rarely interpreted alone</td>
</tr>
<tr>
<td>Low</td>
<td>Low</td>
<td>Scattered</td>
<td>First-mention events often land here</td>
</tr>
</tbody></table>
<p>A worked example from one 24h window: stock A has <code>mentions=48, agencies=40</code>, so <code>mpa=1.2</code>. Stock B has <code>mentions=48, agencies=6</code>, so <code>mpa=8.0</code>. On the ranking they look like twins. In reality, B's count is far more sensitive to the posting habits of a single source.</p>
<h2>Building the Feature from a Daily Panel</h2>
<p>The daily panel returns <code>mentions</code>, <code>agencies</code>, <code>weighted_heat</code> and <code>is_first_mention_day</code> per stock per calendar day, up to 20 codes and 92 days per request. The script below batches a universe, computes <code>mpa</code>, and tags each row with a quadrant.</p>
<pre><code class="language-python"># mpa_panel.py - illustrative; base URL and token are placeholders
import os
import numpy as np
import pandas as pd
import requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
MIN_AGENCIES = 5          # example threshold
BATCH = 20                # panel limit per request

def fetch_panel(codes, start, end):
    resp = requests.get(
        BASE + "/panel/daily",
        params={"type": "stock", "codes": ",".join(codes), "from": start, "to": end},
        headers=HEADERS, timeout=60,
    ).json()
    assert resp["ok"], resp["code"]
    data = resp["data"]
    rows = data if isinstance(data, list) else data.get("list", [])
    df = pd.DataFrame(rows)
    df["request_id"] = resp["meta"]["request_id"]
    return df

universe = ["&lt;CODE_1&gt;", "&lt;CODE_2&gt;"]  # your own universe
panel = pd.concat(
    [fetch_panel(universe[i:i + BATCH], "2026-10-01", "2026-11-13")
     for i in range(0, len(universe), BATCH)],
    ignore_index=True,
)

# Column names (date, ts_code) should be checked against the OpenAPI schema.
eligible = panel["agencies"].where(panel["agencies"] &gt;= MIN_AGENCIES)
panel["mpa"] = panel["mentions"] / eligible
panel["log_mpa"] = np.log(panel["mpa"])

panel["m_pct"] = panel.groupby("date")["mentions"].rank(pct=True)
panel["mpa_pct"] = panel.groupby("date")["log_mpa"].rank(pct=True)
panel["quadrant"] = np.select(
    [
        (panel["m_pct"] &gt;= 0.8) &amp; (panel["mpa_pct"] &gt;= 0.8),
        (panel["m_pct"] &gt;= 0.8),
        (panel["mpa_pct"] &gt;= 0.8),
    ],
    ["high/concentrated", "high/broad", "low/concentrated"],
    default="low/scattered",
)
panel.loc[panel["mpa"].isna(), "quadrant"] = "insufficient_sources"
print(panel["quadrant"].value_counts())
</code></pre>
<p>Two engineering details deserve attention.</p>
<p><strong>Calendar days versus trading days.</strong> The panel includes weekends and holidays. If you align it with trading-day market data, decide first where weekend counts go, sum the raw counters, and only then divide. Averaging two daily ratios is not the same as the ratio of two sums.</p>
<p><strong>Multi-day aggregation.</strong> Summing <code>agencies</code> across days double-counts any source that appears on more than one day. For a seven-day view, request a seven-day window directly instead of summing seven daily rows.</p>
<h2>How <code>mpa</code> Relates to Other Heat Metrics</h2>
<p><code>mpa</code> answers one narrow question: is the discussion spread out or concentrated? It says nothing about who is talking. Pair it with:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Endpoint</th>
<th>Question it answers</th>
</tr>
</thead>
<tbody><tr>
<td><code>weighted_heat</code></td>
<td><code>/panel/daily</code>, <code>/hot/weighted</code></td>
<td>How much heat after weighting each source by its score</td>
</tr>
<tr>
<td><code>cov_7d</code>, <code>cov_30d</code></td>
<td><code>/coverage/stocks</code></td>
<td>Distinct sources over a rolling 7 / 30 days, a steadier denominator</td>
</tr>
<tr>
<td><code>new_cov_cum</code></td>
<td><code>/coverage/stocks</code></td>
<td>Cumulative newly covering sources; new entrants usually pull <code>mpa</code> down</td>
</tr>
<tr>
<td><code>high_score_ratio_30d</code></td>
<td><code>/coverage/stocks</code></td>
<td>Share of high-score sources over 30 days</td>
</tr>
</tbody></table>
<p>One caveat for backtests: <code>weighted_heat</code> on the daily panel uses current source scores, not point-in-time scores. For strict historical work, use the <code>as_of_date</code> parameter on <code>/hot/weighted</code> or the <code>/agencies/pit</code> snapshot.</p>
<h2>A Logging Checklist</h2>
<ul>
<li>Always record the endpoint and interval with the value, for example "<code>/hot/stocks</code>, <code>window=24h</code>".</li>
<li>Applying <code>min_score</code> to <code>/hot/stocks</code> shrinks both counters, so the ratio becomes "concentration among high-score sources". Label it that way.</li>
<li>Do not mix <code>market=all</code> rankings (merged by name) with <code>market=ashare</code> rankings (merged by code) in one cross-section.</li>
<li>Persist <code>meta.server_time</code>, <code>meta.request_id</code>, both raw counters and the computed <code>mpa</code>.</li>
</ul>
<h2>FAQ</h2>
<p><strong>Does a high <code>mpa</code> mean low-quality discussion?</strong>
No. It describes how mentions are distributed across sources and is independent of source scores. Use <code>weighted_heat</code> or <code>high_score_ratio_30d</code> for quality questions.</p>
<p><strong>Can I compute it for concepts?</strong>
Yes. <code>/hot/concepts</code> and <code>/concepts/stocks</code> return both counters, and the interpretation carries over directly.</p>
<p><strong>Why do the ranking and the daily panel give different ratios for the same stock?</strong>
Different intervals. The 24h ranking rolls back from <code>meta.server_time</code>; the panel cuts at calendar-day boundaries. Both are correct for their own interval.</p>
<h2>Wrapping Up</h2>
<p><code>mentions</code> tells you how often a stock was discussed; <code>agencies</code> tells you by how many distinct voices. Dividing one by the other turns a flat ranking into a picture of discussion structure. Fix the interval, enforce a minimum source count, keep nulls as nulls, and the ratio becomes a stable, explainable feature for quant research.</p>
<p>Field definitions and the read-only monitoring views are documented on the <a href="https://lava.ai-dolphin.com/research">Lava research page</a>.</p>
<hr />
<p>中文简介：本文介绍如何用「提及次数 ÷ 机构/大V 数」（mentions per agency）区分同等热度下的广泛讨论与少数主体集中讨论。文中给出分母为 0、小样本、跨窗口不可比等边界规则，以及从日频面板取数并分档的 Python 示例。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[How to Compare Stock Heat Across a Basket: Raw Mentions, Group Share, and Self-Relative Z-Scores]]></title><description><![CDATA[Put five stocks from the same sector next to each other on a heat chart and the result is almost always the same: the largest, most widely followed names…]]></description><link>https://dustinliang.hashnode.dev/how-to-compare-stock-heat-across-a-basket-raw-mentions-group-share-and-self-relative-z-scores</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/how-to-compare-stock-heat-across-a-basket-raw-mentions-group-share-and-self-relative-z-scores</guid><category><![CDATA[Python]]></category><category><![CDATA[Quant]]></category><category><![CDATA[fintech]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 04:27:48 GMT</pubDate><content:encoded><![CDATA[<p>Put five stocks from the same sector next to each other on a heat chart and the result is almost always the same: the largest, most widely followed names sit on top. That is not wrong, but it is rarely the question you were asking. Often you want to know which name is getting unusual attention relative to its own baseline, or how a fixed amount of discussion is split inside a peer group.</p>
<p>This guide breaks cross-stock heat comparison into three normalizations, shows which Lava endpoints supply each one, and gives a script that prints all three rankings side by side. Every number in the examples is illustrative.</p>
<h2>Why Raw Mention Counts Mislead in Peer Comparisons</h2>
<p><code>mentions</code> counts how many times a stock was mentioned by institutions and influencers in a window. It answers "who is being talked about most right now". It does not answer "whose discussion is unusual".</p>
<p>Consider stock A, which normally gets about 120 mentions a day and got 150 today, and stock B, which normally gets 5 and got 30. On raw counts A wins easily. Relative to its own history, B moved much further. Both statements are true; they answer different questions. Pick the question first, then pick the metric.</p>
<p>A second, easily missed variable is the source threshold. <code>/hot/stocks</code> and <code>/heat/windows</code> both accept <code>min_score</code>. On the ranking it filters mentions to sources scoring at or above the threshold; on the windows endpoint it defines the cutoff for the "high-score sources" column (default 1050, roughly the top 10%). Scores range from about 450 to 1500. Every stock in a comparison group must use the same threshold, or the comparison silently mixes two populations.</p>
<h2>Three Normalizations at a Glance</h2>
<table>
<thead>
<tr>
<th>Normalization</th>
<th>Computation</th>
<th>Question it answers</th>
<th>Origin</th>
</tr>
</thead>
<tbody><tr>
<td>Raw mentions</td>
<td><code>mentions</code>, optionally with <code>agencies</code></td>
<td>Who is discussed most in absolute terms</td>
<td>Returned by API</td>
</tr>
<tr>
<td>Group share</td>
<td><code>mentions / sum(mentions in group)</code></td>
<td>How discussion is distributed within this group</td>
<td>Computed locally</td>
</tr>
<tr>
<td>Self-relative</td>
<td><code>heat_z20</code>, a z-score against the stock's own recent 20-day average</td>
<td>Who deviates most from their own normal</td>
<td>Returned by API</td>
</tr>
</tbody></table>
<p>A useful companion is the group share of <code>agencies</code>. When a stock's mention share is much higher than its agency share, its discussion comes from fewer, more repetitive sources.</p>
<h2>A Worked Example</h2>
<p>Same 24h window, three stocks:</p>
<table>
<thead>
<tr>
<th>Stock</th>
<th><code>mentions</code></th>
<th><code>agencies</code></th>
<th>Group share</th>
<th><code>heat_z20</code></th>
<th>Rank by mentions</th>
<th>Rank by z</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>150</td>
<td>88</td>
<td>0.71</td>
<td>0.9</td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td>B</td>
<td>30</td>
<td>17</td>
<td>0.14</td>
<td>4.2</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>C</td>
<td>31</td>
<td>12</td>
<td>0.15</td>
<td>1.6</td>
<td>2</td>
<td>2</td>
</tr>
</tbody></table>
<p>The correct description is: "A holds about 71% of the group's mentions; B shows the largest deviation from its own 20-day baseline." The incorrect one is "B is hotter than A", which is false on raw counts. B and C have nearly identical mention counts, but B's z-score is far higher, telling you B is usually quiet.</p>
<h2>Choosing the Right Endpoint</h2>
<table>
<thead>
<tr>
<th>Path</th>
<th>MCP tool</th>
<th>Stocks per call</th>
<th>Freshness</th>
<th>History</th>
<th>Best for</th>
</tr>
</thead>
<tbody><tr>
<td><code>/heat/windows</code></td>
<td><code>get_stock_heat_windows</code></td>
<td>1, six windows</td>
<td>Real time</td>
<td>Hourly detail for the last 24h only</td>
<td>All three, includes <code>heat_z20</code></td>
</tr>
<tr>
<td><code>/hot/stocks</code></td>
<td><code>get_market_hot_stocks</code></td>
<td>One ranking, <code>limit</code> up to 200</td>
<td>About 1 minute</td>
<td>No</td>
<td>Raw and share; <code>heat_z20</code> attached</td>
</tr>
<tr>
<td><code>/panel/daily</code></td>
<td><code>get_stock_heat_panel</code></td>
<td>Up to 20 codes</td>
<td>Daily</td>
<td>Yes, up to 92 days</td>
<td>Raw and share; z computed locally</td>
</tr>
</tbody></table>
<p>Rules of thumb:</p>
<ul>
<li>For a group of 20 or fewer with history, one panel call is the cheapest option.</li>
<li>For a current snapshot where every member is inside the top 200, a single ranking call is enough. A stock missing from the ranking is <strong>missing</strong>, not zero.</li>
<li>For per-hour detail or all six windows, call <code>/heat/windows</code> once per stock.</li>
</ul>
<h2>Script: Three Rankings Side by Side</h2>
<pre><code class="language-python"># compare_three.py - illustrative; base URL and token are placeholders
import os
import pandas as pd
import requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
GROUP = ["&lt;CODE_A&gt;", "&lt;CODE_B&gt;", "&lt;CODE_C&gt;"]

def heat_windows(code):
    resp = requests.get(BASE + "/heat/windows", params={"ts_code": code},
                        headers=HEADERS, timeout=60).json()
    assert resp["ok"], resp["code"]
    return resp["data"], resp["meta"]

records = []
for code in GROUP:
    data, meta = heat_windows(code)
    # Window structure should be checked against the OpenAPI schema.
    w24 = data.get("windows", {}).get("24h", {}) if isinstance(data, dict) else {}
    records.append({
        "code": code,
        "mentions": w24.get("mentions"),
        "agencies": w24.get("agencies"),
        "heat_z20": data.get("heat_z20"),
        "server_time": meta["server_time"],
        "request_id": meta["request_id"],
    })

df = pd.DataFrame(records)
df["share"] = df["mentions"] / df["mentions"].sum()
df["agency_share"] = df["agencies"] / df["agencies"].sum()
for col in ("mentions", "share", "heat_z20"):
    df[f"{col}_rank"] = df[col].rank(ascending=False, method="min")
print(df[["code", "mentions_rank", "share_rank", "heat_z20_rank"]])
</code></pre>
<p>Disagreeing ranks are the normal case, not a data bug. Report the three columns together and put the window and group membership in the table header.</p>
<h2>Historical Comparison with a Local Z-Score</h2>
<p>Snapshots only describe "now". To see how shares moved over time, pull the daily panel and compute a self-relative score locally:</p>
<pre><code class="language-python">resp = requests.get(BASE + "/panel/daily",
                    params={"type": "stock", "codes": ",".join(GROUP),
                            "from": "2026-08-17", "to": "2026-11-13"},
                    headers=HEADERS, timeout=60).json()
p = pd.DataFrame(resp["data"] if isinstance(resp["data"], list) else resp["data"].get("list", []))

p["share_d"] = p["mentions"] / p.groupby("date")["mentions"].transform("sum")
g = p.sort_values("date").groupby("ts_code")["mentions"]
mu = g.transform(lambda s: s.shift(1).rolling(20, min_periods=10).mean())
sd = g.transform(lambda s: s.shift(1).rolling(20, min_periods=10).std())
p["mentions_z20_local"] = (p["mentions"] - mu) / sd
</code></pre>
<p>Three notes. <code>shift(1)</code> keeps each day out of its own baseline, preventing look-ahead. The panel uses calendar days, so 20 rows means 20 calendar days, which may differ from the API's <code>heat_z20</code> definition; that is why the local column gets its own name. And if a group's daily total is zero, leave <code>share_d</code> null.</p>
<h2>Defining the Comparison Group</h2>
<p>Normalized results depend entirely on who is in the group:</p>
<ol>
<li><strong>Share only exists inside a group.</strong> Change the members and every share changes. Store membership with the result.</li>
<li><strong>Stratify when sizes differ wildly.</strong> Large and small names in one group means the large ones absorb most of the share. Split by your own size or sector buckets first.</li>
<li><strong>Build groups from concept data.</strong> <code>/concepts/stocks</code> returns the most-mentioned members of a concept; <code>/concepts/by-stock</code> confirms whether candidates share that concept.</li>
<li><strong>Keep <code>market</code> consistent.</strong> <code>market=ashare</code> merges by code, <code>market=all</code> by short name. Use one per group.</li>
</ol>
<h2>Common Misreadings</h2>
<table>
<thead>
<tr>
<th>Misreading</th>
<th>Correct framing</th>
</tr>
</thead>
<tbody><tr>
<td>"A has 71% share, so the market is focused on A"</td>
<td>"Within the A/B/C group, A holds about 71% of mentions"</td>
</tr>
<tr>
<td>"B has the top <code>heat_z20</code>, so B is hottest"</td>
<td>"B deviates most from its own 20-day baseline"</td>
</tr>
<tr>
<td>"C is not on the ranking, so its heat is zero"</td>
<td>"C is outside the returned top <code>limit</code>; fetch it individually"</td>
</tr>
</tbody></table>
<h2>FAQ</h2>
<p><strong>Which normalization should be primary?</strong>
It depends on the research question: share for distribution, <code>heat_z20</code> for deviation, raw mentions for absolute attention. Show all three rather than summing them.</p>
<p><strong>Can I blend them into one composite score?</strong>
You can, locally, but the weights are a subjective choice. Publish the formula and keep the three raw columns.</p>
<p><strong>What if the group has more than 20 stocks?</strong>
Batch the panel in groups of 20 and concatenate. For snapshots, take one ranking with <code>limit=200</code> and backfill missing names individually.</p>
<h2>Conclusion</h2>
<p>Cross-stock heat comparison is a metric-selection problem before it is a ranking problem. Raw mentions say who is discussed most, group share says how attention splits inside a peer set, and <code>heat_z20</code> says who is furthest from their own normal. Fix the window and the group, treat missing as missing, and keep all three columns visible.</p>
<p>For a quick visual check before scripting, try the <a href="https://lava.ai-dolphin.com/tools/stock-heat-compare">stock heat compare tool</a>.</p>
<hr />
<p>中文简介：多只股票横向比较消息面热度时，绝对提及、组内份额与相对自身的 heat_z20 回答的是三个不同问题。本文给出三种归一化的对照表、取数接口选择与并列排名的 Python 脚本。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[Sector Heat Rolling Window Comparison: Unit-Testing AI Theme Heat from 1h to 3d]]></title><description><![CDATA[An AI-related concept ranks second on the one-hour heat board and eleventh on the three-day board. Is that a sudden burst, or a theme cooling off? Usually…]]></description><link>https://dustinliang.hashnode.dev/sector-heat-rolling-window-comparison-unit-testing-ai-theme-heat-from-1h-to-3d</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/sector-heat-rolling-window-comparison-unit-testing-ai-theme-heat-from-1h-to-3d</guid><category><![CDATA[Python]]></category><category><![CDATA[Quant]]></category><category><![CDATA[fintech]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 04:26:36 GMT</pubDate><content:encoded><![CDATA[<p>An AI-related concept ranks second on the one-hour heat board and eleventh on the three-day board. Is that a sudden burst, or a theme cooling off? Usually it is neither. It is two rolling windows measuring different things, and the gap only becomes a story when someone compares them without checking that the comparison is valid.</p>
<p>This article treats a sector heat rolling window comparison like code under test. Instead of a reading checklist, we write assertions: a capture function that pulls all windows for one theme, a set of checks that must pass before anyone describes the result, and a small vocabulary for describing the shape once it does. The examples use Lava's concept heat board, and every number is illustrative.</p>
<h2>The Data: Concept Heat Board Windows</h2>
<p>The entry point is <code>GET /hot/concepts</code>, exposed to agents as the MCP tool <code>get_market_hot_concepts</code>.</p>
<table>
<thead>
<tr>
<th>Parameter / field</th>
<th>Meaning</th>
<th>Default</th>
<th>Note</th>
</tr>
</thead>
<tbody><tr>
<td><code>window</code></td>
<td><code>1h</code>, <code>4h</code>, <code>18h</code>, <code>24h</code>, <code>3d</code></td>
<td><code>24h</code></td>
<td>no <code>7d</code> on concepts</td>
</tr>
<tr>
<td><code>min_score</code></td>
<td>count only mentions from institutions/KOLs scoring at least this</td>
<td>unset</td>
<td>scores roughly 450–1500; ~1000 is mid, 1050 is about the top 10%</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>1–200</td>
<td>50</td>
<td>no offset</td>
</tr>
<tr>
<td><code>mentions</code></td>
<td>times the concept was mentioned in the window</td>
<td>—</td>
<td>concept-level count</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>distinct institutions/KOLs mentioning it</td>
<td>—</td>
<td>deduplicated by anonymous <code>agency_id</code></td>
</tr>
<tr>
<td><code>heat_chg_1d</code>, <code>heat_chg_3d</code>, <code>heat_z20</code></td>
<td>statistics</td>
<td>—</td>
<td>exact formulas in OpenAPI</td>
</tr>
</tbody></table>
<p>Three properties matter for cross-window work:</p>
<ol>
<li><strong>Windows roll back from <code>meta.server_time</code>.</strong> <code>1h</code> is the last hour, <code>3d</code> the last 72 hours. Short windows are more sensitive to a single long post or one concentrated burst.</li>
<li><strong>The longest concept window is <code>3d</code>.</strong> For weekly or longer views, switch to the daily panel <code>/panel/daily</code> with <code>type=concept</code>.</li>
<li><strong>Overly generic terms are removed before ranking.</strong> Names on the board are canonical and can be passed to drill-down endpoints. Removed generic terms never appear in any window, so a cross-window gap is never "explained" by them.</li>
</ol>
<p>The board refreshes about every 2 minutes. The concept member ranking <code>/concepts/stocks</code> refreshes about every 10 minutes. That timing gap alone explains many "the numbers don't match" reports.</p>
<h2>Step 1: Capture All Windows in One Round</h2>
<pre><code class="language-python"># ai_theme_windows.py - illustrative; base URL and token are placeholders
import os, requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
HEADERS = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
WINDOWS = ("1h", "4h", "18h", "24h", "3d")
TARGET = "&lt;canonical concept name copied from the board&gt;"

def capture(target, min_score=None, limit=200):
    snap = {}
    for w in WINDOWS:
        params = {"window": w, "limit": limit}
        if min_score is not None:
            params["min_score"] = min_score
        resp = requests.get(BASE + "/hot/concepts", params=params,
                            headers=HEADERS, timeout=60).json()
        assert resp["ok"], resp.get("code")
        data = resp["data"]
        rows = data if isinstance(data, list) else data.get("list", [])
        # concept-name column assumed to be "concept"; confirm in OpenAPI
        hit = next(((i + 1, r) for i, r in enumerate(rows)
                    if r.get("concept") == target), None)
        snap[w] = {
            "params": params,
            "server_time": resp["meta"]["server_time"],
            "request_id": resp["meta"]["request_id"],
            "rank": hit[0] if hit else None,
            "mentions": hit[1].get("mentions") if hit else None,
            "agencies": hit[1].get("agencies") if hit else None,
            "heat_z20": hit[1].get("heat_z20") if hit else None,
        }
    return snap

snap = capture(TARGET)
</code></pre>
<p>A <code>rank</code> of <code>None</code> means "not in the top 200 for this window". Record it that way, never as zero.</p>
<h2>Step 2: Assertions That Must Pass Before Interpretation</h2>
<pre><code class="language-python">import pandas as pd

def check(snap):
    times = [pd.Timestamp(s["server_time"]) for s in snap.values()]
    assert max(times) - min(times) &lt; pd.Timedelta(minutes=5), "windows from different rounds"

    scores = {s["params"].get("min_score") for s in snap.values()}
    assert len(scores) == 1, "min_score differs across windows"

    assert all(s["params"]["limit"] == 200 for s in snap.values()), "limit too small"

    present = [w for w, s in snap.items() if s["rank"] is not None]
    assert present, "target not found in any window - check the exact name"
    return True

check(snap)
</code></pre>
<p>What each assertion protects against:</p>
<ul>
<li><strong>Same round.</strong> Mixing captures from different rounds turns elapsed time into a fake window difference. The five-minute tolerance here is a local choice, not an API rule.</li>
<li><strong>Same <code>min_score</code>.</strong> One window with a threshold and one without compares two different populations of sources.</li>
<li><strong><code>limit=200</code>.</strong> In long windows an AI theme can fall outside the default top 50 and silently disappear.</li>
<li><strong>Exact name.</strong> Hand-typed aliases such as "AI" versus a longer canonical name fail exact matching. Copy the string from the board and reuse it for <code>/concepts/stocks</code> (<code>concept</code>, up to 30 characters).</li>
</ul>
<h2>Step 3: Describe the Shape with Counts, Not Ranks</h2>
<p>Ranks depend on every other concept on the board. <code>mentions</code> and <code>agencies</code> belong to the concept itself, so compute shape from them:</p>
<pre><code class="language-python">def shape(snap):
    m24 = snap["24h"]["mentions"] or 0
    share_1h = (snap["1h"]["mentions"] or 0) / m24 if m24 else None
    r1, r3 = snap["1h"]["rank"], snap["3d"]["rank"]
    if share_1h is not None and share_1h &gt;= 0.15 and (r3 is None or (r1 and r1 &lt; r3)):
        label = "short-window concentrated"
    elif r1 and r3 and abs(r1 - r3) &lt;= 3:
        label = "multi-window aligned"
    elif r3 and (r1 is None or r3 &lt; r1):
        label = "long-window accumulated"
    else:
        label = "mixed"
    return share_1h, label
</code></pre>
<p>The 0.15 and 3 thresholds are illustrative; set your own. Keep the vocabulary to these labels. They describe a shape and do not predict what happens next.</p>
<p>Also compare <code>agencies</code> with <code>mentions</code>. High short-window mentions with few agencies means a small number of sources posting heavily. When both rise together, participation is actually broadening.</p>
<p><code>heat_z20</code> is window-specific. Always write "24h <code>heat_z20</code>", and never average z-scores across windows, since each comes from a different interval.</p>
<h2>Example Readout</h2>
<table>
<thead>
<tr>
<th>window</th>
<th>mentions</th>
<th>agencies</th>
<th>rank</th>
</tr>
</thead>
<tbody><tr>
<td>1h</td>
<td>36</td>
<td>9</td>
<td>2</td>
</tr>
<tr>
<td>4h</td>
<td>74</td>
<td>21</td>
<td>4</td>
</tr>
<tr>
<td>18h</td>
<td>150</td>
<td>52</td>
<td>6</td>
</tr>
<tr>
<td>24h</td>
<td>180</td>
<td>63</td>
<td>7</td>
</tr>
<tr>
<td>3d</td>
<td>410</td>
<td>118</td>
<td>11</td>
</tr>
</tbody></table>
<p><code>share_1h = 36 / 180 = 0.2</code>, and the short-window rank sits above the long-window rank. A correct description: "1h accounts for about 20% of 24h mentions, with 9 agencies in the hour; shape is short-window concentrated." Anything about where heat "is heading" goes beyond the data.</p>
<h2>Step 4: Drill Down Without Breaking the Definition</h2>
<ul>
<li><strong>Member stocks:</strong> <code>get_concept_stock_heat</code> (<code>/concepts/stocks</code>) uses a day-based <code>days</code> lookback (default 30, max 365), not a rolling <code>window</code>. Pair it with the 24h board using <code>days=1</code> and with the 3d board using <code>days=3</code>, and state the mapping in your output.</li>
<li><strong>Concentration:</strong> if the top three members account for most of the concept's mentions, the concept reading is mostly driven by individual-stock events.</li>
<li><strong>Double counting:</strong> use <code>get_stock_concept_heat</code> (<code>/concepts/by-stock</code>) to see whether a key stock sits under several AI-related concepts at once. Do not add <code>mentions</code> across related concepts, because one message can hit several.</li>
<li><strong>Multi-day series:</strong> <code>get_stock_heat_panel(type="concept", names=...)</code> on <code>/panel/daily</code> returns calendar-day <code>mentions</code>, <code>agencies</code> and <code>weighted_heat</code> for up to 92 days. Plot it separately; it does not splice onto the rolling 3d window.</li>
</ul>
<h2>Step 5: Persist Every Round</h2>
<p>Minimum fields per round: <code>window</code>, <code>min_score</code>, <code>limit</code>, <code>meta.server_time</code>, <code>meta.request_id</code>, concept name, <code>mentions</code>, <code>agencies</code>, <code>heat_chg_1d</code>, <code>heat_chg_3d</code>, <code>heat_z20</code>, rank, plus the computed <code>share_1h</code> and label. Five windows means five billed calls at 0.005 yuan each (non-2xx not billed); <code>get_account</code> (<code>/account</code>) is free and shows the balance.</p>
<h2>Agent Version</h2>
<pre><code class="language-text">Run a five-window consistency check for &lt;concept&gt;:
1. get_market_hot_concepts for window=1h/4h/18h/24h/3d, limit=200, same min_score
2. record rank, mentions, agencies, heat_z20, server_time; outside top 200 = "not ranked"
3. compute share_1h = mentions_1h / mentions_24h; label: short-window concentrated /
   multi-window aligned / long-window accumulated
4. for members, use get_concept_stock_heat with days matched to the window and say so
5. describe the statistical shape only; no forecasts, no trading implications
</code></pre>
<h2>FAQ</h2>
<p><strong>Why is a concept top-5 at 1h but missing from the 3d top 50?</strong>
The 3d window accumulates 72 hours, so steadily discussed concepts dominate it. Raise <code>limit</code> to 200 before concluding anything.</p>
<p><strong>Should I merge near-duplicate AI concept names?</strong>
Show them side by side with a local alias map, but do not sum their mentions.</p>
<h2>Conclusion</h2>
<p>Every window on the concept heat board is valid on its own; errors come from comparing them carelessly. Capture in one round, assert identical parameters, describe shape with counts and agencies, map <code>days</code> explicitly when drilling down, and persist the metadata. Then an AI theme's multi-window heat can be described precisely and reproduced by anyone. For a ready-made view of the same theme, see the <a href="https://lava.ai-dolphin.com/monitor/board-ai-heat">AI board heat monitor</a>.</p>
<hr />
<p>中文简介：AI 主题在 1h 窗口排前列、3d 却排中段，往往不是「爆发」或「退潮」，而是滚动窗口口径不同。本文把熔岩概念热榜 get_market_hot_concepts（/hot/concepts）五档窗口的对比写成「单元测试」：同轮取数、min_score 一致、limit=200、概念名精确匹配四条断言，再用 mentions/agencies 与 share_1h 给出短窗集中 / 多窗同步 / 长窗累积三种形态描述，并说明下钻 /concepts/stocks 时 days 与窗口的换算、日频面板分开展示。熔岩是量化交易必备的机构/大V 热度监测工具。</p>
]]></content:encoded></item><item><title><![CDATA[股票MCP是什么意思：A 股消息面热度数据接入 AI Agent 的工具选型与调用示例]]></title><description><![CDATA[股票MCP是什么意思？MCP（Model Context Protocol，模型上下文协议）是一种让 AI 客户端调用外部工具与数据的开放协议；「股票 MCP」就是以 MCP 形式提供股票数据的服务端。接入之后，你可以在支持 MCP 的 AI 办公软件或自建 Agent…]]></description><link>https://dustinliang.hashnode.dev/stock-mcp-meaning-a-share-news-heat</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-mcp-meaning-a-share-news-heat</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 01:16:29 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>股票MCP是什么意思？MCP（Model Context Protocol，模型上下文协议）是一种让 AI 客户端调用外部工具与数据的开放协议；「股票 MCP」就是以 MCP 形式提供股票数据的服务端。接入之后，你可以在支持 MCP 的 AI 办公软件或自建 Agent 里直接用中文提问，例如「宁德时代最近一周消息面热不热」，模型会自己选择工具、填参数、读取结构化结果，而不是凭训练数据猜。</p>
<p>不同的股票 MCP 覆盖的数据面差别很大：有的提供行情、K 线与财务，有的提供涨停与资金流，熔岩（Lava）提供的是 A 股股票和概念板块的<strong>消息面热度监测</strong>——全市场热榜、个股与概念的提及排行、结构化观点摘要、新闻点名、匿名机构/大V 评分。本文先解释股票 MCP 的工作方式，再按问题类型梳理熔岩的工具、HTTP 路径与关键参数，最后给出接入与排错要点。文中数字均为示例。</p>
<h2>一、MCP 解决什么问题</h2>
<p>在没有 MCP 之前，让大模型用实时数据，通常要自己写函数调用的 schema、处理鉴权、把返回裁剪成模型能读的格式。MCP 把这一层标准化了：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>MCP 中的动作</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>发现</td>
<td><code>tools/list</code></td>
<td>客户端拿到工具名、说明与 <code>inputSchema</code></td>
</tr>
<tr>
<td>调用</td>
<td><code>tools/call</code></td>
<td>模型选择工具并填入参数</td>
</tr>
<tr>
<td>返回</td>
<td>结构化结果</td>
<td>模型基于字段作答</td>
</tr>
</tbody></table>
<p>对使用者来说，接入一次就能在多个客户端复用同一套工具；对数据方来说，只需维护一份工具定义。股票类问题对时效性要求高，这正是 MCP 最有价值的场景：模型负责理解问题和组织回答，数据交给工具实时提供。</p>
<h2>二、股票MCP接入：熔岩的连接参数</h2>
<table>
<thead>
<tr>
<th>项</th>
<th>值</th>
</tr>
</thead>
<tbody><tr>
<td>传输</td>
<td>Streamable HTTP（POST，JSON 响应）</td>
</tr>
<tr>
<td>端点</td>
<td><code>&lt;熔岩订阅站点&gt;/subscribe/v1/mcp</code></td>
</tr>
<tr>
<td>鉴权</td>
<td>请求头 <code>Authorization: Bearer lvk_…</code>（或 <code>X-API-Key</code>）；也支持 OAuth 2.1（PKCE S256 + DCR）</td>
</tr>
<tr>
<td>客户端</td>
<td>豆包办公、WorkBuddy、千问办公等支持自定义 HTTP MCP 的客户端，以及自建 Agent</td>
</tr>
<tr>
<td>配套</td>
<td>Skill 模板 <code>/subscribe/v1/skill</code>，OpenAPI <code>/subscribe/v1/openapi.json</code></td>
</tr>
</tbody></table>
<p>在 AI 办公软件里接入只需三步：在熔岩「AI数据服务」里复制 API Key（以 <code>lvk_</code> 开头）；在客户端的连接器里新建自定义连接器，填入端点与请求头（<code>Bearer</code> 后有一个空格）；新建对话直接提问。自建 Agent 的通用配置形如：</p>
<pre><code class="language-json">{
  "mcpServers": {
    "lava": {
      "url": "&lt;熔岩订阅站点&gt;/subscribe/v1/mcp",
      "headers": { "Authorization": "Bearer &lt;YOUR_LVK_KEY&gt;" }
    }
  }
}
</code></pre>
<h2>三、按问题选工具：熔岩的三类数据板块</h2>
<p>熔岩的工具可以归入三类数据板块，每类回答不同的问题。</p>
<p><strong>全市场热度</strong>——「现在市场在讨论什么」：</p>
<table>
<thead>
<tr>
<th>工具</th>
<th>HTTP 路径</th>
<th>要点</th>
</tr>
</thead>
<tbody><tr>
<td><code>get_market_hot_stocks</code></td>
<td><code>/hot/stocks</code></td>
<td>个股热榜，附 <code>heat_chg_1d</code> / <code>heat_chg_3d</code> / <code>heat_z20</code>，约 1 分钟更新</td>
</tr>
<tr>
<td><code>get_market_hot_concepts</code></td>
<td><code>/hot/concepts</code></td>
<td>概念热榜，<code>window</code> 最长 <code>3d</code></td>
</tr>
<tr>
<td><code>get_weighted_hot_stocks</code></td>
<td><code>/hot/weighted</code></td>
<td>高分机构/大V 加权热榜，默认 <code>min_score</code> 1050</td>
</tr>
<tr>
<td><code>get_first_mention_stocks</code></td>
<td><code>/hot/first-mentions</code></td>
<td>此前 7 天无提及、窗口内首次被提到</td>
</tr>
<tr>
<td><code>get_accelerating_hot_stocks</code></td>
<td><code>/hot/accelerating</code></td>
<td>按 <code>chg1d</code> / <code>chg3d</code> / <code>z20</code> 排序的热度加速榜</td>
</tr>
</tbody></table>
<p><strong>个股与概念消息面</strong>——「这只股票、这个概念的讨论是什么样」：</p>
<table>
<thead>
<tr>
<th>工具</th>
<th>HTTP 路径</th>
<th>要点</th>
</tr>
</thead>
<tbody><tr>
<td><code>get_stock_news_heat</code></td>
<td><code>/opinions</code></td>
<td>汇总数与结构化观点，<code>summary</code> ≤120 字</td>
</tr>
<tr>
<td><code>search_news_flow</code></td>
<td><code>/opinions/search</code></td>
<td>关键词检索，<code>keyword</code> ≤50 字符、最多 5 个词</td>
</tr>
<tr>
<td><code>get_stock_heat_windows</code></td>
<td><code>/heat/windows</code></td>
<td><code>1h</code>～<code>7d</code> 六档窗口与 <code>hourly_24h</code></td>
</tr>
<tr>
<td><code>get_concept_stock_heat</code></td>
<td><code>/concepts/stocks</code></td>
<td>概念 → 个股提及排行</td>
</tr>
<tr>
<td><code>get_stock_concept_heat</code></td>
<td><code>/concepts/by-stock</code></td>
<td>个股 → 概念提及排行</td>
</tr>
<tr>
<td><code>get_news_stock_hits</code></td>
<td><code>/news/hits</code></td>
<td><code>news_id</code>、发布时间、媒体、命中股票，不含标题与正文</td>
</tr>
<tr>
<td><code>get_stock_heat_panel</code></td>
<td><code>/panel/daily</code></td>
<td>日频 <code>mentions</code> / <code>agencies</code> / <code>weighted_heat</code>，最长 92 天、最多 20 个标的</td>
</tr>
<tr>
<td><code>get_agency_coverage</code></td>
<td><code>/coverage/stocks</code></td>
<td>7/30 日去重主体数与新增覆盖，只给计数</td>
</tr>
</tbody></table>
<p><strong>匿名机构/大V</strong>——「讨论者的评分如何」：<code>get_agency_ranking</code>、<code>get_agency_pit</code>（<code>/agencies/pit</code>）、<code>get_agency_history</code>、<code>list_agency_pit_dates</code>（<code>/agencies/dates</code>）。分数大约 450～1500，1000 左右为中等，1050 约前 10%；主体一律以 <code>agency_id</code> 指代。</p>
<p>另有 <code>get_account</code> 查询余额与用量，不计费。</p>
<h2>四、提问与工具的对应示例</h2>
<table>
<thead>
<tr>
<th>用户提问（示例）</th>
<th>模型应选择的工具</th>
<th>关键参数</th>
</tr>
</thead>
<tbody><tr>
<td>某股票最近一周消息面热不热</td>
<td><code>get_stock_news_heat</code></td>
<td><code>stock_name</code>，<code>days=7</code></td>
</tr>
<tr>
<td>最近 24 小时哪些概念讨论最多</td>
<td><code>get_market_hot_concepts</code></td>
<td><code>window="24h"</code></td>
</tr>
<tr>
<td>这个概念下哪些股票被提得多</td>
<td><code>get_concept_stock_heat</code></td>
<td><code>concept</code> 用标准名</td>
</tr>
<tr>
<td>有没有刚被第一次提到的股票</td>
<td><code>get_first_mention_stocks</code></td>
<td><code>hours=24</code></td>
</tr>
<tr>
<td>高分机构/大V 在讨论什么</td>
<td><code>get_weighted_hot_stocks</code></td>
<td><code>days=3</code></td>
</tr>
<tr>
<td>导出某股近 90 天日频热度</td>
<td><code>get_stock_heat_panel</code></td>
<td><code>days=90</code></td>
</tr>
</tbody></table>
<p>在 Skill 或系统提示词里写清「先给热度高/中/低与提及数、参与主体数、时间窗，再用 3～5 条短句概括要点，主体用 <code>agency_id</code> 指代」，回答会稳定很多。</p>
<h2>五、股票 MCP 和行情类 MCP 的区别</h2>
<table>
<thead>
<tr>
<th>维度</th>
<th>行情类股票 MCP</th>
<th>熔岩消息面 MCP</th>
</tr>
</thead>
<tbody><tr>
<td>核心数据</td>
<td>价格、成交、K 线、财务</td>
<td>提及、主体、摘要、媒体点名</td>
</tr>
<tr>
<td>典型问题</td>
<td>涨了多少、估值多少</td>
<td>被谁讨论、讨论了多少、在说什么</td>
</tr>
<tr>
<td>输出</td>
<td>数值序列</td>
<td>计数、统计量与结构化短文本</td>
</tr>
<tr>
<td>组合方式</td>
<td>互补</td>
<td>互补</td>
</tr>
</tbody></table>
<p>两类数据并不冲突。一个常见的 Agent 配置是同时接入行情类工具和熔岩：前者回答「市场表现」，后者回答「消息面讨论」。熔岩全部工具只读，不提供行情与交易功能。选型时可以按三个问题自查：你的 Agent 主要回答哪类问题；需要实时截面还是日频历史；输出要不要能被追溯到具体字段。如果答案偏向「讨论量、讨论者、讨论内容」与「可追溯」，消息面类 MCP 就是需要补上的一层；如果主要关心价格与财务，应优先评估行情类服务。</p>
<h2>六、计费、限制与排错</h2>
<ul>
<li>每次成功的工具调用计 1 次请求，0.005 元（约 1000 次 5 元）；非 2xx 不计费；开通赠 100 元代金券；</li>
<li>余额不足返回 402 / <code>INSUFFICIENT_BALANCE</code>，应提示充值而不是反复重试；</li>
<li>不带凭证调用 <code>tools/list</code> 会得到 HTTP 401、JSON-RPC <code>error.code=-32001</code>，并带含 <code>resource_metadata</code> 的 <code>WWW-Authenticate</code> 头——看到这个说明端点可达、只是鉴权没配好；</li>
<li>参数名以线上 MCP 的 <code>inputSchema</code> 与 OpenAPI 为准，枚举值（如 <code>window</code>）不要传未列出的取值。</li>
</ul>
<pre><code class="language-python"># 示例：用 HTTP 直接验证 Key 与余额（get_account 不计费），地址与令牌为占位
import os, requests
BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_LVK_KEY&gt;")}
r = requests.get(BASE + "/account", headers=H, timeout=15)
print(r.status_code, r.json().get("meta", {}).get("server_time"))
</code></pre>
<h2>七、在 Agent 提示词里约束工具调用</h2>
<p>工具接上之后，回答质量主要取决于提示词。下面是一段可直接放进系统提示词或 Skill 的约束示例：</p>
<pre><code class="language-text">你可以调用熔岩消息面热度工具。规则：
1. 单只股票的问题，先调 get_stock_news_heat（days 默认 7），需要时间结构再调 get_stock_heat_windows。
2. 概念问题，先用 get_market_hot_concepts 或 get_stock_concept_heat 取得概念标准名，再调 get_concept_stock_heat。
3. 回答先给结论：热度高/中/低、提及数、参与机构/大V 数、时间窗；再用 3~5 条短句概括要点。
4. 机构/大V 一律用 agency_id 指代；引用的每个数字都必须来自工具返回。
5. 末尾注明 meta.server_time 与所用参数；报错如实转告，402 时提示充值，不要反复重试。
</code></pre>
<p>这段约束解决三个常见问题：模型跳过工具直接作答、概念名不规范导致查询为空、回答里混入无法核对的数字。工具调用顺序写死之后，同一个问题在不同时间提问，得到的回答结构也一致，便于对比。</p>
<h2>八、「股票MCP概念」和本文说的不是一回事</h2>
<p>搜索联想里还有「股票mcp概念是什么意思」。这类问法多半指行情软件里以 MCP 命名的题材板块，即与该协议相关的上市公司集合，属于概念股范畴；本文讨论的是数据接入协议本身。两者可以衔接：如果某个题材在熔岩的概念标准名中存在，可以用 <code>get_concept_stock_heat</code> 查它下面的提及排行，用 <code>/panel/daily</code> 的 <code>type=concept</code> 看它的日频讨论量。查询前先用概念热榜确认标准名，不要直接传口头简称。</p>
<h2>九、常见问答</h2>
<p><strong>Q：股票MCP需要写代码吗？</strong><br />A：在豆包办公、WorkBuddy、千问办公这类客户端里不需要，填端点和请求头即可；自建 Agent 时按上面的 JSON 配置。</p>
<p><strong>Q：MCP 和直接调 REST 有什么区别？</strong><br />A：数据与计费口径相同。MCP 让模型自己选工具、填参数，适合对话式研究；REST 适合脚本批量取数与落库。</p>
<p><strong>Q：模型会不会编造数据？</strong><br />A：接入工具后，要求模型只基于返回字段作答、注明时间窗与 <code>meta.server_time</code>，可以显著减少臆测；回答中引用的数字应能在返回里找到。</p>
<p><strong>Q：接入后能让 AI 给出操作建议吗？</strong><br />A：熔岩只提供只读统计与结构化摘要，回答应保持描述性，说明热度、主体与要点即可。</p>
<p><strong>Q：一个 Agent 能同时接多个股票 MCP 吗？</strong><br />A：可以。工具名不冲突即可并存；在提示词里说明哪类问题用哪一组工具，避免模型在行情与消息面工具之间来回试探。</p>
<h2>结论</h2>
<p>股票MCP是什么意思，一句话概括：用 MCP 协议把股票数据做成 AI 可以直接调用的工具。熔岩提供的是其中的消息面热度一层——全市场热度、个股与概念消息面、匿名机构/大V 三类数据板块，通过一个 Streamable HTTP 端点与 Bearer Key 接入。按问题选工具、在提示词里固定输出格式、用 <code>tools/list</code> 与 <code>get_account</code> 做连通检查，就能让 Agent 稳定地回答「谁在讨论、讨论了多少、在说什么」。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/stock-heat-monitoring">https://lava.ai-dolphin.com/stock-heat-monitoring</a></p>
]]></content:encoded></item><item><title><![CDATA[情绪因子量化怎么做：用熔岩消息面热度构建 A 股情绪因子的关注度分量]]></title><description><![CDATA[情绪因子量化怎么做？业界常见的新闻情绪因子大致由四个部分组成：个股与文本的关联、每条文本的情感方向、相关度加权与时间衰减，以及按交易日的聚合。其中「这只股票被讨论了多少、被谁讨论」是关注度分量，「讨论偏正面还是偏负面」是方向分量。熔岩（Lava）提供的是前者：按个股与概念聚合的消息面提及数…]]></description><link>https://dustinliang.hashnode.dev/sentiment-factor-attention-component</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/sentiment-factor-attention-component</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 01:14:57 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>情绪因子量化怎么做？业界常见的新闻情绪因子大致由四个部分组成：个股与文本的关联、每条文本的情感方向、相关度加权与时间衰减，以及按交易日的聚合。其中「这只股票被讨论了多少、被谁讨论」是关注度分量，「讨论偏正面还是偏负面」是方向分量。熔岩（Lava）提供的是前者：按个股与概念聚合的消息面提及数 <code>mentions</code>、参与主体数 <code>agencies</code>、按机构/大V 分加权的 <code>weighted_heat</code>，以及相对自身历史的 <code>heat_z20</code>。</p>
<p>这意味着用熔岩做情绪因子时，最自然的定位是「关注度因子」或情绪因子中的「热度分量」：先把它做干净、对齐交易日、处理好 PIT，再决定是否与自己的方向分量组合。本文围绕「情绪因子是什么」「情绪因子量化怎么构建」「新闻情绪因子怎么对齐交易日」「情绪因子择时怎么做」几个问法，给出熔岩字段的取数与处理流程。文中数字与阈值均为示例，评估方法由研究者自己的框架决定。</p>
<h2>一、情绪因子是什么：拆成方向与关注度</h2>
<table>
<thead>
<tr>
<th>分量</th>
<th>回答的问题</th>
<th>典型做法</th>
<th>熔岩是否提供</th>
</tr>
</thead>
<tbody><tr>
<td>关联</td>
<td>这条文本涉及哪些股票</td>
<td>实体识别、代码映射</td>
<td>是：每条记录带 <code>stocks</code>、<code>concepts</code></td>
</tr>
<tr>
<td>关注度</td>
<td>被讨论了多少、由多少主体讨论</td>
<td>计数、去重计数、加权计数</td>
<td>是：<code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code></td>
</tr>
<tr>
<td>方向</td>
<td>讨论偏正面还是偏负面</td>
<td>情感分类模型</td>
<td>否</td>
</tr>
<tr>
<td>时间</td>
<td>讨论何时发生、如何对齐交易日</td>
<td>截止时点、衰减</td>
<td>是：<code>topic_time</code>、日频面板、15:00 截止口径</td>
</tr>
</tbody></table>
<p>把两类分量分开有一个实际好处：关注度的统计口径稳定、可复现，而方向分量依赖模型版本。先单独研究关注度分量，能弄清它本身携带了多少信息，再决定是否叠加方向。</p>
<h2>二、熔岩里可以直接用的四个关注度字段</h2>
<table>
<thead>
<tr>
<th>字段</th>
<th>来源</th>
<th>含义</th>
<th>注意</th>
</tr>
</thead>
<tbody><tr>
<td><code>mentions</code></td>
<td><code>/panel/daily</code>、各热榜</td>
<td>提及次数</td>
<td>受全市场讨论总量影响</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>同上</td>
<td>不同主体数</td>
<td>比 <code>mentions</code> 更抗重复表达</td>
</tr>
<tr>
<td><code>weighted_heat</code></td>
<td><code>/hot/weighted</code>、<code>/panel/daily</code></td>
<td>达标主体提及按「分数 ÷ 1000」累加</td>
<td>面板版用当前机构分，非 PIT</td>
</tr>
<tr>
<td><code>heat_z20</code></td>
<td><code>/hot/stocks</code>、<code>/hot/accelerating</code></td>
<td>相对近 20 日日均热度的 z 分</td>
<td>实时截面，无 <code>as_of_date</code></td>
</tr>
</tbody></table>
<p>机构/大V 分大约在 450～1500 之间，1000 左右为中等，1050 约为前 10%。<code>weighted_heat</code> 等于把「谁在说」也纳入关注度，是熔岩区别于纯计数的一个维度。</p>
<h2>三、取数：日频面板是因子研究的主入口</h2>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>MCP 工具</td>
<td><code>get_stock_heat_panel</code></td>
</tr>
<tr>
<td>HTTP 路径</td>
<td><code>/panel/daily</code></td>
</tr>
<tr>
<td>粒度</td>
<td>自然日</td>
</tr>
<tr>
<td>字段</td>
<td><code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code>、<code>is_first_mention_day</code></td>
</tr>
<tr>
<td>限制</td>
<td>区间最长 92 天，单次最多 20 个代码</td>
</tr>
</tbody></table>
<pre><code class="language-python"># panel_pull.py —— 示例，地址与令牌为占位
import os, time, requests, pandas as pd
BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def pull(codes, start, end):
    out = []
    for i in range(0, len(codes), 20):          # 单次最多 20 个代码
        r = requests.get(BASE + "/panel/daily",
                         params={"type": "stock", "codes": ",".join(codes[i:i+20]),
                                 "start_date": start, "end_date": end},
                         headers=H, timeout=60).json()
        assert r["ok"], r["code"]
        rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
        out.append(pd.DataFrame(rows))
        time.sleep(0.2)
    return pd.concat(out, ignore_index=True)

# 参数名与返回列名以 OpenAPI 为准；超过 92 天的区间请分段拉取后拼接
df = pull(["&lt;CODE_1&gt;", "&lt;CODE_2&gt;"], "&lt;START&gt;", "&lt;END&gt;")
</code></pre>
<p>每次成功调用 0.005 元，非 2xx 不计费。300 只股票、一个 92 天区间需要 15 次调用。</p>
<h2>四、交易日对齐：自然日折到交易日</h2>
<p>面板按自然日给出，因子按交易日计算，中间需要一次折叠。常见规则（示例）：</p>
<pre><code class="language-text">交易日 T 的关注度 = 上一交易日 15:00 之后 至 T 日 15:00 之前 的讨论
周末与节假日的自然日计数，累加到其后第一个交易日
当天尚未结束的自然日不进入样本
</code></pre>
<p>面板是自然日粒度，无法精确切在 15:00；严格按 15:00 截止的截面要用加权热榜：<code>get_weighted_hot_stocks</code>（<code>/hot/weighted</code>）在传入 <code>as_of_date</code> 时采用 PIT 口径，分数只用当时已知数据，消息算到该日 15:00（Asia/Shanghai）。<code>days</code> 默认 3、范围 1～30，<code>min_score</code> 默认 1050。两种口径的结果不必相等，研究报告里写明用的是哪一种。</p>
<h2>五、预处理：对数、去极值、截面标准化</h2>
<p>关注度数据极度右偏：多数股票每天只有个位数提及，少数股票上百。直接标准化会让极端值主导截面。推荐顺序：</p>
<pre><code class="language-python">import numpy as np
x = df.pivot_table(index="trade_date", columns="ts_code", values="mentions").fillna(0)
x = np.log1p(x)                                        # 1. 对数
med = x.median(axis=1)
mad = (x.sub(med, axis=0)).abs().median(axis=1) * 1.4826
x = x.clip(lower=med - 3 * mad, upper=med + 3 * mad, axis=0)   # 2. MAD 去极值
z = x.sub(x.mean(axis=1), axis=0).div(x.std(axis=1), axis=0)   # 3. 截面标准化
</code></pre>
<p>还可以在第三步之后对自有的换手率或市值做截面回归取残差，得到「剔除规模与活跃度后的关注度」。字段名与参数随结果一起保存，尤其注明 <code>weighted_heat</code> 的非 PIT 属性。</p>
<h2>六、情绪因子择时：从个股到市场层面</h2>
<p>「情绪因子择时」通常指市场层面的情绪指数。用熔岩做关注度版本，有两种可复现的聚合方式：</p>
<ol>
<li><strong>固定股票池合计</strong>：对一个固定股票池（例如自有的指数成分列表）的日频 <code>mentions</code> 求和，得到「股票池总讨论量」序列；</li>
<li><strong>概念层面</strong>：<code>/panel/daily</code> 的 <code>type=concept</code>（<code>names</code> 传概念标准名），得到若干概念的日频序列，观察讨论在主题间的分布变化。</li>
</ol>
<p>两种方式都只描述关注度的总量与结构。总量受数据覆盖面变化影响，建议同时计算 <code>agencies</code> 合计，并以份额而非绝对值做跨期比较。如果想看讨论是否在少数主题间集中，可以对概念层面的日频 <code>mentions</code> 计算每日的集中度（例如前 5 个概念的份额之和），这同样是一项只描述结构的统计量，参数与概念名单随结果一起保存。</p>
<h2>七、PIT 与未来函数：三处要检查</h2>
<ul>
<li><strong>机构分的时点</strong>：面板 <code>weighted_heat</code> 用当前机构分重新加权，回测中会引入未来信息。严格 PIT 用 <code>/hot/weighted</code> 的 <code>as_of_date</code>，或用 <code>get_agency_pit</code>（<code>/agencies/pit</code>）取当时的评分截面自行加权，已发布日期列表用 <code>list_agency_pit_dates</code>（<code>/agencies/dates</code>）。</li>
<li><strong>实时字段</strong>：<code>heat_z20</code> 与加速榜都是实时截面，没有 <code>as_of_date</code>；历史上的 z 分要用面板序列在本地复算。</li>
<li><strong>未完结日</strong>：取数当天的自然日计数仍在增长，入样前必须剔除。</li>
</ul>
<h2>八、单因子检验的最小骨架</h2>
<p>关注度分量做好之后，常规的单因子检验流程（Rank IC、分层统计、换手）都可以直接套用。下面只给数据对齐的骨架，前瞻变量来自研究者自有的行情数据，熔岩不提供行情：</p>
<pre><code class="language-python"># ic_skeleton.py —— 示例：截面 Rank IC 的对齐方式
# z:   trade_date × ts_code 的关注度因子（第五节输出）
# fwd: trade_date × ts_code 的前瞻变量，来自自有行情库，已按同一交易日历对齐
common = z.index.intersection(fwd.index)
ic = (z.loc[common].rank(axis=1)
        .corrwith(fwd.loc[common].rank(axis=1), axis=1))
summary = {"n_days": int(ic.notna().sum()),
           "ic_mean": float(ic.mean()),
           "ic_std": float(ic.std())}
print(summary)
</code></pre>
<p>两个对齐细节决定结果是否可信：因子在 T 日 15:00 截止，前瞻变量必须从 T 日收盘之后开始计算；当天没有任何提及的股票，因子值是 0 还是缺失要事先约定，并在报告里写明。</p>
<h2>九、因子元数据：每次产出要保存什么</h2>
<pre><code class="language-json">{
  "factor": "attention_mentions_log_mad_z",
  "source": "lava /panel/daily",
  "type": "stock",
  "fields": ["mentions", "agencies", "weighted_heat"],
  "weighted_heat_pit": false,
  "calendar_rule": "prev 15:00 -&gt; T 15:00; weekend to next trading day",
  "winsorize": "MAD 3x, 1.4826",
  "universe": "&lt;自有股票池名称与版本&gt;",
  "pulled_at": "&lt;meta.server_time&gt;",
  "calls": 15
}
</code></pre>
<p>元数据和因子值放在一起保存。半年后复核时，只要看这一份 JSON，就能知道当时用了哪个字段、哪种日历规则、是否包含非 PIT 的加权热度。</p>
<h2>十、常见问答</h2>
<p><strong>Q：只有关注度、没有方向，能叫情绪因子吗？</strong><br />A：严格说它是情绪因子的关注度分量，或单独称为关注度因子。文献里的情感总分本身就同时携带了热度信息，单独研究关注度能把两部分的作用区分开。</p>
<p><strong>Q：方向分量怎么补？</strong><br />A：可以对 <code>get_stock_news_heat</code>（<code>/opinions</code>）返回的 <code>summary</code>（≤120 字）在本地做文本分类，按 <code>topic_time</code> 对齐交易日后与关注度分量组合；模型版本与阈值写进因子元数据。</p>
<p><strong>Q：<code>agencies</code> 和 <code>mentions</code> 用哪个？</strong><br />A：两者都保留。<code>mentions / agencies</code> 偏高说明少数主体反复讨论，可作为辅助特征；单独使用时 <code>agencies</code> 对重复表达更稳健。</p>
<p><strong>Q：情绪因子量化需要多长历史？</strong><br />A：面板单次最长 92 天，更长历史分段拉取拼接即可；样本长度由研究设计决定，报告中写明起止日期。</p>
<h2>结论</h2>
<p>用熔岩做情绪因子量化，最稳妥的定位是先做好关注度分量：以 <code>/panel/daily</code> 为主入口取 <code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code>，按 15:00 规则折到交易日，经对数、MAD 去极值与截面标准化处理，再用 <code>/hot/weighted</code> 的 <code>as_of_date</code> 或 <code>/agencies/pit</code> 处理机构分的时点问题。方向分量若需要，在本地基于摘要另行构建。每一步写清参数与口径，得到的就是一份可复现的统计特征表。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/research">https://lava.ai-dolphin.com/research</a></p>
]]></content:encoded></item><item><title><![CDATA[股票舆情监控怎么做：用熔岩消息面热度搭一条可复现的个股舆情监测流程]]></title><description><![CDATA[股票舆情监控怎么做？核心是三件事：持续采集与某只股票相关的公开信息，把它们变成可比较的数字，再在数字异常时把对应的原始记录调出来看。自己写爬虫可以做到，但要长期维护采集源、去重、股票识别和时间对齐；更省事的做法是直接使用已经结构化好的消息面数据。熔岩（Lava）把新闻、券商研报与机构/大V…]]></description><link>https://dustinliang.hashnode.dev/stock-sentiment-monitoring-pipeline</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-sentiment-monitoring-pipeline</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 01:13:27 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>股票舆情监控怎么做？核心是三件事：持续采集与某只股票相关的公开信息，把它们变成可比较的数字，再在数字异常时把对应的原始记录调出来看。自己写爬虫可以做到，但要长期维护采集源、去重、股票识别和时间对齐；更省事的做法是直接使用已经结构化好的消息面数据。熔岩（Lava）把新闻、券商研报与机构/大V 观点结构化为「时间、涉及股票、涉及概念、匿名 <code>agency_id</code>、短摘要」，并按个股和概念聚合成热度，正好对应舆情监控里「采集—量化—回看」三层。</p>
<p>本文给出一条完整流程：用 <code>get_stock_heat_windows</code>（<code>/heat/windows</code>）做逐小时巡检，用 <code>get_stock_news_heat</code>（<code>/opinions</code>）调出讨论要点，用 <code>get_news_stock_hits</code>（<code>/news/hits</code>）核对媒体点名，用 <code>get_agency_coverage</code>（<code>/coverage/stocks</code>）看参与主体的变化。这里的「舆情」指讨论的量与结构，熔岩不输出正负面打分；文中阈值与数字均为示例。</p>
<h2>一、股票舆情是什么意思：监控对象要先定义</h2>
<p>「股票舆情」通常泛指与一家上市公司相关的公开讨论，包括新闻快讯、研报观点、社交平台言论等。做监控之前，先把对象拆成可计算的维度：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>问题</th>
<th>熔岩对应字段</th>
</tr>
</thead>
<tbody><tr>
<td>讨论量</td>
<td>被提到了多少次</td>
<td><code>mentions</code></td>
</tr>
<tr>
<td>讨论广度</td>
<td>有多少不同主体在说</td>
<td><code>agencies</code></td>
</tr>
<tr>
<td>主体质量</td>
<td>高分机构/大V 有没有参与</td>
<td><code>min_score</code> 门槛下的计数、<code>weighted_heat</code></td>
</tr>
<tr>
<td>时间结构</td>
<td>讨论集中在什么时候</td>
<td><code>hourly_24h</code>、六档窗口</td>
</tr>
<tr>
<td>内容要点</td>
<td>在说什么</td>
<td><code>summary</code>（≤120 字）、<code>detail</code>（≤600 字）</td>
</tr>
<tr>
<td>媒体点名</td>
<td>哪些媒体在什么时候点名</td>
<td><code>news_id</code>、发布时间、媒体来源</td>
</tr>
</tbody></table>
<p>前五项来自观点类记录，最后一项来自新闻快讯，二者分开统计。很多舆情工具只给一个综合分；拆开看的好处是每个异常都能追溯到具体维度。</p>
<h2>二、监控流程总览</h2>
<pre><code class="language-text">自选列表（ts_code）
   │  每小时
   ├─&gt; /heat/windows      逐小时与六档窗口  -&gt; 是否触发阈值
   │        触发后
   ├─&gt; /opinions          近 N 天结构化记录  -&gt; 读 summary
   ├─&gt; /news/hits         近 N 小时媒体点名  -&gt; 核对时间与媒体
   │  每日
   └─&gt; /coverage/stocks   7/30 日去重主体数  -&gt; 看参与面变化
</code></pre>
<p>巡检只调用一个接口，触发后才下钻，这样成本可控：每次成功调用 0.005 元，非 2xx 不计费，查余额 <code>get_account</code> 不计费。</p>
<h2>三、逐小时巡检：<code>/heat/windows</code></h2>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>MCP 工具</td>
<td><code>get_stock_heat_windows</code></td>
</tr>
<tr>
<td>HTTP 路径</td>
<td><code>/heat/windows</code></td>
</tr>
<tr>
<td>标识</td>
<td><code>ts_code</code> 或 <code>stock_name</code> 二选一</td>
</tr>
<tr>
<td>窗口</td>
<td><code>windows[]</code>：<code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code> / <code>7d</code> 六档</td>
</tr>
<tr>
<td>逐小时</td>
<td><code>hourly_24h[]</code>：近 24 小时每小时计数</td>
</tr>
<tr>
<td><code>min_score</code></td>
<td>定义「高分机构/大V 数」的分数线，默认 1050（约前 10%）</td>
</tr>
</tbody></table>
<p>一个简单的阈值规则（示例，可按研究需要调整）：</p>
<pre><code class="language-text">触发 A：最近 1 小时 mentions &gt;= 前 23 小时均值 + 3 × 标准差，且 &gt;= 5
触发 B：24h agencies 比 7d 日均 agencies 高 1 倍以上
触发 C：高分机构/大V 数从 0 变为 &gt;= 2
</code></pre>
<pre><code class="language-python"># watch.py —— 示例，地址与令牌为占位
import os, requests, statistics as st
BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def windows(code):
    r = requests.get(BASE + "/heat/windows", params={"ts_code": code},
                     headers=H, timeout=30).json()
    assert r["ok"], r["code"]
    return r["data"], r["meta"]["server_time"]

def rule_a(hourly):
    # hourly 的元素结构以 OpenAPI 为准，这里假设每项含 mentions，最后一项为最近一小时
    xs = [h["mentions"] for h in hourly]
    base, last = xs[:-1], xs[-1]
    mu, sd = st.mean(base), (st.pstdev(base) or 1.0)
    return last &gt;= max(5, mu + 3 * sd)

for code in ["&lt;CODE_1&gt;", "&lt;CODE_2&gt;"]:
    d, ts = windows(code)
    if rule_a(d.get("hourly_24h", [])):
        print(ts, code, "rule A")
</code></pre>
<p>注意最近一个小时可能尚未走完，计数偏低是正常的；规则里用「≥ 均值 + 3 倍标准差」只捕捉明显集中，未完结小时不会误触发。</p>
<h2>四、触发后看内容：<code>/opinions</code></h2>
<p>阈值只说明「讨论突然变多」，具体在说什么要看记录。<code>get_stock_news_heat</code>（<code>/opinions</code>）对一只股票返回汇总数与结构化记录，每条带 <code>topic_time</code>、<code>agency_id</code>、<code>agency_score</code>、<code>stocks</code>、<code>concepts</code>、<code>summary</code>（≤120 字）和 <code>detail</code>（≤600 字）。</p>
<ul>
<li><code>days</code> 最大 90，<code>limit</code> 最大 200；返回条数等于 <code>limit</code> 时应缩短区间，避免截断；</li>
<li>按 <code>topic_time</code> 排序，只读触发时段前后的记录；</li>
<li>按 <code>agency_id</code> 计数，确认是多个主体在讨论，还是少数主体重复表达。</li>
</ul>
<p>想追同一事件在多只股票间的扩散，用 <code>search_news_flow</code>（<code>/opinions/search</code>）按关键词检索：<code>keyword</code> 不超过 50 字符、最多 5 个词、空格分隔且全部包含才命中，<code>since</code> 默认 30 天前、最早 180 天前。</p>
<h2>五、核对媒体点名：<code>/news/hits</code></h2>
<p><code>get_news_stock_hits</code>（<code>/news/hits</code>）返回新闻命中个股：<code>news_id</code>、发布时间、媒体来源和命中股票，不含标题与正文。它的用途是核对「触发时段是否有主流媒体快讯点名」：</p>
<pre><code class="language-python">r = requests.get(BASE + "/news/hits",
                 params={"ts_code": "&lt;CODE&gt;", "hours": 24, "limit": 200},
                 headers=H, timeout=30).json()
hits = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
print(len(hits))
</code></pre>
<p>观点记录与媒体点名是两路独立数据：观点多而点名少，说明讨论主要来自研报与机构/大V；点名多而观点少，说明以快讯传播为主。两者都写进监控日志，比单一分数更容易解释。</p>
<h2>六、每日看参与面：<code>/coverage/stocks</code></h2>
<p><code>get_agency_coverage</code>（<code>/coverage/stocks</code>）给出每日滚动的 7 日与 30 日去重机构/大V 数、区间累计新增覆盖，以及 30 日高分占比；<code>days</code> 默认 30、范围 7～90，<code>min_score</code> 可选。它只给计数，不返回机构 ID 列表。</p>
<p>日报里记录三项即可：7 日去重主体数、当日新增覆盖、30 日高分占比。新增覆盖持续出现，说明有新的主体开始讨论这只股票；高分占比变化，说明参与者结构在变。</p>
<h2>七、宁德时代情绪监测页：单股舆情的页面形态</h2>
<p>熔岩为部分主题提供只读监测页，宁德时代情绪监测页就是单只股票的舆情视图，适合人工快速浏览当下的讨论状态。页面负责「看」，上面的接口流程负责「留」：需要复盘某次讨论集中时，用 <code>ts_code=300750.SZ</code>（仅作接口示例中的查询对象）按第三至第六节取数并保存 <code>meta.server_time</code>。</p>
<h2>八、股票舆情工具在哪看：三种搭法的取舍</h2>
<p>搜索「股票舆情工具在哪看」的人，通常在三种方案之间犹豫。按维护成本、可解释性与可编程性比较：</p>
<table>
<thead>
<tr>
<th>方案</th>
<th>维护成本</th>
<th>可解释性</th>
<th>可编程性</th>
<th>适合谁</th>
</tr>
</thead>
<tbody><tr>
<td>自建爬虫 + 情感模型</td>
<td>高：采集源、反爬、股票识别都要自己维护</td>
<td>取决于自己的实现</td>
<td>完全自主</td>
<td>有数据工程团队</td>
</tr>
<tr>
<td>商业舆情系统</td>
<td>低</td>
<td>多为综合分，拆解有限</td>
<td>依赖产品导出</td>
<td>关注单家公司口碑</td>
</tr>
<tr>
<td>结构化消息面数据接口</td>
<td>低：只维护调用与存储</td>
<td>每个数字有字段与口径</td>
<td>REST 与 MCP 均可</td>
<td>量化研究、AI Agent</td>
</tr>
</tbody></table>
<p>熔岩属于第三类：数据已经完成采集、去重、股票与概念识别和主体匿名化，调用方只需决定巡检频率、阈值与存储方式。它的边界也要说清：不含新闻标题与正文，不输出正负面标签，主体只以 <code>agency_id</code> 指代。</p>
<h2>九、监控日志的表结构</h2>
<p>把每次巡检与触发都落库，后续才能统计「哪些规则触发最多、触发后讨论持续多久」。一个最小的表结构示例：</p>
<pre><code class="language-sql">-- sentiment_watch_log：示例表结构
CREATE TABLE sentiment_watch_log (
  server_time   TIMESTAMPTZ NOT NULL,   -- 来自 meta.server_time（+08:00）
  ts_code       TEXT        NOT NULL,
  rule          TEXT,                    -- 'A' / 'B' / 'C' / NULL（未触发）
  m_1h          INTEGER,                 -- 1h 窗口 mentions
  m_24h         INTEGER,
  a_24h         INTEGER,                 -- 24h 窗口 agencies
  high_score_n  INTEGER,                 -- min_score=1050 下的高分主体数
  news_hits_24h INTEGER,                 -- /news/hits 条数
  request_id    TEXT,                    -- meta.request_id
  charged_yuan  NUMERIC(8,4),            -- meta.billing.charged_yuan
  PRIMARY KEY (server_time, ts_code)
);
</code></pre>
<p><code>request_id</code> 与 <code>charged_yuan</code> 用于对账；未触发的巡检也写一行，这样才能计算每条规则的触发频率。读摘要后的人工备注另建一张表，与原始计数分开存放，避免把主观判断混进统计字段。</p>
<h2>十、常见问答</h2>
<p><strong>Q：股票舆情分析能直接给出正面或负面吗？</strong><br />A：熔岩不输出正负面打分，提供的是讨论量、广度、主体质量、时间结构与摘要。需要情感标签时，可以在本地对 <code>summary</code> 做文本分类，并把模型版本写进日志。</p>
<p><strong>Q：股票舆情排名在哪看？</strong><br />A：全市场层面用 <code>get_market_hot_stocks</code>（<code>/hot/stocks</code>，约 1 分钟更新），列表附带 <code>heat_chg_1d</code>、<code>heat_chg_3d</code>、<code>heat_z20</code>；想看此前 7 天无人提及、刚被首次提到的股票，用首提流 <code>/hot/first-mentions</code>（<code>hours</code> 默认 24、最多 72）。</p>
<p><strong>Q：监控频率多高合适？</strong><br />A：逐小时巡检足够。<code>/heat/windows</code> 的逐小时序列本身就是小时粒度，更高频的调用只会重复读到同一小时的数据。</p>
<p><strong>Q：触发后要不要通知？</strong><br />A：通知内容建议只写事实：代码、触发规则、窗口计数、<code>server_time</code>。熔岩数据是只读统计，监控结论也应保持描述性。</p>
<h2>结论</h2>
<p>股票舆情监控可以拆成「巡检—下钻—日报」三层：<code>/heat/windows</code> 按小时检测讨论是否集中，<code>/opinions</code> 与 <code>/news/hits</code> 在触发后提供要点与媒体点名，<code>/coverage/stocks</code> 每日记录参与主体的变化。每一层都有明确字段和参数，阈值写成规则，日志保留 <code>server_time</code>，就能得到一条可复现、成本可控的个股舆情监测流程。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/catl-sentiment">https://lava.ai-dolphin.com/monitor/catl-sentiment</a></p>
]]></content:encoded></item><item><title><![CDATA[概念股有哪些股票怎么查：用熔岩概念成分反查与讨论热度做一份可复现的概念股查询]]></title><description><![CDATA[概念股有哪些股票怎么查？最快的办法是输入概念名，直接反查它的成分股名单；如果还想知道这些股票里最近谁被讨论得多，就要在名单之外再加一列消息面热度。熔岩（Lava）的概念股查询正是按这个思路设计的：公开查询页输入题材概念或行业名称，返回相关成分股、所属细分行业与近 7 日机构/消息面讨论热度；程序化场景则用…]]></description><link>https://dustinliang.hashnode.dev/concept-stocks-lookup-by-heat</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/concept-stocks-lookup-by-heat</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 01:12:00 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>概念股有哪些股票怎么查？最快的办法是输入概念名，直接反查它的成分股名单；如果还想知道这些股票里最近谁被讨论得多，就要在名单之外再加一列消息面热度。熔岩（Lava）的概念股查询正是按这个思路设计的：公开查询页输入题材概念或行业名称，返回相关成分股、所属细分行业与近 7 日机构/消息面讨论热度；程序化场景则用 MCP 工具 <code>get_concept_stock_heat</code>（HTTP 路径 <code>/concepts/stocks</code>）按概念取「提及排行」。</p>
<p>两者回答的问题略有不同：名单回答「这个概念包括谁」，提及排行回答「这个概念下最近谁被提到最多」。本文围绕「概念股查询」「概念股有哪些股票」「概念股一览」「概念股是指哪些股票」这几个常见问法，讲清熔岩的成分口径、热度口径、参数与反查方法。文中数字均为示例，排行只描述讨论分布，不是一份标的清单。</p>
<h2>一、概念股是指哪些股票：两种口径</h2>
<p>「概念股」通常指与某个题材、产业链环节或事件相关联的一组股票。问题在于「相关联」怎么定义，常见有两种口径：</p>
<table>
<thead>
<tr>
<th>口径</th>
<th>怎么得到</th>
<th>优点</th>
<th>局限</th>
</tr>
</thead>
<tbody><tr>
<td>成分口径</td>
<td>官方板块、产业链与主营业务归类</td>
<td>稳定、可解释</td>
<td>新题材出现时更新慢</td>
</tr>
<tr>
<td>共现口径</td>
<td>统计近期公开信息里与该题材一起被提到的股票</td>
<td>跟得上新叙事</td>
<td>受讨论噪声影响</td>
</tr>
</tbody></table>
<p>熔岩的概念成分库采用双源核实：一是公开产业链与官方板块成分归类（含主营与业务关联），二是近 30 日机构研报与公开财经信息在该题材下的共现统计。新兴主题会基于公开研究线索的共现动态纳入相关个股。查询页的目录覆盖官方概念指数与熔岩新兴共现题材，且只收录有 8 只以上有效 A 股成分股的概念。</p>
<p>理解这一点很重要：同一个概念在不同软件里成分数量不同，是口径不同，而不是谁错了。引用时写明「熔岩成分口径」即可。</p>
<h2>二、概念股查询的两条路径</h2>
<table>
<thead>
<tr>
<th>场景</th>
<th>入口</th>
<th>返回什么</th>
</tr>
</thead>
<tbody><tr>
<td>人工查一个概念</td>
<td>熔岩概念股查询页（公开、免注册）</td>
<td>成分股、细分行业、近 7 日讨论热度，按热度排序展示前 30 只</td>
</tr>
<tr>
<td>程序批量查、要落库</td>
<td><code>get_concept_stock_heat</code> / <code>/concepts/stocks</code></td>
<td>概念下个股的提及排行与机构/大V 数</td>
</tr>
</tbody></table>
<p>查询页适合快速浏览；要导出完整清单、全量历史热度或接入自动化研究，再走 REST 或 MCP。接口参数：</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td><code>concept</code></td>
<td>必填，<strong>精确匹配</strong>，不超过 30 字符</td>
</tr>
<tr>
<td><code>days</code></td>
<td>回看天数，默认 30，范围 1～365</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>默认 50，最大 100</td>
</tr>
<tr>
<td>更新节奏</td>
<td>约 10 分钟</td>
</tr>
<tr>
<td>计费</td>
<td>每次成功调用 0.005 元，非 2xx 不计费</td>
</tr>
</tbody></table>
<pre><code class="language-python"># concept_stocks.py —— 示例，地址与令牌为占位
import os, requests, pandas as pd
BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def concept_stocks(name, days=7, limit=100):
    r = requests.get(BASE + "/concepts/stocks",
                     params={"concept": name, "days": days, "limit": limit},
                     headers=H, timeout=30).json()
    assert r["ok"], r["code"]
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    df = pd.DataFrame(rows)
    df["concept"], df["days"] = name, days
    df["server_time"] = r["meta"]["server_time"]
    return df

df = concept_stocks("&lt;概念标准名&gt;", days=7)
print(len(df), df.head(10))
</code></pre>
<p><code>days=7</code> 与查询页的「近 7 日」口径对齐；不传则默认 30 天，两者结果不同是正常的。</p>
<h2>三、概念名为什么查不到：精确匹配的处理</h2>
<p><code>concept</code> 是精确匹配，这是「概念股查询返回空」最常见的原因。口头简称、带空格的写法、繁简混用都可能匹配不上。推荐三步拿到标准名：</p>
<ol>
<li>用 <code>get_market_hot_concepts</code>（<code>/hot/concepts</code>，<code>window</code> 默认 <code>24h</code>，<code>limit</code> 最大 200）在全市场概念热榜里找；</li>
<li>找不到时，用 <code>get_stock_concept_heat</code>（<code>/concepts/by-stock</code>）从一只确定相关的股票反查它关联的概念标准名；</li>
<li>把标准名原样传给 <code>/concepts/stocks</code>，不要多次改写近似词重试。</li>
</ol>
<pre><code class="language-python">def find_concept(keyword):
    r = requests.get(BASE + "/hot/concepts",
                     params={"window": "3d", "limit": 200},
                     headers=H, timeout=30).json()
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    # 概念名列名以 OpenAPI 为准，这里假设为 concept
    return [x["concept"] for x in rows if keyword in x.get("concept", "")]
</code></pre>
<p>这一步只是在已有的标准名里做本地包含匹配，不会额外计费，也避免了把错误名称反复发给接口。</p>
<h2>四、概念股一览怎么按热度排序</h2>
<p>查询页默认按近期讨论热度排序，接口返回也是按提及从高到低。读这份排行时建议同时看两列：</p>
<table>
<thead>
<tr>
<th>列</th>
<th>含义</th>
<th>读法</th>
</tr>
</thead>
<tbody><tr>
<td><code>mentions</code></td>
<td>回看期内与该概念一起被提到的次数</td>
<td>讨论量</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>参与讨论的不同机构/大V 数</td>
<td>讨论广度</td>
</tr>
</tbody></table>
<p>示例读数：</p>
<table>
<thead>
<tr>
<th>股票（示例）</th>
<th><code>mentions</code></th>
<th><code>agencies</code></th>
<th>描述</th>
</tr>
</thead>
<tbody><tr>
<td>股票甲</td>
<td>186</td>
<td>52</td>
<td>讨论量大且广</td>
</tr>
<tr>
<td>股票乙</td>
<td>141</td>
<td>9</td>
<td>讨论量大但集中在少数主体</td>
</tr>
<tr>
<td>股票丙</td>
<td>38</td>
<td>21</td>
<td>讨论量中等、参与面较广</td>
</tr>
</tbody></table>
<p>股票乙的情况值得单独标注：提及多主要来自少数主体，<code>mentions / agencies</code> 明显偏高。排行只描述讨论分布，不代表成分权重，更不等于「龙头」——熔岩不判定龙头，常见的「概念股龙头一览」需求在熔岩口径下只能落到「提及排行」这一可复现的统计上。</p>
<h2>五、反过来查：某只股票属于哪些概念</h2>
<p>「这只股票是什么概念股」是同一关系的反方向，用 <code>get_stock_concept_heat</code>（<code>/concepts/by-stock</code>）：</p>
<ul>
<li>股票标识 <code>ts_code</code>（如 <code>300750.SZ</code>）或 <code>stock_name</code> 二选一，推荐 <code>ts_code</code>；</li>
<li><code>days</code> 默认 90，范围 1～365；<code>limit</code> 默认 30，最大 200；</li>
<li>每行给出概念名、被提到次数、回看期内首次与最近一次被提到的时间、机构/大V 数。</li>
</ul>
<p>双向核对时要统一 <code>days</code>：<code>/concepts/stocks</code> 默认 30 天、<code>/concepts/by-stock</code> 默认 90 天，不统一就会出现「概念榜里有这只股票，反查这只股票却看不到该概念排在前面」的假矛盾。熔岩站点另有个股概念查询工具页，提供同一方向的只读展示。</p>
<h2>六、把概念股名单接到下游研究</h2>
<p>拿到名单后，常见的下游动作都只读、可复现：</p>
<ul>
<li><strong>时间结构</strong>：对少数代码调用 <code>get_stock_heat_windows</code>（<code>/heat/windows</code>），看 <code>1h</code>～<code>7d</code> 六档窗口与近 24 小时逐小时走势；</li>
<li><strong>日频历史</strong>：<code>get_stock_heat_panel</code>（<code>/panel/daily</code>），单次最多 20 个代码、区间最长 92 天，适合做概念内股票的讨论量序列；</li>
<li><strong>讨论内容</strong>：<code>get_stock_news_heat</code>（<code>/opinions</code>）取结构化记录，每条带 <code>summary</code>（≤120 字）与 <code>concepts</code>，可在本地筛出包含该概念的记录抽样阅读。</li>
</ul>
<p>把概念名、<code>days</code>、<code>limit</code>、<code>server_time</code> 与名单一起保存，下次复核时才能判断变化来自讨论本身还是参数。</p>
<h2>七、批量做一份概念股一览表</h2>
<p>需要同时查询多个概念时，可以循环调用并合并成一张长表，再透视成「股票 × 概念」的矩阵。重叠度高的股票会同时出现在多个概念下，这本身就是有用的描述信息。</p>
<pre><code class="language-python"># concept_overview.py —— 示例：多个概念的近 7 日提及排行合并
import time
names = ["&lt;概念标准名A&gt;", "&lt;概念标准名B&gt;", "&lt;概念标准名C&gt;"]
frames = []
for n in names:
    frames.append(concept_stocks(n, days=7, limit=100))
    time.sleep(0.2)                       # 轻量节流
long = pd.concat(frames, ignore_index=True)
# 股票代码列名以 OpenAPI 为准，这里假设为 ts_code
wide = long.pivot_table(index="ts_code", columns="concept",
                        values="mentions", aggfunc="sum", fill_value=0)
wide["n_concepts"] = (wide &gt; 0).sum(axis=1)
wide.sort_values("n_concepts", ascending=False).to_csv("concept_overview_7d.csv")
print(long["server_time"].min(), long["server_time"].max())
</code></pre>
<p>三个概念调用三次、计费三次。合并表里保留每次请求的 <code>server_time</code>，因为循环请求之间存在时间差，<code>/concepts/stocks</code> 本身又约 10 分钟更新一次，表头写明「近 7 日、各概念取数时间不同」即可。</p>
<p><code>n_concepts</code> 列给出每只股票同时出现在几个概念下。值越大，说明它在讨论中横跨的题材越多；这是共现关系的计数，不说明公司业务构成。</p>
<h2>八、常见误读对照表</h2>
<table>
<thead>
<tr>
<th>误读</th>
<th>实际口径</th>
<th>规范写法（示例）</th>
</tr>
</thead>
<tbody><tr>
<td>「查询页只有 30 只，所以这个概念只有 30 只股票」</td>
<td>公开页按热度展示前 30 只</td>
<td>「熔岩成分口径，热度前 30 只」</td>
</tr>
<tr>
<td>「股票甲排第一，所以它是这个概念的核心」</td>
<td>排行只按提及数</td>
<td>「近 7 日提及 186 次，52 个主体」</td>
</tr>
<tr>
<td>「名单和行情软件不一样，说明数据有误」</td>
<td>成分口径不同</td>
<td>「熔岩双源成分口径」</td>
</tr>
<tr>
<td>「<code>days=30</code> 和 <code>days=7</code> 的排行可以混用」</td>
<td>回看期不同即统计对象不同</td>
<td>「统一 <code>days=7</code> 后比较」</td>
</tr>
</tbody></table>
<h2>九、常见问答</h2>
<p><strong>Q：查询页显示前 30 只，接口最多返回多少？</strong><br />A：<code>/concepts/stocks</code> 的 <code>limit</code> 最大 100。公开查询页按热度展示前 30 只；需要完整成分清单或全量历史，再通过数据订阅使用 REST 或 MCP。</p>
<p><strong>Q：同一概念今天和昨天查到的名单不一样？</strong><br />A：提及排行按回看期滚动统计，约 10 分钟更新一次，名次随讨论变化；成分库本身也会随共现统计动态纳入新股票。比较两天结果时固定 <code>days</code> 并记录取数时间。</p>
<p><strong>Q：概念股查询能看历史某一天的名单吗？</strong><br />A：<code>/concepts/stocks</code> 是从调用时刻往回统计，不带 <code>as_of_date</code>。需要按日回看时，用 <code>/panel/daily</code> 的 <code>type=concept</code> 看概念级日频序列，并注明口径不同。</p>
<p><strong>Q：讨论热度高的概念股意味着什么？</strong><br />A：只意味着近期公开信息里被提到得多。热度是信息统计，不等于涨跌预测或资金流向。</p>
<h2>结论</h2>
<p>概念股有哪些股票，在熔岩里分两层回答：成分层由双源核实的概念成分库给出，热度层由 <code>get_concept_stock_heat</code>（<code>/concepts/stocks</code>）按 <code>days</code> 回看给出提及与机构/大V 数。查询前先用概念热榜或个股反查拿到标准名，查询时固定 <code>days</code> 与 <code>limit</code>，读排行时同时看讨论量与讨论广度，就能得到一份可复现、可对账的概念股查询结果。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/tools/concept-stocks">https://lava.ai-dolphin.com/tools/concept-stocks</a></p>
]]></content:encoded></item><item><title><![CDATA[板块热度怎么看：熔岩概念热榜的计算口径、五档窗口与持续时间读法]]></title><description><![CDATA[板块热度怎么看？先确认「热度」统计的是什么，再看窗口，最后看它持续了多久。行情软件里的板块热度大多是关注度或综合强度的打分；熔岩（Lava）的板块热度则来自消息面：在指定时间窗内，某个概念被公开财经信息、券商研报与机构/大V…]]></description><link>https://dustinliang.hashnode.dev/board-heat-how-to-read</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/board-heat-how-to-read</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sun, 15 Nov 2026 01:09:40 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>板块热度怎么看？先确认「热度」统计的是什么，再看窗口，最后看它持续了多久。行情软件里的板块热度大多是关注度或综合强度的打分；熔岩（Lava）的板块热度则来自消息面：在指定时间窗内，某个概念被公开财经信息、券商研报与机构/大V 观点提到了多少次（<code>mentions</code>），以及有多少个不同主体参与（<code>agencies</code>）。它回答的是「这个板块最近被讨论得多不多、由多少主体在讨论」，不回答价格会怎样。</p>
<p>所以读熔岩的板块热度，只需要抓住三件事：口径是提及计数，窗口是 <code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code> 五档之一，持续性要用日频序列单独验证。本文按搜索里最常见的几个问法——板块热度怎么看、板块热度是怎么计算的、板块热度排行榜在哪里看、板块热度一般持续多久——逐一给出熔岩口径下的答案、对应的 MCP 工具与 HTTP 路径。文中数字均为示例。</p>
<h2>一、板块热度怎么看：先分清三种「热度」</h2>
<p>市面上被叫作「板块热度」的指标至少有三类，混在一起看是最常见的误读来源。</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>典型来源</th>
<th>统计对象</th>
<th>熔岩是否提供</th>
</tr>
</thead>
<tbody><tr>
<td>行情热度</td>
<td>涨跌幅、成交额、涨停家数</td>
<td>价格与成交</td>
<td>否</td>
</tr>
<tr>
<td>关注度热度</td>
<td>搜索量、自选人数、页面访问</td>
<td>用户行为</td>
<td>否</td>
</tr>
<tr>
<td>消息面热度</td>
<td>新闻、研报、机构/大V 观点中的提及</td>
<td>公开信息中的讨论</td>
<td>是</td>
</tr>
</tbody></table>
<p>熔岩只做第三类。它把每条消息面记录结构化为时间、涉及股票、涉及概念、匿名主体编号 <code>agency_id</code> 与短摘要，再按概念聚合。因此同一天里，一个板块可能行情平淡但消息面热度很高，也可能反过来；两者是不同维度的观察，不能互相替代。</p>
<p>看熔岩板块热度时，第一步就是在笔记里写明「消息面口径」，避免和行情软件的热度值并列比较。</p>
<h2>二、板块热度是怎么计算的：熔岩的计数口径</h2>
<p>熔岩的概念板块热度由全市场概念热榜给出：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>MCP 工具</td>
<td><code>get_market_hot_concepts</code></td>
</tr>
<tr>
<td>HTTP 路径</td>
<td><code>/hot/concepts</code></td>
</tr>
<tr>
<td><code>window</code></td>
<td><code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code>，默认 <code>24h</code>，没有 <code>7d</code></td>
</tr>
<tr>
<td><code>min_score</code></td>
<td>可选，只统计机构/大V 分不低于该值的主体</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>默认 50，最大 200</td>
</tr>
<tr>
<td>排序</td>
<td>按窗口内提及从高到低，已去掉泛词</td>
</tr>
<tr>
<td>更新节奏</td>
<td>约 2 分钟</td>
</tr>
</tbody></table>
<p>计算本身并不复杂：在窗口内，凡是一条记录的 <code>concepts</code> 中包含某个概念，就为该概念累计一次提及；参与这些记录的不同 <code>agency_id</code> 个数就是 <code>agencies</code>。没有隐藏的综合打分，也没有把涨跌幅揉进去。</p>
<p>需要注意两点。第一，「已去掉泛词」意味着过于宽泛、几乎每条记录都会带上的标签不会出现在榜单中，这样排在前面的才是有区分度的板块。第二，加上 <code>min_score</code> 后统计对象变了：机构/大V 分大约在 450～1500 之间，1000 左右为中等，1050 约为前 10%。同一份报告里比较多个板块时，<code>window</code> 与 <code>min_score</code> 必须一致。</p>
<pre><code class="language-python"># 示例：取 24h 概念热榜，地址与令牌为占位
import os, requests
BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
r = requests.get(BASE + "/hot/concepts",
                 params={"window": "24h", "limit": 200},
                 headers=H, timeout=30).json()
assert r["ok"], r["code"]
rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
print(r["meta"]["server_time"], len(rows))
</code></pre>
<p>每次成功调用计费 0.005 元，非 2xx 不计费。返回里的 <code>meta.server_time</code> 带 <code>+08:00</code>，务必与结果一起保存。</p>
<h2>三、板块热度排行榜在哪里看：页面与接口的分工</h2>
<p>想直接看板块热度排行，有两条路：</p>
<ol>
<li><strong>只读监测页</strong>：熔岩的 AI 板块监测页把 AI 相关概念与板块讨论聚合在一个视图里，适合人工浏览「现在这个主题看起来怎样」。</li>
<li><strong>接口取数</strong>：需要复盘、比较或落库时，用 <code>/hot/concepts</code> 取同一口径的排行，并记录参数。</li>
</ol>
<p>页面适合「看」，接口适合「留」。只看页面时很难在第二天说清当时用的是哪个窗口；用接口时把 <code>window</code>、<code>min_score</code>、<code>server_time</code> 写进文件名或表头即可复现。</p>
<p>排行里排第几并不是最有信息量的部分。更值得记录的是相邻名次之间的差距：第 1 名与第 2 名提及相差 5% 和相差 50%，含义完全不同。建议同时保存每个概念的 <code>mentions</code> 与它在榜单合计中的份额。</p>
<h2>四、五档窗口各回答什么问题</h2>
<table>
<thead>
<tr>
<th>窗口</th>
<th>回答的问题</th>
<th>适合的用途</th>
</tr>
</thead>
<tbody><tr>
<td><code>1h</code></td>
<td>刚刚有没有集中讨论</td>
<td>盘中巡检，噪声大</td>
</tr>
<tr>
<td><code>4h</code></td>
<td>半个交易日内的讨论结构</td>
<td>午间或收盘前复核</td>
</tr>
<tr>
<td><code>18h</code></td>
<td>隔夜到早盘的累积</td>
<td>早间整理</td>
</tr>
<tr>
<td><code>24h</code></td>
<td>一个自然日的讨论量</td>
<td>日报默认口径</td>
</tr>
<tr>
<td><code>3d</code></td>
<td>近几日是否持续被讨论</td>
<td>周中回顾</td>
</tr>
</tbody></table>
<p>同一个板块在 <code>1h</code> 排前列、在 <code>3d</code> 排不进前 50，通常说明讨论是短时集中；反过来在 <code>3d</code> 靠前、<code>1h</code> 不明显，说明是持续而平稳的讨论。把五档排名并列成一行，就是一个简单的「窗口形态」描述：</p>
<pre><code class="language-text">概念（示例）   1h  4h  18h  24h  3d
概念甲          2   3    5    6   11   -&gt; 短时集中
概念乙         18  12    4    3    2   -&gt; 持续累积
</code></pre>
<p>这只是对讨论时间结构的描述，不对后续变化做判断。</p>
<h2>五、板块热度一般持续多久：用日频序列验证</h2>
<p>「板块热度一般持续多久」没有统一答案，但可以对任一板块用数据测出来。实时榜单不带 <code>as_of_date</code>，无法回放历史，所以持续性要用日频面板：</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>MCP 工具</td>
<td><code>get_stock_heat_panel</code></td>
</tr>
<tr>
<td>HTTP 路径</td>
<td><code>/panel/daily</code></td>
</tr>
<tr>
<td>概念层面</td>
<td><code>type=concept</code>，<code>names</code> 传概念标准名</td>
</tr>
<tr>
<td>字段</td>
<td>按自然日的 <code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code></td>
</tr>
<tr>
<td>限制</td>
<td>区间最长 92 天，单次最多 20 个标的</td>
</tr>
</tbody></table>
<p>一个可复现的做法：取某概念近 92 天的自然日 <code>mentions</code>，以前 20 日均值加两倍标准差为「偏高」阈值，统计连续偏高的天数段。这样得到的是该板块历史上「热度偏高状态」的持续天数分布，例如「近 92 天出现 3 段，分别持续 2、4、1 天」（示例）。阈值是研究者自定的描述标准，写清即可。</p>
<p><code>weighted_heat</code> 按「分数 ÷ 1000」对达标主体的提及加权，可以与 <code>mentions</code> 并列，看高分主体是否同步参与；面板里的该字段不是 PIT 口径，用于回测时要注明。</p>
<h2>六、从板块下钻到个股</h2>
<p>看完板块热度，下一步通常是「这个板块里谁被提得多」。对应工具：</p>
<ul>
<li><code>get_concept_stock_heat</code>（<code>/concepts/stocks</code>）：<code>concept</code> 精确匹配且不超过 30 字符，<code>days</code> 默认 30、范围 1～365，<code>limit</code> 默认 50、最大 100，约 10 分钟更新；</li>
<li><code>get_stock_heat_windows</code>（<code>/heat/windows</code>）：对少数代码看 <code>1h</code>～<code>7d</code> 六档窗口与近 24 小时逐小时走势。</li>
</ul>
<p>概念名请直接用热榜返回的标准名，不要手写别名；<code>/hot/concepts</code> 约 2 分钟更新、<code>/concepts/stocks</code> 约 10 分钟更新，两者时间戳不同步，对账时记下各自的取数时间。</p>
<h2>七、板块热度指标公式：可以自己复算的四个派生量</h2>
<p>搜索「板块热度指标公式」的人，多半想要一个能自己算的数。熔岩给的是原始计数，派生量可以在本地按固定公式复算，公式写进表头，结果就能被别人复核：</p>
<pre><code class="language-text">份额      share_i     = mentions_i / Σ mentions（同一 window、同一 min_score 的榜单合计）
每主体提及 per_agency_i = mentions_i / agencies_i（agencies 为 0 时记空值）
相对强度   z20_i       = (今日 mentions - 前 20 日均值) / 前 20 日标准差（用 /panel/daily 的 type=concept 复算）
窗口比     ratio_i     = mentions_1h × 24 / mentions_24h（&gt;1 表示最近一小时的讨论密度高于全天平均）
</code></pre>
<p>四个量各有侧重：份额消除了全市场讨论总量的日间波动；每主体提及区分「很多主体各说一次」和「少数主体反复讨论」；相对强度把板块和它自己的历史比；窗口比描述讨论是否正在集中。它们都只是对计数的变换，不引入任何外部价格信息。</p>
<pre><code class="language-python"># derive.py —— 示例：在同一次 /hot/concepts 返回上计算份额与每主体提及
import pandas as pd
df = pd.DataFrame(rows)              # rows 来自第二节的请求
# 列名以 OpenAPI 为准，这里假设为 concept / mentions / agencies
df["share"] = df["mentions"] / df["mentions"].sum()
df["per_agency"] = df["mentions"] / df["agencies"].where(df["agencies"] &gt; 0)
df["small_sample"] = df["agencies"] &lt; 5
df["window"], df["min_score"] = "24h", None
df["server_time"] = r["meta"]["server_time"]
df.sort_values("mentions", ascending=False).to_csv("concept_heat_24h.csv", index=False)
</code></pre>
<p>复算时有三个细节容易出错：份额的分母必须来自同一次请求的榜单，不要跨请求拼接；<code>agencies</code> 很小（例如少于 5）时每主体提及波动极大，应附加「样本小」标记；相对强度只在日频序列上计算，不要拿实时榜单的窗口值去套 20 日均值。</p>
<h2>八、板块热度榜第一名怎么读</h2>
<p>「板块热度榜第一名」是另一个高频问法。在熔岩口径下，第一名只表示该窗口内提及数最高，读的时候建议补三列信息：</p>
<table>
<thead>
<tr>
<th>补充项</th>
<th>来源</th>
<th>作用</th>
</tr>
</thead>
<tbody><tr>
<td>与第二名的差距</td>
<td>同一榜单的 <code>mentions</code></td>
<td>判断是明显领先还是并列</td>
</tr>
<tr>
<td>参与主体数</td>
<td><code>agencies</code></td>
<td>判断讨论是否广泛</td>
</tr>
<tr>
<td>五档窗口排名</td>
<td>分别请求 <code>1h</code>～<code>3d</code></td>
<td>判断是短时集中还是持续累积</td>
</tr>
</tbody></table>
<p>例如（示例）：某概念 <code>24h</code> 排第一，<code>mentions</code> 比第二名高 8%，<code>agencies</code> 也最多，<code>3d</code> 排第二——这是「近几日持续被广泛讨论、当天略微领先」。另一个概念 <code>1h</code> 排第一但 <code>24h</code> 排第 15，<code>agencies</code> 只有个位数——这是「最近一小时少数主体集中讨论」。两者都叫第一名，描述完全不同。</p>
<h2>九、常见问答</h2>
<p><strong>Q：熔岩的板块热度和行情软件的热度值能直接比较吗？</strong><br />A：不能。熔岩是消息面提及计数，行情软件多为关注度或综合打分，统计对象不同，只能并列观察，不能相互换算。</p>
<p><strong>Q：板块热度值在哪里可以看到原始依据？</strong><br />A：热度本身是计数；想看讨论的具体内容，用 <code>get_stock_news_heat</code>（<code>/opinions</code>）取相关个股的结构化记录，每条带 <code>topic_time</code>、<code>concepts</code>、<code>summary</code>（≤120 字）和匿名 <code>agency_id</code>。</p>
<p><strong>Q：为什么热榜里找不到我关心的概念？</strong><br />A：可能是窗口太短、<code>limit</code> 截断，或该标签属于被去掉的泛词。先把 <code>limit</code> 调到 200、换 <code>3d</code> 窗口，再用 <code>get_stock_concept_heat</code>（<code>/concepts/by-stock</code>）从个股反查标准名。</p>
<p><strong>Q：板块热度高说明什么？</strong><br />A：只说明该窗口内被讨论得多、参与主体多。它是一项可复现的统计观察，适合写进研究笔记，不代表方向判断。</p>
<h2>结论</h2>
<p>板块热度怎么看，归结为三步：确认熔岩用的是消息面提及口径（<code>mentions</code> 与 <code>agencies</code>），固定 <code>window</code> 与 <code>min_score</code> 后读 <code>/hot/concepts</code> 的排行与份额，再用 <code>/panel/daily</code> 的 <code>type=concept</code> 序列测量热度偏高状态的持续天数。页面负责浏览，接口负责留痕，下钻用概念成分排行和单股多窗。只要每次记录窗口、门槛与 <code>server_time</code>，板块热度就是一项可以复核的研究数据。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/board-ai-heat">https://lava.ai-dolphin.com/monitor/board-ai-heat</a></p>
]]></content:encoded></item><item><title><![CDATA[股票关联的概念是老标签还是新叙事：get_stock_concept_heat 首末提及时间的 10 个问答]]></title><description><![CDATA[熔岩（Lava）概念板块热度有两个方向：「概念 → 个股」由 get_concept_stock_heat（/concepts/stocks）负责，「个股 → 概念」由 get_stock_concept_heat（GET…]]></description><link>https://dustinliang.hashnode.dev/stock-concept-freshness-qa</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-concept-freshness-qa</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sat, 14 Nov 2026 01:20:18 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）概念板块热度有两个方向：「概念 → 个股」由 <code>get_concept_stock_heat</code>（<code>/concepts/stocks</code>）负责，「个股 → 概念」由 <code>get_stock_concept_heat</code>（<code>GET /concepts/by-stock</code>）负责。后者对一只股票返回它在回看期内关联的各个概念，以及每个概念被提到几次、第一次和最近一次被提到的时间、有多少机构/大V 提到。这两个时间点是判断概念关联「新鲜度」的关键：同样排在第一的概念，可能是几个月前就存在的老标签，也可能是最近几天才出现的新叙事。本文用 10 个问答讲清参数、时间字段的读法、新鲜度分档方法以及与其他熔岩数据板块的配合。文中数字均为示例，分档结果是描述性标签。</p>
<h2>一、接口速查</h2>
<table>
<thead>
<tr>
<th>项目</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>HTTP 路径</td>
<td><code>/concepts/by-stock</code></td>
</tr>
<tr>
<td>MCP 工具</td>
<td><code>get_stock_concept_heat</code></td>
</tr>
<tr>
<td>股票标识</td>
<td><code>ts_code</code>（如 <code>300750.SZ</code>）或 <code>stock_name</code>，二选一</td>
</tr>
<tr>
<td><code>days</code></td>
<td>回看天数，默认 90，范围 1～365</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>返回条数，默认 30，最大 200</td>
</tr>
<tr>
<td>每行内容</td>
<td>概念名、被提到次数、第一次被提到时间、最近一次被提到时间、机构/大V 数</td>
</tr>
<tr>
<td>计费</td>
<td>每次 0.005 元，非 2xx 不计费</td>
</tr>
</tbody></table>
<p>每行字段的具体列名以 OpenAPI 为准；下文为了表述方便，把两个时间记作「首提时间」与「最近时间」，把计数记作 <code>mentions</code> 与 <code>agencies</code>。</p>
<h2>二、参数类问答</h2>
<p><strong>Q：<code>days</code> 默认 90，和概念成分排行的默认 30 为什么不同？</strong><br />A：两个接口回答的问题不同。<code>/concepts/stocks</code> 关注某概念「最近谁被提得多」，默认 30 天；<code>/concepts/by-stock</code> 关注一只股票「挂了哪些概念」，需要更长的回看才能看到完整的概念画像，默认 90 天。两者 <code>days</code> 都可以显式指定，引用结果时写上取值即可。</p>
<p><strong>Q：<code>limit</code> 默认 30 够用吗？</strong><br />A：对大多数股票够用；被广泛讨论的股票可能关联上百个概念。如果目的是做新鲜度分档，建议 <code>limit=200</code>，否则排在 30 名以后的新概念可能被截掉——新概念初期提及数少，恰恰最容易排在后面。</p>
<p><strong>Q：用 <code>stock_name</code> 还是 <code>ts_code</code>？</strong><br />A：优先 <code>ts_code</code>。简称可能变更或重名，代码可以直接与自有行情、行业数据关联。</p>
<h2>三、时间字段类问答</h2>
<p><strong>Q：「首提时间」是这个概念第一次和该股关联的时间吗？</strong><br />A：应理解为<strong>回看期内</strong>第一次被提到的时间。若 <code>days=90</code>，一个一年前就存在的老概念，首提时间也会落在 90 天窗口的起点附近。要区分「真正的新概念」和「回看期起点的老概念」，用较大的 <code>days</code>（例如 365）再取一次：两次首提时间都很近的，才是近期新出现的关联。</p>
<p><strong>Q：「最近时间」能说明什么？</strong><br />A：说明这个关联最近是否仍在被讨论。首提时间早、最近时间也近，是持续活跃的老标签；首提时间早、最近时间很久以前，是已经沉寂的标签。最近时间与调用时刻的差，可以用响应里的 <code>meta.server_time</code>（带 <code>+08:00</code>）计算。</p>
<p><strong>Q：两个时间的时区是什么？</strong><br />A：熔岩消息面记录的时间为北京时间。入库时统一标注为 <code>Asia/Shanghai</code>，计算时间差前先确认两个时间戳都已带时区，避免与 UTC 时间混算。</p>
<p><strong>Q：首提时间与 <code>/hot/first-mentions</code> 的「首次被提到」是同一回事吗？</strong><br />A：不是。首提流 <code>/hot/first-mentions</code>（<code>get_first_mention_stocks</code>）的口径是<strong>股票</strong>在此前 7 天一次都没被提过、在最近 <code>hours</code> 小时内（默认 24，最多 72）首次被提到，返回 <code>first_time</code>、首提主体的 <code>agency_id</code> 与分数；而 <code>/concepts/by-stock</code> 的首提时间是<strong>股票与某个概念的关联</strong>在回看期内第一次出现。前者描述股票本身是否「新冒头」，后者描述概念标签是否「新贴上」。日频面板 <code>/panel/daily</code> 的 <code>is_first_mention_day</code> 又是按自然日标记的股票级口径，三者不要混用。</p>
<h2>四、新鲜度分档示例</h2>
<p>把首提时间与最近时间换算成「距调用时刻的天数」，可以给每个概念打一个描述性标签。以下为示例阈值与示例读数：</p>
<table>
<thead>
<tr>
<th>概念（示例）</th>
<th><code>mentions</code></th>
<th><code>agencies</code></th>
<th>首提距今（天）</th>
<th>最近距今（天）</th>
<th>标签</th>
</tr>
</thead>
<tbody><tr>
<td>概念甲</td>
<td>412</td>
<td>96</td>
<td>88</td>
<td>0</td>
<td>持续活跃</td>
</tr>
<tr>
<td>概念乙</td>
<td>37</td>
<td>14</td>
<td>4</td>
<td>0</td>
<td>新近出现</td>
</tr>
<tr>
<td>概念丙</td>
<td>65</td>
<td>22</td>
<td>85</td>
<td>41</td>
<td>已沉寂</td>
</tr>
<tr>
<td>概念丁</td>
<td>9</td>
<td>3</td>
<td>2</td>
<td>1</td>
<td>新近出现，样本小</td>
</tr>
</tbody></table>
<p>分档规则（示例阈值，可按研究需要调整）：</p>
<pre><code class="language-text">新近出现：首提距今 &lt;= 7 天（且用 days=365 复核后仍 &lt;= 7 天）
持续活跃：首提距今 &gt; 30 天，最近距今 &lt;= 3 天
已沉寂：  最近距今 &gt; 30 天
其他：    记为「中间状态」，不强行归类
样本小：  agencies &lt; 5 时附加此标记
</code></pre>
<pre><code class="language-python"># freshness.py —— 示例，地址与令牌为占位
import os, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def by_stock(code, days):
    r = requests.get(BASE + "/concepts/by-stock",
                     params={"ts_code": code, "days": days, "limit": 200},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    return pd.DataFrame(rows), pd.Timestamp(r["meta"]["server_time"])

d90, now = by_stock("&lt;CODE&gt;", 90)
d365, _ = by_stock("&lt;CODE&gt;", 365)
# 概念名与两个时间的列名以 OpenAPI 为准，这里假设为 concept / first_time / last_time
m = d90.merge(d365[["concept", "first_time"]], on="concept", suffixes=("", "_365"))
for c in ("first_time", "last_time", "first_time_365"):
    m[c + "_age"] = (now - pd.to_datetime(m[c]).dt.tz_localize("Asia/Shanghai")).dt.days
</code></pre>
<p>两次调用计费两次。若时间字符串已带时区，把 <code>tz_localize</code> 换成 <code>tz_convert</code>。</p>
<h2>五、与其他板块配合的问答</h2>
<p><strong>Q：一个「新近出现」的概念，下一步查什么？</strong><br />A：先用 <code>get_market_hot_concepts</code>（<code>/hot/concepts</code>）看该概念在全市场是否也在热榜上；再用 <code>get_concept_stock_heat</code>（<code>/concepts/stocks</code>，<code>concept</code> 精确匹配、≤30 字符）看该概念下还有哪些股票被提到。如果只有这一只股票在讨论该概念，标签更可能是个股层面的叙事。</p>
<p><strong>Q：能用这个接口做历史回测吗？</strong><br />A：它返回的是从调用时刻往回的统计，不带 <code>as_of_date</code> 参数，无法还原历史某一天的概念画像。需要日频历史时，用 <code>/panel/daily</code> 的 <code>type=concept</code>（<code>names</code> 传概念名）取概念层面的自然日序列，并注明口径不同。</p>
<p><strong>Q：概念名可以直接传给其他接口吗？</strong><br />A：可以，而且推荐这样做。<code>/concepts/by-stock</code> 返回的概念名就是标准名，直接作为 <code>/concepts/stocks</code> 的 <code>concept</code> 或 <code>/panel/daily</code> 的 <code>names</code> 传入，可以避免手写别名导致的精确匹配失败。</p>
<p>熔岩站点的个股概念查询工具页提供了同一方向（个股 → 概念）的只读展示，适合人工查看一只股票的概念画像；需要做新鲜度分档、批量或落库时，再用上面的接口复现。</p>
<h2>六、常见误读对照表</h2>
<table>
<thead>
<tr>
<th>误读</th>
<th>实际口径</th>
<th>规范写法（示例）</th>
</tr>
</thead>
<tbody><tr>
<td>「概念乙首提距今 4 天，所以是这只股票的新概念」</td>
<td>首提时间只在回看期内成立</td>
<td>「<code>days=90</code> 与 <code>days=365</code> 两次取数，首提均在 4 天内」</td>
</tr>
<tr>
<td>「概念甲提及最多，所以它是这只股票的主题」</td>
<td>提及多可能来自长期累积</td>
<td>「90 天回看内概念甲 <code>mentions=412</code>，最近距今 0 天」</td>
</tr>
<tr>
<td>「概念丙最近距今 41 天，说明关联已经失效」</td>
<td>只说明回看期内最近一次被提到在 41 天前</td>
<td>「概念丙近 41 天无新提及」</td>
</tr>
<tr>
<td>「两只股票都挂了概念甲，所以它们会同步变化」</td>
<td>概念关联只描述讨论中的共现</td>
<td>「两只股票在回看期内都与概念甲共现」</td>
</tr>
<tr>
<td>「<code>limit=30</code> 没返回的概念就不存在」</td>
<td>只是排在前 30 名之外</td>
<td>「<code>limit=200</code> 下复核」</td>
</tr>
</tbody></table>
<p>误读的共同来源是省略了 <code>days</code>、<code>limit</code> 和时间基准。只要写清这三项，读者就能判断标签的适用范围。</p>
<h2>七、落库字段与复核节奏</h2>
<p>为了让新鲜度标签可以追溯，建议每次保存：</p>
<ul>
<li>请求参数：<code>ts_code</code>、<code>days</code>、<code>limit</code>；</li>
<li>响应元数据：<code>meta.server_time</code>、<code>meta.request_id</code>，以及计费字段 <code>meta.billing.charged_yuan</code>；</li>
<li>每行：概念名、<code>mentions</code>、<code>agencies</code>、首提时间、最近时间；</li>
<li>本地计算列：首提距今天数、最近距今天数、<code>days=365</code> 复核结果、标签与是否样本小。</li>
</ul>
<p>复核节奏可以按研究用途决定：对固定观察列表做周度快照即可看到标签迁移，例如某概念从「新近出现」变为「持续活跃」或「已沉寂」；盘中不需要高频调用，因为概念画像是长回看统计，短时间内变化很小。把相邻两次快照按概念名对齐，统计新增、消失和标签变化的数量，就是一张简单的概念画像变更表。</p>
<p>需要注意，概念画像变更表记录的是讨论中的共现关系变化，不代表公司业务或分类的变化；涉及行业归属的问题，仍以自有的行业分类数据为准。</p>
<h2>八、用消息面记录抽样核对</h2>
<p>分档只基于计数与时间。想看「新近出现」的概念具体是在什么语境下与股票共现，可以用个股消息面 <code>get_stock_news_heat</code>（<code>/opinions</code>）取同一只股票最近几天的记录。每条记录带 <code>topic_time</code>、<code>agency_id</code>、<code>agency_score</code>、<code>stocks</code>、<code>concepts</code>、<code>summary</code>（≤120 字）和 <code>detail</code>（≤600 字），在本地筛出 <code>concepts</code> 中包含该概念名的记录，按 <code>topic_time</code> 排序抽样阅读 <code>summary</code> 即可。</p>
<pre><code class="language-python">r = requests.get(BASE + "/opinions",
                 params={"ts_code": "&lt;CODE&gt;", "days": 7, "limit": 200},
                 headers=H, timeout=60).json()
assert r["ok"], r["code"]
data = r["data"]
recs = data.get("list", []) if isinstance(data, dict) else data
hit = [x for x in recs if "&lt;概念标准名&gt;" in (x.get("concepts") or [])]
print(len(hit), [x["topic_time"] for x in hit[:5]])
</code></pre>
<p>抽样核对有两个用途：确认新概念的共现不是少数主体的重复表达（可按匿名 <code>agency_id</code> 计数），以及确认首提时间附近确实存在相关记录。<code>/opinions</code> 的 <code>days</code> 最大 90，记录受 <code>limit</code> 截断，条数等于 200 时应缩短区间。核对结果仍然只记录事实，不把摘要内容改写为结论。</p>
<h2>结论</h2>
<p><code>get_stock_concept_heat</code> 的价值不只在于「哪个概念提及最多」，更在于每个概念的首提时间与最近时间。用较长的 <code>days</code> 复核首提时间、用 <code>meta.server_time</code> 计算距今天数、给小样本附加标记，就能把一只股票的概念关联区分为持续活跃、新近出现和已沉寂三类，并与概念热榜、概念成分排行和日频面板衔接成完整的只读研究流程。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/tools/stock-concepts">https://lava.ai-dolphin.com/tools/stock-concepts</a></p>
]]></content:encoded></item><item><title><![CDATA[多只股票的熔岩热度怎么横向比：绝对提及、窗口份额与 heat_z20 三种归一化对照表]]></title><description><![CDATA[把几只股票的消息面热度放在一起比较，是熔岩（Lava）最常见的用法之一：同一行业的几家公司、同一概念下的几只成分股、或者自选列表里的若干标的。问题在于，大市值、长期被讨论的股票 mentions…]]></description><link>https://dustinliang.hashnode.dev/multi-stock-heat-normalization-compare</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/multi-stock-heat-normalization-compare</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sat, 14 Nov 2026 01:19:29 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>把几只股票的消息面热度放在一起比较，是熔岩（Lava）最常见的用法之一：同一行业的几家公司、同一概念下的几只成分股、或者自选列表里的若干标的。问题在于，大市值、长期被讨论的股票 <code>mentions</code> 天然更高，直接比较绝对计数，几乎总是同一批股票排在前面。本文把横向比较拆成三种归一化口径——绝对提及、窗口份额、相对自身的 <code>heat_z20</code>——用对照表说明各自回答什么问题、从哪个接口取数、什么时候会误导，并给出一段同窗三口径并列的 Python 示例。文中所有数字均为示例，三种口径都是描述性统计量。</p>
<h2>一、为什么不能只比 mentions</h2>
<p><code>mentions</code> 是窗口内被提到的次数。它适合回答「现在谁被讨论得最多」，但不适合回答「谁的讨论相对平常更不寻常」。示例：股票 A 平时日均提及 120 次，今天 150 次；股票 B 平时日均 5 次，今天 30 次（均为示例数字）。按绝对提及 A 远高于 B；按相对自身变化，B 的偏离更大。两种说法都成立，只是回答的问题不同。横向比较之前，先确定要回答哪个问题，再选口径。</p>
<p>还有一个常被忽略的变量是主体门槛。<code>/hot/stocks</code> 与 <code>/heat/windows</code> 都接受 <code>min_score</code>：前者加上门槛后只统计机构/大V 分 ≥ 该值的提及，后者用它定义「高分机构/大V 数」这一列的分数线（默认 1050，约前 10%）。比较组里的每只股票必须使用同一门槛，否则份额与名次混入了两种统计对象。分数范围约 450～1500，1000 左右为中等，门槛取值写进结果表头即可。</p>
<h2>二、三种口径的定义</h2>
<table>
<thead>
<tr>
<th>口径</th>
<th>计算方式</th>
<th>回答的问题</th>
<th>来源字段</th>
</tr>
</thead>
<tbody><tr>
<td>绝对提及</td>
<td>直接用 <code>mentions</code>，可辅以 <code>agencies</code></td>
<td>谁被讨论得最多</td>
<td>接口返回</td>
</tr>
<tr>
<td>窗口份额</td>
<td>某股 <code>mentions</code> ÷ 比较组 <code>mentions</code> 合计</td>
<td>在这组股票里，讨论量的分配比例</td>
<td>本地计算 <code>share</code></td>
</tr>
<tr>
<td>相对自身</td>
<td><code>heat_z20</code>：相对近 20 日日均热度的 z 分</td>
<td>谁相对自己的常态偏离最大</td>
<td>接口返回，公式以 OpenAPI 为准</td>
</tr>
</tbody></table>
<p>另有一个常用的辅助口径：<code>agencies</code> 的组内份额，用来区分「讨论量份额高」是来自更多主体还是更集中的少数主体。</p>
<h2>三、示例对照</h2>
<p>同一 24h 窗口，三只股票的示例读数（均为示例数字）：</p>
<table>
<thead>
<tr>
<th>股票</th>
<th><code>mentions</code></th>
<th><code>agencies</code></th>
<th>组内 <code>share</code></th>
<th><code>heat_z20</code></th>
<th>绝对提及名次</th>
<th><code>heat_z20</code> 名次</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>150</td>
<td>88</td>
<td>0.71</td>
<td>0.9</td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td>B</td>
<td>30</td>
<td>17</td>
<td>0.14</td>
<td>4.2</td>
<td>2</td>
<td>1</td>
</tr>
<tr>
<td>C</td>
<td>31</td>
<td>12</td>
<td>0.15</td>
<td>1.6</td>
<td>2</td>
<td>2</td>
</tr>
</tbody></table>
<p>规范描述是：「A 在组内讨论量份额约 71%；B 相对自身近 20 日日均偏离最大」。不应写成「B 比 A 更热」——在绝对口径下并非如此。B 与 C 的绝对提及几乎相同，但 B 的 <code>heat_z20</code> 明显更高，说明 B 平时更安静。</p>
<h2>四、取数路径对照</h2>
<p>三种口径都可以从多个接口取到，差别在时效、批量与是否可回看：</p>
<table>
<thead>
<tr>
<th>取数路径</th>
<th>MCP 工具</th>
<th>一次能查几只</th>
<th>时效</th>
<th>能否回看历史</th>
<th>适合的口径</th>
</tr>
</thead>
<tbody><tr>
<td>单股多窗口 <code>/heat/windows</code></td>
<td><code>get_stock_heat_windows</code></td>
<td>1 只，六档窗口</td>
<td>实时</td>
<td>否，逐小时只到近 24 小时</td>
<td>三种都可，含 <code>heat_z20</code></td>
</tr>
<tr>
<td>全市场热榜 <code>/hot/stocks</code></td>
<td><code>get_market_hot_stocks</code></td>
<td>一张榜，<code>limit</code> 最大 200</td>
<td>约 1 分钟更新</td>
<td>否</td>
<td>绝对提及、份额；附 <code>heat_z20</code></td>
</tr>
<tr>
<td>日频面板 <code>/panel/daily</code></td>
<td><code>get_stock_heat_panel</code></td>
<td><code>codes</code> 最多 20 个</td>
<td>日频</td>
<td>是，区间最长 92 天</td>
<td>绝对提及、份额；z 分需本地计算</td>
</tr>
</tbody></table>
<p>几点取舍：</p>
<ul>
<li>比较组在 20 只以内且需要历史序列时，用日频面板一次批量取数最省调用；</li>
<li>只需要当前截面且比较组都在热榜前 200 内，用一次 <code>/hot/stocks</code> 即可，但不在榜上的股票会缺失，缺失不能当作 0；</li>
<li>需要六档窗口或逐小时细节时，用 <code>/heat/windows</code> 逐只调用，调用次数等于股票数，每次 0.005 元，非 2xx 不计费。</li>
</ul>
<h2>五、同窗三口径并列的示例脚本</h2>
<pre><code class="language-python"># compare_three.py —— 示例，地址与令牌为占位
import os, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
CODES = ["&lt;CODE_A&gt;", "&lt;CODE_B&gt;", "&lt;CODE_C&gt;"]

def windows(code):
    r = requests.get(BASE + "/heat/windows", params={"ts_code": code},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    return r["data"], r["meta"]

rows = []
for c in CODES:
    d, m = windows(c)
    # 各窗口的结构与列名以 OpenAPI 为准，这里假设 24h 窗口可按键读取
    w24 = d.get("windows", {}).get("24h", {}) if isinstance(d, dict) else {}
    rows.append({"code": c, "mentions": w24.get("mentions"),
                 "agencies": w24.get("agencies"),
                 "heat_z20": d.get("heat_z20"),
                 "server_time": m["server_time"], "request_id": m["request_id"]})

df = pd.DataFrame(rows)
df["share"] = df["mentions"] / df["mentions"].sum()
df["agency_share"] = df["agencies"] / df["agencies"].sum()
for col in ("mentions", "share", "heat_z20"):
    df[col + "_rank"] = df[col].rank(ascending=False, method="min")
print(df)
</code></pre>
<p>脚本输出三列名次。名次不一致是常态，不是数据问题。报告时把三列并列展示，并在表头写清窗口与比较组成员。</p>
<h2>六、比较组怎么定</h2>
<p>归一化结果高度依赖「和谁比」：</p>
<ol>
<li><strong>份额只在组内有意义。</strong> 换一个比较组，同一只股票的 <code>share</code> 就会变。比较组成员与数量要随结果一起保存。</li>
<li><strong>组内规模差异过大时先分层。</strong> 大市值与小市值股票放在一组，份额几乎总是被前者占据。可以先按自有的市值或行业分层，再在层内计算份额。</li>
<li><strong>概念成员可以从熔岩取。</strong> 想比较「某概念下的几只股票」，用 <code>get_concept_stock_heat</code>（<code>/concepts/stocks</code>，<code>concept</code> 精确匹配，<code>days</code> 默认 30）取前若干只成分股作为比较组；反过来，<code>get_stock_concept_heat</code>（<code>/concepts/by-stock</code>）可以确认这些股票是否共享同一概念。</li>
<li><strong><code>market</code> 口径保持一致。</strong> 热榜 <code>market=ashare</code> 按代码合并，<code>all</code> 按简称合并，同组比较时只用一种。</li>
</ol>
<p>熔岩站点的个股热度对比工具页面提供了多只股票热度并列展示的只读视图，适合快速目测；需要保存口径、做批量或回看时，再用上面的接口复现。</p>
<h2>七、常见误读对照表</h2>
<table>
<thead>
<tr>
<th>误读</th>
<th>实际口径</th>
<th>规范写法（示例）</th>
</tr>
</thead>
<tbody><tr>
<td>「A 的份额 71%，说明市场主要关注 A」</td>
<td>份额只在这三只股票组成的比较组内成立</td>
<td>「在 A、B、C 三只组成的比较组中，A 份额约 71%」</td>
</tr>
<tr>
<td>「B 的 <code>heat_z20</code> 最高，所以 B 最热」</td>
<td>z 分是相对自身偏离，不是绝对热度</td>
<td>「B 相对自身近 20 日日均偏离最大」</td>
</tr>
<tr>
<td>「C 不在热榜上，热度为 0」</td>
<td>只是未进入返回的前 <code>limit</code> 名</td>
<td>改用 <code>/heat/windows</code> 或 <code>/panel/daily</code> 单独取数</td>
</tr>
<tr>
<td>「面板上算出的 z 分和 <code>heat_z20</code> 一样」</td>
<td>面板是自然日序列，本地计算口径与接口统计量不同</td>
<td>本地列另起名，如 <code>mentions_z20_local</code></td>
</tr>
</tbody></table>
<h2>八、用日频面板做历史横向比较</h2>
<p>实时截面只能回答「现在」。要看过去一段时间里比较组内份额怎么变化，用日频面板 <code>/panel/daily</code>（<code>get_stock_heat_panel</code>）一次取回最多 20 只股票、最长 92 天的自然日序列，再在本地逐日计算份额与相对自身的偏离：</p>
<pre><code class="language-python">r = requests.get(BASE + "/panel/daily",
                 params={"type": "stock", "codes": ",".join(CODES),
                         "from": "2026-08-17", "to": "2026-11-13"},
                 headers=H, timeout=60).json()
assert r["ok"], r["code"]
p = pd.DataFrame(r["data"] if isinstance(r["data"], list) else r["data"].get("list", []))

# 日期列与代码列的列名以 OpenAPI 为准，这里假设为 date / ts_code
p["share_d"] = p["mentions"] / p.groupby("date")["mentions"].transform("sum")
g = p.sort_values("date").groupby("ts_code")["mentions"]
mu = g.transform(lambda s: s.shift(1).rolling(20, min_periods=10).mean())
sd = g.transform(lambda s: s.shift(1).rolling(20, min_periods=10).std())
p["mentions_z20_local"] = (p["mentions"] - mu) / sd
</code></pre>
<p>三点说明：</p>
<ul>
<li><code>shift(1)</code> 保证每天的基准只用此前的数据，避免把当天计入自己的基准；</li>
<li>面板是自然日，周末与节假日也有行，滚动 20 行即 20 个自然日，与接口 <code>heat_z20</code> 的「近 20 日」口径未必相同，所以本地列单独命名；</li>
<li>某天比较组合计为 0 时 <code>share_d</code> 为空，保留空值，不要填 0。</li>
</ul>
<p>这样得到的是一张「每天、每只股票、三口径」的长表，可以画组内份额堆叠图，也可以按日统计哪只股票的本地 z 分最高，作为描述性记录。</p>
<h2>FAQ</h2>
<p><strong>Q：三种口径应该选哪一个作为主口径？</strong><br />A：取决于研究问题。描述「讨论量分配」用份额；描述「相对常态的偏离」用 <code>heat_z20</code>；描述「绝对关注度」用 <code>mentions</code>。报告里建议三列并列，不做加总。</p>
<p><strong>Q：能把三种口径合成一个综合分吗？</strong><br />A：可以在本地做，但权重是主观选择，需要在结果中写明公式，并保留原始三列，避免读者只看到一个难以解释的数字。</p>
<p><strong>Q：比较组超过 20 只怎么办？</strong><br />A：日频面板按 20 个一批分批取数后合并；实时截面可以先取一次 <code>/hot/stocks</code> 并设 <code>limit=200</code>，缺失的股票再逐只补取。</p>
<p><strong>Q：<code>agencies</code> 份额和 <code>mentions</code> 份额差很多说明什么？</strong><br />A：说明讨论结构不同：<code>mentions</code> 份额远高于 <code>agencies</code> 份额，表示该股的讨论更集中在少数主体；反之则参与主体更分散。</p>
<p><strong>Q：份额曲线突然跳变，先查什么？</strong><br />A：先查比较组当天的合计是否很小。合计越小，单只股票几次提及就能让份额大幅变化；其次查当天是否有成员缺行。</p>
<h2>结论</h2>
<p>横向比较熔岩热度，关键是先选口径再看名次：绝对提及回答「谁被说得最多」，窗口份额回答「组内如何分配」，<code>heat_z20</code> 回答「谁相对自身最不寻常」。固定窗口与比较组、缺失不当 0、三列并列展示并保存元数据，多只股票的热度比较就能既直观又可复现。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/tools/stock-heat-compare">https://lava.ai-dolphin.com/tools/stock-heat-compare</a></p>
]]></content:encoded></item><item><title><![CDATA[把单只股票的消息面拼成一条时间线：以宁德时代为例合并逐小时热度、意见摘要与新闻点名]]></title><description><![CDATA[研究一只股票的消息面时，熔岩（Lava）提供了几路互补的数据：单股多窗口热度 /heat/windows 给出近 24 小时逐小时走势，个股消息面 /opinions 给出每条记录的时间与一句话摘要，新闻点名 /news/hits 给出主流媒体快讯的发布时间与命中股票，覆盖度 /coverage/stocks…]]></description><link>https://dustinliang.hashnode.dev/single-stock-news-timeline-merge</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/single-stock-news-timeline-merge</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sat, 14 Nov 2026 01:18:40 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>研究一只股票的消息面时，熔岩（Lava）提供了几路互补的数据：单股多窗口热度 <code>/heat/windows</code> 给出近 24 小时逐小时走势，个股消息面 <code>/opinions</code> 给出每条记录的时间与一句话摘要，新闻点名 <code>/news/hits</code> 给出主流媒体快讯的发布时间与命中股票，覆盖度 <code>/coverage/stocks</code> 给出每日滚动的去重主体数，日频面板 <code>/panel/daily</code> 给出按自然日的历史序列。本文以宁德时代（<code>300750.SZ</code>，仅作接口示例中的查询对象）为例，用六步管线把这几路数据合并成一条按时间排序的事件表，并说明时区、时间粒度和字段边界。文中数字均为示例，时间线只描述「何时、哪一路数据、计数多少」，不包含任何方向判断。</p>
<h2>一、五路数据各自负责什么</h2>
<table>
<thead>
<tr>
<th>熔岩数据板块</th>
<th>MCP 工具</th>
<th>HTTP 路径</th>
<th>时间粒度</th>
<th>在时间线里的角色</th>
</tr>
</thead>
<tbody><tr>
<td>个股热度 · 多窗口</td>
<td><code>get_stock_heat_windows</code></td>
<td><code>/heat/windows</code></td>
<td>六档窗口 + 近 24 小时逐小时</td>
<td>热度骨架：哪个小时讨论量抬升</td>
</tr>
<tr>
<td>个股消息面</td>
<td><code>get_stock_news_heat</code></td>
<td><code>/opinions</code></td>
<td>每条记录的 <code>topic_time</code></td>
<td>事件锚点：当时在讨论什么（摘要）</td>
</tr>
<tr>
<td>新闻点名</td>
<td><code>get_news_stock_hits</code></td>
<td><code>/news/hits</code></td>
<td>每条新闻的发布时间</td>
<td>媒体侧锚点：哪家媒体何时点名</td>
</tr>
<tr>
<td>机构/大V 覆盖度</td>
<td><code>get_agency_coverage</code></td>
<td><code>/coverage/stocks</code></td>
<td>每日</td>
<td>参与面：滚动 7/30 日去重主体数</td>
</tr>
<tr>
<td>日频历史面板</td>
<td><code>get_stock_heat_panel</code></td>
<td><code>/panel/daily</code></td>
<td>自然日</td>
<td>背景：最近 90 天的日频位置</td>
</tr>
</tbody></table>
<p>五路数据都只读，都用同一个股票标识查询（<code>ts_code</code> 或 <code>stock_name</code> 二选一）。建议统一用 <code>ts_code</code>，避免简称变更或重名带来的歧义。</p>
<h2>二、先统一时间轴</h2>
<p>时间线能否拼对，取决于时间戳是否在同一个时区、同一个精度上：</p>
<ul>
<li>响应信封里的 <code>meta.server_time</code> 带 <code>+08:00</code>，是本次调用的服务器时刻；</li>
<li><code>/opinions</code> 记录的 <code>topic_time</code> 为北京时间；</li>
<li><code>/news/hits</code> 每条新闻有发布时间，列名以 OpenAPI 为准；</li>
<li><code>hourly_24h</code> 是从调用时刻往回 24 小时的逐小时桶，桶的起止边界以返回为准；</li>
<li><code>/coverage/stocks</code> 与 <code>/panel/daily</code> 是日粒度，日期按 Asia/Shanghai 自然日。</li>
</ul>
<p>本地建议统一转换为带时区的 <code>Asia/Shanghai</code> 时间戳，再按「小时」与「自然日」两个粒度分别建索引。逐条记录挂在小时索引上，覆盖度与面板挂在日索引上，不要把日粒度数字强行摊到小时。</p>
<h2>三、六步管线</h2>
<h3>步骤 1：取热度骨架</h3>
<pre><code class="language-python"># timeline.py —— 示例，地址与令牌为占位
import os, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
CODE = "300750.SZ"

def get(path, **params):
    r = requests.get(BASE + path, params=params, headers=H, timeout=60).json()
    assert r["ok"], (path, r["code"])
    return r["data"], r["meta"]

win, m_win = get("/heat/windows", ts_code=CODE, min_score=1050)
hourly = win.get("hourly_24h", [])   # 逐小时桶，桶内列名以 OpenAPI 为准
</code></pre>
<p><code>min_score=1050</code> 只影响「高分机构/大V 数」这一列的分数线，不改变总提及数。</p>
<h3>步骤 2：取消息面记录</h3>
<pre><code class="language-python">ops, m_ops = get("/opinions", ts_code=CODE, days=3, only_this_stock=0, limit=200)
records = ops.get("list", []) if isinstance(ops, dict) else ops
heat = ops.get("heat", {}) if isinstance(ops, dict) else {}
</code></pre>
<p>每条记录含 <code>opinion_id</code>、<code>topic_time</code>、<code>agency_id</code>、<code>agency_score</code>、<code>stocks</code>、<code>concepts</code>、<code>summary</code>（≤120 字）、<code>detail</code>（≤600 字）；<code>data.heat</code> 给出区间汇总的 <code>mentions</code> 与 <code>agencies</code>。<code>only_this_stock=0</code> 取该股为第一提及股的记录，<code>=1</code> 只要仅涉及该股的记录，两者条数不同，时间线上要注明用了哪一个。</p>
<h3>步骤 3：取新闻点名</h3>
<pre><code class="language-python">news, m_news = get("/news/hits", ts_code=CODE, hours=72, limit=200)
</code></pre>
<p>指定股票时 <code>hours</code> 默认 72、最多 168；可用 <code>src</code> 精确指定一家媒体。返回包含 <code>news_id</code>、发布时间、媒体、命中股票，不输出标题与正文。</p>
<h3>步骤 4：取覆盖度与日频背景</h3>
<pre><code class="language-python">cov, m_cov = get("/coverage/stocks", ts_code=CODE, days=30, min_score=1050)
pnl, m_pnl = get("/panel/daily", type="stock", codes=CODE,
                 **{"from": "2026-08-17", "to": "2026-11-14"})
</code></pre>
<p>覆盖度返回 <code>cov_7d</code>、<code>cov_30d</code>、<code>new_cov_cum</code>、<code>high_score_ratio_30d</code>，只给计数、不返回机构 ID 列表。面板返回 <code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code>、<code>is_first_mention_day</code>，区间不超过 92 天。</p>
<h3>步骤 5：合并为事件表</h3>
<p>把步骤 1～3 转为统一结构：<code>ts</code>（带时区时间戳）、<code>source</code>（windows / opinions / news）、<code>kind</code>（小时计数 / 记录 / 点名）、<code>value</code>（计数或标识）、<code>ref</code>（<code>opinion_id</code> 或 <code>news_id</code>），按 <code>ts</code> 排序。步骤 4 单独生成日表，用日期与事件表关联。</p>
<pre><code class="language-python">TZ = "Asia/Shanghai"
ev = []
for x in records:
    ev.append({"ts": pd.Timestamp(x["topic_time"]).tz_localize(TZ),
               "source": "opinions", "kind": "记录",
               "value": x.get("agency_score"), "ref": x.get("opinion_id")})
news_rows = news.get("list", []) if isinstance(news, dict) else news
for x in news_rows:
    # 发布时间与媒体的列名以 OpenAPI 为准，这里假设为 pub_time / src
    ev.append({"ts": pd.Timestamp(x.get("pub_time")).tz_localize(TZ),
               "source": "news", "kind": "点名",
               "value": x.get("src"), "ref": x.get("news_id")})
tl = pd.DataFrame(ev).sort_values("ts").reset_index(drop=True)
tl["hour"] = tl["ts"].dt.floor("h")   # 与逐小时桶对齐
</code></pre>
<p>若时间字符串已带 <code>+08:00</code>，用 <code>tz_convert</code> 代替 <code>tz_localize</code>，否则会报错或重复偏移。逐小时桶按 <code>hour</code> 与事件表左连接，即可在每个小时下列出当小时的记录与点名。</p>
<h3>步骤 6：保存调用元数据</h3>
<p>五次调用各记录 <code>meta.request_id</code> 与 <code>meta.server_time</code>，连同参数一起写入同一个 JSON。五次调用计费五次（每次 0.005 元，非 2xx 不计费）。</p>
<h2>四、示例时间线</h2>
<p>以下是一条合并后的示例时间线（全部为示例数据，摘要用占位符表示）：</p>
<table>
<thead>
<tr>
<th>时间（Asia/Shanghai）</th>
<th>来源</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td>11-13 09:00～10:00</td>
<td><code>/heat/windows</code></td>
<td>该小时提及 4 次</td>
</tr>
<tr>
<td>11-13 10:12</td>
<td><code>/news/hits</code></td>
<td>媒体 A 点名，<code>news_id=&lt;id&gt;</code></td>
</tr>
<tr>
<td>11-13 10:20</td>
<td><code>/opinions</code></td>
<td><code>opinion_id=&lt;id&gt;</code>，<code>agency_score=1080</code>，摘要：<summary></summary></td>
</tr>
<tr>
<td>11-13 10:00～11:00</td>
<td><code>/heat/windows</code></td>
<td>该小时提及 17 次</td>
</tr>
<tr>
<td>11-13 10:47</td>
<td><code>/news/hits</code></td>
<td>媒体 B 点名</td>
</tr>
<tr>
<td>11-13 11:00～12:00</td>
<td><code>/heat/windows</code></td>
<td>该小时提及 9 次</td>
</tr>
<tr>
<td>11-13（日）</td>
<td><code>/coverage/stocks</code></td>
<td><code>cov_7d=46</code>，<code>cov_30d=131</code></td>
</tr>
</tbody></table>
<p>这张表能回答的问题是：「讨论量抬升的那个小时前后，媒体点名与消息面记录各出现在什么时刻」。它不回答「为什么」或「接下来会怎样」。</p>
<h2>五、三条字段边界</h2>
<ol>
<li><strong>新闻没有标题和正文。</strong> <code>/news/hits</code> 只给发布时间、媒体与命中股票。时间线上的「内容」只能来自 <code>/opinions</code> 的 <code>summary</code>，不要用外部渠道补全文。</li>
<li><strong>逐小时数据只覆盖最近 24 小时。</strong> 更早的小时级时间线需要用 <code>/opinions</code> 记录的 <code>topic_time</code> 自行按小时计数，与 <code>hourly_24h</code> 口径不完全一致，需分开标注。</li>
<li><strong>面板的加权热度不是 PIT。</strong> <code>/panel/daily</code> 的 <code>weighted_heat</code> 用当前机构分加权；时间线上引用历史加权读数时注明这一点，严格的历史加权口径用 <code>/hot/weighted</code> 的 <code>as_of_date</code>。</li>
</ol>
<h2>六、交给 AI Agent 的提示词</h2>
<pre><code class="language-text">为 ts_code=&lt;代码&gt; 生成近 72 小时消息面时间线：
- 依次调用 get_stock_heat_windows、get_stock_news_heat(days=3, limit=200)、
  get_news_stock_hits(hours=72)、get_agency_coverage(days=30)
- 所有时间统一为 Asia/Shanghai，按时间排序输出表格：时间 / 来源 / 计数或标识
- 摘要只引用 summary 字段，不改写为结论；新闻只列媒体与时间
- 末尾列出每次调用的 request_id 与 server_time
- 不输出任何方向判断或操作含义
</code></pre>
<p>熔岩站点上的宁德时代情绪监测页提供了同一只股票的只读走势展示，适合人工浏览；这条管线适合在脚本或 Agent 里按需复现并保存证据链。</p>
<h2>七、时间线质量自检</h2>
<p>合并完成后，用下面几项快速检查时间线是否可信：</p>
<table>
<thead>
<tr>
<th>检查项</th>
<th>方法</th>
<th>不通过时的处理</th>
</tr>
</thead>
<tbody><tr>
<td>时区一致</td>
<td>抽查 3 条记录，<code>ts</code> 均为 <code>+08:00</code></td>
<td>回到步骤 5 修正 <code>tz_localize</code> / <code>tz_convert</code></td>
</tr>
<tr>
<td>记录未截断</td>
<td><code>/opinions</code> 条数小于 <code>limit</code></td>
<td>缩短 <code>days</code> 或改用 <code>start_date</code>/<code>end_date</code> 分段</td>
</tr>
<tr>
<td>点名未截断</td>
<td><code>/news/hits</code> 条数小于 <code>limit</code></td>
<td>缩短 <code>hours</code> 或按 <code>src</code> 分媒体取数</td>
</tr>
<tr>
<td>小时计数可对照</td>
<td>某小时 <code>hourly_24h</code> 计数与该小时记录条数量级接近</td>
<td>差异大时注明「记录受 <code>only_this_stock</code> 口径影响」</td>
</tr>
<tr>
<td>元数据齐全</td>
<td>五个 <code>request_id</code> 都已保存</td>
<td>补记后再归档</td>
</tr>
</tbody></table>
<p>自检结果建议与时间线一起保存，方便日后复盘时判断这条时间线的完整程度。</p>
<h2>FAQ</h2>
<p><strong>Q：<code>/opinions</code> 的条数比 <code>data.heat.mentions</code> 少，是数据缺失吗？</strong><br />A：不一定。记录列表受 <code>limit</code>（最大 200）截断，而汇总数统计的是整个区间。条数等于 <code>limit</code> 时应缩短区间或分段取数。</p>
<p><strong>Q：新闻点名时间和消息面记录时间谁更早有意义吗？</strong><br />A：可以作为描述记录下来，但两路数据的采集与匹配机制不同，时间先后只说明「各自何时出现」，不说明因果。</p>
<p><strong>Q：能把时间线扩展到多只股票吗？</strong><br />A：可以对每只股票重复管线，但调用次数随股票数线性增加。日频背景可以用 <code>/panel/daily</code> 的 <code>codes</code> 一次传最多 20 个代码来节省调用。</p>
<h2>结论</h2>
<p>单只股票的消息面时间线由五路只读数据拼成：逐小时热度给骨架，意见摘要与新闻点名给锚点，覆盖度与日频面板给背景。先统一时区与粒度，再按六步管线取数、合并、记录元数据，就能得到一张可复现、可审计、只描述事实顺序的事件表。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/catl-sentiment">https://lava.ai-dolphin.com/monitor/catl-sentiment</a></p>
]]></content:encoded></item><item><title><![CDATA[AI 主题热度的五档窗口一致性检查：get_market_hot_concepts 从 1h 到 3d 的 15 项清单]]></title><description><![CDATA[熔岩（Lava）概念板块热度的入口是概念热榜 GET /hot/concepts（MCP 工具 get_market_hot_concepts），可选 window 为 1h、4h、18h、24h、3d 五档。以 AI 相关主题为例：同一个概念在 1h 窗口排前列、在 3d…]]></description><link>https://dustinliang.hashnode.dev/ai-theme-concept-window-consistency</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/ai-theme-concept-window-consistency</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sat, 14 Nov 2026 01:17:49 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）概念板块热度的入口是概念热榜 <code>GET /hot/concepts</code>（MCP 工具 <code>get_market_hot_concepts</code>），可选 <code>window</code> 为 1h、4h、18h、24h、3d 五档。以 AI 相关主题为例：同一个概念在 1h 窗口排前列、在 3d 窗口却排在中段，这种情况很常见，也最容易被误读为「突然爆发」或「热度退潮」。本文给出一份 15 项检查清单，把「取数参数是否一致、多窗口形态如何描述、下钻到成分股与日频序列时口径是否连贯、结果如何记录」逐项写清。清单面向量化研究脚本与 AI Agent，文中数字均为示例，所有字段都是描述性统计量。</p>
<h2>一、为什么概念热度要做跨窗口检查</h2>
<p>概念热榜有三个容易被忽略的特点：</p>
<ol>
<li><strong>窗口是滚动的。</strong> 1h 统计的是从 <code>meta.server_time</code> 往回 1 小时，3d 是往回 72 小时。窗口越短，单条长文或一轮集中讨论的影响越大。</li>
<li><strong>没有 7d 档。</strong> 个股热榜 <code>/hot/stocks</code> 有 7d，概念热榜最长到 3d。需要周度概念序列时，应改用日频面板 <code>/panel/daily</code> 的 <code>type=concept</code>。</li>
<li><strong>已去掉泛词。</strong> 概念热榜在统计前去掉了过于宽泛的词，因此榜单上的概念名就是可以继续下钻的标准名；但被去掉的泛词也不会出现在任何窗口里，跨窗口对比时不必为它们找原因。</li>
</ol>
<p>只看单一窗口时，这些特点不会造成问题；一旦把多个窗口的排名放在一起叙述，就必须先确认口径一致。</p>
<h2>二、参数与口径速查</h2>
<table>
<thead>
<tr>
<th>参数 / 字段</th>
<th>取值或含义</th>
<th>默认</th>
<th>备注</th>
</tr>
</thead>
<tbody><tr>
<td><code>window</code></td>
<td><code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code></td>
<td><code>24h</code></td>
<td>无 <code>7d</code></td>
</tr>
<tr>
<td><code>min_score</code></td>
<td>只统计机构/大V 分 ≥ 该值的提及</td>
<td>不设</td>
<td>分数约 450～1500，1000 左右为中等，1050 约前 10%</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>1～200</td>
<td>50</td>
<td>无分页偏移</td>
</tr>
<tr>
<td><code>mentions</code></td>
<td>窗口内被提到的次数</td>
<td>—</td>
<td>概念层面的计数</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>窗口内提到它的去重机构/大V 数</td>
<td>—</td>
<td>按匿名 <code>agency_id</code> 去重</td>
</tr>
<tr>
<td><code>heat_chg_1d</code> / <code>heat_chg_3d</code> / <code>heat_z20</code></td>
<td>纯统计量</td>
<td>—</td>
<td>精确公式以 OpenAPI 为准</td>
</tr>
</tbody></table>
<p>更新节奏：概念热榜约 2 分钟更新一次，概念成分提及排行 <code>/concepts/stocks</code> 约 10 分钟更新一次。两者时间戳不同步，是跨接口对比时最常见的「对不上」原因。</p>
<h2>三、15 项检查清单</h2>
<h3>A. 取数一致性（1～4）</h3>
<ol>
<li><strong>五个窗口在同一轮内连续取数。</strong> 五次调用间隔尽量短，并记录每次的 <code>meta.server_time</code>；跨轮混用会把「时间推移」误当成「窗口差异」。</li>
<li><strong><code>min_score</code> 五档保持一致。</strong> 一档加门槛、一档不加，比较的就是两类主体。</li>
<li><strong><code>limit</code> 足够大。</strong> 目标概念在长窗口里可能排到 50 名以外，默认 50 会让它「消失」。研究 AI 主题时建议 <code>limit=200</code>。</li>
<li><strong>概念名从榜单返回里取。</strong> 不要手写「人工智能」「AI」等别名；精确匹配以榜单返回为准，后续传给 <code>/concepts/stocks</code> 的 <code>concept</code> 也用同一个字符串（≤30 字符）。</li>
</ol>
<h3>B. 多窗口形态（5～9）</h3>
<ol start="5">
<li><strong>先排列绝对计数，再看名次。</strong> 名次受其他概念影响，<code>mentions</code> 是概念自身的读数。</li>
<li><strong>计算短窗占比。</strong> 例如 <code>share_1h = mentions_1h / mentions_24h</code>（本地计算列）。示例：AI 主题 <code>mentions_1h=36</code>、<code>mentions_24h=180</code>，占比 0.2，意味着最近 1 小时占了一天讨论量的五分之一（示例数字）。</li>
<li><strong>对照 <code>agencies</code> 是否同步变化。</strong> 短窗 <code>mentions</code> 高但 <code>agencies</code> 很少，说明是少数主体集中发布；两列同步升高才是参与面扩大。</li>
<li><strong>读 <code>heat_z20</code> 时带上窗口。</strong> 写成「24h 窗口 <code>heat_z20</code>」，不同窗口的 z 分不能互相比较。</li>
<li><strong>用一个形态标签描述结果。</strong> 建议只用三种描述：短窗集中（1h/4h 占比高）、多窗同步（各窗口名次接近）、长窗累积（3d 名次明显高于短窗）。标签是描述，不推断后续走势。</li>
</ol>
<h3>C. 下钻连贯性（10～13）</h3>
<ol start="10">
<li><strong>成分股用 <code>get_concept_stock_heat</code> 下钻。</strong> <code>/concepts/stocks</code> 用 <code>days</code> 回看（默认 30，最大 365），不是滚动 <code>window</code>；与 24h 概念热度对照时设 <code>days=1</code>，与 3d 对照时设 <code>days=3</code>，并在结果里注明。</li>
<li><strong>检查成分集中度。</strong> 前 3 只成分股 <code>mentions</code> 占概念合计的比例过高时，概念热度更像个股事件带动。</li>
<li><strong>反查重点个股的概念归属。</strong> 用 <code>get_stock_concept_heat</code>（<code>/concepts/by-stock</code>）确认该股是否同时挂在多个 AI 相关概念下，避免重复计数。</li>
<li><strong>需要多日序列时切到日频面板。</strong> <code>get_stock_heat_panel(type="concept", names=...)</code>（<code>/panel/daily</code>）按自然日返回概念的 <code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code>，区间最长 92 天；它与滚动窗口口径不同，不要拼接到同一条曲线上。</li>
</ol>
<h3>D. 记录与复现（14～15）</h3>
<ol start="14">
<li><strong>每轮落库最少字段：</strong> <code>window</code>、<code>min_score</code>、<code>limit</code>、<code>meta.server_time</code>、<code>meta.request_id</code>、概念名、<code>mentions</code>、<code>agencies</code>、<code>heat_chg_1d</code>、<code>heat_chg_3d</code>、<code>heat_z20</code>、名次。</li>
<li><strong>记录费用。</strong> 五档窗口即五次调用，每次 0.005 元，非 2xx 不计费；用 <code>get_account</code>（<code>/account</code>，不扣费）定期核对余额。</li>
</ol>
<h2>四、窗口形态示例表</h2>
<p>以下为某 AI 相关概念在同一轮取数中的示例读数（均为示例数字）：</p>
<table>
<thead>
<tr>
<th><code>window</code></th>
<th><code>mentions</code></th>
<th><code>agencies</code></th>
<th>名次</th>
<th>读法</th>
</tr>
</thead>
<tbody><tr>
<td>1h</td>
<td>36</td>
<td>9</td>
<td>2</td>
<td>短窗集中，参与主体较少</td>
</tr>
<tr>
<td>4h</td>
<td>74</td>
<td>21</td>
<td>4</td>
<td>仍偏短窗</td>
</tr>
<tr>
<td>18h</td>
<td>150</td>
<td>52</td>
<td>6</td>
<td>进入中段</td>
</tr>
<tr>
<td>24h</td>
<td>180</td>
<td>63</td>
<td>7</td>
<td>与 18h 接近</td>
</tr>
<tr>
<td>3d</td>
<td>410</td>
<td>118</td>
<td>11</td>
<td>长窗名次低于短窗</td>
</tr>
</tbody></table>
<p>这张表的规范描述是：「该概念 1h 占 24h 讨论量约 20%，短窗名次高于长窗，形态为短窗集中」。不应写成「热度即将扩散」或任何方向判断。</p>
<h2>五、一轮五窗取数脚本</h2>
<pre><code class="language-python"># concept_windows.py —— 示例，地址与令牌为占位
import os, requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
TARGET = "&lt;从榜单返回复制的概念标准名&gt;"

snap = {}
for w in ("1h", "4h", "18h", "24h", "3d"):
    r = requests.get(BASE + "/hot/concepts",
                     params={"window": w, "limit": 200},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    # 概念名列名以 OpenAPI 为准，这里假设为 concept
    hit = [(i + 1, x) for i, x in enumerate(rows) if x.get("concept") == TARGET]
    snap[w] = {"server_time": r["meta"]["server_time"],
               "request_id": r["meta"]["request_id"],
               "rank": hit[0][0] if hit else None,
               "mentions": hit[0][1].get("mentions") if hit else None,
               "agencies": hit[0][1].get("agencies") if hit else None}

base = snap["24h"]["mentions"] or 0
share_1h = (snap["1h"]["mentions"] or 0) / base if base else None
print(snap, "share_1h:", share_1h)
</code></pre>
<p><code>rank</code> 为空表示目标概念在该窗口前 200 名之外，应记为「未进入榜单前 200」，而不是 0。</p>
<h2>六、与只读监测页的关系</h2>
<p>熔岩站点上的 AI 板块热度监测页是只读展示，适合人工快速浏览主题热度的走势；本文的清单适合在 MCP 或 HTTP 层面复现同一主题的多窗口读数。两者的数据来源一致，但展示粒度与刷新时刻可能不同，引用页面上的读数时同样要注明查看时间。</p>
<h2>七、常见误读对照表</h2>
<table>
<thead>
<tr>
<th>误读</th>
<th>实际口径</th>
<th>规范写法（示例）</th>
</tr>
</thead>
<tbody><tr>
<td>「AI 主题 1h 排第 2，说明全市场都在讨论它」</td>
<td>1h 只代表最近 1 小时，且可能由少数主体贡献</td>
<td>「1h 窗口名次 2，<code>agencies=9</code>」</td>
</tr>
<tr>
<td>「3d 名次下降，热度在退」</td>
<td>长窗名次受其他概念累积量影响</td>
<td>「3d 窗口 <code>mentions=410</code>，名次 11」</td>
</tr>
<tr>
<td>「概念热榜和成分排行对不上，数据有问题」</td>
<td>前者约 2 分钟更新、滚动窗口；后者约 10 分钟更新、按 <code>days</code> 回看</td>
<td>注明两者时间戳与区间</td>
</tr>
<tr>
<td>「日频面板的概念序列可以接在 3d 窗口后面」</td>
<td>面板按自然日切分，窗口按 <code>meta.server_time</code> 滚动</td>
<td>两条曲线分开展示</td>
</tr>
<tr>
<td>「加了 <code>min_score</code> 名次更靠前，说明质量更高」</td>
<td>门槛改变了统计对象，只是另一种口径</td>
<td>「<code>min_score=1050</code> 口径下名次 3」</td>
</tr>
</tbody></table>
<p>误读多数来自省略了参数。只要规范写法里带上窗口、门槛与绝对计数，读者就能自行判断读数的适用范围。</p>
<h2>八、给 AI Agent 的执行模板</h2>
<p>把清单压缩成一段系统提示词，Agent 每次被问到「某个主题最近热不热」时按固定顺序执行：</p>
<pre><code class="language-text">对目标概念执行五窗一致性检查：
1. 用 get_market_hot_concepts 依次取 window=1h/4h/18h/24h/3d，limit=200，min_score 保持一致
2. 每档记录名次、mentions、agencies、heat_z20 与 meta.server_time；未进前 200 记为「未进入」
3. 计算 share_1h = mentions_1h / mentions_24h，给出形态标签：短窗集中 / 多窗同步 / 长窗累积
4. 如需成分股，用 get_concept_stock_heat，days 与对照窗口匹配并注明
5. 回答只描述统计形态，不推断后续走势，不给任何操作含义
</code></pre>
<p>模板中的步骤 2 与步骤 5 最重要：前者保证读数可复现，后者保证输出停留在描述层面。</p>
<h2>FAQ</h2>
<p><strong>Q：为什么 1h 排名很高，3d 却不在前 50？</strong><br />A：3d 窗口累积的是 72 小时讨论量，长期稳定讨论的概念会占据前列；一个短时集中被讨论的概念在长窗口里分量被稀释。先把 <code>limit</code> 调到 200 再确认名次。</p>
<p><strong>Q：概念热榜能按 <code>market</code> 筛选吗？</strong><br />A：概念热榜没有 <code>market</code> 参数。需要区分市场时，下钻到成分股后按代码自行过滤。</p>
<p><strong>Q：五档窗口的 <code>heat_z20</code> 能求平均吗？</strong><br />A：不建议。各窗口的统计区间不同，z 分的含义也不同，平均后没有可解释的口径。</p>
<p><strong>Q：清单能直接交给 AI Agent 执行吗？</strong><br />A：可以把 A、B 两组写进系统提示词，要求回答时同时给出 <code>window</code>、<code>min_score</code>、<code>mentions</code>、<code>agencies</code>，并禁止把形态标签改写为方向判断。</p>
<p><strong>Q：同一主题有多个相近概念名，要合并吗？</strong><br />A：可以在本地维护别名映射后并列展示，但不要把多个概念的 <code>mentions</code> 直接相加，同一条消息可能同时命中多个概念，相加会重复计数。</p>
<h2>结论</h2>
<p>概念热度的多窗口读数各自成立，问题出在把它们放在一起叙述时口径不一致。按清单做到同轮取数、参数一致、先看绝对计数、下钻时显式换算 <code>days</code>、需要多日序列时切换到日频面板，并把每轮参数与 <code>meta.server_time</code> 落库，AI 主题的热度形态就能被准确描述和复现。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/board-ai-heat">https://lava.ai-dolphin.com/monitor/board-ai-heat</a></p>
]]></content:encoded></item><item><title><![CDATA[提及次数与机构数之比：用 mentions/agencies 识别熔岩热度里的「少数主体密集讨论」]]></title><description><![CDATA[熔岩（Lava）几乎每个热度板块都同时返回两个计数：mentions（被提到几次）和 agencies（有多少个不同的机构/大V 提到）。多数用法只盯 mentions 排序，但两只 mentions 相同的股票，可能一只是 40 个主体各说一次，另一只是 4…]]></description><link>https://dustinliang.hashnode.dev/mentions-per-agency-concentration</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/mentions-per-agency-concentration</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Sat, 14 Nov 2026 01:16:42 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）几乎每个热度板块都同时返回两个计数：<code>mentions</code>（被提到几次）和 <code>agencies</code>（有多少个不同的机构/大V 提到）。多数用法只盯 <code>mentions</code> 排序，但两只 <code>mentions</code> 相同的股票，可能一只是 40 个主体各说一次，另一只是 4 个主体反复说了十次。本文把两列相除得到「每主体提及数」，说明它在全市场热度、概念板块、日频面板里各自怎么取、怎么处理分母为 0 和小样本，并给出一段从 <code>/panel/daily</code> 取数到截面分档的 Python 走读。文中所有数字均为示例，比值是描述讨论结构的统计量，不是交易信号。</p>
<h2>一、两个计数分别数的是什么</h2>
<table>
<thead>
<tr>
<th>字段</th>
<th>含义</th>
<th>去重方式</th>
</tr>
</thead>
<tbody><tr>
<td><code>mentions</code></td>
<td>统计区间内被提到的次数</td>
<td>按消息记录计数，同一主体多次提及会重复计入</td>
</tr>
<tr>
<td><code>agencies</code></td>
<td>统计区间内提到它的机构/大V 数量</td>
<td>按匿名 <code>agency_id</code> 去重</td>
</tr>
</tbody></table>
<p>两列在以下熔岩数据板块中同时出现，口径都是「同一统计区间内」：</p>
<table>
<thead>
<tr>
<th>熔岩数据板块</th>
<th>MCP 工具</th>
<th>HTTP 路径</th>
<th>统计区间由谁决定</th>
</tr>
</thead>
<tbody><tr>
<td>全市场热度（个股热榜）</td>
<td><code>get_market_hot_stocks</code></td>
<td><code>/hot/stocks</code></td>
<td><code>window</code>：1h / 4h / 18h / 24h / 3d / 7d</td>
</tr>
<tr>
<td>概念板块热度（概念热榜）</td>
<td><code>get_market_hot_concepts</code></td>
<td><code>/hot/concepts</code></td>
<td><code>window</code>：1h ～ 3d</td>
</tr>
<tr>
<td>概念板块 · 概念→个股</td>
<td><code>get_concept_stock_heat</code></td>
<td><code>/concepts/stocks</code></td>
<td><code>days</code>，默认 30</td>
</tr>
<tr>
<td>概念板块 · 个股→概念</td>
<td><code>get_stock_concept_heat</code></td>
<td><code>/concepts/by-stock</code></td>
<td><code>days</code>，默认 90</td>
</tr>
<tr>
<td>个股热度 · 多窗口</td>
<td><code>get_stock_heat_windows</code></td>
<td><code>/heat/windows</code></td>
<td>六档窗口各一组</td>
</tr>
<tr>
<td>个股消息面汇总</td>
<td><code>get_stock_news_heat</code></td>
<td><code>/opinions</code></td>
<td><code>days</code> 或 <code>start_date</code>/<code>end_date</code>，汇总在 <code>data.heat</code></td>
</tr>
<tr>
<td>日频历史面板</td>
<td><code>get_stock_heat_panel</code></td>
<td><code>/panel/daily</code></td>
<td>自然日</td>
</tr>
</tbody></table>
<p>注意：<code>/heat/windows</code> 每个窗口还给出「高分机构/大V 数」，日频面板还给出 <code>weighted_heat</code>。本文只用 <code>mentions</code> 与 <code>agencies</code> 两列，另外两列放在第五节讨论分工。</p>
<h2>二、比值的定义与边界</h2>
<p>把每主体提及数记为 <code>mpa</code>（mentions per agency，本地计算列，不是接口返回字段）：</p>
<pre><code class="language-text">mpa = mentions / agencies          （agencies &gt;= 1）
mpa = 空值                          （agencies = 0）
</code></pre>
<p>几条边界规则：</p>
<ol>
<li><strong>下限是 1。</strong> 每个主体至少贡献一次提及，所以 <code>mpa &gt;= 1</code>；等于 1 表示每个主体只说了一次，讨论最分散。</li>
<li><strong>分母为 0 不填 0。</strong> 某些行可能出现 <code>mentions &gt; 0</code> 而 <code>agencies = 0</code>（例如提及记录没有关联到主体），这类行应标记为空值并单独计数，不要并入「分散」或「集中」任何一档。</li>
<li><strong>小样本不读比值。</strong> <code>agencies</code> 只有 1～2 时，比值波动极大。建议设 <code>agencies &gt;= 5</code>（示例阈值）作为读比值的前提，低于阈值只记录原始计数。</li>
<li><strong>跨区间不可比。</strong> 24h 窗口的 <code>mpa</code> 与 7d 窗口的 <code>mpa</code> 统计区间不同，同一主体在更长区间里有更多机会重复提及，长窗口的比值天然偏高。比较时固定 <code>window</code> 或 <code>days</code>。</li>
<li><strong>取对数更稳。</strong> 截面比较时用 <code>log(mpa)</code>，可以压缩极端值，0 点对应「每主体一次」。</li>
</ol>
<h2>三、四种组合的读法</h2>
<p>把 <code>mentions</code> 的截面分位和 <code>mpa</code> 放在一起，可以得到四类讨论结构：</p>
<table>
<thead>
<tr>
<th><code>mentions</code> 分位</th>
<th><code>mpa</code></th>
<th>讨论结构（描述）</th>
<th>建议同时看</th>
</tr>
</thead>
<tbody><tr>
<td>高</td>
<td>低（接近 1～2）</td>
<td>广泛讨论：很多不同主体各自提到</td>
<td><code>/concepts/by-stock</code> 看是否由概念带动</td>
</tr>
<tr>
<td>高</td>
<td>高</td>
<td>集中讨论：较少主体反复提及</td>
<td><code>/opinions</code> 抽样 <code>agency_id</code> 分布</td>
</tr>
<tr>
<td>低</td>
<td>高</td>
<td>小圈子讨论：讨论量不大且来自少数主体</td>
<td>通常只记录，不单独解读</td>
</tr>
<tr>
<td>低</td>
<td>低</td>
<td>零散提及</td>
<td>首提类事件常落在这里</td>
</tr>
</tbody></table>
<p>示例：同一 24h 窗口，股票 A <code>mentions=48</code>、<code>agencies=40</code>，<code>mpa=1.2</code>；股票 B <code>mentions=48</code>、<code>agencies=6</code>，<code>mpa=8.0</code>（均为示例数字）。两者在热榜上名次接近，但 B 的讨论来自更少的主体，单一主体的表达习惯对计数的影响更大。</p>
<h2>四、Python 走读：从日频面板到截面分档</h2>
<p>日频面板 <code>/panel/daily</code>（<code>get_stock_heat_panel</code>）按自然日返回 <code>mentions</code>、<code>agencies</code>、<code>weighted_heat</code>、<code>is_first_mention_day</code>，单次最多 20 个代码，区间最长 92 天。下面的示例分批取数，计算每日 <code>mpa</code>，再按截面分档。</p>
<pre><code class="language-python"># mpa_panel.py —— 示例，地址与令牌为占位
import os, requests, numpy as np, pandas as pd

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def panel(codes, frm, to):
    r = requests.get(BASE + "/panel/daily",
                     params={"type": "stock", "codes": ",".join(codes),
                             "from": frm, "to": to},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    return pd.DataFrame(rows), r["meta"]["request_id"]

universe = ["&lt;CODE_1&gt;", "&lt;CODE_2&gt;"]  # 自有股票池，按 20 个一批
frames = []
for i in range(0, len(universe), 20):
    df, rid = panel(universe[i:i + 20], "2026-10-01", "2026-11-13")
    df["request_id"] = rid
    frames.append(df)
p = pd.concat(frames, ignore_index=True)

MIN_AG = 5  # 示例阈值
p["mpa"] = p["mentions"] / p["agencies"].where(p["agencies"] &gt;= MIN_AG)
p["log_mpa"] = np.log(p["mpa"])  # 空值保持为空

# 日期列与代码列的列名以 OpenAPI 为准，这里假设为 date / ts_code
p["m_pct"] = p.groupby("date")["mentions"].rank(pct=True)
p["mpa_pct"] = p.groupby("date")["log_mpa"].rank(pct=True)
p["bucket"] = (
    (p["m_pct"] &gt;= 0.8).map({True: "高提及", False: "低提及"})
    + "/" + (p["mpa_pct"] &gt;= 0.8).map({True: "集中", False: "分散"})
)
print(p.groupby("bucket").size())
</code></pre>
<p>示例输出可能是：「高提及/分散」312 行、「高提及/集中」57 行、「低提及/集中」41 行，其余为低提及/分散或空值（示例数字）。每批调用计费一次（0.005 元/次，非 2xx 不计费），<code>request_id</code> 随行保存，方便事后复现。</p>
<p>两个工程细节：</p>
<ul>
<li>面板是<strong>自然日</strong>，周末与节假日也有行。若要和交易日行情对齐，先决定周末的计数归入哪个交易日，再计算比值；先算比值再合并会把两天的比值错误平均。</li>
<li><code>mpa</code> 是两个计数的比，合并多日时应该先把 <code>mentions</code>、<code>agencies</code> 分别汇总再相除。但多日 <code>agencies</code> 简单相加会重复计入同一主体，所以多日口径更适合直接用对应长度的 <code>window</code> 或 <code>days</code> 重新取数。</li>
</ul>
<h2>五、与加权热度、覆盖度的分工</h2>
<table>
<thead>
<tr>
<th>统计量</th>
<th>来源</th>
<th>回答的问题</th>
<th>与 <code>mpa</code> 的关系</th>
</tr>
</thead>
<tbody><tr>
<td><code>weighted_heat</code></td>
<td><code>/panel/daily</code>、<code>/hot/weighted</code></td>
<td>按主体分数加权后热度有多高</td>
<td>回答「谁在说」的质量，<code>mpa</code> 回答「说得是否集中」</td>
</tr>
<tr>
<td><code>cov_7d</code>、<code>cov_30d</code></td>
<td><code>/coverage/stocks</code></td>
<td>滚动 7/30 日去重主体数</td>
<td>跨日去重的分母，比单日 <code>agencies</code> 更稳</td>
</tr>
<tr>
<td><code>new_cov_cum</code></td>
<td><code>/coverage/stocks</code></td>
<td>区间累计新增覆盖</td>
<td>新主体进入时 <code>mpa</code> 往往下降</td>
</tr>
<tr>
<td><code>high_score_ratio_30d</code></td>
<td><code>/coverage/stocks</code></td>
<td>30 日高分主体占比</td>
<td>区分「集中且高分」与「集中但分数一般」</td>
</tr>
</tbody></table>
<p>两点口径提醒：<code>/panel/daily</code> 的 <code>weighted_heat</code> 用<strong>当前</strong>机构分加权，不是当日 PIT；严格回测要用 <code>/hot/weighted</code> 的 <code>as_of_date</code> 或 <code>/agencies/pit</code> 截面。覆盖度接口只给计数，不返回机构 ID 列表，因此无法从它反推「是哪几个主体在集中讨论」，需要时只能从 <code>/opinions</code> 的记录里按匿名 <code>agency_id</code> 统计分布。</p>
<h2>六、口径检查清单</h2>
<ul>
<li>引用 <code>mpa</code> 时写明来源接口与区间，例如「24h 窗口 <code>/hot/stocks</code> 的 <code>mentions/agencies</code>」。</li>
<li><code>market=all</code> 时热榜按简称合并，A 股按代码合并，两种口径的比值不要放进同一张截面。</li>
<li>在 <code>/hot/stocks</code> 上加 <code>min_score</code> 后，只统计高分主体的提及，<code>mentions</code> 与 <code>agencies</code> 同时缩小，比值含义变为「高分主体内部的集中度」。</li>
<li>落库字段至少包括：接口路径、<code>window</code> 或 <code>days</code>、<code>min_score</code>、<code>meta.server_time</code>、<code>meta.request_id</code>、<code>mentions</code>、<code>agencies</code> 与本地计算的 <code>mpa</code>。</li>
</ul>
<h2>FAQ</h2>
<p><strong>Q：<code>mpa</code> 高是不是说明讨论质量低？</strong><br />A：不能这样推。比值只描述讨论在主体之间是否集中，和主体分数无关。质量相关的问题用 <code>weighted_heat</code> 或 <code>high_score_ratio_30d</code> 另外回答。</p>
<p><strong>Q：概念板块也能算这个比值吗？</strong><br />A：可以。<code>/hot/concepts</code> 和 <code>/concepts/stocks</code> 都返回两列计数。概念层面的高比值常见于少数主体反复写同一主题的长系列内容，读法与个股一致。</p>
<p><strong>Q：<code>agencies = 0</code> 的行能删掉吗？</strong><br />A：建议保留并单独计数。删除会让「讨论结构」的统计样本看起来比实际更完整，保留空值才能看出这类行占比有多大。</p>
<p><strong>Q：能把 <code>mpa</code> 做成回测因子吗？</strong><br />A：技术上可以在 <code>/panel/daily</code> 上逐日计算，但要注意自然日对齐与小样本阈值，且结论需要注明口径。比值本身是描述性统计量，不包含方向含义。</p>
<p><strong>Q：同一只股票在热榜和面板上算出的比值不一样，哪个对？</strong><br />A：两者都对，只是统计区间不同。热榜的 24h 是从 <code>meta.server_time</code> 往回的滚动窗口，面板是按自然日切分，两个区间的主体集合不同，去重后的 <code>agencies</code> 自然不同。引用时说明来源即可，不要混用。</p>
<h2>结论</h2>
<p><code>mentions</code> 回答「被说了多少次」，<code>agencies</code> 回答「有多少个不同主体在说」，两列相除得到的每主体提及数能把同样热度下的不同讨论结构区分开。读比值前固定统计区间、设好 <code>agencies</code> 下限、空值单独计数，再配合加权热度与覆盖度看主体质量与广度，就能把熔岩热度从单一排名扩展成更完整的结构描述。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/research">https://lava.ai-dolphin.com/research</a></p>
]]></content:encoded></item><item><title><![CDATA[AI Agent 调用熔岩 MCP 的提示词与结果缓存清单：按更新节奏设 TTL 的 24 项检查]]></title><description><![CDATA[把熔岩（Lava）远程 MCP 接进 AI Agent 后，最常见的浪费不是调用失败，而是「同一个问题在一轮对话里被查了三次」：模型换个说法重新调用 get_market_hot_stocks，或者对同一只股票先后用简称和代码各查一次…]]></description><link>https://dustinliang.hashnode.dev/agent-mcp-cache-prompt-checklist</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/agent-mcp-cache-prompt-checklist</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Fri, 13 Nov 2026 01:15:56 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>把熔岩（Lava）远程 MCP 接进 AI Agent 后，最常见的浪费不是调用失败，而是「同一个问题在一轮对话里被查了三次」：模型换个说法重新调用 <code>get_market_hot_stocks</code>，或者对同一只股票先后用简称和代码各查一次 <code>get_stock_news_heat</code>。熔岩的工具全部只读，成功调用按次计费，数据又各有更新节奏，因此非常适合在 Agent 侧做一层结果缓存，并在提示词里约束调用方式。本文以检查清单的形式给出 24 项要点，分为接入、缓存键、TTL、提示词、错误与计费、审计六组。TTL 数值均为示例，以你的业务节奏为准。</p>
<h2>一、接入前提（4 项）</h2>
<ul>
<li><input disabled type="checkbox" /> <strong>1. 端点与传输。</strong> 远程 MCP 路径为 <code>/subscribe/v1/mcp</code>，传输为 Streamable HTTP。HTTP 版本的路径与 MCP 工具一一对应，缓存层可以同时服务两种调用。</li>
<li><input disabled type="checkbox" /> <strong>2. 鉴权放在请求头。</strong> 使用 <code>Authorization: Bearer &lt;API Key&gt;</code> 或 <code>X-API-Key</code>；也支持 OAuth 2.1（PKCE S256 + 动态注册）。Key 不放进网址，不写进提示词，也不进入缓存值。</li>
<li><input disabled type="checkbox" /> <strong>3. 工具清单固定版本。</strong> 启动时调用一次 <code>tools/list</code>，把工具名与 <code>inputSchema</code> 的哈希记入日志。参数演进时缓存键规则需要同步调整。</li>
<li><input disabled type="checkbox" /> <strong>4. 明确只读边界。</strong> 熔岩工具只返回结构化数字与短摘要，提示词里写明「不生成操作建议」，缓存层也不保存模型的二次加工结论，只保存工具原始返回。</li>
</ul>
<h2>二、缓存键怎么设计（4 项）</h2>
<ul>
<li><input disabled type="checkbox" /> <strong>5. 键 = 工具名 + 规范化参数。</strong> 参数按名称排序，默认值显式补齐。例如 <code>get_market_hot_stocks</code> 不传 <code>window</code> 与传 <code>window=24h</code> 应命中同一个键。</li>
<li><input disabled type="checkbox" /> <strong>6. 标的统一成代码。</strong> <code>stock_name</code> 与 <code>ts_code</code> 二选一的工具（<code>get_stock_news_heat</code>、<code>get_stock_heat_windows</code>、<code>get_stock_concept_heat</code> 等），缓存前先把简称映射成代码，避免一只股票两份缓存。映射表本身可以长期缓存。</li>
<li><input disabled type="checkbox" /> <strong>7. 概念名保持原样。</strong> <code>get_concept_stock_heat</code> 的 <code>concept</code> 精确匹配、不超过 30 字符，缓存键里不要做全半角或空格「清洗」，否则会把两个实际不同的请求合并成一个。</li>
<li><input disabled type="checkbox" /> <strong>8. 市场范围进键。</strong> <code>market=ashare</code> 与 <code>market=all</code> 合并键不同（前者按代码、后者按简称），必须作为独立缓存条目。</li>
</ul>
<h2>三、按数据板块设 TTL（4 项）</h2>
<p>TTL 的上限由数据更新节奏决定：比更新更频繁地刷新只会得到重复内容。文档给出的节奏是：个股热榜约 1 分钟，概念热榜与新闻点名约 2 分钟，概念成分约 10 分钟。其余接口没有公开周期，下表按数据性质给出示例值。</p>
<table>
<thead>
<tr>
<th>熔岩数据板块</th>
<th>MCP 工具</th>
<th>公开更新节奏</th>
<th>示例 TTL</th>
</tr>
</thead>
<tbody><tr>
<td>全市场热度：个股热榜</td>
<td><code>get_market_hot_stocks</code></td>
<td>约 1 分钟</td>
<td>盘中 2～5 分钟</td>
</tr>
<tr>
<td>全市场热度：加速榜、首提流</td>
<td><code>get_accelerating_hot_stocks</code>、<code>get_first_mention_stocks</code></td>
<td>未公开</td>
<td>盘中 5～10 分钟</td>
</tr>
<tr>
<td>概念板块：概念热榜</td>
<td><code>get_market_hot_concepts</code></td>
<td>约 2 分钟</td>
<td>5 分钟</td>
</tr>
<tr>
<td>概念板块：概念成分</td>
<td><code>get_concept_stock_heat</code></td>
<td>约 10 分钟</td>
<td>10～30 分钟</td>
</tr>
<tr>
<td>新闻点名</td>
<td><code>get_news_stock_hits</code></td>
<td>约 2 分钟</td>
<td>5 分钟</td>
</tr>
<tr>
<td>个股热度：消息面、多窗</td>
<td><code>get_stock_news_heat</code>、<code>get_stock_heat_windows</code></td>
<td>未公开</td>
<td>10 分钟</td>
</tr>
<tr>
<td>个股热度：日频面板</td>
<td><code>get_stock_heat_panel</code></td>
<td>自然日聚合</td>
<td>已结束的日期长期有效；含今天则 1 小时</td>
</tr>
<tr>
<td>机构/大V：PIT、日期列表</td>
<td><code>get_agency_pit</code>、<code>list_agency_pit_dates</code></td>
<td>按发布日</td>
<td>已发布截面长期有效</td>
</tr>
<tr>
<td>账户</td>
<td><code>get_account</code></td>
<td>实时，不计费</td>
<td>不缓存或 1 分钟</td>
</tr>
</tbody></table>
<ul>
<li><input disabled type="checkbox" /> <strong>9. 区分「会变」与「已定格」。</strong> 日频面板中已经结束的日期、<code>/agencies/pit</code> 的已发布截面，是可以长期缓存的历史；含今天的数据必须短 TTL。</li>
<li><input disabled type="checkbox" /> <strong>10. <code>as_of_date</code> 的特殊处理。</strong> <code>get_weighted_hot_stocks</code> 传入 <code>as_of_date</code> 时消息截至当天 15:00（Asia/Shanghai）、分数走 PIT。若 <code>as_of_date</code> 是今天且当前早于 15:00，截面尚未定格，只能短缓存；过了截稿时间才可以长期保存。</li>
<li><input disabled type="checkbox" /> <strong>11. 盘中盘后两套 TTL。</strong> 消息面盘后也在更新，但节奏较慢，盘后 TTL 可放大为盘中的 3～6 倍（示例）。</li>
<li><input disabled type="checkbox" /> <strong>12. 交易日历由你提供。</strong> 熔岩接口按自然日或滚动窗口统计，是否为交易时段要用自有日历判断，不要让模型自行推断。</li>
</ul>
<h2>四、提示词约束（4 项）</h2>
<ul>
<li><input disabled type="checkbox" /> <strong>13. 先查缓存再调用。</strong> 在工具描述或系统提示中写明：同一轮对话内，同一工具与同一参数只调用一次，结果复用。</li>
<li><input disabled type="checkbox" /> <strong>14. 回答必须带时间与参数。</strong> 每次引用数字都带 <code>window</code> 或 <code>days</code>、<code>market</code>、<code>min_score</code>，以及 <code>meta.server_time</code>。命中缓存时，回答中的时间应是缓存条目的 <code>server_time</code>，而不是当前时间。</li>
<li><input disabled type="checkbox" /> <strong>15. 禁止模型补写字段。</strong> 返回里没有的字段不得猜测；<code>summary</code> 不超过 120 字、<code>detail</code> 不超过 600 字，都是摘要而不是原文；新闻点名不含标题与正文；机构/大V 只用 <code>agency_id</code> 指代。</li>
<li><input disabled type="checkbox" /> <strong>16. 工具选择写成规则表。</strong> 例如「现在谁最热 → 个股热榜；谁变化最快 → 加速榜；此前 7 天无人提及 → 首提流；高分主体在讨论什么 → 加权热榜；单股时间结构 → 多窗」。规则越明确，模型越少「换个工具再试一次」。</li>
</ul>
<p>示例系统提示片段：</p>
<pre><code class="language-text">你可以调用熔岩只读工具。规则：
1) 调用前检查 cache[(tool, normalized_args)]，命中且未过期则直接使用。
2) stock_name 先映射为 ts_code 再作为缓存键。
3) 回答格式：结论(高/中/低) + 数字 + 参数 + server_time。
4) 机构/大V 只写 agency_id；不推测真名；不补写摘要外的内容。
5) 只描述统计结果，不给出任何操作建议。
</code></pre>
<h2>五、错误、限流与计费（4 项）</h2>
<ul>
<li><input disabled type="checkbox" /> <strong>17. 只缓存成功结果。</strong> <code>ok=false</code> 的响应不进缓存，但要记录 <code>code</code>，避免模型在同一错误上无限重试。</li>
<li><input disabled type="checkbox" /> <strong>18. 429 按 <code>Retry-After</code> 等待。</strong> <code>RATE_LIMITED</code> 时按响应头等待后最多重试一次；<code>DAILY_QUOTA_EXCEEDED</code> 当日停止调用，切换为只读缓存并在回答中说明。</li>
<li><input disabled type="checkbox" /> <strong>19. 402 余额不足如实转告。</strong> <code>INSUFFICIENT_BALANCE</code> 时告诉用户到熔岩「AI数据服务」→「概览与充值」处理，不重试。</li>
<li><input disabled type="checkbox" /> <strong>20. 用响应头核算费用。</strong> 成功调用每次 0.005 元，非 2xx 与 <code>get_account</code> 不计费；响应头 <code>X-Billing-Charged-Yuan</code> 与 <code>X-Balance-Yuan</code> 可直接写入缓存条目的元数据，命中缓存即节省一次计费。</li>
</ul>
<h2>六、审计与复现（4 项）</h2>
<ul>
<li><input disabled type="checkbox" /> <strong>21. 缓存条目带 <code>request_id</code>。</strong> 保存 <code>meta.request_id</code> 与 <code>meta.server_time</code>，任何一条回答都能追溯到具体的那次调用。</li>
<li><input disabled type="checkbox" /> <strong>22. 命中率与节省额入日志。</strong> 每日统计「命中次数 × 单价」作为节省估算。示例：一天 600 次工具请求、命中 380 次，按 0.005 元计节省约 1.9 元（示例数字），更重要的是减少了重复数据进入上下文。</li>
<li><input disabled type="checkbox" /> <strong>23. 上下文只放必要字段。</strong> 榜单返回最多 200 行，放进模型上下文前按问题裁剪列和行，原始返回留在缓存里，需要时再取。</li>
<li><input disabled type="checkbox" /> <strong>24. 定期回归。</strong> 用固定问题集比较「缓存命中回答」与「实时调用回答」的参数和字段是否一致，发现工具参数演进时同步更新键规则。</li>
</ul>
<h2>缓存失效的触发条件</h2>
<p>TTL 到期只是最基本的失效方式。下面几种情况也应主动清除相关条目，否则缓存会把旧口径带进新回答：</p>
<table>
<thead>
<tr>
<th>触发条件</th>
<th>影响范围</th>
<th>处理（示例）</th>
</tr>
</thead>
<tbody><tr>
<td><code>tools/list</code> 哈希变化</td>
<td>参数或字段可能演进</td>
<td>清空对应工具的全部条目，重新核对默认值</td>
</tr>
<tr>
<td>自然日切换（Asia/Shanghai 0 点）</td>
<td>含「今天」的面板、<code>days</code> 类回看</td>
<td>清除包含前一日为终点的条目</td>
</tr>
<tr>
<td>当天 15:00 截稿</td>
<td><code>as_of_date</code> 为今天的加权热榜</td>
<td>15:00 前的短缓存作废，截稿后重新取一次再长期保存</td>
</tr>
<tr>
<td>机构/大V 新发布日</td>
<td><code>get_agency_ranking</code> 当前排行</td>
<td>用 <code>list_agency_pit_dates</code> 发现新日期后刷新排行缓存</td>
</tr>
<tr>
<td>用户显式要求刷新</td>
<td>单个问题</td>
<td>跳过缓存调用一次，并用新结果覆盖</td>
</tr>
</tbody></table>
<p>一个容易忽略的点：<code>get_stock_news_heat</code> 返回的意见记录里，<code>agency_score</code> 是主体的当前分。机构/大V 新发布日之后，同一条记录的分数可能不同，因此消息面缓存也应在新发布日后失效，或在回答中注明「分数为取数时的当前分」。</p>
<h2>一个最小缓存装饰器（示例）</h2>
<pre><code class="language-python"># lava_cache.py —— 示例，地址与令牌为占位
import json, time, hashlib

TTL = {"get_market_hot_stocks": 180, "get_market_hot_concepts": 300,
       "get_concept_stock_heat": 900, "get_news_stock_hits": 300,
       "get_stock_news_heat": 600, "get_stock_heat_windows": 600}
DEFAULTS = {"get_market_hot_stocks": {"window": "24h", "market": "ashare", "limit": 50}}
_store = {}

def key(tool, args):
    a = {**DEFAULTS.get(tool, {}), **args}
    raw = tool + json.dumps(a, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(raw.encode()).hexdigest()

def cached_call(tool, args, call_fn):
    k, now = key(tool, args), time.time()
    hit = _store.get(k)
    if hit and now - hit["t"] &lt; TTL.get(tool, 300):
        return hit["resp"], True
    resp = call_fn(tool, args)          # call_fn 负责实际 MCP/HTTP 调用
    if resp.get("ok"):
        _store[k] = {"t": now, "resp": resp}
    return resp, False
</code></pre>
<p>这段代码只演示键规范化与按工具区分 TTL，生产环境还需要持久化存储、并发锁与盘中盘后切换。</p>
<h2>FAQ</h2>
<p><strong>Q：缓存会不会让 Agent 给出过时的热度？</strong><br />A：只要 TTL 不长于业务可接受的延迟，并且回答里展示的是缓存条目的 <code>server_time</code>，读者就能判断数据时点。需要最新数字时，允许用户显式要求「刷新」，跳过缓存。</p>
<p><strong>Q：MCP 和 HTTP 两条通道能共用一份缓存吗？</strong><br />A：可以。两者对应同一套数据与参数，缓存键用工具名或路径统一映射即可；鉴权信息不进入键和值。</p>
<p><strong>Q：日频面板可以缓存多久？</strong><br />A：已经结束的自然日可以长期保存；包含今天的那一行是未完结日，应短 TTL 或剔除。注意 <code>weighted_heat</code> 是当前机构分口径，若机构分后续更新，重新拉取的历史 <code>weighted_heat</code> 可能变化，缓存时要标注取数日期。</p>
<h2>结论</h2>
<p>熔岩 MCP 的只读、按次计费与分板块更新节奏，决定了 Agent 侧缓存是性价比最高的工程优化。按「工具名 + 规范化参数」设计键，按数据板块的更新节奏设 TTL，区分已定格的历史与仍在变化的今天，再用提示词约束模型复用结果、带上参数与 <code>server_time</code>，就能同时降低费用、减少上下文噪声，并让每一条回答都可以追溯。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/stock-heat-monitoring">https://lava.ai-dolphin.com/stock-heat-monitoring</a></p>
]]></content:encoded></item><item><title><![CDATA[熔岩概念成分提及排行的 days 回看陷阱：get_concept_stock_heat 读数的 10 个坑]]></title><description><![CDATA[熔岩（Lava）的概念成分提及排行 GET /concepts/stocks（MCP 工具 get_concept_stock_heat）回答的是：在某个概念下，最近哪些股票被提到最多、有多少机构/大V 提到。它有一个看似简单的参数 days：默认 30，范围 1～365。正是这个参数，加上 limit…]]></description><link>https://dustinliang.hashnode.dev/concept-stocks-days-lookback-pitfalls</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/concept-stocks-days-lookback-pitfalls</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Fri, 13 Nov 2026 01:15:03 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）的概念成分提及排行 <code>GET /concepts/stocks</code>（MCP 工具 <code>get_concept_stock_heat</code>）回答的是：在某个概念下，最近哪些股票被提到最多、有多少机构/大V 提到。它有一个看似简单的参数 <code>days</code>：默认 30，范围 1～365。正是这个参数，加上 <code>limit</code> 上限、约 10 分钟的更新节奏和概念名精确匹配，造成了大量「两次查询对不上」「榜单突然换人」的困惑。本文用坑位清单的形式逐条列出，每一条都给出现象、原因和处理方式。所有数字均为示例，概念名使用「示例概念甲」等虚构名称。</p>
<h2>先认清这张表在熔岩里的位置</h2>
<p>熔岩的消息面数据分几块：全市场热度（个股热榜、加速榜、首提流、加权热榜）、个股热度（<code>/opinions</code>、<code>/heat/windows</code>、<code>/panel/daily</code>）、概念板块、机构/大V 评分（含 PIT 截面）与新闻点名。概念板块内部又有三条只读路径：</p>
<table>
<thead>
<tr>
<th>MCP 工具</th>
<th>HTTP 路径</th>
<th>时间参数</th>
<th>条数</th>
<th>更新</th>
</tr>
</thead>
<tbody><tr>
<td><code>get_market_hot_concepts</code></td>
<td><code>/hot/concepts</code></td>
<td><code>window</code>：<code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code>，默认 <code>24h</code>，没有 <code>7d</code></td>
<td>默认 50，最大 200</td>
<td>约 2 分钟</td>
</tr>
<tr>
<td><code>get_concept_stock_heat</code></td>
<td><code>/concepts/stocks</code></td>
<td><code>days</code>：默认 30，范围 1～365</td>
<td>默认 50，最大 100</td>
<td>约 10 分钟</td>
</tr>
<tr>
<td><code>get_stock_concept_heat</code></td>
<td><code>/concepts/by-stock</code></td>
<td><code>days</code>：默认 90，范围 1～365</td>
<td>默认 30，最大 200</td>
<td>以接口返回为准</td>
</tr>
</tbody></table>
<p><code>/concepts/stocks</code> 的 <code>concept</code> 参数必填，精确匹配，长度不超过 30 字符。下面的坑大多围绕这张表展开。</p>
<h2>坑 1：把「提及排行」当成官方成分表</h2>
<p><strong>现象</strong>：某只常被归入该概念的股票不在返回列表里。<br /><strong>原因</strong>：返回的是消息面里<strong>被提及</strong>的股票，不是交易所或指数公司的成分表。回看期内没被讨论的股票自然不会出现。<br /><strong>处理</strong>：在研究表里把这一列命名为「概念提及成分（<code>days</code>=N）」，不要命名为「概念成分」。需要官方成分时用你自有的数据源。</p>
<h2>坑 2：不写 days 就默认是「最近」</h2>
<p><strong>现象</strong>：同事说「这个概念最近最热的是 A」，你查出来却是 B。<br /><strong>原因</strong>：一方用了默认 <code>days=30</code>，另一方传了 <code>days=7</code>。30 天的排行被月初的一次集中讨论主导，7 天的排行反映的是本周。<br /><strong>处理</strong>：任何引用都带上 <code>days</code>。示例写法：「示例概念甲，<code>days=7</code>，提及第 1 名」。</p>
<h2>坑 3：长回看把旧事件「冻」在榜首</h2>
<p><strong>现象</strong>：<code>days=90</code> 或更长时，榜首长期不变。<br /><strong>原因</strong>：回看越长，单次大事件的累计提及越难被稀释。示例：某股 60 天前一次事件贡献了 300 次提及（示例数字），之后每天只有个位数，在 <code>days=90</code> 下仍可能排第一。<br /><strong>处理</strong>：长回看用于「这个概念长期由谁承载讨论」，短回看用于「最近谁在承载」。要看变化，把 7/30/90 三档并列，而不是只看一档。</p>
<h2>坑 4：短回看的样本太薄</h2>
<p><strong>现象</strong>：<code>days=1</code> 的排行每次刷新都在大幅换人。<br /><strong>原因</strong>：一天内概念下的讨论条数可能很少，第 5 名和第 30 名只差一两次提及，名次对噪声非常敏感。<br /><strong>处理</strong>：短回看只读前几名，并同时看提及数本身；需要日内结构时，改用单股的 <code>/heat/windows</code>（<code>1h</code> 到 <code>7d</code> 六档加 <code>hourly_24h</code>）。</p>
<h2>坑 5：days 与 window 混为一谈</h2>
<p><strong>现象</strong>：把 <code>/hot/concepts?window=3d</code> 的概念热度和 <code>/concepts/stocks?days=3</code> 的成分排行直接对比，觉得数字「对不上」。<br /><strong>原因</strong>：<code>window</code> 是滚动小时窗口，<code>days</code> 是以天计的回看；两者统计对象也不同，一个是概念整体，一个是概念下各股票。<br /><strong>处理</strong>：对账时只做同一接口内的比较。跨接口只比较方向和覆盖面，不比较绝对数。</p>
<h2>坑 6：limit 上限 100 被忽视</h2>
<p><strong>现象</strong>：想取概念下「全部被提及股票」，结果只有 50 条或 100 条。<br /><strong>原因</strong>：<code>limit</code> 默认 50，最大 100，且没有文档化的分页偏移参数。<br /><strong>处理</strong>：若研究定义是「概念下全部提及股票」，就要在表头写清「前 100 名」；不要假设可以翻页。概念特别大时，可以缩短 <code>days</code> 让排行更集中，或从个股侧用 <code>/concepts/by-stock</code> 反查。</p>
<h2>坑 7：在 10 分钟更新周期内反复刷新</h2>
<p><strong>现象</strong>：脚本每分钟拉一次，结果大多相同，费用却在增长。<br /><strong>原因</strong>：该接口约 10 分钟更新一次；成功调用每次按 0.005 元计费。<br /><strong>处理</strong>：轮询间隔至少 10 分钟，用内容哈希判断是否有变化，没变化就不落新快照。示例：一天盘中 4 小时按 10 分钟间隔约 24 次，按 1 分钟间隔是 240 次，后者大部分是重复数据。</p>
<h2>坑 8：概念名不精确导致的「空榜」</h2>
<p><strong>现象</strong>：返回空列表或参数错误。<br /><strong>原因</strong>：<code>concept</code> 精确匹配。「示例概念甲」能查到，「示例概念甲概念」「示例 概念甲」未必能查到；全半角、空格、超过 30 字符都会失配。<br /><strong>处理</strong>：概念名一律从 <code>/hot/concepts</code> 或 <code>/concepts/by-stock</code> 的返回值复制，并在本地保存规范名表。空结果要如实记录为「该参数下无匹配」，不要换近似词反复试。</p>
<h2>坑 9：把不同时刻的两次快照当成同一截面</h2>
<p><strong>现象</strong>：上午与下午各取一次 <code>days=30</code>，名次变化被解读为「讨论转移」。<br /><strong>原因</strong>：回看窗口在滑动，旧的一天移出、新的一天移入；上午与下午之间还经过了若干次数据更新。<br /><strong>处理</strong>：每次快照保存 <code>meta.server_time</code> 与 <code>meta.request_id</code>。比较「成分变化」时，固定每天同一时点取数，并把进入、退出分开统计。</p>
<h2>坑 10：用 days 的回看去做回测</h2>
<p><strong>现象</strong>：拿今天取到的 <code>days=30</code> 排行，当成 30 天前那一天的概念成分。<br /><strong>原因</strong>：接口没有 <code>as_of_date</code>，回看窗口以取数时刻为终点。今天的结果包含了 30 天前那一天之后的讨论。<br /><strong>处理</strong>：历史研究改用日频面板 <code>/panel/daily</code>（<code>get_stock_heat_panel</code>）：<code>type=stock</code> 配 <code>codes</code>（单次最多 20 个）逐股取自然日序列，或 <code>type=concept</code> 配 <code>names</code> 取概念级序列，区间最长 92 天。若需要高分机构/大V 口径的历史截面，用 <code>/hot/weighted</code> 的 <code>as_of_date</code>（消息截至当天 15:00）或 <code>/agencies/pit</code>。</p>
<h2>一个把 7/30/90 并列的取数示例</h2>
<pre><code class="language-python"># concept_days_ladder.py —— 示例，地址与令牌为占位
import os, requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}
CONCEPT = "示例概念甲"   # 必须是从 /hot/concepts 复制的规范名

def members(days):
    r = requests.get(BASE + "/concepts/stocks",
                     params={"concept": CONCEPT, "days": days, "limit": 100},
                     headers=H, timeout=60).json()
    if not r.get("ok"):
        return None, r.get("code")
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    # 成员标识与提及数列名以 OpenAPI 为准
    return [x.get("ts_code") or x.get("stock_name") for x in rows], r["meta"]["server_time"]

ladder = {d: members(d) for d in (7, 30, 90)}
s7, s30, s90 = (set(ladder[d][0] or []) for d in (7, 30, 90))
print("三档共同:", len(s7 &amp; s30 &amp; s90), "仅 7 天:", len(s7 - s30 - s90))
</code></pre>
<p>示例读法：三档共同出现 18 只、仅在 7 天档出现 6 只（示例数字）。前者是该概念讨论的「长期承载者」，后者是「最近才被带进讨论的股票」。这种分层只描述讨论结构，不涉及任何取舍建议。</p>
<h2>坑位速查表</h2>
<table>
<thead>
<tr>
<th>编号</th>
<th>坑</th>
<th>一句话处理</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>提及排行当成官方成分</td>
<td>列名写「概念提及成分（<code>days</code>=N）」</td>
</tr>
<tr>
<td>2</td>
<td>不写 <code>days</code></td>
<td>引用时必带 <code>days</code></td>
</tr>
<tr>
<td>3</td>
<td>长回看冻住旧事件</td>
<td>7/30/90 三档并列</td>
</tr>
<tr>
<td>4</td>
<td>短回看样本薄</td>
<td>只读前几名，同时看提及数</td>
</tr>
<tr>
<td>5</td>
<td><code>days</code> 与 <code>window</code> 混用</td>
<td>只在同一接口内对账</td>
</tr>
<tr>
<td>6</td>
<td><code>limit</code> 上限 100</td>
<td>表头写「前 100 名」，不假设翻页</td>
</tr>
<tr>
<td>7</td>
<td>10 分钟内反复刷新</td>
<td>间隔 ≥10 分钟 + 内容哈希</td>
</tr>
<tr>
<td>8</td>
<td>概念名不精确</td>
<td>从热榜或个股关联概念复制规范名</td>
</tr>
<tr>
<td>9</td>
<td>不同时刻快照混比</td>
<td>固定时点 + 保存 <code>server_time</code></td>
</tr>
<tr>
<td>10</td>
<td>用回看做回测</td>
<td>改用 <code>/panel/daily</code> 或 PIT 接口</td>
</tr>
</tbody></table>
<h2>与个股反查配合：双向核对</h2>
<p>概念 → 个股（<code>/concepts/stocks</code>）与个股 → 概念（<code>/concepts/by-stock</code>）是同一关系的两个方向，但默认回看不同：前者 <code>days</code> 默认 30，后者默认 90。双向核对时如果不统一 <code>days</code>，就会出现「概念榜里有这只股票，反查这只股票却看不到该概念排在前面」的假矛盾。</p>
<p>建议的核对步骤（示例）：</p>
<ol>
<li>对「示例概念甲」调用 <code>get_concept_stock_heat(concept="示例概念甲", days=30, limit=100)</code>，取前 10 名代码；</li>
<li>对这 10 只逐一调用 <code>get_stock_concept_heat(ts_code=&lt;代码&gt;, days=30)</code>，注意显式传 <code>days=30</code>，不要用默认 90；</li>
<li>检查「示例概念甲」在每只股票的关联概念里排第几；排名靠后说明该股的讨论主要由其他概念承载，示例概念甲只是它的次要标签；</li>
<li>把两边的 <code>meta.server_time</code> 都记下，两者更新节奏不同，时间差过大时重新取数。</li>
</ol>
<p>这一步共 11 次调用，按每次 0.005 元计约 0.055 元（示例估算）。概念板块与个股热度两块数据由此连成一张可解释的关系表。</p>
<h2>FAQ</h2>
<p><strong>Q：<code>/concepts/stocks</code> 支持 <code>min_score</code> 吗？</strong><br />A：是否支持以 OpenAPI 为准。需要高分机构/大V 口径时，可以在概念热榜 <code>/hot/concepts</code> 上加 <code>min_score</code> 观察，或对成分股逐只查看 <code>/heat/windows</code>（高分线默认 1050）。</p>
<p><strong>Q：概念热榜没有 <code>7d</code>，周度怎么看？</strong><br />A：用 <code>/concepts/stocks</code> 的 <code>days=7</code> 看概念下个股的周度提及，或用 <code>/panel/daily</code> 且 <code>type=concept</code> 看概念级日频序列，不要给 <code>/hot/concepts</code> 传未列出的窗口值。</p>
<p><strong>Q：同一只股票在两个概念里排名都很高，怎么解释？</strong><br />A：一条讨论可能同时涉及多个概念，股票也可能关联多个概念。用 <code>/concepts/by-stock</code> 查看它关联的全部概念及提及，比在两个概念榜之间来回比较更清楚。</p>
<p><strong>Q：<code>days</code> 直接设成 365 有意义吗？</strong><br />A：范围允许到 365，适合回答「一年里这个概念主要由哪些股票承载讨论」。但它几乎不反映近期变化，且受 <code>limit</code> 最大 100 限制，长尾会被截掉。建议把 365 档当作背景参照，与 7 天、30 天档一起读，而不是单独使用。</p>
<h2>结论</h2>
<p><code>get_concept_stock_heat</code> 是一张「概念下谁被讨论」的提及排行，读数高度依赖 <code>days</code>。记住几条口径：默认 30、范围 1～365；<code>limit</code> 最大 100；约 10 分钟更新；概念名精确匹配且不超过 30 字符；没有 <code>as_of_date</code>，不能直接拿来回测。把 7/30/90 三档并列、每次快照带上参数与时间，就能避开本文列出的大多数坑。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/tools/concept-stocks">https://lava.ai-dolphin.com/tools/concept-stocks</a></p>
]]></content:encoded></item><item><title><![CDATA[熔岩热度加速榜三种排序对照：order=chg1d、chg3d、z20 与 min_mentions 的组合表]]></title><description><![CDATA[熔岩（Lava）热度加速榜 GET /hot/accelerating（MCP 工具 get_accelerating_hot_stocks）在全市场热榜候选上计算动量统计，再按变化降序排列。同一张榜，order 取 chg1d、chg3d 还是…]]></description><link>https://dustinliang.hashnode.dev/accelerating-order-modes-compare</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/accelerating-order-modes-compare</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Fri, 13 Nov 2026 01:14:05 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）热度加速榜 <code>GET /hot/accelerating</code>（MCP 工具 <code>get_accelerating_hot_stocks</code>）在全市场热榜候选上计算动量统计，再按变化降序排列。同一张榜，<code>order</code> 取 <code>chg1d</code>、<code>chg3d</code> 还是 <code>z20</code>，排在前面的往往是完全不同的一批股票；<code>min_mentions</code> 与 <code>window</code> 又会进一步改变候选池。本文不讲概念入门，而是用多张对照表把「参数组合 → 回答的问题 → 典型偏差 → 该配合看哪个字段」逐项摆出来，方便量化脚本和 AI Agent 直接按表选参数。文中数字均为示例，三列字段都是纯统计量，不是交易信号。</p>
<h2>一、参数总表</h2>
<table>
<thead>
<tr>
<th>参数</th>
<th>取值</th>
<th>默认</th>
<th>作用</th>
</tr>
</thead>
<tbody><tr>
<td><code>window</code></td>
<td><code>1h</code> / <code>4h</code> / <code>18h</code> / <code>24h</code> / <code>3d</code> / <code>7d</code></td>
<td><code>24h</code></td>
<td>「当前热度」统计多长时间</td>
</tr>
<tr>
<td><code>order</code></td>
<td><code>chg1d</code> / <code>chg3d</code> / <code>z20</code></td>
<td><code>chg1d</code></td>
<td>按哪一列动量降序</td>
</tr>
<tr>
<td><code>min_mentions</code></td>
<td>整数</td>
<td>3</td>
<td>当前窗最少提及次数，过滤低基数</td>
</tr>
<tr>
<td><code>market</code></td>
<td><code>ashare</code> / <code>all</code></td>
<td><code>ashare</code></td>
<td>A 股按代码合并；<code>all</code> 含港美股等，按简称合并</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>1～200</td>
<td>50</td>
<td>返回条数，无分页偏移</td>
</tr>
</tbody></table>
<p>返回列表中的每一行都带 <code>heat_chg_1d</code>、<code>heat_chg_3d</code>、<code>heat_z20</code>。普通热榜 <code>/hot/stocks</code>（<code>get_market_hot_stocks</code>）也附带这三列，区别在于普通热榜按热度本身排序，加速榜按变化排序。三列的精确计算公式与基准（比率还是差值、滚动还是自然日）以 OpenAPI 为准；<code>heat_z20</code> 的公开描述是「相对近 20 日日均热度的 z 分」。</p>
<h2>二、三种 order 的对照</h2>
<table>
<thead>
<tr>
<th>维度</th>
<th><code>order=chg1d</code></th>
<th><code>order=chg3d</code></th>
<th><code>order=z20</code></th>
</tr>
</thead>
<tbody><tr>
<td>回答的问题</td>
<td>和上一阶段比，谁的热度变化最大</td>
<td>和更早几天比，谁的变化最大</td>
<td>相对自身近 20 日日均，谁偏离最远</td>
</tr>
<tr>
<td>时间敏感度</td>
<td>最高，单日事件即可拉动</td>
<td>中等，单日偶发被摊薄</td>
<td>取决于自身历史波动</td>
</tr>
<tr>
<td>低基数放大</td>
<td>明显：1 次到 4 次即是数倍</td>
<td>仍有，但略缓</td>
<td>最明显：历史波动小则分母小</td>
</tr>
<tr>
<td>大票表现</td>
<td>绝对变化大但比率常不突出</td>
<td>同左</td>
<td>平常讨论量高、波动大，z 分常居中</td>
</tr>
<tr>
<td>适合的研究问题</td>
<td>盘中异动巡检</td>
<td>多日持续升温筛查</td>
<td>「相对自己不寻常」的事件标记</td>
</tr>
<tr>
<td>建议同时读的字段</td>
<td><code>heat_chg_3d</code>，看是否只是一日脉冲</td>
<td><code>heat_chg_1d</code>，看最近是否仍在延续</td>
<td><code>mentions</code> 绝对值，排除低基数</td>
</tr>
</tbody></table>
<p>一句话总结：<code>chg1d</code> 偏「快」，<code>chg3d</code> 偏「稳」，<code>z20</code> 偏「相对异常」。三者不存在谁更准确的问题，只有和研究问题是否匹配。</p>
<h2>三、min_mentions 的取值对照</h2>
<p><code>min_mentions</code> 是控制低基数噪声最直接的开关。默认 3 很宽松，对 <code>z20</code> 排序尤其不够。</p>
<table>
<thead>
<tr>
<th><code>min_mentions</code>（示例取值）</th>
<th>候选池规模（示例）</th>
<th><code>z20</code> 榜前列的典型样貌</th>
<th>适用场景</th>
</tr>
</thead>
<tbody><tr>
<td>3（默认）</td>
<td>大</td>
<td>冷门股从 0～1 次跳到 3～5 次，z 分极高</td>
<td>首提、冷门异动的宽口径观察</td>
</tr>
<tr>
<td>10</td>
<td>中</td>
<td>低基数样本明显减少，中等讨论量股票浮现</td>
<td>日常巡检</td>
</tr>
<tr>
<td>30</td>
<td>小</td>
<td>以讨论量已较大的股票为主</td>
<td>关注主流讨论中的相对变化</td>
</tr>
</tbody></table>
<p>示例：同一时刻、<code>window=24h</code>、<code>order=z20</code>，<code>min_mentions=3</code> 时榜首的 <code>mentions=4</code>、<code>heat_z20=8.1</code>；改为 <code>min_mentions=10</code> 后，榜首变为 <code>mentions=37</code>、<code>heat_z20=4.6</code>（均为示例数字）。前者是「从几乎无人讨论到有人讨论」，后者是「本来就有讨论、今天明显更多」，两类事件含义不同，不应混在一张结论里。</p>
<h2>四、window 与 order 的组合矩阵</h2>
<table>
<thead>
<tr>
<th><code>window</code> \ <code>order</code></th>
<th><code>chg1d</code></th>
<th><code>chg3d</code></th>
<th><code>z20</code></th>
</tr>
</thead>
<tbody><tr>
<td><code>1h</code> / <code>4h</code></td>
<td>盘中脉冲，噪声最大</td>
<td>短窗对比长基准，读数易失真</td>
<td>短窗热度对 20 日日均，需高 <code>min_mentions</code></td>
</tr>
<tr>
<td><code>18h</code> / <code>24h</code></td>
<td>默认组合，日内到隔夜的变化</td>
<td>一日窗对多日基准，常用</td>
<td>常用的「今日相对自身」读法</td>
</tr>
<tr>
<td><code>3d</code></td>
<td>多日累积对比</td>
<td>与窗口长度接近，变化被平滑</td>
<td>中期偏离</td>
</tr>
<tr>
<td><code>7d</code></td>
<td>周度累积，适合周报</td>
<td>周窗对三日基准，解释需谨慎</td>
<td>周度偏离</td>
</tr>
</tbody></table>
<p>两条规则：引用任何一个动量数字时都要带上 <code>window</code>，例如「24h 窗口 <code>heat_z20</code>」；跨窗口比较排名没有意义，因为候选池和当前热度的统计口径都变了。</p>
<h2>五、加速榜与其他榜单的分工</h2>
<table>
<thead>
<tr>
<th>熔岩数据板块 / 接口</th>
<th>MCP 工具</th>
<th>回答的问题</th>
<th>与加速榜的关系</th>
</tr>
</thead>
<tbody><tr>
<td>全市场热度 <code>/hot/stocks</code></td>
<td><code>get_market_hot_stocks</code></td>
<td>现在谁最热</td>
<td>加速榜的候选来源，也带三列动量</td>
</tr>
<tr>
<td>加速榜 <code>/hot/accelerating</code></td>
<td><code>get_accelerating_hot_stocks</code></td>
<td>谁的热度变化最大</td>
<td>本文主角</td>
</tr>
<tr>
<td>首提流 <code>/hot/first-mentions</code></td>
<td><code>get_first_mention_stocks</code></td>
<td>此前 7 天无提及、窗口内首次被提到</td>
<td>低 <code>min_mentions</code> 的 <code>z20</code> 榜常与它重叠</td>
</tr>
<tr>
<td>加权热榜 <code>/hot/weighted</code></td>
<td><code>get_weighted_hot_stocks</code></td>
<td>高分机构/大V 按分数加权后谁靠前</td>
<td>换一个维度看「谁在讨论」</td>
</tr>
<tr>
<td>单股多窗 <code>/heat/windows</code></td>
<td><code>get_stock_heat_windows</code></td>
<td>某只股票六档窗口与逐小时走势</td>
<td>加速榜上榜后的下钻</td>
</tr>
<tr>
<td>日频面板 <code>/panel/daily</code></td>
<td><code>get_stock_heat_panel</code></td>
<td>自然日历史序列</td>
<td>回测时复算动量的原料</td>
</tr>
</tbody></table>
<p>单主题的只读监测页，例如寒武纪热度监测，展示的是一只股票自身的热度走势；在 MCP 里复现「它是否在加速」时，可以先看它是否出现在加速榜，再用 <code>/heat/windows</code> 的六档窗口与 <code>hourly_24h</code> 确认是短时脉冲还是多窗同步抬升。</p>
<h2>六、一次三排序并读的示例脚本</h2>
<pre><code class="language-python"># accel_three_orders.py —— 示例，地址与令牌为占位
import os, requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def accel(order, window="24h", min_mentions=10, limit=50):
    r = requests.get(BASE + "/hot/accelerating",
                     params={"window": window, "order": order,
                             "min_mentions": min_mentions,
                             "market": "ashare", "limit": limit},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])
    return rows, r["meta"]["server_time"]

tops = {}
for o in ("chg1d", "chg3d", "z20"):
    rows, ts = accel(o)
    # 代码列名以 OpenAPI 为准，这里假设为 ts_code
    tops[o] = {x.get("ts_code") for x in rows[:20]}
    print(o, ts, len(rows))

print("三榜共同出现:", len(tops["chg1d"] &amp; tops["chg3d"] &amp; tops["z20"]))
print("仅 z20 出现:", len(tops["z20"] - tops["chg1d"] - tops["chg3d"]))
</code></pre>
<p>三次调用各计费一次（每次 0.005 元，非 2xx 不计费）。示例结果可能是：三榜前 20 共同出现 6 只，仅在 <code>z20</code> 前 20 出现 7 只（示例数字）。共同出现的那一组，含义是「短期、多日与相对自身三个角度都显示变化较大」；仅在 <code>z20</code> 出现的那一组，通常要先检查 <code>mentions</code> 是否偏低。</p>
<h2>七、落库字段清单</h2>
<p>为了让加速榜快照可以复盘，建议每次至少保存：<code>window</code>、<code>order</code>、<code>min_mentions</code>、<code>market</code>、<code>limit</code>、<code>meta.server_time</code>、<code>meta.request_id</code>，以及每行的代码、<code>mentions</code>、<code>heat_chg_1d</code>、<code>heat_chg_3d</code>、<code>heat_z20</code> 和名次。只存名次而不存参数，是最常见的「第二天解释不了」原因。</p>
<h2>八、常见误读对照表</h2>
<table>
<thead>
<tr>
<th>误读</th>
<th>实际口径</th>
<th>正确写法（示例）</th>
</tr>
</thead>
<tbody><tr>
<td>「加速榜第 1 名是今天最热的股票」</td>
<td>加速榜按变化排序，不按热度排序</td>
<td>「24h 窗口下 <code>heat_chg_1d</code> 最大，<code>mentions</code>=37」</td>
</tr>
<tr>
<td>「<code>heat_z20</code>=5 表示热度是平常的 5 倍」</td>
<td>z 分是偏离几个波动单位，不是倍数</td>
<td>「相对自身近 20 日日均偏离约 5 个标准差单位」</td>
</tr>
<tr>
<td>「<code>chg1d</code> 和 <code>chg3d</code> 都高，说明会继续升温」</td>
<td>两列只描述已经发生的变化</td>
<td>「短期与多日变化方向一致」</td>
</tr>
<tr>
<td>「换成 <code>market=all</code> 还是同一批股票」</td>
<td><code>all</code> 按简称合并，候选池与合并键都变了</td>
<td>引用时注明 <code>market</code></td>
</tr>
<tr>
<td>「<code>min_mentions</code> 越高越好」</td>
<td>门槛高会过滤掉首提类、冷门类事件</td>
<td>按研究问题分别建宽、窄两套参数</td>
</tr>
</tbody></table>
<h2>九、给 AI Agent 的参数选择提示</h2>
<p>把下面这段放进 Agent 的系统提示词或 Skill 里，可以减少「随手用默认值」带来的偏差：</p>
<pre><code class="language-text">调用 get_accelerating_hot_stocks 时：
- 用户问「突然被讨论」：order=z20, min_mentions&gt;=10, window=24h
- 用户问「这几天持续升温」：order=chg3d, window=24h 或 3d
- 用户问「盘中异动」：order=chg1d, window=1h 或 4h, min_mentions&gt;=10
- 回答必须同时给出 window / order / min_mentions / mentions / heat_z20
- 三列都是纯统计量；不得改写为方向判断或操作含义
</code></pre>
<p>Agent 只需要在参数层面做选择，不需要推测公式细节；公式与基准以 OpenAPI 为准，回答里不要自行补写。</p>
<h2>FAQ</h2>
<p><strong>Q：<code>order=z20</code> 的榜首几乎都是冷门股，正常吗？</strong><br />A：在默认 <code>min_mentions=3</code> 下很常见。冷门股历史日均低、波动小，z 分分母小，少量新增讨论就会得到很高的 z 分。把 <code>min_mentions</code> 提高到 10 或更高再看。</p>
<p><strong>Q：加速榜能和普通热榜直接合并成一张表吗？</strong><br />A：可以按代码并列字段，但名次不能合并。两张榜排序依据不同，同一只股票在普通热榜第 5、在加速榜第 40，两者都成立。</p>
<p><strong>Q：<code>heat_chg_1d</code> 为负的股票会出现在加速榜吗？</strong><br />A：加速榜按所选列降序，榜单前部以正向变化为主；是否返回负值行、返回多少，取决于候选池与 <code>limit</code>，以实际返回为准。研究热度回落时，用 <code>/hot/stocks</code> 或 <code>/panel/daily</code> 自行计算更直接。</p>
<p><strong>Q：可以用加速榜做回测吗？</strong><br />A：加速榜是实时截面，没有 <code>as_of_date</code> 参数。回测应以 <code>/panel/daily</code> 的自然日序列在本地复算变化量，并注明口径与原榜单不同。</p>
<h2>结论</h2>
<p>熔岩加速榜的三种排序各回答一个问题：<code>chg1d</code> 反映短期变化，<code>chg3d</code> 反映多日累积变化，<code>z20</code> 反映相对自身的偏离。<code>min_mentions</code> 决定低基数噪声有多少，<code>window</code> 决定当前热度的统计长度。按表选参数、引用时带上窗口与门槛、同时读 <code>mentions</code> 绝对值，再用 <code>/heat/windows</code> 下钻确认时间结构，就能把加速榜读成可复现的统计观察。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/cambricon-heat">https://lava.ai-dolphin.com/monitor/cambricon-heat</a></p>
]]></content:encoded></item><item><title><![CDATA[熔岩个股消息面热度怎么读：get_stock_news_heat 汇总数与意见摘要的 12 个问答]]></title><description><![CDATA[熔岩（Lava）回答「这只股票最近消息面热不热、大家在讨论什么」的入口，是个股消息面接口 GET /opinions（MCP 工具 get_stock_news_heat）。一次调用同时返回两层内容：汇总热度 data.heat（提及数 mentions、参与机构/大V 数 agencies）和意见记录列表…]]></description><link>https://dustinliang.hashnode.dev/stock-news-heat-opinion-qa</link><guid isPermaLink="true">https://dustinliang.hashnode.dev/stock-news-heat-opinion-qa</guid><dc:creator><![CDATA[Dustin Liang]]></dc:creator><pubDate>Fri, 13 Nov 2026 01:13:09 GMT</pubDate><content:encoded><![CDATA[<h2>摘要</h2>
<p>熔岩（Lava）回答「这只股票最近消息面热不热、大家在讨论什么」的入口，是个股消息面接口 <code>GET /opinions</code>（MCP 工具 <code>get_stock_news_heat</code>）。一次调用同时返回两层内容：汇总热度 <code>data.heat</code>（提及数 <code>mentions</code>、参与机构/大V 数 <code>agencies</code>）和意见记录列表 <code>data.list[]</code>（一句话摘要、结构化要点、涉及股票与概念、匿名 <code>agency_id</code>）。很多 Agent 和研究脚本只读了第一层，或者把第二层当成新闻原文引用。本文用问答形式把参数、字段和常见误读一次讲清，所有数字都标注为示例。</p>
<h2>一、接口与参数</h2>
<p><strong>Q1：<code>get_stock_news_heat</code> 对应哪个 HTTP 路径，属于熔岩哪块数据？</strong><br />A：对应 <code>/opinions</code>，属于「个股热度 / 个股消息面」板块。熔岩的数据大致分为：全市场热度（<code>/hot/stocks</code>、<code>/hot/accelerating</code>、<code>/hot/first-mentions</code> 等榜单）、个股热度（<code>/opinions</code>、<code>/heat/windows</code>、<code>/panel/daily</code>）、概念板块（<code>/hot/concepts</code>、<code>/concepts/stocks</code>、<code>/concepts/by-stock</code>）、机构/大V 评分（<code>/agencies/ranking</code>、<code>/agencies/pit</code> 等，含 PIT 截面），以及新闻点名 <code>/news/hits</code>。<code>/opinions</code> 是其中唯一同时给出「数字 + 文字要点」的个股接口。</p>
<p><strong>Q2：参数怎么填？</strong><br />A：按 OpenAPI，常用参数如下：</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>默认</th>
<th>约束</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td><code>stock_name</code> / <code>ts_code</code></td>
<td>—</td>
<td>二选一</td>
<td>简称或代码，代码形态如 <code>300750.SZ</code>（仅示意格式）</td>
</tr>
<tr>
<td><code>days</code></td>
<td>10</td>
<td>1～90</td>
<td>未给 <code>start_date</code> 时生效</td>
</tr>
<tr>
<td><code>start_date</code> / <code>end_date</code></td>
<td><code>end_date</code> 默认今天</td>
<td>区间 ≤ 92 天</td>
<td>显式区间</td>
</tr>
<tr>
<td><code>only_this_stock</code></td>
<td>0</td>
<td>0 / 1</td>
<td>1 = 只要仅涉及该股的记录；0 = 该股为第一提及股的记录</td>
</tr>
<tr>
<td><code>limit</code></td>
<td>50</td>
<td>1～200</td>
<td>列表条数</td>
</tr>
</tbody></table>
<p>官方 Skill 模板里的示范写法是 <code>get_stock_news_heat(stock_name 或 ts_code, days=7)</code>，即看最近一周。</p>
<p><strong>Q3：<code>stock_name</code> 和 <code>ts_code</code> 用哪个更稳？</strong><br />A：研究脚本里优先 <code>ts_code</code>。简称存在改名、重名与全半角差异，代码更适合做主键；面向人的对话里可以用简称，但落库时应回填代码。</p>
<h2>二、汇总热度 <code>data.heat</code></h2>
<p><strong>Q4：<code>data.heat.mentions</code> 和 <code>data.heat.agencies</code> 分别是什么？</strong><br />A：<code>mentions</code> 是所选区间内该股被提到的次数，<code>agencies</code> 是参与提及的机构/大V 数量。前者衡量讨论量，后者衡量讨论的广度。示例：<code>days=7</code> 时 <code>mentions=46</code>、<code>agencies=19</code>（示例数字），可读为「一周内 46 次提及，来自 19 个不同主体」。</p>
<p><strong>Q5：汇总数和列表条数为什么对不上？</strong><br />A：三个常见原因。第一，<code>limit</code> 默认 50，列表被截断，而汇总数统计的是整个区间；第二，<code>only_this_stock</code> 改变了列表样本；第三，部分记录的文本字段可能为空，展示层过滤后看起来「少了」。<code>only_this_stock</code> 是否同时影响汇总数，以 OpenAPI 说明为准，对账前先用同一参数各调一次确认。</p>
<p><strong>Q6：「热度高」有统一阈值吗？</strong><br />A：没有官方阈值。同一 <code>mentions</code> 对大市值热门股与冷门股含义不同。需要「相对自身」的读法时，改看 <code>/heat/windows</code>（<code>get_stock_heat_windows</code>）或榜单里附带的 <code>heat_z20</code>（相对近 20 日日均的 z 分）、<code>heat_chg_1d</code>、<code>heat_chg_3d</code>，它们都是纯统计量。</p>
<h2>三、意见列表 <code>data.list[]</code></h2>
<p><strong>Q7：每条意见记录有哪些字段？</strong><br />A：按 Opinion 结构：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>口径</th>
<th>读法提示</th>
</tr>
</thead>
<tbody><tr>
<td><code>opinion_id</code></td>
<td>记录 ID</td>
<td>去重主键</td>
</tr>
<tr>
<td><code>topic_time</code></td>
<td>消息时间，北京时间</td>
<td>字符串本身不带偏移，入库补 <code>Asia/Shanghai</code></td>
</tr>
<tr>
<td><code>agency_id</code></td>
<td>匿名主体编号，跨接口稳定</td>
<td>只作键，不还原真名；可为空</td>
</tr>
<tr>
<td><code>agency_score</code></td>
<td>该主体<strong>当前</strong>机构/大V 分，约 450～1500</td>
<td>不是当时分，回测改用 <code>/agencies/pit</code></td>
</tr>
<tr>
<td><code>stocks[]</code> / <code>concepts[]</code></td>
<td>涉及的股票与概念</td>
<td>概念名与 <code>/hot/concepts</code> 是否逐字一致，入库前抽样核对</td>
</tr>
<tr>
<td><code>summary</code></td>
<td>一句话摘要，≤120 字，可空</td>
<td>适合列表展示</td>
</tr>
<tr>
<td><code>detail</code></td>
<td>结构化要点，≤600 字，可空</td>
<td>适合关键词检索，不是原文</td>
</tr>
</tbody></table>
<p><strong>Q8：<code>summary</code> 能当新闻标题或原文引用吗？</strong><br />A：不能。它是结构化摘要，不是任何媒体的标题，也不是原文。若需要「哪家媒体在何时点名了这只股票」，应调用 <code>/news/hits</code>（<code>get_news_stock_hits</code>），该接口给出 <code>news_id</code>、发布时间、媒体与命中股票，同样不含标题和正文。</p>
<p><strong>Q9：<code>agency_score</code> 高的记录更值得看吗？</strong><br />A：分数描述的是主体的历史评分，不是这条记录的质量判断。约 1000 为中等、1050 约前 10%。可以把它当作筛选或分层的一个维度，但要记住它是当前分：同一主体三个月前未必在同一档。</p>
<h2>四、组合使用与常见误读</h2>
<p><strong>Q10：<code>/opinions</code> 和关键词检索 <code>/opinions/search</code> 怎么分工？</strong><br />A：<code>/opinions</code> 以股票为入口；<code>/opinions/search</code>（<code>search_news_flow</code>）以关键词为入口，在要点里搜，<code>keyword</code> ≤50 字符、最多 5 个词、空格分隔且全部包含才命中，<code>since</code> 默认 30 天前、最早 180 天前。想追一个事件在多只股票间的扩散，用后者；想看单只股票的讨论全貌，用前者。</p>
<p><strong>Q11：一个可复用的读法顺序是什么？</strong><br />A：示例顺序如下，全部只读：</p>
<pre><code class="language-text">1. get_stock_news_heat(ts_code=&lt;代码&gt;, days=7, only_this_stock=0, limit=50)
   -&gt; 记 data.heat.mentions / data.heat.agencies / meta.server_time
2. 读 list[] 前若干条的 summary，按 topic_time 倒序归纳 3~5 条要点
3. 需要时间结构：get_stock_heat_windows(ts_code=&lt;代码&gt;)
   -&gt; 1h/4h/18h/24h/3d/7d 六档 + hourly_24h
4. 需要概念背景：get_stock_concept_heat(ts_code=&lt;代码&gt;, days=30)
5. 需要媒体点名：get_news_stock_hits(stock_name=&lt;简称&gt;)
</code></pre>
<p>输出时先给结论（热度高/中/低 + 提及数 + 参与主体数 + 时间窗），再给摘要要点，主体一律用 <code>agency_id</code> 指代。</p>
<p><strong>Q12：Agent 最容易犯的错是什么？</strong><br />A：示例中最常见的四类：</p>
<ul>
<li>把 <code>mentions</code> 的绝对值当成跨股票可比的「热度分」；</li>
<li>把 <code>summary</code> 改写成带方向的判断句，或补写记录里没有的内容；</li>
<li>用 <code>agency_score</code> 当前分回填历史，或猜测 <code>agency_id</code> 背后的机构名称；</li>
<li>只给数字不给时间窗，读者无法知道是 7 天还是 10 天的统计。</li>
</ul>
<h2>五、与只读监测页的对应关系</h2>
<p>熔岩的部分只读监测页，本质上是把同一类字段做成固定视图。例如宁德时代情绪监测页对应的就是「单只股票的消息面汇总 + 意见要点」这一组观察；在 MCP 里复现时，用 <code>get_stock_news_heat</code> 取汇总与摘要，用 <code>get_stock_heat_windows</code> 补时间结构，再记录参数与 <code>meta.server_time</code>。页面适合即时浏览，接口适合复盘与入库，两者口径一致但用途不同。</p>
<p>示例笔记格式：</p>
<pre><code class="language-text">[示例] 2026-11-13 09:30 (Asia/Shanghai)
标的: &lt;代码&gt;   接口: /opinions   days=7  only_this_stock=0
heat: mentions=46 agencies=19   (示例数字)
要点: 1) ... 2) ... 3) ...      (摘自 summary，未改写方向)
主体: AG... 等匿名编号
</code></pre>
<h2>六、only_this_stock 两种取值的对照实验</h2>
<p><code>only_this_stock</code> 决定意见列表的样本边界，值得单独做一次对照。取值 <code>0</code>（默认）时，返回「该股为第一提及股」的记录；取值 <code>1</code> 时，只返回「仅涉及该股」的记录。前者覆盖更广，后者文本更聚焦。一个最小实验是在同一时刻、同一 <code>days</code> 下各调一次，比较三组数字：</p>
<table>
<thead>
<tr>
<th>对照项</th>
<th><code>only_this_stock=0</code>（示例）</th>
<th><code>only_this_stock=1</code>（示例）</th>
<th>读法</th>
</tr>
</thead>
<tbody><tr>
<td>列表条数</td>
<td>50（被 <code>limit</code> 截断）</td>
<td>21</td>
<td>1 的样本是 0 的子集式收窄，具体关系以 OpenAPI 为准</td>
</tr>
<tr>
<td>列表内 <code>stocks[]</code> 平均长度</td>
<td>2.6</td>
<td>1.0</td>
<td>0 的记录常同时涉及多只股票</td>
</tr>
<tr>
<td>列表内去重 <code>agency_id</code> 数</td>
<td>24</td>
<td>12</td>
<td>聚焦后参与主体也会减少</td>
</tr>
</tbody></table>
<pre><code class="language-python"># only_this_stock_ab.py —— 示例，地址与令牌为占位
import os, requests

BASE = os.environ.get("LAVA_BASE", "&lt;YOUR_LAVA_BASE&gt;/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "&lt;YOUR_TOKEN&gt;")}

def pull(code, flag, days=7):
    r = requests.get(BASE + "/opinions",
                     params={"ts_code": code, "days": days,
                             "only_this_stock": flag, "limit": 200},
                     headers=H, timeout=60).json()
    assert r["ok"], r["code"]
    d = r["data"]
    rows = d.get("list", [])
    return {"heat": d.get("heat"),
            "n": len(rows),
            "agencies": len({x.get("agency_id") for x in rows if x.get("agency_id")}),
            "server_time": r["meta"]["server_time"]}

print(pull("000000.SZ", 0))   # 示例代码
print(pull("000000.SZ", 1))
</code></pre>
<p>做 NLP 或要点归纳时，<code>1</code> 的文本更干净；做讨论广度统计时，<code>0</code> 更完整。无论选哪个，都应把取值写进笔记，否则同一只股票两天的列表条数无法比较。</p>
<h2>七、时间区间：days 与 start_date / end_date</h2>
<p><code>days</code> 适合「最近 N 天」的滚动观察，<code>start_date</code> / <code>end_date</code> 适合事件复盘。两者不要同时随意混用：给了 <code>start_date</code> 后，<code>days</code> 不再生效。区间上限 92 天，超过会返回参数错误（非 2xx 不计费）。需要更长历史时，汇总数字改走日频面板 <code>/panel/daily</code>（<code>get_stock_heat_panel</code>，<code>from</code>/<code>to</code> 自然日、单次最多 20 个代码），意见文本则按 92 天分段拉取后以 <code>opinion_id</code> 去重合并。</p>
<p>示例：复盘某次公告前后各 10 天的讨论，可传 <code>start_date=2026-10-20</code>、<code>end_date=2026-11-08</code>（示例日期），再用 <code>topic_time</code> 把记录切成「公告前 / 公告后」两段。<code>topic_time</code> 是北京时间，切分点也应使用 Asia/Shanghai。</p>
<h2>FAQ</h2>
<p><strong>Q：<code>data.heat</code> 为 0 且列表为空，是接口出错了吗？</strong><br />A：不是。<code>ok=true</code> 且 <code>data.heat.mentions</code> 为 0、<code>list</code> 为空，表示在该参数下没有记录，应如实写成「该区间无消息面记录」，不要换参数反复重试来凑结果。</p>
<p><strong>Q：一次调用会计费几次？</strong><br />A：每次成功调用计 1 次，按文档单价每次 0.005 元；非 2xx 与查余额 <code>get_account</code> 不计费。读完汇总再读列表不需要额外调用。</p>
<p><strong>Q：可以把多只股票的 <code>/opinions</code> 结果拼成全市场热榜吗？</strong><br />A：不建议。全市场截面请直接用 <code>/hot/stocks</code>（<code>get_market_hot_stocks</code>），它有统一的 <code>window</code>、<code>market</code> 与 <code>min_score</code> 口径，自己拼的榜单很难保证时点一致。</p>
<h2>结论</h2>
<p>读 <code>get_stock_news_heat</code> 要分两层：<code>data.heat</code> 给出区间内的提及数与参与主体数，回答「热不热、多广」；<code>data.list[]</code> 给出带时间与匿名主体编号的摘要和要点，回答「在讨论什么」。参数上记住 <code>days</code> 默认 10、区间不超过 92 天、<code>only_this_stock</code> 默认 0、<code>limit</code> 默认 50；字段上记住 <code>summary</code> ≤120 字、<code>detail</code> ≤600 字、<code>agency_score</code> 是当前分。把这些口径写进笔记和 Agent 提示词，消息面热度就能被准确复述，而不会被改写成别的东西。</p>
<p>数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。</p>
<p>只读监测与字段说明见：<a href="https://lava.ai-dolphin.com/monitor/catl-sentiment">https://lava.ai-dolphin.com/monitor/catl-sentiment</a></p>
]]></content:encoded></item></channel></rss>