无数据修复
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
}
// 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 获取最新交易日期(公开方法)
func (r *StockRepo) GetLatestTradeDate() (string, error) {
return r.getLatestDate()
@@ -58,6 +75,19 @@ func (r *StockRepo) GetLatestTradeDate() (string, error) {
// 包括: 涨跌统计、涨停跌停列表、百日新高列表
// 使用单条聚合查询替代多次 COUNT 查询,提升性能
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{
LimitUpList: []model.StockRank{},
LimitDownList: []model.StockRank{},
@@ -138,11 +168,12 @@ func (r *StockRepo) GetHigh100() ([]model.StockRank, error) {
// GetHigh100ByDate 获取指定日期创百日新高的股票列表,date 为空时使用最新交易日
func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) {
var err error
date, err = r.resolveTradeDate(date)
if err != nil {
return nil, err
}
if date == "" {
date, err = r.getLatestDate()
if err != nil {
return nil, err
}
return []model.StockRank{}, nil
}
var list []model.StockRank
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) {
var latest string
var err error
if endDate != "" {
latest = endDate
} else {
latest, err = r.getLatestDate()
if err != nil {
return nil, err
}
latest, err = r.resolveTradeDate(endDate)
if err != nil {
return nil, err
}
if latest == "" {
return []model.DailySummary{}, nil
}
var result []model.DailySummary
@@ -260,13 +290,9 @@ func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
func (r *StockRepo) GetConsecutiveLimitsByDate(date string) ([]model.ConsecutiveLimit, error) {
var latest string
var err error
if date != "" {
latest = date
} else {
latest, err = r.getLatestDate()
if err != nil {
return nil, err
}
latest, err = r.resolveTradeDate(date)
if err != nil {
return nil, err
}
if latest == "" {
return []model.ConsecutiveLimit{}, nil
+81 -14
View File
@@ -6,8 +6,8 @@ interface Props {
data: DailySummary[];
}
function fmtYi(v: number) {
return +(v / 1e8).toFixed(0);
function fmtPct(v: number) {
return +v.toFixed(2);
}
export default function DailyChart({ data }: Props) {
@@ -23,6 +23,24 @@ export default function DailyChart({ data }: Props) {
const chart = chartRef.current;
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({
tooltip: {
@@ -31,7 +49,7 @@ export default function DailyChart({ data }: Props) {
},
legend: {
top: 0,
data: ['上涨数', '下跌数', '成交额(亿)', '涨停10%', '跌停10%', '涨停20%', '跌停20%', '连板家数'],
data: ['上涨数', '下跌数', '成交额(亿)', '成交额日环比(%)', '涨停10%', '跌停10%', '涨停20%', '跌停20%', '连板家数'],
},
grid: [
{ left: 60, right: 70, top: 60, height: '25%' },
@@ -43,9 +61,45 @@ export default function DailyChart({ data }: Props) {
],
yAxis: [
{ 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', name: '20%家', gridIndex: 1, position: 'right', splitLine: { show: false } },
{
type: 'value',
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 } },
],
dataZoom: [
@@ -77,17 +131,30 @@ export default function DailyChart({ data }: Props) {
type: 'line',
xAxisIndex: 0,
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)' },
lineStyle: { width: 2 },
areaStyle: { color: 'rgba(114,46,209,0.15)' },
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%',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 2,
yAxisIndex: 3,
data: data.map((d) => d.limit_up_10),
itemStyle: { color: '#ff4d4f' },
lineStyle: { width: 2 },
@@ -98,8 +165,8 @@ export default function DailyChart({ data }: Props) {
name: '跌停10%',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 2,
data: data.map((d) => -d.limit_down_10),
yAxisIndex: 3,
data: data.map((d) => Math.abs(d.limit_down_10)),
itemStyle: { color: '#52c41a' },
lineStyle: { width: 2 },
symbol: 'circle',
@@ -109,7 +176,7 @@ export default function DailyChart({ data }: Props) {
name: '涨停20%',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 3,
yAxisIndex: 4,
data: data.map((d) => d.limit_up_20),
itemStyle: { color: '#ff7875' },
lineStyle: { width: 2, type: 'dashed' },
@@ -120,8 +187,8 @@ export default function DailyChart({ data }: Props) {
name: '跌停20%',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 3,
data: data.map((d) => -d.limit_down_20),
yAxisIndex: 4,
data: data.map((d) => Math.abs(d.limit_down_20)),
itemStyle: { color: '#95de64' },
lineStyle: { width: 2, type: 'dashed' },
symbol: 'circle',
@@ -131,7 +198,7 @@ export default function DailyChart({ data }: Props) {
name: '连板家数',
type: 'line',
xAxisIndex: 1,
yAxisIndex: 4,
yAxisIndex: 5,
data: data.map((d) => d.consecutive_limit_count),
itemStyle: { color: '#fa8c16' },
lineStyle: { width: 3 },