src/main/java/com/xcong/excoin/modules/gateApi/Example.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateKlineWebSocketClient.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateTradeExecutor.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateWebSocketClientMain.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/GateWebSocketClientManager.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/celue.outBinary files differ
src/main/java/com/xcong/excoin/modules/gateApi/gate-websocket.txt
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/gateApi-logic.md
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/AbstractPrivateChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/GateChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/CandlestickChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionClosesChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/gateApi/wsHandler/handler/PositionsChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
@@ -17,9 +17,11 @@ 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 OkxConfig config; private final OkxTradeExecutor executor; private final String accountName; private volatile StrategyState state = StrategyState.WAITING_KLINE; @@ -41,21 +43,18 @@ private volatile BigDecimal longPositionSize = BigDecimal.ZERO; private volatile BigDecimal shortPositionSize = BigDecimal.ZERO; private volatile WebSocketClient wsClient; public OkxGridTradeService(OkxConfig config, String accountName) { this.config = config; this.accountName = accountName; this.executor = new OkxTradeExecutor(config, accountName); this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName); } public void setWebSocketClient(WebSocketClient wsClient) { this.wsClient = wsClient; this.executor.setWebSocketClient(wsClient); } public void startGrid() { if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { log.warn("[{}] 策略已在运行中, state:{}", accountName, state); log.warn("[{}] 策略已在运行中, state:{}", config.getContract(), state); return; } state = StrategyState.WAITING_KLINE; @@ -71,19 +70,17 @@ shortActive = false; shortPriceQueue.clear(); longPriceQueue.clear(); log.info("[{}] 网格策略已启动", accountName); log.info("[{}] 网格策略已启动", config.getContract()); } public void stopGrid() { state = StrategyState.STOPPED; executor.cancelAllPriceTriggeredOrders(); executor.shutdown(); log.info("[{}] 策略已停止, 累计盈亏: {}", accountName, cumulativePnl); log.info("[{}] 策略已停止, 累计盈亏: {}", config.getContract(), cumulativePnl); } public void onKline(BigDecimal closePrice) { if (wsClient == null || !wsClient.isOpen()) { return; } lastKlinePrice = closePrice; updateUnrealizedPnl(); if (state == StrategyState.STOPPED) { @@ -92,9 +89,13 @@ if (state == StrategyState.WAITING_KLINE) { state = StrategyState.OPENING; log.info("[{}] 首根K线到达,开基底仓位...", accountName); executor.openLong(wsClient, () -> log.info("[{}] 基底多单已发送", accountName)); executor.openShort(wsClient, () -> log.info("[{}] 基底空单已发送", accountName)); log.info("[{}] 首根K线到达,开基底仓位...", config.getContract()); executor.openLong(config.getQuantity(), () -> { log.info("[{}] 基底多单已提交", config.getContract()); }, null); executor.openShort(config.getQuantity(), () -> { log.info("[{}] 基底空单已提交", config.getContract()); }, null); return; } @@ -120,13 +121,17 @@ longPositionSize = size; longBaseEntryPrice = entryPrice; baseLongOpened = true; log.info("[{}] 基底多成交价: {}", accountName, longBaseEntryPrice); log.info("[{}] 基底多成交价: {}", config.getContract(), longBaseEntryPrice); tryGenerateQueues(); } else if (size.compareTo(longPositionSize) > 0) { longPositionSize = size; BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.add(config.getGridRate())).setScale(1, RoundingMode.HALF_UP); executor.placeTakeProfit(wsClient, OkxEnums.POSSIDE_LONG, tpPrice, config.getQuantity()); log.info("[{}] 多单止盈已设, entry:{}, tp:{}", accountName, entryPrice, tpPrice); if (longPriceQueue.isEmpty()) { log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract()); } else { BigDecimal tpPrice = longPriceQueue.get(0); executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_LONG, config.getQuantity()); log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); } } else { longPositionSize = size; } @@ -142,13 +147,17 @@ shortPositionSize = size; shortBaseEntryPrice = entryPrice; baseShortOpened = true; log.info("[{}] 基底空成交价: {}", accountName, shortBaseEntryPrice); log.info("[{}] 基底空成交价: {}", config.getContract(), shortBaseEntryPrice); tryGenerateQueues(); } else if (size.compareTo(shortPositionSize) > 0) { shortPositionSize = size; BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate())).setScale(1, RoundingMode.HALF_UP); executor.placeTakeProfit(wsClient, OkxEnums.POSSIDE_SHORT, tpPrice, config.getQuantity()); log.info("[{}] 空单止盈已设, entry:{}, tp:{}", accountName, entryPrice, tpPrice); if (shortPriceQueue.isEmpty()) { log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract()); } else { BigDecimal tpPrice = shortPriceQueue.get(0); executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); } } else { shortPositionSize = size; } @@ -164,13 +173,13 @@ return; } cumulativePnl = cumulativePnl.add(pnl); log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", accountName, pnl, posSide, cumulativePnl); log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", config.getContract(), pnl, posSide, cumulativePnl); if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { log.info("[{}] 已达止盈目标 {}→已停止", accountName, cumulativePnl); log.info("[{}] 已达止盈目标 {}→已停止", config.getContract(), cumulativePnl); state = StrategyState.STOPPED; } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) { log.info("[{}] 已达亏损上限 {}→已停止", accountName, cumulativePnl); log.info("[{}] 已达亏损上限 {}→已停止", config.getContract(), cumulativePnl); state = StrategyState.STOPPED; } } @@ -181,7 +190,7 @@ generateLongQueue(); state = StrategyState.ACTIVE; log.info("[{}] 网格队列已生成, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活", accountName, config.getContract(), shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(0), shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(shortPriceQueue.size() - 1), longPriceQueue.isEmpty() ? "N/A" : longPriceQueue.get(0), @@ -196,7 +205,7 @@ shortPriceQueue.add(shortBaseEntryPrice.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); } shortPriceQueue.sort((a, b) -> b.compareTo(a)); log.info("[{}] 空队列:{}", accountName, shortPriceQueue); log.info("[{}] 空队列:{}", config.getContract(), shortPriceQueue); } private void generateLongQueue() { @@ -206,7 +215,7 @@ longPriceQueue.add(longBaseEntryPrice.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); } longPriceQueue.sort(BigDecimal::compareTo); log.info("[{}] 多队列:{}", accountName, longPriceQueue); log.info("[{}] 多队列:{}", config.getContract(), longPriceQueue); } /** @@ -219,10 +228,10 @@ * <h3>执行流程</h3> * <ol> * <li>匹配队列元素 → 为空则直接返回</li> * <li>保证金检查 → 安全则开空一次</li> * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li> * <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li> * <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li> * <li>保证金检查 → 安全则开空一次</li> * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li> * </ol> * * <h3>多仓队列转移过滤</h3> @@ -239,23 +248,12 @@ } } } log.info("[{}] 原空队列:{}", config.getContract(), shortPriceQueue); if (matched.isEmpty()) { log.info("[{}] 空仓队列未触发, 当前价:{}", config.getContract(), currentPrice); return; } log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", accountName, matched.size(), currentPrice); if (!isMarginSafe()) { log.warn("[{}] 保证金超限,跳过空单开仓", accountName); } else { executor.openShort(wsClient, null); if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(shortEntryPrice) > 0 && currentPrice.compareTo(shortEntryPrice) > 0 && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) { executor.openLong(wsClient, null); log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", accountName, currentPrice); } } log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); synchronized (shortPriceQueue) { shortPriceQueue.removeAll(matched); @@ -264,8 +262,10 @@ 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); } synchronized (longPriceQueue) { @@ -275,13 +275,30 @@ 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); } if (!isMarginSafe()) { log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract()); } else { executor.openShort(config.getQuantity(), null, null); if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(shortEntryPrice) > 0 && currentPrice.compareTo(shortEntryPrice) > 0 && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) { executor.openLong(config.getQuantity(), null, null); log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice); } } } @@ -296,10 +313,10 @@ * <h3>执行流程</h3> * <ol> * <li>匹配队列元素 → 为空则直接返回</li> * <li>保证金检查 → 安全则开多一次</li> * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li> * <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li> * <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li> * <li>保证金检查 → 安全则开多一次</li> * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li> * </ol> * * <h3>空仓队列转移过滤</h3> @@ -316,23 +333,12 @@ } } } log.info("[{}] 原多队列:{}", config.getContract(), longPriceQueue); if (matched.isEmpty()) { log.info("[{}] 多仓队列未触发, 当前价:{}", config.getContract(), currentPrice); return; } log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", accountName, matched.size(), currentPrice); if (!isMarginSafe()) { log.warn("[{}] 保证金超限,跳过多单开仓", accountName); } else { executor.openLong(wsClient, null); 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(longEntryPrice) < 0) { executor.openShort(wsClient, null); log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", accountName, currentPrice); } } log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); synchronized (longPriceQueue) { longPriceQueue.removeAll(matched); @@ -341,8 +347,10 @@ 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); } synchronized (shortPriceQueue) { @@ -352,13 +360,30 @@ 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); } if (!isMarginSafe()) { log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract()); } else { executor.openLong(config.getQuantity(), null, null); 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(longEntryPrice) < 0) { executor.openShort(config.getQuantity(), null, null); log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice); } } } @@ -390,6 +415,7 @@ shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price)); } unrealizedPnl = longPnl.add(shortPnl); log.info("[{}] 未实现盈亏: {}", config.getContract(), unrealizedPnl); } public BigDecimal getLastKlinePrice() { return lastKlinePrice; } @@ -397,5 +423,5 @@ public BigDecimal getCumulativePnl() { return cumulativePnl; } public BigDecimal getUnrealizedPnl() { return unrealizedPnl; } public StrategyState getState() { return state; } public String getAccountName() { return accountName; } public String getAccountName() { return config.getContract(); } } src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java
@@ -22,26 +22,44 @@ * WebSocket 消息在回调线程中处理。下单操作参数构建等逻辑 * 提交到独立线程池异步执行,避免阻塞 WS 回调线程。 * * <h3>回调设计</h3> * 每个下单方法接受 onSuccess/onFailure 两个 Runnable。 * 基底开仓时 onSuccess 用于标记基底已开,网格触发时通常为 null(成交状态由仓位推送驱动)。 * * <h3>线程模型</h3> * <ul> * <li><b>单线程</b>:保证下单顺序(开多→开空→止盈单),避免并发竞争</li> * <li><b>有界队列 64</b>:防止堆积</li> * <li><b>有界队列 64</b>:防止堆积。极端行情下最多累积 64 个任务</li> * <li><b>CallerRunsPolicy</b>:队列满时由提交线程直接同步执行,形成自然背压</li> * <li><b>allowCoreThreadTimeOut</b>:60s 空闲后线程回收</li> * <li><b>allowCoreThreadTimeOut</b>:60s 空闲后线程回收,不浪费资源</li> * </ul> * * <h3>调用链</h3> * <pre> * OkxGridTradeService.onKline → executor.openLong/openShort (基底双开 + 网格触发) * OkxGridTradeService.onPositionUpdate → executor.placeTakeProfit (开仓成交后设止盈) * OkxGridTradeService.stopGrid → executor.cancelAllPriceTriggeredOrders * </pre> * * @author Administrator */ @Slf4j public class OkxTradeExecutor { private final OkxConfig config; 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; private final String accountName; private volatile WebSocketClient wsClient; private final ExecutorService executor; public OkxTradeExecutor(OkxConfig config, String accountName) { this.config = config; public OkxTradeExecutor(String contract, String marginMode, String accountName) { this.contract = contract; this.marginMode = marginMode; this.accountName = accountName; this.executor = new ThreadPoolExecutor( 1, 1, @@ -57,6 +75,10 @@ ((ThreadPoolExecutor) executor).allowCoreThreadTimeOut(true); } public void setWebSocketClient(WebSocketClient wsClient) { this.wsClient = wsClient; } public void shutdown() { executor.shutdown(); try { @@ -67,19 +89,33 @@ } } public void openLong(WebSocketClient wsClient, Runnable onSuccess) { openPosition(wsClient, config.getQuantity(), OkxEnums.POSSIDE_LONG, OkxEnums.SIDE_BUY, "开多", onSuccess); /** * 异步市价开多。quantity 为正数(如 "1")。 * * @param quantity 开仓张数(正数) * @param onSuccess 成交成功回调(可为 null) * @param onFailure 成交失败回调(可为 null) */ public void openLong(String quantity, Runnable onSuccess, Runnable onFailure) { openPosition(quantity, OkxEnums.POSSIDE_LONG, OkxEnums.SIDE_BUY, "开多", onSuccess, onFailure); } public void openShort(WebSocketClient wsClient, Runnable onSuccess) { openPosition(wsClient, config.getQuantity(), OkxEnums.POSSIDE_SHORT, OkxEnums.SIDE_SELL, "开空", onSuccess); /** * 异步市价开空。quantity 为正数(如 "1")。 * * @param quantity 开仓张数(正数) * @param onSuccess 成交成功回调(可为 null) * @param onFailure 成交失败回调(可为 null) */ public void openShort(String quantity, Runnable onSuccess, Runnable onFailure) { openPosition(quantity, OkxEnums.POSSIDE_SHORT, OkxEnums.SIDE_SELL, "开空", onSuccess, onFailure); } private void openPosition(WebSocketClient wsClient, String sz, String posSide, String side, String label, Runnable onSuccess) { private void openPosition(String sz, String posSide, String side, String label, Runnable onSuccess, Runnable onFailure) { executor.execute(() -> { try { TradeRequestParam param = buildParam(side, posSide, sz, OkxEnums.ORDTYPE_MARKET); sendOrder(wsClient, param); sendOrder(param); log.info("[TradeExec] {}已发送, 方向:{}, 数量:{}", label, posSide, sz); if (onSuccess != null) { @@ -87,24 +123,94 @@ } } catch (Exception e) { log.error("[TradeExec] {}发送失败", label, e); if (onFailure != null) { onFailure.run(); } } }); } public void placeTakeProfit(WebSocketClient wsClient, String posSide, BigDecimal triggerPrice, String size) { /** * 异步创建止盈条件单(仓位计划止盈止损)。 * * <p>通过 OKX WebSocket 发送 algo order(conditional order),服务器监控价格, * 达到触发价后自动平指定张数。 * * <h3>orderType 说明</h3> * <ul> * <li>plan-close-long-position:平多仓,posSide=long, side=sell</li> * <li>plan-close-short-position:平空仓,posSide=short, side=buy</li> * </ul> * * <p>止盈单创建失败时,立即市价平仓兜底(marketClose)。 * * @param triggerPrice 触发价格 * @param orderType stop 类型(plan-close-long-position / plan-close-short-position) * @param size 平仓张数(正数) */ public void placeTakeProfit(BigDecimal triggerPrice, String orderType, String size) { executor.execute(() -> { try { String side = OkxEnums.POSSIDE_LONG.equals(posSide) ? OkxEnums.SIDE_SELL : OkxEnums.SIDE_BUY; String posSide; String side; if (ORDER_TYPE_CLOSE_LONG.equals(orderType)) { posSide = OkxEnums.POSSIDE_LONG; side = OkxEnums.SIDE_SELL; } else { posSide = OkxEnums.POSSIDE_SHORT; side = OkxEnums.SIDE_BUY; } try { TradeRequestParam param = buildParam(side, posSide, size, OkxEnums.ORDTYPE_LIMIT); param.setMarkPx(triggerPrice.toString()); List<TradeRequestParam> params = new ArrayList<>(); params.add(param); sendBatchOrders(wsClient, params); log.info("[TradeExec] 止盈单已发送, 方向:{}, 触发价:{}, 数量:{}", posSide, triggerPrice, size); sendBatchOrders(params); log.info("[TradeExec] 止盈单已发送, 触发价:{}, 类型:{}, size:{}", triggerPrice, orderType, size); } catch (Exception e) { log.error("[TradeExec] 止盈单发送失败", e); log.error("[TradeExec] 止盈单发送失败, 触发价:{}, size:{}, 立即市价止盈", triggerPrice, size, e); marketClose(side, posSide, size); } }); } /** * 市价止盈:在止盈条件单创建失败时立即市价平仓。 */ private void marketClose(String side, String posSide, String size) { try { TradeRequestParam param = buildParam(side, posSide, size, OkxEnums.ORDTYPE_MARKET); param.setTradeType("3"); sendOrder(param); log.info("[TradeExec] 市价止盈已发送, posSide:{}, size:{}", posSide, size); } catch (Exception e) { log.error("[TradeExec] 市价止盈也失败, posSide:{}, size:{}", posSide, size, e); } } /** * 异步清除指定合约的所有止盈止损条件单。 */ public void cancelAllPriceTriggeredOrders() { executor.execute(() -> { try { if (wsClient == null || !wsClient.isOpen()) { log.warn("[TradeExec] WS未连接,跳过撤销条件单"); return; } JSONArray argsArray = new JSONArray(); JSONObject args = new JSONObject(); args.put("instId", contract); args.put("algoOrdType", "conditional"); argsArray.add(args); String connId = OkxWsUtil.getOrderNum("cancel"); JSONObject msg = OkxWsUtil.buildJsonObject(connId, "cancel-algos", argsArray); wsClient.send(msg.toJSONString()); log.info("[TradeExec] 已发送撤销所有条件单请求"); } catch (Exception e) { log.error("[TradeExec] 撤销条件单失败", e); } }); } @@ -112,8 +218,8 @@ private TradeRequestParam buildParam(String side, String posSide, String sz, String ordType) { TradeRequestParam param = new TradeRequestParam(); param.setAccountName(accountName); param.setInstId(config.getContract()); param.setTdMode(config.getMarginMode()); param.setInstId(contract); param.setTdMode(marginMode); param.setPosSide(posSide); param.setOrdType(ordType); param.setSide(side); @@ -123,7 +229,7 @@ return param; } private void sendOrder(WebSocketClient wsClient, TradeRequestParam param) { private void sendOrder(TradeRequestParam param) { if (wsClient == null || !wsClient.isOpen()) { log.warn("[TradeExec] WS未连接,跳过下单"); return; @@ -150,7 +256,7 @@ log.info("[TradeExec] 发送下单: side={}, sz={}", param.getSide(), param.getSz()); } private void sendBatchOrders(WebSocketClient wsClient, List<TradeRequestParam> params) { private void sendBatchOrders(List<TradeRequestParam> params) { if (wsClient == null || !wsClient.isOpen() || params == null || params.isEmpty()) { log.warn("[TradeExec] WS未连接或参数为空,跳过批量下单"); return; src/main/java/com/xcong/excoin/modules/okxApi/okxApi-logic.md
New file @@ -0,0 +1,724 @@ # OKX API 网格交易策略 — 逻辑文档 --- ## 目录 1. [整体架构](#1-整体架构) 2. [配置层:OkxConfig](#2-配置层okxconfig) 3. [基础设施层](#3-基础设施层) - [3.1 OkxEnums — 常量定义](#31-okxenums--常量定义) - [3.2 OkxWsUtil — WebSocket 工具类](#32-okxwsutil--websocket-工具类) - [3.3 TradeRequestParam — 下单参数](#33-traderequestparam--下单参数) 4. [WebSocket 通信层](#4-websocket-通信层) - [4.1 OkxWebSocketClientManager — 入口管理器](#41-okxwebsocketclientmanager--入口管理器) - [4.2 OkxKlineWebSocketClient — WS 连接客户端](#42-okxklinewebsocketclient--ws-连接客户端) 5. [频道处理器层](#5-频道处理器层) - [5.1 OkxChannelHandler — 处理器接口](#51-okxchannelhandler--处理器接口) - [5.2 OkxCandlestickChannelHandler — K线频道](#52-okxcandlestickchannelhandler--k线频道) - [5.3 OkxPositionsChannelHandler — 持仓频道](#53-okxpositionschannelhandler--持仓频道) - [5.4 OkxAccountChannelHandler — 账户频道](#54-okxaccountchannelhandler--账户频道) - [5.5 OkxOrderInfoChannelHandler — 订单成交频道](#55-okxorderinfochannelhandler--订单成交频道) 6. [策略执行层](#6-策略执行层) - [6.1 OkxTradeExecutor — 异步下单执行器](#61-okxtradeexecutor--异步下单执行器) - [6.2 OkxGridTradeService — 网格策略核心](#62-okxgridtradeservice--网格策略核心) 7. [独立启动类:OkxWebSocketClientMain](#7-独立启动类okxwebsocketclientmain) 8. [完整调用链](#8-完整调用链) 9. [与 Gate API 的差异对比](#9-与-gate-api-的差异对比) --- ## 1. 整体架构 ``` ┌──────────────────────────────────────────────────────────────┐ │ OkxWebSocketClientManager (Spring @Component) │ │ · 读取配置 · 组装所有组件 · 启动 WS 连接 · 生命周期管理 │ └──────────────────────┬───────────────────────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ OkxConfig OkxGridTradeService OkxKlineWebSocketClient (Builder配置) (策略核心) (WS连接客户端) │ │ │ ┌────────┴────────┐ │ │ 4 个频道处理器 │ │ ├─────────────────┤ │ │ K线 | 持仓 │ │ │ 账户 | 订单成交 │ │ └────────┬────────┘ │ │ ▼ │ OkxTradeExecutor │ (异步下单线程池) ◄────────────────┘ │ ▼ 通过 WS 发送下单 JSON ``` **核心数据流**: ``` K线推送 → OkxGridTradeService.onKline() → 匹配网格队列 → OkxTradeExecutor 异步下单 持仓推送 → OkxGridTradeService.onPositionUpdate() → 识别基底成交 → 设置止盈单 → 队列就绪 → 激活策略 订单推送 → OkxGridTradeService.onOrderFilled() → 累计盈亏跟踪 → 达标/超限自动停止 ``` **设计原则**: - **包自包含**:`okxApi` 包不依赖任何其他业务包(`okxNewPrice`、`gateApi`、`newPrice`、`blackchain` 等) - **WS 回调不阻塞**:所有下单操作通过 `OkxTradeExecutor` 单线程池异步执行 - **状态机驱动**:策略状态(`WAITING_KLINE → OPENING → ACTIVE → STOPPED`)严格控制执行流程 --- ## 2. 配置层:OkxConfig ``` 文件:OkxConfig.java ``` 使用 **Builder 模式**构造配置对象,不可变设计,所有字段 `private final`。 ### 配置字段 | 分组 | 字段 | 说明 | |------|------|------| | **API 密钥** | `apiKey` | OKX API Key | | | `secretKey` | OKX Secret Key | | | `passphrase` | OKX Passphrase | | **合约参数** | `contract` | 合约品种(如 `BTC-USDT-SWAP`) | | | `marginMode` | 保证金模式(`cross` 全仓 / `isolated` 逐仓) | | | `tickSz` | 价格精度 | | | `contractMultiplier` | 合约乘数(用于盈亏计算) | | | `leverage` | 杠杆倍数 | | **策略参数** | `quantity` | 每次开仓张数 | | | `gridRate` | 网格间距比例(如 0.01 = 1%) | | | `gridQueueSize` | 网格队列长度 | | | `marginRatioLimit` | 保证金占用比例上限 | | | `overallTp` | 全局止盈目标(累计盈亏 ≥ 此值停止) | | | `maxLoss` | 最大亏损限制(累计盈亏 ≤ 此值停止) | | **环境** | `isProduction` | 是否生产环境(决定 WS URL 域名) | ### Builder 方法链 ```java OkxConfig config = OkxConfig.builder() .apiKey("xxx") .secretKey("xxx") .passphrase("xxx") .contract("BTC-USDT-SWAP") .marginMode("cross") .leverage(1) .quantity("1") .gridRate(new BigDecimal("0.01")) .gridQueueSize(10) .overallTp(new BigDecimal("100")) .isProduction(false) .build(); ``` --- ## 3. 基础设施层 ### 3.1 OkxEnums — 常量定义 ``` 文件:enums/OkxEnums.java ``` 集中管理所有 OKX API 相关字符串常量,替代外部 `CoinEnums` 依赖。 | 常量 | 值 | 用途 | |------|-----|------| | `INSTTYPE_SPOT` | `SPOT` | 现货 | | `INSTTYPE_SWAP` | `SWAP` | 永续合约 | | `POSSIDE_LONG` | `long` | 多仓方向 | | `POSSIDE_SHORT` | `short` | 空仓方向 | | `SIDE_BUY` | `buy` | 买入 | | `SIDE_SELL` | `sell` | 卖出 | | `ORDTYPE_MARKET` | `market` | 市价单 | | `ORDTYPE_LIMIT` | `limit` | 限价单 | | `CHANNEL_POSITIONS` | `positions` | 持仓频道 | | `CHANNEL_CANDLE` | `candle` + 周期 | K线频道 | | `CHANNEL_ACCOUNT` | `account` | 账户频道 | | `CHANNEL_ORDERS` | `orders` | 订单频道 | | `CHANNEL_ORDERS_ALGO` | `orders-algo` | 策略委托频道 | --- ### 3.2 OkxWsUtil — WebSocket 工具类 ``` 文件:OkxWsUtil.java ``` 替代外部 `SSLConfig`、`SignUtils`、`WsParamBuild`、`DateUtil` 等依赖,提供以下静态方法: | 方法 | 用途 | |------|------| | `configureSSL(wsClient)` | 为 `WebSocketClient` 配置 SSL(跳过证书验证,仅测试环境) | | `generateSignature(timestamp, method, requestPath, body, secretKey)` | OKX 签名算法:HMAC-SHA256 + Base64 | | `getOrderNum(side)` | 生成唯一订单 ID(时间戳 + 随机数 + side) | | `timestampToDateTime(timestamp)` | 毫秒时间戳 → `yyyy/MM/dd HH:mm:ss` 格式 | | `timestampToDateToString(timestamp)` | 毫秒时间戳 → `yyyy/MM/dd` 格式 | | `buildJsonObject(connId, channel, args)` | 构建 WS 请求 JSON 对象 | | `buildLoginParam(okxConfig)` | 构建登录认证参数(sign + timestamp) | **签名算法**: ``` sign = Base64(HMAC-SHA256(timestamp + "GET" + requestPath + body, secretKey)) ``` --- ### 3.3 TradeRequestParam — 下单参数 ``` 文件:param/TradeRequestParam.java ``` 纯 POJO,替代外部 `TradeRequestParam` 依赖。 | 字段 | 说明 | |------|------| | `accountName` | 账户标识 | | `instId` | 合约 ID | | `tdMode` | 保证金模式(cross/isolated) | | `posSide` | 持仓方向(long/short) | | `ordType` | 订单类型(market/limit) | | `side` | 买卖方向(buy/sell) | | `clOrdId` | 客户端订单 ID(唯一) | | `sz` | 下单数量 | | `markPx` | 标记价格(限价单用) | | `tradeType` | 交易类型(1=开仓,3=平仓) | --- ## 4. WebSocket 通信层 ### 4.1 OkxWebSocketClientManager — 入口管理器 ``` 文件:OkxWebSocketClientManager.java ``` **Spring `@Component`**,管理完整的 WS 生命周期。 #### 职责 1. **组件组装**:创建 `OkxConfig` → `OkxGridTradeService` → `OkxTradeExecutor`(注入 WS Client)→ 4 个频道处理器 → `OkxKlineWebSocketClient` 2. **生命周期管理**: - `@PostConstruct init()`:初始化和连接(生产环境)或注册 MBean(测试环境) - `@PreDestroy close()`:优雅关闭(停止策略 → 取消条件单 → 关闭 WS) #### 初始化流程 ``` init() ├── configMap — 从本地缓存读取账户配置 ├── OkxGridTradeService.startGrid() ├── OkxTradeExecutor.setWebSocketClient(wsClient) ├── 创建 4 个频道处理器 ├── OkxKlineWebSocketClient.connect() │ ├── 连接 OSKL 公开 WS(K线频道不需要登录) │ ├── 登录私有 WS → 订阅 持仓/账户/订单/策略委托频道 │ └── 启动心跳定时器(30s ping/pong) └── isProduction ? 直接启动 : MBean 注册(JMX 手动控制) ``` --- ### 4.2 OkxKlineWebSocketClient — WS 连接客户端 ``` 文件:OkxKlineWebSocketClient.java ``` 封装 `java-websocket` 客户端,管理物理连接。 #### 连接架构 - **公开频道 WS**(`wss://ws.okx.com:8443/ws/v5/public` 或模拟盘域名):K线推送不需要登录 - **私有频道 WS**(`wss://ws.okx.com:8443/ws/v5/private` 或模拟盘域名):需要登录认证,订阅持仓/账户/订单/策略委托频道 #### 连接流程 ``` connect() ├── 1. 创建公开 WS Client → connect() │ └── onOpen → subscribePublicChannels() → 订阅 K线频道 ├── 2. 创建私有 WS Client → connect() │ └── onOpen → login() → onLoginSuccess → subscribePrivateChannels() │ ├── 订阅 positions 频道 │ ├── 订阅 account 频道 │ ├── 订阅 orders 频道 │ └── 订阅 orders-algo 频道 └── 3. 启动心跳定时器(30s 间隔 ping/pong,60s 超时检测) ``` #### 断线重连机制 - 最多重连 `MAX_RECONNECT_ATTEMPTS` 次(默认 30) - 重连延迟 `RECONNECT_DELAY_MS`(默认 5000ms) - 重连成功后重新执行登录 + 订阅流程 - 兜底机制:重连失败后尝试通过 MBean 重启整个 WS 客户端 #### 消息路由 `onMessage` → 遍历所有 `OkxChannelHandler` → 调用 `handleMessage(response)` 分发到具体处理器 --- ## 5. 频道处理器层 ### 5.1 OkxChannelHandler — 处理器接口 ``` 文件:wsHandler/OkxChannelHandler.java ``` 统一接口,所有频道处理器实现此接口: ```java public interface OkxChannelHandler { String getChannelName(); // 频道名称 void subscribe(WebSocketClient ws); // 订阅 void unsubscribe(WebSocketClient ws); // 取消订阅 boolean handleMessage(JSONObject response); // 处理推送消息 } ``` ### 5.2 OkxCandlestickChannelHandler — K线频道 ``` 文件:wsHandler/handler/OkxCandlestickChannelHandler.java ``` #### 订阅参数 | 参数 | 值 | |------|-----| | `channel` | `candle{period}`(如 `candle1H`) | | `instId` | 合约 ID(如 `BTC-USDT-SWAP`) | #### 数据处理 - 解析 `data[0]` → `[ts, o, h, l, c, vol, volCcy, ...]` - 提取**收盘价** `c` → 调用 `gridTradeService.onKline(closePrice)` - K线为 `candle1H` 时打印整点日志 #### 调用链 ``` onKline(closePrice) ├── WAITING_KLINE → 进入 OPENING 状态,开基底多+空 ├── ACTIVE → processShortGrid(closePrice) + processLongGrid(closePrice) └── STOPPED → 仅更新未实现盈亏 ``` ### 5.3 OkxPositionsChannelHandler — 持仓频道 ``` 文件:wsHandler/handler/OkxPositionsChannelHandler.java ``` #### 订阅参数 | 参数 | 值 | |------|-----| | `channel` | `positions` | | `instType` | `SWAP` | | `instId` | 合约 ID | #### 数据处理 解析 `data[]` 数组 → 提取 `posSide`、`pos`(数量)、`avgPx`(均价)→ 调用 `gridTradeService.onPositionUpdate(posSide, size, avgPx)` #### 在策略中的作用 ``` onPositionUpdate() → 区分 3 种场景: 1. 仓位从无到有(基底开仓成交)→ 标记 baseOpened → 双基底都成后生成网格队列 2. 仓位量增加(网格触发开仓成交)→ 取队列首元素做止盈价 → 设止盈条件单 3. 仓位归零(止盈平仓完成)→ 标记 active=false ``` ### 5.4 OkxAccountChannelHandler — 账户频道 ``` 文件:wsHandler/handler/OkxAccountChannelHandler.java ``` #### 订阅参数 | 参数 | 值 | |------|-----| | `channel` | `account` | #### 数据处理 解析 `data[]` → 提取 `availBal`(可用余额)、`cashBal`(现金余额)、`eq`(权益)、`upl`(未实现盈亏)、`imr`(保证金占用) **当前版本**:仅做日志输出,不做业务判断。后续可扩展保证金安全阀功能。 ### 5.5 OkxOrderInfoChannelHandler — 订单成交频道 ``` 文件:wsHandler/handler/OkxOrderInfoChannelHandler.java ``` #### 订阅参数 | 参数 | 值 | |------|-----| | `channel` | `orders` | | `instType` | `SWAP` | | `instId` | 合约 ID | #### 数据处理 - 过滤 `state=filled` 且 `accFillSz>0` 的订单 - 提取 `posSide`、`accFillSz`(成交数量)、`fillPnl`(已实现盈亏) - 调用 `gridTradeService.onOrderFilled(posSide, accFillSz, fillPnl)` #### 在策略中的作用 ``` onOrderFilled() ├── 累计盈亏 += fillPnl ├── 累计盈亏 ≥ overallTp → 策略停止(止盈达标) └── 累计盈亏 ≤ -maxLoss → 策略停止(亏损超限) ``` --- ## 6. 策略执行层 ### 6.1 OkxTradeExecutor — 异步下单执行器 ``` 文件:OkxTradeExecutor.java ``` #### 设计目的 WS 消息在回调线程处理,下单操作提交到**独立线程池异步执行**,避免阻塞 WS 回调线程。 #### 线程模型 | 参数 | 值 | |------|-----| | 核心线程 | 1 | | 最大线程 | 1 | | 空闲超时 | 60s(`allowCoreThreadTimeOut`) | | 队列类型 | `LinkedBlockingQueue` | | 队列容量 | 64 | | 拒绝策略 | `CallerRunsPolicy`(队列满时由提交线程直接同步执行,形成自然背压) | | 守护线程 | 是 | **单线程的作用**:保证下单顺序(开多 → 开空 → 止盈单),避免并发竞争。 #### 公开方法 | 方法 | 说明 | |------|------| | `openLong(quantity, onSuccess, onFailure)` | 异步市价开多 | | `openShort(quantity, onSuccess, onFailure)` | 异步市价开空 | | `placeTakeProfit(triggerPrice, orderType, size)` | 异步创建止盈条件单(通过 `batch-orders` 发送 algo 委托) | | `cancelAllPriceTriggeredOrders()` | 撤销所有条件单(`cancel-algos`) | | `setWebSocketClient(wsClient)` | 注入 WS 客户端引用 | | `shutdown()` | 优雅关闭(等待 10s,超时强制中断) | #### 止盈兜底机制 ``` placeTakeProfit() ├── 成功 → 发送 batch-orders(algo 条件单) └── 失败 → marketClose() 立即市价平仓兜底 ``` #### 下单方式 所有下单通过 **WebSocket** 发送 JSON(非 REST API),`sendOrder` 构建如下消息: ```json { "id": "order_1712345678901_a1b2c3", "op": "order", "args": [{ "instId": "BTC-USDT-SWAP", "tdMode": "cross", "clOrdId": "buy_1712345678901_d4e5f6", "side": "buy", "posSide": "long", "ordType": "market", "sz": "1" }] } ``` #### 调用链 ``` OkxGridTradeService.onKline └── executor.openLong() / openShort() ← 基底双开 + 网格触发 OkxGridTradeService.onPositionUpdate └── executor.placeTakeProfit() ← 仓位成交后设止盈 OkxGridTradeService.stopGrid └── executor.cancelAllPriceTriggeredOrders() + shutdown() ``` --- ### 6.2 OkxGridTradeService — 网格策略核心 ``` 文件:OkxGridTradeService.java ``` 这是整个包的核心,实现了**多空双开网格交易策略**的所有状态机和网格队列逻辑。 #### 策略状态机 ``` WAITING_KLINE ──(首根K线到达)──▶ OPENING ──(双基底成交)──▶ ACTIVE ──(止盈/止损达标)──▶ STOPPED ▲ │ └──────────────────(startGrid() 重新启动)──────────────────┘ ``` | 状态 | 含义 | |------|------| | `WAITING_KLINE` | 等待首根 K 线到达 | | `OPENING` | 已收到 K 线,正在开基底仓位(多+空各一单) | | `ACTIVE` | 双基底已成交,网格队列已生成,正常交易中 | | `STOPPED` | 已停止(止盈达标 / 亏损超限 / 手动停止) | #### 数据结构 | 字段 | 类型 | 说明 | |------|------|------| | `shortPriceQueue` | `List<BigDecimal>` | 空仓价格队列(**降序**) | | `longPriceQueue` | `List<BigDecimal>` | 多仓价格队列(**升序**) | | `shortBaseEntryPrice` | `BigDecimal` | 空仓基底成交价 | | `longBaseEntryPrice` | `BigDecimal` | 多仓基底成交价 | | `baseLongOpened` | `boolean` | 多仓基底是否已开 | | `baseShortOpened` | `boolean` | 空仓基底是否已开 | | `cumulativePnl` | `BigDecimal` | 累计已实现盈亏 | #### 策略完整生命周期 ``` 1. startGrid() └── 重置所有状态 → WAITING_KLINE 2. onKline(closePrice) → WAITING_KLINE └── 转为 OPENING → 发送基底多单 + 基底空单 3. onPositionUpdate(posSide, size, entryPrice) ├── 仓位从无到有 │ ├── 标记 baseLongOpened / baseShortOpened │ ├── 记录 entryPrice │ ├── 双基底都成交 → tryGenerateQueues() → ACTIVE │ └── 生成网格队列: │ · 空仓队列:entryPrice × (1 - gridRate×1), (1 - gridRate×2), ... 降序排列 │ · 多仓队列:entryPrice × (1 + gridRate×1), (1 + gridRate×2), ... 升序排列 ├── 仓位量增加(网格触发开仓成交) │ └── 检查队列非空 → 取队列首元素做止盈价 → executor.placeTakeProfit() └── 仓位归零 └── 标记 active=false 4. onKline(closePrice) → ACTIVE ├── processShortGrid(closePrice) ← 空仓网格处理 │ ├── 匹配队列中 > closePrice 的元素 │ ├── 移除已匹配 → 尾部补充新元素(递减) │ ├── 多仓队列转移(以对方队列首元素为种子生成递减元素) │ ├── 贴近持仓均价过滤(skip) │ ├── 保证金安全检查 │ ├── 开空一次 │ └── 额外反向开多(价格夹在多/空均价之间且多>空倒挂时) └── processLongGrid(closePrice) ← 多仓网格处理 └── (对称逻辑,方向反转) 5. onOrderFilled(posSide, fillSz, pnl) ├── cumulativePnl += pnl ├── cumulativePnl ≥ overallTp → STOPPED └── cumulativePnl ≤ -maxLoss → STOPPED 6. stopGrid() ├── 状态 → STOPPED ├── cancelAllPriceTriggeredOrders() └── executor.shutdown() ``` #### 空仓网格处理 `processShortGrid(currentPrice)` 详解 ``` 1. 匹配队列元素(空仓队列降序遍历,收集 > currentPrice 的元素) └── 为空 → 直接返回 2. 空仓队列更新 ├── 移除 matched 元素 └── 尾部补充新元素(尾价 × (1 - gridRate) 循环递减)→ 降序排序 3. 多仓队列转移 ├── 以多仓队列首元素(最小价)为种子 ├── 生成 matched.size() 个递减元素加入多仓队列 ├── 贴近持仓均价过滤:元素与多仓均价差距 < gridRate → skip └── 升序排序,超长截断 4. 保证金安全检查 └── 超限 → warn 跳过开仓 5. 开空一次 → executor.openShort() 6. 额外反向开多条件(同时满足): ├── longEntryPrice > shortEntryPrice(多>空倒挂) ├── currentPrice > shortEntryPrice(当前价在空仓均价上方) └── currentPrice < longEntryPrice × (1 - gridRate)(远离多仓均价) └── 满足 → executor.openLong() 额外开多一次 ``` #### 多仓网格处理 `processLongGrid(currentPrice)` 详解 对称逻辑,方向反转。 --- ## 7. 独立启动类:OkxWebSocketClientMain ``` 文件:OkxWebSocketClientMain.java ``` **纯 main 方法启动**,不依赖 Spring 容器。 ### 用途 用于**本地测试和调试**,无需启动整个 Spring 应用。 ### 启动流程 ```java public static void main(String[] args) { OkxConfig config = OkxConfig.builder() .apiKey("xxx") .secretKey("xxx") .passphrase("xxx") .contract("BTC-USDT-SWAP") .marginMode("cross") .leverage(1) .quantity("1") .gridRate(new BigDecimal("0.01")) .gridQueueSize(10) .overallTp(new BigDecimal("100")) .isProduction(false) .build(); OkxGridTradeService service = new OkxGridTradeService(config, "test-account"); service.startGrid(); OkxKlineWebSocketClient wsClient = new OkxKlineWebSocketClient(config, service, ...); wsClient.connect(); // 注册 JVM 关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(() -> { service.stopGrid(); wsClient.close(); })); } ``` --- ## 8. 完整调用链 ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. 启动阶段 │ ├─────────────────────────────────────────────────────────────┤ │ OkxWebSocketClientManager.init() │ │ ├── new OkxConfig.Builder()...build() │ │ ├── new OkxGridTradeService(config, accountName) │ │ │ └── new OkxTradeExecutor(contract, marginMode, name) │ │ ├── gridTradeService.startGrid() → WAITING_KLINE │ │ ├── new OkxCandlestickChannelHandler(instId, candlePeriod, │ │ │ gridTradeService, config) │ │ ├── new OkxPositionsChannelHandler(instId, gridTradeService)│ │ ├── new OkxAccountChannelHandler() │ │ ├── new OkxOrderInfoChannelHandler(instId, gridTradeService,│ │ │ config) │ │ ├── new OkxKlineWebSocketClient(config, handlers, ...) │ │ ├── wsClient.connect() │ │ │ ├── 连接公开 WS → 订阅 K线频道 │ │ │ ├── 连接私有 WS → 登录 → 订阅 持仓/账户/订单/策略委托 │ │ │ └── 启动心跳定时器 │ │ └── executor.setWebSocketClient(privateWsClient) │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ 2. 运行时数据流 │ ├─────────────────────────────────────────────────────────────┤ │ [K线推送] │ │ OkxCandlestickChannelHandler.handleMessage() │ │ └── gridTradeService.onKline(closePrice) │ │ ├── WAITING_KLINE → OPENING │ │ │ ├── executor.openLong(quantity, onSuccess, │ │ │ │ onFailure) │ │ │ └── executor.openShort(quantity, onSuccess, │ │ │ onFailure) │ │ └── ACTIVE │ │ ├── processShortGrid(closePrice) │ │ │ ├── 匹配队列 → 更新队列 → 转移对方队列 │ │ │ └── executor.openShort() │ │ └── processLongGrid(closePrice) │ │ └──(对称逻辑) │ │ │ │ [持仓推送] │ │ OkxPositionsChannelHandler.handleMessage() │ │ └── gridTradeService.onPositionUpdate(posSide, size, avgPx)│ │ ├── 基底成交 → 标记 baseOpened → tryGenerateQueues() │ │ └── 增量成交 → executor.placeTakeProfit(tp, type, sz)│ │ │ │ [订单成交推送] │ │ OkxOrderInfoChannelHandler.handleMessage() │ │ └── gridTradeService.onOrderFilled(posSide, fillSz, pnl) │ │ └── cumulativePnl 累加 → 达标则停止 │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ 3. 停止阶段 │ ├─────────────────────────────────────────────────────────────┤ │ OkxWebSocketClientManager.close() │ │ ├── gridTradeService.stopGrid() │ │ │ ├── state = STOPPED │ │ │ ├── executor.cancelAllPriceTriggeredOrders() │ │ │ │ └── wsClient.send(cancel-algos) │ │ │ └── executor.shutdown() │ │ └── wsClient.close() │ │ └── 关闭公开WS + 私有WS │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 9. 与 Gate API 的差异对比 | 方面 | Gate API (gateApi) | OKX API (okxApi) | |------|-------------------|-----------------| | **下单方式** | REST API (`FuturesApi.createFuturesOrder`) | WebSocket JSON 消息 (`op: "order"`) | | **止盈单** | REST API (`createPriceTriggeredOrder`),`plan-close-*-position` | WS 消息 (`op: "batch-orders"`),`ordType: limit` + `px` 触发价 | | **仓位方向** | 正数=开多、负数=开空(size 带符号) | `posSide: long/short` 显式区分,`sz` 始终正数 | | **保证金模式** | 无(Gate API 隐含) | `tdMode: cross/isolated` 显式指定 | | **客户端订单 ID** | 自动生成(Gate API 隐式处理) | `clOrdId` 显式生成和传入 | | **取消条件单** | REST API (`cancelPriceTriggeredOrderList`) | WS 消息 (`op: "cancel-algos"`) | | **止损失败兜底** | REST `createFuturesOrder` IOC 市价平仓 | WS 消息 `marketClose()`(`tradeType: "3"`) | | **成交识别** | 通过 WS `FuturesOrderBookTicker` 或 REST 查询 | WS `orders` 频道推送 `state=filled` | | **API 认证** | HMAC-SHA256 请求头签名(Gate SDK 封装) | HMAC-SHA256 + Base64 WS 登录消息 | | **WS 连接** | 单一连接,频道订阅混合 | 双连接:公开 WS(K线)+ 私有 WS(持仓/账户/订单) | | **`placeTakeProfit` 签名** | `(triggerPrice, rule, orderType, size)` 多一个 `rule` 参数 | `(triggerPrice, orderType, size)` (OKX 无 rule 概念) | | **止盈单下单** | 单条 `FuturesPriceTriggeredOrder` | `batch-orders` 包装(List 格式,但实际传 1 条) | | **心跳机制** | 应用层 ping/pong JSON 消息 | `java-websocket` 自带 ping/pong + 60s 超时重连 | | **包依赖** | 依赖 Gate SDK (`io.gate.gateapi`) | **完全自包含**,仅依赖 `java-websocket` + `fastjson` + `lombok` |