修改二板涨停情况查询,新增按条件筛选

This commit is contained in:
2026-05-06 18:11:30 +08:00
parent f1734a68ae
commit cf71dd2207
9 changed files with 560 additions and 121 deletions
+380 -64
View File
@@ -28,10 +28,10 @@ type secondProfitCSVRow struct {
Name string `gorm:"column:name"`
Market string `gorm:"column:market"`
Industry string `gorm:"column:industry"`
Open float64 `gorm:"column:open"`
Close float64 `gorm:"column:close_price"`
Open sql.NullFloat64 `gorm:"column:open"`
Close sql.NullFloat64 `gorm:"column:close_price"`
LimitPrice float64 `gorm:"column:limit_price"`
PreviousClose float64 `gorm:"column:previous_close"`
PreviousClose sql.NullFloat64 `gorm:"column:previous_close"`
FirstLimitTime sql.NullString `gorm:"column:tushare_first_time"`
PctChg sql.NullFloat64 `gorm:"column:pct_chg"`
Amount sql.NullFloat64 `gorm:"column:tushare_amount"`
@@ -44,30 +44,128 @@ type secondProfitCSVRow struct {
NextLow sql.NullFloat64 `gorm:"column:next_low"`
NextClose sql.NullFloat64 `gorm:"column:next_close"`
OpenPreviousCloseRatio float64
OpenPctChg sql.NullFloat64
NextOpenReturnPct sql.NullFloat64
NextHighReturnPct sql.NullFloat64
NextLowReturnPct sql.NullFloat64
NextCloseReturnPct sql.NullFloat64
NextOpenProfitable string
NextCloseProfitable string
NextIntradayProfitable string
}
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
startDate := c.DefaultQuery("start_date", defaultSecondProfitStartDate)
endDate := c.DefaultQuery("end_date", defaultSecondProfitEndDate)
if err := validateSecondProfitDateRange(startDate, endDate); err != nil {
type secondProfitQuery struct {
TradeStartDate string
TradeEndDate string
LimitUpTimeFilterType1 string
LimitUpTimeFilterValue1 string
LimitUpTimeFilterType2 string
LimitUpTimeFilterValue2 string
OpenPctChgFilterType1 string
OpenPctChgFilterValue1 string
OpenPctChgFilterType2 string
OpenPctChgFilterValue2 string
LimitUpTimeRule secondProfitCompareFilter
OpenPctChgRule secondProfitCompareFilter
Page int
PageSize int
}
type secondProfitCompareFilter struct {
Conditions []secondProfitCompareCondition
}
type secondProfitCompareCondition struct {
Op string
Value float64
}
type secondProfitListItem struct {
TradeDate string `json:"trade_date"`
TsCode string `json:"ts_code"`
Name string `json:"name"`
Market string `json:"market"`
Industry string `json:"industry"`
NextOpenProfitable string `json:"next_open_profitable"`
NextCloseProfitable string `json:"next_close_profitable"`
NextIntradayProfitable string `json:"next_intraday_profitable"`
NextHighProfitPct *float64 `json:"next_high_profit_pct"`
NextLowProfitPct *float64 `json:"next_low_profit_pct"`
BreakLimitCount *int64 `json:"break_limit_count"`
TurnoverRatePct *float64 `json:"turnover_rate_pct"`
FirstLimitTime string `json:"first_limit_time"`
SealAmount *float64 `json:"seal_amount"`
Amount *float64 `json:"amount"`
}
func (h *LimitBoardHandler) ListSecondBoardProfit(c *gin.Context) {
req, err := parseSecondProfitQuery(c, true)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rows, err := h.loadSecondProfitCSVRows(startDate, endDate)
rows, err := h.loadSecondProfitRows(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total := len(rows)
start := (req.Page - 1) * req.PageSize
if start > total {
start = total
}
end := start + req.PageSize
if end > total {
end = total
}
items := make([]secondProfitListItem, 0, end-start)
for _, row := range rows[start:end] {
items = append(items, secondProfitBuildItem(row))
}
totalPages := 0
if total > 0 {
totalPages = (total + req.PageSize - 1) / req.PageSize
}
c.JSON(http.StatusOK, gin.H{
"trade_start_date": req.TradeStartDate,
"trade_end_date": req.TradeEndDate,
"limit_up_time_filter_type1": req.LimitUpTimeFilterType1,
"limit_up_time_filter_value1": req.LimitUpTimeFilterValue1,
"limit_up_time_filter_type2": req.LimitUpTimeFilterType2,
"limit_up_time_filter_value2": req.LimitUpTimeFilterValue2,
"open_pct_chg_filter_type1": req.OpenPctChgFilterType1,
"open_pct_chg_filter_value1": req.OpenPctChgFilterValue1,
"open_pct_chg_filter_type2": req.OpenPctChgFilterType2,
"open_pct_chg_filter_value2": req.OpenPctChgFilterValue2,
"page": req.Page,
"page_size": req.PageSize,
"total": total,
"total_pages": totalPages,
"list": items,
})
}
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
req, err := parseSecondProfitQuery(c, false)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rows, err := h.loadSecondProfitRows(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
filename := fmt.Sprintf("limit_second_board_profit_%s_%s_daily_detail.csv",
strings.ReplaceAll(startDate, "-", ""),
strings.ReplaceAll(endDate, "-", ""),
strings.ReplaceAll(req.TradeStartDate, "-", ""),
strings.ReplaceAll(req.TradeEndDate, "-", ""),
)
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
@@ -88,22 +186,121 @@ func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
}
}
func parseSecondProfitQuery(c *gin.Context, includePagination bool) (secondProfitQuery, error) {
startDate := c.DefaultQuery("tradeStartDate", c.DefaultQuery("start_date", defaultSecondProfitStartDate))
endDate := c.DefaultQuery("tradeEndDate", c.DefaultQuery("end_date", defaultSecondProfitEndDate))
limitUpTimeType1, limitUpTimeValue1, limitUpTimeType2, limitUpTimeValue2 := secondProfitFilterParts(c, "limitUpTimeFilter")
openPctChgType1, openPctChgValue1, openPctChgType2, openPctChgValue2 := secondProfitFilterParts(c, "openPctChgFilter")
req := secondProfitQuery{
TradeStartDate: startDate,
TradeEndDate: endDate,
LimitUpTimeFilterType1: limitUpTimeType1,
LimitUpTimeFilterValue1: limitUpTimeValue1,
LimitUpTimeFilterType2: limitUpTimeType2,
LimitUpTimeFilterValue2: limitUpTimeValue2,
OpenPctChgFilterType1: openPctChgType1,
OpenPctChgFilterValue1: openPctChgValue1,
OpenPctChgFilterType2: openPctChgType2,
OpenPctChgFilterValue2: openPctChgValue2,
Page: 1,
PageSize: 10,
}
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
return req, err
}
timeRule, err := parseSecondProfitCompareFilter(
req.LimitUpTimeFilterType1,
req.LimitUpTimeFilterValue1,
req.LimitUpTimeFilterType2,
req.LimitUpTimeFilterValue2,
parseSecondProfitTimeValue,
"limitUpTimeFilter",
)
if err != nil {
return req, err
}
req.LimitUpTimeRule = timeRule
pctRule, err := parseSecondProfitCompareFilter(
req.OpenPctChgFilterType1,
req.OpenPctChgFilterValue1,
req.OpenPctChgFilterType2,
req.OpenPctChgFilterValue2,
parseSecondProfitFloatValue,
"openPctChgFilter",
)
if err != nil {
return req, err
}
req.OpenPctChgRule = pctRule
if includePagination {
pageText := strings.TrimSpace(c.DefaultQuery("page", "1"))
page, err := strconv.Atoi(pageText)
if err != nil || page <= 0 {
return req, fmt.Errorf("page 必须为正整数")
}
req.Page = page
pageSizeText := strings.TrimSpace(c.DefaultQuery("pageSize", "10"))
pageSize, err := strconv.Atoi(pageSizeText)
if err != nil || pageSize <= 0 {
return req, fmt.Errorf("pageSize 必须为正整数")
}
req.PageSize = pageSize
}
return req, nil
}
func secondProfitFilterParts(c *gin.Context, base string) (string, string, string, string) {
type1 := strings.TrimSpace(c.Query(base + "Type1"))
value1 := strings.TrimSpace(c.Query(base + "Value1"))
type2 := strings.TrimSpace(c.Query(base + "Type2"))
value2 := strings.TrimSpace(c.Query(base + "Value2"))
if type1 != "" || value1 != "" || type2 != "" || value2 != "" {
return type1, value1, type2, value2
}
values := c.QueryArray(base)
if len(values) == 0 {
values = c.QueryArray(base + "[]")
}
if len(values) > 0 {
return secondProfitFilterArrayValue(values, 0),
secondProfitFilterArrayValue(values, 1),
secondProfitFilterArrayValue(values, 2),
secondProfitFilterArrayValue(values, 3)
}
return "", "", "", ""
}
func secondProfitFilterArrayValue(values []string, index int) string {
if index >= len(values) {
return ""
}
return strings.TrimSpace(values[index])
}
func validateSecondProfitDateRange(startDate, endDate string) error {
start, err := time.Parse("2006-01-02", startDate)
if err != nil {
return fmt.Errorf("start_date 格式必须为 YYYY-MM-DD")
return fmt.Errorf("tradeStartDate 格式必须为 YYYY-MM-DD")
}
end, err := time.Parse("2006-01-02", endDate)
if err != nil {
return fmt.Errorf("end_date 格式必须为 YYYY-MM-DD")
return fmt.Errorf("tradeEndDate 格式必须为 YYYY-MM-DD")
}
if end.Before(start) {
return fmt.Errorf("end_date 不能早于 start_date")
return fmt.Errorf("tradeEndDate 不能早于 tradeStartDate")
}
return nil
}
func (h *LimitBoardHandler) loadSecondProfitCSVRows(startDate, endDate string) ([]secondProfitCSVRow, error) {
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) {
query := `
WITH open_calendar AS (
SELECT
@@ -147,36 +344,49 @@ INNER JOIN stock_limit_combined_data pl
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 = l.trade_date
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 pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date
ON l.ts_code COLLATE utf8mb4_0900_ai_ci = pd.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
ON l.ts_code COLLATE utf8mb4_0900_ai_ci = nd.ts_code AND nd.trade_date = c.next_trade_date
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
AND d.open IS NOT NULL
AND COALESCE(pl.calc_close, pl.tushare_close, pd.close) > 0
AND d.open < COALESCE(pl.calc_close, pl.tushare_close, pd.close) * 1.01
ORDER BY c.trade_date ASC, l.ts_code ASC`
var rawRows []secondProfitCSVRow
if err := h.Service.DB.Raw(query, startDate, endDate).Scan(&rawRows).Error; err != nil {
if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil {
return nil, err
}
rows := make([]secondProfitCSVRow, 0, len(rawRows))
for _, row := range rawRows {
if !isSecondProfitBeforeEleven(row.FirstLimitTime.String) {
if req.LimitUpTimeRule.Enabled() {
limitTimeValue, ok := secondProfitTimeSeconds(row.FirstLimitTime.String)
if !ok || !req.LimitUpTimeRule.Match(float64(limitTimeValue)) {
continue
}
}
if row.Open.Valid && row.PreviousClose.Valid && row.PreviousClose.Float64 > 0 {
row.OpenPreviousCloseRatio = row.Open.Float64 / row.PreviousClose.Float64
row.OpenPctChg = sql.NullFloat64{
Float64: secondProfitRound2((row.Open.Float64 - row.PreviousClose.Float64) / row.PreviousClose.Float64 * 100),
Valid: true,
}
}
if req.OpenPctChgRule.Enabled() && (!row.OpenPctChg.Valid || !req.OpenPctChgRule.Match(row.OpenPctChg.Float64)) {
continue
}
row.OpenPreviousCloseRatio = row.Open / row.PreviousClose
if row.Close > 0 {
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close)
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close)
row.NextCloseReturnPct = secondProfitPctReturn(row.NextClose, row.Close)
if row.Close.Valid && row.Close.Float64 > 0 {
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close.Float64)
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close.Float64)
row.NextLowReturnPct = secondProfitPctReturn(row.NextLow, row.Close.Float64)
row.NextCloseReturnPct = secondProfitPctReturn(row.NextClose, row.Close.Float64)
}
row.NextOpenProfitable = secondProfitLabel(row.NextOpenReturnPct)
row.NextCloseProfitable = secondProfitLabel(row.NextCloseReturnPct)
row.NextIntradayProfitable = secondProfitLabel(row.NextHighReturnPct)
rows = append(rows, row)
@@ -186,62 +396,152 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
func secondProfitCSVHeaders() []string {
return []string{
"交易日", "前一交易日", "前二交易日", "后一交易日", "股票代码", "股票简称", "市场", "行业",
"T日开盘价", "T日收盘价", "T日涨停价", "T-1收盘价", "开盘/T-1收盘%", "首次涨停时间",
"T日涨跌幅%", "成交额", "换手率%", "封单额", "开板次数", "涨停统计",
"T+1开盘价", "T+1最高价", "T+1最低价", "T+1收盘价",
"T+1开盘收益%", "T+1最高收益%", "T+1收盘收益%", "T+1收盘是否盈利", "T+1盘中是否可盈利",
"交易日", "股票代码", "股票简称", "市场", "行业",
"T+1开盘是否盈利", "T+1收盘是否盈利", "T+1盘中是否可盈利",
"T+1最高盈利%", "T+1最低盈利%", "开板次数", "换手率%",
"首次涨停时间", "封单额", "成交额",
}
}
func secondProfitCSVRecord(row secondProfitCSVRow) []string {
return []string{
row.TradeDate,
row.PreviousTradeDate,
row.PreviousTwoTradeDate,
row.NextTradeDate,
row.TsCode,
row.Name,
row.Market,
row.Industry,
secondProfitFormatFloat(row.Open),
secondProfitFormatFloat(row.Close),
secondProfitFormatFloat(row.LimitPrice),
secondProfitFormatFloat(row.PreviousClose),
secondProfitFormatFloat(secondProfitRound2(row.OpenPreviousCloseRatio * 100)),
secondProfitFormatLimitTime(row.FirstLimitTime.String),
secondProfitFormatNullFloat(row.PctChg),
secondProfitFormatNullFloat(row.Amount),
secondProfitFormatNullFloat(row.TurnoverRate),
secondProfitFormatNullFloat(row.SealAmount),
secondProfitFormatNullInt(row.BreakLimitCount),
row.LimitUpStat,
secondProfitFormatNullFloat(row.NextOpen),
secondProfitFormatNullFloat(row.NextHigh),
secondProfitFormatNullFloat(row.NextLow),
secondProfitFormatNullFloat(row.NextClose),
secondProfitFormatNullFloat(row.NextOpenReturnPct),
secondProfitFormatNullFloat(row.NextHighReturnPct),
secondProfitFormatNullFloat(row.NextCloseReturnPct),
row.NextOpenProfitable,
row.NextCloseProfitable,
row.NextIntradayProfitable,
secondProfitFormatNullFloat(row.NextHighReturnPct),
secondProfitFormatNullFloat(row.NextLowReturnPct),
secondProfitFormatNullInt(row.BreakLimitCount),
secondProfitFormatNullFloat(row.TurnoverRate),
secondProfitFormatLimitTime(row.FirstLimitTime.String),
secondProfitFormatNullFloat(row.SealAmount),
secondProfitFormatNullFloat(row.Amount),
}
}
func isSecondProfitBeforeEleven(limitTime string) bool {
func secondProfitBuildItem(row secondProfitCSVRow) secondProfitListItem {
return secondProfitListItem{
TradeDate: row.TradeDate,
TsCode: row.TsCode,
Name: row.Name,
Market: row.Market,
Industry: row.Industry,
NextOpenProfitable: row.NextOpenProfitable,
NextCloseProfitable: row.NextCloseProfitable,
NextIntradayProfitable: row.NextIntradayProfitable,
NextHighProfitPct: secondProfitNullFloatPtr(row.NextHighReturnPct),
NextLowProfitPct: secondProfitNullFloatPtr(row.NextLowReturnPct),
BreakLimitCount: secondProfitNullIntPtr(row.BreakLimitCount),
TurnoverRatePct: secondProfitNullFloatPtr(row.TurnoverRate),
FirstLimitTime: secondProfitFormatLimitTime(row.FirstLimitTime.String),
SealAmount: secondProfitNullFloatPtr(row.SealAmount),
Amount: secondProfitNullFloatPtr(row.Amount),
}
}
func parseSecondProfitCompareFilter(op1, value1, op2, value2 string, parseValue func(string) (float64, error), fieldName string) (secondProfitCompareFilter, error) {
filter := secondProfitCompareFilter{}
if err := appendSecondProfitCompareCondition(&filter, op1, value1, parseValue, fieldName, "第一组"); err != nil {
return filter, err
}
if err := appendSecondProfitCompareCondition(&filter, op2, value2, parseValue, fieldName, "第二组"); err != nil {
return filter, err
}
return filter, nil
}
func appendSecondProfitCompareCondition(filter *secondProfitCompareFilter, op, rawValue string, parseValue func(string) (float64, error), fieldName, groupName string) error {
op = strings.TrimSpace(op)
rawValue = strings.TrimSpace(rawValue)
if op == "" && rawValue == "" {
return nil
}
if op == "" || rawValue == "" {
return fmt.Errorf("%s %s操作符和值必须同时填写", fieldName, groupName)
}
if !isSecondProfitCompareOp(op) {
return fmt.Errorf("%s %s操作符必须为 <、>、<=、>=、=", fieldName, groupName)
}
value, err := parseValue(rawValue)
if err != nil {
return fmt.Errorf("%s %s值格式错误", fieldName, groupName)
}
filter.Conditions = append(filter.Conditions, secondProfitCompareCondition{Op: op, Value: value})
return nil
}
func isSecondProfitCompareOp(op string) bool {
switch op {
case "<", ">", "<=", ">=", "=":
return true
default:
return false
}
}
func (f secondProfitCompareFilter) Enabled() bool {
return len(f.Conditions) > 0
}
func (f secondProfitCompareFilter) Match(value float64) bool {
if !f.Enabled() {
return true
}
for _, condition := range f.Conditions {
if !condition.Match(value) {
return false
}
}
return true
}
func (c secondProfitCompareCondition) Match(value float64) bool {
switch c.Op {
case "<":
return value < c.Value
case "<=":
return value <= c.Value
case ">":
return value > c.Value
case ">=":
return value >= c.Value
case "=":
return value == c.Value
default:
return false
}
}
func parseSecondProfitTimeValue(value string) (float64, error) {
seconds, ok := secondProfitTimeSeconds(value)
if !ok {
return 0, fmt.Errorf("invalid time")
}
return float64(seconds), nil
}
func parseSecondProfitFloatValue(value string) (float64, error) {
return strconv.ParseFloat(strings.TrimSpace(value), 64)
}
func secondProfitTimeSeconds(limitTime string) (int, bool) {
timeText := strings.TrimSpace(limitTime)
if timeText == "" {
return false
return 0, false
}
hour, minute, second := 0, 0, 0
if strings.Contains(timeText, ":") {
if n, _ := fmt.Sscanf(timeText, "%d:%d:%d", &hour, &minute, &second); n < 2 {
return false
return 0, false
}
} else {
if len(timeText) < 4 {
return false
return 0, false
}
switch len(timeText) {
case 4:
@@ -250,26 +550,26 @@ func isSecondProfitBeforeEleven(limitTime string) bool {
timeText = "0" + timeText
case 6:
default:
return false
return 0, false
}
var err error
hour, err = strconv.Atoi(timeText[0:2])
if err != nil {
return false
return 0, false
}
minute, err = strconv.Atoi(timeText[2:4])
if err != nil {
return false
return 0, false
}
second, err = strconv.Atoi(timeText[4:6])
if err != nil {
return false
return 0, false
}
}
if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return false
return 0, false
}
return hour < 11
return hour*3600 + minute*60 + second, true
}
func secondProfitFormatLimitTime(value string) string {
@@ -327,3 +627,19 @@ func secondProfitFormatNullInt(value sql.NullInt64) string {
}
return strconv.FormatInt(value.Int64, 10)
}
func secondProfitNullFloatPtr(value sql.NullFloat64) *float64 {
if !value.Valid {
return nil
}
v := value.Float64
return &v
}
func secondProfitNullIntPtr(value sql.NullInt64) *int64 {
if !value.Valid {
return nil
}
v := value.Int64
return &v
}
+1
View File
@@ -33,6 +33,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
limitBoard.GET("/open-lower-groups", limitBoardApi.GetOpenLowerGroups)
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit)
}
download := v1.Group("/download")
+5 -5
View File
@@ -358,7 +358,7 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
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`
@@ -402,9 +402,9 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
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 pd.ts_code = l.ts_code AND pd.trade_date = ?
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
@@ -448,9 +448,9 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad
l.calc_vol AS vol
FROM stock_limit_combined_data l
LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
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 pd.ts_code = l.ts_code AND pd.trade_date = ?
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
+1 -1
View File
@@ -39,7 +39,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An
// 查询涨停股
query := `SELECT l.ts_code, l.name, d.open, COALESCE(l.calc_high, d.high, 0) AS high FROM stock_limit_combined_data l
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
LEFT JOIN stock_daily d ON l.ts_code COLLATE utf8mb4_0900_ai_ci = d.ts_code AND l.trade_date = d.trade_date
WHERE l.trade_date = ? AND l.final_status = 1`
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {