# 情绪因子量化怎么做：用熔岩消息面热度构建 A 股情绪因子的关注度分量


## 摘要

情绪因子量化怎么做？业界常见的新闻情绪因子大致由四个部分组成：个股与文本的关联、每条文本的情感方向、相关度加权与时间衰减，以及按交易日的聚合。其中「这只股票被讨论了多少、被谁讨论」是关注度分量，「讨论偏正面还是偏负面」是方向分量。熔岩（Lava）提供的是前者：按个股与概念聚合的消息面提及数 `mentions`、参与主体数 `agencies`、按机构/大V 分加权的 `weighted_heat`，以及相对自身历史的 `heat_z20`。

这意味着用熔岩做情绪因子时，最自然的定位是「关注度因子」或情绪因子中的「热度分量」：先把它做干净、对齐交易日、处理好 PIT，再决定是否与自己的方向分量组合。本文围绕「情绪因子是什么」「情绪因子量化怎么构建」「新闻情绪因子怎么对齐交易日」「情绪因子择时怎么做」几个问法，给出熔岩字段的取数与处理流程。文中数字与阈值均为示例，评估方法由研究者自己的框架决定。

## 一、情绪因子是什么：拆成方向与关注度

| 分量 | 回答的问题 | 典型做法 | 熔岩是否提供 |
| --- | --- | --- | --- |
| 关联 | 这条文本涉及哪些股票 | 实体识别、代码映射 | 是：每条记录带 `stocks`、`concepts` |
| 关注度 | 被讨论了多少、由多少主体讨论 | 计数、去重计数、加权计数 | 是：`mentions`、`agencies`、`weighted_heat` |
| 方向 | 讨论偏正面还是偏负面 | 情感分类模型 | 否 |
| 时间 | 讨论何时发生、如何对齐交易日 | 截止时点、衰减 | 是：`topic_time`、日频面板、15:00 截止口径 |

把两类分量分开有一个实际好处：关注度的统计口径稳定、可复现，而方向分量依赖模型版本。先单独研究关注度分量，能弄清它本身携带了多少信息，再决定是否叠加方向。

## 二、熔岩里可以直接用的四个关注度字段

| 字段 | 来源 | 含义 | 注意 |
| --- | --- | --- | --- |
| `mentions` | `/panel/daily`、各热榜 | 提及次数 | 受全市场讨论总量影响 |
| `agencies` | 同上 | 不同主体数 | 比 `mentions` 更抗重复表达 |
| `weighted_heat` | `/hot/weighted`、`/panel/daily` | 达标主体提及按「分数 ÷ 1000」累加 | 面板版用当前机构分，非 PIT |
| `heat_z20` | `/hot/stocks`、`/hot/accelerating` | 相对近 20 日日均热度的 z 分 | 实时截面，无 `as_of_date` |

机构/大V 分大约在 450～1500 之间，1000 左右为中等，1050 约为前 10%。`weighted_heat` 等于把「谁在说」也纳入关注度，是熔岩区别于纯计数的一个维度。

## 三、取数：日频面板是因子研究的主入口

| 项目 | 内容 |
| --- | --- |
| MCP 工具 | `get_stock_heat_panel` |
| HTTP 路径 | `/panel/daily` |
| 粒度 | 自然日 |
| 字段 | `mentions`、`agencies`、`weighted_heat`、`is_first_mention_day` |
| 限制 | 区间最长 92 天，单次最多 20 个代码 |

```python
# panel_pull.py —— 示例，地址与令牌为占位
import os, time, requests, pandas as pd
BASE = os.environ.get("LAVA_BASE", "<YOUR_LAVA_BASE>/subscribe/v1")
H = {"Authorization": "Bearer " + os.environ.get("LAVA_API_KEY", "<YOUR_TOKEN>")}

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(["<CODE_1>", "<CODE_2>"], "<START>", "<END>")
```

每次成功调用 0.005 元，非 2xx 不计费。300 只股票、一个 92 天区间需要 15 次调用。

## 四、交易日对齐：自然日折到交易日

面板按自然日给出，因子按交易日计算，中间需要一次折叠。常见规则（示例）：

```text
交易日 T 的关注度 = 上一交易日 15:00 之后 至 T 日 15:00 之前 的讨论
周末与节假日的自然日计数，累加到其后第一个交易日
当天尚未结束的自然日不进入样本
```

面板是自然日粒度，无法精确切在 15:00；严格按 15:00 截止的截面要用加权热榜：`get_weighted_hot_stocks`（`/hot/weighted`）在传入 `as_of_date` 时采用 PIT 口径，分数只用当时已知数据，消息算到该日 15:00（Asia/Shanghai）。`days` 默认 3、范围 1～30，`min_score` 默认 1050。两种口径的结果不必相等，研究报告里写明用的是哪一种。

