pytest应用实践

一、为什么需要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)定义:

  1. 在编写失败的测试之前,不得编写生产代码
  2. 不得编写超过失败所需的测试(编译错误也视为失败)
  3. 不得编写超过使当前失败测试通过所需的生产代码

这些法则确保了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集成的实际效果,我们来看一个完整的量化测试工作流案例:

场景描述:我们开发了一个新的量化策略,基于机器学习模型进行股票选择。需要对整个策略进行全面的测试验证。

集成工作流

  1. 需求分析与测试计划

    • 使用自然语言描述策略需求
    • AI分析需求,生成测试计划大纲
    • 人工审核并完善测试计划
  2. 测试用例自动生成

    • AI根据需求和代码生成初始测试用例
    • 人工补充特殊场景的测试用例
    • 使用pytest组织测试套件
  3. 智能测试执行

    • 并行执行测试用例(使用pytest-xdist)
    • AI监控测试执行过程
    • 实时分析测试结果
  4. 自动化测试报告

    • 生成HTML格式的测试报告(使用pytest-html)
    • AI分析测试结果,生成质量报告
    • 识别潜在风险和改进点
  5. 持续集成与优化

    • 集成到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结合的强大能力。通过自动化测试用例生成、智能测试执行和分析,我们可以大大提高量化策略开发的效率和质量。

相关推荐
Lary_c1 天前
【测试自动化】pytest + Allure 完整学习指南
运维·自动化·pytest
我的xiaodoujiao4 天前
API接口自动化测试详细图文教程学习系列1--序章
python·学习·pytest
我的xiaodoujiao4 天前
API 接口自动化测试详细图文教程学习系列2--相关Python基础知识
python·学习·测试工具·pytest
H_unique5 天前
博客接口自动化测试--搭建测试环境&库的介绍&安装allure
python·pytest·测试
真智AI5 天前
MCP+pytest自动重构回归:复刻ARIS循环
重构·pytest
零基础的修炼6 天前
自动化测试---pytest
pytest
鲜于言悠9057 天前
博客系统测试报告
python·功能测试·selenium·jmeter·测试用例·集成测试·pytest
0和1的舞者8 天前
高并发论坛系统:单元测试 + 接口自动化 + 性能测试 + CI/CD 全链路测试报告
java·测试开发·测试工具·jmeter·pytest·测试·测试报告
独断万古他化8 天前
Python+Pytest 搭建博客系统接口自动化测试框架(全用例执行+完整代码)
pytest·接口自动化·测试·allure·requests