feat: 完成模拟交易闭环、App 与 A 股行情接入
Checks / verify (push) Failing after 9m52s

统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
2026-10-08 22:58:52 +08:00
parent bffdb3858b
commit 92329ddce2
145 changed files with 18593 additions and 587 deletions
+42
View File
@@ -0,0 +1,42 @@
package model
import (
"encoding/json"
"time"
)
type HistoricalBar struct {
Date string `json:"date"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Close string `json:"close"`
}
type BacktestInput struct {
DataOrigin string `json:"data_origin"`
SourceVersion int64 `json:"source_version"`
DataReceivedAt *time.Time `json:"data_received_at"`
StrategyVersionID string `json:"strategy_version_id"`
DatasetName string `json:"dataset_name"`
Symbol string `json:"symbol"`
InitialCash string `json:"initial_cash"`
Bars []HistoricalBar `json:"bars"`
}
type ResearchInput struct {
BacktestInput
Parameters StrategyParameters `json:"parameters"`
}
type BacktestRun struct {
ID string `db:"id" json:"id"`
StrategyVersionID string `db:"strategy_version_id" json:"strategy_version_id"`
Status string `db:"status" json:"status"`
Input json.RawMessage `db:"input" json:"input,omitempty"`
InputHash string `db:"input_hash" json:"input_hash"`
Result json.RawMessage `db:"result" json:"result"`
Failure string `db:"failure" json:"failure"`
LeaseID *string `db:"lease_id" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
FinishedAt *time.Time `db:"finished_at" json:"finished_at"`
}
func (BacktestRun) TableName() string { return "veyra_backtest_run" }
+183
View File
@@ -0,0 +1,183 @@
package model
import (
"encoding/json"
"net/url"
"regexp"
"strings"
"time"
"unicode/utf8"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
)
type Candidate struct {
ID string `db:"id" json:"id"`
LatestRevision int `db:"latest_revision" json:"latest_revision"`
PublishedRevisionID *string `db:"published_revision_id" json:"published_revision_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (Candidate) TableName() string { return "veyra_candidate" }
type CandidateEvidence struct {
SourceURL string `json:"source_url"`
Locator string `json:"locator"`
Summary string `json:"summary"`
}
type CandidateReferencePlan struct {
EntryLow string `json:"entry_low"`
EntryHigh string `json:"entry_high"`
StopLoss string `json:"stop_loss"`
TargetPrice string `json:"target_price"`
}
type CandidateContent struct {
Symbol string `json:"symbol"`
Exchange string `json:"exchange"`
Name string `json:"name"`
StrategyCode string `json:"strategy_code"`
Title string `json:"title"`
Thesis string `json:"thesis"`
Evidence []CandidateEvidence `json:"evidence"`
CounterEvidence string `json:"counter_evidence"`
Invalidation string `json:"invalidation"`
ReferencePlan CandidateReferencePlan `json:"reference_plan"`
}
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"`
}
func (CandidateRevision) TableName() string { return "veyra_candidate_revision" }
type CandidateReview struct {
ID string `db:"id" json:"id"`
RevisionID string `db:"revision_id" json:"revision_id"`
ReviewerID string `db:"reviewer_id" json:"reviewer_id"`
Decision string `db:"decision" json:"decision"`
InputVersion int64 `db:"input_version" json:"input_version"`
Reason string `db:"reason" json:"reason"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (CandidateReview) TableName() string { return "veyra_candidate_review" }
type CandidateDraftInput struct {
Content CandidateContent `json:"content"`
ObservedAt time.Time `json:"observed_at"`
ValidUntil time.Time `json:"valid_until"`
}
type CandidateRevisionInput struct {
CandidateDraftInput
ExpectedVersion int64 `json:"expected_version"`
}
type CandidateActionInput struct {
ExpectedVersion int64 `json:"expected_version"`
Reason string `json:"reason"`
}
type CandidateDetail struct {
CandidateRevision
Reviews []CandidateReview `json:"reviews"`
}
var symbolPattern = regexp.MustCompile(`^[0-9]{6}$`)
func textValid(s string, max int) bool {
return strings.TrimSpace(s) != "" && utf8.RuneCountInString(s) <= max
}
func ValidateDraft(input CandidateDraftInput, now time.Time) error {
c := input.Content
if !symbolPattern.MatchString(c.Symbol) || c.Exchange != "SSE" && c.Exchange != "SZSE" || !textValid(c.Name, 80) || !textValid(c.Title, 120) || !textValid(c.Thesis, 2000) || !textValid(c.CounterEvidence, 1000) || !textValid(c.Invalidation, 1000) {
return apperror.ErrInvalidInput
}
switch c.StrategyCode {
case "trend_following", "pullback", "event_driven":
default:
return apperror.ErrInvalidInput
}
if len(c.Evidence) < 1 || len(c.Evidence) > 8 {
return apperror.ErrInvalidInput
}
for _, e := range c.Evidence {
u, err := url.Parse(e.SourceURL)
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || len(e.SourceURL) > 2048 || !textValid(e.Locator, 200) || !textValid(e.Summary, 1000) {
return apperror.ErrInvalidInput
}
}
low, e1 := ParseMoney(c.ReferencePlan.EntryLow)
high, e2 := ParseMoney(c.ReferencePlan.EntryHigh)
stop, e3 := ParseMoney(c.ReferencePlan.StopLoss)
target, e4 := ParseMoney(c.ReferencePlan.TargetPrice)
if e1 != nil || e2 != nil || e3 != nil || e4 != nil || stop <= 0 || stop >= low || low > high || target <= high {
return apperror.ErrInvalidInput
}
if input.ObservedAt.IsZero() || input.ObservedAt.After(now.Add(time.Minute)) || input.ObservedAt.Before(now.Add(-24*time.Hour)) || !input.ValidUntil.After(now) || input.ValidUntil.After(now.Add(7*24*time.Hour)) || !input.ValidUntil.After(input.ObservedAt) {
return apperror.ErrInvalidInput
}
encoded, err := json.Marshal(c)
if err != nil || len(encoded) > 12<<10 {
return apperror.ErrInvalidInput
}
return nil
}
// NextStatus is the shared state rule; repositories recheck it under the aggregate lock.
func NextStatus(r CandidateRevision, action string, now time.Time) (string, error) {
if action == "WITHDRAW" {
if r.Status != "PUBLISHED" || !r.IsActivePublication {
return "", apperror.ErrConflict
}
return "WITHDRAWN", nil
}
if !r.IsLatest {
return "", apperror.ErrConflict
}
switch action {
case "SUBMIT":
if r.Status != "DRAFT" {
return "", apperror.ErrConflict
}
if err := ValidateDraft(CandidateDraftInput{Content: r.Snapshot, ObservedAt: r.ObservedAt, ValidUntil: r.ValidUntil}, now); err != nil {
return "", apperror.ErrInvalidInput
}
return "PENDING_REVIEW", nil
case "PUBLISH":
if r.Status != "PENDING_REVIEW" {
return "", apperror.ErrConflict
}
if err := ValidateDraft(CandidateDraftInput{Content: r.Snapshot, ObservedAt: r.ObservedAt, ValidUntil: r.ValidUntil}, now); err != nil {
return "", apperror.ErrInvalidInput
}
return "PUBLISHED", nil
case "RETURN":
if r.Status != "PENDING_REVIEW" {
return "", apperror.ErrConflict
}
return "RETURNED", nil
default:
return "", apperror.ErrInvalidInput
}
}
func ValidateReason(reason string) error {
if !textValid(reason, 1000) {
return apperror.ErrInvalidInput
}
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package model
import "time"
type Instrument struct {
ID string `db:"id" json:"id"`
Symbol string `db:"symbol" json:"symbol"`
Exchange string `db:"exchange" json:"exchange"`
Name string `db:"name" json:"name"`
Price string `json:"price"`
PriceCents int64 `db:"price_cents" json:"-"`
ObservedAt time.Time `db:"observed_at" json:"observed_at"`
ReceivedAt time.Time `db:"received_at" json:"received_at"`
SourceID string `db:"source_id" json:"source_id"`
SourceVersion int64 `db:"source_version" json:"source_version"`
}
func (Instrument) TableName() string { return "veyra_instrument" }
type MarketSync struct {
ID string `db:"id" json:"id"`
SourceID string `db:"source_id" json:"source_id"`
SourceVersion int64 `db:"source_version" json:"source_version"`
InstrumentCount int `db:"instrument_count" json:"instrument_count"`
Status string `db:"status" json:"status"`
Message string `db:"message" json:"message"`
StartedAt time.Time `db:"started_at" json:"started_at"`
FinishedAt time.Time `db:"finished_at" json:"finished_at"`
}
func (MarketSync) TableName() string { return "veyra_market_sync" }
type MarketHistory struct {
Symbol string `json:"symbol"`
Exchange string `json:"exchange"`
Name string `json:"name"`
Source string `json:"source"`
SourceVersion int64 `json:"source_version"`
Adjustment string `json:"adjustment"`
ReceivedAt time.Time `json:"received_at"`
Bars []HistoricalBar `json:"bars"`
}
+9 -1
View File
@@ -33,4 +33,12 @@ func ParseMoney(value string) (int64, error) {
return amount, nil
}
func FormatMoney(minor int64) string { return fmt.Sprintf("%d.%02d", minor/100, minor%100) }
func FormatMoney(minor int64) string {
sign := ""
absolute := uint64(minor)
if minor < 0 {
sign = "-"
absolute = uint64(-(minor + 1)) + 1
}
return fmt.Sprintf("%s%d.%02d", sign, absolute/100, absolute%100)
}
+11
View File
@@ -19,3 +19,14 @@ func TestExactMoney(t *testing.T) {
}
}
}
func TestSignedLedgerMoney(t *testing.T) {
for _, tc := range []struct {
minor int64
want string
}{{-1, "-0.01"}, {-100, "-1.00"}, {-1005, "-10.05"}, {0, "0.00"}, {-9223372036854775808, "-92233720368547758.08"}} {
if got := FormatMoney(tc.minor); got != tc.want {
t.Fatalf("got %s want %s", got, tc.want)
}
}
}
+46
View File
@@ -0,0 +1,46 @@
package model
import "time"
type Notification struct {
ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"-"`
EventID string `db:"event_id" json:"event_id"`
PlanID *string `db:"plan_id" json:"plan_id"`
Title string `db:"title" json:"title"`
Body string `db:"body" json:"body"`
ReadAt *time.Time `db:"read_at" json:"read_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
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"`
}
func (QuoteSnapshot) TableName() string { return "veyra_quote_snapshot" }
type PlanTrigger struct {
ID string `db:"id"`
PlanID string `db:"plan_id"`
QuoteSnapshotID string `db:"quote_snapshot_id"`
QuoteVersion int64 `db:"quote_version"`
TriggeredAt time.Time `db:"triggered_at"`
}
func (PlanTrigger) TableName() string { return "veyra_plan_trigger" }
type NotificationTask struct {
OutboxEvent
RecipientCount int `json:"recipient_count"`
}
+19
View File
@@ -0,0 +1,19 @@
package model
import (
"encoding/json"
"time"
)
type OutboxEvent struct {
ID string `db:"id" json:"id"`
AggregateType string `db:"aggregate_type" json:"aggregate_type"`
AggregateID string `db:"aggregate_id" json:"aggregate_id"`
AggregateVersion int64 `db:"aggregate_version" json:"aggregate_version"`
EventType string `db:"event_type" json:"event_type"`
Payload json.RawMessage `db:"payload" json:"payload"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ProcessedAt *time.Time `db:"processed_at" json:"processed_at"`
}
func (OutboxEvent) TableName() string { return "veyra_outbox_event" }
+59
View File
@@ -0,0 +1,59 @@
package model
import (
"encoding/json"
"time"
)
type ProviderConfig struct {
QuoteBaseURL string `db:"quote_base_url" json:"quote_base_url"`
MarketAdapter string `db:"market_adapter" json:"market_adapter"`
HistoryBaseURL string `db:"history_base_url" json:"history_base_url"`
ID string `db:"id" json:"id"`
ConfigKey string `db:"config_key" json:"config_key"`
Name string `db:"name" json:"name"`
BaseURL string `db:"base_url" json:"base_url"`
ModelName string `db:"model_name" json:"model_name"`
WireAPI string `db:"wire_api" json:"wire_api"`
ModelCatalogURL string `db:"model_catalog_url" json:"model_catalog_url"`
DisableResponseStorage bool `db:"disable_response_storage" json:"disable_response_storage"`
RequiresOpenAIAuth bool `db:"requires_openai_auth" json:"requires_openai_auth"`
Enabled bool `db:"enabled" json:"enabled"`
TimeoutSeconds int `db:"timeout_seconds" json:"timeout_seconds"`
EncryptedAPIKey []byte `db:"encrypted_api_key" json:"-"`
HasAPIKey bool `json:"has_api_key"`
Version int64 `db:"version" json:"version"`
LastTestStatus string `db:"last_test_status" json:"last_test_status"`
LastTestedAt *time.Time `db:"last_tested_at" json:"last_tested_at"`
LastTestMessage string `db:"last_test_message" json:"last_test_message"`
UpdatedBy string `db:"updated_by" json:"updated_by"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (ProviderConfig) TableName() string { return "veyra_provider_config" }
type ProviderConfigHistory struct {
ID string `db:"id" json:"id"`
ConfigID string `db:"config_id" json:"config_id"`
Version int64 `db:"version" json:"version"`
Snapshot json.RawMessage `db:"snapshot" json:"snapshot"`
EncryptedAPIKey []byte `db:"encrypted_api_key" json:"-"`
ActorID string `db:"actor_id" json:"actor_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (ProviderConfigHistory) TableName() string { return "veyra_provider_config_history" }
type AuditEvent struct {
ID string `db:"id" json:"id"`
ActorID string `db:"actor_id" json:"actor_id"`
Action string `db:"action" json:"action"`
ObjectType string `db:"object_type" json:"object_type"`
ObjectID string `db:"object_id" json:"object_id"`
RequestID string `db:"request_id" json:"request_id"`
Details json.RawMessage `db:"details" json:"details"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (AuditEvent) TableName() string { return "veyra_audit_event" }
+42
View File
@@ -0,0 +1,42 @@
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"`
}
type Strategy struct {
ID string `db:"id" json:"id"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
Status string `db:"status" json:"status"`
LatestRevision int `db:"latest_revision" json:"latest_revision"`
ActiveVersionID *string `db:"active_version_id" json:"active_version_id"`
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 (Strategy) TableName() string { return "veyra_strategy" }
type StrategyVersion struct {
ID string `db:"id" json:"id"`
StrategyID string `db:"strategy_id" json:"strategy_id"`
Revision int `db:"revision" json:"revision"`
Algorithm string `db:"algorithm" json:"algorithm"`
Parameters StrategyParameters `db:"parameters" json:"parameters"`
Description string `db:"description" json:"description"`
AuthorID string `db:"author_id" json:"author_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (StrategyVersion) TableName() string { return "veyra_strategy_version" }
type StrategyDetail struct {
Strategy
Versions []StrategyVersion `json:"versions"`
}
+93
View File
@@ -0,0 +1,93 @@
package model
import "time"
type TradeFill struct {
ID string `db:"id" json:"id"`
AccountID string `db:"account_id" json:"account_id"`
PlanID *string `db:"plan_id" json:"plan_id"`
IdempotencyKey string `db:"idempotency_key" json:"idempotency_key"`
InputHash string `db:"input_hash" json:"-"`
PostedSequence int64 `db:"posted_sequence" json:"-"`
Side string `db:"side" json:"side"`
Symbol string `db:"symbol" json:"symbol"`
Exchange string `db:"exchange" json:"exchange"`
Name string `db:"name" json:"name"`
Quantity int64 `db:"quantity" json:"quantity,string"`
PriceMinor int64 `db:"price_minor" json:"-"`
FeeMinor int64 `db:"fee_minor" json:"-"`
CashDeltaMinor int64 `db:"cash_delta_minor" json:"-"`
RealizedMinor int64 `db:"realized_minor" json:"-"`
Price string `json:"price"`
Fee string `json:"fee"`
CashDelta string `json:"cash_delta"`
RealizedPnL string `json:"realized_pnl"`
OccurredAt time.Time `db:"occurred_at" json:"occurred_at"`
SellableAt *time.Time `db:"sellable_at" json:"sellable_at"`
Note string `db:"note" json:"note"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Reversed bool `json:"reversed"`
}
func (TradeFill) TableName() string { return "veyra_trade_fill" }
type TradeReversal struct {
ID string `db:"id" json:"id"`
FillID string `db:"fill_id" json:"fill_id"`
ActorID string `db:"actor_id" json:"actor_id"`
Reason string `db:"reason" json:"reason"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (TradeReversal) TableName() string { return "veyra_trade_reversal" }
type PositionLot struct {
ID string `db:"id"`
AccountID string `db:"account_id"`
BuyFillID string `db:"buy_fill_id"`
Symbol string `db:"symbol"`
Exchange string `db:"exchange"`
RemainingQuantity int64 `db:"remaining_quantity"`
RemainingCostMinor int64 `db:"remaining_cost_minor"`
SellableAt time.Time `db:"sellable_at"`
CreatedAt time.Time `db:"created_at"`
}
func (PositionLot) TableName() string { return "veyra_position_lot" }
type FillAllocation struct {
ID string `db:"id"`
SellFillID string `db:"sell_fill_id"`
LotID string `db:"lot_id"`
Quantity int64 `db:"quantity"`
CostMinor int64 `db:"cost_minor"`
}
func (FillAllocation) TableName() string { return "veyra_fill_allocation" }
type PositionSummary struct {
Symbol string `json:"symbol"`
Exchange string `json:"exchange"`
Name string `json:"name"`
Quantity string `json:"quantity"`
SellableQuantity string `json:"sellable_quantity"`
CostBasis string `json:"cost_basis"`
MarketValue *string `json:"market_value"`
UnrealizedPnL *string `json:"unrealized_pnl"`
}
type LedgerReview struct {
TradeCount int64 `json:"trade_count"`
SellCount int64 `json:"sell_count"`
ProfitableSellCount int64 `json:"profitable_sell_count"`
LosingSellCount int64 `json:"losing_sell_count"`
BuyOutflow string `json:"buy_outflow"`
SellInflow string `json:"sell_inflow"`
TotalFees string `json:"total_fees"`
}
type PortfolioSummary struct {
Review LedgerReview `json:"review"`
AccountID string `json:"account_id"`
CashBalance string `json:"cash_balance"`
RealizedPnL string `json:"realized_pnl"`
Positions []PositionSummary `json:"positions"`
}
+47
View File
@@ -0,0 +1,47 @@
package model
import "time"
type PlanPreview struct {
ID string `db:"id" json:"id"`
AccountID string `db:"account_id" json:"account_id"`
CandidateRevisionID string `db:"candidate_revision_id" json:"candidate_revision_id"`
CandidateVersion int64 `db:"candidate_version" json:"candidate_version"`
Quantity int64 `db:"quantity" json:"quantity,string"`
FeeBudgetMinor int64 `db:"fee_budget_minor" json:"-"`
ReservedMinor int64 `db:"reserved_minor" json:"-"`
ReservedAmount string `json:"reserved_amount"`
FeeBudget string `json:"fee_budget"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (PlanPreview) TableName() string { return "veyra_plan_preview" }
type TradePlan struct {
ID string `db:"id" json:"id"`
PreviewID string `db:"preview_id" json:"preview_id"`
AccountID string `db:"account_id" json:"account_id"`
CandidateRevisionID string `db:"candidate_revision_id" json:"candidate_revision_id"`
CandidateVersion int64 `db:"candidate_version" json:"candidate_version"`
Quantity int64 `db:"quantity" json:"quantity,string"`
FilledQuantity int64 `db:"filled_quantity" json:"filled_quantity,string"`
FeeBudgetMinor int64 `db:"fee_budget_minor" json:"-"`
ReservedMinor int64 `db:"reserved_minor" json:"-"`
ReservedAmount string `json:"reserved_amount"`
Status string `db:"status" json:"status"`
Version int64 `db:"version" json:"version"`
ValidUntil time.Time `db:"valid_until" json:"valid_until"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CandidateContent CandidateContent `json:"candidate_content"`
}
func (TradePlan) TableName() string { return "veyra_trade_plan" }
type PlanBudget struct {
AccountID string `json:"account_id"`
CashBalance string `json:"cash_balance"`
ReservedAmount string `json:"reserved_amount"`
AvailableAmount string `json:"available_amount"`
}