1
0

feat(strategy): 添加股票数据可视化功能

- 在 StrategyApplication 中实现了一个简单的股票数据可视化功能
- 使用 ECharts 和 Amis 渲染股票数据的蜡烛图和均线
- 新增了 TestMarkdown 类,用于测试 Markdown 渲染功能
- 在 application.yml 中添加了 LiteFlow 相关配置
- 更新了 pom.xml,添加了 LiteFlow、CommonMark 等依赖
This commit is contained in:
2025-09-18 18:18:53 +08:00
parent a4db463dbd
commit 01bd5ed178
4 changed files with 385 additions and 26 deletions

View File

@@ -1,28 +1,46 @@
package com.lanyuanxiaoyao.leopard.strategy;
import cn.hutool.core.lang.Tuple;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
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 com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import java.util.stream.Collectors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.ta4j.core.BaseBar;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.indicators.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
@Slf4j
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
@EnableJpaAuditing
public class StrategyApplication {
private static final List<Extension> extensions = List.of(TablesExtension.create());
private static final Parser parser = Parser.builder().extensions(extensions).build();
private static final HtmlRenderer render = HtmlRenderer.builder().extensions(extensions).build();
private static final ObjectMapper mapper = new ObjectMapper();
@Resource
private StockRepository stockRepository;
@Resource
@@ -32,34 +50,316 @@ public class StrategyApplication {
SpringApplication.run(StrategyApplication.class, args);
}
private static void render(Map<String, Object> data) throws IOException {
Files.writeString(
Path.of("result.html"),
StrUtil.format(
// language=HTML
"""
<html lang='zh'>
<head>
<meta charset='utf-8'/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<title>Strategy</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/antd.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/helper.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/iconfont.min.css"/>
<style>
html, body, #root {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id='root'/>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
<script type='text/javascript'>
(function () {
const amis = amisRequire('amis/embed')
const amisJson = {
type: 'page',
title: 'Strategy',
data: {},
body: [
/*{
type: 'table2',
source: '${items}',
columns: [
{
name: 'date',
label: 'Date',
width: 100,
},
{
name: 'code',
label: 'Code',
width: 100,
},
{
name: 'name',
label: 'Name',
width: 100,
},
{
name: 'sma5',
label: 'SMA-5',
width: 100,
},
{
name: 'sma10',
label: 'SMA-10',
width: 100,
},
],
},*/
{
type: 'chart',
height: 800,
config: {
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 = param.data[1].toFixed(2)
const close = param.data[2].toFixed(2)
const lowest = param.data[3].toFixed(2)
const highest = 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: '5%',
bottom: '12%',
containLabel: true,
},
xAxis: {
data: '${xList || []}',
axisLine: {
lineStyle: {
color: '#e0e0e0',
},
},
axisLabel: {
color: '#666',
fontWeight: 'bold',
},
splitLine: {
show: false,
},
axisTick: {
show: false,
},
},
yAxis: {
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,
},
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
show: true,
type: 'slider',
top: '90%',
start: 0,
end: 100,
},
],
series: [
{
type: 'candlestick',
data: '${yList || []}',
itemStyle: {
color: '#eb5454',
color0: '#4aaa93',
borderColor: '#eb5454',
borderColor0: '#4aaa93',
borderWidth: 1,
},
},
{
type: 'line',
data: '${sma10 || []}',
smooth: true,
lineStyle: {
color: '#dcb38a',
},
symbol: 'none',
},
{
type: 'line',
data: '${sma60 || []}',
smooth: true,
lineStyle: {
color: '#6ce3c6',
},
symbol: 'none',
},
{
type: 'line',
data: '${sma120 || []}',
smooth: true,
lineStyle: {
color: '#6cd5e3',
},
symbol: 'none',
},
],
},
},
],
}
amis.embed('#root', amisJson, {}, {theme: 'antd'})
})()
</script>
</html>
""",
mapper.writeValueAsString(data)
)
);
}
@Transactional(rollbackOn = Throwable.class)
@EventListener(ApplicationReadyEvent.class)
public void test() {
var dailies = dailyRepository.findAll(
public void test() throws IOException {
/*var dailies = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(2025),
Sort.by(Daily_.TRADE_DATE)
);
// log.info("OpenDate: {}, CloseDate: {}", dailies.getFirst().getTradeDate(), dailies.getLast().getTradeDate());
// var closes = dailies.stream().map(Daily::getHfqClose).toList();
// var statistics = new DescriptiveStatistics();
// closes.forEach(statistics::addValue);
// log.info("Ascending: {}", (closes.getLast() - closes.getFirst()) * 100.0 / closes.getFirst());
// log.info("STD: {}", statistics.getStandardDeviation());
dailies.stream()
var data = dailies.stream()
.collect(Collectors.groupingBy(Daily::getStock))
.entrySet()
.stream()
.parallelStream()
.map(entry -> {
var stock = entry.getKey();
var statistics = new DescriptiveStatistics();
entry.getValue()
var dailyList = entry.getValue()
.stream()
.map(Daily::getHfqClose)
.forEach(statistics::addValue);
var std = statistics.getStandardDeviation();
return new Tuple(stock, std);
.sorted(Comparator.comparing(Daily::getTradeDate))
.toList();
var statistics = new DescriptiveStatistics();
dailyList.stream().map(Daily::getHfqClose).forEach(statistics::addValue);
var barSeries = new BaseBarSeriesBuilder().build();
dailyList.forEach(daily -> barSeries.addBar(new BaseBar(
Duration.ofDays(1),
daily.getTradeDate().atStartOfDay().atZone(ZoneId.systemDefault()),
daily.getHfqOpen(),
daily.getHfqHigh(),
daily.getHfqLow(),
daily.getHfqClose(),
daily.getVolume()
)));
return Map.<String, String>of(
"code", stock.getCode(),
"name", stock.getName(),
"std", NumberUtil.roundStr(statistics.getStandardDeviation(), 2),
"increase", NumberUtil.roundStr((dailyList.getLast().getHfqClose() - dailyList.getFirst().getHfqClose()) * 100.0 / dailyList.getFirst().getHfqClose(), 2)
);
})
.sorted((t1, t2) -> Double.compare(t2.get(1), t1.get(1)))
.forEachOrdered(tuple -> log.info("Stock: {}, STD: {}", ((Stock) tuple.get(0)).getCode(), tuple.get(1)));
.sorted((t1, t2) -> Double.compare(Double.parseDouble(t2.get("increase")), Double.parseDouble(t1.get("increase"))))
.toList();
render(Map.of("items", data));*/
var dailies = dailyRepository.findAll(
QDaily.daily.stock.code.eq("000002.SZ")
.and(QDaily.daily.tradeDate.year().goe(2023)),
Sort.by(Daily_.TRADE_DATE)
);
var barSeries = new BaseBarSeriesBuilder().build();
dailies.forEach(daily -> barSeries.addBar(new BaseBar(
Duration.ofDays(1),
daily.getTradeDate().atStartOfDay().atZone(ZoneId.systemDefault()),
daily.getHfqOpen(),
daily.getHfqHigh(),
daily.getHfqLow(),
daily.getHfqClose(),
daily.getVolume()
)));
var sma10 = new SMAIndicator(new ClosePriceIndicator(barSeries), 10);
var sma60 = new SMAIndicator(new ClosePriceIndicator(barSeries), 60);
var sma120 = new SMAIndicator(new ClosePriceIndicator(barSeries), 120);
var xList = new ArrayList<String>();
var yList = new ArrayList<List<Double>>();
var sma10List = new ArrayList<Double>();
var sma60List = new ArrayList<Double>();
var sma120List = new ArrayList<Double>();
for (int index = 0; index < dailies.size(); index++) {
var daily = dailies.get(index);
xList.add(daily.getTradeDate().toString());
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
sma10List.add(sma10.getValue(index).doubleValue());
sma60List.add(sma60.getValue(index).doubleValue());
sma120List.add(sma120.getValue(index).doubleValue());
}
render(
MapUtil.<String, Object>builder()
.put("xList", xList)
.put("yList", yList)
.put("sma10", sma10List)
.put("sma60", sma60List)
.put("sma120", sma120List)
.build()
);
}
}

View File

@@ -2,7 +2,7 @@ spring:
application:
name: leopard-strategy
datasource:
url: jdbc:postgresql://81.71.3.24:6785/leopard_dev
url: jdbc:postgresql://81.71.3.24:6785/leopard
username: leopard
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
driver-class-name: org.postgresql.Driver
@@ -12,3 +12,6 @@ spring:
banner-mode: off
fenix:
print-banner: false
liteflow:
print-banner: false
check-node-exists: false