mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
refactor: restructure project directories for better modularity
- Delete llm/ dead code (3 files, zero references) - Split mcp/ into sub-packages: mcp/provider/ (8 providers) and mcp/payment/ (4 payment clients) with registry pattern - Export Client internal fields and ClientHooks interface for sub-package access - Split api/server.go (3892 lines) into 8 domain-specific handler files - Split trader/auto_trader.go (2296 lines) into 5 focused files - Reorganize web/src/components/ flat files into auth/, charts/, trader/, common/, modals/, backtest/ subdirectories - Update all consumer imports to use registry-based provider creation
This commit is contained in:
263
trader/auto_trader_risk.go
Normal file
263
trader/auto_trader_risk.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// startDrawdownMonitor starts drawdown monitoring
|
||||
func (at *AutoTrader) startDrawdownMonitor() {
|
||||
at.monitorWg.Add(1)
|
||||
go func() {
|
||||
defer at.monitorWg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Minute) // Check every minute
|
||||
defer ticker.Stop()
|
||||
|
||||
logger.Info("📊 Started position drawdown monitoring (check every minute)")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
at.checkPositionDrawdown()
|
||||
case <-at.stopMonitorCh:
|
||||
logger.Info("⏹ Stopped position drawdown monitoring")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// checkPositionDrawdown checks position drawdown situation
|
||||
func (at *AutoTrader) checkPositionDrawdown() {
|
||||
// Get current positions
|
||||
positions, err := at.trader.GetPositions()
|
||||
if err != nil {
|
||||
logger.Infof("❌ Drawdown monitoring: failed to get positions: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
symbol := pos["symbol"].(string)
|
||||
side := pos["side"].(string)
|
||||
entryPrice := pos["entryPrice"].(float64)
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
if quantity < 0 {
|
||||
quantity = -quantity // Short position quantity is negative, convert to positive
|
||||
}
|
||||
|
||||
// Calculate current P&L percentage
|
||||
leverage := 10 // Default value
|
||||
if lev, ok := pos["leverage"].(float64); ok {
|
||||
leverage = int(lev)
|
||||
}
|
||||
|
||||
var currentPnLPct float64
|
||||
if side == "long" {
|
||||
currentPnLPct = ((markPrice - entryPrice) / entryPrice) * float64(leverage) * 100
|
||||
} else {
|
||||
currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100
|
||||
}
|
||||
|
||||
// Construct unique position identifier (distinguish long/short)
|
||||
posKey := symbol + "_" + side
|
||||
|
||||
// Get historical peak profit for this position
|
||||
at.peakPnLCacheMutex.RLock()
|
||||
peakPnLPct, exists := at.peakPnLCache[posKey]
|
||||
at.peakPnLCacheMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// If no historical peak record, use current P&L as initial value
|
||||
peakPnLPct = currentPnLPct
|
||||
at.UpdatePeakPnL(symbol, side, currentPnLPct)
|
||||
} else {
|
||||
// Update peak cache
|
||||
at.UpdatePeakPnL(symbol, side, currentPnLPct)
|
||||
}
|
||||
|
||||
// Calculate drawdown (magnitude of decline from peak)
|
||||
var drawdownPct float64
|
||||
if peakPnLPct > 0 && currentPnLPct < peakPnLPct {
|
||||
drawdownPct = ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100
|
||||
}
|
||||
|
||||
// Check close position condition: profit > 5% and drawdown >= 40%
|
||||
if currentPnLPct > 5.0 && drawdownPct >= 40.0 {
|
||||
logger.Infof("🚨 Drawdown close position condition triggered: %s %s | Current profit: %.2f%% | Peak profit: %.2f%% | Drawdown: %.2f%%",
|
||||
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
|
||||
|
||||
// Execute close position
|
||||
if err := at.emergencyClosePosition(symbol, side); err != nil {
|
||||
logger.Infof("❌ Drawdown close position failed (%s %s): %v", symbol, side, err)
|
||||
} else {
|
||||
logger.Infof("✅ Drawdown close position succeeded: %s %s", symbol, side)
|
||||
// Clear cache for this position after closing
|
||||
at.ClearPeakPnLCache(symbol, side)
|
||||
}
|
||||
} else if currentPnLPct > 5.0 {
|
||||
// Record situations close to close position condition (for debugging)
|
||||
logger.Infof("📊 Drawdown monitoring: %s %s | Profit: %.2f%% | Peak: %.2f%% | Drawdown: %.2f%%",
|
||||
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emergencyClosePosition emergency close position function
|
||||
func (at *AutoTrader) emergencyClosePosition(symbol, side string) error {
|
||||
switch side {
|
||||
case "long":
|
||||
order, err := at.trader.CloseLong(symbol, 0) // 0 = close all
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("✅ Emergency close long position succeeded, order ID: %v", order["orderId"])
|
||||
case "short":
|
||||
order, err := at.trader.CloseShort(symbol, 0) // 0 = close all
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("✅ Emergency close short position succeeded, order ID: %v", order["orderId"])
|
||||
default:
|
||||
return fmt.Errorf("unknown position direction: %s", side)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPeakPnLCache gets peak profit cache
|
||||
func (at *AutoTrader) GetPeakPnLCache() map[string]float64 {
|
||||
at.peakPnLCacheMutex.RLock()
|
||||
defer at.peakPnLCacheMutex.RUnlock()
|
||||
|
||||
// Return a copy of the cache
|
||||
cache := make(map[string]float64)
|
||||
for k, v := range at.peakPnLCache {
|
||||
cache[k] = v
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// UpdatePeakPnL updates peak profit cache
|
||||
func (at *AutoTrader) UpdatePeakPnL(symbol, side string, currentPnLPct float64) {
|
||||
at.peakPnLCacheMutex.Lock()
|
||||
defer at.peakPnLCacheMutex.Unlock()
|
||||
|
||||
posKey := symbol + "_" + side
|
||||
if peak, exists := at.peakPnLCache[posKey]; exists {
|
||||
// Update peak (if long, take larger value; if short, currentPnLPct is negative, also compare)
|
||||
if currentPnLPct > peak {
|
||||
at.peakPnLCache[posKey] = currentPnLPct
|
||||
}
|
||||
} else {
|
||||
// First time recording
|
||||
at.peakPnLCache[posKey] = currentPnLPct
|
||||
}
|
||||
}
|
||||
|
||||
// ClearPeakPnLCache clears peak cache for specified position
|
||||
func (at *AutoTrader) ClearPeakPnLCache(symbol, side string) {
|
||||
at.peakPnLCacheMutex.Lock()
|
||||
defer at.peakPnLCacheMutex.Unlock()
|
||||
|
||||
posKey := symbol + "_" + side
|
||||
delete(at.peakPnLCache, posKey)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Risk Control Helpers
|
||||
// ============================================================================
|
||||
|
||||
// isBTCETH checks if a symbol is BTC or ETH
|
||||
func isBTCETH(symbol string) bool {
|
||||
symbol = strings.ToUpper(symbol)
|
||||
return strings.HasPrefix(symbol, "BTC") || strings.HasPrefix(symbol, "ETH")
|
||||
}
|
||||
|
||||
// enforcePositionValueRatio checks and enforces position value ratio limits (CODE ENFORCED)
|
||||
// Returns the adjusted position size (capped if necessary) and whether the position was capped
|
||||
// positionSizeUSD: the original position size in USD
|
||||
// equity: the account equity
|
||||
// symbol: the trading symbol
|
||||
func (at *AutoTrader) enforcePositionValueRatio(positionSizeUSD float64, equity float64, symbol string) (float64, bool) {
|
||||
if at.config.StrategyConfig == nil {
|
||||
return positionSizeUSD, false
|
||||
}
|
||||
|
||||
riskControl := at.config.StrategyConfig.RiskControl
|
||||
|
||||
// Get the appropriate position value ratio limit
|
||||
var maxPositionValueRatio float64
|
||||
if isBTCETH(symbol) {
|
||||
maxPositionValueRatio = riskControl.BTCETHMaxPositionValueRatio
|
||||
if maxPositionValueRatio <= 0 {
|
||||
maxPositionValueRatio = 5.0 // Default: 5x for BTC/ETH
|
||||
}
|
||||
} else {
|
||||
maxPositionValueRatio = riskControl.AltcoinMaxPositionValueRatio
|
||||
if maxPositionValueRatio <= 0 {
|
||||
maxPositionValueRatio = 1.0 // Default: 1x for altcoins
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate max allowed position value = equity × ratio
|
||||
maxPositionValue := equity * maxPositionValueRatio
|
||||
|
||||
// Check if position size exceeds limit
|
||||
if positionSizeUSD > maxPositionValue {
|
||||
logger.Infof(" ⚠️ [RISK CONTROL] Position %.2f USDT exceeds limit (equity %.2f × %.1fx = %.2f USDT max for %s), capping",
|
||||
positionSizeUSD, equity, maxPositionValueRatio, maxPositionValue, symbol)
|
||||
return maxPositionValue, true
|
||||
}
|
||||
|
||||
return positionSizeUSD, false
|
||||
}
|
||||
|
||||
// enforceMinPositionSize checks minimum position size (CODE ENFORCED)
|
||||
func (at *AutoTrader) enforceMinPositionSize(positionSizeUSD float64) error {
|
||||
if at.config.StrategyConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
minSize := at.config.StrategyConfig.RiskControl.MinPositionSize
|
||||
if minSize <= 0 {
|
||||
minSize = 12 // Default: 12 USDT
|
||||
}
|
||||
|
||||
if positionSizeUSD < minSize {
|
||||
return fmt.Errorf("❌ [RISK CONTROL] Position %.2f USDT below minimum (%.2f USDT)", positionSizeUSD, minSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// enforceMaxPositions checks maximum positions count (CODE ENFORCED)
|
||||
func (at *AutoTrader) enforceMaxPositions(currentPositionCount int) error {
|
||||
if at.config.StrategyConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxPositions := at.config.StrategyConfig.RiskControl.MaxPositions
|
||||
if maxPositions <= 0 {
|
||||
maxPositions = 3 // Default: 3 positions
|
||||
}
|
||||
|
||||
if currentPositionCount >= maxPositions {
|
||||
return fmt.Errorf("❌ [RISK CONTROL] Already at max positions (%d/%d)", currentPositionCount, maxPositions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSideFromAction converts order action to side (BUY/SELL)
|
||||
func getSideFromAction(action string) string {
|
||||
switch action {
|
||||
case "open_long", "close_short":
|
||||
return "BUY"
|
||||
case "open_short", "close_long":
|
||||
return "SELL"
|
||||
default:
|
||||
return "BUY"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user