Files
Veyra/internal/repository/screening.go
T

129 lines
7.4 KiB
Go

package repository
import (
"context"
"encoding/json"
"time"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type ScreeningRepository struct{ pool *pgxpool.Pool }
func NewScreeningRepository(pool *pgxpool.Pool) *ScreeningRepository {
return &ScreeningRepository{pool}
}
func (r *ScreeningRepository) Targets(ctx context.Context, c model.ProviderConfig) ([]model.ScreeningTarget, error) {
rows, err := r.pool.Query(ctx, `SELECT s.id,s.name,v.id,v.strategy_id,v.revision,v.algorithm,v.parameters,v.description,v.author_id,v.created_at,i.id,i.symbol,i.exchange,i.name,i.price_cents,i.observed_at,i.received_at,i.source_id,i.source_version
FROM veyra_strategy s JOIN veyra_strategy_version v ON v.id=s.active_version_id
CROSS JOIN LATERAL jsonb_array_elements(COALESCE(v.parameters->'screening'->'securities','[]'::jsonb)) security
JOIN veyra_instrument i ON i.symbol=security->>'symbol' AND i.exchange=security->>'exchange'
JOIN veyra_user u ON u.id=v.author_id AND u.role='ADMIN' AND u.status='ACTIVE'
WHERE s.status='ACTIVE' AND i.source_id=$1 AND i.source_version=$2 AND i.observed_at>=now()-interval '24 hours' AND i.observed_at<=now()+interval '1 minute' AND i.received_at>=now()-interval '10 minutes'
AND ((i.observed_at AT TIME ZONE 'Asia/Shanghai')::date < (now() AT TIME ZONE 'Asia/Shanghai')::date OR EXTRACT(hour FROM now() AT TIME ZONE 'Asia/Shanghai')>=16)
AND NOT EXISTS(SELECT 1 FROM veyra_strategy_screening x WHERE x.strategy_version_id=v.id AND x.symbol=i.symbol AND x.exchange=i.exchange AND x.bar_date=(i.observed_at AT TIME ZONE 'Asia/Shanghai')::date AND x.source_id=$1 AND x.source_version=$2)
ORDER BY s.updated_at,s.id,i.symbol LIMIT 2000`, c.ID, c.Version)
if err != nil {
return nil, mapError(err)
}
defer rows.Close()
targets := make([]model.ScreeningTarget, 0)
for rows.Next() {
var t model.ScreeningTarget
var raw []byte
v := &t.Version
i := &t.Instrument
if err := rows.Scan(&t.StrategyID, &t.StrategyName, &v.ID, &v.StrategyID, &v.Revision, &v.Algorithm, &raw, &v.Description, &v.AuthorID, &v.CreatedAt, &i.ID, &i.Symbol, &i.Exchange, &i.Name, &i.PriceCents, &i.ObservedAt, &i.ReceivedAt, &i.SourceID, &i.SourceVersion); err != nil {
return nil, mapError(err)
}
if json.Unmarshal(raw, &v.Parameters) != nil {
return nil, apperror.ErrUnavailable
}
targets = append(targets, t)
}
return targets, mapError(rows.Err())
}
func (r *ScreeningRepository) Record(ctx context.Context, t model.ScreeningTarget, run model.StrategyScreening, draft *model.CandidateDraftInput) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return mapError(err)
}
defer tx.Rollback(context.Background())
// Lock deployment before creating a candidate; pause/version changes cannot race publication.
var status string
var active *string
if err := tx.QueryRow(ctx, `SELECT status,active_version_id FROM veyra_strategy WHERE id=$1 FOR SHARE`, t.StrategyID).Scan(&status, &active); err != nil {
return mapError(err)
}
if status != "ACTIVE" || active == nil || *active != t.Version.ID {
return apperror.ErrConflict
}
var sourceVersion int64
var enabled bool
if err := tx.QueryRow(ctx, `SELECT version,enabled FROM veyra_provider_config WHERE id=$1 FOR SHARE`, run.SourceID).Scan(&sourceVersion, &enabled); err != nil {
return mapError(err)
}
if !enabled || sourceVersion != run.SourceVersion {
return apperror.ErrConflict
}
var actorValid bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM veyra_user WHERE id=$1 AND status='ACTIVE' AND role='ADMIN')`, t.Version.AuthorID).Scan(&actorValid); err != nil {
return mapError(err)
}
if !actorValid {
return apperror.ErrConflict
}
requestID := uuid.NewString()
var revisionID *string
if draft != nil {
if err := model.ValidateDraft(*draft, time.Now().UTC()); err != nil {
return err
}
candidate, err := createCandidate(ctx, tx, *draft, t.Version.AuthorID, requestID, "", 0)
if err != nil {
return err
}
// Machine submission leaves the approval decision to an administrator.
if _, err := tx.Exec(ctx, `UPDATE veyra_candidate_revision SET status='PENDING_REVIEW',version=version+1 WHERE id=$1`, candidate.ID); err != nil {
return mapError(err)
}
if _, err := tx.Exec(ctx, `INSERT INTO veyra_candidate_review(id,revision_id,reviewer_id,decision,input_version,reason) VALUES($1,$2,$3,'SUBMIT',1,'自动日线筛选提交,等待人工审核')`, uuid.NewString(), candidate.ID, t.Version.AuthorID); err != nil {
return mapError(err)
}
revisionID = &candidate.ID
}
_, err = tx.Exec(ctx, `INSERT INTO veyra_strategy_screening(id,strategy_version_id,symbol,exchange,bar_date,source_id,source_version,source_adapter,observed_at,received_at,input_hash,input,status,candidate_revision_id) VALUES($1,$2,$3,$4,$5::date,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, uuid.NewString(), t.Version.ID, run.Symbol, run.Exchange, run.BarDate, run.SourceID, run.SourceVersion, run.SourceAdapter, run.ObservedAt, run.ReceivedAt, run.InputHash, run.Input, run.Status, revisionID)
if err != nil {
return mapError(err)
} // Unique key rolls back candidate/audit too, including concurrent workers.
if err := appendAudit(ctx, tx, t.Version.AuthorID, "STRATEGY_SCREENING_COMPLETED", "strategy_version", t.Version.ID, requestID, map[string]any{"symbol": run.Symbol, "exchange": run.Exchange, "bar_date": run.BarDate, "status": run.Status, "input_hash": run.InputHash, "candidate_revision_id": revisionID}); err != nil {
return err
}
return mapError(tx.Commit(ctx))
}
func (r *ScreeningRepository) List(ctx context.Context) ([]model.StrategyScreening, error) {
rows, err := r.pool.Query(ctx, `SELECT x.id,x.strategy_version_id,s.name,v.revision,x.symbol,x.exchange,x.bar_date::text,x.source_id,x.source_version,x.source_adapter,x.observed_at,x.received_at,x.input_hash,x.status,x.candidate_revision_id,x.created_at FROM veyra_strategy_screening x JOIN veyra_strategy_version v ON v.id=x.strategy_version_id JOIN veyra_strategy s ON s.id=v.strategy_id ORDER BY x.created_at DESC,x.id DESC LIMIT 100`)
if err != nil {
return nil, mapError(err)
}
defer rows.Close()
list := make([]model.StrategyScreening, 0)
for rows.Next() {
var x model.StrategyScreening
if err := rows.Scan(&x.ID, &x.StrategyVersionID, &x.StrategyName, &x.StrategyRevision, &x.Symbol, &x.Exchange, &x.BarDate, &x.SourceID, &x.SourceVersion, &x.SourceAdapter, &x.ObservedAt, &x.ReceivedAt, &x.InputHash, &x.Status, &x.CandidateRevisionID, &x.CreatedAt); err != nil {
return nil, mapError(err)
}
list = append(list, x)
}
return list, mapError(rows.Err())
}
func (r *ScreeningRepository) Detail(ctx context.Context, id string) (model.StrategyScreening, error) {
var x model.StrategyScreening
err := r.pool.QueryRow(ctx, `SELECT x.id,x.strategy_version_id,s.name,v.revision,x.symbol,x.exchange,x.bar_date::text,x.source_id,x.source_version,x.source_adapter,x.observed_at,x.received_at,x.input_hash,x.status,x.candidate_revision_id,x.created_at,x.input FROM veyra_strategy_screening x JOIN veyra_strategy_version v ON v.id=x.strategy_version_id JOIN veyra_strategy s ON s.id=v.strategy_id WHERE x.id=$1`, id).Scan(&x.ID, &x.StrategyVersionID, &x.StrategyName, &x.StrategyRevision, &x.Symbol, &x.Exchange, &x.BarDate, &x.SourceID, &x.SourceVersion, &x.SourceAdapter, &x.ObservedAt, &x.ReceivedAt, &x.InputHash, &x.Status, &x.CandidateRevisionID, &x.CreatedAt, &x.Input)
return x, mapError(err)
}