fix(stock): 按价格(close vs prev_close)统计涨跌,新增调试接口
This commit is contained in:
@@ -29,6 +29,7 @@ type stockRow struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Close float64 `json:"close"`
|
||||
PrevClose float64 `json:"prev_close,omitempty"`
|
||||
ChangePct float64 `json:"change_pct"`
|
||||
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{}).
|
||||
Select(`
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN change_pct > 0 THEN 1 ELSE 0 END) AS up,
|
||||
SUM(CASE WHEN change_pct < 0 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 up,
|
||||
SUM(CASE WHEN close < prev_close THEN 1 ELSE 0 END) AS down,
|
||||
SUM(CASE WHEN close = prev_close THEN 1 ELSE 0 END) AS flat
|
||||
`).
|
||||
Where(baseWhere, latestDate, indexSymbolsList()).
|
||||
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})
|
||||
}
|
||||
|
||||
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 {
|
||||
list := make([]string, 0, len(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.GET("/roles", adminHandler.ListRoles)
|
||||
admin.POST("/stocks/sync", stockHandler.TriggerSync)
|
||||
admin.GET("/stocks/debug-stats", stockHandler.DebugStats)
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user