Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0aa4c1aff9 | |||
| cf71dd2207 | |||
| f1734a68ae |
@@ -10,3 +10,4 @@
|
||||
|
||||
# 依赖目录 (如果使用 vendor 模式)
|
||||
# vendor/
|
||||
config.yaml
|
||||
@@ -6,6 +6,7 @@
|
||||
# port: 10836
|
||||
# dbname: "level"
|
||||
# charset: "utf8mb4"
|
||||
# collation: "utf8mb4_unicode_ci"
|
||||
|
||||
# 本地数据库
|
||||
database:
|
||||
@@ -15,6 +16,7 @@ database:
|
||||
port: 3306
|
||||
dbname: "level"
|
||||
charset: "utf8mb4"
|
||||
collation: "utf8mb4_unicode_ci"
|
||||
|
||||
server:
|
||||
port: 18080
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- 服务前缀:`/api/v1`
|
||||
- 响应格式:`application/json`
|
||||
- 当前项目接口总数:7
|
||||
- 当前项目接口总数:8
|
||||
|
||||
## 2. 接口列表
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
| 涨停连板 | `GET` | `/api/v1/limit-board/overview` | 获取交易日涨停分组与近 30 日最高板趋势 | [limit-board-overview.md](./limit-board-overview.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/download/limit-board/second-board-profit-csv` | 下载符合二板早盘开盘阈值条件的次日盈利 CSV 明细 | [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/download/limit-board/second-board-profit-csv` | 下载筛选条件范围内的二板早盘次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
|
||||
|
||||
## 3. 错误响应约定
|
||||
|
||||
|
||||
@@ -1,69 +1,185 @@
|
||||
# 二板早盘次日盈利 CSV 下载接口文档
|
||||
# 二板早盘次日盈利接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:二板早盘次日盈利 CSV 下载
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/download/limit-board/second-board-profit-csv`
|
||||
- 接口用途:下载符合二板早盘开盘阈值条件的股票明细,并查看后一交易日是否盈利。
|
||||
本页包含二板早盘次日盈利的列表查询接口和 CSV 下载接口。两个接口使用同一套查询条件和同一套返回字段;列表接口分页,CSV 下载接口导出筛选后的全量数据。
|
||||
|
||||
## 2. 请求参数
|
||||
业务筛选基础条件:
|
||||
|
||||
- T-2 交易日无涨停。
|
||||
- T-1 交易日涨停。
|
||||
- T 交易日涨停。
|
||||
|
||||
## 2. 接口地址
|
||||
|
||||
| 接口名称 | 请求方式 | 请求路径 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 二板早盘次日盈利列表查询 | `GET` | `/api/v1/limit-board/second-board-profit` | 前端页面分页展示数据 |
|
||||
| 二板早盘次日盈利 CSV 下载 | `GET` | `/api/v1/download/limit-board/second-board-profit-csv` | 下载筛选条件范围内的全量 CSV 数据 |
|
||||
|
||||
## 3. 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `start_date` | `string` | 否 | `2020-01-01` | `2026-01-01` | 开始交易日期,格式固定为 `YYYY-MM-DD` |
|
||||
| `end_date` | `string` | 否 | `2026-12-31` | `2026-12-31` | 结束交易日期,格式固定为 `YYYY-MM-DD` |
|
||||
| `tradeStartDate` | `string` | 否 | `2020-01-01` | `2026-01-01` | 交易日开始日期,格式固定为 `YYYY-MM-DD` |
|
||||
| `tradeEndDate` | `string` | 否 | `2026-12-31` | `2026-12-31` | 交易日结束日期,格式固定为 `YYYY-MM-DD` |
|
||||
| `limitUpTimeFilterType1` | `string` | 否 | 空 | `>=` | T 日首次涨停时间第一组筛选操作符,支持 `<`、`>`、`<=`、`>=`、`=` |
|
||||
| `limitUpTimeFilterValue1` | `string` | 否 | 空 | `09:30:00` | T 日首次涨停时间第一组筛选值 |
|
||||
| `limitUpTimeFilterType2` | `string` | 否 | 空 | `<` | T 日首次涨停时间第二组筛选操作符,支持 `<`、`>`、`<=`、`>=`、`=` |
|
||||
| `limitUpTimeFilterValue2` | `string` | 否 | 空 | `11:00:00` | T 日首次涨停时间第二组筛选值 |
|
||||
| `openPctChgFilterType1` | `string` | 否 | 空 | `>=` | T 日开盘涨跌幅第一组筛选操作符,支持 `<`、`>`、`<=`、`>=`、`=` |
|
||||
| `openPctChgFilterValue1` | `number` | 否 | 空 | `-2` | T 日开盘涨跌幅第一组筛选值,按 `(T日开盘价 - T-1收盘价) / T-1收盘价 * 100` 计算 |
|
||||
| `openPctChgFilterType2` | `string` | 否 | 空 | `<=` | T 日开盘涨跌幅第二组筛选操作符,支持 `<`、`>`、`<=`、`>=`、`=` |
|
||||
| `openPctChgFilterValue2` | `number` | 否 | 空 | `3` | T 日开盘涨跌幅第二组筛选值,按 `(T日开盘价 - T-1收盘价) / T-1收盘价 * 100` 计算 |
|
||||
| `page` | `int` | 否 | `1` | `1` | 仅列表接口支持,页码从 1 开始 |
|
||||
| `pageSize` | `int` | 否 | `10` | `20` | 仅列表接口支持,每页数量 |
|
||||
|
||||
- `start_date` 可以等于 `end_date`,用于下载单日数据。
|
||||
- `end_date` 不能早于 `start_date`。
|
||||
兼容说明:CSV 下载接口仍兼容旧参数 `start_date` 和 `end_date`,建议新调用统一使用 `tradeStartDate` 和 `tradeEndDate`。
|
||||
|
||||
请求示例:
|
||||
筛选规则:
|
||||
|
||||
- 每个筛选项最多支持两组条件,第一组为 `Type1 + Value1`,第二组为 `Type2 + Value2`。
|
||||
- 四个值都可以为空;当某组 `Type` 和 `Value` 都为空时,该组条件不生效。
|
||||
- 如果某组条件生效,`Type` 和 `Value` 必须同时填写。
|
||||
- 两组条件同时存在时按 AND 关系过滤,可用于表达区间,例如 `>=09:30:00` 且 `<11:00:00`。
|
||||
- 时间值支持 `HH:MM`、`HH:MM:SS`、`HHMM`、`HHMMSS` 格式。
|
||||
- 实际请求 URL 中 `<`、`>`、`=` 等字符建议做 URL 编码,例如 `>=` 编码为 `%3E%3D`,`<` 编码为 `%3C`。
|
||||
- 后端也兼容数组式传参,例如 `limitUpTimeFilter[]=%3E%3D&limitUpTimeFilter[]=09:30:00&limitUpTimeFilter[]=%3C&limitUpTimeFilter[]=11:00:00`,数组四个值的含义依次等同于 `Type1`、`Value1`、`Type2`、`Value2`。
|
||||
|
||||
## 4. 请求示例
|
||||
|
||||
列表查询:
|
||||
|
||||
```http
|
||||
GET /api/v1/download/limit-board/second-board-profit-csv?start_date=2026-01-01&end_date=2026-12-31
|
||||
GET /api/v1/limit-board/second-board-profit?tradeStartDate=2026-01-01&tradeEndDate=2026-12-31&page=1&pageSize=10
|
||||
```
|
||||
|
||||
单日下载示例:
|
||||
带筛选条件的列表查询:
|
||||
|
||||
```http
|
||||
GET /api/v1/download/limit-board/second-board-profit-csv?start_date=2026-04-29&end_date=2026-04-29
|
||||
GET /api/v1/limit-board/second-board-profit?tradeStartDate=2026-04-01&tradeEndDate=2026-04-30&limitUpTimeFilterType1=%3E%3D&limitUpTimeFilterValue1=09:30:00&limitUpTimeFilterType2=%3C&limitUpTimeFilterValue2=11:00:00&openPctChgFilterType1=%3E%3D&openPctChgFilterValue1=-2&openPctChgFilterType2=%3C%3D&openPctChgFilterValue2=3&page=1&pageSize=20
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
CSV 下载:
|
||||
|
||||
```http
|
||||
GET /api/v1/download/limit-board/second-board-profit-csv?tradeStartDate=2026-04-01&tradeEndDate=2026-04-30&limitUpTimeFilterType1=%3E%3D&limitUpTimeFilterValue1=09:30:00&limitUpTimeFilterType2=%3C&limitUpTimeFilterValue2=11:00:00&openPctChgFilterType1=%3E%3D&openPctChgFilterValue1=-2&openPctChgFilterType2=%3C%3D&openPctChgFilterValue2=3
|
||||
```
|
||||
|
||||
## 5. 列表响应
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `trade_start_date` | `string` | 查询交易日开始日期 |
|
||||
| `trade_end_date` | `string` | 查询交易日结束日期 |
|
||||
| `limit_up_time_filter_type1` | `string` | 首次涨停时间第一组筛选操作符 |
|
||||
| `limit_up_time_filter_value1` | `string` | 首次涨停时间第一组筛选值 |
|
||||
| `limit_up_time_filter_type2` | `string` | 首次涨停时间第二组筛选操作符 |
|
||||
| `limit_up_time_filter_value2` | `string` | 首次涨停时间第二组筛选值 |
|
||||
| `open_pct_chg_filter_type1` | `string` | T 日开盘涨跌幅第一组筛选操作符 |
|
||||
| `open_pct_chg_filter_value1` | `string` | T 日开盘涨跌幅第一组筛选值 |
|
||||
| `open_pct_chg_filter_type2` | `string` | T 日开盘涨跌幅第二组筛选操作符 |
|
||||
| `open_pct_chg_filter_value2` | `string` | T 日开盘涨跌幅第二组筛选值 |
|
||||
| `page` | `int` | 当前页码 |
|
||||
| `page_size` | `int` | 每页数量 |
|
||||
| `total` | `int` | 筛选后的总记录数 |
|
||||
| `total_pages` | `int` | 总页数 |
|
||||
| `list` | `array` | 当前页数据 |
|
||||
|
||||
`list[]` 字段:
|
||||
|
||||
| 字段名 | 类型 | CSV 列名 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `trade_date` | `string` | `交易日` | T 日交易日 |
|
||||
| `ts_code` | `string` | `股票代码` | TS 股票代码 |
|
||||
| `name` | `string` | `股票简称` | 股票简称 |
|
||||
| `market` | `string` | `市场` | 市场信息 |
|
||||
| `industry` | `string` | `行业` | 所属行业 |
|
||||
| `open_pct_chg` | `number \| null` | `T日开盘涨跌幅%` | T 日开盘价相对 T-1 收盘价的涨跌幅百分比 |
|
||||
| `next_open_profitable` | `string` | `T+1开盘是否盈利` | T+1 开盘收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `next_close_profitable` | `string` | `T+1收盘是否盈利` | T+1 收盘收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `next_intraday_profitable` | `string` | `T+1盘中是否可盈利` | T+1 最高收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `next_high_profit_pct` | `number \| null` | `T+1最高盈利%` | T+1 最高价相对 T 日收盘价的收益率百分比 |
|
||||
| `next_low_profit_pct` | `number \| null` | `T+1最低盈利%` | T+1 最低价相对 T 日收盘价的收益率百分比 |
|
||||
| `break_limit_count` | `int \| null` | `开板次数` | T 日开板次数 |
|
||||
| `turnover_rate_pct` | `number \| null` | `换手率%` | T 日换手率 |
|
||||
| `first_limit_time` | `string` | `首次涨停时间` | T 日首次涨停时间,格式化为 `HH:MM:SS` |
|
||||
| `seal_amount` | `number \| null` | `封单额` | T 日封单额 |
|
||||
| `amount` | `number \| null` | `成交额` | T 日成交额 |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"trade_start_date": "2026-04-01",
|
||||
"trade_end_date": "2026-04-30",
|
||||
"limit_up_time_filter_type1": ">=",
|
||||
"limit_up_time_filter_value1": "09:30:00",
|
||||
"limit_up_time_filter_type2": "<",
|
||||
"limit_up_time_filter_value2": "11:00:00",
|
||||
"open_pct_chg_filter_type1": ">=",
|
||||
"open_pct_chg_filter_value1": "-2",
|
||||
"open_pct_chg_filter_type2": "<=",
|
||||
"open_pct_chg_filter_value2": "3",
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total": 26,
|
||||
"total_pages": 3,
|
||||
"list": [
|
||||
{
|
||||
"trade_date": "2026-04-24",
|
||||
"ts_code": "000001.SZ",
|
||||
"name": "示例股份",
|
||||
"market": "主板",
|
||||
"industry": "软件开发",
|
||||
"open_pct_chg": 1.36,
|
||||
"next_open_profitable": "是",
|
||||
"next_close_profitable": "否",
|
||||
"next_intraday_profitable": "是",
|
||||
"next_high_profit_pct": 4.21,
|
||||
"next_low_profit_pct": -2.15,
|
||||
"break_limit_count": 2,
|
||||
"turnover_rate_pct": 18.64,
|
||||
"first_limit_time": "09:35:12",
|
||||
"seal_amount": 286000000,
|
||||
"amount": 1523000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. CSV 响应
|
||||
|
||||
- 响应类型:`text/csv; charset=utf-8`
|
||||
- 响应头:`Content-Disposition: attachment; filename="limit_second_board_profit_YYYYMMDD_YYYYMMDD_daily_detail.csv"`
|
||||
- CSV 文件带 UTF-8 BOM,便于 Excel 直接打开中文列名。
|
||||
- CSV 下载接口不分页,默认导出查询条件范围内的全部数据。
|
||||
|
||||
## 4. CSV 字段
|
||||
CSV 字段顺序:
|
||||
|
||||
| 字段名 | 说明 |
|
||||
| --- | --- |
|
||||
| `交易日` | 符合条件的交易日 |
|
||||
| `前一交易日` | 交易日的前一个交易日 |
|
||||
| `前二交易日` | 交易日的前两个交易日 |
|
||||
| `后一交易日` | 用于判断盈利的后一交易日 |
|
||||
| `交易日` | T 日交易日 |
|
||||
| `股票代码` | TS 股票代码 |
|
||||
| `股票简称` | 股票名称 |
|
||||
| `股票简称` | 股票简称 |
|
||||
| `市场` | 市场信息 |
|
||||
| `行业` | 所属行业 |
|
||||
| `T日开盘价` | 交易日开盘价 |
|
||||
| `T日收盘价` | 交易日收盘价 |
|
||||
| `T日涨停价` | 理论涨停价 |
|
||||
| `T-1收盘价` | 前一交易日收盘价 |
|
||||
| `开盘/T-1收盘%` | 交易日开盘价相对前一交易日收盘价的百分比 |
|
||||
| `首次涨停时间` | 交易日首次涨停时间,格式化为 `HH:MM:SS` |
|
||||
| `T+1开盘收益%` | 后一交易日开盘价相对 T 日收盘价的收益率 |
|
||||
| `T+1最高收益%` | 后一交易日最高价相对 T 日收盘价的收益率 |
|
||||
| `T+1收盘收益%` | 后一交易日收盘价相对 T 日收盘价的收益率 |
|
||||
| `T+1收盘是否盈利` | `T+1收盘收益% > 0` 时为 `是`,否则为 `否` |
|
||||
| `T+1盘中是否可盈利` | `T+1最高收益% > 0` 时为 `是`,否则为 `否` |
|
||||
| `T日开盘涨跌幅%` | T 日开盘价相对 T-1 收盘价的涨跌幅百分比 |
|
||||
| `T+1开盘是否盈利` | T+1 开盘收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `T+1收盘是否盈利` | T+1 收盘收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `T+1盘中是否可盈利` | T+1 最高收益大于 0 时为 `是`,否则为 `否`;无次日数据时为 `无次日数据` |
|
||||
| `T+1最高盈利%` | T+1 最高价相对 T 日收盘价的收益率百分比 |
|
||||
| `T+1最低盈利%` | T+1 最低价相对 T 日收盘价的收益率百分比 |
|
||||
| `开板次数` | T 日开板次数 |
|
||||
| `换手率%` | T 日换手率 |
|
||||
| `首次涨停时间` | T 日首次涨停时间,格式化为 `HH:MM:SS` |
|
||||
| `封单额` | T 日封单额 |
|
||||
| `成交额` | T 日成交额 |
|
||||
|
||||
## 5. 业务规则
|
||||
## 7. 错误响应
|
||||
|
||||
- T 日涨停。
|
||||
- T-1 交易日也涨停。
|
||||
- T-2 交易日必须不涨停。
|
||||
- T 日开盘价必须低于 T-1 收盘价的 101%。
|
||||
- T 日首次涨停时间必须早于 11:00。
|
||||
- 盈利口径默认按 T 日收盘价买入,T+1 收盘价卖出。
|
||||
```json
|
||||
{
|
||||
"error": "参数错误"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,10 +28,10 @@ type secondProfitCSVRow struct {
|
||||
Name string `gorm:"column:name"`
|
||||
Market string `gorm:"column:market"`
|
||||
Industry string `gorm:"column:industry"`
|
||||
Open float64 `gorm:"column:open"`
|
||||
Close float64 `gorm:"column:close_price"`
|
||||
Open sql.NullFloat64 `gorm:"column:open"`
|
||||
Close sql.NullFloat64 `gorm:"column:close_price"`
|
||||
LimitPrice float64 `gorm:"column:limit_price"`
|
||||
PreviousClose float64 `gorm:"column:previous_close"`
|
||||
PreviousClose sql.NullFloat64 `gorm:"column:previous_close"`
|
||||
FirstLimitTime sql.NullString `gorm:"column:tushare_first_time"`
|
||||
PctChg sql.NullFloat64 `gorm:"column:pct_chg"`
|
||||
Amount sql.NullFloat64 `gorm:"column:tushare_amount"`
|
||||
@@ -44,30 +44,129 @@ type secondProfitCSVRow struct {
|
||||
NextLow sql.NullFloat64 `gorm:"column:next_low"`
|
||||
NextClose sql.NullFloat64 `gorm:"column:next_close"`
|
||||
OpenPreviousCloseRatio float64
|
||||
OpenPctChg sql.NullFloat64
|
||||
NextOpenReturnPct sql.NullFloat64
|
||||
NextHighReturnPct sql.NullFloat64
|
||||
NextLowReturnPct sql.NullFloat64
|
||||
NextCloseReturnPct sql.NullFloat64
|
||||
NextOpenProfitable string
|
||||
NextCloseProfitable string
|
||||
NextIntradayProfitable string
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
startDate := c.DefaultQuery("start_date", defaultSecondProfitStartDate)
|
||||
endDate := c.DefaultQuery("end_date", defaultSecondProfitEndDate)
|
||||
if err := validateSecondProfitDateRange(startDate, endDate); err != nil {
|
||||
type secondProfitQuery struct {
|
||||
TradeStartDate string
|
||||
TradeEndDate string
|
||||
LimitUpTimeFilterType1 string
|
||||
LimitUpTimeFilterValue1 string
|
||||
LimitUpTimeFilterType2 string
|
||||
LimitUpTimeFilterValue2 string
|
||||
OpenPctChgFilterType1 string
|
||||
OpenPctChgFilterValue1 string
|
||||
OpenPctChgFilterType2 string
|
||||
OpenPctChgFilterValue2 string
|
||||
LimitUpTimeRule secondProfitCompareFilter
|
||||
OpenPctChgRule secondProfitCompareFilter
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type secondProfitCompareFilter struct {
|
||||
Conditions []secondProfitCompareCondition
|
||||
}
|
||||
|
||||
type secondProfitCompareCondition struct {
|
||||
Op string
|
||||
Value float64
|
||||
}
|
||||
|
||||
type secondProfitListItem struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
TsCode string `json:"ts_code"`
|
||||
Name string `json:"name"`
|
||||
Market string `json:"market"`
|
||||
Industry string `json:"industry"`
|
||||
OpenPctChg *float64 `json:"open_pct_chg"`
|
||||
NextOpenProfitable string `json:"next_open_profitable"`
|
||||
NextCloseProfitable string `json:"next_close_profitable"`
|
||||
NextIntradayProfitable string `json:"next_intraday_profitable"`
|
||||
NextHighProfitPct *float64 `json:"next_high_profit_pct"`
|
||||
NextLowProfitPct *float64 `json:"next_low_profit_pct"`
|
||||
BreakLimitCount *int64 `json:"break_limit_count"`
|
||||
TurnoverRatePct *float64 `json:"turnover_rate_pct"`
|
||||
FirstLimitTime string `json:"first_limit_time"`
|
||||
SealAmount *float64 `json:"seal_amount"`
|
||||
Amount *float64 `json:"amount"`
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) ListSecondBoardProfit(c *gin.Context) {
|
||||
req, err := parseSecondProfitQuery(c, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.loadSecondProfitCSVRows(startDate, endDate)
|
||||
rows, err := h.loadSecondProfitRows(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
total := len(rows)
|
||||
start := (req.Page - 1) * req.PageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + req.PageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
items := make([]secondProfitListItem, 0, end-start)
|
||||
for _, row := range rows[start:end] {
|
||||
items = append(items, secondProfitBuildItem(row))
|
||||
}
|
||||
|
||||
totalPages := 0
|
||||
if total > 0 {
|
||||
totalPages = (total + req.PageSize - 1) / req.PageSize
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"trade_start_date": req.TradeStartDate,
|
||||
"trade_end_date": req.TradeEndDate,
|
||||
"limit_up_time_filter_type1": req.LimitUpTimeFilterType1,
|
||||
"limit_up_time_filter_value1": req.LimitUpTimeFilterValue1,
|
||||
"limit_up_time_filter_type2": req.LimitUpTimeFilterType2,
|
||||
"limit_up_time_filter_value2": req.LimitUpTimeFilterValue2,
|
||||
"open_pct_chg_filter_type1": req.OpenPctChgFilterType1,
|
||||
"open_pct_chg_filter_value1": req.OpenPctChgFilterValue1,
|
||||
"open_pct_chg_filter_type2": req.OpenPctChgFilterType2,
|
||||
"open_pct_chg_filter_value2": req.OpenPctChgFilterValue2,
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
"total": total,
|
||||
"total_pages": totalPages,
|
||||
"list": items,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
req, err := parseSecondProfitQuery(c, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.loadSecondProfitRows(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("limit_second_board_profit_%s_%s_daily_detail.csv",
|
||||
strings.ReplaceAll(startDate, "-", ""),
|
||||
strings.ReplaceAll(endDate, "-", ""),
|
||||
strings.ReplaceAll(req.TradeStartDate, "-", ""),
|
||||
strings.ReplaceAll(req.TradeEndDate, "-", ""),
|
||||
)
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
@@ -88,22 +187,121 @@ func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func parseSecondProfitQuery(c *gin.Context, includePagination bool) (secondProfitQuery, error) {
|
||||
startDate := c.DefaultQuery("tradeStartDate", c.DefaultQuery("start_date", defaultSecondProfitStartDate))
|
||||
endDate := c.DefaultQuery("tradeEndDate", c.DefaultQuery("end_date", defaultSecondProfitEndDate))
|
||||
limitUpTimeType1, limitUpTimeValue1, limitUpTimeType2, limitUpTimeValue2 := secondProfitFilterParts(c, "limitUpTimeFilter")
|
||||
openPctChgType1, openPctChgValue1, openPctChgType2, openPctChgValue2 := secondProfitFilterParts(c, "openPctChgFilter")
|
||||
req := secondProfitQuery{
|
||||
TradeStartDate: startDate,
|
||||
TradeEndDate: endDate,
|
||||
LimitUpTimeFilterType1: limitUpTimeType1,
|
||||
LimitUpTimeFilterValue1: limitUpTimeValue1,
|
||||
LimitUpTimeFilterType2: limitUpTimeType2,
|
||||
LimitUpTimeFilterValue2: limitUpTimeValue2,
|
||||
OpenPctChgFilterType1: openPctChgType1,
|
||||
OpenPctChgFilterValue1: openPctChgValue1,
|
||||
OpenPctChgFilterType2: openPctChgType2,
|
||||
OpenPctChgFilterValue2: openPctChgValue2,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
timeRule, err := parseSecondProfitCompareFilter(
|
||||
req.LimitUpTimeFilterType1,
|
||||
req.LimitUpTimeFilterValue1,
|
||||
req.LimitUpTimeFilterType2,
|
||||
req.LimitUpTimeFilterValue2,
|
||||
parseSecondProfitTimeValue,
|
||||
"limitUpTimeFilter",
|
||||
)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.LimitUpTimeRule = timeRule
|
||||
|
||||
pctRule, err := parseSecondProfitCompareFilter(
|
||||
req.OpenPctChgFilterType1,
|
||||
req.OpenPctChgFilterValue1,
|
||||
req.OpenPctChgFilterType2,
|
||||
req.OpenPctChgFilterValue2,
|
||||
parseSecondProfitFloatValue,
|
||||
"openPctChgFilter",
|
||||
)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.OpenPctChgRule = pctRule
|
||||
|
||||
if includePagination {
|
||||
pageText := strings.TrimSpace(c.DefaultQuery("page", "1"))
|
||||
page, err := strconv.Atoi(pageText)
|
||||
if err != nil || page <= 0 {
|
||||
return req, fmt.Errorf("page 必须为正整数")
|
||||
}
|
||||
req.Page = page
|
||||
|
||||
pageSizeText := strings.TrimSpace(c.DefaultQuery("pageSize", "10"))
|
||||
pageSize, err := strconv.Atoi(pageSizeText)
|
||||
if err != nil || pageSize <= 0 {
|
||||
return req, fmt.Errorf("pageSize 必须为正整数")
|
||||
}
|
||||
req.PageSize = pageSize
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func secondProfitFilterParts(c *gin.Context, base string) (string, string, string, string) {
|
||||
type1 := strings.TrimSpace(c.Query(base + "Type1"))
|
||||
value1 := strings.TrimSpace(c.Query(base + "Value1"))
|
||||
type2 := strings.TrimSpace(c.Query(base + "Type2"))
|
||||
value2 := strings.TrimSpace(c.Query(base + "Value2"))
|
||||
if type1 != "" || value1 != "" || type2 != "" || value2 != "" {
|
||||
return type1, value1, type2, value2
|
||||
}
|
||||
|
||||
values := c.QueryArray(base)
|
||||
if len(values) == 0 {
|
||||
values = c.QueryArray(base + "[]")
|
||||
}
|
||||
if len(values) > 0 {
|
||||
return secondProfitFilterArrayValue(values, 0),
|
||||
secondProfitFilterArrayValue(values, 1),
|
||||
secondProfitFilterArrayValue(values, 2),
|
||||
secondProfitFilterArrayValue(values, 3)
|
||||
}
|
||||
|
||||
return "", "", "", ""
|
||||
}
|
||||
|
||||
func secondProfitFilterArrayValue(values []string, index int) string {
|
||||
if index >= len(values) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(values[index])
|
||||
}
|
||||
|
||||
func validateSecondProfitDateRange(startDate, endDate string) error {
|
||||
start, err := time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start_date 格式必须为 YYYY-MM-DD")
|
||||
return fmt.Errorf("tradeStartDate 格式必须为 YYYY-MM-DD")
|
||||
}
|
||||
end, err := time.Parse("2006-01-02", endDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("end_date 格式必须为 YYYY-MM-DD")
|
||||
return fmt.Errorf("tradeEndDate 格式必须为 YYYY-MM-DD")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return fmt.Errorf("end_date 不能早于 start_date")
|
||||
return fmt.Errorf("tradeEndDate 不能早于 tradeStartDate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) loadSecondProfitCSVRows(startDate, endDate string) ([]secondProfitCSVRow, error) {
|
||||
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) {
|
||||
query := `
|
||||
WITH open_calendar AS (
|
||||
SELECT
|
||||
@@ -147,36 +345,49 @@ INNER JOIN stock_limit_combined_data pl
|
||||
LEFT JOIN stock_limit_combined_data ppl
|
||||
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||
LEFT JOIN stock_daily pd
|
||||
ON pd.ts_code = l.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 nd
|
||||
ON nd.ts_code = l.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.trade_date BETWEEN ? AND ?
|
||||
AND c.previous_trade_date IS NOT NULL
|
||||
AND c.previous_two_trade_date IS NOT NULL
|
||||
AND ppl.ts_code IS NULL
|
||||
AND d.open IS NOT NULL
|
||||
AND COALESCE(pl.calc_close, pl.tushare_close, pd.close) > 0
|
||||
AND d.open < COALESCE(pl.calc_close, pl.tushare_close, pd.close) * 1.01
|
||||
ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
|
||||
var rawRows []secondProfitCSVRow
|
||||
if err := h.Service.DB.Raw(query, startDate, endDate).Scan(&rawRows).Error; err != nil {
|
||||
if err := h.Service.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&rawRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows := make([]secondProfitCSVRow, 0, len(rawRows))
|
||||
for _, row := range rawRows {
|
||||
if !isSecondProfitBeforeEleven(row.FirstLimitTime.String) {
|
||||
if req.LimitUpTimeRule.Enabled() {
|
||||
limitTimeValue, ok := secondProfitTimeSeconds(row.FirstLimitTime.String)
|
||||
if !ok || !req.LimitUpTimeRule.Match(float64(limitTimeValue)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if row.Open.Valid && row.PreviousClose.Valid && row.PreviousClose.Float64 > 0 {
|
||||
row.OpenPreviousCloseRatio = row.Open.Float64 / row.PreviousClose.Float64
|
||||
row.OpenPctChg = sql.NullFloat64{
|
||||
Float64: secondProfitRound2((row.Open.Float64 - row.PreviousClose.Float64) / row.PreviousClose.Float64 * 100),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
if req.OpenPctChgRule.Enabled() && (!row.OpenPctChg.Valid || !req.OpenPctChgRule.Match(row.OpenPctChg.Float64)) {
|
||||
continue
|
||||
}
|
||||
row.OpenPreviousCloseRatio = row.Open / row.PreviousClose
|
||||
if row.Close > 0 {
|
||||
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close)
|
||||
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close)
|
||||
row.NextCloseReturnPct = secondProfitPctReturn(row.NextClose, row.Close)
|
||||
|
||||
if row.Close.Valid && row.Close.Float64 > 0 {
|
||||
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close.Float64)
|
||||
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close.Float64)
|
||||
row.NextLowReturnPct = secondProfitPctReturn(row.NextLow, row.Close.Float64)
|
||||
row.NextCloseReturnPct = secondProfitPctReturn(row.NextClose, row.Close.Float64)
|
||||
}
|
||||
row.NextOpenProfitable = secondProfitLabel(row.NextOpenReturnPct)
|
||||
row.NextCloseProfitable = secondProfitLabel(row.NextCloseReturnPct)
|
||||
row.NextIntradayProfitable = secondProfitLabel(row.NextHighReturnPct)
|
||||
rows = append(rows, row)
|
||||
@@ -186,62 +397,155 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
|
||||
func secondProfitCSVHeaders() []string {
|
||||
return []string{
|
||||
"交易日", "前一交易日", "前二交易日", "后一交易日", "股票代码", "股票简称", "市场", "行业",
|
||||
"T日开盘价", "T日收盘价", "T日涨停价", "T-1收盘价", "开盘/T-1收盘%", "首次涨停时间",
|
||||
"T日涨跌幅%", "成交额", "换手率%", "封单额", "开板次数", "涨停统计",
|
||||
"T+1开盘价", "T+1最高价", "T+1最低价", "T+1收盘价",
|
||||
"T+1开盘收益%", "T+1最高收益%", "T+1收盘收益%", "T+1收盘是否盈利", "T+1盘中是否可盈利",
|
||||
"交易日", "股票代码", "股票简称", "市场", "行业",
|
||||
"T日开盘涨跌幅%",
|
||||
"T+1开盘是否盈利", "T+1收盘是否盈利", "T+1盘中是否可盈利",
|
||||
"T+1最高盈利%", "T+1最低盈利%", "开板次数", "换手率%",
|
||||
"首次涨停时间", "封单额", "成交额",
|
||||
}
|
||||
}
|
||||
|
||||
func secondProfitCSVRecord(row secondProfitCSVRow) []string {
|
||||
return []string{
|
||||
row.TradeDate,
|
||||
row.PreviousTradeDate,
|
||||
row.PreviousTwoTradeDate,
|
||||
row.NextTradeDate,
|
||||
row.TsCode,
|
||||
row.Name,
|
||||
row.Market,
|
||||
row.Industry,
|
||||
secondProfitFormatFloat(row.Open),
|
||||
secondProfitFormatFloat(row.Close),
|
||||
secondProfitFormatFloat(row.LimitPrice),
|
||||
secondProfitFormatFloat(row.PreviousClose),
|
||||
secondProfitFormatFloat(secondProfitRound2(row.OpenPreviousCloseRatio * 100)),
|
||||
secondProfitFormatLimitTime(row.FirstLimitTime.String),
|
||||
secondProfitFormatNullFloat(row.PctChg),
|
||||
secondProfitFormatNullFloat(row.Amount),
|
||||
secondProfitFormatNullFloat(row.TurnoverRate),
|
||||
secondProfitFormatNullFloat(row.SealAmount),
|
||||
secondProfitFormatNullInt(row.BreakLimitCount),
|
||||
row.LimitUpStat,
|
||||
secondProfitFormatNullFloat(row.NextOpen),
|
||||
secondProfitFormatNullFloat(row.NextHigh),
|
||||
secondProfitFormatNullFloat(row.NextLow),
|
||||
secondProfitFormatNullFloat(row.NextClose),
|
||||
secondProfitFormatNullFloat(row.NextOpenReturnPct),
|
||||
secondProfitFormatNullFloat(row.NextHighReturnPct),
|
||||
secondProfitFormatNullFloat(row.NextCloseReturnPct),
|
||||
secondProfitFormatNullFloat(row.OpenPctChg),
|
||||
row.NextOpenProfitable,
|
||||
row.NextCloseProfitable,
|
||||
row.NextIntradayProfitable,
|
||||
secondProfitFormatNullFloat(row.NextHighReturnPct),
|
||||
secondProfitFormatNullFloat(row.NextLowReturnPct),
|
||||
secondProfitFormatNullInt(row.BreakLimitCount),
|
||||
secondProfitFormatNullFloat(row.TurnoverRate),
|
||||
secondProfitFormatLimitTime(row.FirstLimitTime.String),
|
||||
secondProfitFormatNullFloat(row.SealAmount),
|
||||
secondProfitFormatNullFloat(row.Amount),
|
||||
}
|
||||
}
|
||||
|
||||
func isSecondProfitBeforeEleven(limitTime string) bool {
|
||||
func secondProfitBuildItem(row secondProfitCSVRow) secondProfitListItem {
|
||||
return secondProfitListItem{
|
||||
TradeDate: row.TradeDate,
|
||||
TsCode: row.TsCode,
|
||||
Name: row.Name,
|
||||
Market: row.Market,
|
||||
Industry: row.Industry,
|
||||
OpenPctChg: secondProfitNullFloatPtr(row.OpenPctChg),
|
||||
NextOpenProfitable: row.NextOpenProfitable,
|
||||
NextCloseProfitable: row.NextCloseProfitable,
|
||||
NextIntradayProfitable: row.NextIntradayProfitable,
|
||||
NextHighProfitPct: secondProfitNullFloatPtr(row.NextHighReturnPct),
|
||||
NextLowProfitPct: secondProfitNullFloatPtr(row.NextLowReturnPct),
|
||||
BreakLimitCount: secondProfitNullIntPtr(row.BreakLimitCount),
|
||||
TurnoverRatePct: secondProfitNullFloatPtr(row.TurnoverRate),
|
||||
FirstLimitTime: secondProfitFormatLimitTime(row.FirstLimitTime.String),
|
||||
SealAmount: secondProfitNullFloatPtr(row.SealAmount),
|
||||
Amount: secondProfitNullFloatPtr(row.Amount),
|
||||
}
|
||||
}
|
||||
|
||||
func parseSecondProfitCompareFilter(op1, value1, op2, value2 string, parseValue func(string) (float64, error), fieldName string) (secondProfitCompareFilter, error) {
|
||||
filter := secondProfitCompareFilter{}
|
||||
if err := appendSecondProfitCompareCondition(&filter, op1, value1, parseValue, fieldName, "第一组"); err != nil {
|
||||
return filter, err
|
||||
}
|
||||
if err := appendSecondProfitCompareCondition(&filter, op2, value2, parseValue, fieldName, "第二组"); err != nil {
|
||||
return filter, err
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func appendSecondProfitCompareCondition(filter *secondProfitCompareFilter, op, rawValue string, parseValue func(string) (float64, error), fieldName, groupName string) error {
|
||||
op = strings.TrimSpace(op)
|
||||
rawValue = strings.TrimSpace(rawValue)
|
||||
if op == "" && rawValue == "" {
|
||||
return nil
|
||||
}
|
||||
if op == "" || rawValue == "" {
|
||||
return fmt.Errorf("%s %s操作符和值必须同时填写", fieldName, groupName)
|
||||
}
|
||||
if !isSecondProfitCompareOp(op) {
|
||||
return fmt.Errorf("%s %s操作符必须为 <、>、<=、>=、=", fieldName, groupName)
|
||||
}
|
||||
value, err := parseValue(rawValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s值格式错误", fieldName, groupName)
|
||||
}
|
||||
filter.Conditions = append(filter.Conditions, secondProfitCompareCondition{Op: op, Value: value})
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSecondProfitCompareOp(op string) bool {
|
||||
switch op {
|
||||
case "<", ">", "<=", ">=", "=":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (f secondProfitCompareFilter) Enabled() bool {
|
||||
return len(f.Conditions) > 0
|
||||
}
|
||||
|
||||
func (f secondProfitCompareFilter) Match(value float64) bool {
|
||||
if !f.Enabled() {
|
||||
return true
|
||||
}
|
||||
for _, condition := range f.Conditions {
|
||||
if !condition.Match(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c secondProfitCompareCondition) Match(value float64) bool {
|
||||
switch c.Op {
|
||||
case "<":
|
||||
return value < c.Value
|
||||
case "<=":
|
||||
return value <= c.Value
|
||||
case ">":
|
||||
return value > c.Value
|
||||
case ">=":
|
||||
return value >= c.Value
|
||||
case "=":
|
||||
return value == c.Value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseSecondProfitTimeValue(value string) (float64, error) {
|
||||
seconds, ok := secondProfitTimeSeconds(value)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid time")
|
||||
}
|
||||
return float64(seconds), nil
|
||||
}
|
||||
|
||||
func parseSecondProfitFloatValue(value string) (float64, error) {
|
||||
return strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||
}
|
||||
|
||||
func secondProfitTimeSeconds(limitTime string) (int, bool) {
|
||||
timeText := strings.TrimSpace(limitTime)
|
||||
if timeText == "" {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
|
||||
hour, minute, second := 0, 0, 0
|
||||
if strings.Contains(timeText, ":") {
|
||||
if n, _ := fmt.Sscanf(timeText, "%d:%d:%d", &hour, &minute, &second); n < 2 {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
} else {
|
||||
if len(timeText) < 4 {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
switch len(timeText) {
|
||||
case 4:
|
||||
@@ -250,26 +554,26 @@ func isSecondProfitBeforeEleven(limitTime string) bool {
|
||||
timeText = "0" + timeText
|
||||
case 6:
|
||||
default:
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
var err error
|
||||
hour, err = strconv.Atoi(timeText[0:2])
|
||||
if err != nil {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
minute, err = strconv.Atoi(timeText[2:4])
|
||||
if err != nil {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
second, err = strconv.Atoi(timeText[4:6])
|
||||
if err != nil {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
|
||||
return false
|
||||
return 0, false
|
||||
}
|
||||
return hour < 11
|
||||
return hour*3600 + minute*60 + second, true
|
||||
}
|
||||
|
||||
func secondProfitFormatLimitTime(value string) string {
|
||||
@@ -327,3 +631,19 @@ func secondProfitFormatNullInt(value sql.NullInt64) string {
|
||||
}
|
||||
return strconv.FormatInt(value.Int64, 10)
|
||||
}
|
||||
|
||||
func secondProfitNullFloatPtr(value sql.NullFloat64) *float64 {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
v := value.Float64
|
||||
return &v
|
||||
}
|
||||
|
||||
func secondProfitNullIntPtr(value sql.NullInt64) *int64 {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
v := value.Int64
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
||||
limitBoard.GET("/open-lower-groups", limitBoardApi.GetOpenLowerGroups)
|
||||
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
|
||||
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
|
||||
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit)
|
||||
}
|
||||
|
||||
download := v1.Group("/download")
|
||||
|
||||
@@ -358,7 +358,7 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
|
||||
l.calc_vol AS vol
|
||||
FROM stock_limit_combined_data l
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||
WHERE l.trade_date = ? AND l.final_status = 1
|
||||
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
||||
|
||||
@@ -402,9 +402,9 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
|
||||
l.calc_vol AS vol
|
||||
FROM stock_limit_combined_data l
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||
LEFT JOIN stock_daily pd
|
||||
ON pd.ts_code = l.ts_code AND pd.trade_date = ?
|
||||
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||
INNER JOIN stock_limit_combined_data pl
|
||||
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||
WHERE l.trade_date = ? AND l.final_status = 1
|
||||
@@ -448,9 +448,9 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad
|
||||
l.calc_vol AS vol
|
||||
FROM stock_limit_combined_data l
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||
LEFT JOIN stock_daily pd
|
||||
ON pd.ts_code = l.ts_code AND pd.trade_date = ?
|
||||
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||
INNER JOIN stock_limit_combined_data pl
|
||||
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||
WHERE l.trade_date = ? AND l.final_status = 1
|
||||
|
||||
+13
-8
@@ -7,12 +7,13 @@ import (
|
||||
|
||||
type Config struct {
|
||||
Database struct {
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Port int
|
||||
Dbname string
|
||||
Charset string
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Port int
|
||||
Dbname string
|
||||
Charset string
|
||||
Collation string
|
||||
}
|
||||
Server struct {
|
||||
Port int
|
||||
@@ -39,8 +40,12 @@ func InitConfig() {
|
||||
// GetDSN 生成 MySQL 连接字符串
|
||||
func GetDSN() string {
|
||||
db := AppConfig.Database
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||
db.User, db.Password, db.Host, db.Port, db.Dbname, db.Charset)
|
||||
collation := db.Collation
|
||||
if collation == "" {
|
||||
collation = "utf8mb4_unicode_ci"
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&collation=%s&parseTime=True&loc=Local",
|
||||
db.User, db.Password, db.Host, db.Port, db.Dbname, db.Charset, collation)
|
||||
}
|
||||
|
||||
// GetServerPort 获取服务器端口字符串
|
||||
|
||||
Reference in New Issue
Block a user