Files
nofx/trader/binance/futures.go
T
tinkle-community cb31782be4 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)
2026-03-12 12:53:57 +08:00

181 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package binance
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"nofx/hook"
"nofx/logger"
"strings"
"sync"
"time"
"github.com/adshao/go-binance/v2/futures"
)
// getBrOrderID generates unique order ID (for futures contracts)
// Format: x-{BR_ID}{TIMESTAMP}{RANDOM}
// Futures limit is 32 characters, use this limit consistently
// Uses nanosecond timestamp + random number to ensure global uniqueness (collision probability < 10^-20)
func getBrOrderID() string {
brID := "KzrpZaP9" // Futures br ID
// Calculate available space: 32 - len("x-KzrpZaP9") = 32 - 11 = 21 characters
// Allocation: 13-digit timestamp + 8-digit random = 21 characters (perfect utilization)
timestamp := time.Now().UnixNano() % 10000000000000 // 13-digit nanosecond timestamp
// Generate 4-byte random number (8 hex digits)
randomBytes := make([]byte, 4)
rand.Read(randomBytes)
randomHex := hex.EncodeToString(randomBytes)
// Format: x-KzrpZaP9{13-digit timestamp}{8-digit random}
// Example: x-KzrpZaP91234567890123abcdef12 (exactly 31 characters)
orderID := fmt.Sprintf("x-%s%d%s", brID, timestamp, randomHex)
// Ensure not exceeding 32-character limit (theoretically exactly 31 characters)
if len(orderID) > 32 {
orderID = orderID[:32]
}
return orderID
}
// FuturesTrader Binance futures trader
type FuturesTrader struct {
client *futures.Client
// Balance cache
cachedBalance map[string]interface{}
balanceCacheTime time.Time
balanceCacheMutex sync.RWMutex
// Position cache
cachedPositions []map[string]interface{}
positionsCacheTime time.Time
positionsCacheMutex sync.RWMutex
// Cache validity period (15 seconds)
cacheDuration time.Duration
}
// NewFuturesTrader creates futures trader
func NewFuturesTrader(apiKey, secretKey string, userId string) *FuturesTrader {
client := futures.NewClient(apiKey, secretKey)
hookRes := hook.HookExec[hook.NewBinanceTraderResult](hook.NEW_BINANCE_TRADER, userId, client)
if hookRes != nil && hookRes.GetResult() != nil {
client = hookRes.GetResult()
}
// Sync time to avoid "Timestamp ahead" error
syncBinanceServerTime(client)
trader := &FuturesTrader{
client: client,
cacheDuration: 15 * time.Second, // 15-second cache
}
// Set dual-side position mode (Hedge Mode)
// This is required because the code uses PositionSide (LONG/SHORT)
if err := trader.setDualSidePosition(); err != nil {
logger.Infof("⚠️ Failed to set dual-side position mode: %v (ignore this warning if already in dual-side mode)", err)
}
return trader
}
// setDualSidePosition sets dual-side position mode (called during initialization)
func (t *FuturesTrader) setDualSidePosition() error {
// Try to set dual-side position mode
err := t.client.NewChangePositionModeService().
DualSide(true). // true = dual-side position (Hedge Mode)
Do(context.Background())
if err != nil {
// If error message contains "No need to change", it means already in dual-side position mode
if strings.Contains(err.Error(), "No need to change position side") {
logger.Infof(" ✓ Account is already in dual-side position mode (Hedge Mode)")
return nil
}
// Other errors are returned (but won't interrupt initialization in the caller)
return err
}
logger.Infof(" ✓ Account has been switched to dual-side position mode (Hedge Mode)")
logger.Infof(" ️ Dual-side position mode allows holding both long and short positions simultaneously")
return nil
}
// syncBinanceServerTime syncs Binance server time to ensure request timestamps are valid
func syncBinanceServerTime(client *futures.Client) {
serverTime, err := client.NewServerTimeService().Do(context.Background())
if err != nil {
logger.Infof("⚠️ Failed to sync Binance server time: %v", err)
return
}
now := time.Now().UnixMilli()
offset := now - serverTime
client.TimeOffset = offset
logger.Infof("⏱ Binance server time synced, offset %dms", offset)
}
// Helper functions
func contains(s, substr string) bool {
return len(s) >= len(substr) && stringContains(s, substr)
}
func stringContains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// calculatePrecision calculates precision from stepSize
func calculatePrecision(stepSize string) int {
// Remove trailing zeros
stepSize = trimTrailingZeros(stepSize)
// Find decimal point
dotIndex := -1
for i := 0; i < len(stepSize); i++ {
if stepSize[i] == '.' {
dotIndex = i
break
}
}
// If no decimal point or decimal point is at the end, precision is 0
if dotIndex == -1 || dotIndex == len(stepSize)-1 {
return 0
}
// Return number of digits after decimal point
return len(stepSize) - dotIndex - 1
}
// trimTrailingZeros removes trailing zeros
func trimTrailingZeros(s string) string {
// If no decimal point, return directly
if !stringContains(s, ".") {
return s
}
// Iterate backwards to remove trailing zeros
for len(s) > 0 && s[len(s)-1] == '0' {
s = s[:len(s)-1]
}
// If last character is decimal point, remove it too
if len(s) > 0 && s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
return s
}