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