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