
简介这是CSDN博主gws09876发布的Qt股票分析工具工程源码对应博文中的完整实现面向Qt/C开发者与金融信息可视化学习者。工程包含StockAnalyse与shares两个应用模块基于Qt Widgets构建界面行情图表采用QChart绘制覆盖实时股票数据解析、K线/分时图、资金流向、自选列表及个股详情等常见股票软件功能。压缩包共82个文件以34个h头文件、32个cpp源文件和8个ui界面描述文件为主另含2个pro工程文件与1个SQLite数据库文件包体仅232KB目录结构清晰便于模块化阅读与二次开发。目前已有728人学习下载。对于想快速掌握Qt界面与QChart图表联动、理解自定义表格模型与股票数据解析层组织方式的开发者这份源码可直接编译运行按界面层、数据层、图表层逐块拆解复用是较好的进阶参考工程。1. shares 和 stockAnalyse先对齐数据口径再谈分析结论做股票分析的时候很多人拿到 shares 行情数据第一件事就是画 K 线、算指标结果发现同一个标的三款软件显示三个价回测曲线漂亮得不像话实盘一上手就露馅。问题基本出在数据口径和信号时序上而不是公式本身算错。标题里 shares 和 stockAnalyse 正好对应两层工作shares 负责行情数据的获取、清洗和复权处理stockAnalyse 负责指标计算、信号生成和回测验证。把这两层拆开之后数据问题不会干扰策略问题策略结论也不会因为换一个数据源就全部重来。这篇笔记写给正在做盯盘辅助、自制指标或复盘信号的人也写给想把自己的分析逻辑代码化的朋友。你不需要很深的金融背景但要愿意把每一步数据都检查一遍。2. 管好 shares 数据源抓取、清洗与三种复权的选择stockAnalyse 算得准不准九成取决于拿到的 shares 原始数据干不干净。很多人在这一步省时间后面全在填坑。下面先把数据入口固定下来再解决复权口径和数据源选型问题这两件事定了指标计算才有讨论基础。2.1 一次抓取的最小闭环列名、排序、去重不能省不少分析代码直接在线下载数据就开始算指标新手跟着跑通就完事换一个标的后突然报 KeyError往往就是列名大小写、多层列名没处理。我一般会把数据加载统一收敛到一个函数里后续所有 stockAnalyse 计算都只认这一种结构。import pandas as pd def load_daily_csv(csv_path): # 统一入口无论行情软件导出成什么格式先转成约定结构 df pd.read_csv(csv_path, parse_dates[date], index_coldate) df df.sort_index() df.columns [c.lower() for c in df.columns] # 同一交易日出现重复行时保留最后一条避免 rolling 窗口被污染 df df[~df.index.duplicated(keeplast)] return df参数说明parse_dates 把日期字符串解析成 Timestamp索引才具备时间语义后续按日切片、按频率重采样才能正常工作sort_index 保证日期升序rolling 窗口完全依赖顺序一旦乱序算出来的均线是把过去和未来混在一起回测全部失真duplicated 处理的是盘中多次追加导致的重复交易日保留最后一条是常见取舍。如果你用的是开源行情接口列名问题更典型。比如某在线接口返回的列名可能是多层结构要先压平再改名import yfinance as yf # 占位代码请替换成你要跟踪的标的 df_online yf.download(XXXX.XX, start2020-01-01, end2024-12-31, auto_adjustFalse) # 在线接口可能返回 MultiIndex 列名先压平成一层 if isinstance(df_online.columns, pd.MultiIndex): df_online.columns [col[0] for col in df_online.columns] df_online df_online.rename(columns{ Open: open, High: high, Low: low, Close: close, Volume: volume })这里的 auto_adjustFalse 值得单独说。很多接口默认返回前复权价格看似方便但前复权价格会随最新行情不断被重算同一份历史数据两周后重跑就变样回测无法复现。stockAnalyse 里建议保持原始价格入库把复权放在单独一步控制这样每一步都可追溯。2.2 三种复权的选择为什么指标计算建议用后复权很多人把复权设置搞反导致指标和行情软件长期对不上。三种口径要分开记不复权就是真实成交价序列分红派息、送转股的当天价格会留下非交易性跳空均线、RSI 在这种跳空面前会产生假信号前复权以最新价为基准把历史价格修平优点是和软件界面一致缺点是历史价格持续漂移不适合做可复现回测后复权以最早交易日为基准把此后价格向上修历史序列一旦生成就是固定的。所以我的习惯是指标计算和回测统一用后复权看当前盘面才切到前复权。判断数据是否复权正确有个土办法找最近一次大比例分红或送转日看当天价格是否出现明显跳空用后复权数据时这种跳空应已被修平指标线不会出现突变台阶。这一条在换数据源时尤其好用不用看文档直接看曲线就知道口径对不对。2.3 数据源选型与基础校验换源前先跑断言常见做法是保留两个数据通道一个用于快速验证一个用于最终复算。长期只用单一来源一旦接口升级或字段变化问题会被隐藏到很难追查所以基础校验不是可选动作。数据来源最合适的使用方式主要坑在线开源行情接口快速原型、日线研究字段不稳定、限流、复权策略不透明行情软件手动导出 CSV日常复盘、交叉校验导出格式随版本变化字段名要核对本地数据库存储长期积累、多标的扫描需要自己维护增量更新和去重数据校验代码可以直接挂在 load 流程末尾换数据源时先过一遍def check_daily(df): # 索引必须是升序且唯一这是时间序列计算的前提 assert df.index.is_monotonic_increasing, 日期没有按升序排列 assert len(df.index) len(df.index.unique()), 存在重复日期 # high 必须不低于同一根 K 线的 open 和 close这是行情本身的一致性约束 assert (df[high] df[[open, low, close]].max(axis1) - 1e-6).all() return True参数说明is_monotonic_increasing 能抓出拼接数据时的乱序重复日期在增量更新时最常出现high 和 open、low、close 的关系校验可以把字段错位、空值填充不当这类低级错误直接挡在门外。3. stockAnalyse 指标计算MA/RSI/MACD 的参数取舍与风险口径指标计算看起来是套公式实际坑在两类滚动窗口边界怎么处理以及参数默认值适不适合你的标的。下面统一用一个小写 close 序列做输入先把两个基础函数约定好再谈 RSI、MACD 和风险指标。3.1 先定调用约定滚动窗口的语义要统一def ma(series, n20): # min_periodsn窗口内有效数据不足 n 个就不给值避免小样本噪声 return series.rolling(n, min_periodsn).mean() def ema(series, n12): # adjustFalse从序列起点开始连续递推避免默认的初始值拟合 return series.ewm(spann, adjustFalse).mean()这两个约定影响全局。rolling 在序列中间出现 NaN 时窗口会自动扩大去寻找有效值算出的均线会跨越缺失区间看起来平滑实际已经失真min_periodsn 显式要求前 n 个值必须全部有效从语义上堵住这个问题。ema 的 adjustFalse 是让指数均线从第一个数据点就开始递推很多接口默认行为不同同一份数据会算出两套 MACD。周期参数不是越大越稳。短线盯 5/10/20波段看 60/120/250这是按持有周期选的不是按美观选的。换手率高的标的用短周期容易来回打脸低波动的标的用长周期又反应太慢参数和标的不匹配后面所有信号都带着噪声。3.2 RSI 和 MACD默认参数不是不能改RSI 的计算里真正影响结果的是平滑方式。def rsi(close, period14): delta close.diff() gain delta.clip(lower0) loss -delta.clip(upper0) # Wilder 平滑alpha 取 1/period对涨跌幅做递推平均 avg_gain gain.ewm(alpha1 / period, adjustFalse).mean() avg_loss loss.ewm(alpha1 / period, adjustFalse).mean() rs avg_gain / avg_loss return 100 - 100 / (1 rs)参数说明alpha 取 1/period 是常见做法周期越小指标对最近几根 K 线越敏感。日线默认 14 在低波动标的上反应钝化可以调到 7 到 10做短线可以降到 6但要接受频繁触碰超买超卖区。RSI 不是越极端越准它只在震荡行情里有参考价值。MACD 的经典参数 12/26/9 来自早期市场习惯不是数学最优。def macd(close, fast12, slow26, signal9): dif ema(close, fast) - ema(close, slow) # 快慢均线差 dea ema(dif, signal) # 对差再做一次平滑 return dif, dea, (dif - dea) * 2 # 柱值传统画法乘 2参数改动改变的是灵敏度。用日线看中长趋势可以把参数放大到 26/52/18信号数量明显减少假突破也少但每条信号的持续时间更长持仓回撤也会变大。改参数前先想清楚你要抓的是哪个级别的波动而不是看哪个组合历史回测最漂亮。3.3 风险指标252、无风险利率、最大回撤的口径rets close.pct_change().dropna() # 年化波动率日波动率按 252 个交易日开方 vol rets.std() * np.sqrt(252) # 夏普比率无风险利率按年化 2% 折算成日度 sharpe (rets.mean() - 0.02 / 252) / rets.std() * np.sqrt(252) # 最大回撤当前价相对历史最高点的下跌幅度 drawdown close / close.cummax() - 1 max_dd drawdown.min()参数说明252 是年度交易日近似量用 365 会让年化波动率被高估接近两成0.02/252 把年化无风险利率转成日度做策略横向比较时 rf 不敏感但当策略收益很低时夏普会从正转负这时要先确认是公式口径问题还是策略本身不行。close 必须是后复权序列否则分红跳空会被算成真实亏损最大回撤也会虚高。4. 用 stockAnalyse 跑最小回测从信号生成到资金曲线回测的正确性不在公式而在时序。最常见的错误是用今天的收盘价判断今天该不该买这在实盘中根本做不到。下面用金叉死叉搭一个最小可回测框架把信号、持仓、成本和结果逐层展开。4.1 从金叉死叉到持仓状态先把信号和成交分开fast ma(close, 5) slow ma(close, 20) # 金叉快线由下向上穿过慢线 cross_up (fast slow) (fast.shift(1) slow.shift(1)) # 死叉快线由上向下跌破慢线 cross_dn (fast slow) (fast.shift(1) slow.shift(1)) # 目标持仓有信号的当天记录目标状态之后保持不变直到反向信号 target pd.Series(np.nan, indexclose.index) target[cross_up] 1 target[cross_dn] 0 target target.ffill().fillna(0) # 成交延迟信号在收盘后产生最早只能让下一个交易日执行 exec_pos target.shift(1).fillna(0)参数说明shift(1) 是去掉未来函数的关键没有这一步回测等于用了当天收盘价买在信号当天实际交易做不到ffill 让持仓状态在两次信号之间持续fillna(0) 把数据开头尚未产生信号的区域当作空仓。严格一点的做法是用次日开盘价撮合代码里把 exec_pos 和次日 open 对齐用 open 计算成交价再用 close 计算当日收益这样资金曲线更接近实盘。4.2 资金曲线手续费和滑点要单列很多回测虚高是因为把手续费按零算。换仓成本在低频策略里占比不大但在高频信号里能把一年收益吃掉一大块。fee 0.0005 # 单边费率含滑点近似 turnover exec_pos.diff().abs().fillna(0) cost turnover * fee daily_ret exec_pos * rets net daily_ret - cost equity (1 net).cumprod()参数说明turnover 是每日仓位变化量只有换仓那天产生费用daily_ret 用 exec_pos 乘以当日收益因为 exec_pos 已经 shift 过收益时序就是“今天用昨天的信号交易”fee 取 0.0005 是相对保守的单边成本假设具体按自己实际通道定想更保守就调到 0.001 到 0.002。成本假设太乐观是回测和实盘差距的主要来源之一。4.3 回测报告怎么看不是只看净值曲线指标怎么算怎么看年化收益率净值末端按年化看整体赚钱能力最大回撤净值相对前期高点最大跌幅看自己能否承受夏普比率年化超额收益 / 年化波动看单位风险回报交易次数turnover 非零的天数判断样本量和过拟合程度真正容易被忽略的是交易次数。全年交易不到 30 次统计上就是小样本任何胜率和盈亏比都不可信。反过来如果参数微调一点结果就大起大落基本是在过拟合样本内噪声。还有一个建议固定一个基准如果策略年化跑不过“买入持有”那它作为主动交易策略就是失败的。回测的价值是淘汰明显不行的想法而不是证明某个想法一定赚钱。5. stockAnalyse 避坑让结果翻车的 5 个真实细节这一章都是踩过的坑每条现象都很像“数据正常、结果也正常”但细查下去全是口径和时序问题。5.1 同一个标的三款软件三个价复权口径不一致现象stockAnalyse 算出来的均线跟某行情软件差很多尤其在分红除权日后差异明显。原因软件默认前复权你的历史数据可能是不复权或后复权除权日当天价格跳空被不同口径处理后越早的历史数据差异越大均线自然对不上。解决确定一个口径并写进代码注释。我的做法是后复权做指标和回测不复权做量能或现价比较。任何一次数据源切换先对比近两年日线收盘价差异超过 0.5% 就停下来查原因不要直接往下算。5.2 回测曲线漂亮实盘却亏先查未来函数现象资金曲线近乎平滑上涨回撤极小换到实盘却完全对不上甚至反向。原因用了当天或未来的数据。常见位置是信号判断条件里没有 shift或者买卖用当天 close 撮合有时是用了未来一段数据做归一化。解决统一规则所有信号判断只能使用截至昨天收盘的数据成交价最早取今天开盘。检查方法很直接把信号列整体 shift 一位再跑一遍回测如果结果变化很大说明原回测时序有问题。5.3 停牌日的 NaN 会把指标静默算歪现象指标线出现莫名其妙的长平台或跳变回测突然在某一段猛赚。原因停牌日没成交数据源返回 NaN 或直接缺行。rolling 在遇到 NaN 时会扩大有效窗口指标值被隔空计算而回测框架可能把 NaN 段当作空仓跳过两边口径不一致结果自然失真。解决明确数据频率。日线研究建议按完整交易日历重索引先把停牌日暴露出来再决定是保留还是删除不要在不知情的情况下让 rolling 悄悄处理。我习惯在删除前先看这段停牌对指标是否有意义并在代码里保留删除记录方便复查。5.4 前期指标全是 NaN回测的前几天却没有禁用信号现象回测净值曲线从第 1 天就开始计算但 MA20 在第 20 天才第一次有有效值。原因close 从第 1 天就参与回测可 fast 和 slow 在窗口不足时是 NaN某些比较操作会把 NaN 当成 False导致信号状态和实际逻辑不一致。解决把回测起点放在所有指标都有有效值之后。最简单是做一个对齐# 只保留所有指标都有值的区间避免用不足窗口的指标做交易 valid pd.concat([close, fast, slow], axis1).dropna().index df_backtest df.loc[valid]参数说明这样自动跳过窗口不足的区间保证每个持仓日和收益计算都建立在有效指标上回测起点不会虚设。5.5 两个数据源的 close 对不上时区、复权、版本都在作怪现象同一个标的在线接口和本地导出的 CSV 收盘价有时差几分钱个别日期相差明显。原因不同数据源对复权因子计算、派息处理、时区归属的规则不完全一致在线接口的底层库每次更新后结果也可能变化。解决固定一个主数据源用于全部计算另一个只做交叉校验不要混着拼接。每次更换数据源后做一次全量收盘价 diff 统计差异率超过阈值就排查。数据文件里写入数据源标识和生成日期比任何公式都重要——没有这一步两个月后你自己都说不清这份数据是哪来的。6. 进阶把指标合成打分并补齐本地缓存习惯最后分享一个具体做法把多个指标合成一个观察用的打分序列而不是盯着单一指标做决定。# 打分只用于观察不直接当买卖信号 score pd.Series(0.0, indexclose.index) score (close ma(close, 20)).astype(float) score (rsi(close, 14) 30).astype(float) - (rsi(close, 14) 70).astype(float) score (macd(close)[0] macd(close)[1]).astype(float)打分列能一眼看出多指标共振的位置连续多日同向打分比单指标更稳定。但打分不是策略直接按分数阈值下单依然要经过第 4 章的回测流程否则又会回到样本内自嗨。另一个我坚持的习惯是本地缓存。在线接口抓下来的 shares 数据先落一份 CSV再让 stockAnalyse 从本地读。这样即使接口维护、字段升级、网络波动你手里始终有一个稳定版本可以重算。更严格的做法是先覆盖写全量等系统稳定后再改成增量更新增量更新必须去重否则重复日期会污染 rolling 窗口。我自己在这个方向上栽过一次历史数据用的是后复权增量数据用的默认前复权拼接后指标在某个除权日附近出现一段假信号排查一个晚上才发现两段数据根本不是同一个口径。从那以后文件名里永远带数据源和生成日期像 symbol_data_source_20250601.csv 这种格式一眼就能看出这份数据什么时候生成、来自哪里。回测可以重跑数据不会重来。数据口径、信号时序、样本量与成本假设是我看任何一份股票分析结果时最先检查的四件事也是这个方向最值得先花时间的地方。希望帮到你。本文还有配套的精品资源点击获取