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 81b74ed..78d66a1 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 @@ -3,13 +3,11 @@ 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.entity.Stock; 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; @@ -29,9 +27,9 @@ public class TradeEngine { this.dailyRepository = dailyRepository; } - public Asset backtest(List stocks, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) { + public Asset backtest(Stock stock, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) { var dailies = dailyRepository.findAll( - QDaily.daily.stock.id.in(stocks) + QDaily.daily.stock.eq(stock) .and(QDaily.daily.tradeDate.before(endDate)), QDaily.daily.tradeDate.asc() ); @@ -45,67 +43,59 @@ public class TradeEngine { continue; } final var currentDate = now; - var trades = strategy.trade( + var trade = strategy.trade( now, asset, dailies.stream() .filter(daily -> daily.getTradeDate().isBefore(currentDate)) - .collect(Collectors.groupingBy(daily -> daily.getStock().getId())) + .toList() ); - 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 - ); - }); + if (trade == 0) { + continue; } - asset.getHistories().add(new Asset.History( - now, - asset.getCash(), - asset.getStocks(), - trades.stream() - .collect(Collectors.groupingBy(trade -> trade.stockId)) - )); + var daily = dailies.stream() + .filter(d -> ObjectUtil.equals(d.getTradeDate(), currentDate)) + .findFirst() + .orElseThrow(); + asset.addTrade(now, trade, daily.getHfqClose()); } return asset; } public interface TradeStrategy { - List trade(LocalDate now, Asset asset, Map> dailies); + int trade(LocalDate now, Asset asset, List dailies); } @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<>(); + private List trades = new ArrayList<>(); - public record History( + public void addTrade(LocalDate date, int volume, double price) { + trades.add(new Trade(date, volume, price)); + } + + public int getVolume() { + return trades.stream() + .mapToInt(Trade::volume) + .sum(); + } + + public double getCash() { + return trades.stream() + .mapToDouble(trade -> -1 * trade.volume() * trade.price()) + .sum(); + } + + public double getPrice() { + int volume = getVolume(); + return volume == 0 ? 0 : getCash() / volume; + } + + public record Trade( LocalDate date, - double cash, - Map stocks, - Map> trades + int volume, + double price ) { } } - - 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 11ae7ae..d9a44e1 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,6 @@ package com.lanyuanxiaoyao.leopard.strategy; import cn.hutool.core.lang.Dict; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.template.TemplateConfig; import cn.hutool.extra.template.TemplateEngine; @@ -71,33 +70,26 @@ public class StrategyApplication { .forEach(code -> { var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow(); var asset = tradeEngine.backtest( - List.of(stock.getId()), + stockRepository.findById(stock.getId()).orElseThrow(), (now, currentAsset, dailies) -> { - return dailies.entrySet() + var stockDailies = dailies .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) + .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 100; + } else if (yesterday.getHfqClose() < yesterday.getHfqOpen()) { + var hold = currentAsset.getVolume(); + if (hold > 0) { + log.info("{} Sell for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose()); + return -1 * hold; + } + } else { + log.info("{} Hold for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose()); + } + return 0; }, startDate, endDate @@ -121,37 +113,34 @@ public class StrategyApplication { dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); dailyCloseMapping.put(daily.getTradeDate().toString(), daily.getHfqClose()); } - charts.add(Dict.create() - .set("title", code) - .set( - "data", - Dict.create() - .set( - "日线", - Dict.create() - .set("xList", dailyXList) - .set("yList", dailyYList) - .set( - "points", - asset.getHistories() - .stream() - .filter(history -> ObjectUtil.isNotEmpty(history.trades())) - .filter(history -> history.trades().containsKey(stock.getId())) - .filter(history -> ObjectUtil.isNotEmpty(history.trades().get(stock.getId()))) - .map(history -> { - var trade = history.trades().get(stock.getId()).getFirst(); - return Dict.create() - .set("value", trade.volume()) - .set("itemStyle", Dict.create() - .set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5") - ) - .set("coord", List.of(history.date().toString(), dailyCloseMapping.getOrDefault(history.date().toString(), 0.0))); - } - ) - .toList() - ) - ) - ) + charts.add( + Dict.create() + .set("title", code) + .set( + "data", + Dict.create() + .set( + "日线", + Dict.create() + .set("xList", dailyXList) + .set("yList", dailyYList) + .set( + "points", + asset.getTrades() + .stream() + .map(trade -> { + return Dict.create() + .set("value", trade.volume()) + .set("itemStyle", Dict.create() + .set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5") + ) + .set("coord", List.of(trade.date().toString(), dailyCloseMapping.getOrDefault(trade.date().toString(), 0.0))); + } + ) + .toList() + ) + ) + ) ); /*log.info("Final Cash: {}", asset.getCash()); for (var history : asset.getHistories()) {