量化平台中的回测引擎设计与实现

0 阅读7分钟

回测是量化策略开发的核心环节。一个好的回测引擎不仅能够准确评估策略的历史表现,还能为策略优化提供可靠的数据支持。EasyQuant 的回测引擎基于"真实模拟"(Realistic Simulation)原则,尽可能还原真实交易环境。

结论先行:EasyQuant 的回测引擎采用事件驱动架构,支持精确的成本模型、滑点模拟、订单撮合、以及多策略并行回测,确保回测结果的准确性和可复现性。


一、回测引擎架构

1)回测流程

mermaid-drawing.png

2)回测引擎核心组件

// backtest/BacktestEngine.java

@Service
public class BacktestEngine {
    
    private final StrategySignalEngine strategyEngine;
    private final RiskService riskService;
    private final OrderSimulator orderSimulator;
    private final CostModel costModel;
    private final BacktestReporter reporter;
    
    /**
     * 执行回测
     */
    public BacktestResult runBacktest(BacktestRequest request) {
        long startTime = System.currentTimeMillis();
        
        // 1. 加载历史数据
        List<Bar> bars = loadHistoricalData(request);
        
        // 2. 初始化策略
        Strategy strategy = initializeStrategy(request);
        
        // 3. 初始化模拟账户
        SimulatedAccount account = initializeAccount(request);
        
        // 4. 执行回测
        BacktestContext context = executeBacktest(
            strategy, 
            account, 
            bars, 
            request
        );
        
        // 5. 生成回测报告
        BacktestResult result = reporter.generateReport(context);
        
        long duration = System.currentTimeMillis() - startTime;
        result.setDuration(duration);
        
        return result;
    }
    
    /**
     * 执行回测核心逻辑
     */
    private BacktestContext executeBacktest(
        Strategy strategy,
        SimulatedAccount account,
        List<Bar> bars,
        BacktestRequest request
    ) {
        BacktestContext context = new BacktestContext();
        context.setStrategy(strategy);
        context.setAccount(account);
        context.setBars(bars);
        
        // 创建 BarSeries
        BarSeries series = createBarSeries(bars);
        
        // 编译策略
        Strategy compiledStrategy = strategyEngine.compile(
            strategy.getDsl(),
            series
        );
        
        // 时间循环
        for (int i = 0; i < bars.size(); i++) {
            Bar bar = bars.get(i);
            
            // 更新市场数据
            context.setCurrentBar(bar);
            context.setCurrentIndex(i);
            
            // 评估策略信号
            List<Signal> signals = evaluateSignals(
                compiledStrategy,
                series,
                i
            );
            
            // 处理信号
            for (Signal signal : signals) {
                processSignal(signal, context, request);
            }
            
            // 更新持仓价值
            updatePositions(account, bar);
            
            // 记录权益曲线
            recordEquity(account, context);
        }
        
        // 计算最终指标
        calculateFinalMetrics(context);
        
        return context;
    }
    
    /**
     * 评估策略信号
     */
    private List<Signal> evaluateSignals(
        Strategy strategy,
        BarSeries series,
        int index
    ) {
        List<Signal> signals = new ArrayList<>();
        
        // 检查入场信号
        if (strategy.getEntryRule().isSatisfied(index)) {
            signals.add(Signal.builder()
                .type(SignalType.BUY)
                .price(series.getBar(index).getClosePrice().doubleValue())
                .volume(100) // 默认交易量
                .timestamp(series.getBar(index).getEndTime().toInstant())
                .build());
        }
        
        // 检查出场信号
        if (strategy.getExitRule().isSatisfied(index)) {
            signals.add(Signal.builder()
                .type(SignalType.SELL)
                .price(series.getBar(index).getClosePrice().doubleValue())
                .volume(100) // 默认交易量
                .timestamp(series.getBar(index).getEndTime().toInstant())
                .build());
        }
        
        return signals;
    }
    
