621 lines
18 KiB
Go
621 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
"level_go/internal/model"
|
|
)
|
|
|
|
type LimitBoardService struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
const stockLimitCombinedTable = "stock_limit_combined_data"
|
|
|
|
type limitBoardHistoryRow struct {
|
|
TsCode string `gorm:"column:ts_code"`
|
|
TradeDate string `gorm:"column:trade_date"`
|
|
}
|
|
|
|
func boardLabel(boardCount int) string {
|
|
switch boardCount {
|
|
case 1:
|
|
return "首板"
|
|
case 2:
|
|
return "二板"
|
|
case 3:
|
|
return "三板"
|
|
case 4:
|
|
return "四板"
|
|
case 5:
|
|
return "五板"
|
|
case 6:
|
|
return "六板"
|
|
case 7:
|
|
return "七板"
|
|
case 8:
|
|
return "八板"
|
|
case 9:
|
|
return "九板"
|
|
default:
|
|
return fmt.Sprintf("%d板", boardCount)
|
|
}
|
|
}
|
|
|
|
func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) {
|
|
resp := model.LimitBoardResponse{TradeDate: tradeDate}
|
|
|
|
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if dateIndex == -1 {
|
|
return resp, nil
|
|
}
|
|
|
|
resp.RecentMaxBoardStats, err = s.buildRecentMaxBoardStats(tradeDates, dateIndex)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
limitStocks, err := s.loadCurrentLimitStocks(tradeDate)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if len(limitStocks) == 0 {
|
|
return resp, nil
|
|
}
|
|
|
|
historyMap, err := s.loadLimitHistory(limitStocks, tradeDate)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
groupMap, maxBoardCount := buildLimitBoardGroups(limitStocks, tradeDates, dateIndex, historyMap)
|
|
resp.Groups = sortLimitBoardGroups(groupMap)
|
|
resp.Summary = model.LimitBoardSummary{
|
|
MaxBoardCount: maxBoardCount,
|
|
LimitCount: len(limitStocks),
|
|
GroupCount: len(resp.Groups),
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
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 {
|
|
if stock.IsFirstToSecondBoard {
|
|
stock.HighlightLabel = "首板进二板"
|
|
firstToSecondBoardCount++
|
|
}
|
|
isOpenLower := previousTradeDate != "" && stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.PreviousClose
|
|
if isOpenLower && stock.IsFirstToSecondBoard && isBeforeEleven(stock.AkFirstLimitTime) {
|
|
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
|
|
}
|
|
|
|
func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model.LimitOpenThresholdResponse, error) {
|
|
resp := model.LimitOpenThresholdResponse{TradeDate: tradeDate}
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) {
|
|
startIndex := 0
|
|
if dateIndex >= 29 {
|
|
startIndex = dateIndex - 29
|
|
}
|
|
windowDates := tradeDates[startIndex : dateIndex+1]
|
|
if len(windowDates) == 0 {
|
|
return []model.LimitBoardDailyMax{}, nil
|
|
}
|
|
|
|
var history []limitBoardHistoryRow
|
|
if err := s.DB.Table(stockLimitCombinedTable).
|
|
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
|
Where("trade_date BETWEEN ? AND ? AND final_status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
|
|
Order("ts_code ASC, trade_date ASC").
|
|
Scan(&history).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dateByCode := make(map[string]map[string]struct{})
|
|
for _, item := range history {
|
|
if _, ok := dateByCode[item.TsCode]; !ok {
|
|
dateByCode[item.TsCode] = make(map[string]struct{})
|
|
}
|
|
dateByCode[item.TsCode][item.TradeDate] = struct{}{}
|
|
}
|
|
|
|
maxBoardByDate := make(map[string]int, len(windowDates))
|
|
for _, tradeDate := range windowDates {
|
|
maxBoardByDate[tradeDate] = 0
|
|
}
|
|
|
|
for _, limitDates := range dateByCode {
|
|
for idx, tradeDate := range windowDates {
|
|
if _, ok := limitDates[tradeDate]; !ok {
|
|
continue
|
|
}
|
|
|
|
boardCount := 0
|
|
for j := idx; j >= 0; j-- {
|
|
if _, ok := limitDates[windowDates[j]]; !ok {
|
|
break
|
|
}
|
|
boardCount++
|
|
}
|
|
|
|
if boardCount > maxBoardByDate[tradeDate] {
|
|
maxBoardByDate[tradeDate] = boardCount
|
|
}
|
|
}
|
|
}
|
|
|
|
stats := make([]model.LimitBoardDailyMax, 0, len(windowDates))
|
|
for _, tradeDate := range windowDates {
|
|
stats = append(stats, model.LimitBoardDailyMax{
|
|
TradeDate: tradeDate,
|
|
MaxBoardCount: maxBoardByDate[tradeDate],
|
|
})
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) {
|
|
var tradeDates []string
|
|
if err := s.DB.Table("trade_calendar").
|
|
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
|
|
Where("exchange = ? AND is_open = ? AND cal_date <= ?", "SSE", 1, tradeDate).
|
|
Order("cal_date ASC").
|
|
Scan(&tradeDates).Error; err != nil {
|
|
return nil, -1, err
|
|
}
|
|
|
|
dateIndex := -1
|
|
for i, d := range tradeDates {
|
|
if d == tradeDate {
|
|
dateIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
return tradeDates, dateIndex, nil
|
|
}
|
|
|
|
func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) {
|
|
var stocks []model.LimitBoardStock
|
|
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,
|
|
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS 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 COLLATE utf8mb4_0900_ai_ci = d.ts_code AND d.trade_date = l.trade_date
|
|
WHERE l.trade_date = ? AND l.final_status = 1
|
|
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
|
|
|
err := s.DB.Raw(query, tradeDate).Scan(&stocks).Error
|
|
return stocks, err
|
|
}
|
|
|
|
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 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 l.ts_code COLLATE utf8mb4_0900_ai_ci = d.ts_code AND d.trade_date = l.trade_date
|
|
LEFT JOIN stock_daily pd
|
|
ON l.ts_code COLLATE utf8mb4_0900_ai_ci = 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, previousTwoTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
|
|
return stocks, err
|
|
}
|
|
|
|
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 COLLATE utf8mb4_0900_ai_ci = d.ts_code AND d.trade_date = l.trade_date
|
|
LEFT JOIN stock_daily pd
|
|
ON l.ts_code COLLATE utf8mb4_0900_ai_ci = 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
|
|
}
|
|
|
|
func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) {
|
|
codes := make([]string, 0, len(stocks))
|
|
for _, stock := range stocks {
|
|
codes = append(codes, stock.TsCode)
|
|
}
|
|
|
|
var history []limitBoardHistoryRow
|
|
if err := s.DB.Table(stockLimitCombinedTable).
|
|
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
|
Where("ts_code IN ? AND trade_date <= ? AND final_status = ?", codes, tradeDate, 1).
|
|
Scan(&history).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
historyMap := make(map[string]map[string]struct{}, len(codes))
|
|
for _, item := range history {
|
|
if _, ok := historyMap[item.TsCode]; !ok {
|
|
historyMap[item.TsCode] = make(map[string]struct{})
|
|
}
|
|
historyMap[item.TsCode][item.TradeDate] = struct{}{}
|
|
}
|
|
|
|
return historyMap, nil
|
|
}
|
|
|
|
func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, dateIndex int, historyMap map[string]map[string]struct{}) (map[int][]model.LimitBoardStock, int) {
|
|
groupMap := make(map[int][]model.LimitBoardStock)
|
|
maxBoardCount := 0
|
|
|
|
for i := range stocks {
|
|
boardCount := calcBoardCount(tradeDates, dateIndex, historyMap[stocks[i].TsCode])
|
|
stocks[i].BoardCount = boardCount
|
|
stocks[i].BoardLabel = boardLabel(boardCount)
|
|
stocks[i].DisplayStatus = boardLabel(boardCount)
|
|
|
|
if boardCount > maxBoardCount {
|
|
maxBoardCount = boardCount
|
|
}
|
|
groupMap[boardCount] = append(groupMap[boardCount], stocks[i])
|
|
}
|
|
|
|
return groupMap, maxBoardCount
|
|
}
|
|
|
|
func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int {
|
|
boardCount := 0
|
|
for i := dateIndex; i >= 0; i-- {
|
|
if _, ok := limitDates[tradeDates[i]]; !ok {
|
|
break
|
|
}
|
|
boardCount++
|
|
}
|
|
return boardCount
|
|
}
|
|
|
|
func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup {
|
|
boardCounts := make([]int, 0, len(groupMap))
|
|
for boardCount := range groupMap {
|
|
boardCounts = append(boardCounts, boardCount)
|
|
}
|
|
sort.Sort(sort.Reverse(sort.IntSlice(boardCounts)))
|
|
|
|
groups := make([]model.LimitBoardGroup, 0, len(boardCounts))
|
|
for _, boardCount := range boardCounts {
|
|
stocks := groupMap[boardCount]
|
|
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
|
|
})
|
|
|
|
groups = append(groups, model.LimitBoardGroup{
|
|
BoardCount: boardCount,
|
|
Label: boardLabel(boardCount),
|
|
Count: len(stocks),
|
|
Stocks: stocks,
|
|
})
|
|
}
|
|
|
|
return groups
|
|
}
|
|
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|