新增注释
This commit is contained in:
@@ -14,10 +14,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultSecondProfitStartDate 是二板收益统计的默认起始日期。
|
||||
defaultSecondProfitStartDate = "2020-01-01"
|
||||
defaultSecondProfitEndDate = "2026-12-31"
|
||||
// defaultSecondProfitEndDate 是二板收益统计的默认结束日期。
|
||||
defaultSecondProfitEndDate = "2026-12-31"
|
||||
)
|
||||
|
||||
// secondProfitCSVRow 是二板收益查询的内部行结构。
|
||||
//
|
||||
// 前半部分字段直接由 SQL 扫描得到,后半部分字段在 loadSecondProfitRows 中根据行情计算。
|
||||
type secondProfitCSVRow struct {
|
||||
TradeDate string `gorm:"column:trade_date"`
|
||||
PreviousTradeDate string `gorm:"column:previous_trade_date"`
|
||||
@@ -57,6 +62,7 @@ type secondProfitCSVRow struct {
|
||||
NextIntradayProfitable string
|
||||
}
|
||||
|
||||
// secondProfitQuery 保存二板收益接口和 CSV 导出共用的查询条件。
|
||||
type secondProfitQuery struct {
|
||||
TradeStartDate string
|
||||
TradeEndDate string
|
||||
@@ -74,15 +80,20 @@ type secondProfitQuery struct {
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// secondProfitCompareFilter 表示多个比较条件的组合过滤器。
|
||||
//
|
||||
// 当前逻辑使用 AND 关系:所有 Conditions 均满足时才认为匹配。
|
||||
type secondProfitCompareFilter struct {
|
||||
Conditions []secondProfitCompareCondition
|
||||
}
|
||||
|
||||
// secondProfitCompareCondition 表示一个形如 value < 10 或 value >= 09:30 的比较条件。
|
||||
type secondProfitCompareCondition struct {
|
||||
Op string
|
||||
Value float64
|
||||
Op string // 比较操作符,支持 <、>、<=、>=、=。
|
||||
Value float64 // 参与比较的标准化数值;时间会转换为秒数。
|
||||
}
|
||||
|
||||
// secondProfitListItem 是二板收益列表接口返回给前端的单行数据。
|
||||
type secondProfitListItem struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
TsCode string `json:"ts_code"`
|
||||
@@ -104,6 +115,9 @@ type secondProfitListItem struct {
|
||||
Amount *float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// ListSecondBoardProfit 返回二板收益明细的分页 JSON 数据。
|
||||
//
|
||||
// 与 CSV 导出使用同一套查询和筛选逻辑,区别是这里会按 page/pageSize 截取结果。
|
||||
func (h *LimitBoardHandler) ListSecondBoardProfit(c *gin.Context) {
|
||||
req, err := parseSecondProfitQuery(c, true)
|
||||
if err != nil {
|
||||
@@ -156,6 +170,9 @@ func (h *LimitBoardHandler) ListSecondBoardProfit(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadSecondBoardProfitCSV 导出二板收益明细 CSV。
|
||||
//
|
||||
// 输出 UTF-8 BOM,便于 Excel 直接打开中文表头和内容。
|
||||
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
req, err := parseSecondProfitQuery(c, false)
|
||||
if err != nil {
|
||||
@@ -192,6 +209,9 @@ func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// parseSecondProfitQuery 解析二板收益接口查询参数。
|
||||
//
|
||||
// includePagination 为 false 时跳过分页解析,供 CSV 导出复用筛选条件。
|
||||
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))
|
||||
@@ -261,6 +281,9 @@ func parseSecondProfitQuery(c *gin.Context, includePagination bool) (secondProfi
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// secondProfitFilterParts 兼容两种过滤参数格式。
|
||||
//
|
||||
// 既支持 limitUpTimeFilterType1/Value1 这种具名参数,也支持 limitUpTimeFilter[]=... 数组格式。
|
||||
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"))
|
||||
@@ -284,6 +307,7 @@ func secondProfitFilterParts(c *gin.Context, base string) (string, string, strin
|
||||
return "", "", "", ""
|
||||
}
|
||||
|
||||
// secondProfitFilterArrayValue 安全读取数组参数中的指定位置。
|
||||
func secondProfitFilterArrayValue(values []string, index int) string {
|
||||
if index >= len(values) {
|
||||
return ""
|
||||
@@ -291,6 +315,7 @@ func secondProfitFilterArrayValue(values []string, index int) string {
|
||||
return strings.TrimSpace(values[index])
|
||||
}
|
||||
|
||||
// validateSecondProfitDateRange 校验查询日期格式和先后顺序。
|
||||
func validateSecondProfitDateRange(startDate, endDate string) error {
|
||||
start, err := time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
@@ -306,6 +331,10 @@ func validateSecondProfitDateRange(startDate, endDate string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSecondProfitRows 查询并计算二板收益明细。
|
||||
//
|
||||
// SQL 先找出“前一日涨停、前两日未涨停、当日继续涨停”的股票,再补齐 T+1 行情;
|
||||
// Go 代码负责应用动态过滤条件并计算开盘、最高、最低、收盘相对 T 日收盘的收益率。
|
||||
func (h *LimitBoardHandler) loadSecondProfitRows(req secondProfitQuery) ([]secondProfitCSVRow, error) {
|
||||
query := `
|
||||
WITH open_calendar AS (
|
||||
@@ -372,6 +401,7 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
|
||||
rows := make([]secondProfitCSVRow, 0, len(rawRows))
|
||||
for _, row := range rawRows {
|
||||
// 首次涨停时间过滤在内存中执行,便于兼容 0930、93000、09:30:00 等多种时间格式。
|
||||
if req.LimitUpTimeRule.Enabled() {
|
||||
limitTimeValue, ok := secondProfitTimeSeconds(row.FirstLimitTime.String)
|
||||
if !ok || !req.LimitUpTimeRule.Match(float64(limitTimeValue)) {
|
||||
@@ -379,6 +409,7 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
}
|
||||
}
|
||||
|
||||
// 开盘涨跌幅依赖前收价格,只有行情完整且前收大于 0 时才计算。
|
||||
if row.Open.Valid && row.PreviousClose.Valid && row.PreviousClose.Float64 > 0 {
|
||||
row.OpenPreviousCloseRatio = row.Open.Float64 / row.PreviousClose.Float64
|
||||
row.OpenPctChg = sql.NullFloat64{
|
||||
@@ -396,6 +427,7 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
continue
|
||||
}
|
||||
|
||||
// T+1 收益统一以 T 日收盘价为基准,缺失的次日行情会保留为 NullFloat64。
|
||||
if row.Close.Valid && row.Close.Float64 > 0 {
|
||||
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close.Float64)
|
||||
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close.Float64)
|
||||
@@ -410,6 +442,7 @@ ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// secondProfitCSVHeaders 返回二板收益 CSV 表头。
|
||||
func secondProfitCSVHeaders() []string {
|
||||
return []string{
|
||||
"交易日", "股票代码", "股票简称", "市场", "行业",
|
||||
@@ -421,6 +454,7 @@ func secondProfitCSVHeaders() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// secondProfitCSVRecord 把内部行结构转换为 CSV 字段。
|
||||
func secondProfitCSVRecord(row secondProfitCSVRow) []string {
|
||||
return []string{
|
||||
row.TradeDate,
|
||||
@@ -444,6 +478,7 @@ func secondProfitCSVRecord(row secondProfitCSVRow) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// secondProfitBuildItem 把内部行结构转换为 JSON 列表项。
|
||||
func secondProfitBuildItem(row secondProfitCSVRow) secondProfitListItem {
|
||||
return secondProfitListItem{
|
||||
TradeDate: row.TradeDate,
|
||||
@@ -467,6 +502,7 @@ func secondProfitBuildItem(row secondProfitCSVRow) secondProfitListItem {
|
||||
}
|
||||
}
|
||||
|
||||
// parseSecondProfitCompareFilter 根据两组操作符和值构造比较过滤器。
|
||||
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 {
|
||||
@@ -478,6 +514,7 @@ func parseSecondProfitCompareFilter(op1, value1, op2, value2 string, parseValue
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// appendSecondProfitCompareCondition 校验并追加一个比较条件。
|
||||
func appendSecondProfitCompareCondition(filter *secondProfitCompareFilter, op, rawValue string, parseValue func(string) (float64, error), fieldName, groupName string) error {
|
||||
op = strings.TrimSpace(op)
|
||||
rawValue = strings.TrimSpace(rawValue)
|
||||
@@ -498,6 +535,7 @@ func appendSecondProfitCompareCondition(filter *secondProfitCompareFilter, op, r
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSecondProfitCompareOp 判断操作符是否在白名单内。
|
||||
func isSecondProfitCompareOp(op string) bool {
|
||||
switch op {
|
||||
case "<", ">", "<=", ">=", "=":
|
||||
@@ -507,10 +545,12 @@ func isSecondProfitCompareOp(op string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled 表示过滤器中是否存在有效条件。
|
||||
func (f secondProfitCompareFilter) Enabled() bool {
|
||||
return len(f.Conditions) > 0
|
||||
}
|
||||
|
||||
// Match 判断输入值是否满足过滤器中的所有条件。
|
||||
func (f secondProfitCompareFilter) Match(value float64) bool {
|
||||
if !f.Enabled() {
|
||||
return true
|
||||
@@ -523,6 +563,7 @@ func (f secondProfitCompareFilter) Match(value float64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Match 判断输入值是否满足单个比较条件。
|
||||
func (c secondProfitCompareCondition) Match(value float64) bool {
|
||||
switch c.Op {
|
||||
case "<":
|
||||
@@ -540,6 +581,7 @@ func (c secondProfitCompareCondition) Match(value float64) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// parseSecondProfitTimeValue 把时间字符串转换为当天秒数。
|
||||
func parseSecondProfitTimeValue(value string) (float64, error) {
|
||||
seconds, ok := secondProfitTimeSeconds(value)
|
||||
if !ok {
|
||||
@@ -548,10 +590,12 @@ func parseSecondProfitTimeValue(value string) (float64, error) {
|
||||
return float64(seconds), nil
|
||||
}
|
||||
|
||||
// parseSecondProfitFloatValue 解析普通浮点过滤值。
|
||||
func parseSecondProfitFloatValue(value string) (float64, error) {
|
||||
return strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||
}
|
||||
|
||||
// secondProfitTimeSeconds 兼容多种涨停时间格式并转换为当天秒数。
|
||||
func secondProfitTimeSeconds(limitTime string) (int, bool) {
|
||||
timeText := strings.TrimSpace(limitTime)
|
||||
if timeText == "" {
|
||||
@@ -596,6 +640,7 @@ func secondProfitTimeSeconds(limitTime string) (int, bool) {
|
||||
return hour*3600 + minute*60 + second, true
|
||||
}
|
||||
|
||||
// secondProfitFormatLimitTime 把紧凑时间格式格式化为 HH:MM:SS。
|
||||
func secondProfitFormatLimitTime(value string) string {
|
||||
timeText := strings.TrimSpace(value)
|
||||
if timeText == "" || strings.Contains(timeText, ":") {
|
||||
@@ -613,6 +658,7 @@ func secondProfitFormatLimitTime(value string) string {
|
||||
return timeText[0:2] + ":" + timeText[2:4] + ":" + timeText[4:6]
|
||||
}
|
||||
|
||||
// secondProfitPctReturn 计算相对基准价的百分比收益。
|
||||
func secondProfitPctReturn(value sql.NullFloat64, base float64) sql.NullFloat64 {
|
||||
if !value.Valid || base == 0 {
|
||||
return sql.NullFloat64{}
|
||||
@@ -620,6 +666,7 @@ func secondProfitPctReturn(value sql.NullFloat64, base float64) sql.NullFloat64
|
||||
return sql.NullFloat64{Float64: secondProfitRound2((value.Float64 - base) / base * 100), Valid: true}
|
||||
}
|
||||
|
||||
// secondProfitLabel 根据收益是否大于 0 输出中文盈利标签。
|
||||
func secondProfitLabel(value sql.NullFloat64) string {
|
||||
if !value.Valid {
|
||||
return "无次日数据"
|
||||
@@ -630,14 +677,17 @@ func secondProfitLabel(value sql.NullFloat64) string {
|
||||
return "否"
|
||||
}
|
||||
|
||||
// secondProfitRound2 保留两位小数。
|
||||
func secondProfitRound2(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
// secondProfitFormatFloat 使用最短十进制格式输出浮点数。
|
||||
func secondProfitFormatFloat(value float64) string {
|
||||
return strconv.FormatFloat(value, 'f', -1, 64)
|
||||
}
|
||||
|
||||
// secondProfitFormatNullFloat 格式化可空浮点数,空值输出为空字符串。
|
||||
func secondProfitFormatNullFloat(value sql.NullFloat64) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
@@ -645,6 +695,7 @@ func secondProfitFormatNullFloat(value sql.NullFloat64) string {
|
||||
return secondProfitFormatFloat(value.Float64)
|
||||
}
|
||||
|
||||
// secondProfitFormatNullInt 格式化可空整数,空值输出为空字符串。
|
||||
func secondProfitFormatNullInt(value sql.NullInt64) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
@@ -652,6 +703,7 @@ func secondProfitFormatNullInt(value sql.NullInt64) string {
|
||||
return strconv.FormatInt(value.Int64, 10)
|
||||
}
|
||||
|
||||
// secondProfitNullFloatPtr 把 sql.NullFloat64 转换为 JSON 友好的 *float64。
|
||||
func secondProfitNullFloatPtr(value sql.NullFloat64) *float64 {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
@@ -660,6 +712,7 @@ func secondProfitNullFloatPtr(value sql.NullFloat64) *float64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// secondProfitNullIntPtr 把 sql.NullInt64 转换为 JSON 友好的 *int64。
|
||||
func secondProfitNullIntPtr(value sql.NullInt64) *int64 {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user