70 lines
3.5 KiB
Go
70 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
|
)
|
|
|
|
func screeningFixture() (model.ScreeningTarget, model.ProviderConfig, model.MarketHistory, time.Time) {
|
|
now := time.Date(2026, 10, 9, 9, 0, 0, 0, time.UTC) // 17:00 Shanghai
|
|
c := model.ProviderConfig{ID: "source", Version: 2, MarketAdapter: "sina_public", HistoryBaseURL: "https://quotes.example.cn"}
|
|
p := model.StrategyParameters{ShortWindow: 2, LongWindow: 3, Screening: &model.ScreeningParameters{Securities: []model.ScreeningSecurity{{Symbol: "600000", Exchange: "SSE"}}, EntryBandBPS: 100, StopLossBPS: 500, TargetBPS: 1000, ValidHours: 24}}
|
|
t := model.ScreeningTarget{StrategyID: "strategy", StrategyName: "均线研究", Version: model.StrategyVersion{ID: "version", Revision: 3, Parameters: p}, Instrument: model.Instrument{Symbol: "600000", Exchange: "SSE", Name: "fixture", SourceID: c.ID, SourceVersion: c.Version, ObservedAt: now.Add(-2 * time.Hour), ReceivedAt: now}}
|
|
h := model.MarketHistory{Symbol: "600000", Exchange: "SSE", Source: c.MarketAdapter, SourceVersion: c.Version, Adjustment: "unadjusted", ReceivedAt: now, Bars: []model.HistoricalBar{{Date: "2026-10-06", Open: "10.00", High: "10.00", Low: "10.00", Close: "10.00"}, {Date: "2026-10-07", Open: "9.00", High: "9.00", Low: "9.00", Close: "9.00"}, {Date: "2026-10-08", Open: "8.00", High: "8.00", Low: "8.00", Close: "8.00"}, {Date: "2026-10-09", Open: "20.01", High: "20.01", Low: "20.01", Close: "20.01"}}}
|
|
return t, c, h, now
|
|
}
|
|
func TestScreeningExactCompletedBarSignal(t *testing.T) {
|
|
target, c, h, now := screeningFixture()
|
|
run, draft, e := EvaluateScreening(target, c, h, now)
|
|
if e != nil || draft == nil || run.Status != "SIGNAL" || len(run.InputHash) != 64 {
|
|
t.Fatalf("signal: %v %#v", e, run)
|
|
}
|
|
plan := draft.Content.ReferencePlan
|
|
if plan.EntryLow != "19.80" || plan.EntryHigh != "20.22" || plan.StopLoss != "19.00" || plan.TargetPrice != "22.02" {
|
|
t.Fatalf("price precision: %+v", plan)
|
|
}
|
|
again, _, _ := EvaluateScreening(target, c, h, now.Add(time.Minute))
|
|
if again.InputHash != run.InputHash {
|
|
t.Fatal("hash depends on receive time")
|
|
}
|
|
h.Bars[3] = model.HistoricalBar{Date: "2026-10-09", Open: "7", High: "7", Low: "7", Close: "7"}
|
|
run, draft, e = EvaluateScreening(target, c, h, now)
|
|
if e != nil || draft != nil || run.Status != "NO_SIGNAL" {
|
|
t.Fatal("falling average manufactured signal")
|
|
}
|
|
}
|
|
func TestScreeningRejectsUnusableInputs(t *testing.T) {
|
|
for _, name := range []string{"intraday", "stale_observation", "stale_receive", "wrong_source", "missing_bar", "duplicate_bar", "invalid_ohlc", "invalid_rules", "future_receive"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
target, c, h, now := screeningFixture()
|
|
switch name {
|
|
case "intraday":
|
|
now = now.Add(-2 * time.Hour)
|
|
target.Instrument.ReceivedAt = now
|
|
h.ReceivedAt = now
|
|
case "stale_observation":
|
|
target.Instrument.ObservedAt = now.Add(-25 * time.Hour)
|
|
case "stale_receive":
|
|
target.Instrument.ReceivedAt = now.Add(-11 * time.Minute)
|
|
case "wrong_source":
|
|
h.SourceVersion++
|
|
case "missing_bar":
|
|
h.Bars = h.Bars[:3]
|
|
case "duplicate_bar":
|
|
h.Bars[1].Date = h.Bars[0].Date
|
|
case "invalid_ohlc":
|
|
h.Bars[3].High = "19"
|
|
case "invalid_rules":
|
|
target.Version.Parameters.Screening.StopLossBPS = 100
|
|
case "future_receive":
|
|
h.ReceivedAt = now.Add(2 * time.Minute)
|
|
}
|
|
if _, draft, e := EvaluateScreening(target, c, h, now); e == nil || draft != nil {
|
|
t.Fatal("unsafe data generated candidate")
|
|
}
|
|
})
|
|
}
|
|
}
|