Administrator
4 days ago 097ee94cc027baf4970c4fc5daf2aece694d08d9
refactor(gateApi): 重构 Gate API 模块代码结构

- 移除未使用的 GateChannelHandler 导入
- 为 AbstractPrivateChannelHandler 添加详细的 Javadoc 文档
- 优化 subscribe 和 unsubscribe 方法的日志输出格式
- 为 CandlestickChannelHandler 添加完整的类文档注释
- 更新 gateApi-logic.md 文档,添加 GateConfig 和 GateTradeExecutor 组件说明
- 优化 handleMessage 方法的日志输出和错误处理
- 简化方法实现,移除不必要的换行和注释
- 添加 GateConfig 类,提供统一的配置管理和环境切换功能
9 files modified
218 ■■■■■ changed files
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java 91 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateKlineWebSocketClient.java 45 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateTradeExecutor.java 26 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateWebSocketClientManager.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/gateApi-logic.md 16 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/AbstractPrivateChannelHandler.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/CandlestickChannelHandler.java 16 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionClosesChannelHandler.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionsChannelHandler.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -7,7 +7,9 @@
import io.gate.gateapi.api.FuturesApi;
import io.gate.gateapi.models.AccountDetail;
import io.gate.gateapi.models.FuturesAccount;
import io.gate.gateapi.models.FuturesOrder;
import io.gate.gateapi.models.FuturesPriceTrigger;
import io.gate.gateapi.models.Position;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
@@ -76,31 +78,79 @@
            detailClient.setApiKeySecret(config.getApiKey(), config.getApiSecret());
            AccountDetail detail = new AccountApi(detailClient).getAccountDetail();
            this.userId = detail.getUserId();
            log.info("[Gate] uid:{}", userId);
            log.info("[Gate] 用户ID: {}", userId);
            FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE);
            if (!config.getPositionMode().equals(account.getPositionMode())) {
                futuresApi.setPositionMode(SETTLE, config.getPositionMode());
            }
            log.info("[Gate] mode:{} balance:{}", config.getPositionMode(), account.getAvailable());
            log.info("[Gate] 持仓模式: {} 余额: {}", config.getPositionMode(), account.getAvailable());
            futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract());
            log.info("[Gate] old orders cleared");
            log.info("[Gate] 旧条件单已清除");
            closeExistingPositions();
            futuresApi.updateContractPositionLeverageCall(
                    SETTLE, config.getContract(), config.getLeverage(),
                    config.getMarginMode(), config.getPositionMode(), null);
            log.info("[Gate] {}x {}", config.getLeverage(), config.getMarginMode());
            log.info("[Gate] 杠杆: {}x {}", config.getLeverage(), config.getMarginMode());
        } catch (GateApiException e) {
            log.error("[Gate] init fail, label:{}, msg:{}", e.getErrorLabel(), e.getMessage());
            log.error("[Gate] 初始化失败, label:{}, msg:{}", e.getErrorLabel(), e.getMessage());
        } catch (ApiException e) {
            log.error("[Gate] init fail, code:{}", e.getCode());
            log.error("[Gate] 初始化失败, code:{}", e.getCode());
        }
    }
    /**
     * 平掉当前合约所有已有仓位。
     * 策略启动前的准备工作,确保从零持仓状态开始运行。
     */
    private void closeExistingPositions() {
        try {
            java.util.List<Position> positions = futuresApi.listPositions(SETTLE).execute();
            if (positions == null || positions.isEmpty()) {
                log.info("[Gate] 无已有仓位,无需平仓");
                return;
            }
            for (Position pos : positions) {
                if (!config.getContract().equals(pos.getContract())) {
                    continue;
                }
                String sizeStr = pos.getSize();
                long size = Long.parseLong(sizeStr);
                if (size == 0) {
                    continue;
                }
                String closeSize = size > 0 ? String.valueOf(-size) : String.valueOf(Math.abs(size));
                boolean isLong = size > 0;
                Position.ModeEnum mode = pos.getMode();
                FuturesOrder closeOrder = new FuturesOrder();
                closeOrder.setContract(config.getContract());
                closeOrder.setSize(closeSize);
                closeOrder.setPrice("0");
                closeOrder.setTif(FuturesOrder.TifEnum.IOC);
                closeOrder.setReduceOnly(true);
                if (mode != null && mode.getValue() != null && mode.getValue().contains("dual")) {
                    closeOrder.setAutoSize(isLong ? FuturesOrder.AutoSizeEnum.LONG : FuturesOrder.AutoSizeEnum.SHORT);
                }
                closeOrder.setText("t-grid-init-close");
                futuresApi.createFuturesOrder(SETTLE, closeOrder, null);
                log.info("[Gate] 已平掉已有仓位, 方向:{}, sizes:{}, mode:{}", isLong ? "多头" : "空头", sizeStr, mode);
            }
        } catch (GateApiException e) {
            log.warn("[Gate] 平已有仓位失败, label:{}, msg:{}, 可能无仓位", e.getErrorLabel(), e.getMessage());
        } catch (Exception e) {
            log.warn("[Gate] 平已有仓位异常, 可能无仓位", e);
        }
    }
    public void startGrid() {
        if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) {
            log.warn("[Gate] already running, state:{}", state);
            log.warn("[Gate] 策略已在运行中, state:{}", state);
            return;
        }
        state = StrategyState.WAITING_KLINE;
