新增接口及接口文档
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"level_go/internal/model"
|
||||
"level_go/internal/service"
|
||||
)
|
||||
|
||||
type LimitBoardHandler struct {
|
||||
Service *service.LimitBoardService
|
||||
}
|
||||
|
||||
func NewLimitBoardHandler(s *service.LimitBoardService) *LimitBoardHandler {
|
||||
return &LimitBoardHandler{Service: s}
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) GetOverview(c *gin.Context) {
|
||||
var req model.LimitBoardRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "trade_date 参数不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.Service.GetLimitBoardOverview(req.TradeDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
type LimitBoardRequest struct {
|
||||
TradeDate string `form:"trade_date" json:"trade_date"`
|
||||
}
|
||||
|
||||
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"`
|
||||
BoardCount int `json:"board_count"`
|
||||
BoardLabel string `json:"board_label"`
|
||||
DisplayStatus string `json:"display_status"`
|
||||
}
|
||||
|
||||
type LimitBoardGroup struct {
|
||||
BoardCount int `json:"board_count"`
|
||||
Label string `json:"label"`
|
||||
Count int `json:"count"`
|
||||
Stocks []LimitBoardStock `json:"stocks"`
|
||||
}
|
||||
|
||||
type LimitBoardSummary struct {
|
||||
MaxBoardCount int `json:"max_board_count"`
|
||||
LimitCount int `json:"limit_count"`
|
||||
GroupCount int `json:"group_count"`
|
||||
}
|
||||
|
||||
type LimitBoardDailyMax struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
MaxBoardCount int `json:"max_board_count"`
|
||||
}
|
||||
|
||||
type LimitBoardResponse struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
Summary LimitBoardSummary `json:"summary"`
|
||||
RecentMaxBoardStats []LimitBoardDailyMax `json:"recent_max_board_stats"`
|
||||
Groups []LimitBoardGroup `json:"groups"`
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"level_go/internal/api"
|
||||
)
|
||||
|
||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.Engine {
|
||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 跨域中间件 (根据需要开启)
|
||||
@@ -25,6 +25,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.En
|
||||
v1.GET("/common/trade-calendar", commonApi.GetTradeCalendar)
|
||||
v1.POST("/stock/analyze", stockApi.Analyze)
|
||||
v1.GET("/stock/kline/:code", stockApi.GetKlineData)
|
||||
v1.GET("/limit-board/overview", limitBoardApi.GetOverview)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"level_go/internal/model"
|
||||
)
|
||||
|
||||
type LimitBoardService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type limitBoardHistoryRow struct {
|
||||
TsCode string `gorm:"column:ts_code"`
|
||||
TradeDate string `gorm:"column:trade_date"`
|
||||
}
|
||||
|
||||
func boardLabel(boardCount int) string {
|
||||
switch boardCount {
|
||||
case 1:
|
||||
return "首板"
|
||||
case 2:
|
||||
return "二板"
|
||||
case 3:
|
||||
return "三板"
|
||||
case 4:
|
||||
return "四板"
|
||||
case 5:
|
||||
return "五板"
|
||||
case 6:
|
||||
return "六板"
|
||||
case 7:
|
||||
return "七板"
|
||||
case 8:
|
||||
return "八板"
|
||||
case 9:
|
||||
return "九板"
|
||||
default:
|
||||
return fmt.Sprintf("%d板", boardCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) {
|
||||
resp := model.LimitBoardResponse{TradeDate: tradeDate}
|
||||
|
||||
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if dateIndex == -1 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.RecentMaxBoardStats, err = s.buildRecentMaxBoardStats(tradeDates, dateIndex)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
limitStocks, err := s.loadCurrentLimitStocks(tradeDate)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if len(limitStocks) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
historyMap, err := s.loadLimitHistory(limitStocks, tradeDate)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
groupMap, maxBoardCount := buildLimitBoardGroups(limitStocks, tradeDates, dateIndex, historyMap)
|
||||
resp.Groups = sortLimitBoardGroups(groupMap)
|
||||
resp.Summary = model.LimitBoardSummary{
|
||||
MaxBoardCount: maxBoardCount,
|
||||
LimitCount: len(limitStocks),
|
||||
GroupCount: len(resp.Groups),
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) {
|
||||
startIndex := 0
|
||||
if dateIndex >= 29 {
|
||||
startIndex = dateIndex - 29
|
||||
}
|
||||
windowDates := tradeDates[startIndex : dateIndex+1]
|
||||
if len(windowDates) == 0 {
|
||||
return []model.LimitBoardDailyMax{}, nil
|
||||
}
|
||||
|
||||
var history []limitBoardHistoryRow
|
||||
if err := s.DB.Table("stock_limit_data").
|
||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("trade_date BETWEEN ? AND ? AND status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
|
||||
Order("ts_code ASC, trade_date ASC").
|
||||
Scan(&history).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dateByCode := make(map[string]map[string]struct{})
|
||||
for _, item := range history {
|
||||
if _, ok := dateByCode[item.TsCode]; !ok {
|
||||
dateByCode[item.TsCode] = make(map[string]struct{})
|
||||
}
|
||||
dateByCode[item.TsCode][item.TradeDate] = struct{}{}
|
||||
}
|
||||
|
||||
maxBoardByDate := make(map[string]int, len(windowDates))
|
||||
for _, tradeDate := range windowDates {
|
||||
maxBoardByDate[tradeDate] = 0
|
||||
}
|
||||
|
||||
for _, limitDates := range dateByCode {
|
||||
for idx, tradeDate := range windowDates {
|
||||
if _, ok := limitDates[tradeDate]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
boardCount := 0
|
||||
for j := idx; j >= 0; j-- {
|
||||
if _, ok := limitDates[windowDates[j]]; !ok {
|
||||
break
|
||||
}
|
||||
boardCount++
|
||||
}
|
||||
|
||||
if boardCount > maxBoardByDate[tradeDate] {
|
||||
maxBoardByDate[tradeDate] = boardCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats := make([]model.LimitBoardDailyMax, 0, len(windowDates))
|
||||
for _, tradeDate := range windowDates {
|
||||
stats = append(stats, model.LimitBoardDailyMax{
|
||||
TradeDate: tradeDate,
|
||||
MaxBoardCount: maxBoardByDate[tradeDate],
|
||||
})
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) {
|
||||
var tradeDates []string
|
||||
if err := s.DB.Table("trade_calendar").
|
||||
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("exchange = ? AND is_open = ? AND cal_date <= ?", "SSE", 1, tradeDate).
|
||||
Order("cal_date ASC").
|
||||
Scan(&tradeDates).Error; err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
dateIndex := -1
|
||||
for i, d := range tradeDates {
|
||||
if d == tradeDate {
|
||||
dateIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return tradeDates, dateIndex, nil
|
||||
}
|
||||
|
||||
func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) {
|
||||
var stocks []model.LimitBoardStock
|
||||
query := `SELECT
|
||||
l.symbol,
|
||||
l.ts_code,
|
||||
l.name,
|
||||
COALESCE(l.market, '') AS market,
|
||||
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
||||
COALESCE(d.close, l.close, 0) AS close,
|
||||
COALESCE(l.limit_price, 0) AS limit_price,
|
||||
COALESCE(d.pct_chg, 0) AS pct_chg
|
||||
FROM stock_limit_data l
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
WHERE l.trade_date = ? AND l.status = 1
|
||||
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
||||
|
||||
err := s.DB.Raw(query, tradeDate).Scan(&stocks).Error
|
||||
return stocks, err
|
||||
}
|
||||
|
||||
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 {
|
||||
codes = append(codes, stock.TsCode)
|
||||
}
|
||||
|
||||
var history []limitBoardHistoryRow
|
||||
if err := s.DB.Table("stock_limit_data").
|
||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("ts_code IN ? AND trade_date <= ? AND status = ?", codes, tradeDate, 1).
|
||||
Scan(&history).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
historyMap := make(map[string]map[string]struct{}, len(codes))
|
||||
for _, item := range history {
|
||||
if _, ok := historyMap[item.TsCode]; !ok {
|
||||
historyMap[item.TsCode] = make(map[string]struct{})
|
||||
}
|
||||
historyMap[item.TsCode][item.TradeDate] = struct{}{}
|
||||
}
|
||||
|
||||
return historyMap, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
for i := range stocks {
|
||||
boardCount := calcBoardCount(tradeDates, dateIndex, historyMap[stocks[i].TsCode])
|
||||
stocks[i].BoardCount = boardCount
|
||||
stocks[i].BoardLabel = boardLabel(boardCount)
|
||||
stocks[i].DisplayStatus = boardLabel(boardCount)
|
||||
|
||||
if boardCount > maxBoardCount {
|
||||
maxBoardCount = boardCount
|
||||
}
|
||||
groupMap[boardCount] = append(groupMap[boardCount], stocks[i])
|
||||
}
|
||||
|
||||
return groupMap, maxBoardCount
|
||||
}
|
||||
|
||||
func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int {
|
||||
boardCount := 0
|
||||
for i := dateIndex; i >= 0; i-- {
|
||||
if _, ok := limitDates[tradeDates[i]]; !ok {
|
||||
break
|
||||
}
|
||||
boardCount++
|
||||
}
|
||||
return boardCount
|
||||
}
|
||||
|
||||
func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup {
|
||||
boardCounts := make([]int, 0, len(groupMap))
|
||||
for boardCount := range groupMap {
|
||||
boardCounts = append(boardCounts, boardCount)
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(boardCounts)))
|
||||
|
||||
groups := make([]model.LimitBoardGroup, 0, len(boardCounts))
|
||||
for _, boardCount := range boardCounts {
|
||||
stocks := groupMap[boardCount]
|
||||
sort.Slice(stocks, func(i, j int) bool {
|
||||
if stocks[i].PctChg == stocks[j].PctChg {
|
||||
return stocks[i].TsCode < stocks[j].TsCode
|
||||
}
|
||||
return stocks[i].PctChg > stocks[j].PctChg
|
||||
})
|
||||
|
||||
groups = append(groups, model.LimitBoardGroup{
|
||||
BoardCount: boardCount,
|
||||
Label: boardLabel(boardCount),
|
||||
Count: len(stocks),
|
||||
Stocks: stocks,
|
||||
})
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
Reference in New Issue
Block a user