Refactor/trading actions (#1169)

* refactor: 简化交易动作,移除 update_stop_loss/update_take_profit/partial_close
- 移除 Decision 结构体中的 NewStopLoss, NewTakeProfit, ClosePercentage 字段
- 删除 executeUpdateStopLossWithRecord, executeUpdateTakeProfitWithRecord, executePartialCloseWithRecord 函数
- 简化 logger 中的 partial_close 聚合逻辑
- 更新 AI prompt 和验证逻辑,只保留 6 个核心动作
- 清理相关测试代码
保留的交易动作: open_long, open_short, close_long, close_short, hold, wait
* refactor: 移除 AI学习与反思 模块
- 删除前端 AILearning.tsx 组件和相关引用
- 删除后端 /performance API 接口
- 删除 logger 中 AnalyzePerformance、calculateSharpeRatio 等函数
- 删除 PerformanceAnalysis、TradeOutcome、SymbolPerformance 等结构体
- 删除 Context 中的 Performance 字段
- 移除 AI prompt 中夏普比率自我进化相关内容
- 清理 i18n 翻译文件中的相关条目
该模块基于磁盘存储计算,经常出错,做减法移除
* refactor: 将数据库操作统一迁移到 store 包
- 新增 store/ 包,统一管理所有数据库操作
  - store.go: 主 Store 结构,懒加载各子模块
  - user.go, ai_model.go, exchange.go, trader.go 等子模块
  - 支持加密/解密函数注入 (SetCryptoFuncs)
- 更新 main.go 使用 store.New() 替代 config.NewDatabase()
- 更新 api/server.go 使用 *store.Store 替代 *config.Database
- 更新 manager/trader_manager.go:
  - 新增 LoadTradersFromStore, LoadUserTradersFromStore 方法
  - 删除旧版 LoadUserTraders, LoadTraderByID, loadSingleTrader 等方法
  - 移除 nofx/config 依赖
- 删除 config/database.go 和 config/database_test.go
- 更新 api/server_test.go 使用 store.Trader 类型
- 清理 logger/ 包中未使用的 telegram 相关代码
* refactor: unify encryption key management via .env
- Remove redundant EncryptionManager and SecureStorage
- Simplify CryptoService to load keys from environment variables only
  - RSA_PRIVATE_KEY: RSA private key for client-server encryption
  - DATA_ENCRYPTION_KEY: AES-256 key for database encryption
  - JWT_SECRET: JWT signing key for authentication
- Update start.sh to auto-generate missing keys on first run
- Remove secrets/ directory and file-based key storage
- Delete obsolete encryption setup scripts
- Update .env.example with all required keys
* refactor: unify logger usage across mcp package
- Add MCPLogger adapter in logger package to implement mcp.Logger interface
- Update mcp/config.go to use global logger by default
- Remove redundant defaultLogger from mcp/logger.go
- Keep noopLogger for testing purposes
* chore: remove leftover test RSA key file
* chore: remove unused bootstrap package
* refactor: unify logging to use logger package instead of fmt/log
- Replace all fmt.Print/log.Print calls with logger package
- Add auto-initialization in logger package init() for test compatibility
- Update main.go to initialize logger at startup
- Migrate all packages: api, backtest, config, decision, manager, market, store, trader
* refactor: rename database file from config.db to data.db
- Update main.go, start.sh, docker-compose.yml
- Update migration script and documentation
- Update .gitignore and translations
* fix: add RSA_PRIVATE_KEY to docker-compose environment
* fix: add registration_enabled to /api/config response
* fix: Fix navigation between login and register pages
Use window.location.href instead of react-router's navigate() to fix
the issue where URL changes but the page doesn't reload due to App.tsx
using custom route state management.
* fix: Switch SQLite from WAL to DELETE mode for Docker compatibility
WAL mode causes data sync issues with Docker bind mounts on macOS due
to incompatible file locking mechanisms between the container and host.
DELETE mode (traditional journaling) ensures data is written directly
to the main database file.
* refactor: Remove default user from database initialization
The default user was a legacy placeholder that is no longer needed now
that proper user registration is in place.
* feat: Add order tracking system with centralized status sync
- Add trader_orders table for tracking all order lifecycle
- Implement GetOrderStatus interface for all exchanges (Binance, Bybit, Hyperliquid, Aster, Lighter)
- Create OrderSyncManager for centralized order status polling
- Add trading statistics (Sharpe ratio, win rate, profit factor) to AI context
- Include recent completed orders in AI decision input
- Remove per-order goroutine polling in favor of global sync manager
* feat: Add TradingView K-line chart to dashboard
- Create TradingViewChart component with exchange/symbol selectors
- Support Binance, Bybit, OKX, Coinbase, Kraken, KuCoin exchanges
- Add popular symbols quick selection
- Support multiple timeframes (1m to 1W)
- Add fullscreen mode
- Integrate with Dashboard page below equity chart
- Add i18n translations for zh/en
* refactor: Replace separate charts with tabbed ChartTabs component
- Create ChartTabs component with tab switching between equity curve and K-line
- Add embedded mode support for EquityChart and TradingViewChart
- User can now switch between account equity and market chart in same area
* fix: Use ChartTabs in App.tsx and fix embedded mode in EquityChart
- Replace EquityChart with ChartTabs in App.tsx (the actual dashboard renderer)
- Fix EquityChart embedded mode for error and empty data states
- Rename interval state to timeInterval to avoid shadowing window.setInterval
- Add debug logging to ChartTabs component
* feat: Add position tracking system for accurate trade history
- Add trader_positions table to track complete open/close trades
- Add PositionSyncManager to detect manual closes via polling
- Record position on open, update on close with PnL calculation
- Use positions table for trading stats and recent trades (replacing orders table)
- Fix TradingView chart symbol format (add .P suffix for futures)
- Fix DecisionCard wait/hold action color (gray instead of red)
- Auto-append USDT suffix for custom symbol input
* update
---------
This commit is contained in:
tinkle-community
2025-12-06 01:04:26 +08:00
parent 010676c591
commit f4ece051e7
87 changed files with 6870 additions and 10584 deletions

294
store/ai_model.go Normal file
View File

@@ -0,0 +1,294 @@
package store
import (
"database/sql"
"errors"
"fmt"
"nofx/logger"
"strings"
"time"
)
// AIModelStore AI模型存储
type AIModelStore struct {
db *sql.DB
encryptFunc func(string) string
decryptFunc func(string) string
}
// AIModel AI模型配置
type AIModel struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
CustomAPIURL string `json:"customApiUrl"`
CustomModelName string `json:"customModelName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *AIModelStore) initTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS ai_models (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
provider TEXT NOT NULL,
enabled BOOLEAN DEFAULT 0,
api_key TEXT DEFAULT '',
custom_api_url TEXT DEFAULT '',
custom_model_name TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`)
if err != nil {
return err
}
// 触发器
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at
AFTER UPDATE ON ai_models
BEGIN
UPDATE ai_models SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
`)
if err != nil {
return err
}
// 向后兼容:添加可能缺失的列
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 ''`)
return nil
}
func (s *AIModelStore) initDefaultData() error {
models := []struct {
id, name, provider string
}{
{"deepseek", "DeepSeek", "deepseek"},
{"qwen", "Qwen", "qwen"},
}
for _, model := range models {
_, err := s.db.Exec(`
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled)
VALUES (?, 'default', ?, ?, 0)
`, model.id, model.name, model.provider)
if err != nil {
return fmt.Errorf("初始化AI模型失败: %w", err)
}
}
return nil
}
func (s *AIModelStore) encrypt(plaintext string) string {
if s.encryptFunc != nil {
return s.encryptFunc(plaintext)
}
return plaintext
}
func (s *AIModelStore) decrypt(encrypted string) string {
if s.decryptFunc != nil {
return s.decryptFunc(encrypted)
}
return encrypted
}
// List 获取用户的AI模型列表
func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
rows, err := s.db.Query(`
SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, '') as custom_api_url,
COALESCE(custom_model_name, '') as custom_model_name,
created_at, updated_at
FROM ai_models WHERE user_id = ? ORDER BY id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
models := make([]*AIModel, 0)
for rows.Next() {
var model AIModel
var createdAt, updatedAt string
err := rows.Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
model.APIKey = s.decrypt(model.APIKey)
models = append(models, &model)
}
return models, nil
}
// Get 获取单个AI模型
func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) {
if modelID == "" {
return nil, fmt.Errorf("模型ID不能为空")
}
candidates := []string{}
if userID != "" {
candidates = append(candidates, userID)
}
if userID != "default" {
candidates = append(candidates, "default")
}
if len(candidates) == 0 {
candidates = append(candidates, "default")
}
for _, uid := range candidates {
var model AIModel
var createdAt, updatedAt string
err := s.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1
`, uid, modelID).Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
&createdAt, &updatedAt,
)
if err == nil {
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
model.APIKey = s.decrypt(model.APIKey)
return &model, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
}
return nil, sql.ErrNoRows
}
// GetDefault 获取默认启用的AI模型
func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) {
if userID == "" {
userID = "default"
}
model, err := s.firstEnabled(userID)
if err == nil {
return model, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
if userID != "default" {
return s.firstEnabled("default")
}
return nil, fmt.Errorf("请先在系统中配置可用的AI模型")
}
func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) {
var model AIModel
var createdAt, updatedAt string
err := s.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
FROM ai_models WHERE user_id = ? AND enabled = 1
ORDER BY datetime(updated_at) DESC, id ASC LIMIT 1
`, userID).Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
model.APIKey = s.decrypt(model.APIKey)
return &model, nil
}
// Update 更新AI模型不存在则创建
func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
// 先尝试精确匹配ID
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 {
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')
WHERE id = ? AND user_id = ?
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
return err
}
// 尝试兼容旧逻辑将id作为provider查找
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)
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')
WHERE id = ? AND user_id = ?
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
return err
}
// 创建新记录
if provider == id && (provider == "deepseek" || provider == "qwen") {
provider = id
} else {
parts := strings.Split(id, "_")
if len(parts) >= 2 {
provider = parts[len(parts)-1]
} else {
provider = id
}
}
var name string
err = s.db.QueryRow(`SELECT name FROM ai_models WHERE provider = ? LIMIT 1`, provider).Scan(&name)
if err != nil {
if provider == "deepseek" {
name = "DeepSeek AI"
} else if provider == "qwen" {
name = "Qwen AI"
} else {
name = provider + " AI"
}
}
newModelID := id
if id == provider {
newModelID = fmt.Sprintf("%s_%s", userID, provider)
}
logger.Infof("✓ 创建新的 AI 模型配置: 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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, newModelID, userID, name, provider, enabled, encryptedAPIKey, customAPIURL, customModelName)
return err
}
// Create 创建AI模型
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)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
return err
}

583
store/backtest.go Normal file
View File

@@ -0,0 +1,583 @@
package store
import (
"database/sql"
"encoding/json"
"fmt"
"time"
)
// BacktestStore 回测数据存储
type BacktestStore struct {
db *sql.DB
}
// RunState 回测状态
type RunState string
const (
RunStateCreated RunState = "created"
RunStateRunning RunState = "running"
RunStatePaused RunState = "paused"
RunStateCompleted RunState = "completed"
RunStateFailed RunState = "failed"
)
// RunMetadata 回测元数据
type RunMetadata struct {
RunID string `json:"run_id"`
UserID string `json:"user_id"`
Version int `json:"version"`
State RunState `json:"state"`
Label string `json:"label"`
LastError string `json:"last_error"`
Summary RunSummary `json:"summary"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// RunSummary 回测摘要
type RunSummary struct {
SymbolCount int `json:"symbol_count"`
DecisionTF string `json:"decision_tf"`
ProcessedBars int `json:"processed_bars"`
ProgressPct float64 `json:"progress_pct"`
EquityLast float64 `json:"equity_last"`
MaxDrawdownPct float64 `json:"max_drawdown_pct"`
Liquidated bool `json:"liquidated"`
LiquidationNote string `json:"liquidation_note"`
}
// EquityPoint 权益点
type EquityPoint struct {
Timestamp int64 `json:"timestamp"`
Equity float64 `json:"equity"`
Available float64 `json:"available"`
PnL float64 `json:"pnl"`
PnLPct float64 `json:"pnl_pct"`
DrawdownPct float64 `json:"drawdown_pct"`
Cycle int `json:"cycle"`
}
// TradeEvent 交易事件
type TradeEvent struct {
Timestamp int64 `json:"timestamp"`
Symbol string `json:"symbol"`
Action string `json:"action"`
Side string `json:"side"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
Fee float64 `json:"fee"`
Slippage float64 `json:"slippage"`
OrderValue float64 `json:"order_value"`
RealizedPnL float64 `json:"realized_pnl"`
Leverage int `json:"leverage"`
Cycle int `json:"cycle"`
PositionAfter float64 `json:"position_after"`
LiquidationFlag bool `json:"liquidation_flag"`
Note string `json:"note"`
}
// RunIndexEntry 回测索引条目
type RunIndexEntry struct {
RunID string `json:"run_id"`
State string `json:"state"`
Symbols []string `json:"symbols"`
DecisionTF string `json:"decision_tf"`
EquityLast float64 `json:"equity_last"`
MaxDrawdownPct float64 `json:"max_drawdown_pct"`
StartTS int64 `json:"start_ts"`
EndTS int64 `json:"end_ts"`
CreatedAtISO string `json:"created_at"`
UpdatedAtISO string `json:"updated_at"`
}
// initTables 初始化回测相关表
func (s *BacktestStore) initTables() error {
queries := []string{
// 回测运行主表
`CREATE TABLE IF NOT EXISTS backtest_runs (
run_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL DEFAULT '',
config_json TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL DEFAULT 'created',
label TEXT DEFAULT '',
symbol_count INTEGER DEFAULT 0,
decision_tf TEXT DEFAULT '',
processed_bars INTEGER DEFAULT 0,
progress_pct REAL DEFAULT 0,
equity_last REAL DEFAULT 0,
max_drawdown_pct REAL DEFAULT 0,
liquidated BOOLEAN DEFAULT 0,
liquidation_note TEXT DEFAULT '',
prompt_template TEXT DEFAULT '',
custom_prompt TEXT DEFAULT '',
override_prompt BOOLEAN DEFAULT 0,
ai_provider TEXT DEFAULT '',
ai_model TEXT DEFAULT '',
last_error TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// 回测检查点
`CREATE TABLE IF NOT EXISTS backtest_checkpoints (
run_id TEXT PRIMARY KEY,
payload BLOB NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
)`,
// 回测权益曲线
`CREATE TABLE IF NOT EXISTS backtest_equity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
ts INTEGER NOT NULL,
equity REAL NOT NULL,
available REAL NOT NULL,
pnl REAL NOT NULL,
pnl_pct REAL NOT NULL,
dd_pct REAL NOT NULL,
cycle INTEGER NOT NULL,
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
)`,
// 回测交易记录
`CREATE TABLE IF NOT EXISTS backtest_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
ts INTEGER NOT NULL,
symbol TEXT NOT NULL,
action TEXT NOT NULL,
side TEXT DEFAULT '',
qty REAL DEFAULT 0,
price REAL DEFAULT 0,
fee REAL DEFAULT 0,
slippage REAL DEFAULT 0,
order_value REAL DEFAULT 0,
realized_pnl REAL DEFAULT 0,
leverage INTEGER DEFAULT 0,
cycle INTEGER DEFAULT 0,
position_after REAL DEFAULT 0,
liquidation BOOLEAN DEFAULT 0,
note TEXT DEFAULT '',
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
)`,
// 回测指标
`CREATE TABLE IF NOT EXISTS backtest_metrics (
run_id TEXT PRIMARY KEY,
payload BLOB NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
)`,
// 回测决策日志
`CREATE TABLE IF NOT EXISTS backtest_decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
cycle INTEGER NOT NULL,
payload BLOB NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE
)`,
// 索引
`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)`,
`CREATE INDEX IF NOT EXISTS idx_backtest_decisions_run_cycle ON backtest_decisions(run_id, cycle)`,
}
for _, query := range queries {
if _, err := s.db.Exec(query); err != nil {
return fmt.Errorf("执行SQL失败: %w", err)
}
}
// 添加可能缺失的列(向后兼容)
s.addColumnIfNotExists("backtest_runs", "label", "TEXT DEFAULT ''")
s.addColumnIfNotExists("backtest_runs", "last_error", "TEXT DEFAULT ''")
s.addColumnIfNotExists("backtest_trades", "leverage", "INTEGER DEFAULT 0")
return nil
}
func (s *BacktestStore) addColumnIfNotExists(table, column, definition string) {
rows, err := s.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var cid int
var name, ctype string
var notnull, pk int
var dflt interface{}
if err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt, &pk); err != nil {
continue
}
if name == column {
return // 列已存在
}
}
s.db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition))
}
// SaveCheckpoint 保存检查点
func (s *BacktestStore) SaveCheckpoint(runID string, payload []byte) error {
_, err := s.db.Exec(`
INSERT INTO backtest_checkpoints (run_id, payload, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload, updated_at=CURRENT_TIMESTAMP
`, runID, payload)
return err
}
// LoadCheckpoint 加载检查点
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 保存运行元数据
func (s *BacktestStore) SaveRunMetadata(meta *RunMetadata) error {
created := meta.CreatedAt.UTC().Format(time.RFC3339)
updated := meta.UpdatedAt.UTC().Format(time.RFC3339)
userID := meta.UserID
if _, err := s.db.Exec(`
INSERT INTO backtest_runs (run_id, user_id, label, last_error, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(run_id) DO NOTHING
`, meta.RunID, userID, meta.Label, meta.LastError, created, updated); err != nil {
return err
}
_, err := s.db.Exec(`
UPDATE backtest_runs
SET user_id = ?, state = ?, symbol_count = ?, decision_tf = ?, processed_bars = ?,
progress_pct = ?, equity_last = ?, max_drawdown_pct = ?, liquidated = ?,
liquidation_note = ?, label = ?, last_error = ?, updated_at = ?
WHERE run_id = ?
`, userID, string(meta.State), meta.Summary.SymbolCount, meta.Summary.DecisionTF,
meta.Summary.ProcessedBars, meta.Summary.ProgressPct, meta.Summary.EquityLast,
meta.Summary.MaxDrawdownPct, meta.Summary.Liquidated, meta.Summary.LiquidationNote,
meta.Label, meta.LastError, updated, meta.RunID)
return err
}
// LoadRunMetadata 加载运行元数据
func (s *BacktestStore) LoadRunMetadata(runID string) (*RunMetadata, error) {
var (
userID string
state string
label string
lastErr string
symbolCount int
decisionTF string
processedBars int
progressPct float64
equityLast float64
maxDD float64
liquidated bool
liquidationNote string
createdISO string
updatedISO string
)
err := s.db.QueryRow(`
SELECT user_id, state, label, last_error, symbol_count, decision_tf, processed_bars,
progress_pct, equity_last, max_drawdown_pct, liquidated, liquidation_note,
created_at, updated_at
FROM backtest_runs WHERE run_id = ?
`, runID).Scan(&userID, &state, &label, &lastErr, &symbolCount, &decisionTF,
&processedBars, &progressPct, &equityLast, &maxDD, &liquidated, &liquidationNote,
&createdISO, &updatedISO)
if err != nil {
return nil, err
}
meta := &RunMetadata{
RunID: runID,
UserID: userID,
Version: 1,
State: RunState(state),
Label: label,
LastError: lastErr,
Summary: RunSummary{
SymbolCount: symbolCount,
DecisionTF: decisionTF,
ProcessedBars: processedBars,
ProgressPct: progressPct,
EquityLast: equityLast,
MaxDrawdownPct: maxDD,
Liquidated: liquidated,
LiquidationNote: liquidationNote,
},
}
meta.CreatedAt, _ = time.Parse(time.RFC3339, createdISO)
meta.UpdatedAt, _ = time.Parse(time.RFC3339, updatedISO)
return meta, nil
}
// ListRunIDs 列出所有运行ID
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 {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var runID string
if err := rows.Scan(&runID); err != nil {
return nil, err
}
ids = append(ids, runID)
}
return ids, rows.Err()
}
// AppendEquityPoint 添加权益点
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, runID, point.Timestamp, point.Equity, point.Available, point.PnL,
point.PnLPct, point.DrawdownPct, point.Cycle)
return err
}
// LoadEquityPoints 加载权益点
func (s *BacktestStore) LoadEquityPoints(runID string) ([]EquityPoint, error) {
rows, err := s.db.Query(`
SELECT ts, equity, available, pnl, pnl_pct, dd_pct, cycle
FROM backtest_equity WHERE run_id = ? ORDER BY ts ASC
`, runID)
if err != nil {
return nil, err
}
defer rows.Close()
points := make([]EquityPoint, 0)
for rows.Next() {
var point EquityPoint
if err := rows.Scan(&point.Timestamp, &point.Equity, &point.Available,
&point.PnL, &point.PnLPct, &point.DrawdownPct, &point.Cycle); err != nil {
return nil, err
}
points = append(points, point)
}
return points, rows.Err()
}
// AppendTradeEvent 添加交易事件
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,
slippage, order_value, realized_pnl, leverage, cycle,
position_after, liquidation, note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, runID, event.Timestamp, event.Symbol, event.Action, event.Side, event.Quantity,
event.Price, event.Fee, event.Slippage, event.OrderValue, event.RealizedPnL,
event.Leverage, event.Cycle, event.PositionAfter, event.LiquidationFlag, event.Note)
return err
}
// LoadTradeEvents 加载交易事件
func (s *BacktestStore) LoadTradeEvents(runID string) ([]TradeEvent, error) {
rows, err := s.db.Query(`
SELECT ts, symbol, action, side, qty, price, fee, slippage, order_value,
realized_pnl, leverage, cycle, position_after, liquidation, note
FROM backtest_trades WHERE run_id = ? ORDER BY ts ASC
`, runID)
if err != nil {
return nil, err
}
defer rows.Close()
events := make([]TradeEvent, 0)
for rows.Next() {
var event TradeEvent
if err := rows.Scan(&event.Timestamp, &event.Symbol, &event.Action, &event.Side,
&event.Quantity, &event.Price, &event.Fee, &event.Slippage, &event.OrderValue,
&event.RealizedPnL, &event.Leverage, &event.Cycle, &event.PositionAfter,
&event.LiquidationFlag, &event.Note); err != nil {
return nil, err
}
events = append(events, event)
}
return events, rows.Err()
}
// SaveMetrics 保存指标
func (s *BacktestStore) SaveMetrics(runID string, payload []byte) error {
_, err := s.db.Exec(`
INSERT INTO backtest_metrics (run_id, payload, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload, updated_at=CURRENT_TIMESTAMP
`, runID, payload)
return err
}
// LoadMetrics 加载指标
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 保存决策记录
func (s *BacktestStore) SaveDecisionRecord(runID string, cycle int, payload []byte) error {
_, err := s.db.Exec(`
INSERT INTO backtest_decisions (run_id, cycle, payload)
VALUES (?, ?, ?)
`, runID, cycle, payload)
return err
}
// LoadDecisionRecords 加载决策记录
func (s *BacktestStore) LoadDecisionRecords(runID string, limit, offset int) ([]json.RawMessage, error) {
rows, err := s.db.Query(`
SELECT payload FROM backtest_decisions
WHERE run_id = ?
ORDER BY id DESC
LIMIT ? OFFSET ?
`, runID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
records := make([]json.RawMessage, 0, limit)
for rows.Next() {
var payload []byte
if err := rows.Scan(&payload); err != nil {
return nil, err
}
records = append(records, json.RawMessage(payload))
}
return records, rows.Err()
}
// LoadLatestDecision 加载最新决策
func (s *BacktestStore) LoadLatestDecision(runID string, cycle int) ([]byte, error) {
var query string
var args []interface{}
if cycle > 0 {
query = `SELECT payload FROM backtest_decisions WHERE run_id = ? AND cycle = ? ORDER BY datetime(created_at) DESC LIMIT 1`
args = []interface{}{runID, cycle}
} else {
query = `SELECT payload FROM backtest_decisions WHERE run_id = ? ORDER BY datetime(created_at) DESC LIMIT 1`
args = []interface{}{runID}
}
var payload []byte
err := s.db.QueryRow(query, args...).Scan(&payload)
return payload, err
}
// UpdateProgress 更新进度
func (s *BacktestStore) UpdateProgress(runID string, progressPct, equity float64, barIndex int, liquidated bool) error {
_, err := s.db.Exec(`
UPDATE backtest_runs
SET progress_pct = ?, equity_last = ?, processed_bars = ?, liquidated = ?, updated_at = CURRENT_TIMESTAMP
WHERE run_id = ?
`, progressPct, equity, barIndex, liquidated, runID)
return err
}
// ListIndexEntries 列出索引条目
func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) {
rows, err := s.db.Query(`
SELECT run_id, state, symbol_count, decision_tf, equity_last, max_drawdown_pct,
created_at, updated_at, config_json
FROM backtest_runs
ORDER BY datetime(updated_at) DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []RunIndexEntry
for rows.Next() {
var entry RunIndexEntry
var symbolCnt int
var cfgJSON []byte
var createdISO, updatedISO string
if err := rows.Scan(&entry.RunID, &entry.State, &symbolCnt, &entry.DecisionTF,
&entry.EquityLast, &entry.MaxDrawdownPct, &createdISO, &updatedISO, &cfgJSON); err != nil {
return nil, err
}
entry.CreatedAtISO = createdISO
entry.UpdatedAtISO = updatedISO
entry.Symbols = make([]string, 0, symbolCnt)
// 尝试从配置中提取更多信息
if len(cfgJSON) > 0 {
var cfg struct {
Symbols []string `json:"symbols"`
StartTS int64 `json:"start_ts"`
EndTS int64 `json:"end_ts"`
}
if json.Unmarshal(cfgJSON, &cfg) == nil {
entry.Symbols = cfg.Symbols
entry.StartTS = cfg.StartTS
entry.EndTS = cfg.EndTS
}
}
entries = append(entries, entry)
}
return entries, rows.Err()
}
// DeleteRun 删除运行
func (s *BacktestStore) DeleteRun(runID string) error {
_, err := s.db.Exec(`DELETE FROM backtest_runs WHERE run_id = ?`, runID)
return err
}
// SaveConfig 保存配置
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 == "" {
userID = "default"
}
_, err := s.db.Exec(`
INSERT INTO backtest_runs (run_id, user_id, config_json, prompt_template, custom_prompt,
override_prompt, ai_provider, ai_model, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(run_id) DO NOTHING
`, runID, userID, configJSON, template, customPrompt, override, provider, model, now, now)
if err != nil {
return err
}
_, err = s.db.Exec(`
UPDATE backtest_runs
SET user_id = ?, config_json = ?, prompt_template = ?, custom_prompt = ?,
override_prompt = ?, ai_provider = ?, ai_model = ?, updated_at = CURRENT_TIMESTAMP
WHERE run_id = ?
`, userID, configJSON, template, customPrompt, override, provider, model, runID)
return err
}
// LoadConfig 加载配置
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)
return payload, err
}

121
store/beta_code.go Normal file
View File

@@ -0,0 +1,121 @@
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
}

530
store/decision.go Normal file
View File

@@ -0,0 +1,530 @@
package store
import (
"database/sql"
"encoding/json"
"fmt"
"time"
)
// DecisionStore 决策日志存储
type DecisionStore struct {
db *sql.DB
}
// DecisionRecord 决策记录
type DecisionRecord struct {
ID int64 `json:"id"`
TraderID string `json:"trader_id"`
CycleNumber int `json:"cycle_number"`
Timestamp time.Time `json:"timestamp"`
SystemPrompt string `json:"system_prompt"`
InputPrompt string `json:"input_prompt"`
CoTTrace string `json:"cot_trace"`
DecisionJSON string `json:"decision_json"`
CandidateCoins []string `json:"candidate_coins"`
ExecutionLog []string `json:"execution_log"`
Success bool `json:"success"`
ErrorMessage string `json:"error_message"`
AIRequestDurationMs int64 `json:"ai_request_duration_ms"`
AccountState AccountSnapshot `json:"account_state"`
Positions []PositionSnapshot `json:"positions"`
Decisions []DecisionAction `json:"decisions"`
}
// AccountSnapshot 账户状态快照
type AccountSnapshot struct {
TotalBalance float64 `json:"total_balance"`
AvailableBalance float64 `json:"available_balance"`
TotalUnrealizedProfit float64 `json:"total_unrealized_profit"`
PositionCount int `json:"position_count"`
MarginUsedPct float64 `json:"margin_used_pct"`
InitialBalance float64 `json:"initial_balance"`
}
// PositionSnapshot 持仓快照
type PositionSnapshot struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
PositionAmt float64 `json:"position_amt"`
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
UnrealizedProfit float64 `json:"unrealized_profit"`
Leverage float64 `json:"leverage"`
LiquidationPrice float64 `json:"liquidation_price"`
}
// DecisionAction 决策动作
type DecisionAction struct {
Action string `json:"action"`
Symbol string `json:"symbol"`
Quantity float64 `json:"quantity"`
Leverage int `json:"leverage"`
Price float64 `json:"price"`
OrderID int64 `json:"order_id"`
Timestamp time.Time `json:"timestamp"`
Success bool `json:"success"`
Error string `json:"error"`
}
// Statistics 统计信息
type Statistics struct {
TotalCycles int `json:"total_cycles"`
SuccessfulCycles int `json:"successful_cycles"`
FailedCycles int `json:"failed_cycles"`
TotalOpenPositions int `json:"total_open_positions"`
TotalClosePositions int `json:"total_close_positions"`
}
// initTables 初始化决策相关表
func (s *DecisionStore) initTables() error {
queries := []string{
// 决策记录主表
`CREATE TABLE IF NOT EXISTS decision_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trader_id TEXT NOT NULL,
cycle_number INTEGER NOT NULL,
timestamp DATETIME NOT NULL,
system_prompt TEXT DEFAULT '',
input_prompt TEXT DEFAULT '',
cot_trace TEXT DEFAULT '',
decision_json TEXT DEFAULT '',
candidate_coins TEXT DEFAULT '',
execution_log TEXT DEFAULT '',
success BOOLEAN DEFAULT 0,
error_message TEXT DEFAULT '',
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 {
if _, err := s.db.Exec(query); err != nil {
return fmt.Errorf("执行SQL失败: %w", err)
}
}
return nil
}
// LogDecision 记录决策
func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
if record.Timestamp.IsZero() {
record.Timestamp = time.Now().UTC()
} else {
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(`
INSERT INTO decision_records (
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
cot_trace, decision_json, candidate_coins, execution_log,
success, error_message, ai_request_duration_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
record.TraderID, record.CycleNumber, record.Timestamp.Format(time.RFC3339),
record.SystemPrompt, record.InputPrompt, record.CoTTrace, record.DecisionJSON,
string(candidateCoinsJSON), string(executionLogJSON),
record.Success, record.ErrorMessage, record.AIRequestDurationMs,
)
if err != nil {
return fmt.Errorf("插入决策记录失败: %w", err)
}
decisionID, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取决策ID失败: %w", err)
}
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
}
// GetLatestRecords 获取指定交易员最近N条记录按时间正序从旧到新
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,
cot_trace, decision_json, candidate_coins, execution_log,
success, error_message, ai_request_duration_ms
FROM decision_records
WHERE trader_id = ?
ORDER BY timestamp DESC
LIMIT ?
`, traderID, n)
if err != nil {
return nil, fmt.Errorf("查询决策记录失败: %w", err)
}
defer rows.Close()
var records []*DecisionRecord
for rows.Next() {
record, err := s.scanDecisionRecord(rows)
if err != nil {
continue
}
records = append(records, record)
}
// 填充关联数据
for _, record := range records {
s.fillRecordDetails(record)
}
// 反转数组,让时间从旧到新排列
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
return records, nil
}
// GetAllLatestRecords 获取所有交易员最近N条记录
func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
rows, err := s.db.Query(`
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
cot_trace, decision_json, candidate_coins, execution_log,
success, error_message, ai_request_duration_ms
FROM decision_records
ORDER BY timestamp DESC
LIMIT ?
`, n)
if err != nil {
return nil, fmt.Errorf("查询决策记录失败: %w", err)
}
defer rows.Close()
var records []*DecisionRecord
for rows.Next() {
record, err := s.scanDecisionRecord(rows)
if err != nil {
continue
}
records = append(records, record)
}
// 反转数组
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
return records, nil
}
// GetRecordsByDate 获取指定交易员指定日期的所有记录
func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*DecisionRecord, error) {
dateStr := date.Format("2006-01-02")
rows, err := s.db.Query(`
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
cot_trace, decision_json, candidate_coins, execution_log,
success, error_message, ai_request_duration_ms
FROM decision_records
WHERE trader_id = ? AND DATE(timestamp) = ?
ORDER BY timestamp ASC
`, traderID, dateStr)
if err != nil {
return nil, fmt.Errorf("查询决策记录失败: %w", err)
}
defer rows.Close()
var records []*DecisionRecord
for rows.Next() {
record, err := s.scanDecisionRecord(rows)
if err != nil {
continue
}
records = append(records, record)
}
return records, nil
}
// CleanOldRecords 清理N天前的旧记录
func (s *DecisionStore) CleanOldRecords(traderID string, days int) (int64, error) {
cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339)
result, err := s.db.Exec(`
DELETE FROM decision_records
WHERE trader_id = ? AND timestamp < ?
`, traderID, cutoffTime)
if err != nil {
return 0, fmt.Errorf("清理旧记录失败: %w", err)
}
return result.RowsAffected()
}
// GetStatistics 获取指定交易员的统计信息
func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) {
stats := &Statistics{}
err := s.db.QueryRow(`
SELECT COUNT(*) FROM decision_records WHERE trader_id = ?
`, traderID).Scan(&stats.TotalCycles)
if err != nil {
return nil, fmt.Errorf("查询总周期数失败: %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)
}
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')
`, 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')
`, traderID).Scan(&stats.TotalClosePositions)
if err != nil {
return nil, fmt.Errorf("查询平仓次数失败: %w", err)
}
return stats, nil
}
// GetAllStatistics 获取所有交易员的统计信息
func (s *DecisionStore) GetAllStatistics() (*Statistics, error) {
stats := &Statistics{}
s.db.QueryRow(`SELECT COUNT(*) FROM decision_records`).Scan(&stats.TotalCycles)
s.db.QueryRow(`SELECT COUNT(*) FROM decision_records WHERE success = 1`).Scan(&stats.SuccessfulCycles)
stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles
s.db.QueryRow(`
SELECT COUNT(*) FROM decision_actions
WHERE success = 1 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')
`).Scan(&stats.TotalClosePositions)
return stats, nil
}
// GetLastCycleNumber 获取指定交易员的最后周期编号
func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) {
var cycleNumber int
err := s.db.QueryRow(`
SELECT COALESCE(MAX(cycle_number), 0) FROM decision_records WHERE trader_id = ?
`, traderID).Scan(&cycleNumber)
if err != nil {
return 0, err
}
return cycleNumber, nil
}
// scanDecisionRecord 从行中扫描决策记录
func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, error) {
var record DecisionRecord
var timestampStr string
var candidateCoinsJSON, executionLogJSON string
err := rows.Scan(
&record.ID, &record.TraderID, &record.CycleNumber, &timestampStr,
&record.SystemPrompt, &record.InputPrompt, &record.CoTTrace,
&record.DecisionJSON, &candidateCoinsJSON, &executionLogJSON,
&record.Success, &record.ErrorMessage, &record.AIRequestDurationMs,
)
if err != nil {
return nil, err
}
record.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
json.Unmarshal([]byte(candidateCoinsJSON), &record.CandidateCoins)
json.Unmarshal([]byte(executionLogJSON), &record.ExecutionLog)
return &record, nil
}
// fillRecordDetails 填充决策记录的关联数据
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)
}
}
}

