feat: 自动候选筛选与股票研究工作台
This commit is contained in:
@@ -60,16 +60,16 @@ func (r *NotificationRepository) Tasks(ctx context.Context) ([]model.Notificatio
|
||||
return list, mapError(rows.Err())
|
||||
}
|
||||
|
||||
const quoteColumns = `id,symbol,exchange,data_mode,price_minor,observed_at,version,created_at,updated_at`
|
||||
const quoteColumns = `id,symbol,exchange,data_mode,price_minor,observed_at,version,created_at,updated_at,source_id,source_version`
|
||||
|
||||
func scanQuote(row pgx.Row) (model.QuoteSnapshot, error) {
|
||||
var q model.QuoteSnapshot
|
||||
err := row.Scan(&q.ID, &q.Symbol, &q.Exchange, &q.DataMode, &q.PriceMinor, &q.ObservedAt, &q.Version, &q.CreatedAt, &q.UpdatedAt)
|
||||
err := row.Scan(&q.ID, &q.Symbol, &q.Exchange, &q.DataMode, &q.PriceMinor, &q.ObservedAt, &q.Version, &q.CreatedAt, &q.UpdatedAt, &q.SourceID, &q.SourceVersion)
|
||||
q.Price = model.FormatMoney(q.PriceMinor)
|
||||
return q, mapError(err)
|
||||
}
|
||||
func (r *NotificationRepository) Quotes(ctx context.Context) ([]model.QuoteSnapshot, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT `+quoteColumns+` FROM veyra_quote_snapshot ORDER BY updated_at DESC,id DESC LIMIT 100`)
|
||||
rows, err := r.pool.Query(ctx, `SELECT `+quoteColumns+` FROM veyra_quote_snapshot WHERE data_mode='simulation' ORDER BY updated_at DESC,id DESC LIMIT 100`)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
@@ -127,6 +127,19 @@ func processQuote(ctx context.Context, tx pgx.Tx, event model.OutboxEvent) error
|
||||
if current.Version != event.AggregateVersion || time.Since(current.ObservedAt) > time.Minute || current.ObservedAt.After(time.Now().Add(time.Minute)) {
|
||||
return nil
|
||||
}
|
||||
if current.DataMode == "public" {
|
||||
if current.SourceID == nil || current.SourceVersion == nil {
|
||||
return apperror.ErrUnavailable
|
||||
}
|
||||
var version int64
|
||||
var enabled bool
|
||||
if err := tx.QueryRow(ctx, `SELECT version,enabled FROM veyra_provider_config WHERE id=$1 FOR SHARE`, *current.SourceID).Scan(&version, &enabled); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
if !enabled || version != *current.SourceVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
rows, err := tx.Query(ctx, `SELECT p.id,p.candidate_revision_id,a.user_id FROM veyra_trade_plan p JOIN veyra_candidate_revision r ON r.id=p.candidate_revision_id JOIN veyra_account a ON a.id=p.account_id JOIN veyra_user u ON u.id=a.user_id WHERE p.status IN ('ACTIVE','PART_FILLED') AND u.status='ACTIVE' AND r.snapshot->>'symbol'=$1 AND r.snapshot->>'exchange'=$2 ORDER BY r.candidate_id,p.id`, current.Symbol, current.Exchange)
|
||||
if err != nil {
|
||||
return mapError(err)
|
||||
@@ -176,7 +189,11 @@ func processQuote(ctx context.Context, tx pgx.Tx, event model.OutboxEvent) error
|
||||
if tag.RowsAffected() == 0 {
|
||||
continue
|
||||
}
|
||||
body := fmt.Sprintf("模拟价格 ¥%s 进入 %s 的参考区间。观察时间 %s。请打开计划核对当前状态;此提醒不代表成交。", current.Price, publication.Snapshot.Name, current.ObservedAt.Format(time.RFC3339))
|
||||
label := "模拟价格"
|
||||
if current.DataMode == "public" {
|
||||
label = "公开行情价格"
|
||||
}
|
||||
body := fmt.Sprintf("%s ¥%s 进入 %s 的参考区间。观察时间 %s。请打开计划核对当前状态;此提醒不代表成交。", label, current.Price, publication.Snapshot.Name, current.ObservedAt.Format(time.RFC3339))
|
||||
if err := insertNotification(ctx, tx, target.user, event.ID, &target.plan, "模拟计划条件已满足", body); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,7 +245,7 @@ func (r *NotificationRepository) ProcessNext(ctx context.Context) (bool, error)
|
||||
var event model.OutboxEvent
|
||||
err = tx.QueryRow(ctx, `SELECT id,aggregate_type,aggregate_id,aggregate_version,event_type,payload,created_at,processed_at FROM veyra_outbox_event WHERE processed_at IS NULL ORDER BY created_at,id LIMIT 1 FOR UPDATE SKIP LOCKED`).Scan(&event.ID, &event.AggregateType, &event.AggregateID, &event.AggregateVersion, &event.EventType, &event.Payload, &event.CreatedAt, &event.ProcessedAt)
|
||||
if err == pgx.ErrNoRows {
|
||||
rows, err := tx.Query(ctx, `SELECT DISTINCT a.user_id FROM veyra_trade_plan p JOIN veyra_account a ON a.id=p.account_id JOIN veyra_candidate_revision r ON r.id=p.candidate_revision_id JOIN veyra_candidate c ON c.id=r.candidate_id WHERE p.status IN ('ACTIVE','PART_FILLED') AND (p.valid_until<=now() OR r.valid_until<=now() OR r.status<>'PUBLISHED' OR c.published_revision_id IS DISTINCT FROM r.id) LIMIT 100`)
|
||||
rows, err := tx.Query(ctx, `SELECT DISTINCT a.user_id FROM veyra_trade_plan p JOIN veyra_account a ON a.id=p.account_id JOIN veyra_candidate_revision r ON r.id=p.candidate_revision_id JOIN veyra_candidate c ON c.id=r.candidate_id WHERE p.status IN ('ACTIVE','PART_FILLED') AND (p.valid_until<=now() OR r.valid_until<=now() OR r.status<>'PUBLISHED' OR c.published_revision_id IS DISTINCT FROM r.id OR NOT (`+screeningEligibility+`)) LIMIT 100`)
|
||||
if err != nil {
|
||||
return false, mapError(err)
|
||||
}
|
||||
@@ -265,7 +282,7 @@ func (r *NotificationRepository) ProcessNext(ctx context.Context) (bool, error)
|
||||
var plan string
|
||||
err = tx.QueryRow(ctx, `SELECT a.user_id,p.id FROM veyra_trade_plan p JOIN veyra_account a ON a.id=p.account_id WHERE p.id=$1`, event.AggregateID).Scan(&user, &plan)
|
||||
if err == nil {
|
||||
err = insertNotification(ctx, tx, user, event.ID, &plan, "计划已失效", "对应候选已失效或计划已过期,保留预算已释放。已记录成交与持仓保留。")
|
||||
err = insertNotification(ctx, tx, user, event.ID, &plan, "计划已失效", "对应候选、策略版本已失效或计划已过期,保留预算已释放。已记录成交与持仓保留。")
|
||||
}
|
||||
case "candidate.published", "position.changed":
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user