市场情绪分析功能实现:综合分数+F&G指标

情绪是市场的短期定价因子之一。价格可以被操纵,成交量可以被对倒,但情绪指标往往能提供另一维度的参考。这篇文章记录我在自己的量化系统里实现市场情绪分析模块的过程,包括 F&G 指数获取、综合情绪分数计算、极端值信号,以及定时推送。

一、为什么要做情绪分析

做趋势策略的时候,最难受的不是震荡,而是趋势末端的情绪亢奋期。价格连续拉升,系统不断给出买入信号,但实际风险在快速积累。纯粹依赖价格和成交量的策略,很难识别"市场已经过热"这件事。

情绪指标的价值在于:它提供的是与价格相关性较低的信息。当综合情绪分数进入极端区域,可以作为仓位管理的辅助因子------不是用来反向开单,而是用来调整仓位系数。

我的实现思路:

  1. 抓取外部情绪数据(以 Fear & Greed Index 为主)
  2. 计算本地可量化的情绪代理指标(波动率、涨跌比、资金费率等)
  3. 加权合成一个 0-100 的综合情绪分数
  4. 在极端区间产生信号
  5. 定时推送到 Telegram / 邮件

二、F&G 指数数据获取

Alternative.me 提供的 F&G 数据是最常用的免费源,覆盖加密货币市场。接口简单,返回 JSON。

python 复制代码
import requests

import pandas as pd

from datetime import datetime



FG_URL = "https://api.alternative.me/fng/"



def fetch_fear_greed(limit: int = 30) -> pd.DataFrame:

    """

    获取恐惧贪婪指数历史数据

    limit: 返回最近 N 天的数据

    """

    params = {"limit": limit, "format": "json"}

    resp = requests.get(FG_URL, params=params, timeout=10)

    resp.raise_for_status()

    data = resp.json()["data"]



    df = pd.DataFrame(data)

    df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="s")

    df["value"] = df["value"].astype(int)

    df = df[["timestamp", "value", "value_classification"]]

    df = df.sort_values("timestamp").reset_index(drop=True)

    return df





if __name__ == "__main__":

    fg = fetch_fear_greed(limit=10)

    print(fg.tail())

返回字段里 value_classification 是官方给的文字标签(Extreme Fear / Fear / Neutral / Greed / Extreme Greed),可以直接用,也可以自己按区间划分。

注意这个接口偶尔会超时,生产环境建议加一层重试和本地缓存:

python 复制代码
import time

import json

from pathlib import Path



CACHE_FILE = Path("fg_cache.json")



def fetch_fg_with_cache(limit: int = 30) -> pd.DataFrame:

    try:

        df = fetch_fear_greed(limit)

        df.to_json(CACHE_FILE, orient="records", date_format="iso")

        return df

    except Exception as e:

        print(f"[warn] fetch failed: {e}, fallback to cache")

        if CACHE_FILE.exists():

            return pd.read_json(CACHE_FILE)

        raise

三、综合情绪分数计算

F&G 只有一个维度,而且更新频率是日级。要构建更有弹性的情绪分数,我会把几个本地可计算的指标合成进来。

3.1 情绪代理指标

选取四个维度:

指标 含义 情绪方向
波动率分位 近期 ATR / 长期 ATR 高波动 → 情绪极端
涨跌比 近 N 根 K 线上涨占比 越高越贪婪
动量偏离 当前价相对 MA20 的偏离 偏离越大越亢奋
资金费率 永续合约资金费率 正费率越高越贪婪

3.2 归一化到 0-100

每个指标先做 min-max 或分位数归一化,再加权。

python 复制代码
import numpy as np



def normalize_series(s: pd.Series, window: int = 90) -> pd.Series:

    """滚动 min-max 归一化到 0-100"""

    roll_min = s.rolling(window, min_periods=10).min()

    roll_max = s.rolling(window, min_periods=10).max()

    rng = (roll_max - roll_min).replace(0, np.nan)

    return ((s - roll_min) / rng * 100).clip(0, 100)





