mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
This commit is contained in:
290
trader/binance/futures_positions.go
Normal file
290
trader/binance/futures_positions.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package binance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance/v2/futures"
|
||||
)
|
||||
|
||||
// GetPositions gets all positions (with cache)
|
||||
func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
// First check if cache is valid
|
||||
t.positionsCacheMutex.RLock()
|
||||
if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration {
|
||||
cacheAge := time.Since(t.positionsCacheTime)
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
logger.Infof("✓ Using cached position information (cache age: %.1f seconds ago)", cacheAge.Seconds())
|
||||
return t.cachedPositions, nil
|
||||
}
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
|
||||
// Cache expired or doesn't exist, call API
|
||||
logger.Infof("🔄 Cache expired, calling Binance API to get position information...")
|
||||
positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get positions: %w", err)
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, pos := range positions {
|
||||
posAmt, _ := strconv.ParseFloat(pos.PositionAmt, 64)
|
||||
if posAmt == 0 {
|
||||
continue // Skip positions with zero amount
|
||||
}
|
||||
|
||||
posMap := make(map[string]interface{})
|
||||
posMap["symbol"] = pos.Symbol
|
||||
posMap["positionAmt"], _ = strconv.ParseFloat(pos.PositionAmt, 64)
|
||||
posMap["entryPrice"], _ = strconv.ParseFloat(pos.EntryPrice, 64)
|
||||
posMap["markPrice"], _ = strconv.ParseFloat(pos.MarkPrice, 64)
|
||||
posMap["unRealizedProfit"], _ = strconv.ParseFloat(pos.UnRealizedProfit, 64)
|
||||
posMap["leverage"], _ = strconv.ParseFloat(pos.Leverage, 64)
|
||||
posMap["liquidationPrice"], _ = strconv.ParseFloat(pos.LiquidationPrice, 64)
|
||||
// Note: Binance SDK doesn't expose updateTime field, will fallback to local tracking
|
||||
|
||||
// Determine direction
|
||||
if posAmt > 0 {
|
||||
posMap["side"] = "long"
|
||||
} else {
|
||||
posMap["side"] = "short"
|
||||
}
|
||||
|
||||
result = append(result, posMap)
|
||||
}
|
||||
|
||||
// Update cache
|
||||
t.positionsCacheMutex.Lock()
|
||||
t.cachedPositions = result
|
||||
t.positionsCacheTime = time.Now()
|
||||
t.positionsCacheMutex.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetMarginMode sets margin mode
|
||||
func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
var marginType futures.MarginType
|
||||
if isCrossMargin {
|
||||
marginType = futures.MarginTypeCrossed
|
||||
} else {
|
||||
marginType = futures.MarginTypeIsolated
|
||||
}
|
||||
|
||||
// Try to set margin mode
|
||||
err := t.client.NewChangeMarginTypeService().
|
||||
Symbol(symbol).
|
||||
MarginType(marginType).
|
||||
Do(context.Background())
|
||||
|
||||
marginModeStr := "Cross Margin"
|
||||
if !isCrossMargin {
|
||||
marginModeStr = "Isolated Margin"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// If error message contains "No need to change", margin mode is already set to target value
|
||||
if contains(err.Error(), "No need to change margin type") {
|
||||
logger.Infof(" ✓ %s margin mode is already %s", symbol, marginModeStr)
|
||||
return nil
|
||||
}
|
||||
// If there is an open position, margin mode cannot be changed, but this doesn't affect trading
|
||||
if contains(err.Error(), "Margin type cannot be changed if there exists position") {
|
||||
logger.Infof(" ⚠️ %s has open positions, cannot change margin mode, continuing with current mode", symbol)
|
||||
return nil
|
||||
}
|
||||
// Detect Multi-Assets mode (error code -4168)
|
||||
if contains(err.Error(), "Multi-Assets mode") || contains(err.Error(), "-4168") || contains(err.Error(), "4168") {
|
||||
logger.Infof(" ⚠️ %s detected Multi-Assets mode, forcing Cross Margin mode", symbol)
|
||||
logger.Infof(" 💡 Tip: To use Isolated Margin mode, please disable Multi-Assets mode in Binance")
|
||||
return nil
|
||||
}
|
||||
// Detect Unified Account API (Portfolio Margin)
|
||||
if contains(err.Error(), "unified") || contains(err.Error(), "portfolio") || contains(err.Error(), "Portfolio") {
|
||||
logger.Infof(" ❌ %s detected Unified Account API, unable to trade futures", symbol)
|
||||
return fmt.Errorf("please use 'Spot & Futures Trading' API permission, do not use 'Unified Account API'")
|
||||
}
|
||||
logger.Infof(" ⚠️ Failed to set margin mode: %v", err)
|
||||
// Don't return error, let trading continue
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ %s margin mode set to %s", symbol, marginModeStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLeverage sets leverage (with smart detection and cooldown period)
|
||||
func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
|
||||
// First try to get current leverage (from position information)
|
||||
currentLeverage := 0
|
||||
positions, err := t.GetPositions()
|
||||
if err == nil {
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == symbol {
|
||||
if lev, ok := pos["leverage"].(float64); ok {
|
||||
currentLeverage = int(lev)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If current leverage is already the target leverage, skip
|
||||
if currentLeverage == leverage && currentLeverage > 0 {
|
||||
logger.Infof(" ✓ %s leverage is already %dx, no need to change", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Change leverage
|
||||
_, err = t.client.NewChangeLeverageService().
|
||||
Symbol(symbol).
|
||||
Leverage(leverage).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
// If error message contains "No need to change", leverage is already the target value
|
||||
if contains(err.Error(), "No need to change") {
|
||||
logger.Infof(" ✓ %s leverage is already %dx", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to set leverage: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ %s leverage changed to %dx", symbol, leverage)
|
||||
|
||||
// Wait 5 seconds after changing leverage (to avoid cooldown period errors)
|
||||
logger.Infof(" ⏱ Waiting 5 seconds for cooldown period...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMarketPrice gets market price
|
||||
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
|
||||
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get price: %w", err)
|
||||
}
|
||||
|
||||
if len(prices) == 0 {
|
||||
return 0, fmt.Errorf("price not found")
|
||||
}
|
||||
|
||||
price, err := strconv.ParseFloat(prices[0].Price, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return price, nil
|
||||
}
|
||||
|
||||
// CalculatePositionSize calculates position size
|
||||
func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float64, leverage int) float64 {
|
||||
riskAmount := balance * (riskPercent / 100.0)
|
||||
positionValue := riskAmount * float64(leverage)
|
||||
quantity := positionValue / price
|
||||
return quantity
|
||||
}
|
||||
|
||||
// GetMinNotional gets minimum notional value (Binance requirement)
|
||||
func (t *FuturesTrader) GetMinNotional(symbol string) float64 {
|
||||
// Use conservative default value of 10 USDT to ensure order passes exchange validation
|
||||
return 10.0
|
||||
}
|
||||
|
||||
// CheckMinNotional checks if order meets minimum notional value requirement
|
||||
func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error {
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get market price: %w", err)
|
||||
}
|
||||
|
||||
notionalValue := quantity * price
|
||||
minNotional := t.GetMinNotional(symbol)
|
||||
|
||||
if notionalValue < minNotional {
|
||||
return fmt.Errorf(
|
||||
"order amount %.2f USDT is below minimum requirement %.2f USDT (quantity: %.4f, price: %.4f)",
|
||||
notionalValue, minNotional, quantity, price,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSymbolPrecision gets the quantity precision for a trading pair
|
||||
func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
|
||||
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get trading rules: %w", err)
|
||||
}
|
||||
|
||||
for _, s := range exchangeInfo.Symbols {
|
||||
if s.Symbol == symbol {
|
||||
// Get precision from LOT_SIZE filter
|
||||
for _, filter := range s.Filters {
|
||||
if filter["filterType"] == "LOT_SIZE" {
|
||||
stepSize := filter["stepSize"].(string)
|
||||
precision := calculatePrecision(stepSize)
|
||||
logger.Infof(" %s quantity precision: %d (stepSize: %s)", symbol, precision, stepSize)
|
||||
return precision, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof(" ⚠ %s precision information not found, using default precision 3", symbol)
|
||||
return 3, nil // Default precision is 3
|
||||
}
|
||||
|
||||
// FormatQuantity formats quantity to correct precision
|
||||
func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||
precision, err := t.GetSymbolPrecision(symbol)
|
||||
if err != nil {
|
||||
// If retrieval fails, use default format
|
||||
return fmt.Sprintf("%.3f", quantity), nil
|
||||
}
|
||||
|
||||
format := fmt.Sprintf("%%.%df", precision)
|
||||
return fmt.Sprintf(format, quantity), nil
|
||||
}
|
||||
|
||||
// GetSymbolPricePrecision gets the price precision for a trading pair
|
||||
func (t *FuturesTrader) GetSymbolPricePrecision(symbol string) (int, error) {
|
||||
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get trading rules: %w", err)
|
||||
}
|
||||
|
||||
for _, s := range exchangeInfo.Symbols {
|
||||
if s.Symbol == symbol {
|
||||
// Get precision from PRICE_FILTER filter
|
||||
for _, filter := range s.Filters {
|
||||
if filter["filterType"] == "PRICE_FILTER" {
|
||||
tickSize := filter["tickSize"].(string)
|
||||
precision := calculatePrecision(tickSize)
|
||||
return precision, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 2 decimal places for price
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
// FormatPrice formats price to correct precision
|
||||
func (t *FuturesTrader) FormatPrice(symbol string, price float64) (string, error) {
|
||||
precision, err := t.GetSymbolPricePrecision(symbol)
|
||||
if err != nil {
|
||||
// If retrieval fails, use default format
|
||||
return fmt.Sprintf("%.2f", price), nil
|
||||
}
|
||||
|
||||
format := fmt.Sprintf("%%.%df", precision)
|
||||
return fmt.Sprintf(format, price), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user