Administrator
yesterday 6a51f45e6a00b65a9e7b0b0707b453c11311f3ef
feat(okxApi): 添加仓位模式配置和REST客户端功能

- 在OkxConfig中添加posMode字段及相关getter和builder方法
- 添加getRestBaseUrl方法用于获取REST API基础地址
- 在OkxEnums中添加条件订单类型和计划平仓订单类型常量
- 创建OkxRestClient类实现 setPositionMode 和 setLeverage 功能
- 重构OkxGridTradeService中的网格队列生成逻辑,使用固定步长计算
- 提取replenishOwnQueue和transferBetweenQueues方法优化队列补货逻辑
- 更新止盈单逻辑使用条件订单类型并通过algo接口发送
- 在OkxWebSocketClientManager中添加setupAccount方法配置账户参数
- 优化OkxWsUtil中的时间格式化和随机数生成性能
7 files modified
2 files added
40304 ■■■■■ changed files
src/main/java/com/xcong/excoin/modules/okxApi/OkxConfig.java 13 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java 171 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxRestClient.java 125 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java 72 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxWsUtil.java 38 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/enums/OkxEnums.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/okx文档.txt 39846 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OkxOrderInfoChannelHandler.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxApi/OkxConfig.java
@@ -48,6 +48,7 @@
    private final String contract;
    private final String leverage;
    private final String marginMode;
    private final String posMode;
    private final BigDecimal gridRate;
    private final BigDecimal overallTp;
    private final BigDecimal maxLoss;
@@ -65,6 +66,7 @@
        this.contract = builder.contract;
        this.leverage = builder.leverage;
        this.marginMode = builder.marginMode;
        this.posMode = builder.posMode;
        this.gridRate = builder.gridRate;
        this.overallTp = builder.overallTp;
        this.maxLoss = builder.maxLoss;
@@ -90,6 +92,14 @@
                : "wss://wspap.okx.com:8443/ws/v5/private";
    }
    // ==================== REST 地址 ====================
    public String getRestBaseUrl() {
        return isProduction
                ? "https://www.okx.com"
                : "https://www.okx.cab";
    }
    // ==================== 认证信息 ====================
    public String getApiKey() { return apiKey; }
@@ -104,6 +114,7 @@
    // ==================== 持仓配置 ====================
    public String getMarginMode() { return marginMode; }
    public String getPosMode() { return posMode; }
    // ==================== 策略参数 ====================
@@ -137,6 +148,7 @@
        private String contract = "BTC-USDT-SWAP";
        private String leverage = "100";
        private String marginMode = "cross";
        private String posMode = "long_short_mode";
        private BigDecimal gridRate = new BigDecimal("0.0035");
        private BigDecimal overallTp = new BigDecimal("5");
        private BigDecimal maxLoss = new BigDecimal("15");
@@ -153,6 +165,7 @@
        public Builder contract(String contract) { this.contract = contract; return this; }
        public Builder leverage(String leverage) { this.leverage = leverage; return this; }
        public Builder marginMode(String marginMode) { this.marginMode = marginMode; return this; }
        public Builder posMode(String posMode) { this.posMode = posMode; return this; }
        public Builder gridRate(BigDecimal gridRate) { this.gridRate = gridRate; return this; }
        public Builder overallTp(BigDecimal overallTp) { this.overallTp = overallTp; return this; }
        public Builder maxLoss(BigDecimal maxLoss) { this.maxLoss = maxLoss; return this; }
src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
@@ -8,6 +8,7 @@
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@Slf4j
@@ -17,9 +18,7 @@
        WAITING_KLINE, OPENING, ACTIVE, STOPPED
    }
    private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position";
    private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position";
    private final String accountName;
    private final OkxConfig config;
    private final OkxTradeExecutor executor;
@@ -45,6 +44,7 @@
    public OkxGridTradeService(OkxConfig config, String accountName) {
        this.config = config;
        this.accountName = accountName;
        this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName);
    }
