mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 17:12:25 +08:00
refactor: standardize code comments
This commit is contained in:
@@ -9,14 +9,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AIModelStore AI模型存储
|
||||
// AIModelStore AI model storage
|
||||
type AIModelStore struct {
|
||||
db *sql.DB
|
||||
encryptFunc func(string) string
|
||||
decryptFunc func(string) string
|
||||
}
|
||||
|
||||
// AIModel AI模型配置
|
||||
// AIModel AI model configuration
|
||||
type AIModel struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -49,7 +49,7 @@ func (s *AIModelStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发器
|
||||
// Trigger
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at
|
||||
AFTER UPDATE ON ai_models
|
||||
@@ -61,7 +61,7 @@ func (s *AIModelStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 向后兼容:添加可能缺失的列
|
||||
// Backward compatibility: add potentially missing columns
|
||||
s.db.Exec(`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`)
|
||||
s.db.Exec(`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`)
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *AIModelStore) initDefaultData() error {
|
||||
VALUES (?, 'default', ?, ?, 0)
|
||||
`, model.id, model.name, model.provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化AI模型失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize AI model: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -102,7 +102,7 @@ func (s *AIModelStore) decrypt(encrypted string) string {
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// List 获取用户的AI模型列表
|
||||
// List retrieves user's AI model list
|
||||
func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, user_id, name, provider, enabled, api_key,
|
||||
@@ -136,10 +136,10 @@ func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
|
||||
return models, nil
|
||||
}
|
||||
|
||||
// Get 获取单个AI模型
|
||||
// Get retrieves a single AI model
|
||||
func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) {
|
||||
if modelID == "" {
|
||||
return nil, fmt.Errorf("模型ID不能为空")
|
||||
return nil, fmt.Errorf("model ID cannot be empty")
|
||||
}
|
||||
|
||||
candidates := []string{}
|
||||
@@ -178,7 +178,7 @@ func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
// GetDefault 获取默认启用的AI模型
|
||||
// GetDefault retrieves the default enabled AI model
|
||||
func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) {
|
||||
if userID == "" {
|
||||
userID = "default"
|
||||
@@ -193,7 +193,7 @@ func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) {
|
||||
if userID != "default" {
|
||||
return s.firstEnabled("default")
|
||||
}
|
||||
return nil, fmt.Errorf("请先在系统中配置可用的AI模型")
|
||||
return nil, fmt.Errorf("please configure an available AI model in the system first")
|
||||
}
|
||||
|
||||
func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) {
|
||||
@@ -218,9 +218,9 @@ func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) {
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
// Update 更新AI模型,不存在则创建
|
||||
// Update updates AI model, creates if not exists
|
||||
func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
||||
// 先尝试精确匹配ID
|
||||
// Try exact ID match first
|
||||
var existingID string
|
||||
err := s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1`, userID, id).Scan(&existingID)
|
||||
if err == nil {
|
||||
@@ -232,11 +232,11 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
||||
return err
|
||||
}
|
||||
|
||||
// 尝试兼容旧逻辑:将id作为provider查找
|
||||
// Try legacy logic compatibility: use id as provider to search
|
||||
provider := id
|
||||
err = s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1`, userID, provider).Scan(&existingID)
|
||||
if err == nil {
|
||||
logger.Warnf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
||||
logger.Warnf("⚠️ Using legacy provider matching to update model: %s -> %s", provider, existingID)
|
||||
encryptedAPIKey := s.encrypt(apiKey)
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||||
@@ -245,7 +245,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建新记录
|
||||
// Create new record
|
||||
if provider == id && (provider == "deepseek" || provider == "qwen") {
|
||||
provider = id
|
||||
} else {
|
||||
@@ -274,7 +274,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
||||
newModelID = fmt.Sprintf("%s_%s", userID, provider)
|
||||
}
|
||||
|
||||
logger.Infof("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name)
|
||||
logger.Infof("✓ Creating new AI model configuration: ID=%s, Provider=%s, Name=%s", newModelID, provider, name)
|
||||
encryptedAPIKey := s.encrypt(apiKey)
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at)
|
||||
@@ -283,7 +283,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
||||
return err
|
||||
}
|
||||
|
||||
// Create 创建AI模型
|
||||
// Create creates an AI model
|
||||
func (s *AIModelStore) Create(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BacktestStore 回测数据存储
|
||||
// BacktestStore backtest data storage
|
||||
type BacktestStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// RunState 回测状态
|
||||
// RunState backtest state
|
||||
type RunState string
|
||||
|
||||
const (
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
RunStateFailed RunState = "failed"
|
||||
)
|
||||
|
||||
// RunMetadata 回测元数据
|
||||
// RunMetadata backtest metadata
|
||||
type RunMetadata struct {
|
||||
RunID string `json:"run_id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -36,7 +36,7 @@ type RunMetadata struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// RunSummary 回测摘要
|
||||
// RunSummary backtest summary
|
||||
type RunSummary struct {
|
||||
SymbolCount int `json:"symbol_count"`
|
||||
DecisionTF string `json:"decision_tf"`
|
||||
@@ -48,7 +48,7 @@ type RunSummary struct {
|
||||
LiquidationNote string `json:"liquidation_note"`
|
||||
}
|
||||
|
||||
// EquityPoint 权益点
|
||||
// EquityPoint equity point
|
||||
type EquityPoint struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Equity float64 `json:"equity"`
|
||||
@@ -59,7 +59,7 @@ type EquityPoint struct {
|
||||
Cycle int `json:"cycle"`
|
||||
}
|
||||
|
||||
// TradeEvent 交易事件
|
||||
// TradeEvent trade event
|
||||
type TradeEvent struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Symbol string `json:"symbol"`
|
||||
@@ -78,7 +78,7 @@ type TradeEvent struct {
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
// RunIndexEntry 回测索引条目
|
||||
// RunIndexEntry backtest index entry
|
||||
type RunIndexEntry struct {
|
||||
RunID string `json:"run_id"`
|
||||
State string `json:"state"`
|
||||
@@ -92,10 +92,10 @@ type RunIndexEntry struct {
|
||||
UpdatedAtISO string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// initTables 初始化回测相关表
|
||||
// initTables initializes backtest related tables
|
||||
func (s *BacktestStore) initTables() error {
|
||||
queries := []string{
|
||||
// 回测运行主表
|
||||
// Backtest runs main table
|
||||
`CREATE TABLE IF NOT EXISTS backtest_runs (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL DEFAULT '',
|
||||
@@ -120,7 +120,7 @@ func (s *BacktestStore) initTables() error {
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// 回测检查点
|
||||
// Backtest checkpoints
|
||||
`CREATE TABLE IF NOT EXISTS backtest_checkpoints (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
payload BLOB NOT NULL,
|
||||
@@ -128,7 +128,7 @@ func (s *BacktestStore) initTables() error {
|
||||
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 回测权益曲线
|
||||
// Backtest equity curve
|
||||
`CREATE TABLE IF NOT EXISTS backtest_equity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
@@ -142,7 +142,7 @@ func (s *BacktestStore) initTables() error {
|
||||
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 回测交易记录
|
||||
// Backtest trade records
|
||||
`CREATE TABLE IF NOT EXISTS backtest_trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
@@ -164,7 +164,7 @@ func (s *BacktestStore) initTables() error {
|
||||
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 回测指标
|
||||
// Backtest metrics
|
||||
`CREATE TABLE IF NOT EXISTS backtest_metrics (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
payload BLOB NOT NULL,
|
||||
@@ -172,7 +172,7 @@ func (s *BacktestStore) initTables() error {
|
||||
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 回测决策日志
|
||||
// Backtest decision logs
|
||||
`CREATE TABLE IF NOT EXISTS backtest_decisions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
@@ -182,7 +182,7 @@ func (s *BacktestStore) initTables() error {
|
||||
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 索引
|
||||
// Indexes
|
||||
`CREATE INDEX IF NOT EXISTS idx_backtest_runs_state ON backtest_runs(state, updated_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_backtest_equity_run_ts ON backtest_equity(run_id, ts)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_backtest_trades_run_ts ON backtest_trades(run_id, ts)`,
|
||||
@@ -191,11 +191,11 @@ func (s *BacktestStore) initTables() error {
|
||||
|
||||
for _, query := range queries {
|
||||
if _, err := s.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("执行SQL失败: %w", err)
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加可能缺失的列(向后兼容)
|
||||
// Add potentially missing columns (backward compatibility)
|
||||
s.addColumnIfNotExists("backtest_runs", "label", "TEXT DEFAULT ''")
|
||||
s.addColumnIfNotExists("backtest_runs", "last_error", "TEXT DEFAULT ''")
|
||||
s.addColumnIfNotExists("backtest_trades", "leverage", "INTEGER DEFAULT 0")
|
||||
@@ -219,14 +219,14 @@ func (s *BacktestStore) addColumnIfNotExists(table, column, definition string) {
|
||||
continue
|
||||
}
|
||||
if name == column {
|
||||
return // 列已存在
|
||||
return // Column already exists
|
||||
}
|
||||
}
|
||||
|
||||
s.db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition))
|
||||
}
|
||||
|
||||
// SaveCheckpoint 保存检查点
|
||||
// SaveCheckpoint saves checkpoint
|
||||
func (s *BacktestStore) SaveCheckpoint(runID string, payload []byte) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO backtest_checkpoints (run_id, payload, updated_at)
|
||||
@@ -236,14 +236,14 @@ func (s *BacktestStore) SaveCheckpoint(runID string, payload []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadCheckpoint 加载检查点
|
||||
// LoadCheckpoint loads checkpoint
|
||||
func (s *BacktestStore) LoadCheckpoint(runID string) ([]byte, error) {
|
||||
var payload []byte
|
||||
err := s.db.QueryRow(`SELECT payload FROM backtest_checkpoints WHERE run_id = ?`, runID).Scan(&payload)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// SaveRunMetadata 保存运行元数据
|
||||
// SaveRunMetadata saves run metadata
|
||||
func (s *BacktestStore) SaveRunMetadata(meta *RunMetadata) error {
|
||||
created := meta.CreatedAt.UTC().Format(time.RFC3339)
|
||||
updated := meta.UpdatedAt.UTC().Format(time.RFC3339)
|
||||
@@ -270,7 +270,7 @@ func (s *BacktestStore) SaveRunMetadata(meta *RunMetadata) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadRunMetadata 加载运行元数据
|
||||
// LoadRunMetadata loads run metadata
|
||||
func (s *BacktestStore) LoadRunMetadata(runID string) (*RunMetadata, error) {
|
||||
var (
|
||||
userID string
|
||||
@@ -326,7 +326,7 @@ func (s *BacktestStore) LoadRunMetadata(runID string) (*RunMetadata, error) {
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// ListRunIDs 列出所有运行ID
|
||||
// ListRunIDs lists all run IDs
|
||||
func (s *BacktestStore) ListRunIDs() ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT run_id FROM backtest_runs ORDER BY datetime(updated_at) DESC`)
|
||||
if err != nil {
|
||||
@@ -345,7 +345,7 @@ func (s *BacktestStore) ListRunIDs() ([]string, error) {
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
// AppendEquityPoint 添加权益点
|
||||
// AppendEquityPoint appends equity point
|
||||
func (s *BacktestStore) AppendEquityPoint(runID string, point EquityPoint) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO backtest_equity (run_id, ts, equity, available, pnl, pnl_pct, dd_pct, cycle)
|
||||
@@ -355,7 +355,7 @@ func (s *BacktestStore) AppendEquityPoint(runID string, point EquityPoint) error
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadEquityPoints 加载权益点
|
||||
// LoadEquityPoints loads equity points
|
||||
func (s *BacktestStore) LoadEquityPoints(runID string) ([]EquityPoint, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT ts, equity, available, pnl, pnl_pct, dd_pct, cycle
|
||||
@@ -378,7 +378,7 @@ func (s *BacktestStore) LoadEquityPoints(runID string) ([]EquityPoint, error) {
|
||||
return points, rows.Err()
|
||||
}
|
||||
|
||||
// AppendTradeEvent 添加交易事件
|
||||
// AppendTradeEvent appends trade event
|
||||
func (s *BacktestStore) AppendTradeEvent(runID string, event TradeEvent) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO backtest_trades (run_id, ts, symbol, action, side, qty, price, fee,
|
||||
@@ -391,7 +391,7 @@ func (s *BacktestStore) AppendTradeEvent(runID string, event TradeEvent) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadTradeEvents 加载交易事件
|
||||
// LoadTradeEvents loads trade events
|
||||
func (s *BacktestStore) LoadTradeEvents(runID string) ([]TradeEvent, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT ts, symbol, action, side, qty, price, fee, slippage, order_value,
|
||||
@@ -417,7 +417,7 @@ func (s *BacktestStore) LoadTradeEvents(runID string) ([]TradeEvent, error) {
|
||||
return events, rows.Err()
|
||||
}
|
||||
|
||||
// SaveMetrics 保存指标
|
||||
// SaveMetrics saves metrics
|
||||
func (s *BacktestStore) SaveMetrics(runID string, payload []byte) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO backtest_metrics (run_id, payload, updated_at)
|
||||
@@ -427,14 +427,14 @@ func (s *BacktestStore) SaveMetrics(runID string, payload []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadMetrics 加载指标
|
||||
// LoadMetrics loads metrics
|
||||
func (s *BacktestStore) LoadMetrics(runID string) ([]byte, error) {
|
||||
var payload []byte
|
||||
err := s.db.QueryRow(`SELECT payload FROM backtest_metrics WHERE run_id = ?`, runID).Scan(&payload)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// SaveDecisionRecord 保存决策记录
|
||||
// SaveDecisionRecord saves decision record
|
||||
func (s *BacktestStore) SaveDecisionRecord(runID string, cycle int, payload []byte) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO backtest_decisions (run_id, cycle, payload)
|
||||
@@ -443,7 +443,7 @@ func (s *BacktestStore) SaveDecisionRecord(runID string, cycle int, payload []by
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadDecisionRecords 加载决策记录
|
||||
// LoadDecisionRecords loads decision records
|
||||
func (s *BacktestStore) LoadDecisionRecords(runID string, limit, offset int) ([]json.RawMessage, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT payload FROM backtest_decisions
|
||||
@@ -467,7 +467,7 @@ func (s *BacktestStore) LoadDecisionRecords(runID string, limit, offset int) ([]
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
// LoadLatestDecision 加载最新决策
|
||||
// LoadLatestDecision loads latest decision
|
||||
func (s *BacktestStore) LoadLatestDecision(runID string, cycle int) ([]byte, error) {
|
||||
var query string
|
||||
var args []interface{}
|
||||
@@ -485,7 +485,7 @@ func (s *BacktestStore) LoadLatestDecision(runID string, cycle int) ([]byte, err
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// UpdateProgress 更新进度
|
||||
// UpdateProgress updates progress
|
||||
func (s *BacktestStore) UpdateProgress(runID string, progressPct, equity float64, barIndex int, liquidated bool) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE backtest_runs
|
||||
@@ -495,7 +495,7 @@ func (s *BacktestStore) UpdateProgress(runID string, progressPct, equity float64
|
||||
return err
|
||||
}
|
||||
|
||||
// ListIndexEntries 列出索引条目
|
||||
// ListIndexEntries lists index entries
|
||||
func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT run_id, state, symbol_count, decision_tf, equity_last, max_drawdown_pct,
|
||||
@@ -524,7 +524,7 @@ func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) {
|
||||
entry.UpdatedAtISO = updatedISO
|
||||
entry.Symbols = make([]string, 0, symbolCnt)
|
||||
|
||||
// 尝试从配置中提取更多信息
|
||||
// Try to extract more information from config
|
||||
if len(cfgJSON) > 0 {
|
||||
var cfg struct {
|
||||
Symbols []string `json:"symbols"`
|
||||
@@ -543,13 +543,13 @@ func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) {
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteRun 删除运行
|
||||
// DeleteRun deletes run
|
||||
func (s *BacktestStore) DeleteRun(runID string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM backtest_runs WHERE run_id = ?`, runID)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
// SaveConfig saves config
|
||||
func (s *BacktestStore) SaveConfig(runID, userID, template, customPrompt, provider, model string, override bool, configJSON []byte) error {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
if userID == "" {
|
||||
@@ -575,7 +575,7 @@ func (s *BacktestStore) SaveConfig(runID, userID, template, customPrompt, provid
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置
|
||||
// LoadConfig loads config
|
||||
func (s *BacktestStore) LoadConfig(runID string) ([]byte, error) {
|
||||
var payload []byte
|
||||
err := s.db.QueryRow(`SELECT config_json FROM backtest_runs WHERE run_id = ?`, runID).Scan(&payload)
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DecisionStore 决策日志存储
|
||||
// DecisionStore decision log storage
|
||||
type DecisionStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// DecisionRecord 决策记录
|
||||
// DecisionRecord decision record
|
||||
type DecisionRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
TraderID string `json:"trader_id"`
|
||||
@@ -32,7 +32,7 @@ type DecisionRecord struct {
|
||||
Decisions []DecisionAction `json:"decisions"`
|
||||
}
|
||||
|
||||
// AccountSnapshot 账户状态快照
|
||||
// AccountSnapshot account state snapshot
|
||||
type AccountSnapshot struct {
|
||||
TotalBalance float64 `json:"total_balance"`
|
||||
AvailableBalance float64 `json:"available_balance"`
|
||||
@@ -42,7 +42,7 @@ type AccountSnapshot struct {
|
||||
InitialBalance float64 `json:"initial_balance"`
|
||||
}
|
||||
|
||||
// PositionSnapshot 持仓快照
|
||||
// PositionSnapshot position snapshot
|
||||
type PositionSnapshot struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
@@ -54,8 +54,8 @@ type PositionSnapshot struct {
|
||||
LiquidationPrice float64 `json:"liquidation_price"`
|
||||
}
|
||||
|
||||
// DecisionAction 决策动作
|
||||
type DecisionAction struct {
|
||||
// DecisionAction decision action
|
||||
type DecisionAction struct{
|
||||
Action string `json:"action"`
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
@@ -67,7 +67,7 @@ type DecisionAction struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Statistics 统计信息
|
||||
// Statistics statistics information
|
||||
type Statistics struct {
|
||||
TotalCycles int `json:"total_cycles"`
|
||||
SuccessfulCycles int `json:"successful_cycles"`
|
||||
@@ -76,11 +76,11 @@ type Statistics struct {
|
||||
TotalClosePositions int `json:"total_close_positions"`
|
||||
}
|
||||
|
||||
// initTables 初始化 AI 决策日志表
|
||||
// 注意:账户净值曲线数据已迁移到 trader_equity_snapshots 表(由 EquityStore 管理)
|
||||
// initTables initializes AI decision log tables
|
||||
// Note: Account equity curve data has been migrated to trader_equity_snapshots table (managed by EquityStore)
|
||||
func (s *DecisionStore) initTables() error {
|
||||
queries := []string{
|
||||
// AI 决策日志表(记录 AI 的输入输出、思维链等)
|
||||
// AI decision log table (records AI input/output, chain of thought, etc.)
|
||||
`CREATE TABLE IF NOT EXISTS decision_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trader_id TEXT NOT NULL,
|
||||
@@ -97,21 +97,21 @@ func (s *DecisionStore) initTables() error {
|
||||
ai_request_duration_ms INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
// 索引
|
||||
// Indexes
|
||||
`CREATE INDEX IF NOT EXISTS idx_decision_records_trader_time ON decision_records(trader_id, timestamp DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_decision_records_timestamp ON decision_records(timestamp DESC)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if _, err := s.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("执行SQL失败: %w", err)
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogDecision 记录决策(仅保存 AI 决策日志,净值曲线已迁移到 equity 表)
|
||||
// LogDecision logs decision (only saves AI decision log, equity curve has been migrated to equity table)
|
||||
func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
if record.Timestamp.IsZero() {
|
||||
record.Timestamp = time.Now().UTC()
|
||||
@@ -119,11 +119,11 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
record.Timestamp = record.Timestamp.UTC()
|
||||
}
|
||||
|
||||
// 序列化候选币种和执行日志为 JSON
|
||||
// Serialize candidate coins and execution log to JSON
|
||||
candidateCoinsJSON, _ := json.Marshal(record.CandidateCoins)
|
||||
executionLogJSON, _ := json.Marshal(record.ExecutionLog)
|
||||
|
||||
// 插入决策记录主表(仅保存 AI 决策相关内容)
|
||||
// Insert decision record main table (only save AI decision related content)
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO decision_records (
|
||||
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
@@ -137,19 +137,19 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
record.Success, record.ErrorMessage, record.AIRequestDurationMs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入决策记录失败: %w", err)
|
||||
return fmt.Errorf("failed to insert decision record: %w", err)
|
||||
}
|
||||
|
||||
decisionID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取决策ID失败: %w", err)
|
||||
return fmt.Errorf("failed to get decision ID: %w", err)
|
||||
}
|
||||
record.ID = decisionID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatestRecords 获取指定交易员最近N条记录(按时间正序:从旧到新)
|
||||
// GetLatestRecords gets the latest N records for specified trader (sorted by time in ascending order: old to new)
|
||||
func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRecord, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
@@ -161,7 +161,7 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec
|
||||
LIMIT ?
|
||||
`, traderID, n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询决策记录失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query decision records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -174,12 +174,12 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
// 填充关联数据
|
||||
// Fill associated data
|
||||
for _, record := range records {
|
||||
s.fillRecordDetails(record)
|
||||
}
|
||||
|
||||
// 反转数组,让时间从旧到新排列
|
||||
// Reverse array to sort time from old to new
|
||||
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
|
||||
records[i], records[j] = records[j], records[i]
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// GetAllLatestRecords 获取所有交易员最近N条记录
|
||||
// GetAllLatestRecords gets the latest N records for all traders
|
||||
func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
@@ -198,7 +198,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
||||
LIMIT ?
|
||||
`, n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询决策记录失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query decision records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -211,7 +211,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
// 反转数组
|
||||
// Reverse array
|
||||
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
|
||||
records[i], records[j] = records[j], records[i]
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// GetRecordsByDate 获取指定交易员指定日期的所有记录
|
||||
// GetRecordsByDate gets all records for a specified trader on a specified date
|
||||
func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*DecisionRecord, error) {
|
||||
dateStr := date.Format("2006-01-02")
|
||||
|
||||
@@ -232,7 +232,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De
|
||||
ORDER BY timestamp ASC
|
||||
`, traderID, dateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询决策记录失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query decision records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -248,7 +248,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// CleanOldRecords 清理N天前的旧记录
|
||||
// CleanOldRecords cleans old records from N days ago
|
||||
func (s *DecisionStore) CleanOldRecords(traderID string, days int) (int64, error) {
|
||||
cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339)
|
||||
|
||||
@@ -257,13 +257,13 @@ func (s *DecisionStore) CleanOldRecords(traderID string, days int) (int64, error
|
||||
WHERE trader_id = ? AND timestamp < ?
|
||||
`, traderID, cutoffTime)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("清理旧记录失败: %w", err)
|
||||
return 0, fmt.Errorf("failed to clean old records: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// GetStatistics 获取指定交易员的统计信息
|
||||
// GetStatistics gets statistics information for specified trader
|
||||
func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) {
|
||||
stats := &Statistics{}
|
||||
|
||||
@@ -271,24 +271,24 @@ func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) {
|
||||
SELECT COUNT(*) FROM decision_records WHERE trader_id = ?
|
||||
`, traderID).Scan(&stats.TotalCycles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询总周期数失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query total cycles: %w", err)
|
||||
}
|
||||
|
||||
err = s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM decision_records WHERE trader_id = ? AND success = 1
|
||||
`, traderID).Scan(&stats.SuccessfulCycles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询成功周期数失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query successful cycles: %w", err)
|
||||
}
|
||||
stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles
|
||||
|
||||
// 从 trader_orders 表统计开仓次数
|
||||
// Count open positions from trader_orders table
|
||||
s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM trader_orders
|
||||
WHERE trader_id = ? AND status = 'FILLED' AND action IN ('open_long', 'open_short')
|
||||
`, traderID).Scan(&stats.TotalOpenPositions)
|
||||
|
||||
// 从 trader_orders 表统计平仓次数
|
||||
// Count close positions from trader_orders table
|
||||
s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM trader_orders
|
||||
WHERE trader_id = ? AND status = 'FILLED' AND action IN ('close_long', 'close_short', 'auto_close_long', 'auto_close_short')
|
||||
@@ -297,7 +297,7 @@ func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetAllStatistics 获取所有交易员的统计信息
|
||||
// GetAllStatistics gets statistics information for all traders
|
||||
func (s *DecisionStore) GetAllStatistics() (*Statistics, error) {
|
||||
stats := &Statistics{}
|
||||
|
||||
@@ -305,7 +305,7 @@ func (s *DecisionStore) GetAllStatistics() (*Statistics, error) {
|
||||
s.db.QueryRow(`SELECT COUNT(*) FROM decision_records WHERE success = 1`).Scan(&stats.SuccessfulCycles)
|
||||
stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles
|
||||
|
||||
// 从 trader_orders 表统计
|
||||
// Count from trader_orders table
|
||||
s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM trader_orders
|
||||
WHERE status = 'FILLED' AND action IN ('open_long', 'open_short')
|
||||
@@ -319,7 +319,7 @@ func (s *DecisionStore) GetAllStatistics() (*Statistics, error) {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetLastCycleNumber 获取指定交易员的最后周期编号
|
||||
// GetLastCycleNumber gets the last cycle number for specified trader
|
||||
func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) {
|
||||
var cycleNumber int
|
||||
err := s.db.QueryRow(`
|
||||
@@ -331,7 +331,7 @@ func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) {
|
||||
return cycleNumber, nil
|
||||
}
|
||||
|
||||
// scanDecisionRecord 从行中扫描决策记录
|
||||
// scanDecisionRecord scans decision record from row
|
||||
func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, error) {
|
||||
var record DecisionRecord
|
||||
var timestampStr string
|
||||
@@ -354,11 +354,11 @@ func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, err
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// fillRecordDetails 填充决策记录的关联数据(旧的关联表已删除,此函数保留用于兼容性)
|
||||
// 注意:账户快照、持仓快照、决策动作等数据已不再存储在 decision 相关表中
|
||||
// - 净值数据请使用 EquityStore.GetLatest()
|
||||
// - 订单数据请使用 OrderStore
|
||||
// fillRecordDetails fills associated data for decision record (old associated tables removed, this function kept for compatibility)
|
||||
// Note: Account snapshot, position snapshot, decision action data are no longer stored in decision related tables
|
||||
// - For equity data use EquityStore.GetLatest()
|
||||
// - For order data use OrderStore
|
||||
func (s *DecisionStore) fillRecordDetails(record *DecisionRecord) {
|
||||
// 旧的关联表已删除,不再需要填充
|
||||
// AccountState, Positions, Decisions 字段将保持为零值
|
||||
// Old associated tables removed, no longer need to fill
|
||||
// AccountState, Positions, Decisions fields will remain at zero values
|
||||
}
|
||||
|
||||
@@ -6,27 +6,27 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EquityStore 账户净值存储(用于绘制收益率曲线)
|
||||
// EquityStore account equity storage (for plotting return curves)
|
||||
type EquityStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// EquitySnapshot 净值快照
|
||||
// EquitySnapshot equity snapshot
|
||||
type EquitySnapshot struct {
|
||||
ID int64 `json:"id"`
|
||||
TraderID string `json:"trader_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
TotalEquity float64 `json:"total_equity"` // 账户净值 (余额 + 未实现盈亏)
|
||||
Balance float64 `json:"balance"` // 账户余额
|
||||
UnrealizedPnL float64 `json:"unrealized_pnl"` // 未实现盈亏
|
||||
PositionCount int `json:"position_count"` // 持仓数量
|
||||
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
|
||||
TotalEquity float64 `json:"total_equity"` // Account equity (balance + unrealized PnL)
|
||||
Balance float64 `json:"balance"` // Account balance
|
||||
UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized profit and loss
|
||||
PositionCount int `json:"position_count"` // Position count
|
||||
MarginUsedPct float64 `json:"margin_used_pct"` // Margin usage percentage
|
||||
}
|
||||
|
||||
// initTables 初始化净值表
|
||||
// initTables initializes equity tables
|
||||
func (s *EquityStore) initTables() error {
|
||||
queries := []string{
|
||||
// 净值快照表 - 专门用于收益率曲线
|
||||
// Equity snapshot table - specifically for return curves
|
||||
`CREATE TABLE IF NOT EXISTS trader_equity_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trader_id TEXT NOT NULL,
|
||||
@@ -38,21 +38,21 @@ func (s *EquityStore) initTables() error {
|
||||
margin_used_pct REAL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
// 索引
|
||||
// Indexes
|
||||
`CREATE INDEX IF NOT EXISTS idx_equity_trader_time ON trader_equity_snapshots(trader_id, timestamp DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_equity_timestamp ON trader_equity_snapshots(timestamp DESC)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if _, err := s.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("执行SQL失败: %w", err)
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save 保存净值快照
|
||||
// Save saves equity snapshot
|
||||
func (s *EquityStore) Save(snapshot *EquitySnapshot) error {
|
||||
if snapshot.Timestamp.IsZero() {
|
||||
snapshot.Timestamp = time.Now().UTC()
|
||||
@@ -75,7 +75,7 @@ func (s *EquityStore) Save(snapshot *EquitySnapshot) error {
|
||||
snapshot.MarginUsedPct,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存净值快照失败: %w", err)
|
||||
return fmt.Errorf("failed to save equity snapshot: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
@@ -83,7 +83,7 @@ func (s *EquityStore) Save(snapshot *EquitySnapshot) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatest 获取指定交易员最近N条净值记录(按时间正序:从旧到新)
|
||||
// GetLatest gets the latest N equity records for specified trader (sorted in ascending chronological order: old to new)
|
||||
func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, timestamp, total_equity, balance,
|
||||
@@ -94,7 +94,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot,
|
||||
LIMIT ?
|
||||
`, traderID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询净值记录失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query equity records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -113,7 +113,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot,
|
||||
snapshots = append(snapshots, snap)
|
||||
}
|
||||
|
||||
// 反转数组,让时间从旧到新排列(适合绘制曲线)
|
||||
// Reverse the array to sort time from old to new (suitable for plotting curves)
|
||||
for i, j := 0, len(snapshots)-1; i < j; i, j = i+1, j-1 {
|
||||
snapshots[i], snapshots[j] = snapshots[j], snapshots[i]
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot,
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// GetByTimeRange 获取指定时间范围内的净值记录
|
||||
// GetByTimeRange gets equity records within specified time range
|
||||
func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]*EquitySnapshot, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, timestamp, total_equity, balance,
|
||||
@@ -131,7 +131,7 @@ func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]*
|
||||
ORDER BY timestamp ASC
|
||||
`, traderID, start.Format(time.RFC3339), end.Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询净值记录失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query equity records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -153,7 +153,7 @@ func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]*
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// GetAllTradersLatest 获取所有交易员的最新净值(用于排行榜)
|
||||
// GetAllTradersLatest gets latest equity for all traders (for leaderboards)
|
||||
func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT e.id, e.trader_id, e.timestamp, e.total_equity, e.balance,
|
||||
@@ -166,7 +166,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error)
|
||||
) latest ON e.trader_id = latest.trader_id AND e.timestamp = latest.max_ts
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询最新净值失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query latest equity: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -188,7 +188,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CleanOldRecords 清理N天前的旧记录
|
||||
// CleanOldRecords cleans old records from N days ago
|
||||
func (s *EquityStore) CleanOldRecords(traderID string, days int) (int64, error) {
|
||||
cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339)
|
||||
|
||||
@@ -197,13 +197,13 @@ func (s *EquityStore) CleanOldRecords(traderID string, days int) (int64, error)
|
||||
WHERE trader_id = ? AND timestamp < ?
|
||||
`, traderID, cutoffTime)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("清理旧记录失败: %w", err)
|
||||
return 0, fmt.Errorf("failed to clean old records: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// GetCount 获取指定交易员的记录数
|
||||
// GetCount gets record count for specified trader
|
||||
func (s *EquityStore) GetCount(traderID string) (int, error) {
|
||||
var count int
|
||||
err := s.db.QueryRow(`
|
||||
@@ -212,26 +212,26 @@ func (s *EquityStore) GetCount(traderID string) (int, error) {
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MigrateFromDecision 从旧的 decision_account_snapshots 迁移数据
|
||||
// MigrateFromDecision migrates data from old decision_account_snapshots table
|
||||
func (s *EquityStore) MigrateFromDecision() (int64, error) {
|
||||
// 检查是否需要迁移(新表是否为空)
|
||||
// Check if migration is needed (whether new table is empty)
|
||||
var count int
|
||||
s.db.QueryRow(`SELECT COUNT(*) FROM trader_equity_snapshots`).Scan(&count)
|
||||
if count > 0 {
|
||||
return 0, nil // 已有数据,跳过迁移
|
||||
return 0, nil // Already has data, skip migration
|
||||
}
|
||||
|
||||
// 检查旧表是否存在
|
||||
// Check if old table exists
|
||||
var tableName string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='decision_account_snapshots'
|
||||
`).Scan(&tableName)
|
||||
if err != nil {
|
||||
return 0, nil // 旧表不存在,跳过
|
||||
return 0, nil // Old table doesn't exist, skip
|
||||
}
|
||||
|
||||
// 迁移数据:从 decision_records + decision_account_snapshots 联合查询
|
||||
// Migrate data: join query from decision_records + decision_account_snapshots
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO trader_equity_snapshots (
|
||||
trader_id, timestamp, total_equity, balance,
|
||||
@@ -250,7 +250,7 @@ func (s *EquityStore) MigrateFromDecision() (int64, error) {
|
||||
ORDER BY dr.timestamp ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("迁移数据失败: %w", err)
|
||||
return 0, fmt.Errorf("failed to migrate data: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExchangeStore 交易所存储
|
||||
// ExchangeStore exchange storage
|
||||
type ExchangeStore struct {
|
||||
db *sql.DB
|
||||
encryptFunc func(string) string
|
||||
decryptFunc func(string) string
|
||||
}
|
||||
|
||||
// Exchange 交易所配置
|
||||
// Exchange exchange configuration
|
||||
type Exchange struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -24,7 +24,7 @@ type Exchange struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Passphrase string `json:"passphrase"` // OKX专用
|
||||
Passphrase string `json:"passphrase"` // OKX-specific
|
||||
Testnet bool `json:"testnet"`
|
||||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
|
||||
AsterUser string `json:"asterUser"`
|
||||
@@ -65,10 +65,10 @@ func (s *ExchangeStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移:添加 passphrase 列(如果不存在)
|
||||
// Migration: add passphrase column (if not exists)
|
||||
s.db.Exec(`ALTER TABLE exchanges ADD COLUMN passphrase TEXT DEFAULT ''`)
|
||||
|
||||
// 触发器
|
||||
// Trigger
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
|
||||
AFTER UPDATE ON exchanges
|
||||
@@ -97,7 +97,7 @@ func (s *ExchangeStore) initDefaultData() error {
|
||||
VALUES (?, 'default', ?, ?, 0)
|
||||
`, exchange.id, exchange.name, exchange.typ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化交易所失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize exchange: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -117,7 +117,7 @@ func (s *ExchangeStore) decrypt(encrypted string) string {
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// EnsureUserExchanges 确保用户有所有支持的交易所记录
|
||||
// EnsureUserExchanges ensures user has records for all supported exchanges
|
||||
func (s *ExchangeStore) EnsureUserExchanges(userID string) error {
|
||||
exchanges := []struct {
|
||||
id, name, typ string
|
||||
@@ -136,17 +136,17 @@ func (s *ExchangeStore) EnsureUserExchanges(userID string) error {
|
||||
VALUES (?, ?, ?, ?, 0)
|
||||
`, exchange.id, userID, exchange.name, exchange.typ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("确保用户交易所失败: %w", err)
|
||||
return fmt.Errorf("failed to ensure user exchanges: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户的交易所列表
|
||||
// List gets user's exchange list
|
||||
func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
||||
// 确保用户有所有支持的交易所记录
|
||||
// Ensure user has records for all supported exchanges
|
||||
if err := s.EnsureUserExchanges(userID); err != nil {
|
||||
logger.Debugf("⚠️ 确保用户交易所记录失败: %v", err)
|
||||
logger.Debugf("Warning: failed to ensure user exchange records: %v", err)
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(`
|
||||
@@ -194,7 +194,7 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
||||
return exchanges, nil
|
||||
}
|
||||
|
||||
// Update 更新交易所配置
|
||||
// Update updates exchange configuration
|
||||
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool,
|
||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string) error {
|
||||
|
||||
@@ -246,7 +246,7 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
// 创建新记录,type 使用交易所 ID 以便后续正确识别
|
||||
// Create new record, use exchange ID as type for correct identification
|
||||
var name, typ string
|
||||
switch id {
|
||||
case "binance":
|
||||
@@ -278,7 +278,7 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建交易所配置
|
||||
// Create creates exchange configuration
|
||||
func (s *ExchangeStore) Create(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool,
|
||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error {
|
||||
_, err := s.db.Exec(`
|
||||
|
||||
146
store/order.go
146
store/order.go
@@ -7,73 +7,73 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TraderOrder 交易员订单记录
|
||||
// TraderOrder trader order record
|
||||
type TraderOrder struct {
|
||||
ID int64 `json:"id"`
|
||||
TraderID string `json:"trader_id"` // 交易员ID
|
||||
OrderID string `json:"order_id"` // 交易所订单ID
|
||||
ClientOrderID string `json:"client_order_id"` // 客户端订单ID
|
||||
Symbol string `json:"symbol"` // 交易对
|
||||
TraderID string `json:"trader_id"` // Trader ID
|
||||
OrderID string `json:"order_id"` // Exchange order ID
|
||||
ClientOrderID string `json:"client_order_id"` // Client order ID
|
||||
Symbol string `json:"symbol"` // Trading pair
|
||||
Side string `json:"side"` // BUY/SELL
|
||||
PositionSide string `json:"position_side"` // LONG/SHORT/BOTH
|
||||
Action string `json:"action"` // open_long/close_long/open_short/close_short
|
||||
OrderType string `json:"order_type"` // MARKET/LIMIT
|
||||
Quantity float64 `json:"quantity"` // 订单数量
|
||||
Price float64 `json:"price"` // 订单价格
|
||||
AvgPrice float64 `json:"avg_price"` // 实际成交均价
|
||||
ExecutedQty float64 `json:"executed_qty"` // 已成交数量
|
||||
Leverage int `json:"leverage"` // 杠杆倍数
|
||||
Quantity float64 `json:"quantity"` // Order quantity
|
||||
Price float64 `json:"price"` // Order price
|
||||
AvgPrice float64 `json:"avg_price"` // Actual average execution price
|
||||
ExecutedQty float64 `json:"executed_qty"` // Executed quantity
|
||||
Leverage int `json:"leverage"` // Leverage multiplier
|
||||
Status string `json:"status"` // NEW/FILLED/CANCELED/EXPIRED
|
||||
Fee float64 `json:"fee"` // 手续费
|
||||
FeeAsset string `json:"fee_asset"` // 手续费资产
|
||||
RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏(平仓时)
|
||||
EntryPrice float64 `json:"entry_price"` // 开仓价(平仓时记录)
|
||||
Fee float64 `json:"fee"` // Fee
|
||||
FeeAsset string `json:"fee_asset"` // Fee asset
|
||||
RealizedPnL float64 `json:"realized_pnl"` // Realized PnL (when closing)
|
||||
EntryPrice float64 `json:"entry_price"` // Entry price (recorded when closing)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
FilledAt time.Time `json:"filled_at"` // 成交时间
|
||||
FilledAt time.Time `json:"filled_at"` // Filled time
|
||||
}
|
||||
|
||||
// TraderStats 交易统计指标
|
||||
// TraderStats trading statistics metrics
|
||||
type TraderStats struct {
|
||||
TotalTrades int `json:"total_trades"` // 总交易数(已平仓)
|
||||
WinTrades int `json:"win_trades"` // 盈利交易数
|
||||
LossTrades int `json:"loss_trades"` // 亏损交易数
|
||||
WinRate float64 `json:"win_rate"` // 胜率 (%)
|
||||
ProfitFactor float64 `json:"profit_factor"` // 盈亏比
|
||||
SharpeRatio float64 `json:"sharpe_ratio"` // 夏普比
|
||||
TotalPnL float64 `json:"total_pnl"` // 总盈亏
|
||||
TotalFee float64 `json:"total_fee"` // 总手续费
|
||||
AvgWin float64 `json:"avg_win"` // 平均盈利
|
||||
AvgLoss float64 `json:"avg_loss"` // 平均亏损
|
||||
MaxDrawdownPct float64 `json:"max_drawdown_pct"` // 最大回撤 (%)
|
||||
TotalTrades int `json:"total_trades"` // Total trades (closed)
|
||||
WinTrades int `json:"win_trades"` // Winning trades
|
||||
LossTrades int `json:"loss_trades"` // Losing trades
|
||||
WinRate float64 `json:"win_rate"` // Win rate (%)
|
||||
ProfitFactor float64 `json:"profit_factor"` // Profit factor
|
||||
SharpeRatio float64 `json:"sharpe_ratio"` // Sharpe ratio
|
||||
TotalPnL float64 `json:"total_pnl"` // Total PnL
|
||||
TotalFee float64 `json:"total_fee"` // Total fees
|
||||
AvgWin float64 `json:"avg_win"` // Average win
|
||||
AvgLoss float64 `json:"avg_loss"` // Average loss
|
||||
MaxDrawdownPct float64 `json:"max_drawdown_pct"` // Max drawdown (%)
|
||||
}
|
||||
|
||||
// CompletedOrder 已完成订单(用于AI输入)
|
||||
// CompletedOrder completed order (for AI input)
|
||||
type CompletedOrder struct {
|
||||
Symbol string `json:"symbol"` // 交易对
|
||||
Symbol string `json:"symbol"` // Trading pair
|
||||
Action string `json:"action"` // close_long/close_short
|
||||
Side string `json:"side"` // long/short
|
||||
Quantity float64 `json:"quantity"` // 数量
|
||||
EntryPrice float64 `json:"entry_price"` // 开仓价
|
||||
ExitPrice float64 `json:"exit_price"` // 平仓价
|
||||
RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏
|
||||
PnLPct float64 `json:"pnl_pct"` // 盈亏百分比
|
||||
Fee float64 `json:"fee"` // 手续费
|
||||
Leverage int `json:"leverage"` // 杠杆
|
||||
FilledAt time.Time `json:"filled_at"` // 成交时间
|
||||
Quantity float64 `json:"quantity"` // Quantity
|
||||
EntryPrice float64 `json:"entry_price"` // Entry price
|
||||
ExitPrice float64 `json:"exit_price"` // Exit price
|
||||
RealizedPnL float64 `json:"realized_pnl"` // Realized PnL
|
||||
PnLPct float64 `json:"pnl_pct"` // PnL percentage
|
||||
Fee float64 `json:"fee"` // Fee
|
||||
Leverage int `json:"leverage"` // Leverage
|
||||
FilledAt time.Time `json:"filled_at"` // Filled time
|
||||
}
|
||||
|
||||
// OrderStore 订单存储
|
||||
// OrderStore order storage
|
||||
type OrderStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewOrderStore 创建订单存储实例
|
||||
// NewOrderStore creates order storage instance
|
||||
func NewOrderStore(db *sql.DB) *OrderStore {
|
||||
return &OrderStore{db: db}
|
||||
}
|
||||
|
||||
// InitTables 初始化订单表
|
||||
// InitTables initializes order tables
|
||||
func (s *OrderStore) InitTables() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS trader_orders (
|
||||
@@ -103,10 +103,10 @@ func (s *OrderStore) InitTables() error {
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建trader_orders表失败: %w", err)
|
||||
return fmt.Errorf("failed to create trader_orders table: %w", err)
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
// Create indexes
|
||||
indices := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_trader_orders_trader ON trader_orders(trader_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_trader_orders_status ON trader_orders(trader_id, status)`,
|
||||
@@ -115,14 +115,14 @@ func (s *OrderStore) InitTables() error {
|
||||
}
|
||||
for _, idx := range indices {
|
||||
if _, err := s.db.Exec(idx); err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建订单记录
|
||||
// Create creates order record
|
||||
func (s *OrderStore) Create(order *TraderOrder) error {
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
result, err := s.db.Exec(`
|
||||
@@ -140,7 +140,7 @@ func (s *OrderStore) Create(order *TraderOrder) error {
|
||||
order.RealizedPnL, order.EntryPrice, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建订单记录失败: %w", err)
|
||||
return fmt.Errorf("failed to create order record: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
@@ -148,7 +148,7 @@ func (s *OrderStore) Create(order *TraderOrder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 更新订单记录
|
||||
// Update updates order record
|
||||
func (s *OrderStore) Update(order *TraderOrder) error {
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
filledAt := ""
|
||||
@@ -167,12 +167,12 @@ func (s *OrderStore) Update(order *TraderOrder) error {
|
||||
order.TraderID, order.OrderID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新订单记录失败: %w", err)
|
||||
return fmt.Errorf("failed to update order record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByOrderID 根据订单ID获取订单
|
||||
// GetByOrderID gets order by order ID
|
||||
func (s *OrderStore) GetByOrderID(traderID, orderID string) (*TraderOrder, error) {
|
||||
var order TraderOrder
|
||||
var createdAt, updatedAt, filledAt sql.NullString
|
||||
@@ -208,9 +208,9 @@ func (s *OrderStore) GetByOrderID(traderID, orderID string) (*TraderOrder, error
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// GetLatestOpenOrder 获取某币种最近的开仓订单(用于计算平仓盈亏)
|
||||
// GetLatestOpenOrder gets the latest open order for a symbol (for calculating close PnL)
|
||||
func (s *OrderStore) GetLatestOpenOrder(traderID, symbol, side string) (*TraderOrder, error) {
|
||||
// side: long -> 找 open_long, short -> 找 open_short
|
||||
// side: long -> find open_long, short -> find open_short
|
||||
action := "open_long"
|
||||
if side == "short" {
|
||||
action = "open_short"
|
||||
@@ -252,7 +252,7 @@ func (s *OrderStore) GetLatestOpenOrder(traderID, symbol, side string) (*TraderO
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
// GetRecentCompletedOrders 获取最近已完成的平仓订单
|
||||
// GetRecentCompletedOrders gets recent completed close orders
|
||||
func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]CompletedOrder, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT symbol, action, side, executed_qty, entry_price, avg_price,
|
||||
@@ -264,7 +264,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com
|
||||
LIMIT ?
|
||||
`, traderID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询已完成订单失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query completed orders: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -282,7 +282,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据action推断side
|
||||
// Infer side from action
|
||||
if o.Action == "close_long" {
|
||||
o.Side = "long"
|
||||
} else if o.Action == "close_short" {
|
||||
@@ -291,7 +291,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com
|
||||
o.Side = side.String
|
||||
}
|
||||
|
||||
// 计算盈亏百分比
|
||||
// Calculate PnL percentage
|
||||
if o.EntryPrice > 0 {
|
||||
if o.Side == "long" {
|
||||
o.PnLPct = (o.ExitPrice - o.EntryPrice) / o.EntryPrice * 100 * float64(o.Leverage)
|
||||
@@ -310,11 +310,11 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
// GetTraderStats 获取交易统计指标
|
||||
// GetTraderStats gets trading statistics metrics
|
||||
func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
|
||||
stats := &TraderStats{}
|
||||
|
||||
// 查询所有已完成的平仓订单
|
||||
// Query all completed close orders
|
||||
rows, err := s.db.Query(`
|
||||
SELECT realized_pnl, fee, filled_at
|
||||
FROM trader_orders
|
||||
@@ -323,7 +323,7 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
|
||||
ORDER BY filled_at ASC
|
||||
`, traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询订单统计失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query order statistics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -351,17 +351,17 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算胜率
|
||||
// Calculate win rate
|
||||
if stats.TotalTrades > 0 {
|
||||
stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100
|
||||
}
|
||||
|
||||
// 计算盈亏比
|
||||
// Calculate profit factor
|
||||
if totalLoss > 0 {
|
||||
stats.ProfitFactor = totalWin / totalLoss
|
||||
}
|
||||
|
||||
// 计算平均盈亏
|
||||
// Calculate average win/loss
|
||||
if stats.WinTrades > 0 {
|
||||
stats.AvgWin = totalWin / float64(stats.WinTrades)
|
||||
}
|
||||
@@ -369,12 +369,12 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
|
||||
stats.AvgLoss = totalLoss / float64(stats.LossTrades)
|
||||
}
|
||||
|
||||
// 计算夏普比(使用盈亏序列)
|
||||
// Calculate Sharpe ratio (using PnL sequence)
|
||||
if len(pnls) > 1 {
|
||||
stats.SharpeRatio = calculateSharpeRatio(pnls)
|
||||
}
|
||||
|
||||
// 计算最大回撤
|
||||
// Calculate max drawdown
|
||||
if len(pnls) > 0 {
|
||||
stats.MaxDrawdownPct = calculateMaxDrawdown(pnls)
|
||||
}
|
||||
@@ -382,20 +382,20 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// calculateSharpeRatio 计算夏普比
|
||||
// calculateSharpeRatio calculates Sharpe ratio
|
||||
func calculateSharpeRatio(pnls []float64) float64 {
|
||||
if len(pnls) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算平均收益
|
||||
// Calculate average return
|
||||
var sum float64
|
||||
for _, pnl := range pnls {
|
||||
sum += pnl
|
||||
}
|
||||
mean := sum / float64(len(pnls))
|
||||
|
||||
// 计算标准差
|
||||
// Calculate standard deviation
|
||||
var variance float64
|
||||
for _, pnl := range pnls {
|
||||
variance += (pnl - mean) * (pnl - mean)
|
||||
@@ -406,17 +406,17 @@ func calculateSharpeRatio(pnls []float64) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 夏普比 = 平均收益 / 标准差
|
||||
// Sharpe ratio = average return / standard deviation
|
||||
return mean / stdDev
|
||||
}
|
||||
|
||||
// calculateMaxDrawdown 计算最大回撤
|
||||
// calculateMaxDrawdown calculates max drawdown
|
||||
func calculateMaxDrawdown(pnls []float64) float64 {
|
||||
if len(pnls) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算累计权益曲线
|
||||
// Calculate cumulative equity curve
|
||||
var cumulative float64
|
||||
var peak float64
|
||||
var maxDD float64
|
||||
@@ -437,7 +437,7 @@ func calculateMaxDrawdown(pnls []float64) float64 {
|
||||
return maxDD
|
||||
}
|
||||
|
||||
// GetPendingOrders 获取未成交的订单(用于轮询)
|
||||
// GetPendingOrders gets pending orders (for polling)
|
||||
func (s *OrderStore) GetPendingOrders(traderID string) ([]*TraderOrder, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side,
|
||||
@@ -449,14 +449,14 @@ func (s *OrderStore) GetPendingOrders(traderID string) ([]*TraderOrder, error) {
|
||||
ORDER BY created_at ASC
|
||||
`, traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询未成交订单失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query pending orders: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return s.scanOrders(rows)
|
||||
}
|
||||
|
||||
// GetAllPendingOrders 获取所有未成交的订单(用于全局同步)
|
||||
// GetAllPendingOrders gets all pending orders (for global sync)
|
||||
func (s *OrderStore) GetAllPendingOrders() ([]*TraderOrder, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side,
|
||||
@@ -468,14 +468,14 @@ func (s *OrderStore) GetAllPendingOrders() ([]*TraderOrder, error) {
|
||||
ORDER BY trader_id, created_at ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询未成交订单失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query pending orders: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return s.scanOrders(rows)
|
||||
}
|
||||
|
||||
// scanOrders 扫描订单行到结构体
|
||||
// scanOrders scans order rows to structs
|
||||
func (s *OrderStore) scanOrders(rows *sql.Rows) ([]*TraderOrder, error) {
|
||||
var orders []*TraderOrder
|
||||
for rows.Next() {
|
||||
|
||||
@@ -7,40 +7,40 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TraderPosition 仓位记录(完整的开平仓追踪)
|
||||
// TraderPosition position record (complete open/close position tracking)
|
||||
type TraderPosition struct {
|
||||
ID int64 `json:"id"`
|
||||
TraderID string `json:"trader_id"`
|
||||
ExchangeID string `json:"exchange_id"` // 交易所ID: binance/bybit/hyperliquid/aster/lighter
|
||||
ExchangeID string `json:"exchange_id"` // Exchange ID: binance/bybit/hyperliquid/aster/lighter
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"` // LONG/SHORT
|
||||
Quantity float64 `json:"quantity"` // 开仓数量
|
||||
EntryPrice float64 `json:"entry_price"` // 开仓均价
|
||||
EntryOrderID string `json:"entry_order_id"` // 开仓订单ID
|
||||
EntryTime time.Time `json:"entry_time"` // 开仓时间
|
||||
ExitPrice float64 `json:"exit_price"` // 平仓均价
|
||||
ExitOrderID string `json:"exit_order_id"` // 平仓订单ID
|
||||
ExitTime *time.Time `json:"exit_time"` // 平仓时间
|
||||
RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏
|
||||
Fee float64 `json:"fee"` // 手续费
|
||||
Leverage int `json:"leverage"` // 杠杆倍数
|
||||
Quantity float64 `json:"quantity"` // Opening quantity
|
||||
EntryPrice float64 `json:"entry_price"` // Entry price
|
||||
EntryOrderID string `json:"entry_order_id"` // Entry order ID
|
||||
EntryTime time.Time `json:"entry_time"` // Entry time
|
||||
ExitPrice float64 `json:"exit_price"` // Exit price
|
||||
ExitOrderID string `json:"exit_order_id"` // Exit order ID
|
||||
ExitTime *time.Time `json:"exit_time"` // Exit time
|
||||
RealizedPnL float64 `json:"realized_pnl"` // Realized profit and loss
|
||||
Fee float64 `json:"fee"` // Fee
|
||||
Leverage int `json:"leverage"` // Leverage multiplier
|
||||
Status string `json:"status"` // OPEN/CLOSED
|
||||
CloseReason string `json:"close_reason"` // 平仓原因: ai_decision/manual/stop_loss/take_profit
|
||||
CloseReason string `json:"close_reason"` // Close reason: ai_decision/manual/stop_loss/take_profit
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PositionStore 仓位存储
|
||||
// PositionStore position storage
|
||||
type PositionStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewPositionStore 创建仓位存储实例
|
||||
// NewPositionStore creates position storage instance
|
||||
func NewPositionStore(db *sql.DB) *PositionStore {
|
||||
return &PositionStore{db: db}
|
||||
}
|
||||
|
||||
// InitTables 初始化仓位表
|
||||
// InitTables initializes position tables
|
||||
func (s *PositionStore) InitTables() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS trader_positions (
|
||||
@@ -66,14 +66,14 @@ func (s *PositionStore) InitTables() error {
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建trader_positions表失败: %w", err)
|
||||
return fmt.Errorf("failed to create trader_positions table: %w", err)
|
||||
}
|
||||
|
||||
// 迁移:为现有表添加 exchange_id 列(如果不存在)
|
||||
// 必须在创建索引之前执行!
|
||||
// Migration: add exchange_id column to existing table (if not exists)
|
||||
// Must be executed before creating indexes!
|
||||
s.db.Exec(`ALTER TABLE trader_positions ADD COLUMN exchange_id TEXT NOT NULL DEFAULT ''`)
|
||||
|
||||
// 创建索引(在迁移之后)
|
||||
// Create indexes (after migration)
|
||||
indices := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_positions_trader ON trader_positions(trader_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_positions_exchange ON trader_positions(exchange_id)`,
|
||||
@@ -84,14 +84,14 @@ func (s *PositionStore) InitTables() error {
|
||||
}
|
||||
for _, idx := range indices {
|
||||
if _, err := s.db.Exec(idx); err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建仓位记录(开仓时调用)
|
||||
// Create creates position record (called when opening position)
|
||||
func (s *PositionStore) Create(pos *TraderPosition) error {
|
||||
now := time.Now()
|
||||
pos.CreatedAt = now
|
||||
@@ -109,7 +109,7 @@ func (s *PositionStore) Create(pos *TraderPosition) error {
|
||||
pos.Status, now.Format(time.RFC3339), now.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建仓位记录失败: %w", err)
|
||||
return fmt.Errorf("failed to create position record: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
@@ -117,7 +117,7 @@ func (s *PositionStore) Create(pos *TraderPosition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosePosition 平仓(更新仓位记录)
|
||||
// ClosePosition closes position (updates position record)
|
||||
func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID string, realizedPnL float64, fee float64, closeReason string) error {
|
||||
now := time.Now()
|
||||
_, err := s.db.Exec(`
|
||||
@@ -131,12 +131,12 @@ func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID s
|
||||
realizedPnL, fee, closeReason, now.Format(time.RFC3339), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新仓位记录失败: %w", err)
|
||||
return fmt.Errorf("failed to update position record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOpenPositions 获取所有未平仓位
|
||||
// GetOpenPositions gets all open positions
|
||||
func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id,
|
||||
@@ -147,14 +147,14 @@ func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, er
|
||||
ORDER BY entry_time DESC
|
||||
`, traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询未平仓位失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query open positions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return s.scanPositions(rows)
|
||||
}
|
||||
|
||||
// GetOpenPositionBySymbol 获取指定币种方向的未平仓位
|
||||
// GetOpenPositionBySymbol gets open position for specified symbol and direction
|
||||
func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) {
|
||||
var pos TraderPosition
|
||||
var entryTime, exitTime, createdAt, updatedAt sql.NullString
|
||||
@@ -183,7 +183,7 @@ func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (
|
||||
return &pos, nil
|
||||
}
|
||||
|
||||
// GetClosedPositions 获取已平仓位(历史记录)
|
||||
// GetClosedPositions gets closed positions (historical records)
|
||||
func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*TraderPosition, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id,
|
||||
@@ -195,14 +195,14 @@ func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*Trade
|
||||
LIMIT ?
|
||||
`, traderID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询已平仓位失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query closed positions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return s.scanPositions(rows)
|
||||
}
|
||||
|
||||
// GetAllOpenPositions 获取所有trader的未平仓位(用于全局同步)
|
||||
// GetAllOpenPositions gets all traders' open positions (for global sync)
|
||||
func (s *PositionStore) GetAllOpenPositions() ([]*TraderPosition, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id,
|
||||
@@ -213,18 +213,18 @@ func (s *PositionStore) GetAllOpenPositions() ([]*TraderPosition, error) {
|
||||
ORDER BY trader_id, entry_time DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询所有未平仓位失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query all open positions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return s.scanPositions(rows)
|
||||
}
|
||||
|
||||
// GetPositionStats 获取仓位统计(简单版)
|
||||
// GetPositionStats gets position statistics (simplified version)
|
||||
func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// 总交易数
|
||||
// Total trades
|
||||
var totalTrades, winTrades int
|
||||
var totalPnL, totalFee float64
|
||||
|
||||
@@ -254,11 +254,11 @@ func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetFullStats 获取完整的交易统计(与 TraderStats 兼容)
|
||||
// GetFullStats gets complete trading statistics (compatible with TraderStats)
|
||||
func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
stats := &TraderStats{}
|
||||
|
||||
// 查询所有已平仓位
|
||||
// Query all closed positions
|
||||
rows, err := s.db.Query(`
|
||||
SELECT realized_pnl, fee, exit_time
|
||||
FROM trader_positions
|
||||
@@ -266,7 +266,7 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
ORDER BY exit_time ASC
|
||||
`, traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询仓位统计失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query position statistics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -290,21 +290,21 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
totalWin += pnl
|
||||
} else if pnl < 0 {
|
||||
stats.LossTrades++
|
||||
totalLoss += -pnl // 转为正数
|
||||
totalLoss += -pnl // Convert to positive
|
||||
}
|
||||
}
|
||||
|
||||
// 计算胜率
|
||||
// Calculate win rate
|
||||
if stats.TotalTrades > 0 {
|
||||
stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100
|
||||
}
|
||||
|
||||
// 计算盈亏比
|
||||
// Calculate profit factor
|
||||
if totalLoss > 0 {
|
||||
stats.ProfitFactor = totalWin / totalLoss
|
||||
}
|
||||
|
||||
// 计算平均盈亏
|
||||
// Calculate average profit/loss
|
||||
if stats.WinTrades > 0 {
|
||||
stats.AvgWin = totalWin / float64(stats.WinTrades)
|
||||
}
|
||||
@@ -312,12 +312,12 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
stats.AvgLoss = totalLoss / float64(stats.LossTrades)
|
||||
}
|
||||
|
||||
// 计算夏普比
|
||||
// Calculate Sharpe ratio
|
||||
if len(pnls) > 1 {
|
||||
stats.SharpeRatio = calculateSharpeRatioFromPnls(pnls)
|
||||
}
|
||||
|
||||
// 计算最大回撤
|
||||
// Calculate maximum drawdown
|
||||
if len(pnls) > 0 {
|
||||
stats.MaxDrawdownPct = calculateMaxDrawdownFromPnls(pnls)
|
||||
}
|
||||
@@ -325,7 +325,7 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// RecentTrade 最近的交易记录(用于AI输入)
|
||||
// RecentTrade recent trade record (for AI input)
|
||||
type RecentTrade struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"` // long/short
|
||||
@@ -336,7 +336,7 @@ type RecentTrade struct {
|
||||
ExitTime string `json:"exit_time"`
|
||||
}
|
||||
|
||||
// GetRecentTrades 获取最近的已平仓交易
|
||||
// GetRecentTrades gets recent closed trades
|
||||
func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTrade, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT symbol, side, entry_price, exit_price, realized_pnl, leverage, exit_time
|
||||
@@ -346,7 +346,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra
|
||||
LIMIT ?
|
||||
`, traderID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询最近交易失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to query recent trades: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -361,14 +361,14 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra
|
||||
continue
|
||||
}
|
||||
|
||||
// 转换 side 格式
|
||||
// Convert side format
|
||||
if t.Side == "LONG" {
|
||||
t.Side = "long"
|
||||
} else if t.Side == "SHORT" {
|
||||
t.Side = "short"
|
||||
}
|
||||
|
||||
// 计算盈亏百分比
|
||||
// Calculate profit/loss percentage
|
||||
if t.EntryPrice > 0 {
|
||||
if t.Side == "long" {
|
||||
t.PnLPct = (t.ExitPrice - t.EntryPrice) / t.EntryPrice * 100 * float64(leverage)
|
||||
@@ -377,7 +377,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
// Format time
|
||||
if exitTime.Valid {
|
||||
if parsed, err := time.Parse(time.RFC3339, exitTime.String); err == nil {
|
||||
t.ExitTime = parsed.Format("01-02 15:04")
|
||||
@@ -390,7 +390,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// calculateSharpeRatioFromPnls 计算夏普比
|
||||
// calculateSharpeRatioFromPnls calculates Sharpe ratio
|
||||
func calculateSharpeRatioFromPnls(pnls []float64) float64 {
|
||||
if len(pnls) < 2 {
|
||||
return 0
|
||||
@@ -415,7 +415,7 @@ func calculateSharpeRatioFromPnls(pnls []float64) float64 {
|
||||
return mean / stdDev
|
||||
}
|
||||
|
||||
// calculateMaxDrawdownFromPnls 计算最大回撤
|
||||
// calculateMaxDrawdownFromPnls calculates maximum drawdown
|
||||
func calculateMaxDrawdownFromPnls(pnls []float64) float64 {
|
||||
if len(pnls) == 0 {
|
||||
return 0
|
||||
@@ -438,7 +438,7 @@ func calculateMaxDrawdownFromPnls(pnls []float64) float64 {
|
||||
return maxDD
|
||||
}
|
||||
|
||||
// scanPositions 扫描仓位行到结构体
|
||||
// scanPositions scans position rows into structs
|
||||
func (s *PositionStore) scanPositions(rows *sql.Rows) ([]*TraderPosition, error) {
|
||||
var positions []*TraderPosition
|
||||
for rows.Next() {
|
||||
@@ -462,7 +462,7 @@ func (s *PositionStore) scanPositions(rows *sql.Rows) ([]*TraderPosition, error)
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// parsePositionTimes 解析时间字段
|
||||
// parsePositionTimes parses time fields
|
||||
func (s *PositionStore) parsePositionTimes(pos *TraderPosition, entryTime, exitTime, createdAt, updatedAt sql.NullString) {
|
||||
if entryTime.Valid {
|
||||
pos.EntryTime, _ = time.Parse(time.RFC3339, entryTime.String)
|
||||
|
||||
114
store/store.go
114
store/store.go
@@ -1,5 +1,5 @@
|
||||
// Package store 提供统一的数据库存储层
|
||||
// 所有数据库操作都应该通过这个包进行
|
||||
// Package store provides unified database storage layer
|
||||
// All database operations should go through this package
|
||||
package store
|
||||
|
||||
import (
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Store 统一的数据存储接口
|
||||
// Store unified data storage interface
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
|
||||
// 子存储(延迟初始化)
|
||||
// Sub-stores (lazy initialization)
|
||||
user *UserStore
|
||||
aiModel *AIModelStore
|
||||
exchange *ExchangeStore
|
||||
@@ -27,80 +27,80 @@ type Store struct {
|
||||
strategy *StrategyStore
|
||||
equity *EquityStore
|
||||
|
||||
// 加密函数
|
||||
// Encryption functions
|
||||
encryptFunc func(string) string
|
||||
decryptFunc func(string) string
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// New 创建新的 Store 实例
|
||||
// New creates new Store instance
|
||||
func New(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// SQLite 配置
|
||||
// SQLite configuration
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
// 启用外键约束
|
||||
// Enable foreign key constraints
|
||||
if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("启用外键失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
// 使用 DELETE 模式(传统模式)以确保 Docker bind mount 兼容性
|
||||
// 注意:WAL 模式在 macOS Docker 下会导致数据同步问题
|
||||
// Use DELETE mode (traditional mode) to ensure Docker bind mount compatibility
|
||||
// Note: WAL mode causes data sync issues on macOS Docker
|
||||
if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("设置journal_mode失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to set journal_mode: %w", err)
|
||||
}
|
||||
|
||||
// 设置 synchronous=FULL
|
||||
// Set synchronous=FULL
|
||||
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("设置synchronous失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to set synchronous: %w", err)
|
||||
}
|
||||
|
||||
// 设置 busy_timeout
|
||||
// Set busy_timeout
|
||||
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("设置busy_timeout失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
|
||||
}
|
||||
|
||||
s := &Store{db: db}
|
||||
|
||||
// 初始化所有表结构
|
||||
// Initialize all table structures
|
||||
if err := s.initTables(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("初始化表结构失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
|
||||
}
|
||||
|
||||
// 初始化默认数据
|
||||
// Initialize default data
|
||||
if err := s.initDefaultData(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("初始化默认数据失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to initialize default data: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("✅ 数据库已启用 DELETE 模式和 FULL 同步")
|
||||
logger.Info("✅ Database enabled DELETE mode and FULL sync")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewFromDB 从现有数据库连接创建 Store
|
||||
// NewFromDB creates Store from existing database connection
|
||||
func NewFromDB(db *sql.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
// SetCryptoFuncs 设置加密解密函数
|
||||
// SetCryptoFuncs sets encryption/decryption functions
|
||||
func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.encryptFunc = encrypt
|
||||
s.decryptFunc = decrypt
|
||||
|
||||
// 更新已初始化的子存储
|
||||
// Update already initialized sub-stores
|
||||
if s.aiModel != nil {
|
||||
s.aiModel.encryptFunc = encrypt
|
||||
s.aiModel.decryptFunc = decrypt
|
||||
@@ -114,43 +114,43 @@ func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) {
|
||||
}
|
||||
}
|
||||
|
||||
// initTables 初始化所有数据库表
|
||||
// initTables initializes all database tables
|
||||
func (s *Store) initTables() error {
|
||||
// 按依赖顺序初始化
|
||||
// Initialize in dependency order
|
||||
if err := s.User().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化用户表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize user tables: %w", err)
|
||||
}
|
||||
if err := s.AIModel().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化AI模型表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize AI model tables: %w", err)
|
||||
}
|
||||
if err := s.Exchange().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化交易所表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize exchange tables: %w", err)
|
||||
}
|
||||
if err := s.Trader().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化交易员表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize trader tables: %w", err)
|
||||
}
|
||||
if err := s.Decision().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化决策日志表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize decision log tables: %w", err)
|
||||
}
|
||||
if err := s.Backtest().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化回测表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize backtest tables: %w", err)
|
||||
}
|
||||
if err := s.Order().InitTables(); err != nil {
|
||||
return fmt.Errorf("初始化订单表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize order tables: %w", err)
|
||||
}
|
||||
if err := s.Position().InitTables(); err != nil {
|
||||
return fmt.Errorf("初始化仓位表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize position tables: %w", err)
|
||||
}
|
||||
if err := s.Strategy().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化策略表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize strategy tables: %w", err)
|
||||
}
|
||||
if err := s.Equity().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化净值表失败: %w", err)
|
||||
return fmt.Errorf("failed to initialize equity tables: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initDefaultData 初始化默认数据
|
||||
// initDefaultData initializes default data
|
||||
func (s *Store) initDefaultData() error {
|
||||
if err := s.AIModel().initDefaultData(); err != nil {
|
||||
return err
|
||||
@@ -161,16 +161,16 @@ func (s *Store) initDefaultData() error {
|
||||
if err := s.Strategy().initDefaultData(); err != nil {
|
||||
return err
|
||||
}
|
||||
// 迁移旧的 decision_account_snapshots 数据到新的 trader_equity_snapshots 表
|
||||
// Migrate old decision_account_snapshots data to new trader_equity_snapshots table
|
||||
if migrated, err := s.Equity().MigrateFromDecision(); err != nil {
|
||||
logger.Warnf("迁移净值数据失败: %v", err)
|
||||
logger.Warnf("failed to migrate equity data: %v", err)
|
||||
} else if migrated > 0 {
|
||||
logger.Infof("✅ 已迁移 %d 条净值数据到新表", migrated)
|
||||
logger.Infof("✅ Migrated %d equity records to new table", migrated)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// User 获取用户存储
|
||||
// User gets user storage
|
||||
func (s *Store) User() *UserStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -180,7 +180,7 @@ func (s *Store) User() *UserStore {
|
||||
return s.user
|
||||
}
|
||||
|
||||
// AIModel 获取AI模型存储
|
||||
// AIModel gets AI model storage
|
||||
func (s *Store) AIModel() *AIModelStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -194,7 +194,7 @@ func (s *Store) AIModel() *AIModelStore {
|
||||
return s.aiModel
|
||||
}
|
||||
|
||||
// Exchange 获取交易所存储
|
||||
// Exchange gets exchange storage
|
||||
func (s *Store) Exchange() *ExchangeStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -208,7 +208,7 @@ func (s *Store) Exchange() *ExchangeStore {
|
||||
return s.exchange
|
||||
}
|
||||
|
||||
// Trader 获取交易员存储
|
||||
// Trader gets trader storage
|
||||
func (s *Store) Trader() *TraderStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -221,7 +221,7 @@ func (s *Store) Trader() *TraderStore {
|
||||
return s.trader
|
||||
}
|
||||
|
||||
// Decision 获取决策日志存储
|
||||
// Decision gets decision log storage
|
||||
func (s *Store) Decision() *DecisionStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -231,7 +231,7 @@ func (s *Store) Decision() *DecisionStore {
|
||||
return s.decision
|
||||
}
|
||||
|
||||
// Backtest 获取回测数据存储
|
||||
// Backtest gets backtest data storage
|
||||
func (s *Store) Backtest() *BacktestStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -241,7 +241,7 @@ func (s *Store) Backtest() *BacktestStore {
|
||||
return s.backtest
|
||||
}
|
||||
|
||||
// Order 获取订单存储
|
||||
// Order gets order storage
|
||||
func (s *Store) Order() *OrderStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -251,7 +251,7 @@ func (s *Store) Order() *OrderStore {
|
||||
return s.order
|
||||
}
|
||||
|
||||
// Position 获取仓位存储
|
||||
// Position gets position storage
|
||||
func (s *Store) Position() *PositionStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -261,7 +261,7 @@ func (s *Store) Position() *PositionStore {
|
||||
return s.position
|
||||
}
|
||||
|
||||
// Strategy 获取策略存储
|
||||
// Strategy gets strategy storage
|
||||
func (s *Store) Strategy() *StrategyStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -271,7 +271,7 @@ func (s *Store) Strategy() *StrategyStore {
|
||||
return s.strategy
|
||||
}
|
||||
|
||||
// Equity 获取净值存储
|
||||
// Equity gets equity storage
|
||||
func (s *Store) Equity() *EquityStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -281,22 +281,22 @@ func (s *Store) Equity() *EquityStore {
|
||||
return s.equity
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
// Close closes database connection
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// DB 获取底层数据库连接(仅用于兼容旧代码,逐步废弃)
|
||||
// Deprecated: 使用 Store 的方法代替
|
||||
// DB gets underlying database connection (for legacy code compatibility, gradually deprecated)
|
||||
// Deprecated: use Store methods instead
|
||||
func (s *Store) DB() *sql.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
// Transaction 执行事务
|
||||
// Transaction executes transaction
|
||||
func (s *Store) Transaction(fn func(tx *sql.Tx) error) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始事务失败: %w", err)
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
@@ -305,7 +305,7 @@ func (s *Store) Transaction(fn func(tx *sql.Tx) error) error {
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交事务失败: %w", err)
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,139 +7,139 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StrategyStore 策略存储
|
||||
// StrategyStore strategy storage
|
||||
type StrategyStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Strategy 策略配置
|
||||
// Strategy strategy configuration
|
||||
type Strategy struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"` // 是否激活(一个用户只能有一个激活的策略)
|
||||
IsDefault bool `json:"is_default"` // 是否为系统默认策略
|
||||
Config string `json:"config"` // JSON 格式的策略配置
|
||||
IsActive bool `json:"is_active"` // whether it is active (a user can only have one active strategy)
|
||||
IsDefault bool `json:"is_default"` // whether it is a system default strategy
|
||||
Config string `json:"config"` // strategy configuration in JSON format
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// StrategyConfig 策略配置详情(JSON 结构)
|
||||
// StrategyConfig strategy configuration details (JSON structure)
|
||||
type StrategyConfig struct {
|
||||
// 币种来源配置
|
||||
// coin source configuration
|
||||
CoinSource CoinSourceConfig `json:"coin_source"`
|
||||
// 量化数据配置
|
||||
// quantitative data configuration
|
||||
Indicators IndicatorConfig `json:"indicators"`
|
||||
// 自定义 Prompt(附加在最后)
|
||||
// custom prompt (appended at the end)
|
||||
CustomPrompt string `json:"custom_prompt,omitempty"`
|
||||
// 风险控制配置
|
||||
// risk control configuration
|
||||
RiskControl RiskControlConfig `json:"risk_control"`
|
||||
// System Prompt 可编辑部分
|
||||
// editable sections of System Prompt
|
||||
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
|
||||
}
|
||||
|
||||
// PromptSectionsConfig System Prompt 可编辑部分
|
||||
// PromptSectionsConfig editable sections of System Prompt
|
||||
type PromptSectionsConfig struct {
|
||||
// 角色定义(标题+描述)
|
||||
// role definition (title + description)
|
||||
RoleDefinition string `json:"role_definition,omitempty"`
|
||||
// 交易频率认知
|
||||
// trading frequency awareness
|
||||
TradingFrequency string `json:"trading_frequency,omitempty"`
|
||||
// 开仓标准
|
||||
// entry standards
|
||||
EntryStandards string `json:"entry_standards,omitempty"`
|
||||
// 决策流程
|
||||
// decision process
|
||||
DecisionProcess string `json:"decision_process,omitempty"`
|
||||
}
|
||||
|
||||
// CoinSourceConfig 币种来源配置
|
||||
// CoinSourceConfig coin source configuration
|
||||
type CoinSourceConfig struct {
|
||||
// 来源类型: "static" | "coinpool" | "oi_top" | "mixed"
|
||||
// source type: "static" | "coinpool" | "oi_top" | "mixed"
|
||||
SourceType string `json:"source_type"`
|
||||
// 静态币种列表(当 source_type = "static" 时使用)
|
||||
// static coin list (used when source_type = "static")
|
||||
StaticCoins []string `json:"static_coins,omitempty"`
|
||||
// 是否使用 AI500 币种池
|
||||
// whether to use AI500 coin pool
|
||||
UseCoinPool bool `json:"use_coin_pool"`
|
||||
// AI500 币种池最大数量
|
||||
// AI500 coin pool maximum count
|
||||
CoinPoolLimit int `json:"coin_pool_limit,omitempty"`
|
||||
// AI500 币种池 API URL(策略级别配置)
|
||||
// AI500 coin pool API URL (strategy-level configuration)
|
||||
CoinPoolAPIURL string `json:"coin_pool_api_url,omitempty"`
|
||||
// 是否使用 OI Top
|
||||
// whether to use OI Top
|
||||
UseOITop bool `json:"use_oi_top"`
|
||||
// OI Top 最大数量
|
||||
// OI Top maximum count
|
||||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||||
// OI Top API URL(策略级别配置)
|
||||
// OI Top API URL (strategy-level configuration)
|
||||
OITopAPIURL string `json:"oi_top_api_url,omitempty"`
|
||||
}
|
||||
|
||||
// IndicatorConfig 指标配置
|
||||
// IndicatorConfig indicator configuration
|
||||
type IndicatorConfig struct {
|
||||
// K线配置
|
||||
// K-line configuration
|
||||
Klines KlineConfig `json:"klines"`
|
||||
// 技术指标开关
|
||||
// technical indicator switches
|
||||
EnableEMA bool `json:"enable_ema"`
|
||||
EnableMACD bool `json:"enable_macd"`
|
||||
EnableRSI bool `json:"enable_rsi"`
|
||||
EnableATR bool `json:"enable_atr"`
|
||||
EnableVolume bool `json:"enable_volume"`
|
||||
EnableOI bool `json:"enable_oi"` // 持仓量
|
||||
EnableFundingRate bool `json:"enable_funding_rate"` // 资金费率
|
||||
// EMA 周期配置
|
||||
EMAPeriods []int `json:"ema_periods,omitempty"` // 默认 [20, 50]
|
||||
// RSI 周期配置
|
||||
RSIPeriods []int `json:"rsi_periods,omitempty"` // 默认 [7, 14]
|
||||
// ATR 周期配置
|
||||
ATRPeriods []int `json:"atr_periods,omitempty"` // 默认 [14]
|
||||
// 外部数据源
|
||||
EnableOI bool `json:"enable_oi"` // open interest
|
||||
EnableFundingRate bool `json:"enable_funding_rate"` // funding rate
|
||||
// EMA period configuration
|
||||
EMAPeriods []int `json:"ema_periods,omitempty"` // default [20, 50]
|
||||
// RSI period configuration
|
||||
RSIPeriods []int `json:"rsi_periods,omitempty"` // default [7, 14]
|
||||
// ATR period configuration
|
||||
ATRPeriods []int `json:"atr_periods,omitempty"` // default [14]
|
||||
// external data sources
|
||||
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
|
||||
// 量化数据源(资金流向、持仓变化、价格变化)
|
||||
EnableQuantData bool `json:"enable_quant_data"` // 是否启用量化数据
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // 量化数据 API 地址
|
||||
// quantitative data sources (capital flow, position changes, price changes)
|
||||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address
|
||||
}
|
||||
|
||||
// KlineConfig K线配置
|
||||
// KlineConfig K-line configuration
|
||||
type KlineConfig struct {
|
||||
// 主时间周期: "1m", "3m", "5m", "15m", "1h", "4h"
|
||||
// primary timeframe: "1m", "3m", "5m", "15m", "1h", "4h"
|
||||
PrimaryTimeframe string `json:"primary_timeframe"`
|
||||
// 主时间周期 K 线数量
|
||||
// primary timeframe K-line count
|
||||
PrimaryCount int `json:"primary_count"`
|
||||
// 长周期时间框架
|
||||
// longer timeframe
|
||||
LongerTimeframe string `json:"longer_timeframe,omitempty"`
|
||||
// 长周期 K 线数量
|
||||
// longer timeframe K-line count
|
||||
LongerCount int `json:"longer_count,omitempty"`
|
||||
// 是否启用多时间框架分析
|
||||
// whether to enable multi-timeframe analysis
|
||||
EnableMultiTimeframe bool `json:"enable_multi_timeframe"`
|
||||
// 选中的时间周期列表(新增:支持多周期选择)
|
||||
// selected timeframe list (new: supports multi-timeframe selection)
|
||||
SelectedTimeframes []string `json:"selected_timeframes,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalDataSource 外部数据源配置
|
||||
// ExternalDataSource external data source configuration
|
||||
type ExternalDataSource struct {
|
||||
Name string `json:"name"` // 数据源名称
|
||||
Type string `json:"type"` // 类型: "api" | "webhook"
|
||||
Name string `json:"name"` // data source name
|
||||
Type string `json:"type"` // type: "api" | "webhook"
|
||||
URL string `json:"url"` // API URL
|
||||
Method string `json:"method"` // HTTP 方法
|
||||
Method string `json:"method"` // HTTP method
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
DataPath string `json:"data_path,omitempty"` // JSON 数据路径
|
||||
RefreshSecs int `json:"refresh_secs,omitempty"` // 刷新间隔(秒)
|
||||
DataPath string `json:"data_path,omitempty"` // JSON data path
|
||||
RefreshSecs int `json:"refresh_secs,omitempty"` // refresh interval (seconds)
|
||||
}
|
||||
|
||||
// RiskControlConfig 风险控制配置
|
||||
// RiskControlConfig risk control configuration
|
||||
type RiskControlConfig struct {
|
||||
// 最大持仓数量
|
||||
// maximum number of positions
|
||||
MaxPositions int `json:"max_positions"`
|
||||
// BTC/ETH 最大杠杆
|
||||
// BTC/ETH maximum leverage
|
||||
BTCETHMaxLeverage int `json:"btc_eth_max_leverage"`
|
||||
// 山寨币最大杠杆
|
||||
// altcoin maximum leverage
|
||||
AltcoinMaxLeverage int `json:"altcoin_max_leverage"`
|
||||
// 最小风险回报比
|
||||
// minimum risk-reward ratio
|
||||
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
|
||||
// 最大保证金使用率
|
||||
// maximum margin usage
|
||||
MaxMarginUsage float64 `json:"max_margin_usage"`
|
||||
// 单币种最大仓位比例(相对账户净值)
|
||||
// maximum position ratio per coin (relative to account equity)
|
||||
MaxPositionRatio float64 `json:"max_position_ratio"`
|
||||
// 最小开仓金额(USDT)
|
||||
// minimum position size (USDT)
|
||||
MinPositionSize float64 `json:"min_position_size"`
|
||||
// 最小信心度
|
||||
// minimum confidence level
|
||||
MinConfidence int `json:"min_confidence"`
|
||||
}
|
||||
|
||||
@@ -161,11 +161,11 @@ func (s *StrategyStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
// create indexes
|
||||
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_user_id ON strategies(user_id)`)
|
||||
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_is_active ON strategies(is_active)`)
|
||||
|
||||
// 触发器:更新时自动更新 updated_at
|
||||
// trigger: automatically update updated_at on update
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_strategies_updated_at
|
||||
AFTER UPDATE ON strategies
|
||||
@@ -178,14 +178,14 @@ func (s *StrategyStore) initTables() error {
|
||||
}
|
||||
|
||||
func (s *StrategyStore) initDefaultData() error {
|
||||
// 检查是否已有默认策略
|
||||
// check if default strategy already exists
|
||||
var count int
|
||||
s.db.QueryRow(`SELECT COUNT(*) FROM strategies WHERE is_default = 1`).Scan(&count)
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建系统默认策略
|
||||
// create system default strategy
|
||||
defaultConfig := StrategyConfig{
|
||||
CoinSource: CoinSourceConfig{
|
||||
SourceType: "coinpool",
|
||||
@@ -228,23 +228,23 @@ func (s *StrategyStore) initDefaultData() error {
|
||||
MinConfidence: 75,
|
||||
},
|
||||
PromptSections: PromptSectionsConfig{
|
||||
RoleDefinition: `# 你是专业的加密货币交易AI
|
||||
RoleDefinition: `# You are a professional cryptocurrency trading AI
|
||||
|
||||
你的任务是根据提供的市场数据做出交易决策。你是一位经验丰富的量化交易员,擅长技术分析和风险管理。`,
|
||||
TradingFrequency: `# ⏱️ 交易频率认知
|
||||
Your task is to make trading decisions based on the provided market data. You are an experienced quantitative trader skilled in technical analysis and risk management.`,
|
||||
TradingFrequency: `# ⏱️ Trading Frequency Awareness
|
||||
|
||||
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
|
||||
- 每小时>2笔 = 过度交易
|
||||
- 单笔持仓时间≥30-60分钟
|
||||
如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`,
|
||||
EntryStandards: `# 🎯 开仓标准(严格)
|
||||
- Excellent trader: 2-4 trades per day ≈ 0.1-0.2 trades per hour
|
||||
- >2 trades per hour = overtrading
|
||||
- Single position holding time ≥ 30-60 minutes
|
||||
If you find yourself trading every cycle → standards are too low; if closing positions in <30 minutes → too impulsive.`,
|
||||
EntryStandards: `# 🎯 Entry Standards (Strict)
|
||||
|
||||
只在多重信号共振时开仓。自由运用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。`,
|
||||
DecisionProcess: `# 📋 决策流程
|
||||
Only enter positions when multiple signals resonate. Freely use any effective analysis methods, avoid low-quality behaviors such as single indicators, contradictory signals, sideways oscillation, or immediately restarting after closing positions.`,
|
||||
DecisionProcess: `# 📋 Decision Process
|
||||
|
||||
1. 检查持仓 → 是否该止盈/止损
|
||||
2. 扫描候选币 + 多时间框 → 是否存在强信号
|
||||
3. 先写思维链,再输出结构化JSON`,
|
||||
1. Check positions → whether to take profit/stop loss
|
||||
2. Scan candidate coins + multi-timeframe → whether strong signals exist
|
||||
3. Write chain of thought first, then output structured JSON`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -252,13 +252,13 @@ func (s *StrategyStore) initDefaultData() error {
|
||||
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config)
|
||||
VALUES ('default', 'system', '默认山寨策略', '系统默认的山寨币交易策略,使用 AI500 币种池,包含完整的技术指标', 0, 1, ?)
|
||||
VALUES ('default', 'system', 'Default Altcoin Strategy', 'System default altcoin trading strategy, uses AI500 coin pool, includes complete technical indicators', 0, 1, ?)
|
||||
`, string(configJSON))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Create 创建策略
|
||||
// Create create a strategy
|
||||
func (s *StrategyStore) Create(strategy *Strategy) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config)
|
||||
@@ -267,7 +267,7 @@ func (s *StrategyStore) Create(strategy *Strategy) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新策略
|
||||
// Update update a strategy
|
||||
func (s *StrategyStore) Update(strategy *Strategy) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE strategies SET
|
||||
@@ -277,22 +277,22 @@ func (s *StrategyStore) Update(strategy *Strategy) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除策略
|
||||
// Delete delete a strategy
|
||||
func (s *StrategyStore) Delete(userID, id string) error {
|
||||
// 不允许删除系统默认策略
|
||||
// do not allow deleting system default strategy
|
||||
var isDefault bool
|
||||
s.db.QueryRow(`SELECT is_default FROM strategies WHERE id = ?`, id).Scan(&isDefault)
|
||||
if isDefault {
|
||||
return fmt.Errorf("不能删除系统默认策略")
|
||||
return fmt.Errorf("cannot delete system default strategy")
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(`DELETE FROM strategies WHERE id = ? AND user_id = ?`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// List 获取用户的策略列表
|
||||
// List get user's strategy list
|
||||
func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
|
||||
// 获取用户自己的策略 + 系统默认策略
|
||||
// get user's own strategies + system default strategy
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
||||
FROM strategies
|
||||
@@ -323,7 +323,7 @@ func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
|
||||
return strategies, nil
|
||||
}
|
||||
|
||||
// Get 获取单个策略
|
||||
// Get get a single strategy
|
||||
func (s *StrategyStore) Get(userID, id string) (*Strategy, error) {
|
||||
var st Strategy
|
||||
var createdAt, updatedAt string
|
||||
@@ -344,7 +344,7 @@ func (s *StrategyStore) Get(userID, id string) (*Strategy, error) {
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
// GetActive 获取用户当前激活的策略
|
||||
// GetActive get user's currently active strategy
|
||||
func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
|
||||
var st Strategy
|
||||
var createdAt, updatedAt string
|
||||
@@ -358,7 +358,7 @@ func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
// 没有激活的策略,返回系统默认策略
|
||||
// no active strategy, return system default strategy
|
||||
return s.GetDefault()
|
||||
}
|
||||
if err != nil {
|
||||
@@ -369,7 +369,7 @@ func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
// GetDefault 获取系统默认策略
|
||||
// GetDefault get system default strategy
|
||||
func (s *StrategyStore) GetDefault() (*Strategy, error) {
|
||||
var st Strategy
|
||||
var createdAt, updatedAt string
|
||||
@@ -391,22 +391,22 @@ func (s *StrategyStore) GetDefault() (*Strategy, error) {
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
// SetActive 设置激活策略(会先取消其他策略的激活状态)
|
||||
// SetActive set active strategy (will first deactivate other strategies)
|
||||
func (s *StrategyStore) SetActive(userID, strategyID string) error {
|
||||
// 开启事务
|
||||
// begin transaction
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 先取消该用户所有策略的激活状态
|
||||
// first deactivate all strategies for the user
|
||||
_, err = tx.Exec(`UPDATE strategies SET is_active = 0 WHERE user_id = ?`, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 激活指定策略
|
||||
// activate specified strategy
|
||||
_, err = tx.Exec(`UPDATE strategies SET is_active = 1 WHERE id = ? AND (user_id = ? OR is_default = 1)`, strategyID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -415,20 +415,20 @@ func (s *StrategyStore) SetActive(userID, strategyID string) error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Duplicate 复制策略(用于基于默认策略创建自定义策略)
|
||||
// Duplicate duplicate a strategy (used to create custom strategy based on default strategy)
|
||||
func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error {
|
||||
// 获取源策略
|
||||
// get source strategy
|
||||
source, err := s.Get(userID, sourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取源策略失败: %w", err)
|
||||
return fmt.Errorf("failed to get source strategy: %w", err)
|
||||
}
|
||||
|
||||
// 创建新策略
|
||||
// create new strategy
|
||||
newStrategy := &Strategy{
|
||||
ID: newID,
|
||||
UserID: userID,
|
||||
Name: newName,
|
||||
Description: "基于 [" + source.Name + "] 创建",
|
||||
Description: "Created based on [" + source.Name + "]",
|
||||
IsActive: false,
|
||||
IsDefault: false,
|
||||
Config: source.Config,
|
||||
@@ -437,20 +437,20 @@ func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error
|
||||
return s.Create(newStrategy)
|
||||
}
|
||||
|
||||
// ParseConfig 解析策略配置 JSON
|
||||
// ParseConfig parse strategy configuration JSON
|
||||
func (s *Strategy) ParseConfig() (*StrategyConfig, error) {
|
||||
var config StrategyConfig
|
||||
if err := json.Unmarshal([]byte(s.Config), &config); err != nil {
|
||||
return nil, fmt.Errorf("解析策略配置失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse strategy configuration: %w", err)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SetConfig 设置策略配置
|
||||
// SetConfig set strategy configuration
|
||||
func (s *Strategy) SetConfig(config *StrategyConfig) error {
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化策略配置失败: %w", err)
|
||||
return fmt.Errorf("failed to serialize strategy configuration: %w", err)
|
||||
}
|
||||
s.Config = string(data)
|
||||
return nil
|
||||
|
||||
@@ -5,20 +5,20 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TraderStore 交易员存储
|
||||
// TraderStore trader storage
|
||||
type TraderStore struct {
|
||||
db *sql.DB
|
||||
decryptFunc func(string) string
|
||||
}
|
||||
|
||||
// Trader 交易员配置
|
||||
// Trader trader configuration
|
||||
type Trader struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
AIModelID string `json:"ai_model_id"`
|
||||
ExchangeID string `json:"exchange_id"`
|
||||
StrategyID string `json:"strategy_id"` // 关联策略ID
|
||||
StrategyID string `json:"strategy_id"` // Associated strategy ID
|
||||
InitialBalance float64 `json:"initial_balance"`
|
||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||
IsRunning bool `json:"is_running"`
|
||||
@@ -26,7 +26,7 @@ type Trader struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// 以下字段已废弃,保留用于向后兼容,新交易员应使用 StrategyID
|
||||
// Following fields are deprecated, kept for backward compatibility, new traders should use StrategyID
|
||||
BTCETHLeverage int `json:"btc_eth_leverage,omitempty"`
|
||||
AltcoinLeverage int `json:"altcoin_leverage,omitempty"`
|
||||
TradingSymbols string `json:"trading_symbols,omitempty"`
|
||||
@@ -37,12 +37,12 @@ type Trader struct {
|
||||
SystemPromptTemplate string `json:"system_prompt_template,omitempty"`
|
||||
}
|
||||
|
||||
// TraderFullConfig 交易员完整配置(包含AI模型、交易所和策略)
|
||||
// TraderFullConfig trader full configuration (includes AI model, exchange and strategy)
|
||||
type TraderFullConfig struct {
|
||||
Trader *Trader
|
||||
AIModel *AIModel
|
||||
Exchange *Exchange
|
||||
Strategy *Strategy // 关联的策略配置
|
||||
Strategy *Strategy // Associated strategy configuration
|
||||
}
|
||||
|
||||
func (s *TraderStore) initTables() error {
|
||||
@@ -74,7 +74,7 @@ func (s *TraderStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发器
|
||||
// Trigger
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_traders_updated_at
|
||||
AFTER UPDATE ON traders
|
||||
@@ -86,7 +86,7 @@ func (s *TraderStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 向后兼容
|
||||
// Backward compatibility
|
||||
alterQueries := []string{
|
||||
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
|
||||
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
|
||||
@@ -113,7 +113,7 @@ func (s *TraderStore) decrypt(encrypted string) string {
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// Create 创建交易员
|
||||
// Create creates trader
|
||||
func (s *TraderStore) Create(trader *Trader) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, strategy_id, initial_balance,
|
||||
@@ -128,7 +128,7 @@ func (s *TraderStore) Create(trader *Trader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// List 获取用户的交易员列表
|
||||
// List gets user's trader list
|
||||
func (s *TraderStore) List(userID string) ([]*Trader, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''),
|
||||
@@ -165,13 +165,13 @@ func (s *TraderStore) List(userID string) ([]*Trader, error) {
|
||||
return traders, nil
|
||||
}
|
||||
|
||||
// UpdateStatus 更新交易员运行状态
|
||||
// UpdateStatus updates trader running status
|
||||
func (s *TraderStore) UpdateStatus(userID, id string, isRunning bool) error {
|
||||
_, err := s.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ? AND user_id = ?`, isRunning, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新交易员配置
|
||||
// Update updates trader configuration
|
||||
func (s *TraderStore) Update(trader *Trader) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE traders SET
|
||||
@@ -184,26 +184,26 @@ func (s *TraderStore) Update(trader *Trader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateInitialBalance 更新初始余额
|
||||
// UpdateInitialBalance updates initial balance
|
||||
func (s *TraderStore) UpdateInitialBalance(userID, id string, newBalance float64) error {
|
||||
_, err := s.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateCustomPrompt 更新自定义提示词
|
||||
// UpdateCustomPrompt updates custom prompt
|
||||
func (s *TraderStore) UpdateCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
|
||||
_, err := s.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`,
|
||||
customPrompt, overrideBase, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除交易员
|
||||
// Delete deletes trader
|
||||
func (s *TraderStore) Delete(userID, id string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFullConfig 获取交易员完整配置
|
||||
// GetFullConfig gets trader full configuration
|
||||
func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, error) {
|
||||
var trader Trader
|
||||
var aiModel AIModel
|
||||
@@ -255,7 +255,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt)
|
||||
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt)
|
||||
|
||||
// 解密
|
||||
// Decrypt
|
||||
aiModel.APIKey = s.decrypt(aiModel.APIKey)
|
||||
exchange.APIKey = s.decrypt(exchange.APIKey)
|
||||
exchange.SecretKey = s.decrypt(exchange.SecretKey)
|
||||
@@ -264,12 +264,12 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
exchange.LighterPrivateKey = s.decrypt(exchange.LighterPrivateKey)
|
||||
exchange.LighterAPIKeyPrivateKey = s.decrypt(exchange.LighterAPIKeyPrivateKey)
|
||||
|
||||
// 加载关联的策略
|
||||
// Load associated strategy
|
||||
var strategy *Strategy
|
||||
if trader.StrategyID != "" {
|
||||
strategy, _ = s.getStrategyByID(userID, trader.StrategyID)
|
||||
}
|
||||
// 如果没有关联策略,获取用户的激活策略或默认策略
|
||||
// If no associated strategy, get user's active strategy or default strategy
|
||||
if strategy == nil {
|
||||
strategy, _ = s.getActiveOrDefaultStrategy(userID)
|
||||
}
|
||||
@@ -282,7 +282,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getStrategyByID 内部方法:根据ID获取策略
|
||||
// getStrategyByID internal method: gets strategy by ID
|
||||
func (s *TraderStore) getStrategyByID(userID, strategyID string) (*Strategy, error) {
|
||||
var strategy Strategy
|
||||
var createdAt, updatedAt string
|
||||
@@ -301,12 +301,12 @@ func (s *TraderStore) getStrategyByID(userID, strategyID string) (*Strategy, err
|
||||
return &strategy, nil
|
||||
}
|
||||
|
||||
// getActiveOrDefaultStrategy 内部方法:获取用户激活的策略或系统默认策略
|
||||
// getActiveOrDefaultStrategy internal method: gets user's active strategy or system default strategy
|
||||
func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, error) {
|
||||
var strategy Strategy
|
||||
var createdAt, updatedAt string
|
||||
|
||||
// 先尝试获取用户激活的策略
|
||||
// First try to get user's active strategy
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
||||
FROM strategies WHERE user_id = ? AND is_active = 1
|
||||
@@ -320,7 +320,7 @@ func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, erro
|
||||
return &strategy, nil
|
||||
}
|
||||
|
||||
// 回退到系统默认策略
|
||||
// Fallback to system default strategy
|
||||
err = s.db.QueryRow(`
|
||||
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
||||
FROM strategies WHERE is_default = 1 LIMIT 1
|
||||
@@ -336,7 +336,7 @@ func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, erro
|
||||
return &strategy, nil
|
||||
}
|
||||
|
||||
// ListAll 获取所有用户的交易员列表
|
||||
// ListAll gets all users' trader list
|
||||
func (s *TraderStore) ListAll() ([]*Trader, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''),
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserStore 用户存储
|
||||
// UserStore user storage
|
||||
type UserStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// User 用户
|
||||
// User user
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -23,7 +23,7 @@ type User struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GenerateOTPSecret 生成OTP密钥
|
||||
// GenerateOTPSecret generates OTP secret
|
||||
func GenerateOTPSecret() (string, error) {
|
||||
secret := make([]byte, 20)
|
||||
_, err := rand.Read(secret)
|
||||
@@ -49,7 +49,7 @@ func (s *UserStore) initTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发器
|
||||
// Trigger
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_users_updated_at
|
||||
AFTER UPDATE ON users
|
||||
@@ -64,7 +64,7 @@ func (s *UserStore) initTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
// Create creates user
|
||||
func (s *UserStore) Create(user *User) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
|
||||
@@ -73,7 +73,7 @@ func (s *UserStore) Create(user *User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetByEmail 通过邮箱获取用户
|
||||
// GetByEmail gets user by email
|
||||
func (s *UserStore) GetByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
var createdAt, updatedAt string
|
||||
@@ -92,7 +92,7 @@ func (s *UserStore) GetByEmail(email string) (*User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByID 通过ID获取用户
|
||||
// GetByID gets user by ID
|
||||
func (s *UserStore) GetByID(userID string) (*User, error) {
|
||||
var user User
|
||||
var createdAt, updatedAt string
|
||||
@@ -111,7 +111,7 @@ func (s *UserStore) GetByID(userID string) (*User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetAllIDs 获取所有用户ID
|
||||
// GetAllIDs gets all user IDs
|
||||
func (s *UserStore) GetAllIDs() ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT id FROM users ORDER BY id`)
|
||||
if err != nil {
|
||||
@@ -130,13 +130,13 @@ func (s *UserStore) GetAllIDs() ([]string, error) {
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
// UpdateOTPVerified 更新OTP验证状态
|
||||
// UpdateOTPVerified updates OTP verification status
|
||||
func (s *UserStore) UpdateOTPVerified(userID string, verified bool) error {
|
||||
_, err := s.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码
|
||||
// UpdatePassword updates password
|
||||
func (s *UserStore) UpdatePassword(userID, passwordHash string) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
||||
@@ -144,7 +144,7 @@ func (s *UserStore) UpdatePassword(userID, passwordHash string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// EnsureAdmin 确保admin用户存在
|
||||
// EnsureAdmin ensures admin user exists
|
||||
func (s *UserStore) EnsureAdmin() error {
|
||||
var count int
|
||||
err := s.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count)
|
||||
|
||||
Reference in New Issue
Block a user