245
store/exchange.go Normal file
View File

@@ -0,0 +1,245 @@
package store
import (
"database/sql"
"fmt"
"nofx/logger"
"strings"
"time"
)
// ExchangeStore 交易所存储
type ExchangeStore struct {
db *sql.DB
encryptFunc func(string) string
decryptFunc func(string) string
}
// Exchange 交易所配置
type Exchange struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
Testnet bool `json:"testnet"`
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
AsterUser string `json:"asterUser"`
AsterSigner string `json:"asterSigner"`
AsterPrivateKey string `json:"asterPrivateKey"`
LighterWalletAddr string `json:"lighterWalletAddr"`
LighterPrivateKey string `json:"lighterPrivateKey"`
LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *ExchangeStore) initTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS exchanges (
id TEXT NOT NULL,
user_id TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
type TEXT NOT NULL,
enabled BOOLEAN DEFAULT 0,
api_key TEXT DEFAULT '',
secret_key TEXT DEFAULT '',
testnet BOOLEAN DEFAULT 0,
hyperliquid_wallet_addr TEXT DEFAULT '',
aster_user TEXT DEFAULT '',
aster_signer TEXT DEFAULT '',
aster_private_key TEXT DEFAULT '',
lighter_wallet_addr TEXT DEFAULT '',
lighter_private_key TEXT DEFAULT '',
lighter_api_key_private_key TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`)
if err != nil {
return err
}
// 触发器
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
AFTER UPDATE ON exchanges
BEGIN
UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id AND user_id = NEW.user_id;
END
`)
return err
}
func (s *ExchangeStore) initDefaultData() error {
exchanges := []struct {
id, name, typ string
}{
{"binance", "Binance Futures", "binance"},
{"bybit", "Bybit Futures", "bybit"},
{"hyperliquid", "Hyperliquid", "hyperliquid"},
{"aster", "Aster DEX", "aster"},
{"lighter", "LIGHTER DEX", "lighter"},
}
for _, exchange := range exchanges {
_, err := s.db.Exec(`
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled)
VALUES (?, 'default', ?, ?, 0)
`, exchange.id, exchange.name, exchange.typ)
if err != nil {
return fmt.Errorf("初始化交易所失败: %w", err)
}
}
return nil
}
func (s *ExchangeStore) encrypt(plaintext string) string {
if s.encryptFunc != nil {
return s.encryptFunc(plaintext)
}
return plaintext
}
func (s *ExchangeStore) decrypt(encrypted string) string {
if s.decryptFunc != nil {
return s.decryptFunc(encrypted)
}
return encrypted
}
// List 获取用户的交易所列表
func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
rows, err := s.db.Query(`
SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet,
COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
COALESCE(aster_user, '') as aster_user,
COALESCE(aster_signer, '') as aster_signer,
COALESCE(aster_private_key, '') as aster_private_key,
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(lighter_private_key, '') as lighter_private_key,
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
created_at, updated_at
FROM exchanges WHERE user_id = ? ORDER BY id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
exchanges := make([]*Exchange, 0)
for rows.Next() {
var e Exchange
var createdAt, updatedAt string
err := rows.Scan(
&e.ID, &e.UserID, &e.Name, &e.Type,
&e.Enabled, &e.APIKey, &e.SecretKey, &e.Testnet,
&e.HyperliquidWalletAddr, &e.AsterUser, &e.AsterSigner, &e.AsterPrivateKey,
&e.LighterWalletAddr, &e.LighterPrivateKey, &e.LighterAPIKeyPrivateKey,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
e.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
e.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
e.APIKey = s.decrypt(e.APIKey)
e.SecretKey = s.decrypt(e.SecretKey)
e.AsterPrivateKey = s.decrypt(e.AsterPrivateKey)
e.LighterPrivateKey = s.decrypt(e.LighterPrivateKey)
e.LighterAPIKeyPrivateKey = s.decrypt(e.LighterAPIKeyPrivateKey)
exchanges = append(exchanges, &e)
}
return exchanges, nil
}
// Update 更新交易所配置
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey string, testnet bool,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error {
logger.Debugf("🔧 ExchangeStore.Update: userID=%s, id=%s, enabled=%v", userID, id, enabled)
setClauses := []string{
"enabled = ?",
"testnet = ?",
"hyperliquid_wallet_addr = ?",
"aster_user = ?",
"aster_signer = ?",
"lighter_wallet_addr = ?",
"updated_at = datetime('now')",
}
args := []interface{}{enabled, testnet, hyperliquidWalletAddr, asterUser, asterSigner, lighterWalletAddr}
if apiKey != "" {
setClauses = append(setClauses, "api_key = ?")
args = append(args, s.encrypt(apiKey))
}
if secretKey != "" {
setClauses = append(setClauses, "secret_key = ?")
args = append(args, s.encrypt(secretKey))
}
if asterPrivateKey != "" {
setClauses = append(setClauses, "aster_private_key = ?")
args = append(args, s.encrypt(asterPrivateKey))
}
if lighterPrivateKey != "" {
setClauses = append(setClauses, "lighter_private_key = ?")
args = append(args, s.encrypt(lighterPrivateKey))
}
args = append(args, id, userID)
query := fmt.Sprintf(`UPDATE exchanges SET %s WHERE id = ? AND user_id = ?`, strings.Join(setClauses, ", "))
result, err := s.db.Exec(query, args...)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
// 创建新记录
var name, typ string
switch id {
case "binance":
name, typ = "Binance Futures", "cex"
case "bybit":
name, typ = "Bybit Futures", "cex"
case "hyperliquid":
name, typ = "Hyperliquid", "dex"
case "aster":
name, typ = "Aster DEX", "dex"
case "lighter":
name, typ = "LIGHTER DEX", "dex"
default:
name, typ = id+" Exchange", "cex"
}
_, err = s.db.Exec(`
INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet,
hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key,
lighter_wallet_addr, lighter_private_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), testnet,
hyperliquidWalletAddr, asterUser, asterSigner, s.encrypt(asterPrivateKey),
lighterWalletAddr, s.encrypt(lighterPrivateKey))
return err
}
return nil
}
// Create 创建交易所配置
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(`
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet,
hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key,
lighter_wallet_addr, lighter_private_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '')
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), testnet,
hyperliquidWalletAddr, asterUser, asterSigner, s.encrypt(asterPrivateKey))
return err
}

511
store/order.go Normal file
View File

@@ -0,0 +1,511 @@
package store
import (
"database/sql"
"fmt"
"math"
"time"
)
// TraderOrder 交易员订单记录
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"` // 交易对
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"` // 杠杆倍数
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"` // 开仓价(平仓时记录)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
FilledAt time.Time `json:"filled_at"` // 成交时间
}
// TraderStats 交易统计指标
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"` // 最大回撤 (%)
}
// CompletedOrder 已完成订单用于AI输入
type CompletedOrder struct {
Symbol string `json:"symbol"` // 交易对
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"` // 成交时间
}
// OrderStore 订单存储
type OrderStore struct {
db *sql.DB
}
// NewOrderStore 创建订单存储实例
func NewOrderStore(db *sql.DB) *OrderStore {
return &OrderStore{db: db}
}
// InitTables 初始化订单表
func (s *OrderStore) InitTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS trader_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trader_id TEXT NOT NULL,
order_id TEXT NOT NULL,
client_order_id TEXT DEFAULT '',
symbol TEXT NOT NULL,
side TEXT NOT NULL,
position_side TEXT DEFAULT '',
action TEXT NOT NULL,
order_type TEXT DEFAULT 'MARKET',
quantity REAL NOT NULL,
price REAL DEFAULT 0,
avg_price REAL DEFAULT 0,
executed_qty REAL DEFAULT 0,
leverage INTEGER DEFAULT 1,
status TEXT DEFAULT 'NEW',
fee REAL DEFAULT 0,
fee_asset TEXT DEFAULT 'USDT',
realized_pnl REAL DEFAULT 0,
entry_price REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
filled_at DATETIME,
UNIQUE(trader_id, order_id)
)
`)
if err != nil {
return fmt.Errorf("创建trader_orders表失败: %w", err)
}
// 创建索引
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)`,
`CREATE INDEX IF NOT EXISTS idx_trader_orders_symbol ON trader_orders(trader_id, symbol)`,
`CREATE INDEX IF NOT EXISTS idx_trader_orders_filled ON trader_orders(trader_id, filled_at DESC)`,
}
for _, idx := range indices {
if _, err := s.db.Exec(idx); err != nil {
return fmt.Errorf("创建索引失败: %w", err)
}
}
return nil
}
// Create 创建订单记录
func (s *OrderStore) Create(order *TraderOrder) error {
now := time.Now().Format(time.RFC3339)
result, err := s.db.Exec(`
INSERT INTO trader_orders (
trader_id, order_id, client_order_id, symbol, side, position_side,
action, order_type, quantity, price, avg_price, executed_qty,
leverage, status, fee, fee_asset, realized_pnl, entry_price,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
order.TraderID, order.OrderID, order.ClientOrderID, order.Symbol,
order.Side, order.PositionSide, order.Action, order.OrderType,
order.Quantity, order.Price, order.AvgPrice, order.ExecutedQty,
order.Leverage, order.Status, order.Fee, order.FeeAsset,
order.RealizedPnL, order.EntryPrice, now, now,
)
if err != nil {
return fmt.Errorf("创建订单记录失败: %w", err)
}
id, _ := result.LastInsertId()
order.ID = id
return nil
}
// Update 更新订单记录
func (s *OrderStore) Update(order *TraderOrder) error {
now := time.Now().Format(time.RFC3339)
filledAt := ""
if !order.FilledAt.IsZero() {
filledAt = order.FilledAt.Format(time.RFC3339)
}
_, err := s.db.Exec(`
UPDATE trader_orders SET
avg_price = ?, executed_qty = ?, status = ?, fee = ?,
realized_pnl = ?, entry_price = ?, updated_at = ?, filled_at = ?
WHERE trader_id = ? AND order_id = ?
`,
order.AvgPrice, order.ExecutedQty, order.Status, order.Fee,
order.RealizedPnL, order.EntryPrice, now, filledAt,
order.TraderID, order.OrderID,
)
if err != nil {
return fmt.Errorf("更新订单记录失败: %w", err)
}
return nil
}
// GetByOrderID 根据订单ID获取订单
func (s *OrderStore) GetByOrderID(traderID, orderID string) (*TraderOrder, error) {
var order TraderOrder
var createdAt, updatedAt, filledAt sql.NullString
err := s.db.QueryRow(`
SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side,
action, order_type, quantity, price, avg_price, executed_qty,
leverage, status, fee, fee_asset, realized_pnl, entry_price,
created_at, updated_at, filled_at
FROM trader_orders WHERE trader_id = ? AND order_id = ?
`, traderID, orderID).Scan(
&order.ID, &order.TraderID, &order.OrderID, &order.ClientOrderID,
&order.Symbol, &order.Side, &order.PositionSide, &order.Action,
&order.OrderType, &order.Quantity, &order.Price, &order.AvgPrice,
&order.ExecutedQty, &order.Leverage, &order.Status, &order.Fee,
&order.FeeAsset, &order.RealizedPnL, &order.EntryPrice,
&createdAt, &updatedAt, &filledAt,
)
if err != nil {
return nil, err
}
if createdAt.Valid {
order.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
}
if updatedAt.Valid {
order.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt.String)
}
if filledAt.Valid {
order.FilledAt, _ = time.Parse(time.RFC3339, filledAt.String)
}
return &order, nil
}
// GetLatestOpenOrder 获取某币种最近的开仓订单(用于计算平仓盈亏)
func (s *OrderStore) GetLatestOpenOrder(traderID, symbol, side string) (*TraderOrder, error) {
// side: long -> 找 open_long, short -> 找 open_short
action := "open_long"
if side == "short" {
action = "open_short"
}
var order TraderOrder
var createdAt, updatedAt, filledAt sql.NullString
err := s.db.QueryRow(`
SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side,
action, order_type, quantity, price, avg_price, executed_qty,
leverage, status, fee, fee_asset, realized_pnl, entry_price,
created_at, updated_at, filled_at
FROM trader_orders
WHERE trader_id = ? AND symbol = ? AND action = ? AND status = 'FILLED'
ORDER BY filled_at DESC LIMIT 1
`, traderID, symbol, action).Scan(
&order.ID, &order.TraderID, &order.OrderID, &order.ClientOrderID,
&order.Symbol, &order.Side, &order.PositionSide, &order.Action,
&order.OrderType, &order.Quantity, &order.Price, &order.AvgPrice,
&order.ExecutedQty, &order.Leverage, &order.Status, &order.Fee,
&order.FeeAsset, &order.RealizedPnL, &order.EntryPrice,
&createdAt, &updatedAt, &filledAt,
)
if err != nil {
return nil, err
}
if createdAt.Valid {
order.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
}
if updatedAt.Valid {
order.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt.String)
}
if filledAt.Valid {
order.FilledAt, _ = time.Parse(time.RFC3339, filledAt.String)
}
return &order, nil
}
// GetRecentCompletedOrders 获取最近已完成的平仓订单
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,
realized_pnl, fee, leverage, filled_at
FROM trader_orders
WHERE trader_id = ? AND status = 'FILLED'
AND (action = 'close_long' OR action = 'close_short')
ORDER BY filled_at DESC
LIMIT ?
`, traderID, limit)
if err != nil {
return nil, fmt.Errorf("查询已完成订单失败: %w", err)
}
defer rows.Close()
var orders []CompletedOrder
for rows.Next() {
var o CompletedOrder
var filledAt sql.NullString
var side sql.NullString
err := rows.Scan(
&o.Symbol, &o.Action, &side, &o.Quantity, &o.EntryPrice, &o.ExitPrice,
&o.RealizedPnL, &o.Fee, &o.Leverage, &filledAt,
)
if err != nil {
continue
}
// 根据action推断side
if o.Action == "close_long" {
o.Side = "long"
} else if o.Action == "close_short" {
o.Side = "short"
} else if side.Valid {
o.Side = side.String
}
// 计算盈亏百分比
if o.EntryPrice > 0 {
if o.Side == "long" {
o.PnLPct = (o.ExitPrice - o.EntryPrice) / o.EntryPrice * 100 * float64(o.Leverage)
} else {
o.PnLPct = (o.EntryPrice - o.ExitPrice) / o.EntryPrice * 100 * float64(o.Leverage)
}
}
if filledAt.Valid {
o.FilledAt, _ = time.Parse(time.RFC3339, filledAt.String)
}
orders = append(orders, o)
}
return orders, nil
}
// GetTraderStats 获取交易统计指标
func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) {
stats := &TraderStats{}
// 查询所有已完成的平仓订单
rows, err := s.db.Query(`
SELECT realized_pnl, fee, filled_at
FROM trader_orders
WHERE trader_id = ? AND status = 'FILLED'
AND (action = 'close_long' OR action = 'close_short')
ORDER BY filled_at ASC
`, traderID)
if err != nil {
return nil, fmt.Errorf("查询订单统计失败: %w", err)
}
defer rows.Close()
var pnls []float64
var totalWin, totalLoss float64
for rows.Next() {
var pnl, fee float64
var filledAt sql.NullString
if err := rows.Scan(&pnl, &fee, &filledAt); err != nil {
continue
}
stats.TotalTrades++
stats.TotalPnL += pnl
stats.TotalFee += fee
pnls = append(pnls, pnl)
if pnl > 0 {
stats.WinTrades++
totalWin += pnl
} else if pnl < 0 {
stats.LossTrades++
totalLoss += math.Abs(pnl)
}
}
// 计算胜率
if stats.TotalTrades > 0 {
stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100
}
// 计算盈亏比
if totalLoss > 0 {
stats.ProfitFactor = totalWin / totalLoss
}
// 计算平均盈亏
if stats.WinTrades > 0 {
stats.AvgWin = totalWin / float64(stats.WinTrades)
}
if stats.LossTrades > 0 {
stats.AvgLoss = totalLoss / float64(stats.LossTrades)
}
// 计算夏普比(使用盈亏序列)
if len(pnls) > 1 {
stats.SharpeRatio = calculateSharpeRatio(pnls)
}
// 计算最大回撤
if len(pnls) > 0 {
stats.MaxDrawdownPct = calculateMaxDrawdown(pnls)
}
return stats, nil
}
// calculateSharpeRatio 计算夏普比
func calculateSharpeRatio(pnls []float64) float64 {
if len(pnls) < 2 {
return 0
}
// 计算平均收益
var sum float64
for _, pnl := range pnls {
sum += pnl
}
mean := sum / float64(len(pnls))
// 计算标准差
var variance float64
for _, pnl := range pnls {
variance += (pnl - mean) * (pnl - mean)
}
stdDev := math.Sqrt(variance / float64(len(pnls)-1))
if stdDev == 0 {
return 0
}
// 夏普比 = 平均收益 / 标准差
return mean / stdDev
}
// calculateMaxDrawdown 计算最大回撤
func calculateMaxDrawdown(pnls []float64) float64 {
if len(pnls) == 0 {
return 0
}
// 计算累计权益曲线
var cumulative float64
var peak float64
var maxDD float64
for _, pnl := range pnls {
cumulative += pnl
if cumulative > peak {
peak = cumulative
}
if peak > 0 {
dd := (peak - cumulative) / peak * 100
if dd > maxDD {
maxDD = dd
}
}
}
return maxDD
}
// GetPendingOrders 获取未成交的订单(用于轮询)
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,
action, order_type, quantity, price, avg_price, executed_qty,
leverage, status, fee, fee_asset, realized_pnl, entry_price,
created_at, updated_at, filled_at
FROM trader_orders
WHERE trader_id = ? AND status = 'NEW'
ORDER BY created_at ASC
`, traderID)
if err != nil {
return nil, fmt.Errorf("查询未成交订单失败: %w", err)
}
defer rows.Close()
return s.scanOrders(rows)
}
// GetAllPendingOrders 获取所有未成交的订单(用于全局同步)
func (s *OrderStore) GetAllPendingOrders() ([]*TraderOrder, error) {
rows, err := s.db.Query(`
SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side,
action, order_type, quantity, price, avg_price, executed_qty,
leverage, status, fee, fee_asset, realized_pnl, entry_price,
created_at, updated_at, filled_at
FROM trader_orders
WHERE status = 'NEW'
ORDER BY trader_id, created_at ASC
`)
if err != nil {
return nil, fmt.Errorf("查询未成交订单失败: %w", err)
}
defer rows.Close()
return s.scanOrders(rows)
}
// scanOrders 扫描订单行到结构体
func (s *OrderStore) scanOrders(rows *sql.Rows) ([]*TraderOrder, error) {
var orders []*TraderOrder
for rows.Next() {
var order TraderOrder
var createdAt, updatedAt, filledAt sql.NullString
err := rows.Scan(
&order.ID, &order.TraderID, &order.OrderID, &order.ClientOrderID,
&order.Symbol, &order.Side, &order.PositionSide, &order.Action,
&order.OrderType, &order.Quantity, &order.Price, &order.AvgPrice,
&order.ExecutedQty, &order.Leverage, &order.Status, &order.Fee,
&order.FeeAsset, &order.RealizedPnL, &order.EntryPrice,
&createdAt, &updatedAt, &filledAt,
)
if err != nil {
continue
}
if createdAt.Valid {
order.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
}
if updatedAt.Valid {
order.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt.String)
}
if filledAt.Valid {
order.FilledAt, _ = time.Parse(time.RFC3339, filledAt.String)
}
orders = append(orders, &order)
}
return orders, nil
}

473
store/position.go Normal file
View File

@@ -0,0 +1,473 @@
package store
import (
"database/sql"
"fmt"
"math"
"time"
)
// TraderPosition 仓位记录(完整的开平仓追踪)
type TraderPosition struct {
ID int64 `json:"id"`
TraderID string `json:"trader_id"`
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"` // 杠杆倍数
Status string `json:"status"` // OPEN/CLOSED
CloseReason string `json:"close_reason"` // 平仓原因: ai_decision/manual/stop_loss/take_profit
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PositionStore 仓位存储
type PositionStore struct {
db *sql.DB
}
// NewPositionStore 创建仓位存储实例
func NewPositionStore(db *sql.DB) *PositionStore {
return &PositionStore{db: db}
}
// InitTables 初始化仓位表
func (s *PositionStore) InitTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS trader_positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trader_id TEXT NOT NULL,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
quantity REAL NOT NULL,
entry_price REAL NOT NULL,
entry_order_id TEXT DEFAULT '',
entry_time DATETIME NOT NULL,
exit_price REAL DEFAULT 0,
exit_order_id TEXT DEFAULT '',
exit_time DATETIME,
realized_pnl REAL DEFAULT 0,
fee REAL DEFAULT 0,
leverage INTEGER DEFAULT 1,
status TEXT DEFAULT 'OPEN',
close_reason TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return fmt.Errorf("创建trader_positions表失败: %w", err)
}
// 创建索引
indices := []string{
`CREATE INDEX IF NOT EXISTS idx_positions_trader ON trader_positions(trader_id)`,
`CREATE INDEX IF NOT EXISTS idx_positions_status ON trader_positions(trader_id, status)`,
`CREATE INDEX IF NOT EXISTS idx_positions_symbol ON trader_positions(trader_id, symbol, side, status)`,
`CREATE INDEX IF NOT EXISTS idx_positions_entry ON trader_positions(trader_id, entry_time DESC)`,
`CREATE INDEX IF NOT EXISTS idx_positions_exit ON trader_positions(trader_id, exit_time DESC)`,
}
for _, idx := range indices {
if _, err := s.db.Exec(idx); err != nil {
return fmt.Errorf("创建索引失败: %w", err)
}
}
return nil
}
// Create 创建仓位记录(开仓时调用)
func (s *PositionStore) Create(pos *TraderPosition) error {
now := time.Now()
pos.CreatedAt = now
pos.UpdatedAt = now
pos.Status = "OPEN"
result, err := s.db.Exec(`
INSERT INTO trader_positions (
trader_id, symbol, side, quantity, entry_price, entry_order_id,
entry_time, leverage, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
pos.TraderID, pos.Symbol, pos.Side, pos.Quantity, pos.EntryPrice,
pos.EntryOrderID, pos.EntryTime.Format(time.RFC3339), pos.Leverage,
pos.Status, now.Format(time.RFC3339), now.Format(time.RFC3339),
)
if err != nil {
return fmt.Errorf("创建仓位记录失败: %w", err)
}
id, _ := result.LastInsertId()
pos.ID = id
return nil
}
// ClosePosition 平仓(更新仓位记录)
func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID string, realizedPnL float64, fee float64, closeReason string) error {
now := time.Now()
_, err := s.db.Exec(`
UPDATE trader_positions SET
exit_price = ?, exit_order_id = ?, exit_time = ?,
realized_pnl = ?, fee = ?, status = 'CLOSED',
close_reason = ?, updated_at = ?
WHERE id = ?
`,
exitPrice, exitOrderID, now.Format(time.RFC3339),
realizedPnL, fee, closeReason, now.Format(time.RFC3339), id,
)
if err != nil {
return fmt.Errorf("更新仓位记录失败: %w", err)
}
return nil
}
// GetOpenPositions 获取所有未平仓位
func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, error) {
rows, err := s.db.Query(`
SELECT id, trader_id, symbol, side, quantity, entry_price, entry_order_id,
entry_time, exit_price, exit_order_id, exit_time, realized_pnl, fee,
leverage, status, close_reason, created_at, updated_at
FROM trader_positions
WHERE trader_id = ? AND status = 'OPEN'
ORDER BY entry_time DESC
`, traderID)
if err != nil {
return nil, fmt.Errorf("查询未平仓位失败: %w", err)
}
defer rows.Close()
return s.scanPositions(rows)
}
// GetOpenPositionBySymbol 获取指定币种方向的未平仓位
func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) {
var pos TraderPosition
var entryTime, exitTime, createdAt, updatedAt sql.NullString
err := s.db.QueryRow(`
SELECT id, trader_id, symbol, side, quantity, entry_price, entry_order_id,
entry_time, exit_price, exit_order_id, exit_time, realized_pnl, fee,
leverage, status, close_reason, created_at, updated_at
FROM trader_positions
WHERE trader_id = ? AND symbol = ? AND side = ? AND status = 'OPEN'
ORDER BY entry_time DESC LIMIT 1
`, traderID, symbol, side).Scan(
&pos.ID, &pos.TraderID, &pos.Symbol, &pos.Side, &pos.Quantity,
&pos.EntryPrice, &pos.EntryOrderID, &entryTime, &pos.ExitPrice,
&pos.ExitOrderID, &exitTime, &pos.RealizedPnL, &pos.Fee,
&pos.Leverage, &pos.Status, &pos.CloseReason, &createdAt, &updatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
s.parsePositionTimes(&pos, entryTime, exitTime, createdAt, updatedAt)
return &pos, nil
}
// GetClosedPositions 获取已平仓位(历史记录)
func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*TraderPosition, error) {
rows, err := s.db.Query(`
SELECT id, trader_id, symbol, side, quantity, entry_price, entry_order_id,
entry_time, exit_price, exit_order_id, exit_time, realized_pnl, fee,
leverage, status, close_reason, created_at, updated_at
FROM trader_positions
WHERE trader_id = ? AND status = 'CLOSED'
ORDER BY exit_time DESC
LIMIT ?
`, traderID, limit)
if err != nil {
return nil, fmt.Errorf("查询已平仓位失败: %w", err)
}
defer rows.Close()
return s.scanPositions(rows)
}
// GetAllOpenPositions 获取所有trader的未平仓位用于全局同步
func (s *PositionStore) GetAllOpenPositions() ([]*TraderPosition, error) {
rows, err := s.db.Query(`
SELECT id, trader_id, symbol, side, quantity, entry_price, entry_order_id,
entry_time, exit_price, exit_order_id, exit_time, realized_pnl, fee,
leverage, status, close_reason, created_at, updated_at
FROM trader_positions
WHERE status = 'OPEN'
ORDER BY trader_id, entry_time DESC
`)
if err != nil {
return nil, fmt.Errorf("查询所有未平仓位失败: %w", err)
}
defer rows.Close()
return s.scanPositions(rows)
}
// GetPositionStats 获取仓位统计(简单版)
func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// 总交易数
var totalTrades, winTrades int
var totalPnL, totalFee float64
err := s.db.QueryRow(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN realized_pnl > 0 THEN 1 ELSE 0 END) as wins,
COALESCE(SUM(realized_pnl), 0) as total_pnl,
COALESCE(SUM(fee), 0) as total_fee
FROM trader_positions
WHERE trader_id = ? AND status = 'CLOSED'
`, traderID).Scan(&totalTrades, &winTrades, &totalPnL, &totalFee)
if err != nil {
return nil, err
}
stats["total_trades"] = totalTrades
stats["win_trades"] = winTrades
stats["total_pnl"] = totalPnL
stats["total_fee"] = totalFee
if totalTrades > 0 {
stats["win_rate"] = float64(winTrades) / float64(totalTrades) * 100
} else {
stats["win_rate"] = 0.0
}
return stats, nil
}
// GetFullStats 获取完整的交易统计(与 TraderStats 兼容)
func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
stats := &TraderStats{}
// 查询所有已平仓位
rows, err := s.db.Query(`
SELECT realized_pnl, fee, exit_time
FROM trader_positions
WHERE trader_id = ? AND status = 'CLOSED'
ORDER BY exit_time ASC
`, traderID)
if err != nil {
return nil, fmt.Errorf("查询仓位统计失败: %w", err)
}
defer rows.Close()
var pnls []float64
var totalWin, totalLoss float64
for rows.Next() {
var pnl, fee float64
var exitTime sql.NullString
if err := rows.Scan(&pnl, &fee, &exitTime); err != nil {
continue
}
stats.TotalTrades++
stats.TotalPnL += pnl
stats.TotalFee += fee
pnls = append(pnls, pnl)
if pnl > 0 {
stats.WinTrades++
totalWin += pnl
} else if pnl < 0 {
stats.LossTrades++
totalLoss += -pnl // 转为正数
}
}
// 计算胜率
if stats.TotalTrades > 0 {
stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100
}
// 计算盈亏比
if totalLoss > 0 {
stats.ProfitFactor = totalWin / totalLoss
}
// 计算平均盈亏
if stats.WinTrades > 0 {
stats.AvgWin = totalWin / float64(stats.WinTrades)
}
if stats.LossTrades > 0 {
stats.AvgLoss = totalLoss / float64(stats.LossTrades)
}
// 计算夏普比
if len(pnls) > 1 {
stats.SharpeRatio = calculateSharpeRatioFromPnls(pnls)
}
// 计算最大回撤
if len(pnls) > 0 {
stats.MaxDrawdownPct = calculateMaxDrawdownFromPnls(pnls)
}
return stats, nil
}
// RecentTrade 最近的交易记录用于AI输入
type RecentTrade struct {
Symbol string `json:"symbol"`
Side string `json:"side"` // long/short
EntryPrice float64 `json:"entry_price"`
ExitPrice float64 `json:"exit_price"`
RealizedPnL float64 `json:"realized_pnl"`
PnLPct float64 `json:"pnl_pct"`
ExitTime string `json:"exit_time"`
}
// GetRecentTrades 获取最近的已平仓交易
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
FROM trader_positions
WHERE trader_id = ? AND status = 'CLOSED'
ORDER BY exit_time DESC
LIMIT ?
`, traderID, limit)
if err != nil {
return nil, fmt.Errorf("查询最近交易失败: %w", err)
}
defer rows.Close()
var trades []RecentTrade
for rows.Next() {
var t RecentTrade
var leverage int
var exitTime sql.NullString
err := rows.Scan(&t.Symbol, &t.Side, &t.EntryPrice, &t.ExitPrice, &t.RealizedPnL, &leverage, &exitTime)
if err != nil {
continue
}
// 转换 side 格式
if t.Side == "LONG" {
t.Side = "long"
} else if t.Side == "SHORT" {
t.Side = "short"
}
// 计算盈亏百分比
if t.EntryPrice > 0 {
if t.Side == "long" {
t.PnLPct = (t.ExitPrice - t.EntryPrice) / t.EntryPrice * 100 * float64(leverage)
} else {
t.PnLPct = (t.EntryPrice - t.ExitPrice) / t.EntryPrice * 100 * float64(leverage)
}
}
// 格式化时间
if exitTime.Valid {
if parsed, err := time.Parse(time.RFC3339, exitTime.String); err == nil {
t.ExitTime = parsed.Format("01-02 15:04")
}
}
trades = append(trades, t)
}
return trades, nil
}
// calculateSharpeRatioFromPnls 计算夏普比
func calculateSharpeRatioFromPnls(pnls []float64) float64 {
if len(pnls) < 2 {
return 0
}
var sum float64
for _, pnl := range pnls {
sum += pnl
}
mean := sum / float64(len(pnls))
var variance float64
for _, pnl := range pnls {
variance += (pnl - mean) * (pnl - mean)
}
stdDev := math.Sqrt(variance / float64(len(pnls)-1))
if stdDev == 0 {
return 0
}
return mean / stdDev
}
// calculateMaxDrawdownFromPnls 计算最大回撤
func calculateMaxDrawdownFromPnls(pnls []float64) float64 {
if len(pnls) == 0 {
return 0
}
var cumulative, peak, maxDD float64
for _, pnl := range pnls {
cumulative += pnl
if cumulative > peak {
peak = cumulative
}
if peak > 0 {
dd := (peak - cumulative) / peak * 100
if dd > maxDD {
maxDD = dd
}
}
}
return maxDD
}
// scanPositions 扫描仓位行到结构体
func (s *PositionStore) scanPositions(rows *sql.Rows) ([]*TraderPosition, error) {
var positions []*TraderPosition
for rows.Next() {
var pos TraderPosition
var entryTime, exitTime, createdAt, updatedAt sql.NullString
err := rows.Scan(
&pos.ID, &pos.TraderID, &pos.Symbol, &pos.Side, &pos.Quantity,
&pos.EntryPrice, &pos.EntryOrderID, &entryTime, &pos.ExitPrice,
&pos.ExitOrderID, &exitTime, &pos.RealizedPnL, &pos.Fee,
&pos.Leverage, &pos.Status, &pos.CloseReason, &createdAt, &updatedAt,
)
if err != nil {
continue
}
s.parsePositionTimes(&pos, entryTime, exitTime, createdAt, updatedAt)
positions = append(positions, &pos)
}
return positions, nil
}
// parsePositionTimes 解析时间字段
func (s *PositionStore) parsePositionTimes(pos *TraderPosition, entryTime, exitTime, createdAt, updatedAt sql.NullString) {
if entryTime.Valid {
pos.EntryTime, _ = time.Parse(time.RFC3339, entryTime.String)
}
if exitTime.Valid {
t, _ := time.Parse(time.RFC3339, exitTime.String)
pos.ExitTime = &t
}
if createdAt.Valid {
pos.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
}
if updatedAt.Valid {
pos.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt.String)
}
}

86
store/signal_source.go Normal file
View File

@@ -0,0 +1,86 @@
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
}

319
store/store.go Normal file
View File

@@ -0,0 +1,319 @@
// Package store 提供统一的数据库存储层
// 所有数据库操作都应该通过这个包进行
package store
import (
"database/sql"
"fmt"
"nofx/logger"
"sync"
_ "modernc.org/sqlite"
)
// Store 统一的数据存储接口
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
// 加密函数
encryptFunc func(string) string
decryptFunc func(string) string
mu sync.RWMutex
}
// New 创建新的 Store 实例
func New(dbPath string) (*Store, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %w", err)
}
// SQLite 配置
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
// 启用外键约束
if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil {
db.Close()
return nil, fmt.Errorf("启用外键失败: %w", err)
}
// 使用 DELETE 模式(传统模式)以确保 Docker bind mount 兼容性
// 注意WAL 模式在 macOS Docker 下会导致数据同步问题
if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil {
db.Close()
return nil, fmt.Errorf("设置journal_mode失败: %w", err)
}
// 设置 synchronous=FULL
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
db.Close()
return nil, fmt.Errorf("设置synchronous失败: %w", err)
}
// 设置 busy_timeout
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
db.Close()
return nil, fmt.Errorf("设置busy_timeout失败: %w", err)
}
s := &Store{db: db}
// 初始化所有表结构
if err := s.initTables(); err != nil {
db.Close()
return nil, fmt.Errorf("初始化表结构失败: %w", err)
}
// 初始化默认数据
if err := s.initDefaultData(); err != nil {
db.Close()
return nil, fmt.Errorf("初始化默认数据失败: %w", err)
}
logger.Info("✅ 数据库已启用 DELETE 模式和 FULL 同步")
return s, nil
}
// NewFromDB 从现有数据库连接创建 Store
func NewFromDB(db *sql.DB) *Store {
return &Store{db: db}
}
// SetCryptoFuncs 设置加密解密函数
func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) {
s.mu.Lock()
defer s.mu.Unlock()
s.encryptFunc = encrypt
s.decryptFunc = decrypt
// 更新已初始化的子存储
if s.aiModel != nil {
s.aiModel.encryptFunc = encrypt
s.aiModel.decryptFunc = decrypt
}
if s.exchange != nil {
s.exchange.encryptFunc = encrypt
s.exchange.decryptFunc = decrypt
}
if s.trader != nil {
s.trader.decryptFunc = decrypt
}
}
// initTables 初始化所有数据库表
func (s *Store) initTables() error {
// 按依赖顺序初始化
if err := s.User().initTables(); err != nil {
return fmt.Errorf("初始化用户表失败: %w", err)
}
if err := s.AIModel().initTables(); err != nil {
return fmt.Errorf("初始化AI模型表失败: %w", err)
}
if err := s.Exchange().initTables(); err != nil {
return fmt.Errorf("初始化交易所表失败: %w", err)
}
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)
}
if err := s.Backtest().initTables(); err != nil {
return fmt.Errorf("初始化回测表失败: %w", err)
}
if err := s.Order().InitTables(); err != nil {
return fmt.Errorf("初始化订单表失败: %w", err)
}
if err := s.Position().InitTables(); err != nil {
return fmt.Errorf("初始化仓位表失败: %w", err)
}
return nil
}
// initDefaultData 初始化默认数据
func (s *Store) initDefaultData() error {
if err := s.AIModel().initDefaultData(); err != nil {
return err
}
if err := s.Exchange().initDefaultData(); err != nil {
return err
}
if err := s.SystemConfig().initDefaultData(); err != nil {
return err
}
return nil
}
// User 获取用户存储
func (s *Store) User() *UserStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.user == nil {
s.user = &UserStore{db: s.db}
}
return s.user
}
// AIModel 获取AI模型存储
func (s *Store) AIModel() *AIModelStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.aiModel == nil {
s.aiModel = &AIModelStore{
db: s.db,
encryptFunc: s.encryptFunc,
decryptFunc: s.decryptFunc,
}
}
return s.aiModel
}
// Exchange 获取交易所存储
func (s *Store) Exchange() *ExchangeStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.exchange == nil {
s.exchange = &ExchangeStore{
db: s.db,
encryptFunc: s.encryptFunc,
decryptFunc: s.decryptFunc,
}
}
return s.exchange
}
// Trader 获取交易员存储
func (s *Store) Trader() *TraderStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.trader == nil {
s.trader = &TraderStore{
db: s.db,
decryptFunc: s.decryptFunc,
}
}
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()
defer s.mu.Unlock()
if s.decision == nil {
s.decision = &DecisionStore{db: s.db}
}
return s.decision
}
// Backtest 获取回测数据存储
func (s *Store) Backtest() *BacktestStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.backtest == nil {
s.backtest = &BacktestStore{db: s.db}
}
return s.backtest
}
// Order 获取订单存储
func (s *Store) Order() *OrderStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.order == nil {
s.order = NewOrderStore(s.db)
}
return s.order
}
// Position 获取仓位存储
func (s *Store) Position() *PositionStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.position == nil {
s.position = NewPositionStore(s.db)
}
return s.position
}
// Close 关闭数据库连接
func (s *Store) Close() error {
return s.db.Close()
}
// DB 获取底层数据库连接(仅用于兼容旧代码,逐步废弃)
// Deprecated: 使用 Store 的方法代替
func (s *Store) DB() *sql.DB {
return s.db
}
// 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)
}
if err := fn(tx); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}

70
store/system_config.go Normal file
View File

@@ -0,0 +1,70 @@
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",
"use_default_coins": "true",
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`,
"max_daily_loss": "10.0",
"max_drawdown": "20.0",
"stop_trading_minutes": "60",
"btc_eth_leverage": "5",
"altcoin_leverage": "5",
"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
}

