From 50b10fa8809636e0e83b9f4126b8bac95069d477 Mon Sep 17 00:00:00 2001 From: mianyang <784310601@qq.com> Date: Wed, 13 May 2026 21:26:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=BB=E5=BA=95=E8=A7=A3=E5=86=B3=20sql=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/limit-board-break-next-performance.md | 2 +- docs/mysql-unified-ddl.sql | 149 ---------------- docs/mysql-unify-collation.sql | 19 --- internal/api/limit_board_handler.go | 14 +- .../api/limit_second_profit_csv_handler.go | 40 +++-- internal/dbcompat/dbcompat.go | 147 ---------------- ...it_board_break_next_performance_service.go | 40 +++-- .../limit_board_open_compare_service.go | 9 +- .../limit_board_open_threshold_service.go | 7 +- .../service/limit_board_overview_service.go | 3 +- internal/service/second_cal_service.go | 76 +++++---- internal/service/stock_service.go | 3 +- model.txt | 159 ------------------ 13 files changed, 109 insertions(+), 559 deletions(-) delete mode 100644 docs/mysql-unified-ddl.sql delete mode 100644 docs/mysql-unify-collation.sql delete mode 100644 internal/dbcompat/dbcompat.go delete mode 100644 model.txt diff --git a/docs/limit-board-break-next-performance.md b/docs/limit-board-break-next-performance.md index c1a9418..661855c 100644 --- a/docs/limit-board-break-next-performance.md +++ b/docs/limit-board-break-next-performance.md @@ -19,7 +19,7 @@ | `tOpenGainLt101` | `bool` | 否 | 无 | 筛选 T 日开盘价是否小于 T-1 收盘价的 101%,支持别名 `tOpenLessThan101`、`t_open_gain_lt_101` | | `breakTimeBefore1100` | `bool` | 否 | 无 | 筛选 T 日炸板时间是否在 `11:00:00` 前,空炸板时间按 `true` 处理,支持别名 `tBreakTimeBefore1100`、`break_time_before_1100` | -兼容 `start_date`、`end_date` 作为日期参数别名。 +兼容 `start_date`、`end_date`、`trade_start_date`、`trade_end_date` 作为日期参数别名。 示例: diff --git a/docs/mysql-unified-ddl.sql b/docs/mysql-unified-ddl.sql deleted file mode 100644 index 39c3d86..0000000 --- a/docs/mysql-unified-ddl.sql +++ /dev/null @@ -1,149 +0,0 @@ -ALTER DATABASE `level` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -CREATE TABLE `limit_list_d` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `trade_date` date NOT NULL COMMENT '交易日期', - `ts_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票代码', - `industry` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属行业', - `name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '股票名称', - `close` decimal(16,4) DEFAULT NULL COMMENT '收盘价', - `pct_chg` decimal(10,4) DEFAULT NULL COMMENT '涨跌幅', - `amount` decimal(24,4) DEFAULT NULL COMMENT '成交额', - `limit_amount` decimal(24,4) DEFAULT NULL COMMENT '板上成交金额', - `float_mv` decimal(24,4) DEFAULT NULL COMMENT '流通市值', - `total_mv` decimal(24,4) DEFAULT NULL COMMENT '总市值', - `turnover_ratio` decimal(10,4) DEFAULT NULL COMMENT '换手率', - `fd_amount` decimal(24,4) DEFAULT NULL COMMENT '封单金额', - `first_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '首次封板时间', - `last_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后封板时间', - `open_times` int DEFAULT NULL COMMENT '打开次数', - `up_stat` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '涨停统计', - `limit_times` int DEFAULT NULL COMMENT '连板数', - `limit` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'D跌停 U涨停 Z炸板', - PRIMARY KEY (`id`), - UNIQUE KEY `uniq_trade_code_limit` (`trade_date`,`ts_code`,`limit`), - KEY `idx_trade_date` (`trade_date`), - KEY `idx_ts_code` (`ts_code`), - KEY `idx_limit` (`limit`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -CREATE TABLE `stock_basic` ( - `ts_code` text COLLATE utf8mb4_unicode_ci, - `symbol` text COLLATE utf8mb4_unicode_ci, - `name` text COLLATE utf8mb4_unicode_ci, - `area` text COLLATE utf8mb4_unicode_ci, - `industry` text COLLATE utf8mb4_unicode_ci, - `fullname` text COLLATE utf8mb4_unicode_ci, - `enname` text COLLATE utf8mb4_unicode_ci, - `cnspell` text COLLATE utf8mb4_unicode_ci, - `market` text COLLATE utf8mb4_unicode_ci, - `exchange` text COLLATE utf8mb4_unicode_ci, - `curr_type` text COLLATE utf8mb4_unicode_ci, - `list_status` text COLLATE utf8mb4_unicode_ci, - `list_date` text COLLATE utf8mb4_unicode_ci, - `delist_date` text COLLATE utf8mb4_unicode_ci, - `is_hs` text COLLATE utf8mb4_unicode_ci, - `act_name` text COLLATE utf8mb4_unicode_ci, - `act_ent_type` text COLLATE utf8mb4_unicode_ci -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -CREATE TABLE `stock_daily` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `ts_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, - `symbol` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL, - `trade_date` date NOT NULL, - `open` decimal(16,4) DEFAULT NULL, - `high` decimal(16,4) DEFAULT NULL, - `low` decimal(16,4) DEFAULT NULL, - `close` decimal(16,4) DEFAULT NULL, - `pre_close` decimal(16,4) DEFAULT NULL, - `change_val` decimal(16,4) DEFAULT NULL, - `pct_chg` decimal(10,4) DEFAULT NULL, - `vol` bigint DEFAULT NULL, - `amount` bigint DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uniq_code_date` (`ts_code`,`trade_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -CREATE TABLE `stock_limit_combined_data` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `trade_date` date NOT NULL COMMENT '交易日期', - `symbol` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票代码,以stock_daily为准', - `ts_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'TS股票代码,以stock_daily为准', - `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票名称,以stock_daily关联基础表为准', - `final_status` tinyint NOT NULL COMMENT '融合状态:1-涨停,2-炸板', - `source_count` int NOT NULL DEFAULT '0' COMMENT '命中的数据源数量', - `source_flags` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '命中的数据源:calc,tushare', - `calc_status` tinyint DEFAULT NULL COMMENT '本地行情计算状态:1-涨停,2-炸板', - `calc_limit_price` decimal(16,4) DEFAULT NULL COMMENT '本地行情计算涨停价', - `calc_high` decimal(16,4) DEFAULT NULL COMMENT 'stock_daily最高价', - `calc_close` decimal(16,4) DEFAULT NULL COMMENT 'stock_daily收盘价', - `calc_pre_close` decimal(16,4) DEFAULT NULL COMMENT 'stock_daily前收盘价', - `calc_vol` bigint DEFAULT NULL COMMENT 'stock_daily成交量', - `market` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属板块', - `tushare_limit_type` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-D跌停U涨停Z炸板', - `tushare_industry` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-所属行业', - `tushare_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-名称', - `tushare_close` decimal(16,4) DEFAULT NULL COMMENT 'Tushare-收盘价', - `tushare_pct_chg` decimal(10,4) DEFAULT NULL COMMENT 'Tushare-涨跌幅', - `tushare_amount` decimal(24,4) DEFAULT NULL COMMENT 'Tushare-成交额', - `tushare_limit_amount` decimal(24,4) DEFAULT NULL COMMENT 'Tushare-板上成交金额', - `tushare_float_mv` decimal(24,4) DEFAULT NULL COMMENT 'Tushare-流通市值', - `tushare_total_mv` decimal(24,4) DEFAULT NULL COMMENT 'Tushare-总市值', - `tushare_turnover_ratio` decimal(10,4) DEFAULT NULL COMMENT 'Tushare-换手率', - `tushare_fd_amount` decimal(24,4) DEFAULT NULL COMMENT 'Tushare-封单金额', - `tushare_first_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-首次封板时间', - `tushare_last_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-最后封板时间', - `tushare_open_times` int DEFAULT NULL COMMENT 'Tushare-炸板/开板次数', - `tushare_up_stat` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Tushare-涨停统计', - `tushare_limit_times` int DEFAULT NULL COMMENT 'Tushare-连板数', - PRIMARY KEY (`id`), - UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`), - KEY `idx_trade_date` (`trade_date`), - KEY `idx_symbol` (`symbol`), - KEY `idx_trade_date_status` (`trade_date`,`final_status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -CREATE TABLE `trade_calendar` ( - `cal_date` date NOT NULL, - `exchange` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL, - `is_open` tinyint DEFAULT NULL, - `pretrade_date` date DEFAULT NULL, - PRIMARY KEY (`cal_date`,`exchange`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -CREATE TABLE `stock_limit_data` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `trade_date` date NOT NULL COMMENT '交易日期', - `symbol` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票代码', - `ts_code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'TS股票代码', - `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票名称', - `status` tinyint NOT NULL COMMENT '状态:1-涨停,2-炸板', - `limit_price` decimal(16,4) DEFAULT NULL COMMENT '计算出的理论涨停价', - `close` decimal(16,4) DEFAULT NULL COMMENT '实际收盘价', - `vol` bigint DEFAULT NULL COMMENT '成交量', - `market` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属板块', - `ak_code` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-代码', - `ak_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-名称', - `ak_pct_chg` decimal(10,4) DEFAULT NULL COMMENT 'AKShare-涨跌幅', - `ak_latest_price` decimal(16,4) DEFAULT NULL COMMENT 'AKShare-最新价', - `ak_amount` decimal(24,4) DEFAULT NULL COMMENT 'AKShare-成交额', - `ak_float_market_value` decimal(24,4) DEFAULT NULL COMMENT 'AKShare-流通市值', - `ak_total_market_value` decimal(24,4) DEFAULT NULL COMMENT 'AKShare-总市值', - `ak_turnover_rate` decimal(10,4) DEFAULT NULL COMMENT 'AKShare-换手率', - `ak_seal_amount` decimal(24,4) DEFAULT NULL COMMENT 'AKShare-封板资金', - `ak_first_limit_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-首次封板时间', - `ak_last_limit_time` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-最后封板时间', - `ak_break_limit_count` int DEFAULT NULL COMMENT 'AKShare-炸板次数', - `ak_limit_up_stat` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-涨停统计', - `ak_limit_up_days` int DEFAULT NULL COMMENT 'AKShare-连板数', - `ak_industry` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AKShare-所属行业', - PRIMARY KEY (`id`), - UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - diff --git a/docs/mysql-unify-collation.sql b/docs/mysql-unify-collation.sql deleted file mode 100644 index 23b3946..0000000 --- a/docs/mysql-unify-collation.sql +++ /dev/null @@ -1,19 +0,0 @@ -ALTER DATABASE `level` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `limit_list_d` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `stock_basic` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `stock_daily` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `stock_limit_combined_data` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `trade_calendar` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `stock_limit_data` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/internal/api/limit_board_handler.go b/internal/api/limit_board_handler.go index 3a2fb5e..90c8846 100644 --- a/internal/api/limit_board_handler.go +++ b/internal/api/limit_board_handler.go @@ -112,8 +112,8 @@ func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) { // includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。 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"))), + TradeStartDate: firstNonEmptyQuery(c, "tradeStartDate", "trade_start_date", "start_date"), + TradeEndDate: firstNonEmptyQuery(c, "tradeEndDate", "trade_end_date", "end_date"), Page: 1, PageSize: 20, } @@ -159,6 +159,16 @@ func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination boo return req, nil } +// firstNonEmptyQuery 按优先级读取第一个非空查询参数。 +func firstNonEmptyQuery(c *gin.Context, keys ...string) string { + for _, key := range keys { + if value := strings.TrimSpace(c.Query(key)); value != "" { + return value + } + } + return "" +} + // parseOptionalBoolQuery 从多个兼容参数名中读取可选布尔值。 // // 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。 diff --git a/internal/api/limit_second_profit_csv_handler.go b/internal/api/limit_second_profit_csv_handler.go index 133f11d..cc11b78 100644 --- a/internal/api/limit_second_profit_csv_handler.go +++ b/internal/api/limit_second_profit_csv_handler.go @@ -11,7 +11,6 @@ import ( "time" "github.com/gin-gonic/gin" - "level_go/internal/dbcompat" ) const ( @@ -337,16 +336,21 @@ func validateSecondProfitDateRange(startDate, endDate string) error { // SQL 先找出“前一日涨停、前两日未涨停、当日继续涨停”的股票,再补齐 T+1 行情; // Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。 func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) { - seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(h.Service.DB, req.TradeStartDate, req.TradeEndDate) - if err != nil { - return nil, err - } - if len(seeds) == 0 { - return []secondProfitCSVRow{}, nil - } - calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds) - query := ` +WITH calendar AS ( + SELECT + cal_date AS trade_date, + LAG(cal_date) 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) OVER (ORDER BY cal_date) AS next_trade_date + FROM trade_calendar + WHERE exchange = 'SSE' AND is_open = 1 +), +calendar_window AS ( + SELECT * + FROM calendar + WHERE trade_date BETWEEN ? AND ? +) 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, @@ -374,28 +378,28 @@ SELECT nd.high AS next_high, nd.low AS next_low, nd.close AS next_close -FROM (` + calendarTable + `) c +FROM calendar_window c INNER JOIN stock_limit_combined_data l ON l.trade_date = c.trade_date AND l.final_status = 1 INNER JOIN stock_limit_combined_data pl - ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1 + 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 ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1 + 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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date + ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date LEFT JOIN stock_daily pd - ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = c.previous_trade_date + ON l.ts_code = pd.ts_code AND pd.trade_date = c.previous_trade_date LEFT JOIN stock_daily ppd - ON ` + dbcompat.TsCodeEqual("l.ts_code", "ppd.ts_code") + ` AND ppd.trade_date = c.previous_two_trade_date + ON l.ts_code = ppd.ts_code AND ppd.trade_date = c.previous_two_trade_date LEFT JOIN stock_daily nd - ON ` + dbcompat.TsCodeEqual("l.ts_code", "nd.ts_code") + ` AND nd.trade_date = c.next_trade_date + ON l.ts_code = nd.ts_code AND nd.trade_date = c.next_trade_date WHERE c.previous_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL AND ppl.ts_code IS NULL ORDER BY c.trade_date ASC, l.ts_code ASC` var rawRows []secondProfitCSVRow - if err := h.Service.DB.Raw(query, calendarArgs...).Scan(&rawRows).Error; err != nil { + if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil { return nil, err } diff --git a/internal/dbcompat/dbcompat.go b/internal/dbcompat/dbcompat.go deleted file mode 100644 index 31d7a1f..0000000 --- a/internal/dbcompat/dbcompat.go +++ /dev/null @@ -1,147 +0,0 @@ -package dbcompat - -import ( - "strings" - - "gorm.io/gorm" -) - -// CalendarSeed is a MySQL-version-neutral replacement for LAG/LEAD over the -// trade calendar. Keeping the adjacent dates in Go avoids depending on MySQL 8. -type CalendarSeed struct { - TradeDate string - PreviousTradeDate string - PreviousTwoTradeDate string - NextTradeDate string -} - -type calendarDateRow struct { - TradeDate string `gorm:"column:trade_date"` -} - -type columnCollationRow struct { - CollationName string `gorm:"column:collation_name"` -} - -// LoadSSEOpenCalendarSeeds loads all SSE open dates and builds the date window -// needed by range queries. The returned seeds are limited to [startDate, endDate]. -func LoadSSEOpenCalendarSeeds(db *gorm.DB, startDate, endDate string) ([]CalendarSeed, error) { - var rows []calendarDateRow - if err := db.Table("trade_calendar"). - Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date"). - Where("exchange = ? AND is_open = ?", "SSE", 1). - Order("cal_date ASC"). - Scan(&rows).Error; err != nil { - return nil, err - } - - seeds := make([]CalendarSeed, 0) - for i, row := range rows { - if row.TradeDate < startDate || row.TradeDate > endDate { - continue - } - - seed := CalendarSeed{TradeDate: row.TradeDate} - if i > 0 { - seed.PreviousTradeDate = rows[i-1].TradeDate - } - if i > 1 { - seed.PreviousTwoTradeDate = rows[i-2].TradeDate - } - if i+1 < len(rows) { - seed.NextTradeDate = rows[i+1].TradeDate - } - seeds = append(seeds, seed) - } - - return seeds, nil -} - -// CalendarSeedTable converts calendar seeds into a derived table that works on -// MySQL 5.7, MySQL 8, and MariaDB without CTE/window-function support. -func CalendarSeedTable(seeds []CalendarSeed) (string, []any) { - if len(seeds) == 0 { - return `SELECT - CAST(NULL AS DATE) AS trade_date, - CAST(NULL AS DATE) AS previous_trade_date, - CAST(NULL AS DATE) AS previous_two_trade_date, - CAST(NULL AS DATE) AS next_trade_date -WHERE 1 = 0`, nil - } - - parts := make([]string, 0, len(seeds)) - args := make([]any, 0, len(seeds)*4) - for _, seed := range seeds { - parts = append(parts, `SELECT - CAST(? AS DATE) AS trade_date, - CAST(? AS DATE) AS previous_trade_date, - CAST(? AS DATE) AS previous_two_trade_date, - CAST(? AS DATE) AS next_trade_date`) - args = append(args, - nullableDate(seed.TradeDate), - nullableDate(seed.PreviousTradeDate), - nullableDate(seed.PreviousTwoTradeDate), - nullableDate(seed.NextTradeDate), - ) - } - - return strings.Join(parts, "\nUNION ALL\n"), args -} - -// TsCodeEqual keeps joins free of hard-coded COLLATE clauses. This avoids -// binding SQL to MySQL-8-only collations while preserving index-friendly joins. -func TsCodeEqual(left, right string) string { - return left + " = " + right -} - -// ColumnCollation returns the current database collation for one column. -func ColumnCollation(db *gorm.DB, tableName, columnName string) (string, error) { - var row columnCollationRow - err := db.Raw(` -SELECT COLLATION_NAME AS collation_name -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = ? - AND COLUMN_NAME = ? -LIMIT 1`, tableName, columnName).Scan(&row).Error - if err != nil { - return "", err - } - return row.CollationName, nil -} - -// TsCodeEqualUnicode compares a utf8mb4_unicode_ci column with a column that -// may be utf8mb4_0900_ai_ci. utf8mb4_unicode_ci exists on both MySQL 5.7 and 8. -func TsCodeEqualUnicode(left, right string) string { - return left + " = " + right + " COLLATE utf8mb4_unicode_ci" -} - -// TsCodeEqualRightCollation compares two stock-code columns using a collation -// known to exist on the connected database. This avoids MySQL-version-specific -// literals while preserving the fast side of joins such as stock_daily.ts_code. -func TsCodeEqualRightCollation(left, right, collation string) string { - if !safeCollationName(collation) { - return TsCodeEqual(left, right) - } - return left + " = " + right + " COLLATE " + collation -} - -func safeCollationName(collation string) bool { - if collation == "" { - return false - } - for _, r := range collation { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { - continue - } - return false - } - return true -} - -func nullableDate(date string) any { - if strings.TrimSpace(date) == "" { - return nil - } - return date -} diff --git a/internal/service/limit_board_break_next_performance_service.go b/internal/service/limit_board_break_next_performance_service.go index 3387f54..58b6877 100644 --- a/internal/service/limit_board_break_next_performance_service.go +++ b/internal/service/limit_board_break_next_performance_service.go @@ -5,7 +5,6 @@ import ( "strconv" "strings" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -105,16 +104,21 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB // // 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。 func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) { - seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate) - if err != nil { - return nil, err - } - if len(seeds) == 0 { - return []limitBreakNextPerformanceRow{}, nil - } - calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds) - query := ` +WITH calendar AS ( + SELECT + cal_date AS trade_date, + LAG(cal_date) 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) OVER (ORDER BY cal_date) AS next_trade_date + FROM trade_calendar + WHERE exchange = 'SSE' AND is_open = 1 +), +calendar_window AS ( + SELECT * + FROM calendar + WHERE trade_date BETWEEN ? AND ? +) SELECT * FROM ( SELECT @@ -143,21 +147,21 @@ FROM ( nd.high AS next_high, nd.low AS next_low, nd.close AS next_close - FROM (` + calendarTable + `) c + FROM calendar_window 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 ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1 + 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 ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1 + 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 ` + dbcompat.TsCodeEqual("d.ts_code", "l.ts_code") + ` AND d.trade_date = c.trade_date + ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date LEFT JOIN stock_daily pd - ON ` + dbcompat.TsCodeEqual("pd.ts_code", "l.ts_code") + ` AND pd.trade_date = c.previous_trade_date + ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date LEFT JOIN stock_daily nd - ON ` + dbcompat.TsCodeEqual("nd.ts_code", "l.ts_code") + ` AND nd.trade_date = c.next_trade_date + ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date LEFT JOIN stock_basic sb - ON ` + dbcompat.TsCodeEqualUnicode("sb.ts_code", "l.ts_code") + ` + ON sb.ts_code = l.ts_code WHERE c.previous_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL AND ppl.ts_code IS NULL @@ -167,7 +171,7 @@ WHERE previous_close IS NOT NULL ORDER BY trade_date ASC, ts_code ASC` var rows []limitBreakNextPerformanceRow - err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error + err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error return rows, err } diff --git a/internal/service/limit_board_open_compare_service.go b/internal/service/limit_board_open_compare_service.go index d2aae6b..3ad9458 100644 --- a/internal/service/limit_board_open_compare_service.go +++ b/internal/service/limit_board_open_compare_service.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -147,7 +146,7 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD (? <> '' AND NOT EXISTS ( SELECT 1 FROM stock_limit_combined_data ppl - WHERE ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = ? AND ppl.final_status = 1 + 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, @@ -165,11 +164,11 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD l.calc_vol AS vol FROM stock_limit_combined_data l LEFT JOIN stock_daily d - ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date + ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date LEFT JOIN stock_daily pd - ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ? + ON l.ts_code = pd.ts_code AND pd.trade_date = ? INNER JOIN stock_limit_combined_data pl - ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1 + 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` diff --git a/internal/service/limit_board_open_threshold_service.go b/internal/service/limit_board_open_threshold_service.go index 3a8438f..2bbe44e 100644 --- a/internal/service/limit_board_open_threshold_service.go +++ b/internal/service/limit_board_open_threshold_service.go @@ -3,7 +3,6 @@ package service import ( "sort" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -94,11 +93,11 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad l.calc_vol AS vol FROM stock_limit_combined_data l LEFT JOIN stock_daily d - ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date + ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date LEFT JOIN stock_daily pd - ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ? + ON l.ts_code = pd.ts_code AND pd.trade_date = ? INNER JOIN stock_limit_combined_data pl - ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1 + 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` diff --git a/internal/service/limit_board_overview_service.go b/internal/service/limit_board_overview_service.go index 51ee2a5..abd481e 100644 --- a/internal/service/limit_board_overview_service.go +++ b/internal/service/limit_board_overview_service.go @@ -3,7 +3,6 @@ package service import ( "sort" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -147,7 +146,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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date + 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` diff --git a/internal/service/second_cal_service.go b/internal/service/second_cal_service.go index 780c7b4..ab2cd48 100644 --- a/internal/service/second_cal_service.go +++ b/internal/service/second_cal_service.go @@ -3,7 +3,6 @@ package service import ( "math" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -88,19 +87,29 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco // loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。 func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) { - seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate) - if err != nil { + query := ` +WITH calendar AS ( + SELECT + cal_date AS trade_date, + LAG(cal_date) 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) OVER (ORDER BY cal_date) AS next_trade_date + FROM trade_calendar + WHERE exchange = 'SSE' AND is_open = 1 +) +SELECT + DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date, + COALESCE(DATE_FORMAT(previous_trade_date, '%Y-%m-%d'), '') AS previous_trade_date, + COALESCE(DATE_FORMAT(previous_two_trade_date, '%Y-%m-%d'), '') AS previous_two_trade_date, + COALESCE(DATE_FORMAT(next_trade_date, '%Y-%m-%d'), '') AS next_trade_date +FROM calendar +WHERE trade_date BETWEEN ? AND ? +ORDER BY trade_date ASC` + + var days []secondCalDaySeed + if err := s.DB.Raw(query, startDate, endDate).Scan(&days).Error; err != nil { return nil, nil, err } - days := make([]secondCalDaySeed, 0, len(seeds)) - for _, seed := range seeds { - days = append(days, secondCalDaySeed{ - TradeDate: seed.TradeDate, - PreviousTradeDate: seed.PreviousTradeDate, - PreviousTwoTradeDate: seed.PreviousTwoTradeDate, - NextTradeDate: seed.NextTradeDate, - }) - } // 构建交易日到响应数组下标的映射,后续聚合明细时可以 O(1) 定位。 dayIndex := make(map[string]int, len(days)) @@ -112,20 +121,21 @@ func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]seco // loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。 func (s *LimitBoardService) loadSecondCalRows(startDate, endDate string) ([]secondCalRow, error) { - seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate) - if err != nil { - return nil, err - } - if len(seeds) == 0 { - return []secondCalRow{}, nil - } - calendarTable, calendarArgs := dbcompat.CalendarSeedTable(seeds) - stockDailyTsCodeCollation, err := dbcompat.ColumnCollation(s.DB, "stock_daily", "ts_code") - if err != nil { - return nil, err - } - query := ` +WITH calendar AS ( + SELECT + cal_date AS trade_date, + LAG(cal_date) 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) OVER (ORDER BY cal_date) AS next_trade_date + FROM trade_calendar + WHERE exchange = 'SSE' AND is_open = 1 +), +calendar_window AS ( + SELECT * + FROM calendar + WHERE trade_date BETWEEN ? AND ? +) SELECT DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date, DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date, @@ -190,21 +200,21 @@ FROM ( END AS open_times, nd.high AS next_high, nd.close AS next_close - FROM (` + calendarTable + `) c + FROM calendar_window c INNER JOIN limit_list_d pl ON pl.trade_date = c.previous_trade_date AND pl.` + "`limit`" + ` = 'U' LEFT JOIN limit_list_d ppl - ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "pl.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U' + ON ppl.ts_code = pl.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U' INNER JOIN stock_daily d - ON ` + dbcompat.TsCodeEqualRightCollation("d.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND d.trade_date = c.trade_date + ON d.ts_code = pl.ts_code AND d.trade_date = c.trade_date LEFT JOIN stock_daily nd - ON ` + dbcompat.TsCodeEqualRightCollation("nd.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND nd.trade_date = c.next_trade_date + ON nd.ts_code = pl.ts_code AND nd.trade_date = c.next_trade_date LEFT JOIN limit_list_d tlu - ON ` + dbcompat.TsCodeEqual("tlu.ts_code", "pl.ts_code") + ` AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U' + ON tlu.ts_code = pl.ts_code AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U' LEFT JOIN limit_list_d tlz - ON ` + dbcompat.TsCodeEqual("tlz.ts_code", "pl.ts_code") + ` AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z' + ON tlz.ts_code = pl.ts_code AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z' LEFT JOIN stock_basic sb - ON ` + dbcompat.TsCodeEqual("sb.ts_code", "pl.ts_code") + ` + ON sb.ts_code = pl.ts_code WHERE c.previous_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL AND ppl.ts_code IS NULL @@ -219,7 +229,7 @@ WHERE previous_close IS NOT NULL ORDER BY trade_date ASC, ts_code ASC` var rows []secondCalRow - err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error + err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error return rows, err } diff --git a/internal/service/stock_service.go b/internal/service/stock_service.go index 4ac6ad9..5d1d69e 100644 --- a/internal/service/stock_service.go +++ b/internal/service/stock_service.go @@ -5,7 +5,6 @@ import ( "sort" "gorm.io/gorm" - "level_go/internal/dbcompat" "level_go/internal/model" ) @@ -46,7 +45,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An // 查询 startDate 当日涨停股,并优先使用涨停合并表中的计算最高价。 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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND l.trade_date = d.trade_date + LEFT JOIN stock_daily d ON l.ts_code = 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 { diff --git a/model.txt b/model.txt deleted file mode 100644 index 2d35152..0000000 --- a/model.txt +++ /dev/null @@ -1,159 +0,0 @@ -macbook pro 本地 --- auto-generated definition -create table stock_basic -( - ts_code text null, - symbol text null, - name text null, - area text null, - industry text null, - fullname text null, - enname text null, - cnspell text null, - market text null, - exchange text null, - curr_type text null, - list_status text null, - list_date text null, - delist_date text null, - is_hs text null, - act_name text null, - act_ent_type text null -); - --- auto-generated definition -create table stock_daily -( - id bigint auto_increment - primary key, - ts_code varchar(20) not null, - symbol varchar(10) not null, - trade_date date not null, - open decimal(16, 4) null, - high decimal(16, 4) null, - low decimal(16, 4) null, - close decimal(16, 4) null, - pre_close decimal(16, 4) null, - change_val decimal(16, 4) null, - pct_chg decimal(10, 4) null, - vol bigint null, - amount bigint null, - constraint uniq_code_date - unique (ts_code, trade_date) -) - charset = utf8mb4; - --- auto-generated definition -create table stock_limit_combined_data -( - id bigint auto_increment - primary key, - trade_date date not null comment '交易日期', - symbol varchar(10) not null comment '股票代码,以stock_daily为准', - ts_code varchar(20) not null comment 'TS股票代码,以stock_daily为准', - name varchar(50) not null comment '股票名称,以stock_daily关联基础表为准', - final_status tinyint not null comment '融合状态:1-涨停,2-炸板', - source_count int default 0 not null comment '命中的数据源数量', - source_flags varchar(50) null comment '命中的数据源:calc,tushare', - calc_status tinyint null comment '本地行情计算状态:1-涨停,2-炸板', - calc_limit_price decimal(16, 4) null comment '本地行情计算涨停价', - calc_high decimal(16, 4) null comment 'stock_daily最高价', - calc_close decimal(16, 4) null comment 'stock_daily收盘价', - calc_pre_close decimal(16, 4) null comment 'stock_daily前收盘价', - calc_vol bigint null comment 'stock_daily成交量', - market varchar(20) null comment '所属板块', - tushare_limit_type varchar(1) null comment 'Tushare-D跌停U涨停Z炸板', - tushare_industry varchar(100) null comment 'Tushare-所属行业', - tushare_name varchar(50) null comment 'Tushare-名称', - tushare_close decimal(16, 4) null comment 'Tushare-收盘价', - tushare_pct_chg decimal(10, 4) null comment 'Tushare-涨跌幅', - tushare_amount decimal(24, 4) null comment 'Tushare-成交额', - tushare_limit_amount decimal(24, 4) null comment 'Tushare-板上成交金额', - tushare_float_mv decimal(24, 4) null comment 'Tushare-流通市值', - tushare_total_mv decimal(24, 4) null comment 'Tushare-总市值', - tushare_turnover_ratio decimal(10, 4) null comment 'Tushare-换手率', - tushare_fd_amount decimal(24, 4) null comment 'Tushare-封单金额', - tushare_first_time varchar(20) null comment 'Tushare-首次封板时间', - tushare_last_time varchar(20) null comment 'Tushare-最后封板时间', - tushare_open_times int null comment 'Tushare-炸板/开板次数', - tushare_up_stat varchar(50) null comment 'Tushare-涨停统计', - tushare_limit_times int null comment 'Tushare-连板数', - constraint uniq_date_code - unique (trade_date, ts_code) -); - -create index idx_symbol - on stock_limit_combined_data (symbol); - -create index idx_trade_date - on stock_limit_combined_data (trade_date); - -create index idx_trade_date_status - on stock_limit_combined_data (trade_date, final_status); - --- auto-generated definition -create table trade_calendar -( - cal_date date not null, - exchange varchar(10) not null, - is_open tinyint null, - pretrade_date date null, - primary key (cal_date, exchange) -) - charset = utf8mb4; - -阿里云 --- auto-generated definition -create table stock_basic -( - ts_code text null, - symbol text null, - name text null, - area text null, - industry text null, - fullname text null, - enname text null, - cnspell text null, - market text null, - exchange text null, - curr_type text null, - list_status text null, - list_date text null, - delist_date text null, - is_hs text null, - act_name text null, - act_ent_type text null -); - --- auto-generated definition -create table stock_daily -( - id bigint auto_increment - primary key, - ts_code varchar(20) not null, - symbol varchar(10) not null, - trade_date date not null, - open decimal(16, 4) null, - high decimal(16, 4) null, - low decimal(16, 4) null, - close decimal(16, 4) null, - pre_close decimal(16, 4) null, - change_val decimal(16, 4) null, - pct_chg decimal(10, 4) null, - vol bigint null, - amount bigint null, - constraint uniq_code_date - unique (ts_code, trade_date) -); - --- auto-generated definition -create table trade_calendar -( - cal_date date not null, - exchange varchar(10) not null, - is_open tinyint null, - pretrade_date date null, - primary key (cal_date, exchange) -) - charset = utf8mb4; - -- 2.43.0