diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/strategy/TradeEngine.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/strategy/TradeEngine.java index b3531af..81b74ed 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/strategy/TradeEngine.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/strategy/TradeEngine.java @@ -1,11 +1,16 @@ 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.QDaily; import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import java.time.LocalDate; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,23 +29,80 @@ public class TradeEngine { this.dailyRepository = dailyRepository; } - public void backtest(List stocks, TradeStrategy strategy) { + public Asset backtest(List 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 { List trade(LocalDate now, Asset asset, Map> dailies); } - public record Asset( - Double cash, - Map stocks - ) { - public Asset() { - this(0.0, new HashMap<>()); + @Data + public static final class Asset { + private double cash = 0; + private double profit = 0.0; + private Map stocks = new HashMap<>(); + private List histories = new ArrayList<>(); + + public record History( + LocalDate date, + double cash, + Map stocks, + Map> trades + ) { } } public record Trade( + LocalDate date, Long stockId, // 用正负数表达买卖 Integer volume diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java index b362679..e88df2a 100644 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java +++ b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java @@ -1,7 +1,9 @@ package com.lanyuanxiaoyao.leopard.strategy; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; 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.repository.DailyRepository; 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.TuShareService; import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector; +import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine; import jakarta.annotation.Resource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; @@ -39,6 +45,8 @@ public class StrategyApplication { private StockRepository stockRepository; @Resource private StockService stockService; + @Resource + private TradeEngine tradeEngine; public static void main(String[] args) { SpringApplication.run(StrategyApplication.class, args); @@ -279,9 +287,41 @@ public class StrategyApplication { @EventListener(ApplicationReadyEvent.class) public void test() { var stock = stockRepository.findOne(QStock.stock.code.eq("000001.SZ")).orElseThrow(); - var weeklies = stockService.findWeeklyRecent(stock.getId(), 2); - for (var weekly : weeklies) { - log.info("{}", weekly); + var asset = tradeEngine.backtest( + List.of(stock.getId()), + (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()); } } }