@@ -109,14 +159,14 @@
        shortActive = false;
        longReopenFails = 0;
        shortReopenFails = 0;
        log.info("[Gate] grid started");
        log.info("[Gate] 网格策略已启动");
    }
    public void stopGrid() {
        state = StrategyState.STOPPED;
        executor.cancelAllPriceTriggeredOrders();
        executor.shutdown();
        log.info("[Gate] stopped, pnl:{}", cumulativePnl);
        log.info("[Gate] 策略已停止, 累计盈亏: {}", cumulativePnl);
    }
    /**
@@ -129,7 +179,7 @@
        }
        state = StrategyState.OPENING;
        log.info("[Gate] first kline, opening...");
        log.info("[Gate] 首根K线到达,开始双开...");
        executor.openLong(config.getQuantity(), () -> {
            synchronized (this) {
@@ -148,7 +198,7 @@
                    "close-short-position", "close_short");
            if (longActive && shortActive && state != StrategyState.STOPPED) {
                state = StrategyState.ACTIVE;
                log.info("[Gate] active, long:{}, short:{}, tpL:{}, tpS:{}",
                log.info("[Gate] 已激活, 多头入场:{}, 空头入场:{}, 多头止盈:{}, 空头止盈:{}",
                        longEntryPrice, shortEntryPrice, longTpPrice(), shortTpPrice());
            }
        });
@@ -166,7 +216,7 @@
        if ("dual_long".equals(mode)) {
            if (longActive && !hasPosition) {
                log.info("[Gate] long closed");
                log.info("[Gate] 多头已平仓");
                longActive = false;
                tryReopenLong(0);
            } else if (hasPosition) {
@@ -175,7 +225,7 @@
            }
        } else if ("dual_short".equals(mode)) {
            if (shortActive && !hasPosition) {
                log.info("[Gate] short closed");
                log.info("[Gate] 空头已平仓");
                shortActive = false;
                tryReopenShort(0);
            } else if (hasPosition) {
@@ -193,18 +243,18 @@
            return;
        }
        cumulativePnl = cumulativePnl.add(pnl);
        log.info("[Gate] pnl+{}, side:{}, total:{}", pnl, side, cumulativePnl);
        log.info("[Gate] 盈亏累加:{}, 方向:{}, 累计:{}", pnl, side, cumulativePnl);
        if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) {
            log.info("[Gate] TP reached {}→STOPPED", cumulativePnl);
            log.info("[Gate] 已达止盈目标 {}→已停止", cumulativePnl);
            state = StrategyState.STOPPED;
        } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) {
            log.info("[Gate] loss {}→STOPPED", cumulativePnl);
            log.info("[Gate] 已达亏损上限 {}→已停止", cumulativePnl);
            state = StrategyState.STOPPED;
        }
    }
    // ---- reopen with retry ----
    // ---- 补仓(含重试) ----
    private void tryReopenLong(int retry) {
        if (state == StrategyState.STOPPED) {
@@ -226,7 +276,7 @@
            if (state != StrategyState.STOPPED) {
                state = StrategyState.ACTIVE;
            }
            log.info("[Gate] long reopened, price:{}", longEntryPrice);
            log.info("[Gate] 多头已补开, 价格:{}", longEntryPrice);
        });
    }
@@ -250,11 +300,11 @@
            if (state != StrategyState.STOPPED) {
                state = StrategyState.ACTIVE;
            }
            log.info("[Gate] short reopened, price:{}", shortEntryPrice);
            log.info("[Gate] 空头已补开, 价格:{}", shortEntryPrice);
        });
    }
    // ---- util ----
    // ---- 止盈价格计算 ----
    private BigDecimal longTpPrice() {
        return lastKlinePrice.multiply(BigDecimal.ONE.add(config.getGridRate()))
@@ -266,6 +316,7 @@
                .setScale(1, RoundingMode.HALF_UP);
    }
    /** 对数量取反(开多用正数,开空用负数) */
    private String negate(String qty) {
        return qty.startsWith("-") ? qty.substring(1) : "-" + qty;
    }
