news 2026/8/13 9:28:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python量化选股实战:从零搭建轻量级回测框架

如果你正在学习Python数据分析,或者对量化投资感兴趣,但总觉得那些复杂的金融模型和庞大的回测框架离自己太远,那么这篇文章就是为你准备的。

我花了三天时间,用Python写了一个轻量级的选股工具。它的核心逻辑并不复杂,但当我用它跑了一遍A股数据后,筛选出的股票组合在模拟回测中的表现,确实让我有些意外——它以一种简单直接的方式,验证了某些基础策略的有效性。这让我意识到,对于大多数想入门量化、验证自己想法的开发者来说,一个能快速跑通的“最小可行产品”(MVP),远比一个功能庞杂但难以上手的系统更有价值。

本文将完整分享这个工具的设计思路、实现代码和使用方法。你不会看到复杂的机器学习模型或高频交易算法,我们将聚焦于如何用最基础的pandasnumpytushare(一个免费的财经数据接口),构建一个从数据获取、因子计算到结果筛选的完整流程。更重要的是,我会详细拆解其中容易踩坑的细节,比如数据清洗的陷阱、回测中常见的“未来函数”问题,以及如何客观地看待回测结果。

无论你是想将Python应用于金融分析的初学者,还是希望有一个轻量级框架来快速验证策略想法的开发者,这篇文章都能提供一个清晰的路径和一套可直接运行的源码。

1. 这个工具解决了什么问题:从想法到验证的快速通道

在量化投资领域,一个新策略从构思到验证,往往面临很高的工程门槛。你可能有一个关于“低市盈率+高股息率”股票表现更好的想法,但要验证它,你需要:

  1. 获取历史股票数据(价格、财务指标)。
  2. 编写代码计算每个时间点的市盈率、股息率。
  3. 根据规则筛选出符合条件的股票。
  4. 计算这些股票在后续一段时间内的收益。
  5. 与市场基准(如沪深300指数)进行对比。

这个过程涉及数据管理、时间序列处理、性能计算等多个环节。对于新手,很容易在数据对齐、避免“未来函数”(使用了未来的信息)等基础问题上犯错,导致回测结果失真,毫无参考价值。

这个Python选股工具的核心价值,就在于它搭建了一个标准化、可复用的轻量级回测框架。它帮你处理了数据获取、清洗、时间对齐等脏活累活,让你能专注于最核心的部分:定义你的选股逻辑(因子)。你可以把它看作一个“策略实验室”,用最小的代码改动,快速测试不同因子组合的有效性。

2. 核心概念与工具选型:为什么是它们?

在开始编码前,我们需要明确几个关键概念和工具选择的原因。

2.1 什么是“因子”?

在量化选股中,“因子”就是一个用于评价股票的标准或指标。它通常是一个可计算的数值。例如:

  • 估值因子:市盈率(PE)、市净率(PB)。
  • 盈利因子:净资产收益率(ROE)、毛利率。
  • 成长因子:营业收入增长率、净利润增长率。
  • 技术因子:动量、波动率。

我们的工具就是让用户定义一组因子和筛选条件(如“市盈率小于20且ROE大于15%”),程序会自动找出所有满足条件的股票。

2.2 工具栈选择:轻量、免费、易用

  • tushare:一个提供国内股票、基金、期货等金融数据的免费Python库。对于个人学习和研究来说,其基础数据完全够用。它避免了我们去爬取各类财经网站,提供了结构化的API。
  • pandas:数据分析的核心。股票数据本质上是带时间戳的表格数据,pandasDataFrame非常适合进行切片、聚合、合并等操作。
  • numpy:进行高效的数值计算,比如计算收益率、统计指标等。
  • matplotlib:用于可视化回测结果,绘制收益曲线图。

这个组合摒弃了BacktraderZipline等重型回测框架的复杂性,让我们能更直观地理解回测的每一个步骤。

2.3 警惕“未来函数”与过拟合

这是两个新手最容易掉入的陷阱,我们的工具设计会特别注意规避。

  • 未来函数:指在回测中,使用了在交易当时无法获得的信息。例如,在计算2019年1月1日的指标时,错误地使用了2019年1月2日才公布的财务数据。这会导致回测结果过于乐观,实盘必然失败。我们的工具将通过严格的数据发布日期对齐来解决。
  • 过拟合:指策略在历史数据上表现完美,但对未来数据(样本外)预测能力很差。通常因为策略参数过于复杂,恰好“记住”了历史噪声。我们将通过强调策略逻辑的简洁性和进行样本外测试的建议来提醒读者。

