第八章 实战项目——构建你的“因子谎言检测系统”

搭建从数据获取到盘中预警的完整系统

306页 · 预计45

本章实战输出:从数据获取到盘中预警,搭建一套完整的因子谎言检测系统。包含数据抓取、因子计算、拥挤度监控、失效预警、钉钉/微信推送,以及AI增强模块。你只需要复制代码、修改配置,就能在一天内跑起来。

8.1 项目整体架构:数据层→因子层→评估层→报警层

在开始写代码之前,我们先梳理清楚整套系统的逻辑。这套系统分成四层,从上到下依次是数据层、因子层、评估层、报警层。

第一层是数据层。负责从免费数据源,比如AKshare、Tushare,获取股票日线数据、财务数据、成分股列表等。每天收盘后自动运行,把数据存到本地CSV或者SQLite数据库。这一层不需要实时,收盘后跑就行。
第二层是因子层。负责计算你关心的因子值。比如你想监控小市值因子、反转因子、低波动因子,那就每天用最新数据算出每只股票的因子值,并保存。这一层可以和数据层合并运行。
第三层是评估层。这是整个系统的核心。它读取历史因子值和未来收益数据,计算IC、拥挤度、换手率、分组收益等指标,判断每个因子是否出现“谎言”信号,也就是我在前几章讲过的预警信号。这一层每天跑一次,通常在数据更新后立即运行。
第四层是报警层。如果评估层发现某个因子触发了预警,比如拥挤度超过阈值、IC波动率异常,就通过钉钉机器人、微信企业号或邮件发送报警消息。你收到消息后,可以人工决策是否减仓。

系统结构很简单,但每个模块都有不少细节。这一章我会带着你从零开始搭建,每个模块给出完整代码,并解释关键点。你不需要一次性全部运行,可以边看边写,遇到问题没关系,后面有代码调试指南。

8.2 数据获取模块:多源免费数据自动抓取

我们先写数据获取模块。目标是每天自动获取全市场股票的日线数据,开高低收、成交量、换手率,并保存到本地。为了效率,我们只获取最近N天的数据,然后增量更新。

选择数据源
我选择AKshare作为主力,Tushare作为备份。代码会先尝试AKshare,如果失败则自动切换到Tushare。

代码实现
创建一个文件 data_fetcher.py,内容如下,我会逐块解释:

import os
import pandas as pd
import numpy as np
import akshare as ak
import time
from datetime import datetime, timedelta
def get_all_stock_codes():
    """获取全市场股票代码列表(包括退市和ST)"""
    # AKshare获取A股股票代码
    try:
        stock_info = ak.stock_info_a_code_name()
        codes = stock_info['code'].tolist()
        # 格式化为6位数字,去掉可能的前缀
        codes = [c.zfill(6) for c in codes]
    except Exception as e:
        print(f"获取股票列表失败: {e}")
        # 备用列表可以从文件读取
        codes = []
    return codes
def fetch_daily_data(code, start_date, end_date, source='akshare'):
    """获取单只股票的日线数据,返回DataFrame"""
    if source == 'akshare':
        try:
            df = ak.stock_zh_a_hist(symbol=code, period='daily', 
                                    start_date=start_date, end_date=end_date, 
                                    adjust='qfq')
            if df.empty:
                return None
            # 统一列名
            df.rename(columns={
                '日期': 'date',
                '开盘': 'open',
                '收盘': 'close',
                '最高': 'high',
                '最低': 'low',
                '成交量': 'volume',
                '成交额': 'amount',
                '换手率': 'turnover'
            }, inplace=True)
            df['code'] = code
            return df[['date', 'code', 'open', 'close', 'high', 'low', 'volume', 'amount', 'turnover']]
        except Exception as e:
            print(f"AKshare获取{code}失败: {e}")
            # 可以fallback到Tushare,这里省略
            return None
    else:
        # Tushare备用代码略
        return None
