feat: 实现基本的回测图表绘制
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record DailyDouble(LocalDate date, Double value) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
public record YearAndMonth(int year, int month) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
public record YearAndWeek(int year, int week) {
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.helper;
|
package com.lanyuanxiaoyao.leopard.core.helper;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -34,4 +36,30 @@ public class TaHelper {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double maxFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.max()
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double minFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.min()
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double sumFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.service;
|
package com.lanyuanxiaoyao.leopard.core.service;
|
||||||
|
|
||||||
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.FinanceIndicator;
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
||||||
@@ -8,6 +7,8 @@ import com.lanyuanxiaoyao.leopard.core.entity.QFinanceIndicator;
|
|||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly;
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndMonth;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndWeek;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.dto.Yearly;
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Yearly;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
||||||
@@ -19,12 +20,15 @@ import java.time.temporal.WeekFields;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.maxFromDaily;
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.minFromDaily;
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.sumFromDaily;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lanyuanxiaoyao
|
* @author lanyuanxiaoyao
|
||||||
* @version 20250828
|
* @version 20250828
|
||||||
@@ -143,9 +147,9 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
var open = dailies.getFirst().getHfqOpen();
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
var close = dailies.getLast().getHfqClose();
|
var close = dailies.getLast().getHfqClose();
|
||||||
return new Monthly(
|
return new Monthly(
|
||||||
LocalDate.of(yearAndMonth.year, yearAndMonth.month, 1),
|
LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1),
|
||||||
yearAndMonth.year,
|
yearAndMonth.year(),
|
||||||
yearAndMonth.month,
|
yearAndMonth.month(),
|
||||||
open,
|
open,
|
||||||
maxFromDaily(dailies, Daily::getHfqHigh),
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
minFromDaily(dailies, Daily::getHfqLow),
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
@@ -193,9 +197,9 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
var open = dailies.getFirst().getHfqOpen();
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
var close = dailies.getLast().getHfqClose();
|
var close = dailies.getLast().getHfqClose();
|
||||||
return new Weekly(
|
return new Weekly(
|
||||||
LocalDate.of(yearAndWeek.year, 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week),
|
LocalDate.of(yearAndWeek.year(), 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week()),
|
||||||
yearAndWeek.year,
|
yearAndWeek.year(),
|
||||||
yearAndWeek.week,
|
yearAndWeek.week(),
|
||||||
open,
|
open,
|
||||||
maxFromDaily(dailies, Daily::getHfqHigh),
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
minFromDaily(dailies, Daily::getHfqLow),
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
@@ -209,36 +213,4 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Double maxFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
|
||||||
return dailies.stream()
|
|
||||||
.map(function)
|
|
||||||
.filter(ObjectUtil::isNotNull)
|
|
||||||
.mapToDouble(Double::doubleValue)
|
|
||||||
.max()
|
|
||||||
.orElse(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double minFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
|
||||||
return dailies.stream()
|
|
||||||
.map(function)
|
|
||||||
.filter(ObjectUtil::isNotNull)
|
|
||||||
.mapToDouble(Double::doubleValue)
|
|
||||||
.min()
|
|
||||||
.orElse(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double sumFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
|
||||||
return dailies.stream()
|
|
||||||
.map(function)
|
|
||||||
.filter(ObjectUtil::isNotNull)
|
|
||||||
.mapToDouble(Double::doubleValue)
|
|
||||||
.sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
private record YearAndMonth(int year, int month) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record YearAndWeek(int year, int week) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
||||||
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
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.extra.template.TemplateConfig;
|
import cn.hutool.extra.template.TemplateConfig;
|
||||||
import cn.hutool.extra.template.TemplateEngine;
|
import cn.hutool.extra.template.TemplateEngine;
|
||||||
import cn.hutool.extra.template.TemplateUtil;
|
import cn.hutool.extra.template.TemplateUtil;
|
||||||
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.QStock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndMonth;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndWeek;
|
||||||
import com.lanyuanxiaoyao.leopard.core.helper.TaHelper;
|
import com.lanyuanxiaoyao.leopard.core.helper.TaHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.WeekFields;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
@@ -24,12 +37,20 @@ import org.springframework.context.event.EventListener;
|
|||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.maxFromDaily;
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.minFromDaily;
|
||||||
|
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.sumFromDaily;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
public class StrategyApplication {
|
public class StrategyApplication {
|
||||||
private static final TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
private static final TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TradeEngine tradeEngine;
|
||||||
|
@Resource
|
||||||
|
private StockRepository stockRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private DailyRepository dailyRepository;
|
private DailyRepository dailyRepository;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -41,38 +62,234 @@ public class StrategyApplication {
|
|||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void test() throws IOException {
|
public void backtest() throws IOException {
|
||||||
var charts = Dict.create();
|
var startDate = LocalDate.of(2024, 12, 1);
|
||||||
List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ", "000651.SZ", "000848.SZ", "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH")
|
var endDate = LocalDate.of(2024, 12, 31);
|
||||||
|
var charts = new ArrayList<Dict>();
|
||||||
|
List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ"/*, "000651.SZ", "000848.SZ", "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH"*/)
|
||||||
.parallelStream()
|
.parallelStream()
|
||||||
.forEach(code -> {
|
.forEach(code -> {
|
||||||
var dailies = dailyRepository.findAll(
|
var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow();
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
var sources = dailyRepository.findAll(
|
||||||
QDaily.daily.stock.code.eq(code)
|
QDaily.daily.stock.code.eq(code)
|
||||||
.and(QDaily.daily.tradeDate.after(LocalDate.of(2025, 1, 1))),
|
.and(QDaily.daily.tradeDate.after(startDate.minusDays(150)))
|
||||||
|
.and(QDaily.daily.tradeDate.before(endDate)),
|
||||||
QDaily.daily.tradeDate.asc()
|
QDaily.daily.tradeDate.asc()
|
||||||
);
|
);
|
||||||
|
|
||||||
var sma30 = TaHelper.sma(dailies, 30, Daily::getHfqClose);
|
var dailies = sources.stream()
|
||||||
var slopes = new ArrayList<Double>();
|
.filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate))
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var dailyXList = new ArrayList<String>();
|
||||||
|
var dailyYList = new ArrayList<List<Double>>();
|
||||||
|
var dailyCloseMapping = new HashMap<String, Double>();
|
||||||
|
for (var daily : dailies) {
|
||||||
|
dailyXList.add(daily.getTradeDate().toString());
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
/*log.info("Final Cash: {}", asset.getCash());
|
||||||
|
for (var history : asset.getHistories()) {
|
||||||
|
log.info("Date: {} Cash: {} Trade: {}", history.date(), history.cash(), history.trades().values());
|
||||||
|
}*/
|
||||||
|
});
|
||||||
|
var template = engine.getTemplate("backtest_report.html");
|
||||||
|
Files.writeString(Path.of("backtest_report.html"), template.render(
|
||||||
|
Dict.create().set("charts", charts)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void test() throws IOException {
|
||||||
|
var dailyRange = 150;
|
||||||
|
var weekRange = 24;
|
||||||
|
var monthRange = 12;
|
||||||
|
var charts = Dict.create();
|
||||||
|
List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ", "000651.SZ", "000848.SZ"/*, "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH"*/)
|
||||||
|
.parallelStream()
|
||||||
|
.forEach(code -> {
|
||||||
|
var sources = dailyRepository.findAll(
|
||||||
|
QDaily.daily.stock.code.eq(code)
|
||||||
|
.and(QDaily.daily.tradeDate.after(LocalDate.now().minusMonths(12))),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
);
|
||||||
|
|
||||||
|
var dailies = sources.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusDays(dailyRange)))
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var dailyXList = new ArrayList<String>();
|
||||||
|
var dailyYList = new ArrayList<List<Double>>();
|
||||||
|
for (var daily : dailies) {
|
||||||
|
dailyXList.add(daily.getTradeDate().toString());
|
||||||
|
dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 30日均线和均线斜率
|
||||||
|
var sma30 = TaHelper.sma(sources, 30, Daily::getHfqClose).subList(sources.size() - dailyRange, sources.size());
|
||||||
|
/*var slopes = new ArrayList<Double>();
|
||||||
slopes.add(0.0);
|
slopes.add(0.0);
|
||||||
for (int i = 1; i < sma30.size(); i++) {
|
for (int i = 1; i < sma30.size(); i++) {
|
||||||
slopes.add(((sma30.get(i) - sma30.get(i - 1)) * 1000.0) / sma30.get(i - 1));
|
slopes.add(((sma30.get(i) - sma30.get(i - 1)) * 1000.0) / sma30.get(i - 1));
|
||||||
}
|
}*/
|
||||||
|
var sma60 = TaHelper.sma(sources, 60, Daily::getHfqClose).subList(sources.size() - dailyRange, sources.size());
|
||||||
|
|
||||||
var xList = new ArrayList<String>();
|
charts.set(
|
||||||
var yList = new ArrayList<List<Double>>();
|
StrUtil.format("日线 {}", code),
|
||||||
for (var daily : dailies) {
|
Dict.create()
|
||||||
xList.add(daily.getTradeDate().toString());
|
.set("xList", dailyXList)
|
||||||
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
|
.set("yList", dailyYList)
|
||||||
|
.set("sma30", sma30)
|
||||||
|
.set("sma60", sma60)
|
||||||
|
// .set("sma30Slopes", slopes)
|
||||||
|
);
|
||||||
|
|
||||||
|
var weeklies = sources.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusWeeks(weekRange)))
|
||||||
|
.collect(Collectors.groupingBy(daily -> new YearAndWeek(daily.getTradeDate().getYear(), daily.getTradeDate().get(WeekFields.ISO.weekOfYear()))))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var yearAndWeek = entry.getKey();
|
||||||
|
var subDailies = entry.getValue();
|
||||||
|
var open = subDailies.getFirst().getHfqOpen();
|
||||||
|
var close = subDailies.getLast().getHfqClose();
|
||||||
|
return new Weekly(
|
||||||
|
LocalDate.of(yearAndWeek.year(), 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week()),
|
||||||
|
yearAndWeek.year(),
|
||||||
|
yearAndWeek.week(),
|
||||||
|
open,
|
||||||
|
maxFromDaily(subDailies, Daily::getHfqHigh),
|
||||||
|
minFromDaily(subDailies, Daily::getHfqLow),
|
||||||
|
close,
|
||||||
|
close - open,
|
||||||
|
(close - open) / open * 100,
|
||||||
|
sumFromDaily(subDailies, Daily::getVolume),
|
||||||
|
sumFromDaily(subDailies, Daily::getTurnover)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var weekXList = new ArrayList<String>();
|
||||||
|
var weekYList = new ArrayList<List<Double>>();
|
||||||
|
for (var weekly : weeklies) {
|
||||||
|
weekXList.add(weekly.tradeDate().toString());
|
||||||
|
weekYList.add(List.of(weekly.open(), weekly.close(), weekly.low(), weekly.high()));
|
||||||
}
|
}
|
||||||
|
|
||||||
charts.set(
|
charts.set(
|
||||||
code,
|
StrUtil.format("周线 {}", code),
|
||||||
Dict.create()
|
Dict.create()
|
||||||
.set("xList", xList)
|
.set("xList", weekXList)
|
||||||
.set("yList", yList)
|
.set("yList", weekYList)
|
||||||
.set("sma30", sma30)
|
);
|
||||||
.set("sma30Slopes", slopes)
|
|
||||||
|
var monthlies = sources.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusMonths(monthRange)))
|
||||||
|
.collect(Collectors.groupingBy(daily -> new YearAndMonth(daily.getTradeDate().getYear(), daily.getTradeDate().getMonthValue())))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var yearAndMonth = entry.getKey();
|
||||||
|
var subDailies = entry.getValue();
|
||||||
|
var open = subDailies.getFirst().getHfqOpen();
|
||||||
|
var close = subDailies.getLast().getHfqClose();
|
||||||
|
return new Monthly(
|
||||||
|
LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1),
|
||||||
|
yearAndMonth.year(),
|
||||||
|
yearAndMonth.month(),
|
||||||
|
open,
|
||||||
|
maxFromDaily(subDailies, Daily::getHfqHigh),
|
||||||
|
minFromDaily(subDailies, Daily::getHfqLow),
|
||||||
|
close,
|
||||||
|
close - open,
|
||||||
|
(close - open) / open * 100,
|
||||||
|
sumFromDaily(subDailies, Daily::getVolume),
|
||||||
|
sumFromDaily(subDailies, Daily::getTurnover)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparingInt(monthly -> monthly.year() * 100 + monthly.month()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var monthXList = new ArrayList<String>();
|
||||||
|
var monthYList = new ArrayList<List<Double>>();
|
||||||
|
for (var month : monthlies) {
|
||||||
|
monthXList.add(month.tradeDate().toString());
|
||||||
|
monthYList.add(List.of(month.open(), month.close(), month.low(), month.high()));
|
||||||
|
}
|
||||||
|
|
||||||
|
charts.set(
|
||||||
|
StrUtil.format("月线 {}", code),
|
||||||
|
Dict.create()
|
||||||
|
.set("xList", monthXList)
|
||||||
|
.set("yList", monthYList)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
||||||
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
|
||||||
<root level="ERROR">
|
<root level="ERROR">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<html lang='zh'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'/>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||||
|
<meta content='width=device-width, initial-scale=1.0' name='viewport'/>
|
||||||
|
<title>Strategy</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/antd.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/helper.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/iconfont.min.css" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
html, body, #root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='root'></div>
|
||||||
|
</body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
||||||
|
<script th:inline="javascript" type='text/javascript'>
|
||||||
|
function candleChart(title, data) {
|
||||||
|
return {
|
||||||
|
type: 'service',
|
||||||
|
data: data,
|
||||||
|
body: {
|
||||||
|
className: 'mt-2',
|
||||||
|
type: 'chart',
|
||||||
|
height: 800,
|
||||||
|
config: {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
animation: true,
|
||||||
|
animationDuration: 1000,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
borderColor: '#333',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
padding: 12,
|
||||||
|
formatter: function (params) {
|
||||||
|
const param = params[0]
|
||||||
|
const open = parseFloat(param.data[1]).toFixed(2)
|
||||||
|
const close = parseFloat(param.data[2]).toFixed(2)
|
||||||
|
const lowest = parseFloat(param.data[3]).toFixed(2)
|
||||||
|
const highest = parseFloat(param.data[4]).toFixed(2)
|
||||||
|
|
||||||
|
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<span>开盘:</span>
|
||||||
|
<span class="font-bold ml-4">${open}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<span>收盘:</span>
|
||||||
|
<span class="font-bold ml-4">${close}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<span>最低:</span>
|
||||||
|
<span class="font-bold ml-4">${lowest}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<span>最高:</span>
|
||||||
|
<span class="font-bold ml-4">${highest}</span>
|
||||||
|
</div>`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '2%',
|
||||||
|
right: '2%',
|
||||||
|
top: '15%',
|
||||||
|
bottom: '15%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
data: '${xList || []}',
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
position: 'left',
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
formatter: function (value) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 'right',
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
formatter: function (value) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
top: '90%',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'candlestick',
|
||||||
|
data: '${yList || []}',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#eb5454',
|
||||||
|
color0: '#4aaa93',
|
||||||
|
borderColor: '#eb5454',
|
||||||
|
borderColor0: '#4aaa93',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
markPoint: {
|
||||||
|
data: '${points || []}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
data: '${sma30 || []}',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(0,111,255,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
data: '${sma60 || []}',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(115,0,255,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 1,
|
||||||
|
data: '${sma30Slopes || []}',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(0,255,81,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = /*[[${charts}]]*/ [];
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const amis = amisRequire('amis/embed')
|
||||||
|
const amisJson = {
|
||||||
|
type: 'page',
|
||||||
|
title: 'Strategy',
|
||||||
|
body: {
|
||||||
|
type: 'tabs',
|
||||||
|
tabsMode: 'vertical',
|
||||||
|
tabs: data.map(item => {
|
||||||
|
return {
|
||||||
|
title: item?.title,
|
||||||
|
body: Object.keys(item?.data ?? {})
|
||||||
|
.map(key => candleChart(key, item.data[key])),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
amis.embed('#root', amisJson, {}, {theme: 'antd'})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -205,6 +205,16 @@
|
|||||||
color: 'rgba(0,111,255,0.5)',
|
color: 'rgba(0,111,255,0.5)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
data: '${sma60 || []}',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(115,0,255,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user