def compute_volatility_score(df: pd.DataFrame, window: int = 90) -> pd.Series:

    """ATR 比率 -> 情绪分"""

    high, low, close = df["high"], df["low"], df["close"]

    tr = pd.concat([

        high - low,

        (high - close.shift()).abs(),

        (low - close.shift()).abs()

    ], axis=1).max(axis=1)

    atr_short = tr.rolling(14).mean()

    atr_long = tr.rolling(60).mean()

    ratio = atr_short / atr_long

    return normalize_series(ratio, window)





def compute_breadth_score(df: pd.DataFrame, n: int = 20, window: int = 90) -> pd.Series:

    """近 n 根 K 线上涨占比"""

    up = (df["close"].diff() > 0).astype(int)

    breadth = up.rolling(n).mean() * 100

    return normalize_series(breadth, window)





def compute_momentum_score(df: pd.DataFrame, window: int = 90) -> pd.Series:

    """价格相对 MA20 的偏离"""

    ma20 = df["close"].rolling(20).mean()

    dev = (df["close"] - ma20) / ma20

    return normalize_series(dev, window)

3.3 合成综合分数

权重我给的是经验值:波动率 0.25、涨跌比 0.25、动量 0.3、F&G 0.2。可以根据自己的策略回测调整。

python 复制代码
def compute_composite_score(df: pd.DataFrame, fg_df: pd.DataFrame) -> pd.DataFrame:

    """

    df: 含 open/high/low/close 的日线数据,index 为 datetime

    fg_df: F&G 数据,含 timestamp / value

    """

    out = pd.DataFrame(index=df.index)

    out["vol_score"] = compute_volatility_score(df)

    out["breadth_score"] = compute_breadth_score(df)

    out["mom_score"] = compute_momentum_score(df)



    # 对齐 F&G 数据

    fg = fg_df.set_index("timestamp")["value"].rename("fg_score")

    out = out.join(fg, how="left")

    out["fg_score"] = out["fg_score"].ffill()



    weights = {

        "vol_score": 0.25,

        "breadth_score": 0.25,

        "mom_score": 0.30,

        "fg_score": 0.20,

    }

    out["composite"] = sum(out[k] * w for k, w in weights.items())

    out["composite"] = out["composite"].clip(0, 100)

    return out

跑完之后,composite 就是 0-100 的综合情绪分数。低于 20 视为极度恐慌,高于 80 视为极度贪婪。

四、极端值作为交易参考

情绪分数本身不是开仓信号,它是仓位调节器。我的做法是:

  • 综合分数 > 80:多头仓位系数 × 0.5,做空信号权重 × 1.2
  • 综合分数 < 20:多头仓位系数 × 1.2,做空信号权重 × 0.5
  • 其他区间:系数 1.0
python 复制代码
def sentiment_position_factor(score: float) -> float:

    """根据情绪分数返回仓位系数"""

    if score >= 80:

        return 0.5

    if score >= 65:

        return 0.8

    if score <= 20:

        return 1.2

    if score <= 35:

        return 1.1

    return 1.0





def detect_extreme(score_series: pd.Series, high: float = 80, low: float = 20) -> pd.Series:

    """标记情绪极端点"""

    signal = pd.Series(0, index=score_series.index)

    signal[score_series >= high] = 1    # 贪婪

    signal[score_series <= low] = -1    # 恐慌

    return signal

更稳妥的用法是结合极值持续时间。单日进入 80 以上意义不大,连续 3 天以上进入极值区,才说明情绪确实被推到了极端。可以加一个滚动窗口:

python 复制代码
def persistent_extreme(score_series: pd.Series, days: int = 3) -> pd.Series:

    high_flag = (score_series >= 80).rolling(days).sum() == days

    low_flag = (score_series <= 20).rolling(days).sum() == days

    out = pd.Series(0, index=score_series.index)

    out[high_flag] = 1

    out[low_flag] = -1

    return out

五、定时推送情绪报告