    /**
     * 处理信号
     */
    private void processSignal(
        Signal signal,
        BacktestContext context,
        BacktestRequest request
    ) {
        // 构建风控检查上下文
        RiskCheckContext riskContext = buildRiskCheckContext(
            signal,
            context.getAccount()
        );
        
        // 执行风控检查
        RiskCheckResult riskResult = riskService.preCheckAll(riskContext);
        
        if (!riskResult.isPassed()) {
            // 记录拒绝原因
            context.addRejectedSignal(signal, riskResult);
            return;
        }
        
        // 模拟订单撮合
        OrderResult orderResult = orderSimulator.simulateOrder(
            signal,
            context.getCurrentBar(),
            request.getOrderType()
        );
        
        if (!orderResult.isFilled()) {
            // 订单未成交
            context.addUnfilledOrder(signal, orderResult);
            return;
        }
        
        // 计算交易成本
        CostDetail costDetail = costModel.calculateCost(
            orderResult,
            request.getCostModel()
        );
        
        // 更新持仓
        updatePosition(
            context.getAccount(),
            signal,
            orderResult,
            costDetail
        );
        
        // 记录交易
        context.addTrade(Trade.builder()
            .signal(signal)
            .orderResult(orderResult)
            .costDetail(costDetail)
            .timestamp(context.getCurrentBar().getEndTime().toInstant())
            .build());
    }
    
    /**
     * 更新持仓价值
     */
    private void updatePositions(SimulatedAccount account, Bar bar) {
        for (Position position : account.getPositions()) {
            if (position.getSymbol().equals(bar.getSymbol())) {
                position.setCurrentPrice(bar.getClosePrice().doubleValue());
                position.updateValue();
            }
        }
    }
    
    /**
     * 记录权益曲线
     */
    private void recordEquity(SimulatedAccount account, BacktestContext context) {
        context.getEquityCurve().add(EquityPoint.builder()
            .timestamp(context.getCurrentBar().getEndTime().toInstant())
            .equity(account.getTotalEquity())
            .cash(account.getCash())
            .positionValue(account.getPositionValue())
            .build());
    }
    
    /**
     * 计算最终指标
     */
    private void calculateFinalMetrics(BacktestContext context) {
        List<EquityPoint> equityCurve = context.getEquityCurve();
        
        // 总收益率
        double totalReturn = calculateTotalReturn(equityCurve);
        context.setTotalReturn(totalReturn);
        
        // 年化收益率
        double annualizedReturn = calculateAnnualizedReturn(
            equityCurve,
            context.getBars()
        );
        context.setAnnualizedReturn(annualizedReturn);
        
        // 最大回撤
        double maxDrawdown = calculateMaxDrawdown(equityCurve);
        context.setMaxDrawdown(maxDrawdown);
        
        // 夏普比率
        double sharpeRatio = calculateSharpeRatio(equityCurve);
        context.setSharpeRatio(sharpeRatio);
        
        // 波动率
        double volatility = calculateVolatility(equityCurve);
        context.setVolatility(volatility);
        
        // 胜率
        double winRate = calculateWinRate(context.getTrades());
        context.setWinRate(winRate);
        
        // 盈亏比
        double profitLossRatio = calculateProfitLossRatio(context.getTrades());
        context.setProfitLossRatio(profitLossRatio);
    }
    
    private double calculateTotalReturn(List<EquityPoint> equityCurve) {
        if (equityCurve.isEmpty()) {
            return 0.0;
        }
        
        double initialEquity = equityCurve.get(0).getEquity();
        double finalEquity = equityCurve.get(equityCurve.size() - 1).getEquity();
        
        return (finalEquity - initialEquity) / initialEquity;
    }
    
    private double calculateAnnualizedReturn(
        List<EquityPoint> equityCurve,
        List<Bar> bars
    ) {
        if (equityCurve.isEmpty() || bars.isEmpty()) {
            return 0.0;
        }
        
        double totalReturn = calculateTotalReturn(equityCurve);
        
        // 计算回测天数
        long days = ChronoUnit.DAYS.between(
            bars.get(0).getEndTime(),
            bars.get(bars.size() - 1).getEndTime()
        );
        
        if (days == 0) {
            return 0.0;
        }
        
        // 年化收益率
        return Math.pow(1 + totalReturn, 365.0 / days) - 1;
    }
    
    private double calculateMaxDrawdown(List<EquityPoint> equityCurve) {
        if (equityCurve.isEmpty()) {
            return 0.0;
        }
        
        double maxEquity = equityCurve.get(0).getEquity();
        double maxDrawdown = 0.0;
        
        for (EquityPoint point : equityCurve) {
            if (point.getEquity() > maxEquity) {
                maxEquity = point.getEquity();
            }
            
            double drawdown = (maxEquity - point.getEquity()) / maxEquity;
            if (drawdown > maxDrawdown) {
                maxDrawdown = drawdown;
            }
        }
        
        return maxDrawdown;
    }
    
