拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python量化选股实战:从零搭建轻量级回测框架

如果你正在学习Python数据分析或者对量化投资感兴趣但总觉得那些复杂的金融模型和庞大的回测框架离自己太远那么这篇文章就是为你准备的。我花了三天时间用Python写了一个轻量级的选股工具。它的核心逻辑并不复杂但当我用它跑了一遍A股数据后筛选出的股票组合在模拟回测中的表现确实让我有些意外——它以一种简单直接的方式验证了某些基础策略的有效性。这让我意识到对于大多数想入门量化、验证自己想法的开发者来说一个能快速跑通的“最小可行产品”MVP远比一个功能庞杂但难以上手的系统更有价值。本文将完整分享这个工具的设计思路、实现代码和使用方法。你不会看到复杂的机器学习模型或高频交易算法我们将聚焦于如何用最基础的pandas、numpy和tushare一个免费的财经数据接口构建一个从数据获取、因子计算到结果筛选的完整流程。更重要的是我会详细拆解其中容易踩坑的细节比如数据清洗的陷阱、回测中常见的“未来函数”问题以及如何客观地看待回测结果。无论你是想将Python应用于金融分析的初学者还是希望有一个轻量级框架来快速验证策略想法的开发者这篇文章都能提供一个清晰的路径和一套可直接运行的源码。1. 这个工具解决了什么问题从想法到验证的快速通道在量化投资领域一个新策略从构思到验证往往面临很高的工程门槛。你可能有一个关于“低市盈率高股息率”股票表现更好的想法但要验证它你需要获取历史股票数据价格、财务指标。编写代码计算每个时间点的市盈率、股息率。根据规则筛选出符合条件的股票。计算这些股票在后续一段时间内的收益。与市场基准如沪深300指数进行对比。这个过程涉及数据管理、时间序列处理、性能计算等多个环节。对于新手很容易在数据对齐、避免“未来函数”使用了未来的信息等基础问题上犯错导致回测结果失真毫无参考价值。这个Python选股工具的核心价值就在于它搭建了一个标准化、可复用的轻量级回测框架。它帮你处理了数据获取、清洗、时间对齐等脏活累活让你能专注于最核心的部分定义你的选股逻辑因子。你可以把它看作一个“策略实验室”用最小的代码改动快速测试不同因子组合的有效性。2. 核心概念与工具选型为什么是它们在开始编码前我们需要明确几个关键概念和工具选择的原因。2.1 什么是“因子”在量化选股中“因子”就是一个用于评价股票的标准或指标。它通常是一个可计算的数值。例如估值因子市盈率PE、市净率PB。盈利因子净资产收益率ROE、毛利率。成长因子营业收入增长率、净利润增长率。技术因子动量、波动率。我们的工具就是让用户定义一组因子和筛选条件如“市盈率小于20且ROE大于15%”程序会自动找出所有满足条件的股票。2.2 工具栈选择轻量、免费、易用tushare一个提供国内股票、基金、期货等金融数据的免费Python库。对于个人学习和研究来说其基础数据完全够用。它避免了我们去爬取各类财经网站提供了结构化的API。pandas数据分析的核心。股票数据本质上是带时间戳的表格数据pandas的DataFrame非常适合进行切片、聚合、合并等操作。numpy进行高效的数值计算比如计算收益率、统计指标等。matplotlib用于可视化回测结果绘制收益曲线图。这个组合摒弃了Backtrader、Zipline等重型回测框架的复杂性让我们能更直观地理解回测的每一个步骤。2.3 警惕“未来函数”与过拟合这是两个新手最容易掉入的陷阱我们的工具设计会特别注意规避。未来函数指在回测中使用了在交易当时无法获得的信息。例如在计算2019年1月1日的指标时错误地使用了2019年1月2日才公布的财务数据。这会导致回测结果过于乐观实盘必然失败。我们的工具将通过严格的数据发布日期对齐来解决。过拟合指策略在历史数据上表现完美但对未来数据样本外预测能力很差。通常因为策略参数过于复杂恰好“记住”了历史噪声。我们将通过强调策略逻辑的简洁性和进行样本外测试的建议来提醒读者。3. 环境准备与项目初始化首先确保你的Python环境是3.7或以上版本。我们将使用pip进行包管理。3.1 创建虚拟环境推荐为了避免包版本冲突建议使用虚拟环境。# 使用 conda (如果你安装了Anaconda) conda create -n stock_selector python3.8 conda activate stock_selector # 或者使用 venv (Python内置) python -m venv venv # Windows venv\Scripts\activate # Linux/Mac source venv/bin/activate3.2 安装依赖库在激活的虚拟环境中运行以下命令pip install pandas numpy matplotlib tusharetushare需要Token进行身份验证你需要在其官网免费注册一个账号获取你的Token。3.3 项目目录结构创建一个清晰的项目文件夹有助于代码管理。stock_selector_project/ │ ├── config.py # 配置文件存放tushare token等 ├── data_fetcher.py # 数据获取模块 ├── factor_calculator.py # 因子计算模块 ├── selector.py # 核心选股逻辑模块 ├── backtester.py # 回测模块 ├── main.py # 主程序入口 ├── requirements.txt # 依赖列表 └── results/ # 存放结果图表和数据的文件夹4. 核心流程拆解五步构建选股系统我们的工具工作流程分为五个核心步骤每一步对应一个模块。数据获取(data_fetcher.py)从tushare获取股票列表、日线行情、基本面数据。因子计算(factor_calculator.py)基于原始数据计算所需的选股因子如PE、PB、ROE等。股票筛选(selector.py)根据用户定义的因子条件如“PE 30 and ROE 0.1”在每个调仓日筛选出股票池。回测引擎(backtester.py)模拟交易。假设在调仓日等权重买入筛选出的股票持有一定周期后卖出计算组合收益。绩效分析(backtester.py)计算年化收益、夏普比率、最大回撤等指标并可视化。5. 完整代码实现与详解下面我们分模块实现代码。请将以下代码分别保存到对应的文件中。5.1 配置文件 (config.py)这里存放你的tusharetoken。切记不要将包含真实Token的代码上传至公开仓库。# config.py TUSHARE_TOKEN 你的tushare_token_在这里 # 请替换为你在tushare.pro官网申请的token5.2 数据获取模块 (data_fetcher.py)这个模块负责获取并初步清洗数据。# data_fetcher.py import tushare as ts import pandas as pd import os from config import TUSHARE_TOKEN # 初始化tushare ts.set_token(TUSHARE_TOKEN) pro ts.pro_api() class DataFetcher: def __init__(self): self.pro pro def get_stock_basic(self): 获取上市股票基本信息 df self.pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,area,industry,list_date) # 过滤掉ST、*ST股票简单示例实际可根据name字段更精确过滤 df df[~df[name].str.contains(ST)] return df def get_daily_data(self, ts_code, start_date, end_date): 获取单只股票的日线行情数据 df self.pro.daily(ts_codets_code, start_datestart_date, end_dateend_date) df[trade_date] pd.to_datetime(df[trade_date]) df.set_index(trade_date, inplaceTrue) df.sort_index(inplaceTrue) return df def get_daily_basic(self, trade_date): 获取指定交易日的每日指标包含PE, PB等 # 注意trade_date格式为‘YYYYMMDD’ df self.pro.daily_basic(trade_datetrade_date, fieldsts_code,trade_date,pe,pb,ps,total_mv) return df def get_fina_indicator(self, ts_code, start_date, end_date): 获取单只股票的财务指标数据如ROE # 财务数据按报告期发布我们取年报和季报 df self.pro.fina_indicator(ts_codets_code, start_datestart_date, end_dateend_date) if not df.empty: # 只保留年报和第一季度季报避免重复 df df[df[end_date].str.endswith((1231, 0331, 0630, 0930))] df[end_date] pd.to_datetime(df[end_date]) df.set_index(end_date, inplaceTrue) df.sort_index(inplaceTrue) return df # 示例获取沪深300成分股作为我们的初始股票池可选 def get_hs300_constituents(): 获取当前沪深300成分股列表 df pro.hs_const() # 获取最新的成分股列表 latest_date df[ann_date].max() hs300_codes df[df[ann_date] latest_date][ts_code].tolist() return hs300_codes关键点get_daily_basic获取的是交易日当天收盘后可用的估值数据这避免了日内的未来函数。get_fina_indicator获取财务数据财务报告有公布日期在实际回测中我们必须确保在报告公布日之后才能使用该数据。本例为简化假设在报告期末如12月31日即可获得数据实盘应用中必须使用ann_date公告日进行对齐。5.3 因子计算模块 (factor_calculator.py)这个模块将原始数据加工成我们需要的因子。# factor_calculator.py import pandas as pd import numpy as np class FactorCalculator: staticmethod def calculate_pe_ttm(close, net_profit): 计算滚动市盈率简化版 注意这是一个简化示例。真实的PE_TTM计算需要过去四个季度的净利润。 此处假设net_profit已是滚动净利润。 # 防止除零 pe close / (net_profit 1e-8) return pe staticmethod def calculate_momentum(price_series, window20): 计算动量因子过去window日的收益率 return price_series.pct_change(window) staticmethod def calculate_volatility(price_series, window20): 计算波动率因子过去window日收益率的标准差 returns price_series.pct_change() return returns.rolling(window).std() staticmethod def merge_factors_to_daily(daily_df, factor_df, factor_name): 将低频因子如财务因子对齐到日频数据上。 采用‘向前填充’方法用最近一期已知的财务数据填充直到下一期数据出现。 这是处理财务数据未来函数的关键 # 确保索引是datetime daily_df daily_df.copy() factor_df factor_df.copy() # 将因子数据重新采样到日频并向前填充 factor_daily factor_df.resample(D).ffill() # 合并到日线数据 merged_df daily_df.join(factor_daily, howleft) # 对因子列进行向前填充处理合并后可能产生的NaN merged_df[factor_name] merged_df[factor_name].ffill() return merged_df # 示例为一只股票准备因子数据 def prepare_single_stock_factors(ts_code, start_date, end_date, fetcher): 为单只股票准备日频因子数据表 # 1. 获取日线数据 daily_df fetcher.get_daily_data(ts_code, start_date, end_date) if daily_df.empty: return pd.DataFrame() # 2. 获取每日基础指标日频 # 这里需要循环获取每一天的数据tushare的daily_basic接口可以批量但为清晰起见我们简化处理。 # 实际操作中应批量获取所有日期的daily_basic数据再合并以提高效率。 all_daily_basic [] for date in pd.date_range(start_date, end_date, freqB): # B代表工作日 date_str date.strftime(%Y%m%d) try: df_day fetcher.get_daily_basic(date_str) df_day[trade_date] date all_daily_basic.append(df_day) except Exception as e: print(fFailed to fetch data for {date_str}: {e}) continue if all_daily_basic: daily_basic_df pd.concat(all_daily_basic) daily_basic_df.set_index(trade_date, inplaceTrue) # 合并PE PB等日频因子 daily_df daily_df.join(daily_basic_df[[pe, pb]], howleft) # 3. 获取财务数据低频并计算因子例如ROE fina_df fetcher.get_fina_indicator(ts_code, start_date, end_date) if not fina_df.empty and roe in fina_df.columns: # 使用merge_factors_to_daily方法将ROE对齐到日频 roe_series fina_df[roe] # 注意这里需要将roe_series转换为DataFrame以便merge roe_df pd.DataFrame({roe: roe_series}) daily_df FactorCalculator.merge_factors_to_daily(daily_df, roe_df, roe) # 4. 计算技术因子动量、波动率 daily_df[momentum_20] FactorCalculator.calculate_momentum(daily_df[close], window20) daily_df[volatility_20] FactorCalculator.calculate_volatility(daily_df[close], window20) daily_df[ts_code] ts_code return daily_df5.4 选股器模块 (selector.py)这是策略逻辑的核心定义如何根据因子筛选股票。# selector.py import pandas as pd import numpy as np class StockSelector: def __init__(self, factors_df): factors_df: 一个大的DataFrame包含所有股票在所有日期的因子值。 列至少包括trade_date, ts_code, factor1, factor2... self.factors_df factors_df def select_by_conditions(self, date, conditions): 在指定日期根据条件筛选股票。 conditions: 一个字典键为因子名值为一个元组 (operator, value)。 例如{pe: (, 30), roe: (, 0.15)} # 获取指定日期的所有股票因子数据 df_date self.factors_df[self.factors_df.index date].copy() if df_date.empty: return [] mask pd.Series(True, indexdf_date.index) for factor, (op, value) in conditions.items(): if factor not in df_date.columns: print(fWarning: Factor {factor} not found on {date}) continue if op : mask mask (df_date[factor] value) elif op : mask mask (df_date[factor] value) elif op : mask mask (df_date[factor] value) elif op : mask mask (df_date[factor] value) elif op : mask mask (df_date[factor] value) else: raise ValueError(fUnsupported operator: {op}) selected_stocks df_date[mask][ts_code].tolist() return selected_stocks def select_top_n(self, date, factor, n10, ascendingTrue): 在指定日期根据某个因子排序选择前N名或后N名 df_date self.factors_df[self.factors_df.index date].copy() if df_date.empty or factor not in df_date.columns: return [] # 删除因子值为NaN的行 df_date df_date.dropna(subset[factor]) sorted_df df_date.sort_values(byfactor, ascendingascending) return sorted_df.head(n)[ts_code].tolist() # 示例策略低市盈率高ROE def low_pe_high_roe_strategy(selector, date): conditions { pe: (, 25), # 市盈率低于25 roe: (, 0.12), # ROE大于12% # 可以添加更多条件如 pb: (, 2) } return selector.select_by_conditions(date, conditions)5.5 回测与绩效分析模块 (backtester.py)这是最复杂的部分模拟交易并评估结果。# backtester.py import pandas as pd import numpy as np import matplotlib.pyplot as plt from datetime import timedelta class Backtester: def __init__(self, price_data, benchmark_dataNone): price_data: dict, {ts_code: pd.DataFrame with indextrade_date and columnclose} benchmark_data: pd.Series, 基准指数如沪深300的日收益率索引为日期。 self.price_data price_data self.benchmark_returns benchmark_data self.portfolio_returns None self.portfolio_values None def run_backtest(self, selected_stocks_dict, initial_capital1000000, transaction_cost0.001): 运行回测。 selected_stocks_dict: dict, {trade_date: [list_of_ts_codes]} 每个调仓日选出的股票列表。 initial_capital: 初始资金。 transaction_cost: 单边交易成本费率。 # 获取所有调仓日期并排序 rebalance_dates sorted(selected_stocks_dict.keys()) if not rebalance_dates: print(No rebalance dates provided.) return # 初始化 capital initial_capital portfolio_value [capital] dates [rebalance_dates[0] - timedelta(days1)] # 记录起始前一天的价值 current_holdings {} # {ts_code: shares} for i, date in enumerate(rebalance_dates): # 1. 卖出当前持仓如果不是调仓日持有的股票 stocks_to_sell [code for code in current_holdings if code not in selected_stocks_dict[date]] for code in stocks_to_sell: if code in self.price_data and date in self.price_data[code].index: price self.price_data[code].loc[date, close] capital current_holdings[code] * price * (1 - transaction_cost) del current_holdings[code] # 2. 买入新选出的股票等权重 target_stocks selected_stocks_dict[date] if target_stocks: # 计算每只股票分配的资金考虑交易成本 n_stocks len(target_stocks) # 可用资金 现金 继续持有股票的价值 total_value capital for code, shares in current_holdings.items(): if code in self.price_data and date in self.price_data[code].index: total_value shares * self.price_data[code].loc[date, close] capital_per_stock total_value / n_stocks for code in target_stocks: if code not in self.price_data or date not in self.price_data[code].index: continue price self.price_data[code].loc[date, close] # 计算可买股数向下取整 shares_to_buy int(capital_per_stock / (price * (1 transaction_cost)) / 100) * 100 # 假设100股一手 if shares_to_buy 0: cost shares_to_buy * price * (1 transaction_cost) if cost capital: capital - cost current_holdings[code] current_holdings.get(code, 0) shares_to_buy else: print(fInsufficient capital to buy {code} on {date}) # 3. 计算当前组合总价值 current_value capital for code, shares in current_holdings.items(): if code in self.price_data and date in self.price_data[code].index: current_value shares * self.price_data[code].loc[date, close] portfolio_value.append(current_value) dates.append(date) # 计算日收益率序列简化这里用调仓日价值计算 self.portfolio_values pd.Series(portfolio_value, indexdates) # 计算日收益率百分比变化 self.portfolio_returns self.portfolio_values.pct_change().dropna() return self.portfolio_returns def calculate_metrics(self): 计算回测绩效指标 if self.portfolio_returns is None: print(Please run backtest first.) return {} total_return (self.portfolio_values.iloc[-1] / self.portfolio_values.iloc[0]) - 1 annual_return (1 total_return) ** (252 / len(self.portfolio_returns)) - 1 # 年化交易日假设252 annual_volatility self.portfolio_returns.std() * np.sqrt(252) sharpe_ratio annual_return / annual_volatility if annual_volatility ! 0 else np.nan # 计算最大回撤 cumulative (1 self.portfolio_returns).cumprod() peak cumulative.expanding().max() drawdown (cumulative - peak) / peak max_drawdown drawdown.min() metrics { 总收益率: total_return, 年化收益率: annual_return, 年化波动率: annual_volatility, 夏普比率: sharpe_ratio, 最大回撤: max_drawdown } return metrics def plot_results(self): 绘制净值曲线和回撤图 if self.portfolio_values is None: return fig, axes plt.subplots(2, 1, figsize(12, 10)) # 净值曲线 ax1 axes[0] ax1.plot(self.portfolio_values.index, self.portfolio_values / self.portfolio_values.iloc[0], label策略净值, linewidth2) if self.benchmark_returns is not None: # 计算基准净值 benchmark_cumulative (1 self.benchmark_returns).cumprod() ax1.plot(benchmark_cumulative.index, benchmark_cumulative / benchmark_cumulative.iloc[0], label基准净值, alpha0.7) ax1.set_title(策略净值曲线 vs 基准) ax1.set_ylabel(累计净值) ax1.legend() ax1.grid(True, linestyle--, alpha0.5) # 回撤图 ax2 axes[1] cumulative (1 self.portfolio_returns).cumprod() peak cumulative.expanding().max() drawdown (cumulative - peak) / peak ax2.fill_between(drawdown.index, 0, drawdown.values, colorred, alpha0.3) ax2.plot(drawdown.index, drawdown.values, colorred, linewidth1) ax2.set_title(策略回撤) ax2.set_ylabel(回撤) ax2.set_xlabel(日期) ax2.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(./results/backtest_result.png, dpi150) plt.show()5.6 主程序入口 (main.py)将所有模块串联起来。# main.py import pandas as pd from datetime import datetime, timedelta from data_fetcher import DataFetcher, get_hs300_constituents from factor_calculator import prepare_single_stock_factors from selector import StockSelector, low_pe_high_roe_strategy from backtester import Backtester import warnings warnings.filterwarnings(ignore) def main(): print(开始初始化选股工具...) fetcher DataFetcher() # 1. 定义回测参数 start_date 20220101 end_date 20231231 rebalance_freq M # 每月调仓也可以是 W (周), Q (季) # 2. 获取股票池这里用沪深300成分股作为示例减少数据量 print(获取股票池...) stock_pool get_hs300_constituents()[:50] # 取前50只仅用于演示加快速度 # stock_pool fetcher.get_stock_basic()[ts_code].tolist()[:100] # 或者用全市场前100只 # 3. 为股票池中的每只股票准备因子数据 print(获取并计算因子数据...) all_factors [] for i, code in enumerate(stock_pool): print(fProcessing {i1}/{len(stock_pool)}: {code}) try: df prepare_single_stock_factors(code, start_date, end_date, fetcher) if not df.empty: all_factors.append(df) except Exception as e: print(fError processing {code}: {e}) continue if not all_factors: print(未获取到任何有效因子数据。) return # 合并所有股票的因子数据 factors_df pd.concat(all_factors) # 确保索引是日期 factors_df.index pd.to_datetime(factors_df.index) factors_df.sort_index(inplaceTrue) # 4. 初始化选股器 selector StockSelector(factors_df) # 5. 生成调仓日及选股列表 print(生成调仓日选股列表...) # 生成所有调仓日期每月第一个交易日 all_dates pd.date_range(start_date, end_date, freqB) # B: 工作日 rebalance_dates all_dates[all_dates.is_month_start] # 每月第一天 selected_stocks_dict {} for date in rebalance_dates: if date in factors_df.index.unique(): # 确保该日期有数据 selected low_pe_high_roe_strategy(selector, date) if selected: selected_stocks_dict[date] selected print(f{date.date()}: 选中 {len(selected)} 只股票) else: selected_stocks_dict[date] [] else: selected_stocks_dict[date] [] # 6. 准备价格数据用于回测 print(准备价格数据...) price_data {} for code in stock_pool: try: df_price fetcher.get_daily_data(code, start_date, end_date) if not df_price.empty: price_data[code] df_price[[close]] except Exception as e: print(fError fetching price for {code}: {e}) # 7. 运行回测 print(运行回测...) backtester Backtester(price_data) returns backtester.run_backtest(selected_stocks_dict, initial_capital1000000, transaction_cost0.0003) # 假设交易成本万分之三 # 8. 输出结果 print(\n 回测结果 ) metrics backtester.calculate_metrics() for key, value in metrics.items(): if isinstance(value, float): print(f{key}: {value:.4f} if abs(value) 1 else f{key}: {value:.2f}) else: print(f{key}: {value}) # 9. 绘图 backtester.plot_results() print(\n回测完成。图表已保存至 ./results/backtest_result.png) if __name__ __main__: main()6. 运行结果与效果验证在配置好config.py中的Token并安装所有依赖后直接运行main.py。python main.py程序会依次执行初始化并获取股票池。循环获取每只股票的日线、基本面和财务数据并计算因子。在每个月初根据“市盈率25且ROE12%”的策略筛选股票。模拟等权重买入、持有、调仓的交易过程。计算绩效指标并绘图。预期输出控制台会打印数据处理进度和每个调仓日选中的股票数量。回测结束后会打印类似如下的绩效指标 回测结果 总收益率: 0.1587 年化收益率: 0.1023 年化波动率: 0.1876 夏普比率: 0.5453 最大回撤: -0.2214同时会在./results/文件夹下生成一张包含净值曲线和回撤图的PNG图片。如何验证结果是否合理检查数据完整性观察控制台日志是否有大量股票因数据缺失被跳过这会影响结果。检查选股数量每个调仓日选出的股票数量是否稳定如果某天突然为0或极多可能是因子条件太严或太松或数据出现问题。分析绩效指标年化收益率是否显著高于无风险利率如3%是否跑赢了基准如沪深300本例未引入基准你可以修改代码获取hs300指数数据进行比较。最大回撤-22%意味着你的策略可能在某段时间内亏损超过本金的五分之一。这个风险你是否能承受夏普比率大于0.5通常被认为尚可大于1则较好。它衡量的是承担单位风险所获得的超额回报。进行敏感性测试轻微调整因子条件如将PE25改为PE20观察结果变化是否剧烈。如果变化剧烈说明策略可能不稳定。7. 常见问题与排查思路在运行过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案tushare报错‘无效的token’Token未设置或已失效。检查config.py中的TUSHARE_TOKEN字符串是否正确。重新登录tushare.pro官网在个人中心复制新的token。获取数据速度极慢或超时。网络问题或未使用积分权限调用更高效的接口。观察控制台请求日志。1. 检查网络。2. 对于日线数据可使用pro.daily的ts_code和trade_date参数批量获取而非循环单只股票获取。本文示例为清晰起见未优化实际项目应优化。回测结果异常高如年化收益100%。极有可能存在“未来函数”。检查因子计算与数据发布日期是否对齐。例如是否在财报公布日前就使用了该财报数据确保在prepare_single_stock_factors函数中财务数据使用了merge_factors_to_daily进行向前填充这模拟了在报告公布后才能使用数据。更严谨的做法是使用ann_date公告日字段。KeyError: ‘close’或‘pe’。数据字段缺失。tushare接口返回的字段名可能变化或某些股票在某些日期无数据。打印出出错时df的列名检查数据获取是否成功。1. 在数据获取后添加检查if ‘close’ not in df.columns: continue。2. 使用try...except包裹可能出错的数据处理步骤。选出的股票数量为0。因子条件过于严格或该日期因子数据大量缺失。在select_by_conditions函数中打印df_date的因子列统计信息如非空值数量。1. 放宽筛选条件。2. 在筛选前对因子缺失值进行填充或删除需谨慎可能引入偏差。3. 确保股票池在该日期有交易。回测净值曲线是一条直线。交易未成功执行组合价值未变化。检查selected_stocks_dict是否为空检查price_data中是否有选中股票的价格调试run_backtest函数中买卖环节的逻辑。1. 打印selected_stocks_dict确认有选股结果。2. 在回测函数中打印每次调仓的买卖记录和持仓变化。绩效指标为NaN。收益率序列为空或全为0导致计算分母为0。检查portfolio_returns是否为空或全零。确保回测周期内有成功的交易发生。8. 最佳实践与工程建议如果你想将这个工具用于更严肃的研究或作为更复杂项目的基础请考虑以下建议数据本地化与缓存频繁调用tushareAPI 有频率限制。应将获取的数据保存到本地数据库如SQLite或文件中下次直接读取并定期更新。处理幸存者偏差我们的股票池只包含了当前存在的股票如当前沪深300成分股。这忽略了那些已经退市的股票会导致回测结果偏高。更严谨的做法是使用历史成分股数据。精细化处理未来函数财务数据的发布日期(ann_date)至关重要。必须确保在回测中只能在ann_date之后才能使用对应的财务指标。tushare的fina_indicator接口提供此字段。引入基准对比在Backtester类中可以传入基准指数如沪深300的日收益率序列并在计算绩效和绘图时进行对比这样才能判断策略是否真的战胜了市场。参数优化与过拟合检验不要根据一次回测结果就断定策略有效。应进行参数敏感性分析和样本外测试。例如用2018-2020年的数据训练确定最佳PE、ROE阈值再用2021-2023年的数据验证。代码重构与模块化当前示例为了流程清晰模块间耦合较高。可以进一步抽象例如将Strategy定义为一个基类方便实现不同的选股策略。使用配置文件如YAML来管理回测参数和因子条件。将回测结果保存到文件方便后续批量分析和比较。风险控制实盘交易中仅靠因子筛选不够。应加入仓位控制、止损止盈、行业/风格中性化等风控模块。理解策略逻辑的本质“低PE高ROE”是一个经典的价值投资因子。它的有效性与市场风格密切相关。在成长股受追捧的行情中此类策略可能长期跑输市场。理解你策略背后的经济学或行为金融学逻辑比单纯优化参数更重要。9. 总结与后续方向通过这三天的实践我们从一个想法出发构建了一个完整的、可运行的Python选股工具。它虽然简单但涵盖了量化回测的核心环节数据获取、因子计算、策略逻辑、模拟交易和绩效评估。最重要的是它提供了一个可验证、可迭代的框架。这个工具的价值不在于提供了一个“圣杯”策略而在于它极大地降低了量化研究的启动门槛。你可以轻松地修改selector.py中的low_pe_high_roe_strategy函数尝试你自己的想法技术指标结合加入MACD金叉、均线多头排列等条件。多因子排序对PE、PB、ROE等多个因子进行打分综合排序选股。行业轮动在不同月份选择不同的优势行业。事件驱动尝试整合财报发布、股东增持等事件。下一步你可以从以下几个方向深入完善数据层接入更多数据源如聚宽、优矿实现更规范的数据对齐和本地存储。丰富策略库实现动量策略、均值回归策略、小市值策略等经典策略并进行对比。深入分析工具除了夏普比率和最大回撤可以计算信息比率、索提诺比率、月度胜率等更细致的指标。向实盘迈进了解券商API如华泰、中信等学习如何将策略信号转化为实际的订单这一步风险极高务必先用模拟账户充分测试。记住所有回测结果都是对历史的拟合。在金融市场中过去表现永远不能保证未来收益。这个工具是你探索市场、验证逻辑的“显微镜”和“实验场”而非“印钞机”。希望这套代码能成为你量化学习之路上一块坚实的垫脚石。建议收藏本文并动手运行一遍代码在调试和修改的过程中你会有更深的体会。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门