第 8 章 · 03 比特币大波动分类案例 本节摘要:本节用一个完整案例把前两节串起来——精读 ( ),ABU 机器学习与交易决策衔接的最佳示例。BtcBigWaveClf 解决一个具体问题:预测今天比特币会不会有大波动。它把"大波动"定义为 (日震荡 5.5%),作为分类标签 y;注入 ma5/ma10/ma21/ma60 四条均线作为特征;用三倍数据增强( / / 向前跳 1/2 条各生成一组特征)把样本量翻三倍;核心变换 把"连续 3 天 K 线"压缩成"1 条特征":用 标准化连续特征(消除绝对价格差异),第 1/2 天取全部特征改 / 前缀,第 3 天只取 open/low/preclose/dateweek + bigwave(作 y),用 把"周几"离散化成 one-hot。
本节摘要:本节用一个完整案例把前两节串起来——精读
BtcBigWaveClf(ABuMLPd.py:158-289),ABU 机器学习与交易决策衔接的最佳示例。BtcBigWaveClf 解决一个具体问题:预测今天比特币会不会有大波动。它把"大波动"定义为(high - low) / pre_close > 0.055(日震荡 5.5%),作为分类标签 y;注入 ma5/ma10/ma21/ma60 四条均线作为特征;用三倍数据增强(btc_siblings_df(raw)/raw[1:]/raw[2:]向前跳 1/2 条各生成一组特征)把样本量翻三倍;核心变换btc_siblings_df把"连续 3 天 K 线"压缩成"1 条特征":用scaler_std标准化连续特征(消除绝对价格差异),第 1/2 天取全部特征改one_*/two_*前缀,第 3 天只取 open/low/pre_close/date_week + big_wave(作 y),用pd.get_dummies把"周几"离散化成 one-hot。训练好后,AbuBTCDayBuy.fit_day(ABuFactorBuyDemo.py:210-329)在回测中调用btc_ml.predict(btc_today_x),预测为大波动才考虑买入——这是 ABU 把 ML 预测作为买入信号的标准范式。本节最后总结 ABU 的三条 ML 接入路径:① UMP 裁判用predict_proba_threshold拦截;② BtcBigWaveClf 把predict作为买入信号;③ AbuCrossVal 用相关度做策略稳健性验证。读完本节,你将看到 ABU 是怎么把第 1 节的三层架构、第 2 节的概率阈值搜索,落地成一个可回测的完整策略。
内容来源:原项目源码
abupy/MLBu/ABuMLPd.py的 BtcBigWaveClf 类(L158-289)+abupy/FactorBuyBu/ABuFactorBuyDemo.py的 AbuBTCDayBuy 类(L210-329)。
⚠️ 注意:本节是第 8 章的总结。建议先读懂前两节(三层架构 + 概率阈值搜索),再读本节。重点理解"三倍数据增强"为什么有效(比特币样本少,3 天窗口的滑动切片能在不引入未来函数的前提下扩样),以及
btc_siblings_df为什么是 ABU 全书最有"特征工程味道"的代码。
阅读完本节,你应当能够:
(high - low) / pre_close > 0.055)及 0.055 阈值的业务含义。raw / raw[1:] / raw[2:] 各跑一次 btc_siblings_df),以及为什么不引入未来函数。btc_siblings_df:3 天一组切片 → scaler_std 标准化连续特征 → 第 1/2 天改 one_*/two_* 前缀、第 3 天只取子集改 today_* 前缀 + big_wave → concat 成一条。pd.get_dummies 周几离散化的作用,以及 AbuBTCDayBuy 在运行时怎么手动 one-hot。AbuBTCDayBuy.fit_day:make_btc_today 构造特征 → btc_ml.predict → similar_predict 投票 → buy_today()。比特币日内交易的核心痛点:今天值不值得做交易。如果今天波动很小(比如全天振幅 1%),日内交易根本赚不到钱(覆盖不了手续费和滑点);只有大波动日才值得做。
BtcBigWaveClf(ABuMLPd.py:158-171)就是把这个判断 ML 化:
158 class BtcBigWaveClf(AbuMLPd): 159 """ 160 任何大的决策其实都是由很多看极起来极不起眼的小事组成的,如果我们是做比特币日内的交易者, 161 首先你需要判断今天适不适合做交易,做出这个判断的依据里有一条即是今天的波动需要足够大 162 """ 163 164 def __init__(self, **kwarg): 169 self.btc = kwarg.pop('btc', None) 170 super(BtcBigWaveClf, self).__init__(**kwarg)
BtcBigWaveClf 继承 AbuMLPd(第 1 节讲的业务层抽象基类),__init__ 把外部传入的 btc 数据 pop 出来,再调父类构造。父类 __init__ 会自动调 make_xy(下面详讲),把金融数据切成 x/y/df 三件套,然后构造 self.fiter = AbuML(...)。从此 BtcBigWaveClf 实例就拥有了 AbuML 的全部方法(__getattr__ 代理)。
180 # .055的日震荡幅度可以成做大波动的交易对比特币来说,下面对数据添加新列big_wave 181 btc['big_wave'] = (btc.high - btc.low) / btc.pre_close > 0.055 182 btc['big_wave'] = btc['big_wave'].astype(int)
(high - low) / pre_close 是日震荡率(最高价减最低价除以昨收)。> 0.055 即日震荡超过 5.5%,astype(int) 把布尔转成 0/1。5.5% 是 ABU 的经验阈值——比特币日波动大,5.5% 算"够大"的日子。
这个 0/1 就是 y 标签。注意这是个二分类问题,所以第 1 节的 entry_wrapper 会把 E_FIT_AUTO 归约成 E_FIT_CLF(因为 len(np.unique(y)) = 2 ≤ 10)。BtcBigWaveClf 不用显式声明分类,自动判断。
make_xy(ABuMLPd.py:172-237)是 BtcBigWaveClf 的核心。先看特征注入(L193-201):
193 # 下面为训练集和测试集数据都加上5,10,21,60日均线特征 194 def calc_ma(tc, p_ma): 195 ma_key = 'p_ma{}'.format(p_ma) 196 tc[ma_key] = ABuNDMa.calc_ma_from_prices(tc.close, p_ma, min_periods=1) 197 198 for ma in [5, 10, 21, 60]: 199 calc_ma(btc_train_raw, ma) 200 if btc_test_raw is not None: 201 calc_ma(btc_test_raw, ma)
注入四条均线:ma5(周线)、ma10(半月线)、ma21(月线)、ma60(季线)。这四条均线代表短/中/长期趋势。calc_ma_from_prices 是 ABu 的均线计算函数(第 9 章详讲),min_periods=1 保证开头数据不足时也能算(用部分数据)。
均线是技术分析的核心特征——它们捕捉"价格相对趋势的位置"。大波动往往发生在价格突破均线(ma5 上穿 ma60)或均线收敛后发散时,这些信息原始 OHLC 给不了,必须注入。
203 # 下面使用训练集数据btc_train_raw做为参数抽取组合特征,重新组合好的特征 204 btc_train0 = self.btc_siblings_df(btc_train_raw) 205 # 由于每3条连续交易日数据组合成一个特征,只要向前跳一条数据进行特征组合抽取即可以得到另一组新特征 206 btc_train1 = self.btc_siblings_df(btc_train_raw[1:]) 207 btc_train2 = self.btc_siblings_df(btc_train_raw[2:]) 208 209 # 把周几这个特征使用pd.get_dummies进行离散化处理 210 btc_train = pd.concat([btc_train0, btc_train1, btc_train2])
关键三行(204/206/207):
btc_train0 = self.btc_siblings_df(btc_train_raw):从第 0 行开始,每 3 天一组切。btc_train1 = self.btc_siblings_df(btc_train_raw[1:]):从第 1 行开始(跳过第 0 天),每 3 天一组切。btc_train2 = self.btc_siblings_df(btc_train_raw[2:]):从第 2 行开始(跳过前 2 天),每 3 天一组切。三组结果 concat,样本量接近三倍。为什么这有效?因为 btc_siblings_df 是按"固定 3 天一组"切的(第 0-2 天一组、第 3-5 天一组...),中间的"第 1-3 天""第 2-4 天"组合被漏掉。向前跳 1/2 条再切,就把这些漏掉的组合都纳入——本质是滑动窗口,步长从 3 降到 1。
**为什么不引入未来函数?**每个 3 天窗口里的特征都来自窗口内的 3 天(第 1/2 天的全部 + 第 3 天的子集),y 是第 3 天的 big_wave。预测时输入的是"前 2 天 + 今天部分特征",输出"今天是否大波动"——预测用的是过去和当下,没有未来信息。三倍增强只是增加了训练样本的多样性,没有改变"用过去预测当下"的因果结构。
测试集也做同样的三倍增强(L222-237),保持训练/测试特征分布一致。
💡 解剖要点:三倍数据增强是处理"金融数据样本少"的经典手法。比特币 2013-2017 年约 1400 个交易日,去掉 60 天测试集剩 1340 天。如果固定 3 天一组,只能切 446 个样本;滑动窗口(步长 1)能切 1338 个,接近三倍。这种增强对 ML 特别重要——样本越多,模型越不容易过拟合。但要注意:增强后的样本不是完全独立的(相邻窗口共享 2 天数据),所以交叉验证的 KFold 可能高估精度。ABU 没有显式处理这个相关性,是已知的统计瑕疵。
btc_siblings_df(ABuMLPd.py:240-289)是全节最精妙的函数,把 3 天 K 线压缩成 1 条 ML 特征:
240 def btc_siblings_df(self, btc_raw): 253 # 将所有交易日以3个为一组,切割成多个子df 254 btc_siblings = [btc_raw.iloc[sib_ind * 3:(sib_ind + 1) * 3, :] 255 for sib_ind in np.arange(0, int(btc_raw.shape[0] / 3))] 256 257 btc_df = pd.DataFrame() 258 for sib_btc in btc_siblings: 259 # 使用数据标准化将连续3天交易日中的连续数值特征进行标准化操作 260 sib_btc_scale = ABuScalerUtil.scaler_std( 261 sib_btc.filter(['open', 'close', 'high', 'low', 'volume', 'pre_close', 262 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14'])) 263 # 把标准化后的和big_wave,date_week连接起来 264 sib_btc_scale = pd.concat([sib_btc['big_wave'], sib_btc_scale, sib_btc['date_week']], axis=1)
btc_siblings = [btc_raw.iloc[sib_ind * 3:(sib_ind + 1) * 3, :] for sib_ind in np.arange(0, int(btc_raw.shape[0] / 3))]
列表推导,每个 sib_btc 是 3 行(3 天)的子 DataFrame。int(btc_raw.shape[0] / 3) 算能切多少组,余数(最后 1-2 天)丢弃。
sib_btc_scale = ABuScalerUtil.scaler_std( sib_btc.filter(['open', 'close', 'high', 'low', 'volume', 'pre_close', 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14']))
scaler_std 是 ABU 的标准化工具(封装 sklearn StandardScaler)。关键:标准化是按 3 天一组做的,不是全局。这消除了"绝对价格差异"——比特币 2013 年 100 美元,2017 年 4000 美元,绝对价格差 40 倍,直接用原始价格会让模型只学到"价格高=某个时期"。标准化后,模型学到的是"这 3 天内的相对形态"(涨跌、波动率),这才是跨周期稳定的信号。
filter 选 12 个连续特征:OHLCV + pre_close + 4 条均线 + 2 个 ATR(平均真实波幅)。这些都是连续数值,需要标准化。
sib_btc_scale = pd.concat([sib_btc['big_wave'], sib_btc_scale, sib_btc['date_week']], axis=1)
big_wave 是 0/1 离散标签(不标准化),date_week 是 0-6 离散(周几,不标准化)。concat 拼回标准化后的连续特征。
266 # 抽取第一天,第二天的大多数特征分别改名字以one,two为特征前缀 267 a0 = sib_btc_scale.iloc[0].filter(['open', 'close', 'high', 'low', 'volume', 'pre_close', 268 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14', 'date_week']) 269 a0.rename(index={'open': 'one_open', 'close': 'one_close', 'high': 'one_high', 'low': 'one_low', 270 'volume': 'one_volume', 'pre_close': 'one_pre_close', 271 'ma5': 'one_ma5', 'ma10': 'one_ma10', 'ma21': 'one_ma21', 272 'ma60': 'one_ma60', 'atr21': 'one_atr21', 'atr14': 'one_atr14', 273 'date_week': 'one_date_week'}, inplace=True) 274 275 a1 = sib_btc_scale.iloc[1].filter([...]) # 同 a0,前缀改 two_ 281 282 # 第三天的特征只使用'open', 'low', 'pre_close', 'date_week',该名前缀today 283 a2 = sib_btc_scale.iloc[2].filter(['big_wave', 'open', 'low', 'pre_close', 'date_week']) 284 a2.rename(index={'open': 'today_open', 'low': 'today_low', 285 'pre_close': 'today_pre_close', 286 'date_week': 'today_date_week'}, inplace=True)
三天的处理策略不同:
| 天 | 取哪些特征 | 改什么前缀 | 为啥 |
|---|---|---|---|
| 第 1 天(前天) | 全部 12 连续 + date_week | one_* |
历史完整信息,全要 |
| 第 2 天(昨天) | 全部 12 连续 + date_week | two_* |
历史完整信息,全要 |
| 第 3 天(今天) | 只 big_wave + open + low + pre_close + date_week | today_* |
刻意不全 |
第 3 天只取 open/low/pre_close 是反常识的——明明有 close/high 为什么不要?答案是避免泄漏当天完整信息。big_wave 是用 (high - low) / pre_close > 0.055 算的,如果第 3 天带上 high,模型直接学"high - low > 0.055 × pre_close → big_wave=1",变成恒等式,没有任何预测价值。
实战中,AbuBTCDayBuy 是当日买入(下面讲),决策时刻只有 open(开盘能知道)。low 是盘中实时更新,close/high 要收盘才知道。所以第 3 天的特征必须是"决策时刻能拿到的信息"——open + pre_close + date_week 是开盘就确定的,low 是盘中近似可得(虽然收盘才精确)。big_wave 作为 y,只在训练时用(运行时预测不算 big_wave,而是预测它)。
288 btc_df = btc_df.append(pd.concat([a0, a1, a2], axis=0), ignore_index=True)
a0(13 字段)+ a1(13 字段)+ a2(5 字段)= 31 字段,组成一条。a0/a1/a2 都是 Series(concat axis=0 拼成 31 长的 Series),append 到 btc_df 成一行。循环完所有 3 天组,btc_df 就是 ML 用的特征矩阵。
211 dummies_one_week = pd.get_dummies(btc_train['one_date_week'], prefix='one_date_week') 212 dummies_two_week = pd.get_dummies(btc_train['two_date_week'], prefix='two_date_week') 213 dummies_today_week = pd.get_dummies(btc_train['today_date_week'], prefix='today_date_week') 214 btc_train.drop(['one_date_week', 'two_date_week', 'today_date_week'], inplace=True, axis=1) 215 # make_xy中需要确定self.df 216 self.df = pd.concat([btc_train, dummies_one_week, dummies_two_week, dummies_today_week], axis=1) 217 # make_xy中需要确定x, y 218 train_matrix = self.df.as_matrix() 219 self.y = train_matrix[:, 0] 220 self.x = train_matrix[:, 1:]
pd.get_dummies 把"周几"(0-6 整数)展开成 7 列 one-hot(monday=0 → [1,0,0,0,0,0,0])。三个周几字段(第 1/2/3 天)各展开 7 列,共 21 列。drop 掉原始的整数列,concat one-hot 列。
为什么要 one-hot?因为 date_week 是离散类别,不是有序数值——周一(0)和周二(1)在数值上差 1,但语义上没有"周二比周一大"的关系。如果直接用整数,模型(尤其是线性模型、距离模型如 KNN)会误以为有大小关系。one-hot 让每个周几独立成一维,模型可以学到"周一容易大波动""周末波动小"这种周期性模式。
219-220 行遵循 ABU 的硬约定:y 在第一列(big_wave,因为 btc_siblings_df 里 a2 第一个字段是 big_wave),x 是其余列。这跟第 1 节讲的 list(self.df.columns)[1:] 跳过 y 完全一致。
训练好 BtcBigWaveClf 后,怎么在回测里用?AbuBTCDayBuy(ABuFactorBuyDemo.py:210-329)给出范式:
210 class AbuBTCDayBuy(AbuFactorBuyBase, BuyCallMixin): 211 """ 212 比特币日交易策略: 213 1. 买入条件1: 当日这100个股票60%以上都是上涨的 214 2. 买入条件2: 使用在第12节:机器学习与比特币示例中编写的:信号发出今天比特币会有大行情 215 """ 216 237 def fit_day(self, today): 242 # 忽略不符合买入的天(统计周期内前两天, 因为btc的机器学习特证需要三天交易数据) 243 if self.today_ind < 2: 244 return None 245 246 # 今天,昨天,前天三天的交易数据进行特证转换 247 btc = self.kl_pd[self.today_ind - 2:self.today_ind + 1] 248 # 三天的交易数据进行转换后得到btc_today_x 249 btc_today_x = self.make_btc_today(btc) 250 251 # btc_ml并没有在这里传入,实际如果要使用,需要对外部的btc_ml进行本地序列化后,构造读取本地 252 # 买入条件2: 使用在第12节:机器学习与比特币示例中编写的:信号发出今天比特币会有大行情 253 if btc_ml.predict(btc_today_x): 254 # 买入条件1: 当日这100个股票60%以上都是上涨的 255 vote_val = self.similar_predict(today.date) 256 if vote_val > self.btc_vote_val: 257 # 没有使用当天交易日的close等数据,且btc_ml判断的大波动是当日,所以当日买入 258 return self.buy_today()
两个买入条件 AND:
make_btc_today 把回测中的 3 天 K 线用与训练时相同的逻辑(标准化 + one/two/today 前缀 + 周几 one-hot)转成特征向量。两个条件都满足,buy_today()(258 行)当日买入。注意是 buy_today 不是 buy_tomorrow——因为比特币 7×24 交易,且大波动判定是当日的,信号出来当天就得买(第 3 章讲过 buy_today 适合 7×24 市场)。
make_btc_today 是 btc_siblings_df 的"单样本运行时版"——训练时批量处理,运行时一次处理一条:
261 def make_btc_today(self, sib_btc): 265 sib_btc['big_wave'] = (sib_btc.high - sib_btc.low) / sib_btc.pre_close > 0.55 # ← 注意 0.55 不是 0.055 266 sib_btc['big_wave'] = sib_btc['big_wave'].astype(int) 267 sib_btc_scale = ABuScalerUtil.scaler_std( 268 sib_btc.filter(['open', 'close', 'high', 'low', 'volume', 'pre_close', 269 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14']))
注意 265 行的 0.55 是源码笔误(训练时是 0.055),但这无所谓——运行时 big_wave 不参与预测(只是特征构造流程的一部分),predict 用的 x 会去掉 y 列(L310 [:, 1:])。这种小瑕疵在 ABU 源码里偶尔出现,读源码时要警惕。
之后流程与 btc_siblings_df 完全一致:scaler_std 标准化 → a0/a1 取全部改 one_/two_ → a2 取子集改 today_ → concat。唯一区别是周几 one-hot(L298-309):
298 dummies_week_col = btc_ml.df.filter(regex='(^one_date_week_|^two_date_week_|^today_date_week_)').columns 299 dummies_week_df = pd.DataFrame(np.zeros((1, len(dummies_week_col))), columns=dummies_week_col) 300 301 # 手动修改每一天的one hot 302 one_day_key = 'one_date_week_{}'.format(btc_today.one_date_week.values[0]) 303 dummies_week_df[one_day_key] = 1
训练时用 pd.get_dummies 自动 one-hot(因为批量),运行时手动 one-hot(因为只有一条):先创建全 0 的 DataFrame(列名从 btc_ml.df 抄,保证列顺序一致),再根据实际周几把对应列置 1。这种"训练自动 + 运行时手动"的设计,保证了运行时特征列与训练时完全对齐(否则 predict 会因列数不匹配报错)。
💡 解剖要点:make_btc_today 揭示了 ML 工程化的核心难点——训练特征构造与运行时特征构造必须完全一致。同一个标准化逻辑(scaler_std)、同一个前缀规则(one/two/today)、同一个 one-hot 列顺序。任何一处不一致都会让 predict 失效。ABU 用"复制 btc_siblings_df 的逻辑"实现一致性,代价是两份代码需要同步维护。生产级 ML 系统通常用 sklearn Pipeline + ColumnTransformer 把特征工程流水线化,避免这种复制。ABU 受限于代码风格,选择了复制路线。
为什么 AbuBTCDayBuy 要 AND 两个条件,而不是只用 ML?答案是信号正交性。ML 学的是"基于历史 K 线形态,今天是否大波动";similar_predict 学的是"基于市场其他股票,今天比特币是否被同步推动"。两个信号来源完全不同(技术面 vs 市场情绪),误判的相关性低。AND 之后,只有"ML 说是大波动 + 市场也确认"才买入,大幅降低误判。
similar_predict(ABuFactorBuyDemo.py:312-328)的实现:对 btc_similar_top 里每个相关股票,查它今天涨还是跌,乘以 vote_direction(负相关股票反向),统计"方向一致的比例"。1 - False 占比 = 支持率。这跟第 6 章的"市场宽度"思路一脉相承。
至此,第 8 章的三条 ML 接入路径全部讲完。汇总如下:
| 路径 | 接入点 | ML 角色 | 技术核心 |
|---|---|---|---|
| ① UMP 拦截 | 回测 make_buy_order 钩子 | 决策是否拦截已发出的订单 | GMM 聚类找危险簇 / predict_proba_threshold 高放行低拦截 |
| ② 买入信号 | 买入因子 fit_day | 决策是否买入 | btc_ml.predict(btc_today_x) 作为 AND 条件之一 |
| ③ 稳健性验证 | 回测后分析 | 决策策略是否可信 | AbuCrossVal 用策略间相关度判断过拟合 |
路径①是第 7 章的主题——UMP 裁判用 GMM(主裁)或 KNN(边裁)学习历史交易特征,对新订单 predict_proba_threshold 拦截。本节第 2 章讲的 cross_val_prob_accuracy_score + search_match_pos_threshold 就是 UMP 找最优阈值的工具。
路径②是本节的主题——BtcBigWaveClf 把 ML 预测直接作为买入信号。AbuBTCDayBuy 的 fit_day 调 btc_ml.predict,这是 ABU 唯一一个"ML 直接决定买入"的示例因子。
路径③AbuCrossVal 不在本章范围(属于第 11 章回测分析),但它用 ML 的相关度做策略过拟合检测——把多个策略的收益序列算相关矩阵,相关性高的策略组(>0.7)很可能是对同一批数据的过拟合,实盘会同时失效。这是 ML 在"策略评估"层的应用。
三条路径共同体现 ABU 的设计哲学:ML 不是孤立的预测模型,而是嵌入回测各环节的决策辅助。从"要不要买"(路径②)、"买了要不要拦"(路径①)、"策略可不可信"(路径③),ML 在每个决策点都提供服务。这种"ML as a service"的架构,让 ABU 的回测流水线具备了"自我进化"的能力——回测越多,UMP 越聪明,买入信号越准,策略评估越可靠。
(high - low) / pre_close > 0.055(日震荡 5.5%),astype(int) 转 0/1。继承 AbuMLPd,自动归约成 CLF。btc_siblings_df(raw) / raw[1:] / raw[2:] 各切一次。本质是滑动窗口(步长 1 替代步长 3),把样本量翻三倍,不引入未来函数(每个窗口只用过去和当下信息)。第 8 章到此结束。下一章(第 9 章)讲 ABU 的技术分析模块(TLineBu 趋势线、IndicatorBu 指标),它们为 ML 提供更多特征(均线、ATR、MACD 等),也为买入/卖出因子提供更多触发条件。