150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"level_go/internal/dbcompat"
|
|
"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 ` + 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, 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
|
|
})
|
|
}
|