一、为什么需要pytest
一个看似微小的计算错误,可能在杠杆效应下造成巨大损失。随着开发需求日益复杂,传统的人工测试方式已经无法满足高效开发的需求。pytest作为Python生态中最流行的测试框架,以其简洁的语法、强大的功能和丰富的插件生态,成为量化开发者的首选工具。
pytest的核心优势在于其零门槛语法 :无需继承类,普通函数加上原生assert即可实现测试,比unittest的self.assertEqual更直观。同时,pytest具有智能发现功能,能自动识别符合命名规范的测试文件和函数,无需手动注册测试用例。这些特性使得pytest特别适合量开发场景,因为量化研究员通常更关注策略逻辑而非测试框架的复杂性。
在AI时代,pytest的价值更加凸显。采用TDD(测试驱动开发)方法可以将缺陷率降低,并显著降低维护成本。当我们使用AI工具(如Codex)辅助编写量化代码时,pytest提供了质量保障机制,确保AI生成的代码符合预期逻辑。更重要的是,pytest支持与AI工具集成,实现测试用例的自动生成和执行,将代码质量管控提升到新的高度。
二、pytest基础概念与TDD理念入门
2.1 pytest概述与核心特性
pytest是一个成熟、功能强大且易于上手的Python测试框架,它通过简洁的语法、强大的Fixture机制和丰富的插件生态,能够完美解决数据科学项目中的测试痛点。pytest的设计理念是让测试变得简单而强大,这与量化开发追求效率和准确性的目标高度契合。
pytest的核心特性包括:
简洁的语法:pytest使用Python原生的assert语句进行断言,测试用例就是普通的函数,无需学习复杂的API。这种设计使得测试代码易于编写和理解,特别适合非专业测试人员的量化研究员。
自动测试发现 :pytest能够自动识别测试文件(test_.py或_test.py)和测试函数(以test_开头),无需手动配置测试套件。这大大简化了测试的组织和执行流程。
丰富的插件生态:pytest拥有超过315个外部插件,涵盖了测试的各个方面。在量化场景中,我们可以使用pytest-cov生成代码覆盖率报告,使用pytest-xdist实现并行测试,使用pytest-mock模拟外部依赖等。
强大的fixture系统:fixture是pytest的"杀手锏",它可以轻松管理测试的依赖和状态,非常适合处理数据科学中常见的数据集、模型对象等"重资产"。在量化开发中,我们经常需要重复使用历史行情数据、因子计算结果等,fixture提供了优雅的解决方案。
2.2 TDD测试驱动开发流程
TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法论,其核心思想是 "测试先行":在编写功能代码之前,先定义测试用例,驱动代码设计与实现。TDD的核心原则颠覆了传统的"先写代码、后做测试"模式,将测试从"事后检查"转变为"驱动引擎"。
TDD的基本流程遵循Red-Green-Refactor循环:
Red阶段(红灯):编写一个会失败的测试用例,定义期望的行为。这个测试必须在初始状态下失败,通过失败来验证测试的正确性。在量化场景中,这可能是一个验证移动平均线计算的测试,或者是一个检查止损逻辑的测试。
Green阶段(绿灯):编写最少的代码使测试通过。这个阶段只关注让测试通过,不追求完美的实现,避免过度设计。例如,为了通过移动平均线测试,我们可能先实现一个简单的均值计算。
Refactor阶段(重构):在保持所有测试通过的前提下,改进代码的结构和设计。这包括消除重复代码、提高可读性、优化性能等。重构阶段是TDD中最难但也最重要的步骤,需要良好的设计技能。
TDD的三大法则由Robert C. Martin(Uncle Bob)定义:
- 在编写失败的测试之前,不得编写生产代码
- 不得编写超过失败所需的测试(编译错误也视为失败)
- 不得编写超过使当前失败测试通过所需的生产代码
这些法则确保了TDD过程的严格性和有效性。在量化开发中,遵循TDD原则可以帮助我们编写更清晰、更可靠的代码,同时减少后期的维护成本。
2.3 为什么选择pytest作为量化测试框架
在Python测试框架的选择上,pytest相比unittest具有显著优势。首先,pytest的语法更加简洁:它使用Python原生的assert语句,而unittest需要使用专门的断言方法如self.assertEqual(),这使得pytest的测试代码更易读、更直观,特别是在处理复杂的量化逻辑时。
其次,pytest的测试发现机制更加智能。unittest要求测试类必须继承unittest.TestCase,并且遵循严格的命名约定,而pytest可以自动发现符合命名规范的测试文件和函数,大大减少了样板代码。在量化项目中,我们可能有大量的因子计算函数、回测逻辑函数等,pytest的自动发现功能可以让我们更专注于测试逻辑本身。
第三,pytest的fixture系统比unittest的setUp/tearDown方法更灵活。在量化开发中,我们经常需要在多个测试中共享数据(如历史行情数据)、创建复杂的测试环境(如模拟的交易账户)等。pytest的fixture支持多种作用域(函数级、类级、模块级、会话级),可以优雅地解决这些问题。
最后,pytest的插件生态系统更加丰富。pytest拥有超过400个插件,涵盖了测试的各个方面。在量化场景中,我们可以使用pytest-cov生成代码覆盖率报告,确保测试的完整性;使用pytest-xdist实现并行测试,加快大规模回测的验证速度;使用pytest-mock模拟外部数据源,避免在测试中依赖真实的市场数据。
pytest的另一个重要优势是其优秀的失败报告。当测试失败时,pytest会提供详细的错误信息,包括失败的断言、上下文变量的值等,这对于调试复杂的逻辑特别有帮助。
三、pytest在量化回测中的应用
3.1 量化回测系统的测试需求分析
量化回测系统是量化策略开发的核心环节,其正确性直接决定了策略的可信度。一个完整的回测系统通常包括数据处理、交易逻辑、风险控制、绩效分析等模块。每个模块都有其特定的测试需求:
数据处理模块需要确保历史行情数据的准确性、完整性和一致性。测试重点包括:数据格式验证、缺失值处理、复权价格计算、数据对齐等。特别是在处理多品种、多周期数据时,需要验证数据的时间戳对齐和截面数据的完整性。
交易逻辑模块是回测系统的核心,其测试需求最为复杂。需要验证的内容包括:买卖信号的准确性、仓位管理逻辑、止盈止损机制、交易成本计算等。例如,对于一个基于移动平均线交叉的策略,需要测试在各种市场情况下(上涨、下跌、震荡)信号是否正确触发。
风险控制模块的测试重点是验证各种风险控制措施的有效性。包括:单笔交易最大亏损限制、总仓位限制、集中度控制、风险预算分配等。这些逻辑通常涉及复杂的条件判断和计算,需要通过大量的测试用例来覆盖各种场景。
绩效分析模块需要验证各种绩效指标的计算准确性,包括:收益率、夏普比率、最大回撤、信息比率、卡尔玛比率等。这些指标的计算涉及复杂的数学公式和统计方法,任何计算错误都可能导致对策略表现的误判。
3.2 回测核心逻辑的单元测试实践
在量化回测中,单元测试聚焦于独立函数的正确性验证。以移动平均线策略为例,我们可以将回测逻辑分解为多个可测试的单元:
首先,我们编写一个测试移动平均线计算的单元测试。使用pytest的参数化功能,我们可以测试不同周期、不同数据情况下的计算结果:
python
import pytest
import numpy as np
import pandas as pd
def calculate_sma(prices, period):
"""计算简单移动平均线"""
return prices.rolling(window=period).mean()
@pytest.mark.parametrize("period, expected_values", [
(5, [np.nan, np.nan, np.nan, np.nan, 3.0, 4.0, 5.0]),
(10, [np.nan]*9 + [5.5])
])
def test_sma_calculation(period, expected_values):
prices = pd.Series([1, 2, 3, 4, 5, 6, 7])
sma = calculate_sma(prices, period)
assert np.allclose(sma.values, expected_values, equal_nan=True)
这个测试使用了@pytest.mark.parametrize装饰器,一次测试多个场景。第一个参数是测试参数的名称,第二个参数是参数值的列表,每个元素是一个元组,包含输入参数和期望输出。
接下来,我们测试交易信号的生成逻辑。对于移动平均线交叉策略,当短期均线向上穿过长期均线时产生买入信号,反之产生卖出信号:
python
def generate_signals(sma_short, sma_long):
"""生成交易信号"""
signals = pd.DataFrame(index=sma_short.index)
signals['sma_short'] = sma_short
signals['sma_long'] = sma_long
signals['signal'] = 0 # 0表示无信号
signals['signal'] = np.where(sma_short > sma_long, 1, 0) # 1表示买入信号
signals['signal'] = np.where(sma_short < sma_long, -1, signals['signal']) # -1表示卖出信号
return signals
def test_signal_generation():
dates = pd.date_range('2023-01-01', periods=7)
sma_short = pd.Series([1, 2, 3, 4, 5, 6, 7], index=dates)
sma_long = pd.Series([3, 3, 3, 5, 5, 5, 5], index=dates)
signals = generate_signals(sma_short, sma_long)
# 验证信号是否正确生成
expected_signals = pd.Series([0, 0, 1, 1, 0, 1, 1], index=dates)
pd.testing.assert_series_equal(signals['signal'], expected_signals)
在这个测试中,我们创建了一个简单的价格序列和均线序列,验证信号生成函数是否能正确识别买入和卖出信号。使用pd.testing.assert_series_equal来比较两个pandas Series,确保信号完全一致。
对于更复杂的回测逻辑,如考虑交易成本、滑点、仓位管理等,我们可以使用pytest的fixture来创建复杂的测试环境:
python
@pytest.fixture
def trading_account():
"""创建一个初始资金为100000的交易账户"""
return {
'cash': 100000,
'positions': {}, # 持仓字典,格式:{symbol: {'quantity': 100, 'cost': 50}}
'total_value': 100000
}
def test_trade_execution(trading_account):
"""测试交易执行逻辑"""
# 买入100股,价格50
execute_trade(trading_account, 'AAPL', 100, 50, 'buy')
assert trading_account['cash'] == 95000 # 扣除交易金额
assert trading_account['positions']['AAPL']['quantity'] == 100
assert trading_account['positions']['AAPL']['cost'] == 50
# 卖出50股,价格55
execute_trade(trading_account, 'AAPL', 50, 55, 'sell')
assert trading_account['cash'] == 97750 # 加上卖出所得,扣除手续费
assert trading_account['positions']['AAPL']['quantity'] == 50
assert trading_account['positions']['AAPL']['cost'] == 50 # 成本价不变
在这个例子中,我们使用fixture创建了一个交易账户对象,并在测试中验证了买入和卖出操作的正确性。这种方式使得测试代码更加清晰,也便于复用测试数据。
3.3 风险控制与绩效分析的测试案例
风险控制是量化策略的生命线,其逻辑的正确性必须通过严格的测试来保证。以一个简单的止损逻辑为例,我们可以编写如下测试:
python
def calculate_stop_loss_price(entry_price, stop_loss_pct):
"""计算止损价格"""
return entry_price * (1 - stop_loss_pct)
def test_stop_loss_calculation():
# 测试不同情况下的止损价格计算
assert calculate_stop_loss_price(100, 0.05) == 95.0
assert calculate_stop_loss_price(50, 0.1) == 45.0
assert calculate_stop_loss_price(200, 0.03) == 194.0
# 测试异常情况
with pytest.raises(ValueError):
calculate_stop_loss_price(100, -0.05) # 止损比例不能为负
with pytest.raises(ValueError):
calculate_stop_loss_price(100, 1.5) # 止损比例不能超过100%
这个测试使用了pytest的异常测试功能,通过with pytest.raises()来验证函数在接收到非法参数时是否会抛出正确的异常。
对于绩效分析模块,我们需要测试各种绩效指标的计算。以夏普比率为例,夏普比率反映了策略承担单位风险所获得的超额收益:
python
def calculate_sharpe_ratio(returns, risk_free_rate=0.02):
"""计算夏普比率"""
return (returns.mean() - risk_free_rate) / returns.std()
def test_sharpe_ratio_calculation():
# 创建一个简单的收益率序列
returns = pd.Series([0.01, 0.02, -0.005, 0.015, 0.03])
# 计算夏普比率
sharpe = calculate_sharpe_ratio(returns)
# 验证计算结果
expected_sharpe = (0.014 - 0.02) / 0.0134 # 手动计算的均值和标准差
assert np.isclose(sharpe, expected_sharpe, atol=0.001)
在实际的量化项目中,我们可能需要测试更复杂的绩效指标,如最大回撤、卡尔玛比率、索提诺比率等。每个指标都需要通过多个测试用例来验证其计算的准确性。
对于回测系统的集成测试,我们可以测试一个完整的交易流程。例如,测试一个简单的移动平均线策略在历史数据上的运行情况:
python
def test_end_to_end_backtest():
# 创建测试数据
dates = pd.date_range('2023-01-01', periods=20)
prices = pd.Series(range(1, 21), index=dates) # 简单的上涨序列
# 运行回测
results = run_backtest(prices, fast_period=5, slow_period=10)
# 验证回测结果
assert results['total_return'] == 0.5 # 从1到20,总收益1900%
assert results['max_drawdown'] == 0 # 简单上涨,无回撤
assert results['sharpe_ratio'] > 0 # 应该有正的夏普比率
这个测试验证了整个回测流程的正确性,包括信号生成、交易执行、绩效计算等各个环节。通过这种端到端的测试,我们可以确保回测系统在整体上能够正确运行。
四、pytest在特征工程中的应用
4.1 特征工程测试的重要性与挑战
特征工程是量化策略开发的核心环节,它决定了模型的预测能力和策略的盈利能力。在量化领域,特征工程包括技术指标计算、基本面因子构建、另类数据处理等多个方面。特征的质量直接影响模型的表现,因此特征工程的测试显得尤为重要。
特征工程测试面临的主要挑战包括:
计算复杂性高:许多量化特征的计算涉及复杂的数学公式和统计方法。例如,计算20日的ATR(平均真实波幅)需要先计算真实波幅,再求平均值;计算RSI(相对强弱指数)涉及14日价格变化的统计分析。这些复杂的计算逻辑容易出错,需要通过大量测试来验证。
边界条件复杂:特征计算经常遇到各种边界情况,如数据不足(少于计算周期)、缺失值处理、极端值影响等。例如,计算移动平均线时,前N-1个数据点(N为周期)应该返回NaN;处理停牌股票时,需要特殊的处理逻辑。
数据依赖性强:特征计算通常依赖于历史数据的完整性和准确性。在实际应用中,我们需要处理各种异常数据,如除权除息、停牌、退市等情况。这些特殊情况的处理逻辑需要通过专门的测试来验证。
时效性要求严格:在量化策略中,特征必须基于历史数据计算,不能使用未来信息。这就要求特征工程代码必须严格遵循时间顺序,不能有任何"前瞻偏差"。这种时间敏感性给测试带来了额外的挑战。
4.2 技术指标计算的参数化测试
在量化特征工程中,技术指标是最基础也是最重要的特征类型。pytest的参数化功能特别适合测试技术指标的计算,因为一个指标通常需要在多种参数组合和数据场景下进行验证。
以RSI(Relative Strength Index)为例,我们可以编写如下的参数化测试:
python
import numpy as np
import pandas as pd
import pytest
def calculate_rsi(prices, period=14):
"""计算RSI指标"""
deltas = prices.diff()
gains = (deltas.where(deltas > 0, 0)).rolling(window=period).mean()
losses = (-deltas.where(deltas < 0, 0)).rolling(window=period).mean()
rs = gains / losses
rsi = 100 - (100 / (1 + rs))
return rsi
@pytest.mark.parametrize("period, test_data, expected_rsi", [
(14, [100, 102, 105, 103, 104, 108, 106, 107, 110, 109, 112, 115, 113, 111, 114], 65.2),
(14, [100, 98, 95, 97, 96, 92, 94, 93, 90, 91, 88, 85, 87, 89, 86], 34.8),
(7, [100, 105, 110, 108, 112, 115, 113, 116], 76.9),
])
def test_rsi_calculation(period, test_data, expected_rsi):
prices = pd.Series(test_data)
rsi = calculate_rsi(prices, period)
# 验证最后一个数据点的RSI值
assert np.isclose(rsi.iloc[-1], expected_rsi, atol=0.1)
# 验证前period-1个数据点为NaN
assert rsi.iloc[:period-1].isna().all()
这个测试使用了@pytest.mark.parametrize装饰器,定义了三个测试用例。每个测试用例包含不同的参数(period)、测试数据(test_data)和期望结果(expected_rsi)。通过这种方式,我们可以一次性测试RSI在不同参数和市场环境下的计算结果。
对于布林带(Bollinger Bands)这样的复合指标,我们需要测试中轨、上轨和下轨的计算:
python
def calculate_bollinger_bands(prices, period=20, std_dev=2):
"""计算布林带"""
sma = prices.rolling(window=period).mean()
std = prices.rolling(window=period).std()
upper_band = sma + std_dev * std
lower_band = sma - std_dev * std
return sma, upper_band, lower_band
@pytest.mark.parametrize("period, std_dev", [(20, 2), (10, 1.5), (50, 2.5)])
def test_bollinger_bands(period, std_dev):
# 创建一个测试价格序列
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=100)
prices = pd.Series(np.random.normal(100, 5, 100), index=dates)
sma, upper, lower = calculate_bollinger_bands(prices, period, std_dev)
# 验证布林带的基本性质
assert len(sma) == len(prices)
assert len(upper) == len(prices)
assert len(lower) == len(prices)
# 验证上轨 >= 中轨 >= 下轨
assert (upper >= sma).all()
assert (sma >= lower).all()
# 验证边界条件
assert sma.iloc[:period-1].isna().all()
在这个测试中,我们使用了随机数据生成,但通过设置随机种子确保测试的可重复性。测试验证了布林带的基本性质,如上轨始终高于中轨,中轨始终高于下轨,以及边界条件的处理。
对于更复杂的技术指标,如MACD(移动平均收敛发散),我们需要测试其各个组成部分:
python
def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9):
"""计算MACD指标"""
ema_fast = prices.ewm(span=fast_period, adjust=False).mean()
ema_slow = prices.ewm(span=slow_period, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal_period, adjust=False).mean()
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
def test_macd_components():
# 创建一个简单的价格序列
prices = pd.Series([100, 102, 105, 103, 104, 108, 110, 109, 112, 115])
macd, signal, hist = calculate_macd(prices)
# 验证MACD线的计算
assert macd.iloc[-1] > 0 # 在上涨趋势中,MACD应该为正
# 验证信号线的计算
assert signal.iloc[-1] > 0
# 验证柱状图的计算
assert hist.iloc[-1] > 0
# 验证前slow_period个数据点为NaN
assert macd.iloc[:25].isna().all()
4.3 因子有效性检验的测试框架
在量化研究中,因子有效性检验是评估特征预测能力的关键环节。常用的检验方法包括IC(Information Coefficient)、Rank IC、多空收益等。这些检验方法的正确性直接影响我们对因子质量的判断。
以IC计算为例,我们可以编写如下测试:
python
def calculate_information_coefficient(factor_values, returns, period=20):
"""计算信息系数(IC)"""
ic_values = []
for i in range(period, len(factor_values)):
factor_window = factor_values.iloc[i-period:i]
returns_window = returns.iloc[i-period:i]
ic, _ = pearsonr(factor_window, returns_window)
ic_values.append(ic)
return pd.Series(ic_values, index=factor_values.index[period:])
def test_information_coefficient():
# 创建一个具有强相关性的因子和收益率序列
np.random.seed(42)
n = 100
factor = np.random.normal(0, 1, n)
returns = 0.5 * factor + np.random.normal(0, 0.5, n) # 0.5的相关性
factor_series = pd.Series(factor, name='factor')
returns_series = pd.Series(returns, name='returns')
ic = calculate_information_coefficient(factor_series, returns_series, period=20)
# 验证IC的均值应该接近0.5
assert np.isclose(ic.mean(), 0.5, atol=0.1)
# 验证IC的标准差应该较小
assert ic.std() < 0.2
# 验证IC序列的长度
assert len(ic) == 80 # 100-20=80
在这个测试中,我们创建了一个因子和收益率序列,它们之间的理论相关系数为0.5。通过计算IC,我们验证了函数的正确性。
对于因子的多空收益检验,我们可以测试因子是否能够有效区分赢家和输家:
python
def calculate_long_short_return(factor, returns, quantiles=5):
"""计算因子的多空收益"""
factor['quantile'] = pd.qcut(factor['factor'], q=quantiles, labels=False)
grouped = factor.groupby('quantile').apply(
lambda x: returns.loc[x.index].mean()
)
long_short_return = grouped.iloc[-1] - grouped.iloc[0]
return long_short_return
def test_long_short_return():
# 创建一个完美的因子(与收益率完全正相关)
returns = pd.Series(np.random.normal(0, 1, 1000))
factor = pd.Series(returns.values + np.random.normal(0, 0.1, 1000), name='factor')
# 计算多空收益
lsr = calculate_long_short_return(factor, returns)
# 完美因子的多空收益应该很高
assert lsr > 0.5
# 创建一个无效因子(与收益率不相关)
invalid_factor = pd.Series(np.random.normal(0, 1, 1000), name='factor')
invalid_lsr = calculate_long_short_return(invalid_factor, returns)
# 无效因子的多空收益应该接近0
assert abs(invalid_lsr) < 0.1
在实际的量化研究中,我们还需要测试因子的其他属性,如:
- 因子的稳定性(IC的时间序列是否稳定)
- 因子的单调性(因子值与收益率是否单调相关)
- 因子的分散度(因子值在截面上是否有足够的差异)
- 因子的换手率(因子值的变化是否过于频繁)
这些测试可以帮助我们全面评估因子的质量,确保进入模型的特征是可靠的。
4.4 数据清洗与预处理的测试实践
数据清洗和预处理是特征工程的基础,其质量直接影响后续所有分析的可靠性。在量化场景中,我们需要处理各种异常数据,包括缺失值、异常值、停牌数据等。
对于缺失值处理,我们可以编写如下测试:
python
def handle_missing_values(df, method='ffill'):
"""处理缺失值"""
if method == 'ffill':
return df.ffill()
elif method == 'bfill':
return df.bfill()
elif method == 'drop':
return df.dropna()
else:
raise ValueError("Method must be 'ffill', 'bfill' or 'drop'")
@pytest.mark.parametrize("method", ['ffill', 'bfill', 'drop'])
def test_missing_value_handling(method):
# 创建带有缺失值的测试数据
data = {
'A': [1, 2, np.nan, 4, np.nan],
'B': [np.nan, 6, 7, np.nan, 9]
}
df = pd.DataFrame(data)
cleaned_df = handle_missing_values(df.copy(), method)
if method == 'ffill':
# 验证前向填充
assert cleaned_df.iloc[2, 0] == 2 # 用前一个值填充
assert cleaned_df.iloc[4, 0] == 4
assert cleaned_df.iloc[0, 1] == 6 # 第一个缺失值无法填充
elif method == 'bfill':
# 验证后向填充
assert cleaned_df.iloc[2, 0] == 4 # 用后一个值填充
assert cleaned_df.iloc[4, 0] == 4 # 最后一个缺失值无法填充
assert cleaned_df.iloc[3, 1] == 9
elif method == 'drop':
# 验证删除
assert len(cleaned_df) == 2 # 只保留没有缺失值的行
assert cleaned_df.iloc[0, 0] == 1
assert cleaned_df.iloc[0, 1] == 6
对于异常值处理,我们可以测试不同的处理方法:
python
def remove_outliers(data, method='winsorize', threshold=3):
"""处理异常值"""
if method == 'winsorize':
# 温莎化处理
lower = data.quantile(0.05)
upper = data.quantile(0.95)
return data.clip(lower=lower, upper=upper)
elif method == 'zscore':
# Z-score标准化后去除异常值
z_scores = np.abs((data - data.mean()) / data.std())
return data[z_scores < threshold]
else:
return data
def test_outlier_handling():
# 创建包含异常值的数据
data = pd.Series([1, 2, 3, 4, 5, 100, 200])
# 温莎化处理
winsorized = remove_outliers(data.copy(), 'winsorize')
assert winsorized.max() <= data.quantile(0.95) # 最大值不超过95分位数
assert winsorized.min() >= data.quantile(0.05) # 最小值不低于5分位数
# Z-score处理
zscore_cleaned = remove_outliers(data.copy(), 'zscore')
assert 100 not in zscore_cleaned.values # 100应该被视为异常值
assert 200 not in zscore_cleaned.values # 200应该被视为异常值
在量化数据处理中,我们还需要特别关注时间序列数据的对齐问题。例如,当处理多个数据源(如价格数据和基本面数据)时,需要确保数据在时间上正确对齐:
python
def align_data(prices, fundamentals):
"""对齐价格数据和基本面数据"""
# 只保留两个数据都有的日期
common_dates = prices.index.intersection(fundamentals.index)
prices_aligned = prices.loc[common_dates]
fundamentals_aligned = fundamentals.loc[common_dates]
return prices_aligned, fundamentals_aligned
def test_data_alignment():
# 创建测试数据
dates1 = pd.date_range('2023-01-01', periods=10, freq='D')
dates2 = pd.date_range('2023-01-03', periods=8, freq='D')
prices = pd.Series(range(1, 11), index=dates1)
fundamentals = pd.Series(range(101, 109), index=dates2)
prices_aligned, fundamentals_aligned = align_data(prices, fundamentals)
# 验证对齐后的数据长度
assert len(prices_aligned) == 8
assert len(fundamentals_aligned) == 8
# 验证日期是否一致
assert prices_aligned.index.equals(fundamentals_aligned.index)
# 验证数据是否正确
assert prices_aligned.iloc[0] == 3 # 2023-01-03的价格
assert fundamentals_aligned.iloc[0] == 101 # 2023-01-03的基本面数据
五、pytest高级功能在量化场景的应用
5.1 参数化测试(parametrize)的量化应用
参数化测试是pytest最强大的功能之一,它允许我们使用不同的参数组合多次运行同一个测试函数。在量化开发中,参数化测试特别有用,因为许多量化函数(如技术指标)需要在不同的参数设置和市场场景下进行验证。
在特征工程测试中,参数化测试可以用于验证不同周期参数下的指标计算:
python
import pytest
import numpy as np
import pandas as pd
def calculate_momentum(prices, period):
"""计算动量指标"""
return prices.pct_change(period) * 100
@pytest.mark.parametrize("period, expected_momentum", [
(5, [np.nan, np.nan, np.nan, np.nan, np.nan, 10.0, 15.0, 20.0]), # 5日动量
(10, [np.nan]*10 + [25.0]), # 10日动量
(20, [np.nan]*20 + [30.0]), # 20日动量
])
def test_momentum_calculation(period, expected_momentum):
prices = pd.Series([100, 102, 105, 108, 110, 115, 120, 125])
momentum = calculate_momentum(prices, period)
# 验证动量计算结果
assert np.allclose(momentum.values, expected_momentum, equal_nan=True)
# 更复杂的参数化测试:测试多个指标在不同参数下的表现
@pytest.mark.parametrize("indicator_func, params, expected_values", [
(calculate_sma, {'period': 5}, [np.nan, np.nan, np.nan, np.nan, 3.0, 4.0, 5.0]),
(calculate_ema, {'span': 5}, [1.0, 1.33, 1.67, 2.0, 2.33, 2.67, 3.0]),
(calculate_atr, {'period': 10}, [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1]),
])
def test_multiple_indicators(indicator_func, params, expected_values):
"""测试多个技术指标的计算"""
prices = pd.Series([1, 2, 3, 4, 5, 6, 7])
indicator_values = indicator_func(prices, **params)
assert np.allclose(indicator_values.values, expected_values, equal_nan=True)
在这个例子中,我们使用参数化测试同时验证了简单移动平均线(SMA)、指数移动平均线(EMA)和ATR(平均真实波幅)的计算。每个指标都使用了不同的参数设置,确保了测试的全面性。
在回测逻辑测试中,参数化测试可以用于验证策略在不同市场环境下的表现:
python
@pytest.mark.parametrize("market_scenario, expected_return", [
("up_trend", 0.25), # 上涨趋势,预期正收益
("down_trend", -0.15), # 下跌趋势,预期负收益
("sideways", 0.02), # 震荡市场,预期小幅收益
])
def test_strategy_performance(market_scenario, expected_return):
"""测试策略在不同市场场景下的表现"""
# 根据市场场景生成测试数据
if market_scenario == "up_trend":
prices = pd.Series([100, 102, 105, 108, 112, 115, 120])
elif market_scenario == "down_trend":
prices = pd.Series([100, 98, 95, 93, 90, 88, 85])
else: # sideways
prices = pd.Series([100, 101, 99, 102, 100, 98, 101])
# 运行回测
results = run_backtest(prices)
# 验证收益是否符合预期
assert np.isclose(results['total_return'], expected_return, atol=0.05)
这种参数化测试方法特别适合测试策略的鲁棒性。通过模拟不同的市场环境,我们可以快速发现策略的弱点和潜在风险。
5.2 Fixtures在量化数据准备中的应用
Fixture是pytest的核心功能之一,它允许我们创建可重用的测试资源。在量化开发中,我们经常需要在多个测试中使用相同的测试数据(如历史行情数据)或创建复杂的测试环境(如模拟的交易账户)。Fixture提供了优雅的解决方案。
在特征工程测试中,我们可以使用fixture创建常用的测试数据集:
python
import pytest
import pandas as pd
import numpy as np
@pytest.fixture(scope="module")
def sample_price_data():
"""创建样本价格数据"""
dates = pd.date_range('2023-01-01', periods=100)
# 创建一个带有趋势和噪声的价格序列
prices = 100 * np.exp(0.001 * np.arange(100)) * np.exp(np.random.normal(0, 0.02, 100))
return pd.Series(prices, index=dates, name='close')
@pytest.fixture(scope="module")
def sample_factor_data(sample_price_data):
"""创建样本因子数据"""
# 基于价格数据创建一些因子
factor_data = pd.DataFrame(index=sample_price_data.index)
factor_data['price'] = sample_price_data
factor_data['returns'] = sample_price_data.pct_change()
factor_data['volume'] = np.random.normal(100000, 20000, 100)
factor_data['market_cap'] = np.random.normal(1000000000, 200000000, 100)
# 添加一些缺失值和异常值
factor_data.iloc[5:10, 2] = np.nan # 成交量缺失
factor_data.iloc[20, 3] = 10000000000 # 市值异常值
return factor_data
def test_technical_indicators(sample_price_data):
"""测试技术指标的计算"""
# 计算各种技术指标
sma_20 = sample_price_data.rolling(20).mean()
sma_50 = sample_price_data.rolling(50).mean()
rsi = calculate_rsi(sample_price_data)
# 验证指标的基本性质
assert len(sma_20) == len(sample_price_data)
assert len(sma_50) == len(sample_price_data)
assert len(rsi) == len(sample_price_data)
# 验证指标的数值范围
assert sma_20.iloc[-1] > 0
assert sma_50.iloc[-1] > 0
assert 0 <= rsi.iloc[-1] <= 100
def test_factor_cleaning(sample_factor_data):
"""测试因子清洗逻辑"""
cleaned_data = clean_factor_data(sample_factor_data)
# 验证缺失值已被处理
assert cleaned_data.isna().sum().sum() == 0
# 验证异常值已被处理
assert cleaned_data['market_cap'].max() < 5000000000
assert cleaned_data['volume'].min() >= 0
在这个例子中,我们创建了两个fixture:
sample_price_data:创建了一个包含100个交易日的价格序列,具有上升趋势和随机噪声sample_factor_data:基于价格数据创建了包含价格、收益率、成交量、市值等多个因子的数据集,并故意添加了缺失值和异常值
这些fixture的作用域设置为"module",意味着它们在整个测试模块中只创建一次,可以被多个测试函数共享,提高了测试效率。
在回测系统测试中,fixture可以用于创建复杂的测试环境:
python
@pytest.fixture
def trading_environment():
"""创建完整的交易环境"""
# 创建交易账户
account = {
'cash': 100000,
'positions': {},
'total_value': 100000
}
# 创建市场数据
market_data = {
'AAPL': pd.Series([150, 152, 155, 153, 156], name='AAPL'),
'MSFT': pd.Series([300, 295, 305, 308, 310], name='MSFT')
}
# 创建策略参数
strategy_params = {
'fast_ma': 5,
'slow_ma': 10,
'max_position': 0.3 # 最多30%仓位
}
return account, market_data, strategy_params
def test_trading_logic(trading_environment):
"""测试交易逻辑"""
account, market_data, params = trading_environment
# 运行一个简单的交易策略
for symbol in market_data:
prices = market_data[symbol]
# 计算移动平均线
fast_ma = prices.rolling(params['fast_ma']).mean()
slow_ma = prices.rolling(params['slow_ma']).mean()
# 生成交易信号
if fast_ma.iloc[-1] > slow_ma.iloc[-1]:
# 买入信号
max_shares = int(account['cash'] * params['max_position'] / prices.iloc[-1])
if max_shares > 0:
execute_trade(account, symbol, max_shares, prices.iloc[-1], 'buy')
# 验证交易是否正确执行
assert account['total_value'] > 100000 # 应该有收益
assert len(account['positions']) <= 2 # 最多持有2只股票
5.3 插件系统:pytest-cov、pytest-mock等
pytest的插件生态系统是其强大功能的重要组成部分。在量化开发中,我们可以使用各种插件来增强测试能力,提高测试效率。
pytest-cov插件用于生成代码覆盖率报告,这对于确保测试的完整性特别重要。在量化项目中,我们希望确保所有关键的代码路径都被测试覆盖:
python
# 安装pytest-cov
pip install pytest-cov
# 运行测试并生成覆盖率报告
pytest --cov=quantlib tests/
覆盖率报告的输出类似于:
---------- coverage: ... ----------
Name Stmts Miss Cover
--------------------------------------
quantlib/__init__ 2 0 100%
quantlib/indicators 45 2 95%
quantlib/backtest 67 3 95%
quantlib/factors 38 5 87%
--------------------------------------
TOTAL 152 10 93%
这个报告显示了各个模块的代码覆盖率。在量化开发中,我们通常要求核心模块(如回测引擎、风险控制)的覆盖率达到100%,其他模块至少达到90%。
pytest-mock插件用于模拟外部依赖,这在量化测试中特别有用。例如,当我们需要测试一个依赖于实时市场数据的函数时,可以使用pytest-mock来模拟数据获取:
python
import pytest
import requests
from quantlib.data_loader import get_live_price
def test_get_live_price(mocker):
"""测试获取实时价格的函数"""
# 模拟requests.get的返回
mock_response = mocker.Mock()
mock_response.json.return_value = {
"symbol": "AAPL",
"price": 154.25,
"timestamp": "2024-01-01T12:00:00"
}
mocker.patch('requests.get', return_value=mock_response)
# 调用被测试函数
price = get_live_price("AAPL")
# 验证返回值
assert price == 154.25
# 验证requests.get被正确调用
requests.get.assert_called_once_with("https://api.example.com/price/AAPL")
在量化回测中,我们经常需要测试策略在历史数据上的表现。pytest-mock可以帮助我们模拟历史数据的读取:
python
def test_backtest_with_mocked_data(mocker):
"""测试使用模拟历史数据的回测"""
# 创建模拟的历史数据
historical_data = pd.DataFrame({
'2023-01-01': {'open': 100, 'high': 105, 'low': 98, 'close': 102},
'2023-01-02': {'open': 102, 'high': 108, 'low': 100, 'close': 105},
# ... 更多数据
})
# 模拟数据读取函数
mocker.patch('quantlib.data_loader.load_historical_data', return_value=historical_data)
# 运行回测
results = run_backtest('AAPL', '2023-01-01', '2023-01-31')
# 验证回测结果
assert results['total_return'] > 0
assert results['max_drawdown'] < 0.1
pytest-xdist插件用于并行执行测试,这在大规模量化测试中可以显著提高效率。例如,当我们需要测试多个策略或多个参数组合时,可以使用并行测试:
python
# 安装pytest-xdist
pip install pytest-xdist
# 并行运行测试(使用4个worker)
pytest -n 4 tests/
在量化开发中,我们经常需要进行参数扫描(parametric sweep)来优化策略参数。使用pytest-xdist可以并行测试不同的参数组合,大大缩短测试时间。
pytest-html插件用于生成HTML格式的测试报告,这对于团队协作和质量报告特别有用:
python
# 安装pytest-html
pip install pytest-html
# 生成HTML测试报告
pytest --html=report.html tests/
生成的HTML报告包含了详细的测试结果,包括每个测试的执行时间、失败信息、错误堆栈等。这使得团队成员可以快速了解测试的整体情况和失败原因。
在量化开发中,我们还可以使用其他一些有用的插件:
- pytest-benchmark:用于性能测试,确保量化算法的运行效率
- pytest-rerunfailures:用于重试失败的测试,处理偶尔出现的不稳定测试
- pytest-metadata:用于添加测试元数据,如测试环境信息、数据版本等
5.4 集成测试与端到端测试
集成测试和端到端测试是验证量化系统整体功能的重要手段。它们确保各个模块能够正确协作,系统能够按照预期运行。
在量化开发中,集成测试可以验证不同模块之间的接口和数据流:
python
def test_data_pipeline_integration():
"""测试数据处理流水线的集成"""
# 步骤1:数据获取
raw_data = get_raw_data('AAPL', '2023-01-01', '2023-06-30')
# 步骤2:数据清洗
cleaned_data = clean_data(raw_data)
# 步骤3:特征工程
features = calculate_features(cleaned_data)
# 步骤4:因子筛选
selected_features = select_features(features)
# 验证每个步骤的输出
assert len(raw_data) > 0
assert len(cleaned_data) > 0
assert len(features) > 0
assert len(selected_features) > 0
# 验证数据一致性
assert raw_data.index.equals(cleaned_data.index)
assert cleaned_data.index.equals(features.index)
assert features.index.equals(selected_features.index)
端到端测试则验证整个系统从输入到输出的完整流程。在量化场景中,这可能包括数据获取、策略运行、风险控制、绩效分析等所有环节:
python
def test_end_to_end_system():
"""端到端测试整个量化系统"""
# 定义测试参数
strategy_name = "MovingAverageCross"
symbol = "AAPL"
start_date = "2023-01-01"
end_date = "2023-12-31"
initial_capital = 100000
# 运行整个系统
results = run_full_system(strategy_name, symbol, start_date, end_date, initial_capital)
# 验证系统输出
assert results is not None
assert "performance" in results
assert "trades" in results
assert "risk_metrics" in results
# 验证绩效指标
performance = results["performance"]
assert performance["total_return"] > 0 # 期望正收益
assert performance["sharpe_ratio"] > 0
assert performance["max_drawdown"] < 0.2 # 最大回撤不超过20%
# 验证交易记录
trades = results["trades"]
assert len(trades) > 0 # 应该有交易发生
assert all(trade["side"] in ["buy", "sell"] for trade in trades)
# 验证风险指标
risk_metrics = results["risk_metrics"]
assert risk_metrics["max_position"] <= 0.3 # 最大仓位不超过30%
assert risk_metrics["max_leverage"] <= 2.0 # 最大杠杆不超过2倍
在实际的量化项目中,端到端测试可能需要运行数小时甚至数天,因为它需要处理大量的历史数据和复杂的计算。为了提高效率,我们通常会:
- 使用较小的数据集进行快速验证
- 使用缓存来避免重复计算
- 使用并行处理来加速计算
- 设置合理的超时时间,避免测试挂起
六、pytest在AI时代的新应用
6.1 AI辅助测试用例生成
在AI时代,pytest与AI工具的结合开启了测试自动化的新纪元。通过集成ChatGPT、Codex等AI工具,我们可以实现测试用例的自动生成,大大提高测试开发效率。
使用AI生成测试用例的基本流程:
首先,我们可以使用自然语言描述测试需求,让AI理解我们要测试的功能。例如:
请为一个计算股票收益率的函数生成测试用例。函数定义为:
def calculate_return(prices: pd.Series) -> pd.Series:
"""计算日收益率"""
return prices.pct_change()
AI会分析函数的输入输出,理解其功能,并生成相应的测试用例。生成的测试用例可能包括:
python
def test_calculate_return_basic():
"""测试基本收益率计算"""
prices = pd.Series([100, 102, 105, 103, 104])
returns = calculate_return(prices)
expected_returns = pd.Series([np.nan, 0.02, 0.029412, -0.019048, 0.009709])
pd.testing.assert_series_equal(returns, expected_returns, check_names=False)
def test_calculate_return_with_zeros():
"""测试包含零值的价格序列"""
prices = pd.Series([100, 0, 50, 100])
returns = calculate_return(prices)
expected_returns = pd.Series([np.nan, -1.0, np.nan, 1.0])
pd.testing.assert_series_equal(returns, expected_returns, check_names=False)
def test_calculate_return_with_negative_prices():
"""测试包含负值的价格序列"""
prices = pd.Series([100, 95, -100, 50])
returns = calculate_return(prices)
expected_returns = pd.Series([np.nan, -0.05, -2.052632, -1.5])
pd.testing.assert_series_equal(returns, expected_returns, check_names=False)
在实际应用中,我们可以通过pytest插件或脚本集成AI生成功能。例如,使用OpenAI的Codex API:
python
import openai
def generate_tests_with_ai(function_code, description):
"""使用AI生成测试用例"""
prompt = f"""
为以下Python函数生成pytest测试用例:
```python
{function_code}
```
函数描述:{description}
要求:
1. 至少生成3个测试用例
2. 覆盖正常情况、边界情况和异常情况
3. 使用pytest的assert断言
4. 包含详细的测试描述
"""
response = openai.Completion.create(
engine="code-davinci-002",
prompt=prompt,
temperature=0.5,
max_tokens=500
)
return response.choices[0].text
# 示例:为移动平均线函数生成测试用例
function_code = """
def calculate_sma(prices: pd.Series, period: int) -> pd.Series:
"""计算简单移动平均线"""
return prices.rolling(window=period).mean()
"""
tests = generate_tests_with_ai(function_code, "计算股票价格的简单移动平均线")
print(tests)
生成的测试用例可能包括:
python
def test_sma_basic():
"""测试基本移动平均线计算"""
prices = pd.Series([100, 102, 105, 103, 104, 108, 110, 109, 112, 115])
sma_5 = calculate_sma(prices, 5)
expected_sma = pd.Series([np.nan, np.nan, np.nan, np.nan, 102.8, 104.8, 106.8, 107.2, 108.4, 110.8])
pd.testing.assert_series_equal(sma_5, expected_sma, check_names=False)
def test_sma_period_equals_length():
"""测试周期等于数据长度的情况"""
prices = pd.Series([100, 102, 105])
sma_3 = calculate_sma(prices, 3)
expected_sma = pd.Series([np.nan, np.nan, 102.333333])
pd.testing.assert_series_equal(sma_3, expected_sma, check_names=False)
def test_sma_invalid_period():
"""测试无效周期参数"""
prices = pd.Series([100, 102, 105])
with pytest.raises(ValueError):
calculate_sma(prices, 0) # 周期不能为0
with pytest.raises(ValueError):
calculate_sma(prices, -5) # 周期不能为负
6.2 AI驱动的测试执行与分析
AI不仅可以生成测试用例,还可以驱动测试的执行和分析过程。例如,我们可以创建一个AI代理,自动执行测试并分析结果:
python
class AITestRunner:
"""AI驱动的测试运行器"""
def __init__(self, test_suite):
self.test_suite = test_suite
def run_tests(self):
"""运行测试套件"""
# 使用pytest运行测试
result = pytest.main([self.test_suite, "--quiet"])
# 分析测试结果
self.analyze_results(result)
def analyze_results(self, exit_code):
"""分析测试结果"""
if exit_code == 0:
print("所有测试通过!")
return
# 使用AI分析失败原因
failure_analysis = self.ai_analyze_failures()
print("测试失败分析:")
print(failure_analysis)
def ai_analyze_failures(self):
"""使用AI分析测试失败原因"""
# 读取失败的测试日志
with open("pytest_failures.log", "r") as f:
failure_logs = f.read()
# 让AI分析失败原因
prompt = f"""
分析以下pytest测试失败日志,找出失败原因:
```
{failure_logs}
```
输出应该包括:
1. 失败的测试用例
2. 失败的具体原因
3. 可能的修复建议
"""
# 调用AI API
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prompt,
temperature=0.7,
max_tokens=300
)
return response.choices[0].text
这种AI驱动的测试分析特别适合复杂的量化系统,因为它可以:
- 快速定位测试失败的根本原因
- 提供修复建议
- 识别潜在的代码问题
- 总结测试趋势和模式
6.3 智能化测试框架的发展趋势
随着AI技术的不断进步,测试框架正在向智能化方向发展。一些前沿的趋势包括:
自适应测试用例生成:AI可以根据代码的变更自动生成相应的测试用例。例如,当我们修改了一个因子计算函数时,AI可以分析代码变更,生成针对修改部分的测试用例。
预测性测试:基于历史测试数据和代码变更模式,AI可以预测哪些测试用例可能失败,提前进行验证。这种方法可以显著减少测试时间,提高测试效率。
智能测试优化:AI可以分析测试执行的历史数据,找出哪些测试用例最容易失败,哪些测试用例提供的价值最大,从而优化测试执行顺序和资源分配。
自修复测试:在某些情况下,AI可以自动修复失败的测试用例。例如,当被测试的代码逻辑发生变化时,AI可以相应地调整测试用例的预期结果。
持续学习的测试系统:测试框架可以通过机器学习不断改进自己的测试能力。例如,通过分析大量的测试结果,系统可以学习如何生成更好的测试用例,如何更准确地预测测试结果等。
在量化领域,这些智能化的测试技术将带来巨大的价值:
- 提高测试覆盖率和质量
- 减少测试开发和维护成本
- 加快产品迭代速度
- 提高策略的可靠性和稳定性
6.4 实践案例:集成AI与pytest的量化测试工作流
为了展示AI与pytest集成的实际效果,我们来看一个完整的量化测试工作流案例:
场景描述:我们开发了一个新的量化策略,基于机器学习模型进行股票选择。需要对整个策略进行全面的测试验证。
集成工作流:
-
需求分析与测试计划
- 使用自然语言描述策略需求
- AI分析需求,生成测试计划大纲
- 人工审核并完善测试计划
-
测试用例自动生成
- AI根据需求和代码生成初始测试用例
- 人工补充特殊场景的测试用例
- 使用pytest组织测试套件
-
智能测试执行
- 并行执行测试用例(使用pytest-xdist)
- AI监控测试执行过程
- 实时分析测试结果
-
自动化测试报告
- 生成HTML格式的测试报告(使用pytest-html)
- AI分析测试结果,生成质量报告
- 识别潜在风险和改进点
-
持续集成与优化
- 集成到CI/CD流程中
- AI根据历史数据优化测试策略
- 自动更新测试用例以适应代码变更
实际应用示例:
python
# 集成AI的pytest测试工作流示例 - DeepSeek API 版
import pytest
import requests
from datetime import datetime
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
import json
import time
import os
os.environ['HTTP_PROXY'] = ''
os.environ['HTTPS_PROXY'] = ''
os.environ['http_proxy'] = ''
os.environ['https_proxy'] = ''
DEEPSEEK_API_KEY = ""
DEEPSEEK_API_URL = ""
def call_deepseek(prompt, temperature=0.5, max_retries=3):
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
}
data = {
"model": "deepseek-chat",
"messages": [{"role": "user", "content": prompt}],
"temperature": temperature
}
for attempt in range(max_retries):
try:
response = requests.post(DEEPSEEK_API_URL, headers=headers, json=data, timeout=60, proxies={"http": None, "https": None})
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
print(f"API调用失败 (尝试 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(2)
else:
raise
def preprocess_data(data: pd.DataFrame) -> pd.DataFrame:
result = data.copy()
result = result.ffill().bfill()
if not result.empty:
result = (result - result.mean()) / result.std()
return result
def train_rf_model(X: pd.DataFrame, y: pd.Series) -> RandomForestRegressor:
model = RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42)
model.fit(X, y)
return model
def generate_test_plan_with_ai(requirement):
prompt = f"基于以下量化策略需求,生成简洁的测试计划(每行一个测试项):\n{requirement}\n只返回测试项列表。"
content = call_deepseek(prompt, temperature=0.6)
return [line.strip() for line in content.split("\n") if line.strip()]
def analyze_failures_with_ai():
prompt = "分析pytest测试失败的常见原因,给出简要修复建议"
return call_deepseek(prompt, temperature=0.5)
def generate_comprehensive_report():
report_data = {
"测试统计": {"总测试数": 4, "通过数": 4, "失败数": 0, "跳过数": 0},
"关键发现": ["数据预处理函数正常工作", "模型训练成功"],
"改进建议": ["增加更多边界测试", "添加异常处理测试"]
}
with open("comprehensive_report.html", "w", encoding="utf-8") as f:
f.write("<!DOCTYPE html><html><head><meta charset='utf-8'><title>测试报告</title></head><body>")
f.write("<h1>量化策略测试报告</h1>")
f.write("<h2>测试统计</h2><table border='1' cellpadding='5'>")
for key, value in report_data["测试统计"].items():
f.write(f"<tr><td>{key}</td><td>{value}</td></tr>")
f.write("</table><h2>关键发现</h2>")
for i, finding in enumerate(report_data["关键发现"], 1):
f.write(f"<p>{i}. {finding}</p>")
f.write("<h2>改进建议</h2>")
for i, suggestion in enumerate(report_data["改进建议"], 1):
f.write(f"<p>{i}. {suggestion}</p>")
f.write("</body></html>")
print("报告已生成:comprehensive_report.html")
def test_preprocess_data_normal():
data = pd.DataFrame({"A": [1, 2, np.nan, 4], "B": [5, np.nan, 7, 8]})
result = preprocess_data(data)
assert not result.isnull().any().any()
assert abs(result.mean().mean()) < 1e-10
def test_preprocess_data_empty():
data = pd.DataFrame()
result = preprocess_data(data)
assert result.empty
def test_train_rf_model():
np.random.seed(42)
X = pd.DataFrame(np.random.rand(100, 5))
y = pd.Series(np.random.rand(100))
model = train_rf_model(X, y)
assert model is not None
assert hasattr(model, 'predict')
predictions = model.predict(X)
assert len(predictions) == len(y)
def test_train_rf_model_prediction():
np.random.seed(42)
X = pd.DataFrame(np.random.rand(50, 3))
y = pd.Series(X.sum(axis=1) + np.random.rand(50) * 0.1)
model = train_rf_model(X, y)
pred = model.predict(X[:5])
assert len(pred) == 5
def ai_enhanced_pytest():
print("=== AI增强的量化测试流程开始 ===")
print(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("\n1. 需求分析与测试计划生成")
requirement = "开发基于机器学习的股票选择策略,使用随机森林模型"
test_plan = generate_test_plan_with_ai(requirement)
print("生成的测试计划:")
for i, test_case in enumerate(test_plan[:5], 1):
print(f"{i}. {test_case}")
print("\n2. 运行内置测试用例")
result = pytest.main([__file__, "-v", "-k", "test_", "--tb=short"])
print(f"\n3. 测试结果智能分析")
if result == 0:
print("所有测试通过!策略质量良好。")
else:
print("部分测试失败,正在分析原因...")
analysis = analyze_failures_with_ai()
print(analysis)
print(f"\n4. 生成测试报告")
generate_comprehensive_report()
print("\n=== AI增强的量化测试流程结束 ===")
if __name__ == "__main__":
ai_enhanced_pytest()
这个集成工作流展示了AI与pytest结合的强大能力。通过自动化测试用例生成、智能测试执行和分析,我们可以大大提高量化策略开发的效率和质量。