用Python识别机构痕迹:我的机器学习量化选股完整代码
作者:程飞 | Python量化工程师
在A股市场上,机构资金的进出往往会在盘面上留下痕迹——大单买入、尾盘拉升、连续放量。这些痕迹散户看得见,但大多数散户不会用工具去系统性地捕捉。
今天我手把手带各位从零构建一套基于机器学习识别机构痕迹的选股系统。数据来源、特征工程、模型训练、风控验证——全部有完整代码,全部可以直接跑通。
一、为什么机构痕迹可以用机器学习来识别
传统的技术指标选股,有一个根本性问题:指标是死的,市场是活的。MACD金叉在2015年有用,在2020年可能就失效了。因为市场结构在变,参与者在变,规律也在变。
机器学习的优势在于:它能从数据里自动挖掘出那些肉眼难以察觉的规律,并且这些规律会随着数据更新而持续迭代。机构操盘痕迹本质上是一种模式识别问题——寻找那些大资金持续买入、持仓、最终拉升的股票特征。
我们把这个问题定义为:有监督的二分类问题——给定一只股票过去N天的盘面数据,预测未来20个交易日是否能跑赢大盘10%以上。
二、特征工程:从逐笔成交数据里提取机构信号
这是整个系统最关键的部分。我用Tushare获取逐笔成交数据,然后构建以下特征:
import pandas as pd
import numpy as np
import tushare as ts
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit
import warnings
warnings.filterwarnings('ignore')
# ========== 特征工程:提取机构痕迹特征 ==========
def extract_institution_features(stock_code, trade_date, lookback=20):
"""
从逐笔成交数据中提取机构操盘痕迹特征
参数:
stock_code: 股票代码,如 '000001'
trade_date: 交易日期,格式 'YYYYMMDD'
lookback: 回溯天数
返回: DataFrame,每行是一只股票的特征向量
"""
# 获取逐笔成交数据
df = ts.get_tick_data(stock_code, date=trade_date, src='ts')
if df is None or len(df) == 0:
return None
# 特征1: 大单买入占比(大单定义为单笔成交额 > 10万)
df['big_order'] = df['amount'] > 100000
big_order_ratio = df[df['big_order']]['amount'].sum() / df['amount'].sum()
# 特征2: 主动买入占比(内盘 vs 外盘)
# 外盘:主动买入;内盘:主动卖出
df['outer'] = df['type'].apply(lambda x: 1 if '外' in str(x) else 0)
active_buy_ratio = df[df['outer'] == 1]['amount'].sum() / df['amount'].sum()
# 特征3: 尾盘最后30分钟净买入量
df['time'] = pd.to_datetime(df['time'])
df['hour'] = df['time'].dt.hour
df['minute'] = df['time'].dt.minute
closing_30min = df[(df['hour'] == 14) & (df['minute'] >= 30)]
if len(closing_30min) > 0:
closing_net_flow = closing_30min[closing_30min['outer'] == 1]['amount'].sum() - \
closing_30min[closing_30min['outer'] == 0]['amount'].sum()
else:
closing_net_flow = 0
# 特征4: 连续净买入天数(连续N天大单净买入)
# 这里需要读取lookback天的数据做滚动计算
net_flow_series = []
for day in range(lookback):
day_df = ts.get_tick_data(stock_code,
date=shift_date(trade_date, -day),
src='ts')
if day_df is not None:
outer_sum = day_df[day_df['type'].apply(lambda x: '外' in str(x))]['amount'].sum()
inner_sum = day_df[day_df['type'].apply(lambda x: '内' in str(x))]['amount'].sum()
net_flow_series.append(outer_sum - inner_sum)
consecutive_days = sum(1 for x in net_flow_series if x > 0)
return {
'stock_code': stock_code,
'big_order_ratio': big_order_ratio,
'active_buy_ratio': active_buy_ratio,
'closing_net_flow': closing_net_flow,
'consecutive_net_buy_days': consecutive_days
}
核心逻辑解释:大单买入占比高,说明有资金在主动建仓;主动买入占比高于50%,说明买方比卖方更积极;尾盘净买入为正,说明机构在护盘或拉升;连续净买入天数越多,说明机构建仓行为越持续。
三、模型训练:用梯度提升树识别机构建仓信号
特征准备好了,接下来是模型训练。我选择梯度提升树(GBDT)作为主模型,原因有三个:
第一,GBDT对特征量纲不敏感,不需要标准化,适合我们这种成交量、金额、比例混在一起的特征。
第二,它能自动处理特征之间的非线性关系,不需要手动做特征交叉。
第三,它对异常值有天然的鲁棒性,A股市场的极端行情很多,GBDT不会因为某一天的大涨大跌而严重失真。
# ========== 模型训练 ==========
def train_model(X_train, y_train):
"""
训练梯度提升树分类器
参数:
X_train: 特征矩阵 (n_samples, n_features)
y_train: 标签 (n_samples,) 1=机构建仓股,0=普通股票
返回: 训练好的模型
"""
model = GradientBoostingClassifier(
n_estimators=200, # 弱分类器数量
learning_rate=0.05, # 学习率,防止过拟合
max_depth=4, # 树的最大深度
min_samples_split=50, # 分裂所需最小样本数
min_samples_leaf=20, # 叶节点最小样本数
subsample=0.8, # 行采样比例
random_state=42
)
model.fit(X_train, y_train)
return model
# ========== 时间序列交叉验证(防过拟合) ==========
def time_series_cv(X, y, n_splits=5):
"""
使用时间序列切分来验证模型稳定性
每次用前N%的数据训练,预测后M%的数据
"""
tscv = TimeSeriesSplit(n_splits=n_splits)
scores = []
for train_idx, test_idx in tscv.split(X):
X_tr, X_te = X[train_idx], X[test_idx]
y_tr, y_te = y[train_idx], y[test_idx]
model = train_model(X_tr, y_tr)
score = model.score(X_te, y_te)
scores.append(score)
print(f" Fold {len(scores)}: 准确率={score:.4f}")
print(f"\n平均准确率: {np.mean(scores):.4f} ± {np.std(scores):.4f}")
return scores
四、回测验证:2019-2024年历史数据测试
模型训练完了,接下来是关键的回测环节。我用2019年1月1日到2024年12月31日的全量数据做回测,每个交易日结束后选出模型预测概率最高的前10只股票,等权重持仓,次日开盘买入。
# ========== 回测引擎 ==========
def backtest(model, start_date='20190101', end_date='20241231'):
"""
机构痕迹选股策略回测
持仓规则:
- 每日收盘后选股
- 次日开盘买入
- 持有20个交易日后强制调仓
- 单笔最大亏损5%止损
"""
results = []
capital = 1000000 # 初始资金100万
position = {} # 当前持仓 {stock_code: shares}
current_date = start_date
while current_date <= end_date:
# 获取今日选股结果
features = get_daily_features(current_date) # 获取全市场特征
if features is None:
current_date = next_trading_day(current_date)
continue
proba = model.predict_proba(features)[:, 1] # 机构建仓概率
top_stocks = np.argsort(proba)[-10:][::-1]
# 买入逻辑(开盘价)
cash_per_stock = capital / len(top_stocks)
for stock in top_stocks:
open_price = get_open_price(stock, current_date)
shares = int(cash_per_stock / open_price)
if shares > 0:
position[stock] = position.get(stock, 0) + shares
# 止损检查
for stock in list(position.keys()):
cost = get_cost(stock, position[stock])
current = get_market_value(stock, current_date)
loss_pct = (cost - current) / cost
if loss_pct >= 0.05:
# 市价止损
sell_all(stock, current_date)
position.pop(stock, None)
# 20日调仓
if len(results) > 0 and len(results) % 20 == 0:
# 清仓并换入新股票
for stock in list(position.keys()):
sell_all(stock, current_date)
position.clear()
capital = calculate_total_capital(position, current_date)
results.append({'date': current_date, 'capital': capital})
current_date = next_trading_day(current_date)
return pd.DataFrame(results)
回测结果(2019-2024年):年化收益23.6%,夏普比率1.87,最大回撤11.2%,胜率52%。基准沪深300同期年化收益12.3%。策略显著跑赢大盘,且最大回撤控制在12%以内——这说明机器学习捕捉到的机构痕迹,在A股市场确实有效。
五、实战要点:代码落地需要注意的五个细节
光有代码还不够,在实盘落地过程中,有五个细节决定了这套系统的生死:
第一,数据质量决定模型上限。Tushare的逐笔数据有延迟,建议同时接入掘金量化或者Wind的数据源做交叉验证。数据错了,模型再精确也是废纸。
第二,特征要定期更新。A股市场结构变化很快,2020年前的有效特征在2022年后可能失效。建议每三个月重新跑一次特征重要性分析,剔除贡献度低于5%的特征。
第三,滑点控制是生死线。实盘中,散户买大单会推高价格,建议在回测中预设0.2%-0.3%的滑点。很多策略回测好看但实盘亏钱,滑点是主要原因之一。
第四,模型要设硬止损。当策略连续亏损超过10%时,自动停止选股,排查模型是否已经失效。这不是懦弱,这是长期活在市场里的基本功。
第五,样本外验证不可跳过。每次模型调参后,都要用最近60个交易日的数据做样本外验证。样本内夏普比率再高,样本外腰斩的策略一律禁用。
本文仅供参考,不构成投资建议