mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
refactor: simplify config and remove unused database tables
- Remove system_config, beta_codes, signal_source tables and related code - Simplify config.go to only read from .env (APIServerPort, JWTSecret, RegistrationEnabled) - Remove GetCustomCoins, use all USDT perpetual contracts for WSMonitor - Add trader_equity_snapshots table for equity tracking - Remove signal source modal from frontend AITradersPage - Fix WSMonitor nil panic by restoring initialization in main.go
This commit is contained in:
@@ -1,121 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BetaCodeStore 内测码存储
|
||||
type BetaCodeStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (s *BetaCodeStore) initTables() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS beta_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
used BOOLEAN DEFAULT 0,
|
||||
used_by TEXT DEFAULT '',
|
||||
used_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadFromFile 从文件加载内测码
|
||||
func (s *BetaCodeStore) LoadFromFile(filePath string) error {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取内测码文件失败: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var codes []string
|
||||
for _, line := range lines {
|
||||
code := strings.TrimSpace(line)
|
||||
if code != "" && !strings.HasPrefix(code, "#") {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare(`INSERT OR IGNORE INTO beta_codes (code) VALUES (?)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备语句失败: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
insertedCount := 0
|
||||
for _, code := range codes {
|
||||
result, err := stmt.Exec(code)
|
||||
if err != nil {
|
||||
logger.Warnf("插入内测码 %s 失败: %v", code, err)
|
||||
continue
|
||||
}
|
||||
if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 {
|
||||
insertedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交事务失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✅ 成功加载 %d 个内测码到数据库 (总计 %d 个)", insertedCount, len(codes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证内测码是否有效
|
||||
func (s *BetaCodeStore) Validate(code string) (bool, error) {
|
||||
var used bool
|
||||
err := s.db.QueryRow(`SELECT used FROM beta_codes WHERE code = ?`, code).Scan(&used)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return !used, nil
|
||||
}
|
||||
|
||||
// Use 使用内测码
|
||||
func (s *BetaCodeStore) Use(code, userEmail string) error {
|
||||
result, err := s.db.Exec(`
|
||||
UPDATE beta_codes SET used = 1, used_by = ?, used_at = CURRENT_TIMESTAMP
|
||||
WHERE code = ? AND used = 0
|
||||
`, userEmail, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("内测码无效或已被使用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取内测码统计
|
||||
func (s *BetaCodeStore) GetStats() (total, used int, err error) {
|
||||
err = s.db.QueryRow(`SELECT COUNT(*) FROM beta_codes`).Scan(&total)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
err = s.db.QueryRow(`SELECT COUNT(*) FROM beta_codes WHERE used = 1`).Scan(&used)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return total, used, nil
|
||||
}
|
||||
@@ -76,10 +76,11 @@ type Statistics struct {
|
||||
TotalClosePositions int `json:"total_close_positions"`
|
||||
}
|
||||
|
||||
// initTables 初始化决策相关表
|
||||
// initTables 初始化 AI 决策日志表
|
||||
// 注意:账户净值曲线数据已迁移到 trader_equity_snapshots 表(由 EquityStore 管理)
|
||||
func (s *DecisionStore) initTables() error {
|
||||
queries := []string{
|
||||
// 决策记录主表
|
||||
// AI 决策日志表(记录 AI 的输入输出、思维链等)
|
||||
`CREATE TABLE IF NOT EXISTS decision_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trader_id TEXT NOT NULL,
|
||||
@@ -96,58 +97,9 @@ func (s *DecisionStore) initTables() error {
|
||||
ai_request_duration_ms INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// 账户状态快照表
|
||||
`CREATE TABLE IF NOT EXISTS decision_account_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL,
|
||||
total_balance REAL DEFAULT 0,
|
||||
available_balance REAL DEFAULT 0,
|
||||
total_unrealized_profit REAL DEFAULT 0,
|
||||
position_count INTEGER DEFAULT 0,
|
||||
margin_used_pct REAL DEFAULT 0,
|
||||
initial_balance REAL DEFAULT 0,
|
||||
FOREIGN KEY (decision_id) REFERENCES decision_records(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 持仓快照表
|
||||
`CREATE TABLE IF NOT EXISTS decision_position_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT DEFAULT '',
|
||||
position_amt REAL DEFAULT 0,
|
||||
entry_price REAL DEFAULT 0,
|
||||
mark_price REAL DEFAULT 0,
|
||||
unrealized_profit REAL DEFAULT 0,
|
||||
leverage REAL DEFAULT 0,
|
||||
liquidation_price REAL DEFAULT 0,
|
||||
FOREIGN KEY (decision_id) REFERENCES decision_records(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 决策动作表(订单详情)
|
||||
`CREATE TABLE IF NOT EXISTS decision_actions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL,
|
||||
trader_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
quantity REAL DEFAULT 0,
|
||||
leverage INTEGER DEFAULT 0,
|
||||
price REAL DEFAULT 0,
|
||||
order_id INTEGER DEFAULT 0,
|
||||
timestamp DATETIME NOT NULL,
|
||||
success BOOLEAN DEFAULT 0,
|
||||
error TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (decision_id) REFERENCES decision_records(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 索引
|
||||
`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)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_decision_actions_trader ON decision_actions(trader_id, timestamp DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_decision_actions_symbol ON decision_actions(symbol, timestamp DESC)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
@@ -159,7 +111,7 @@ func (s *DecisionStore) initTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogDecision 记录决策
|
||||
// LogDecision 记录决策(仅保存 AI 决策日志,净值曲线已迁移到 equity 表)
|
||||
func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
if record.Timestamp.IsZero() {
|
||||
record.Timestamp = time.Now().UTC()
|
||||
@@ -167,19 +119,12 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
record.Timestamp = record.Timestamp.UTC()
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 序列化候选币种和执行日志为 JSON
|
||||
candidateCoinsJSON, _ := json.Marshal(record.CandidateCoins)
|
||||
executionLogJSON, _ := json.Marshal(record.ExecutionLog)
|
||||
|
||||
// 插入决策记录主表
|
||||
result, err := tx.Exec(`
|
||||
// 插入决策记录主表(仅保存 AI 决策相关内容)
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO decision_records (
|
||||
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
cot_trace, decision_json, candidate_coins, execution_log,
|
||||
@@ -201,63 +146,6 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
}
|
||||
record.ID = decisionID
|
||||
|
||||
// 插入账户状态快照
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO decision_account_snapshots (
|
||||
decision_id, total_balance, available_balance, total_unrealized_profit,
|
||||
position_count, margin_used_pct, initial_balance
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
decisionID, record.AccountState.TotalBalance, record.AccountState.AvailableBalance,
|
||||
record.AccountState.TotalUnrealizedProfit, record.AccountState.PositionCount,
|
||||
record.AccountState.MarginUsedPct, record.AccountState.InitialBalance,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入账户快照失败: %w", err)
|
||||
}
|
||||
|
||||
// 插入持仓快照
|
||||
for _, pos := range record.Positions {
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO decision_position_snapshots (
|
||||
decision_id, symbol, side, position_amt, entry_price,
|
||||
mark_price, unrealized_profit, leverage, liquidation_price
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
decisionID, pos.Symbol, pos.Side, pos.PositionAmt, pos.EntryPrice,
|
||||
pos.MarkPrice, pos.UnrealizedProfit, pos.Leverage, pos.LiquidationPrice,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入持仓快照失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 插入决策动作(订单详情)
|
||||
for _, action := range record.Decisions {
|
||||
actionTimestamp := action.Timestamp
|
||||
if actionTimestamp.IsZero() {
|
||||
actionTimestamp = record.Timestamp
|
||||
}
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO decision_actions (
|
||||
decision_id, trader_id, action, symbol, quantity, leverage,
|
||||
price, order_id, timestamp, success, error
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
decisionID, record.TraderID, action.Action, action.Symbol, action.Quantity,
|
||||
action.Leverage, action.Price, action.OrderID,
|
||||
actionTimestamp.Format(time.RFC3339), action.Success, action.Error,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入决策动作失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交事务失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -394,21 +282,17 @@ func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) {
|
||||
}
|
||||
stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles
|
||||
|
||||
err = s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM decision_actions
|
||||
WHERE trader_id = ? AND success = 1 AND action IN ('open_long', 'open_short')
|
||||
// 从 trader_orders 表统计开仓次数
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询开仓次数失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM decision_actions
|
||||
WHERE trader_id = ? AND success = 1 AND action IN ('close_long', 'close_short', 'auto_close_long', 'auto_close_short')
|
||||
// 从 trader_orders 表统计平仓次数
|
||||
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')
|
||||
`, traderID).Scan(&stats.TotalClosePositions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询平仓次数失败: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
@@ -421,14 +305,15 @@ 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 表统计
|
||||
s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM decision_actions
|
||||
WHERE success = 1 AND action IN ('open_long', 'open_short')
|
||||
SELECT COUNT(*) FROM trader_orders
|
||||
WHERE status = 'FILLED' AND action IN ('open_long', 'open_short')
|
||||
`).Scan(&stats.TotalOpenPositions)
|
||||
|
||||
s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM decision_actions
|
||||
WHERE success = 1 AND action IN ('close_long', 'close_short', 'auto_close_long', 'auto_close_short')
|
||||
SELECT COUNT(*) FROM trader_orders
|
||||
WHERE status = 'FILLED' AND action IN ('close_long', 'close_short', 'auto_close_long', 'auto_close_short')
|
||||
`).Scan(&stats.TotalClosePositions)
|
||||
|
||||
return stats, nil
|
||||
@@ -469,62 +354,11 @@ func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, err
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// fillRecordDetails 填充决策记录的关联数据
|
||||
// fillRecordDetails 填充决策记录的关联数据(旧的关联表已删除,此函数保留用于兼容性)
|
||||
// 注意:账户快照、持仓快照、决策动作等数据已不再存储在 decision 相关表中
|
||||
// - 净值数据请使用 EquityStore.GetLatest()
|
||||
// - 订单数据请使用 OrderStore
|
||||
func (s *DecisionStore) fillRecordDetails(record *DecisionRecord) {
|
||||
// 查询账户状态
|
||||
s.db.QueryRow(`
|
||||
SELECT total_balance, available_balance, total_unrealized_profit,
|
||||
position_count, margin_used_pct, initial_balance
|
||||
FROM decision_account_snapshots
|
||||
WHERE decision_id = ?
|
||||
`, record.ID).Scan(
|
||||
&record.AccountState.TotalBalance,
|
||||
&record.AccountState.AvailableBalance,
|
||||
&record.AccountState.TotalUnrealizedProfit,
|
||||
&record.AccountState.PositionCount,
|
||||
&record.AccountState.MarginUsedPct,
|
||||
&record.AccountState.InitialBalance,
|
||||
)
|
||||
|
||||
// 查询持仓快照
|
||||
posRows, err := s.db.Query(`
|
||||
SELECT symbol, side, position_amt, entry_price, mark_price,
|
||||
unrealized_profit, leverage, liquidation_price
|
||||
FROM decision_position_snapshots
|
||||
WHERE decision_id = ?
|
||||
`, record.ID)
|
||||
if err == nil {
|
||||
defer posRows.Close()
|
||||
for posRows.Next() {
|
||||
var pos PositionSnapshot
|
||||
posRows.Scan(
|
||||
&pos.Symbol, &pos.Side, &pos.PositionAmt, &pos.EntryPrice,
|
||||
&pos.MarkPrice, &pos.UnrealizedProfit, &pos.Leverage,
|
||||
&pos.LiquidationPrice,
|
||||
)
|
||||
record.Positions = append(record.Positions, pos)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询决策动作
|
||||
actionRows, err := s.db.Query(`
|
||||
SELECT action, symbol, quantity, leverage, price, order_id,
|
||||
timestamp, success, error
|
||||
FROM decision_actions
|
||||
WHERE decision_id = ?
|
||||
`, record.ID)
|
||||
if err == nil {
|
||||
defer actionRows.Close()
|
||||
for actionRows.Next() {
|
||||
var action DecisionAction
|
||||
var timestampStr string
|
||||
actionRows.Scan(
|
||||
&action.Action, &action.Symbol, &action.Quantity,
|
||||
&action.Leverage, &action.Price, &action.OrderID,
|
||||
×tampStr, &action.Success, &action.Error,
|
||||
)
|
||||
action.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
record.Decisions = append(record.Decisions, action)
|
||||
}
|
||||
}
|
||||
// 旧的关联表已删除,不再需要填充
|
||||
// AccountState, Positions, Decisions 字段将保持为零值
|
||||
}
|
||||
|
||||
257
store/equity.go
Normal file
257
store/equity.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EquityStore 账户净值存储(用于绘制收益率曲线)
|
||||
type EquityStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// EquitySnapshot 净值快照
|
||||
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"` // 保证金使用率
|
||||
}
|
||||
|
||||
// initTables 初始化净值表
|
||||
func (s *EquityStore) initTables() error {
|
||||
queries := []string{
|
||||
// 净值快照表 - 专门用于收益率曲线
|
||||
`CREATE TABLE IF NOT EXISTS trader_equity_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trader_id TEXT NOT NULL,
|
||||
timestamp DATETIME NOT NULL,
|
||||
total_equity REAL NOT NULL DEFAULT 0,
|
||||
balance REAL NOT NULL DEFAULT 0,
|
||||
unrealized_pnl REAL NOT NULL DEFAULT 0,
|
||||
position_count INTEGER DEFAULT 0,
|
||||
margin_used_pct REAL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
// 索引
|
||||
`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 nil
|
||||
}
|
||||
|
||||
// Save 保存净值快照
|
||||
func (s *EquityStore) Save(snapshot *EquitySnapshot) error {
|
||||
if snapshot.Timestamp.IsZero() {
|
||||
snapshot.Timestamp = time.Now().UTC()
|
||||
} else {
|
||||
snapshot.Timestamp = snapshot.Timestamp.UTC()
|
||||
}
|
||||
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO trader_equity_snapshots (
|
||||
trader_id, timestamp, total_equity, balance,
|
||||
unrealized_pnl, position_count, margin_used_pct
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
snapshot.TraderID,
|
||||
snapshot.Timestamp.Format(time.RFC3339),
|
||||
snapshot.TotalEquity,
|
||||
snapshot.Balance,
|
||||
snapshot.UnrealizedPnL,
|
||||
snapshot.PositionCount,
|
||||
snapshot.MarginUsedPct,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存净值快照失败: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
snapshot.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatest 获取指定交易员最近N条净值记录(按时间正序:从旧到新)
|
||||
func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, timestamp, total_equity, balance,
|
||||
unrealized_pnl, position_count, margin_used_pct
|
||||
FROM trader_equity_snapshots
|
||||
WHERE trader_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`, traderID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询净值记录失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var snapshots []*EquitySnapshot
|
||||
for rows.Next() {
|
||||
snap := &EquitySnapshot{}
|
||||
var timestampStr string
|
||||
err := rows.Scan(
|
||||
&snap.ID, &snap.TraderID, ×tampStr, &snap.TotalEquity,
|
||||
&snap.Balance, &snap.UnrealizedPnL, &snap.PositionCount, &snap.MarginUsedPct,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
snap.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
snapshots = append(snapshots, snap)
|
||||
}
|
||||
|
||||
// 反转数组,让时间从旧到新排列(适合绘制曲线)
|
||||
for i, j := 0, len(snapshots)-1; i < j; i, j = i+1, j-1 {
|
||||
snapshots[i], snapshots[j] = snapshots[j], snapshots[i]
|
||||
}
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// GetByTimeRange 获取指定时间范围内的净值记录
|
||||
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,
|
||||
unrealized_pnl, position_count, margin_used_pct
|
||||
FROM trader_equity_snapshots
|
||||
WHERE trader_id = ? AND timestamp >= ? AND timestamp <= ?
|
||||
ORDER BY timestamp ASC
|
||||
`, traderID, start.Format(time.RFC3339), end.Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询净值记录失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var snapshots []*EquitySnapshot
|
||||
for rows.Next() {
|
||||
snap := &EquitySnapshot{}
|
||||
var timestampStr string
|
||||
err := rows.Scan(
|
||||
&snap.ID, &snap.TraderID, ×tampStr, &snap.TotalEquity,
|
||||
&snap.Balance, &snap.UnrealizedPnL, &snap.PositionCount, &snap.MarginUsedPct,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
snap.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
snapshots = append(snapshots, snap)
|
||||
}
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// GetAllTradersLatest 获取所有交易员的最新净值(用于排行榜)
|
||||
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,
|
||||
e.unrealized_pnl, e.position_count, e.margin_used_pct
|
||||
FROM trader_equity_snapshots e
|
||||
INNER JOIN (
|
||||
SELECT trader_id, MAX(timestamp) as max_ts
|
||||
FROM trader_equity_snapshots
|
||||
GROUP BY trader_id
|
||||
) latest ON e.trader_id = latest.trader_id AND e.timestamp = latest.max_ts
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询最新净值失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]*EquitySnapshot)
|
||||
for rows.Next() {
|
||||
snap := &EquitySnapshot{}
|
||||
var timestampStr string
|
||||
err := rows.Scan(
|
||||
&snap.ID, &snap.TraderID, ×tampStr, &snap.TotalEquity,
|
||||
&snap.Balance, &snap.UnrealizedPnL, &snap.PositionCount, &snap.MarginUsedPct,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
snap.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
result[snap.TraderID] = snap
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CleanOldRecords 清理N天前的旧记录
|
||||
func (s *EquityStore) CleanOldRecords(traderID string, days int) (int64, error) {
|
||||
cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339)
|
||||
|
||||
result, err := s.db.Exec(`
|
||||
DELETE FROM trader_equity_snapshots
|
||||
WHERE trader_id = ? AND timestamp < ?
|
||||
`, traderID, cutoffTime)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("清理旧记录失败: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// GetCount 获取指定交易员的记录数
|
||||
func (s *EquityStore) GetCount(traderID string) (int, error) {
|
||||
var count int
|
||||
err := s.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM trader_equity_snapshots WHERE trader_id = ?
|
||||
`, traderID).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MigrateFromDecision 从旧的 decision_account_snapshots 迁移数据
|
||||
func (s *EquityStore) MigrateFromDecision() (int64, error) {
|
||||
// 检查是否需要迁移(新表是否为空)
|
||||
var count int
|
||||
s.db.QueryRow(`SELECT COUNT(*) FROM trader_equity_snapshots`).Scan(&count)
|
||||
if count > 0 {
|
||||
return 0, nil // 已有数据,跳过迁移
|
||||
}
|
||||
|
||||
// 检查旧表是否存在
|
||||
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 // 旧表不存在,跳过
|
||||
}
|
||||
|
||||
// 迁移数据:从 decision_records + decision_account_snapshots 联合查询
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO trader_equity_snapshots (
|
||||
trader_id, timestamp, total_equity, balance,
|
||||
unrealized_pnl, position_count, margin_used_pct
|
||||
)
|
||||
SELECT
|
||||
dr.trader_id,
|
||||
dr.timestamp,
|
||||
das.total_balance,
|
||||
das.available_balance,
|
||||
das.total_unrealized_profit,
|
||||
das.position_count,
|
||||
das.margin_used_pct
|
||||
FROM decision_records dr
|
||||
JOIN decision_account_snapshots das ON dr.id = das.decision_id
|
||||
ORDER BY dr.timestamp ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("迁移数据失败: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignalSourceStore 信号源存储
|
||||
type SignalSourceStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// SignalSource 用户信号源配置
|
||||
type SignalSource struct {
|
||||
ID int `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
CoinPoolURL string `json:"coin_pool_url"`
|
||||
OITopURL string `json:"oi_top_url"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s *SignalSourceStore) initTables() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_signal_sources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
coin_pool_url TEXT DEFAULT '',
|
||||
oi_top_url TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发器
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_user_signal_sources_updated_at
|
||||
AFTER UPDATE ON user_signal_sources
|
||||
BEGIN
|
||||
UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create 创建信号源配置
|
||||
func (s *SignalSourceStore) Create(userID, coinPoolURL, oiTopURL string) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR REPLACE INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`, userID, coinPoolURL, oiTopURL)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get 获取信号源配置
|
||||
func (s *SignalSourceStore) Get(userID string) (*SignalSource, error) {
|
||||
var source SignalSource
|
||||
var createdAt, updatedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
|
||||
FROM user_signal_sources WHERE user_id = ?
|
||||
`, userID).Scan(
|
||||
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
source.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
return &source, nil
|
||||
}
|
||||
|
||||
// Update 更新信号源配置
|
||||
func (s *SignalSourceStore) Update(userID, coinPoolURL, oiTopURL string) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE user_signal_sources SET coin_pool_url = ?, oi_top_url = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ?
|
||||
`, coinPoolURL, oiTopURL, userID)
|
||||
return err
|
||||
}
|
||||
@@ -16,18 +16,16 @@ type Store struct {
|
||||
db *sql.DB
|
||||
|
||||
// 子存储(延迟初始化)
|
||||
user *UserStore
|
||||
aiModel *AIModelStore
|
||||
exchange *ExchangeStore
|
||||
trader *TraderStore
|
||||
systemConfig *SystemConfigStore
|
||||
betaCode *BetaCodeStore
|
||||
signalSource *SignalSourceStore
|
||||
decision *DecisionStore
|
||||
backtest *BacktestStore
|
||||
order *OrderStore
|
||||
position *PositionStore
|
||||
strategy *StrategyStore
|
||||
user *UserStore
|
||||
aiModel *AIModelStore
|
||||
exchange *ExchangeStore
|
||||
trader *TraderStore
|
||||
decision *DecisionStore
|
||||
backtest *BacktestStore
|
||||
order *OrderStore
|
||||
position *PositionStore
|
||||
strategy *StrategyStore
|
||||
equity *EquityStore
|
||||
|
||||
// 加密函数
|
||||
encryptFunc func(string) string
|
||||
@@ -131,15 +129,6 @@ func (s *Store) initTables() error {
|
||||
if err := s.Trader().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化交易员表失败: %w", err)
|
||||
}
|
||||
if err := s.SystemConfig().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化系统配置表失败: %w", err)
|
||||
}
|
||||
if err := s.BetaCode().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化内测码表失败: %w", err)
|
||||
}
|
||||
if err := s.SignalSource().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化信号源表失败: %w", err)
|
||||
}
|
||||
if err := s.Decision().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化决策日志表失败: %w", err)
|
||||
}
|
||||
@@ -155,6 +144,9 @@ func (s *Store) initTables() error {
|
||||
if err := s.Strategy().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化策略表失败: %w", err)
|
||||
}
|
||||
if err := s.Equity().initTables(); err != nil {
|
||||
return fmt.Errorf("初始化净值表失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -166,12 +158,15 @@ func (s *Store) initDefaultData() error {
|
||||
if err := s.Exchange().initDefaultData(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SystemConfig().initDefaultData(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Strategy().initDefaultData(); err != nil {
|
||||
return err
|
||||
}
|
||||
// 迁移旧的 decision_account_snapshots 数据到新的 trader_equity_snapshots 表
|
||||
if migrated, err := s.Equity().MigrateFromDecision(); err != nil {
|
||||
logger.Warnf("迁移净值数据失败: %v", err)
|
||||
} else if migrated > 0 {
|
||||
logger.Infof("✅ 已迁移 %d 条净值数据到新表", migrated)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,36 +221,6 @@ func (s *Store) Trader() *TraderStore {
|
||||
return s.trader
|
||||
}
|
||||
|
||||
// SystemConfig 获取系统配置存储
|
||||
func (s *Store) SystemConfig() *SystemConfigStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.systemConfig == nil {
|
||||
s.systemConfig = &SystemConfigStore{db: s.db}
|
||||
}
|
||||
return s.systemConfig
|
||||
}
|
||||
|
||||
// BetaCode 获取内测码存储
|
||||
func (s *Store) BetaCode() *BetaCodeStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.betaCode == nil {
|
||||
s.betaCode = &BetaCodeStore{db: s.db}
|
||||
}
|
||||
return s.betaCode
|
||||
}
|
||||
|
||||
// SignalSource 获取信号源存储
|
||||
func (s *Store) SignalSource() *SignalSourceStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.signalSource == nil {
|
||||
s.signalSource = &SignalSourceStore{db: s.db}
|
||||
}
|
||||
return s.signalSource
|
||||
}
|
||||
|
||||
// Decision 获取决策日志存储
|
||||
func (s *Store) Decision() *DecisionStore {
|
||||
s.mu.Lock()
|
||||
@@ -306,6 +271,16 @@ func (s *Store) Strategy() *StrategyStore {
|
||||
return s.strategy
|
||||
}
|
||||
|
||||
// Equity 获取净值存储
|
||||
func (s *Store) Equity() *EquityStore {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.equity == nil {
|
||||
s.equity = &EquityStore{db: s.db}
|
||||
}
|
||||
return s.equity
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// SystemConfigStore 系统配置存储
|
||||
type SystemConfigStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (s *SystemConfigStore) initTables() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 触发器
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at
|
||||
AFTER UPDATE ON system_config
|
||||
BEGIN
|
||||
UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
|
||||
END
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SystemConfigStore) initDefaultData() error {
|
||||
configs := map[string]string{
|
||||
"beta_mode": "false",
|
||||
"api_server_port": "8080",
|
||||
"max_daily_loss": "10.0",
|
||||
"max_drawdown": "20.0",
|
||||
"stop_trading_minutes": "60",
|
||||
"jwt_secret": "",
|
||||
"registration_enabled": "true",
|
||||
}
|
||||
|
||||
for key, value := range configs {
|
||||
_, err := s.db.Exec(`INSERT OR IGNORE INTO system_config (key, value) VALUES (?, ?)`, key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取配置值
|
||||
func (s *SystemConfigStore) Get(key string) (string, error) {
|
||||
var value string
|
||||
err := s.db.QueryRow(`SELECT value FROM system_config WHERE key = ?`, key).Scan(&value)
|
||||
return value, err
|
||||
}
|
||||
|
||||
// Set 设置配置值
|
||||
func (s *SystemConfigStore) Set(key, value string) error {
|
||||
_, err := s.db.Exec(`INSERT OR REPLACE INTO system_config (key, value) VALUES (?, ?)`, key, value)
|
||||
return err
|
||||
}
|
||||
@@ -2,11 +2,6 @@ package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -341,43 +336,6 @@ func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, erro
|
||||
return &strategy, nil
|
||||
}
|
||||
|
||||
// GetCustomCoins 获取所有交易员自定义币种
|
||||
func (s *TraderStore) GetCustomCoins() []string {
|
||||
var symbol string
|
||||
var symbols []string
|
||||
_ = s.db.QueryRow(`
|
||||
SELECT GROUP_CONCAT(trading_symbols, ',') as symbol
|
||||
FROM traders WHERE trading_symbols != ''
|
||||
`).Scan(&symbol)
|
||||
|
||||
// 如果没有自定义币种,返回默认币种
|
||||
if symbol == "" {
|
||||
var symbolJSON string
|
||||
_ = s.db.QueryRow(`SELECT value FROM system_config WHERE key = 'default_coins'`).Scan(&symbolJSON)
|
||||
if symbolJSON != "" {
|
||||
if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil {
|
||||
logger.Warnf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err)
|
||||
symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"}
|
||||
}
|
||||
} else {
|
||||
symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// 处理并去重币种列表
|
||||
for _, s := range strings.Split(symbol, ",") {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
coin := market.Normalize(s)
|
||||
if !slices.Contains(symbols, coin) {
|
||||
symbols = append(symbols, coin)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// ListAll 获取所有用户的交易员列表
|
||||
func (s *TraderStore) ListAll() ([]*Trader, error) {
|
||||
rows, err := s.db.Query(`
|
||||
|
||||
Reference in New Issue
Block a user