347 lines
8.7 KiB
Go
347 lines
8.7 KiB
Go
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
|
||
)
|
||
|
||
var (
|
||
desc []string
|
||
)
|
||
|
||
type MacdResult struct {
|
||
Score int
|
||
Val float64
|
||
Desc string
|
||
}
|
||
|
||
// RunMacd 根据MACD红绿柱及价量关系,对当前标的进行打分与描述
|
||
func RunMacd(code string) *MacdResult {
|
||
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
|
||
}
|