Files
gostock/internal/logic/strategy/indicator/macd.go
2026-02-27 00:53:07 +08:00

344 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
"strings"
"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
)
type MacdResult struct {
Score int
Val float64
Desc string
}
// RunMacd 根据MACD红绿柱及价量关系对当前标的进行打分与描述
func RunMacd(code string) *MacdResult {
var desc []string
args, _, err := GetArgConfig(code)
if err != nil {
return &MacdResult{Score: -1, Desc: "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 = ?", code).
Order("trade_date desc").
Limit(limit).
Pluck("close", &closes)
impl.DBService.Model(models.StockDaily{}).
Where("ts_code = ?", code).
Order("trade_date desc").
Limit(limit).
Pluck("vol", &vols)
if len(closes) < slow+signal+5 {
return &MacdResult{Score: -1, Desc: "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 &MacdResult{Score: -1, Desc: "MACD 计算结果不足"}
}
lastIdx := n - 1
prevIdx := n - 2
lastHist := hist[lastIdx]
prevHist := hist[prevIdx]
lastMacd := macdLine[lastIdx]
lastSignal := signalLine[lastIdx]
macdVal := utils.FloatRound(lastHist, 4)
score := -1
// 3.1 基础买入信号:绿柱转红柱,零轴下方或附近
if prevHist < 0 && lastHist > 0 && lastMacd <= 0 {
score = maxInt(score, 1)
desc = append(desc, "MACD红绿柱反转由绿转红零轴下方/附近")
}
// 3.2 趋势跟随买入红柱连续伸长DIF/DEA在零轴上方
if lastMacd > 0 && lastSignal > 0 && isGrowingPositive(hist, 3) {
score = maxInt(score, 2)
desc = append(desc, "MACD红柱连续伸长DIF/DEA零轴上方趋势跟随买入信号")
}
// 3.4 底背离买入:价格新低但绿柱底部抬高,随后红柱确认
if lastHist > 0 && hasBottomDivergence(closes, hist) {
score = maxInt(score, 2)
desc = append(desc, "MACD价格新低但绿柱底部抬高后续红柱确认")
}
// 5.1 零轴下金叉后的拒绝死叉:金叉发生在零轴下方,之后未形成有效死叉,红柱再次放大
if lastHist > 0 && isGrowingPositive(hist, 2) && hasGoldenCrossRejection(macdLine, signalLine, hist) {
score = maxInt(score, 3)
desc = append(desc, "MACD零轴下金叉后拒绝死叉红柱再次放大")
}
// 成交量验证:红柱放大但缩量,降低一次评分
if score > 0 && vols != nil && lastHist > 0 && isGrowingPositive(hist, 2) {
if !isVolumeConfirmed(vols) {
score--
if score <= 0 {
score = -1
}
return &MacdResult{Score: score, Val: macdVal, Desc: "MACD红柱放大但成交量未同步放大信号可靠性降低"}
}
}
if score == -1 {
if lastHist <= 0 {
return &MacdResult{Score: -1, Val: macdVal, Desc: fmt.Sprintf("MACD未出现明确多头信号hist=%.4f", lastHist)}
}
}
if score > 0 {
return &MacdResult{Score: score, Val: macdVal, Desc: strings.Join(desc, "||")}
}
return &MacdResult{Score: -1, Val: macdVal, Desc: "无信号"}
}
// 最近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
}