feat(strategy): 添加股票数据可视化功能
- 在 StrategyApplication 中实现了一个简单的股票数据可视化功能 - 使用 ECharts 和 Amis 渲染股票数据的蜡烛图和均线 - 新增了 TestMarkdown 类,用于测试 Markdown 渲染功能 - 在 application.yml 中添加了 LiteFlow 相关配置 - 更新了 pom.xml,添加了 LiteFlow、CommonMark 等依赖
This commit is contained in:
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.lanyuanxiaoyao.leopard.strategy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
|
||||
/**
|
||||
* Markdown Render
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250918
|
||||
*/
|
||||
public class TestMarkdown {
|
||||
public static void main(String[] args) throws IOException {
|
||||
var extensions = List.of(TablesExtension.create());
|
||||
var parser = Parser.builder()
|
||||
.extensions(extensions)
|
||||
.build();
|
||||
var render = HtmlRenderer.builder()
|
||||
.extensions(extensions)
|
||||
.build();
|
||||
var result = render.render(parser.parse(
|
||||
// language=Markdown
|
||||
"""
|
||||
### Hello
|
||||
|
||||
```echarts
|
||||
System.out.println("go");
|
||||
```
|
||||
|
||||
> I am iron man
|
||||
|
||||
| | |
|
||||
|------|----|
|
||||
| Tony | 12 |
|
||||
"""
|
||||
));
|
||||
Files.writeString(Path.of("result.html"), result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user