## 五、预处理：对数、去极值、截面标准化

关注度数据极度右偏：多数股票每天只有个位数提及，少数股票上百。直接标准化会让极端值主导截面。推荐顺序：

```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. 截面标准化
```

还可以在第三步之后对自有的换手率或市值做截面回归取残差，得到「剔除规模与活跃度后的关注度」。字段名与参数随结果一起保存，尤其注明 `weighted_heat` 的非 PIT 属性。

## 六、情绪因子择时：从个股到市场层面

「情绪因子择时」通常指市场层面的情绪指数。用熔岩做关注度版本，有两种可复现的聚合方式：

1. **固定股票池合计**：对一个固定股票池（例如自有的指数成分列表）的日频 `mentions` 求和，得到「股票池总讨论量」序列；
2. **概念层面**：`/panel/daily` 的 `type=concept`（`names` 传概念标准名），得到若干概念的日频序列，观察讨论在主题间的分布变化。

两种方式都只描述关注度的总量与结构。总量受数据覆盖面变化影响，建议同时计算 `agencies` 合计，并以份额而非绝对值做跨期比较。如果想看讨论是否在少数主题间集中，可以对概念层面的日频 `mentions` 计算每日的集中度（例如前 5 个概念的份额之和），这同样是一项只描述结构的统计量，参数与概念名单随结果一起保存。

## 七、PIT 与未来函数：三处要检查

- **机构分的时点**：面板 `weighted_heat` 用当前机构分重新加权，回测中会引入未来信息。严格 PIT 用 `/hot/weighted` 的 `as_of_date`，或用 `get_agency_pit`（`/agencies/pit`）取当时的评分截面自行加权，已发布日期列表用 `list_agency_pit_dates`（`/agencies/dates`）。
- **实时字段**：`heat_z20` 与加速榜都是实时截面，没有 `as_of_date`；历史上的 z 分要用面板序列在本地复算。
- **未完结日**：取数当天的自然日计数仍在增长，入样前必须剔除。

## 八、单因子检验的最小骨架

关注度分量做好之后，常规的单因子检验流程（Rank IC、分层统计、换手）都可以直接套用。下面只给数据对齐的骨架，前瞻变量来自研究者自有的行情数据，熔岩不提供行情：

```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)
```

两个对齐细节决定结果是否可信：因子在 T 日 15:00 截止，前瞻变量必须从 T 日收盘之后开始计算；当天没有任何提及的股票，因子值是 0 还是缺失要事先约定，并在报告里写明。

## 九、因子元数据：每次产出要保存什么

```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 -> T 15:00; weekend to next trading day",
  "winsorize": "MAD 3x, 1.4826",
  "universe": "<自有股票池名称与版本>",
  "pulled_at": "<meta.server_time>",
  "calls": 15
}
```

元数据和因子值放在一起保存。半年后复核时，只要看这一份 JSON，就能知道当时用了哪个字段、哪种日历规则、是否包含非 PIT 的加权热度。

## 十、常见问答

**Q：只有关注度、没有方向，能叫情绪因子吗？**  
A：严格说它是情绪因子的关注度分量，或单独称为关注度因子。文献里的情感总分本身就同时携带了热度信息，单独研究关注度能把两部分的作用区分开。

**Q：方向分量怎么补？**  
A：可以对 `get_stock_news_heat`（`/opinions`）返回的 `summary`（≤120 字）在本地做文本分类，按 `topic_time` 对齐交易日后与关注度分量组合；模型版本与阈值写进因子元数据。

**Q：`agencies` 和 `mentions` 用哪个？**  
A：两者都保留。`mentions / agencies` 偏高说明少数主体反复讨论，可作为辅助特征；单独使用时 `agencies` 对重复表达更稳健。

**Q：情绪因子量化需要多长历史？**  
A：面板单次最长 92 天，更长历史分段拉取拼接即可；样本长度由研究设计决定，报告中写明起止日期。

## 结论

用熔岩做情绪因子量化，最稳妥的定位是先做好关注度分量：以 `/panel/daily` 为主入口取 `mentions`、`agencies`、`weighted_heat`，按 15:00 规则折到交易日，经对数、MAD 去极值与截面标准化处理，再用 `/hot/weighted` 的 `as_of_date` 或 `/agencies/pit` 处理机构分的时点问题。方向分量若需要，在本地基于摘要另行构建。每一步写清参数与口径，得到的就是一份可复现的统计特征表。

数据来自熔岩消息面热度监测，仅供研究参考，不构成投资建议。

只读监测与字段说明见：https://lava.ai-dolphin.com/research
