fix(stock): 按价格(close vs prev_close)统计涨跌,新增调试接口
This commit is contained in:
@@ -29,6 +29,7 @@ type stockRow struct {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Close float64 `json:"close"`
|
Close float64 `json:"close"`
|
||||||
|
PrevClose float64 `json:"prev_close,omitempty"`
|
||||||
ChangePct float64 `json:"change_pct"`
|
ChangePct float64 `json:"change_pct"`
|
||||||
Amount float64 `json:"amount,omitempty"`
|
Amount float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -71,9 +72,9 @@ func (h *StockHandler) Overview(c *gin.Context) {
|
|||||||
if err := h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
if err := h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(*) AS total,
|
COUNT(*) AS total,
|
||||||
SUM(CASE WHEN change_pct > 0 THEN 1 ELSE 0 END) AS up,
|
SUM(CASE WHEN close > prev_close THEN 1 ELSE 0 END) AS up,
|
||||||
SUM(CASE WHEN change_pct < 0 THEN 1 ELSE 0 END) AS down,
|
SUM(CASE WHEN close < prev_close THEN 1 ELSE 0 END) AS down,
|
||||||
SUM(CASE WHEN change_pct = 0 THEN 1 ELSE 0 END) AS flat
|
SUM(CASE WHEN close = prev_close THEN 1 ELSE 0 END) AS flat
|
||||||
`).
|
`).
|
||||||
Where(baseWhere, latestDate, indexSymbolsList()).
|
Where(baseWhere, latestDate, indexSymbolsList()).
|
||||||
Scan(&counts).Error; err != nil {
|
Scan(&counts).Error; err != nil {
|
||||||
@@ -154,6 +155,84 @@ func (h *StockHandler) TriggerSync(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"success": true, "records_count": count})
|
c.JSON(http.StatusOK, gin.H{"success": true, "records_count": count})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type debugStatsResponse struct {
|
||||||
|
TradeDate string `json:"trade_date"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
IndexCount int64 `json:"index_count"`
|
||||||
|
SuspendedCount int64 `json:"suspended_count"`
|
||||||
|
UpByPrice int64 `json:"up_by_price"`
|
||||||
|
DownByPrice int64 `json:"down_by_price"`
|
||||||
|
FlatByPrice int64 `json:"flat_by_price"`
|
||||||
|
UpByPct int64 `json:"up_by_pct"`
|
||||||
|
DownByPct int64 `json:"down_by_pct"`
|
||||||
|
FlatByPct int64 `json:"flat_by_pct"`
|
||||||
|
FlatSamples []stockRow `json:"flat_samples"`
|
||||||
|
BoundarySamples []stockRow `json:"boundary_samples"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugStats 返回最新交易日的详细统计与边界样本,便于对齐第三方口径。
|
||||||
|
func (h *StockHandler) DebugStats(c *gin.Context) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var latestDate time.Time
|
||||||
|
if err := h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Select("MAX(trade_date)").Scan(&latestDate).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "查询最新交易日失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if latestDate.IsZero() {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": debugStatsResponse{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idxList := indexSymbolsList()
|
||||||
|
baseWhere := "trade_date = ? AND symbol NOT IN ? AND NOT (volume = 0 AND change_pct = 0)"
|
||||||
|
|
||||||
|
var stats debugStatsResponse
|
||||||
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Select(`
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN close > prev_close THEN 1 ELSE 0 END) AS up_by_price,
|
||||||
|
SUM(CASE WHEN close < prev_close THEN 1 ELSE 0 END) AS down_by_price,
|
||||||
|
SUM(CASE WHEN close = prev_close THEN 1 ELSE 0 END) AS flat_by_price,
|
||||||
|
SUM(CASE WHEN change_pct > 0 THEN 1 ELSE 0 END) AS up_by_pct,
|
||||||
|
SUM(CASE WHEN change_pct < 0 THEN 1 ELSE 0 END) AS down_by_pct,
|
||||||
|
SUM(CASE WHEN change_pct = 0 THEN 1 ELSE 0 END) AS flat_by_pct
|
||||||
|
`).
|
||||||
|
Where(baseWhere, latestDate, idxList).
|
||||||
|
Scan(&stats)
|
||||||
|
|
||||||
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Where("trade_date = ? AND symbol IN ?", latestDate, idxList).
|
||||||
|
Count(&stats.IndexCount)
|
||||||
|
|
||||||
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Where("trade_date = ? AND symbol NOT IN ? AND volume = 0 AND change_pct = 0", latestDate, idxList).
|
||||||
|
Count(&stats.SuspendedCount)
|
||||||
|
|
||||||
|
stats.TradeDate = latestDate.Format("2006-01-02")
|
||||||
|
|
||||||
|
var flatSamples []stockRow
|
||||||
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Select("symbol, name, close, prev_close, change_pct").
|
||||||
|
Where("trade_date = ? AND symbol NOT IN ? AND close = prev_close", latestDate, idxList).
|
||||||
|
Order("amount DESC").
|
||||||
|
Limit(20).
|
||||||
|
Scan(&flatSamples)
|
||||||
|
stats.FlatSamples = flatSamples
|
||||||
|
|
||||||
|
var boundarySamples []stockRow
|
||||||
|
h.DB.WithContext(ctx).Model(&models.StockDailyQuote{}).
|
||||||
|
Select("symbol, name, close, prev_close, change_pct").
|
||||||
|
Where("trade_date = ? AND symbol NOT IN ?", latestDate, idxList).
|
||||||
|
Order("ABS(change_pct) ASC").
|
||||||
|
Limit(20).
|
||||||
|
Scan(&boundarySamples)
|
||||||
|
stats.BoundarySamples = boundarySamples
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": stats})
|
||||||
|
}
|
||||||
|
|
||||||
func indexSymbolsList() []string {
|
func indexSymbolsList() []string {
|
||||||
list := make([]string, 0, len(coreIndexSymbols))
|
list := make([]string, 0, len(coreIndexSymbols))
|
||||||
for s := range coreIndexSymbols {
|
for s := range coreIndexSymbols {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func Setup(cfg *config.Config, db *gorm.DB, stockSyncSvc *services.StockSyncServ
|
|||||||
admin.DELETE("/users/:id", adminHandler.DeleteUser)
|
admin.DELETE("/users/:id", adminHandler.DeleteUser)
|
||||||
admin.GET("/roles", adminHandler.ListRoles)
|
admin.GET("/roles", adminHandler.ListRoles)
|
||||||
admin.POST("/stocks/sync", stockHandler.TriggerSync)
|
admin.POST("/stocks/sync", stockHandler.TriggerSync)
|
||||||
|
admin.GET("/stocks/debug-stats", stockHandler.DebugStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
Reference in New Issue
Block a user