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
passphrase string
// Margin mode setting
isCrossMargin bool
// HTTP client (proxy disabled)
httpClient *http.Client
@@ -328,8 +331,8 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
var result []map[string]interface{}
for _, pos := range positions {
posAmt, _ := strconv.ParseFloat(pos.Pos, 64)
if posAmt == 0 {
contractCount, _ := strconv.ParseFloat(pos.Pos, 64)
if contractCount == 0 {
continue
}
@@ -342,14 +345,23 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
// Convert symbol format
symbol := t.convertSymbolBack(pos.InstId)
// Determine direction and ensure posAmt is positive
// Determine direction and ensure contractCount is positive
side := "long"
if pos.PosSide == "short" {
side = "short"
}
// OKX short position's pos is negative, need to take absolute value
if posAmt < 0 {
posAmt = -posAmt
if contractCount < 0 {
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
@@ -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)
}
// OKX uses contract size, need to convert based on contract value
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, fmt.Errorf("failed to get market price: %w", err)
}
// Calculate contract size = quantity * price / contract value
sz := quantity * price / inst.CtVal
// OKX uses contract count, need to convert quantity (in base asset) to contract count
// sz = quantity / ctVal (number of contracts = asset amount / asset per contract)
sz := quantity / inst.CtVal
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
if inst.MaxMktSz > 0 && 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)
}
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, fmt.Errorf("failed to get market price: %w", err)
}
sz := quantity * price / inst.CtVal
// OKX uses contract count, need to convert quantity (in base asset) to contract count
// sz = quantity / ctVal (number of contracts = asset amount / asset per contract)
sz := quantity / inst.CtVal
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
if inst.MaxMktSz > 0 && 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) {
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 {
positions, err := t.GetPositions()
if err != nil {
@@ -676,7 +690,7 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
}
for _, pos := range positions {
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
}
}
@@ -685,17 +699,13 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter
}
}
// Get instrument info for formatting contract size
inst, err := t.getInstrument(symbol)
if err != nil {
return nil, fmt.Errorf("failed to get instrument info: %w", err)
}
// Convert quantity (base asset) to contract count
// contracts = quantity / ctVal
contracts := quantity / inst.CtVal
szStr := t.formatSize(contracts, inst)
// quantity is already contract size, format directly
szStr := t.formatSize(quantity, inst)
logger.Infof("🔻 OKX close long parameters: symbol=%s, instId=%s, quantity(contracts)=%f, szStr=%s",
symbol, instId, quantity, szStr)
logger.Infof("🔻 OKX close long: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
symbol, quantity, inst.CtVal, contracts, szStr)
body := map[string]interface{}{
"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) {
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 {
positions, err := t.GetPositions()
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",
pos["symbol"], pos["side"], pos["positionAmt"])
if pos["symbol"] == symbol && pos["side"] == "short" {
quantity = pos["positionAmt"].(float64)
logger.Infof("🔍 OKX found short position: quantity=%f", quantity)
quantity = pos["positionAmt"].(float64) // This is in base asset (BTC)
logger.Infof("🔍 OKX found short position: quantity=%f (base asset)", quantity)
break
}
}
@@ -773,20 +789,13 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
quantity = -quantity
}
// Get instrument info for formatting contract size
inst, err := t.getInstrument(symbol)
if err != nil {
return nil, fmt.Errorf("failed to get instrument info: %w", err)
}
// Convert quantity (base asset) to contract count
// contracts = quantity / ctVal
contracts := quantity / inst.CtVal
szStr := t.formatSize(contracts, inst)
logger.Infof("🔍 OKX instrument info: instId=%s, lotSz=%f, minSz=%f, ctVal=%f",
inst.InstID, inst.LotSz, inst.MinSz, inst.CtVal)
// 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)
logger.Infof("🔻 OKX close short: symbol=%s, quantity=%.6f, ctVal=%.6f, contracts=%.2f, szStr=%s",
symbol, quantity, inst.CtVal, contracts, szStr)
body := map[string]interface{}{
"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)
}
// Calculate contract size
price, _ := t.GetMarketPrice(symbol)
sz := quantity * price / inst.CtVal
// Calculate contract size: quantity (in base asset) / ctVal (asset per contract)
sz := quantity / inst.CtVal
szStr := t.formatSize(sz, inst)
// 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)
}
// Calculate contract size
price, _ := t.GetMarketPrice(symbol)
sz := quantity * price / inst.CtVal
// Calculate contract size: quantity (in base asset) / ctVal (asset per contract)
sz := quantity / inst.CtVal
szStr := t.formatSize(sz, inst)
// Determine direction
@@ -1053,20 +1060,15 @@ func (t *OKXTrader) CancelStopOrders(symbol string) error {
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) {
inst, err := t.getInstrument(symbol)
if err != nil {
return fmt.Sprintf("%.3f", quantity), nil
}
// OKX uses contract size
price, _ := t.GetMarketPrice(symbol)
if price == 0 {
return fmt.Sprintf("%.0f", quantity), nil
}
sz := quantity * price / inst.CtVal
// OKX uses contract count: quantity (in base asset) / ctVal (asset per contract)
sz := quantity / inst.CtVal
return t.formatSize(sz, inst), nil
}
@@ -1124,11 +1126,20 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in
order := orders[0]
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)
cTime, _ := strconv.ParseInt(order.CTime, 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
statusMap := map[string]string{
"filled": "FILLED",
@@ -1147,7 +1158,7 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in
"symbol": symbol,
"status": status,
"avgPrice": avgPrice,
"executedQty": fillSz,
"executedQty": executedQty,
"side": order.Side,
"type": order.OrdType,
"time": cTime,