同一只股票的K线为什么有人能看出一套节奏有人只会反复调整均线参数核心差距往往不在行情软件而在对K线序列的理解。很多人一开始都是在同花顺这类行情软件里点“内置指标”后来发现内置指标不够用就开始搜索“源码”粘贴别人做的均线、MACD、缠论画笔。可这些源码不一定透明也不一定知道自己粘贴的公式有没有未来函数、有没有处理复权、版本能不能兼容。更值得投入的方案是把指标计算从行情软件里拆出来用 Python 自己实现一套可验证的K线指标引擎。这篇文章会围绕“K线指标”和“公式源码”展开。先解释指标的本质再用合成K线数据搭建一个小项目然后实现 MA、BOLL、MACD 三个基础指标再进一步做缠论分型和自动画笔的最小版本。最后还会回到同花顺公式讨论为什么同一套算法在 Python 和公式编辑器里可能算出不同结果以及生产环境应该如何合规、稳妥地扩展。整个过程不依赖任何私有接口数据先用本地生成的 demo 数据跑通逻辑适合想从“抄公式”走向“写公式”的开发者。1. 为什么要把指标从看盘软件里拆出来单独跑1.1 K线上显示指标本质上是在做序列运算无论是同花顺、通达信还是其他行情终端K线图展示的都是一个时间序列。每一根K线至少包含日期、开盘价、最高价、最低价、收盘价、成交量六个基本字段。看盘软件里内置的MA、BOLL、MACD本质上是把这几个序列放进一个数学函数里得到一条新的序列再叠加绘制在图表上。先看一个最小例子。MA5就是最近5根K线的收盘价求平均。对于第 t 根K线它依赖的是第 t-4 到第 t 根K线的收盘价。求均值的过程就是一次滑动窗口计算。把这个过程放到浏览器里、放到Python里、放到行情软件的公式编辑器里数学结果是相同的。理解了这一点就很容易理解“自定义指标”。自定义指标的源码不是一段神秘的脚本而是对开高低收量这组序列做变换。最终要么输出一条线要么输出一个标记要么输出一个选股条件。当终端只能做展示时计算逻辑越独立越有价值。原因有几点想验证指标是否能稳定工作必须能做历史回测。想对比不同版本参数需要有可复现的计算结果。想接入自己的数据源需要脱离特定软件的公式语言。想给别人讲解自己的策略不能只给一张截图要能给代码和运行结果。1.2 直接复制他人“公式源码”的三个问题在网上找现成指标源码确实是最快的入门路径。但直接复制到同花顺或类似软件里通常会遇到几个问题第一个问题是公式语言兼容性。同花顺有自己的公式编辑器通达信也有自己的公式语法看起来像却不代表完全一样。同一个函数名在不同软件里的参数含义可能有差别同一段脚本在这家能用在另一家会报“未定义的标识符”。第二个问题是不会排错。很多源码没有注释当屏幕上出现一条不符合预期的线条时使用者完全不知道是参数写错了、数据没有复权还是公式里引用了未来数据。第三个问题是无法验证。网上的指标公式一般不会顺带提供完整的验证数据和回测结果。你不知道它在历史行情里表现如何也不知道它的计算过程中是不是用了“未来跳变”的算法。对于缠论自动画笔这类依赖历史K线确认的指标这一点尤其关键。所以更合理的做法是把指标的数学逻辑用通用编程语言先跑通再用行情软件公式作为展示层。这样可以单元测试、可以回放、可以控制数据来源不容易被一段来源不明的公式影响判断。1.3 开发环境和生产环境的数据来源要分开还要强调一个容易被忽略的点K线数据属于有版权、有来源授权的数据。开发阶段用合成数据没有任何问题但如果你要把指标接入真实行情必须通过合规渠道获取数据比如有资质的行情服务商、数据供应商或者自己维护的交易所授权数据。不要通过破解客户端文件、抓取非公开接口等手段获取数据。这类行为既不稳定也可能给个人或公司带来法律风险。本文所有示例都不依赖真实同花顺数据而是先本地生成一份模拟的OHLCV数据。这样既能验证算法也能保证读者在完全合规的环境下把代码跑起来。真正进入生产环境时再把数据源替换成你采购并授权的行情数据即可。2. 准备项目骨架用合成K线先验证算法2.1 演示环境与依赖本文示例使用 Python 编写。你只需要一个支持 pandas 和 numpy 的 Python 环境例如 Python 3.10 或更新的版本即可。绘图部分会用到 matplotlib 和 mplfinance后者负责把K线画得更接近行情软件的风格。演示环境依赖建议如下依赖包用途常见安装版本pandas表格数据处理、窗口计算2.xnumpy随机数生成、数值计算1.24matplotlib图表底层渲染3.7mplfinance金融K线图绘制0.12.x安装命令pip install pandas numpy matplotlib mplfinance如果网络环境比较复杂可以先建立一个虚拟环境再做安装。安装完成后可以用下面的命令确认依赖是否正常python -c import pandas, numpy, matplotlib, mplfinance; print(ok)实际项目里建议把依赖版本固定到requirements.txt方便其他人复现这样不会因为 pandas 大版本升级导致行为变化。2.2 生成一份模拟K线数据由于本文不依赖真实的同花顺接口第一步先用随机游走方式生成一份模拟K线数据。生成数据的目标不是预测行情而是提供一组符合 OHLCV 基本关系的记录最高价一定不低于开盘价和收盘价最低价一定不高于开盘价和收盘价成交量是大于0的整数。import numpy as np import pandas as pd def generate_demo_kline(n: int 180, seed: int 20240701) - pd.DataFrame: rng np.random.default_rng(seed) rows [] prev_close 10.0 trade_date pd.date_range(2023-01-02, periodsn, freqB) for i in range(n): open_price prev_close * (1.0 rng.normal(0, 0.004)) close_price open_price * (1.0 rng.normal(0, 0.016)) high_price max(open_price, close_price) * (1.0 abs(rng.normal(0, 0.004))) low_price min(open_price, close_price) * (1.0 - abs(rng.normal(0, 0.004))) volume int(rng.integers(100000, 900000)) rows.append([ trade_date[i], round(open_price, 3), round(high_price, 3), round(low_price, 3), round(close_price, 3), volume, ]) prev_close close_price df pd.DataFrame( rows, columns[date, open, high, low, close, volume], ) df.to_csv(demo_kline.csv, indexFalse) return df if __name__ __main__: df generate_demo_kline() print(df.head(10)) print(df.tail(10))运行这段代码后会在当前目录生成demo_kline.csv。虽然它是随机生成的数据但已经具备了K线图的基本结构。后面的所有指标计算都可以在这种数据上完成。2.3 定义统一的K线读取函数后续所有指标计算都从同一份数据读取开始。为了避免日期排序、字段缺失、价格异常影响结果读取逻辑要统一收敛到一个函数里。import pandas as pd def load_kline(path: str) - pd.DataFrame: df pd.read_csv(path, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) for col in [open, high, low, close]: df[col] pd.to_numeric(df[col], errorscoerce) df df.dropna(subset[open, high, low, close]) df df[df[high] df[[open, close]].max(axis1)] df df[df[low] df[[open, close]].min(axis1)] df df.reset_index(dropTrue) return df解读一下这段代码先按日期排序避免数据乱序造成滑动窗口异常。再把关键价格列转成数值类型遇到空字符串或非法值会变成NaN。随后剔除价格顺序异常的数据比如最高价低于开盘价或收盘价的数据。最后重建索引让后续rolling得到的下标与 DataFrame 对齐。这样做的目的是减少数据质量问题混入指标计算阶段。真实项目里这里还可以增加交易日去重、成交量非负校验等逻辑。3. 先实现 MA、BOLL、MACD 三个基础指标3.1 MA 均线理解 rolling 滑动窗口移动平均线是最容易验证的指标。它把窗口内的收盘价取平均值然后不断右移窗口就形成一条均线。def add_ma(df: pd.DataFrame, periods(5, 10, 20)) - pd.DataFrame: out df.copy() for period in periods: out[fma{period}] out[close].rolling( windowperiod, min_periodsperiod, ).mean() return out这里需要注意min_periodsperiod的效果。不设置这个参数时如果窗口内的数据不足结果就是NaN设置了和窗口长度一样的min_periods前period - 1根K线不会出现有效均线值。这是一个很容易踩的坑有人只看到图形前方没有均线线头以为是程序错了实际是因为数据样本不足。3.2 BOLL 布林带平均值与标准差组合BOLL 也是围绕滑动窗口。它的计算公式是中轨最近 N 根K线收盘价的均值。上轨中轨加上 K 倍标准差。下轨中轨减去 K 倍标准差。下面是 Python 实现def add_boll( df: pd.DataFrame, window: int 20, k: float 2.0, ) - pd.DataFrame: out df.copy() mid out[close].rolling(windowwindow, min_periodswindow).mean() std out[close].rolling(windowwindow, min_periodswindow).std(ddof0) out[boll_mid] mid out[boll_upper] mid k * std out[boll_lower] mid - k * std return outddof0表示计算总体标准差分母是 N。很多行情软件公式里的标准差可能默认也是总体标准差但具体取决于软件实现。如果你发现 Python 计算结果和同花顺显示结果不一致第一个要查的就是标准差ddof和均值窗口是否一致。3.3 MACD EMA 的递归计算MACD 与 MA 的区别在于它使用指数移动平均EMA。EMA 的经典递推方式是EMA_today alpha * price_today (1 - alpha) * EMA_yesterday其中alpha 2 / (span 1)。上面的算法依赖前一天的 EMA所以它天然带有递归性质。Python 的 pandas 使用ewm很容易实现def add_macd( df: pd.DataFrame, fast: int 12, slow: int 26, signal: int 9, ) - pd.DataFrame: out df.copy() ema_fast out[close].ewm(spanfast, adjustFalse).mean() ema_slow out[close].ewm(spanslow, adjustFalse).mean() out[dif] ema_fast - ema_slow out[dea] out[dif].ewm(spansignal, adjustFalse).mean() out[macd_hist] (out[dif] - out[dea]) * 2 return out这里有几个需要解释的地方adjustFalse表示使用标准递推式前值对当前值的影响会逐步衰减。DIF 是快线EMA减去慢线EMA。DEA 是 DIF 的 EMA。MACD 柱通常是(DIF - DEA) * 2不同软件在倍数上可能不完全相同但图形收敛方向一致。一个常见错误是认为只要三条线方向一致就是“必然信号”。实际这只是一种技术指标任何指标都需要结合数据质量和固定规则使用不能把指标结果当成收益率承诺。3.4 运行验证检查输出是否正常把三个函数串联起来运行一个最小验证脚本from pathlib import Path from load_kline import load_kline from indicators import add_ma, add_boll, add_macd if __name__ __main__: demo_path Path(demo_kline.csv) df load_kline(demo_path) df add_ma(df) df add_boll(df) df add_macd(df) print(df[[date, close, ma5, ma20, dif, dea, macd_hist]].tail(10))验证标准包括前几行ma20为NaN这是正常现象因为窗口长度不足。后半段ma5、ma20都有数值。dif与dea没有出现大量空值。数据长度没有发生变化增加列后仍保持原有行数。如果输出里出现了大量NaN优先检查窗口长度、数据行数和数据是否包含空值。4. 分型与“画笔”的落地从规则到状态机4.1 分型不是神秘信号而是一组可复用的判断条件很多人在同花顺里搜索“缠论自动画笔”这个词核心目的之一是想让软件自动标出顶分型和底分型再连接成笔。既然想在软件里实现就必须用计算机能理解的语言描述规则。缠论分型有不同层次的严格定义。实际应用中还涉及包含K线处理也就是两根K线存在包含关系时需要先合并处理。先给出一个简化的分型识别模型顶分型中间K线的最高价大于左右两根K线最高价并且中间K线的最低价也大于左右两根K线最低价。底分型中间K线的最低价比左右两根K线都低并且中间K线的最高价也比左右两根K线都低。如果行情走势连续上涨不进行K线包含处理很难形成稳定的分型所以下面的实现只能算教学演示。真正要用于复盘还需要在前处理阶段增加包含关系合并逻辑。4.2 用 Python 标记顶底分型先把简化的分型识别写出来def detect_fractal(df: pd.DataFrame): out df.copy() highs out[high].to_numpy(dtypefloat) lows out[low].to_numpy(dtypefloat) markers [0] * len(out) for i in range(1, len(out) - 1): h_prev highs[i - 1] h_curr highs[i] h_next highs[i 1] l_prev lows[i - 1] l_curr lows[i] l_next lows[i 1] if h_curr h_prev and h_curr h_next and l_curr l_prev and l_curr l_next: markers[i] 1 # 顶分型 elif h_curr h_prev and h_curr h_next and l_curr l_prev and l_curr l_next: markers[i] -1 # 底分型 out[fractal] markers return out代码里用1表示顶分型用-1表示底分型用0表示普通K线。遍历范围从1到len(out) - 2因为分型需要左右各有一根K线来参照。这段代码的可扩展点在于可以把h_curr h_prev改成或不同定义会得到不同数量的分型。可以在判断前先对包含关系做预处理。可以把顶部价格和底部价格单独保存下来用于后续画趋势线。4.3 从交替分型中生成简化的“画笔”真正的画笔需要先确认底分型再连接底到顶的线段并且还要满足最低K线数量、价格突破方向等条件。为了让示例可运行这里只做简化版本在识别出的分型中筛选出顶底交替序列。def build_simple_pens(df: pd.DataFrame): fractal_df df.loc[df[fractal] ! 0].copy() pens [] last_kind None for _, row in fractal_df.iterrows(): kind top if row[fractal] 1 else bottom if last_kind is None or kind ! last_kind: pens.append(row) last_kind kind return pd.DataFrame(pens)这个函数没有做严格的高低点比较产生的结果可能比较粗糙。它的价值在于演示“状态机”思路一根K线确认一个分型后不能立刻下结论必须等右侧K线确认后才能确定分型是否稳定。如果用这段代码去对照同花顺里的自动画笔你会发现同花顺内置画笔的确认规则更严格。这是正常现象完整实现需要处理包含关系、分型确认