def update_database():
    """增量更新数据库"""
    # 设置存储路径
    data_dir = './stock_data'
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    
    # 获取所有股票代码(实际测试可以先取前100只,全跑太慢)
    codes = get_all_stock_codes()
    # 测试阶段取前100只
    codes = codes[:100]
    
    # 确定需要获取的日期范围:上次最后日期到昨天
    last_file = os.path.join(data_dir, 'last_update.txt')
    if os.path.exists(last_file):
        with open(last_file, 'r') as f:
            last_date = f.read().strip()
        start_date = (datetime.strptime(last_date, '%Y%m%d') + timedelta(days=1)).strftime('%Y%m%d')
    else:
        # 首次运行,取过去3年
        start_date = (datetime.now() - timedelta(days=3*365)).strftime('%Y%m%d')
    
    end_date = (datetime.now() - timedelta(days=1)).strftime('%Y%m%d')
    if start_date >= end_date:
        print("数据已是最新,无需更新")
        return
    
    print(f"更新数据从 {start_date} 到 {end_date}")
    
    for code in codes:
        df = fetch_daily_data(code, start_date, end_date)
        if df is not None:
            # 保存为CSV,每个股票一个文件(或合并到一个大文件,看个人喜好)
            file_path = os.path.join(data_dir, f'{code}.csv')
            if os.path.exists(file_path):
                old = pd.read_csv(file_path)
                new = pd.concat([old, df], ignore_index=True).drop_duplicates('date').sort_values('date')
            else:
                new = df
            new.to_csv(file_path, index=False)
        time.sleep(0.5)  # 避免请求过快被限制
    
    # 更新最后日期
    with open(last_file, 'w') as f:
        f.write(end_date)
    print("数据更新完成")

代码调试指南
第一次运行时,获取全市场股票列表可能失败,因为AKshare的接口有时会变。如果报错,你可以手动准备一个股票列表CSV文件,从网上下载最新股票代码。
获取单只股票数据如果超时,可以增加time.sleep的时长,但整个更新会变慢。建议只在测试时跑少量股票,正式使用时可以用多线程,注意不要太快,会被封IP。
数据存储方式可以优化,用Parquet格式比CSV快,用SQLite更方便查询。你可以根据自己的习惯选择。

这个模块写好后,你可以每天下午6点左右运行一次,因为A股收盘后数据大概在17:00-18:00更新完毕。你可以用cron或Windows任务计划自动执行。

8.3 因子库模块:内置10个传统因子 + 5个古怪因子

数据有了,接下来定义我们要监控的因子。我把因子分成两类:传统因子,也就是大家都用的,和古怪因子,也就是我们自己的。每个因子都要实现一个函数:给定某一天的全市场数据,返回每只股票的因子值,Series格式,index为股票代码。

传统因子一共10个:
第一个是小市值因子,市值等于总股本乘以股价。在A股,总股本数据可以从财务数据获取,但为了简化,我们可以用收盘价和流通股本计算,AKshare有stock_individual_info_em接口。这里我用流通市值近似,更符合流动性逻辑。
第二个是反转因子,过去20日累计收益率,负值越大得分越高。
第三个是动量因子,过去60日累计收益率,剔除最近5日。
第四个是低波动因子,过去60日日收益率的波动率,也就是标准差,越低越好。
第五个是换手率因子,过去20日平均换手率,越高越活跃,但要注意反转效应。
第六个是价值因子,市净率倒数,也就是PB倒数,越高越低估。
第七个是盈利因子,ROE,也就是净资产收益率,越高越好。
第八个是成长因子,过去4个季度净利润同比增长率。
第九个是情绪因子,基于股吧情感得分,这里简化为过去5日换手率变化率。
第十个是质量因子,毛利率、净利率的复合得分。

古怪因子列表,示范5个:
第一个是“阴线实体长度”因子,最近5日阴线实体长度,收盘价减开盘价,取负值,的总和。认为连续阴线后可能反弹。
第二个是“临近收盘异动”因子,最后30分钟涨幅与全天涨幅的比值。尾盘拉升可能第二天低开。
第三个是“涨停封单占比”因子,涨停时的封单量除以流通股本。封单太大可能是假象。
第四个是“龙虎榜机构买入占比”因子,如果上了龙虎榜,机构买入金额占比高的股票后续有超额收益。
第五个是“高温天气”因子,公司所在地当日气温超过35度,可能影响生产,卖空信号,开玩笑,但这个因子我见过论文。

在实际系统中,我们不需要所有因子都实时计算,而是可以选择你关心的3-5个。下面以小市值因子和反转因子为例给出计算代码。

因子计算框架
创建一个文件 factor_library.py

import pandas as pd
import numpy as np
def factor_market_cap(daily_data, float_shares_data):
    """
    小市值因子:流通市值 = 收盘价 * 流通股本
    daily_data: DataFrame 包含 code, date, close
    float_shares_data: Series 或 dict,每只股票的流通股本(股)
    """
    # 假设 float_shares_data 是 code -> 流通股本(股)的映射
    # 对于每个股票,取最新close
    latest_close = daily_data.groupby('code')['close'].last()
    cap = latest_close * float_shares_data[latest_close.index]
    # 因子值越小越好,所以我们返回 -cap(或者直接返回cap,在组合构建时反向排序)
    return -cap
