Compare commits

..

10 Commits

Author SHA1 Message Date
yang a3292c3dd9 新增二板历史角度 2026-05-17 13:19:18 +08:00
yang 507b79f257 1、新增首页合计详情页;
2、新增索引和缓存;
2026-05-14 11:19:36 +08:00
yang 49cd4f85df Merge pull request '项目优化' (#7) from main into master
Reviewed-on: #7
2026-05-13 21:56:21 +08:00
yang 2f36adcb43 项目优化 2026-05-13 21:55:29 +08:00
yang 94e8a4f770 Merge pull request '彻底解决 sql 兼容性问题' (#6) from main into master
Reviewed-on: #6
2026-05-13 21:26:54 +08:00
yang 50b10fa880 彻底解决 sql 兼容性问题 2026-05-13 21:26:22 +08:00
yang 61cc29d7b3 适配mysql 2026-05-13 17:46:03 +08:00
yang 81dc55baaf 11点涨停逻辑变更为10点 2026-05-12 17:23:53 +08:00
yang faf535d889 Merge pull request '新增首页' (#5) from main into master
Reviewed-on: #5
2026-05-12 00:38:23 +08:00
yang d4bb849f79 新增首页 2026-05-12 00:36:25 +08:00
21 changed files with 1629 additions and 280 deletions
+6 -1
View File
@@ -10,4 +10,9 @@
# 依赖目录 (如果使用 vendor 模式)
# vendor/
config.yaml
# 本地运行配置,包含数据库账号密码,不提交到仓库
/config.yaml
/config.yml
/config/config.yaml
/config/config.yml
+42
View File
@@ -0,0 +1,42 @@
# level
## 本地配置
服务启动时会读取项目根目录的 `config.yaml`,也兼容读取 `config/config.yaml`。该文件包含数据库账号密码,已加入 `.gitignore`,不要提交到 Git 仓库。
配置格式如下:
```yaml
database:
user: "your_mysql_user"
password: "your_mysql_password"
host: "localhost"
port: 3306
dbname: "level"
charset: "utf8mb4"
collation: "utf8mb4_unicode_ci"
server:
port: 18080
```
如果 `config.yaml` 已经被远程仓库跟踪过,仅添加 `.gitignore` 不会自动停止跟踪,需要执行:
```bash
git rm --cached config.yaml
git commit -m "chore: stop tracking local config"
```
这只会从 Git 索引中移除配置文件,本地文件仍会保留。若远程历史里已经包含真实密码,需要同时更换相关密码;如需彻底清理 Git 历史,可再用 `git filter-repo` 或 BFG 处理。
## 数据库 DDL
当前代码使用的表结构保存在 `database/ddl/current_tables.sql`。这些表包括:
- `trade_calendar`
- `stock_daily`
- `stock_basic`
- `limit_list_d`
- `stock_limit_combined_data`
该文件由本地 `level` 库执行 `SHOW CREATE TABLE` 导出。
+9
View File
@@ -1,6 +1,7 @@
package main
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"level_go/internal/api"
@@ -12,6 +13,8 @@ import (
// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。
func main() {
gin.SetMode(gin.ReleaseMode)
// 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。
config.InitConfig()
@@ -21,6 +24,12 @@ func main() {
if err != nil {
log.Fatal("数据库连接失败:", err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatal("获取数据库连接池失败:", err)
}
sqlDB.SetMaxIdleConns(20)
sqlDB.SetMaxOpenConns(100)
// 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。
commonSrv := &service.CommonService{DB: db}
-22
View File
@@ -1,22 +0,0 @@
## 阿里云数据库
#database:
# user: "yangfan"
# password: "aA%8023321088"
# host: "8.159.129.156"
# port: 10836
# dbname: "level"
# charset: "utf8mb4"
# collation: "utf8mb4_unicode_ci"
# 本地数据库
database:
user: "yangfan"
password: "admin"
host: "localhost"
port: 3306
dbname: "level"
charset: "utf8mb4"
collation: "utf8mb4_unicode_ci"
server:
port: 18080
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE `stock_limit_combined_data`
ADD INDEX `idx_ts_code_date_status` (`ts_code`, `trade_date`, `final_status`),
ADD INDEX `idx_trade_status_code` (`trade_date`, `final_status`, `ts_code`);
+116
View File
@@ -0,0 +1,116 @@
-- DDL exported from local MySQL with SHOW CREATE TABLE.
-- Database: level
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_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 AUTO_INCREMENT=9372644 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 `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 AUTO_INCREMENT=154380 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) 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`),
KEY `idx_ts_code_date_status` (`ts_code`,`trade_date`,`final_status`),
KEY `idx_trade_status_code` (`trade_date`,`final_status`,`ts_code`)
) ENGINE=InnoDB AUTO_INCREMENT=167035 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+4 -1
View File
@@ -4,7 +4,7 @@
- 服务前缀:`/api/v1`
- 响应格式:`application/json`
- 当前项目接口总数:10
- 当前项目接口总数:13
## 2. 接口列表
@@ -17,6 +17,9 @@
| 涨停低开分组 | `GET` | `/api/v1/limit-board/open-lower-groups` | 获取查询日和前一交易日均涨停的股票,并按是否低于前一交易日收盘价分组 | [limit-board-open-lower-groups.md](./limit-board-open-lower-groups.md) |
| 涨停开盘阈值分组 | `GET` | `/api/v1/limit-board/open-threshold` | 获取查询日和前一交易日均涨停的股票,并按开盘价是否低于前一交易日收盘价 101% 分组 | [limit-board-open-threshold.md](./limit-board-open-threshold.md) |
| 二板早盘次日盈利 | `GET` | `/api/v1/limit-board/second-board-profit` | 分页查询 T-2 无涨停、T-1 涨停、T 涨停的次日盈利明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
| 二板获利综合 | `GET` | `/api/v1/limit-board/second-board-profit-summary` | 汇总每只个股 2020 年至今 09:31:00-09:59:59 二板后的次日最大获利,并按累计获利倒序排序 | [second-board-profit-summary.md](./second-board-profit-summary.md) |
| 个股二板获利详情 | `GET` | `/api/v1/limit-board/second-board-profit-detail` | 查询单只个股每次 09:31:00-09:59:59 二板后的次日最大获利 | [second-board-profit-summary.md](./second-board-profit-summary.md) |
| 二板日历 | `GET` | `/api/v1/limit-board/second-cal` | 按交易日统计二板数量、封板/炸板数量和 T+1 盘中盈利概率 | [second-cal.md](./second-cal.md) |
| 炸板次日表现 | `GET` | `/api/v1/limit-board/break-next-performance` | 分页查询 T-2 未涨停、T-1 涨停、T 日炸板股票及 T+1 表现 | [limit-board-break-next-performance.md](./limit-board-break-next-performance.md) |
| 下载 | `GET` | `/api/v1/download/limit-board/second-board-profit-csv` | 下载筛选条件范围内的二板早盘次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
| 下载 | `GET` | `/api/v1/download/limit-board/break-next-performance-csv` | 下载筛选条件范围内的炸板次日表现 CSV 明细 | [limit-board-break-next-performance.md](./limit-board-break-next-performance.md) |
+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` 作为日期参数别名。
示例:
+230
View File
@@ -0,0 +1,230 @@
# 二板获利综合与个股详情接口文档
## 1. 接口说明
本页包含两个二板获利统计接口:
- 二板获利综合:汇总每只个股 2020 年至今早盘二板后的 T+1 最大获利,并按累计获利从高到低排序。
- 个股二板获利详情:查询单只个股每次早盘二板后的 T+1 最大获利情况。
接口当前读取缓存表,不再在请求时实时联表计算:
- `second_board_profit_detail_cache`:保存每次命中的二板详情和 T+1 最大获利。
- `second_board_profit_summary_cache`:保存完整缓存范围内按个股聚合后的综合数据。
业务筛选基础条件:
- T-2 交易日无涨停。
- T-1 交易日涨停。
- T 交易日再次触及涨停价,T 日可为封板或炸板。
- T 日二板首次涨停时间在 `09:31:00` 到 `09:59:59` 之间,包含起止时间。
## 2. 接口地址
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 二板获利综合 | `GET` | `/api/v1/limit-board/second-board-profit-summary` | 汇总每只个股 2020 年至今 09:31:00-09:59:59 二板后的次日最大获利 |
| 个股二板获利详情 | `GET` | `/api/v1/limit-board/second-board-profit-detail` | 查询单只个股每次 09:31:00-09:59:59 二板后的次日最大获利 |
## 3. 统计规则
- 统计口径复用二板日历接口:T-2 未涨停、T-1 涨停、T 日再次触及涨停价。
- T+1 最大获利百分比为 `next_high_profit_pct`,按 `(T+1最高价 - T日最高价) / T日最高价 * 100` 计算并保留两位小数。
- 二板获利综合接口按个股累计 `next_high_profit_pct`,得到 `total_next_high_profit_pct`,并按该字段从高到低排序。
- 没有 T+1 最高价或 T 日最高价的数据仍会计入二板次数,但不参与获利累计和平均值计算。
- 当请求日期范围覆盖整张明细缓存表时,二板获利综合接口直接读取 `second_board_profit_summary_cache`。
- 当请求日期范围小于明细缓存表覆盖范围时,二板获利综合接口从 `second_board_profit_detail_cache` 按日期范围动态聚合。
- 个股二板获利详情接口始终从 `second_board_profit_detail_cache` 按日期范围和股票代码查询。
## 4. 缓存刷新
缓存表需要由离线脚本或人工任务刷新。接口只返回缓存表中已经落地的数据;如果最新交易日或历史补数未写入缓存表,接口不会临时回退到实时联表计算。
当前校验结果:
- `second_board_profit_detail_cache` 当前覆盖 `2020-01-06` 到 `2026-05-14`。
- 在该覆盖范围内,缓存明细和原实时计算口径数量一致、键一致、字段一致。
- 默认查询范围 `2020-01-01` 到当天如果覆盖了缓存表之外的日期,需要先刷新缓存表,否则接口只返回已落地缓存数据。
## 5. 请求参数
两个接口都支持以下日期参数:
| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `tradeStartDate` | `string` | 否 | `2020-01-01` | `2020-01-01` | T 日交易开始日期,格式 `YYYY-MM-DD` |
| `tradeEndDate` | `string` | 否 | 当天 | `2026-05-16` | T 日交易结束日期,格式 `YYYY-MM-DD` |
个股二板获利详情接口还需要传入股票代码,`tsCode` 和 `symbol` 二选一:
| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `tsCode` | `string` | 否 | 空 | `000001.SZ` | TuShare 股票代码,优先使用 |
| `symbol` | `string` | 否 | 空 | `000001` | 市场展示股票代码;未传 `tsCode` 时使用 |
兼容说明:日期参数兼容 `trade_start_date`、`start_date`、`trade_end_date`、`end_date`;`symbol` 兼容别名 `code`。
## 6. 请求示例
二板获利综合:
```http
GET /api/v1/limit-board/second-board-profit-summary
```
指定日期范围的二板获利综合:
```http
GET /api/v1/limit-board/second-board-profit-summary?tradeStartDate=2020-01-01&tradeEndDate=2026-05-16
```
个股二板获利详情:
```http
GET /api/v1/limit-board/second-board-profit-detail?tsCode=000001.SZ
```
## 7. 二板获利综合响应
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `trade_start_date` | `string` | 查询起始 T 日 |
| `trade_end_date` | `string` | 查询结束 T 日 |
| `start_time` | `string` | 固定筛选起点,当前为 `09:31:00` |
| `end_time` | `string` | 固定筛选终点,当前为 `09:59:59` |
| `total` | `int` | 命中的个股数量 |
| `list` | `array` | 按累计获利从高到低排序的个股汇总数据 |
`list[]` 字段:
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `symbol` | `string` | 股票代码 |
| `ts_code` | `string` | TuShare 股票代码 |
| `name` | `string` | 股票简称 |
| `market` | `string` | 所属板块 |
| `industry` | `string` | 所属行业 |
| `second_board_count` | `int` | 09:31:00-09:59:59 期间命中的二板次数 |
| `limit_count` | `int` | 二板封板次数 |
| `break_count` | `int` | 二板炸板次数 |
| `profit_data_count` | `int` | 具备 T+1 最大获利百分比的次数 |
| `total_next_high_profit_pct` | `number` | T+1 最大获利百分比累计值 |
| `average_next_high_profit_pct` | `number` | T+1 最大获利百分比平均值 |
| `first_trade_date` | `string` | 首次命中交易日 |
| `last_trade_date` | `string` | 最近命中交易日 |
响应示例:
```json
{
"trade_start_date": "2020-01-01",
"trade_end_date": "2026-05-16",
"start_time": "09:31:00",
"end_time": "09:59:59",
"total": 1,
"list": [
{
"symbol": "000001",
"ts_code": "000001.SZ",
"name": "示例股份",
"market": "主板",
"industry": "银行",
"second_board_count": 3,
"limit_count": 2,
"break_count": 1,
"profit_data_count": 3,
"total_next_high_profit_pct": 12.36,
"average_next_high_profit_pct": 4.12,
"first_trade_date": "2021-03-18",
"last_trade_date": "2026-04-24"
}
]
}
```
## 8. 个股二板获利详情响应
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `trade_start_date` | `string` | 查询起始 T 日 |
| `trade_end_date` | `string` | 查询结束 T 日 |
| `start_time` | `string` | 固定筛选起点,当前为 `09:31:00` |
| `end_time` | `string` | 固定筛选终点,当前为 `09:59:59` |
| `symbol` | `string` | 股票代码 |
| `ts_code` | `string` | TuShare 股票代码 |
| `name` | `string` | 股票简称 |
| `market` | `string` | 所属板块 |
| `industry` | `string` | 所属行业 |
| `second_board_count` | `int` | 命中的二板次数 |
| `profit_data_count` | `int` | 具备 T+1 最大获利百分比的次数 |
| `total_next_high_profit_pct` | `number` | T+1 最大获利百分比累计值 |
| `average_next_high_profit_pct` | `number` | T+1 最大获利百分比平均值 |
| `list` | `array` | 每次二板获利详情,字段同二板日历接口 `SecondCalStock` |
`list[]` 关键字段:
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `trade_date` | `string` | T 日交易日 |
| `previous_trade_date` | `string` | T-1 交易日 |
| `previous_two_trade_date` | `string` | T-2 交易日 |
| `next_trade_date` | `string` | T+1 交易日 |
| `status` | `string` | `limit` 表示封板,`break` 表示炸板 |
| `first_limit_time` | `string` | T 日二板首次涨停时间,固定落在 `09:31:00` 到 `09:59:59` |
| `t_high` | `number \| null` | T 日最高价 |
| `next_high` | `number \| null` | T+1 最高价 |
| `next_intraday_profitable` | `boolean \| null` | T+1 最高价是否高于 T 日最高价 |
| `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的最大获利百分比 |
响应示例:
```json
{
"trade_start_date": "2020-01-01",
"trade_end_date": "2026-05-16",
"start_time": "09:31:00",
"end_time": "09:59:59",
"symbol": "000001",
"ts_code": "000001.SZ",
"name": "示例股份",
"market": "主板",
"industry": "银行",
"second_board_count": 1,
"profit_data_count": 1,
"total_next_high_profit_pct": 4.12,
"average_next_high_profit_pct": 4.12,
"list": [
{
"trade_date": "2026-04-24",
"previous_trade_date": "2026-04-23",
"previous_two_trade_date": "2026-04-22",
"next_trade_date": "2026-04-27",
"symbol": "000001",
"ts_code": "000001.SZ",
"name": "示例股份",
"market": "主板",
"industry": "银行",
"previous_close": 10,
"limit_price": 11,
"t_high": 11,
"t_close": 11,
"t_pct_chg": 10,
"status": "limit",
"first_limit_time": "09:35:12",
"open_times": 0,
"next_high": 11.45,
"next_close": 11.2,
"next_intraday_profitable": true,
"next_high_profit_pct": 4.09
}
]
}
```
## 9. 错误响应
```json
{
"error": "参数错误"
}
```
+181
View File
@@ -0,0 +1,181 @@
# 二板日历接口文档
## 1. 基本信息
- 接口名称:二板日历
- 请求方法:`GET`
- 请求路径:`/api/v1/limit-board/second-cal`
- 接口用途:按交易日统计每日二板数量、二板封板数量、二板炸板数量,以及二板股票 T+1 盘中盈利概率。
## 2. 请求参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `tradeStartDate` | `string` | 是 | 无 | T 日起始日期,格式 `YYYY-MM-DD` |
| `tradeEndDate` | `string` | 是 | 无 | T 日结束日期,格式 `YYYY-MM-DD` |
兼容 `start_date`、`end_date` 作为日期参数别名。
请求示例:
```http
GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-05-11
```
## 3. 统计规则
- 以 `trade_calendar` 中 `exchange = 'SSE'` 且 `is_open = 1` 的记录推导 T-2、T-1、T、T+1 交易日。
- T-1 涨停样本来自 `limit_list_d`,条件为 `limit = 'U'`。
- T-2 同一股票不能在 `limit_list_d` 中存在 `limit = 'U'` 的记录。
- 股票所属板块和行业来自 `stock_basic.market`、`stock_basic.industry`。
- T 日行情来自 `stock_daily`,当 T 日最高价 `high` 触及按 T-1 收盘价和板块涨跌幅规则计算的涨停价时,计为二板。
- 涨停价比例按板块计算:北交所 30%,创业板/科创板 20%,其余名称包含 ST 的股票 5%,其他股票 10%。
- T 日 `close` 与 `high` 一致时,计为二板封板,`status = "limit"`。
- 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 最高价的数据。
## 4. 响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `trade_start_date` | `string` | 查询起始 T 日 |
| `trade_end_date` | `string` | 查询结束 T 日 |
| `summary` | `object` | 日期范围汇总统计 |
| `list` | `array` | 每个交易日的二板日历统计 |
### 4.1 summary/list 单日统计字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `second_board_count` | `int` | 二板股票数量 |
| `limit_count` | `int` | 二板封板数量 |
| `break_count` | `int` | 二板炸板数量 |
| `next_trade_data_count` | `int` | 具备 T+1 盈利判断数据的数量 |
| `next_intraday_profit_count` | `int` | T+1 盘中盈利数量 |
| `next_intraday_profit_rate` | `number` | T+1 盘中盈利概率,百分比 |
| `limit_next_data_count` | `int` | 封板股中具备 T+1 盈利判断数据的数量 |
| `limit_next_profit_count` | `int` | 封板股 T+1 盘中盈利数量 |
| `limit_next_profit_rate` | `number` | 封板股 T+1 盘中盈利概率,百分比 |
| `break_next_data_count` | `int` | 炸板股中具备 T+1 盈利判断数据的数量 |
| `break_next_profit_count` | `int` | 炸板股 T+1 盘中盈利数量 |
| `break_next_profit_rate` | `number` | 炸板股 T+1 盘中盈利概率,百分比 |
### 4.2 list[].stocks 明细字段
`list[].stocks` 保留当日全部二板明细。
`list[].total_stocks` 为当日涨停和炸板合并后的明细列表,字段结构与 `stocks` 相同;该列表会剔除 `first_limit_time` 为空的数据,并按 `first_limit_time` 升序排序,时间相同时按 `ts_code` 升序排序。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `trade_date` | `string` | T 日 |
| `previous_trade_date` | `string` | T-1 交易日 |
| `previous_two_trade_date` | `string` | T-2 交易日 |
| `next_trade_date` | `string` | T+1 交易日 |
| `symbol` / `ts_code` / `name` | `string` | 股票标识 |
| `market` / `industry` | `string` | 板块和行业 |
| `previous_close` | `number \| null` | T-1 收盘价 |
| `limit_price` | `number \| null` | T 日涨停价 |
| `t_high` / `t_close` | `number \| null` | T 日最高价和收盘价 |
| `t_pct_chg` | `number \| null` | T 日收盘涨跌幅 |
| `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_intraday_profitable` | `boolean \| null` | T+1 盘中是否盈利 |
| `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的收益率 |
## 5. 响应示例
```json
{
"trade_start_date": "2026-05-07",
"trade_end_date": "2026-05-11",
"summary": {
"trade_day_count": 3,
"second_board_count": 51,
"limit_count": 41,
"break_count": 10,
"next_trade_data_count": 35,
"next_intraday_profit_count": 34,
"next_intraday_profit_rate": 97.14,
"limit_next_data_count": 29,
"limit_next_profit_count": 28,
"limit_next_profit_rate": 96.55,
"break_next_data_count": 6,
"break_next_profit_count": 6,
"break_next_profit_rate": 100
},
"list": [
{
"trade_date": "2026-05-07",
"previous_trade_date": "2026-05-06",
"previous_two_trade_date": "2026-04-30",
"next_trade_date": "2026-05-08",
"second_board_count": 19,
"limit_count": 16,
"break_count": 3,
"next_trade_data_count": 19,
"next_intraday_profit_count": 18,
"next_intraday_profit_rate": 94.74,
"limit_next_data_count": 16,
"limit_next_profit_count": 15,
"limit_next_profit_rate": 93.75,
"break_next_data_count": 3,
"break_next_profit_count": 3,
"break_next_profit_rate": 100,
"total_stocks": [
{
"trade_date": "2026-05-07",
"previous_trade_date": "2026-05-06",
"previous_two_trade_date": "2026-04-30",
"next_trade_date": "2026-05-08",
"symbol": "000001",
"ts_code": "000001.SZ",
"name": "示例股份",
"market": "主板",
"industry": "银行",
"previous_close": 10,
"limit_price": 11,
"t_high": 11,
"t_close": 11,
"t_pct_chg": 10,
"status": "limit",
"first_limit_time": "09:35:12",
"open_times": 0,
"next_high": 11.2,
"next_close": 11.1,
"next_intraday_profitable": true,
"next_high_profit_pct": 1.82
}
],
"stocks": [
{
"trade_date": "2026-05-07",
"previous_trade_date": "2026-05-06",
"previous_two_trade_date": "2026-04-30",
"next_trade_date": "2026-05-08",
"symbol": "000001",
"ts_code": "000001.SZ",
"name": "示例股份",
"market": "主板",
"industry": "银行",
"previous_close": 10,
"limit_price": 11,
"t_high": 11,
"t_close": 11,
"t_pct_chg": 10,
"status": "limit",
"first_limit_time": "09:35:12",
"open_times": 0,
"next_high": 11.2,
"next_close": 11.1,
"next_intraday_profitable": true,
"next_high_profit_pct": 1.82
}
]
}
]
}
```
+47 -2
View File
@@ -72,13 +72,48 @@ func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// GetSecondCal 返回日期范围内的二板日历统计。
func (h *LimitBoardHandler) GetSecondCal(c *gin.Context) {
req, err := parseSecondCalRequest(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.Service.GetSecondCal(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// parseSecondCalRequest 解析二板日历接口的日期范围参数。
func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) {
req := model.SecondCalRequest{
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
}
if req.TradeStartDate == "" {
return req, errors.New("tradeStartDate 参数不能为空")
}
if req.TradeEndDate == "" {
return req, errors.New("tradeEndDate 参数不能为空")
}
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
return req, err
}
return req, nil
}
// parseLimitBreakNextPerformanceRequest 解析炸板次日表现接口的查询参数。
//
// 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,
}
@@ -124,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 过滤。
+16 -12
View File
@@ -337,20 +337,25 @@ func validateSecondProfitDateRange(startDate, endDate string) error {
// Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) {
query := `
WITH open_calendar AS (
WITH 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
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
c.trade_date,
c.previous_trade_date,
c.previous_two_trade_date,
COALESCE(c.next_trade_date, '') AS next_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_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date,
COALESCE(DATE_FORMAT(c.next_trade_date, '%Y-%m-%d'), '') AS next_trade_date,
l.ts_code,
COALESCE(l.symbol, '') AS symbol,
COALESCE(l.name, '') AS name,
@@ -373,7 +378,7 @@ SELECT
nd.high AS next_high,
nd.low AS next_low,
nd.close AS next_close
FROM open_calendar 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
@@ -388,8 +393,7 @@ LEFT JOIN stock_daily ppd
ON l.ts_code = ppd.ts_code AND ppd.trade_date = c.previous_two_trade_date
LEFT JOIN stock_daily nd
ON l.ts_code = nd.ts_code AND nd.trade_date = c.next_trade_date
WHERE c.trade_date BETWEEN ? AND ?
AND c.previous_trade_date IS NOT NULL
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`
@@ -0,0 +1,67 @@
package api
import (
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"level_go/internal/model"
)
// GetSecondBoardProfitSummary 返回 2020 年至今二板早盘股票的 T+1 最大获利汇总。
func (h *LimitBoardHandler) GetSecondBoardProfitSummary(c *gin.Context) {
req, err := parseSecondBoardProfitRequest(c, false)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.Service.GetSecondBoardProfitSummary(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// GetSecondBoardProfitDetail 返回单只股票二板早盘后的 T+1 最大获利详情。
func (h *LimitBoardHandler) GetSecondBoardProfitDetail(c *gin.Context) {
req, err := parseSecondBoardProfitRequest(c, true)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp, err := h.Service.GetSecondBoardProfitDetail(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// parseSecondBoardProfitRequest 解析二板早盘获利综合和详情接口参数。
func parseSecondBoardProfitRequest(c *gin.Context, requireCode bool) (model.SecondBoardProfitRequest, error) {
req := model.SecondBoardProfitRequest{
TradeStartDate: firstNonEmptyQuery(c, "tradeStartDate", "trade_start_date", "start_date"),
TradeEndDate: firstNonEmptyQuery(c, "tradeEndDate", "trade_end_date", "end_date"),
TsCode: firstNonEmptyQuery(c, "tsCode", "ts_code"),
Symbol: firstNonEmptyQuery(c, "symbol", "code"),
}
if req.TradeStartDate == "" {
req.TradeStartDate = defaultSecondProfitStartDate
}
if req.TradeEndDate == "" {
req.TradeEndDate = time.Now().Format("2006-01-02")
}
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
return req, err
}
if requireCode && req.TsCode == "" && req.Symbol == "" {
return req, errors.New("tsCode 或 symbol 参数不能为空")
}
return req, nil
}
+131
View File
@@ -15,6 +15,20 @@ type LimitBreakNextPerformanceRequest struct {
BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"` // 可选过滤:炸板时间是否早于 11:00。
}
// SecondCalRequest 是二板日历接口的日期范围请求。
type SecondCalRequest struct {
TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` // 查询起始 T 日。
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日。
}
// SecondBoardProfitRequest 是二板获利综合和详情接口的日期范围请求。
type SecondBoardProfitRequest struct {
TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` // 查询起始 T 日,默认 2020-01-01。
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日,默认当天。
TsCode string `form:"tsCode" json:"ts_code"` // 可选 TuShare 股票代码,详情接口必填。
Symbol string `form:"symbol" json:"symbol"` // 可选市场股票代码,详情接口可替代 tsCode。
}
// LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。
type LimitBoardStock struct {
Symbol string `json:"symbol"` // 市场展示用股票代码。
@@ -134,6 +148,76 @@ type LimitBreakNextPerformanceStock struct {
NextClosePctChg *float64 `json:"next_close_pct_chg"` // T+1 收盘相对 T 日收盘的收益率。
}
// SecondCalStock 表示二板日历中单只二板股票的 T 日和 T+1 表现。
type SecondCalStock struct {
TradeDate string `json:"trade_date"` // T 日交易日。
PreviousTradeDate string `json:"previous_trade_date"` // T-1 交易日。
PreviousTwoTradeDate string `json:"previous_two_trade_date"` // T-2 交易日。
NextTradeDate string `json:"next_trade_date"` // T+1 交易日。
Symbol string `json:"symbol"` // 股票代码。
TsCode string `json:"ts_code"` // TuShare 股票代码。
Name string `json:"name"` // 股票简称。
Market string `json:"market"` // 所属板块,来自 stock_basic.market。
Industry string `json:"industry"` // 所属行业,来自 stock_basic.industry。
PreviousClose *float64 `json:"previous_close"` // T-1 收盘价。
LimitPrice *float64 `json:"limit_price"` // 按 T-1 收盘价和板块涨跌幅规则计算的 T 日涨停价。
THigh *float64 `json:"t_high"` // T 日最高价。
TClose *float64 `json:"t_close"` // T 日收盘价。
TPctChg *float64 `json:"t_pct_chg"` // T 日收盘涨跌幅。
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 日最高价。
NextClose *float64 `json:"next_close"` // T+1 日收盘价。
NextIntradayProfitable *bool `json:"next_intraday_profitable"` // T+1 盘中最高价是否高于 T 日最高价。
NextHighProfitPct *float64 `json:"next_high_profit_pct"` // T+1 最高价相对 T 日最高价的收益率。
}
// SecondBoardProfitSummaryItem 表示单只股票二板后的次日最大获利汇总。
type SecondBoardProfitSummaryItem struct {
Symbol string `json:"symbol"` // 股票代码。
TsCode string `json:"ts_code"` // TuShare 股票代码。
Name string `json:"name"` // 股票简称。
Market string `json:"market"` // 所属板块。
Industry string `json:"industry"` // 所属行业。
SecondBoardCount int `json:"second_board_count"` // 09:31:00-09:59:59 期间命中的二板次数。
LimitCount int `json:"limit_count"` // 二板封板次数。
BreakCount int `json:"break_count"` // 二板炸板次数。
ProfitDataCount int `json:"profit_data_count"` // 具备 T+1 最大获利百分比的次数。
TotalNextHighProfitPct float64 `json:"total_next_high_profit_pct"` // T+1 最大获利百分比累计值。
AverageNextHighProfitPct float64 `json:"average_next_high_profit_pct"` // T+1 最大获利百分比平均值。
FirstTradeDate string `json:"first_trade_date"` // 首次命中交易日。
LastTradeDate string `json:"last_trade_date"` // 最近命中交易日。
}
// SecondBoardProfitSummaryResponse 是二板获利综合接口响应。
type SecondBoardProfitSummaryResponse struct {
TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。
TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。
StartTime string `json:"start_time"` // 二板首次涨停时间筛选起点。
EndTime string `json:"end_time"` // 二板首次涨停时间筛选终点。
Total int `json:"total"` // 返回的股票数量。
List []SecondBoardProfitSummaryItem `json:"list"` // 按累计获利从高到低排序。
}
// SecondBoardProfitDetailResponse 是单只股票二板获利详情接口响应。
type SecondBoardProfitDetailResponse struct {
TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。
TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。
StartTime string `json:"start_time"` // 二板首次涨停时间筛选起点。
EndTime string `json:"end_time"` // 二板首次涨停时间筛选终点。
Symbol string `json:"symbol"` // 股票代码。
TsCode string `json:"ts_code"` // TuShare 股票代码。
Name string `json:"name"` // 股票简称。
Market string `json:"market"` // 所属板块。
Industry string `json:"industry"` // 所属行业。
SecondBoardCount int `json:"second_board_count"` // 命中二板次数。
ProfitDataCount int `json:"profit_data_count"` // 具备 T+1 最大获利百分比的次数。
TotalNextHighProfitPct float64 `json:"total_next_high_profit_pct"` // T+1 最大获利百分比累计值。
AverageNextHighProfitPct float64 `json:"average_next_high_profit_pct"` // T+1 最大获利百分比平均值。
List []SecondCalStock `json:"list"` // 每次二板获利详情。
}
// LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。
type LimitOpenCompareGroup struct {
Key string `json:"key"` // 分组唯一标识。
@@ -173,6 +257,23 @@ type LimitBreakNextPerformanceSummary struct {
NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 行情数据的股票数量。
}
// SecondCalSummary 汇总日期范围内所有二板日历指标。
type SecondCalSummary struct {
TradeDayCount int `json:"trade_day_count"` // 返回的交易日数量。
SecondBoardCount int `json:"second_board_count"` // 触及二板涨停价的股票总数。
LimitCount int `json:"limit_count"` // 二板中封住涨停的股票数量。
BreakCount int `json:"break_count"` // 二板中炸板的股票数量。
NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 最高价和 T 日最高价的股票数量。
NextIntradayProfitCount int `json:"next_intraday_profit_count"` // T+1 最高价高于 T 日最高价的股票数量。
NextIntradayProfitRate float64 `json:"next_intraday_profit_rate"` // T+1 盘中盈利概率,百分比。
LimitNextDataCount int `json:"limit_next_data_count"` // T 日封板股票中具备 T+1 盈利判断数据的数量。
LimitNextProfitCount int `json:"limit_next_profit_count"` // T 日封板股票中 T+1 盘中盈利数量。
LimitNextProfitRate float64 `json:"limit_next_profit_rate"` // T 日封板股票的 T+1 盘中盈利概率,百分比。
BreakNextDataCount int `json:"break_next_data_count"` // T 日炸板股票中具备 T+1 盈利判断数据的数量。
BreakNextProfitCount int `json:"break_next_profit_count"` // T 日炸板股票中 T+1 盘中盈利数量。
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // T 日炸板股票的 T+1 盘中盈利概率,百分比。
}
// LimitOpenCompareResponse 是开盘价低于前收接口响应。
type LimitOpenCompareResponse struct {
TradeDate string `json:"trade_date"` // 查询交易日。
@@ -194,6 +295,36 @@ type LimitBreakNextPerformanceResponse struct {
List []LimitBreakNextPerformanceStock `json:"list"` // 当前页记录。
}
// SecondCalDay 表示二板日历中单个交易日的聚合统计。
type SecondCalDay struct {
TradeDate string `json:"trade_date"` // T 日交易日。
PreviousTradeDate string `json:"previous_trade_date"` // T-1 交易日。
PreviousTwoTradeDate string `json:"previous_two_trade_date"` // T-2 交易日。
NextTradeDate string `json:"next_trade_date"` // T+1 交易日。
SecondBoardCount int `json:"second_board_count"` // 当日触及二板涨停价的股票数量。
LimitCount int `json:"limit_count"` // 当日二板封板数量。
BreakCount int `json:"break_count"` // 当日二板炸板数量。
NextTradeDataCount int `json:"next_trade_data_count"` // 当日具备 T+1 盈利判断数据的股票数量。
NextIntradayProfitCount int `json:"next_intraday_profit_count"` // 当日 T+1 最高价高于 T 日最高价数量。
NextIntradayProfitRate float64 `json:"next_intraday_profit_rate"` // 当日 T+1 盘中盈利概率,百分比。
LimitNextDataCount int `json:"limit_next_data_count"` // 当日封板股中具备 T+1 盈利判断数据的数量。
LimitNextProfitCount int `json:"limit_next_profit_count"` // 当日封板股 T+1 盘中盈利数量。
LimitNextProfitRate float64 `json:"limit_next_profit_rate"` // 当日封板股 T+1 盘中盈利概率,百分比。
BreakNextDataCount int `json:"break_next_data_count"` // 当日炸板股中具备 T+1 盈利判断数据的数量。
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
TotalStocks []SecondCalStock `json:"total_stocks"` // 涨停和炸板合并后按首次涨停时间排序的明细。
}
// SecondCalResponse 是二板日历接口响应。
type SecondCalResponse struct {
TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。
TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。
Summary SecondCalSummary `json:"summary"` // 日期范围汇总统计。
List []SecondCalDay `json:"list"` // 按交易日升序排列的日历统计。
}
// LimitOpenThresholdResponse 是开盘价阈值分组接口响应。
type LimitOpenThresholdResponse struct {
TradeDate string `json:"trade_date"` // 查询交易日。
+115
View File
@@ -0,0 +1,115 @@
package router
import (
"bytes"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
const (
responseCacheTTL = 5 * time.Minute
responseCacheMaxBody = 4 << 20
responseCacheMaxKeys = 512
)
type responseCacheEntry struct {
status int
contentType string
body []byte
expiresAt time.Time
}
type responseCacheStore struct {
mu sync.RWMutex
items map[string]responseCacheEntry
}
func newResponseCacheStore() *responseCacheStore {
return &responseCacheStore{items: make(map[string]responseCacheEntry)}
}
func (s *responseCacheStore) get(key string) (responseCacheEntry, bool) {
now := time.Now()
s.mu.RLock()
entry, ok := s.items[key]
s.mu.RUnlock()
if !ok {
return responseCacheEntry{}, false
}
if now.After(entry.expiresAt) {
s.mu.Lock()
if current, exists := s.items[key]; exists && now.After(current.expiresAt) {
delete(s.items, key)
}
s.mu.Unlock()
return responseCacheEntry{}, false
}
return entry, true
}
func (s *responseCacheStore) set(key string, entry responseCacheEntry) {
s.mu.Lock()
if len(s.items) >= responseCacheMaxKeys {
now := time.Now()
for cacheKey, current := range s.items {
if now.After(current.expiresAt) || len(s.items) >= responseCacheMaxKeys {
delete(s.items, cacheKey)
}
}
}
s.items[key] = entry
s.mu.Unlock()
}
type cachedResponseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w cachedResponseWriter) Write(data []byte) (int, error) {
w.body.Write(data)
return w.ResponseWriter.Write(data)
}
func cacheableGETResponseMiddleware(store *responseCacheStore) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method != http.MethodGet || strings.HasPrefix(c.Request.URL.Path, "/api/v1/download/") {
c.Next()
return
}
key := c.Request.URL.RequestURI()
if entry, ok := store.get(key); ok {
c.Header("Content-Type", entry.contentType)
c.Data(entry.status, entry.contentType, entry.body)
return
}
body := bytes.NewBuffer(nil)
writer := cachedResponseWriter{ResponseWriter: c.Writer, body: body}
c.Writer = writer
c.Next()
status := c.Writer.Status()
if status != http.StatusOK || body.Len() == 0 || body.Len() > responseCacheMaxBody {
return
}
contentType := c.Writer.Header().Get("Content-Type")
if !strings.Contains(contentType, "application/json") {
return
}
store.set(key, responseCacheEntry{
status: status,
contentType: contentType,
body: append([]byte(nil), body.Bytes()...),
expiresAt: time.Now().Add(responseCacheTTL),
})
}
}
+6 -1
View File
@@ -9,7 +9,8 @@ import (
//
// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
r := gin.Default()
r := gin.New()
r.Use(gin.Recovery())
// 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。
r.Use(func(c *gin.Context) {
@@ -23,6 +24,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
}
c.Next()
})
r.Use(cacheableGETResponseMiddleware(newResponseCacheStore()))
v1 := r.Group("/api/v1")
{
@@ -37,6 +39,9 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit)
limitBoard.GET("/second-board-profit-summary", limitBoardApi.GetSecondBoardProfitSummary)
limitBoard.GET("/second-board-profit-detail", limitBoardApi.GetSecondBoardProfitDetail)
limitBoard.GET("/second-cal", limitBoardApi.GetSecondCal)
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
}
@@ -2,6 +2,7 @@ package service
import (
"math"
"strconv"
"strings"
"level_go/internal/model"
@@ -104,16 +105,22 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB
// 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。
func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) {
query := `
WITH open_calendar AS (
WITH calendar AS (
SELECT
cal_date AS trade_date,
LAG(cal_date, 1) OVER (ORDER BY cal_date) AS previous_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, 1) OVER (ORDER BY cal_date) AS next_trade_date
LEAD(cal_date) OVER (ORDER BY cal_date) AS next_trade_date
FROM trade_calendar
WHERE exchange = 'SSE' AND is_open = 1
),
candidate AS (
calendar_window AS (
SELECT *
FROM calendar
WHERE trade_date BETWEEN ? AND ?
)
SELECT *
FROM (
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,
@@ -140,7 +147,7 @@ candidate AS (
nd.high AS next_high,
nd.low AS next_low,
nd.close AS next_close
FROM open_calendar 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
@@ -154,14 +161,11 @@ candidate AS (
LEFT JOIN stock_daily nd
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date
LEFT JOIN stock_basic sb
ON sb.ts_code = l.ts_code COLLATE utf8mb4_unicode_ci
WHERE c.trade_date BETWEEN ? AND ?
AND c.previous_trade_date IS NOT NULL
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
)
SELECT *
FROM candidate
) candidate
WHERE previous_close IS NOT NULL
AND t_close IS NOT NULL
ORDER BY trade_date ASC, ts_code ASC`
@@ -193,7 +197,7 @@ func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model
if row.FirstTime != nil {
item.FirstTime = formatLimitTime(*row.FirstTime)
}
item.BreakTimeBefore1100 = isLimitBreakTimeBeforeEleven(item.FirstTime)
item.BreakTimeBefore1100 = isLimitBreakTimeBeforeTen(item.FirstTime)
if row.TPreClose != nil && *row.TPreClose > 0 {
// T 日开盘涨幅以 T 日前收为基准,低于 101% 用于筛选低开/小幅高开炸板。
item.TOpenPctChg = pctChangePtr(row.TOpen, *row.TPreClose)
@@ -251,12 +255,38 @@ func formatLimitTime(value string) string {
return timeText[0:2] + ":" + timeText[2:4] + ":" + timeText[4:6]
}
// isLimitBreakTimeBeforeEleven 判断炸板时间是否早于 11:00。
// isLimitBreakTimeBeforeTen 判断炸板时间是否早于 10:00。
//
// 空时间在当前业务中视为满足条件,保持历史接口行为。
func isLimitBreakTimeBeforeEleven(limitTime string) bool {
func isLimitBreakTimeBeforeTen(limitTime string) bool {
if strings.TrimSpace(limitTime) == "" {
return true
}
return isBeforeEleven(&limitTime)
timeText := formatLimitTime(limitTime)
parts := strings.Split(timeText, ":")
if len(parts) < 2 || len(parts) > 3 {
return false
}
hour, err := strconv.Atoi(parts[0])
if err != nil {
return false
}
minute, err := strconv.Atoi(parts[1])
if err != nil {
return false
}
second := 0
if len(parts) == 3 {
second, err = strconv.Atoi(parts[2])
if err != nil {
return false
}
}
if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return false
}
return hour < 10
}
@@ -1,66 +0,0 @@
package service
import "testing"
// TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh 验证 T+1 盘中可盈利口径。
//
// 业务要求用 T+1 最高价覆盖 T 日最高价来判断是否可解套,而不是只和 T 日收盘价比较。
func TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh(t *testing.T) {
tClose := 9.50
tHigh := 10.00
tests := []struct {
name string
nextHigh *float64
want *bool
}{
{
name: "next high below T high cannot cover T high buy loss",
nextHigh: float64Ptr(9.99),
want: boolPtr(false),
},
{
name: "next high equal T high covers T high buy loss",
nextHigh: float64Ptr(10.00),
want: boolPtr(true),
},
{
name: "missing next high returns nil",
nextHigh: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
item := buildLimitBreakNextPerformanceStock(limitBreakNextPerformanceRow{
TClose: &tClose,
THigh: &tHigh,
NextHigh: tt.nextHigh,
})
if tt.want == nil {
if item.NextIntradayProfitable != nil {
t.Fatalf("NextIntradayProfitable = %v, want nil", *item.NextIntradayProfitable)
}
return
}
if item.NextIntradayProfitable == nil {
t.Fatalf("NextIntradayProfitable = nil, want %v", *tt.want)
}
if *item.NextIntradayProfitable != *tt.want {
t.Fatalf("NextIntradayProfitable = %v, want %v", *item.NextIntradayProfitable, *tt.want)
}
})
}
}
// float64Ptr 是测试用指针辅助函数。
func float64Ptr(value float64) *float64 {
return &value
}
// boolPtr 是测试用指针辅助函数。
func boolPtr(value bool) *bool {
return &value
}
@@ -0,0 +1,257 @@
package service // 声明 service 包,保持与涨停板服务层一致。
import ( // 引入本文件需要的标准库和项目包。
"strings" // 用于清理请求中的股票代码空格。
"level_go/internal/model" // 引入接口请求和响应模型。
)
const ( // 定义二板获利缓存接口使用的常量。
secondBoardProfitStartTime = "09:31:00" // 固定的二板首次涨停时间筛选起点。
secondBoardProfitEndTime = "09:59:59" // 固定的二板首次涨停时间筛选终点。
)
type secondBoardProfitCacheRange struct { // 表示明细缓存表当前覆盖的交易日期范围。
StartDate string `gorm:"column:start_date"` // 缓存明细最早交易日,格式 YYYY-MM-DD。
EndDate string `gorm:"column:end_date"` // 缓存明细最晚交易日,格式 YYYY-MM-DD。
}
type secondBoardProfitDetailCacheRow struct { // 表示从二板获利详情缓存表扫描出的单行数据。
TradeDate string `gorm:"column:trade_date"` // T 日交易日。
PreviousTradeDate string `gorm:"column:previous_trade_date"` // T-1 交易日。
PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"` // T-2 交易日。
NextTradeDate string `gorm:"column:next_trade_date"` // T+1 交易日。
Symbol string `gorm:"column:symbol"` // 股票代码。
TsCode string `gorm:"column:ts_code"` // TuShare 股票代码。
Name string `gorm:"column:name"` // 股票简称。
Market string `gorm:"column:market"` // 所属板块。
Industry string `gorm:"column:industry"` // 所属行业。
PreviousClose *float64 `gorm:"column:previous_close"` // T-1 收盘价。
LimitPrice *float64 `gorm:"column:limit_price"` // T 日涨停价。
THigh *float64 `gorm:"column:t_high"` // T 日最高价。
TClose *float64 `gorm:"column:t_close"` // T 日收盘价。
TPctChg *float64 `gorm:"column:t_pct_chg"` // T 日涨跌幅。
Status string `gorm:"column:status"` // 二板状态,limit 表示封板,break 表示炸板。
FirstLimitTime string `gorm:"column:first_limit_time"` // T 日二板首次涨停时间。
OpenTimes *int `gorm:"column:open_times"` // T 日开板次数。
NextHigh *float64 `gorm:"column:next_high"` // T+1 最高价。
NextClose *float64 `gorm:"column:next_close"` // T+1 收盘价。
NextIntradayProfitable *int `gorm:"column:next_intraday_profitable"` // T+1 盘中是否盈利,数据库中为 0/1/null。
NextHighProfitPct *float64 `gorm:"column:next_high_profit_pct"` // T+1 最大获利百分比。
}
// GetSecondBoardProfitSummary 返回二板获利综合数据。
func (s *LimitBoardService) GetSecondBoardProfitSummary(req model.SecondBoardProfitRequest) (model.SecondBoardProfitSummaryResponse, error) { // 定义二板获利综合服务方法。
resp := model.SecondBoardProfitSummaryResponse{ // 初始化响应结构。
TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。
TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。
StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。
EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。
List: []model.SecondBoardProfitSummaryItem{}, // 初始化空列表,避免 JSON 返回 null。
}
useSummaryCache, err := s.shouldUseSecondBoardProfitSummaryCache(req) // 判断当前日期范围是否可以直接使用汇总缓存表。
if err != nil { // 判断缓存范围查询是否失败。
return resp, err // 返回已初始化响应和数据库错误。
}
if useSummaryCache { // 请求范围覆盖整张明细缓存表时,直接读取预聚合汇总缓存表。
return s.loadSecondBoardProfitSummaryCache(resp) // 从 summary cache 加载结果并返回。
}
return s.loadSecondBoardProfitSummaryFromDetail(req, resp) // 窄日期范围从 detail cache 动态聚合,保持日期筛选语义。
}
// GetSecondBoardProfitDetail 返回单只股票二板获利详情数据。
func (s *LimitBoardService) GetSecondBoardProfitDetail(req model.SecondBoardProfitRequest) (model.SecondBoardProfitDetailResponse, error) { // 定义个股二板详情服务方法。
resp := model.SecondBoardProfitDetailResponse{ // 初始化响应结构。
TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。
TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。
StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。
EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。
Symbol: strings.TrimSpace(req.Symbol), // 回显请求中的股票代码。
TsCode: strings.TrimSpace(req.TsCode), // 回显请求中的 TuShare 股票代码。
List: []model.SecondCalStock{}, // 初始化空列表,避免 JSON 返回 null。
}
rows, err := s.loadSecondBoardProfitDetailRows(req) // 从详情缓存表读取个股明细行。
if err != nil { // 判断数据库查询是否失败。
return resp, err // 返回已初始化响应和数据库错误。
}
for _, row := range rows { // 遍历缓存明细行并构建响应。
stock := buildSecondBoardProfitStockFromCache(row) // 将缓存行转换为原接口复用的股票明细结构。
if resp.SecondBoardCount == 0 { // 第一条命中记录用于补齐个股基础信息。
resp.Symbol = stock.Symbol // 设置响应股票代码。
resp.TsCode = stock.TsCode // 设置响应 TuShare 股票代码。
resp.Name = stock.Name // 设置响应股票简称。
resp.Market = stock.Market // 设置响应所属板块。
resp.Industry = stock.Industry // 设置响应所属行业。
}
resp.SecondBoardCount++ // 累加命中的二板次数。
if stock.NextHighProfitPct != nil { // 只统计具备 T+1 最大获利百分比的数据。
resp.ProfitDataCount++ // 累加具备收益数据的次数。
resp.TotalNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct + *stock.NextHighProfitPct) // 累加最大获利百分比。
}
resp.List = append(resp.List, stock) // 追加单次二板详情。
}
if resp.ProfitDataCount > 0 { // 判断是否存在可计算平均值的数据。
resp.AverageNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct / float64(resp.ProfitDataCount)) // 计算平均最大获利百分比。
}
return resp, nil // 返回详情响应。
}
// shouldUseSecondBoardProfitSummaryCache 判断是否可以使用预聚合汇总缓存表。
func (s *LimitBoardService) shouldUseSecondBoardProfitSummaryCache(req model.SecondBoardProfitRequest) (bool, error) { // 定义汇总缓存使用判断函数。
cacheRange, err := s.loadSecondBoardProfitCacheRange() // 查询明细缓存表当前覆盖日期范围。
if err != nil { // 判断缓存范围查询是否失败。
return false, err // 查询失败时返回错误。
}
if cacheRange.StartDate == "" || cacheRange.EndDate == "" { // 判断缓存表是否为空。
return false, nil // 缓存为空时不能使用汇总缓存。
}
return req.TradeStartDate <= cacheRange.StartDate && req.TradeEndDate >= cacheRange.EndDate, nil // 请求覆盖缓存全范围时使用汇总缓存。
}
// loadSecondBoardProfitCacheRange 查询二板获利详情缓存表的日期覆盖范围。
func (s *LimitBoardService) loadSecondBoardProfitCacheRange() (secondBoardProfitCacheRange, error) { // 定义缓存日期范围查询函数。
// 定义查询缓存范围 SQL。
query := `
SELECT
COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS start_date,
COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS end_date
FROM second_board_profit_detail_cache` // 从明细缓存表读取最小和最大交易日。
var cacheRange secondBoardProfitCacheRange // 声明缓存范围结果变量。
err := s.DB.Raw(query).Scan(&cacheRange).Error // 执行 SQL 并扫描结果。
return cacheRange, err // 返回缓存范围和可能的错误。
}
// loadSecondBoardProfitSummaryCache 从预聚合汇总缓存表读取完整汇总数据。
func (s *LimitBoardService) loadSecondBoardProfitSummaryCache(resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义汇总缓存读取函数。
// 定义汇总缓存查询 SQL。
query := `
SELECT
symbol,
ts_code,
name,
market,
industry,
second_board_count,
limit_count,
break_count,
profit_data_count,
total_next_high_profit_pct,
average_next_high_profit_pct,
COALESCE(DATE_FORMAT(first_trade_date, '%Y-%m-%d'), '') AS first_trade_date,
COALESCE(DATE_FORMAT(last_trade_date, '%Y-%m-%d'), '') AS last_trade_date
FROM second_board_profit_summary_cache
ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 按累计获利、数据次数和股票代码稳定排序。
if err := s.DB.Raw(query).Scan(&resp.List).Error; err != nil { // 执行汇总缓存查询。
return resp, err // 查询失败时返回错误。
}
resp.Total = len(resp.List) // 设置返回股票数量。
return resp, nil // 返回汇总响应。
}
// loadSecondBoardProfitSummaryFromDetail 从明细缓存表按日期范围动态聚合汇总数据。
func (s *LimitBoardService) loadSecondBoardProfitSummaryFromDetail(req model.SecondBoardProfitRequest, resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义明细聚合函数。
// 定义日期范围聚合 SQL。
query := `
SELECT
MAX(symbol) AS symbol,
ts_code,
MAX(name) AS name,
MAX(market) AS market,
MAX(industry) AS industry,
COUNT(*) AS second_board_count,
SUM(CASE WHEN status = 'limit' THEN 1 ELSE 0 END) AS limit_count,
SUM(CASE WHEN status = 'break' THEN 1 ELSE 0 END) AS break_count,
COUNT(next_high_profit_pct) AS profit_data_count,
ROUND(SUM(COALESCE(next_high_profit_pct, 0)), 2) AS total_next_high_profit_pct,
COALESCE(ROUND(SUM(COALESCE(next_high_profit_pct, 0)) / NULLIF(COUNT(next_high_profit_pct), 0), 2), 0) AS average_next_high_profit_pct,
COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS first_trade_date,
COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS last_trade_date
FROM second_board_profit_detail_cache
WHERE trade_date BETWEEN ? AND ?
GROUP BY ts_code
ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 在明细缓存表内按请求日期范围聚合。
if err := s.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&resp.List).Error; err != nil { // 执行聚合查询。
return resp, err // 查询失败时返回错误。
}
resp.Total = len(resp.List) // 设置返回股票数量。
return resp, nil // 返回汇总响应。
}
// loadSecondBoardProfitDetailRows 从明细缓存表读取单只股票详情。
func (s *LimitBoardService) loadSecondBoardProfitDetailRows(req model.SecondBoardProfitRequest) ([]secondBoardProfitDetailCacheRow, error) { // 定义明细缓存查询函数。
tsCode := strings.TrimSpace(req.TsCode) // 清理 TuShare 股票代码参数。
symbol := strings.TrimSpace(req.Symbol) // 清理市场股票代码参数。
// 定义明细缓存查询 SQL。
query := `
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,
symbol,
ts_code,
name,
market,
industry,
previous_close,
limit_price,
t_high,
t_close,
t_pct_chg,
status,
first_limit_time,
open_times,
next_high,
next_close,
next_intraday_profitable,
next_high_profit_pct
FROM second_board_profit_detail_cache
WHERE trade_date BETWEEN ? AND ?` // 先按日期范围过滤明细缓存。
args := []interface{}{req.TradeStartDate, req.TradeEndDate} // 初始化 SQL 参数为日期范围。
if tsCode != "" { // 优先按 TuShare 股票代码查询。
query += " AND ts_code = ?" // 追加 TuShare 股票代码条件。
args = append(args, tsCode) // 追加 TuShare 股票代码参数。
} else { // 未传 TuShare 股票代码时使用市场股票代码。
query += " AND symbol = ?" // 追加市场股票代码条件。
args = append(args, symbol) // 追加市场股票代码参数。
}
query += " ORDER BY trade_date ASC, ts_code ASC" // 追加稳定排序条件。
var rows []secondBoardProfitDetailCacheRow // 声明明细缓存扫描结果。
err := s.DB.Raw(query, args...).Scan(&rows).Error // 执行 SQL 并扫描结果。
return rows, err // 返回明细行和可能的错误。
}
// buildSecondBoardProfitStockFromCache 将缓存行转换为接口复用的二板股票明细模型。
func buildSecondBoardProfitStockFromCache(row secondBoardProfitDetailCacheRow) model.SecondCalStock { // 定义缓存行到响应模型的转换函数。
item := model.SecondCalStock{ // 初始化二板股票明细。
TradeDate: row.TradeDate, // 设置 T 日交易日。
PreviousTradeDate: row.PreviousTradeDate, // 设置 T-1 交易日。
PreviousTwoTradeDate: row.PreviousTwoTradeDate, // 设置 T-2 交易日。
NextTradeDate: row.NextTradeDate, // 设置 T+1 交易日。
Symbol: row.Symbol, // 设置股票代码。
TsCode: row.TsCode, // 设置 TuShare 股票代码。
Name: row.Name, // 设置股票简称。
Market: row.Market, // 设置所属板块。
Industry: row.Industry, // 设置所属行业。
PreviousClose: row.PreviousClose, // 设置 T-1 收盘价。
LimitPrice: row.LimitPrice, // 设置 T 日涨停价。
THigh: row.THigh, // 设置 T 日最高价。
TClose: row.TClose, // 设置 T 日收盘价。
TPctChg: row.TPctChg, // 设置 T 日涨跌幅。
Status: row.Status, // 设置二板状态。
FirstLimitTime: row.FirstLimitTime, // 设置首次涨停时间。
OpenTimes: row.OpenTimes, // 设置开板次数。
NextHigh: row.NextHigh, // 设置 T+1 最高价。
NextClose: row.NextClose, // 设置 T+1 收盘价。
NextHighProfitPct: row.NextHighProfitPct, // 设置 T+1 最大获利百分比。
}
if row.NextIntradayProfitable != nil { // 判断缓存中是否存在盈利布尔数据。
nextIntradayProfitable := *row.NextIntradayProfitable != 0 // 将 0/1 转换为 bool。
item.NextIntradayProfitable = &nextIntradayProfitable // 设置 T+1 盘中是否盈利。
}
return item // 返回转换后的股票明细。
}
+353
View File
@@ -0,0 +1,353 @@
package service
import (
"math"
"sort"
"strings"
"level_go/internal/model"
)
// secondCalRow 是二板日历 SQL 查询扫描出的单股基础数据。
type secondCalRow struct {
TradeDate string `gorm:"column:trade_date"` // T 日交易日。
PreviousTradeDate string `gorm:"column:previous_trade_date"` // T-1 交易日。
PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"` // T-2 交易日。
NextTradeDate string `gorm:"column:next_trade_date"` // T+1 交易日。
Symbol string `gorm:"column:symbol"` // 股票代码。
TsCode string `gorm:"column:ts_code"` // TuShare 股票代码。
Name string `gorm:"column:name"` // 股票简称。
Market string `gorm:"column:market"` // 所属板块。
Industry string `gorm:"column:industry"` // 所属行业。
PreviousClose *float64 `gorm:"column:previous_close"` // T-1 收盘价。
LimitPrice *float64 `gorm:"column:limit_price"` // T 日涨停价。
THigh *float64 `gorm:"column:t_high"` // T 日最高价。
TClose *float64 `gorm:"column:t_close"` // T 日收盘价。
TPctChg *float64 `gorm:"column:t_pct_chg"` // 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 日最高价。
NextClose *float64 `gorm:"column:next_close"` // T+1 日收盘价。
}
// secondCalDaySeed 是日期范围内每个交易日的前后交易日信息。
type secondCalDaySeed struct {
TradeDate string `gorm:"column:trade_date"` // T 日交易日。
PreviousTradeDate string `gorm:"column:previous_trade_date"` // T-1 交易日。
PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"` // T-2 交易日。
NextTradeDate string `gorm:"column:next_trade_date"` // T+1 交易日。
}
// GetSecondCal 返回按交易日聚合的二板日历统计。
func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.SecondCalResponse, error) {
resp := model.SecondCalResponse{
TradeStartDate: req.TradeStartDate,
TradeEndDate: req.TradeEndDate,
List: []model.SecondCalDay{},
}
// 先加载完整交易日骨架,确保没有二板的交易日也能出现在日历中。
days, dayIndex, err := s.loadSecondCalDays(req.TradeStartDate, req.TradeEndDate)
if err != nil {
return resp, err
}
for _, day := range days {
resp.List = append(resp.List, model.SecondCalDay{
TradeDate: day.TradeDate,
PreviousTradeDate: day.PreviousTradeDate,
PreviousTwoTradeDate: day.PreviousTwoTradeDate,
NextTradeDate: day.NextTradeDate,
Stocks: []model.SecondCalStock{},
TotalStocks: []model.SecondCalStock{},
})
}
// 再加载命中二板口径的单股明细,并累加到对应交易日。
rows, err := s.loadSecondCalRows(req.TradeStartDate, req.TradeEndDate)
if err != nil {
return resp, err
}
for _, row := range rows {
item := buildSecondCalStock(row)
idx, ok := dayIndex[item.TradeDate]
if !ok {
continue
}
accumulateSecondCalDay(&resp.List[idx], item)
}
// 最后汇总整个日期范围,并补齐各交易日的概率字段。
resp.Summary.TradeDayCount = len(resp.List)
for i := range resp.List {
finalizeSecondCalDay(&resp.List[i])
accumulateSecondCalSummary(&resp.Summary, resp.List[i])
}
finalizeSecondCalSummary(&resp.Summary)
return resp, nil
}
// loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。
func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) {
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
}
// 构建交易日到响应数组下标的映射,后续聚合明细时可以 O(1) 定位。
dayIndex := make(map[string]int, len(days))
for i, day := range days {
dayIndex[day.TradeDate] = i
}
return days, dayIndex, nil
}
// loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。
func (s *LimitBoardService) loadSecondCalRows(startDate, endDate string) ([]secondCalRow, error) {
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,
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,
first_limit_time,
open_times,
next_high,
next_close
FROM (
SELECT
c.trade_date,
c.previous_trade_date,
c.previous_two_trade_date,
c.next_trade_date,
d.symbol,
pl.ts_code,
COALESCE(NULLIF(sb.name, ''), NULLIF(pl.name, ''), '') AS name,
COALESCE(NULLIF(sb.market, ''), '') AS market,
COALESCE(NULLIF(sb.industry, ''), NULLIF(pl.industry, ''), '') AS industry,
COALESCE(d.pre_close, pl.close) AS previous_close,
ROUND(
COALESCE(d.pre_close, pl.close) *
CASE
WHEN COALESCE(sb.market, '') = '北交所' THEN 1.30
WHEN COALESCE(sb.market, '') IN ('创业板', '科创板') THEN 1.20
WHEN COALESCE(sb.name, pl.name, '') LIKE '%ST%' THEN 1.05
ELSE 1.10
END,
2
) AS limit_price,
d.high AS t_high,
d.close AS t_close,
d.pct_chg AS t_pct_chg,
CASE
WHEN ABS(d.close - d.high) < 0.0001 THEN 'limit'
WHEN d.close < d.high THEN 'break'
ELSE 'unknown'
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.close AS next_close
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 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 d.ts_code = pl.ts_code AND d.trade_date = c.trade_date
LEFT JOIN stock_daily nd
ON nd.ts_code = pl.ts_code AND nd.trade_date = c.next_trade_date
LEFT JOIN limit_list_d tlu
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 tlz.ts_code = pl.ts_code AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z'
LEFT JOIN stock_basic sb
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
AND d.high IS NOT NULL
AND d.close IS NOT NULL
) candidate
WHERE previous_close IS NOT NULL
AND limit_price IS NOT NULL
AND limit_price > 0
AND t_high >= limit_price - 0.0001
AND status IN ('limit', 'break')
ORDER BY trade_date ASC, ts_code ASC`
var rows []secondCalRow
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
return rows, err
}
// buildSecondCalStock 把数据库行转换成接口明细,并计算 T+1 盘中收益。
func buildSecondCalStock(row secondCalRow) model.SecondCalStock {
item := model.SecondCalStock{
TradeDate: row.TradeDate,
PreviousTradeDate: row.PreviousTradeDate,
PreviousTwoTradeDate: row.PreviousTwoTradeDate,
NextTradeDate: row.NextTradeDate,
Symbol: row.Symbol,
TsCode: row.TsCode,
Name: row.Name,
Market: row.Market,
Industry: row.Industry,
PreviousClose: row.PreviousClose,
LimitPrice: row.LimitPrice,
THigh: row.THigh,
TClose: row.TClose,
TPctChg: row.TPctChg,
Status: row.Status,
OpenTimes: row.OpenTimes,
NextHigh: row.NextHigh,
NextClose: row.NextClose,
}
if row.FirstLimitTime != nil {
item.FirstLimitTime = formatLimitTime(*row.FirstLimitTime)
}
if row.THigh != nil && *row.THigh > 0 && row.NextHigh != nil {
// T+1 盘中盈利统一按 T+1 最高价是否高于 T 日最高价判断。
profitable := *row.NextHigh > *row.THigh
item.NextIntradayProfitable = &profitable
profitPct := roundSecondCalPct((*row.NextHigh - *row.THigh) / *row.THigh * 100)
item.NextHighProfitPct = &profitPct
}
return item
}
// accumulateSecondCalDay 把单只股票累加到对应交易日统计中。
func accumulateSecondCalDay(day *model.SecondCalDay, item model.SecondCalStock) {
day.SecondBoardCount++
switch item.Status {
case "limit":
day.LimitCount++
case "break":
day.BreakCount++
}
if item.NextIntradayProfitable != nil {
day.NextTradeDataCount++
if item.Status == "limit" {
day.LimitNextDataCount++
}
if item.Status == "break" {
day.BreakNextDataCount++
}
if *item.NextIntradayProfitable {
day.NextIntradayProfitCount++
if item.Status == "limit" {
day.LimitNextProfitCount++
}
if item.Status == "break" {
day.BreakNextProfitCount++
}
}
}
day.Stocks = append(day.Stocks, item)
if strings.TrimSpace(item.FirstLimitTime) != "" {
day.TotalStocks = append(day.TotalStocks, item)
}
}
// finalizeSecondCalDay 计算单个交易日的概率字段。
func finalizeSecondCalDay(day *model.SecondCalDay) {
sort.SliceStable(day.TotalStocks, func(i, j int) bool {
if day.TotalStocks[i].FirstLimitTime == day.TotalStocks[j].FirstLimitTime {
return day.TotalStocks[i].TsCode < day.TotalStocks[j].TsCode
}
return day.TotalStocks[i].FirstLimitTime < day.TotalStocks[j].FirstLimitTime
})
day.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount)
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)
}
// accumulateSecondCalSummary 把单日统计累加到日期范围汇总中。
func accumulateSecondCalSummary(summary *model.SecondCalSummary, day model.SecondCalDay) {
summary.SecondBoardCount += day.SecondBoardCount
summary.LimitCount += day.LimitCount
summary.BreakCount += day.BreakCount
summary.NextTradeDataCount += day.NextTradeDataCount
summary.NextIntradayProfitCount += day.NextIntradayProfitCount
summary.LimitNextDataCount += day.LimitNextDataCount
summary.LimitNextProfitCount += day.LimitNextProfitCount
summary.BreakNextDataCount += day.BreakNextDataCount
summary.BreakNextProfitCount += day.BreakNextProfitCount
}
// finalizeSecondCalSummary 计算日期范围汇总的概率字段。
func finalizeSecondCalSummary(summary *model.SecondCalSummary) {
summary.NextIntradayProfitRate = secondCalRate(summary.NextIntradayProfitCount, summary.NextTradeDataCount)
summary.LimitNextProfitRate = secondCalRate(summary.LimitNextProfitCount, summary.LimitNextDataCount)
summary.BreakNextProfitRate = secondCalRate(summary.BreakNextProfitCount, summary.BreakNextDataCount)
}
// secondCalRate 计算百分比概率并保留两位小数。
func secondCalRate(numerator, denominator int) float64 {
if denominator == 0 {
return 0
}
return roundSecondCalPct(float64(numerator) / float64(denominator) * 100)
}
// roundSecondCalPct 对二板日历中的百分比做两位小数舍入。
func roundSecondCalPct(value float64) float64 {
return math.Round(value*100) / 100
}
-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;