feat: 自动候选筛选与股票研究工作台

This commit is contained in:
2026-10-09 20:39:23 +08:00
parent 92329ddce2
commit 57882ff568
32 changed files with 1343 additions and 87 deletions
+17 -16
View File
@@ -45,22 +45,23 @@ type CandidateContent struct {
}
type CandidateRevision struct {
ID string `db:"id" json:"id"`
CandidateID string `db:"candidate_id" json:"candidate_id"`
Revision int `db:"revision" json:"revision"`
Version int64 `db:"version" json:"version"`
Status string `db:"status" json:"status"`
DataMode string `db:"data_mode" json:"data_mode"`
Snapshot CandidateContent `db:"snapshot" json:"content"`
AuthorID string `db:"author_id" json:"author_id"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
ValidUntil time.Time `db:"valid_until" json:"valid_until"`
PublishedAt *time.Time `db:"published_at" json:"published_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
IsLatest bool `json:"is_latest"`
WithdrawalReason string `json:"withdrawal_reason"`
IsActivePublication bool `json:"is_active_publication"`
Screening *StrategyScreening `json:"screening,omitempty"`
ID string `db:"id" json:"id"`
CandidateID string `db:"candidate_id" json:"candidate_id"`
Revision int `db:"revision" json:"revision"`
Version int64 `db:"version" json:"version"`
Status string `db:"status" json:"status"`
DataMode string `db:"data_mode" json:"data_mode"`
Snapshot CandidateContent `db:"snapshot" json:"content"`
AuthorID string `db:"author_id" json:"author_id"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
ValidUntil time.Time `db:"valid_until" json:"valid_until"`
PublishedAt *time.Time `db:"published_at" json:"published_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
IsLatest bool `json:"is_latest"`
WithdrawalReason string `json:"withdrawal_reason"`
IsActivePublication bool `json:"is_active_publication"`
}
func (CandidateRevision) TableName() string { return "veyra_candidate_revision" }
+12 -10
View File
@@ -16,16 +16,18 @@ type Notification struct {
func (Notification) TableName() string { return "veyra_notification" }
type QuoteSnapshot struct {
ID string `db:"id" json:"id"`
Symbol string `db:"symbol" json:"symbol"`
Exchange string `db:"exchange" json:"exchange"`
DataMode string `db:"data_mode" json:"data_mode"`
PriceMinor int64 `db:"price_minor" json:"-"`
Price string `json:"price"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
Version int64 `db:"version" json:"version"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
SourceID *string `db:"source_id" json:"source_id,omitempty"`
SourceVersion *int64 `db:"source_version" json:"source_version,omitempty"`
ID string `db:"id" json:"id"`
Symbol string `db:"symbol" json:"symbol"`
Exchange string `db:"exchange" json:"exchange"`
DataMode string `db:"data_mode" json:"data_mode"`
PriceMinor int64 `db:"price_minor" json:"-"`
Price string `json:"price"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
Version int64 `db:"version" json:"version"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (QuoteSnapshot) TableName() string { return "veyra_quote_snapshot" }
+79
View File
@@ -0,0 +1,79 @@
package model
import (
"encoding/json"
"time"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
)
// Screening rules belong to an immutable strategy version, and are opt-in.
type ScreeningSecurity struct {
Symbol string `json:"symbol"`
Exchange string `json:"exchange"`
}
type ScreeningParameters struct {
Securities []ScreeningSecurity `json:"securities"`
EntryBandBPS int64 `json:"entry_band_bps"`
StopLossBPS int64 `json:"stop_loss_bps"`
TargetBPS int64 `json:"target_bps"`
ValidHours int `json:"valid_hours"`
}
func ValidateScreening(p *ScreeningParameters) error {
if p == nil {
return nil
}
if len(p.Securities) < 1 || len(p.Securities) > 100 || p.EntryBandBPS < 0 || p.EntryBandBPS > 1000 || p.StopLossBPS <= p.EntryBandBPS || p.StopLossBPS > 5000 || p.TargetBPS <= p.EntryBandBPS || p.TargetBPS > 10000 || p.ValidHours < 1 || p.ValidHours > 168 {
return apperror.ErrInvalidInput
}
seen := map[ScreeningSecurity]bool{}
for _, v := range p.Securities {
if !symbolPattern.MatchString(v.Symbol) || (v.Exchange != "SSE" && v.Exchange != "SZSE") || seen[v] {
return apperror.ErrInvalidInput
}
seen[v] = true
}
return nil
}
type ScreeningTarget struct {
StrategyID string
StrategyName string
Version StrategyVersion
Instrument Instrument
}
type StrategyScreening struct {
ID string `db:"id" json:"id"`
StrategyVersionID string `db:"strategy_version_id" json:"strategy_version_id"`
StrategyName string `json:"strategy_name"`
StrategyRevision int `json:"strategy_revision"`
Symbol string `db:"symbol" json:"symbol"`
Exchange string `db:"exchange" json:"exchange"`
BarDate string `db:"bar_date" json:"bar_date"`
SourceID string `db:"source_id" json:"source_id"`
SourceVersion int64 `db:"source_version" json:"source_version"`
SourceAdapter string `db:"source_adapter" json:"source_adapter"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
ReceivedAt time.Time `db:"received_at" json:"received_at"`
InputHash string `db:"input_hash" json:"input_hash"`
Input json.RawMessage `db:"input" json:"input,omitempty"`
Status string `db:"status" json:"status"`
CandidateRevisionID *string `db:"candidate_revision_id" json:"candidate_revision_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (StrategyScreening) TableName() string { return "veyra_strategy_screening" }
// Worker state is process-local; durable results remain in StrategyScreening.
type ScreeningWorkerState struct {
State string `json:"state"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
Targets int `json:"targets"`
Completed int `json:"completed"`
Generated int `json:"generated"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
Message string `json:"message"`
}
+6 -5
View File
@@ -3,11 +3,12 @@ package model
import "time"
type StrategyParameters struct {
ShortWindow int `json:"short_window"`
LongWindow int `json:"long_window"`
FeeBPS int `json:"fee_bps"`
SlippageBPS int `json:"slippage_bps"`
LotSize int64 `json:"lot_size"`
Screening *ScreeningParameters `json:"screening,omitempty"`
ShortWindow int `json:"short_window"`
LongWindow int `json:"long_window"`
FeeBPS int `json:"fee_bps"`
SlippageBPS int `json:"slippage_bps"`
LotSize int64 `json:"lot_size"`
}
type Strategy struct {
ID string `db:"id" json:"id"`