Files
Veyra/internal/repository/strategy.go
T
Blizzard 92329ddce2
Checks / verify (push) Failing after 9m52s
feat: 完成模拟交易闭环、App 与 A 股行情接入
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
2026-10-08 22:58:52 +08:00

154 lines
5.9 KiB
Go

package repository
import (
"context"
"encoding/json"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"git.sundynix.cn/Blizzard/Veyra/internal/service"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type StrategyRepository struct{ pool *pgxpool.Pool }
func NewStrategyRepository(pool *pgxpool.Pool) *StrategyRepository { return &StrategyRepository{pool} }
const strategyColumns = `id,code,name,status,latest_revision,active_version_id,version,created_at,updated_at`
func scanStrategy(row pgx.Row) (model.Strategy, error) {
var s model.Strategy
err := row.Scan(&s.ID, &s.Code, &s.Name, &s.Status, &s.LatestRevision, &s.ActiveVersionID, &s.Version, &s.CreatedAt, &s.UpdatedAt)
return s, mapError(err)
}
func (r *StrategyRepository) List(ctx context.Context) ([]model.Strategy, error) {
rows, err := r.pool.Query(ctx, `SELECT `+strategyColumns+` FROM veyra_strategy ORDER BY updated_at DESC,id DESC LIMIT 100`)
if err != nil {
return nil, mapError(err)
}
defer rows.Close()
list := make([]model.Strategy, 0)
for rows.Next() {
s, err := scanStrategy(rows)
if err != nil {
return nil, err
}
list = append(list, s)
}
return list, mapError(rows.Err())
}
func (r *StrategyRepository) Detail(ctx context.Context, id string) (model.StrategyDetail, error) {
tx, err := r.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
defer tx.Rollback(context.Background())
return strategyDetail(ctx, tx, id)
}
func strategyDetail(ctx context.Context, tx pgx.Tx, id string) (model.StrategyDetail, error) {
s, err := scanStrategy(tx.QueryRow(ctx, `SELECT `+strategyColumns+` FROM veyra_strategy WHERE id=$1`, id))
detail := model.StrategyDetail{Strategy: s, Versions: make([]model.StrategyVersion, 0)}
if err != nil {
return detail, err
}
rows, err := tx.Query(ctx, `SELECT id,strategy_id,revision,algorithm,parameters,description,author_id,created_at FROM veyra_strategy_version WHERE strategy_id=$1 ORDER BY revision DESC`, id)
if err != nil {
return detail, mapError(err)
}
defer rows.Close()
for rows.Next() {
var v model.StrategyVersion
var parameters []byte
if err := rows.Scan(&v.ID, &v.StrategyID, &v.Revision, &v.Algorithm, &parameters, &v.Description, &v.AuthorID, &v.CreatedAt); err != nil {
return detail, mapError(err)
}
if json.Unmarshal(parameters, &v.Parameters) != nil {
return detail, apperror.ErrUnavailable
}
detail.Versions = append(detail.Versions, v)
}
return detail, mapError(rows.Err())
}
func (r *StrategyRepository) Save(ctx context.Context, id string, input service.StrategyInput, actor, requestID string) (model.StrategyDetail, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
defer tx.Rollback(context.Background())
revision := 1
if id == "" {
id = uuid.NewString()
_, err = tx.Exec(ctx, `INSERT INTO veyra_strategy(id,code,name) VALUES($1,$2,$3)`, id, input.Code, input.Name)
} else {
s, e := scanStrategy(tx.QueryRow(ctx, `SELECT `+strategyColumns+` FROM veyra_strategy WHERE id=$1 FOR UPDATE`, id))
if e != nil {
return model.StrategyDetail{}, e
}
if s.Version != input.ExpectedVersion || s.Code != input.Code {
return model.StrategyDetail{}, apperror.ErrConflict
}
revision = s.LatestRevision + 1
_, err = tx.Exec(ctx, `UPDATE veyra_strategy SET name=$2,latest_revision=$3,version=version+1,updated_at=now() WHERE id=$1`, id, input.Name, revision)
}
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
parameters, err := json.Marshal(input.Parameters)
if err != nil {
return model.StrategyDetail{}, apperror.ErrInvalidInput
}
versionID := uuid.NewString()
_, err = tx.Exec(ctx, `INSERT INTO veyra_strategy_version(id,strategy_id,revision,algorithm,parameters,description,author_id) VALUES($1,$2,$3,'sma_cross',$4,$5,$6)`, versionID, id, revision, parameters, input.Description, actor)
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
if err := appendAudit(ctx, tx, actor, "STRATEGY_VERSION_CREATED", "strategy", id, requestID, map[string]any{"revision": revision, "version_id": versionID}); err != nil {
return model.StrategyDetail{}, err
}
detail, err := strategyDetail(ctx, tx, id)
if err != nil {
return detail, err
}
return detail, mapError(tx.Commit(ctx))
}
func (r *StrategyRepository) Deploy(ctx context.Context, id, versionID, status string, expected int64, reason, actor, requestID string) (model.StrategyDetail, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
defer tx.Rollback(context.Background())
s, err := scanStrategy(tx.QueryRow(ctx, `SELECT `+strategyColumns+` FROM veyra_strategy WHERE id=$1 FOR UPDATE`, id))
if err != nil {
return model.StrategyDetail{}, err
}
if s.Version != expected {
return model.StrategyDetail{}, apperror.ErrConflict
}
var selected *string
if status == "ACTIVE" {
var exists bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM veyra_strategy_version WHERE id=$1 AND strategy_id=$2)`, versionID, id).Scan(&exists); err != nil {
return model.StrategyDetail{}, mapError(err)
}
if !exists {
return model.StrategyDetail{}, apperror.ErrNotFound
}
selected = &versionID
} else {
selected = s.ActiveVersionID
}
_, err = tx.Exec(ctx, `UPDATE veyra_strategy SET status=$2,active_version_id=$3,version=version+1,updated_at=now() WHERE id=$1`, id, status, selected)
if err != nil {
return model.StrategyDetail{}, mapError(err)
}
if err := appendAudit(ctx, tx, actor, "STRATEGY_DEPLOYMENT_CHANGED", "strategy", id, requestID, map[string]any{"status": status, "version_id": selected, "reason": reason}); err != nil {
return model.StrategyDetail{}, err
}
detail, err := strategyDetail(ctx, tx, id)
if err != nil {
return detail, err
}
return detail, mapError(tx.Commit(ctx))
}