def factor_reversal(daily_data, window=20):
    """
    反转因子:过去window日累计收益率(%)
    返回因子值,值越大越好(前期跌得多,预期反弹)
    """
    # 计算每日收益率
    daily_data['ret'] = daily_data.groupby('code')['close'].pct_change()
    # 计算过去window日的累计收益(跳过最最近一天,避免前视)
    # 注意:我们需要滞后一天,即使用昨天及之前的数据
    reversal = daily_data.groupby('code')['ret'].rolling(window).sum() - daily_data['ret']
    # shift(1) 避免未来信息
    factor = -reversal.groupby('code').shift(1)  # 负值表示跌得多,所以加负号变成正向因子
    return factor
# 可以继续添加其他因子...

代码调试指南
计算市值需要流通股本数据。AKshare有stock_individual_info_em接口,可以获取总股本和流通股本,但每次调用会被限制频率。建议每周更新一次股本数据,存成本地文件。
反转因子中的rolling(window).sum()在大数据量上可能很慢。可以预先计算每日收益率,然后用rolling,但要注意分组。上面的代码用了两次groupby,效率不高,可以改成先透视成宽表再计算,但宽表可能内存不够。实际中可以根据你的数据量权衡。
因子计算完成后,需要标准化,也就是减去均值除以标准差,否则不同因子的量纲不一致。标准化可以用scipy.stats.zscore

为了简化,本章后续的评估模块我们只监控一个因子,比如小市值因子,你理解逻辑后可以扩展到多个。

8.4 回测与评估模块:滚动IC、拥挤度计算、失效概率预测

这是整个系统的核心。我们要对每个因子每天计算三个核心指标:
第一个是滚动IC,过去60日的IC均值,看趋势。
第二个是拥挤度,因子持仓与全市场量化产品持仓的重合度,或者简单用因子换手率、因子波动率等代理。
第三个是失效概率,用第五章训练的XGBoost模型输出。

代码实现
创建一个文件 factor_monitor.py

import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
import joblib  # 用于加载预训练模型
def compute_ic(factor_series, forward_returns):
    """
    计算单日IC
    factor_series: Series,index=股票代码,值=因子值
    forward_returns: Series,index=股票代码,值=未来收益(比如次日收益)
    """
    # 剔除NaN
    combined = pd.DataFrame({'factor': factor_series, 'ret': forward_returns}).dropna()
    if len(combined) < 10:
        return np.nan
    return combined['factor'].corr(combined['ret'], method='spearman')
def compute_rolling_ic(factor_history, returns_history, window=60):
    """
    计算滚动IC序列
    factor_history: DataFrame,index=日期,columns=股票代码,values=因子值
    returns_history: DataFrame,index=日期,columns=股票代码,values=次日收益率
    """
    dates = factor_history.index
    ic_series = []
    for i in range(window, len(dates)):
        current_date = dates[i]
        # 取过去window天的因子和收益
        factor_window = factor_history.iloc[i-window:i]
        ret_window = returns_history.iloc[i-window:i]
        # 逐日计算IC(只取当天截面)
        # 实际更严谨:计算每天IC后平均
        daily_ic = []
        for j in range(window):
            f = factor_window.iloc[j]
            r = ret_window.iloc[j]
            daily_ic.append(compute_ic(f, r))
        ic_series.append(pd.Series(daily_ic).mean())
    return pd.Series(ic_series, index=dates[window:])
def compute_crowding(factor_series, market_quant_holdings):
    """
    计算拥挤度:因子持仓与主流量化产品持仓的交集占比
    factor_series: 因子值Series,取最高10%作为“因子持仓”
    market_quant_holdings: list,主流量化产品持仓的股票代码列表
    """
    # 选择因子值最高的10%股票
    n = max(1, int(len(factor_series) * 0.1))
    top_stocks = factor_series.nlargest(n).index.tolist()
    if not market_quant_holdings:
        return np.nan
    intersect = set(top_stocks) & set(market_quant_holdings)
    return len(intersect) / len(top_stocks)
def compute_turnover_ratio(factor_series, prev_factor_series, top_n=50):
    """
    计算因子换手率:当前top_n股票与上一期top_n股票的变化比例
    """
    current_top = set(factor_series.nlargest(top_n).index)
    prev_top = set(prev_factor_series.nlargest(top_n).index) if prev_factor_series is not None else set()
    turnover = len(current_top - prev_top) / top_n
    return turnover
def predict_failure(features):
    """
    使用预训练的XGBoost模型预测失效概率
    features: dict, 包含当前因子特征(IC趋势、拥挤度、换手率等)
    """
    # 加载模型(模型保存在文件)
    model = joblib.load('failure_model.pkl')
    # 将features转为DataFrame
    X = pd.DataFrame([features])
    prob = model.predict_proba(X)[0, 1]
    return prob