    private double calculateSharpeRatio(List<EquityPoint> equityCurve) {
        if (equityCurve.size() < 2) {
            return 0.0;
        }
        
        // 计算日收益率
        List<Double> dailyReturns = new ArrayList<>();
        for (int i = 1; i < equityCurve.size(); i++) {
            double prevEquity = equityCurve.get(i - 1).getEquity();
            double currEquity = equityCurve.get(i).getEquity();
            dailyReturns.add((currEquity - prevEquity) / prevEquity);
        }
        
        // 计算平均收益率
        double meanReturn = dailyReturns.stream()
            .mapToDouble(Double::doubleValue)
            .average()
            .orElse(0.0);
        
        // 计算标准差
        double stdDev = Math.sqrt(dailyReturns.stream()
            .mapToDouble(r -> Math.pow(r - meanReturn, 2))
            .average()
            .orElse(0.0));
        
        if (stdDev == 0) {
            return 0.0;
        }
        
        // 年化夏普比率(假设无风险利率为 3%)
        double riskFreeRate = 0.03 / 365;
        return (meanReturn - riskFreeRate) / stdDev * Math.sqrt(365);
    }
    
    private double calculateVolatility(List<EquityPoint> equityCurve) {
        if (equityCurve.size() < 2) {
            return 0.0;
        }
        
        // 计算日收益率
        List<Double> dailyReturns = new ArrayList<>();
        for (int i = 1; i < equityCurve.size(); i++) {
            double prevEquity = equityCurve.get(i - 1).getEquity();
            double currEquity = equityCurve.get(i).getEquity();
            dailyReturns.add((currEquity - prevEquity) / prevEquity);
        }
        
        // 计算标准差
        double stdDev = Math.sqrt(dailyReturns.stream()
            .mapToDouble(r -> Math.pow(r, 2))
            .average()
            .orElse(0.0));
        
        // 年化波动率
        return stdDev * Math.sqrt(365);
    }
    
    private double calculateWinRate(List<Trade> trades) {
        if (trades.isEmpty()) {
            return 0.0;
        }
        
        long winCount = trades.stream()
            .filter(t -> t.getProfit() > 0)
            .count();
        
        return (double) winCount / trades.size();
    }
    
    private double calculateProfitLossRatio(List<Trade> trades) {
        List<Trade> profitableTrades = trades.stream()
            .filter(t -> t.getProfit() > 0)
            .toList();
        
        List<Trade> lossTrades = trades.stream()
            .filter(t -> t.getProfit() < 0)
            .toList();
        
        if (lossTrades.isEmpty()) {
            return Double.MAX_VALUE;
        }
        
        double avgProfit = profitableTrades.stream()
            .mapToDouble(Trade::getProfit)
            .average()
            .orElse(0.0);
        
        double avgLoss = Math.abs(lossTrades.stream()
            .mapToDouble(Trade::getProfit)
            .average()
            .orElse(0.0));
        
        if (avgLoss == 0) {
            return Double.MAX_VALUE;
        }
        
        return avgProfit / avgLoss;
    }
}

二、订单模拟器

1)订单模拟器接口

// backtest/OrderSimulator.java

public interface OrderSimulator {
    
    /**
     * 模拟订单撮合
     */
    OrderResult simulateOrder(
        Signal signal,
        Bar bar,
        OrderType orderType
    );
}

// backtest/OrderType.java

public enum OrderType {
    MARKET,      // 市价单
    LIMIT,       // 限价单
    STOP,        // 止损单
    STOP_LIMIT   // 止损限价单
}

// backtest/OrderResult.java

@Data
@Builder
public class OrderResult {
    
    private boolean filled;           // 是否成交
    private double fillPrice;          // 成交价格
    private long fillVolume;           // 成交数量
    private double slippage;           // 滑点
    private Instant fillTime;          // 成交时间
    private String rejectReason;      // 拒绝原因
}

2)市价单模拟器

// backtest/MarketOrderSimulator.java

@Component
public class MarketOrderSimulator implements OrderSimulator {
    
    @Value("${backtest.slippage.rate:0.0001}")
    private double slippageRate;
    
    @Value("${backtest.slippage.enabled:true}")
    private boolean slippageEnabled;
    
    @Override
    public OrderResult simulateOrder(
        Signal signal,
        Bar bar,
        OrderType orderType
    ) {
        if (orderType != OrderType.MARKET) {
            return OrderResult.builder()
                .filled(false)
                .rejectReason("不支持的订单类型")
                .build();
        }
        
        // 计算成交价格(考虑滑点)
        double fillPrice = calculateFillPrice(signal, bar);
        
        // 计算滑点
        double slippage = slippageEnabled ? 
            calculateSlippage(signal, bar, fillPrice) : 0.0;
        
        // 更新成交价格
        fillPrice += slippage;
        
        return OrderResult.builder()
            .filled(true)
            .fillPrice(fillPrice)
            .fillVolume(signal.getVolume())
            .slippage(slippage)
            .fillTime(bar.getEndTime().toInstant())
            .build();
    }
    
