feat: 简化交易计算
This commit is contained in:
@@ -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
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user