mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
feat(trader): honor OKX margin mode and explicitly route nofx data via claw402 wallet (#1476)
Merges PR #1476 from deanokk with the following additions: - OKX margin mode fix: cross/isolated honored end-to-end via mgnMode/tdMode - claw402 wallet routing fix: trader path now explicitly resolves user wallet - refactor: ResolveClaw402WalletKey extracted to store layer (no duplication) - OKX SetMarginMode documented as local-state-only (no legacy API call) - OKX margin mode tests expanded: cross mode, OpenShort, SetTakeProfit
This commit is contained in:
+14
-1
@@ -516,8 +516,17 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
req.PromptVariant = "balanced"
|
||||
}
|
||||
|
||||
claw402WalletKey, err := s.resolveStrategyDataWalletKey(userID, req.AIModelID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
"ai_response": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create strategy engine to build prompt
|
||||
engine := kernel.NewStrategyEngine(&req.Config)
|
||||
engine := kernel.NewStrategyEngine(&req.Config, claw402WalletKey)
|
||||
|
||||
// Get candidate coins
|
||||
candidates, err := engine.GetCandidateCoins()
|
||||
@@ -697,3 +706,7 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *Server) resolveStrategyDataWalletKey(userID, selectedModelID string) (string, error) {
|
||||
return s.store.AIModel().ResolveClaw402WalletKey(userID, selectedModelID)
|
||||
}
|
||||
|
||||
@@ -703,6 +703,8 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
||||
traderConfig.CustomAPIKey = string(aiModelCfg.APIKey)
|
||||
}
|
||||
|
||||
traderConfig.Claw402WalletKey = resolveTraderDataWalletKey(st, traderCfg.UserID, aiModelCfg)
|
||||
|
||||
// Create trader instance
|
||||
at, err := trader.NewAutoTrader(traderConfig, st, traderCfg.UserID)
|
||||
if err != nil {
|
||||
@@ -741,3 +743,26 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveTraderDataWalletKey(st *store.Store, userID string, selectedModel *store.AIModel) string {
|
||||
// Fast path: selected model is itself a claw402 model.
|
||||
if selectedModel != nil && selectedModel.Provider == "claw402" {
|
||||
if walletKey := string(selectedModel.APIKey); walletKey != "" {
|
||||
return walletKey
|
||||
}
|
||||
}
|
||||
|
||||
if st == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Fallback: find any configured claw402 model for this user so that paid
|
||||
// NofxAI data sources work even when a non-claw402 model (e.g. deepseek) is
|
||||
// selected as the AI brain.
|
||||
preferredID := ""
|
||||
walletKey, err := st.AIModel().ResolveClaw402WalletKey(userID, preferredID)
|
||||
if err != nil {
|
||||
logger.Warnf("⚠️ Failed to load claw402 wallet for trader data routing: %v", err)
|
||||
return ""
|
||||
}
|
||||
return walletKey
|
||||
}
|
||||
|
||||
@@ -253,6 +253,43 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI
|
||||
}
|
||||
|
||||
// Create creates an AI model
|
||||
// ResolveClaw402WalletKey returns the claw402 wallet private key for a user.
|
||||
// If preferredModelID is non-empty and points to a claw402 model, its key is returned first.
|
||||
// Otherwise the first enabled claw402 model in the user's model list is used.
|
||||
// Returns ("", nil) when no claw402 model is configured — callers should treat this as
|
||||
// "no paid data routing" rather than an error.
|
||||
func (s *AIModelStore) ResolveClaw402WalletKey(userID, preferredModelID string) (string, error) {
|
||||
if preferredModelID != "" {
|
||||
model, err := s.Get(userID, preferredModelID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load selected AI model")
|
||||
}
|
||||
if model.Provider == "claw402" {
|
||||
walletKey := string(model.APIKey)
|
||||
if walletKey == "" {
|
||||
return "", fmt.Errorf("selected claw402 model is missing wallet private key")
|
||||
}
|
||||
return walletKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
models, err := s.List(userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load AI models")
|
||||
}
|
||||
|
||||
for _, model := range models {
|
||||
if model == nil || model.Provider != "claw402" {
|
||||
continue
|
||||
}
|
||||
if walletKey := string(model.APIKey); walletKey != "" {
|
||||
return walletKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *AIModelStore) Create(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
|
||||
model := &AIModel{
|
||||
ID: id,
|
||||
|
||||
+11
-10
@@ -2,14 +2,13 @@ package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"nofx/kernel"
|
||||
"nofx/logger"
|
||||
"nofx/mcp"
|
||||
_ "nofx/mcp/payment"
|
||||
_ "nofx/mcp/provider"
|
||||
"nofx/store"
|
||||
"nofx/wallet"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"nofx/trader/aster"
|
||||
"nofx/trader/binance"
|
||||
"nofx/trader/bitget"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"nofx/trader/kucoin"
|
||||
"nofx/trader/lighter"
|
||||
"nofx/trader/okx"
|
||||
"nofx/wallet"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -90,9 +90,10 @@ type AutoTraderConfig struct {
|
||||
QwenKey string
|
||||
|
||||
// Custom AI API configuration
|
||||
CustomAPIURL string
|
||||
CustomAPIKey string
|
||||
CustomModelName string
|
||||
CustomAPIURL string
|
||||
CustomAPIKey string
|
||||
CustomModelName string
|
||||
Claw402WalletKey string
|
||||
|
||||
// Scan configuration
|
||||
ScanInterval time.Duration // Scan interval (recommended 3 minutes)
|
||||
@@ -148,9 +149,9 @@ type AutoTrader struct {
|
||||
userID string // User ID
|
||||
gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading")
|
||||
claw402WalletAddr string // Claw402 wallet address (derived from private key at start)
|
||||
consecutiveAIFailures int // Consecutive AI call failures
|
||||
safeMode bool // Safe mode: no new positions, protect existing ones
|
||||
safeModeReason string // Why safe mode was activated
|
||||
consecutiveAIFailures int // Consecutive AI call failures
|
||||
safeMode bool // Safe mode: no new positions, protect existing ones
|
||||
safeModeReason string // Why safe mode was activated
|
||||
}
|
||||
|
||||
// NewAutoTrader creates an automatic trader
|
||||
@@ -335,8 +336,8 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
||||
}
|
||||
// Pass claw402 wallet key to strategy engine so nofxos data requests
|
||||
// are routed through claw402 (reuses the same wallet as AI calls)
|
||||
var claw402Key string
|
||||
if config.AIModel == "claw402" && config.CustomAPIKey != "" {
|
||||
claw402Key := config.Claw402WalletKey
|
||||
if claw402Key == "" && config.AIModel == "claw402" && config.CustomAPIKey != "" {
|
||||
claw402Key = config.CustomAPIKey
|
||||
}
|
||||
strategyEngine := kernel.NewStrategyEngine(config.StrategyConfig, claw402Key)
|
||||
|
||||
@@ -324,6 +324,17 @@ func (at *AutoTrader) InitializeGrid() error {
|
||||
|
||||
at.gridState.IsInitialized = true
|
||||
|
||||
// Keep grid orders aligned with the trader's configured cross/isolated mode.
|
||||
if err := at.trader.SetMarginMode(gridConfig.Symbol, at.config.IsCrossMargin); err != nil {
|
||||
logger.Warnf("[Grid] Failed to set margin mode for %s: %v", gridConfig.Symbol, err)
|
||||
} else {
|
||||
marginMode := "cross"
|
||||
if !at.config.IsCrossMargin {
|
||||
marginMode = "isolated"
|
||||
}
|
||||
logger.Infof("[Grid] Margin mode set to %s for %s", marginMode, gridConfig.Symbol)
|
||||
}
|
||||
|
||||
// CRITICAL: Set leverage on exchange before trading
|
||||
if err := at.trader.SetLeverage(gridConfig.Symbol, gridConfig.Leverage); err != nil {
|
||||
logger.Warnf("[Grid] Failed to set leverage %dx on exchange: %v", gridConfig.Leverage, err)
|
||||
|
||||
+11
-2
@@ -41,7 +41,7 @@ type OKXTrader struct {
|
||||
secretKey string
|
||||
passphrase string
|
||||
|
||||
// Margin mode setting
|
||||
// Margin mode setting used for new orders and leverage changes.
|
||||
isCrossMargin bool
|
||||
|
||||
// Position mode: "long_short_mode" (hedge) or "net_mode" (one-way)
|
||||
@@ -121,6 +121,7 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
|
||||
apiKey: apiKey,
|
||||
secretKey: secretKey,
|
||||
passphrase: passphrase,
|
||||
isCrossMargin: true,
|
||||
httpClient: httpClient,
|
||||
cacheDuration: 15 * time.Second,
|
||||
instrumentsCache: make(map[string]*OKXInstrument),
|
||||
@@ -139,10 +140,18 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("✓ OKX trader initialized with position mode: %s", trader.positionMode)
|
||||
logger.Infof("✓ OKX trader initialized with position mode: %s, default margin mode: %s",
|
||||
trader.positionMode, trader.marginMode())
|
||||
return trader
|
||||
}
|
||||
|
||||
func (t *OKXTrader) marginMode() string {
|
||||
if t.isCrossMargin {
|
||||
return "cross"
|
||||
}
|
||||
return "isolated"
|
||||
}
|
||||
|
||||
// detectPositionMode gets current position mode from account config
|
||||
func (t *OKXTrader) detectPositionMode() error {
|
||||
data, err := t.doRequest("GET", okxAccountConfigPath, nil)
|
||||
|
||||
@@ -80,49 +80,42 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetMarginMode sets margin mode
|
||||
// SetMarginMode configures the margin mode (cross/isolated) that will be applied
|
||||
// to all subsequent leverage and order requests for this trader instance.
|
||||
//
|
||||
// OKX V5 unified accounts do not expose a per-symbol mode-switch endpoint that
|
||||
// works reliably — the legacy /api/v5/account/set-isolated-mode endpoint returns
|
||||
// error 51000 ("Parameter isoMode error") when called on a unified account.
|
||||
// Instead, OKX applies the mode per-request via the mgnMode field on
|
||||
// /api/v5/account/set-leverage and via the tdMode field on order placement.
|
||||
//
|
||||
// This implementation therefore stores the configured mode locally and injects it
|
||||
// into each subsequent API request, rather than making an API call here.
|
||||
// NOTE: unlike Binance/Bybit implementations of this interface, no network call
|
||||
// is made — the method only updates local state.
|
||||
func (t *OKXTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
instId := t.convertSymbol(symbol)
|
||||
t.isCrossMargin = isCrossMargin
|
||||
mgnMode := t.marginMode()
|
||||
|
||||
mgnMode := "isolated"
|
||||
if isCrossMargin {
|
||||
mgnMode = "cross"
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"mgnMode": mgnMode,
|
||||
}
|
||||
|
||||
_, err := t.doRequest("POST", "/api/v5/account/set-isolated-mode", body)
|
||||
if err != nil {
|
||||
// Ignore error if already in target mode
|
||||
if strings.Contains(err.Error(), "already") {
|
||||
logger.Infof(" ✓ %s margin mode is already %s", symbol, mgnMode)
|
||||
return nil
|
||||
}
|
||||
// Cannot change when there are positions
|
||||
if strings.Contains(err.Error(), "position") {
|
||||
logger.Infof(" ⚠️ %s has positions, cannot change margin mode", symbol)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ %s margin mode set to %s", symbol, mgnMode)
|
||||
// OKX V5 unified account applies cross/isolated per order via tdMode,
|
||||
// while leverage uses mgnMode on /account/set-leverage.
|
||||
// Persist the configured mode locally so subsequent leverage/order calls use it,
|
||||
// instead of calling the legacy isolated-mode endpoint that returns 51000 errors.
|
||||
logger.Infof(" ✓ %s margin mode configured as %s (applied via tdMode/mgnMode on subsequent requests)", symbol, mgnMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLeverage sets leverage
|
||||
func (t *OKXTrader) SetLeverage(symbol string, leverage int) error {
|
||||
instId := t.convertSymbol(symbol)
|
||||
marginMode := t.marginMode()
|
||||
|
||||
// Set leverage for both long and short
|
||||
for _, posSide := range []string{"long", "short"} {
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"lever": strconv.Itoa(leverage),
|
||||
"mgnMode": "cross",
|
||||
"mgnMode": marginMode,
|
||||
"posSide": posSide,
|
||||
}
|
||||
|
||||
@@ -136,7 +129,7 @@ func (t *OKXTrader) SetLeverage(symbol string, leverage int) error {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ %s leverage set to %dx", symbol, leverage)
|
||||
logger.Infof(" ✓ %s leverage set to %dx (%s)", symbol, leverage, marginMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package okx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"nofx/trader/types"
|
||||
)
|
||||
|
||||
type capturedRequest struct {
|
||||
Method string
|
||||
Path string
|
||||
Body map[string]interface{}
|
||||
}
|
||||
|
||||
type recordingTransport struct {
|
||||
requests []capturedRequest
|
||||
}
|
||||
|
||||
func (rt *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var body map[string]interface{}
|
||||
if req.Body != nil {
|
||||
data, _ := io.ReadAll(req.Body)
|
||||
if len(data) > 0 && strings.HasPrefix(strings.TrimSpace(string(data)), "{") {
|
||||
_ = json.Unmarshal(data, &body)
|
||||
}
|
||||
}
|
||||
|
||||
rt.requests = append(rt.requests, capturedRequest{
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Body: body,
|
||||
})
|
||||
|
||||
response := `{"code":"0","msg":"","data":[]}`
|
||||
switch req.URL.Path {
|
||||
case okxInstrumentsPath:
|
||||
response = `{"code":"0","msg":"","data":[{"instId":"BTC-USDT-SWAP","ctVal":"0.01","ctMult":"1","lotSz":"1","minSz":"1","maxMktSz":"100000","tickSz":"0.1","ctType":"linear"}]}`
|
||||
case okxOrderPath:
|
||||
response = `{"code":"0","msg":"","data":[{"ordId":"123","clOrdId":"abc","sCode":"0","sMsg":""}]}`
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewBufferString(response)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rt *recordingTransport) requestsForPath(path string) []capturedRequest {
|
||||
var matches []capturedRequest
|
||||
for _, req := range rt.requests {
|
||||
if req.Path == path {
|
||||
matches = append(matches, req)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func newTestOKXTrader(rt *recordingTransport, isCrossMargin bool) *OKXTrader {
|
||||
return &OKXTrader{
|
||||
apiKey: "key",
|
||||
secretKey: "secret",
|
||||
passphrase: "pass",
|
||||
isCrossMargin: isCrossMargin,
|
||||
positionMode: "long_short_mode",
|
||||
httpClient: &http.Client{
|
||||
Transport: rt,
|
||||
},
|
||||
cacheDuration: 15 * time.Second,
|
||||
instrumentsCache: make(map[string]*OKXInstrument),
|
||||
instrumentsCacheTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXSetLeverageUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false)
|
||||
|
||||
if err := trader.SetLeverage("BTCUSDT", 5); err != nil {
|
||||
t.Fatalf("SetLeverage failed: %v", err)
|
||||
}
|
||||
|
||||
leverageRequests := rt.requestsForPath(okxLeveragePath)
|
||||
if len(leverageRequests) != 2 {
|
||||
t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
|
||||
}
|
||||
|
||||
for _, req := range leverageRequests {
|
||||
if req.Body["mgnMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated leverage mode, got %#v", req.Body["mgnMode"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXSetMarginModeUpdatesFutureRequestsWithoutAPIError(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, true)
|
||||
|
||||
if err := trader.SetMarginMode("BTCUSDT", false); err != nil {
|
||||
t.Fatalf("SetMarginMode failed: %v", err)
|
||||
}
|
||||
|
||||
if len(rt.requestsForPath("/api/v5/account/set-isolated-mode")) != 0 {
|
||||
t.Fatal("expected SetMarginMode not to call legacy isolated-mode endpoint")
|
||||
}
|
||||
|
||||
if err := trader.SetLeverage("BTCUSDT", 5); err != nil {
|
||||
t.Fatalf("SetLeverage failed: %v", err)
|
||||
}
|
||||
|
||||
leverageRequests := rt.requestsForPath(okxLeveragePath)
|
||||
if len(leverageRequests) != 2 {
|
||||
t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
|
||||
}
|
||||
|
||||
for _, req := range leverageRequests {
|
||||
if req.Body["mgnMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated leverage mode after SetMarginMode(false), got %#v", req.Body["mgnMode"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXOpenLongUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false)
|
||||
|
||||
if _, err := trader.OpenLong("BTCUSDT", 0.1, 5); err != nil {
|
||||
t.Fatalf("OpenLong failed: %v", err)
|
||||
}
|
||||
|
||||
orderRequests := rt.requestsForPath(okxOrderPath)
|
||||
if len(orderRequests) == 0 {
|
||||
t.Fatal("expected at least one order request")
|
||||
}
|
||||
|
||||
lastOrder := orderRequests[len(orderRequests)-1]
|
||||
if lastOrder.Body["tdMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated tdMode, got %#v", lastOrder.Body["tdMode"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXSetStopLossUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false)
|
||||
|
||||
if err := trader.SetStopLoss("BTCUSDT", "LONG", 0.1, 90000); err != nil {
|
||||
t.Fatalf("SetStopLoss failed: %v", err)
|
||||
}
|
||||
|
||||
algoRequests := rt.requestsForPath(okxAlgoOrderPath)
|
||||
if len(algoRequests) != 1 {
|
||||
t.Fatalf("expected 1 algo order request, got %d", len(algoRequests))
|
||||
}
|
||||
|
||||
if algoRequests[0].Body["tdMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated tdMode, got %#v", algoRequests[0].Body["tdMode"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXPlaceLimitOrderUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false)
|
||||
|
||||
_, err := trader.PlaceLimitOrder(&types.LimitOrderRequest{
|
||||
Symbol: "BTCUSDT",
|
||||
Side: "BUY",
|
||||
PositionSide: "LONG",
|
||||
Price: 95000,
|
||||
Quantity: 0.1,
|
||||
Leverage: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PlaceLimitOrder failed: %v", err)
|
||||
}
|
||||
|
||||
orderRequests := rt.requestsForPath(okxOrderPath)
|
||||
if len(orderRequests) != 1 {
|
||||
t.Fatalf("expected 1 limit order request, got %d", len(orderRequests))
|
||||
}
|
||||
|
||||
if orderRequests[0].Body["tdMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated tdMode, got %#v", orderRequests[0].Body["tdMode"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXCrossMarginModeUsedInLeverage(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, true) // cross margin
|
||||
|
||||
if err := trader.SetLeverage("BTCUSDT", 10); err != nil {
|
||||
t.Fatalf("SetLeverage failed: %v", err)
|
||||
}
|
||||
|
||||
leverageRequests := rt.requestsForPath(okxLeveragePath)
|
||||
if len(leverageRequests) != 2 {
|
||||
t.Fatalf("expected 2 leverage requests, got %d", len(leverageRequests))
|
||||
}
|
||||
|
||||
for _, req := range leverageRequests {
|
||||
if req.Body["mgnMode"] != "cross" {
|
||||
t.Fatalf("expected cross leverage mode, got %#v", req.Body["mgnMode"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXOpenShortUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false) // isolated
|
||||
|
||||
if _, err := trader.OpenShort("BTCUSDT", 0.1, 5); err != nil {
|
||||
t.Fatalf("OpenShort failed: %v", err)
|
||||
}
|
||||
|
||||
orderRequests := rt.requestsForPath(okxOrderPath)
|
||||
if len(orderRequests) == 0 {
|
||||
t.Fatal("expected at least one order request")
|
||||
}
|
||||
|
||||
lastOrder := orderRequests[len(orderRequests)-1]
|
||||
if lastOrder.Body["tdMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated tdMode for OpenShort, got %#v", lastOrder.Body["tdMode"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKXSetTakeProfitUsesConfiguredMarginMode(t *testing.T) {
|
||||
rt := &recordingTransport{}
|
||||
trader := newTestOKXTrader(rt, false) // isolated
|
||||
|
||||
if err := trader.SetTakeProfit("BTCUSDT", "LONG", 0.1, 100000); err != nil {
|
||||
t.Fatalf("SetTakeProfit failed: %v", err)
|
||||
}
|
||||
|
||||
algoRequests := rt.requestsForPath(okxAlgoOrderPath)
|
||||
if len(algoRequests) != 1 {
|
||||
t.Fatalf("expected 1 algo order request, got %d", len(algoRequests))
|
||||
}
|
||||
|
||||
if algoRequests[0].Body["tdMode"] != "isolated" {
|
||||
t.Fatalf("expected isolated tdMode for SetTakeProfit, got %#v", algoRequests[0].Body["tdMode"])
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,11 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map
|
||||
szStr = t.formatSize(sz, inst)
|
||||
}
|
||||
|
||||
marginMode := t.marginMode()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"tdMode": "cross",
|
||||
"tdMode": marginMode,
|
||||
"side": "buy",
|
||||
"posSide": "long",
|
||||
"ordType": "market",
|
||||
@@ -118,9 +120,11 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma
|
||||
szStr = t.formatSize(sz, inst)
|
||||
}
|
||||
|
||||
marginMode := t.marginMode()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"tdMode": "cross",
|
||||
"tdMode": marginMode,
|
||||
"side": "sell",
|
||||
"posSide": "short",
|
||||
"ordType": "market",
|
||||
@@ -410,9 +414,11 @@ func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, st
|
||||
posSide = "short"
|
||||
}
|
||||
|
||||
marginMode := t.marginMode()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"tdMode": "cross",
|
||||
"tdMode": marginMode,
|
||||
"side": side,
|
||||
"posSide": posSide,
|
||||
"ordType": "conditional",
|
||||
@@ -453,9 +459,11 @@ func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity,
|
||||
posSide = "short"
|
||||
}
|
||||
|
||||
marginMode := t.marginMode()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"tdMode": "cross",
|
||||
"tdMode": marginMode,
|
||||
"side": side,
|
||||
"posSide": posSide,
|
||||
"ordType": "conditional",
|
||||
@@ -815,9 +823,11 @@ func (t *OKXTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitO
|
||||
posSide = "short"
|
||||
}
|
||||
|
||||
marginMode := t.marginMode()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"instId": instId,
|
||||
"tdMode": "cross",
|
||||
"tdMode": marginMode,
|
||||
"side": side,
|
||||
"posSide": posSide,
|
||||
"ordType": "limit",
|
||||
|
||||
Reference in New Issue
Block a user