适配mysql

This commit is contained in:
2026-05-13 17:46:03 +08:00
parent 81dc55baaf
commit 61cc29d7b3
12 changed files with 456 additions and 114 deletions
+149
View File
@@ -0,0 +1,149 @@
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
@@ -0,0 +1,19 @@
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;
+5
View File
@@ -32,6 +32,7 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
- 涨停价比例按板块计算:北交所 30%,创业板/科创板 20%,其余名称包含 ST 的股票 5%,其他股票 10%。 - 涨停价比例按板块计算:北交所 30%,创业板/科创板 20%,其余名称包含 ST 的股票 5%,其他股票 10%。
- T 日 `close` 与 `high` 一致时,计为二板封板,`status = "limit"`。 - T 日 `close` 与 `high` 一致时,计为二板封板,`status = "limit"`。
- T 日 `close` 小于 `high` 时,计为二板炸板,`status = "break"`。 - T 日 `close` 小于 `high` 时,计为二板炸板,`status = "break"`。
- `first_limit_time` 和 `open_times` 来自 T 日二板记录,封板取 `limit_list_d.limit = 'U'` 记录,炸板取 `limit_list_d.limit = 'Z'` 记录;时间格式化为 `HH:MM:SS`。
- T+1 盘中盈利统一按 `T+1 high > T high` 判断,概率分母只统计具备 T 日最高价和 T+1 最高价的数据。 - T+1 盘中盈利统一按 `T+1 high > T high` 判断,概率分母只统计具备 T 日最高价和 T+1 最高价的数据。
## 4. 响应字段 ## 4. 响应字段
@@ -75,6 +76,8 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
| `t_high` / `t_close` | `number \| null` | T 日最高价和收盘价 | | `t_high` / `t_close` | `number \| null` | T 日最高价和收盘价 |
| `t_pct_chg` | `number \| null` | T 日收盘涨跌幅 | | `t_pct_chg` | `number \| null` | T 日收盘涨跌幅 |
| `status` | `string` | `limit` 表示封板,`break` 表示炸板 | | `status` | `string` | `limit` 表示封板,`break` 表示炸板 |
| `first_limit_time` | `string` | T 日二板首次涨停时间,格式化为 `HH:MM:SS`;无数据时为空字符串 |
| `open_times` | `int \| null` | T 日二板开板次数 |
| `next_high` / `next_close` | `number \| null` | T+1 最高价和收盘价 | | `next_high` / `next_close` | `number \| null` | T+1 最高价和收盘价 |
| `next_intraday_profitable` | `boolean \| null` | T+1 盘中是否盈利 | | `next_intraday_profitable` | `boolean \| null` | T+1 盘中是否盈利 |
| `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的收益率 | | `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的收益率 |
@@ -135,6 +138,8 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
"t_close": 11, "t_close": 11,
"t_pct_chg": 10, "t_pct_chg": 10,
"status": "limit", "status": "limit",
"first_limit_time": "09:35:12",
"open_times": 0,
"next_high": 11.2, "next_high": 11.2,
"next_close": 11.1, "next_close": 11.1,
"next_intraday_profitable": true, "next_intraday_profitable": true,
+23 -23
View File
@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"level_go/internal/dbcompat"
) )
const ( const (
@@ -336,21 +337,21 @@ func validateSecondProfitDateRange(startDate, endDate string) error {
// SQL 先找出“前一日涨停、前两日未涨停、当日继续涨停”的股票,再补齐 T+1 行情; // SQL 先找出“前一日涨停、前两日未涨停、当日继续涨停”的股票,再补齐 T+1 行情;
// Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。 // Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) { 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 := ` query := `
WITH open_calendar AS (
SELECT
DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date,
LAG(DATE_FORMAT(cal_date, '%Y-%m-%d'), 1) OVER (ORDER BY cal_date) AS previous_trade_date,
LAG(DATE_FORMAT(cal_date, '%Y-%m-%d'), 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
LEAD(DATE_FORMAT(cal_date, '%Y-%m-%d'), 1) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
)
SELECT SELECT
c.trade_date, DATE_FORMAT(c.trade_date, '%Y-%m-%d') AS trade_date,
c.previous_trade_date, DATE_FORMAT(c.previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
c.previous_two_trade_date, DATE_FORMAT(c.previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date,
COALESCE(c.next_trade_date, '') AS next_trade_date, COALESCE(DATE_FORMAT(c.next_trade_date, '%Y-%m-%d'), '') AS next_trade_date,
l.ts_code, l.ts_code,
COALESCE(l.symbol, '') AS symbol, COALESCE(l.symbol, '') AS symbol,
COALESCE(l.name, '') AS name, COALESCE(l.name, '') AS name,
@@ -373,29 +374,28 @@ SELECT
nd.high AS next_high, nd.high AS next_high,
nd.low AS next_low, nd.low AS next_low,
nd.close AS next_close nd.close AS next_close
FROM open_calendar c FROM (` + calendarTable + `) c
INNER JOIN stock_limit_combined_data l INNER JOIN stock_limit_combined_data l
ON l.trade_date = c.trade_date AND l.final_status = 1 ON l.trade_date = c.trade_date AND l.final_status = 1
INNER JOIN stock_limit_combined_data pl 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 ON ` + dbcompat.TsCodeEqual("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 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 ON ` + dbcompat.TsCodeEqual("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 LEFT JOIN stock_daily d
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd LEFT JOIN stock_daily pd
ON l.ts_code = pd.ts_code AND pd.trade_date = c.previous_trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = c.previous_trade_date
LEFT JOIN stock_daily ppd LEFT JOIN stock_daily ppd
ON l.ts_code = ppd.ts_code AND ppd.trade_date = c.previous_two_trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "ppd.ts_code") + ` AND ppd.trade_date = c.previous_two_trade_date
LEFT JOIN stock_daily nd LEFT JOIN stock_daily nd
ON l.ts_code = nd.ts_code AND nd.trade_date = c.next_trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "nd.ts_code") + ` AND nd.trade_date = c.next_trade_date
WHERE c.trade_date BETWEEN ? AND ? WHERE c.previous_trade_date IS NOT NULL
AND c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL AND ppl.ts_code IS NULL
ORDER BY c.trade_date ASC, l.ts_code ASC` ORDER BY c.trade_date ASC, l.ts_code ASC`
var rawRows []secondProfitCSVRow var rawRows []secondProfitCSVRow
if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil { if err := h.Service.DB.Raw(query, calendarArgs...).Scan(&rawRows).Error; err != nil {
return nil, err return nil, err
} }
+147
View File
@@ -0,0 +1,147 @@
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
}
+2
View File
@@ -157,6 +157,8 @@ type SecondCalStock struct {
TClose *float64 `json:"t_close"` // T 日收盘价。 TClose *float64 `json:"t_close"` // T 日收盘价。
TPctChg *float64 `json:"t_pct_chg"` // T 日收盘涨跌幅。 TPctChg *float64 `json:"t_pct_chg"` // T 日收盘涨跌幅。
Status string `json:"status"` // T 日二板状态:limit 表示封板,break 表示炸板。 Status string `json:"status"` // T 日二板状态:limit 表示封板,break 表示炸板。
FirstLimitTime string `json:"first_limit_time"` // T 日二板首次涨停时间,格式化为 HH:MM:SS。
OpenTimes *int `json:"open_times"` // T 日二板开板次数。
NextHigh *float64 `json:"next_high"` // T+1 日最高价。 NextHigh *float64 `json:"next_high"` // T+1 日最高价。
NextClose *float64 `json:"next_close"` // T+1 日收盘价。 NextClose *float64 `json:"next_close"` // T+1 日收盘价。
NextIntradayProfitable *bool `json:"next_intraday_profitable"` // T+1 盘中最高价是否高于 T 日最高价。 NextIntradayProfitable *bool `json:"next_intraday_profitable"` // T+1 盘中最高价是否高于 T 日最高价。
@@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -104,17 +105,18 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB
// //
// 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。 // 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。
func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) { 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 := ` query := `
WITH open_calendar AS ( SELECT *
SELECT FROM (
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 SELECT
DATE_FORMAT(c.trade_date, '%Y-%m-%d') AS trade_date, 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_trade_date, '%Y-%m-%d') AS previous_trade_date,
@@ -141,34 +143,31 @@ candidate AS (
nd.high AS next_high, nd.high AS next_high,
nd.low AS next_low, nd.low AS next_low,
nd.close AS next_close nd.close AS next_close
FROM open_calendar c FROM (` + calendarTable + `) c
INNER JOIN stock_limit_combined_data l INNER JOIN stock_limit_combined_data l
ON l.trade_date = c.trade_date AND l.final_status = 2 ON l.trade_date = c.trade_date AND l.final_status = 2
INNER JOIN stock_limit_combined_data pl 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 ON ` + dbcompat.TsCodeEqual("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 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 ON ` + dbcompat.TsCodeEqual("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 LEFT JOIN stock_daily d
ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date ON ` + dbcompat.TsCodeEqual("d.ts_code", "l.ts_code") + ` AND d.trade_date = c.trade_date
LEFT JOIN stock_daily pd LEFT JOIN stock_daily pd
ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date ON ` + dbcompat.TsCodeEqual("pd.ts_code", "l.ts_code") + ` AND pd.trade_date = c.previous_trade_date
LEFT JOIN stock_daily nd LEFT JOIN stock_daily nd
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date ON ` + dbcompat.TsCodeEqual("nd.ts_code", "l.ts_code") + ` AND nd.trade_date = c.next_trade_date
LEFT JOIN stock_basic sb LEFT JOIN stock_basic sb
ON sb.ts_code = l.ts_code COLLATE utf8mb4_unicode_ci ON ` + dbcompat.TsCodeEqualUnicode("sb.ts_code", "l.ts_code") + `
WHERE c.trade_date BETWEEN ? AND ? WHERE c.previous_trade_date IS NOT NULL
AND c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL AND ppl.ts_code IS NULL
) ) candidate
SELECT *
FROM candidate
WHERE previous_close IS NOT NULL WHERE previous_close IS NOT NULL
AND t_close IS NOT NULL AND t_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC` ORDER BY trade_date ASC, ts_code ASC`
var rows []limitBreakNextPerformanceRow var rows []limitBreakNextPerformanceRow
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error
return rows, err return rows, err
} }
@@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -146,7 +147,7 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
(? <> '' AND NOT EXISTS ( (? <> '' AND NOT EXISTS (
SELECT 1 SELECT 1
FROM stock_limit_combined_data ppl FROM stock_limit_combined_data ppl
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.final_status = 1 WHERE ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = ? AND ppl.final_status = 1
)) AS is_first_to_second_board, )) AS is_first_to_second_board,
COALESCE(l.calc_limit_price, 0) AS limit_price, COALESCE(l.calc_limit_price, 0) AS limit_price,
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg, COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
@@ -164,11 +165,11 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
l.calc_vol AS vol l.calc_vol AS vol
FROM stock_limit_combined_data l FROM stock_limit_combined_data l
LEFT JOIN stock_daily d LEFT JOIN stock_daily d
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd LEFT JOIN stock_daily pd
ON l.ts_code = pd.ts_code AND pd.trade_date = ? ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
INNER JOIN stock_limit_combined_data pl INNER JOIN stock_limit_combined_data pl
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1 ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
WHERE l.trade_date = ? AND l.final_status = 1 WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC` ORDER BY d.pct_chg DESC, l.ts_code ASC`
@@ -3,6 +3,7 @@ package service
import ( import (
"sort" "sort"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -93,11 +94,11 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad
l.calc_vol AS vol l.calc_vol AS vol
FROM stock_limit_combined_data l FROM stock_limit_combined_data l
LEFT JOIN stock_daily d LEFT JOIN stock_daily d
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
LEFT JOIN stock_daily pd LEFT JOIN stock_daily pd
ON l.ts_code = pd.ts_code AND pd.trade_date = ? ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
INNER JOIN stock_limit_combined_data pl INNER JOIN stock_limit_combined_data pl
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1 ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
WHERE l.trade_date = ? AND l.final_status = 1 WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC` ORDER BY d.pct_chg DESC, l.ts_code ASC`
@@ -3,6 +3,7 @@ package service
import ( import (
"sort" "sort"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -146,7 +147,7 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
l.calc_vol AS vol l.calc_vol AS vol
FROM stock_limit_combined_data l FROM stock_limit_combined_data l
LEFT JOIN stock_daily d LEFT JOIN stock_daily d
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
WHERE l.trade_date = ? AND l.final_status = 1 WHERE l.trade_date = ? AND l.final_status = 1
ORDER BY d.pct_chg DESC, l.ts_code ASC` ORDER BY d.pct_chg DESC, l.ts_code ASC`
+76 -59
View File
@@ -3,6 +3,7 @@ package service
import ( import (
"math" "math"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -23,6 +24,8 @@ type secondCalRow struct {
TClose *float64 `gorm:"column:t_close"` // T 日收盘价。 TClose *float64 `gorm:"column:t_close"` // T 日收盘价。
TPctChg *float64 `gorm:"column:t_pct_chg"` // T 日收盘涨跌幅。 TPctChg *float64 `gorm:"column:t_pct_chg"` // T 日收盘涨跌幅。
Status string `gorm:"column:status"` // T 日二板状态。 Status string `gorm:"column:status"` // T 日二板状态。
FirstLimitTime *string `gorm:"column:first_limit_time"` // T 日首次涨停时间。
OpenTimes *int `gorm:"column:open_times"` // T 日开板次数。
NextHigh *float64 `gorm:"column:next_high"` // T+1 日最高价。 NextHigh *float64 `gorm:"column:next_high"` // T+1 日最高价。
NextClose *float64 `gorm:"column:next_close"` // T+1 日收盘价。 NextClose *float64 `gorm:"column:next_close"` // T+1 日收盘价。
} }
@@ -85,29 +88,19 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco
// loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。 // loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。
func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) { func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) {
query := ` seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate)
WITH open_calendar AS ( if err != nil {
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
)
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 open_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 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) 定位。 // 构建交易日到响应数组下标的映射,后续聚合明细时可以 O(1) 定位。
dayIndex := make(map[string]int, len(days)) dayIndex := make(map[string]int, len(days))
@@ -119,17 +112,41 @@ ORDER BY trade_date ASC`
// loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。 // loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。
func (s *LimitBoardService) loadSecondCalRows(startDate, endDate string) ([]secondCalRow, error) { 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 := ` query := `
WITH open_calendar AS ( SELECT
SELECT DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
cal_date AS trade_date, DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
LAG(cal_date, 1) OVER (ORDER BY cal_date) AS previous_trade_date, DATE_FORMAT(previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date,
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date, COALESCE(DATE_FORMAT(next_trade_date, '%Y-%m-%d'), '') AS next_trade_date,
LEAD(cal_date, 1) OVER (ORDER BY cal_date) AS next_trade_date COALESCE(symbol, '') AS symbol,
FROM trade_calendar ts_code,
WHERE exchange = 'SSE' AND is_open = 1 name,
), market,
candidate AS ( industry,
previous_close,
limit_price,
t_high,
t_close,
t_pct_chg,
status,
first_limit_time,
open_times,
next_high,
next_close
FROM (
SELECT SELECT
c.trade_date, c.trade_date,
c.previous_trade_date, c.previous_trade_date,
@@ -159,45 +176,41 @@ candidate AS (
WHEN d.close < d.high THEN 'break' WHEN d.close < d.high THEN 'break'
ELSE 'unknown' ELSE 'unknown'
END AS status, END AS status,
CASE
WHEN ABS(d.close - d.high) < 0.0001
THEN NULLIF(NULLIF(TRIM(tlu.first_time), ''), 'None')
WHEN d.close < d.high
THEN NULLIF(NULLIF(TRIM(tlz.first_time), ''), 'None')
ELSE NULL
END AS first_limit_time,
CASE
WHEN ABS(d.close - d.high) < 0.0001 THEN tlu.open_times
WHEN d.close < d.high THEN tlz.open_times
ELSE NULL
END AS open_times,
nd.high AS next_high, nd.high AS next_high,
nd.close AS next_close nd.close AS next_close
FROM open_calendar c FROM (` + calendarTable + `) c
INNER JOIN limit_list_d pl INNER JOIN limit_list_d pl
ON pl.trade_date = c.previous_trade_date AND pl.` + "`limit`" + ` = 'U' ON pl.trade_date = c.previous_trade_date AND pl.` + "`limit`" + ` = 'U'
LEFT JOIN limit_list_d ppl LEFT JOIN limit_list_d ppl
ON ppl.ts_code = pl.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U' ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "pl.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
INNER JOIN stock_daily d INNER JOIN stock_daily d
ON d.ts_code = pl.ts_code COLLATE utf8mb4_0900_ai_ci AND d.trade_date = c.trade_date ON ` + dbcompat.TsCodeEqualRightCollation("d.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND d.trade_date = c.trade_date
LEFT JOIN stock_daily nd LEFT JOIN stock_daily nd
ON nd.ts_code = pl.ts_code COLLATE utf8mb4_0900_ai_ci AND nd.trade_date = c.next_trade_date ON ` + dbcompat.TsCodeEqualRightCollation("nd.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` 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'
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'
LEFT JOIN stock_basic sb LEFT JOIN stock_basic sb
ON sb.ts_code = pl.ts_code COLLATE utf8mb4_unicode_ci ON ` + dbcompat.TsCodeEqual("sb.ts_code", "pl.ts_code") + `
WHERE c.trade_date BETWEEN ? AND ? WHERE c.previous_trade_date IS NOT NULL
AND c.previous_trade_date IS NOT NULL
AND c.previous_two_trade_date IS NOT NULL AND c.previous_two_trade_date IS NOT NULL
AND ppl.ts_code IS NULL AND ppl.ts_code IS NULL
AND d.high IS NOT NULL AND d.high IS NOT NULL
AND d.close IS NOT NULL AND d.close IS NOT NULL
) ) candidate
SELECT
DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
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,
COALESCE(symbol, '') AS symbol,
ts_code,
name,
market,
industry,
previous_close,
limit_price,
t_high,
t_close,
t_pct_chg,
status,
next_high,
next_close
FROM candidate
WHERE previous_close IS NOT NULL WHERE previous_close IS NOT NULL
AND limit_price IS NOT NULL AND limit_price IS NOT NULL
AND limit_price > 0 AND limit_price > 0
@@ -206,7 +219,7 @@ WHERE previous_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC` ORDER BY trade_date ASC, ts_code ASC`
var rows []secondCalRow var rows []secondCalRow
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error
return rows, err return rows, err
} }
@@ -228,9 +241,13 @@ func buildSecondCalStock(row secondCalRow) model.SecondCalStock {
TClose: row.TClose, TClose: row.TClose,
TPctChg: row.TPctChg, TPctChg: row.TPctChg,
Status: row.Status, Status: row.Status,
OpenTimes: row.OpenTimes,
NextHigh: row.NextHigh, NextHigh: row.NextHigh,
NextClose: row.NextClose, NextClose: row.NextClose,
} }
if row.FirstLimitTime != nil {
item.FirstLimitTime = formatLimitTime(*row.FirstLimitTime)
}
if row.THigh != nil && *row.THigh > 0 && row.NextHigh != nil { if row.THigh != nil && *row.THigh > 0 && row.NextHigh != nil {
// T+1 盘中盈利统一按 T+1 最高价是否高于 T 日最高价判断。 // T+1 盘中盈利统一按 T+1 最高价是否高于 T 日最高价判断。
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"sort" "sort"
"gorm.io/gorm" "gorm.io/gorm"
"level_go/internal/dbcompat"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -45,7 +46,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An
// 查询 startDate 当日涨停股,并优先使用涨停合并表中的计算最高价。 // 查询 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 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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND l.trade_date = d.trade_date
WHERE l.trade_date = ? AND l.final_status = 1` WHERE l.trade_date = ? AND l.final_status = 1`
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil { if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {