fix: OKX contract size conversion issues

- Fix OpenLong/OpenShort: use quantity/ctVal instead of quantity*price/ctVal
- Fix GetPositions: convert contract count to base asset (posAmt = contracts * ctVal)
- Fix CloseLong/CloseShort: convert base asset to contracts before sending order
- Fix GetOrderStatus: convert executedQty from contracts to base asset
- Fix SetStopLoss/SetTakeProfit/FormatQuantity: same contract conversion fix
This commit is contained in:
tinkle-community
2025-12-11 11:17:43 +08:00
parent 19937ee260
commit 438f55bc30

View File

@@ -41,6 +41,9 @@ type OKXTrader struct {
secretKey string secretKey string
passphrase string passphrase string
// Margin mode setting
isCrossMargin bool
// HTTP client (proxy disabled) // HTTP client (proxy disabled)
httpClient *http.Client httpClient *http.Client
@@ -328,8 +331,8 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
var result []map[string]interface{} var result []map[string]interface{}
for _, pos := range positions { for _, pos := range positions {
posAmt, _ := strconv.ParseFloat(pos.Pos, 64) contractCount, _ := strconv.ParseFloat(pos.Pos, 64)
if posAmt == 0 { if contractCount == 0 {
continue continue
} }
@@ -342,14 +345,23 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
// Convert symbol format // Convert symbol format
symbol := t.convertSymbolBack(pos.InstId) symbol := t.convertSymbolBack(pos.InstId)
// Determine direction and ensure posAmt is positive // Determine direction and ensure contractCount is positive
side := "long" side := "long"
if pos.PosSide == "short" { if pos.PosSide == "short" {
side = "short" side = "short"
} }
// OKX short position's pos is negative, need to take absolute value // OKX short position's pos is negative, need to take absolute value
if posAmt < 0 { if contractCount < 0 {
posAmt = -posAmt contractCount = -contractCount
}
// Convert contract count to actual position amount (in base asset)
// positionAmt = contractCount * ctVal
inst, err := t.getInstrument(symbol)
posAmt := contractCount
if err == nil && inst.CtVal > 0 {
posAmt = contractCount * inst.CtVal
logger.Debugf(" 📊 OKX position %s: contracts=%.4f, ctVal=%.6f, posAmt=%.6f", symbol, contractCount, inst.CtVal, posAmt)
} }
// Parse timestamps // Parse timestamps
@@ -524,16 +536,13 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map
return nil, fmt.Errorf("failed to get instrument info: %w", err) return nil, fmt.Errorf("failed to get instrument info: %w", err)
} }
// OKX uses contract size, need to convert based on contract value // OKX uses contract count, need to convert quantity (in base asset) to contract count
price, err := t.GetMarketPrice(symbol) // sz = quantity / ctVal (number of contracts = asset amount / asset per contract)
if err != nil { sz := quantity / inst.CtVal
return nil, fmt.Errorf("failed to get market price: %w", err)
}
// Calculate contract size = quantity * price / contract value
sz := quantity * price / inst.CtVal
szStr := t.formatSize(sz, inst) szStr := t.formatSize(sz, inst)
logger.Infof(" 📊 OKX OpenLong: quantity=%.6f, ctVal=%.6f, contracts=%.2f", quantity, inst.CtVal, sz)
// Check max market order size limit // Check max market order size limit
if inst.MaxMktSz > 0 && sz > inst.MaxMktSz { if inst.MaxMktSz > 0 && sz > inst.MaxMktSz {
logger.Infof(" ⚠️ OKX market order size %.2f exceeds max %.2f, reducing to max", sz, inst.MaxMktSz) logger.Infof(" ⚠️ OKX market order size %.2f exceeds max %.2f, reducing to max", sz, inst.MaxMktSz)
@@ -604,14 +613,13 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma
return nil, fmt.Errorf("failed to get instrument info: %w", err) return nil, fmt.Errorf("failed to get instrument info: %w", err)
} }
price, err := t.GetMarketPrice(symbol) // OKX uses contract count, need to convert quantity (in base asset) to contract count
if err != nil { // sz = quantity / ctVal (number of contracts = asset amount / asset per contract)
return nil, fmt.Errorf("failed to get market price: %w", err) sz := quantity / inst.CtVal
}
sz := quantity * price / inst.CtVal
szStr := t.formatSize(sz, inst) szStr := t.formatSize(sz, inst)
logger.Infof(" 📊 OKX OpenShort: quantity=%.6f, ctVal=%.6f, contracts=%.2f", quantity, inst.CtVal, sz)
// Check max market order size limit // Check max market order size limit
if inst.MaxMktSz > 0 && sz > inst.MaxMktSz { if inst.MaxMktSz > 0 && sz > inst.MaxMktSz {
logger.Infof(" ⚠️ OKX market order size %.2f exceeds max %.2f, reducing to max", sz, inst.MaxMktSz) logger.Infof(" ⚠️ OKX market order size %.2f exceeds max %.2f, reducing to max", sz, inst.MaxMktSz)
@@ -668,7 +676,13 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma
func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
instId := t.convertSymbol(symbol) instId := t.convertSymbol(symbol)
// If quantity is 0, get current position (positionAmt is the contract size) // Get instrument info for contract conversion
inst, err := t.getInstrument(symbol)
if err != nil {
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)
if quantity == 0 { if quantity == 0 {
positions, err := t.GetPositions() positions, err := t.GetPositions()
if err != nil { if err != nil {
@@ -676,7 +690,7 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
} }
for _, pos := range positions { for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "long" { if pos["symbol"] == symbol && pos["side"] == "long" {
quantity = pos["positionAmt"].(float64) // This is already contract size quantity = pos["positionAmt"].(float64) // This is in base asset (BTC)
break break
} }
} }
@@ -685,17 +699,13 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
} }
} }
// Get instrument info for formatting contract size // Convert quantity (base asset) to contract count
inst, err := t.getInstrument(symbol) // contracts = quantity / ctVal
if err != nil { contracts := quantity / inst.CtVal
return nil, fmt.Errorf("failed to get instrument info: %w", err) szStr := t.formatSize(contracts, inst)
}
// quantity is already contract size, format directly logger.Infof("🔻 OKX close long: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
szStr := t.formatSize(quantity, inst) symbol, quantity, inst.CtVal, contracts, szStr)
logger.Infof("🔻 OKX close long parameters: symbol=%s, instId=%s, quantity(contracts)=%f, szStr=%s",
symbol, instId, quantity, szStr)
body := map[string]interface{}{ body := map[string]interface{}{
"instId": instId, "instId": instId,
@@ -747,7 +757,13 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
instId := t.convertSymbol(symbol) instId := t.convertSymbol(symbol)
// If quantity is 0, get current position (positionAmt is the contract size) // Get instrument info for contract conversion
inst, err := t.getInstrument(symbol)
if err != nil {
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)
if quantity == 0 { if quantity == 0 {
positions, err := t.GetPositions() positions, err := t.GetPositions()
if err != nil { if err != nil {
@@ -758,8 +774,8 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v", logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v",
pos["symbol"], pos["side"], pos["positionAmt"]) pos["symbol"], pos["side"], pos["positionAmt"])
if pos["symbol"] == symbol && pos["side"] == "short" { if pos["symbol"] == symbol && pos["side"] == "short" {
quantity = pos["positionAmt"].(float64) quantity = pos["positionAmt"].(float64) // This is in base asset (BTC)
logger.Infof("🔍 OKX found short position: quantity=%f", quantity) logger.Infof("🔍 OKX found short position: quantity=%f (base asset)", quantity)
break break
} }
} }
@@ -773,20 +789,13 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
quantity = -quantity quantity = -quantity
} }
// Get instrument info for formatting contract size // Convert quantity (base asset) to contract count
inst, err := t.getInstrument(symbol) // contracts = quantity / ctVal
if err != nil { contracts := quantity / inst.CtVal
return nil, fmt.Errorf("failed to get instrument info: %w", err) szStr := t.formatSize(contracts, inst)
}
logger.Infof("🔍 OKX instrument info: instId=%s, lotSz=%f, minSz=%f, ctVal=%f", logger.Infof("🔻 OKX close short: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
inst.InstID, inst.LotSz, inst.MinSz, inst.CtVal) symbol, quantity, inst.CtVal, contracts, szStr)
// quantity is already contract size, format directly
szStr := t.formatSize(quantity, inst)
logger.Infof("🔻 OKX close short parameters: symbol=%s, instId=%s, quantity(contracts)=%f, szStr=%s",
symbol, instId, quantity, szStr)
body := map[string]interface{}{ body := map[string]interface{}{
"instId": instId, "instId": instId,
@@ -877,9 +886,8 @@ func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, st
return fmt.Errorf("failed to get instrument info: %w", err) return fmt.Errorf("failed to get instrument info: %w", err)
} }
// Calculate contract size // Calculate contract size: quantity (in base asset) / ctVal (asset per contract)
price, _ := t.GetMarketPrice(symbol) sz := quantity / inst.CtVal
sz := quantity * price / inst.CtVal
szStr := t.formatSize(sz, inst) szStr := t.formatSize(sz, inst)
// Determine direction // Determine direction
@@ -921,9 +929,8 @@ func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity,
return fmt.Errorf("failed to get instrument info: %w", err) return fmt.Errorf("failed to get instrument info: %w", err)
} }
// Calculate contract size // Calculate contract size: quantity (in base asset) / ctVal (asset per contract)
price, _ := t.GetMarketPrice(symbol) sz := quantity / inst.CtVal
sz := quantity * price / inst.CtVal
szStr := t.formatSize(sz, inst) szStr := t.formatSize(sz, inst)
// Determine direction // Determine direction
@@ -1053,20 +1060,15 @@ func (t *OKXTrader) CancelStopOrders(symbol string) error {
return t.cancelAlgoOrders(symbol, "") return t.cancelAlgoOrders(symbol, "")
} }
// FormatQuantity formats quantity // FormatQuantity formats quantity (converts base asset quantity to contract count)
func (t *OKXTrader) FormatQuantity(symbol string, quantity float64) (string, error) { func (t *OKXTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
inst, err := t.getInstrument(symbol) inst, err := t.getInstrument(symbol)
if err != nil { if err != nil {
return fmt.Sprintf("%.3f", quantity), nil return fmt.Sprintf("%.3f", quantity), nil
} }
// OKX uses contract size // OKX uses contract count: quantity (in base asset) / ctVal (asset per contract)
price, _ := t.GetMarketPrice(symbol) sz := quantity / inst.CtVal
if price == 0 {
return fmt.Sprintf("%.0f", quantity), nil
}
sz := quantity * price / inst.CtVal
return t.formatSize(sz, inst), nil return t.formatSize(sz, inst), nil
} }
@@ -1124,11 +1126,20 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in
order := orders[0] order := orders[0]
avgPrice, _ := strconv.ParseFloat(order.AvgPx, 64) avgPrice, _ := strconv.ParseFloat(order.AvgPx, 64)
fillSz, _ := strconv.ParseFloat(order.AccFillSz, 64) fillSz, _ := strconv.ParseFloat(order.AccFillSz, 64) // This is in contracts
fee, _ := strconv.ParseFloat(order.Fee, 64) fee, _ := strconv.ParseFloat(order.Fee, 64)
cTime, _ := strconv.ParseInt(order.CTime, 10, 64) cTime, _ := strconv.ParseInt(order.CTime, 10, 64)
uTime, _ := strconv.ParseInt(order.UTime, 10, 64) uTime, _ := strconv.ParseInt(order.UTime, 10, 64)
// Convert contract count to base asset quantity
// executedQty = contracts * ctVal
executedQty := fillSz
inst, err := t.getInstrument(symbol)
if err == nil && inst.CtVal > 0 {
executedQty = fillSz * inst.CtVal
logger.Debugf(" 📊 OKX order %s: fillSz(contracts)=%.4f, ctVal=%.6f, executedQty=%.6f", orderID, fillSz, inst.CtVal, executedQty)
}
// Status mapping // Status mapping
statusMap := map[string]string{ statusMap := map[string]string{
"filled": "FILLED", "filled": "FILLED",
@@ -1147,7 +1158,7 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in
"symbol": symbol, "symbol": symbol,
"status": status, "status": status,
"avgPrice": avgPrice, "avgPrice": avgPrice,
"executedQty": fillSz, "executedQty": executedQty,
"side": order.Side, "side": order.Side,
"type": order.OrdType, "type": order.OrdType,
"time": cTime, "time": cTime,