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:
tinkle-community
2025-12-07 20:17:03 +08:00
parent 07ac8e4ecd
commit 2334d78e4a
15 changed files with 490 additions and 1493 deletions

View File

@@ -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
}

View File

@@ -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,
&timestampStr, &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
View 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, &timestampStr, &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, &timestampStr, &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, &timestampStr, &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()
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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(`