@@ -129,7 +129,7 @@
                        log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract());
                    } else {
                        BigDecimal tpPrice = longPriceQueue.get(0);
                        executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_LONG, config.getQuantity());
                        executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_LONG, config.getQuantity());
                        log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity());
                    }
                } else {
@@ -155,7 +155,7 @@
                        log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract());
                    } else {
                        BigDecimal tpPrice = shortPriceQueue.get(0);
                        executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_SHORT, config.getQuantity());
                        executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_SHORT, config.getQuantity());
                        log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity());
                    }
                } else {
@@ -200,22 +200,26 @@
    private void generateShortQueue() {
        shortPriceQueue.clear();
        BigDecimal step = config.getGridRate();
        for (int i = 1; i <= config.getGridQueueSize(); i++) {
            shortPriceQueue.add(shortBaseEntryPrice.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP));
        BigDecimal fixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        BigDecimal prev = shortBaseEntryPrice;
        for (int i = 0; i < config.getGridQueueSize(); i++) {
            prev = prev.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP);
            shortPriceQueue.add(prev);
        }
        shortPriceQueue.sort((a, b) -> b.compareTo(a));
        log.info("[{}] 空队列:{}", config.getContract(), shortPriceQueue);
        log.info("[{}] 空队列:{} 步长:{}", config.getContract(), shortPriceQueue, fixedStep);
    }
    private void generateLongQueue() {
        longPriceQueue.clear();
        BigDecimal step = config.getGridRate();
        for (int i = 1; i <= config.getGridQueueSize(); i++) {
            longPriceQueue.add(longBaseEntryPrice.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP));
        BigDecimal fixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        BigDecimal prev = longBaseEntryPrice;
        for (int i = 0; i < config.getGridQueueSize(); i++) {
            prev = prev.add(fixedStep).setScale(1, RoundingMode.HALF_UP);
            longPriceQueue.add(prev);
        }
        longPriceQueue.sort(BigDecimal::compareTo);
        log.info("[{}] 多队列:{}", config.getContract(), longPriceQueue);
        Collections.sort(longPriceQueue);
        log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep);
    }
    /**
@@ -228,8 +232,8 @@
     * <h3>执行流程</h3>
     * <ol>
     *   <li>匹配队列元素 → 为空则直接返回</li>
     *   <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li>
     *   <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li>
     *   <li>空仓队列:移除 matched 元素,尾部以固定步长(shortBasePrice × gridRate)递减补充新元素</li>
     *   <li>多仓队列:以多仓队列首元素(最小价)为种子,以多仓固定步长递减加入新元素</li>
     *   <li>保证金检查 → 安全则开空一次</li>
     *   <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li>
     * </ol>
@@ -255,38 +259,12 @@
        }
        log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice);
        synchronized (shortPriceQueue) {
            shortPriceQueue.removeAll(matched);
            BigDecimal min = shortPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : shortPriceQueue.get(shortPriceQueue.size() - 1);
            BigDecimal step = config.getGridRate();
            for (int i = 0; i < matched.size(); i++) {
                min = min.multiply(BigDecimal.ONE.subtract(step)).setScale(1, RoundingMode.HALF_UP);
                shortPriceQueue.add(min);
                log.info("[{}] 空队列增加:{}", config.getContract(), min);
            }
            shortPriceQueue.sort((a, b) -> b.compareTo(a));
            log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue);
        }
        BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        replenishOwnQueue(shortPriceQueue, matched, shortFixedStep, true, "空");
        synchronized (longPriceQueue) {
            BigDecimal first = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(0);
            BigDecimal step = config.getGridRate();
            for (int i = 1; i <= matched.size(); i++) {
                BigDecimal elem = first.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP);
                if (longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                        && currentPrice.subtract(longEntryPrice).abs().compareTo(longEntryPrice.multiply(step)) < 0) {
                    log.info("[{}] 多队列跳过(price≈longEntry):{}", config.getContract(), elem);
                    continue;
                }
                longPriceQueue.add(elem);
                log.info("[{}] 多队列增加:{}", config.getContract(), elem);
            }
            longPriceQueue.sort(BigDecimal::compareTo);
            while (longPriceQueue.size() > config.getGridQueueSize()) {
                longPriceQueue.remove(longPriceQueue.size() - 1);
            }
            log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue);
        }
        transferBetweenQueues(longPriceQueue, matched, matched.get(matched.size() - 1),
                longFixedStep, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice);
        if (!isMarginSafe()) {
            log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract());
@@ -296,7 +274,7 @@
                    && longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) {
                    && currentPrice.compareTo(longEntryPrice.subtract(longFixedStep)) < 0) {
                executor.openLong(config.getQuantity(), null, null);
                log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice);
            }
@@ -313,8 +291,8 @@
     * <h3>执行流程</h3>
     * <ol>
     *   <li>匹配队列元素 → 为空则直接返回</li>
     *   <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li>
     *   <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li>
     *   <li>多仓队列:移除 matched 元素,尾部以固定步长(longBasePrice × gridRate)递增补充新元素</li>
     *   <li>空仓队列:以空仓队列首元素(最高价)为种子,以空仓固定步长递增加入新元素</li>
     *   <li>保证金检查 → 安全则开多一次</li>
     *   <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li>
     * </ol>
@@ -340,38 +318,12 @@
        }
        log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice);
        synchronized (longPriceQueue) {
            longPriceQueue.removeAll(matched);
            BigDecimal max = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(longPriceQueue.size() - 1);
            BigDecimal step = config.getGridRate();
            for (int i = 0; i < matched.size(); i++) {
                max = max.multiply(BigDecimal.ONE.add(step)).setScale(1, RoundingMode.HALF_UP);
                longPriceQueue.add(max);
                log.info("[{}] 多队列增加:{}", config.getContract(), max);
            }
            longPriceQueue.sort(BigDecimal::compareTo);
            log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue);
        }
        BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP);
        replenishOwnQueue(longPriceQueue, matched, longFixedStep, false, "多");
        synchronized (shortPriceQueue) {
            BigDecimal first = shortPriceQueue.isEmpty() ? matched.get(0) : shortPriceQueue.get(0);
            BigDecimal step = config.getGridRate();
            for (int i = 1; i <= matched.size(); i++) {
                BigDecimal elem = first.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP);
                if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0
                        && currentPrice.subtract(shortEntryPrice).abs().compareTo(shortEntryPrice.multiply(step)) < 0) {
                    log.info("[{}] 空队列跳过(price≈shortEntry):{}", config.getContract(), elem);
                    continue;
                }
                shortPriceQueue.add(elem);
                log.info("[{}] 空队列增加:{}", config.getContract(), elem);
            }
            shortPriceQueue.sort((a, b) -> b.compareTo(a));
            while (shortPriceQueue.size() > config.getGridQueueSize()) {
                shortPriceQueue.remove(shortPriceQueue.size() - 1);
            }
            log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue);
        }
        transferBetweenQueues(shortPriceQueue, matched, matched.get(0),
                shortFixedStep, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice);
        if (!isMarginSafe()) {
            log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract());
@@ -380,7 +332,7 @@
            if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(shortEntryPrice.multiply(BigDecimal.ONE.add(config.getGridRate()))) > 0
                    && currentPrice.compareTo(shortEntryPrice.add(shortFixedStep)) > 0
                    && currentPrice.compareTo(longEntryPrice) < 0) {
                executor.openShort(config.getQuantity(), null, null);
                log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice);
@@ -388,14 +340,51 @@
        }
    }
    /**
     * 保证金安全阀检查。
     *
     * <p>当前版本通过 wsHandler 推送的账户/持仓数据间接判断保证金状态。
     * 后续可通过 OKX REST API 实时查询保证金占用比例,超 marginRatioLimit 时拒绝开仓。
     *
     * @return true=安全可开仓 / false=保证金超限跳过开仓
     */
    private void replenishOwnQueue(List<BigDecimal> queue, List<BigDecimal> matched, BigDecimal fixedStep,
                                    boolean isShort, String label) {
        synchronized (queue) {
            queue.removeAll(matched);
            BigDecimal tail = queue.isEmpty() ? matched.get(matched.size() - 1) : queue.get(queue.size() - 1);
            Comparator<BigDecimal> comparator = isShort ? (a, b) -> b.compareTo(a) : BigDecimal::compareTo;
            for (int i = 0; i < matched.size(); i++) {
                tail = isShort
                        ? tail.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP)
                        : tail.add(fixedStep).setScale(1, RoundingMode.HALF_UP);
                queue.add(tail);
                log.info("[{}] {}队列增加:{}", config.getContract(), label, tail);
            }
            queue.sort(comparator);
            log.info("[{}] 现{}队列:{}", config.getContract(), label, queue);
        }
    }
    private void transferBetweenQueues(List<BigDecimal> targetQueue, List<BigDecimal> matched,
                                        BigDecimal firstFallback, BigDecimal fixedStep, boolean isShort,
                                        BigDecimal filterEntryPrice, Comparator<BigDecimal> comparator,
                                        String label, BigDecimal currentPrice) {
        synchronized (targetQueue) {
            BigDecimal first = targetQueue.isEmpty() ? firstFallback : targetQueue.get(0);
            for (int i = 1; i <= matched.size(); i++) {
                BigDecimal offset = fixedStep.multiply(BigDecimal.valueOf(i));
                BigDecimal elem = isShort
                        ? first.add(offset).setScale(1, RoundingMode.HALF_UP)
                        : first.subtract(offset).setScale(1, RoundingMode.HALF_UP);
                if (filterEntryPrice.compareTo(BigDecimal.ZERO) > 0
                        && currentPrice.subtract(filterEntryPrice).abs().compareTo(filterEntryPrice.multiply(config.getGridRate())) < 0) {
                    log.info("[{}] {}队列跳过(price≈entry):{}", config.getContract(), label, elem);
                    continue;
                }
                targetQueue.add(elem);
                log.info("[{}] {}队列增加:{}", config.getContract(), label, elem);
            }
            targetQueue.sort(comparator);
            while (targetQueue.size() > config.getGridQueueSize()) {
                targetQueue.remove(targetQueue.size() - 1);
            }
            log.info("[{}] 现{}队列:{}", config.getContract(), label, targetQueue);
        }
    }
    private boolean isMarginSafe() {
        return true;
    }
