无数据修复
continuous-integration/drone Build is passing

This commit is contained in:
曾志威
2026-05-18 02:22:28 +08:00
parent cd3e918462
commit 163fec9940
2 changed files with 125 additions and 32 deletions
+44 -18
View File
@@ -49,6 +49,23 @@ func (r *StockRepo) getLatestDate() (string, error) {
return result.MaxDate, nil return result.MaxDate, nil
} }
// resolveTradeDate 将用户指定日期映射到不晚于该日期的最近交易日。
// 当用户选中非交易日时,使用最近一个有数据的交易日,避免页面空白。
func (r *StockRepo) resolveTradeDate(date string) (string, error) {
if date == "" {
return r.getLatestDate()
}
var result struct {
ResolvedDate string
}
err := r.db.Raw("SELECT MAX(date) as resolved_date FROM stock_daily WHERE date <= ?", date).Scan(&result).Error
if err != nil {
return "", err
}
return result.ResolvedDate, nil
}
// GetLatestTradeDate 获取最新交易日期(公开方法) // GetLatestTradeDate 获取最新交易日期(公开方法)
func (r *StockRepo) GetLatestTradeDate() (string, error) { func (r *StockRepo) GetLatestTradeDate() (string, error) {
return r.getLatestDate() return r.getLatestDate()
@@ -58,6 +75,19 @@ func (r *StockRepo) GetLatestTradeDate() (string, error) {
// 包括: 涨跌统计、涨停跌停列表、百日新高列表 // 包括: 涨跌统计、涨停跌停列表、百日新高列表
// 使用单条聚合查询替代多次 COUNT 查询,提升性能 // 使用单条聚合查询替代多次 COUNT 查询,提升性能
func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, error) { func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, error) {
var err error
date, err = r.resolveTradeDate(date)
if err != nil {
return nil, err
}
if date == "" {
return &model.DashboardSummary{
LimitUpList: []model.StockRank{},
LimitDownList: []model.StockRank{},
High100List: []model.StockRank{},
}, nil
}
summary := &model.DashboardSummary{ summary := &model.DashboardSummary{
LimitUpList: []model.StockRank{}, LimitUpList: []model.StockRank{},
LimitDownList: []model.StockRank{}, LimitDownList: []model.StockRank{},
@@ -138,11 +168,12 @@ func (r *StockRepo) GetHigh100() ([]model.StockRank, error) {
// GetHigh100ByDate 获取指定日期创百日新高的股票列表,date 为空时使用最新交易日 // GetHigh100ByDate 获取指定日期创百日新高的股票列表,date 为空时使用最新交易日
func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) { func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) {
var err error var err error
date, err = r.resolveTradeDate(date)
if err != nil {
return nil, err
}
if date == "" { if date == "" {
date, err = r.getLatestDate() return []model.StockRank{}, nil
if err != nil {
return nil, err
}
} }
var list []model.StockRank var list []model.StockRank
r.db.Raw(` r.db.Raw(`
@@ -172,13 +203,12 @@ func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) {
func (r *StockRepo) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) { func (r *StockRepo) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) {
var latest string var latest string
var err error var err error
if endDate != "" { latest, err = r.resolveTradeDate(endDate)
latest = endDate if err != nil {
} else { return nil, err
latest, err = r.getLatestDate() }
if err != nil { if latest == "" {
return nil, err return []model.DailySummary{}, nil
}
} }
var result []model.DailySummary var result []model.DailySummary
@@ -260,13 +290,9 @@ func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
func (r *StockRepo) GetConsecutiveLimitsByDate(date string) ([]model.ConsecutiveLimit, error) { func (r *StockRepo) GetConsecutiveLimitsByDate(date string) ([]model.ConsecutiveLimit, error) {
var latest string var latest string
var err error var err error
if date != "" { latest, err = r.resolveTradeDate(date)
latest = date if err != nil {
} else { return nil, err
latest, err = r.getLatestDate()
if err != nil {
return nil, err
}
} }
if latest == "" { if latest == "" {
return []model.ConsecutiveLimit{}, nil return []model.ConsecutiveLimit{}, nil
+81 -14
View File
@@ -6,8 +6,8 @@ interface Props {
data: DailySummary[]; data: DailySummary[];
} }
function fmtYi(v: number) { function fmtPct(v: number) {
return +(v / 1e8).toFixed(0); return +v.toFixed(2);
} }
export default function DailyChart({ data }: Props) { export default function DailyChart({ data }: Props) {
@@ -23,6 +23,24 @@ export default function DailyChart({ data }: Props) {
const chart = chartRef.current; const chart = chartRef.current;
const dates = data.map((d) => d.date.slice(0, 10)); const dates = data.map((d) => d.date.slice(0, 10));
const turnoverYi = data.map((d) => d.total_turnover / 1e8);
const up10Max = Math.max(...data.map((d) => d.limit_up_10), 0);
const down10Max = Math.max(...data.map((d) => d.limit_down_10), 0);
const tenAxisLimit = Math.max(up10Max, down10Max, 1);
const up20Max = Math.max(...data.map((d) => d.limit_up_20), 0);
const down20Max = Math.max(...data.map((d) => d.limit_down_20), 0);
const twentyAxisLimit = Math.max(up20Max, down20Max, 1);
const turnoverChangePct = turnoverYi.map((value, index) => {
if (index === 0) return 0;
const prev = turnoverYi[index - 1];
if (!prev) return 0;
return ((value - prev) / prev) * 100;
});
const turnoverMin = Math.min(...turnoverYi);
const turnoverMax = Math.max(...turnoverYi);
const turnoverPad = Math.max((turnoverMax - turnoverMin) * 0.15, turnoverMax * 0.08, 1);
const turnoverAxisMin = Math.max(0, Math.floor((turnoverMin - turnoverPad) * 10) / 10);
const turnoverAxisMax = Math.ceil((turnoverMax + turnoverPad) * 10) / 10;
chart.setOption({ chart.setOption({
tooltip: { tooltip: {
@@ -31,7 +49,7 @@ export default function DailyChart({ data }: Props) {
}, },
legend: { legend: {
top: 0, top: 0,
data: ['上涨数', '下跌数', '成交额(亿)', '涨停10%', '跌停10%', '涨停20%', '跌停20%', '连板家数'], data: ['上涨数', '下跌数', '成交额(亿)', '成交额日环比(%)', '涨停10%', '跌停10%', '涨停20%', '跌停20%', '连板家数'],
}, },
grid: [ grid: [
{ left: 60, right: 70, top: 60, height: '25%' }, { left: 60, right: 70, top: 60, height: '25%' },
@@ -43,9 +61,45 @@ export default function DailyChart({ data }: Props) {
], ],
yAxis: [ yAxis: [
{ type: 'value', name: '股票数', gridIndex: 0 }, { type: 'value', name: '股票数', gridIndex: 0 },
{ type: 'value', name: '亿', gridIndex: 0, position: 'right', splitLine: { show: false } }, {
{ type: 'value', name: '10%家', gridIndex: 1, position: 'left' }, type: 'value',
{ type: 'value', name: '20%家', gridIndex: 1, position: 'right', splitLine: { show: false } }, name: '亿',
gridIndex: 0,
position: 'right',
scale: true,
min: turnoverAxisMin,
max: turnoverAxisMax,
splitNumber: 4,
splitLine: { show: false },
axisLabel: {
formatter: (value: number) => value.toFixed(1),
},
},
{
type: 'value',
name: '%',
gridIndex: 0,
position: 'right',
offset: 70,
splitLine: { show: false },
},
{
type: 'value',
name: '10%家',
gridIndex: 1,
position: 'left',
min: 0,
max: tenAxisLimit,
},
{
type: 'value',
name: '20%家',
gridIndex: 1,
position: 'right',
min: 0,
max: twentyAxisLimit,
splitLine: { show: false },
},
{ type: 'value', name: '连板', gridIndex: 1, position: 'right', offset: 60, splitLine: { show: false } }, { type: 'value', name: '连板', gridIndex: 1, position: 'right', offset: 60, splitLine: { show: false } },
], ],
dataZoom: [ dataZoom: [
@@ -77,17 +131,30 @@ export default function DailyChart({ data }: Props) {
type: 'line', type: 'line',
xAxisIndex: 0, xAxisIndex: 0,
yAxisIndex: 1, yAxisIndex: 1,
data: data.map((d) => fmtYi(d.total_turnover)), data: turnoverYi.map((v) => +v.toFixed(0)),
itemStyle: { color: 'rgba(114,46,209,0.6)' }, itemStyle: { color: 'rgba(114,46,209,0.6)' },
lineStyle: { width: 2 }, lineStyle: { width: 2 },
areaStyle: { color: 'rgba(114,46,209,0.15)' }, areaStyle: { color: 'rgba(114,46,209,0.15)' },
symbol: 'none', symbol: 'none',
}, },
{
name: '成交额日环比(%)',
type: 'line',
xAxisIndex: 0,
yAxisIndex: 2,
data: turnoverChangePct.map((v) => fmtPct(v)),
itemStyle: { color: '#13c2c2' },
lineStyle: { width: 2, type: 'dashed' },
symbol: 'circle',
symbolSize: 4,
smooth: true,
areaStyle: { color: 'rgba(19,194,194,0.08)' },
},
{ {
name: '涨停10%', name: '涨停10%',
type: 'line', type: 'line',
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 2, yAxisIndex: 3,
data: data.map((d) => d.limit_up_10), data: data.map((d) => d.limit_up_10),
itemStyle: { color: '#ff4d4f' }, itemStyle: { color: '#ff4d4f' },
lineStyle: { width: 2 }, lineStyle: { width: 2 },
@@ -98,8 +165,8 @@ export default function DailyChart({ data }: Props) {
name: '跌停10%', name: '跌停10%',
type: 'line', type: 'line',
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 2, yAxisIndex: 3,
data: data.map((d) => -d.limit_down_10), data: data.map((d) => Math.abs(d.limit_down_10)),
itemStyle: { color: '#52c41a' }, itemStyle: { color: '#52c41a' },
lineStyle: { width: 2 }, lineStyle: { width: 2 },
symbol: 'circle', symbol: 'circle',
@@ -109,7 +176,7 @@ export default function DailyChart({ data }: Props) {
name: '涨停20%', name: '涨停20%',
type: 'line', type: 'line',
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 3, yAxisIndex: 4,
data: data.map((d) => d.limit_up_20), data: data.map((d) => d.limit_up_20),
itemStyle: { color: '#ff7875' }, itemStyle: { color: '#ff7875' },
lineStyle: { width: 2, type: 'dashed' }, lineStyle: { width: 2, type: 'dashed' },
@@ -120,8 +187,8 @@ export default function DailyChart({ data }: Props) {
name: '跌停20%', name: '跌停20%',
type: 'line', type: 'line',
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 3, yAxisIndex: 4,
data: data.map((d) => -d.limit_down_20), data: data.map((d) => Math.abs(d.limit_down_20)),
itemStyle: { color: '#95de64' }, itemStyle: { color: '#95de64' },
lineStyle: { width: 2, type: 'dashed' }, lineStyle: { width: 2, type: 'dashed' },
symbol: 'circle', symbol: 'circle',
@@ -131,7 +198,7 @@ export default function DailyChart({ data }: Props) {
name: '连板家数', name: '连板家数',
type: 'line', type: 'line',
xAxisIndex: 1, xAxisIndex: 1,
yAxisIndex: 4, yAxisIndex: 5,
data: data.map((d) => d.consecutive_limit_count), data: data.map((d) => d.consecutive_limit_count),
itemStyle: { color: '#fa8c16' }, itemStyle: { color: '#fa8c16' },
lineStyle: { width: 3 }, lineStyle: { width: 3 },