news 2026/10/4 8:04:21

缠论自动交易系统实战:Python量化实现买卖点识别与回测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
缠论自动交易系统实战:Python量化实现买卖点识别与回测

1. 缠论自动交易:这套系统到底干了什么

缠论在国内交易圈子里一直是个非常有争议的话题。有人把它捧成“交易圣杯”,有人觉得它不过是事后画图的说书理论。我的态度比较务实:缠论本质上是一套基于K线形态递归推导出来的趋势判断框架,它把走势拆解成“分型、笔、线段、中枢、背驰、买卖点”这一整套自底向上的结构。这套结构有一个很关键的特点——它是有明确规则的。有规则,就有量化实现的前提。而一旦能用Python把分型、笔、中枢、背驰这些概念用代码固定下来,就完全可以做成一套自动识别买卖信号、甚至自动下单的交易系统。

这套系统我前前后后写了三个多月,中间推翻重来了两次。目前已经实现了从行情数据获取、缠论结构实时计算、买卖点信号生成、到模拟盘/实盘自动下单的全部流程。这篇文章会把整个项目的设计思路、核心代码、回测细节、以及我踩过的坑一次讲清楚。无论你打算自己写一套缠论自动交易系统,还是只是想了解“缠论到底能不能量化”,这篇文章都值得你看完。

适合谁看?首先是懂一点Python基础、同时对缠论技术分析感兴趣的人;其次是已经在做量化交易、想引入形态识别因子的开发者;最后就是那些“听说缠论很厉害但不知从何入手”的小白,看完至少能明白缠论量化不是什么玄学,它就是一个带规则的信号识别引擎。

2. 缠论量化的底层设计思路

2.1 为什么选择Python而不是其他语言

做金融交易系统,语言选型其实是个很容易纠结的问题。C++性能强但开发效率低;Java生态成熟但表达繁琐;而Python的优势在于:数据分析库齐全、代码表达力强、实盘接口生态丰富。对一个以“信号计算和策略逻辑”为核心的缠论系统来说,Python完全是够用的。

在实际项目中,pandas负责K线数据处理和滑动窗口计算,numpy处理数组运算,matplotlib或pyecharts做可视化,ccxt或tushare/akshare做数据接入,后端如果用券商接口就用官方API或者easytrader这类封装库。缠论本身的计算逻辑虽然复杂,但核心还是K线关系比较和递归划分,这些操作在Python里可以写得非常直观。

性能方面,单品种单周期的实时计算在毫秒级完成,完全不会成为瓶颈。如果未来要扩展到多品种多周期,再用多进程或者把核心计算下沉到numba/Cython也不迟。我的原则是:先用Python把逻辑跑通,确认策略有效后,再考虑性能优化。

2.2 系统的整体架构与模块划分

这套系统我按照数据流的方向分成了四个模块,分别是数据源模块、特征计算模块、信号生成模块、执行与风控模块。

数据源模块负责拉取K线数据,并处理除权除息、停牌、脏数据过滤等问题。特征计算模块是核心中的核心,它负责把原始K线一步步转化成缠论结构状态,比如当前分型是否成立、笔是否已经生成、中枢区间在哪里、是否出现背驰。信号生成模块基于特征计算的结果,判断是否触发第一、二、三类买卖点信号。执行与风控模块拿到信号后,结合当前持仓、账户资金和风控规则,决定是否下单以及下多少手。

这四层各司其职,层与层之间通过标准化的数据对象通信,比如特征计算层输出一个“TradingState”对象,里面保存了当前所有缠论结构的状态;信号层读取这个对象之后,输出一个“Signal”对象给执行层。这样做的最大好处是每一层都可以独立测试和替换,比如你想换一个数据源,只需要改数据源模块;想优化买卖点判断,只需要改信号生成模块,不影响其他部分。

