mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
release: merge dev into main (2026-04-17) (#1484)
* feat(store): prevent deletion of active strategies and update translations (#1461) Co-authored-by: Dean <afei.wuhao@gmail.com> * fix: allow model switching without re-entering wallet key Users with existing wallets could not switch AI models because the "Start Trading" button required a valid private key even when one was already configured. Now the button is enabled when hasExistingWallet is true, and handleSubmit passes an empty key so the backend preserves the existing key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace window.location with useNavigate for routing in auth components (#1470) Co-authored-by: Dean <afei.wuhao@gmail.com> * feat(trader): implement margin mode handling for order and leverage settings * refactor(trader): update SetMarginMode to avoid legacy endpoint and improve logging * feat(api): enhance strategy handling by integrating claw402 wallet key validation Added validation for the claw402 model's wallet key during strategy test runs. If the selected AI model is claw402, the server now checks for a valid wallet key and returns appropriate error messages if it's missing or if the model fails to load. This ensures better error handling and user feedback when working with AI models. * refactor(api): streamline claw402 wallet key retrieval and error handling Refactored the strategy handling logic to encapsulate claw402 wallet key retrieval in a new method, `resolveStrategyDataWalletKey`. This improves code readability and maintains consistent error handling for missing or invalid wallet keys during strategy test runs. The changes enhance the overall robustness of the AI model integration. * feat(trader): add claw402 wallet key resolution for trader configuration Implemented a new method, `resolveTraderDataWalletKey`, to retrieve the claw402 wallet key based on the selected AI model and user ID. This enhancement allows for better integration of the claw402 model within the trader configuration, ensuring that the correct wallet key is used for trading operations. The `AutoTraderConfig` struct has been updated to include the new `Claw402WalletKey` field, improving the overall handling of wallet keys in the trading process. * feat(claw402): preflight USDC balance before AI calls (#1479) * chore: ignore nofx-server build artifact Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(claw402): preflight USDC balance before AI calls Short-circuit claw402 Call/CallWithRequestFull when the wallet balance can't cover the estimated cost of the call, surfacing ErrInsufficientFunds instead of letting x402 fail mid-flight after the sign step. - wallet: cached balance lookup (30s TTL, per-address mutex) to avoid hammering the Base RPC; separate error-returning and display-only APIs so callers can distinguish zero balance from an unreachable RPC. - claw402: 1.5× safety multiplier on the flat per-call estimate, 4.0× for reasoner models whose chain-of-thought cost can blow past the flat rate. Fail-open on RPC errors — x402 still gates actually-empty wallets, and we prefer availability over extra strictness. - shortAddr redacts the wallet in error strings to avoid leaking the full address into telemetry bundles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix(telemetry): report token usage for SSE streaming paths (#1475) * fix(telemetry): report token usage for SSE streaming paths ParseSSEStream already parsed the usage block from SSE chunks but only printed it, so claw402 streaming calls (and native streaming) never fired TokenUsageCallback. GA4 therefore undercounted AI usage on the streaming path. Return the parsed usage from ParseSSEStream and have both callers fire the callback with their own Provider/Model. * chore: drop leftover debug Printf in ParseSSEStream Telemetry is now wired via TokenUsageCallback, so the Printf is redundant noise in the stream path. * fix(gemini): update default model to gemini-3.1-pro Google discontinued gemini-3-pro-preview on 2026-03-26 and directs all callers to gemini-3.1-pro / gemini-3.1-pro-preview. Users on their own API key were getting errors from the native Gemini endpoint because the provider default pointed at the retired ID. Claw402 was unaffected because its route map already used gemini-3.1-pro. Align both the native provider default and the handler's preset list with gemini-3.1-pro so every code path sends a live model ID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract ResolveClaw402WalletKey to store layer and expand OKX margin mode tests - Move duplicated claw402 wallet resolution logic into store.AIModelStore.ResolveClaw402WalletKey - api/strategy.go and manager/trader_manager.go now delegate to the shared method - Add detailed doc comment on OKX SetMarginMode explaining the local-state-only approach and why the legacy /api/v5/account/set-isolated-mode endpoint is not called - Add 3 new test cases: cross mode leverage, OpenShort tdMode, SetTakeProfit tdMode * fix(auth): prevent SetupPage remount from wiping freshly-set auth token (#1481) After #1470 moved routing into react-router, SetupPage is rendered at two different tree positions (top-level guard + /setup Route). When register success flushSync-sets `user`, the top-level guard stops matching and the Route-level SetupPage mounts as a new instance, re-running its cleanup useEffect and removing the auth_token that handlePostAuthSuccess just wrote. Subsequent requests 401 and bounce the user back to /login. Redirect /setup to /welcome when user is already set so SetupPage is never re-mounted during the auth transition. * fix(wallet): handle JSON-RPC null error field in balance query Some RPC implementations return explicit "error": null on success. json.RawMessage deserializes this as the 4-byte literal "null", so len() > 0 was true, causing every balance query to fail with "rpc error: null". Skip the null literal to avoid false positives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: deanokk <wuhao@vergex.trade> Co-authored-by: Dean <afei.wuhao@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: root <root@localhost.localdomain>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
247
trader/okx/trader_margin_mode_test.go
Normal file
247
trader/okx/trader_margin_mode_test.go
Normal file
@@ -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