backtestify/src/main/java/com/yangwale/backtestify/service/impl/BacktestEngineImpl.java
2026-08-03 11:45:30 +08:00

617 lines
27 KiB
Java

package com.yangwale.backtestify.service.impl;
import com.alibaba.fastjson2.JSON;
import com.yangwale.backtestify.common.BusinessException;
import com.yangwale.backtestify.common.ErrorCode;
import com.yangwale.backtestify.config.WebMvcConfig;
import com.yangwale.backtestify.entity.StrategyConfig;
import com.yangwale.backtestify.entity.StrategyResult;
import com.yangwale.backtestify.entity.TradeDetail;
import com.yangwale.backtestify.enums.Direction;
import com.yangwale.backtestify.enums.KLinePeriod;
import com.yangwale.backtestify.enums.StopUnit;
import com.yangwale.backtestify.enums.TradeAction;
import com.yangwale.backtestify.mapper.StrategyConfigMapper;
import com.yangwale.backtestify.mapper.StrategyResultMapper;
import com.yangwale.backtestify.mapper.TradeDetailMapper;
import com.yangwale.backtestify.model.dto.KLineData;
import com.yangwale.backtestify.model.request.BacktestRequest;
import com.yangwale.backtestify.model.response.BacktestResponse;
import com.yangwale.backtestify.service.BacktestEngine;
import com.yangwale.backtestify.service.MarketDataService;
import com.yangwale.backtestify.service.signal.SignalStrategy;
import com.yangwale.backtestify.service.signal.SignalStrategyFactory;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
/**
* 回测引擎核心实现
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BacktestEngineImpl implements BacktestEngine {
private final MarketDataService marketDataService;
private final SignalStrategyFactory signalStrategyFactory;
private final StrategyConfigMapper strategyConfigMapper;
private final StrategyResultMapper strategyResultMapper;
private final TradeDetailMapper tradeDetailMapper;
@Value("${backtest.initial-capital:1000000}")
private BigDecimal initialCapital;
@Value("${backtest.margin-ratio:0.10}")
private BigDecimal marginRatio;
@Value("${backtest.fee-rate:0.00005}")
private BigDecimal feeRate;
@Value("${backtest.risk-free-rate:0.025}")
private BigDecimal riskFreeRate;
@Override
@Transactional(rollbackFor = Exception.class)
public BacktestResponse execute(BacktestRequest request) {
// 1. 参数解析
Direction direction = Direction.of(request.getDirection());
KLinePeriod period = KLinePeriod.of(request.getKlinePeriod());
List<SignalStrategy> strategies = signalStrategyFactory.get(request.getIndicators());
BigDecimal priceTick = resolvePriceTick(request);
// 2. 计算回测时间范围
LocalDateTime now = LocalDateTime.now();
LocalDateTime startTime = calcStartTime(now, request.getBacktestPeriod());
// 3. 获取历史K线数据
List<KLineData> kLines = marketDataService.getKLineData(
request.getContractCode(), period.getCode(), startTime, now);
if (kLines.isEmpty()) {
throw new BusinessException(ErrorCode.MARKET_DATA_UNAVAILABLE, "回测区间内无可用K线数据");
}
log.info("回测区间: {} ~ {}, K线数量: {}", kLines.getFirst().getTime(), kLines.getLast().getTime(), kLines.size());
// 4. 创建回测上下文
BigDecimal initialCapitalAmount = money(initialCapital);
BacktestContext ctx = new BacktestContext(initialCapitalAmount, marginRatio, feeRate);
// 5. 逐根K线遍历
for (int i = 0; i < kLines.size(); i++) {
KLineData kline = kLines.get(i);
// 5a. 检查止盈/止损
if (ctx.hasPosition() && checkStopCondition(ctx, kline, request, direction, priceTick)) {
closePosition(ctx, kline, direction);
ctx.recordEquity(kline, direction);
continue;
}
// 5b. 检查信号策略
if (!ctx.hasPosition()) {
boolean allBuy = strategies.stream().allMatch(s -> s.isBuySignal(kline, kLines));
boolean allSell = strategies.stream().allMatch(s -> s.isSellSignal(kline, kLines));
if (direction == Direction.LONG && allBuy) {
openPosition(ctx, kline, direction, request.getOpenVolume());
} else if (direction == Direction.SHORT && allSell) {
openPosition(ctx, kline, direction, request.getOpenVolume());
}
}
ctx.recordEquity(kline, direction);
}
// 6. 遍历结束,强制平仓
if (ctx.hasPosition()) {
KLineData lastKline = kLines.getLast();
closePosition(ctx, lastKline, direction);
ctx.recordEquity(lastKline, direction);
}
ctx.calculateDailyYields();
// 7. 计算指标
LocalDate startDate = kLines.getFirst().getTime().toLocalDate();
LocalDate endDate = kLines.getLast().getTime().toLocalDate();
long totalDays = ChronoUnit.DAYS.between(startDate, endDate) + 1;
BigDecimal finalCapital = money(ctx.getTotalEquity());
BigDecimal maxEquity = money(ctx.maxEquity);
BigDecimal minEquity = money(ctx.minEquity);
BigDecimal totalYield = calcTotalYield(finalCapital, initialCapitalAmount);
BigDecimal profitAmount = money(finalCapital.subtract(initialCapitalAmount));
BigDecimal annualizedYield = calcAnnualizedYield(totalYield, totalDays);
BigDecimal maxDrawdown = calcMaxDrawdown(ctx);
BigDecimal sharpeRatio = calcSharpeRatio(ctx.dailyYields);
BigDecimal winRate = calcWinRate(ctx.tradeRecords);
// 8. 持久化
StrategyConfig config = saveStrategyConfig(request);
saveStrategyResult(config.getId(), initialCapitalAmount, finalCapital,
maxEquity, minEquity, totalYield, profitAmount, annualizedYield,
ctx.tradeRecords.size(), maxDrawdown, sharpeRatio, winRate,
startDate, endDate, ctx.dailyEquityCurve);
List<TradeDetail> tradeDetails = saveTradeDetails(config.getId(), ctx.tradeRecords);
// 9. 构建响应
return buildResponse(request, config.getId(), initialCapitalAmount, finalCapital,
maxEquity, minEquity, totalYield, profitAmount, annualizedYield,
ctx.tradeRecords.size(), maxDrawdown, sharpeRatio, winRate,
startDate, endDate, ctx, tradeDetails);
}
// ==================== 交易操作 ====================
private void openPosition(BacktestContext ctx, KLineData kline, Direction direction, int volume) {
BigDecimal price = kline.getClose();
BigDecimal turnover = price.multiply(BigDecimal.valueOf(volume));
BigDecimal margin = turnover.multiply(marginRatio);
BigDecimal fee = turnover.multiply(feeRate);
if (ctx.availableCapital.compareTo(margin.add(fee)) < 0) {
log.debug("保证金不足,跳过开仓。可用: {}, 需要: {}", ctx.availableCapital, margin.add(fee));
return;
}
ctx.availableCapital = ctx.availableCapital.subtract(margin).subtract(fee);
ctx.position = volume;
ctx.avgCostPrice = price;
ctx.marginLocked = margin;
ctx.openFee = fee;
TradeAction action = direction == Direction.LONG ? TradeAction.BUY_OPEN : TradeAction.SELL_OPEN;
String signal = direction == Direction.LONG ? "B" : "S";
ctx.addRecord(action, price, volume, turnover, kline.getTime(), signal, null);
log.debug("开仓: {} {}手 @ {}, 保证金={}, 手续费={}", action.getLabel(), volume, price, margin, fee);
}
private void closePosition(BacktestContext ctx, KLineData kline, Direction direction) {
BigDecimal price = kline.getClose();
int volume = ctx.position;
BigDecimal turnover = price.multiply(BigDecimal.valueOf(volume));
BigDecimal fee = turnover.multiply(feeRate);
// 释放保证金,计算盈亏,更新可用资金
BigDecimal profit = turnover.subtract(ctx.avgCostPrice.multiply(BigDecimal.valueOf(volume)));
if (direction == Direction.SHORT) {
profit = BigDecimal.ZERO.subtract(profit); // 做空盈亏反向
}
BigDecimal netProfit = profit.subtract(ctx.openFee).subtract(fee);
ctx.availableCapital = ctx.availableCapital.add(ctx.marginLocked).add(profit).subtract(fee);
ctx.position = 0;
ctx.marginLocked = BigDecimal.ZERO;
ctx.avgCostPrice = BigDecimal.ZERO;
ctx.openFee = BigDecimal.ZERO;
TradeAction action = direction == Direction.LONG ? TradeAction.SELL_CLOSE : TradeAction.BUY_CLOSE;
String signal = direction == Direction.LONG ? "S" : "B";
ctx.addRecord(action, price, volume, turnover, kline.getTime(), signal, netProfit);
log.debug("平仓: {} {}手 @ {}, 盈亏={}, 手续费={}", action.getLabel(), volume, price, profit, fee);
}
// ==================== 止盈止损检查 ====================
private boolean checkStopCondition(BacktestContext ctx, KLineData kline, BacktestRequest request,
Direction direction, BigDecimal priceTick) {
BigDecimal price = kline.getClose();
BigDecimal cost = ctx.avgCostPrice;
if (request.getStopLossValue() != null) {
BigDecimal stopDistance = calculateStopDistance(
cost, request.getStopLossValue(), StopUnit.of(request.getStopLossUnit()), priceTick);
BigDecimal stopPrice = direction == Direction.LONG
? cost.subtract(stopDistance)
: cost.add(stopDistance);
boolean triggered = direction == Direction.LONG
? price.compareTo(stopPrice) <= 0
: price.compareTo(stopPrice) >= 0;
if (triggered) {
log.info("触发止损: 价格={}, 止损价={}", price, stopPrice);
return true;
}
}
if (request.getTakeProfitValue() != null) {
BigDecimal takeProfitDistance = calculateStopDistance(
cost, request.getTakeProfitValue(), StopUnit.of(request.getTakeProfitUnit()), priceTick);
BigDecimal takeProfitPrice = direction == Direction.LONG
? cost.add(takeProfitDistance)
: cost.subtract(takeProfitDistance);
boolean triggered = direction == Direction.LONG
? price.compareTo(takeProfitPrice) >= 0
: price.compareTo(takeProfitPrice) <= 0;
if (triggered) {
log.info("触发止盈: 价格={}, 止盈价={}", price, takeProfitPrice);
return true;
}
}
return false;
}
private BigDecimal resolvePriceTick(BacktestRequest request) {
boolean tickStopLoss = request.getStopLossValue() != null
&& StopUnit.TICK == StopUnit.of(request.getStopLossUnit());
boolean tickTakeProfit = request.getTakeProfitValue() != null
&& StopUnit.TICK == StopUnit.of(request.getTakeProfitUnit());
return tickStopLoss || tickTakeProfit
? marketDataService.getPriceTick(request.getContractCode())
: null;
}
private BigDecimal calculateStopDistance(BigDecimal cost, BigDecimal value,
StopUnit unit, BigDecimal priceTick) {
if (unit == StopUnit.TICK) {
if (priceTick == null || priceTick.signum() <= 0) {
throw new BusinessException(ErrorCode.MARKET_DATA_UNAVAILABLE,
"TICK止盈止损缺少有效的最小变动价位");
}
return value.multiply(priceTick);
}
BigDecimal percentage = value.divide(BigDecimal.valueOf(100), 8, RoundingMode.HALF_UP);
return cost.multiply(percentage);
}
// ==================== 指标计算 ====================
private BigDecimal calcTotalYield(BigDecimal finalCapital, BigDecimal initialCapitalAmount) {
return finalCapital.subtract(initialCapitalAmount)
.divide(initialCapitalAmount, 8, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(4, RoundingMode.HALF_UP);
}
private BigDecimal money(BigDecimal value) {
return value.setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal calcAnnualizedYield(BigDecimal totalYield, long totalDays) {
if (totalDays <= 0) return BigDecimal.ZERO;
return totalYield.divide(BigDecimal.valueOf(totalDays), 8, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(365))
.setScale(4, RoundingMode.HALF_UP);
}
private BigDecimal calcMaxDrawdown(BacktestContext ctx) {
BigDecimal peak = BigDecimal.ZERO;
BigDecimal maximum = BigDecimal.ZERO;
for (BigDecimal equity : ctx.equityHistory) {
if (equity.compareTo(peak) > 0) {
peak = equity;
}
if (peak.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal drawdown = peak.subtract(equity)
.divide(peak, 8, RoundingMode.HALF_UP);
if (drawdown.compareTo(maximum) > 0) {
maximum = drawdown;
}
}
}
return maximum.multiply(BigDecimal.valueOf(100))
.setScale(4, RoundingMode.HALF_UP);
}
private BigDecimal calcSharpeRatio(List<BigDecimal> dailyYields) {
if (dailyYields == null || dailyYields.size() < 5) return BigDecimal.ZERO;
// 日收益率平均值
BigDecimal sum = dailyYields.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal avgDaily = sum.divide(BigDecimal.valueOf(dailyYields.size()), 8, RoundingMode.HALF_UP);
// 日收益率标准差
BigDecimal variance = dailyYields.stream()
.map(y -> y.subtract(avgDaily).pow(2))
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(dailyYields.size()), 8, RoundingMode.HALF_UP);
BigDecimal stdDaily = BigDecimal.valueOf(Math.sqrt(variance.doubleValue()));
if (stdDaily.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
// 年化夏普
BigDecimal annualizedReturn = avgDaily.multiply(BigDecimal.valueOf(252)); // 252交易日
BigDecimal annualizedStd = stdDaily.multiply(BigDecimal.valueOf(Math.sqrt(252)));
return annualizedReturn.subtract(riskFreeRate)
.divide(annualizedStd, 8, RoundingMode.HALF_UP)
.setScale(4, RoundingMode.HALF_UP);
}
private BigDecimal calcWinRate(List<BacktestContext.TradeRecord> records) {
if (records.isEmpty()) return BigDecimal.ZERO;
long winCount = 0;
long totalClose = 0;
for (BacktestContext.TradeRecord r : records) {
if (r.action == TradeAction.SELL_CLOSE || r.action == TradeAction.BUY_CLOSE) {
totalClose++;
if (r.netProfit != null && r.netProfit.compareTo(BigDecimal.ZERO) > 0) {
winCount++;
}
}
}
return totalClose == 0 ? BigDecimal.ZERO :
BigDecimal.valueOf(winCount).divide(BigDecimal.valueOf(totalClose), 8, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP);
}
// ==================== 时间范围计算 ====================
private LocalDateTime calcStartTime(LocalDateTime now, String backtestPeriod) {
return switch (backtestPeriod) {
case "1m" -> now.minusMonths(1);
case "6m" -> now.minusMonths(6);
case "1y" -> now.minusYears(1);
default -> now.minusMonths(3);
};
}
// ==================== 持久化 ====================
private StrategyConfig saveStrategyConfig(BacktestRequest request) {
StrategyConfig config = new StrategyConfig();
config.setUserId(WebMvcConfig.RequestContextHolder.getUserId());
config.setContractCode(request.getContractCode());
config.setContractName(request.getContractName());
config.setDirection(request.getDirection().toUpperCase());
config.setKlinePeriod(request.getKlinePeriod());
config.setIndicators(JSON.toJSONString(request.getIndicators()));
config.setOpenVolume(request.getOpenVolume());
config.setVolumeUnit(request.getVolumeUnit());
config.setStopLossValue(request.getStopLossValue());
config.setStopLossUnit(request.getStopLossUnit());
config.setTakeProfitValue(request.getTakeProfitValue());
config.setTakeProfitUnit(request.getTakeProfitUnit());
config.setBacktestPeriod(request.getBacktestPeriod());
config.setStatus(0); // 已保存
strategyConfigMapper.insert(config);
return config;
}
private void saveStrategyResult(Long strategyId, BigDecimal initialCapital,
BigDecimal finalCapital, BigDecimal maxEquity,
BigDecimal minEquity, BigDecimal totalYield,
BigDecimal profitAmount, BigDecimal annualizedYield,
int tradeCount, BigDecimal maxDrawdown,
BigDecimal sharpeRatio, BigDecimal winRate,
LocalDate startDate, LocalDate endDate,
List<BacktestContext.DailyEquity> dailyCurve) {
StrategyResult result = new StrategyResult();
result.setStrategyId(strategyId);
result.setInitialCapital(initialCapital);
result.setFinalCapital(finalCapital);
result.setMaxEquity(maxEquity);
result.setMinEquity(minEquity);
result.setTotalYield(totalYield);
result.setProfitAmount(profitAmount);
result.setAnnualizedYield(annualizedYield);
result.setTradeCount(tradeCount);
result.setMaxDrawdown(maxDrawdown);
result.setSharpeRatio(sharpeRatio);
result.setWinRate(winRate);
result.setStartDate(startDate);
result.setEndDate(endDate);
result.setDailyEquityCurve(JSON.toJSONString(dailyCurve));
strategyResultMapper.insert(result);
}
private List<TradeDetail> saveTradeDetails(Long strategyId,
List<BacktestContext.TradeRecord> records) {
List<TradeDetail> details = new ArrayList<>();
for (BacktestContext.TradeRecord r : records) {
TradeDetail detail = new TradeDetail();
detail.setStrategyId(strategyId);
detail.setAction(r.action.name());
detail.setPrice(r.price);
detail.setVolume(r.volume);
detail.setTurnover(r.turnover);
detail.setTradeTime(r.tradeTime);
detail.setKlineTime(r.klineTime);
detail.setSignalType(r.signal);
details.add(detail);
}
if (!details.isEmpty()) {
tradeDetailMapper.insert(details);
}
return details;
}
// ==================== 构建响应 ====================
private BacktestResponse buildResponse(BacktestRequest request, Long strategyId,
BigDecimal initialCapital, BigDecimal finalCapital,
BigDecimal maxEquity, BigDecimal minEquity,
BigDecimal totalYield, BigDecimal profitAmount,
BigDecimal annualizedYield, int tradeCount,
BigDecimal maxDrawdown, BigDecimal sharpeRatio,
BigDecimal winRate,
LocalDate startDate, LocalDate endDate,
BacktestContext ctx,
List<TradeDetail> tradeDetails) {
List<BacktestResponse.DailyEquityPoint> curve = ctx.dailyEquityCurve.stream()
.map(d -> BacktestResponse.DailyEquityPoint.builder()
.date(d.date)
.equity(d.equity)
.yield(d.yield)
.build())
.toList();
List<BacktestResponse.TradeDetailItem> trades = tradeDetails.stream()
.map(t -> BacktestResponse.TradeDetailItem.builder()
.action(t.getAction())
.price(t.getPrice())
.volume(t.getVolume())
.turnover(t.getTurnover())
.tradeTime(t.getTradeTime() != null ? t.getTradeTime().toString() : null)
.signalType(t.getSignalType())
.build())
.toList();
List<BacktestResponse.SignalPointItem> signals = ctx.tradeRecords.stream()
.map(r -> BacktestResponse.SignalPointItem.builder()
.time(r.klineTime.toString())
.type(r.signal)
.price(r.price)
.build())
.toList();
return BacktestResponse.builder()
.strategyId(strategyId)
.contractCode(request.getContractCode())
.contractName(request.getContractName())
.direction(request.getDirection())
.klinePeriod(request.getKlinePeriod())
.indicators(request.getIndicators())
.openVolume(request.getOpenVolume())
.volumeUnit(request.getVolumeUnit())
.stopLossValue(request.getStopLossValue())
.stopLossUnit(request.getStopLossUnit())
.takeProfitValue(request.getTakeProfitValue())
.takeProfitUnit(request.getTakeProfitUnit())
.backtestPeriod(request.getBacktestPeriod())
.initialCapital(initialCapital)
.finalCapital(finalCapital)
.maxEquity(maxEquity)
.minEquity(minEquity)
.totalYield(totalYield)
.profitAmount(profitAmount)
.annualizedYield(annualizedYield)
.tradeCount(tradeCount)
.maxDrawdown(maxDrawdown)
.sharpeRatio(sharpeRatio)
.winRate(winRate)
.startDate(startDate)
.endDate(endDate)
.dailyEquityCurve(curve)
.tradeDetails(trades)
.signalPoints(signals)
.build();
}
// ==================== 回测上下文(内部类) ====================
static class BacktestContext {
BigDecimal availableCapital;
BigDecimal marginRatio;
BigDecimal feeRate;
int position = 0;
BigDecimal avgCostPrice = BigDecimal.ZERO;
BigDecimal marginLocked = BigDecimal.ZERO;
BigDecimal openFee = BigDecimal.ZERO;
BigDecimal initialCapital;
BigDecimal maxEquity;
BigDecimal minEquity;
List<TradeRecord> tradeRecords = new ArrayList<>();
List<DailyEquity> dailyEquityCurve = new ArrayList<>();
List<BigDecimal> dailyYields = new ArrayList<>();
List<BigDecimal> equityHistory = new ArrayList<>();
BacktestContext(BigDecimal capital, BigDecimal marginRatio, BigDecimal feeRate) {
this.initialCapital = capital;
this.availableCapital = capital;
this.marginRatio = marginRatio;
this.feeRate = feeRate;
this.maxEquity = capital;
this.minEquity = capital;
this.equityHistory.add(capital);
}
boolean hasPosition() {
return position > 0;
}
BigDecimal getTotalEquity() {
return availableCapital.add(marginLocked);
}
void recordEquity(KLineData kline, Direction direction) {
BigDecimal equity = getTotalEquity();
if (hasPosition()) {
BigDecimal unrealized = kline.getClose().subtract(avgCostPrice)
.multiply(BigDecimal.valueOf(position));
if (direction == Direction.SHORT) {
unrealized = unrealized.negate();
}
equity = equity.add(unrealized);
}
equityHistory.add(equity);
if (equity.compareTo(maxEquity) > 0) maxEquity = equity;
if (equity.compareTo(minEquity) < 0) minEquity = equity;
BigDecimal displayEquity = equity.setScale(2, RoundingMode.HALF_UP);
BigDecimal cumulativeYield = displayEquity.subtract(initialCapital)
.divide(initialCapital, 8, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(4, RoundingMode.HALF_UP);
DailyEquity point = new DailyEquity(
kline.getTime().toLocalDate(), displayEquity, cumulativeYield);
if (!dailyEquityCurve.isEmpty()
&& dailyEquityCurve.getLast().date.equals(point.date)) {
dailyEquityCurve.set(dailyEquityCurve.size() - 1, point);
} else {
dailyEquityCurve.add(point);
}
}
void calculateDailyYields() {
dailyYields.clear();
BigDecimal previousEquity = initialCapital;
for (DailyEquity point : dailyEquityCurve) {
BigDecimal dailyYield = previousEquity.compareTo(BigDecimal.ZERO) == 0
? BigDecimal.ZERO
: point.equity.subtract(previousEquity)
.divide(previousEquity, 8, RoundingMode.HALF_UP);
dailyYields.add(dailyYield);
previousEquity = point.equity;
}
}
void addRecord(TradeAction action, BigDecimal price, int volume, BigDecimal turnover,
LocalDateTime klineTime, String signal, BigDecimal netProfit) {
tradeRecords.add(new TradeRecord(
action, price, volume, turnover, klineTime, klineTime, signal, netProfit));
}
static class TradeRecord {
TradeAction action;
BigDecimal price;
int volume;
BigDecimal turnover;
LocalDateTime tradeTime;
LocalDateTime klineTime;
String signal;
BigDecimal netProfit;
TradeRecord(TradeAction action, BigDecimal price, int volume, BigDecimal turnover,
LocalDateTime tradeTime, LocalDateTime klineTime, String signal,
BigDecimal netProfit) {
this.action = action;
this.price = price;
this.volume = volume;
this.turnover = turnover;
this.tradeTime = tradeTime;
this.klineTime = klineTime;
this.signal = signal;
this.netProfit = netProfit;
}
}
@Getter
static class DailyEquity {
LocalDate date;
BigDecimal equity;
BigDecimal yield;
DailyEquity(LocalDate date, BigDecimal equity, BigDecimal yield) {
this.date = date;
this.equity = equity;
this.yield = yield;
}
}
}
}