对比一下“拿到信号就直接撸代码下单”的做法:那种方式写Demo很快,但后续每改一个参数都可能引入新的bug,数据出错时也很难定位问题。分层架构前期开发成本高一些,但系统的稳定性和可维护性会好得多,值得付出这点成本。

2.3 数据源与K线周期的选择逻辑

我在数据源上做了两套兼容:一套是A股和港美股用的tushare/akshare,另一套是数字货币用的ccxt。股票数据需要小心除权除息,因为前复权、后复权、不复权三种模式下K线价格完全不同,缠论的分型判断对价格连续性的要求很高,直接用不复权数据在除权日会出现巨大的假分型。数字货币则要注意不同交易所的K线对齐规则,有些交易所的K线起始时间是按UTC对齐,有些是本地时间,混用会导致结构错乱。

周期选择上,我主要跑30分钟和60分钟级别,原因很简单:1分钟和5分钟级别噪音太多,分型频繁失效,笔的划分不稳定,交易成本也会吃掉利润;日线级别信号太少,一个月可能都出不了几次买卖点,资金利用率低。30分钟级别在信号频率和稳定性之间相对平衡,回测效果也最稳。当然,这个结论只针对我测试的品种,不同品种的最优周期可能会有差异,建议大家做参数扫描。

3. 核心公式与关键代码实现

3.1 分型识别:一切递归的基础

缠论的整个结构是递归向上的,而递归的起点就是分型。分型的定义很明确:

  • 顶分型:三根连续K线,中间那根的最高价最高,且中间那根的最低价也最高。
  • 底分型:三根连续K线,中间那根的最低价最低,且中间那根的最高价也最低。

但要真正写成代码,有几个细节必须处理干净。第一是包含关系处理。缠论要求先处理K线的包含关系,也就是相邻两根K线如果一根的最高价和最低价都大于另一根,就视为包含,需要合并成一根新K线。第二是包含关系的方向依赖前一根K线处于上升还是下降趋势,所以不能单纯比较当前两根K线,必须维护一个“当前方向”的状态。第三是合并之后的分型判断需要在新合并的K线序列上进行,不能直接用原始K线。

以下是我实现的分型识别核心代码,使用pandas向量化处理包含关系的基础上做逐根判断:

import pandas as pd import numpy as np def process_contains(df): """ 处理K线包含关系,返回处理后的K线序列。 df需要包含high、low两列,按时间升序排列。 返回的DataFrame中,merged_flag记录该K线是否由合并产生。 """ highs = df['high'].values lows = df['low'].values dates = df.index new_rows = [] i = 0 direction = 1 # 1=向上,-1=向下,用于决定包含合并的方向 while i < len(df): if i == 0: new_rows.append({ 'high': highs[i], 'low': lows[i], 'date': dates[i], 'merged_flag': False }) i += 1 continue prev = new_rows[-1] cur_high, cur_low = highs[i], lows[i] # 判断包含:前一根包含当前,或当前包含前一根 if (prev['high'] >= cur_high and prev['low'] <= cur_low) or \ (cur_high >= prev['high'] and cur_low <= prev['low']): # 有包含关系,按方向取极值合并 if direction > 0: new_high = max(prev['high'], cur_high) new_low = max(prev['low'], cur_low) else: new_high = min(prev['high'], cur_high) new_low = min(prev['low'], cur_low) # 用合并后的新K线替换前一根 new_rows[-1] = { 'high': new_high, 'low': new_low, 'date': prev['date'], 'merged_flag': True } else: # 无包含关系,正常推进并更新方向 if cur_high > prev['high']: direction = 1 else: direction = -1 new_rows.append({ 'high': cur_high, 'low': cur_low, 'date': dates[i], 'merged_flag': False }) i += 1 return pd.DataFrame(new_rows)

这段代码里有两处容易踩坑。第一,包含关系的方向判断必须依赖“上一次无包含关系时的方向”,而不是当前合并的方向。第二,合并之后前一根K线可能继续与后续K线形成包含关系,所以合并操作需要循环处理,代码里用“更新new_rows最后一个元素”实现了这个逻辑。