src/main/java/com/xcong/excoin/modules/gateApi/GateKlineWebSocketClient.java
@@ -48,6 +48,7 @@
 *
 * @author Administrator
 */
@SuppressWarnings("ALL")
@Slf4j
public class GateKlineWebSocketClient {
@@ -100,7 +101,7 @@
     */
    public void init() {
        if (!isInitialized.compareAndSet(false, true)) {
            log.warn("[WS] already init, skip");
            log.warn("[WS] 已初始化过,跳过重复初始化");
            return;
        }
        connect();
@@ -113,7 +114,7 @@
     * 避免 onClose 回调中的 reconnectWithBackoff 访问已关闭的线程池。
     */
    public void destroy() {
        log.info("[WS] destroy...");
        log.info("[WS] 开始销毁...");
        if (webSocketClient != null && webSocketClient.isOpen()) {
            for (GateChannelHandler handler : channelHandlers) {
@@ -123,7 +124,7 @@
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.warn("[WS] unsubscribe wait interrupted");
                log.warn("[WS] 取消订阅等待被中断");
            }
        }
@@ -132,7 +133,7 @@
                webSocketClient.closeBlocking();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.warn("[WS] close interrupted");
                log.warn("[WS] 关闭连接时被中断");
            }
        }
@@ -146,7 +147,7 @@
        }
        shutdownExecutorGracefully(sharedExecutor);
        log.info("[WS] destroyed");
        log.info("[WS] 销毁完成");
    }
    /**
@@ -155,7 +156,7 @@
     */
    private void connect() {
        if (isConnecting.get() || !isConnecting.compareAndSet(false, true)) {
            log.info("[WS] already connecting");
            log.info("[WS] 连接进行中,跳过重复请求");
            return;
        }
        try {
@@ -168,7 +169,7 @@
            webSocketClient = new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake handshake) {
                    log.info("[WS] connected");
                    log.info("[WS] 连接成功");
                    isConnected.set(true);
                    isConnecting.set(false);
                    if (sharedExecutor != null && !sharedExecutor.isShutdown()) {
@@ -178,7 +179,7 @@
                        }
                        sendPing();
                    } else {
                        log.warn("[WS] shutting down, ignore onOpen");
                        log.warn("[WS] 应用正在关闭,忽略连接成功回调");
                    }
                }
