package service import ( "sort" "level_go/internal/model" ) // GetLimitOpenThresholdGroups 返回昨日涨停股今日开盘是否低于前收 101% 的分组。 func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model.LimitOpenThresholdResponse, error) { resp := model.LimitOpenThresholdResponse{TradeDate: tradeDate} // thresholdRatio 是开盘价阈值,1.01 表示前收的 101%。 const thresholdRatio = 1.01 tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate) if err != nil { return resp, err } if dateIndex == -1 { resp.Summary.ThresholdRatio = thresholdRatio resp.Groups = buildLimitOpenThresholdGroups(nil, nil) return resp, nil } previousTradeDate := "" if dateIndex > 0 { previousTradeDate = tradeDates[dateIndex-1] } resp.PreviousTradeDate = previousTradeDate stocks, err := s.loadLimitOpenThresholdStocks(tradeDate, previousTradeDate) if err != nil { return resp, err } withinThresholdStocks := make([]model.LimitOpenThresholdStock, 0) atOrAboveThresholdStocks := make([]model.LimitOpenThresholdStock, 0) for _, stock := range stocks { // 前收价格有效时才计算阈值价格和开盘/前收比值,避免除零和误导性数据。 if stock.HasPreviousClose && stock.PreviousClose > 0 { stock.OpenThresholdPrice = stock.PreviousClose * thresholdRatio } if stock.HasOpen && stock.HasPreviousClose && stock.PreviousClose > 0 { stock.OpenPreviousCloseRatio = stock.Open / stock.PreviousClose } if stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.OpenThresholdPrice { withinThresholdStocks = append(withinThresholdStocks, stock) continue } atOrAboveThresholdStocks = append(atOrAboveThresholdStocks, stock) } resp.Summary = model.LimitOpenThresholdSummary{ LimitCount: len(stocks), OpenWithinThresholdCount: len(withinThresholdStocks), OpenAtOrAboveThresholdCount: len(atOrAboveThresholdStocks), ThresholdRatio: thresholdRatio, } resp.Groups = buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks) return resp, nil } // loadLimitOpenThresholdStocks 查询昨日涨停且今日继续涨停的股票,并补齐今日开盘和前收。 func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTradeDate string) ([]model.LimitOpenThresholdStock, error) { var stocks []model.LimitOpenThresholdStock 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, (pl.calc_close IS NOT NULL OR pl.tushare_close IS NOT NULL OR pd.close IS NOT NULL) AS has_previous_close, COALESCE(pl.calc_close, pl.tushare_close, pd.close, 0) AS previous_close, 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 l.ts_code = d.ts_code AND d.trade_date = l.trade_date LEFT JOIN stock_daily pd ON l.ts_code = pd.ts_code AND pd.trade_date = ? INNER JOIN stock_limit_combined_data pl ON 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, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error return stocks, err } // buildLimitOpenThresholdGroups 构造低于阈值和达到/超过阈值两个固定分组。 func buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks []model.LimitOpenThresholdStock) []model.LimitOpenThresholdGroup { if withinThresholdStocks == nil { withinThresholdStocks = []model.LimitOpenThresholdStock{} } if atOrAboveThresholdStocks == nil { atOrAboveThresholdStocks = []model.LimitOpenThresholdStock{} } sortLimitOpenThresholdStocks(withinThresholdStocks) sortLimitOpenThresholdStocks(atOrAboveThresholdStocks) return []model.LimitOpenThresholdGroup{ { Key: "open_within_previous_close_101", Label: "开盘价低于前收101%", Count: len(withinThresholdStocks), Stocks: withinThresholdStocks, }, { Key: "open_at_or_above_previous_close_101", Label: "开盘价大于等于前收101%", Count: len(atOrAboveThresholdStocks), Stocks: atOrAboveThresholdStocks, }, } } // sortLimitOpenThresholdStocks 按涨跌幅降序、股票代码升序稳定展示分组内股票。 func sortLimitOpenThresholdStocks(stocks []model.LimitOpenThresholdStock) { 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 }) }