mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
fix: prevent panics from unsafe type assertions in trading code + add request body limit
Security & Reliability: - Add requestBodyLimitMiddleware (1MB) to prevent OOM from oversized API payloads - Fix defer resp.Body.Close() inside loop in getPublicIPFromAPI (connection leak) - Add posFloat64/posString safe helpers for position map access Panic Prevention (critical for trading): - Convert 30+ unsafe type assertions (pos["key"].(type)) to safe comma-ok pattern across all exchange traders: OKX, Hyperliquid, Aster, Bybit, KuCoin, Gate, Bitget, Binance - Fix auto_trader_risk.go: drawdown monitor could panic and silently stop monitoring, leaving positions unprotected - Fix auto_trader_decision.go & auto_trader_loop.go: core trading loop position parsing now crash-proof - All trader/ code now has zero unsafe type assertions Frontend: - Fix config.ts: rejected promise cached forever on network error (never retries)
This commit is contained in:
@@ -36,6 +36,9 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
// Limit request body size to prevent OOM from oversized payloads (1MB default)
|
||||
router.Use(requestBodyLimitMiddleware(1 << 20))
|
||||
|
||||
// Enable CORS
|
||||
router.Use(corsMiddleware())
|
||||
|
||||
@@ -57,6 +60,17 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ
|
||||
return s
|
||||
}
|
||||
|
||||
// requestBodyLimitMiddleware limits the size of incoming request bodies.
|
||||
// Returns 413 Payload Too Large if the body exceeds maxBytes.
|
||||
func requestBodyLimitMiddleware(maxBytes int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Body != nil {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// corsMiddleware CORS middleware with configurable allowed origins.
|
||||
// Set CORS_ALLOWED_ORIGINS env var to a comma-separated list of origins
|
||||
// (e.g. "http://localhost:5173,https://nofx.example.com").
|
||||
@@ -445,25 +459,33 @@ func getPublicIPFromAPI() string {
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
resp, err := client.Get(service)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ip := func() string {
|
||||
resp, err := client.Get(service)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ""
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body := make([]byte, 128)
|
||||
n, err := resp.Body.Read(body)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
continue
|
||||
return ""
|
||||
}
|
||||
|
||||
ip := strings.TrimSpace(string(body[:n]))
|
||||
parsedIP := net.ParseIP(ip)
|
||||
candidate := strings.TrimSpace(string(body[:n]))
|
||||
parsedIP := net.ParseIP(candidate)
|
||||
// Verify if it's a valid IPv4 address (not containing ":")
|
||||
if parsedIP != nil && parsedIP.To4() != nil {
|
||||
return ip
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +70,12 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
|
||||
totalMarginUsed := 0.0
|
||||
realUnrealizedPnl := 0.0
|
||||
for _, pos := range positions {
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
markPrice, _ := pos["markPrice"].(float64)
|
||||
quantity, _ := pos["positionAmt"].(float64)
|
||||
if quantity < 0 {
|
||||
quantity = -quantity
|
||||
}
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
unrealizedPnl, _ := pos["unRealizedProfit"].(float64)
|
||||
realUnrealizedPnl += unrealizedPnl
|
||||
|
||||
leverage := 10
|
||||
|
||||
@@ -166,7 +166,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
// Aster's GetPositions has already converted short position quantity to positive, use directly
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +133,12 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) {
|
||||
totalMarginUsed := 0.0
|
||||
totalUnrealizedPnLCalculated := 0.0
|
||||
for _, pos := range positions {
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
markPrice := posFloat64(pos, "markPrice")
|
||||
quantity := posFloat64(pos, "positionAmt")
|
||||
if quantity < 0 {
|
||||
quantity = -quantity
|
||||
}
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
unrealizedPnl := posFloat64(pos, "unRealizedProfit")
|
||||
totalUnrealizedPnLCalculated += unrealizedPnl
|
||||
|
||||
leverage := 10
|
||||
@@ -199,16 +199,16 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, pos := range positions {
|
||||
symbol := pos["symbol"].(string)
|
||||
side := pos["side"].(string)
|
||||
entryPrice := pos["entryPrice"].(float64)
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
symbol := posString(pos, "symbol")
|
||||
side := posString(pos, "side")
|
||||
entryPrice := posFloat64(pos, "entryPrice")
|
||||
markPrice := posFloat64(pos, "markPrice")
|
||||
quantity := posFloat64(pos, "positionAmt")
|
||||
if quantity < 0 {
|
||||
quantity = -quantity
|
||||
}
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
liquidationPrice := pos["liquidationPrice"].(float64)
|
||||
unrealizedPnl := posFloat64(pos, "unRealizedProfit")
|
||||
liquidationPrice := posFloat64(pos, "liquidationPrice")
|
||||
|
||||
leverage := 10
|
||||
if lev, ok := pos["leverage"].(float64); ok {
|
||||
|
||||
@@ -331,11 +331,11 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
||||
currentPositionKeys := make(map[string]bool)
|
||||
|
||||
for _, pos := range positions {
|
||||
symbol := pos["symbol"].(string)
|
||||
side := pos["side"].(string)
|
||||
entryPrice := pos["entryPrice"].(float64)
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
symbol := posString(pos, "symbol")
|
||||
side := posString(pos, "side")
|
||||
entryPrice := posFloat64(pos, "entryPrice")
|
||||
markPrice := posFloat64(pos, "markPrice")
|
||||
quantity := posFloat64(pos, "positionAmt")
|
||||
if quantity < 0 {
|
||||
quantity = -quantity // Short position quantity is negative, convert to positive
|
||||
}
|
||||
@@ -345,8 +345,8 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
liquidationPrice := pos["liquidationPrice"].(float64)
|
||||
unrealizedPnl := posFloat64(pos, "unRealizedProfit")
|
||||
liquidationPrice := posFloat64(pos, "liquidationPrice")
|
||||
|
||||
// Calculate margin used (estimated)
|
||||
leverage := 10 // Default value, should actually be fetched from position info
|
||||
|
||||
@@ -41,11 +41,14 @@ func (at *AutoTrader) checkPositionDrawdown() {
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
symbol := pos["symbol"].(string)
|
||||
side := pos["side"].(string)
|
||||
entryPrice := pos["entryPrice"].(float64)
|
||||
markPrice := pos["markPrice"].(float64)
|
||||
quantity := pos["positionAmt"].(float64)
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
side, _ := pos["side"].(string)
|
||||
entryPrice, _ := pos["entryPrice"].(float64)
|
||||
markPrice, _ := pos["markPrice"].(float64)
|
||||
quantity, _ := pos["positionAmt"].(float64)
|
||||
if symbol == "" || side == "" || entryPrice == 0 {
|
||||
continue // skip malformed position data
|
||||
}
|
||||
if quantity < 0 {
|
||||
quantity = -quantity // Short position quantity is negative, convert to positive
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]i
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
|
||||
// Get precision from LOT_SIZE filter
|
||||
for _, filter := range s.Filters {
|
||||
if filter["filterType"] == "LOT_SIZE" {
|
||||
stepSize := filter["stepSize"].(string)
|
||||
stepSize, _ := filter["stepSize"].(string)
|
||||
precision := calculatePrecision(stepSize)
|
||||
logger.Infof(" %s quantity precision: %d (stepSize: %s)", symbol, precision, stepSize)
|
||||
return precision, nil
|
||||
@@ -264,7 +264,7 @@ func (t *FuturesTrader) GetSymbolPricePrecision(symbol string) (int, error) {
|
||||
// Get precision from PRICE_FILTER filter
|
||||
for _, filter := range s.Filters {
|
||||
if filter["filterType"] == "PRICE_FILTER" {
|
||||
tickSize := filter["tickSize"].(string)
|
||||
tickSize, _ := filter["tickSize"].(string)
|
||||
precision := calculatePrecision(tickSize)
|
||||
return precision, nil
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func (t *BitgetTrader) CloseLong(symbol string, quantity float64) (map[string]in
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (t *BitgetTrader) CloseShort(symbol string, quantity float64) (map[string]i
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (t *BybitTrader) CloseLong(symbol string, quantity float64) (map[string]int
|
||||
for _, pos := range positions {
|
||||
side, _ := pos["side"].(string)
|
||||
if pos["symbol"] == symbol && strings.ToLower(side) == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,10 @@ func (t *GateTrader) CloseLong(symbol string, quantity float64) (map[string]inte
|
||||
return nil, err
|
||||
}
|
||||
for _, pos := range positions {
|
||||
posSymbol := t.convertSymbol(pos["symbol"].(string))
|
||||
rawSymbol, _ := pos["symbol"].(string)
|
||||
posSymbol := t.convertSymbol(rawSymbol)
|
||||
if posSymbol == symbol && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -233,9 +234,10 @@ func (t *GateTrader) CloseShort(symbol string, quantity float64) (map[string]int
|
||||
return nil, err
|
||||
}
|
||||
for _, pos := range positions {
|
||||
posSymbol := t.convertSymbol(pos["symbol"].(string))
|
||||
rawSymbol, _ := pos["symbol"].(string)
|
||||
posSymbol := t.convertSymbol(rawSymbol)
|
||||
if posSymbol == symbol && pos["side"] == "short" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,19 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// posFloat64 extracts a float64 from a position map, returning 0 on failure.
|
||||
// Use in loops where a malformed position should be skipped, not crash.
|
||||
func posFloat64(pos map[string]interface{}, key string) float64 {
|
||||
v, _ := SafeFloat64(pos, key)
|
||||
return v
|
||||
}
|
||||
|
||||
// posString extracts a string from a position map, returning "" on failure.
|
||||
func posString(pos map[string]interface{}, key string) string {
|
||||
v, _ := SafeString(pos, key)
|
||||
return v
|
||||
}
|
||||
|
||||
// SafeFloat64 Safely extract float64 value from map
|
||||
func SafeFloat64(data map[string]interface{}, key string) (float64, error) {
|
||||
value, ok := data[key]
|
||||
|
||||
@@ -179,9 +179,9 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
posSymbol := pos["symbol"].(string)
|
||||
posSymbol, _ := pos["symbol"].(string)
|
||||
if (posSymbol == symbol || posSymbol == searchSymbol) && pos["side"] == "long" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -266,9 +266,9 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
posSymbol := pos["symbol"].(string)
|
||||
posSymbol, _ := pos["symbol"].(string)
|
||||
if (posSymbol == symbol || posSymbol == searchSymbol) && pos["side"] == "short" {
|
||||
quantity = pos["positionAmt"].(float64)
|
||||
quantity, _ = pos["positionAmt"].(float64)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func (t *KuCoinTrader) CloseLong(symbol string, quantity float64) (map[string]in
|
||||
var marginMode string = "CROSS" // Default to CROSS
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
||||
actualQty = pos["positionAmt"].(float64)
|
||||
actualQty, _ = pos["positionAmt"].(float64)
|
||||
posFound = true
|
||||
// Get margin mode from position
|
||||
if mgnMode, ok := pos["mgnMode"].(string); ok {
|
||||
@@ -242,7 +242,7 @@ func (t *KuCoinTrader) CloseShort(symbol string, quantity float64) (map[string]i
|
||||
var marginMode string = "CROSS" // Default to CROSS
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
actualQty = pos["positionAmt"].(float64)
|
||||
actualQty, _ = pos["positionAmt"].(float64)
|
||||
posFound = true
|
||||
// Get margin mode from position
|
||||
if mgnMode, ok := pos["mgnMode"].(string); ok {
|
||||
|
||||
@@ -188,11 +188,11 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
||||
for _, pos := range positions {
|
||||
logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v, mgnMode=%v", pos["symbol"], pos["side"], pos["positionAmt"], pos["mgnMode"])
|
||||
if pos["symbol"] == symbol {
|
||||
side := pos["side"].(string)
|
||||
side, _ := pos["side"].(string)
|
||||
// In net_mode, "long" means positive position
|
||||
// In dual mode, check explicit "long" side
|
||||
if side == "long" || (t.positionMode == "net_mode" && side == "long") {
|
||||
actualQty = pos["positionAmt"].(float64)
|
||||
actualQty, _ = pos["positionAmt"].(float64)
|
||||
posFound = true
|
||||
if mgnMode, ok := pos["mgnMode"].(string); ok && mgnMode != "" {
|
||||
posMgnMode = mgnMode
|
||||
@@ -300,7 +300,7 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
||||
logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v, mgnMode=%v",
|
||||
pos["symbol"], pos["side"], pos["positionAmt"], pos["mgnMode"])
|
||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
||||
actualQty = pos["positionAmt"].(float64)
|
||||
actualQty, _ = pos["positionAmt"].(float64)
|
||||
posFound = true
|
||||
if mgnMode, ok := pos["mgnMode"].(string); ok && mgnMode != "" {
|
||||
posMgnMode = mgnMode
|
||||
|
||||
@@ -19,6 +19,11 @@ export function getSystemConfig(): Promise<SystemConfig> {
|
||||
cachedConfig = data
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
// Reset so next call retries instead of returning the rejected promise forever
|
||||
configPromise = null
|
||||
throw err
|
||||
})
|
||||
return configPromise
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user