彻底解决 sql 兼容性问题 #6

Merged
yang merged 1 commits from main into master 2026-05-13 21:26:54 +08:00
13 changed files with 109 additions and 559 deletions
+1 -1
View File
@@ -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` 作为日期参数别名。
示例:
-149
View File
@@ -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;
-19
View File
@@ -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;
+12 -2
View File
@@ -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 过滤。
+22 -18
View File
@@ -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
}
-147
View File
@@ -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
}
@@ -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
}
@@ -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`
@@ -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`
@@ -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`
+43 -33
View File
@@ -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
}
+1 -2
View File
@@ -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 {
-159
View File
@@ -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;