feat: 增加简单回测
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.strategy;
|
package com.lanyuanxiaoyao.leopard.core.strategy;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -24,23 +29,80 @@ public class TradeEngine {
|
|||||||
this.dailyRepository = dailyRepository;
|
this.dailyRepository = dailyRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backtest(List<Long> stocks, TradeStrategy strategy) {
|
public Asset backtest(List<Long> stocks, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) {
|
||||||
|
var dailies = dailyRepository.findAll(
|
||||||
|
QDaily.daily.stock.id.in(stocks)
|
||||||
|
.and(QDaily.daily.tradeDate.before(endDate)),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
);
|
||||||
|
var validTradeDates = dailies.stream()
|
||||||
|
.map(Daily::getTradeDate)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
var asset = new Asset();
|
||||||
|
for (var now = startDate; now.isBefore(endDate) || now.isEqual(endDate); now = now.plusDays(1)) {
|
||||||
|
if (!validTradeDates.contains(now)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final var currentDate = now;
|
||||||
|
var trades = strategy.trade(
|
||||||
|
now,
|
||||||
|
asset,
|
||||||
|
dailies.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isBefore(currentDate))
|
||||||
|
.collect(Collectors.groupingBy(daily -> daily.getStock().getId()))
|
||||||
|
);
|
||||||
|
for (var trade : trades) {
|
||||||
|
dailies.stream()
|
||||||
|
.filter(daily -> ObjectUtil.equals(daily.getStock().getId(), trade.stockId))
|
||||||
|
.filter(daily -> ObjectUtil.equals(daily.getTradeDate(), currentDate))
|
||||||
|
.findFirst()
|
||||||
|
.map(Daily::getHfqClose)
|
||||||
|
.ifPresent(close -> {
|
||||||
|
if (trade.volume < 0) {
|
||||||
|
asset.setCash(asset.getCash() + Math.abs(trade.volume) * close);
|
||||||
|
} else if (trade.volume > 0) {
|
||||||
|
asset.setCash(asset.getCash() - Math.abs(trade.volume) * close);
|
||||||
|
}
|
||||||
|
asset.getStocks().put(
|
||||||
|
trade.stockId,
|
||||||
|
asset.getStocks().getOrDefault(trade.stockId, 0) + trade.volume
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
asset.getHistories().add(new Asset.History(
|
||||||
|
now,
|
||||||
|
asset.getCash(),
|
||||||
|
asset.getStocks(),
|
||||||
|
trades.stream()
|
||||||
|
.collect(Collectors.groupingBy(trade -> trade.stockId))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface TradeStrategy {
|
public interface TradeStrategy {
|
||||||
List<Trade> trade(LocalDate now, Asset asset, Map<Long, List<Daily>> dailies);
|
List<Trade> trade(LocalDate now, Asset asset, Map<Long, List<Daily>> dailies);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Asset(
|
@Data
|
||||||
Double cash,
|
public static final class Asset {
|
||||||
Map<Long, Double> stocks
|
private double cash = 0;
|
||||||
) {
|
private double profit = 0.0;
|
||||||
public Asset() {
|
private Map<Long, Integer> stocks = new HashMap<>();
|
||||||
this(0.0, new HashMap<>());
|
private List<History> histories = new ArrayList<>();
|
||||||
|
|
||||||
|
public record History(
|
||||||
|
LocalDate date,
|
||||||
|
double cash,
|
||||||
|
Map<Long, Integer> stocks,
|
||||||
|
Map<Long, List<Trade>> trades
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Trade(
|
public record Trade(
|
||||||
|
LocalDate date,
|
||||||
Long stockId,
|
Long stockId,
|
||||||
// 用正负数表达买卖
|
// 用正负数表达买卖
|
||||||
Integer volume
|
Integer volume
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.strategy;
|
package com.lanyuanxiaoyao.leopard.strategy;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
|
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
@@ -9,10 +11,14 @@ import com.lanyuanxiaoyao.leopard.core.service.AssessmentService;
|
|||||||
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
|
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
@@ -39,6 +45,8 @@ public class StrategyApplication {
|
|||||||
private StockRepository stockRepository;
|
private StockRepository stockRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private StockService stockService;
|
private StockService stockService;
|
||||||
|
@Resource
|
||||||
|
private TradeEngine tradeEngine;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(StrategyApplication.class, args);
|
SpringApplication.run(StrategyApplication.class, args);
|
||||||
@@ -279,9 +287,41 @@ public class StrategyApplication {
|
|||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void test() {
|
public void test() {
|
||||||
var stock = stockRepository.findOne(QStock.stock.code.eq("000001.SZ")).orElseThrow();
|
var stock = stockRepository.findOne(QStock.stock.code.eq("000001.SZ")).orElseThrow();
|
||||||
var weeklies = stockService.findWeeklyRecent(stock.getId(), 2);
|
var asset = tradeEngine.backtest(
|
||||||
for (var weekly : weeklies) {
|
List.of(stock.getId()),
|
||||||
log.info("{}", weekly);
|
(now, currentAsset, dailies) -> {
|
||||||
|
return dailies.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var stockId = entry.getKey();
|
||||||
|
var stockDailies = entry.getValue()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var yesterday = stockDailies.getLast();
|
||||||
|
if (yesterday.getHfqClose() > yesterday.getHfqOpen()) {
|
||||||
|
log.info("{} Buy for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose());
|
||||||
|
return new TradeEngine.Trade(now, stockId, 100);
|
||||||
|
} else if (yesterday.getHfqClose() < yesterday.getHfqOpen()) {
|
||||||
|
var hold = currentAsset.getStocks().getOrDefault(stockId, 0);
|
||||||
|
if (hold > 0) {
|
||||||
|
log.info("{} Sell for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose());
|
||||||
|
return new TradeEngine.Trade(now, stockId, -1 * hold);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("{} Hold for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
LocalDate.of(2024, 12, 1),
|
||||||
|
LocalDate.of(2024, 12, 31)
|
||||||
|
);
|
||||||
|
log.info("Final Cash: {}", asset.getCash());
|
||||||
|
for (var history : asset.getHistories()) {
|
||||||
|
log.info("Date: {} Cash: {} Trade: {}", history.date(), history.cash(), history.trades().values());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user