diff --git a/internal/logic/restful/starter.go b/internal/logic/restful/starter.go index e561073..8850653 100644 --- a/internal/logic/restful/starter.go +++ b/internal/logic/restful/starter.go @@ -3,9 +3,11 @@ package restful import ( "log" + "git.apinb.com/bsm-sdk/core/utils" "git.apinb.com/quant/gostock/internal/impl" "git.apinb.com/quant/gostock/internal/logic/mock" "git.apinb.com/quant/gostock/internal/logic/strategy" + "git.apinb.com/quant/gostock/internal/logic/strategy/indicator" "git.apinb.com/quant/gostock/internal/logic/strategy/rule" "git.apinb.com/quant/gostock/internal/models" "github.com/gin-gonic/gin" @@ -13,41 +15,67 @@ import ( func Starter(ctx *gin.Context) { log.Println("Strategy START.") - ymd := models.GetYmd() + ymdQuery := ctx.DefaultQuery("ymd", "") + var ymd int + if ymdQuery != "" { + ymd = utils.String2Int(ymdQuery) + } + + if ymd == 0 { + ymd = models.GetYmd() + } + for _, code := range strategy.GetStocks() { - strategy.InitCacheByCode(code) + basic := strategy.GetBasic(code) model := models.NewStratModel("selector", code, ymd) stratRule := rule.NewRule(model) { // 规则:上市时间 - stratRule.RunUpDate(strategy.Cache[code].Basic.ListDate) + stratRule.RunUpDate(basic.ListDate) // 规则:是否是ST - stratRule.RunST(strategy.Cache[code].Basic.Name) + stratRule.RunST(basic.Name) // 规则:行业,剔除夕阳和中性行业 - stratRule.RunIndustry(strategy.Cache[code].Basic.Industry) + stratRule.RunIndustry(basic.Industry) // 规则:最近20天每天最低价高于5元 stratRule.RunPrice(code) // 规则:每天交易额超过10亿 stratRule.RunAmount(code) // 规则:ROE 市盈率必须为正 stratRule.RunRoe(code) - // 规则:RSI指标贴近下轨并成上涨趋势 - stratRule.RunRsi(code) // 满足以上规则在让Deepseek分析 - if model.UpDateDay > 360 && model.StScore > 0 && model.IndustryScore > 1 && model.GtPrice > 0 && model.GtAmount > 0 && model.GtRoe > 0 && model.ScoreRsi > 0 { + if model.UpDateDay > 360 && model.StScore > 0 && model.IndustryScore > 1 && model.GtPrice > 0 && model.GtAmount > 0 && model.RoeScore > 0 { model.AiScore = 0 // 待分析 - model.TotalScore = float64(model.IndustryScore + model.StScore + model.GtPrice + model.GtAmount + model.GtRoe + model.ScoreRsi) + model.Status = 1 + model.TotalScore = float64(model.IndustryScore + model.StScore + model.GtPrice + model.GtAmount + model.RoeScore) model.RecommendDesc = "Rule规则" } else { + model.Status = -1 model.AiScore = -2 - model.AddDesc("无需AI分析") + model.AddDesc("Rule规则不满足,不加入标的") } } + model.Save() } + var allowStocks []*models.StratModel + impl.DBService.Model(&models.StratModel{}).Where("status=1 and ymd=?", ymd).Find(&allowStocks) + for _, m := range allowStocks { + // CTA:RSI指标贴近下轨并成上涨趋势 + indicator.New(m).RunRsi() + + // 满足以上规则在让Deepseek分析 + if m.ScoreRsi < 0 { + impl.DBService.Model(m).Where("id=?", m.ID).Updates(map[string]any{"ai_score": -2, "recommend_desc": m.RecommendDesc + "||" + "无需AI分析,RsiScore:" + utils.Int2String(m.ScoreRsi)}) + } + + // CTA:MACD指标红绿柱及价量关系 + score, desc := indicator.New(m).RunMacd() + impl.DBService.Model(m).Where("id=?", m.ID).Updates(map[string]any{"macd_score": score, "recommend_desc": m.RecommendDesc + "||" + desc}) + } + // 加入资金流向特大的标的 var codes []string impl.DBService.Model(&models.MoneyTotal{}).Where("is_greater_pervious = ? and last3_day_mf_amount>?", true, 100000).Pluck("code", &codes) diff --git a/internal/logic/strategy/indicator/macd.go b/internal/logic/strategy/indicator/macd.go new file mode 100644 index 0000000..a6ad826 --- /dev/null +++ b/internal/logic/strategy/indicator/macd.go @@ -0,0 +1,332 @@ +package indicator + +/* +以MACD指标红绿柱(能量柱)为核心决策依据的A股交易规则系统。旨在通过捕捉柱体的“由绿变红”(空转多)和“由红变绿”(多转空)的临界点,结合柱体的伸缩长度及与股价的背离关系,来量化买卖信号,减少主观情绪干扰。 +系统需能识别并提示以下几类基于红绿柱的买入场景: +参数: + 日线 + 参数(12,26,9) + +3.1 基础买入信号(柱体反转) +需求描述:捕捉下跌动能衰竭、上涨动能启动的瞬间。 + +判定标准: + +在零轴下方或零轴附近。 +MACD绿色柱体开始缩短的最后一根(或第一根红色柱体出现)。 +量化确认:当MACD柱状线由绿变红(负值变正值),视为买入信号。根据上海证券报的历史回测,在周线级别出现红柱且数值超过5后的第二周开盘买入,长期收益显著。 + +3.2 趋势跟随买入(红柱伸长) +需求描述:在多头趋势中,捕捉主升浪阶段。 +判定标准: +DIF与DEA在零轴上方运行(水上)。 +红柱连续增长(每一根比前一根高),表明上涨动能持续增强。 +持有规则:只要红柱持续伸长,继续持有;若虽为红柱但开始缩短,则应考虑部分止盈。 + + +3.4 底背离买入(左侧交易) +需求描述:捕捉价格新低但动能不再的潜在反转点。 +判定标准: +股价创出新低(或收盘价新低)。 +MACD绿柱的底部(绝对值低点)较前一波绿柱底部抬高(即绿柱长度变短,下跌缩量)。 +随后出现红柱确认时买入。 + +5. 特殊形态与过滤条件 +为提高胜率,系统需识别以下复合形态: +零轴水下金叉后的拒绝死叉: +MACD在0轴下方金叉后,股价小幅调整导致快慢线黏合,但并未形成死叉(或形成失败的死叉),随后红柱再次加长。这是洗盘结束、主升浪启动的信号。 +成交量验证: +红柱放大时,需要成交量同步放大的确认。若红柱放大但缩量,信号的可靠性降低。 +双线联合形态: +零轴上,DIF与DEA线粘合后再次分离向上发散,同时红柱伸长,是加速上涨信号 +*/ + +import ( + "fmt" + "math" + + "git.apinb.com/bsm-sdk/core/utils" + "git.apinb.com/quant/gostock/internal/impl" + "git.apinb.com/quant/gostock/internal/models" + talib "github.com/markcheno/go-talib" +) + +const ( + defaultMacdFast = 12 + defaultMacdSlow = 26 + defaultMacdSignal = 9 +) + +// RunMacd 根据MACD红绿柱及价量关系,对当前标的进行打分与描述 +func (r *IndicatorFactory) RunMacd() (int, string) { + args, _, err := GetArgConfig(r.Model.Code) + if err != nil { + return -1, "MACD参数错误!" + } + + fast := args.EmaFast + slow := args.EmaSlow + signal := defaultMacdSignal + + if fast <= 0 { + fast = defaultMacdFast + } + if slow <= 0 { + slow = defaultMacdSlow + } + + // 为了有足够的数据观察柱体形态,这里多取一些K线 + limit := slow * 6 + if limit < 200 { + limit = 200 + } + + var closes []float64 + var vols []float64 + + impl.DBService.Model(models.StockDaily{}). + Where("ts_code = ?", r.Model.Code). + Order("trade_date desc"). + Limit(limit). + Pluck("close", &closes) + + impl.DBService.Model(models.StockDaily{}). + Where("ts_code = ?", r.Model.Code). + Order("trade_date desc"). + Limit(limit). + Pluck("vol", &vols) + + if len(closes) < slow+signal+5 { + return -1, "MACD 数据不足" + } + + closes = ReverseSlice(closes) + if len(vols) >= len(closes) { + vols = vols[:len(closes)] + vols = ReverseSlice(vols) + } else { + // 成交量不足时不做成交量验证 + vols = nil + } + + macdLine, signalLine, hist := talib.Macd(closes, fast, slow, signal) + n := len(hist) + if n < 5 { + return -1, "MACD 计算结果不足" + } + + lastIdx := n - 1 + prevIdx := n - 2 + + lastHist := hist[lastIdx] + prevHist := hist[prevIdx] + lastMacd := macdLine[lastIdx] + lastSignal := signalLine[lastIdx] + + r.Model.MacdVal = utils.FloatRound(lastHist, 4) + + score := -1 + + // 3.1 基础买入信号:绿柱转红柱,零轴下方或附近 + if prevHist < 0 && lastHist > 0 && lastMacd <= 0 { + score = maxInt(score, 1) + return score, fmt.Sprintf("MACD红绿柱反转:由绿转红,零轴下方/附近,hist=%.4f", lastHist) + } + + // 3.2 趋势跟随买入:红柱连续伸长,DIF/DEA在零轴上方 + if lastMacd > 0 && lastSignal > 0 && isGrowingPositive(hist, 3) { + score = maxInt(score, 2) + return score, "MACD红柱连续伸长,DIF/DEA零轴上方,趋势跟随买入信号" + + } + + // 3.4 底背离买入:价格新低但绿柱底部抬高,随后红柱确认 + if lastHist > 0 && hasBottomDivergence(closes, hist) { + score = maxInt(score, 2) + return score, "MACD底背离:价格创新低但绿柱底部抬高,红柱确认" + } + + // 5.1 零轴下金叉后的拒绝死叉:金叉发生在零轴下方,之后未形成有效死叉,红柱再次放大 + if lastHist > 0 && isGrowingPositive(hist, 2) && hasGoldenCrossRejection(macdLine, signalLine, hist) { + score = maxInt(score, 3) + return score, "MACD零轴下金叉后拒绝死叉,红柱再次放大,疑似洗盘结束主升浪启动" + } + + // 成交量验证:红柱放大但缩量,降低一次评分 + if score > 0 && vols != nil && lastHist > 0 && isGrowingPositive(hist, 2) { + if !isVolumeConfirmed(vols) { + score-- + if score <= 0 { + score = -1 + } + return score, "MACD红柱放大但成交量未同步放大,信号可靠性降低" + } + } + + if score == -1 { + if lastHist <= 0 { + return -1, fmt.Sprintf("MACD未出现明确多头信号,hist=%.4f", lastHist) + } + } + + return score, "无信号" +} + +// 最近k根红柱是否连续伸长(严格递增且均为正) +func isGrowingPositive(hist []float64, k int) bool { + n := len(hist) + if k <= 1 || n < k { + return false + } + + start := n - k + prev := hist[start] + if prev <= 0 { + return false + } + + for i := start + 1; i < n; i++ { + if hist[i] <= 0 || hist[i] <= prev { + return false + } + prev = hist[i] + } + return true +} + +// 底背离:最近两段绿柱,后一段对应的价格更低但绿柱绝对值更小 +func hasBottomDivergence(closes, hist []float64) bool { + n := len(hist) + if n < 20 || len(closes) != n { + return false + } + + type valley struct { + idx int + hist float64 + px float64 + } + + var valleys []valley + inNeg := false + curMinIdx := -1 + + for i := 1; i < n-1; i++ { + if hist[i] < 0 { + if !inNeg { + inNeg = true + curMinIdx = i + } + if curMinIdx == -1 || hist[i] < hist[curMinIdx] { + curMinIdx = i + } + } else if inNeg { + // 负柱结束,记录一段绿柱的底部 + valleys = append(valleys, valley{ + idx: curMinIdx, + hist: hist[curMinIdx], + px: closes[curMinIdx], + }) + inNeg = false + curMinIdx = -1 + } + } + + if inNeg && curMinIdx != -1 { + valleys = append(valleys, valley{ + idx: curMinIdx, + hist: hist[curMinIdx], + px: closes[curMinIdx], + }) + } + + if len(valleys) < 2 { + return false + } + + v1 := valleys[len(valleys)-2] + v2 := valleys[len(valleys)-1] + + // 价格创新低,但绿柱绝对值变小(底部抬高) + if v2.px >= v1.px { + return false + } + if math.Abs(v2.hist) >= math.Abs(v1.hist) { + return false + } + + return true +} + +// 零轴下方金叉后,未形成死叉且红柱再次放大 +func hasGoldenCrossRejection(macdLine, signalLine, hist []float64) bool { + n := len(macdLine) + if n < 10 || len(signalLine) != n || len(hist) != n { + return false + } + + lastIdx := n - 1 + goldenIdx := -1 + + // 在最近一段时间内(最多往前看60根),寻找最近一次位于零轴下方的金叉 + for i := lastIdx; i >= 1 && i >= n-60; i-- { + if macdLine[i-1] < signalLine[i-1] && + macdLine[i] > signalLine[i] && + macdLine[i] < 0 && + signalLine[i] < 0 { + goldenIdx = i + break + } + } + + if goldenIdx == -1 { + return false + } + + // 金叉之后是否出现了真正的死叉?若出现则不算“拒绝死叉” + for i := goldenIdx + 1; i <= lastIdx; i++ { + if macdLine[i-1] > signalLine[i-1] && macdLine[i] < signalLine[i] { + return false + } + } + + // 红柱再次放大 + if hist[lastIdx] <= 0 { + return false + } + if hist[lastIdx] <= hist[goldenIdx] { + return false + } + + return true +} + +// 成交量确认:最近3根K线成交量均值应不低于之前3根 +func isVolumeConfirmed(vols []float64) bool { + n := len(vols) + if n < 6 { + // 数据不足时不强制要求放量 + return true + } + + var lastSum, prevSum float64 + for i := n - 3; i < n; i++ { + lastSum += vols[i] + } + for i := n - 6; i < n-3; i++ { + prevSum += vols[i] + } + + lastAvg := lastSum / 3 + prevAvg := prevSum / 3 + + return lastAvg >= prevAvg +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + diff --git a/internal/logic/strategy/indicator/new.go b/internal/logic/strategy/indicator/new.go new file mode 100644 index 0000000..a3b9cad --- /dev/null +++ b/internal/logic/strategy/indicator/new.go @@ -0,0 +1,13 @@ +package indicator + +import "git.apinb.com/quant/gostock/internal/models" + +type IndicatorFactory struct { + Model *models.StratModel +} + +func New(m *models.StratModel) *IndicatorFactory { + return &IndicatorFactory{ + Model: m, + } +} diff --git a/internal/logic/strategy/rule/rsi.go b/internal/logic/strategy/indicator/rsi.go similarity index 92% rename from internal/logic/strategy/rule/rsi.go rename to internal/logic/strategy/indicator/rsi.go index 5d7899e..935d28d 100644 --- a/internal/logic/strategy/rule/rsi.go +++ b/internal/logic/strategy/indicator/rsi.go @@ -1,4 +1,4 @@ -package rule +package indicator import ( "encoding/json" @@ -38,8 +38,8 @@ func GetArgConfig(code string) (*models.StockArgs, *StockArgConf, error) { return &args, &conf, nil } -func (r *RuleFactory) RunRsi(code string) { - args, conf, err := GetArgConfig(code) +func (r *IndicatorFactory) RunRsi() { + args, conf, err := GetArgConfig(r.Model.Code) if err != nil { r.Model.ScoreRsi = -1 r.Model.AddDesc("RSI参数错误!") @@ -59,14 +59,14 @@ func (r *RuleFactory) RunRsi(code string) { } var close []float64 - impl.DBService.Model(models.StockDaily{}).Where("ts_code = ?", code).Order("trade_date desc").Limit(args.RsiPeriod*4).Pluck("close", &close) + impl.DBService.Model(models.StockDaily{}).Where("ts_code = ?", r.Model.Code).Order("trade_date desc").Limit(args.RsiPeriod*4).Pluck("close", &close) if len(close) < args.RsiPeriod { r.Model.ScoreRsi = -1 r.Model.AddDesc("数据不足") return } - newCloses := reverseSlice(close) + newCloses := ReverseSlice(close) args.RsiOversold = args.RsiOversold + offset rsiResult := talib.Rsi(newCloses, args.RsiPeriod) diff --git a/internal/logic/strategy/rule/utils.go b/internal/logic/strategy/indicator/utils.go similarity index 72% rename from internal/logic/strategy/rule/utils.go rename to internal/logic/strategy/indicator/utils.go index a144a69..64d2bca 100644 --- a/internal/logic/strategy/rule/utils.go +++ b/internal/logic/strategy/indicator/utils.go @@ -1,12 +1,12 @@ -package rule +package indicator // 如果数据是降序(最新在前),需要反转 -func reverseSlice(s []float64) []float64 { - result := make([]float64, len(s)) - for i, j := 0, len(s)-1; i < len(s); i, j = i+1, j-1 { - result[i] = s[j] - } - return result +func ReverseSlice(s []float64) []float64 { + result := make([]float64, len(s)) + for i, j := 0, len(s)-1; i < len(s); i, j = i+1, j-1 { + result[i] = s[j] + } + return result } // 检查值是否为数组最后N个元素中的最小值 diff --git a/internal/logic/strategy/rule/roe.go b/internal/logic/strategy/rule/roe.go index 53f28e7..22d130d 100644 --- a/internal/logic/strategy/rule/roe.go +++ b/internal/logic/strategy/rule/roe.go @@ -19,8 +19,7 @@ func (r *RuleFactory) RunRoe(code string) { var data models.StockFinaIndicator err := impl.DBService.Where("ts_code = ?", code).Order("period desc").Limit(1).First(&data).Error if err != nil { - r.Model.GtRoe = -1 - r.Model.ValRoe = -1 + r.Model.RoeScore = -1 r.Model.AddDesc("最近无财报,无ROE值!") return } @@ -29,12 +28,13 @@ func (r *RuleFactory) RunRoe(code string) { r.Model.ValRoe = data.Roe if data.Roe < MinRoe { - r.Model.GtRoe = -1 + r.Model.RoeScore = -1 r.Model.AddDesc(fmt.Sprintf("ROE=%.2f 低于%.2f", data.Roe, MinRoe)) return } - r.Model.GtRoe = 1 + r.Model.RoeScore = 1 + r.Model.ValRoe = data.Roe r.Model.AddDesc(fmt.Sprintf("ROE=%.2f 高于%.2f", data.Roe, MinRoe)) return } diff --git a/internal/models/strat_desc.go b/internal/models/strat_desc.go deleted file mode 100644 index 784ca76..0000000 --- a/internal/models/strat_desc.go +++ /dev/null @@ -1,24 +0,0 @@ -package models - -import ( - "time" - - "git.apinb.com/bsm-sdk/core/database" -) - -type StratDesc struct { - ID uint `gorm:"primarykey"` - CreatedAt time.Time - StratModelID uint `gorm:"column:strat_model_id"` - Hash string `gorm:"uniqueIndex"` - Desc string -} - -func init() { - database.AppendMigrate(&StratDesc{}) -} - -// TableName 设置表名 -func (StratDesc) TableName() string { - return "strat_desc" -} diff --git a/internal/models/strat_model.go b/internal/models/strat_model.go index a168de4..1879674 100644 --- a/internal/models/strat_model.go +++ b/internal/models/strat_model.go @@ -16,21 +16,25 @@ type StratModel struct { StratKey string Ymd int Code string - UpDateDay int //上市时间,天数 - IndustryScore int // 行业分组 - StScore int // 是否是ST - GtAmount int // 每日交易额大于设定值 - GtPrice int // 最近20日交易日价格大于设定值 - GtRoe int // ROE 是否大于设定值 + UpDateDay int //上市时间,天数 + IndustryScore int // 行业分组 + StScore int // 是否是ST + GtAmount int // 每日交易额大于设定值 + GtPrice int // 最近20日交易日价格大于设定值 + RoeScore int // ROE 是否大于设定值 + ValRoe float64 // ROE 值 ScoreRsi int RecommendDesc string // 推荐描述 + Status int // 状态 -1:不推荐,0:正常 1:推荐 + + // macd + MacdScore int + MacdVal float64 // 值 - ValRoe float64 ValRsiOversold int ValRsiPrve float64 ValRsiLast float64 - Desc []StratDesc `gorm:"foreignKey:StratModelID"` //AI AiSummary string