mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
fix: use position's actual margin mode when closing OKX positions
- Parse mgnMode field from OKX positions API response - Use position's mgnMode (cross/isolated) in close orders instead of hardcoding cross - This fixes 'no position in this direction' error when closing isolated margin positions
This commit is contained in:
@@ -33,6 +33,7 @@ const (
|
|||||||
okxCancelAlgoPath = "/api/v5/trade/cancel-algos"
|
okxCancelAlgoPath = "/api/v5/trade/cancel-algos"
|
||||||
okxAlgoPendingPath = "/api/v5/trade/orders-algo-pending"
|
okxAlgoPendingPath = "/api/v5/trade/orders-algo-pending"
|
||||||
okxPositionModePath = "/api/v5/account/set-position-mode"
|
okxPositionModePath = "/api/v5/account/set-position-mode"
|
||||||
|
okxAccountConfigPath = "/api/v5/account/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OKXTrader OKX futures trader
|
// OKXTrader OKX futures trader
|
||||||
@@ -44,6 +45,9 @@ type OKXTrader struct {
|
|||||||
// Margin mode setting
|
// Margin mode setting
|
||||||
isCrossMargin bool
|
isCrossMargin bool
|
||||||
|
|
||||||
|
// Position mode: "long_short_mode" (hedge) or "net_mode" (one-way)
|
||||||
|
positionMode string
|
||||||
|
|
||||||
// HTTP client (proxy disabled)
|
// HTTP client (proxy disabled)
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
@@ -117,14 +121,46 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
|
|||||||
instrumentsCache: make(map[string]*OKXInstrument),
|
instrumentsCache: make(map[string]*OKXInstrument),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set dual position mode
|
// Get current position mode first
|
||||||
if err := trader.setPositionMode(); err != nil {
|
if err := trader.detectPositionMode(); err != nil {
|
||||||
logger.Infof("⚠️ Failed to set OKX position mode: %v (ignore if already in dual mode)", err)
|
logger.Infof("⚠️ Failed to detect OKX position mode: %v, assuming dual mode", err)
|
||||||
|
trader.positionMode = "long_short_mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to set dual position mode (only if not already)
|
||||||
|
if trader.positionMode != "long_short_mode" {
|
||||||
|
if err := trader.setPositionMode(); err != nil {
|
||||||
|
logger.Infof("⚠️ Failed to set OKX position mode: %v (current mode: %s)", err, trader.positionMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("✓ OKX trader initialized with position mode: %s", trader.positionMode)
|
||||||
return trader
|
return trader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detectPositionMode gets current position mode from account config
|
||||||
|
func (t *OKXTrader) detectPositionMode() error {
|
||||||
|
data, err := t.doRequest("GET", okxAccountConfigPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get account config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []struct {
|
||||||
|
PosMode string `json:"posMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &configs); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse account config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configs) > 0 {
|
||||||
|
t.positionMode = configs[0].PosMode
|
||||||
|
logger.Infof("✓ Detected OKX position mode: %s", t.positionMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// setPositionMode sets dual position mode
|
// setPositionMode sets dual position mode
|
||||||
func (t *OKXTrader) setPositionMode() error {
|
func (t *OKXTrader) setPositionMode() error {
|
||||||
body := map[string]string{
|
body := map[string]string{
|
||||||
@@ -321,16 +357,19 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|||||||
Lever string `json:"lever"`
|
Lever string `json:"lever"`
|
||||||
LiqPx string `json:"liqPx"`
|
LiqPx string `json:"liqPx"`
|
||||||
Margin string `json:"margin"`
|
Margin string `json:"margin"`
|
||||||
CTime string `json:"cTime"` // Position created time (ms)
|
MgnMode string `json:"mgnMode"` // Margin mode: "cross" or "isolated"
|
||||||
UTime string `json:"uTime"` // Position last update time (ms)
|
CTime string `json:"cTime"` // Position created time (ms)
|
||||||
|
UTime string `json:"uTime"` // Position last update time (ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &positions); err != nil {
|
if err := json.Unmarshal(data, &positions); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse position data: %w", err)
|
return nil, fmt.Errorf("failed to parse position data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Infof("🔍 OKX raw positions response: %d positions", len(positions))
|
||||||
var result []map[string]interface{}
|
var result []map[string]interface{}
|
||||||
for _, pos := range positions {
|
for _, pos := range positions {
|
||||||
|
logger.Infof("🔍 OKX raw position: instId=%s, posSide=%s, pos=%s, mgnMode=%s", pos.InstId, pos.PosSide, pos.Pos, pos.MgnMode)
|
||||||
contractCount, _ := strconv.ParseFloat(pos.Pos, 64)
|
contractCount, _ := strconv.ParseFloat(pos.Pos, 64)
|
||||||
if contractCount == 0 {
|
if contractCount == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -344,6 +383,7 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|||||||
|
|
||||||
// Convert symbol format
|
// Convert symbol format
|
||||||
symbol := t.convertSymbolBack(pos.InstId)
|
symbol := t.convertSymbolBack(pos.InstId)
|
||||||
|
logger.Infof("🔍 OKX symbol conversion: %s → %s", pos.InstId, symbol)
|
||||||
|
|
||||||
// Determine direction and ensure contractCount is positive
|
// Determine direction and ensure contractCount is positive
|
||||||
side := "long"
|
side := "long"
|
||||||
@@ -368,6 +408,12 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|||||||
cTime, _ := strconv.ParseInt(pos.CTime, 10, 64)
|
cTime, _ := strconv.ParseInt(pos.CTime, 10, 64)
|
||||||
uTime, _ := strconv.ParseInt(pos.UTime, 10, 64)
|
uTime, _ := strconv.ParseInt(pos.UTime, 10, 64)
|
||||||
|
|
||||||
|
// Default to cross margin mode if not specified
|
||||||
|
mgnMode := pos.MgnMode
|
||||||
|
if mgnMode == "" {
|
||||||
|
mgnMode = "cross"
|
||||||
|
}
|
||||||
|
|
||||||
posMap := map[string]interface{}{
|
posMap := map[string]interface{}{
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"positionAmt": posAmt,
|
"positionAmt": posAmt,
|
||||||
@@ -377,8 +423,9 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|||||||
"leverage": leverage,
|
"leverage": leverage,
|
||||||
"liquidationPrice": liqPrice,
|
"liquidationPrice": liqPrice,
|
||||||
"side": side,
|
"side": side,
|
||||||
"createdTime": cTime, // Position open time (ms)
|
"mgnMode": mgnMode, // Margin mode: "cross" or "isolated"
|
||||||
"updatedTime": uTime, // Position last update time (ms)
|
"createdTime": cTime, // Position open time (ms)
|
||||||
|
"updatedTime": uTime, // Position last update time (ms)
|
||||||
}
|
}
|
||||||
result = append(result, posMap)
|
result = append(result, posMap)
|
||||||
}
|
}
|
||||||
@@ -392,6 +439,14 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvalidatePositionCache clears the position cache to force fresh data on next call
|
||||||
|
func (t *OKXTrader) InvalidatePositionCache() {
|
||||||
|
t.positionsCacheMutex.Lock()
|
||||||
|
t.cachedPositions = nil
|
||||||
|
t.positionsCacheTime = time.Time{}
|
||||||
|
t.positionsCacheMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// getInstrument gets instrument info
|
// getInstrument gets instrument info
|
||||||
func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) {
|
func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) {
|
||||||
instId := t.convertSymbol(symbol)
|
instId := t.convertSymbol(symbol)
|
||||||
@@ -682,21 +737,47 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
|||||||
return nil, fmt.Errorf("failed to get instrument info: %w", err)
|
return nil, fmt.Errorf("failed to get instrument info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If quantity is 0, get current position (positionAmt is in base asset, e.g. BTC)
|
// Invalidate position cache and get fresh positions
|
||||||
if quantity == 0 {
|
t.InvalidatePositionCache()
|
||||||
positions, err := t.GetPositions()
|
positions, err := t.GetPositions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to get positions: %w", err)
|
||||||
}
|
}
|
||||||
for _, pos := range positions {
|
|
||||||
if pos["symbol"] == symbol && pos["side"] == "long" {
|
// Find actual position from exchange
|
||||||
quantity = pos["positionAmt"].(float64) // This is in base asset (BTC)
|
var actualQty float64
|
||||||
|
var posFound bool
|
||||||
|
var posMgnMode string = "cross" // Default to cross margin
|
||||||
|
logger.Infof("🔍 OKX CloseLong: searching for symbol=%s in %d positions", symbol, len(positions))
|
||||||
|
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)
|
||||||
|
// 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)
|
||||||
|
posFound = true
|
||||||
|
if mgnMode, ok := pos["mgnMode"].(string); ok && mgnMode != "" {
|
||||||
|
posMgnMode = mgnMode
|
||||||
|
}
|
||||||
|
logger.Infof("🔍 OKX CloseLong: found matching position! qty=%.6f, mgnMode=%s", actualQty, posMgnMode)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if quantity == 0 {
|
}
|
||||||
return nil, fmt.Errorf("long position not found for %s", symbol)
|
|
||||||
}
|
if !posFound || actualQty == 0 {
|
||||||
|
logger.Infof("🔍 OKX CloseLong: NO position found for %s LONG", symbol)
|
||||||
|
return map[string]interface{}{
|
||||||
|
"status": "NO_POSITION",
|
||||||
|
"message": fmt.Sprintf("No long position found for %s on OKX", symbol),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use actual quantity from exchange (more accurate than passed quantity)
|
||||||
|
if quantity == 0 || quantity > actualQty {
|
||||||
|
quantity = actualQty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert quantity (base asset) to contract count
|
// Convert quantity (base asset) to contract count
|
||||||
@@ -704,20 +785,24 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
|
|||||||
contracts := quantity / inst.CtVal
|
contracts := quantity / inst.CtVal
|
||||||
szStr := t.formatSize(contracts, inst)
|
szStr := t.formatSize(contracts, inst)
|
||||||
|
|
||||||
logger.Infof("🔻 OKX close long: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
|
logger.Infof("🔻 OKX close long: symbol=%s, instId=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s, posMode=%s, mgnMode=%s",
|
||||||
symbol, quantity, inst.CtVal, contracts, szStr)
|
symbol, instId, quantity, inst.CtVal, contracts, szStr, t.positionMode, posMgnMode)
|
||||||
|
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"instId": instId,
|
"instId": instId,
|
||||||
"tdMode": "cross",
|
"tdMode": posMgnMode, // Use position's actual margin mode (cross or isolated)
|
||||||
"side": "sell",
|
"side": "sell",
|
||||||
"posSide": "long",
|
|
||||||
"ordType": "market",
|
"ordType": "market",
|
||||||
"sz": szStr,
|
"sz": szStr,
|
||||||
"clOrdId": genOkxClOrdID(),
|
"clOrdId": genOkxClOrdID(),
|
||||||
"tag": okxTag,
|
"tag": okxTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add posSide in dual mode (long_short_mode)
|
||||||
|
if t.positionMode == "long_short_mode" {
|
||||||
|
body["posSide"] = "long"
|
||||||
|
}
|
||||||
|
|
||||||
data, err := t.doRequest("POST", okxOrderPath, body)
|
data, err := t.doRequest("POST", okxOrderPath, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to close long position: %w", err)
|
return nil, fmt.Errorf("failed to close long position: %w", err)
|
||||||
@@ -763,25 +848,42 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
|||||||
return nil, fmt.Errorf("failed to get instrument info: %w", err)
|
return nil, fmt.Errorf("failed to get instrument info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If quantity is 0, get current position (positionAmt is in base asset, e.g. BTC)
|
// Invalidate position cache and get fresh positions
|
||||||
if quantity == 0 {
|
t.InvalidatePositionCache()
|
||||||
positions, err := t.GetPositions()
|
positions, err := t.GetPositions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to get positions: %w", err)
|
||||||
}
|
}
|
||||||
logger.Infof("🔍 OKX CloseShort searching positions: symbol=%s, current position count=%d", symbol, len(positions))
|
|
||||||
for _, pos := range positions {
|
// Find actual position from exchange
|
||||||
logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v",
|
var actualQty float64
|
||||||
pos["symbol"], pos["side"], pos["positionAmt"])
|
var posFound bool
|
||||||
if pos["symbol"] == symbol && pos["side"] == "short" {
|
var posMgnMode string = "cross" // Default to cross margin
|
||||||
quantity = pos["positionAmt"].(float64) // This is in base asset (BTC)
|
logger.Infof("🔍 OKX CloseShort searching positions: symbol=%s, current position count=%d", symbol, len(positions))
|
||||||
logger.Infof("🔍 OKX found short position: quantity=%f (base asset)", quantity)
|
for _, pos := range positions {
|
||||||
break
|
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)
|
||||||
|
posFound = true
|
||||||
|
if mgnMode, ok := pos["mgnMode"].(string); ok && mgnMode != "" {
|
||||||
|
posMgnMode = mgnMode
|
||||||
}
|
}
|
||||||
|
logger.Infof("🔍 OKX found short position: quantity=%f (base asset), mgnMode=%s", actualQty, posMgnMode)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if quantity == 0 {
|
}
|
||||||
return nil, fmt.Errorf("short position not found for %s", symbol)
|
|
||||||
}
|
if !posFound || actualQty == 0 {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"status": "NO_POSITION",
|
||||||
|
"message": fmt.Sprintf("No short position found for %s on OKX", symbol),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use actual quantity from exchange (more accurate than passed quantity)
|
||||||
|
if quantity == 0 || quantity > actualQty {
|
||||||
|
quantity = actualQty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure quantity is positive (OKX sz parameter must be positive)
|
// Ensure quantity is positive (OKX sz parameter must be positive)
|
||||||
@@ -794,20 +896,24 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
|
|||||||
contracts := quantity / inst.CtVal
|
contracts := quantity / inst.CtVal
|
||||||
szStr := t.formatSize(contracts, inst)
|
szStr := t.formatSize(contracts, inst)
|
||||||
|
|
||||||
logger.Infof("🔻 OKX close short: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
|
logger.Infof("🔻 OKX close short: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s, posMode=%s, mgnMode=%s",
|
||||||
symbol, quantity, inst.CtVal, contracts, szStr)
|
symbol, quantity, inst.CtVal, contracts, szStr, t.positionMode, posMgnMode)
|
||||||
|
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"instId": instId,
|
"instId": instId,
|
||||||
"tdMode": "cross",
|
"tdMode": posMgnMode, // Use position's actual margin mode (cross or isolated)
|
||||||
"side": "buy",
|
"side": "buy",
|
||||||
"posSide": "short",
|
|
||||||
"ordType": "market",
|
"ordType": "market",
|
||||||
"sz": szStr,
|
"sz": szStr,
|
||||||
"clOrdId": genOkxClOrdID(),
|
"clOrdId": genOkxClOrdID(),
|
||||||
"tag": okxTag,
|
"tag": okxTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add posSide in dual mode (long_short_mode)
|
||||||
|
if t.positionMode == "long_short_mode" {
|
||||||
|
body["posSide"] = "short"
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("🔻 OKX close short request body: %+v", body)
|
logger.Infof("🔻 OKX close short request body: %+v", body)
|
||||||
|
|
||||||
data, err := t.doRequest("POST", okxOrderPath, body)
|
data, err := t.doRequest("POST", okxOrderPath, body)
|
||||||
|
|||||||
Reference in New Issue
Block a user