    private double calculateFillPrice(Signal signal, Bar bar) {
        // 市价单使用收盘价
        return bar.getClosePrice().doubleValue();
    }
    
    private double calculateSlippage(
        Signal signal,
        Bar bar,
        double fillPrice
    ) {
        // 买入滑点为正,卖出滑点为负
        double direction = signal.getType() == SignalType.BUY ? 1.0 : -1.0;
        
        // 滑点 = 价格 * 滑点率 * 方向
        return fillPrice * slippageRate * direction;
    }
}

3)限价单模拟器

// backtest/LimitOrderSimulator.java

@Component
public class LimitOrderSimulator implements OrderSimulator {
    
    @Value("${backtest.slippage.rate:0.0001}")
    private double slippageRate;
    
    @Value("${backtest.slippage.enabled:true}")
    private boolean slippageEnabled;
    
    @Override
    public OrderResult simulateOrder(
        Signal signal,
        Bar bar,
        OrderType orderType
    ) {
        if (orderType != OrderType.LIMIT) {
            return OrderResult.builder()
                .filled(false)
                .rejectReason("不支持的订单类型")
                .build();
        }
        
        // 获取限价
        double limitPrice = signal.getLimitPrice();
        if (limitPrice <= 0) {
            return OrderResult.builder()
                .filled(false)
                .rejectReason("限价无效")
                .build();
        }
        
        // 检查是否可以成交
        boolean canFill = canFill(signal, bar, limitPrice);
        
        if (!canFill) {
            return OrderResult.builder()
                .filled(false)
                .rejectReason("限价未触发")
                .build();
        }
        
        // 计算成交价格
        double fillPrice = calculateFillPrice(signal, bar, limitPrice);
        
        // 计算滑点
        double slippage = slippageEnabled ? 
            calculateSlippage(signal, bar, fillPrice) : 0.0;
        
        // 更新成交价格
        fillPrice += slippage;
        
        return OrderResult.builder()
            .filled(true)
            .fillPrice(fillPrice)
            .fillVolume(signal.getVolume())
            .slippage(slippage)
            .fillTime(bar.getEndTime().toInstant())
            .build();
    }
    
    private boolean canFill(
        Signal signal,
        Bar bar,
        double limitPrice
    ) {
        if (signal.getType() == SignalType.BUY) {
            // 买入:最低价 <= 限价
            return bar.getLowPrice().doubleValue() <= limitPrice;
        } else {
            // 卖出:最高价 >= 限价
            return bar.getHighPrice().doubleValue() >= limitPrice;
        }
    }
    
    private double calculateFillPrice(
        Signal signal,
        Bar bar,
        double limitPrice
    ) {
        if (signal.getType() == SignalType.BUY) {
            // 买入:取 min(限价, 收盘价)
            return Math.min(limitPrice, bar.getClosePrice().doubleValue());
        } else {
            // 卖出:取 max(限价, 收盘价)
            return Math.max(limitPrice, bar.getClosePrice().doubleValue());
        }
    }
    
    private double calculateSlippage(
        Signal signal,
        Bar bar,
        double fillPrice
    ) {
        // 限价单滑点较小
        double direction = signal.getType() == SignalType.BUY ? 1.0 : -1.0;
        
        return fillPrice * slippageRate * 0.5 * direction;
    }
}

三、成本模型

1)成本模型接口

// backtest/CostModel.java

public interface CostModel {
    
    /**
     * 计算交易成本
     */
    CostDetail calculateCost(
        OrderResult orderResult,
        CostModelConfig config
    );
}

// backtest/CostDetail.java

@Data
@Builder
public class CostDetail {
    
    private double commission;      // 手续费
    private double slippage;         // 滑点成本
    private double tax;              // 税费
    private double totalCost;       // 总成本
}

// backtest/CostModelConfig.java

@Data
@Builder
public class CostModelConfig {
    
    private double commissionRate;      // 手续费率
    private double minCommission;       // 最小手续费
    private double taxRate;              // 税率
    private double slippageRate;         // 滑点率
    private boolean slippageEnabled;     // 是否启用滑点
}

2)标准成本模型

// backtest/StandardCostModel.java

@Component
public class StandardCostModel implements CostModel {
    
