diff --git a/leopard-strategy/src/main/resources/templates/report.html b/leopard-strategy/src/main/resources/templates/report.html index d11a9c7..367d3b7 100644 --- a/leopard-strategy/src/main/resources/templates/report.html +++ b/leopard-strategy/src/main/resources/templates/report.html @@ -34,74 +34,163 @@ '2025-01-08': [112, 118, 110, 121], '2025-01-09': [118, 121, 117, 123], '2025-01-10': [121, 119, 118, 122], - }; - - // Derive arrays for the chart from the KV map - const dates = Object.keys(data).sort(); - // For custom rendering, augment with category index: [idx, open, close, low, high] - const ohlcData = dates.map((d, i) => [i, ...data[d]]); - - // Styling and helpers - const UP_COLOR = '#000000'; // up: black - const DOWN_COLOR = '#9e9e9e'; // down: gray - - function renderOHLCMinimal(params, api) { - var idx = api.value(0); - var open = api.value(1); - var close = api.value(2); - var low = api.value(3); - var high = api.value(4); - var up = close >= open; - - var x = api.coord([idx, 0])[0]; - var highPoint = api.coord([idx, high]); - var lowPoint = api.coord([idx, low]); - var openPoint = api.coord([idx, open]); - var closePoint = api.coord([idx, close]); - - var band = api.size([1, 0])[0]; - // Keep dash length unchanged from the previous baseline - var tick = Math.max(4, Math.min(10, band * 0.4)); - var color = up ? UP_COLOR : DOWN_COLOR; - - return { - type: 'group', - children: [ - { - type: 'line', - shape: { x1: x, y1: highPoint[1], x2: x, y2: lowPoint[1] }, - style: { stroke: color, lineWidth: 1, opacity: 0.9 }, - }, - { - type: 'line', - shape: { x1: x - tick, y1: openPoint[1], x2: x, y2: openPoint[1] }, - style: { stroke: color, lineWidth: 1.2, opacity: 0.95 }, - }, - { - type: 'line', - shape: { x1: x, y1: closePoint[1], x2: x + tick, y2: closePoint[1] }, - style: { stroke: color, lineWidth: 1.6, opacity: 0.95 }, - }, - ] - }; } - function formatTooltip(params) { - var p = Array.isArray(params) ? params[0] : params; - var v = p.data; // [idx, open, close, low, high] - var d = dates[v[0]]; - var o = v[1], c = v[2], l = v[3], h = v[4]; - var chg = (c - o); - var chgPct = o ? (chg / o * 100) : 0; - var sign = chg >= 0 ? '+' : ''; - return [ - d, - 'O: ' + o, - 'C: ' + c, - 'H: ' + h, - 'L: ' + l, - 'Chg: ' + sign + chg.toFixed(2) + ' (' + sign + chgPct.toFixed(2) + '%)' - ].join('
'); + // 全局配置(颜色、尺寸、间距等),集中管理,便于统一调整 + 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('
') + } + } + + // 通用基础配置构建(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 () { @@ -112,49 +201,22 @@ body: { type: 'tabs', tabsMode: 'vertical', - // amis expects `tabs` to be an array. Using an object causes `.map` errors at runtime. tabs: [ { - title: '测试', - body: { - type: 'chart', - height: 800, - // ECharts candlestick example - config: { - animation: false, - // Minimal style: hide legend for a cleaner look - legend: { show: false }, - tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, formatter: formatTooltip }, - grid: { left: '10%', right: '8%', top: 50, bottom: 80 }, - xAxis: { - type: 'category', - // Use computed arrays directly to avoid template interpolation issues - data: dates, - boundaryGap: true, - axisLine: { onZero: false }, + title: 'Charts', + // 在一个 Tab 中展示两张图,便于同屏对比 + body: [ + { + type: 'chart', + height: 500, + config: buildRangeCloseOption(data), }, - yAxis: { - scale: true, + { + type: 'chart', + height: 500, + config: buildOHLCMinimalOption(data), }, - dataZoom: [ - // Inside zoom for wheel/gesture; default selects all - { type: 'inside', xAxisIndex: 0, start: 0, end: 100 }, - // Slider placed under the x-axis; default selects all - { show: true, type: 'slider', xAxisIndex: 0, bottom: 16, height: 26, start: 0, end: 100 }, - ], - series: [ - { - name: 'ohlc', - type: 'custom', - // Minimal OHLC: neutral stem (high-low) + short dashes at open/close - renderItem: renderOHLCMinimal, - encode: { x: 0, y: [1, 2, 3, 4] }, - data: ohlcData, - z: 10, - }, - ], - }, - }, + ], }, ], },