Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af318b2604 | |||
| 7a7e28684c |
+17
-48
@@ -1,68 +1,37 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"level_go/internal/api"
|
||||||
|
"level_go/internal/router"
|
||||||
"level_go/internal/service"
|
"level_go/internal/service"
|
||||||
"level_go/pkg/config"
|
"level_go/pkg/config" // 引入配置包
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 1. 加载配置
|
// 1. 初始化配置
|
||||||
config.InitConfig()
|
config.InitConfig()
|
||||||
dbConf := config.AppConfig.Database
|
|
||||||
|
|
||||||
// 2. 连接数据库
|
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
|
||||||
dbConf.User, dbConf.Password, dbConf.Host, dbConf.Port, dbConf.Dbname, dbConf.Charset)
|
|
||||||
|
|
||||||
|
// 2. 使用配置中的 DSN 连接数据库
|
||||||
|
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 {
|
||||||
panic("数据库连接失败: " + err.Error())
|
log.Fatal("数据库连接失败:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 注入 Service
|
// 3. 依赖注入
|
||||||
stockSvc := &service.StockService{DB: db}
|
commonSrv := &service.CommonService{DB: db}
|
||||||
|
stockSrv := &service.StockService{DB: db}
|
||||||
|
|
||||||
// 4. 路由设置
|
commonHandler := &api.CommonHandler{Service: commonSrv}
|
||||||
r := gin.Default()
|
stockHandler := &api.StockHandler{Service: stockSrv}
|
||||||
|
|
||||||
// 跨域中间件
|
// 4. 启动路由
|
||||||
r.Use(func(c *gin.Context) {
|
r := router.NewRouter(commonHandler, stockHandler)
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
||||||
if c.Request.Method == "OPTIONS" {
|
|
||||||
c.AbortWithStatus(204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
api := r.Group("/api/v1")
|
port := config.GetServerPort()
|
||||||
{
|
log.Printf("服务启动于端口 %s", port)
|
||||||
api.GET("/stock/analyze", func(c *gin.Context) {
|
|
||||||
start := c.Query("start_date")
|
|
||||||
end := c.Query("end_date")
|
|
||||||
|
|
||||||
if start == "" || end == "" {
|
|
||||||
c.JSON(400, gin.H{"error": "参数缺失"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := stockSvc.AnalyzePerformance(start, end)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(200, data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 启动
|
|
||||||
port := fmt.Sprintf(":%d", config.AppConfig.Server.Port)
|
|
||||||
fmt.Printf("服务已启动,监听端口 %s\n", port)
|
|
||||||
r.Run(port)
|
r.Run(port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"level_go/internal/service"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommonHandler struct {
|
||||||
|
Service *service.CommonService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommonHandler) GetTradeCalendar(c *gin.Context) {
|
||||||
|
resp, err := h.Service.GetTradeCalendar()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"level_go/internal/model"
|
||||||
|
"level_go/internal/service"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StockHandler struct {
|
||||||
|
Service *service.StockService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStockHandler(s *service.StockService) *StockHandler {
|
||||||
|
return &StockHandler{Service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKlineData 获取指定股票从2024-09-24至今的K线
|
||||||
|
func (h *StockHandler) GetKlineData(c *gin.Context) {
|
||||||
|
code := c.Param("code")
|
||||||
|
if code == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "股票代码不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.Service.GetHistoryKline(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StockHandler) Analyze(c *gin.Context) {
|
||||||
|
var req model.AnalyzeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := h.Service.AnalyzePerformance(req.StartDate, req.EndDate)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, results)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type TradeCalendar struct {
|
||||||
|
CalDate time.Time `gorm:"column:cal_date;primaryKey"`
|
||||||
|
Exchange string `gorm:"column:exchange;primaryKey"`
|
||||||
|
IsOpen int `gorm:"column:is_open"`
|
||||||
|
PretradeDate time.Time `gorm:"column:pretrade_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TradeCalendar) TableName() string { return "trade_calendar" }
|
||||||
|
|
||||||
|
type CalendarResponse struct {
|
||||||
|
OpenDates []string `json:"open_dates"`
|
||||||
|
MaxDate string `json:"max_date"`
|
||||||
|
}
|
||||||
+26
-16
@@ -2,25 +2,35 @@ package model
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// StockDaily 对应数据库中的 stock_daily 表
|
||||||
type StockDaily struct {
|
type StockDaily struct {
|
||||||
TsCode string `gorm:"column:ts_code"`
|
TsCode string `gorm:"column:ts_code" json:"ts_code"`
|
||||||
TradeDate time.Time `gorm:"column:trade_date"`
|
TradeDate time.Time `gorm:"column:trade_date" json:"trade_date"`
|
||||||
Open float64 `gorm:"column:open"`
|
Open float64 `gorm:"column:open" json:"open"`
|
||||||
High float64 `gorm:"column:high"`
|
High float64 `gorm:"column:high" json:"high"`
|
||||||
Low float64 `gorm:"column:low"`
|
Low float64 `gorm:"column:low" json:"low"`
|
||||||
Close float64 `gorm:"column:close"`
|
Close float64 `gorm:"column:close" json:"close"`
|
||||||
|
PctChg float64 `gorm:"column:pct_chg" json:"pct_chg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (StockDaily) TableName() string { return "stock_daily" }
|
// AnalyzeRequest 用于接收前端 POST 请求的参数
|
||||||
|
type AnalyzeRequest struct {
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"` // 逻辑最高点日期
|
||||||
PostMinLow float64 `json:"post_min_low"`
|
OverallMaxHigh float64 `json:"overall_max_high"` // 区间绝对最高点
|
||||||
DropPct *float64 `json:"drop_pct"` // 改为指针以支持 null
|
OverallMaxDate string `json:"overall_max_date"` // 区间绝对最高点日期
|
||||||
ReboundPct *float64 `json:"rebound_pct"` // 改为指针以支持 null
|
PostMinLow float64 `json:"post_min_low"` // 涨停日后的最低点
|
||||||
|
DropPct *float64 `json:"drop_pct"` // 降幅 (跌破涨停价才有)
|
||||||
|
ReboundPct *float64 `json:"rebound_pct"` // 涨幅/反弹
|
||||||
|
ClosePrices []float64 `json:"close_prices"` // 用于前端画图的收盘价序列
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"level_go/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 跨域中间件 (根据需要开启)
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
{
|
||||||
|
v1.GET("/common/trade-calendar", commonApi.GetTradeCalendar)
|
||||||
|
v1.POST("/stock/analyze", stockApi.Analyze)
|
||||||
|
v1.GET("/stock/kline/:code", stockApi.GetKlineData)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"level_go/internal/model"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StockService struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundTo4(val float64) float64 {
|
|
||||||
return math.Round(val*10000) / 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.AnalysisResponse, error) {
|
|
||||||
var results []model.AnalysisResponse
|
|
||||||
var limits []struct {
|
|
||||||
TsCode string
|
|
||||||
Name string
|
|
||||||
Open float64
|
|
||||||
High float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 获取选定日期当天涨停的股票
|
|
||||||
query := `
|
|
||||||
SELECT l.ts_code, l.name, d.open, d.high
|
|
||||||
FROM stock_limit_data l
|
|
||||||
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
|
|
||||||
WHERE l.trade_date = ? AND l.status = 1`
|
|
||||||
|
|
||||||
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stock := range limits {
|
|
||||||
var dailies []model.StockDaily
|
|
||||||
s.DB.Where("ts_code = ? AND trade_date BETWEEN ? AND ?", stock.TsCode, startDate, endDate).
|
|
||||||
Order("trade_date ASC").Find(&dailies)
|
|
||||||
|
|
||||||
if len(dailies) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 步骤 1:寻找全区间的最高点 (MaxHigh) ---
|
|
||||||
maxPrice := -1.0
|
|
||||||
maxIdx := 0
|
|
||||||
for i, d := range dailies {
|
|
||||||
if d.High > maxPrice {
|
|
||||||
maxPrice = d.High
|
|
||||||
maxIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dropPct, reboundPct *float64
|
|
||||||
var postMinLow float64
|
|
||||||
maxDateStr := dailies[maxIdx].TradeDate.Format("2006-01-02")
|
|
||||||
|
|
||||||
// --- 步骤 2:寻找最高点之后的最低点 (PostMinLow) ---
|
|
||||||
if maxIdx < len(dailies)-1 {
|
|
||||||
postMaxData := dailies[maxIdx+1:]
|
|
||||||
minLow := postMaxData[0].Low
|
|
||||||
minLowIdx := 0
|
|
||||||
|
|
||||||
for i, d := range postMaxData {
|
|
||||||
if d.Low < minLow {
|
|
||||||
minLow = d.Low
|
|
||||||
minLowIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postMinLow = minLow
|
|
||||||
|
|
||||||
// 只要出现比 MaxHigh 低的价格,就计算回落幅度
|
|
||||||
if minLow < maxPrice {
|
|
||||||
dVal := roundTo4(((maxPrice - minLow) / maxPrice) * 100)
|
|
||||||
dropPct = &dVal
|
|
||||||
|
|
||||||
// --- 步骤 3:寻找最低点之后的反弹 ---
|
|
||||||
if minLowIdx < len(postMaxData)-1 {
|
|
||||||
lastClose := postMaxData[len(postMaxData)-1].Close
|
|
||||||
if lastClose > minLow {
|
|
||||||
rVal := roundTo4(((lastClose - minLow) / minLow) * 100)
|
|
||||||
reboundPct = &rVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 步骤 4:处理特殊形态(如持续上涨无回落) ---
|
|
||||||
if dropPct == nil {
|
|
||||||
lastClose := dailies[len(dailies)-1].Close
|
|
||||||
firstDayHigh := dailies[0].High
|
|
||||||
if lastClose > firstDayHigh {
|
|
||||||
rVal := roundTo4(((lastClose - firstDayHigh) / firstDayHigh) * 100)
|
|
||||||
reboundPct = &rVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, model.AnalysisResponse{
|
|
||||||
Code: stock.TsCode,
|
|
||||||
Name: stock.Name,
|
|
||||||
DayOpen: stock.Open,
|
|
||||||
DayHigh: stock.High,
|
|
||||||
MaxHigh: maxPrice,
|
|
||||||
MaxDate: maxDateStr,
|
|
||||||
PostMinLow: postMinLow,
|
|
||||||
DropPct: dropPct,
|
|
||||||
ReboundPct: reboundPct,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 基础排序(仅按回落幅度降序,不处理 null 特殊逻辑)
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
valI, valJ := -1.0, -1.0
|
|
||||||
if results[i].DropPct != nil {
|
|
||||||
valI = *results[i].DropPct
|
|
||||||
}
|
|
||||||
if results[j].DropPct != nil {
|
|
||||||
valJ = *results[j].DropPct
|
|
||||||
}
|
|
||||||
return valI > valJ
|
|
||||||
})
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommonService struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommonService) GetTradeCalendar() (model.CalendarResponse, error) {
|
||||||
|
var results []struct {
|
||||||
|
DateStr string `gorm:"column:ds"`
|
||||||
|
}
|
||||||
|
var resp model.CalendarResponse
|
||||||
|
|
||||||
|
err := s.DB.Model(&model.TradeCalendar{}).
|
||||||
|
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') as ds").
|
||||||
|
Where("is_open = ? AND exchange = ?", 1, "SSE").
|
||||||
|
Order("cal_date ASC").Scan(&results).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
resp.OpenDates = append(resp.OpenDates, r.DateStr)
|
||||||
|
resp.MaxDate = r.DateStr
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"level_go/internal/model"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StockService struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StockService) GetHistoryKline(code string) ([]model.StockDaily, error) {
|
||||||
|
var dailies []model.StockDaily
|
||||||
|
startDate := "2024-09-24"
|
||||||
|
|
||||||
|
err := s.DB.Table("stock_daily").
|
||||||
|
Where("ts_code = ? AND trade_date >= ?", code, startDate).
|
||||||
|
Order("trade_date ASC").
|
||||||
|
Find(&dailies).Error
|
||||||
|
|
||||||
|
return dailies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTo2(val float64) float64 {
|
||||||
|
return math.Round(val*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.AnalysisResponse, error) {
|
||||||
|
var results []model.AnalysisResponse
|
||||||
|
var limits []struct {
|
||||||
|
TsCode string
|
||||||
|
Name string
|
||||||
|
Open float64
|
||||||
|
High float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询涨停股
|
||||||
|
query := `SELECT l.ts_code, l.name, d.open, d.high FROM stock_limit_data l
|
||||||
|
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
|
||||||
|
WHERE l.trade_date = ? AND l.status = 1`
|
||||||
|
|
||||||
|
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(limits) == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
codes := make([]string, 0, len(limits))
|
||||||
|
for _, stock := range limits {
|
||||||
|
codes = append(codes, stock.TsCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDailies []model.StockDaily
|
||||||
|
if err := s.DB.Table("stock_daily").
|
||||||
|
Select("ts_code", "trade_date", "open", "high", "low", "close").
|
||||||
|
Where("ts_code IN ? AND trade_date BETWEEN ? AND ?", codes, startDate, endDate).
|
||||||
|
Order("ts_code ASC, trade_date ASC").
|
||||||
|
Find(&allDailies).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dailiesByCode := make(map[string][]model.StockDaily, len(codes))
|
||||||
|
for _, daily := range allDailies {
|
||||||
|
dailiesByCode[daily.TsCode] = append(dailiesByCode[daily.TsCode], daily)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stock := range limits {
|
||||||
|
dailies := dailiesByCode[stock.TsCode]
|
||||||
|
if len(dailies) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// A. 计算区间绝对最高价
|
||||||
|
overallMax := dailies[0].High
|
||||||
|
overallMaxDate := dailies[0].TradeDate.Format("2006-01-02")
|
||||||
|
for _, d := range dailies {
|
||||||
|
if d.High > overallMax {
|
||||||
|
overallMax = d.High
|
||||||
|
overallMaxDate = d.TradeDate.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. 逻辑计算
|
||||||
|
limitHigh := stock.High
|
||||||
|
postDailies := dailies[1:]
|
||||||
|
|
||||||
|
// 找出涨停后的全局最低
|
||||||
|
minLowAfterLimit := postDailies[0].Low
|
||||||
|
for _, d := range postDailies {
|
||||||
|
if d.Low < minLowAfterLimit {
|
||||||
|
minLowAfterLimit = d.Low
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dropPct, reboundPct *float64
|
||||||
|
var logicMaxHigh float64
|
||||||
|
var logicMaxDate string
|
||||||
|
|
||||||
|
if minLowAfterLimit >= limitHigh {
|
||||||
|
// 形态 1:强势。计算涨幅
|
||||||
|
currMax := limitHigh
|
||||||
|
maxIdx := -1
|
||||||
|
for i, d := range postDailies {
|
||||||
|
if d.High > currMax {
|
||||||
|
currMax = d.High
|
||||||
|
maxIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logicMaxHigh = currMax
|
||||||
|
logicMaxDate = startDate
|
||||||
|
if maxIdx != -1 {
|
||||||
|
logicMaxDate = postDailies[maxIdx].TradeDate.Format("2006-01-02")
|
||||||
|
val := roundTo2(((logicMaxHigh - limitHigh) / limitHigh) * 100)
|
||||||
|
reboundPct = &val
|
||||||
|
}
|
||||||
|
dropPct = nil
|
||||||
|
} else {
|
||||||
|
// 形态 2:破位。计算降幅和反弹
|
||||||
|
absMinLow := postDailies[0].Low
|
||||||
|
minIdx := 0
|
||||||
|
for i, d := range postDailies {
|
||||||
|
if d.Low < absMinLow {
|
||||||
|
absMinLow = d.Low
|
||||||
|
minIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 找到涨停到该最低点之间的最高
|
||||||
|
rangeDailies := dailies[:minIdx+2]
|
||||||
|
rangeMax := dailies[0].High
|
||||||
|
maxD := startDate
|
||||||
|
for _, d := range rangeDailies {
|
||||||
|
if d.High > rangeMax {
|
||||||
|
rangeMax = d.High
|
||||||
|
maxD = d.TradeDate.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logicMaxHigh = rangeMax
|
||||||
|
logicMaxDate = maxD
|
||||||
|
|
||||||
|
dVal := roundTo2(((rangeMax - absMinLow) / rangeMax) * 100)
|
||||||
|
dropPct = &dVal
|
||||||
|
|
||||||
|
lastClose := postDailies[len(postDailies)-1].Close
|
||||||
|
rVal := roundTo2(((lastClose - absMinLow) / absMinLow) * 100)
|
||||||
|
reboundPct = &rVal
|
||||||
|
}
|
||||||
|
|
||||||
|
var closePrices []float64
|
||||||
|
for _, d := range dailies {
|
||||||
|
closePrices = append(closePrices, d.Close)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, model.AnalysisResponse{
|
||||||
|
Code: stock.TsCode,
|
||||||
|
Name: stock.Name,
|
||||||
|
DayOpen: stock.Open,
|
||||||
|
DayHigh: stock.High,
|
||||||
|
MaxHigh: logicMaxHigh,
|
||||||
|
MaxDate: logicMaxDate,
|
||||||
|
OverallMaxHigh: overallMax,
|
||||||
|
OverallMaxDate: overallMaxDate,
|
||||||
|
PostMinLow: minLowAfterLimit,
|
||||||
|
DropPct: dropPct,
|
||||||
|
ReboundPct: reboundPct,
|
||||||
|
ClosePrices: closePrices,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
if results[i].DropPct == nil && results[j].DropPct != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if results[i].DropPct != nil && results[j].DropPct == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if results[i].DropPct != nil && results[j].DropPct != nil {
|
||||||
|
return *results[i].DropPct > *results[j].DropPct
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
+14
-1
@@ -24,7 +24,8 @@ var AppConfig *Config
|
|||||||
func InitConfig() {
|
func InitConfig() {
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
viper.AddConfigPath(".") // 在当前目录查找 config.yaml
|
viper.AddConfigPath(".") // 在当前目录查找 config.yaml
|
||||||
|
viper.AddConfigPath("./config") // 兼容多一层目录的情况
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
panic(fmt.Errorf("无法读取配置文件: %w", err))
|
panic(fmt.Errorf("无法读取配置文件: %w", err))
|
||||||
@@ -34,3 +35,15 @@ func InitConfig() {
|
|||||||
panic(fmt.Errorf("无法解析配置结构体: %w", err))
|
panic(fmt.Errorf("无法解析配置结构体: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDSN 生成 MySQL 连接字符串
|
||||||
|
func GetDSN() string {
|
||||||
|
db := AppConfig.Database
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||||
|
db.User, db.Password, db.Host, db.Port, db.Dbname, db.Charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServerPort 获取服务器端口字符串
|
||||||
|
func GetServerPort() string {
|
||||||
|
return fmt.Sprintf(":%d", AppConfig.Server.Port)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user