统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user