简介:这份Python量化代码文档面向金融数据分析初学者与量化策略开发者,以可编辑Word形式呈现,方便非编程人员理解与修改代码,解决策略研究入门与团队协作传递的问题。包内共1个docx文件,大小约13KB,内容涵盖NumPy、Pandas、Matplotlib等核心库的使用示例,以及获取股票与指数数据、处理时间序列、计算年化收益率和最大回撤等完整函数实现。读者可从中获得量化交易策略开发的基础操作流程,包括CSV数据读取、筛选排序、列表转换与回测准备,同时了解跨平台路径兼容与future模块的版本适配思路。文档代码片段可直接参考修改,适合作为量化分析实践的入门模板。目前已有65人学习,便于快速上手金融数据处理与策略评估。
1. 一份「可编辑 Word 版」量化代码文档,到底该怎么用
很多人第一次拿到「Python量化代码(可编辑修改word版).docx」这类文件时,第一反应是把它当成一份教程去读,读完就丢在硬盘里吃灰。但真正在一线做策略的人会把它当成另一种东西:一份可以拆开、改参数、接自己数据源的策略骨架清单。它解决的不是「教你学 Python」的问题,而是「把一段能跑通的策略逻辑,从文档里抠出来,变成自己账户上能回测、能验证的脚本」的问题。适合谁?适合已经会写基础 Python、但不知道一个完整策略从信号到下单要拆成几块的人,也适合手里有一堆零散代码片段、想整理成工程结构的人。这一章先把这份文档的定位讲清楚,后面几章再一层层拆开。
2. 把 Word 里的代码抠出来:从文档到可运行脚本的完整路径
Word 文档最大的坑不是内容,而是格式。代码在 Word 里会被自动编号、自动换行、引号变成中文引号、缩进变成空格和制表符混排。直接复制粘贴到编辑器里,十有八九第一行就报IndentationError。所以第一步不是读策略,而是做一次干净的「代码提取」。
2.1 为什么不能直接复制粘贴
Word 的自动更正会把英文直引号"转成弯引号",把--转成破折号,把行首的缩进用「首行缩进」这种段落属性实现,而不是真正的空格字符。你复制出来看到的缩进,在 Python 解释器眼里可能根本不存在。常见做法是先把文档另存为纯文本.txt,再用编辑器打开,这样段落属性会被丢弃,只保留字符。
具体操作路径:在 Word 里选「文件 → 另存为 → 纯文本 (*.txt)」,编码选 UTF-8。然后用 VS Code 或 Notepad++ 打开这个 txt。此时你会看到代码块的缩进可能全没了,但至少引号和特殊符号是干净的。
2.2 用脚本批量清洗提取出来的代码
手动改缩进不现实,一个策略几百行。我一般写一个小清洗脚本,把常见的中文标点和多余空行处理掉,再人工补缩进。下面这段是清洗脚本的核心逻辑:
import re def clean_code_block(raw_text: str) -> str: # 中文标点替换为英文标点,这是 Word 文档最常见的污染 trans_table = str.maketrans({ '"': '"', '"': '"', # 弯引号还原 ''': "'", ''': "'", '(': '(', ')': ')', ',': ',', ':': ':', ';': ';', '!': '!', }) text = raw_text.translate(trans_table) # 去掉 Word 可能插入的零宽字符 text = text.replace('\u200b', '').replace('\xa0', ' ') # 合并超过两个的连续空行 text = re.sub(r'\n{3,}', '\n\n', text) # 去掉行尾多余空格 lines = [line.rstrip() for line in text.split('\n')] return '\n'.join(lines) if __name__ == '__main__': with open('raw_from_word.txt', 'r', encoding='utf-8') as f: raw = f.read() cleaned = clean_code_block(raw) with open('cleaned.py', 'w', encoding='utf-8') as f: f.write(cleaned) print('清洗完成,请人工检查缩进')这段脚本的逻辑说明:str.maketrans建立字符映射表,把 Word 常见的中文标点一次性替换掉;\u200b是零宽空格,肉眼看不见但会让 Python 报语法错误;\xa0是不间断空格,同样要换成普通空格。参数上,re.sub(r'\n{3,}', '\n\n', text)里的{3,}表示三个及以上换行合并成两个,避免代码块之间空太多行。跑完之后必须人工过一遍缩进,因为脚本无法判断哪一行属于哪个代码块。
2.3 建立最小可运行工程结构
代码抠出来之后不要全塞进一个文件。一个能长期维护的量化脚本,至少拆成四块:数据获取、信号计算、回测执行、参数配置。下面是我常用的目录结构,你可以直接照搬:
quant_demo/ ├── config.py # 参数集中管理 ├── data_loader.py # 数据读取与清洗 ├── strategy.py # 信号逻辑 ├── backtest.py # 回测循环 └── main.py # 入口config.py里放所有可调参数,比如均线周期、手续费率、初始资金。这样做的好处是,改参数不用翻遍所有文件。一个典型的配置长这样:
# config.py SYMBOL = 'DEMO' # 标的代码,替换成你自己的 START_DATE = '2022-01-01' END_DATE = '2023-12-31' FAST_MA = 5 # 短均线周期 SLOW_MA = 20 # 长均线周期 INIT_CASH = 100000 # 初始资金 COMMISSION = 0.0003 # 单边手续费率 SLIPPAGE = 0.001 # 滑点参数说明:FAST_MA和SLOW_MA是最容易过拟合的两个值,后面避坑章节会专门讲。COMMISSION和SLIPPAGE很多人回测时设成 0,这是自欺欺人,实盘一定亏在这上面。
3. 双均线策略的代码骨架:从信号到回测的每一步
文档里如果给的是双均线这类经典策略,不要觉得简单就跳过。双均线是验证一套回测框架是否正确的「标准试纸」——如果双均线都跑不对,复杂策略更不可能对。这一章把信号计算和回测循环拆开讲,每一步都落到代码。
3.1 信号计算:金叉死叉到底怎么判定
金叉的本质是「短均线从下方穿越长均线」,不是「短均线大于长均线」。这两者差别很大:前者只在穿越那一天产生一次信号,后者会在短均线持续在上方的每一天都返回 True。很多人回测收益虚高,就是因为把状态判断当成了信号判断。
import pandas as pd def generate_signal(df: pd.DataFrame, fast: int, slow: int) -> pd.DataFrame: df = df.copy() df['ma_fast'] = df['close'].rolling(window=fast).mean() df['ma_slow'] = df['close'].rolling(window=slow).mean() # 用 shift(1) 判断前一天的位置关系,避免用到未来数据 df['prev_fast'] = df['ma_fast'].shift(1) df['prev_slow'] = df['ma_slow'].shift(1) # 金叉:昨天短在下,今天短在上 df['golden_cross'] = (df['prev_fast'] <= df['prev_slow']) & (df['ma_fast'] > df['ma_slow']) # 死叉:昨天短在上,今天短在下 df['death_cross'] = (df['prev_fast'] >= df['prev_slow']) & (df['ma_fast'] < df['ma_slow']) return df逻辑说明:rolling(window=fast).mean()计算滚动均值,前fast-1行会是 NaN,这是正常的。关键在shift(1)——判断穿越必须比较「昨天的相对位置」和「今天的相对位置」,如果只用今天的大小关系,就会把持续状态误判成信号。参数上,fast和slow从config.py传入,不要写死在函数里。
3.2 回测循环:为什么必须逐行遍历
向量化回测看起来快,但容易在仓位管理和资金曲线上出错。新手我建议先用逐行循环,逻辑清晰,出错能定位。等框架稳定了再考虑向量化优化。
def run_backtest(df: pd.DataFrame, init_cash: float, commission: float, slippage: float) -> pd.DataFrame: cash = init_cash position = 0 # 持仓数量 records = [] for i in range(len(df)): price = df['close'].iloc[i] # 先处理卖出信号 if df['death_cross'].iloc[i] and position > 0: sell_price = price * (1 - slippage) cash += position * sell_price * (1 - commission) records.append({'date': df.index[i], 'action': 'sell', 'price': sell_price, 'cash': cash}) position = 0 # 再处理买入信号 elif df['golden_cross'].iloc[i] and position == 0: buy_price = price * (1 + slippage) # 用全部现金买入,向下取整到整数股 shares = int(cash / (buy_price * (1 + commission))) if shares > 0: cash -= shares * buy_price * (1 + commission) position = shares records.append({'date': df.index[i], 'action': 'buy', 'price': buy_price, 'cash': cash}) return pd.DataFrame(records)逻辑说明:循环里先判断卖出再判断买入,避免同一天又买又卖。sell_price乘以(1 - slippage)表示卖出时成交价比收盘价低,buy_price乘以(1 + slippage)表示买入时成交价比收盘价高,这是滑点最朴素的建模方式。int(cash / ...)向下取整,因为 A 股最小交易单位是 100 股(这里简化为 1 股,实盘要改成// 100 * 100)。参数commission和slippage直接决定回测收益的真实性,后面会讲怎么设。
3.3 绩效指标:别只看总收益
回测跑完只报一个总收益率,等于没做回测。至少要算最大回撤、夏普比率、胜率三个指标。
def performance(equity_curve: pd.Series) -> dict: total_return = equity_curve.iloc[-1] / equity_curve.iloc[0] - 1 # 最大回撤 rolling_max = equity_curve.cummax() drawdown = (equity_curve - rolling_max) / rolling_max max_dd = drawdown.min() # 日收益与夏普(假设无风险利率为 0) daily_ret = equity_curve.pct_change().dropna() sharpe = daily_ret.mean() / daily_ret.std() * (252 ** 0.5) if daily_ret.std() > 0 else 0 return {'total_return': round(total_return, 4), 'max_drawdown': round(max_dd, 4), 'sharpe': round(sharpe, 2)}参数说明:252是一年交易日数量,用于年化夏普。cummax()计算历史最高净值,drawdown是当前净值相对历史高点的回撤比例,取最小值就是最大回撤。这三个指标要一起看:总收益高但最大回撤 50%,实盘根本拿不住。
4. 参数怎么设才不翻车:手续费、滑点与均线周期的血泪经验
回测和实盘的差距,八成来自参数设置太乐观。这一章把三个最容易翻车的参数单独拎出来讲,每个都给出可落地的设定方法和验证手段。
4.1 手续费和滑点:设 0 等于自欺欺人
很多人回测时把COMMISSION和SLIPPAGE都设成 0,跑出来一条漂亮的向上曲线,实盘一上就亏。手续费是交易所和券商实打实扣的,滑点是你的成交价和预期价的偏差。常见做法是:手续费按单边万分之三起步,滑点按千分之一起步,高频策略还要更高。
验证方法很简单:把手续费和滑点翻倍,再跑一遍回测。如果收益从 30% 掉到 5%,说明这个策略对交易成本极度敏感,实盘基本没戏。一个稳健的策略,在成本翻倍后收益应该只下降有限比例。
# 成本敏感性测试 for comm in [0.0003, 0.0006, 0.001]: for slip in [0.001, 0.002, 0.003]: result = run_backtest(df, INIT_CASH, comm, slip) perf = performance(result) print(f'手续费{comm} 滑点{slip} -> {perf}')这段代码把成本和滑点做成网格,一次性看不同组合下的绩效。参数上,comm从万三到千一,slip从千一到千三,覆盖了大多数中低频策略的合理区间。
4.2 均线周期:为什么 5 和 20 不是万能钥匙
FAST_MA=5、SLOW_MA=20是文档里最常见的默认值,但它是针对特定标的、特定时间段调出来的。直接套到你的标的上,大概率过拟合。正确做法是:先固定一个较宽的参数区间,看绩效的稳定性,而不是找最高点。
| 短周期 | 长周期 | 总收益 | 最大回撤 | 夏普 |
|---|---|---|---|---|
| 5 | 20 | 28% | -18% | 1.1 |
| 10 | 30 | 22% | -14% | 1.0 |
| 10 | 60 | 15% | -10% | 0.8 |
| 20 | 60 | 12% | -8% | 0.7 |
看这张表,不是选收益最高的 5/20,而是看哪个组合在收益和回撤之间平衡得最好。如果 5/20 收益最高但回撤也最大,而 10/30 收益略低但回撤小很多,后者实盘体验更好。参数说明:短周期越小,信号越频繁,交易成本越高;长周期越大,信号越少,但滞后越严重。
4.3 用样本外测试验证参数是否过拟合
在历史数据上表现好,不代表未来好。把数据切成两段:前 70% 用来调参,后 30% 用来验证。如果后 30% 的绩效比前 70% 差很多,说明参数过拟合了。
split = int(len(df) * 0.7) train = df.iloc[:split] test = df.iloc[split:] # 在 train 上选参数,在 test 上验证 train_signal = generate_signal(train, FAST_MA, SLOW_MA) test_signal = generate_signal(test, FAST_MA, SLOW_MA)逻辑说明:split是切分点,train是样本内,test是样本外。注意generate_signal要在切分后分别调用,不能先算好全量再切,否则样本外数据会通过滚动均值「泄露」到样本内。这是量化里最隐蔽的坑之一。
5. 避坑与排查:Word 转代码后最常见的 5 个翻车现场
这一章是我自己踩过的坑,按「现象 → 原因 → 解决」写,你对照排查能省不少时间。
现象一:复制出来的代码第一行就报IndentationError。原因:Word 的缩进是段落属性,不是空格字符,复制后缩进丢失或变成制表符。 解决:先另存为纯文本,再用编辑器的「显示空白字符」功能检查,把制表符统一替换成 4 个空格。VS Code 里可以开Render Whitespace。
现象二:代码里出现SyntaxError: invalid character in identifier。原因:中文引号、中文括号、零宽字符混进了代码。 解决:跑一遍第 2 章的清洗脚本,重点检查引号和括号。零宽字符肉眼看不见,用repr()打印可疑行能看出来。
现象三:回测收益高得离谱,年化 200%。原因:用了未来数据,比如在计算信号时用了当天的收盘价,却假设当天开盘就能成交。 解决:所有信号计算必须shift(1),成交价用第二天开盘价或当天收盘价加滑点。检查generate_signal里有没有直接比较当天均线大小。
现象四:回测和实盘成交价差很多。原因:滑点设得太小,或者用了收盘价成交但实盘是市价单。 解决:滑点至少设千分之一,成交价用close * (1 + slippage)。如果是流动性差的标的,滑点要设到千分之三以上。
现象五:参数在历史上完美,换一段时间就亏。原因:过拟合,参数是针对特定历史区间调出来的。 解决:做样本外测试,或者用滚动窗口验证。参数不要选绩效最高的,选绩效最稳定的。
6. 把文档变成自己的策略库:一个可复用的参数扫描技巧
最后讲一个我常用的进阶技巧:把 Word 文档里的策略当成模板,用参数扫描快速判断它值不值得深入。不要手动改参数跑几十遍,写个循环一次跑完,把结果存成表格,一眼就能看出这个策略的「体质」。
import itertools import pandas as pd def param_scan(df: pd.DataFrame, fast_range: list, slow_range: list) -> pd.DataFrame: results = [] for fast, slow in itertools.product(fast_range, slow_range): if fast >= slow: # 短周期必须小于长周期,跳过无效组合 continue sig = generate_signal(df, fast, slow) trades = run_backtest(sig, INIT_CASH, COMMISSION, SLIPPAGE) if trades.empty: continue # 用交易记录重建净值曲线 equity = INIT_CASH curve = [] for _, row in trades.iterrows(): equity = row['cash'] if row['action'] == 'sell' else equity curve.append(equity) if len(curve) < 2: continue perf = performance(pd.Series(curve)) results.append({'fast': fast, 'slow': slow, **perf}) return pd.DataFrame(results).sort_values('sharpe', ascending=False) # 使用示例 scan = param_scan(df, fast_range=[5, 10, 15, 20], slow_range=[20, 30, 40, 60]) print(scan.head(10))逻辑说明:itertools.product生成所有参数组合,fast >= slow直接跳过,因为短均线周期不可能大于等于长均线。run_backtest返回交易记录,这里用交易记录重建净值曲线,再算绩效。参数上,fast_range和slow_range不要设得太密,步长 5 左右就够,太密容易过拟合。sort_values('sharpe', ascending=False)按夏普排序,但我一般会同时看最大回撤,夏普高但回撤大的组合直接排除。
这个扫描跑完,你会得到一张表。如果表里夏普最高的几个组合,参数都集中在某个小区间,说明策略对这个区间敏感,实盘要小心。如果夏普高的组合分散在不同参数上,说明策略比较稳健。我自己的习惯是:扫描结果里夏普排名前 10 的组合,如果它们的最大回撤都超过 20%,这个策略我直接放弃,不管收益多高。回撤控制不住,实盘拿不住,赚多少都是纸上富贵。
希望帮到你。
本文还有配套的精品资源,点击获取