@@ -423,5 +412,5 @@
    public BigDecimal getCumulativePnl() { return cumulativePnl; }
    public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
    public StrategyState getState() { return state; }
    public String getAccountName() { return config.getContract(); }
    public String getAccountName() { return accountName; }
}
src/main/java/com/xcong/excoin/modules/okxApi/OkxRestClient.java
New file
@@ -0,0 +1,125 @@
package com.xcong.excoin.modules.okxApi;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@Slf4j
public class OkxRestClient {
    private final String baseUrl;
    private final String apiKey;
    private final String secretKey;
    private final String passphrase;
    private final boolean isSimulate;
    public OkxRestClient(String baseUrl, String apiKey, String secretKey, String passphrase, boolean isSimulate) {
        this.baseUrl = baseUrl;
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.passphrase = passphrase;
        this.isSimulate = isSimulate;
    }
    public boolean setPositionMode(String posMode) {
        JSONObject params = new JSONObject();
        params.put("posMode", posMode);
        JSONObject result = post("/api/v5/account/set-position-mode", params);
        return isSuccess(result, "设置持仓方式");
    }
    public boolean setLeverage(String instId, String lever, String mgnMode) {
        JSONObject params = new JSONObject();
        params.put("instId", instId);
        params.put("lever", lever);
        params.put("mgnMode", mgnMode);
        JSONObject result = post("/api/v5/account/set-leverage", params);
        return isSuccess(result, "设置杠杆");
    }
    private JSONObject post(String path, JSONObject body) {
        HttpURLConnection conn = null;
        try {
            String bodyStr = body.toJSONString();
            String timestamp = OkxWsUtil.getIso8601Timestamp();
            String sign = OkxWsUtil.signRest(timestamp, "POST", path, bodyStr, secretKey);
            URL url = new URL(baseUrl + path);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setConnectTimeout(15000);
            conn.setReadTimeout(15000);
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setRequestProperty("OK-ACCESS-KEY", apiKey);
            conn.setRequestProperty("OK-ACCESS-SIGN", sign);
            conn.setRequestProperty("OK-ACCESS-TIMESTAMP", timestamp);
            conn.setRequestProperty("OK-ACCESS-PASSPHRASE", passphrase);
            if (isSimulate) {
                conn.setRequestProperty("x-simulated-trading", "1");
            }
            try (OutputStream os = conn.getOutputStream()) {
                os.write(bodyStr.getBytes(StandardCharsets.UTF_8));
                os.flush();
            }
            int code = conn.getResponseCode();
            String response = readResponse(conn);
            log.info("[REST] POST {} → HTTP {} body:{}", path, code, response);
            return JSON.parseObject(response);
        } catch (Exception e) {
            log.error("[REST] POST {} 失败: {}", path, e.getMessage());
            return null;
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }
    private String readResponse(HttpURLConnection conn) throws Exception {
        StringBuilder sb = new StringBuilder();
        String line;
        if (conn.getResponseCode() >= 200 && conn.getResponseCode() < 300) {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }
            }
        } else {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) {
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }
            }
        }
        return sb.toString();
    }
    private boolean isSuccess(JSONObject result, String label) {
        if (result == null) {
            log.error("[REST] {}失败: 无响应", label);
            return false;
        }
        String code = result.getString("code");
        if ("0".equals(code)) {
            log.info("[REST] {}成功", label);
            return true;
        }
        if ("59000".equals(code)) {
            log.info("[REST] {}已设置(59000:配置未变更)", label);
            return true;
        }
        log.error("[REST] {}失败, code:{}, msg:{}", label, code, result.getString("msg"));
        return false;
    }
}
src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java
@@ -8,7 +8,6 @@
import org.java_websocket.client.WebSocketClient;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
@@ -45,9 +44,6 @@
 */