处理完包含关系之后,就可以在合并后的K线序列上找分型:

def find_fractals(processed_df): """ 在已处理包含关系的K线序列上识别分型。 返回DataFrame,每行是一个分型点,type=top表示顶分型,type=bottom表示底分型。 """ highs = processed_df['high'].values lows = processed_df['low'].values dates = processed_df['date'].values fractals = [] for i in range(1, len(processed_df) - 1): left_h, mid_h, right_h = highs[i-1], highs[i], highs[i+1] left_l, mid_l, right_l = lows[i-1], lows[i], lows[i+1] if mid_h > left_h and mid_h > right_h and mid_l > left_l and mid_l > right_l: fractals.append({'type': 'top', 'index': i, 'date': dates[i], 'price': mid_h}) elif mid_l < left_l and mid_l < right_l and mid_h < left_h and mid_h < right_h: fractals.append({'type': 'bottom', 'index': i, 'date': dates[i], 'price': mid_l}) return pd.DataFrame(fractals)

3.2 笔的划分:关键到底怎么取舍

分型是结构的最小单元,但真正用于判断买卖点的基础结构是“笔”。笔的定义是:相邻的顶分型和底分型之间,至少包含一根独立K线(也就是顶底分型之间至少有5根K线),且顶分型的最高点必须大于底分型的最高点,底分型的最低点必须小于顶分型的最低点,这样一顶一底连起来就构成一笔。

笔的作用是过滤掉微小波动,把趋势抽象成相对清晰的上涨阶段和下跌阶段。如果没有笔的概念,直接在分型级别找买卖点,信号会多到失控;有了笔之后,你关注的是“从某个底分型到某个顶分型”这一段明确方向,只有在笔的级别上标志性反转才有意义。

我见过的缠论实现里,对笔的判定五花八门,最主要的分歧是“新笔”和“老笔”的差别。老笔要求顶底分型之间至少有一根独立K线,且这5根K线之间不存在任何交叉;新笔则放宽到“只需要有独立K线即可”。我最终采用的是老笔的严格定义,因为自动交易系统最怕的就是信号频繁误触发,宁可少给一些信号,也要保证信号的有效性。如果你追求灵敏度,可以考虑新笔,但要做好回测验证,别裸奔上实盘。

笔的代码实现通常是一个递归或循环的状态机。核心逻辑是:遍历分型序列,维护当前的“最后一个分型”,遇到同类型分型时保留更极端的那个,遇到反向分型且满足K线数量要求时生成一笔:

def build_bi(fractals, kline_df): """ 根据分型序列和原始K线构建笔序列。 fractals: find_fractals的返回值 kline_df: 已处理包含关系的K线DataFrame,索引为K线序号 返回笔序列,每一项是(bi_type, start_price, end_price, start_index, end_index) """ bis = [] last_fractal = None for _, f in fractals.iterrows(): idx = f['index'] if last_fractal is None: last_fractal = f continue # 顶后遇顶,保留更高的顶 if last_fractal['type'] == 'top' and f['type'] == 'top': if f['price'] > last_fractal['price']: last_fractal = f continue # 底后遇底,保留更低的底 if last_fractal['type'] == 'bottom' and f['type'] == 'bottom': if f['price'] < last_fractal['price']: last_fractal = f continue # 反向分型,检查K线数量是否满足一笔 min_k = 5 # 顶底分型之间至少包含5根K线 if abs(idx - last_fractal['index']) >= min_k: bis.append({ 'type': 'up' if last_fractal['type'] == 'bottom' else 'down', 'start_index': last_fractal['index'], 'end_index': idx, 'start_price': last_fractal['price'], 'end_price': f['price'] }) last_fractal = f return bis