@@ -191,28 +192,28 @@
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    log.warn("[WS] closed, code:{}, reason:{}", code, reason);
                    log.warn("[WS] 连接关闭, code:{}, reason:{}", code, reason);
                    isConnected.set(false);
                    isConnecting.set(false);
                    cancelPongTimeout();
                    if (sharedExecutor != null && !sharedExecutor.isShutdown() && !sharedExecutor.isTerminated()) {
                        sharedExecutor.execute(() -> {
                            try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("[WS] reconnect fail", e); }
                            try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("[WS] 重连失败", e); }
                        });
                    } else {
                        log.warn("[WS] executor closed, no reconnect");
                        log.warn("[WS] 线程池已关闭,不执行重连");
                    }
                }
                @Override
                public void onError(Exception ex) {
                    log.error("[WS] error", ex);
                    log.error("[WS] 发生错误", ex);
                    isConnected.set(false);
                }
            };
            webSocketClient.connect();
        } catch (URISyntaxException e) {
            log.error("[WS] bad uri", e);
            log.error("[WS] URI格式错误", e);
            isConnecting.set(false);
        }
    }
@@ -229,21 +230,21 @@
            String event = response.getString("event");
            if (FUTURES_PONG.equals(channel)) {
                log.debug("[WS] pong received");
                log.debug("[WS] 收到 pong 响应");
                cancelPongTimeout();
                return;
            }
            if ("subscribe".equals(event)) {
                log.info("[WS] {} subscribed: {}", channel, response.getJSONObject("result"));
                log.info("[WS] {} 订阅成功: {}", channel, response.getJSONObject("result"));
                return;
            }
            if ("unsubscribe".equals(event)) {
                log.info("[WS] {} unsubscribed", channel);
                log.info("[WS] {} 取消订阅成功", channel);
                return;
            }
            if ("error".equals(event)) {
                JSONObject error = response.getJSONObject("error");
                log.error("[WS] {} error, code:{}, msg:{}",
                log.error("[WS] {} 错误, code:{}, msg:{}",
                        channel,
                        error != null ? error.getInteger("code") : "N/A",
                        error != null ? error.getString("message") : response.getString("msg"));
@@ -255,7 +256,7 @@
                }
            }
        } catch (Exception e) {
            log.error("[WS] handle msg fail: {}", message, e);
            log.error("[WS] 处理消息失败: {}", message, e);
        }
    }
@@ -286,9 +287,9 @@
                pingMsg.put("time", System.currentTimeMillis() / 1000);
                pingMsg.put("channel", FUTURES_PING);
                webSocketClient.send(pingMsg.toJSONString());
                log.debug("[WS] ping sent");
                log.debug("[WS] 发送 ping 请求");
            }
        } catch (Exception e) { log.warn("[WS] ping fail", e); }
        } catch (Exception e) { log.warn("[WS] 发送 ping 失败", e); }
    }
    private synchronized void cancelPongTimeout() {
@@ -301,9 +302,9 @@
        int attempt = 0, maxAttempts = 3;
        long delayMs = 5000;
        while (attempt < maxAttempts) {
            try { Thread.sleep(delayMs); connect(); return; } catch (Exception e) { log.warn("[WS] reconnect attempt {} fail", attempt + 1, e); delayMs *= 2; attempt++; }
            try { Thread.sleep(delayMs); connect(); return; } catch (Exception e) { log.warn("[WS] 第{}次重连失败", attempt + 1, e); delayMs *= 2; attempt++; }
        }
        log.error("[WS] reconnect exhausted after {} attempts", maxAttempts);
        log.error("[WS] 超过最大重试次数({}),放弃重连", maxAttempts);
    }
    private void shutdownExecutorGracefully(ExecutorService executor) {
src/main/java/com/xcong/excoin/modules/gateApi/GateTradeExecutor.java
@@ -100,8 +100,10 @@
                order.setTif(FuturesOrder.TifEnum.IOC);
                order.setText("t-grid-long");
                FuturesOrder result = futuresApi.createFuturesOrder(SETTLE, order, null);
                log.info("[TradeExec] 开多成功, price:{}, id:{}", result.getFillPrice(), result.getId());
                if (onSuccess != null) onSuccess.run();
                log.info("[TradeExec] 开多成功, 价格:{}, id:{}", result.getFillPrice(), result.getId());
                if (onSuccess != null) {
                    onSuccess.run();
                }
            } catch (Exception e) {
                log.error("[TradeExec] 开多失败", e);
            }
