This commit is contained in:
2026-02-24 16:29:28 +08:00
parent 5340b07795
commit 24fe2a97cf
8 changed files with 411 additions and 58 deletions

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,117 @@
package indicator
import (
"encoding/json"
"fmt"
"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"
)
var (
offset = 1 // 偏移量
)
type StockArgConf struct {
BestByDrawdown string `json:"best_by_drawdown"`
BestByProfit string `json:"best_by_profit"`
BestByReturn string `json:"best_by_return"`
BestBySharpe string `json:"best_by_sharpe"`
BestByWinRate string `json:"best_by_win_rate"`
}
func GetArgConfig(code string) (*models.StockArgs, *StockArgConf, error) {
var args models.StockArgs
err := impl.DBService.Where("ts_code = ?", code).First(&args).Error
if err != nil {
return nil, nil, err
}
var conf StockArgConf
err = json.Unmarshal([]byte(args.Config), &conf)
if err != nil {
return nil, nil, err
}
return &args, &conf, nil
}
func (r *IndicatorFactory) RunRsi() {
args, conf, err := GetArgConfig(r.Model.Code)
if err != nil {
r.Model.ScoreRsi = -1
r.Model.AddDesc("RSI参数错误")
return
}
if args.RsiOversold == 0 {
r.Model.ScoreRsi = -1
r.Model.AddDesc("RSI RsiOversold=0,参数错误!")
return
}
if conf.BestByProfit != "rsi" {
r.Model.ScoreRsi = -1
r.Model.AddDesc("BestByProfit不是RSI")
return
}
var close []float64
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)
args.RsiOversold = args.RsiOversold + offset
rsiResult := talib.Rsi(newCloses, args.RsiPeriod)
prveRsi := utils.FloatRound(rsiResult[len(rsiResult)-2], 2)
lastRsi := utils.FloatRound(rsiResult[len(rsiResult)-1], 2)
r.Model.ValRsiLast = lastRsi
r.Model.ValRsiPrve = prveRsi
r.Model.ValRsiOversold = args.RsiOversold
prveRsiInt := int(prveRsi)
lastRsiInt := int(lastRsi)
if lastRsiInt == 0 {
r.Model.ScoreRsi = -1
r.Model.AddDesc("RSI lastRsiInt=0,计算错误!")
return
}
// 跌破RSI下轨
if lastRsiInt > args.RsiOversold {
if CheckLowest(close, lastRsiInt, 14) {
r.Model.ScoreRsi = 1
r.Model.AddDesc(fmt.Sprintf("RSI=%d 跌破下轨,14日最低", lastRsiInt))
return
}
if CheckLowest(close, lastRsiInt, 20) {
r.Model.ScoreRsi = 1
r.Model.AddDesc(fmt.Sprintf("RSI=%d 跌破下轨,20日最低", lastRsiInt))
return
}
r.Model.ScoreRsi = -1
r.Model.AddDesc(fmt.Sprintf("RSI=%d 高于Oversold%d", lastRsiInt, args.RsiOversold))
return
}
// RSI跌破下轨后呈上涨趋势
if lastRsiInt < prveRsiInt {
r.Model.ScoreRsi = -1
r.Model.AddDesc(fmt.Sprintf("Rsi=%d prveRsi=%d,突破下轨,持续下跌", lastRsiInt, prveRsiInt))
return
} else if lastRsiInt == prveRsiInt {
r.Model.ScoreRsi = 1
r.Model.AddDesc(fmt.Sprintf("Rsi=%d prveRsi=%d,突破下轨,与前一交易日无太大波动", lastRsiInt, prveRsiInt))
return
}
r.Model.ScoreRsi = 2
r.Model.AddDesc(fmt.Sprintf("Rsi=%d prveRsi=%d,突破下轨后呈上涨趋势", lastRsiInt, prveRsiInt))
return
}

View File

@@ -0,0 +1,35 @@
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
}
// 检查值是否为数组最后N个元素中的最小值
func CheckLowest(prices []float64, target int, n int) bool {
if len(prices) == 0 || n <= 0 {
return false
}
// 如果n大于数组长度使用整个数组
if n > len(prices) {
n = len(prices)
}
// 获取最后n个元素
slice := prices[len(prices)-n:]
// 查找最小值
minVal := slice[0]
for _, val := range slice[1:] {
if val < minVal {
minVal = val
}
}
return target == int(minVal)
}