这里有个值得细说的细节:很多实现在生成笔之后就直接把前一笔固定下来,但我实际测试发现,最新一笔经常会随着新K线的产生而发生变化——比如原本以为已经成型的顶分型,可能因为后续K线包含关系的合并而被推翻。解决方案是:已经确定的历史笔保持不变,但最新的一笔始终处于“未完成”状态,只有在新分型出现并且满足反向条件时,才把上一笔正式确认。这个实时更新的状态机逻辑比“一次性扫全量数据”复杂得多,但它才是实盘能用的版本。

3.3 中枢识别与区间确认

笔和线段之上,是操作级别的核心结构——中枢。中枢的定义是:至少有三笔重叠的价格区间。这个区间由前三笔决定:前一笔的起点和后一笔的终点(或者说前三个次级别走势的重叠部分)构成中枢的上下沿。

为什么中枢重要?因为缠论的全部买卖点本质上都是围绕中枢来定义的。第一类买点出现在趋势最后一个中枢之后的背驰段;第二类买点出现在中枢下方回调稳定之后;第三类买点出现在中枢突破之后回踩确认的位置。没有中枢,就没法定义“趋势是否背驰”“突破是否有效”,所以中枢识别是信号生成的直接前置条件。

中枢的代码识别其实是个区间集合操作。我们维护一个“已进入中枢的笔列表”,每当出现新的一笔时,检查它和之前若干笔的重叠区间是否形成至少三笔重叠:

def identify_zs(bis): """ 从笔序列中识别中枢。 中枢合并规则:有重叠的连续笔构成一个中枢。 返回中枢列表,每个中枢是(zg, zd, start_idx, end_idx) zg=中枢上沿,zd=中枢下沿 """ zs_list = [] i = 0 while i < len(bis) - 2: # 取前三笔判断是否重叠 b1, b2, b3 = bis[i], bis[i+1], bis[i+2] overlap_high = min(b1['start_price'], b1['end_price'], b2['start_price'], b2['end_price'], b3['start_price'], b3['end_price']) overlap_low = max(b1['start_price'], b1['end_price'], b2['start_price'], b2['end_price'], b3['start_price'], b3['end_price']) if overlap_high > overlap_low: # 三笔有重叠,合并成中枢区间 zg = overlap_high zd = overlap_low end = i + 2 # 扩展中枢:后续笔如果与当前中枢区间有重叠,并入 while end + 1 < len(bis): next_b = bis[end + 1] nb_high = max(next_b['start_price'], next_b['end_price']) nb_low = min(next_b['start_price'], next_b['end_price']) if nb_low <= zg and nb_high >= zd: zg = min(zg, nb_high) zd = max(zd, nb_low) end += 1 else: break zs_list.append({ 'zg': zg, 'zd': zd, 'start_idx': i, 'end_idx': end }) i = end + 1 else: i += 1 return zs_list

注意我这里的zg和zd取的是重叠区间的上沿和下沿,但不同缠论流派对中枢区间的几何定义略有区别。有些直接用前三笔的最低点和最高点作为区间,有些则严格用重叠部分。我用重叠部分的好处是更贴近缠论原文“重叠区间”的意思,回测中也更稳健。

3.4 买卖点判断与背驰计算

买卖点判断是整个系统最后一步,也是最难写的一步。我实现了三类买卖点,但其中最核心、也最能体现缠论特色的是一类买卖点的背驰判断。

先讲背驰。背驰指的是价格创了新高或新低,但内在动能已经减弱。缠论原文里用MACD面积来辅助判断:当价格创新高时,如果第二段上涨对应的MACD柱子总面积小于第一段上涨对应的总面积,就构成背驰。这个比较要发生在“同一个中枢”之前和之后,才有意义。

我用代码实现背驰时,先把从当前中枢离开的那一段笔找出来,计算这段笔对应时间范围内的MACD柱子面积(取正值的和,因为上涨看红柱,下跌看绿柱),再和进入中枢前的那一段同向走势的面积做对比。核心代码如下:

