搭建从数据获取到盘中预警的完整系统
第306页 · 预计45页
本章实战输出:从数据获取到盘中预警,搭建一套完整的因子谎言检测系统。包含数据抓取、因子计算、拥挤度监控、失效预警、钉钉/微信推送,以及AI增强模块。你只需要复制代码、修改配置,就能在一天内跑起来。
在开始写代码之前,我们先梳理清楚整套系统的逻辑。这套系统分成四层,从上到下依次是数据层、因子层、评估层、报警层。
第一层是数据层。负责从免费数据源,比如AKshare、Tushare,获取股票日线数据、财务数据、成分股列表等。每天收盘后自动运行,把数据存到本地CSV或者SQLite数据库。这一层不需要实时,收盘后跑就行。
第二层是因子层。负责计算你关心的因子值。比如你想监控小市值因子、反转因子、低波动因子,那就每天用最新数据算出每只股票的因子值,并保存。这一层可以和数据层合并运行。
第三层是评估层。这是整个系统的核心。它读取历史因子值和未来收益数据,计算IC、拥挤度、换手率、分组收益等指标,判断每个因子是否出现“谎言”信号,也就是我在前几章讲过的预警信号。这一层每天跑一次,通常在数据更新后立即运行。
第四层是报警层。如果评估层发现某个因子触发了预警,比如拥挤度超过阈值、IC波动率异常,就通过钉钉机器人、微信企业号或邮件发送报警消息。你收到消息后,可以人工决策是否减仓。
系统结构很简单,但每个模块都有不少细节。这一章我会带着你从零开始搭建,每个模块给出完整代码,并解释关键点。你不需要一次性全部运行,可以边看边写,遇到问题没关系,后面有代码调试指南。
我们先写数据获取模块。目标是每天自动获取全市场股票的日线数据,开高低收、成交量、换手率,并保存到本地。为了效率,我们只获取最近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任务计划自动执行。
数据有了,接下来定义我们要监控的因子。我把因子分成两类:传统因子,也就是大家都用的,和古怪因子,也就是我们自己的。每个因子都要实现一个函数:给定某一天的全市场数据,返回每只股票的因子值,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。
为了简化,本章后续的评估模块我们只监控一个因子,比如小市值因子,你理解逻辑后可以扩展到多个。
这是整个系统的核心。我们要对每个因子每天计算三个核心指标:
第一个是滚动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上找到完整的参考实现。
除了拥挤度和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
这个模块可以作为额外的过滤器:当你持有的股票出现高分陷阱信号时,即使因子没失效,也应该考虑减仓。
我们使用钉钉机器人和微信企业号发送报警。钉钉机器人更简单,适合个人。首先在钉钉群里添加一个自定义机器人,获取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)
你也可以发送更详细的信息,比如附上最近几日因子的表现图表链接,需要将图表上传到图床。
这一节我们做两个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会产生费用,建议只在发现异常时调用,或者每天只调用一次。
这一节是引流内容。如果你的系统检测到某个因子当前健康,低拥挤、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,但请务必用自己的回测框架独立验证。
你不需要买昂贵的服务器。对于个人使用,一台每月几十元的云服务器就足够,比如阿里云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限制,遇到时考虑使用代理或切换到备用源。
问题1:我没有服务器,能在自己的电脑上跑吗?
可以,只要你每天定时开机并运行脚本。但建议还是用服务器,省心。
问题2:Level2数据怎么获取?
部分券商对个人提供免费的Level2行情,比如华泰、国泰君安,但需要开户并申请。你可以从券商APP中导出数据,或者使用开源项目easyquotation,部分免费。注意合规问题,不要用于商业用途。
问题3:XGBoost模型需要每天重新训练吗?
不需要,可以每周或每月重新训练一次,用滚动窗口更新数据。训练可以放在另一台机器上,然后上传模型文件到服务器。
问题4:这套系统的运行成本大概多少?
云服务器每月约50-100元,数据免费,LLM API调用费用每天不到0.1元,如果只调一次的话。整体可以控制在每月100元以内。
问题5:如果因子一直没触发预警,是不是就可以一直用?
不一定。预警信号只是参考。如果因子长期有效但拥挤度持续攀升,最终必然会失效。建议定期,每季度,人工审视因子的逻辑是否依然成立。
本章核心逻辑以代码实现为主,关键指标计算公式与前五章一致,可直接参考对应章节的公式卡。
搭建步骤
购买云服务器,可选。
安装Python环境。
复制本章代码到服务器。
配置数据源,AKshare等。
运行数据获取模块,确保能下载到数据。
运行因子计算模块,确保因子值正确。
运行监控模块,测试报警,可先设置较低的阈值触发测试。
配置钉钉机器人,发送测试消息。
设置定时任务,观察几天运行情况。
日常维护
每周检查一次日志,确认无报错。
每月重新训练XGBoost模型,可选。
每季度更新退市股票列表和成分股列表。
关注证监会网站新规,确保系统合规。
第八章到此结束。
读完这一章,你应该有能力搭建一套完全属于自己的因子监测系统。这不是一个“黑盒子”,而是你可以自由修改、扩展的工具箱。随着你对因子的理解加深,你可以加入更多因子、更复杂的预警逻辑。
下一章是第九章,也是最后一章。我们会聊一聊量化因子的伦理、局限性,以及未来的发展方向。我会给你一些最后的忠告,帮助你在这个充满谎言的市场中保持清醒。
⚠️ 风险提示:本书内容仅为量化研究与知识分享,不构成任何投资建议。套利交易存在基差、费率、流动性、平台与极端行情等风险,历史表现不代表未来收益。投资有风险,入市需谨慎,请自主决策、量力而行。
💡 键盘 ←/→ 翻章 · T 键切换目录