@@ -125,8 +127,10 @@
                order.setTif(FuturesOrder.TifEnum.IOC);
                order.setText("t-grid-short");
                FuturesOrder result = futuresApi.createFuturesOrder(SETTLE, order, null);
                log.info("[TradeExec] 开空成功, price:{}, id:{}", result.getFillPrice(), result.getId());
                if (onSuccess != null) onSuccess.run();
                log.info("[TradeExec] 开空成功, 价格:{}, id:{}", result.getFillPrice(), result.getId());
                if (onSuccess != null) {
                    onSuccess.run();
                }
            } catch (Exception e) {
                log.error("[TradeExec] 开空失败", e);
            }
@@ -151,23 +155,23 @@
            FuturesPriceTriggeredOrder order = buildTriggeredOrder(triggerPrice, rule, orderType, autoSize);
            try {
                TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order);
                log.info("[TradeExec] 止盈单已创建, tp:{}, orderType:{}, id:{}",
                log.info("[TradeExec] 止盈单已创建, 触发价:{}, 类型:{}, id:{}",
                        triggerPrice, orderType, response.getId());
            } catch (GateApiException e) {
                if ("AUTO_USER_EXIST_POSITION_ORDER".equals(e.getErrorLabel())) {
                    log.warn("[TradeExec] 止盈单已存在,清除后重试");
                    log.warn("[TradeExec] 止盈单已存在,清除旧单后重试");
                    try {
                        futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract);
                        TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order);
                        log.info("[TradeExec] 止盈单重试成功, tp:{}, id:{}", triggerPrice, response.getId());
                        log.info("[TradeExec] 止盈单重试成功, 触发价:{}, id:{}", triggerPrice, response.getId());
                    } catch (Exception retryEx) {
                        log.error("[TradeExec] 止盈单重试失败", retryEx);
                    }
                } else {
                    log.error("[TradeExec] 止盈单创建失败, tp:{}", triggerPrice, e);
                    log.error("[TradeExec] 止盈单创建失败, 触发价:{}", triggerPrice, e);
                }
            } catch (Exception e) {
                log.error("[TradeExec] 止盈单创建失败, tp:{}", triggerPrice, e);
                log.error("[TradeExec] 止盈单创建失败, 触发价:{}", triggerPrice, e);
            }
        });
    }
@@ -179,9 +183,9 @@
        executor.execute(() -> {
            try {
                futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract);
                log.info("[TradeExec] 已清除所有止盈止损单");
                log.info("[TradeExec] 已清除所有止盈止损条件单");
            } catch (Exception e) {
                log.error("[TradeExec] 清除止盈止损单失败", e);
                log.error("[TradeExec] 清除止盈止损条件单失败", e);
            }
        });
    }
src/main/java/com/xcong/excoin/modules/gateApi/GateWebSocketClientManager.java
@@ -45,7 +45,7 @@
    @PostConstruct
    public void init() {
        log.info("[GateMgr] init...");
        log.info("[管理器] 开始初始化...");
        try {
            config = GateConfig.builder()
@@ -73,24 +73,24 @@
            wsClient.addChannelHandler(new PositionClosesChannelHandler(
                    config.getApiKey(), config.getApiSecret(), config.getContract(), gridTradeService));
            wsClient.init();
            log.info("[GateMgr] ws connected, 3 handlers registered");
            log.info("[管理器] WS已连接, 已注册 3 个频道处理器");
            gridTradeService.startGrid();
        } catch (Exception e) {
            log.error("[GateMgr] init fail", e);
            log.error("[管理器] 初始化失败", e);
        }
    }
    @PreDestroy
    public void destroy() {
        log.info("[GateMgr] destroy...");
        log.info("[管理器] 开始销毁...");
        if (gridTradeService != null) {
            gridTradeService.stopGrid();
        }
        if (wsClient != null) {
            wsClient.destroy();
        }
        log.info("[GateMgr] destroyed");
        log.info("[管理器] 销毁完成");
    }
    public GateKlineWebSocketClient getKlineWebSocketClient() { return wsClient; }