344
store/trader.go Normal file
View File

@@ -0,0 +1,344 @@
package store
import (
"database/sql"
"encoding/json"
"nofx/logger"
"nofx/market"
"slices"
"strings"
"time"
)
// TraderStore 交易员存储
type TraderStore struct {
db *sql.DB
decryptFunc func(string) string
}
// Trader 交易员配置
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"`
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsRunning bool `json:"is_running"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
UseCoinPool bool `json:"use_coin_pool"`
UseOITop bool `json:"use_oi_top"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
SystemPromptTemplate string `json:"system_prompt_template"`
IsCrossMargin bool `json:"is_cross_margin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TraderFullConfig 交易员完整配置包含AI模型和交易所
type TraderFullConfig struct {
Trader *Trader
AIModel *AIModel
Exchange *Exchange
}
func (s *TraderStore) initTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS traders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
ai_model_id TEXT NOT NULL,
exchange_id TEXT NOT NULL,
initial_balance REAL NOT NULL,
scan_interval_minutes INTEGER DEFAULT 3,
is_running BOOLEAN DEFAULT 0,
btc_eth_leverage INTEGER DEFAULT 5,
altcoin_leverage INTEGER DEFAULT 5,
trading_symbols TEXT DEFAULT '',
use_coin_pool BOOLEAN DEFAULT 0,
use_oi_top BOOLEAN DEFAULT 0,
custom_prompt TEXT DEFAULT '',
override_base_prompt BOOLEAN DEFAULT 0,
system_prompt_template TEXT DEFAULT 'default',
is_cross_margin BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`)
if err != nil {
return err
}
// 触发器
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS update_traders_updated_at
AFTER UPDATE ON traders
BEGIN
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
`)
if err != nil {
return err
}
// 向后兼容
alterQueries := []string{
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`,
`ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`,
`ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`,
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`,
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`,
}
for _, q := range alterQueries {
s.db.Exec(q)
}
return nil
}
func (s *TraderStore) decrypt(encrypted string) string {
if s.decryptFunc != nil {
return s.decryptFunc(encrypted)
}
return encrypted
}
// Create 创建交易员
func (s *TraderStore) Create(trader *Trader) error {
_, err := s.db.Exec(`
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes,
is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool,
use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance,
trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage,
trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt,
trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
return err
}
// List 获取用户的交易员列表
func (s *TraderStore) List(userID string) ([]*Trader, error) {
rows, err := s.db.Query(`
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5), COALESCE(trading_symbols, ''),
COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0), COALESCE(custom_prompt, ''),
COALESCE(override_base_prompt, 0), COALESCE(system_prompt_template, 'default'),
COALESCE(is_cross_margin, 1), created_at, updated_at
FROM traders WHERE user_id = ? ORDER BY created_at DESC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var traders []*Trader
for rows.Next() {
var t Trader
var createdAt, updatedAt string
err := rows.Scan(
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning,
&t.BTCETHLeverage, &t.AltcoinLeverage, &t.TradingSymbols,
&t.UseCoinPool, &t.UseOITop, &t.CustomPrompt, &t.OverrideBasePrompt,
&t.SystemPromptTemplate, &t.IsCrossMargin, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
t.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
t.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
traders = append(traders, &t)
}
return traders, nil
}
// UpdateStatus 更新交易员运行状态
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 更新交易员配置
func (s *TraderStore) Update(trader *Trader) error {
_, err := s.db.Exec(`
UPDATE traders SET
name = ?, ai_model_id = ?, exchange_id = ?, scan_interval_minutes = ?,
btc_eth_leverage = ?, altcoin_leverage = ?, trading_symbols = ?,
custom_prompt = ?, override_base_prompt = ?, system_prompt_template = ?,
is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.ScanIntervalMinutes,
trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols,
trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate,
trader.IsCrossMargin, trader.ID, trader.UserID)
return err
}
// UpdateInitialBalance 更新初始余额
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 更新自定义提示词
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 删除交易员
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 获取交易员完整配置
func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, error) {
var trader Trader
var aiModel AIModel
var exchange Exchange
var traderCreatedAt, traderUpdatedAt string
var aiModelCreatedAt, aiModelUpdatedAt string
var exchangeCreatedAt, exchangeUpdatedAt string
err := s.db.QueryRow(`
SELECT
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
COALESCE(t.btc_eth_leverage, 5), COALESCE(t.altcoin_leverage, 5), COALESCE(t.trading_symbols, ''),
COALESCE(t.use_coin_pool, 0), COALESCE(t.use_oi_top, 0), COALESCE(t.custom_prompt, ''),
COALESCE(t.override_base_prompt, 0), COALESCE(t.system_prompt_template, 'default'),
COALESCE(t.is_cross_margin, 1), t.created_at, t.updated_at,
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key,
COALESCE(a.custom_api_url, ''), COALESCE(a.custom_model_name, ''), a.created_at, a.updated_at,
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
COALESCE(e.hyperliquid_wallet_addr, ''), COALESCE(e.aster_user, ''), COALESCE(e.aster_signer, ''),
COALESCE(e.aster_private_key, ''), COALESCE(e.lighter_wallet_addr, ''), COALESCE(e.lighter_private_key, ''),
COALESCE(e.lighter_api_key_private_key, ''), e.created_at, e.updated_at
FROM traders t
JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id
JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id
WHERE t.id = ? AND t.user_id = ?
`, traderID, userID).Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
&trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt,
&trader.SystemPromptTemplate, &trader.IsCrossMargin, &traderCreatedAt, &traderUpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CustomAPIURL, &aiModel.CustomModelName, &aiModelCreatedAt, &aiModelUpdatedAt,
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.HyperliquidWalletAddr,
&exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey,
&exchangeCreatedAt, &exchangeUpdatedAt,
)
if err != nil {
return nil, err
}
trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", traderCreatedAt)
trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", traderUpdatedAt)
aiModel.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelCreatedAt)
aiModel.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelUpdatedAt)
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt)
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt)
// 解密
aiModel.APIKey = s.decrypt(aiModel.APIKey)
exchange.APIKey = s.decrypt(exchange.APIKey)
exchange.SecretKey = s.decrypt(exchange.SecretKey)
exchange.AsterPrivateKey = s.decrypt(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = s.decrypt(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = s.decrypt(exchange.LighterAPIKeyPrivateKey)
return &TraderFullConfig{
Trader: &trader,
AIModel: &aiModel,
Exchange: &exchange,
}, 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(`
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5), COALESCE(trading_symbols, ''),
COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0), COALESCE(custom_prompt, ''),
COALESCE(override_base_prompt, 0), COALESCE(system_prompt_template, 'default'),
COALESCE(is_cross_margin, 1), created_at, updated_at
FROM traders ORDER BY created_at DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var traders []*Trader
for rows.Next() {
var t Trader
var createdAt, updatedAt string
err := rows.Scan(
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning,
&t.BTCETHLeverage, &t.AltcoinLeverage, &t.TradingSymbols,
&t.UseCoinPool, &t.UseOITop, &t.CustomPrompt, &t.OverrideBasePrompt,
&t.SystemPromptTemplate, &t.IsCrossMargin, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
t.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
t.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
traders = append(traders, &t)
}
return traders, nil
}