3. 环境准备与项目初始化

首先,确保你的Python环境是3.7或以上版本。我们将使用pip进行包管理。

3.1 创建虚拟环境(推荐)

为了避免包版本冲突,建议使用虚拟环境。

# 使用 conda (如果你安装了Anaconda) conda create -n stock_selector python=3.8 conda activate stock_selector # 或者使用 venv (Python内置) python -m venv venv # Windows venv\Scripts\activate # Linux/Mac source venv/bin/activate

3.2 安装依赖库

在激活的虚拟环境中,运行以下命令:

pip install pandas numpy matplotlib tushare

tushare需要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. 核心流程拆解:五步构建选股系统

我们的工具工作流程分为五个核心步骤,每一步对应一个模块。

  1. 数据获取(data_fetcher.py):从tushare获取股票列表、日线行情、基本面数据。
  2. 因子计算(factor_calculator.py):基于原始数据,计算所需的选股因子,如PE、PB、ROE等。
  3. 股票筛选(selector.py):根据用户定义的因子条件(如“PE < 30 and ROE > 0.1”),在每个调仓日筛选出股票池。
  4. 回测引擎(backtester.py):模拟交易。假设在调仓日等权重买入筛选出的股票,持有一定周期后卖出,计算组合收益。
  5. 绩效分析(backtester.py):计算年化收益、夏普比率、最大回撤等指标,并可视化。

5. 完整代码实现与详解

下面我们分模块实现代码。请将以下代码分别保存到对应的文件中。

5.1 配置文件 (config.py)

这里存放你的tusharetoken。切记不要将包含真实Token的代码上传至公开仓库。

# config.py TUSHARE_TOKEN = '你的tushare_token_在这里' # 请替换为你在tushare.pro官网申请的token

5.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_status='L', fields='ts_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_code=ts_code, start_date=start_date, end_date=end_date) df['trade_date'] = pd.to_datetime(df['trade_date']) df.set_index('trade_date', inplace=True) df.sort_index(inplace=True) return df def get_daily_basic(self, trade_date): """获取指定交易日的每日指标(包含PE, PB等)""" # 注意:trade_date格式为‘YYYYMMDD’ df = self.pro.daily_basic(trade_date=trade_date, fields='ts_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_code=ts_code, start_date=start_date, end_date=end_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', inplace=True) df.sort_index(inplace=True) 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, window=20): """计算动量因子:过去window日的收益率""" return price_series.pct_change(window) @staticmethod def calculate_volatility(price_series, window=20): """计算波动率因子:过去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, how='left') # 对因子列进行向前填充,处理合并后可能产生的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, freq='B'): # 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(f"Failed 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', inplace=True) # 合并PE, PB等日频因子 daily_df = daily_df.join(daily_basic_df[['pe', 'pb']], how='left') # 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'], window=20) daily_df['volatility_20'] = FactorCalculator.calculate_volatility(daily_df['close'], window=20) daily_df['ts_code'] = ts_code return daily_df