用 schedule 或者 APScheduler 都可以,这里用最轻量的 schedule。推送渠道以 Telegram 为例,邮件类似。

python 复制代码
import schedule

import time

import requests



TG_TOKEN = "YOUR_BOT_TOKEN"

TG_CHAT_ID = "YOUR_CHAT_ID"





def send_telegram(text: str):

    url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"

    payload = {"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"}

    try:

        requests.post(url, json=payload, timeout=10)

    except Exception as e:

        print(f"[error] telegram push failed: {e}")





def build_report(df: pd.DataFrame, scores: pd.DataFrame) -> str:

    last = scores.iloc[-1]

    prev = scores.iloc[-2]

    delta = last["composite"] - prev["composite"]



    fg_value = int(last["fg_score"]) if not pd.isna(last["fg_score"]) else -1

    label = "极度贪婪" if last["composite"] >= 80 else \

            "贪婪" if last["composite"] >= 65 else \

            "极度恐慌" if last["composite"] <= 20 else \

            "恐慌" if last["composite"] <= 35 else "中性"



    arrow = "↑" if delta > 0 else "↓" if delta < 0 else "→"



    report = (

        f"*市场情绪日报* {last.name.date()}\n"

        f"综合分数: `{last['composite']:.1f}` {arrow} ({delta:+.1f})\n"

        f"状态: *{label}*\n"

        f"F&G: `{fg_value}`\n"

        f"波动率分: `{last['vol_score']:.1f}`\n"

        f"涨跌比分: `{last['breadth_score']:.1f}`\n"

        f"动量分: `{last['mom_score']:.1f}`\n"

        f"仓位系数: `{sentiment_position_factor(last['composite']):.2f}`"

    )

    return report





def job():

    fg_df = fetch_fg_with_cache(limit=180)

    # df 从你自己的行情源获取,这里省略

    # df = load_ohlcv("BTCUSDT", "1d", limit=500)

    scores = compute_composite_score(df, fg_df)

    report = build_report(df, scores)

    send_telegram(report)





if __name__ == "__main__":

    schedule.every().day.at("08:05").do(job)

    while True:

        schedule.run_pending()

        time.sleep(30)

推送时间建议放在你所在时区的早晨,数据源刚完成更新,同时你也方便看一眼再决定当天的操作。

六、几个实现细节

数据对齐:F&G 是 UTC 时间戳,本地 K 线可能是 UTC+8,join 之前统一时区,否则会出现 NaN。

缺失值处理 :F&G 接口偶尔抽风,ffill() 是必须的,但不要 bfill,那会引入未来数据。

权重不要过拟合:我给的权重是拍的,但不要为了回测好看去优化它。情绪分数的意义是稳健,不是精确。

分数不要单独用:单独看情绪分数会经常误判,它必须和价格结构、趋势状态一起判断。我的系统里它只占仓位系数的 20% 权重。

日志留存:每次计算完把分数写进数据库或 CSV,方便事后复盘哪次情绪信号有效、哪次失效。

七、小结

这套实现的代码量不大,但补上了纯价格策略的一个盲区。核心逻辑是:

  1. 用 F&G 提供外部情绪锚点
  2. 用本地指标补充高频信息
  3. 加权合成 0-100 分数
  4. 极端值 + 持续性判断产生信号
  5. 定时推送,人工复核

代码可以直接跑,替换数据源和推送渠道就能接入自己的系统。后续可以考虑把情绪分数做成因子,回测它和未来 N 日收益的相关性,验证权重是否合理。

更多关于 Python 量化实战、自动化交易系统的内容,请关注本站。

复制代码
        </div>

        <div class="post-tags"><span class="tag-badge">市场情绪</span><span class="tag-badge">F&amp;G指标</span><span class="tag-badge">情绪分析</span><span class="tag-badge">Python</span></div>

        <div class="post-footer">

            <span>作者: mattleeee</span>

            <span><a href="https://github.com/mattleeee">GitHub</a></span>