diff --git a/backend/internal/handlers/stock.go b/backend/internal/handlers/stock.go index 45d2d68..5d05d71 100644 --- a/backend/internal/handlers/stock.go +++ b/backend/internal/handlers/stock.go @@ -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 { diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go index 8435729..c1c02f3 100644 --- a/backend/internal/routes/routes.go +++ b/backend/internal/routes/routes.go @@ -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