diff --git a/cmd/server/main.go b/cmd/server/main.go index 661cd9d..56a4e11 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,22 +6,23 @@ import ( "level_go/internal/api" "level_go/internal/router" "level_go/internal/service" - "level_go/pkg/config" // 引入配置包 + "level_go/pkg/config" "log" ) +// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。 func main() { - // 1. 初始化配置 + // 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。 config.InitConfig() - // 2. 使用配置中的 DSN 连接数据库 + // 使用配置中的 MySQL DSN 建立 GORM 连接;连接失败时直接终止进程,避免服务在不可用状态下启动。 dsn := config.GetDSN() db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("数据库连接失败:", err) } - // 3. 依赖注入 + // 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。 commonSrv := &service.CommonService{DB: db} stockSrv := &service.StockService{DB: db} limitBoardSrv := &service.LimitBoardService{DB: db} @@ -30,7 +31,7 @@ func main() { stockHandler := &api.StockHandler{Service: stockSrv} limitBoardHandler := api.NewLimitBoardHandler(limitBoardSrv) - // 4. 启动路由 + // 创建 Gin 路由并启动 HTTP 服务。 r := router.NewRouter(commonHandler, stockHandler, limitBoardHandler) port := config.GetServerPort() diff --git a/internal/api/common_handler.go b/internal/api/common_handler.go index 1ac2eb2..a8f71e3 100644 --- a/internal/api/common_handler.go +++ b/internal/api/common_handler.go @@ -6,10 +6,12 @@ import ( "net/http" ) +// CommonHandler 承载通用基础数据接口。 type CommonHandler struct { - Service *service.CommonService + Service *service.CommonService // 通用数据服务,负责访问交易日历等基础表。 } +// GetTradeCalendar 返回上交所开市日期列表和最新开市日期。 func (h *CommonHandler) GetTradeCalendar(c *gin.Context) { resp, err := h.Service.GetTradeCalendar() if err != nil { diff --git a/internal/api/limit_board_break_next_performance_csv_handler.go b/internal/api/limit_board_break_next_performance_csv_handler.go index 6d2fe2b..96ccc7d 100644 --- a/internal/api/limit_board_break_next_performance_csv_handler.go +++ b/internal/api/limit_board_break_next_performance_csv_handler.go @@ -11,6 +11,9 @@ import ( "level_go/internal/model" ) +// DownloadBreakNextPerformanceCSV 导出炸板股 T+1 表现明细 CSV。 +// +// 查询参数与 GetBreakNextPerformance 共用解析逻辑,但导出时不分页,直接输出全部命中记录。 func (h *LimitBoardHandler) DownloadBreakNextPerformanceCSV(c *gin.Context) { req, err := parseLimitBreakNextPerformanceRequest(c, false) if err != nil { @@ -47,6 +50,7 @@ func (h *LimitBoardHandler) DownloadBreakNextPerformanceCSV(c *gin.Context) { } } +// breakNextPerformanceCSVHeaders 返回 CSV 表头,顺序必须与 breakNextPerformanceCSVRecord 保持一致。 func breakNextPerformanceCSVHeaders() []string { return []string{ "T日", @@ -75,6 +79,7 @@ func breakNextPerformanceCSVHeaders() []string { } } +// breakNextPerformanceCSVRecord 把单条炸板表现记录转换为 CSV 字段。 func breakNextPerformanceCSVRecord(row model.LimitBreakNextPerformanceStock) []string { return []string{ row.TradeDate, @@ -103,6 +108,7 @@ func breakNextPerformanceCSVRecord(row model.LimitBreakNextPerformanceStock) []s } } +// breakNextPerformanceFormatFloatPtr 格式化可空浮点数,nil 输出为空字段。 func breakNextPerformanceFormatFloatPtr(value *float64) string { if value == nil { return "" @@ -110,6 +116,7 @@ func breakNextPerformanceFormatFloatPtr(value *float64) string { return strconv.FormatFloat(*value, 'f', -1, 64) } +// breakNextPerformanceBoolLabel 把布尔值转换为中文是否标签。 func breakNextPerformanceBoolLabel(value bool) string { if value { return "是" @@ -117,6 +124,7 @@ func breakNextPerformanceBoolLabel(value bool) string { return "否" } +// breakNextPerformanceBoolPtrLabel 格式化可空布尔值,用于区分无次日行情和明确的是/否。 func breakNextPerformanceBoolPtrLabel(value *bool) string { if value == nil { return "无次日数据" diff --git a/internal/api/limit_board_handler.go b/internal/api/limit_board_handler.go index e8f4a41..583d9a8 100644 --- a/internal/api/limit_board_handler.go +++ b/internal/api/limit_board_handler.go @@ -11,14 +11,17 @@ import ( "level_go/internal/service" ) +// LimitBoardHandler 承载涨停板相关 HTTP 接口。 type LimitBoardHandler struct { - Service *service.LimitBoardService + Service *service.LimitBoardService // 涨停板服务,封装统计、分组和导出逻辑。 } +// NewLimitBoardHandler 创建涨停板接口 Handler。 func NewLimitBoardHandler(s *service.LimitBoardService) *LimitBoardHandler { return &LimitBoardHandler{Service: s} } +// GetOverview 返回指定交易日的涨停股连板概览。 func (h *LimitBoardHandler) GetOverview(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { @@ -35,6 +38,7 @@ func (h *LimitBoardHandler) GetOverview(c *gin.Context) { c.JSON(http.StatusOK, resp) } +// GetOpenLowerGroups 返回昨日涨停且今日继续涨停股票的开盘对比分组。 func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { @@ -51,6 +55,7 @@ func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) { c.JSON(http.StatusOK, resp) } +// GetBreakNextPerformance 返回首板后次日炸板股票的 T+1 表现分页结果。 func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) { req, err := parseLimitBreakNextPerformanceRequest(c, true) if err != nil { @@ -67,6 +72,9 @@ func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) { c.JSON(http.StatusOK, resp) } +// parseLimitBreakNextPerformanceRequest 解析炸板次日表现接口的查询参数。 +// +// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。 func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) { req := model.LimitBreakNextPerformanceRequest{ TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))), @@ -116,6 +124,9 @@ func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination boo return req, nil } +// parseOptionalBoolQuery 从多个兼容参数名中读取可选布尔值。 +// +// 返回 nil 表示用户没有传入该过滤项;返回 *bool 表示需要按 true/false 过滤。 func parseOptionalBoolQuery(c *gin.Context, keys ...string) (*bool, error) { for _, key := range keys { valueText, exists := c.GetQuery(key) @@ -135,6 +146,7 @@ func parseOptionalBoolQuery(c *gin.Context, keys ...string) (*bool, error) { return nil, nil } +// GetOpenThresholdGroups 返回昨日涨停股今日开盘是否低于前收 101% 的分组。 func (h *LimitBoardHandler) GetOpenThresholdGroups(c *gin.Context) { var req model.LimitBoardRequest if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" { diff --git a/internal/api/limit_second_profit_csv_handler.go b/internal/api/limit_second_profit_csv_handler.go index 4912426..b146b48 100644 --- a/internal/api/limit_second_profit_csv_handler.go +++ b/internal/api/limit_second_profit_csv_handler.go @@ -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 diff --git a/internal/api/stock_handler.go b/internal/api/stock_handler.go index 725a26a..22e57c7 100644 --- a/internal/api/stock_handler.go +++ b/internal/api/stock_handler.go @@ -7,15 +7,19 @@ import ( "net/http" ) +// StockHandler 承载股票行情和走势分析接口。 type StockHandler struct { - Service *service.StockService + Service *service.StockService // 股票服务,封装行情查询和区间分析逻辑。 } +// NewStockHandler 创建股票接口 Handler。 func NewStockHandler(s *service.StockService) *StockHandler { return &StockHandler{Service: s} } -// GetKlineData 获取指定股票从2024-09-24至今的K线 +// GetKlineData 获取指定股票的历史 K 线数据。 +// +// 股票代码来自路径参数 :code,返回按交易日升序排列的 stock_daily 记录。 func (h *StockHandler) GetKlineData(c *gin.Context) { code := c.Param("code") if code == "" { @@ -32,6 +36,7 @@ func (h *StockHandler) GetKlineData(c *gin.Context) { c.JSON(http.StatusOK, data) } +// Analyze 根据前端传入的起止日期分析涨停后走势。 func (h *StockHandler) Analyze(c *gin.Context) { var req model.AnalyzeRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/model/calendar.go b/internal/model/calendar.go index e613a31..bf2f005 100644 --- a/internal/model/calendar.go +++ b/internal/model/calendar.go @@ -2,16 +2,19 @@ package model import "time" +// TradeCalendar 映射交易日历表,用于判断某天是否为指定交易所的开市日。 type TradeCalendar struct { - CalDate time.Time `gorm:"column:cal_date;primaryKey"` - Exchange string `gorm:"column:exchange;primaryKey"` - IsOpen int `gorm:"column:is_open"` - PretradeDate time.Time `gorm:"column:pretrade_date"` + CalDate time.Time `gorm:"column:cal_date;primaryKey"` // 日历日期。 + Exchange string `gorm:"column:exchange;primaryKey"` // 交易所代码,例如 SSE。 + IsOpen int `gorm:"column:is_open"` // 是否开市,1 表示开市。 + PretradeDate time.Time `gorm:"column:pretrade_date"` // 上一个交易日。 } +// TableName 指定 GORM 查询时使用 trade_calendar 表。 func (TradeCalendar) TableName() string { return "trade_calendar" } +// CalendarResponse 是交易日历接口响应。 type CalendarResponse struct { - OpenDates []string `json:"open_dates"` - MaxDate string `json:"max_date"` + OpenDates []string `json:"open_dates"` // 所有开市日期,格式 YYYY-MM-DD。 + MaxDate string `json:"max_date"` // 当前返回结果中的最大开市日期。 } diff --git a/internal/model/limit_board.go b/internal/model/limit_board.go index e6b222c..9af961b 100644 --- a/internal/model/limit_board.go +++ b/internal/model/limit_board.go @@ -1,214 +1,232 @@ package model +// LimitBoardRequest 是按交易日查询涨停板数据的通用请求。 type LimitBoardRequest struct { - TradeDate string `form:"trade_date" json:"trade_date"` + TradeDate string `form:"trade_date" json:"trade_date"` // 目标交易日,格式 YYYY-MM-DD。 } +// LimitBreakNextPerformanceRequest 是查询炸板股次日表现的请求。 type LimitBreakNextPerformanceRequest struct { - TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` - TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` - Page int `form:"page" json:"page"` - PageSize int `form:"pageSize" json:"page_size"` - TOpenGainLt101 *bool `json:"t_open_gain_lt_101,omitempty"` - BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"` + TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` // 查询起始交易日。 + TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束交易日。 + Page int `form:"page" json:"page"` // 当前页码,从 1 开始。 + PageSize int `form:"pageSize" json:"page_size"` // 每页数量。 + TOpenGainLt101 *bool `json:"t_open_gain_lt_101,omitempty"` // 可选过滤:T 日开盘是否低于前收 101%。 + BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"` // 可选过滤:炸板时间是否早于 11:00。 } +// LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。 type LimitBoardStock struct { - Symbol string `json:"symbol"` - TsCode string `json:"ts_code"` - Name string `json:"name"` - Market string `json:"market"` - TradeDate string `json:"trade_date"` - Close float64 `json:"close"` - LimitPrice float64 `json:"limit_price"` - PctChg float64 `json:"pct_chg"` - AkAmount *float64 `json:"ak_amount"` - AkFloatMarketValue *float64 `json:"ak_float_market_value"` - AkTotalMarketValue *float64 `json:"ak_total_market_value"` - AkTurnoverRate *float64 `json:"ak_turnover_rate"` - AkSealAmount *float64 `json:"ak_seal_amount"` - AkFirstLimitTime *string `json:"ak_first_limit_time"` - AkLastLimitTime *string `json:"ak_last_limit_time"` - AkBreakLimitCount *int `json:"ak_break_limit_count"` - AkLimitUpStat *string `json:"ak_limit_up_stat"` - AkLimitUpDays *int `json:"ak_limit_up_days"` - AkIndustry *string `json:"ak_industry"` - Vol *int64 `json:"vol"` - BoardCount int `json:"board_count"` - BoardLabel string `json:"board_label"` - DisplayStatus string `json:"display_status"` + Symbol string `json:"symbol"` // 市场展示用股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属市场。 + TradeDate string `json:"trade_date"` // 交易日。 + Close float64 `json:"close"` // 收盘价。 + LimitPrice float64 `json:"limit_price"` // 涨停价。 + PctChg float64 `json:"pct_chg"` // 当日涨跌幅。 + AkAmount *float64 `json:"ak_amount"` // 成交额,数据库为空时返回 null。 + AkFloatMarketValue *float64 `json:"ak_float_market_value"` // 流通市值。 + AkTotalMarketValue *float64 `json:"ak_total_market_value"` // 总市值。 + AkTurnoverRate *float64 `json:"ak_turnover_rate"` // 换手率。 + AkSealAmount *float64 `json:"ak_seal_amount"` // 封单额。 + AkFirstLimitTime *string `json:"ak_first_limit_time"` // 首次涨停时间。 + AkLastLimitTime *string `json:"ak_last_limit_time"` // 最后涨停时间。 + AkBreakLimitCount *int `json:"ak_break_limit_count"` // 开板次数。 + AkLimitUpStat *string `json:"ak_limit_up_stat"` // 连板状态原始描述。 + AkLimitUpDays *int `json:"ak_limit_up_days"` // 连续涨停天数。 + AkIndustry *string `json:"ak_industry"` // 行业。 + Vol *int64 `json:"vol"` // 成交量。 + BoardCount int `json:"board_count"` // 计算出的连板数。 + BoardLabel string `json:"board_label"` // 连板标签,例如 二板。 + DisplayStatus string `json:"display_status"` // 前端展示状态,当前与 BoardLabel 保持一致。 } +// LimitOpenCompareStock 表示“今日涨停且昨日涨停”的开盘价对比数据。 type LimitOpenCompareStock struct { - Symbol string `json:"symbol"` - TsCode string `json:"ts_code"` - Name string `json:"name"` - Market string `json:"market"` - TradeDate string `json:"trade_date"` - HasOpen bool `json:"-"` - Open float64 `json:"open"` - Close float64 `json:"close"` - PreviousTradeDate string `json:"previous_trade_date"` - PreviousTwoTradeDate string `json:"previous_two_trade_date"` - HasPreviousClose bool `json:"-"` - PreviousClose float64 `json:"previous_close"` - IsFirstToSecondBoard bool `json:"is_first_to_second_board"` - IsFirstSecondBeforeEleven bool `json:"is_first_second_before_eleven"` - HighlightLabel string `json:"highlight_label"` - LimitPrice float64 `json:"limit_price"` - PctChg float64 `json:"pct_chg"` - AkAmount *float64 `json:"ak_amount"` - AkFloatMarketValue *float64 `json:"ak_float_market_value"` - AkTotalMarketValue *float64 `json:"ak_total_market_value"` - AkTurnoverRate *float64 `json:"ak_turnover_rate"` - AkSealAmount *float64 `json:"ak_seal_amount"` - AkFirstLimitTime *string `json:"ak_first_limit_time"` - AkLastLimitTime *string `json:"ak_last_limit_time"` - AkBreakLimitCount *int `json:"ak_break_limit_count"` - AkLimitUpStat *string `json:"ak_limit_up_stat"` - AkLimitUpDays *int `json:"ak_limit_up_days"` - AkIndustry *string `json:"ak_industry"` - Vol *int64 `json:"vol"` + Symbol string `json:"symbol"` // 市场展示用股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属市场。 + TradeDate string `json:"trade_date"` // 查询交易日。 + HasOpen bool `json:"-"` // 数据库是否存在今日开盘价,内部计算使用。 + Open float64 `json:"open"` // 今日开盘价。 + Close float64 `json:"close"` // 今日收盘价。 + PreviousTradeDate string `json:"previous_trade_date"` // 上一个交易日。 + PreviousTwoTradeDate string `json:"previous_two_trade_date"` // 上上个交易日。 + HasPreviousClose bool `json:"-"` // 数据库是否存在前收价,内部计算使用。 + PreviousClose float64 `json:"previous_close"` // 上一个交易日收盘价。 + IsFirstToSecondBoard bool `json:"is_first_to_second_board"` // 是否属于首板进二板。 + IsFirstSecondBeforeEleven bool `json:"is_first_second_before_eleven"` // 首板进二板且 11 点前封板。 + HighlightLabel string `json:"highlight_label"` // 前端高亮标签。 + LimitPrice float64 `json:"limit_price"` // 今日涨停价。 + PctChg float64 `json:"pct_chg"` // 今日涨跌幅。 + AkAmount *float64 `json:"ak_amount"` // 成交额。 + AkFloatMarketValue *float64 `json:"ak_float_market_value"` // 流通市值。 + AkTotalMarketValue *float64 `json:"ak_total_market_value"` // 总市值。 + AkTurnoverRate *float64 `json:"ak_turnover_rate"` // 换手率。 + AkSealAmount *float64 `json:"ak_seal_amount"` // 封单额。 + AkFirstLimitTime *string `json:"ak_first_limit_time"` // 首次涨停时间。 + AkLastLimitTime *string `json:"ak_last_limit_time"` // 最后涨停时间。 + AkBreakLimitCount *int `json:"ak_break_limit_count"` // 开板次数。 + AkLimitUpStat *string `json:"ak_limit_up_stat"` // 连板状态原始描述。 + AkLimitUpDays *int `json:"ak_limit_up_days"` // 连续涨停天数。 + AkIndustry *string `json:"ak_industry"` // 行业。 + Vol *int64 `json:"vol"` // 成交量。 } +// LimitOpenThresholdStock 表示昨日涨停股在今日开盘阈值分组中的单股数据。 type LimitOpenThresholdStock struct { - Symbol string `json:"symbol"` - TsCode string `json:"ts_code"` - Name string `json:"name"` - Market string `json:"market"` - TradeDate string `json:"trade_date"` - HasOpen bool `json:"-"` - Open float64 `json:"open"` - Close float64 `json:"close"` - PreviousTradeDate string `json:"previous_trade_date"` - HasPreviousClose bool `json:"-"` - PreviousClose float64 `json:"previous_close"` - OpenThresholdPrice float64 `json:"open_threshold_price"` - OpenPreviousCloseRatio float64 `json:"open_previous_close_ratio"` - LimitPrice float64 `json:"limit_price"` - PctChg float64 `json:"pct_chg"` - AkAmount *float64 `json:"ak_amount"` - AkFloatMarketValue *float64 `json:"ak_float_market_value"` - AkTotalMarketValue *float64 `json:"ak_total_market_value"` - AkTurnoverRate *float64 `json:"ak_turnover_rate"` - AkSealAmount *float64 `json:"ak_seal_amount"` - AkFirstLimitTime *string `json:"ak_first_limit_time"` - AkLastLimitTime *string `json:"ak_last_limit_time"` - AkBreakLimitCount *int `json:"ak_break_limit_count"` - AkLimitUpStat *string `json:"ak_limit_up_stat"` - AkLimitUpDays *int `json:"ak_limit_up_days"` - AkIndustry *string `json:"ak_industry"` - Vol *int64 `json:"vol"` + Symbol string `json:"symbol"` // 市场展示用股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属市场。 + TradeDate string `json:"trade_date"` // 查询交易日。 + HasOpen bool `json:"-"` // 数据库是否存在今日开盘价,内部计算使用。 + Open float64 `json:"open"` // 今日开盘价。 + Close float64 `json:"close"` // 今日收盘价。 + PreviousTradeDate string `json:"previous_trade_date"` // 上一个交易日。 + HasPreviousClose bool `json:"-"` // 数据库是否存在前收价,内部计算使用。 + PreviousClose float64 `json:"previous_close"` // 上一个交易日收盘价。 + OpenThresholdPrice float64 `json:"open_threshold_price"` // 前收价乘以阈值比例后的开盘阈值。 + OpenPreviousCloseRatio float64 `json:"open_previous_close_ratio"` // 今日开盘价与前收价的比值。 + LimitPrice float64 `json:"limit_price"` // 今日涨停价。 + PctChg float64 `json:"pct_chg"` // 今日涨跌幅。 + AkAmount *float64 `json:"ak_amount"` // 成交额。 + AkFloatMarketValue *float64 `json:"ak_float_market_value"` // 流通市值。 + AkTotalMarketValue *float64 `json:"ak_total_market_value"` // 总市值。 + AkTurnoverRate *float64 `json:"ak_turnover_rate"` // 换手率。 + AkSealAmount *float64 `json:"ak_seal_amount"` // 封单额。 + AkFirstLimitTime *string `json:"ak_first_limit_time"` // 首次涨停时间。 + AkLastLimitTime *string `json:"ak_last_limit_time"` // 最后涨停时间。 + AkBreakLimitCount *int `json:"ak_break_limit_count"` // 开板次数。 + AkLimitUpStat *string `json:"ak_limit_up_stat"` // 连板状态原始描述。 + AkLimitUpDays *int `json:"ak_limit_up_days"` // 连续涨停天数。 + AkIndustry *string `json:"ak_industry"` // 行业。 + Vol *int64 `json:"vol"` // 成交量。 } +// LimitBreakNextPerformanceStock 表示首板后次日炸板股票的 T+1 表现。 type LimitBreakNextPerformanceStock struct { - TradeDate string `json:"trade_date"` - Symbol string `json:"symbol"` - TsCode string `json:"ts_code"` - Name string `json:"name"` - Market string `json:"market"` - Industry string `json:"industry"` - TOpen *float64 `json:"t_open"` - TOpenPctChg *float64 `json:"t_open_pct_chg"` - TOpenGainLt101 bool `json:"t_open_gain_lt_101"` - THigh *float64 `json:"t_high"` - TClose *float64 `json:"t_close"` - TPctChg *float64 `json:"t_pct_chg"` - FirstTime string `json:"first_time"` - BreakTimeBefore1100 bool `json:"break_time_before_1100"` - HasTushareBreakData bool `json:"has_tushare_break_data"` - NextOpen *float64 `json:"next_open"` - NextHigh *float64 `json:"next_high"` - NextLow *float64 `json:"next_low"` - NextClose *float64 `json:"next_close"` - NextOpenPctChg *float64 `json:"next_open_pct_chg"` - NextIntradayProfitable *bool `json:"next_intraday_profitable"` - NextHighProfitPct *float64 `json:"next_high_profit_pct"` - NextLowLossPct *float64 `json:"next_low_loss_pct"` - NextClosePctChg *float64 `json:"next_close_pct_chg"` + TradeDate string `json:"trade_date"` // T 日交易日,即炸板发生日。 + Symbol string `json:"symbol"` // 市场展示用股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属市场。 + Industry string `json:"industry"` // 行业。 + TOpen *float64 `json:"t_open"` // T 日开盘价。 + TOpenPctChg *float64 `json:"t_open_pct_chg"` // T 日开盘相对前收的涨跌幅。 + TOpenGainLt101 bool `json:"t_open_gain_lt_101"` // T 日开盘价是否低于前收 101%。 + THigh *float64 `json:"t_high"` // T 日最高价。 + TClose *float64 `json:"t_close"` // T 日收盘价。 + TPctChg *float64 `json:"t_pct_chg"` // T 日涨跌幅。 + FirstTime string `json:"first_time"` // TuShare 首次炸板/涨停时间,已格式化为 HH:MM:SS。 + BreakTimeBefore1100 bool `json:"break_time_before_1100"` // 炸板相关时间是否早于 11:00。 + HasTushareBreakData bool `json:"has_tushare_break_data"` // TuShare 是否明确标记为炸板。 + NextOpen *float64 `json:"next_open"` // T+1 开盘价。 + NextHigh *float64 `json:"next_high"` // T+1 最高价。 + NextLow *float64 `json:"next_low"` // T+1 最低价。 + NextClose *float64 `json:"next_close"` // T+1 收盘价。 + NextOpenPctChg *float64 `json:"next_open_pct_chg"` // T+1 开盘相对 T 日收盘的收益率。 + NextIntradayProfitable *bool `json:"next_intraday_profitable"` // T+1 最高价是否覆盖 T 日最高价。 + NextHighProfitPct *float64 `json:"next_high_profit_pct"` // T+1 最高价相对 T 日收盘的最大收益率。 + NextLowLossPct *float64 `json:"next_low_loss_pct"` // T+1 最低价相对 T 日收盘的最大回撤率。 + NextClosePctChg *float64 `json:"next_close_pct_chg"` // T+1 收盘相对 T 日收盘的收益率。 } +// LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。 type LimitOpenCompareGroup struct { - Key string `json:"key"` - Label string `json:"label"` - Count int `json:"count"` - Stocks []LimitOpenCompareStock `json:"stocks"` + Key string `json:"key"` // 分组唯一标识。 + Label string `json:"label"` // 分组展示名称。 + Count int `json:"count"` // 分组内股票数量。 + Stocks []LimitOpenCompareStock `json:"stocks"` // 分组内股票列表。 } +// LimitOpenThresholdGroup 是开盘价阈值接口的分组结果。 type LimitOpenThresholdGroup struct { - Key string `json:"key"` - Label string `json:"label"` - Count int `json:"count"` - Stocks []LimitOpenThresholdStock `json:"stocks"` + Key string `json:"key"` // 分组唯一标识。 + Label string `json:"label"` // 分组展示名称。 + Count int `json:"count"` // 分组内股票数量。 + Stocks []LimitOpenThresholdStock `json:"stocks"` // 分组内股票列表。 } +// LimitOpenCompareSummary 汇总昨日涨停股今日开盘表现。 type LimitOpenCompareSummary struct { - LimitCount int `json:"limit_count"` - OpenLowerPreviousCloseCount int `json:"open_lower_previous_close_count"` - OthersCount int `json:"others_count"` - FirstToSecondBoardCount int `json:"first_to_second_board_count"` - FirstSecondBeforeElevenCount int `json:"first_second_before_eleven_count"` + LimitCount int `json:"limit_count"` // 昨日涨停且今日继续涨停的股票总数。 + OpenLowerPreviousCloseCount int `json:"open_lower_previous_close_count"` // 今日开盘低于昨日收盘的股票数量。 + OthersCount int `json:"others_count"` // 未进入低开分组的其他股票数量。 + FirstToSecondBoardCount int `json:"first_to_second_board_count"` // 首板进二板股票数量。 + FirstSecondBeforeElevenCount int `json:"first_second_before_eleven_count"` // 首板进二板且 11 点前封板数量。 } +// LimitOpenThresholdSummary 汇总开盘阈值分组统计。 type LimitOpenThresholdSummary struct { - LimitCount int `json:"limit_count"` - OpenWithinThresholdCount int `json:"open_within_threshold_count"` - OpenAtOrAboveThresholdCount int `json:"open_at_or_above_threshold_count"` - ThresholdRatio float64 `json:"threshold_ratio"` + LimitCount int `json:"limit_count"` // 昨日涨停且今日继续涨停的股票总数。 + OpenWithinThresholdCount int `json:"open_within_threshold_count"` // 今日开盘低于阈值的股票数量。 + OpenAtOrAboveThresholdCount int `json:"open_at_or_above_threshold_count"` // 今日开盘达到或超过阈值的股票数量。 + ThresholdRatio float64 `json:"threshold_ratio"` // 阈值比例,例如 1.01 表示前收 101%。 } +// LimitBreakNextPerformanceSummary 汇总炸板股次日表现统计。 type LimitBreakNextPerformanceSummary struct { - BreakCount int `json:"break_count"` - NextTradeDataCount int `json:"next_trade_data_count"` + BreakCount int `json:"break_count"` // 命中过滤条件的炸板股票数量。 + NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 行情数据的股票数量。 } +// LimitOpenCompareResponse 是开盘价低于前收接口响应。 type LimitOpenCompareResponse struct { - TradeDate string `json:"trade_date"` - PreviousTradeDate string `json:"previous_trade_date"` - PreviousTwoTradeDate string `json:"previous_two_trade_date"` - Summary LimitOpenCompareSummary `json:"summary"` - Groups []LimitOpenCompareGroup `json:"groups"` + TradeDate string `json:"trade_date"` // 查询交易日。 + PreviousTradeDate string `json:"previous_trade_date"` // 上一个交易日。 + PreviousTwoTradeDate string `json:"previous_two_trade_date"` // 上上个交易日。 + Summary LimitOpenCompareSummary `json:"summary"` // 汇总统计。 + Groups []LimitOpenCompareGroup `json:"groups"` // 固定分组结果。 } +// LimitBreakNextPerformanceResponse 是炸板股次日表现接口响应。 type LimitBreakNextPerformanceResponse struct { - TradeStartDate string `json:"trade_start_date"` - TradeEndDate string `json:"trade_end_date"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Total int `json:"total"` - TotalPages int `json:"total_pages"` - Summary LimitBreakNextPerformanceSummary `json:"summary"` - List []LimitBreakNextPerformanceStock `json:"list"` + TradeStartDate string `json:"trade_start_date"` // 查询起始交易日。 + TradeEndDate string `json:"trade_end_date"` // 查询结束交易日。 + Page int `json:"page"` // 当前页码。 + PageSize int `json:"page_size"` // 每页数量。 + Total int `json:"total"` // 命中过滤条件的总记录数。 + TotalPages int `json:"total_pages"` // 总页数。 + Summary LimitBreakNextPerformanceSummary `json:"summary"` // 汇总统计。 + List []LimitBreakNextPerformanceStock `json:"list"` // 当前页记录。 } +// LimitOpenThresholdResponse 是开盘价阈值分组接口响应。 type LimitOpenThresholdResponse struct { - TradeDate string `json:"trade_date"` - PreviousTradeDate string `json:"previous_trade_date"` - Summary LimitOpenThresholdSummary `json:"summary"` - Groups []LimitOpenThresholdGroup `json:"groups"` + TradeDate string `json:"trade_date"` // 查询交易日。 + PreviousTradeDate string `json:"previous_trade_date"` // 上一个交易日。 + Summary LimitOpenThresholdSummary `json:"summary"` // 汇总统计。 + Groups []LimitOpenThresholdGroup `json:"groups"` // 固定分组结果。 } +// LimitBoardGroup 是涨停概览按连板数聚合后的分组。 type LimitBoardGroup struct { - BoardCount int `json:"board_count"` - Label string `json:"label"` - Count int `json:"count"` - Stocks []LimitBoardStock `json:"stocks"` + BoardCount int `json:"board_count"` // 连板数。 + Label string `json:"label"` // 连板展示标签。 + Count int `json:"count"` // 分组股票数量。 + Stocks []LimitBoardStock `json:"stocks"` // 分组股票列表。 } +// LimitBoardSummary 汇总涨停概览的核心统计。 type LimitBoardSummary struct { - MaxBoardCount int `json:"max_board_count"` - LimitCount int `json:"limit_count"` - GroupCount int `json:"group_count"` + MaxBoardCount int `json:"max_board_count"` // 当日最高连板数。 + LimitCount int `json:"limit_count"` // 当日涨停股票数量。 + GroupCount int `json:"group_count"` // 连板分组数量。 } +// LimitBoardDailyMax 表示某个交易日的最高连板数。 type LimitBoardDailyMax struct { - TradeDate string `json:"trade_date"` - MaxBoardCount int `json:"max_board_count"` + TradeDate string `json:"trade_date"` // 交易日。 + MaxBoardCount int `json:"max_board_count"` // 当日最高连板数。 } +// LimitBoardResponse 是涨停概览接口响应。 type LimitBoardResponse struct { - TradeDate string `json:"trade_date"` - Summary LimitBoardSummary `json:"summary"` - RecentMaxBoardStats []LimitBoardDailyMax `json:"recent_max_board_stats"` - Groups []LimitBoardGroup `json:"groups"` + TradeDate string `json:"trade_date"` // 查询交易日。 + Summary LimitBoardSummary `json:"summary"` // 当日概览统计。 + RecentMaxBoardStats []LimitBoardDailyMax `json:"recent_max_board_stats"` // 最近窗口内每日最高连板数。 + Groups []LimitBoardGroup `json:"groups"` // 按连板数倒序排列的股票分组。 } diff --git a/internal/model/stock.go b/internal/model/stock.go index 309ff89..e3b5e33 100644 --- a/internal/model/stock.go +++ b/internal/model/stock.go @@ -2,30 +2,30 @@ package model import "time" -// StockDaily 对应数据库中的 stock_daily 表 +// StockDaily 对应数据库中的 stock_daily 表,保存单只股票每日行情。 type StockDaily struct { - TsCode string `gorm:"column:ts_code" json:"ts_code"` - TradeDate time.Time `gorm:"column:trade_date" json:"trade_date"` - Open float64 `gorm:"column:open" json:"open"` - High float64 `gorm:"column:high" json:"high"` - Low float64 `gorm:"column:low" json:"low"` - Close float64 `gorm:"column:close" json:"close"` - PctChg float64 `gorm:"column:pct_chg" json:"pct_chg"` - Vol float64 `gorm:"column:vol" json:"vol"` + TsCode string `gorm:"column:ts_code" json:"ts_code"` // 股票代码,通常为 TuShare ts_code。 + TradeDate time.Time `gorm:"column:trade_date" json:"trade_date"` // 交易日期。 + Open float64 `gorm:"column:open" json:"open"` // 开盘价。 + High float64 `gorm:"column:high" json:"high"` // 最高价。 + Low float64 `gorm:"column:low" json:"low"` // 最低价。 + Close float64 `gorm:"column:close" json:"close"` // 收盘价。 + PctChg float64 `gorm:"column:pct_chg" json:"pct_chg"` // 当日涨跌幅。 + Vol float64 `gorm:"column:vol" json:"vol"` // 成交量。 } -// AnalyzeRequest 用于接收前端 POST 请求的参数 +// AnalyzeRequest 用于接收前端 POST 请求的分析区间参数。 type AnalyzeRequest struct { - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` + StartDate string `json:"start_date"` // 起始交易日。 + EndDate string `json:"end_date"` // 结束交易日。 } -// AnalysisResponse 用于向前端返回分析结果 +// AnalysisResponse 用于向前端返回涨停后走势分析结果。 type AnalysisResponse struct { - Code string `json:"code"` - Name string `json:"name"` - DayOpen float64 `json:"day_open"` - DayHigh float64 `json:"day_high"` + Code string `json:"code"` // 股票代码。 + Name string `json:"name"` // 股票简称。 + DayOpen float64 `json:"day_open"` // 涨停日开盘价。 + DayHigh float64 `json:"day_high"` // 涨停日最高价。 MaxHigh float64 `json:"max_high"` // 逻辑最高点(用于计算降幅) MaxDate string `json:"max_date"` // 逻辑最高点日期 OverallMaxHigh float64 `json:"overall_max_high"` // 区间绝对最高点 diff --git a/internal/router/router.go b/internal/router/router.go index 543d1d5..f2ecc5e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -5,10 +5,13 @@ import ( "level_go/internal/api" ) +// NewRouter 创建并配置 API 路由。 +// +// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。 func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine { r := gin.Default() - // 跨域中间件 (根据需要开启) + // 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。 r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") diff --git a/internal/service/common_service.go b/internal/service/common_service.go index e07ec48..880a2ec 100644 --- a/internal/service/common_service.go +++ b/internal/service/common_service.go @@ -5,10 +5,12 @@ import ( "level_go/internal/model" ) +// CommonService 提供通用基础数据查询能力。 type CommonService struct { - DB *gorm.DB + DB *gorm.DB // 数据库连接。 } +// GetTradeCalendar 查询上交所全部开市日期,并返回最新开市日期。 func (s *CommonService) GetTradeCalendar() (model.CalendarResponse, error) { var results []struct { DateStr string `gorm:"column:ds"` @@ -24,6 +26,7 @@ func (s *CommonService) GetTradeCalendar() (model.CalendarResponse, error) { return resp, err } + // 按日期升序扫描,因此循环中最后一次赋值即为最大开市日期。 for _, r := range results { resp.OpenDates = append(resp.OpenDates, r.DateStr) resp.MaxDate = r.DateStr diff --git a/internal/service/limit_board_break_next_performance_service.go b/internal/service/limit_board_break_next_performance_service.go index dd4b71c..d803f07 100644 --- a/internal/service/limit_board_break_next_performance_service.go +++ b/internal/service/limit_board_break_next_performance_service.go @@ -7,6 +7,7 @@ import ( "level_go/internal/model" ) +// limitBreakNextPerformanceRow 是炸板股次日表现查询的数据库扫描结构。 type limitBreakNextPerformanceRow struct { TradeDate string `gorm:"column:trade_date"` PreviousTradeDate string `gorm:"column:previous_trade_date"` @@ -31,6 +32,9 @@ type limitBreakNextPerformanceRow struct { NextClose *float64 `gorm:"column:next_close"` } +// GetLimitBreakNextPerformance 返回炸板股次日表现的分页响应。 +// +// 该方法负责分页和汇总统计,实际查询与单股指标计算由 ListLimitBreakNextPerformanceStocks 完成。 func (s *LimitBoardService) GetLimitBreakNextPerformance(req model.LimitBreakNextPerformanceRequest) (model.LimitBreakNextPerformanceResponse, error) { resp := model.LimitBreakNextPerformanceResponse{ TradeStartDate: req.TradeStartDate, @@ -67,6 +71,9 @@ func (s *LimitBoardService) GetLimitBreakNextPerformance(req model.LimitBreakNex return resp, nil } +// ListLimitBreakNextPerformanceStocks 查询并计算全部命中的炸板股次日表现。 +// +// 返回值中的 int 表示拥有任意 T+1 行情字段的记录数量。 func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitBreakNextPerformanceRequest) ([]model.LimitBreakNextPerformanceStock, int, error) { rows, err := s.loadLimitBreakNextPerformanceRows(req.TradeStartDate, req.TradeEndDate) if err != nil { @@ -77,6 +84,7 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB nextTradeDataCount := 0 for _, row := range rows { item := buildLimitBreakNextPerformanceStock(row) + // 可选布尔过滤在指标计算后执行,便于复用同一批基础查询结果。 if req.TOpenGainLt101 != nil && item.TOpenGainLt101 != *req.TOpenGainLt101 { continue } @@ -91,6 +99,9 @@ func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitB return items, nextTradeDataCount, nil } +// loadLimitBreakNextPerformanceRows 查询“首板后次日炸板”的候选股票和 T+1 行情。 +// +// 口径:前一交易日涨停、前两交易日未涨停、当前交易日 final_status = 2。 func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, error) { query := ` WITH open_calendar AS ( @@ -160,6 +171,7 @@ ORDER BY trade_date ASC, ts_code ASC` return rows, err } +// buildLimitBreakNextPerformanceStock 根据数据库行计算前端需要的派生指标。 func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model.LimitBreakNextPerformanceStock { item := model.LimitBreakNextPerformanceStock{ TradeDate: row.TradeDate, @@ -183,17 +195,20 @@ func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model } item.BreakTimeBefore1100 = isLimitBreakTimeBeforeEleven(item.FirstTime) if row.TPreClose != nil && *row.TPreClose > 0 { + // T 日开盘涨幅以 T 日前收为基准,低于 101% 用于筛选低开/小幅高开炸板。 item.TOpenPctChg = pctChangePtr(row.TOpen, *row.TPreClose) if row.TOpen != nil { item.TOpenGainLt101 = *row.TOpen < *row.TPreClose*1.01 } } if row.TClose != nil && *row.TClose > 0 { + // T+1 的开高低收收益均以 T 日收盘价作为基准。 item.NextOpenPctChg = pctChangePtr(row.NextOpen, *row.TClose) item.NextHighProfitPct = pctChangePtr(row.NextHigh, *row.TClose) item.NextLowLossPct = pctChangePtr(row.NextLow, *row.TClose) item.NextClosePctChg = pctChangePtr(row.NextClose, *row.TClose) if row.NextHigh != nil && row.THigh != nil { + // 是否盘中可盈利按 T+1 最高价能否覆盖 T 日最高买入成本判断。 nextIntradayProfitable := *row.NextHigh >= *row.THigh item.NextIntradayProfitable = &nextIntradayProfitable } @@ -201,6 +216,7 @@ func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model return item } +// pctChangePtr 计算相对基准价的百分比变化,输入缺失时返回 nil。 func pctChangePtr(value *float64, base float64) *float64 { if value == nil || base == 0 { return nil @@ -209,10 +225,12 @@ func pctChangePtr(value *float64, base float64) *float64 { return &result } +// roundLimitBreakPct 保留两位小数。 func roundLimitBreakPct(value float64) float64 { return math.Round(value*100) / 100 } +// formatLimitTime 将 TuShare 紧凑时间格式转换为 HH:MM:SS。 func formatLimitTime(value string) string { timeText := strings.TrimSpace(value) if timeText == "" || strings.EqualFold(timeText, "none") || strings.EqualFold(timeText, "null") { @@ -233,6 +251,9 @@ func formatLimitTime(value string) string { return timeText[0:2] + ":" + timeText[2:4] + ":" + timeText[4:6] } +// isLimitBreakTimeBeforeEleven 判断炸板时间是否早于 11:00。 +// +// 空时间在当前业务中视为满足条件,保持历史接口行为。 func isLimitBreakTimeBeforeEleven(limitTime string) bool { if strings.TrimSpace(limitTime) == "" { return true diff --git a/internal/service/limit_board_break_next_performance_service_test.go b/internal/service/limit_board_break_next_performance_service_test.go index 75ed7ed..fc7df0a 100644 --- a/internal/service/limit_board_break_next_performance_service_test.go +++ b/internal/service/limit_board_break_next_performance_service_test.go @@ -2,6 +2,9 @@ package service import "testing" +// TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh 验证 T+1 盘中可盈利口径。 +// +// 业务要求用 T+1 最高价覆盖 T 日最高价来判断是否可解套,而不是只和 T 日收盘价比较。 func TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh(t *testing.T) { tClose := 9.50 tHigh := 10.00 @@ -52,10 +55,12 @@ func TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh(t *testi } } +// float64Ptr 是测试用指针辅助函数。 func float64Ptr(value float64) *float64 { return &value } +// boolPtr 是测试用指针辅助函数。 func boolPtr(value bool) *bool { return &value } diff --git a/internal/service/limit_board_open_compare_service.go b/internal/service/limit_board_open_compare_service.go index b7a6698..3ad9458 100644 --- a/internal/service/limit_board_open_compare_service.go +++ b/internal/service/limit_board_open_compare_service.go @@ -9,6 +9,9 @@ import ( "level_go/internal/model" ) +// GetLimitOpenCompareGroups 返回昨日涨停且今日继续涨停股票的开盘对比分组。 +// +// 重点识别两类股票:今日开盘低于前收的股票,以及首板进二板且 11 点前首次涨停的股票。 func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.LimitOpenCompareResponse, error) { resp := model.LimitOpenCompareResponse{TradeDate: tradeDate} @@ -43,12 +46,14 @@ func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.L firstToSecondBoardCount := 0 firstSecondBeforeElevenCount := 0 for _, stock := range stocks { + // previousTwoTradeDate 未涨停、previousTradeDate 涨停、tradeDate 涨停时,认为是首板进二板。 if stock.IsFirstToSecondBoard { stock.HighlightLabel = "首板进二板" firstToSecondBoardCount++ } isOpenLower := previousTradeDate != "" && stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.PreviousClose if isOpenLower && stock.IsFirstToSecondBoard && isBeforeEleven(stock.AkFirstLimitTime) { + // 首板进二板且 11 点前封板的低开股,前端会用更强提示标签展示。 stock.IsFirstSecondBeforeEleven = true stock.HighlightLabel = "首二一一" firstSecondBeforeElevenCount++ @@ -72,6 +77,9 @@ func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.L return resp, nil } +// isBeforeEleven 判断涨停时间是否早于 11:00。 +// +// 兼容 HH:MM:SS、930、0930、93000、093000 等时间格式。 func isBeforeEleven(limitTime *string) bool { if limitTime == nil || *limitTime == "" { return false @@ -119,6 +127,7 @@ func isBeforeEleven(limitTime *string) bool { return hour < 11 } +// loadLimitOpenCompareStocks 查询昨日涨停且今日继续涨停的股票,并带出今日开盘和昨日收盘。 func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate string) ([]model.LimitOpenCompareStock, error) { var stocks []model.LimitOpenCompareStock query := `SELECT @@ -171,6 +180,7 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD return stocks, err } +// buildLimitOpenCompareGroups 构造“开盘低于前收”和“其他涨停股”两个固定分组。 func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenCompareStock) []model.LimitOpenCompareGroup { if openLowerStocks == nil { openLowerStocks = []model.LimitOpenCompareStock{} @@ -198,6 +208,7 @@ func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenC } } +// sortLimitOpenCompareStocks 按涨跌幅降序、股票代码升序稳定展示分组内股票。 func sortLimitOpenCompareStocks(stocks []model.LimitOpenCompareStock) { sort.Slice(stocks, func(i, j int) bool { if stocks[i].PctChg == stocks[j].PctChg { diff --git a/internal/service/limit_board_open_threshold_service.go b/internal/service/limit_board_open_threshold_service.go index dfc455b..2bbe44e 100644 --- a/internal/service/limit_board_open_threshold_service.go +++ b/internal/service/limit_board_open_threshold_service.go @@ -6,8 +6,10 @@ import ( "level_go/internal/model" ) +// GetLimitOpenThresholdGroups 返回昨日涨停股今日开盘是否低于前收 101% 的分组。 func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model.LimitOpenThresholdResponse, error) { resp := model.LimitOpenThresholdResponse{TradeDate: tradeDate} + // thresholdRatio 是开盘价阈值,1.01 表示前收的 101%。 const thresholdRatio = 1.01 tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate) @@ -34,6 +36,7 @@ func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model withinThresholdStocks := make([]model.LimitOpenThresholdStock, 0) atOrAboveThresholdStocks := make([]model.LimitOpenThresholdStock, 0) for _, stock := range stocks { + // 前收价格有效时才计算阈值价格和开盘/前收比值,避免除零和误导性数据。 if stock.HasPreviousClose && stock.PreviousClose > 0 { stock.OpenThresholdPrice = stock.PreviousClose * thresholdRatio } @@ -59,6 +62,7 @@ func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model return resp, nil } +// loadLimitOpenThresholdStocks 查询昨日涨停且今日继续涨停的股票,并补齐今日开盘和前收。 func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTradeDate string) ([]model.LimitOpenThresholdStock, error) { var stocks []model.LimitOpenThresholdStock query := `SELECT @@ -105,6 +109,7 @@ func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTrad return stocks, err } +// buildLimitOpenThresholdGroups 构造低于阈值和达到/超过阈值两个固定分组。 func buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks []model.LimitOpenThresholdStock) []model.LimitOpenThresholdGroup { if withinThresholdStocks == nil { withinThresholdStocks = []model.LimitOpenThresholdStock{} @@ -132,6 +137,7 @@ func buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStoc } } +// sortLimitOpenThresholdStocks 按涨跌幅降序、股票代码升序稳定展示分组内股票。 func sortLimitOpenThresholdStocks(stocks []model.LimitOpenThresholdStock) { sort.Slice(stocks, func(i, j int) bool { if stocks[i].PctChg == stocks[j].PctChg { diff --git a/internal/service/limit_board_overview_service.go b/internal/service/limit_board_overview_service.go index 22fa905..abd481e 100644 --- a/internal/service/limit_board_overview_service.go +++ b/internal/service/limit_board_overview_service.go @@ -6,11 +6,15 @@ import ( "level_go/internal/model" ) +// limitBoardHistoryRow 是连板计算所需的最小历史涨停记录。 type limitBoardHistoryRow struct { - TsCode string `gorm:"column:ts_code"` - TradeDate string `gorm:"column:trade_date"` + TsCode string `gorm:"column:ts_code"` // 股票代码。 + TradeDate string `gorm:"column:trade_date"` // 涨停交易日。 } +// GetLimitBoardOverview 返回指定交易日的涨停连板概览。 +// +// 结果包括最近 30 个交易日最高连板数、当日涨停股按连板数分组后的列表和汇总统计。 func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) { resp := model.LimitBoardResponse{TradeDate: tradeDate} @@ -51,6 +55,7 @@ func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.Limit return resp, nil } +// buildRecentMaxBoardStats 计算最近最多 30 个交易日的每日最高连板数。 func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) { startIndex := 0 if dateIndex >= 29 { @@ -83,6 +88,7 @@ func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIn maxBoardByDate[tradeDate] = 0 } + // 对每只股票在窗口内逐日回看连续涨停天数,并更新当天最高连板数。 for _, limitDates := range dateByCode { for idx, tradeDate := range windowDates { if _, ok := limitDates[tradeDate]; !ok { @@ -114,6 +120,7 @@ func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIn return stats, nil } +// loadCurrentLimitStocks 查询指定交易日所有最终状态为涨停的股票。 func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) { var stocks []model.LimitBoardStock query := `SELECT @@ -147,6 +154,7 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li return stocks, err } +// loadLimitHistory 查询指定股票集合在 tradeDate 之前及当天的涨停历史。 func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) { codes := make([]string, 0, len(stocks)) for _, stock := range stocks { @@ -172,6 +180,7 @@ func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tra return historyMap, nil } +// buildLimitBoardGroups 计算每只股票的连板数,并按连板数构造分组映射。 func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, dateIndex int, historyMap map[string]map[string]struct{}) (map[int][]model.LimitBoardStock, int) { groupMap := make(map[int][]model.LimitBoardStock) maxBoardCount := 0 @@ -191,6 +200,7 @@ func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, return groupMap, maxBoardCount } +// calcBoardCount 从目标交易日向前回溯,直到遇到非涨停日为止,得到连续涨停天数。 func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int { boardCount := 0 for i := dateIndex; i >= 0; i-- { @@ -202,6 +212,7 @@ func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]st return boardCount } +// sortLimitBoardGroups 将连板分组按连板数倒序排列,组内按涨跌幅降序排列。 func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup { boardCounts := make([]int, 0, len(groupMap)) for boardCount := range groupMap { diff --git a/internal/service/limit_board_service.go b/internal/service/limit_board_service.go index 0aac900..016e0d0 100644 --- a/internal/service/limit_board_service.go +++ b/internal/service/limit_board_service.go @@ -6,12 +6,15 @@ import ( "gorm.io/gorm" ) +// LimitBoardService 提供涨停板概览、分组、收益统计等业务能力。 type LimitBoardService struct { - DB *gorm.DB + DB *gorm.DB // 数据库连接。 } +// stockLimitCombinedTable 是涨停合并数据表名,集中定义便于多个查询复用。 const stockLimitCombinedTable = "stock_limit_combined_data" +// boardLabel 把连续涨停天数转换为中文连板标签。 func boardLabel(boardCount int) string { switch boardCount { case 1: @@ -37,6 +40,9 @@ func boardLabel(boardCount int) string { } } +// loadTradeCalendar 查询不晚于 tradeDate 的上交所开市日,并返回 tradeDate 在切片中的索引。 +// +// dateIndex 为 -1 表示传入日期不是当前交易日历中的开市日。 func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) { var tradeDates []string if err := s.DB.Table("trade_calendar"). diff --git a/internal/service/stock_service.go b/internal/service/stock_service.go index 6bd93ae..5d1d69e 100644 --- a/internal/service/stock_service.go +++ b/internal/service/stock_service.go @@ -8,10 +8,12 @@ import ( "level_go/internal/model" ) +// StockService 提供股票行情查询和区间表现分析能力。 type StockService struct { - DB *gorm.DB + DB *gorm.DB // 数据库连接。 } +// GetHistoryKline 查询指定股票自固定起点以来的日 K 线。 func (s *StockService) GetHistoryKline(code string) ([]model.StockDaily, error) { var dailies []model.StockDaily startDate := "2020-01-02" @@ -24,10 +26,14 @@ func (s *StockService) GetHistoryKline(code string) ([]model.StockDaily, error) return dailies, err } +// roundTo2 把浮点数四舍五入到两位小数。 func roundTo2(val float64) float64 { return math.Round(val*100) / 100 } +// AnalyzePerformance 分析某个涨停日之后到结束日期之间的下跌和反弹表现。 +// +// startDate 既是涨停筛选日期,也是行情区间起点;endDate 是行情区间终点。 func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.AnalysisResponse, error) { var results []model.AnalysisResponse var limits []struct { @@ -37,7 +43,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An High float64 } - // 查询涨停股 + // 查询 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 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` @@ -75,7 +81,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An continue } - // A. 计算区间绝对最高价 + // A. 计算区间绝对最高价,用于前端展示真实区间高点。 overallMax := dailies[0].High overallMaxDate := dailies[0].TradeDate.Format("2006-01-02") for _, d := range dailies { @@ -85,11 +91,11 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An } } - // B. 逻辑计算 + // B. 逻辑计算以涨停日最高价为分界,区分“未跌破涨停高点”和“跌破后反弹”两类形态。 limitHigh := stock.High postDailies := dailies[1:] - // 找出涨停后的全局最低 + // 找出涨停后的全局最低,用来判断是否跌破涨停日最高价。 minLowAfterLimit := postDailies[0].Low for _, d := range postDailies { if d.Low < minLowAfterLimit { @@ -102,7 +108,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An var logicMaxDate string if minLowAfterLimit >= limitHigh { - // 形态 1:强势。计算涨幅 + // 形态 1:强势,后续最低价仍高于涨停日最高价,因此只统计向上空间。 currMax := limitHigh maxIdx := -1 for i, d := range postDailies { @@ -120,7 +126,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An } dropPct = nil } else { - // 形态 2:破位。计算降幅和反弹 + // 形态 2:破位,先找到破位最低点,再计算从此前最高点到最低点的降幅。 absMinLow := postDailies[0].Low minIdx := 0 for i, d := range postDailies { @@ -129,7 +135,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An minIdx = i } } - // 找到涨停到该最低点之间的最高 + // 找到涨停日到最低点之间的最高价,作为本次回撤的逻辑高点。 rangeDailies := dailies[:minIdx+2] rangeMax := dailies[0].High maxD := startDate @@ -150,6 +156,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An reboundPct = &rVal } + // 收盘价序列给前端画趋势图使用。 var closePrices []float64 for _, d := range dailies { closePrices = append(closePrices, d.Close) @@ -171,6 +178,7 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An }) } + // 优先展示跌幅更大的破位股票;强势股票由于 DropPct 为 nil 排在后面。 sort.Slice(results, func(i, j int) bool { if results[i].DropPct == nil && results[j].DropPct != nil { return false diff --git a/pkg/config/config.go b/pkg/config/config.go index 116af2b..56e108f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,26 +2,37 @@ package config import ( "fmt" + "github.com/spf13/viper" ) +// Config 描述 config.yaml 中使用到的应用配置。 +// +// Database 保存 MySQL 连接参数,Server 保存 HTTP 服务参数。结构体字段名需要与配置文件 +// 中的层级保持一致,Viper 会在 InitConfig 中把 YAML 内容反序列化到该结构体。 type Config struct { + // Database 是 MySQL 连接配置。 Database struct { - User string - Password string - Host string - Port int - Dbname string - Charset string - Collation string + User string // 数据库用户名。 + Password string // 数据库密码。 + Host string // 数据库主机地址。 + Port int // 数据库端口。 + Dbname string // 数据库名称。 + Charset string // 连接字符集,例如 utf8mb4。 + Collation string // 排序规则;为空时 GetDSN 会使用 utf8mb4_unicode_ci。 } + // Server 是 HTTP 服务配置。 Server struct { - Port int + Port int // 服务监听端口。 } } +// AppConfig 保存进程级全局配置。调用 InitConfig 后才可安全读取。 var AppConfig *Config +// InitConfig 从当前目录或 ./config 目录读取 config.yaml,并解析到 AppConfig。 +// +// 这里使用 panic 暴露配置错误,因为配置缺失或格式错误会导致服务无法正确启动。 func InitConfig() { viper.SetConfigName("config") viper.SetConfigType("yaml") @@ -37,7 +48,7 @@ func InitConfig() { } } -// GetDSN 生成 MySQL 连接字符串 +// GetDSN 根据 AppConfig 生成 GORM/MySQL 使用的 DSN 连接字符串。 func GetDSN() string { db := AppConfig.Database collation := db.Collation @@ -48,7 +59,7 @@ func GetDSN() string { db.User, db.Password, db.Host, db.Port, db.Dbname, db.Charset, collation) } -// GetServerPort 获取服务器端口字符串 +// GetServerPort 返回 Gin Run 所需的端口格式,例如 ":8080"。 func GetServerPort() string { return fmt.Sprintf(":%d", AppConfig.Server.Port) }