集成进主流程
在每日更新完成后,我们调用上面的函数,生成报告。具体的实现依赖于你的数据存储格式,这里不展开。你可以在GitHub上找到完整的参考实现。

8.5 谎言识别模块:检测因子是否即将失效 + 主力反杀信号

除了拥挤度和IC,我们还要加入前面章节提到的“主力反杀”信号,比如盘口异常,也就是虚假申报、诱导拉升。这个需要Level2数据,我们可以单独写一个模块。
由于Level2数据获取较为复杂,需要券商API或付费,这里我提供一个简化版本:基于日线数据的“疑似反杀”信号。
比如盘中大幅冲高回落:当日最高价减最低价大于5%,且收盘价位于当日振幅的底部30%分位。这可能是主力拉高出货的痕迹。
又比如尾盘异常拉升:最后30分钟涨幅占全天涨幅的80%以上,但全天涨幅不超过3%。这可能是为了做收盘价,第二天容易低开。
把这些信号整合成一个“反杀风险评分”。

def compute_trap_score(daily_bar):
    """
    输入单只股票的单日K线
    返回陷阱分数(0-10)
    """
    high, low, close, open_price = daily_bar['high'], daily_bar['low'], daily_bar['close'], daily_bar['open']
    amplitude = (high - low) / low
    if amplitude > 0.05:
        position = (close - low) / (high - low)
        if position < 0.3:
            # 冲高回落严重,得高分
            score = 8
        else:
            score = 2
    else:
        score = 0
    # 还可以加入尾盘异动等
    return score

这个模块可以作为额外的过滤器:当你持有的股票出现高分陷阱信号时,即使因子没失效,也应该考虑减仓。

8.6 报警模块:钉钉/微信自动推送

我们使用钉钉机器人和微信企业号发送报警。钉钉机器人更简单,适合个人。首先在钉钉群里添加一个自定义机器人,获取Webhook URL。

钉钉报警代码

import requests
import json
def send_dingtalk_alert(message, webhook_url):
    headers = {'Content-Type': 'application/json'}
    data = {
        "msgtype": "text",
        "text": {
            "content": message
        }
    }
    response = requests.post(webhook_url, headers=headers, data=json.dumps(data))
    if response.status_code == 200:
        print("报警发送成功")
    else:
        print(f"报警发送失败: {response.text}")

然后在监控脚本中,当某个预警条件触发时调用该函数。举个例子:

if ic_trend_declining and crowding > 0.8:
    send_dingtalk_alert(f"小市值因子预警:IC衰减,拥挤度{拥挤度:.2f}", webhook_url)

你也可以发送更详细的信息,比如附上最近几日因子的表现图表链接,需要将图表上传到图床。

8.7 AI集成:每日自动运行XGBoost失效概率模型 + LLM生成简要解读报告

这一节我们做两个AI增强:一是用训练好的XGBoost模型预测失效概率,二是用大语言模型生成一段自然语言解读,供你快速理解报告内容。

XGBoost预测
假设你已经在第五章训练好了模型,并保存为failure_model.pkl。在每日监控中,调用predict_failure,如果概率大于0.7,发送报警。

LLM生成解读
我们可以将当天的关键指标,IC、拥挤度、换手率、失效概率等,拼接成一段文本,然后调用DeepSeek的API,让它用通俗的语言解释当前因子健康状况。

import openai  # 需要安装openai库,兼容DeepSeek的API
def generate_llm_report(factor_name, metrics):
    prompt = f"""
    你是一个量化投资专家。请根据以下因子监测指标,写一段简短的健康度解读(不超过100字):
    因子名称:{factor_name}
    最近20日IC均值:{metrics['ic_mean']:.3f}
    拥挤度:{metrics['crowding']:.2%}
    换手率:{metrics['turnover']:.2%}
    失效概率:{metrics['fail_prob']:.2%}
    是否需要减仓?
    """
    response = openai.ChatCompletion.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": prompt}],
        api_key="your_api_key"
    )
    return response['choices'][0]['message']['content']

你可以将生成的解读一起通过钉钉发送,也可以单独保存到日志中。
注意:调用LLM API会产生费用,建议只在发现异常时调用,或者每天只调用一次。

8.8 引流扩展:从因子检测到实时选股信号推送——对接A-Sig.com风格接口

