1
0

feat: 简化交易计算

This commit is contained in:
2025-11-07 23:08:56 +08:00
parent db8a094c8f
commit 7703f88d7f
2 changed files with 83 additions and 104 deletions

View File

@@ -3,13 +3,11 @@ package com.lanyuanxiaoyao.leopard.core.strategy;
import cn.hutool.core.util.ObjectUtil; 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.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
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.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -29,9 +27,9 @@ public class TradeEngine {
this.dailyRepository = dailyRepository; this.dailyRepository = dailyRepository;
} }
public Asset backtest(List<Long> stocks, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) { public Asset backtest(Stock stock, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) {
var dailies = dailyRepository.findAll( var dailies = dailyRepository.findAll(
QDaily.daily.stock.id.in(stocks) QDaily.daily.stock.eq(stock)
.and(QDaily.daily.tradeDate.before(endDate)), .and(QDaily.daily.tradeDate.before(endDate)),
QDaily.daily.tradeDate.asc() QDaily.daily.tradeDate.asc()
); );
@@ -45,67 +43,59 @@ public class TradeEngine {
continue; continue;
} }
final var currentDate = now; final var currentDate = now;
var trades = strategy.trade( var trade = strategy.trade(
now, now,
asset, asset,
dailies.stream() dailies.stream()
.filter(daily -> daily.getTradeDate().isBefore(currentDate)) .filter(daily -> daily.getTradeDate().isBefore(currentDate))
.collect(Collectors.groupingBy(daily -> daily.getStock().getId())) .toList()
); );
for (var trade : trades) { if (trade == 0) {
dailies.stream() continue;
.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( var daily = dailies.stream()
now, .filter(d -> ObjectUtil.equals(d.getTradeDate(), currentDate))
asset.getCash(), .findFirst()
asset.getStocks(), .orElseThrow();
trades.stream() asset.addTrade(now, trade, daily.getHfqClose());
.collect(Collectors.groupingBy(trade -> trade.stockId))
));
} }
return asset; return asset;
} }
public interface TradeStrategy { public interface TradeStrategy {
List<Trade> trade(LocalDate now, Asset asset, Map<Long, List<Daily>> dailies); int trade(LocalDate now, Asset asset, List<Daily> dailies);
} }
@Data @Data
public static final class Asset { public static final class Asset {
private double cash = 0; private List<Trade> trades = new ArrayList<>();
private double profit = 0.0;
private Map<Long, Integer> stocks = new HashMap<>();
private List<History> histories = 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, LocalDate date,
double cash, int volume,
Map<Long, Integer> stocks, double price
Map<Long, List<Trade>> trades
) { ) {
} }
} }
public record Trade(
LocalDate date,
Long stockId,
// 用正负数表达买卖
Integer volume
) {
}
} }

View File

@@ -1,7 +1,6 @@
package com.lanyuanxiaoyao.leopard.strategy; package com.lanyuanxiaoyao.leopard.strategy;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.TemplateConfig; import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine; import cn.hutool.extra.template.TemplateEngine;
@@ -71,33 +70,26 @@ public class StrategyApplication {
.forEach(code -> { .forEach(code -> {
var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow(); var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow();
var asset = tradeEngine.backtest( var asset = tradeEngine.backtest(
List.of(stock.getId()), stockRepository.findById(stock.getId()).orElseThrow(),
(now, currentAsset, dailies) -> { (now, currentAsset, dailies) -> {
return dailies.entrySet() var stockDailies = dailies
.stream() .stream()
.map(entry -> { .sorted(Comparator.comparing(Daily::getTradeDate))
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(); .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, startDate,
endDate endDate
@@ -121,37 +113,34 @@ public class StrategyApplication {
dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
dailyCloseMapping.put(daily.getTradeDate().toString(), daily.getHfqClose()); dailyCloseMapping.put(daily.getTradeDate().toString(), daily.getHfqClose());
} }
charts.add(Dict.create() charts.add(
.set("title", code) Dict.create()
.set( .set("title", code)
"data", .set(
Dict.create() "data",
.set( Dict.create()
"日线", .set(
Dict.create() "日线",
.set("xList", dailyXList) Dict.create()
.set("yList", dailyYList) .set("xList", dailyXList)
.set( .set("yList", dailyYList)
"points", .set(
asset.getHistories() "points",
.stream() asset.getTrades()
.filter(history -> ObjectUtil.isNotEmpty(history.trades())) .stream()
.filter(history -> history.trades().containsKey(stock.getId())) .map(trade -> {
.filter(history -> ObjectUtil.isNotEmpty(history.trades().get(stock.getId()))) return Dict.create()
.map(history -> { .set("value", trade.volume())
var trade = history.trades().get(stock.getId()).getFirst(); .set("itemStyle", Dict.create()
return Dict.create() .set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5")
.set("value", trade.volume()) )
.set("itemStyle", Dict.create() .set("coord", List.of(trade.date().toString(), dailyCloseMapping.getOrDefault(trade.date().toString(), 0.0)));
.set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5") }
) )
.set("coord", List.of(history.date().toString(), dailyCloseMapping.getOrDefault(history.date().toString(), 0.0))); .toList()
} )
) )
.toList() )
)
)
)
); );
/*log.info("Final Cash: {}", asset.getCash()); /*log.info("Final Cash: {}", asset.getCash());
for (var history : asset.getHistories()) { for (var history : asset.getHistories()) {