feat: 自动候选筛选与股票研究工作台
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user