1、新增二板炸板情况统计;

2、更新项目结构;
This commit is contained in:
2026-05-09 16:59:45 +08:00
parent 9ea4b61ee3
commit d5c053e59d
12 changed files with 1312 additions and 562 deletions
@@ -0,0 +1,125 @@
package api
import (
"encoding/csv"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"level_go/internal/model"
)
func (h *LimitBoardHandler) DownloadBreakNextPerformanceCSV(c *gin.Context) {
req, err := parseLimitBreakNextPerformanceRequest(c, false)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rows, _, err := h.Service.ListLimitBreakNextPerformanceStocks(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
filename := fmt.Sprintf("limit_break_next_performance_%s_%s_daily_detail.csv",
strings.ReplaceAll(req.TradeStartDate, "-", ""),
strings.ReplaceAll(req.TradeEndDate, "-", ""),
)
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
c.Status(http.StatusOK)
if _, err := c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
return
}
writer := csv.NewWriter(c.Writer)
defer writer.Flush()
_ = writer.Write(breakNextPerformanceCSVHeaders())
for _, row := range rows {
if err := writer.Write(breakNextPerformanceCSVRecord(row)); err != nil {
return
}
}
}
func breakNextPerformanceCSVHeaders() []string {
return []string{
"T日",
"股票代码",
"股票简称",
"T涨跌幅",
"T开盘<1%",
"tushare炸板时间",
"11点前炸板",
"T+1盘中可盈利",
"T+1最高获利",
"T+1开盘涨幅",
"T+1收盘涨跌",
"市场",
"行业",
"T开盘",
"T开盘涨幅",
"T最高",
"T收盘",
"tushare是否炸板",
"T+1开盘",
"T+1最高",
"T+1最低",
"T+1收盘",
"T+1最大亏损",
}
}
func breakNextPerformanceCSVRecord(row model.LimitBreakNextPerformanceStock) []string {
return []string{
row.TradeDate,
row.TsCode,
row.Name,
breakNextPerformanceFormatFloatPtr(row.TPctChg),
breakNextPerformanceBoolLabel(row.TOpenGainLt101),
row.FirstTime,
breakNextPerformanceBoolLabel(row.BreakTimeBefore1100),
breakNextPerformanceBoolPtrLabel(row.NextIntradayProfitable),
breakNextPerformanceFormatFloatPtr(row.NextHighProfitPct),
breakNextPerformanceFormatFloatPtr(row.NextOpenPctChg),
breakNextPerformanceFormatFloatPtr(row.NextClosePctChg),
row.Market,
row.Industry,
breakNextPerformanceFormatFloatPtr(row.TOpen),
breakNextPerformanceFormatFloatPtr(row.TOpenPctChg),
breakNextPerformanceFormatFloatPtr(row.THigh),
breakNextPerformanceFormatFloatPtr(row.TClose),
breakNextPerformanceBoolLabel(row.HasTushareBreakData),
breakNextPerformanceFormatFloatPtr(row.NextOpen),
breakNextPerformanceFormatFloatPtr(row.NextHigh),
breakNextPerformanceFormatFloatPtr(row.NextLow),
breakNextPerformanceFormatFloatPtr(row.NextClose),
breakNextPerformanceFormatFloatPtr(row.NextLowLossPct),
}
}
func breakNextPerformanceFormatFloatPtr(value *float64) string {
if value == nil {
return ""
}
return strconv.FormatFloat(*value, 'f', -1, 64)
}
func breakNextPerformanceBoolLabel(value bool) string {
if value {
return "是"
}
return "否"
}
func breakNextPerformanceBoolPtrLabel(value *bool) string {
if value == nil {
return "无次日数据"
}
return breakNextPerformanceBoolLabel(*value)
}
+87
View File
@@ -1,7 +1,10 @@
package api
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"level_go/internal/model"
@@ -48,6 +51,90 @@ func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) {
req, err := parseLimitBreakNextPerformanceRequest(c, true)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.Service.GetLimitBreakNextPerformance(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) {
req := model.LimitBreakNextPerformanceRequest{
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
Page: 1,
PageSize: 20,
}
if req.TradeStartDate == "" {
return req, errors.New("tradeStartDate 参数不能为空")
}
if req.TradeEndDate == "" {
return req, errors.New("tradeEndDate 参数不能为空")
}
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
return req, err
}
if includePagination {
pageText := strings.TrimSpace(c.DefaultQuery("page", "1"))
page, err := strconv.Atoi(pageText)
if err != nil || page <= 0 {
return req, errors.New("page 必须为正整数")
}
req.Page = page
pageSizeText := strings.TrimSpace(c.DefaultQuery("pageSize", "20"))
pageSize, err := strconv.Atoi(pageSizeText)
if err != nil || pageSize <= 0 {
return req, errors.New("pageSize 必须为正整数")
}
req.PageSize = pageSize
}
tOpenGainLt101, err := parseOptionalBoolQuery(c, "tOpenGainLt101", "tOpenLessThan101", "t_open_gain_lt_101")
if err != nil {
return req, errors.New("tOpenGainLt101 必须为布尔值")
}
req.TOpenGainLt101 = tOpenGainLt101
breakTimeBefore1100, err := parseOptionalBoolQuery(c, "breakTimeBefore1100", "tBreakTimeBefore1100", "break_time_before_1100")
if err != nil {
return req, errors.New("breakTimeBefore1100 必须为布尔值")
}
req.BreakTimeBefore1100 = breakTimeBefore1100
return req, nil
}
func parseOptionalBoolQuery(c *gin.Context, keys ...string) (*bool, error) {
for _, key := range keys {
valueText, exists := c.GetQuery(key)
if !exists {
continue
}
valueText = strings.TrimSpace(valueText)
if valueText == "" {
continue
}
value, err := strconv.ParseBool(valueText)
if err != nil {
return nil, err
}
return &value, nil
}
return nil, nil
}
func (h *LimitBoardHandler) GetOpenThresholdGroups(c *gin.Context) {
var req model.LimitBoardRequest
if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" {
+52
View File
@@ -4,6 +4,15 @@ type LimitBoardRequest struct {
TradeDate string `form:"trade_date" json:"trade_date"`
}
type LimitBreakNextPerformanceRequest struct {
TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"`
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"`
Page int `form:"page" json:"page"`
PageSize int `form:"pageSize" json:"page_size"`
TOpenGainLt101 *bool `json:"t_open_gain_lt_101,omitempty"`
BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"`
}
type LimitBoardStock struct {
Symbol string `json:"symbol"`
TsCode string `json:"ts_code"`
@@ -92,6 +101,33 @@ type LimitOpenThresholdStock struct {
Vol *int64 `json:"vol"`
}
type LimitBreakNextPerformanceStock struct {
TradeDate string `json:"trade_date"`
Symbol string `json:"symbol"`
TsCode string `json:"ts_code"`
Name string `json:"name"`
Market string `json:"market"`
Industry string `json:"industry"`
TOpen *float64 `json:"t_open"`
TOpenPctChg *float64 `json:"t_open_pct_chg"`
TOpenGainLt101 bool `json:"t_open_gain_lt_101"`
THigh *float64 `json:"t_high"`
TClose *float64 `json:"t_close"`
TPctChg *float64 `json:"t_pct_chg"`
FirstTime string `json:"first_time"`
BreakTimeBefore1100 bool `json:"break_time_before_1100"`
HasTushareBreakData bool `json:"has_tushare_break_data"`
NextOpen *float64 `json:"next_open"`
NextHigh *float64 `json:"next_high"`
NextLow *float64 `json:"next_low"`
NextClose *float64 `json:"next_close"`
NextOpenPctChg *float64 `json:"next_open_pct_chg"`
NextIntradayProfitable *bool `json:"next_intraday_profitable"`
NextHighProfitPct *float64 `json:"next_high_profit_pct"`
NextLowLossPct *float64 `json:"next_low_loss_pct"`
NextClosePctChg *float64 `json:"next_close_pct_chg"`
}
type LimitOpenCompareGroup struct {
Key string `json:"key"`
Label string `json:"label"`
@@ -121,6 +157,11 @@ type LimitOpenThresholdSummary struct {
ThresholdRatio float64 `json:"threshold_ratio"`
}
type LimitBreakNextPerformanceSummary struct {
BreakCount int `json:"break_count"`
NextTradeDataCount int `json:"next_trade_data_count"`
}
type LimitOpenCompareResponse struct {
TradeDate string `json:"trade_date"`
PreviousTradeDate string `json:"previous_trade_date"`
@@ -129,6 +170,17 @@ type LimitOpenCompareResponse struct {
Groups []LimitOpenCompareGroup `json:"groups"`
}
type LimitBreakNextPerformanceResponse struct {
TradeStartDate string `json:"trade_start_date"`
TradeEndDate string `json:"trade_end_date"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
Summary LimitBreakNextPerformanceSummary `json:"summary"`
List []LimitBreakNextPerformanceStock `json:"list"`
}
type LimitOpenThresholdResponse struct {
TradeDate string `json:"trade_date"`
PreviousTradeDate string `json:"previous_trade_date"`
+2
View File
@@ -34,11 +34,13 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit)
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
}
download := v1.Group("/download")
{
download.GET("/limit-board/second-board-profit-csv", limitBoardApi.DownloadSecondBoardProfitCSV)
download.GET("/limit-board/break-next-performance-csv", limitBoardApi.DownloadBreakNextPerformanceCSV)
}
}
return r
@@ -0,0 +1,241 @@
package service
import (
"math"
"strings"
"level_go/internal/model"
)
type limitBreakNextPerformanceRow struct {
TradeDate string `gorm:"column:trade_date"`
PreviousTradeDate string `gorm:"column:previous_trade_date"`
PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"`
NextTradeDate string `gorm:"column:next_trade_date"`
Symbol string `gorm:"column:symbol"`
TsCode string `gorm:"column:ts_code"`
Name string `gorm:"column:name"`
Market string `gorm:"column:market"`
Industry string `gorm:"column:industry"`
PreviousClose *float64 `gorm:"column:previous_close"`
TPreClose *float64 `gorm:"column:t_pre_close"`
TOpen *float64 `gorm:"column:t_open"`
THigh *float64 `gorm:"column:t_high"`
TClose *float64 `gorm:"column:t_close"`
TPctChg *float64 `gorm:"column:t_pct_chg"`
FirstTime *string `gorm:"column:first_time"`
HasTushareBreakData bool `gorm:"column:has_tushare_break_data"`
NextOpen *float64 `gorm:"column:next_open"`
NextHigh *float64 `gorm:"column:next_high"`
NextLow *float64 `gorm:"column:next_low"`
NextClose *float64 `gorm:"column:next_close"`
}
func (s *LimitBoardService) GetLimitBreakNextPerformance(req model.LimitBreakNextPerformanceRequest) (model.LimitBreakNextPerformanceResponse, error) {
resp := model.LimitBreakNextPerformanceResponse{
TradeStartDate: req.TradeStartDate,
TradeEndDate: req.TradeEndDate,
Page: req.Page,
PageSize: req.PageSize,
List: []model.LimitBreakNextPerformanceStock{},
}
items, nextTradeDataCount, err := s.ListLimitBreakNextPerformanceStocks(req)
if err != nil {
return resp, err
}
resp.Total = len(items)
if resp.Total > 0 {
resp.TotalPages = (resp.Total + req.PageSize - 1) / req.PageSize
}
resp.Summary = model.LimitBreakNextPerformanceSummary{
BreakCount: resp.Total,
NextTradeDataCount: nextTradeDataCount,
}
start := (req.Page - 1) * req.PageSize
if start > resp.Total {
start = resp.Total
}
end := start + req.PageSize
if end > resp.Total {
end = resp.Total
}
resp.List = items[start:end]
return resp, nil
}
func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitBreakNextPerformanceRequest) ([]model.LimitBreakNextPerformanceStock, int, error) {
rows, err := s.loadLimitBreakNextPerformanceRows(req.TradeStartDate, req.TradeEndDate)
if err != nil {
return nil, 0, err
}
items := make([]model.LimitBreakNextPerformanceStock, 0, len(rows))
nextTradeDataCount := 0
for _, row := range rows {
item := buildLimitBreakNextPerformanceStock(row)
if req.TOpenGainLt101 != nil && item.TOpenGainLt101 != *req.TOpenGainLt101 {
continue
}
if req.BreakTimeBefore1100 != nil && item.BreakTimeBefore1100 != *req.BreakTimeBefore1100 {
continue
}
if item.NextOpen != nil || item.NextHigh != nil || item.NextLow != nil || item.NextClose != nil {
nextTradeDataCount++
}
items = append(items, item)
}
return items, nextTradeDataCount, nil
}
func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) {
query := `
WITH open_calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date, 1) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(cal_date, 1) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
),
candidate AS (
SELECT
DATE_FORMAT(c.trade_date, '%Y-%m-%d') AS trade_date,
DATE_FORMAT(c.previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
DATE_FORMAT(c.previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date,
COALESCE(DATE_FORMAT(c.next_trade_date, '%Y-%m-%d'), '') AS next_trade_date,
l.symbol,
l.ts_code,
l.name,
COALESCE(NULLIF(l.market, ''), '') AS market,
COALESCE(NULLIF(l.tushare_industry, ''), NULLIF(sb.industry, ''), '') AS industry,
COALESCE(d.pre_close, l.calc_pre_close, pd.close, pl.calc_close, pl.tushare_close) AS previous_close,
COALESCE(d.pre_close, l.calc_pre_close, pd.close, pl.calc_close, pl.tushare_close) AS t_pre_close,
d.open AS t_open,
d.high AS t_high,
COALESCE(l.calc_close, l.tushare_close, d.close) AS t_close,
COALESCE(l.tushare_pct_chg, d.pct_chg) AS t_pct_chg,
CASE
WHEN l.tushare_limit_type = 'Z'
THEN NULLIF(NULLIF(TRIM(l.tushare_first_time), ''), 'None')
ELSE NULL
END AS first_time,
(l.tushare_limit_type = 'Z') AS has_tushare_break_data,
nd.open AS next_open,
nd.high AS next_high,
nd.low AS next_low,
nd.close AS next_close
FROM open_calendar c
INNER JOIN stock_limit_combined_data l
ON l.trade_date = c.trade_date AND l.final_status = 2
INNER JOIN stock_limit_combined_data pl
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
LEFT JOIN stock_limit_combined_data ppl
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date
LEFT JOIN stock_daily pd
ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date
LEFT JOIN stock_daily nd
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date
LEFT JOIN stock_basic sb
ON sb.ts_code = l.ts_code COLLATE utf8mb4_unicode_ci
WHERE c.trade_date BETWEEN ? AND ?
AND c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL
)
SELECT *
FROM candidate
WHERE previous_close IS NOT NULL
AND t_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC`
var rows []limitBreakNextPerformanceRow
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
return rows, err
}
func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model.LimitBreakNextPerformanceStock {
item := model.LimitBreakNextPerformanceStock{
TradeDate: row.TradeDate,
Symbol: row.Symbol,
TsCode: row.TsCode,
Name: row.Name,
Market: row.Market,
Industry: row.Industry,
TOpen: row.TOpen,
THigh: row.THigh,
TClose: row.TClose,
TPctChg: row.TPctChg,
HasTushareBreakData: row.HasTushareBreakData,
NextOpen: row.NextOpen,
NextHigh: row.NextHigh,
NextLow: row.NextLow,
NextClose: row.NextClose,
}
if row.FirstTime != nil {
item.FirstTime = formatLimitTime(*row.FirstTime)
}
item.BreakTimeBefore1100 = isLimitBreakTimeBeforeEleven(item.FirstTime)
if row.TPreClose != nil && *row.TPreClose > 0 {
item.TOpenPctChg = pctChangePtr(row.TOpen, *row.TPreClose)
if row.TOpen != nil {
item.TOpenGainLt101 = *row.TOpen < *row.TPreClose*1.01
}
}
if row.TClose != nil && *row.TClose > 0 {
item.NextOpenPctChg = pctChangePtr(row.NextOpen, *row.TClose)
item.NextHighProfitPct = pctChangePtr(row.NextHigh, *row.TClose)
item.NextLowLossPct = pctChangePtr(row.NextLow, *row.TClose)
item.NextClosePctChg = pctChangePtr(row.NextClose, *row.TClose)
if row.NextHigh != nil && row.THigh != nil {
nextIntradayProfitable := *row.NextHigh >= *row.THigh
item.NextIntradayProfitable = &nextIntradayProfitable
}
}
return item
}
func pctChangePtr(value *float64, base float64) *float64 {
if value == nil || base == 0 {
return nil
}
result := roundLimitBreakPct((*value - base) / base * 100)
return &result
}
func roundLimitBreakPct(value float64) float64 {
return math.Round(value*100) / 100
}
func formatLimitTime(value string) string {
timeText := strings.TrimSpace(value)
if timeText == "" || strings.EqualFold(timeText, "none") || strings.EqualFold(timeText, "null") {
return ""
}
if strings.Contains(timeText, ":") {
return timeText
}
switch len(timeText) {
case 4:
timeText += "00"
case 5:
timeText = "0" + timeText
case 6:
default:
return value
}
return timeText[0:2] + ":" + timeText[2:4] + ":" + timeText[4:6]
}
func isLimitBreakTimeBeforeEleven(limitTime string) bool {
if strings.TrimSpace(limitTime) == "" {
return true
}
return isBeforeEleven(&limitTime)
}
@@ -0,0 +1,61 @@
package service
import "testing"
func TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh(t *testing.T) {
tClose := 9.50
tHigh := 10.00
tests := []struct {
name string
nextHigh *float64
want *bool
}{
{
name: "next high below T high cannot cover T high buy loss",
nextHigh: float64Ptr(9.99),
want: boolPtr(false),
},
{
name: "next high equal T high covers T high buy loss",
nextHigh: float64Ptr(10.00),
want: boolPtr(true),
},
{
name: "missing next high returns nil",
nextHigh: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
item := buildLimitBreakNextPerformanceStock(limitBreakNextPerformanceRow{
TClose: &tClose,
THigh: &tHigh,
NextHigh: tt.nextHigh,
})
if tt.want == nil {
if item.NextIntradayProfitable != nil {
t.Fatalf("NextIntradayProfitable = %v, want nil", *item.NextIntradayProfitable)
}
return
}
if item.NextIntradayProfitable == nil {
t.Fatalf("NextIntradayProfitable = nil, want %v", *tt.want)
}
if *item.NextIntradayProfitable != *tt.want {
t.Fatalf("NextIntradayProfitable = %v, want %v", *item.NextIntradayProfitable, *tt.want)
}
})
}
}
func float64Ptr(value float64) *float64 {
return &value
}
func boolPtr(value bool) *bool {
return &value
}
@@ -0,0 +1,208 @@
package service
import (
"fmt"
"sort"
"strconv"
"strings"
"level_go/internal/model"
)
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 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) 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 = 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, previousTwoTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
return stocks, err
}
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 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
})
}
@@ -0,0 +1,142 @@
package service
import (
"sort"
"level_go/internal/model"
)
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 (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
}
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 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
})
}
@@ -0,0 +1,231 @@
package service
import (
"sort"
"level_go/internal/model"
)
type limitBoardHistoryRow struct {
TsCode string `gorm:"column:ts_code"`
TradeDate string `gorm:"column:trade_date"`
}
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) 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) 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 = 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) 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
}
-561
View File
@@ -2,12 +2,8 @@ package service
import (
"fmt"
"sort"
"strconv"
"strings"
"gorm.io/gorm"
"level_go/internal/model"
)
type LimitBoardService struct {
@@ -16,11 +12,6 @@ type LimitBoardService struct {
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:
@@ -46,272 +37,6 @@ func boardLabel(boardCount int) string {
}
}
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").
@@ -332,289 +57,3 @@ func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int,
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 = 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 = 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, 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 = 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
}
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
})
}