feat(crypto策略): 支持巴菲特策略纯 rawMessage 信号推送

Motivation:
新增巴菲特激进/稳健策略(PUTEJJ/PUTEWJ)的跟单推送能力。该类信号仅有 rawMessage、无 action 字段,需按原文直接推送,并过滤启动文案、本地化时间字段。

Changes:

* 支持无 action 的纯 rawMessage 信号,映射为 trade.message 事件
* 新增 not_contains 条件操作符,用于过滤启动文案
* 新增 replace 模板函数,将 Time: 替换为推送时间:
* 补充巴菲特策略模板、规则与渠道配置文档
This commit is contained in:
2026-08-17 16:38:09 +08:00
parent 33b2a96bcf
commit f7ea2e7cf9
9 changed files with 152 additions and 5 deletions
+5
View File
@@ -46,6 +46,11 @@ func evaluateOne(c model.Condition, data map[string]interface{}) bool {
return false
}
return strings.Contains(fmt.Sprintf("%v", fieldVal), c.Value)
case "not_contains":
if !fieldExists {
return true
}
return !strings.Contains(fmt.Sprintf("%v", fieldVal), c.Value)
case "gt", "gte", "lt", "lte":
if !fieldExists {
return false
+13
View File
@@ -54,3 +54,16 @@ func TestEvaluate_Contains(t *testing.T) {
t.Error("msg contains 'error', should pass")
}
}
func TestEvaluate_NotContains(t *testing.T) {
conds := []model.Condition{{Field: "rawMessage", Op: "not_contains", Value: "启动"}}
if !Evaluate(conds, map[string]interface{}{"rawMessage": "激进版AI 1.0\n市价开空"}) {
t.Fatal("open text should pass")
}
if Evaluate(conds, map[string]interface{}{"rawMessage": "普达特量化机器人激进版启动\n账户余额:100000.00"}) {
t.Fatal("startup text should be filtered")
}
if !Evaluate(conds, map[string]interface{}{"strategyCode": "PUTEJJ"}) {
t.Fatal("missing rawMessage should pass")
}
}
+3 -2
View File
@@ -17,8 +17,9 @@ func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (stri
tmpl, err := template.New("notify").
Option("missingkey=zero").
Funcs(template.FuncMap{
"line": templateLine,
"case": templateCase,
"line": templateLine,
"case": templateCase,
"replace": strings.ReplaceAll,
}).
Parse(tmplContent)
if err != nil {
+13
View File
@@ -98,6 +98,19 @@ func TestRendererCaseEventSuffix(t *testing.T) {
}
}
func TestRendererReplace(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{replace .rawMessage "Time:" "推送时间:"}}`, map[string]interface{}{
"rawMessage": "激进版AI 1.0\n市价开空\nTime: 2026.08.17 14:46:04",
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "推送时间: 2026.08.17 14:46:04") || strings.Contains(out, "Time:") {
t.Fatalf("out=%q", out)
}
}
func TestRendererCaseDefault(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{case .action "OPEN" "开仓" "未知"}}`, map[string]interface{}{"action": "HOLD"})
+8 -1
View File
@@ -29,7 +29,14 @@ func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error)
return "", nil, fmt.Errorf("%w: %v", ErrInvalidSignal, err)
}
if strings.TrimSpace(sig.Action) == "" {
return "", nil, fmt.Errorf("%w: missing action", ErrInvalidSignal)
if strings.TrimSpace(sig.RawMessage) == "" {
return "", nil, fmt.Errorf("%w: missing action", ErrInvalidSignal)
}
data, err := toData(body, &sig)
if err != nil {
return "", nil, err
}
return "trade.message", data, nil
}
out := Apply(&sig, c.overrideFor(sig.StrategyCode))
snap := c.positions.Apply(out)
@@ -62,3 +62,19 @@ func TestConvertMissingAction(t *testing.T) {
t.Fatalf("err=%v", err)
}
}
func TestConvertRawMessageWithoutAction(t *testing.T) {
event, data, err := NewConverter(nil).Convert([]byte(`{
"strategyCode":"PUTEJJ",
"rawMessage":"4.7.15~2026.8.15 本周期 10万本金 期末109640,盈利9.6%"
}`))
if err != nil {
t.Fatal(err)
}
if event != "trade.message" {
t.Fatalf("event=%q", event)
}
if data["rawMessage"] != "4.7.15~2026.8.15 本周期 10万本金 期末109640,盈利9.6%" {
t.Fatalf("data=%v", data)
}
}
@@ -22,6 +22,7 @@ type Signal struct {
StopLossRatio *float64 `json:"stopLossRatio"`
PnL *float64 `json:"pnl"`
AccountBalance *float64 `json:"accountBalance"`
RawMessage string `json:"rawMessage"`
}
func (s *Signal) ParsedEventTime() time.Time {