package service import ( "math" "sort" "gorm.io/gorm" "level_go/internal/dbcompat" "level_go/internal/model" ) // StockService 提供股票行情查询和区间表现分析能力。 type StockService struct { DB *gorm.DB // 数据库连接。 } // GetHistoryKline 查询指定股票自固定起点以来的日 K 线。 func (s *StockService) GetHistoryKline(code string) ([]model.StockDaily, error) { var dailies []model.StockDaily startDate := "2020-01-02" err := s.DB.Table("stock_daily"). Where("ts_code = ? AND trade_date >= ?", code, startDate). Order("trade_date ASC"). Find(&dailies).Error return dailies, err } // roundTo2 把浮点数四舍五入到两位小数。 func roundTo2(val float64) float64 { return math.Round(val*100) / 100 } // AnalyzePerformance 分析某个涨停日之后到结束日期之间的下跌和反弹表现。 // // startDate 既是涨停筛选日期,也是行情区间起点;endDate 是行情区间终点。 func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.AnalysisResponse, error) { var results []model.AnalysisResponse var limits []struct { TsCode string Name string Open float64 High float64 } // 查询 startDate 当日涨停股,并优先使用涨停合并表中的计算最高价。 query := `SELECT l.ts_code, l.name, d.open, COALESCE(l.calc_high, d.high, 0) AS high FROM stock_limit_combined_data l LEFT JOIN stock_daily d ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND l.trade_date = d.trade_date WHERE l.trade_date = ? AND l.final_status = 1` if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil { return nil, err } if len(limits) == 0 { return results, nil } codes := make([]string, 0, len(limits)) for _, stock := range limits { codes = append(codes, stock.TsCode) } var allDailies []model.StockDaily if err := s.DB.Table("stock_daily"). Select("ts_code", "trade_date", "open", "high", "low", "close"). Where("ts_code IN ? AND trade_date BETWEEN ? AND ?", codes, startDate, endDate). Order("ts_code ASC, trade_date ASC"). Find(&allDailies).Error; err != nil { return nil, err } dailiesByCode := make(map[string][]model.StockDaily, len(codes)) for _, daily := range allDailies { dailiesByCode[daily.TsCode] = append(dailiesByCode[daily.TsCode], daily) } for _, stock := range limits { dailies := dailiesByCode[stock.TsCode] if len(dailies) <= 1 { continue } // A. 计算区间绝对最高价,用于前端展示真实区间高点。 overallMax := dailies[0].High overallMaxDate := dailies[0].TradeDate.Format("2006-01-02") for _, d := range dailies { if d.High > overallMax { overallMax = d.High overallMaxDate = d.TradeDate.Format("2006-01-02") } } // B. 逻辑计算以涨停日最高价为分界,区分“未跌破涨停高点”和“跌破后反弹”两类形态。 limitHigh := stock.High postDailies := dailies[1:] // 找出涨停后的全局最低,用来判断是否跌破涨停日最高价。 minLowAfterLimit := postDailies[0].Low for _, d := range postDailies { if d.Low < minLowAfterLimit { minLowAfterLimit = d.Low } } var dropPct, reboundPct *float64 var logicMaxHigh float64 var logicMaxDate string if minLowAfterLimit >= limitHigh { // 形态 1:强势,后续最低价仍高于涨停日最高价,因此只统计向上空间。 currMax := limitHigh maxIdx := -1 for i, d := range postDailies { if d.High > currMax { currMax = d.High maxIdx = i } } logicMaxHigh = currMax logicMaxDate = startDate if maxIdx != -1 { logicMaxDate = postDailies[maxIdx].TradeDate.Format("2006-01-02") val := roundTo2(((logicMaxHigh - limitHigh) / limitHigh) * 100) reboundPct = &val } dropPct = nil } else { // 形态 2:破位,先找到破位最低点,再计算从此前最高点到最低点的降幅。 absMinLow := postDailies[0].Low minIdx := 0 for i, d := range postDailies { if d.Low < absMinLow { absMinLow = d.Low minIdx = i } } // 找到涨停日到最低点之间的最高价,作为本次回撤的逻辑高点。 rangeDailies := dailies[:minIdx+2] rangeMax := dailies[0].High maxD := startDate for _, d := range rangeDailies { if d.High > rangeMax { rangeMax = d.High maxD = d.TradeDate.Format("2006-01-02") } } logicMaxHigh = rangeMax logicMaxDate = maxD dVal := roundTo2(((rangeMax - absMinLow) / rangeMax) * 100) dropPct = &dVal lastClose := postDailies[len(postDailies)-1].Close rVal := roundTo2(((lastClose - absMinLow) / absMinLow) * 100) reboundPct = &rVal } // 收盘价序列给前端画趋势图使用。 var closePrices []float64 for _, d := range dailies { closePrices = append(closePrices, d.Close) } results = append(results, model.AnalysisResponse{ Code: stock.TsCode, Name: stock.Name, DayOpen: stock.Open, DayHigh: stock.High, MaxHigh: logicMaxHigh, MaxDate: logicMaxDate, OverallMaxHigh: overallMax, OverallMaxDate: overallMaxDate, PostMinLow: minLowAfterLimit, DropPct: dropPct, ReboundPct: reboundPct, ClosePrices: closePrices, }) } // 优先展示跌幅更大的破位股票;强势股票由于 DropPct 为 nil 排在后面。 sort.Slice(results, func(i, j int) bool { if results[i].DropPct == nil && results[j].DropPct != nil { return false } if results[i].DropPct != nil && results[j].DropPct == nil { return true } if results[i].DropPct != nil && results[j].DropPct != nil { return *results[i].DropPct > *results[j].DropPct } return false }) return results, nil }