def compute_beichi(macd_hist, bi_start, bi_end): """ 计算一段笔内的MACD柱子总面积。 macd_hist: MACD柱的序列 bi_start, bi_end: 笔对应的K线索引 """ area = np.sum(np.abs(macd_hist[bi_start:bi_end + 1])) return area def judge_bi1_buy(macd_hist, zs, bis, current_bi): """ 判断是否出现第一类买点。 条件:当前笔是新低,且从最后一个中枢向下的这一笔MACD面积小于前一段向下笔的面积。 """ if current_bi['type'] != 'down': return False # 找到最近一个中枢 if not zs: return False last_zs = zs[-1] # 找到中枢前的同向笔和中枢后的同向笔 prev_down = None curr_down = None for b in bis: if b['end_index'] < last_zs['start_idx'] and b['type'] == 'down': prev_down = b if b['start_index'] > last_zs['end_idx'] and b['type'] == 'down' and b['end_index'] <= current_bi['end_index']: curr_down = b if prev_down is None or curr_down is None: return False area_prev = compute_beichi(macd_hist, prev_down['start_index'], prev_down['end_index']) area_curr = compute_beichi(macd_hist, curr_down['start_index'], curr_down['end_index']) # 价格创新低,但面积缩小 if curr_down['end_price'] < prev_down['end_price'] and area_curr < area_prev: return True return False

这段代码有个明显的简化:它只比较了“一段笔”的面积,没有严格区分“进入中枢”和“离开中枢”的走势是否属于同一个级别。实战中我会加一个级别过滤:如果离开中枢的笔相比进入中枢的笔,中间还嵌套了一个更小级别的中枢,那么级别不匹配,不构成严谨的背驰。这个过滤逻辑很复杂,但它是提高精度的关键。

第二类买点的代码逻辑相对简单:第一类买点出现之后,价格会出现一段次级别反弹,然后回调到不创新低的位置,我在回调的底分型处触发买入。第三类买点则出现在中枢被向上突破后、价格回调到中枢上沿之上时,这是“趋势确立”的信号。

4. 回测与实战中的细节打磨

4.1 一套可复现的回测流程

回测是检验策略的有效手段,但很多新手会把回测做得过于理想化。我的回测框架并不复杂,流程是:加载历史K线数据,按时间顺序逐根推进,每来一根新K线,重新计算当前缠论结构,判断是否有信号产生。有信号就记录,并在下一根K线开盘时以开盘价模拟成交,同时扣除手续费和滑点。

滑点这个参数很多人会忽略,但实盘和回测差异最大的就是这里。我用的经验值是:流动性好的品种滑点设为固定价差的1.5倍,流动性差的品种设为2到3倍。手续费方面,股票要计算佣金和印花税,期货要计算交易所手续费和保证金,数字货币则要区分taker和maker费率。

下面是我做回测时的一段核心流程代码,用起来就是“喂数据、给参数、跑一遍、看曲线”:

def run_backtest(kline_df, macd_hist, fee_rate=0.0003, slippage=0.0005): """ kline_df: 历史K线数据,按时间升序 macd_hist: 对应的MACD柱序列 fee_rate: 单边手续费率 slippage: 滑点比例 """ signals = [] # 记录每次信号 position = 0 # 当前持仓 equity = 100000.0 # 初始资金 stock = 0 # 持仓数量 entry_price = None for i in range(len(kline_df)): if i < 50: # 前50根K线用于预热指标 continue # 逐步构建缠论结构 processed = process_contains(kline_df.iloc[:i+1]) fractals = find_fractals(processed) bis = build_bi(fractals, kline_df.iloc[:i+1]) zs_list = identify_zs(bis) # 判断买卖点 if position == 0: if judge_bi1_buy(macd_hist[:i+1], zs_list, bis, bis[-1]): price = kline_df.iloc[i]['open'] * (1 + slippage) stock = equity / price / 100 * 100 # 按股数取整 stock = int(stock // 100 * 100) cost = stock * price * (1 + fee_rate) if cost <= equity: equity -= cost position = 1 entry_price = price signals.append((i, 'buy', price)) else: if judge_bi1_sell(macd_hist[:i+1], zs_list, bis, bis[-1]): price = kline_df.iloc[i]['open'] * (1 - slippage) income = stock * price * (1 - fee_rate) equity += income position = 0 signals.append((i, 'sell', price)) return equity, signals