    @Override
    public CostDetail calculateCost(
        OrderResult orderResult,
        CostModelConfig config
    ) {
        // 计算交易金额
        double tradeAmount = orderResult.getFillPrice() 
            * orderResult.getFillVolume();
        
        // 计算手续费
        double commission = calculateCommission(
            tradeAmount,
            config.getCommissionRate(),
            config.getMinCommission()
        );
        
        // 计算滑点成本
        double slippageCost = config.isSlippageEnabled() ? 
            orderResult.getSlippage() * orderResult.getFillVolume() : 0.0;
        
        // 计算税费
        double tax = calculateTax(
            tradeAmount,
            config.getTaxRate()
        );
        
        // 计算总成本
        double totalCost = commission + slippageCost + tax;
        
        return CostDetail.builder()
            .commission(commission)
            .slippage(slippageCost)
            .tax(tax)
            .totalCost(totalCost)
            .build();
    }
    
    private double calculateCommission(
        double tradeAmount,
        double commissionRate,
        double minCommission
    ) {
        double commission = tradeAmount * commissionRate;
        return Math.max(commission, minCommission);
    }
    
    private double calculateTax(double tradeAmount, double taxRate) {
        return tradeAmount * taxRate;
    }
}

四、回测报告生成

1)回测报告

// backtest/BacktestReporter.java

@Component
public class BacktestReporter {
    
    /**
     * 生成回测报告
     */
    public BacktestResult generateReport(BacktestContext context) {
        BacktestResult result = new BacktestResult();
        
        // 基本信息
        result.setStrategyName(context.getStrategy().getName());
        result.setStartTime(context.getBars().get(0).getEndTime().toInstant());
        result.setEndTime(context.getBars()
            .get(context.getBars().size() - 1)
            .getEndTime().toInstant());
        
        // 性能指标
        result.setTotalReturn(context.getTotalReturn());
        result.setAnnualizedReturn(context.getAnnualizedReturn());
        result.setMaxDrawdown(context.getMaxDrawdown());
        result.setSharpeRatio(context.getSharpeRatio());
        result.setVolatility(context.getVolatility());
        result.setWinRate(context.getWinRate());
        result.setProfitLossRatio(context.getProfitLossRatio());
        
        // 交易统计
        result.setTotalTrades(context.getTrades().size());
        result.setWinningTrades((int) context.getTrades().stream()
            .filter(t -> t.getProfit() > 0)
            .count());
        result.setLosingTrades((int) context.getTrades().stream()
            .filter(t -> t.getProfit() < 0)
            .count());
        
        // 权益曲线
        result.setEquityCurve(context.getEquityCurve());
        
        // 交易明细
        result.setTrades(context.getTrades());
        
        // 拒绝信号
        result.setRejectedSignals(context.getRejectedSignals());
        
        return result;
    }
}

2)回测结果

// backtest/BacktestResult.java

@Data
public class BacktestResult {
    
    // 基本信息
    private String strategyName;
    private Instant startTime;
    private Instant endTime;
    private long duration;
    
    // 性能指标
    private double totalReturn;
    private double annualizedReturn;
    private double maxDrawdown;
    private double sharpeRatio;
    private double volatility;
    private double winRate;
    private double profitLossRatio;
    
    // 交易统计
    private int totalTrades;
    private int winningTrades;
    private int losingTrades;
    
    // 权益曲线
    private List<EquityPoint> equityCurve;
    
    // 交易明细
    private List<Trade> trades;
    
    // 拒绝信号
    private List<RejectedSignal> rejectedSignals;
}

五、最佳实践总结

1)回测引擎设计原则

  • 真实模拟:尽可能还原真实交易环境
  • 事件驱动:基于事件的回测架构
  • 可配置性:成本模型、滑点等可配置
  • 可复现性:回测结果可复现

2)订单模拟原则

  • 准确性:订单撮合必须准确
  • 灵活性:支持多种订单类型
  • 性能:订单模拟必须高效

3)成本模型原则

  • 精确性:成本计算必须精确
  • 可配置性:成本参数可配置
  • 可扩展性:支持自定义成本模型

结语:回测引擎是策略验证的核心

回测引擎是策略验证的核心,它帮助量化分析师评估策略的历史表现,优化策略参数,提高策略的实盘表现。

关键优势总结:

  1. 真实模拟:尽可能还原真实交易环境
  2. 事件驱动:基于事件的回测架构
  3. 可配置性:成本模型、滑点等可配置
  4. 可复现性:回测结果可复现
  5. 多策略并行:支持多策略并行回测

对于正在构建量化平台的团队,完善的回测引擎是必不可少的。