feat: 简化交易计算
This commit is contained in:
@@ -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<Long> 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> trade(LocalDate now, Asset asset, Map<Long, List<Daily>> dailies);
|
||||
int trade(LocalDate now, Asset asset, List<Daily> dailies);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static final class Asset {
|
||||
private double cash = 0;
|
||||
private double profit = 0.0;
|
||||
private Map<Long, Integer> stocks = new HashMap<>();
|
||||
private List<History> histories = new ArrayList<>();
|
||||
private List<Trade> 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<Long, Integer> stocks,
|
||||
Map<Long, List<Trade>> trades
|
||||
int volume,
|
||||
double price
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
public record Trade(
|
||||
LocalDate date,
|
||||
Long stockId,
|
||||
// 用正负数表达买卖
|
||||
Integer volume
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user