这个回测是单次全量扫描,没有处理“最新一笔可能未完成”的问题,所以回测信号看起来会比实盘更多一点。我更推荐的模式是“滚动窗口实盘模拟”——每来一根新K线,只计算一次当前状态,判断信号,然后等待下一根。这种模式下的表现和实盘更接近。

4.2 止损、仓位与资金管理的硬性规则

信号判断只是系统的一部分,真正决定账户生死的是仓位管理和风控。我第一次用这套系统做模拟交易时,因为没有设止损,一个长下影线的假信号就吃掉了两个月的利润。后来我强制给自己加了三条硬性规则,每条都写死在代码里:

第一条是单笔最大亏损不超过账户净值的1%。用ATR指标动态计算止损距离,止损价等于入场价减去1.5倍的ATR,再反推仓位大小。如果按公式算出来的仓位超过总资金的30%,就强制降到30%。这套规则的本质是让任何一笔交易的亏损都在可控范围内,避免“扛单”和情绪化交易。

第二条是连续亏损三次之后,系统自动暂停开仓两天。我实测发现,缠论结构在震荡市会连续给假信号,三个信号中至少有两次是亏损的。强制休息两天可以让结构重新整理,也避免在同一段行情里反复被打止损。

第三条是持仓浮盈超过1.5倍盈亏比之后,启动移动止损。这样做的好处是让利润奔跑,同时又能保证大波段回撤时不会亏掉大部分浮盈。这里的参数都偏保守,适合你没有完全信任某个品种的时候;等跑出稳定数据了,再根据品种特性做微调。

以下是我在系统里固定的止损止盈配置文件,用一套参数控制所有品种的初始风险:

