Compare commits
6 Commits
61cc29d7b3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a3292c3dd9 | |||
| 507b79f257 | |||
| 49cd4f85df | |||
| 2f36adcb43 | |||
| 94e8a4f770 | |||
| 50b10fa880 |
+6
-1
@@ -10,4 +10,9 @@
|
|||||||
|
|
||||||
# 依赖目录 (如果使用 vendor 模式)
|
# 依赖目录 (如果使用 vendor 模式)
|
||||||
# vendor/
|
# vendor/
|
||||||
config.yaml
|
|
||||||
|
# 本地运行配置,包含数据库账号密码,不提交到仓库
|
||||||
|
/config.yaml
|
||||||
|
/config.yml
|
||||||
|
/config/config.yaml
|
||||||
|
/config/config.yml
|
||||||
|
|||||||
@@ -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` 导出。
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"level_go/internal/api"
|
"level_go/internal/api"
|
||||||
@@ -12,6 +13,8 @@ import (
|
|||||||
|
|
||||||
// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。
|
// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。
|
||||||
func main() {
|
func main() {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
// 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。
|
// 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。
|
||||||
config.InitConfig()
|
config.InitConfig()
|
||||||
|
|
||||||
@@ -21,6 +24,12 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("数据库连接失败:", err)
|
log.Fatal("数据库连接失败:", err)
|
||||||
}
|
}
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("获取数据库连接池失败:", err)
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxIdleConns(20)
|
||||||
|
sqlDB.SetMaxOpenConns(100)
|
||||||
|
|
||||||
// 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。
|
// 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。
|
||||||
commonSrv := &service.CommonService{DB: db}
|
commonSrv := &service.CommonService{DB: db}
|
||||||
|
|||||||
-22
@@ -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
|
|
||||||
@@ -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`);
|
||||||
@@ -1,4 +1,51 @@
|
|||||||
ALTER DATABASE `level` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
-- 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` (
|
CREATE TABLE `limit_list_d` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
@@ -25,54 +72,13 @@ CREATE TABLE `limit_list_d` (
|
|||||||
KEY `idx_trade_date` (`trade_date`),
|
KEY `idx_trade_date` (`trade_date`),
|
||||||
KEY `idx_ts_code` (`ts_code`),
|
KEY `idx_ts_code` (`ts_code`),
|
||||||
KEY `idx_limit` (`limit`)
|
KEY `idx_limit` (`limit`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=154380 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` (
|
CREATE TABLE `stock_limit_combined_data` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
`trade_date` date NOT NULL COMMENT '交易日期',
|
`trade_date` date NOT NULL COMMENT '交易日期',
|
||||||
`symbol` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票代码,以stock_daily为准',
|
`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为准',
|
`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关联基础表为准',
|
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '股票名称,以stock_daily关联基础表为准',
|
||||||
`final_status` tinyint NOT NULL COMMENT '融合状态:1-涨停,2-炸板',
|
`final_status` tinyint NOT NULL COMMENT '融合状态:1-涨停,2-炸板',
|
||||||
`source_count` int NOT NULL DEFAULT '0' COMMENT '命中的数据源数量',
|
`source_count` int NOT NULL DEFAULT '0' COMMENT '命中的数据源数量',
|
||||||
@@ -104,46 +110,7 @@ CREATE TABLE `stock_limit_combined_data` (
|
|||||||
UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`),
|
UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`),
|
||||||
KEY `idx_trade_date` (`trade_date`),
|
KEY `idx_trade_date` (`trade_date`),
|
||||||
KEY `idx_symbol` (`symbol`),
|
KEY `idx_symbol` (`symbol`),
|
||||||
KEY `idx_trade_date_status` (`trade_date`,`final_status`)
|
KEY `idx_trade_date_status` (`trade_date`,`final_status`),
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
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;
|
||||||
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;
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- 服务前缀:`/api/v1`
|
- 服务前缀:`/api/v1`
|
||||||
- 响应格式:`application/json`
|
- 响应格式:`application/json`
|
||||||
- 当前项目接口总数:11
|
- 当前项目接口总数:13
|
||||||
|
|
||||||
## 2. 接口列表
|
## 2. 接口列表
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
| 涨停低开分组 | `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-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/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` | 分页查询 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/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/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/second-board-profit-csv` | 下载筛选条件范围内的二板早盘次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
| `tOpenGainLt101` | `bool` | 否 | 无 | 筛选 T 日开盘价是否小于 T-1 收盘价的 101%,支持别名 `tOpenLessThan101`、`t_open_gain_lt_101` |
|
| `tOpenGainLt101` | `bool` | 否 | 无 | 筛选 T 日开盘价是否小于 T-1 收盘价的 101%,支持别名 `tOpenLessThan101`、`t_open_gain_lt_101` |
|
||||||
| `breakTimeBefore1100` | `bool` | 否 | 无 | 筛选 T 日炸板时间是否在 `11:00:00` 前,空炸板时间按 `true` 处理,支持别名 `tBreakTimeBefore1100`、`break_time_before_1100` |
|
| `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` 作为日期参数别名。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
ALTER DATABASE `level` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `limit_list_d`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `stock_basic`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `stock_daily`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `stock_limit_combined_data`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `trade_calendar`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
ALTER TABLE `stock_limit_data`
|
|
||||||
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
@@ -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": "参数错误"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -63,6 +63,10 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
|
|||||||
|
|
||||||
### 4.2 list[].stocks 明细字段
|
### 4.2 list[].stocks 明细字段
|
||||||
|
|
||||||
|
`list[].stocks` 保留当日全部二板明细。
|
||||||
|
|
||||||
|
`list[].total_stocks` 为当日涨停和炸板合并后的明细列表,字段结构与 `stocks` 相同;该列表会剔除 `first_limit_time` 为空的数据,并按 `first_limit_time` 升序排序,时间相同时按 `ts_code` 升序排序。
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `trade_date` | `string` | T 日 |
|
| `trade_date` | `string` | T 日 |
|
||||||
@@ -121,6 +125,31 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
|
|||||||
"break_next_data_count": 3,
|
"break_next_data_count": 3,
|
||||||
"break_next_profit_count": 3,
|
"break_next_profit_count": 3,
|
||||||
"break_next_profit_rate": 100,
|
"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": [
|
"stocks": [
|
||||||
{
|
{
|
||||||
"trade_date": "2026-05-07",
|
"trade_date": "2026-05-07",
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) {
|
|||||||
// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。
|
// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。
|
||||||
func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) {
|
func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) {
|
||||||
req := model.LimitBreakNextPerformanceRequest{
|
req := model.LimitBreakNextPerformanceRequest{
|
||||||
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
|
TradeStartDate: firstNonEmptyQuery(c, "tradeStartDate", "trade_start_date", "start_date"),
|
||||||
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
|
TradeEndDate: firstNonEmptyQuery(c, "tradeEndDate", "trade_end_date", "end_date"),
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 20,
|
PageSize: 20,
|
||||||
}
|
}
|
||||||
@@ -159,6 +159,16 @@ func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination boo
|
|||||||
return req, nil
|
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 从多个兼容参数名中读取可选布尔值。
|
// parseOptionalBoolQuery 从多个兼容参数名中读取可选布尔值。
|
||||||
//
|
//
|
||||||
// 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。
|
// 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -337,16 +336,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 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
|
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,
|
||||||
@@ -374,28 +378,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 (` + calendarTable + `) c
|
FROM calendar_window 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 ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
|
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
|
||||||
LEFT JOIN stock_limit_combined_data ppl
|
LEFT JOIN stock_limit_combined_data ppl
|
||||||
ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
||||||
LEFT JOIN stock_daily d
|
LEFT JOIN stock_daily d
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
LEFT JOIN stock_daily pd
|
LEFT JOIN stock_daily pd
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = c.previous_trade_date
|
ON l.ts_code = pd.ts_code AND pd.trade_date = c.previous_trade_date
|
||||||
LEFT JOIN stock_daily ppd
|
LEFT JOIN stock_daily ppd
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "ppd.ts_code") + ` AND ppd.trade_date = c.previous_two_trade_date
|
ON l.ts_code = ppd.ts_code AND ppd.trade_date = c.previous_two_trade_date
|
||||||
LEFT JOIN stock_daily nd
|
LEFT JOIN stock_daily nd
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "nd.ts_code") + ` AND nd.trade_date = c.next_trade_date
|
ON l.ts_code = nd.ts_code AND nd.trade_date = c.next_trade_date
|
||||||
WHERE c.previous_trade_date IS NOT NULL
|
WHERE 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, calendarArgs...).Scan(&rawRows).Error; err != nil {
|
if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package dbcompat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CalendarSeed is a MySQL-version-neutral replacement for LAG/LEAD over the
|
|
||||||
// trade calendar. Keeping the adjacent dates in Go avoids depending on MySQL 8.
|
|
||||||
type CalendarSeed struct {
|
|
||||||
TradeDate string
|
|
||||||
PreviousTradeDate string
|
|
||||||
PreviousTwoTradeDate string
|
|
||||||
NextTradeDate string
|
|
||||||
}
|
|
||||||
|
|
||||||
type calendarDateRow struct {
|
|
||||||
TradeDate string `gorm:"column:trade_date"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type columnCollationRow struct {
|
|
||||||
CollationName string `gorm:"column:collation_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSSEOpenCalendarSeeds loads all SSE open dates and builds the date window
|
|
||||||
// needed by range queries. The returned seeds are limited to [startDate, endDate].
|
|
||||||
func LoadSSEOpenCalendarSeeds(db *gorm.DB, startDate, endDate string) ([]CalendarSeed, error) {
|
|
||||||
var rows []calendarDateRow
|
|
||||||
if err := db.Table("trade_calendar").
|
|
||||||
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
|
|
||||||
Where("exchange = ? AND is_open = ?", "SSE", 1).
|
|
||||||
Order("cal_date ASC").
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
seeds := make([]CalendarSeed, 0)
|
|
||||||
for i, row := range rows {
|
|
||||||
if row.TradeDate < startDate || row.TradeDate > endDate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
seed := CalendarSeed{TradeDate: row.TradeDate}
|
|
||||||
if i > 0 {
|
|
||||||
seed.PreviousTradeDate = rows[i-1].TradeDate
|
|
||||||
}
|
|
||||||
if i > 1 {
|
|
||||||
seed.PreviousTwoTradeDate = rows[i-2].TradeDate
|
|
||||||
}
|
|
||||||
if i+1 < len(rows) {
|
|
||||||
seed.NextTradeDate = rows[i+1].TradeDate
|
|
||||||
}
|
|
||||||
seeds = append(seeds, seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return seeds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalendarSeedTable converts calendar seeds into a derived table that works on
|
|
||||||
// MySQL 5.7, MySQL 8, and MariaDB without CTE/window-function support.
|
|
||||||
func CalendarSeedTable(seeds []CalendarSeed) (string, []any) {
|
|
||||||
if len(seeds) == 0 {
|
|
||||||
return `SELECT
|
|
||||||
CAST(NULL AS DATE) AS trade_date,
|
|
||||||
CAST(NULL AS DATE) AS previous_trade_date,
|
|
||||||
CAST(NULL AS DATE) AS previous_two_trade_date,
|
|
||||||
CAST(NULL AS DATE) AS next_trade_date
|
|
||||||
WHERE 1 = 0`, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := make([]string, 0, len(seeds))
|
|
||||||
args := make([]any, 0, len(seeds)*4)
|
|
||||||
for _, seed := range seeds {
|
|
||||||
parts = append(parts, `SELECT
|
|
||||||
CAST(? AS DATE) AS trade_date,
|
|
||||||
CAST(? AS DATE) AS previous_trade_date,
|
|
||||||
CAST(? AS DATE) AS previous_two_trade_date,
|
|
||||||
CAST(? AS DATE) AS next_trade_date`)
|
|
||||||
args = append(args,
|
|
||||||
nullableDate(seed.TradeDate),
|
|
||||||
nullableDate(seed.PreviousTradeDate),
|
|
||||||
nullableDate(seed.PreviousTwoTradeDate),
|
|
||||||
nullableDate(seed.NextTradeDate),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, "\nUNION ALL\n"), args
|
|
||||||
}
|
|
||||||
|
|
||||||
// TsCodeEqual keeps joins free of hard-coded COLLATE clauses. This avoids
|
|
||||||
// binding SQL to MySQL-8-only collations while preserving index-friendly joins.
|
|
||||||
func TsCodeEqual(left, right string) string {
|
|
||||||
return left + " = " + right
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColumnCollation returns the current database collation for one column.
|
|
||||||
func ColumnCollation(db *gorm.DB, tableName, columnName string) (string, error) {
|
|
||||||
var row columnCollationRow
|
|
||||||
err := db.Raw(`
|
|
||||||
SELECT COLLATION_NAME AS collation_name
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = ?
|
|
||||||
AND COLUMN_NAME = ?
|
|
||||||
LIMIT 1`, tableName, columnName).Scan(&row).Error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return row.CollationName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TsCodeEqualUnicode compares a utf8mb4_unicode_ci column with a column that
|
|
||||||
// may be utf8mb4_0900_ai_ci. utf8mb4_unicode_ci exists on both MySQL 5.7 and 8.
|
|
||||||
func TsCodeEqualUnicode(left, right string) string {
|
|
||||||
return left + " = " + right + " COLLATE utf8mb4_unicode_ci"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TsCodeEqualRightCollation compares two stock-code columns using a collation
|
|
||||||
// known to exist on the connected database. This avoids MySQL-version-specific
|
|
||||||
// literals while preserving the fast side of joins such as stock_daily.ts_code.
|
|
||||||
func TsCodeEqualRightCollation(left, right, collation string) string {
|
|
||||||
if !safeCollationName(collation) {
|
|
||||||
return TsCodeEqual(left, right)
|
|
||||||
}
|
|
||||||
return left + " = " + right + " COLLATE " + collation
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeCollationName(collation string) bool {
|
|
||||||
if collation == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, r := range collation {
|
|
||||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullableDate(date string) any {
|
|
||||||
if strings.TrimSpace(date) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,14 @@ type SecondCalRequest struct {
|
|||||||
TradeEndDate string `form:"tradeEndDate" json:"trade_end_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 表示单只涨停股在涨停概览中的展示数据。
|
// LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。
|
||||||
type LimitBoardStock struct {
|
type LimitBoardStock struct {
|
||||||
Symbol string `json:"symbol"` // 市场展示用股票代码。
|
Symbol string `json:"symbol"` // 市场展示用股票代码。
|
||||||
@@ -165,6 +173,51 @@ type SecondCalStock struct {
|
|||||||
NextHighProfitPct *float64 `json:"next_high_profit_pct"` // 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 是开盘价低于前收接口的分组结果。
|
// LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。
|
||||||
type LimitOpenCompareGroup struct {
|
type LimitOpenCompareGroup struct {
|
||||||
Key string `json:"key"` // 分组唯一标识。
|
Key string `json:"key"` // 分组唯一标识。
|
||||||
@@ -261,6 +314,7 @@ type SecondCalDay struct {
|
|||||||
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
|
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
|
||||||
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
|
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
|
||||||
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
|
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
|
||||||
|
TotalStocks []SecondCalStock `json:"total_stocks"` // 涨停和炸板合并后按首次涨停时间排序的明细。
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecondCalResponse 是二板日历接口响应。
|
// SecondCalResponse 是二板日历接口响应。
|
||||||
|
|||||||
@@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
//
|
//
|
||||||
// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。
|
// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。
|
||||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
|
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。
|
// 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
@@ -23,6 +24,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
|||||||
}
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
r.Use(cacheableGETResponseMiddleware(newResponseCacheStore()))
|
||||||
|
|
||||||
v1 := r.Group("/api/v1")
|
v1 := r.Group("/api/v1")
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,8 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
|||||||
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
|
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
|
||||||
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
|
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
|
||||||
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit)
|
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("/second-cal", limitBoardApi.GetSecondCal)
|
||||||
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
|
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,16 +104,21 @@ 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 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 *
|
SELECT *
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -143,21 +147,21 @@ FROM (
|
|||||||
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 (` + calendarTable + `) c
|
FROM calendar_window 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 ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
|
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
|
||||||
LEFT JOIN stock_limit_combined_data ppl
|
LEFT JOIN stock_limit_combined_data ppl
|
||||||
ON ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
||||||
LEFT JOIN stock_daily d
|
LEFT JOIN stock_daily d
|
||||||
ON ` + dbcompat.TsCodeEqual("d.ts_code", "l.ts_code") + ` AND d.trade_date = c.trade_date
|
ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date
|
||||||
LEFT JOIN stock_daily pd
|
LEFT JOIN stock_daily pd
|
||||||
ON ` + dbcompat.TsCodeEqual("pd.ts_code", "l.ts_code") + ` AND pd.trade_date = c.previous_trade_date
|
ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date
|
||||||
LEFT JOIN stock_daily nd
|
LEFT JOIN stock_daily nd
|
||||||
ON ` + dbcompat.TsCodeEqual("nd.ts_code", "l.ts_code") + ` AND nd.trade_date = c.next_trade_date
|
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date
|
||||||
LEFT JOIN stock_basic sb
|
LEFT JOIN stock_basic sb
|
||||||
ON ` + dbcompat.TsCodeEqualUnicode("sb.ts_code", "l.ts_code") + `
|
ON sb.ts_code = l.ts_code
|
||||||
WHERE c.previous_trade_date IS NOT NULL
|
WHERE 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
|
||||||
@@ -167,7 +171,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 []limitBreakNextPerformanceRow
|
var rows []limitBreakNextPerformanceRow
|
||||||
err = s.DB.Raw(query, calendarArgs...).Scan(&rows).Error
|
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
|
||||||
return rows, err
|
return rows, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +146,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 ` + dbcompat.TsCodeEqual("ppl.ts_code", "l.ts_code") + ` AND ppl.trade_date = ? AND ppl.final_status = 1
|
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.final_status = 1
|
||||||
)) AS is_first_to_second_board,
|
)) 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,
|
||||||
@@ -165,11 +164,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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
LEFT JOIN stock_daily pd
|
LEFT JOIN stock_daily pd
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
|
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||||
INNER JOIN stock_limit_combined_data pl
|
INNER JOIN stock_limit_combined_data pl
|
||||||
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
|
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
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,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,11 +93,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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
LEFT JOIN stock_daily pd
|
LEFT JOIN stock_daily pd
|
||||||
ON ` + dbcompat.TsCodeEqual("l.ts_code", "pd.ts_code") + ` AND pd.trade_date = ?
|
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||||
INNER JOIN stock_limit_combined_data pl
|
INNER JOIN stock_limit_combined_data pl
|
||||||
ON ` + dbcompat.TsCodeEqual("pl.ts_code", "l.ts_code") + ` AND pl.trade_date = ? AND pl.final_status = 1
|
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
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,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +146,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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND d.trade_date = l.trade_date
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
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`
|
||||||
|
|
||||||
|
|||||||
@@ -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 // 返回转换后的股票明细。
|
||||||
|
}
|
||||||
@@ -2,8 +2,9 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco
|
|||||||
PreviousTwoTradeDate: day.PreviousTwoTradeDate,
|
PreviousTwoTradeDate: day.PreviousTwoTradeDate,
|
||||||
NextTradeDate: day.NextTradeDate,
|
NextTradeDate: day.NextTradeDate,
|
||||||
Stocks: []model.SecondCalStock{},
|
Stocks: []model.SecondCalStock{},
|
||||||
|
TotalStocks: []model.SecondCalStock{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,19 +90,29 @@ 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) {
|
||||||
seeds, err := dbcompat.LoadSSEOpenCalendarSeeds(s.DB, startDate, endDate)
|
query := `
|
||||||
if err != nil {
|
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
|
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))
|
||||||
@@ -112,20 +124,21 @@ func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]seco
|
|||||||
|
|
||||||
// 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 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
|
SELECT
|
||||||
DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
|
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_trade_date, '%Y-%m-%d') AS previous_trade_date,
|
||||||
@@ -190,21 +203,21 @@ FROM (
|
|||||||
END AS open_times,
|
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 (` + calendarTable + `) c
|
FROM calendar_window 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 ` + dbcompat.TsCodeEqual("ppl.ts_code", "pl.ts_code") + ` AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
|
ON ppl.ts_code = pl.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
|
||||||
INNER JOIN stock_daily d
|
INNER JOIN stock_daily d
|
||||||
ON ` + dbcompat.TsCodeEqualRightCollation("d.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND d.trade_date = c.trade_date
|
ON d.ts_code = pl.ts_code AND d.trade_date = c.trade_date
|
||||||
LEFT JOIN stock_daily nd
|
LEFT JOIN stock_daily nd
|
||||||
ON ` + dbcompat.TsCodeEqualRightCollation("nd.ts_code", "pl.ts_code", stockDailyTsCodeCollation) + ` AND nd.trade_date = c.next_trade_date
|
ON nd.ts_code = pl.ts_code AND nd.trade_date = c.next_trade_date
|
||||||
LEFT JOIN limit_list_d tlu
|
LEFT JOIN limit_list_d tlu
|
||||||
ON ` + dbcompat.TsCodeEqual("tlu.ts_code", "pl.ts_code") + ` AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U'
|
ON tlu.ts_code = pl.ts_code AND tlu.trade_date = c.trade_date AND tlu.` + "`limit`" + ` = 'U'
|
||||||
LEFT JOIN limit_list_d tlz
|
LEFT JOIN limit_list_d tlz
|
||||||
ON ` + dbcompat.TsCodeEqual("tlz.ts_code", "pl.ts_code") + ` AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z'
|
ON tlz.ts_code = pl.ts_code AND tlz.trade_date = c.trade_date AND tlz.` + "`limit`" + ` = 'Z'
|
||||||
LEFT JOIN stock_basic sb
|
LEFT JOIN stock_basic sb
|
||||||
ON ` + dbcompat.TsCodeEqual("sb.ts_code", "pl.ts_code") + `
|
ON sb.ts_code = pl.ts_code
|
||||||
WHERE c.previous_trade_date IS NOT NULL
|
WHERE 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
|
||||||
@@ -219,7 +232,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, calendarArgs...).Scan(&rows).Error
|
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
|
||||||
return rows, err
|
return rows, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,10 +301,19 @@ func accumulateSecondCalDay(day *model.SecondCalDay, item model.SecondCalStock)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
day.Stocks = append(day.Stocks, item)
|
day.Stocks = append(day.Stocks, item)
|
||||||
|
if strings.TrimSpace(item.FirstLimitTime) != "" {
|
||||||
|
day.TotalStocks = append(day.TotalStocks, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// finalizeSecondCalDay 计算单个交易日的概率字段。
|
// finalizeSecondCalDay 计算单个交易日的概率字段。
|
||||||
func finalizeSecondCalDay(day *model.SecondCalDay) {
|
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.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount)
|
||||||
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
|
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
|
||||||
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)
|
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"level_go/internal/dbcompat"
|
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +45,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 ` + dbcompat.TsCodeEqual("l.ts_code", "d.ts_code") + ` AND l.trade_date = d.trade_date
|
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1`
|
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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user