SHA256
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user