src/main/java/com/xcong/excoin/modules/gateApi/gateApi-logic.md
@@ -127,7 +127,7 @@
Spring @PostConstruct
  → GateConfig.builder()...build()
  → GateGridTradeService(config)
    → init(): 查ID → 查账户切持仓 → 清旧条件单 → 设杠杆
    → init(): 查ID → 查账户切持仓 → 清旧条件单 → 平已有仓位 → 设杠杆
  → GateKlineWebSocketClient(config.getWsUrl())
    → addChannelHandler x3 → init() → connect()
      → onOpen: handlers依次subscribe → sendPing
@@ -230,15 +230,25 @@
|------|-----|------|
| 获取用户ID | `GET /account/detail` | `AccountApi.getAccountDetail()` |
| 切持仓模式 | `POST /futures/usdt/set_position_mode` | `FuturesApi.setPositionMode()` |
| 查仓位 | `GET /futures/usdt/positions` | `FuturesApi.listPositions()` |
| 市价平仓 | `POST /futures/usdt/orders` (reduce_only, IOC) | `FuturesApi.createFuturesOrder()` |
| 设杠杆 | `POST /futures/usdt/positions/{contract}/leverage` | `FuturesApi.updateContractPositionLeverageCall()` |
| 查账户 | `GET /futures/usdt/accounts` | `FuturesApi.listFuturesAccounts()` |
| 清除条件单 | `DELETE /futures/usdt/price_orders` | `FuturesApi.cancelPriceTriggeredOrderList()` |
| 市价单 | `POST /futures/usdt/orders` (price=0, IOC) | `FuturesApi.createFuturesOrder()` |
| 条件单 | `POST /futures/usdt/price_orders` | `FuturesApi.createPriceTriggeredOrder()` |
---
**初始化顺序** (`init()`):
```
1. 获取用户 ID
2. 查账户 → 如需要切持仓模式
3. 清除旧的止盈止损条件单
4. 市价平掉当前合约所有已有仓位(确保从零持仓开始)
5. 设杠杆
6. 打印账户余额
```
## GateWebSocketClientMain
---
独立 `main()` 方法入口,通过 Spring XML 上下文启动,运行后手动关闭。
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/AbstractPrivateChannelHandler.java
@@ -64,7 +64,7 @@
        long timeSec = System.currentTimeMillis() / 1000;
        JSONObject msg = buildAuthRequest("subscribe", buildUid(), timeSec);
        ws.send(msg.toJSONString());
        log.info("[{}] subscribed, contract:{}", channelName, contract);
        log.info("[{}] 订阅成功, 合约:{}", channelName, contract);
    }
    /**
@@ -87,7 +87,7 @@
        auth.put("SIGN", hs512Sign("unsubscribe", timeSec));
        msg.put("auth", auth);
        ws.send(msg.toJSONString());
        log.info("[{}] unsubscribed, contract:{}", channelName, contract);
        log.info("[{}] 取消订阅成功, 合约:{}", channelName, contract);
    }
    protected GateGridTradeService getGridTradeService() { return gridTradeService; }
@@ -139,7 +139,7 @@
            }
            return hex.toString();
        } catch (Exception e) {
            log.error("[{}] sign fail", channelName, e);
            log.error("[{}] 签名计算失败", channelName, e);
            return "";
        }
    }
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/CandlestickChannelHandler.java
@@ -57,7 +57,7 @@
        payload.add(contract);
        msg.put("payload", payload);
        ws.send(msg.toJSONString());
        log.info("[{}] subscribed, contract:{}, interval:{}", CHANNEL_NAME, contract, INTERVAL);
        log.info("[{}] 订阅成功, 合约:{}, 周期:{}", CHANNEL_NAME, contract, INTERVAL);
    }
    @Override
@@ -71,7 +71,7 @@
        payload.add(contract);
        msg.put("payload", payload);
        ws.send(msg.toJSONString());
        log.info("[{}] unsubscribed", CHANNEL_NAME);
        log.info("[{}] 取消订阅成功", CHANNEL_NAME);
    }
    @Override
@@ -79,20 +79,20 @@
        if (!CHANNEL_NAME.equals(response.getString("channel"))) return false;
        try {
            JSONArray resultArray = response.getJSONArray("result");
            if (resultArray == null || resultArray.isEmpty()) { log.warn("[{}] empty", CHANNEL_NAME); return true; }
            if (resultArray == null || resultArray.isEmpty()) { log.warn("[{}] 数据为空", CHANNEL_NAME); return true; }
            JSONObject data = resultArray.getJSONObject(0);
            BigDecimal closePx = new BigDecimal(data.getString("c"));
            log.info("========== Gate K线 ==========");
            log.info("n:{} t:{} o:{} h:{} l:{} c:{} v:{} a:{} w:{}",
                    data.getString("n"), DateUtil.TimeStampToDateTime(data.getLong("t")),
            log.info("========== Gate K线数据 ==========");
            log.info("名称: {} 时间: {}", data.getString("n"), DateUtil.TimeStampToDateTime(data.getLong("t")));
            log.info("开盘: {} 最高: {} 最低: {} 收盘: {} 成交量: {} 成交额: {} 已完结: {}",
                    data.getString("o"), data.getString("h"), data.getString("l"),
                    data.getString("c"), data.getString("v"), data.getString("a"),
                    data.getBooleanValue("w"));
            log.info("==============================");
            log.info("==================================");
            if (gridTradeService != null) gridTradeService.onKline(closePx);
        } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); }
        } catch (Exception e) { log.error("[{}] 处理数据失败", CHANNEL_NAME, e); }
        return true;
    }
}
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionClosesChannelHandler.java
@@ -45,12 +45,12 @@
                if (!getContract().equals(item.getString("contract"))) continue;
                BigDecimal pnl = new BigDecimal(item.getString("pnl"));
                String side = item.getString("side");
                log.info("[{}] side:{}, pnl:{}", CHANNEL_NAME, side, pnl);
                log.info("[{}] 平仓更新, 方向:{}, 盈亏:{}", CHANNEL_NAME, side, pnl);
                if (getGridTradeService() != null) {
                    getGridTradeService().onPositionClose(getContract(), side, pnl);
                }
            }
        } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); }
        } catch (Exception e) { log.error("[{}] 处理数据失败", CHANNEL_NAME, e); }
        return true;
    }
}
src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionsChannelHandler.java
@@ -47,12 +47,12 @@
                String mode = pos.getString("mode");
                BigDecimal size = new BigDecimal(pos.getString("size"));
                BigDecimal entryPrice = new BigDecimal(pos.getString("entry_price"));
                log.info("[{}] mode:{}, size:{}, entry:{}", CHANNEL_NAME, mode, size, entryPrice);
                log.info("[{}] 持仓更新, 模式:{}, 数量:{}, 入场价:{}", CHANNEL_NAME, mode, size, entryPrice);
                if (getGridTradeService() != null) {
                    getGridTradeService().onPositionUpdate(getContract(), mode, size, entryPrice);
                }
            }
        } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); }
        } catch (Exception e) { log.error("[{}] 处理数据失败", CHANNEL_NAME, e); }
        return true;
    }
}