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") } }) } }