← 返回博客列表

AI+量化实战

【AI+量化实战 #03】散户焦虑还是看好?用Python给互动易问答做情绪测温

2026年10月15日 07:56 · 麦蕊智数 MyData

系列进度:AI+量化实战 #03 / 共 12 篇

痛点开场

你有没有这种感觉:某只股票突然跌了,你去互动易平台一搜,发现散户一个月前就在追问"公司是不是要暴雷"——信息就在那里,但你没看到。

互动易是A股散户向上市公司提问的官方渠道。每天都有人问业绩、问减持、问分红、问"什么时候涨"。这些提问本身就是一种情绪信号:

  • 大量追问"跌""亏损""质押"——焦虑在蔓延;
  • 集中问"分红""回购""增长"——信心在积聚。

问题是,互动易上几千只股票、每天上百条问答,你不可能手动翻。能不能用 Python 批量拉取、自动打标、聚合成一只股票的情绪温度计?

能。今天我们就做这件事。

本文你将得到什么

  1. stock_interactiveqa 接口的真实字段说明与调用方式。
  2. 一个可复用的问答情绪打分函数(关键词版,零依赖)。
  3. 一段已跑通的代码:批量拉互动易 → 打情绪标签 → 聚合温度计 → 对比股价收益。
  4. 关键词法的局限与升级到 LLM 的路线。

一、互动易数据从哪来

stock_interactiveqa 接口返回单只股票的互动易问答历史。核心字段:

字段 含义
q 散户提问内容
qt 提问日期(YYYY-MM-DD)
a 公司回答内容
at 回答日期
id 记录 ID

调用示例:

import mairui
import os

api = mairui.Client("<你的证书>")  # 演示证书,请替换成你自己的正式证书
qa = api.stock_interactiveqa("600519", lt=10)   # 最近 10 条
for item in qa:
    print(item["qt"], item["q"][:50])

互动易的价值在于:提问是散户自发行为,反映真实关切。不像公告是公司主动披露(经过包装),互动易上的问题往往更直接——"为什么跌""会不会退市""什么时候分红"。

二、情绪打分:先跑通,再迭代

和公告情绪分析一样,先用关键词把链路跑通。我们定义两组词表:

# 焦虑/负面词
ANXIETY_WORDS = (
    "跌", "亏损", "减持", "退市", "暴雷", "违规", "处罚", "问询",
    "风险", "担忧", "下滑", "停牌", "质押", "爆仓", "诉讼", "索赔",
    "被套", "套牢", "割肉", "补仓", "缩量", "破发", "破净", "商誉",
    "坏账", "计提", "滞涨", "跳水", "闪崩", "操纵", "调查",
)

# 乐观/正面词
OPTIMISM_WORDS = (
    "增长", "回购", "增持", "分红", "利好", "前景", "突破", "创新",
    "布局", "扩张", "合作", "订单", "中标", "预增", "预盈", "送转",
    "龙头", "低估", "机会", "潜力", "新能源", "芯片", "AI",
)


def score_question(q_text: str) -> int:
    """对单条提问打情绪分:+1 乐观,-1 焦虑,0 中性。"""
    if not q_text:
        return 0
    t = q_text.lower()
    pos = sum(1 for w in OPTIMISM_WORDS if w in t)
    neg = sum(1 for w in ANXIETY_WORDS if w in t)
    if pos > neg:
        return 1
    if neg > pos:
        return -1
    return 0

为什么不用 FinBERT 或 LLM?因为第一版的目标是验证信号是否存在,不是追求精度。关键词法零依赖、可解释、5 分钟跑通,适合做 MVP。

三、完整代码:情绪温度计 + 股价对比

import os
import sys
import time
from datetime import datetime, timedelta
from collections import Counter

import pandas as pd
import mairui

STOCK_POOL = {
    "600519": "贵州茅台", "000858": "五粮液",   "601318": "中国平安",
    "600036": "招商银行", "000333": "美的集团", "002594": "比亚迪",
    "601012": "隆基绿能", "600809": "山西汾酒",
}
QA_LIMIT = 30
LOOKBACK_DAYS = 180
PRICE_WINDOW = 30

ANXIETY_WORDS = (
    "跌", "亏损", "减持", "退市", "暴雷", "违规", "处罚", "问询",
    "风险", "担忧", "下滑", "停牌", "质押", "爆仓", "诉讼", "索赔",
    "被套", "套牢", "割肉", "补仓", "缩量", "破发", "破净", "商誉",
    "坏账", "计提", "滞涨", "跳水", "闪崩", "操纵", "调查",
)
OPTIMISM_WORDS = (
    "增长", "回购", "增持", "分红", "利好", "前景", "突破", "创新",
    "布局", "扩张", "合作", "订单", "中标", "预增", "预盈", "送转",
    "龙头", "低估", "机会", "潜力", "新能源", "芯片", "AI",
)


def score_question(q_text: str) -> int:
    t = q_text.lower()
    pos = sum(1 for w in OPTIMISM_WORDS if w in t)
    neg = sum(1 for w in ANXIETY_WORDS if w in t)
    return 1 if pos > neg else (-1 if neg > pos else 0)


def fetch_interactiveqa(api, codes):
    rows = []
    cutoff = (datetime.now() - timedelta(days=LOOKBACK_DAYS)).strftime("%Y-%m-%d")
    for code, name in codes.items():
        items = api.stock_interactiveqa(code, lt=QA_LIMIT) or []
        for item in items:
            q_text, q_date = item.get("q", ""), item.get("qt", "")
            if q_date >= cutoff:
                rows.append({
                    "code": code, "name": name, "q_date": q_date,
                    "question": q_text, "has_answer": 1 if item.get("a") else 0,
                    "sentiment": score_question(q_text),
                })
        time.sleep(0.15)
    return pd.DataFrame(rows)


