package service import ( "context" "encoding/json" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "github.com/google/uuid" "go.uber.org/zap" "strings" "time" "unicode/utf8" ) type BacktestRepository interface { Create(context.Context, model.BacktestInput, string, string) (model.BacktestRun, error) List(context.Context) ([]model.BacktestRun, error) Detail(context.Context, string) (model.BacktestRun, error) Claim(context.Context) (model.BacktestRun, error) Finish(context.Context, model.BacktestRun, json.RawMessage, string) error } type ResearchEngine interface { Execute(context.Context, json.RawMessage) (json.RawMessage, error) } type BacktestService struct { repo BacktestRepository engine ResearchEngine development bool market *MarketService } func NewBacktestService(repo BacktestRepository, engine ResearchEngine, development bool, markets ...*MarketService) *BacktestService { s := &BacktestService{repo: repo, engine: engine, development: development} if len(markets) > 0 { s.market = markets[0] } return s } func (s *BacktestService) authorize(u model.User) error { if u.Role != "ADMIN" || u.Status != "ACTIVE" { return apperror.ErrForbidden } if s == nil || s.repo == nil || !s.development { return apperror.ErrUnavailable } return nil } func (s *BacktestService) List(ctx context.Context, u model.User) ([]model.BacktestRun, error) { if e := s.authorize(u); e != nil { return nil, e } return s.repo.List(ctx) } func (s *BacktestService) Detail(ctx context.Context, u model.User, id string) (model.BacktestRun, error) { if e := s.authorize(u); e != nil { return model.BacktestRun{}, e } if _, e := uuid.Parse(id); e != nil { return model.BacktestRun{}, apperror.ErrInvalidInput } return s.repo.Detail(ctx, id) } func (s *BacktestService) Create(ctx context.Context, u model.User, in model.BacktestInput, requestID string) (model.BacktestRun, error) { in.DataOrigin = "manual" in.SourceVersion = 0 in.DataReceivedAt = nil return s.createValidated(ctx, u, in, requestID) } func (s *BacktestService) createValidated(ctx context.Context, u model.User, in model.BacktestInput, requestID string) (model.BacktestRun, error) { if e := s.authorize(u); e != nil { return model.BacktestRun{}, e } if _, e := uuid.Parse(in.StrategyVersionID); e != nil { return model.BacktestRun{}, apperror.ErrInvalidInput } in.DatasetName = strings.TrimSpace(in.DatasetName) cash, e := model.ParseMoney(in.InitialCash) if e != nil || cash < 10000 || cash > 100000000000 || in.DatasetName == "" || utf8.RuneCountInString(in.DatasetName) > 120 || len(in.Symbol) != 6 || len(in.Bars) < 4 || len(in.Bars) > 1000 { return model.BacktestRun{}, apperror.ErrInvalidInput } for _, r := range in.Symbol { if r < '0' || r > '9' { return model.BacktestRun{}, apperror.ErrInvalidInput } } last := "" for _, b := range in.Bars { date, e := time.Parse("2006-01-02", b.Date) if e != nil || b.Date <= last || date.After(time.Now()) { return model.BacktestRun{}, apperror.ErrInvalidInput } last = b.Date o, e1 := model.ParseMoney(b.Open) h, e2 := model.ParseMoney(b.High) l, e3 := model.ParseMoney(b.Low) c, e4 := model.ParseMoney(b.Close) if e1 != nil || e2 != nil || e3 != nil || e4 != nil || l <= 0 || h > 100000000 || l > o || l > c || h < o || h < c { return model.BacktestRun{}, apperror.ErrInvalidInput } } in.InitialCash = model.FormatMoney(cash) return s.repo.Create(ctx, in, u.ID, requestID) } func (s *BacktestService) ProcessNext(ctx context.Context) (bool, error) { job, e := s.repo.Claim(ctx) if e == apperror.ErrNotFound { return false, nil } if e != nil { return false, e } result, e := s.engine.Execute(ctx, job.Input) if ctx.Err() != nil { return true, ctx.Err() } // The lease recovers interrupted work after restart. failure := "" if e != nil { failure = "Python 执行失败,请检查解释器安装和输入;本任务未生成研究结果。" result = nil } return true, s.repo.Finish(ctx, job, result, failure) } func (s *BacktestService) Run(ctx context.Context, logger *zap.Logger) { if s == nil || s.repo == nil || s.engine == nil || !s.development { return } timer := time.NewTicker(2 * time.Second) defer timer.Stop() for { select { case <-ctx.Done(): return case <-timer.C: if _, e := s.ProcessNext(ctx); e != nil && ctx.Err() == nil { logger.Error("backtest worker failed", zap.Error(e)) } } } } func (s *BacktestService) CreateMarket(ctx context.Context, u model.User, version, symbol, exchange, start, end, cash, requestID string) (model.BacktestRun, error) { if e := s.authorize(u); e != nil { return model.BacktestRun{}, e } if s.market == nil { return model.BacktestRun{}, apperror.ErrUnavailable } history, e := s.market.History(ctx, u, symbol, exchange, start, end) if e != nil { return model.BacktestRun{}, e } input := model.BacktestInput{StrategyVersionID: version, Symbol: symbol, InitialCash: cash, Bars: history.Bars, DatasetName: history.Source + " 日线 · 不复权 · " + start + " 至 " + end, DataOrigin: history.Source, SourceVersion: history.SourceVersion, DataReceivedAt: &history.ReceivedAt} return s.createValidated(ctx, u, input, requestID) }