5.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, index=df_date.index) for factor, (op, value) in conditions.items(): if factor not in df_date.columns: print(f"Warning: 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(f"Unsupported operator: {op}") selected_stocks = df_date[mask]['ts_code'].tolist() return selected_stocks def select_top_n(self, date, factor, n=10, ascending=True): """在指定日期,根据某个因子排序,选择前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(by=factor, ascending=ascending) 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_data=None): """ price_data: dict, {ts_code: pd.DataFrame with index=trade_date and column='close'} 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_capital=1000000, transaction_cost=0.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(days=1)] # 记录起始前一天的价值 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(f"Insufficient 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, index=dates) # 计算日收益率(百分比变化) 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='策略净值', linewidth=2) 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='基准净值', alpha=0.7) ax1.set_title('策略净值曲线 vs 基准') ax1.set_ylabel('累计净值') ax1.legend() ax1.grid(True, linestyle='--', alpha=0.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, color='red', alpha=0.3) ax2.plot(drawdown.index, drawdown.values, color='red', linewidth=1) ax2.set_title('策略回撤') ax2.set_ylabel('回撤') ax2.set_xlabel('日期') ax2.grid(True, linestyle='--', alpha=0.5) plt.tight_layout() plt.savefig('./results/backtest_result.png', dpi=150) 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(f"Processing {i+1}/{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(f"Error 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(inplace=True) # 4. 初始化选股器 selector = StockSelector(factors_df) # 5. 生成调仓日及选股列表 print("生成调仓日选股列表...") # 生成所有调仓日期(每月第一个交易日) all_dates = pd.date_range(start_date, end_date, freq='B') # 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(f"Error fetching price for {code}: {e}") # 7. 运行回测 print("运行回测...") backtester = Backtester(price_data) returns = backtester.run_backtest(selected_stocks_dict, initial_capital=1000000, transaction_cost=0.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

程序会依次执行:

  1. 初始化并获取股票池。
  2. 循环获取每只股票的日线、基本面和财务数据,并计算因子。
  3. 在每个月初,根据“市盈率<25且ROE>12%”的策略筛选股票。
  4. 模拟等权重买入、持有、调仓的交易过程。
  5. 计算绩效指标并绘图。

预期输出

  • 控制台会打印数据处理进度和每个调仓日选中的股票数量。
  • 回测结束后,会打印类似如下的绩效指标:
========== 回测结果 ========== 总收益率: 0.1587 年化收益率: 0.1023 年化波动率: 0.1876 夏普比率: 0.5453 最大回撤: -0.2214
  • 同时,会在./results/文件夹下生成一张包含净值曲线和回撤图的PNG图片。

如何验证结果是否合理?

  1. 检查数据完整性:观察控制台日志,是否有大量股票因数据缺失被跳过?这会影响结果。
  2. 检查选股数量:每个调仓日选出的股票数量是否稳定?如果某天突然为0或极多,可能是因子条件太严或太松,或数据出现问题。
  3. 分析绩效指标
    • 年化收益率:是否显著高于无风险利率(如3%)?是否跑赢了基准(如沪深300)?本例未引入基准,你可以修改代码获取hs300指数数据进行比较。
    • 最大回撤:-22%意味着你的策略可能在某段时间内亏损超过本金的五分之一。这个风险你是否能承受?
    • 夏普比率:大于0.5通常被认为尚可,大于1则较好。它衡量的是承担单位风险所获得的超额回报。
  4. 进行敏感性测试:轻微调整因子条件(如将PE<25改为PE<20),观察结果变化是否剧烈。如果变化剧烈,说明策略可能不稳定。

7. 常见问题与排查思路

在运行过程中,你可能会遇到以下问题:

问题现象可能原因排查方式解决方案
tushare报错‘无效的token’Token未设置或已失效。检查config.py中的TUSHARE_TOKEN字符串是否正确。重新登录tushare.pro官网,在个人中心复制新的token。
获取数据速度极慢或超时。网络问题,或未使用积分权限调用更高效的接口。观察控制台请求日志。1. 检查网络。
2. 对于日线数据,可使用pro.dailyts_codetrade_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. 最佳实践与工程建议

如果你想将这个工具用于更严肃的研究或作为更复杂项目的基础,请考虑以下建议:

  1. 数据本地化与缓存:频繁调用tushareAPI 有频率限制。应将获取的数据保存到本地数据库(如SQLite)或文件中,下次直接读取,并定期更新。
  2. 处理幸存者偏差:我们的股票池只包含了当前存在的股票(如当前沪深300成分股)。这忽略了那些已经退市的股票,会导致回测结果偏高。更严谨的做法是使用历史成分股数据。
  3. 精细化处理未来函数:财务数据的发布日期(ann_date)至关重要。必须确保在回测中,只能在ann_date之后才能使用对应的财务指标。tusharefina_indicator接口提供此字段。
  4. 引入基准对比:在Backtester类中,可以传入基准指数(如沪深300)的日收益率序列,并在计算绩效和绘图时进行对比,这样才能判断策略是否真的战胜了市场。
  5. 参数优化与过拟合检验:不要根据一次回测结果就断定策略有效。应进行参数敏感性分析样本外测试。例如,用2018-2020年的数据训练(确定最佳PE、ROE阈值),再用2021-2023年的数据验证。
  6. 代码重构与模块化:当前示例为了流程清晰,模块间耦合较高。可以进一步抽象,例如:
    • Strategy定义为一个基类,方便实现不同的选股策略。
    • 使用配置文件(如YAML)来管理回测参数和因子条件。
    • 将回测结果保存到文件,方便后续批量分析和比较。
  7. 风险控制:实盘交易中,仅靠因子筛选不够。应加入仓位控制、止损止盈、行业/风格中性化等风控模块。
  8. 理解策略逻辑的本质:“低PE高ROE”是一个经典的价值投资因子。它的有效性与市场风格密切相关。在成长股受追捧的行情中,此类策略可能长期跑输市场。理解你策略背后的经济学或行为金融学逻辑,比单纯优化参数更重要。

9. 总结与后续方向

通过这三天的实践,我们从一个想法出发,构建了一个完整的、可运行的Python选股工具。它虽然简单,但涵盖了量化回测的核心环节:数据获取、因子计算、策略逻辑、模拟交易和绩效评估。最重要的是,它提供了一个可验证、可迭代的框架。

这个工具的价值不在于提供了一个“圣杯”策略,而在于它极大地降低了量化研究的启动门槛。你可以轻松地修改selector.py中的low_pe_high_roe_strategy函数,尝试你自己的想法:

  • 技术指标结合:加入MACD金叉、均线多头排列等条件。
  • 多因子排序:对PE、PB、ROE等多个因子进行打分,综合排序选股。
  • 行业轮动:在不同月份选择不同的优势行业。
  • 事件驱动:尝试整合财报发布、股东增持等事件。

下一步,你可以从以下几个方向深入:

  1. 完善数据层:接入更多数据源(如聚宽、优矿),实现更规范的数据对齐和本地存储。
  2. 丰富策略库:实现动量策略、均值回归策略、小市值策略等经典策略,并进行对比。
  3. 深入分析工具:除了夏普比率和最大回撤,可以计算信息比率、索提诺比率、月度胜率等更细致的指标。
  4. 向实盘迈进:了解券商API(如华泰、中信等),学习如何将策略信号转化为实际的订单(这一步风险极高,务必先用模拟账户充分测试)。

记住,所有回测结果都是对历史的拟合。在金融市场中,过去表现永远不能保证未来收益。这个工具是你探索市场、验证逻辑的“显微镜”和“实验场”,而非“印钞机”。希望这套代码能成为你量化学习之路上一块坚实的垫脚石。建议收藏本文,并动手运行一遍代码,在调试和修改的过程中,你会有更深的体会。

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

STM32 HAL库GPIO驱动入门:从点灯到工程实践与调试

1. 项目概述&#xff1a;从点灯开始&#xff0c;理解STM32 HAL库的编程范式 拿到一块STM32开发板&#xff0c;第一件事是什么&#xff1f;我相信绝大多数工程师和爱好者的答案都是&#xff1a;点灯。这个看似简单的“Hello World”操作&#xff0c;却是我们叩开STM32 HAL库开发…

作者头像 李华
网站建设 2026/8/13 9:22:43

Scratch Workspaces:解决LLM复杂任务处理难题的工程范式

你有没有遇到过这种情况&#xff1a;给一个大语言模型&#xff08;LLM&#xff09;一个复杂的、多步骤的任务&#xff0c;比如“分析这份财报&#xff0c;总结关键风险&#xff0c;并生成一份给管理层的建议报告”。模型开始输出了&#xff0c;前半部分分析得头头是道&#xff…

作者头像 李华
网站建设 2026/8/13 9:22:26

RAG系统智能检索路由:四层框架解决多路径选择难题

1. 项目概述&#xff1a;从面试题到工程化思考“你的 RAG 有几种检索路径&#xff1f;怎么决定走哪条&#xff1f;” 这问题一抛出来&#xff0c;很多做 RAG 的朋友可能心里会咯噔一下。我们平时聊 RAG&#xff0c;张口闭口就是向量检索、Embedding 模型、Chunk 策略&#xff0…

作者头像 李华
网站建设 2026/8/13 9:21:41

第十七届CMC数学A类试题深度解析:从核心考点到高效备赛策略

1. 赛题概览与核心价值&#xff1a;为什么这份试卷值得深挖&#xff1f;又到了每年一度的全国大学生数学竞赛&#xff08;CMC&#xff09;赛季&#xff0c;对于数学专业&#xff08;A类&#xff09;的同学来说&#xff0c;拿到一份新鲜出炉的真题&#xff0c;心情总是既兴奋又忐…

作者头像 李华
网站建设 2026/8/13 9:21:07

别只教孩子写提示词:把 AI 回答拆成一条可验证的流水线

很多面向孩子的 AI 课程&#xff0c;第一节就开始教提示词公式&#xff1a;角色、任务、背景、格式。这个方法当然有用&#xff0c;但如果课程到这里就停下来&#xff0c;孩子学到的仍然只是“怎样让模型更像一个熟练的回答者”。真正困难的问题在后面&#xff1a;• 它的回答依…

作者头像 李华