164
store/user.go Normal file
View File

@@ -0,0 +1,164 @@
package store
import (
"crypto/rand"
"database/sql"
"encoding/base32"
"time"
)
// UserStore 用户存储
type UserStore struct {
db *sql.DB
}
// User 用户
type User struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
OTPSecret string `json:"-"`
OTPVerified bool `json:"otp_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GenerateOTPSecret 生成OTP密钥
func GenerateOTPSecret() (string, error) {
secret := make([]byte, 20)
_, err := rand.Read(secret)
if err != nil {
return "", err
}
return base32.StdEncoding.EncodeToString(secret), nil
}
func (s *UserStore) initTables() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
otp_secret TEXT,
otp_verified BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}
// 触发器
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS update_users_updated_at
AFTER UPDATE ON users
BEGIN
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
`)
if err != nil {
return err
}
return nil
}
// Create 创建用户
func (s *UserStore) Create(user *User) error {
_, err := s.db.Exec(`
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
VALUES (?, ?, ?, ?, ?)
`, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified)
return err
}
// GetByEmail 通过邮箱获取用户
func (s *UserStore) GetByEmail(email string) (*User, error) {
var user User
var createdAt, updatedAt string
err := s.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE email = ?
`, email).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &user, nil
}
// GetByID 通过ID获取用户
func (s *UserStore) GetByID(userID string) (*User, error) {
var user User
var createdAt, updatedAt string
err := s.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE id = ?
`, userID).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &user, nil
}
// GetAllIDs 获取所有用户ID
func (s *UserStore) GetAllIDs() ([]string, error) {
rows, err := s.db.Query(`SELECT id FROM users ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var userIDs []string
for rows.Next() {
var userID string
if err := rows.Scan(&userID); err != nil {
return nil, err
}
userIDs = append(userIDs, userID)
}
return userIDs, nil
}
// UpdateOTPVerified 更新OTP验证状态
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 更新密码
func (s *UserStore) UpdatePassword(userID, passwordHash string) error {
_, err := s.db.Exec(`
UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, passwordHash, userID)
return err
}
// EnsureAdmin 确保admin用户存在
func (s *UserStore) EnsureAdmin() error {
var count int
err := s.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count)
if err != nil {
return err
}
if count > 0 {
return nil
}
return s.Create(&User{
ID: "admin",
Email: "admin@localhost",
PasswordHash: "",
OTPSecret: "",
OTPVerified: true,
})
}