这一节是引流内容。如果你的系统检测到某个因子当前健康,低拥挤、IC稳定,那么你可以将基于该因子的选股信号推送到A-Sig.com风格的API接口上。这样其他用户也可以看到这些信号,形成社区效应。
具体实现:在每天收盘后,如果因子健康,则根据因子值选出前N只股票,发送HTTP POST请求到你的网站后台。

import requests
def push_signals(date, stock_list, scores):
    payload = {
        'date': date,
        'signals': [{'code': code, 'score': score} for code, score in zip(stock_list, scores)]
    }
    # 假设你的网站API端点为 https://a-sig.com/api/signals
    response = requests.post('https://a-sig.com/api/signals', json=payload)
    if response.status_code == 200:
        print("信号推送成功")
    else:
        print("推送失败")

引流说明:如果你想获取每日的因子信号,可以访问A-Sig.com,但请务必用自己的回测框架独立验证。

8.9 系统部署与维护:云服务器低成本方案

你不需要买昂贵的服务器。对于个人使用,一台每月几十元的云服务器就足够,比如阿里云ECS、腾讯云轻量应用服务器。配置建议:2核CPU、4GB内存、40GB硬盘。操作系统选Ubuntu 20.04 LTS。

部署步骤:
第一步,安装Python 3.8+,pip,virtualenv。
第二步,上传代码到服务器,用git clone或者scp都行。
第三步,安装依赖,pip install -r requirements.txt
第四步,设置定时任务,使用crontab,每天下午18:30执行数据更新和监控脚本。

30 18 * * * cd /home/ubuntu/factor_monitor && python3 data_fetcher.py >> logs/data.log 2>&1
35 18 * * * cd /home/ubuntu/factor_monitor && python3 factor_monitor.py >> logs/monitor.log 2>&1

第五步,监控日志,定期检查logs目录是否有错误。

代码调试指南
服务器时间要和北京时间一致,使用timedatectl set-timezone Asia/Shanghai命令设置。
定时任务环境变量可能不完整,建议在脚本开头使用绝对路径。
免费数据源可能会被服务器IP限制,遇到时考虑使用代理或切换到备用源。

8.10 本章FAQ

问题1:我没有服务器,能在自己的电脑上跑吗?
可以,只要你每天定时开机并运行脚本。但建议还是用服务器,省心。

问题2:Level2数据怎么获取?
部分券商对个人提供免费的Level2行情,比如华泰、国泰君安,但需要开户并申请。你可以从券商APP中导出数据,或者使用开源项目easyquotation,部分免费。注意合规问题,不要用于商业用途。

问题3:XGBoost模型需要每天重新训练吗?
不需要,可以每周或每月重新训练一次,用滚动窗口更新数据。训练可以放在另一台机器上,然后上传模型文件到服务器。

问题4:这套系统的运行成本大概多少?
云服务器每月约50-100元,数据免费,LLM API调用费用每天不到0.1元,如果只调一次的话。整体可以控制在每月100元以内。

问题5:如果因子一直没触发预警,是不是就可以一直用?
不一定。预警信号只是参考。如果因子长期有效但拥挤度持续攀升,最终必然会失效。建议定期,每季度,人工审视因子的逻辑是否依然成立。

本章核心公式卡

本章核心逻辑以代码实现为主,关键指标计算公式与前五章一致,可直接参考对应章节的公式卡。

本章实战清单汇总

搭建步骤
购买云服务器,可选。
安装Python环境。
复制本章代码到服务器。
配置数据源,AKshare等。
运行数据获取模块,确保能下载到数据。
运行因子计算模块,确保因子值正确。
运行监控模块,测试报警,可先设置较低的阈值触发测试。
配置钉钉机器人,发送测试消息。
设置定时任务,观察几天运行情况。

日常维护
每周检查一次日志,确认无报错。
每月重新训练XGBoost模型,可选。
每季度更新退市股票列表和成分股列表。
关注证监会网站新规,确保系统合规。

第八章到此结束。
读完这一章,你应该有能力搭建一套完全属于自己的因子监测系统。这不是一个“黑盒子”,而是你可以自由修改、扩展的工具箱。随着你对因子的理解加深,你可以加入更多因子、更复杂的预警逻辑。
下一章是第九章,也是最后一章。我们会聊一聊量化因子的伦理、局限性,以及未来的发展方向。我会给你一些最后的忠告,帮助你在这个充满谎言的市场中保持清醒。

⚠️ 风险提示:本书内容仅为量化研究与知识分享,不构成任何投资建议。套利交易存在基差、费率、流动性、平台与极端行情等风险,历史表现不代表未来收益。投资有风险,入市需谨慎,请自主决策、量力而行。

💡 键盘 ←/→ 翻章 · T 键切换目录