@Slf4j
public class OkxTradeExecutor {
    private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position";
    private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position";
    private final String contract;
    private final String marginMode;
@@ -152,21 +148,42 @@
        executor.execute(() -> {
            String posSide;
            String side;
            if (ORDER_TYPE_CLOSE_LONG.equals(orderType)) {
            if (OkxEnums.ORDER_TYPE_CLOSE_LONG.equals(orderType)) {
                posSide = OkxEnums.POSSIDE_LONG;
                side = OkxEnums.SIDE_SELL;
            } else {
            } else if (OkxEnums.ORDER_TYPE_CLOSE_SHORT.equals(orderType)) {
                posSide = OkxEnums.POSSIDE_SHORT;
                side = OkxEnums.SIDE_BUY;
            } else {
                log.error("[TradeExec] 未知止盈类型: {}", orderType);
                return;
            }
            try {
                TradeRequestParam param = buildParam(side, posSide, size, OkxEnums.ORDTYPE_LIMIT);
                param.setMarkPx(triggerPrice.toString());
                if (wsClient == null || !wsClient.isOpen()) {
                    log.warn("[TradeExec] WS未连接,跳过止盈单");
                    return;
                }
                if (BigDecimal.ZERO.compareTo(new BigDecimal(size)) >= 0) {
                    log.warn("[TradeExec] 止盈数量<=0,跳过");
                    return;
                }
                List<TradeRequestParam> params = new ArrayList<>();
                params.add(param);
                sendBatchOrders(params);
                JSONArray argsArray = new JSONArray();
                JSONObject args = new JSONObject();
                args.put("instId", contract);
                args.put("tdMode", marginMode);
                args.put("side", side);
                args.put("posSide", posSide);
                args.put("ordType", OkxEnums.ORDTYPE_CONDITIONAL);
                args.put("sz", size);
                args.put("tpTriggerPx", triggerPrice.toString());
                args.put("tpOrdPx", "-1");
                argsArray.add(args);
                String connId = OkxWsUtil.getOrderNum("algo");
                JSONObject msg = OkxWsUtil.buildJsonObject(connId, "order-algo", argsArray);
                wsClient.send(msg.toJSONString());
                log.info("[TradeExec] 止盈单已发送, 触发价:{}, 类型:{}, size:{}", triggerPrice, orderType, size);
            } catch (Exception e) {
                log.error("[TradeExec] 止盈单发送失败, 触发价:{}, size:{}, 立即市价止盈", triggerPrice, size, e);
@@ -229,6 +246,18 @@
        return param;
    }
    private JSONObject buildOrderArgs(TradeRequestParam param) {
        JSONObject args = new JSONObject();
        args.put("instId", param.getInstId());
        args.put("tdMode", param.getTdMode());
        args.put("clOrdId", param.getClOrdId());
        args.put("side", param.getSide());
        args.put("posSide", param.getPosSide());
        args.put("ordType", param.getOrdType());
        args.put("sz", param.getSz());
        return args;
    }
    private void sendOrder(TradeRequestParam param) {
        if (wsClient == null || !wsClient.isOpen()) {
            log.warn("[TradeExec] WS未连接,跳过下单");
@@ -240,15 +269,7 @@
        }
        JSONArray argsArray = new JSONArray();
        JSONObject args = new JSONObject();
        args.put("instId", param.getInstId());
        args.put("tdMode", param.getTdMode());
        args.put("clOrdId", param.getClOrdId());
        args.put("side", param.getSide());
        args.put("posSide", param.getPosSide());
        args.put("ordType", param.getOrdType());
        args.put("sz", param.getSz());
        argsArray.add(args);
        argsArray.add(buildOrderArgs(param));
        String connId = OkxWsUtil.getOrderNum("order");
        JSONObject msg = OkxWsUtil.buildJsonObject(connId, "order", argsArray);
@@ -264,16 +285,7 @@
        JSONArray argsArray = new JSONArray();
        for (TradeRequestParam p : params) {
            JSONObject args = new JSONObject();
            args.put("instId", p.getInstId());
            args.put("tdMode", p.getTdMode());
            args.put("clOrdId", p.getClOrdId());
            args.put("side", p.getSide());
            args.put("posSide", p.getPosSide());
            args.put("ordType", p.getOrdType());
            args.put("sz", p.getSz());
            args.put("px", p.getMarkPx());
            argsArray.add(args);
            argsArray.add(buildOrderArgs(p));
        }
        String connId = OkxWsUtil.getOrderNum(null);
src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java
@@ -32,6 +32,7 @@
                    .contract("BTC-USDT-SWAP")
                    .leverage("100")
                    .marginMode("cross")
                    .posMode("long_short_mode")
                    .gridRate(new BigDecimal("0.0015"))
                    .overallTp(new BigDecimal("5"))
                    .maxLoss(new BigDecimal("15"))
@@ -40,6 +41,8 @@
                    .unrealizedPnlPriceMode(OkxConfig.PnLPriceMode.LAST_PRICE)
                    .isProduction(false)
                    .build();
            setupAccount();
            String accountName = "OKX_API";
            gridTradeService = new OkxGridTradeService(config, accountName);
@@ -85,6 +88,30 @@
        log.info("[管理器] 销毁完成");
    }
    private void setupAccount() {
        log.info("[管理器] 开始配置账户...");
        OkxRestClient restClient = new OkxRestClient(
                config.getRestBaseUrl(),
                config.getApiKey(),
                config.getSecretKey(),
                config.getPassphrase(),
                !config.isProduction());
        boolean posModeOk = restClient.setPositionMode(config.getPosMode());
        if (!posModeOk) {
            log.error("[管理器] 设置持仓方式失败,策略可能无法正常运作");
        }
        boolean leverOk = restClient.setLeverage(
                config.getContract(), config.getLeverage(), config.getMarginMode());
        if (!leverOk) {
            log.error("[管理器] 设置杠杆倍数失败,策略可能无法正常运作");
        }
        log.info("[管理器] 账户配置完成, posMode:{}, leverage:{}, marginMode:{}",
                config.getPosMode(), config.getLeverage(), config.getMarginMode());
    }
    public OkxKlineWebSocketClient getKlineWebSocketClient() { return wsKlineClient; }
    public OkxKlineWebSocketClient getPrivateWebSocketClient() { return wsPrivateClient; }
    public OkxGridTradeService getGridTradeService() { return gridTradeService; }
src/main/java/com/xcong/excoin/modules/okxApi/OkxWsUtil.java
@@ -51,8 +51,16 @@
    // ==================== HMAC-SHA256 签名 ====================
    public static String signWebsocket(String timestamp, String secretKey) {
        return sign(timestamp, "GET", "/users/self/verify", "", secretKey);
    }
    public static String signRest(String timestamp, String method, String path, String body, String secretKey) {
        return sign(timestamp, method.toUpperCase(), path, body, secretKey);
    }
    private static String sign(String timestamp, String method, String path, String body, String secretKey) {
        try {
            String message = String.format("%s%s%s", timestamp, "GET", "/users/self/verify");
            String message = String.format("%s%s%s%s", timestamp, method, path, body);
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec spec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
            mac.init(spec);
@@ -66,9 +74,14 @@
    // ==================== 订单ID ====================
    private static final ThreadLocal<SimpleDateFormat> ORDER_ID_DF =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMddHHmmss"));
    private static final ThreadLocal<Random> RANDOM =
            ThreadLocal.withInitial(Random::new);
    public static String getOrderNum(String prefix) {
        SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
        String dd = df.format(new Date());
        String dd = ORDER_ID_DF.get().format(new Date());
        if (prefix != null && !prefix.isEmpty()) {
            return prefix + dd + getRandomNum(5);
        }
@@ -77,7 +90,7 @@
    private static String getRandomNum(int length) {
        String str = "0123456789";
        Random random = new Random();
        Random random = RANDOM.get();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; ++i) {
            sb.append(str.charAt(random.nextInt(str.length())));
@@ -99,8 +112,21 @@
    // ==================== 日期格式化 ====================
    private static final ThreadLocal<SimpleDateFormat> DATE_TIME_DF =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    private static final ThreadLocal<SimpleDateFormat> ISO8601_DF =
            ThreadLocal.withInitial(() -> {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
                return sdf;
            });
    public static String getIso8601Timestamp() {
        return ISO8601_DF.get().format(new Date());
    }
    public static String timestampToDateTime(long timestamp) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(new Date(timestamp));
        return DATE_TIME_DF.get().format(new Date(timestamp));
    }
}
src/main/java/com/xcong/excoin/modules/okxApi/enums/OkxEnums.java
@@ -15,9 +15,13 @@
    public static final String SIDE_SELL = "sell";
    public static final String ORDTYPE_MARKET = "market";
    public static final String ORDTYPE_LIMIT = "limit";
    public static final String ORDTYPE_CONDITIONAL = "conditional";
    public static final String INSTTYPE_SWAP = "SWAP";
    public static final String MARGIN_CROSS = "cross";
    public static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position";
    public static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position";
    public static final String CHANNEL_CANDLE = "candle1m";
    public static final String CHANNEL_POSITIONS = "positions";
    public static final String CHANNEL_ACCOUNT = "account";
src/main/java/com/xcong/excoin/modules/okxApi/okx文档.txt
New file
Diff too large
src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OkxOrderInfoChannelHandler.java
@@ -98,18 +98,18 @@
                }
                String state = detail.getString("state");
                String accFillSz = detail.getString("accFillSz");
                String fillPnl = detail.getString("fillPnl");
                String pnl = detail.getString("pnl");
                String posSide = detail.getString("posSide");
                String avgPx = detail.getString("avgPx");
                String clOrdId = detail.getString("clOrdId");
                log.info("[{}] 订单, 方向:{}, 状态:{}, 成交量:{}, 均价:{}, 盈亏:{}, 编号:{}",
                        CHANNEL_NAME, posSide, state, accFillSz, avgPx, fillPnl, clOrdId);
                        CHANNEL_NAME, posSide, state, accFillSz, avgPx, pnl, clOrdId);
                if ("filled".equals(state) && accFillSz != null && new BigDecimal(accFillSz).compareTo(BigDecimal.ZERO) > 0) {
                    if (gridTradeService != null) {
                        BigDecimal pnl = fillPnl != null ? new BigDecimal(fillPnl) : BigDecimal.ZERO;
                        gridTradeService.onOrderFilled(posSide, new BigDecimal(accFillSz), pnl);
                        BigDecimal pnlVal = pnl != null ? new BigDecimal(pnl) : BigDecimal.ZERO;
                        gridTradeService.onOrderFilled(posSide, new BigDecimal(accFillSz), pnlVal);
                    }
                }
            }