mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 13:54:41 +08:00
* 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 ---------
512 lines
15 KiB
Go
512 lines
15 KiB
Go
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
|
||
}
|