feat: 优化回测的行情展示
This commit is contained in:
@@ -101,12 +101,13 @@ public class StrategyApplication {
|
|||||||
.filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate))
|
.filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate))
|
||||||
.sorted(Comparator.comparing(Daily::getTradeDate))
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
.toList();
|
.toList();
|
||||||
var dailyXList = new ArrayList<String>();
|
var oclhList = new HashMap<>();
|
||||||
var dailyYList = new ArrayList<List<Double>>();
|
|
||||||
var dailyCloseMapping = new HashMap<String, Double>();
|
var dailyCloseMapping = new HashMap<String, Double>();
|
||||||
for (var daily : dailies) {
|
for (var daily : dailies) {
|
||||||
dailyXList.add(daily.getTradeDate().toString());
|
oclhList.put(
|
||||||
dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
|
daily.getTradeDate().toString(),
|
||||||
|
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(
|
charts.add(
|
||||||
@@ -120,8 +121,7 @@ public class StrategyApplication {
|
|||||||
"日线",
|
"日线",
|
||||||
Dict.create()
|
Dict.create()
|
||||||
.set("type", "candle")
|
.set("type", "candle")
|
||||||
.set("xList", dailyXList)
|
.set("oclh", oclhList)
|
||||||
.set("yList", dailyYList)
|
|
||||||
.set(
|
.set(
|
||||||
"points",
|
"points",
|
||||||
asset.getTrades()
|
asset.getTrades()
|
||||||
|
|||||||
@@ -22,6 +22,163 @@
|
|||||||
</body>
|
</body>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
||||||
<script th:inline="javascript" type='text/javascript'>
|
<script th:inline="javascript" type='text/javascript'>
|
||||||
|
// 全局配置(颜色、尺寸、间距等),集中管理,便于统一调整
|
||||||
|
const CONFIG = {
|
||||||
|
colors: {up: '#000000FF', down: '#00000045'},
|
||||||
|
grid: {left: '2%', right: '2%', top: 40, bottom: 110},
|
||||||
|
zoom: {bottom: 16, height: 50},
|
||||||
|
linewidth: {stem: 1.5, openTick: 1.2, closeTick: 1.6, closeLine: 1.5},
|
||||||
|
tick: {min: 4, max: 10, scale: 0.4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 tooltip 格式化(按索引回读原始 O/H/L/C)
|
||||||
|
function makeTooltipFormatter(dataMap, dates) {
|
||||||
|
return function (params) {
|
||||||
|
let p = Array.isArray(params) ? params[0] : params
|
||||||
|
let idx = p.dataIndex
|
||||||
|
let d = dates[idx]
|
||||||
|
let ohlc = dataMap[d] || []
|
||||||
|
let o = ohlc[0], c = ohlc[1], l = ohlc[2], h = ohlc[3]
|
||||||
|
let chg = (c - o)
|
||||||
|
let chgPct = o ? (chg / o * 100) : 0
|
||||||
|
let sign = chg >= 0 ? '+' : ''
|
||||||
|
return [
|
||||||
|
d,
|
||||||
|
'O: ' + o,
|
||||||
|
'C: ' + c,
|
||||||
|
'H: ' + h,
|
||||||
|
'L: ' + l,
|
||||||
|
'Chg: ' + sign + chg.toFixed(2) + ' (' + sign + chgPct.toFixed(2) + '%)',
|
||||||
|
].join('<br/>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用基础配置构建(legend/tooltip/grid/xAxis/yAxis/dataZoom)
|
||||||
|
function buildBaseOption(dates, series, formatter) {
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
legend: {show: false},
|
||||||
|
tooltip: {trigger: 'axis', axisPointer: {type: 'cross'}, formatter},
|
||||||
|
grid: CONFIG.grid,
|
||||||
|
xAxis: {type: 'category', data: dates, boundaryGap: true, axisLine: {onZero: false}},
|
||||||
|
yAxis: {scale: true},
|
||||||
|
dataZoom: [
|
||||||
|
{type: 'inside', xAxisIndex: 0, start: 0, end: 100},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: 0,
|
||||||
|
bottom: CONFIG.zoom.bottom,
|
||||||
|
height: CONFIG.zoom.height,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range Band + Close Line(高低区间带 + 收盘线):趋势与波动范围直观
|
||||||
|
function buildRangeCloseOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const lowArr = dates.map(d => dataMap[d][2])
|
||||||
|
const highArr = dates.map(d => dataMap[d][3])
|
||||||
|
const closeArr = dates.map(d => dataMap[d][1])
|
||||||
|
const rangeArr = highArr.map((h, i) => h - lowArr[i])
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'Low',
|
||||||
|
type: 'line',
|
||||||
|
data: lowArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
emphasis: {disabled: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Range',
|
||||||
|
type: 'line',
|
||||||
|
data: rangeArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
areaStyle: {color: CONFIG.colors.down, opacity: 0.6},
|
||||||
|
z: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
type: 'line',
|
||||||
|
data: closeArr,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {color: CONFIG.colors.up, width: CONFIG.linewidth.closeLine},
|
||||||
|
z: 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal OHLC(竖线 + 左/右短横):信息等价于K线但形态简洁
|
||||||
|
function buildOHLCMinimalOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const ohlcData = dates.map((d, i) => [i, ...dataMap[d]])
|
||||||
|
|
||||||
|
// 自定义渲染:竖线=高低区间;左短横=开盘;右短横=收盘;颜色=涨跌
|
||||||
|
function renderOHLCMinimal(params, api) {
|
||||||
|
let idx = api.value(0)
|
||||||
|
let open = api.value(1)
|
||||||
|
let close = api.value(2)
|
||||||
|
let low = api.value(3)
|
||||||
|
let high = api.value(4)
|
||||||
|
let up = close >= open
|
||||||
|
|
||||||
|
let x = api.coord([idx, 0])[0]
|
||||||
|
let highPoint = api.coord([idx, high])
|
||||||
|
let lowPoint = api.coord([idx, low])
|
||||||
|
let openPoint = api.coord([idx, open])
|
||||||
|
let closePoint = api.coord([idx, close])
|
||||||
|
|
||||||
|
let band = api.size([1, 0])[0]
|
||||||
|
let tick = Math.max(CONFIG.tick.min, Math.min(CONFIG.tick.max, band * CONFIG.tick.scale))
|
||||||
|
let color = up ? CONFIG.colors.up : CONFIG.colors.down
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: highPoint[1], x2: x, y2: lowPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.stem, opacity: 0.9},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x - tick, y1: openPoint[1], x2: x, y2: openPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.openTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: closePoint[1], x2: x + tick, y2: closePoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.closeTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'ohlc',
|
||||||
|
type: 'custom',
|
||||||
|
renderItem: renderOHLCMinimal,
|
||||||
|
encode: {x: 0, y: [1, 2, 3, 4]},
|
||||||
|
data: ohlcData,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
function candleChart(title, data) {
|
function candleChart(title, data) {
|
||||||
return {
|
return {
|
||||||
type: 'service',
|
type: 'service',
|
||||||
@@ -34,201 +191,7 @@
|
|||||||
title: {
|
title: {
|
||||||
text: title,
|
text: title,
|
||||||
},
|
},
|
||||||
backgroundColor: '#fff',
|
...buildRangeCloseOption(data['oclh']),
|
||||||
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)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user