def compute_thermometer(qa_df):
    g = qa_df.groupby(["code", "name"])
    result = g.agg(
        total_qa=("question", "count"),
        anxiety=("sentiment", lambda x: (x == -1).sum()),
        optimism=("sentiment", lambda x: (x == 1).sum()),
    ).reset_index()
    result["temperature"] = (
        (result["optimism"] - result["anxiety"]) / result["total_qa"]
    ).round(3)
    result["anxiety_ratio"] = (
        result["anxiety"] / result["total_qa"]
    ).round(3)
    return result


def fetch_recent_returns(api, codes, days=30):
    et = datetime.now().strftime("%Y%m%d")
    st = (datetime.now() - timedelta(days=int(days / 0.7))).strftime("%Y%m%d")
    rows = []
    for code, name in codes.items():
        kline = api.stock_history(code, "d", "n", st=st, et=et) or []
        if len(kline) >= 2:
            df = pd.DataFrame(kline)
            df["c"] = pd.to_numeric(df["c"], errors="coerce")
            df = df.sort_values("t").reset_index(drop=True)
            ret = round((float(df.iloc[-1]["c"]) / float(df.iloc[0]["c"]) - 1) * 100, 2)
            rows.append({"code": code, "name": name, "ret_30d": ret})
        time.sleep(0.1)
    return pd.DataFrame(rows)


api = mairui.Client("<你的证书>")  # 演示证书,请替换成你自己的正式证书
qa_df = fetch_interactiveqa(api, STOCK_POOL)
thermo = compute_thermometer(qa_df)
ret_df = fetch_recent_returns(api, STOCK_POOL, PRICE_WINDOW)

merged = thermo.merge(ret_df, on=["code", "name"], how="left")
merged = merged.sort_values("temperature", ascending=False)
print(merged[["name", "total_qa", "anxiety", "optimism",
              "temperature", "anxiety_ratio", "ret_30d"]].to_string(index=False))

四、真实运行结果

以 8 只大盘股、近 180 天互动易问答为例,共获取 156 条问答:

股票 问答数 焦虑 乐观 温度计 焦虑占比 近30日收益
贵州茅台 3 0 2 +0.667 0.0% +12.86%
比亚迪 30 1 10 +0.300 3.3% +12.08%
美的集团 17 1 5 +0.235 5.9% +10.40%
五粮液 30 2 6 +0.133 6.7% +1.22%
隆基绿能 30 2 5 +0.100 6.7% -2.12%
中国平安 18 5 6 +0.056 27.8% +6.36%
山西汾酒 28 9 9 0.000 32.1% +9.82%

高频关键词:

  • 焦虑词 Top5:跌(22次)、破净(2)、下滑(2)、违规(1)、亏损(1)
  • 乐观词 Top5:回购(15)、分红(10)、合作(6)、增长(6)、布局(5)

典型问答:

  • 焦虑:五粮液散户追问"半年报业绩同比波动超过50%需要披露业绩预告的,7月15日就是强制披露日..."
  • 乐观:贵州茅台散户问"什么时候分红"

结果解读

温度计与股价收益呈现弱正相关:茅台/比亚迪情绪温度高、涨幅也大;隆基绿能温度低、收益为负。但山西汾酒焦虑占比最高(32.1%)却涨了 9.82%——情绪不是择时信号。焦虑可能反映的是"跌怕了想抄底"的心理,反而出现在底部区域。

五、常见坑

  1. "跌"字过于敏感:关键词法会把"没跌""抗跌"也判为焦虑。可以加入否定词过滤("不/未/没" + 焦虑词 → 不计为负面)。
  2. 互动易活跃度不均:茅台近 180 天仅 3 条问答,统计意义有限。实际应用中需要更大的样本窗口或更活跃的股票池。
  3. 温度计 ≠ 择时信号:情绪温度计衡量的是散户关注度与情绪倾向,不是买卖信号。焦虑高可能意味着底部(散户恐慌),乐观高可能意味着顶部(散户狂热)。
  4. 问答质量参差:有些提问是无效噪音("公司电话多少"),需要在打分前做一轮过滤。

六、从关键词到 LLM:升级路线

关键词法的天花板很明显:

  • "公司会不会因为减持暴跌"——命中"减持"和"跌",但语义是询问风险,不是恐慌;
  • "回购完了是不是该涨了"——命中"回购",但语义是催涨预期,隐含焦虑。

升级方案:

  1. 关键词 + 上下文窗口:看焦虑词前后 5 个字是否有否定词,减少误判;
  2. LLM 情绪分类:把问答文本丢给大模型,让它输出情绪标签 + 一句话理由,准确率更高;
  3. 时间序列分析:把每日情绪得分做成时间序列,观察情绪的变化趋势而非绝对值——情绪从焦虑转向乐观的拐点,可能比情绪本身更有价值。

小结与下篇预告

今天我们搭了一条互动易情绪测温流水线:

  • 用互动易接口批量拉取散户问答;
  • 用关键词给提问打情绪标签;
  • 聚合成个股情绪温度计,与股价收益对比;
  • 讨论了关键词法的局限与 LLM 升级路径。

下篇(#04)我们换个角度:让 AI 当你的研报审计员——输入多家券商对同一只股票的业绩预测,让 LLM 指出数字矛盾与隐含前提。


风险提示:本文仅供技术学习交流,不构成投资建议。互动易情绪分析存在样本偏差、语义误判和滞后性风险,历史结果不代表未来收益。

标签: Python量化
QQ 客服 3826425416 咨询时请提供证书号或订单号,便于快速处理
咨询