risk_config = { 'max_loss_per_trade_pct': 0.01, # 单笔最大亏损占净值1% 'max_position_pct': 0.30, # 最大仓位30% 'atr_multiplier_stop': 1.5, # 止损距离 = 1.5倍ATR 'profit_risk_ratio': 1.5, # 移动止损触发阈值 'max_consecutive_losses': 3, # 连续亏损3次暂停 'pause_after_losses_days': 2, # 暂停2个交易日 }

4.3 实际运行中踩过的坑

第一个坑是未来函数。我在早期版本里直接对整个历史数据一次性计算缠论结构,然后把每一个“历史买卖点”都当作有效信号拿去做回测。这看起来没问题,但里面藏着一个很隐蔽的bug:当一笔新K线还没有收盘时,你用“这一根K线的最终最高价”去判断分型,实际上已经用了未来信息。要修正就必须在实时模式下用“已收盘K线”参与计算,正在跳动的那根K线只能用于触发,不能用于确认。我最后用一个简单的办法解决了:凡是需要确认结构变化的信号,统一延迟一根K线;也就是说,当前K线收盘后,在下一根K线开盘时执行操作。

第二个坑是数据源K线时间对齐问题。股票数据里,一根日K线的日期是当天的交易日期,而30分钟K线的结束时间在不同数据商之间可能差几分钟;数字货币更明显,部分交易所的K线时间是本交易所所在时区,部分统一成UTC。我一开始没注意,导致分型识别时有两根K线重复或漏掉,结构完全错乱。后面统一在数据入库时把时间戳归一化到交易所本地时间,再做一次去重和重采样。

第三个坑是除权除息日的假分型。A股遇到高送转或者大比例分红,前后的K线价格会出现跳空,如果不做前复权处理,就会在除权日产生一个巨大的顶分型或底分型,进而触发假信号。系统的数据源模块里我加了一行复权设置,tushare里取前复权数据,akshare里用qfq参数,这样K线的连续性才有保证。

第四个坑是“背驰之后还有背驰”。单看MACD面积缩小并不一定意味着趋势马上反转,尤其在强单边行情中,背驰之后继续走背驰是常有的事。我的应对是:在触发一类买卖点之后,不立即满仓进场,而是先下一小部分试探仓,等价格进一步确认(比如形成二买/二卖结构)再补仓。这就把“判断信号”和“执行加仓”分成了两个独立步骤,大幅减少了在趋势中段被甩下车的概率。

5. 高频踩坑排查与优化实录

5.1 典型问题速查表

把实盘运行中遇到的典型问题整理成一张表,供直接对照:

问题现象可能原因排查思路与解决方案
同一段K线,回测信号多、实盘信号少回测用了全量数据,包含了未来信息切换为滚动窗口模式,只在已收盘K线上做信号计算
分型频繁失效,结构重新计算后不一致包含关系处理依赖的新K线未被真正合并检查process_contains函数中循环合并逻辑,确保方向是动态更新
中枢区间频繁跳变笔的最新一笔未确认就参与中枢计算将最新一笔标记为“未完成”,中枢计算只使用已确认的笔
某一天信号特别多,连续开仓数据源包含重复K线或未除权数据数据入库时加时间戳唯一性校验和复权处理
数字货币信号和交易所K线对不上K线时间对齐方式不同统一使用交易所服务器时间,去除本地时区干扰
背驰信号触发后价格继续同向大幅波动背驰级别不匹配,可能只是小级别背驰增加级别过滤,只在大级别离开中枢结构成立时触发一类买卖点
程序运行几天后内存增长明显每次新K线都重新计算全量结构,没有增量更新改为增量计算:只更新最近几十根K线,不再全量扫描
模拟盘盈利,实盘亏损没有考虑手续费和滑点,或者执行延迟回测中加入手续费/滑点模型,实盘使用限价单等待成交

5.2 从“能跑”到“跑得稳”的优化经验

代码层面的优化主要做三件事。第一件是把K线数据处理改成增量更新。最初版本每次来一根新K线都重新计算全量结构,运行一段时间之后程序越跑越慢。后来我改成只保留最近500根K线的窗口作为计算范围,结构状态用JSON序列化保存到本地,每次新K线到达时加载上次的状态并继续推进,耗时从秒级降到了毫秒级。

第二件是引入多周期共振过滤。单看30分钟级别的三类买卖点,在趋势末期经常会给出假信号。我在信号生成层加了一个更高周期(比如4小时)的趋势过滤:如果4小时级别是上涨趋势中,只做多不做空;如果是下跌趋势中,只做空不做多。这样一来,同一时间段内30分钟出现的买卖点只有在与4小时同向时才会被接受,信号数量大幅减少,但胜率有可感知的提升。

第三件是在信号触发时增加“结构完整性校验”。这个校验会确认当前中枢、当前笔、当前分型三者之间的状态是否互相匹配,比如中枢区间是否和笔的端点合理对应。校验不通过就放弃信号,并记录一条日志。这一步看起来是“防御性编程”,但它能把那些因数据异常导致的假信号在源头拦截掉,排查问题的时候帮助尤其大。另有收益的就是日志系统,每条信号、每次下单、每次风控触发都留痕,后期复盘时可以直接还原现场,不用靠回忆。

如果真要给“从能跑到跑得稳”的过程排个优先级,我的建议是:先解决未来函数问题,再补手续费和滑点模型,然后做增量更新提升性能,最后才是用多周期过滤提高胜率。前两步是避免“假象盈利”,后两步才是“优化真实收益”。

我的体会与后续扩展方向

这套缠论自动交易系统做到现在,最大的感受是:缠论并不是什么玄学,它更像一种“带主观参数的形态学”证券分析框架。只要你愿意把规则写死,把参数固定下来,它完全可以变成一套严格的、可复现的量化策略。难点偏偏就在“把规则写死”这一步上——千人千缠,说的是每个人都用自己的偏好定义笔、线段和中枢,而在代码里没有任何“模糊地带”。

我踩过最大的坑就是用“理想化的规则”做回测,忽略了K线未闭合、数据源错位、未来函数、手续费滑点这些看起来不重要的细节。但它们恰恰是实盘和回测之间差距的全部来源。如果你在复现类似系统,先把这些“脏活”处理干净,再谈优化策略,绝对能省下大量时间。

关于这套系统后续的扩展方向,我自己有几个计划在跑:一是增加多品种组合回测,通过多个低相关品种分散风险,平滑资金曲线;二是把缠论结构作为特征输入到机器学习模型里,与传统技术信号做融合判断;三是开发一个简单的Web界面,方便随时查看当前结构的可视化状态。代码目前还在整理中,后续会考虑拆出通用版本放到开源平台分享,但前提是确保不再有未来函数这类硬伤。

最后说一句实在话:自动交易系统能帮你稳定执行规则,但没有任何规则能保证一定盈利。缠论也好,其他技术分析也罢,它们提供的是“概率优势”而不是“必然赚钱”。我的做法是先在模拟盘上连续跑两三个月,把参数跑稳了、日志跑通了,再上一小笔实盘资金做验证。安全第一,收益第二,这套系统的意义在于帮你守纪律,而不是帮你做预言。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 8:01:19

光伏组件热斑与缺陷检测数据集 | 光伏组件 热斑检测 PID识别 红外检测 光伏巡检9148期

光伏组件热斑与缺陷检测数据集 | 光伏组件 热斑检测 PID识别 红外检测 光伏巡检9148期 数据集概述 本数据集专注于红外热成像下的光伏组件热斑与缺陷检测&#xff0c;服务于光伏电站智能巡检、故障诊断及运维管理。数据涵盖热斑、潜在缺陷及光伏组件区域&#xff0c;适配无人机…

作者头像 李华
网站建设 2026/10/4 7:59:01

在家居士略感

作为一个在家居士&#xff0c;余生该如何度过&#xff0c;如何安排好这一生&#xff0c;一方面需要做一个合格的人&#xff0c;一方面还需要不忘记回家的路&#xff0c;念佛回归极乐&#xff0c;佛法说&#xff0c;世间法与佛法是不二&#xff0c;随其心净则土净&#xff0c;当…

作者头像 李华
网站建设 2026/10/4 7:58:18

ZYNQ上KSZ9031 PHY调试:MDIO与MMD读取0xFFFF的排查实践

最近在一块 ZYNQ-7000 板卡上调千兆以太网&#xff0c;PS 端 GEM0 外接的是 Microchip 的 KSZ9031RNX&#xff0c;软件层面用裸机 LwIP 跑 TCP/IP 协议栈。原本觉得这套组合很常见&#xff0c;结果调试日志直接停在PHY init failed&#xff0c;用XEmacPs_PhyRead读 PHY ID 寄存…

作者头像 李华
网站建设 2026/10/4 7:56:42

ZYNQ无DDR运行方案:OCM片上存储器的启动流程与链接脚本实战

1. 为什么要折腾“不带DDR的ZYNQ”&#xff1a;OCM在无外置内存方案中的真实位置1.1 先搞清楚ZYNQ里OCM到底是什么ZYNQ这类芯片和普通单片机最大的不同&#xff0c;就是它内部集成了ARM Cortex-A9双核处理器&#xff0c;但这并不意味着离开了外部DDR它就不会工作。很多人拿到Vi…

作者头像 李华