package service import ( "fmt" "sort" "strconv" "strings" "level_go/internal/dbcompat" "level_go/internal/model" ) // GetLimitOpenCompareGroups 返回昨日涨停且今日继续涨停股票的开盘对比分组。 // // 重点识别两类股票:今日开盘低于前收的股票,以及首板进二板且 11 点前首次涨停的股票。 func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.LimitOpenCompareResponse, error) { resp := model.LimitOpenCompareResponse{TradeDate: tradeDate} tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate) if err != nil { return resp, err } if dateIndex == -1 { resp.Groups = buildLimitOpenCompareGroups(nil, nil) return resp, nil } previousTradeDate := "" if dateIndex > 0 { previousTradeDate = tradeDates[dateIndex-1] } resp.PreviousTradeDate = previousTradeDate previousTwoTradeDate := "" if dateIndex > 1 { previousTwoTradeDate = tradeDates[dateIndex-2] } resp.PreviousTwoTradeDate = previousTwoTradeDate stocks, err := s.loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate) if err != nil { return resp, err } openLowerStocks := make([]model.LimitOpenCompareStock, 0) otherStocks := make([]model.LimitOpenCompareStock, 0) firstToSecondBoardCount := 0 firstSecondBeforeElevenCount := 0 for _, stock := range stocks { // previousTwoTradeDate 未涨停、previousTradeDate 涨停、tradeDate 涨停时,认为是首板进二板。 if stock.IsFirstToSecondBoard { stock.HighlightLabel = "首板进二板" firstToSecondBoardCount++ } isOpenLower := previousTradeDate != "" && stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.PreviousClose if isOpenLower && stock.IsFirstToSecondBoard && isBeforeEleven(stock.AkFirstLimitTime) { // 首板进二板且 11 点前封板的低开股,前端会用更强提示标签展示。 stock.IsFirstSecondBeforeEleven = true stock.HighlightLabel = "首二一一" firstSecondBeforeElevenCount++ } if isOpenLower { openLowerStocks = append(openLowerStocks, stock) continue } otherStocks = append(otherStocks, stock) } resp.Summary = model.LimitOpenCompareSummary{ LimitCount: len(stocks), OpenLowerPreviousCloseCount: len(openLowerStocks), OthersCount: len(otherStocks), FirstToSecondBoardCount: firstToSecondBoardCount, FirstSecondBeforeElevenCount: firstSecondBeforeElevenCount, } resp.Groups = buildLimitOpenCompareGroups(openLowerStocks, otherStocks) return resp, nil } // isBeforeEleven 判断涨停时间是否早于 11:00。 // // 兼容 HH:MM:SS、930、0930、93000、093000 等时间格式。 func isBeforeEleven(limitTime *string) bool { if limitTime == nil || *limitTime == "" { return false } timeText := strings.TrimSpace(*limitTime) hour := 0 minute := 0 second := 0 if strings.Contains(timeText, ":") { if n, _ := fmt.Sscanf(timeText, "%d:%d:%d", &hour, &minute, &second); n < 2 { return false } } else { if len(timeText) < 4 { return false } switch len(timeText) { case 4: timeText += "00" case 5: timeText = "0" + timeText case 6: default: return false } var err error hour, err = strconv.Atoi(timeText[0:2]) if err != nil { return false } minute, err = strconv.Atoi(timeText[2:4]) if err != nil { return false } second, err = strconv.Atoi(timeText[4:6]) if err != nil { return false } } if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 { return false } return hour < 11 } // loadLimitOpenCompareStocks 查询昨日涨停且今日继续涨停的股票,并带出今日开盘和昨日收盘。 func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate string) ([]model.LimitOpenCompareStock, error) { var stocks []model.LimitOpenCompareStock query := `SELECT l.symbol, l.ts_code, l.name, COALESCE(l.market, '') AS market, DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date, d.open IS NOT NULL AS has_open, COALESCE(d.open, 0) AS open, COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close, ? AS previous_trade_date, ? AS previous_two_trade_date, pd.close IS NOT NULL AS has_previous_close, COALESCE(pd.close, 0) AS previous_close, (? <> '' AND NOT EXISTS ( SELECT 1 FROM stock_limit_combined_data ppl WHERE ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = ? AND ppl.final_status = 1 )) AS is_first_to_second_board, COALESCE(l.calc_limit_price, 0) AS limit_price, COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg, l.tushare_amount AS ak_amount, l.tushare_float_mv AS ak_float_market_value, l.tushare_total_mv AS ak_total_market_value, l.tushare_turnover_ratio AS ak_turnover_rate, l.tushare_fd_amount AS ak_seal_amount, l.tushare_first_time AS ak_first_limit_time, l.tushare_last_time AS ak_last_limit_time, l.tushare_open_times AS ak_break_limit_count, l.tushare_up_stat AS ak_limit_up_stat, l.tushare_limit_times AS ak_limit_up_days, l.tushare_industry AS ak_industry, l.calc_vol AS vol FROM stock_limit_combined_data l LEFT JOIN stock_daily d ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date LEFT JOIN stock_daily pd ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ? INNER JOIN stock_limit_combined_data pl ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1 WHERE l.trade_date = ? AND l.final_status = 1 ORDER BY d.pct_chg DESC, l.ts_code ASC` if previousTradeDate == "" { return stocks, nil } err := s.DB.Raw(query, previousTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error return stocks, err } // buildLimitOpenCompareGroups 构造“开盘低于前收”和“其他涨停股”两个固定分组。 func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenCompareStock) []model.LimitOpenCompareGroup { if openLowerStocks == nil { openLowerStocks = []model.LimitOpenCompareStock{} } if otherStocks == nil { otherStocks = []model.LimitOpenCompareStock{} } sortLimitOpenCompareStocks(openLowerStocks) sortLimitOpenCompareStocks(otherStocks) return []model.LimitOpenCompareGroup{ { Key: "open_lower_previous_close", Label: "开盘低于前收", Count: len(openLowerStocks), Stocks: openLowerStocks, }, { Key: "others", Label: "其他涨停股", Count: len(otherStocks), Stocks: otherStocks, }, } } // sortLimitOpenCompareStocks 按涨跌幅降序、股票代码升序稳定展示分组内股票。 func sortLimitOpenCompareStocks(stocks []model.LimitOpenCompareStock) { sort.Slice(stocks, func(i, j int) bool { if stocks[i].PctChg == stocks[j].PctChg { return stocks[i].TsCode < stocks[j].TsCode } return stocks[i].PctChg > stocks[j].PctChg }) }