回测是量化策略开发的核心环节。一个好的回测引擎不仅能够准确评估策略的历史表现,还能为策略优化提供可靠的数据支持。EasyQuant 的回测引擎基于"真实模拟"(Realistic Simulation)原则,尽可能还原真实交易环境。
结论先行:EasyQuant 的回测引擎采用事件驱动架构,支持精确的成本模型、滑点模拟、订单撮合、以及多策略并行回测,确保回测结果的准确性和可复现性。
一、回测引擎架构
1)回测流程
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)成本模型原则
- 精确性:成本计算必须精确
- 可配置性:成本参数可配置
- 可扩展性:支持自定义成本模型
结语:回测引擎是策略验证的核心
回测引擎是策略验证的核心,它帮助量化分析师评估策略的历史表现,优化策略参数,提高策略的实盘表现。
关键优势总结:
- 真实模拟:尽可能还原真实交易环境
- 事件驱动:基于事件的回测架构
- 可配置性:成本模型、滑点等可配置
- 可复现性:回测结果可复现
- 多策略并行:支持多策略并行回测
对于正在构建量化平台的团队,完善的回测引擎是必不可少的。