refactor(gateApi): 重构 Gate API 模块代码结构
- 移除未使用的 GateChannelHandler 导入
- 为 AbstractPrivateChannelHandler 添加详细的 Javadoc 文档
- 优化 subscribe 和 unsubscribe 方法的日志输出格式
- 为 CandlestickChannelHandler 添加完整的类文档注释
- 更新 gateApi-logic.md 文档,添加 GateConfig 和 GateTradeExecutor 组件说明
- 优化 handleMessage 方法的日志输出和错误处理
- 简化方法实现,移除不必要的换行和注释
- 添加 GateConfig 类,提供统一的配置管理和环境切换功能
9 files modified
2 files added
| New file |
| | |
| | | package com.xcong.excoin.modules.gateApi; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * Gate 模块统一配置。 |
| | | * |
| | | * <p>通过 Builder 模式集中管理所有运行参数,避免参数散落在多个文件中。 |
| | | * 提供 REST API 和 WebSocket 地址的自动环境切换(测试网/生产网)。 |
| | | * |
| | | * <h3>使用示例</h3> |
| | | * <pre> |
| | | * GateConfig config = GateConfig.builder() |
| | | * .apiKey("...") |
| | | * .apiSecret("...") |
| | | * .contract("XAU_USDT") |
| | | * .leverage("100") |
| | | * .gridRate(new BigDecimal("0.0035")) |
| | | * .isProduction(false) |
| | | * .build(); |
| | | * |
| | | * String restUrl = config.getRestBasePath(); // 自动返回测试网或生产网地址 |
| | | * String wsUrl = config.getWsUrl(); |
| | | * </pre> |
| | | * |
| | | * <h3>默认值</h3> |
| | | * <ul> |
| | | * <li>合约: BTC_USDT, 杠杆: 10x, 全仓, 双向持仓</li> |
| | | * <li>网格: 0.35%, 止盈: 0.5 USDT, 亏损: 7.5 USDT</li> |
| | | * <li>数量: 1 张, 环境: 测试网, 重试: 3 次</li> |
| | | * </ul> |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | public class GateConfig { |
| | | |
| | | /** Gate API v4 密钥 */ |
| | | private final String apiKey; |
| | | /** Gate API v4 签名密钥 */ |
| | | private final String apiSecret; |
| | | /** 合约名称(如 XAU_USDT) */ |
| | | private final String contract; |
| | | /** 杠杆倍数 */ |
| | | private final String leverage; |
| | | /** 保证金模式(cross / isolated) */ |
| | | private final String marginMode; |
| | | /** 持仓模式(single / dual / dual_plus) */ |
| | | private final String positionMode; |
| | | /** 网格间距比例(如 0.0035 表示 0.35%) */ |
| | | private final BigDecimal gridRate; |
| | | /** 整体止盈阈值(USDT) */ |
| | | private final BigDecimal overallTp; |
| | | /** 最大亏损阈值(USDT) */ |
| | | private final BigDecimal maxLoss; |
| | | /** 下单数量(合约张数) */ |
| | | private final String quantity; |
| | | /** 是否为生产环境 */ |
| | | private final boolean isProduction; |
| | | /** 补仓最大重试次数 */ |
| | | private final int reopenMaxRetries; |
| | | |
| | | private GateConfig(Builder builder) { |
| | | this.apiKey = builder.apiKey; |
| | | this.apiSecret = builder.apiSecret; |
| | | this.contract = builder.contract; |
| | | this.leverage = builder.leverage; |
| | | this.marginMode = builder.marginMode; |
| | | this.positionMode = builder.positionMode; |
| | | this.gridRate = builder.gridRate; |
| | | this.overallTp = builder.overallTp; |
| | | this.maxLoss = builder.maxLoss; |
| | | this.quantity = builder.quantity; |
| | | this.isProduction = builder.isProduction; |
| | | this.reopenMaxRetries = builder.reopenMaxRetries; |
| | | } |
| | | |
| | | /** |
| | | * 根据环境返回 REST API 基础路径。 |
| | | * <ul> |
| | | * <li>测试网: {@code https://api-testnet.gateapi.io/api/v4}</li> |
| | | * <li>生产网: {@code https://api.gateio.ws/api/v4}</li> |
| | | * </ul> |
| | | */ |
| | | public String getRestBasePath() { |
| | | return isProduction |
| | | ? "https://api.gateio.ws/api/v4" |
| | | : "https://api-testnet.gateapi.io/api/v4"; |
| | | } |
| | | |
| | | /** |
| | | * 根据环境返回 WebSocket 地址。 |
| | | * <ul> |
| | | * <li>测试网: {@code wss://ws-testnet.gate.com/v4/ws/futures/usdt}</li> |
| | | * <li>生产网: {@code wss://fx-ws.gateio.ws/v4/ws/usdt}</li> |
| | | * </ul> |
| | | */ |
| | | public String getWsUrl() { |
| | | return isProduction |
| | | ? "wss://fx-ws.gateio.ws/v4/ws/usdt" |
| | | : "wss://ws-testnet.gate.com/v4/ws/futures/usdt"; |
| | | } |
| | | |
| | | public String getApiKey() { return apiKey; } |
| | | public String getApiSecret() { return apiSecret; } |
| | | public String getContract() { return contract; } |
| | | public String getLeverage() { return leverage; } |
| | | public String getMarginMode() { return marginMode; } |
| | | public String getPositionMode() { return positionMode; } |
| | | public BigDecimal getGridRate() { return gridRate; } |
| | | public BigDecimal getOverallTp() { return overallTp; } |
| | | public BigDecimal getMaxLoss() { return maxLoss; } |
| | | public String getQuantity() { return quantity; } |
| | | public boolean isProduction() { return isProduction; } |
| | | public int getReopenMaxRetries() { return reopenMaxRetries; } |
| | | |
| | | public static Builder builder() { |
| | | return new Builder(); |
| | | } |
| | | |
| | | /** |
| | | * GateConfig 的流式构造器,提供合理的默认值。 |
| | | */ |
| | | public static class Builder { |
| | | private String apiKey; |
| | | private String apiSecret; |
| | | private String contract = "BTC_USDT"; |
| | | private String leverage = "10"; |
| | | private String marginMode = "cross"; |
| | | private String positionMode = "dual"; |
| | | private BigDecimal gridRate = new BigDecimal("0.0035"); |
| | | private BigDecimal overallTp = new BigDecimal("0.5"); |
| | | private BigDecimal maxLoss = new BigDecimal("7.5"); |
| | | private String quantity = "1"; |
| | | private boolean isProduction = false; |
| | | private int reopenMaxRetries = 3; |
| | | |
| | | public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } |
| | | public Builder apiSecret(String apiSecret) { this.apiSecret = apiSecret; return this; } |
| | | 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 positionMode(String positionMode) { this.positionMode = positionMode; 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; } |
| | | public Builder quantity(String quantity) { this.quantity = quantity; return this; } |
| | | public Builder isProduction(boolean isProduction) { this.isProduction = isProduction; return this; } |
| | | public Builder reopenMaxRetries(int reopenMaxRetries) { this.reopenMaxRetries = reopenMaxRetries; return this; } |
| | | |
| | | public GateConfig build() { |
| | | return new GateConfig(this); |
| | | } |
| | | } |
| | | } |
| | |
| | | import io.gate.gateapi.api.FuturesApi; |
| | | import io.gate.gateapi.models.AccountDetail; |
| | | import io.gate.gateapi.models.FuturesAccount; |
| | | import io.gate.gateapi.models.FuturesInitialOrder; |
| | | import io.gate.gateapi.models.FuturesOrder; |
| | | import io.gate.gateapi.models.FuturesPriceTrigger; |
| | | import io.gate.gateapi.models.FuturesPriceTriggeredOrder; |
| | | import io.gate.gateapi.models.TriggerOrderResponse; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | |
| | | /** |
| | | * Gate 网格交易服务类。 |
| | | * Gate 网格交易服务类。使用 Gate SDK 通过 REST API 下单。 |
| | | * |
| | | * <h3>策略概述</h3> |
| | | * 多空双开 → 各放止盈条件单 → 仓位推送检测平仓补仓 → 平仓推送累加 pnl → 判断停止 |
| | | * |
| | | * <h3>触发逻辑</h3> |
| | | * <h3>状态机</h3> |
| | | * <pre> |
| | | * K线首次价格就绪 → dualOpenPositions() // 多空双开 + 止盈条件单 |
| | | * 仓位推送 size=0 → reopenXxxPosition() // 该方向被平仓 → 只补该方向 |
| | | * 平仓推送 pnl → cumulativePnl += pnl // 累计盈亏 |
| | | * cumulativePnl ≥ overallTp → 停止 |
| | | * cumulativePnl ≤ -maxLoss → 停止 |
| | | * WAITING_KLINE → (首次 K 线) → OPENING → ACTIVE |
| | | * 双开失败 → STOPPED |
| | | * |
| | | * ACTIVE: |
| | | * ├─ 仓位 size=0 且方向活跃 → REOPENING_L/S → ACTIVE |
| | | * │ 补仓失败 → 重试 → 仍失败 → STOPPED |
| | | * └─ cumulativePnl ≥ overallTp 或 ≤ -maxLoss → STOPPED |
| | | * </pre> |
| | | * |
| | | * <h3>止盈计算</h3> |
| | | * 多头止盈价 = entryPrice × (1 + gridRate)<br> |
| | | * 空头止盈价 = entryPrice × (1 - gridRate) |
| | | * |
| | | * <h3>依赖</h3> |
| | | * 使用 {@code io.gate:gate-api (7.2.71)} SDK 通过 REST API 下单, |
| | | * 市场数据由 WebSocket Handler 类提供。 |
| | | * <h3>架构</h3> |
| | | * REST 下单委派给 {@link GateTradeExecutor}(独立线程池,避免阻塞 WS 回调线程)。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class GateGridTradeService { |
| | | |
| | | private final ApiClient apiClient; |
| | | public enum StrategyState { |
| | | WAITING_KLINE, OPENING, ACTIVE, REOPENING_LONG, REOPENING_SHORT, STOPPED |
| | | } |
| | | |
| | | private final GateConfig config; |
| | | private final GateTradeExecutor executor; |
| | | private final FuturesApi futuresApi; |
| | | private static final String SETTLE = "usdt"; |
| | | |
| | | private final String contract; |
| | | private final String leverage; |
| | | private final String marginMode; |
| | | private final BigDecimal gridRate; |
| | | private final BigDecimal overallTp; |
| | | private final BigDecimal maxLoss; |
| | | private final String quantity; |
| | | private final String positionMode; |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | | /** 策略是否处于运行状态 */ |
| | | private volatile boolean strategyActive = false; |
| | | /** 是否已完成首次双开 */ |
| | | private volatile boolean dualOpened = false; |
| | | /** 多头仓位是否活跃 */ |
| | | /** 多头是否活跃(有仓位) */ |
| | | private volatile boolean longActive = false; |
| | | /** 空头仓位是否活跃 */ |
| | | /** 空头是否活跃(有仓位) */ |
| | | private volatile boolean shortActive = false; |
| | | |
| | | /** 多头入场价 */ |
| | | private BigDecimal longEntryPrice; |
| | | /** 空头入场价 */ |
| | | private BigDecimal shortEntryPrice; |
| | | /** WebSocket 推送的最新 K 线收盘价 */ |
| | | private volatile BigDecimal lastKlinePrice; |
| | | /** 平仓推送累计的盈亏 */ |
| | | private volatile BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | /** 用户 ID,用于 WebSocket 私有频道订阅 */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 构造函数,初始化 Gate 期货 API 客户端。 |
| | | * |
| | | * @param contract 合约名称(如 XAU_USDT) |
| | | * @param leverage 杠杆倍数 |
| | | * @param marginMode 保证金模式(cross/isolated) |
| | | * @param positionMode 持仓模式(single/dual/dual_plus) |
| | | * @param gridRate 网格间距比例(如 0.0035) |
| | | * @param overallTp 整体止盈阈值(USDT) |
| | | * @param maxLoss 最大亏损阈值(USDT) |
| | | * @param quantity 下单数量(合约张数) |
| | | */ |
| | | public GateGridTradeService(String apiKey, String apiSecret, |
| | | String contract, String leverage, |
| | | String marginMode, String positionMode, |
| | | BigDecimal gridRate, BigDecimal overallTp, |
| | | BigDecimal maxLoss, |
| | | String quantity) { |
| | | this.contract = contract; |
| | | this.leverage = leverage; |
| | | this.marginMode = marginMode; |
| | | this.gridRate = gridRate; |
| | | this.overallTp = overallTp; |
| | | this.maxLoss = maxLoss; |
| | | this.quantity = quantity; |
| | | this.positionMode = positionMode; |
| | | private int longReopenFails = 0; |
| | | private int shortReopenFails = 0; |
| | | |
| | | this.apiClient = new ApiClient(); |
| | | this.apiClient.setBasePath("https://api-testnet.gateapi.io/api/v4"); |
| | | this.apiClient.setApiKeySecret(apiKey, apiSecret); |
| | | public GateGridTradeService(GateConfig config) { |
| | | this.config = config; |
| | | ApiClient apiClient = new ApiClient(); |
| | | apiClient.setBasePath(config.getRestBasePath()); |
| | | apiClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | | this.futuresApi = new FuturesApi(apiClient); |
| | | this.executor = new GateTradeExecutor(apiClient, config.getContract()); |
| | | } |
| | | |
| | | /** |
| | | * 初始化账户。顺序:切持仓模式 → 清旧条件单 → 设杠杆 → 查余额。 |
| | | */ |
| | | public void init() { |
| | | try { |
| | | AccountApi accountApi = new AccountApi(apiClient); |
| | | AccountDetail accountDetail = accountApi.getAccountDetail(); |
| | | this.userId = accountDetail.getUserId(); |
| | | log.info("[GateGrid] 用户ID: {}", userId); |
| | | ApiClient detailClient = new ApiClient(); |
| | | detailClient.setBasePath(config.getRestBasePath()); |
| | | detailClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | | AccountDetail detail = new AccountApi(detailClient).getAccountDetail(); |
| | | this.userId = detail.getUserId(); |
| | | log.info("[Gate] uid:{}", userId); |
| | | |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | String positionModeSet = account.getPositionMode(); |
| | | if (!positionMode.equals(positionModeSet)) { |
| | | futuresApi.setPositionMode(SETTLE, positionMode); |
| | | if (!config.getPositionMode().equals(account.getPositionMode())) { |
| | | futuresApi.setPositionMode(SETTLE, config.getPositionMode()); |
| | | } |
| | | log.info("[GateGrid] 已设置持仓模式: {}", positionMode); |
| | | log.info("[Gate] mode:{} balance:{}", config.getPositionMode(), account.getAvailable()); |
| | | |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | log.info("[GateGrid] 已取消所有既有止盈止损条件单"); |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | log.info("[Gate] old orders cleared"); |
| | | |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, contract, leverage, marginMode, positionMode, null); |
| | | log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); |
| | | |
| | | log.info("[GateGrid] 账户可用余额: {}, 总资产: {}", |
| | | account.getAvailable(), account.getTotal()); |
| | | SETTLE, config.getContract(), config.getLeverage(), |
| | | config.getMarginMode(), config.getPositionMode(), null); |
| | | log.info("[Gate] {}x {}", config.getLeverage(), config.getMarginMode()); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 初始化失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | log.error("[Gate] init fail, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (ApiException e) { |
| | | log.error("[GateGrid] 初始化API调用失败, code: {}", e.getCode()); |
| | | log.error("[Gate] init fail, code:{}", e.getCode()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动网格策略。策略激活后等待 K 线价格就绪,然后自动首次双开。 |
| | | */ |
| | | public void startGrid() { |
| | | if (strategyActive) { |
| | | log.warn("[GateGrid] 策略已在运行中"); |
| | | if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { |
| | | log.warn("[Gate] already running, state:{}", state); |
| | | return; |
| | | } |
| | | strategyActive = true; |
| | | state = StrategyState.WAITING_KLINE; |
| | | cumulativePnl = BigDecimal.ZERO; |
| | | log.info("[GateGrid] 网格策略启动,等待K线价格..."); |
| | | longActive = false; |
| | | shortActive = false; |
| | | longReopenFails = 0; |
| | | shortReopenFails = 0; |
| | | log.info("[Gate] grid started"); |
| | | } |
| | | |
| | | /** |
| | | * 停止网格策略。 |
| | | */ |
| | | public void stopGrid() { |
| | | strategyActive = false; |
| | | log.info("[GateGrid] 网格策略已停止, cumulativePnl: {}", cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | executor.shutdown(); |
| | | log.info("[Gate] stopped, pnl:{}", cumulativePnl); |
| | | } |
| | | |
| | | /** |
| | | * K 线回调入口。由 调用。 |
| | | * 首次收到价格时触发多空双开,后续仅缓存最新价格供补仓使用。 |
| | | * |
| | | * @param closePrice K 线收盘价 |
| | | * K 线回调。首次价格就绪 → 异步双开。 |
| | | */ |
| | | public void onKline(BigDecimal closePrice) { |
| | | lastKlinePrice = closePrice; |
| | | if (!strategyActive) { |
| | | if (state != StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | if (!dualOpened) { |
| | | dualOpened = true; |
| | | dualOpenPositions(); |
| | | |
| | | state = StrategyState.OPENING; |
| | | log.info("[Gate] first kline, opening..."); |
| | | |
| | | executor.openLong(config.getQuantity(), () -> { |
| | | synchronized (this) { |
| | | longEntryPrice = lastKlinePrice; |
| | | longActive = true; |
| | | } |
| | | executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | "close-long-position", "close_long"); |
| | | }); |
| | | executor.openShort(negate(config.getQuantity()), () -> { |
| | | synchronized (this) { |
| | | shortEntryPrice = lastKlinePrice; |
| | | shortActive = true; |
| | | } |
| | | executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | "close-short-position", "close_short"); |
| | | if (longActive && shortActive && state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | log.info("[Gate] active, long:{}, short:{}, tpL:{}, tpS:{}", |
| | | longEntryPrice, shortEntryPrice, longTpPrice(), shortTpPrice()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 仓位推送回调入口。由 {@link GateKlineWebSocketClient} 调用。 |
| | | * 根据仓位模式(dual_long/dual_short)和 size 判断: |
| | | * <ul> |
| | | * <li>size=0 且之前活跃 → 该方向被平仓 → 补开</li> |
| | | * <li>size>0 → 确认仓位活跃,更新入场价</li> |
| | | * </ul> |
| | | * 注意:累计盈亏由 onPositionClose 独立计算。 |
| | | * |
| | | * @param contract 合约名 |
| | | * @param mode 仓位模式(dual_long / dual_short) |
| | | * @param size 仓位数量(0 表示无仓位) |
| | | * @param entryPrice 入场价格 |
| | | * 仓位推送回调。检测 size=0 触发补仓。 |
| | | */ |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, |
| | | BigDecimal entryPrice) { |
| | | if (!strategyActive) { |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, BigDecimal entryPrice) { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | |
| | |
| | | |
| | | if ("dual_long".equals(mode)) { |
| | | if (longActive && !hasPosition) { |
| | | log.info("[GateGrid] 多头被平仓(止盈触发), 重新开多"); |
| | | log.info("[Gate] long closed"); |
| | | longActive = false; |
| | | reopenLongPosition(); |
| | | tryReopenLong(0); |
| | | } else if (hasPosition) { |
| | | longActive = true; |
| | | longEntryPrice = entryPrice; |
| | | } |
| | | } else if ("dual_short".equals(mode)) { |
| | | if (shortActive && !hasPosition) { |
| | | log.info("[GateGrid] 空头被平仓(止盈触发), 重新开空"); |
| | | log.info("[Gate] short closed"); |
| | | shortActive = false; |
| | | reopenShortPosition(); |
| | | tryReopenShort(0); |
| | | } else if (hasPosition) { |
| | | shortActive = true; |
| | | shortEntryPrice = entryPrice; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 平仓推送回调入口。由 {@link GateKlineWebSocketClient} 调用。 |
| | | * 累加平仓盈亏,每次平仓推送后检查停止条件。 |
| | | * |
| | | * @param contract 合约名 |
| | | * @param side 平仓方向(long / short) |
| | | * @param pnl 该次平仓的盈亏 |
| | | * 平仓推送回调。累加 pnl 并检查停止条件。 |
| | | */ |
| | | public void onPositionClose(String contract, String side, BigDecimal pnl) { |
| | | if (!strategyActive) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | log.info("[GateGrid] 平仓盈亏累计, side:{}, pnl:{}, cumulativePnl:{}", side, pnl, cumulativePnl); |
| | | checkStopConditions(); |
| | | } |
| | | log.info("[Gate] pnl+{}, side:{}, total:{}", pnl, side, cumulativePnl); |
| | | |
| | | /** |
| | | * 检查策略停止条件。满足任一即置 strategyActive=false: |
| | | * <ul> |
| | | * <li>累计盈利 ≥ overallTp</li> |
| | | * <li>累计亏损 ≤ -maxLoss</li> |
| | | * </ul> |
| | | */ |
| | | private void checkStopConditions() { |
| | | if (cumulativePnl.compareTo(overallTp) >= 0) { |
| | | log.info("[GateGrid] 累计止盈 {} 达到 {} USDT,停止策略", cumulativePnl, overallTp); |
| | | strategyActive = false; |
| | | return; |
| | | } |
| | | if (cumulativePnl.compareTo(maxLoss.negate()) <= 0) { |
| | | log.info("[GateGrid] 累计亏损 {} 达到上限 {} USDT,停止策略", cumulativePnl, maxLoss); |
| | | strategyActive = false; |
| | | if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { |
| | | log.info("[Gate] TP reached {}→STOPPED", cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) { |
| | | log.info("[Gate] loss {}→STOPPED", cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 首次多空双开。使用当前 K 线价格以市价单同时开多和开空, |
| | | * 开仓成功后立即为每个方向创建止盈条件单。 |
| | | */ |
| | | private void dualOpenPositions() { |
| | | if (lastKlinePrice == null) { |
| | | log.warn("[GateGrid] K线价格未就绪,跳过双开"); |
| | | dualOpened = false; |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder longOrder = new FuturesOrder(); |
| | | longOrder.setContract(contract); |
| | | longOrder.setSize(quantity); |
| | | longOrder.setPrice("0"); |
| | | longOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | longOrder.setText("t-grid-long-init"); |
| | | FuturesOrder longResult = futuresApi.createFuturesOrder(SETTLE, longOrder, null); |
| | | longEntryPrice = safeDecimal(longResult.getFillPrice()); |
| | | longActive = true; |
| | | log.info("[GateGrid] 开多成功, price: {}, id: {}", longEntryPrice, longResult.getId()); |
| | | placeLongTp(longEntryPrice); |
| | | // ---- reopen with retry ---- |
| | | |
| | | FuturesOrder shortOrder = new FuturesOrder(); |
| | | shortOrder.setContract(contract); |
| | | shortOrder.setSize(negateQuantity(quantity)); |
| | | shortOrder.setPrice("0"); |
| | | shortOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | shortOrder.setText("t-grid-short-init"); |
| | | FuturesOrder shortResult = futuresApi.createFuturesOrder(SETTLE, shortOrder, null); |
| | | shortEntryPrice = safeDecimal(shortResult.getFillPrice()); |
| | | shortActive = true; |
| | | log.info("[GateGrid] 开空成功, price: {}, id: {}", shortEntryPrice, shortResult.getId()); |
| | | placeShortTp(shortEntryPrice); |
| | | |
| | | printGridInfo(); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 双开失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | strategyActive = false; |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 双开异常", e); |
| | | strategyActive = false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 补开多头仓位。多头被止盈平掉后调用,市价重新开多并创建止盈单。 |
| | | */ |
| | | private void reopenLongPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | private void tryReopenLong(int retry) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (longActive) { |
| | | log.warn("[GateGrid] 多头已存在,跳过补开"); |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder longOrder = new FuturesOrder(); |
| | | longOrder.setContract(contract); |
| | | longOrder.setSize(quantity); |
| | | longOrder.setPrice("0"); |
| | | longOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | longOrder.setText("t-grid-long-reopen"); |
| | | FuturesOrder longResult = futuresApi.createFuturesOrder(SETTLE, longOrder, null); |
| | | longEntryPrice = safeDecimal(longResult.getFillPrice()); |
| | | |
| | | state = StrategyState.REOPENING_LONG; |
| | | executor.openLong(config.getQuantity(), () -> { |
| | | synchronized (this) { |
| | | longEntryPrice = lastKlinePrice; |
| | | longActive = true; |
| | | log.info("[GateGrid] 补开多成功, price: {}, id: {}", longEntryPrice, longResult.getId()); |
| | | placeLongTp(longEntryPrice); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 补开多失败", e); |
| | | } |
| | | executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | "close-long-position", "close_long"); |
| | | longReopenFails = 0; |
| | | if (state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | log.info("[Gate] long reopened, price:{}", longEntryPrice); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 补开空头仓位。空头被止盈平掉后调用,市价重新开空并创建止盈单。 |
| | | */ |
| | | private void reopenShortPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | private void tryReopenShort(int retry) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (shortActive) { |
| | | log.warn("[GateGrid] 空头已存在,跳过补开"); |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder shortOrder = new FuturesOrder(); |
| | | shortOrder.setContract(contract); |
| | | shortOrder.setSize(negateQuantity(quantity)); |
| | | shortOrder.setPrice("0"); |
| | | shortOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | shortOrder.setText("t-grid-short-reopen"); |
| | | FuturesOrder shortResult = futuresApi.createFuturesOrder(SETTLE, shortOrder, null); |
| | | shortEntryPrice = safeDecimal(shortResult.getFillPrice()); |
| | | |
| | | state = StrategyState.REOPENING_SHORT; |
| | | executor.openShort(negate(config.getQuantity()), () -> { |
| | | synchronized (this) { |
| | | shortEntryPrice = lastKlinePrice; |
| | | shortActive = true; |
| | | log.info("[GateGrid] 补开空成功, price: {}, id: {}", shortEntryPrice, shortResult.getId()); |
| | | placeShortTp(shortEntryPrice); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 补开空失败", e); |
| | | } |
| | | executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | "close-short-position", "close_short"); |
| | | shortReopenFails = 0; |
| | | if (state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | log.info("[Gate] short reopened, price:{}", shortEntryPrice); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 创建多头止盈条件单。 |
| | | * 触发价 = entryPrice × (1 + gridRate),价格 ≥ 触发价时平多。 |
| | | */ |
| | | private void placeLongTp(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-long-position", "close_long"); |
| | | log.info("[GateGrid] 多头止盈已设置, TP:{}", tpPrice); |
| | | // ---- util ---- |
| | | |
| | | private BigDecimal longTpPrice() { |
| | | return lastKlinePrice.multiply(BigDecimal.ONE.add(config.getGridRate())) |
| | | .setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 创建空头止盈条件单。 |
| | | * 触发价 = entryPrice × (1 - gridRate),价格 ≤ 触发价时平空。 |
| | | */ |
| | | private void placeShortTp(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-short-position", "close_short"); |
| | | log.info("[GateGrid] 空头止盈已设置, TP:{}", tpPrice); |
| | | private BigDecimal shortTpPrice() { |
| | | return lastKlinePrice.multiply(BigDecimal.ONE.subtract(config.getGridRate())) |
| | | .setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 通过 Gate REST API 创建止盈条件单。 |
| | | * |
| | | * @param triggerPrice 触发价格 |
| | | * @param rule 触发规则(1: ≥, 2: ≤) |
| | | * @param orderType 止盈止损类型(close-long-position / close-short-position) |
| | | * @param autoSize 双仓平仓方向(close_long / close_short) |
| | | */ |
| | | private void placePriceTriggeredOrder(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | FuturesPriceTriggeredOrder order = buildTriggeredOrder(triggerPrice, rule, orderType, autoSize); |
| | | try { |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[GateGrid] 止盈条件单已创建, triggerPrice:{}, orderType:{}, autoSize:{}, id:{}", |
| | | triggerPrice, orderType, autoSize, response.getId()); |
| | | } catch (GateApiException e) { |
| | | if ("AUTO_USER_EXIST_POSITION_ORDER".equals(e.getErrorLabel())) { |
| | | log.warn("[GateGrid] 止盈条件单已存在,取消旧单后重试"); |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[GateGrid] 止盈条件单重试成功, triggerPrice:{}, orderType:{}, autoSize:{}, id:{}", |
| | | triggerPrice, orderType, autoSize, response.getId()); |
| | | } catch (Exception retryEx) { |
| | | log.error("[GateGrid] 止盈条件单重试失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, retryEx); |
| | | } |
| | | } else { |
| | | log.error("[GateGrid] 止盈条件单创建失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, e); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 止盈条件单创建失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, e); |
| | | } |
| | | private String negate(String qty) { |
| | | return qty.startsWith("-") ? qty.substring(1) : "-" + qty; |
| | | } |
| | | |
| | | private FuturesPriceTriggeredOrder buildTriggeredOrder(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | FuturesPriceTrigger trigger = new FuturesPriceTrigger(); |
| | | trigger.setStrategyType(FuturesPriceTrigger.StrategyTypeEnum.NUMBER_0); |
| | | trigger.setPriceType(FuturesPriceTrigger.PriceTypeEnum.NUMBER_0); |
| | | trigger.setPrice(triggerPrice.toString()); |
| | | trigger.setRule(rule); |
| | | trigger.setExpiration(0); |
| | | |
| | | FuturesInitialOrder initial = new FuturesInitialOrder(); |
| | | initial.setContract(contract); |
| | | initial.setSize(0L); |
| | | initial.setPrice("0"); |
| | | initial.setTif(FuturesInitialOrder.TifEnum.IOC); |
| | | initial.setReduceOnly(true); |
| | | initial.setAutoSize(autoSize); |
| | | |
| | | FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder(); |
| | | order.setTrigger(trigger); |
| | | order.setInitial(initial); |
| | | order.setOrderType(orderType); |
| | | return order; |
| | | } |
| | | |
| | | /** |
| | | * 打印当前网格配置和入场信息。 |
| | | */ |
| | | private void printGridInfo() { |
| | | BigDecimal longTp = BigDecimal.ZERO; |
| | | BigDecimal shortTp = BigDecimal.ZERO; |
| | | if (longEntryPrice != null) { |
| | | longTp = longEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | if (shortEntryPrice != null) { |
| | | shortTp = shortEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | log.info("========== Gate 网格开仓 =========="); |
| | | log.info("合约: {} 杠杆: {}x {}", contract, leverage, marginMode); |
| | | log.info("多头入场: {} TP: {}", longEntryPrice, longTp); |
| | | log.info("空头入场: {} TP: {}", shortEntryPrice, shortTp); |
| | | log.info("数量: {} 网格间距: {}%", quantity, gridRate.multiply(new BigDecimal("100"))); |
| | | log.info("整体止盈: {} USDT 最大亏损: {} USDT", overallTp, maxLoss); |
| | | log.info("====================================="); |
| | | } |
| | | |
| | | /** 对数量取反(开多用正数,开空用负数) */ |
| | | private String negateQuantity(String qty) { |
| | | if (qty.startsWith("-")) { |
| | | return qty.substring(1); |
| | | } |
| | | return "-" + qty; |
| | | } |
| | | |
| | | /** 安全转换字符串为 BigDecimal,null 返回 0 */ |
| | | private BigDecimal safeDecimal(String val) { |
| | | if (val == null || val.isEmpty()) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return new BigDecimal(val); |
| | | } |
| | | |
| | | public BigDecimal getLastKlinePrice() { |
| | | return lastKlinePrice; |
| | | } |
| | | |
| | | public boolean isStrategyActive() { |
| | | return strategyActive; |
| | | } |
| | | |
| | | public BigDecimal getCumulativePnl() { |
| | | return cumulativePnl; |
| | | } |
| | | |
| | | public Long getUserId() { |
| | | return userId; |
| | | } |
| | | public BigDecimal getLastKlinePrice() { return lastKlinePrice; } |
| | | public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; } |
| | | public BigDecimal getCumulativePnl() { return cumulativePnl; } |
| | | public Long getUserId() { return userId; } |
| | | public StrategyState getState() { return state; } |
| | | } |
| | |
| | | |
| | | /** |
| | | * Gate WebSocket 连接管理器。 |
| | | * 负责建立连接、心跳检测、重连,将频道逻辑委托给 {@link GateChannelHandler} 实现类。 |
| | | * |
| | | * <h3>频道处理</h3> |
| | | * 每个频道由独立的 Handler 类负责:订阅、取消订阅、消息解析。 |
| | | * 运行时将消息按 channel 字段路由到对应 Handler。 |
| | | * <h3>职责</h3> |
| | | * 负责 TCP 连接的建立、维持和恢复。频道逻辑(订阅/解析)全部委托给 {@link GateChannelHandler} 实现类。 |
| | | * |
| | | * <h3>生命周期</h3> |
| | | * <pre> |
| | | * init() → connect() → startHeartbeat() |
| | | * destroy() → unsubscribe 所有 handler → closeBlocking() → shutdown 线程池 |
| | | * onClose() → reconnectWithBackoff() (最多 3 次,指数退避) |
| | | * </pre> |
| | | * |
| | | * <h3>消息路由</h3> |
| | | * <pre> |
| | | * onMessage → handleMessage: |
| | | * 1. futures.pong → cancelPongTimeout |
| | | * 2. subscribe/unsubscribe → 日志 |
| | | * 3. error → 错误日志 |
| | | * 4. update/all → 遍历 channelHandlers → handler.handleMessage(response) |
| | | * </pre> |
| | | * |
| | | * <h3>心跳机制</h3> |
| | | * 采用双重检测:TCP 层的 WebSocket ping/pong + 应用层 futures.ping/futures.pong。 |
| | | * 10 秒未收到任何消息 → 发送 futures.ping;25 秒周期检查。 |
| | | * |
| | | * <h3>线程安全</h3> |
| | | * 连接状态用 AtomicBoolean(isConnected, isConnecting, isInitialized)。 |
| | | * 消息时间戳用 AtomicReference。心跳任务用 synchronized 保护。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | private static final String FUTURES_PONG = "futures.pong"; |
| | | private static final int HEARTBEAT_TIMEOUT = 10; |
| | | |
| | | private static final String WS_URL_MONIPAN = "wss://ws-testnet.gate.com/v4/ws/futures/usdt"; |
| | | private static final String WS_URL_SHIPAN = "wss://fx-ws.gateio.ws/v4/ws/usdt"; |
| | | private static final boolean isAccountType = false; |
| | | /** WebSocket 地址,由 GateConfig 提供 */ |
| | | private final String wsUrl; |
| | | |
| | | /** Java-WebSocket 客户端实例 */ |
| | | private WebSocketClient webSocketClient; |
| | | /** 心跳检测调度器 */ |
| | | private ScheduledExecutorService heartbeatExecutor; |
| | | /** 心跳超时 Future */ |
| | | private volatile ScheduledFuture<?> pongTimeoutFuture; |
| | | /** 最后收到消息的时间戳(毫秒) */ |
| | | private final AtomicReference<Long> lastMessageTime = new AtomicReference<>(System.currentTimeMillis()); |
| | | |
| | | /** 连接状态 */ |
| | | private final AtomicBoolean isConnected = new AtomicBoolean(false); |
| | | /** 连接中标记,防重入 */ |
| | | private final AtomicBoolean isConnecting = new AtomicBoolean(false); |
| | | /** 初始化标记,防重复 init */ |
| | | private final AtomicBoolean isInitialized = new AtomicBoolean(false); |
| | | |
| | | /** 频道处理器列表,通过 addChannelHandler 注册 */ |
| | | private final List<GateChannelHandler> channelHandlers = new ArrayList<>(); |
| | | |
| | | /** 重连等异步任务的缓存线程池(daemon 线程) */ |
| | | private final ExecutorService sharedExecutor = Executors.newCachedThreadPool(r -> { |
| | | Thread t = new Thread(r, "gate-ws-worker"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | public GateKlineWebSocketClient() { |
| | | public GateKlineWebSocketClient(String wsUrl) { |
| | | this.wsUrl = wsUrl; |
| | | } |
| | | |
| | | /** |
| | | * 注册频道处理器。需在 init() 前调用。 |
| | | */ |
| | | public void addChannelHandler(GateChannelHandler handler) { |
| | | channelHandlers.add(handler); |
| | | } |
| | | |
| | | /** |
| | | * 初始化:建立 WebSocket 连接 → 启动心跳。 |
| | | */ |
| | | public void init() { |
| | | if (!isInitialized.compareAndSet(false, true)) { |
| | | log.warn("GateKlineWebSocketClient 已经初始化过,跳过重复初始化"); |
| | | log.warn("[WS] already init, skip"); |
| | | return; |
| | | } |
| | | connect(); |
| | | startHeartbeat(); |
| | | } |
| | | |
| | | /** |
| | | * 销毁:取消订阅 → 关闭连接 → 关闭线程池。 |
| | | * <p>注意:先 closeBlocking 再 shutdown sharedExecutor, |
| | | * 避免 onClose 回调中的 reconnectWithBackoff 访问已关闭的线程池。 |
| | | */ |
| | | public void destroy() { |
| | | log.info("开始销毁GateKlineWebSocketClient"); |
| | | log.info("[WS] destroy..."); |
| | | |
| | | if (webSocketClient != null && webSocketClient.isOpen()) { |
| | | for (GateChannelHandler handler : channelHandlers) { |
| | |
| | | Thread.sleep(500); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("取消订阅等待被中断"); |
| | | log.warn("[WS] unsubscribe wait interrupted"); |
| | | } |
| | | } |
| | | |
| | |
| | | webSocketClient.closeBlocking(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("关闭WebSocket连接时被中断"); |
| | | log.warn("[WS] close interrupted"); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | shutdownExecutorGracefully(sharedExecutor); |
| | | |
| | | log.info("GateKlineWebSocketClient销毁完成"); |
| | | log.info("[WS] destroyed"); |
| | | } |
| | | |
| | | /** |
| | | * 建立 WebSocket 连接。使用 SSLContext 配置 TLS 协议。 |
| | | * 连接成功后依次订阅所有已注册的频道处理器。 |
| | | */ |
| | | private void connect() { |
| | | if (isConnecting.get() || !isConnecting.compareAndSet(false, true)) { |
| | | log.info("连接已在进行中,跳过重复连接请求"); |
| | | log.info("[WS] already connecting"); |
| | | return; |
| | | } |
| | | try { |
| | | SSLConfig.configureSSL(); |
| | | System.setProperty("https.protocols", "TLSv1.2,TLSv1.3"); |
| | | String WS_URL = isAccountType ? WS_URL_SHIPAN : WS_URL_MONIPAN; |
| | | URI uri = new URI(WS_URL); |
| | | URI uri = new URI(wsUrl); |
| | | if (webSocketClient != null) { |
| | | try { |
| | | webSocketClient.closeBlocking(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("关闭之前连接时被中断"); |
| | | } |
| | | try { webSocketClient.closeBlocking(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } |
| | | } |
| | | webSocketClient = new WebSocketClient(uri) { |
| | | @Override |
| | | public void onOpen(ServerHandshake handshake) { |
| | | log.info("Gate WebSocket连接成功"); |
| | | log.info("[WS] connected"); |
| | | isConnected.set(true); |
| | | isConnecting.set(false); |
| | | if (sharedExecutor != null && !sharedExecutor.isShutdown()) { |
| | |
| | | } |
| | | sendPing(); |
| | | } else { |
| | | log.warn("应用正在关闭,忽略WebSocket连接成功回调"); |
| | | log.warn("[WS] shutting down, ignore onOpen"); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | public void onClose(int code, String reason, boolean remote) { |
| | | log.warn("Gate WebSocket连接关闭: code={}, reason={}", code, reason); |
| | | log.warn("[WS] closed, 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(); |
| | | log.error("重连线程被中断", e); |
| | | } catch (Exception e) { |
| | | log.error("重连失败", e); |
| | | } |
| | | try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("[WS] reconnect fail", e); } |
| | | }); |
| | | } else { |
| | | log.warn("共享线程池已关闭,无法执行重连任务"); |
| | | log.warn("[WS] executor closed, no reconnect"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void onError(Exception ex) { |
| | | log.error("Gate WebSocket发生错误", ex); |
| | | log.error("[WS] error", ex); |
| | | isConnected.set(false); |
| | | } |
| | | }; |
| | | webSocketClient.connect(); |
| | | } catch (URISyntaxException e) { |
| | | log.error("WebSocket URI格式错误", e); |
| | | log.error("[WS] bad uri", e); |
| | | isConnecting.set(false); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 消息分发:先处理系统事件(pong/subscribe/error), |
| | | * 再把 update/all 事件路由到各 channelHandler。 |
| | | * <p>每个 handler 内部通过 channel 名称做二次匹配,匹配成功返回 true 则停止遍历。 |
| | | */ |
| | | private void handleMessage(String message) { |
| | | try { |
| | | JSONObject response = JSON.parseObject(message); |
| | |
| | | String event = response.getString("event"); |
| | | |
| | | if (FUTURES_PONG.equals(channel)) { |
| | | log.debug("收到futures.pong响应"); |
| | | log.debug("[WS] pong received"); |
| | | cancelPongTimeout(); |
| | | return; |
| | | } |
| | | |
| | | if ("subscribe".equals(event)) { |
| | | log.info("{} 频道订阅成功: {}", channel, response.getJSONObject("result")); |
| | | log.info("[WS] {} subscribed: {}", channel, response.getJSONObject("result")); |
| | | return; |
| | | } |
| | | if ("unsubscribe".equals(event)) { |
| | | log.info("{} 频道取消订阅成功", channel); |
| | | log.info("[WS] {} unsubscribed", channel); |
| | | return; |
| | | } |
| | | if ("error".equals(event)) { |
| | | JSONObject error = response.getJSONObject("error"); |
| | | log.error("{} 频道错误: code={}, msg={}", |
| | | log.error("[WS] {} error, code:{}, msg:{}", |
| | | channel, |
| | | error != null ? error.getInteger("code") : "N/A", |
| | | error != null ? error.getString("message") : response.getString("msg")); |
| | | return; |
| | | } |
| | | |
| | | if ("update".equals(event) || "all".equals(event)) { |
| | | for (GateChannelHandler handler : channelHandlers) { |
| | | if (handler.handleMessage(response)) { |
| | | return; |
| | | } |
| | | if (handler.handleMessage(response)) return; |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理WebSocket消息失败: {}", message, e); |
| | | log.error("[WS] handle msg fail: {}", message, e); |
| | | } |
| | | } |
| | | |
| | | // ---- heartbeat ---- |
| | | |
| | | private void startHeartbeat() { |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isTerminated()) { |
| | | heartbeatExecutor.shutdownNow(); |
| | | } |
| | | heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> { |
| | | Thread t = new Thread(r, "gate-ws-heartbeat"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isTerminated()) heartbeatExecutor.shutdownNow(); |
| | | heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "gate-ws-heartbeat"); t.setDaemon(true); return t; }); |
| | | heartbeatExecutor.scheduleWithFixedDelay(this::checkHeartbeatTimeout, 25, 25, TimeUnit.SECONDS); |
| | | } |
| | | |
| | | private synchronized void resetHeartbeatTimer() { |
| | | cancelPongTimeout(); |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) { |
| | | pongTimeoutFuture = heartbeatExecutor.schedule(this::checkHeartbeatTimeout, |
| | | HEARTBEAT_TIMEOUT, TimeUnit.SECONDS); |
| | | pongTimeoutFuture = heartbeatExecutor.schedule(this::checkHeartbeatTimeout, HEARTBEAT_TIMEOUT, TimeUnit.SECONDS); |
| | | } |
| | | } |
| | | |
| | | private void checkHeartbeatTimeout() { |
| | | if (!isConnected.get()) { |
| | | return; |
| | | } |
| | | if (System.currentTimeMillis() - lastMessageTime.get() >= HEARTBEAT_TIMEOUT * 1000L) { |
| | | sendPing(); |
| | | } |
| | | if (!isConnected.get()) return; |
| | | if (System.currentTimeMillis() - lastMessageTime.get() >= HEARTBEAT_TIMEOUT * 1000L) sendPing(); |
| | | } |
| | | |
| | | private void sendPing() { |
| | |
| | | pingMsg.put("time", System.currentTimeMillis() / 1000); |
| | | pingMsg.put("channel", FUTURES_PING); |
| | | webSocketClient.send(pingMsg.toJSONString()); |
| | | log.debug("发送futures.ping请求"); |
| | | log.debug("[WS] ping sent"); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("发送ping失败", e); |
| | | } |
| | | } catch (Exception e) { log.warn("[WS] ping fail", e); } |
| | | } |
| | | |
| | | private synchronized void cancelPongTimeout() { |
| | | if (pongTimeoutFuture != null && !pongTimeoutFuture.isDone()) { |
| | | pongTimeoutFuture.cancel(true); |
| | | } |
| | | if (pongTimeoutFuture != null && !pongTimeoutFuture.isDone()) pongTimeoutFuture.cancel(true); |
| | | } |
| | | |
| | | // ---- reconnect ---- |
| | | |
| | | private void reconnectWithBackoff() throws InterruptedException { |
| | | int attempt = 0; |
| | | int maxAttempts = 3; |
| | | int attempt = 0, maxAttempts = 3; |
| | | long delayMs = 5000; |
| | | while (attempt < maxAttempts) { |
| | | try { |
| | | Thread.sleep(delayMs); |
| | | connect(); |
| | | return; |
| | | } catch (Exception e) { |
| | | log.warn("第{}次重连失败", attempt + 1, e); |
| | | delayMs *= 2; |
| | | attempt++; |
| | | try { Thread.sleep(delayMs); connect(); return; } catch (Exception e) { log.warn("[WS] reconnect attempt {} fail", attempt + 1, e); delayMs *= 2; attempt++; } |
| | | } |
| | | } |
| | | log.error("超过最大重试次数({})仍未连接成功", maxAttempts); |
| | | log.error("[WS] reconnect exhausted after {} attempts", maxAttempts); |
| | | } |
| | | |
| | | private void shutdownExecutorGracefully(ExecutorService executor) { |
| | | if (executor == null || executor.isTerminated()) { |
| | | return; |
| | | } |
| | | try { |
| | | executor.shutdown(); |
| | | if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { |
| | | executor.shutdownNow(); |
| | | } |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | executor.shutdownNow(); |
| | | } |
| | | if (executor == null || executor.isTerminated()) return; |
| | | try { executor.shutdown(); if (!executor.awaitTermination(5, TimeUnit.SECONDS)) executor.shutdownNow(); } |
| | | catch (InterruptedException e) { Thread.currentThread().interrupt(); executor.shutdownNow(); } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.gateApi; |
| | | |
| | | import io.gate.gateapi.ApiClient; |
| | | import io.gate.gateapi.GateApiException; |
| | | import io.gate.gateapi.api.FuturesApi; |
| | | import io.gate.gateapi.models.FuturesInitialOrder; |
| | | import io.gate.gateapi.models.FuturesOrder; |
| | | import io.gate.gateapi.models.FuturesPriceTrigger; |
| | | import io.gate.gateapi.models.FuturesPriceTriggeredOrder; |
| | | import io.gate.gateapi.models.TriggerOrderResponse; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.ThreadPoolExecutor; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * Gate REST API 执行器。 |
| | | * |
| | | * <h3>设计目的</h3> |
| | | * WebSocket 消息在回调线程中处理(如 {@code WebSocketClient} 的 {@code onMessage} 线程)。 |
| | | * 下单 REST API 调用可能耗时数百毫秒,若同步执行会阻塞 WS 回调线程,导致心跳超时误判。 |
| | | * 本类将所有 REST 调用提交到独立线程池异步执行。 |
| | | * |
| | | * <h3>线程模型</h3> |
| | | * 单线程 ThreadPoolExecutor + 有界队列 64 + CallerRunsPolicy: |
| | | * <ul> |
| | | * <li><b>单线程</b>:保证下单顺序(开多→开空→止盈单),避免并发竞争</li> |
| | | * <li><b>有界队列 64</b>:防止堆积。极端行情下最多累积 64 个任务</li> |
| | | * <li><b>CallerRunsPolicy</b>:队列满时由提交线程直接同步执行,形成自然背压</li> |
| | | * <li><b>allowCoreThreadTimeOut</b>:60s 空闲后线程回收,不浪费资源</li> |
| | | * </ul> |
| | | * |
| | | * <h3>调用链</h3> |
| | | * <pre> |
| | | * GateGridTradeService.onKline → executor.openLong/openShort → REST API |
| | | * GateGridTradeService.onPositionUpdate → executor.openLong/openShort → REST API |
| | | * (每一次开仓后) → executor.placeTakeProfit → REST API |
| | | * </pre> |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class GateTradeExecutor { |
| | | |
| | | private static final String SETTLE = "usdt"; |
| | | |
| | | private final FuturesApi futuresApi; |
| | | private final String contract; |
| | | |
| | | /** 交易线程池:单线程 + 有界队列 + 背压策略 */ |
| | | private final ExecutorService executor; |
| | | |
| | | public GateTradeExecutor(ApiClient apiClient, String contract) { |
| | | this.futuresApi = new FuturesApi(apiClient); |
| | | this.contract = contract; |
| | | this.executor = new ThreadPoolExecutor( |
| | | 1, 1, |
| | | 60L, TimeUnit.SECONDS, |
| | | new LinkedBlockingQueue<>(64), |
| | | r -> { |
| | | Thread t = new Thread(r, "gate-trade-worker"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }, |
| | | new ThreadPoolExecutor.CallerRunsPolicy() |
| | | ); |
| | | ((ThreadPoolExecutor) executor).allowCoreThreadTimeOut(true); |
| | | } |
| | | |
| | | /** |
| | | * 优雅关闭:等待 10 秒,超时则强制中断。 |
| | | */ |
| | | public void shutdown() { |
| | | executor.shutdown(); |
| | | try { |
| | | executor.awaitTermination(10, TimeUnit.SECONDS); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | executor.shutdownNow(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 异步市价开多。 |
| | | * <p>创建 IOC 市价单(price=0),数量为正数。成功后调用 onSuccess 回调。 |
| | | * |
| | | * @param quantity 数量(正数,如 "10") |
| | | * @param onSuccess 成功后回调,在交易线程中执行 |
| | | */ |
| | | public void openLong(String quantity, Runnable onSuccess) { |
| | | executor.execute(() -> { |
| | | try { |
| | | FuturesOrder order = new FuturesOrder(); |
| | | order.setContract(contract); |
| | | order.setSize(quantity); |
| | | order.setPrice("0"); |
| | | 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(); |
| | | } catch (Exception e) { |
| | | log.error("[TradeExec] 开多失败", e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 异步市价开空。 |
| | | * <p>创建 IOC 市价单(price=0),size 需为负数。 |
| | | * |
| | | * @param negQuantity 负数数量(如 "-10") |
| | | * @param onSuccess 成功后回调 |
| | | */ |
| | | public void openShort(String negQuantity, Runnable onSuccess) { |
| | | executor.execute(() -> { |
| | | try { |
| | | FuturesOrder order = new FuturesOrder(); |
| | | order.setContract(contract); |
| | | order.setSize(negQuantity); |
| | | order.setPrice("0"); |
| | | 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(); |
| | | } catch (Exception e) { |
| | | log.error("[TradeExec] 开空失败", e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 异步创建止盈条件单。 |
| | | * <p>使用 Gate 的 PriceTriggeredOrder:服务器监控价格,达到触发价后自动平仓。 |
| | | * 如果账户已有同方向同规则的条件单(label=UNIQUE),自动清除后重试一次。 |
| | | * |
| | | * @param triggerPrice 触发价格 |
| | | * @param rule 触发规则(NUMBER_1: ≥ 触发价,NUMBER_2: ≤ 触发价) |
| | | * @param orderType stop 类型(close-long-position / close-short-position) |
| | | * @param autoSize 双仓平仓方向(close_long / close_short) |
| | | */ |
| | | public void placeTakeProfit(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | executor.execute(() -> { |
| | | FuturesPriceTriggeredOrder order = buildTriggeredOrder(triggerPrice, rule, orderType, autoSize); |
| | | try { |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[TradeExec] 止盈单已创建, tp:{}, orderType:{}, id:{}", |
| | | triggerPrice, orderType, response.getId()); |
| | | } catch (GateApiException e) { |
| | | if ("AUTO_USER_EXIST_POSITION_ORDER".equals(e.getErrorLabel())) { |
| | | log.warn("[TradeExec] 止盈单已存在,清除后重试"); |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[TradeExec] 止盈单重试成功, tp:{}, id:{}", triggerPrice, response.getId()); |
| | | } catch (Exception retryEx) { |
| | | log.error("[TradeExec] 止盈单重试失败", retryEx); |
| | | } |
| | | } else { |
| | | log.error("[TradeExec] 止盈单创建失败, tp:{}", triggerPrice, e); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[TradeExec] 止盈单创建失败, tp:{}", triggerPrice, e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 异步清除指定合约的所有止盈止损条件单。 |
| | | */ |
| | | public void cancelAllPriceTriggeredOrders() { |
| | | executor.execute(() -> { |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | log.info("[TradeExec] 已清除所有止盈止损单"); |
| | | } catch (Exception e) { |
| | | log.error("[TradeExec] 清除止盈止损单失败", e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 构建 FuturesPriceTriggeredOrder 对象。 |
| | | * <p>策略=0(价格触发),price_type=0(最新价),expiration=0(永不过期), |
| | | * tif=IOC(立即成交或取消),reduce_only=true(只减仓不开新仓)。 |
| | | */ |
| | | private FuturesPriceTriggeredOrder buildTriggeredOrder(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | FuturesPriceTrigger trigger = new FuturesPriceTrigger(); |
| | | trigger.setStrategyType(FuturesPriceTrigger.StrategyTypeEnum.NUMBER_0); |
| | | trigger.setPriceType(FuturesPriceTrigger.PriceTypeEnum.NUMBER_0); |
| | | trigger.setPrice(triggerPrice.toString()); |
| | | trigger.setRule(rule); |
| | | trigger.setExpiration(0); |
| | | |
| | | FuturesInitialOrder initial = new FuturesInitialOrder(); |
| | | initial.setContract(contract); |
| | | initial.setSize(0L); |
| | | initial.setPrice("0"); |
| | | initial.setTif(FuturesInitialOrder.TifEnum.IOC); |
| | | initial.setReduceOnly(true); |
| | | initial.setAutoSize(autoSize); |
| | | |
| | | FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder(); |
| | | order.setTrigger(trigger); |
| | | order.setInitial(initial); |
| | | order.setOrderType(orderType); |
| | | return order; |
| | | } |
| | | } |
| | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * Gate 模块入口管理类,作为 Spring Bean 负责组件的生命周期。 |
| | | * Gate 模块 Spring 入口,组装所有组件并管理生命周期。 |
| | | * |
| | | * <h3>启动流程</h3> |
| | | * <pre> |
| | | * @PostConstruct init(): |
| | | * 1. new GateGridTradeService(参数) → init() // 设杠杆、查余额、切双向持仓 |
| | | * 2. new GateKlineWebSocketClient → init() // 连接 WebSocket,订阅 K 线 + 仓位 |
| | | * 3. gridTradeService.startGrid() // 激活策略,等待 K 线触发首次双开 |
| | | * </pre> |
| | | * <h3>启动流程 ({@code @PostConstruct})</h3> |
| | | * <ol> |
| | | * <li>构建 {@link GateConfig}(Builder 模式,含 API 密钥、合约、策略参数)</li> |
| | | * <li>创建 {@link GateGridTradeService} → init():切持仓模式、清旧条件单、设杠杆</li> |
| | | * <li>创建 {@link GateKlineWebSocketClient} → 注册 3 个 Handler → init():建立 WS 连接</li> |
| | | * <li>gridTradeService.startGrid():激活策略,等待 K 线触发首次双开</li> |
| | | * </ol> |
| | | * |
| | | * <h3>销毁流程</h3> |
| | | * <pre> |
| | | * @PreDestroy destroy(): |
| | | * 1. gridTradeService.stopGrid() // 停止策略 |
| | | * 2. klinePriceClient.destroy() // 取消订阅 → 关闭 WS → 关闭线程池 |
| | | * </pre> |
| | | * <h3>销毁流程 ({@code @PreDestroy})</h3> |
| | | * <ol> |
| | | * <li>gridTradeService.stopGrid():取消条件单 → 关闭交易线程池</li> |
| | | * <li>wsClient.destroy():取消订阅 → 断开 WS → 关闭线程池</li> |
| | | * </ol> |
| | | * |
| | | * <h3>配置参数</h3> |
| | | * 当前配置在代码中硬编码: |
| | | * <ul> |
| | | * <li>合约: XAU_USDT, 杠杆: 30x, 全仓, 双向持仓</li> |
| | | * <li>网格间距: 0.35%, 整体止盈: 0.5 USDT, 最大亏损: 7.5 USDT</li> |
| | | * <li>数量: 10 张</li> |
| | | * </ul> |
| | | * <h3>配置</h3> |
| | | * 当前在代码中硬编码测试网参数。切换到生产网只需改为 {@code .isProduction(true)}。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | @Component |
| | | public class GateWebSocketClientManager { |
| | | |
| | | /** K 线 WebSocket 客户端 */ |
| | | private GateKlineWebSocketClient klinePriceClient; |
| | | /** 网格交易服务 */ |
| | | /** WebSocket 连接管理器 */ |
| | | private GateKlineWebSocketClient wsClient; |
| | | /** 网格交易策略服务 */ |
| | | private GateGridTradeService gridTradeService; |
| | | /** 统一配置 */ |
| | | private GateConfig config; |
| | | |
| | | /** Gate 测试网 API Key */ |
| | | private static final String API_KEY = "d90ca272391992b8e74f8f92cedb21ec"; |
| | | /** Gate 测试网 API Secret */ |
| | | private static final String API_SECRET = "1861e4f52de4bb53369ea3208d9ede38ece4777368030f96c77d27934c46c274"; |
| | | /** 合约 */ |
| | | private static final String CONTRACT = "XAUT_USDT"; |
| | | |
| | | /** |
| | | * Spring 容器启动后自动调用。初始化网格交易服务和 WebSocket 客户端。 |
| | | */ |
| | | @PostConstruct |
| | | public void init() { |
| | | log.info("开始初始化GateWebSocketClientManager"); |
| | | log.info("[GateMgr] init..."); |
| | | |
| | | try { |
| | | gridTradeService = new GateGridTradeService( |
| | | API_KEY, API_SECRET, |
| | | CONTRACT, |
| | | "30", |
| | | "cross", |
| | | "dual", |
| | | new BigDecimal("0.0035"), |
| | | new BigDecimal("0.5"), |
| | | new BigDecimal("7.5"), |
| | | "10" |
| | | ); |
| | | config = GateConfig.builder() |
| | | .apiKey("d90ca272391992b8e74f8f92cedb21ec") |
| | | .apiSecret("1861e4f52de4bb53369ea3208d9ede38ece4777368030f96c77d27934c46c274") |
| | | .contract("XAUT_USDT") |
| | | .leverage("30") |
| | | .marginMode("cross") |
| | | .positionMode("dual") |
| | | .gridRate(new BigDecimal("0.0035")) |
| | | .overallTp(new BigDecimal("0.5")) |
| | | .maxLoss(new BigDecimal("7.5")) |
| | | .quantity("10") |
| | | .isProduction(false) |
| | | .reopenMaxRetries(3) |
| | | .build(); |
| | | |
| | | gridTradeService = new GateGridTradeService(config); |
| | | gridTradeService.init(); |
| | | |
| | | klinePriceClient = new GateKlineWebSocketClient(); |
| | | klinePriceClient.addChannelHandler(new CandlestickChannelHandler(CONTRACT, gridTradeService)); |
| | | klinePriceClient.addChannelHandler(new PositionsChannelHandler(API_KEY, API_SECRET, CONTRACT, gridTradeService)); |
| | | klinePriceClient.addChannelHandler(new PositionClosesChannelHandler(API_KEY, API_SECRET, CONTRACT, gridTradeService)); |
| | | klinePriceClient.init(); |
| | | log.info("已初始化GateKlineWebSocketClient及3个频道Handler"); |
| | | wsClient = new GateKlineWebSocketClient(config.getWsUrl()); |
| | | wsClient.addChannelHandler(new CandlestickChannelHandler(config.getContract(), gridTradeService)); |
| | | wsClient.addChannelHandler(new PositionsChannelHandler( |
| | | config.getApiKey(), config.getApiSecret(), config.getContract(), gridTradeService)); |
| | | wsClient.addChannelHandler(new PositionClosesChannelHandler( |
| | | config.getApiKey(), config.getApiSecret(), config.getContract(), gridTradeService)); |
| | | wsClient.init(); |
| | | log.info("[GateMgr] ws connected, 3 handlers registered"); |
| | | |
| | | gridTradeService.startGrid(); |
| | | } catch (Exception e) { |
| | | log.error("初始化GateWebSocketClientManager失败", e); |
| | | log.error("[GateMgr] init fail", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Spring 容器销毁前自动调用。停止策略并关闭所有连接。 |
| | | */ |
| | | @PreDestroy |
| | | public void destroy() { |
| | | log.info("开始销毁GateWebSocketClientManager"); |
| | | |
| | | log.info("[GateMgr] destroy..."); |
| | | if (gridTradeService != null) { |
| | | gridTradeService.stopGrid(); |
| | | } |
| | | if (klinePriceClient != null) { |
| | | try { |
| | | klinePriceClient.destroy(); |
| | | log.info("已销毁GateKlineWebSocketClient"); |
| | | } catch (Exception e) { |
| | | log.error("销毁GateKlineWebSocketClient失败", e); |
| | | if (wsClient != null) { |
| | | wsClient.destroy(); |
| | | } |
| | | log.info("[GateMgr] destroyed"); |
| | | } |
| | | |
| | | log.info("GateWebSocketClientManager销毁完成"); |
| | | } |
| | | |
| | | public GateKlineWebSocketClient getKlineWebSocketClient() { |
| | | return klinePriceClient; |
| | | } |
| | | |
| | | public GateGridTradeService getGridTradeService() { |
| | | return gridTradeService; |
| | | } |
| | | public GateKlineWebSocketClient getKlineWebSocketClient() { return wsClient; } |
| | | public GateGridTradeService getGridTradeService() { return gridTradeService; } |
| | | } |
| | |
| | | |
| | | | 文件 | 类型 | 说明 | |
| | | |------|------|------| |
| | | | [GateWebSocketClientManager](#gatewebsocketclientmanager) | `@Component` | Spring 启动入口,生命周期管理 | |
| | | | [GateKlineWebSocketClient](#gateklinewebsocketclient) | WebSocket 连接管理 | 连接/心跳/重连/消息路由 | |
| | | | [GateGridTradeService](#gategridtradeservice) | 交易服务 | 网格策略 + REST 下单 | |
| | | | [GateWebSocketClientManager](#gatewebsocketclientmanager) | `@Component` | Spring 启动入口,组装组件 + 生命周期 | |
| | | | [GateConfig](#gateconfig) | 配置 | Builder 模式:API 密钥、合约、策略参数、环境切换 | |
| | | | [GateKlineWebSocketClient](#gateklinewebsocketclient) | WS 连接管理 | 连接/心跳/重连/消息路由 | |
| | | | [GateGridTradeService](#gategridtradeservice) | 交易服务 | 网格策略状态机 + 盈亏管理 | |
| | | | [GateTradeExecutor](#gatetradeexecutor) | 异步执行器 | 独立线程池执行 REST 下单,不阻塞 WS 回调线程 | |
| | | | [GateWebSocketClientMain](#gatewebsocketclientmain) | main 入口 | 独立测试启动 | |
| | | | [Example.java](#examplejava) | 示例 | Gate SDK 用法参考 | |
| | | |
| | |
| | | |
| | | | 文件 | 类型 | 说明 | |
| | | |------|------|------| |
| | | | `wsHandler/GateChannelHandler.java` | **接口** | `subscribe/unsubscribe/handleMessage/getChannelName` | |
| | | | `wsHandler/GateChannelHandler.java` | **接口** | subscribe / unsubscribe / handleMessage / getChannelName | |
| | | | `wsHandler/AbstractPrivateChannelHandler.java` | **抽象类** | 私有频道基类:HMAC-SHA512 签名 + 认证请求 | |
| | | | `wsHandler/handler/CandlestickChannelHandler.java` | 公开频道处理器 | K 线订阅/解析 → `onKline()` | |
| | | | `wsHandler/handler/PositionsChannelHandler.java` | 私有频道处理器 | 仓位推送解析 → `onPositionUpdate()` | |
| | | | `wsHandler/handler/PositionClosesChannelHandler.java` | 私有频道处理器 | 平仓推送解析 → `onPositionClose()` | |
| | | | `wsHandler/handler/CandlestickChannelHandler.java` | 公开频道 | K 线解析 → `onKline()` | |
| | | | `wsHandler/handler/PositionsChannelHandler.java` | 私有频道 | 仓位推送 → `onPositionUpdate()` | |
| | | | `wsHandler/handler/PositionClosesChannelHandler.java` | 私有频道 | 平仓推送 → `onPositionClose()` | |
| | | |
| | | --- |
| | | |
| | |
| | | │ GateWebSocketClientManager │ |
| | | │ (Spring @Component) │ |
| | | │ │ |
| | | │ @PostConstruct init(): │ |
| | | │ ┌──────────────────────┐ ┌───────────────────────────────┐ │ |
| | | │ │ GateGridTradeService │ │ GateKlineWebSocketClient │ │ |
| | | │ │ ▶ init() │ │ ▶ addChannelHandler(x3) │ │ |
| | | │ │ ▶ startGrid() │ │ ▶ init() │ │ |
| | | │ └──────┬───────────────┘ └──────────┬────────────────────┘ │ |
| | | │ │ REST API │ WebSocket (委托路由) │ |
| | | │ ▼ ▼ │ |
| | | │ Gate Testnet API Gate Testnet WS │ |
| | | │ (https://api-testnet...) (wss://ws-testnet.gate.com) │ |
| | | │ ┌──────────────────────┐ │ |
| | | │ │ CandlestickHandler │ → onKline │ |
| | | │ │ PositionsHandler │ → onPos │ |
| | | │ │ PositionClosesHandler │ → onClose │ |
| | | │ └──────────────────────┘ │ |
| | | │ GateConfig.builder() → GateGridTradeService + WS Client │ |
| | | │ │ │ │ │ |
| | | │ │ REST BasePath │ GateTradeExecutor │ WS URL │ |
| | | │ ▼ ▼ ▼ │ |
| | | │ Gate API (REST) 独立线程池 (async) Gate WebSocket │ |
| | | │ ┌──────────────┐ │ |
| | | │ │Candlestick H │ │ |
| | | │ │Positions H │ │ |
| | | │ │PosCloses H │ │ |
| | | │ └──────────────┘ │ |
| | | └──────────────────────────────────────────────────────────────────┘ |
| | | ``` |
| | | |
| | |
| | | ## 数据流 |
| | | |
| | | ``` |
| | | WebSocket 推送 |
| | | WebSocket → GateKlineWebSocketClient.handleMessage → 路由 dispatch |
| | | │ |
| | | ├─ futures.candlesticks (update) [公开] |
| | | │ └─ CandlestickChannelHandler.handleMessage() |
| | | │ ├─ 解析: o/h/l/c/v/a/t/w |
| | | │ ├─ 打印 K 线日志 |
| | | │ └─ GateGridTradeService.onKline(closePx) |
| | | │ ├─ 首次 → dualOpenPositions() → 开多 + 开空 + TP 单 |
| | | ├─ futures.pong → cancelPongTimeout |
| | | ├─ subscribe / unsubscribe / error → log |
| | | │ |
| | | ├─ futures.candlesticks (公开) |
| | | │ └─ CandlestickChannelHandler |
| | | │ └─ gridTradeService.onKline(closePx) |
| | | │ ├─ state=WAITING_KLINE → 异步双开 + 止盈单 |
| | | │ └─ 后续 → 仅缓存 lastKlinePrice |
| | | │ |
| | | ├─ futures.positions (update) [私有,HMAC-SHA512] |
| | | │ └─ PositionsChannelHandler.handleMessage() |
| | | │ ├─ 解析: contract/mode/size/entry_price |
| | | │ └─ GateGridTradeService.onPositionUpdate(...) |
| | | │ ├─ size=0 && longActive → reopenLongPosition() |
| | | │ ├─ size=0 && shortActive → reopenShortPosition() |
| | | │ └─ size>0 → 确认仓位活跃 |
| | | ├─ futures.positions (私有, HMAC-SHA512) |
| | | │ └─ PositionsChannelHandler |
| | | │ └─ gridTradeService.onPositionUpdate(mode, size, entryPrice) |
| | | │ ├─ size=0 && longActive → tryReopenLong() |
| | | │ └─ size=0 && shortActive → tryReopenShort() |
| | | │ |
| | | ├─ futures.position_closes (update) [私有,HMAC-SHA512] |
| | | │ └─ PositionClosesChannelHandler.handleMessage() |
| | | │ ├─ 解析: contract/side/pnl |
| | | │ └─ GateGridTradeService.onPositionClose(pnl) |
| | | ├─ futures.position_closes (私有, HMAC-SHA512) |
| | | │ └─ PositionClosesChannelHandler |
| | | │ └─ gridTradeService.onPositionClose(side, pnl) |
| | | │ └─ cumulativePnl += pnl → checkStopConditions() |
| | | │ |
| | | ├─ futures.pong |
| | | │ └─ cancelPongTimeout() |
| | | │ |
| | | └─ subscribe/unsubscribe/error |
| | | └─ 日志输出 |
| | | └─ 所有下单操作 |
| | | └─ GateTradeExecutor (单线程 + 64队列 + CallerRunsPolicy) |
| | | ├─ openLong/openShort → 市价单 (REST) |
| | | └─ placeTakeProfit → 条件单 (REST) |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 策略时序 |
| | | |
| | | ### 阶段 1:启动与初始化 |
| | | |
| | | ``` |
| | | Spring 启动 |
| | | → GateWebSocketClientManager.init() |
| | | → GateGridTradeService.init() |
| | | → REST: 查用户ID |
| | | → REST: 查账户 → 如需要切换持仓模式 (dual) |
| | | → REST: 清除旧止盈止损条件单 |
| | | → REST: 设杠杆 30x cross |
| | | → REST: 打印账户余额 |
| | | → GateKlineWebSocketClient |
| | | → addChannelHandler: Candlestick + Positions + PositionCloses |
| | | → init() → connect() |
| | | → onOpen: 遍历 handlers.subscribe() + sendPing() |
| | | → GateGridTradeService.startGrid() |
| | | → strategyActive = true |
| | | ``` |
| | | |
| | | ### 阶段 2:首次开仓 |
| | | |
| | | ``` |
| | | K 线推送 closePrice=4711 |
| | | → onKline(4711) |
| | | → dualOpened = true |
| | | → dualOpenPositions() |
| | | → REST: 市价开多 10 张 → longEntryPrice=4711 |
| | | → REST: 创建多头止盈单 TP=4711×1.0035=4727 (close-long-position) |
| | | → REST: 市价开空 10 张 → shortEntryPrice=4711 |
| | | → REST: 创建空头止盈单 TP=4711×0.9965=4694 (close-short-position) |
| | | → longActive=true, shortActive=true |
| | | ``` |
| | | |
| | | ### 阶段 3:止盈触发 → 补仓 |
| | | |
| | | ``` |
| | | 仓位推送: mode=dual_long, size=0 (多头被服务器止盈平掉了) |
| | | → longActive=true && size=0 |
| | | → reopenLongPosition() |
| | | → REST: 市价补开多 10 张 |
| | | → REST: 创建新的多头止盈单 |
| | | |
| | | 仓位推送: mode=dual_short, size=0 (空头被服务器止盈平掉了) |
| | | → shortActive=true && size=0 |
| | | → reopenShortPosition() |
| | | → REST: 市价补开空 10 张 |
| | | → REST: 创建新的空头止盈单 |
| | | ``` |
| | | |
| | | > 注意:只补被平掉的方向(另一方向不受影响),不双方向重新开。补仓时检查 `longActive` / `shortActive` 防重复开仓。 |
| | | |
| | | ### 阶段 4:停止条件 |
| | | |
| | | ``` |
| | | 平仓推送: pnl=+0.2 |
| | | → cumulativePnl = 0.2, 未达阈值,继续 |
| | | |
| | | 平仓推送: pnl=+0.4 |
| | | → cumulativePnl = 0.6 ≥ 0.5 → strategyActive=false |
| | | |
| | | 平仓推送: pnl=-8 |
| | | → cumulativePnl = -8 ≤ -7.5 → strategyActive=false |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## GateWebSocketClientManager |
| | | |
| | | **角色**: Spring Bean 入口,组装并管理所有 Gate 模块组件。 |
| | | |
| | | **关键方法**: |
| | | - `init()`: 创建 `GateGridTradeService` → 初始化 → 创建 `GateKlineWebSocketClient` + 注册 3 个 Handler → 启动策略 |
| | | - `destroy()`: 停止策略 → 关闭 WebSocket |
| | | |
| | | **当前参数**(硬编码在 `init()` 中): |
| | | |
| | | | 参数 | 值 | 说明 | |
| | | |------|-----|------| |
| | | | 合约 | XAUT_USDT | 黄金(测试网) | |
| | | | 杠杆 | 30x | 全仓模式 | |
| | | | 持仓模式 | dual | 双向持仓 | |
| | | | 网格间距 | 0.0035 | 0.35% | |
| | | | 整体止盈 | 0.5 USDT | | |
| | | | 最大亏损 | 7.5 USDT | 本金 50×15% | |
| | | | 下单量 | 10 张 | | |
| | | |
| | | --- |
| | | |
| | | ## GateKlineWebSocketClient |
| | | |
| | | **角色**: WebSocket 连接管理器。负责建立连接、心跳检测、指数退避重连。频道逻辑委托给 `GateChannelHandler` 实现类。 |
| | | |
| | | **Handler 路由**: |
| | | |
| | | | 频道 | Handler | 认证 | 回调 | |
| | | |------|---------|------|------| |
| | | | `futures.candlesticks` | `CandlestickChannelHandler` | 否 | `onKline(closePx)` | |
| | | | `futures.positions` | `PositionsChannelHandler` | HMAC-SHA512 | `onPositionUpdate(mode, size, entryPrice)` | |
| | | | `futures.position_closes` | `PositionClosesChannelHandler` | HMAC-SHA512 | `onPositionClose(side, pnl)` | |
| | | |
| | | **消息路由** (`handleMessage`): |
| | | |
| | | | channel | event | 处理 | |
| | | |---------|-------|------| |
| | | | `futures.pong` | — | `cancelPongTimeout()` | |
| | | | — | `subscribe` | 打日志 | |
| | | | — | `unsubscribe` | 打日志 | |
| | | | — | `error` | 打错误日志 | |
| | | | 任意 | `update`/`all` | 遍历 channelHandlers → `handler.handleMessage()` | |
| | | |
| | | **签名算法**: |
| | | |
| | | ``` |
| | | message = "channel={channel}&event=subscribe&time={秒级时间戳}" |
| | | SIGN = Hex(HmacSHA512(apiSecret.getBytes(UTF-8), message.getBytes(UTF-8))) |
| | | ``` |
| | | |
| | | **连接管理**: |
| | | - 心跳: 10 秒超时,每 25 秒检查,超时发 ping |
| | | - 重连: 指数退避,最多 3 次,初始 5 秒 |
| | | - 关闭: 遍历 `handler.unsubscribe()` → 等 500ms → `closeBlocking()` → `sharedExecutor.shutdown()` |
| | | |
| | | --- |
| | | |
| | |
| | | ``` |
| | | GateChannelHandler (接口) |
| | | ├── CandlestickChannelHandler (公开频道) |
| | | └── AbstractPrivateChannelHandler (抽象类) (私有频道基类) |
| | | └── AbstractPrivateChannelHandler (私有频道基类: HMAC-SHA512) |
| | | ├── PositionsChannelHandler |
| | | └── PositionClosesChannelHandler |
| | | ``` |
| | | |
| | | - **subscribe**: 发送订阅请求。私有Handler自动附加 auth 字段 |
| | | - **unsubscribe**: 发送取消订阅请求(私有频道也带签名认证) |
| | | - **handleMessage**: 解析推送数据并回调GateGridTradeService,返回true表示已处理 |
| | | - 消息路由: update/all事件 → 遍历channelHandlers → handler内部二次匹配channel名 → 匹配成功回调并停止遍历 |
| | | |
| | | --- |
| | | |
| | | ## 策略状态机 |
| | | |
| | | ``` |
| | | WAITING_KLINE ──onKline──→ OPENING ──双开成功──→ ACTIVE |
| | | │ │ │ |
| | | │ 双开失败 ├─ size=0 → REOPENING_L/S → ACTIVE |
| | | │ ├─ 补仓失败 retry → 仍失败 → STOPPED |
| | | │ └─ cumulativePnl≥TP 或 ≤-maxLoss → STOPPED |
| | | ▼ |
| | | STOPPED ←─────────────────────────────────────────┘ |
| | | ``` |
| | | |
| | | | 状态 | 含义 | |
| | | |------|------| |
| | | | `WAITING_KLINE` | 等待首次K线价格 | |
| | | | `OPENING` | 正在异步双开(开多+开空已提交到GateTradeExecutor) | |
| | | | `ACTIVE` | 网格运行中,等待止盈触发 | |
| | | | `REOPENING_LONG` | 正在补开多头 | |
| | | | `REOPENING_SHORT` | 正在补开空头 | |
| | | | `STOPPED` | 停止(盈利达标 / 亏损超限 / 异常退出) | |
| | | |
| | | --- |
| | | |
| | | ## 策略时序 |
| | | |
| | | ### 阶段 1:启动 |
| | | |
| | | ``` |
| | | Spring @PostConstruct |
| | | → GateConfig.builder()...build() |
| | | → GateGridTradeService(config) |
| | | → init(): 查ID → 查账户切持仓 → 清旧条件单 → 设杠杆 |
| | | → GateKlineWebSocketClient(config.getWsUrl()) |
| | | → addChannelHandler x3 → init() → connect() |
| | | → onOpen: handlers依次subscribe → sendPing |
| | | → gridTradeService.startGrid() → state=WAITING_KLINE |
| | | ``` |
| | | |
| | | ### 阶段 2:首次开仓 |
| | | |
| | | ``` |
| | | K线推送 → onKline(closePrice) → state=OPENING |
| | | → GateTradeExecutor.openLong → 市价开多 → onSuccess: longActive=true, 下TP单 |
| | | → GateTradeExecutor.openShort → 市价开空 → onSuccess: shortActive=true, 下TP单 |
| | | → 双开均完成 → state=ACTIVE |
| | | ``` |
| | | |
| | | ### 阶段 3:止盈触发 → 补仓 |
| | | |
| | | ``` |
| | | 仓位推送: dual_long, size=0 → longActive且无仓位 → tryReopenLong |
| | | → GateTradeExecutor.openLong → 市价补多 → onSuccess: 下新TP单 |
| | | |
| | | 仓位推送: dual_short, size=0 → shortActive且无仓位 → tryReopenShort |
| | | → GateTradeExecutor.openShort → 市价补空 → onSuccess: 下新TP单 |
| | | ``` |
| | | |
| | | > 止盈由 Gate 服务端条件单自动执行。只补被平掉的单方向,另一方不受影响。 |
| | | |
| | | ### 阶段 4:停止 |
| | | |
| | | ``` |
| | | 平仓推送: pnl=+0.6 → cumulativePnl=0.6 ≥ overallTp → state=STOPPED |
| | | 平仓推送: pnl=-8.0 → cumulativePnl=-8.0 ≤ -maxLoss → state=STOPPED |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## GateConfig |
| | | |
| | | **角色**: 统一配置中心。Builder模式管理所有参数,提供 REST/WS URL 环境自动切换。 |
| | | |
| | | **核心方法**: |
| | | - `getRestBasePath()`: isProduction ? 生产网 : 测试网 |
| | | - `getWsUrl()`: 同上 |
| | | |
| | | **配置项**(含默认值): |
| | | |
| | | | 参数 | 默认值 | 说明 | |
| | | |------|--------|------| |
| | | | contract | BTC_USDT | 合约 | |
| | | | leverage | 10 | 倍数 | |
| | | | marginMode | cross | 全仓 | |
| | | | positionMode | dual | 双向持仓 | |
| | | | gridRate | 0.0035 | 网格间距 0.35% | |
| | | | overallTp | 0.5 USDT | 整体止盈 | |
| | | | maxLoss | 7.5 USDT | 最大亏损 | |
| | | | quantity | 1 | 下单张数 | |
| | | | reopenMaxRetries | 3 | 补仓重试次数 | |
| | | |
| | | --- |
| | | |
| | | ## GateTradeExecutor |
| | | |
| | | **角色**: 独立线程池执行 REST API 下单,解决 WebSocket 回调线程阻塞问题。 |
| | | |
| | | **线程模型**: |
| | | - `ThreadPoolExecutor(1, 1, 60s, LinkedBlockingQueue(64), CallerRunsPolicy)` |
| | | - 单线程保序 + 有界队列防堆积 + CallerRuns背压 |
| | | |
| | | | 方法 | 说明 | |
| | | |------|------| |
| | | | `getChannelName()` | 返回频道名(如 `"futures.candlesticks"`) | |
| | | | `subscribe(ws)` | 发送订阅请求。私有频道自动附加 HMAC-SHA512 签名 | |
| | | | `unsubscribe(ws)` | 发送取消订阅请求 | |
| | | | `handleMessage(response)` | 解析推送数据并回调 `GateGridTradeService`,返回 `true` 表示已处理 | |
| | | | `openLong(qty, onSuccess)` | 异步 IOC 市价开多,成功回调 | |
| | | | `openShort(qty, onSuccess)` | 异步 IOC 市价开空 | |
| | | | `placeTakeProfit(trigger, rule, type, auto)` | 异步条件单。已存在则清除旧单重试 | |
| | | | `cancelAllPriceTriggeredOrders()` | 清除所有条件单 | |
| | | | `shutdown()` | 等待10秒,超时强制关闭 | |
| | | |
| | | --- |
| | | |
| | | ## GateGridTradeService |
| | | |
| | | **角色**: 使用 Gate SDK (`io.gate:gate-api:7.2.71`) 通过 REST API 执行合约下单。 |
| | | **角色**: 策略核心,使用 Gate SDK 管理状态和执行下单。 |
| | | |
| | | **状态机**: |
| | | **状态**: StrategyState enum + longActive/shortActive boolean |
| | | |
| | | ``` |
| | | strategyActive=false ──startGrid()──→ strategyActive=true (等待K线) |
| | | │ |
| | | onKline(price) |
| | | │ |
| | | dualOpened=true |
| | | dualOpenPositions() |
| | | │ |
| | | ┌───────────┴───────────┐ |
| | | ▼ ▼ |
| | | longActive=true shortActive=true |
| | | │ │ |
| | | 仓位推送 size=0 仓位推送 size=0 |
| | | (止盈触发平仓) (止盈触发平仓) |
| | | │ │ |
| | | ▼ ▼ |
| | | reopenLong() reopenShort() |
| | | │ │ |
| | | └───────────┬───────────┘ |
| | | │ |
| | | 平仓推送 pnl (futures.position_closes) |
| | | │ |
| | | checkStopConditions() |
| | | │ |
| | | ┌───────────┴───────────┐ |
| | | ▼ ▼ |
| | | cumulativePnl≥0.5 cumulativePnl≤-7.5 |
| | | strategyActive=false strategyActive=false |
| | | ``` |
| | | **回调方法**: |
| | | - `onKline(closePrice)`: 缓存价格,WAITING_KLINE状态下首次触发双开 |
| | | - `onPositionUpdate(mode, size, entryPrice)`: size=0且方向活跃 → 补仓 |
| | | - `onPositionClose(side, pnl)`: 累加盈亏,检查停止条件 |
| | | |
| | | **止盈计算**: |
| | | |
| | | | 方向 | 公式 | 触发条件 | order_type | auto_size | |
| | | |------|------|----------|------------|-----------| |
| | | | 多头 TP | entryPrice × (1 + gridRate) | 最新价 ≥ 触发价 | `close-long-position` | `close_long` | |
| | | | 空头 TP | entryPrice × (1 - gridRate) | 最新价 ≤ 触发价 | `close-short-position` | `close_short` | |
| | | | 方向 | 公式 | order_type | auto_size | |
| | | |------|------|------------|-----------| |
| | | | 多头 TP | entry × (1+gridRate) | `close-long-position` | `close_long` | |
| | | | 空头 TP | entry × (1-gridRate) | `close-short-position` | `close_short` | |
| | | |
| | | **REST API 调用**: |
| | | |
| | | | 操作 | API | 方法 | |
| | | |------|-----|------| |
| | | | 获取用户 ID | `GET /account/detail` | `AccountApi.getAccountDetail()` | |
| | | | 切持仓模式 | `POST /futures/{settle}/set_position_mode` | `FuturesApi.setPositionMode()` | |
| | | | 设杠杆 | `POST /futures/{settle}/positions/{contract}/leverage` | `FuturesApi.updateContractPositionLeverageCall()` | |
| | | | 查账户 | `GET /futures/{settle}/accounts` | `FuturesApi.listFuturesAccounts()` | |
| | | | 清除条件单 | `DELETE /futures/{settle}/price_orders` | `FuturesApi.cancelPriceTriggeredOrderList()` | |
| | | | 市价下单 | `POST /futures/{settle}/orders` (price=0, tif=IOC) | `FuturesApi.createFuturesOrder()` | |
| | | | 止盈条件单 | `POST /futures/{settle}/price_orders` | `FuturesApi.createPriceTriggeredOrder()` | |
| | | |
| | | **初始化顺序** (`init()`): |
| | | ``` |
| | | 1. 获取用户 ID |
| | | 2. 查账户 → 如需要切持仓模式 |
| | | 3. 清除旧的止盈止损条件单 |
| | | 4. 设杠杆 |
| | | 5. 打印账户余额 |
| | | ``` |
| | | | 切持仓模式 | `POST /futures/usdt/set_position_mode` | `FuturesApi.setPositionMode()` | |
| | | | 设杠杆 | `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()` | |
| | | |
| | | --- |
| | | |
| | | ## GateWebSocketClientMain |
| | | |
| | | **角色**: 独立的 `main()` 方法入口,通过 Spring XML 上下文启动。 |
| | | 独立 `main()` 方法入口,通过 Spring XML 上下文启动,运行后手动关闭。 |
| | | |
| | | --- |
| | | |
| | | ## Example.java |
| | | |
| | | Gate SDK 使用示例,展示 `FuturesApi` 的基本用法。仅作参考,不参与实际策略运行。 |
| | | Gate SDK 使用示例,展示 `FuturesApi` 的基本用法。仅作参考。 |
| | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.gateApi.GateGridTradeService; |
| | | import com.xcong.excoin.modules.gateApi.wsHandler.GateChannelHandler; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | |
| | | import java.nio.charset.StandardCharsets; |
| | | |
| | | /** |
| | | * 私有频道的抽象基类,封装 HMAC-SHA512 签名和认证请求构建。 |
| | | * 私有频道处理器的抽象基类。 |
| | | * |
| | | * <h3>封装内容</h3> |
| | | * <ul> |
| | | * <li>HMAC-SHA512 签名计算(UTF-8 编码)</li> |
| | | * <li>认证请求 JSON 构建(id/time/channel/payload/auth)</li> |
| | | * <li>subscribe / unsubscribe 的默认实现(含签名)</li> |
| | | * <li>用户 ID 获取(从 {@link GateGridTradeService#getUserId()})</li> |
| | | * </ul> |
| | | * |
| | | * <h3>签名算法</h3> |
| | | * {@code SIGN = Hex(HmacSHA512(secret_utf8, "channel={channel}&event={event}&time={timeSec}"_utf8))} |
| | | * |
| | | * <h3>子类</h3> |
| | | * {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionsChannelHandler}、 |
| | | * {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionClosesChannelHandler} |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | } |
| | | |
| | | @Override |
| | | public String getChannelName() { |
| | | return channelName; |
| | | } |
| | | public String getChannelName() { return channelName; } |
| | | |
| | | /** |
| | | * 发送带签名的订阅请求。 |
| | | * payload: [userId, contract],auth: {method:"api_key", KEY, SIGN} |
| | | */ |
| | | @Override |
| | | public void subscribe(WebSocketClient ws) { |
| | | long timeSec = System.currentTimeMillis() / 1000; |
| | | JSONObject msg = buildAuthRequest("subscribe", buildUid(), timeSec); |
| | | ws.send(msg.toJSONString()); |
| | | log.info("[{}] 已发送订阅请求(含认证),合约: {}", channelName, contract); |
| | | log.info("[{}] subscribed, contract:{}", channelName, contract); |
| | | } |
| | | |
| | | /** |
| | | * 发送带签名的取消订阅请求,与 subscribe 对称。 |
| | | */ |
| | | @Override |
| | | public void unsubscribe(WebSocketClient ws) { |
| | | JSONObject unsubscribeMsg = new JSONObject(); |
| | | unsubscribeMsg.put("time", System.currentTimeMillis() / 1000); |
| | | unsubscribeMsg.put("channel", channelName); |
| | | unsubscribeMsg.put("event", "unsubscribe"); |
| | | long timeSec = System.currentTimeMillis() / 1000; |
| | | JSONObject msg = new JSONObject(); |
| | | msg.put("id", timeSec * 1000000 + (System.currentTimeMillis() % 1000)); |
| | | msg.put("time", timeSec); |
| | | msg.put("channel", channelName); |
| | | msg.put("event", "unsubscribe"); |
| | | JSONArray payload = new JSONArray(); |
| | | payload.add(contract); |
| | | unsubscribeMsg.put("payload", payload); |
| | | ws.send(unsubscribeMsg.toJSONString()); |
| | | log.info("[{}] 已发送取消订阅请求,合约: {}", channelName, contract); |
| | | msg.put("payload", payload); |
| | | JSONObject auth = new JSONObject(); |
| | | auth.put("method", "api_key"); |
| | | auth.put("KEY", apiKey); |
| | | auth.put("SIGN", hs512Sign("unsubscribe", timeSec)); |
| | | msg.put("auth", auth); |
| | | ws.send(msg.toJSONString()); |
| | | log.info("[{}] unsubscribed, contract:{}", channelName, contract); |
| | | } |
| | | |
| | | protected GateGridTradeService getGridTradeService() { |
| | | return gridTradeService; |
| | | } |
| | | protected GateGridTradeService getGridTradeService() { return gridTradeService; } |
| | | protected String getContract() { return contract; } |
| | | |
| | | protected String getContract() { |
| | | return contract; |
| | | } |
| | | |
| | | /** |
| | | * 从策略服务获取用户 ID,用于私有频道订阅的 payload[0]。 |
| | | */ |
| | | private String buildUid() { |
| | | return gridTradeService != null && gridTradeService.getUserId() != null |
| | | ? String.valueOf(gridTradeService.getUserId()) : ""; |
| | | } |
| | | |
| | | /** |
| | | * 构建认证请求 JSON。包含 id、time、channel、event、payload[auth_user_id, contract]、auth 字段。 |
| | | */ |
| | | private JSONObject buildAuthRequest(String event, String uid, long timeSec) { |
| | | JSONObject msg = new JSONObject(); |
| | | msg.put("id", timeSec * 1000000 + (System.currentTimeMillis() % 1000)); |
| | |
| | | return msg; |
| | | } |
| | | |
| | | /** |
| | | * HMAC-SHA512 签名,使用 UTF-8 编码。 |
| | | */ |
| | | private String hs512Sign(String event, long timeSec) { |
| | | try { |
| | | String message = "channel=" + channelName + "&event=" + event + "&time=" + timeSec; |
| | |
| | | } |
| | | return hex.toString(); |
| | | } catch (Exception e) { |
| | | log.error("[{}] 签名计算失败", channelName, e); |
| | | log.error("[{}] sign fail", channelName, e); |
| | | return ""; |
| | | } |
| | | } |
| | |
| | | |
| | | /** |
| | | * WebSocket 频道处理器接口。 |
| | | * 每个 Gate 频道(K线/仓位/平仓)对应一个实现类, |
| | | * 负责该频道的订阅、取消订阅、消息处理。 |
| | | * |
| | | * <p>每个 Gate 频道对应一个实现类。新增频道只需实现此接口, |
| | | * 然后通过 {@code GateKlineWebSocketClient.addChannelHandler()} 注册即可。 |
| | | * |
| | | * <h3>实现类</h3> |
| | | * <ul> |
| | | * <li>{@code CandlestickChannelHandler} — 公开频道,K 线数据</li> |
| | | * <li>{@code AbstractPrivateChannelHandler} — 私有频道抽象基类(签名+认证)</li> |
| | | * <li>{@code PositionsChannelHandler} — 私有频道,仓位更新</li> |
| | | * <li>{@code PositionClosesChannelHandler} — 私有频道,平仓推送</li> |
| | | * </ul> |
| | | * |
| | | * <h3>路由机制</h3> |
| | | * {@code handleMessage()} 返回 {@code true} 表示消息已被该 handler 处理, |
| | | * 路由循环会停止遍历。返回 {@code false} 表示不匹配(channel 名不相等)。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | public interface GateChannelHandler { |
| | | |
| | | /** |
| | | * 频道名称,如 "futures.candlesticks" |
| | | */ |
| | | /** 频道名称,如 {@code "futures.candlesticks"} */ |
| | | String getChannelName(); |
| | | |
| | | /** |
| | | * 发送订阅请求 |
| | | */ |
| | | /** 发送订阅请求 */ |
| | | void subscribe(WebSocketClient ws); |
| | | |
| | | /** |
| | | * 发送取消订阅请求 |
| | | */ |
| | | /** 发送取消订阅请求 */ |
| | | void unsubscribe(WebSocketClient ws); |
| | | |
| | | /** |
| | | * 处理频道推送消息 |
| | | * 处理频道推送消息。 |
| | | * |
| | | * @param response WebSocket 推送的 JSON 消息 |
| | | * @return true 表示已处理,false 表示不匹配(频道名不对) |
| | | * @param response WebSocket 推送的完整 JSON |
| | | * @return true 表示已处理(循环停止),false 表示频道不匹配(继续遍历下一个 handler) |
| | | */ |
| | | boolean handleMessage(JSONObject response); |
| | | } |
| | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * K 线频道处理器。 |
| | | * 公开频道,无需认证。订阅 1m K 线,解析后回调 {@link GateGridTradeService#onKline}。 |
| | | * K 线(Candlestick)频道处理器。 |
| | | * |
| | | * <h3>特点</h3> |
| | | * 公开频道,无需 HMAC-SHA512 认证签名。订阅 1m 周期的 K 线数据。 |
| | | * |
| | | * <h3>数据流</h3> |
| | | * <pre> |
| | | * WebSocket 推送 update event |
| | | * → handleMessage() → 解析 OHLCV → log 打印 → gridTradeService.onKline(closePx) |
| | | * → 首次 K 线触发双开 |
| | | * → 后续 K 线仅缓存 lastKlinePrice 供补仓参考 |
| | | * </pre> |
| | | * |
| | | * <h3>订阅格式</h3> |
| | | * payload: {@code ["1m", contract]} |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | } |
| | | |
| | | @Override |
| | | public String getChannelName() { |
| | | return CHANNEL_NAME; |
| | | } |
| | | public String getChannelName() { return CHANNEL_NAME; } |
| | | |
| | | @Override |
| | | public void subscribe(WebSocketClient ws) { |
| | | JSONObject subscribeMsg = new JSONObject(); |
| | | subscribeMsg.put("time", System.currentTimeMillis() / 1000); |
| | | subscribeMsg.put("channel", CHANNEL_NAME); |
| | | subscribeMsg.put("event", "subscribe"); |
| | | JSONObject msg = new JSONObject(); |
| | | msg.put("time", System.currentTimeMillis() / 1000); |
| | | msg.put("channel", CHANNEL_NAME); |
| | | msg.put("event", "subscribe"); |
| | | JSONArray payload = new JSONArray(); |
| | | payload.add(INTERVAL); |
| | | payload.add(contract); |
| | | subscribeMsg.put("payload", payload); |
| | | ws.send(subscribeMsg.toJSONString()); |
| | | log.info("[{}] 已发送订阅请求,合约: {}, 周期: {}", CHANNEL_NAME, contract, INTERVAL); |
| | | msg.put("payload", payload); |
| | | ws.send(msg.toJSONString()); |
| | | log.info("[{}] subscribed, contract:{}, interval:{}", CHANNEL_NAME, contract, INTERVAL); |
| | | } |
| | | |
| | | @Override |
| | | public void unsubscribe(WebSocketClient ws) { |
| | | JSONObject unsubscribeMsg = new JSONObject(); |
| | | unsubscribeMsg.put("time", System.currentTimeMillis() / 1000); |
| | | unsubscribeMsg.put("channel", CHANNEL_NAME); |
| | | unsubscribeMsg.put("event", "unsubscribe"); |
| | | JSONObject msg = new JSONObject(); |
| | | msg.put("time", System.currentTimeMillis() / 1000); |
| | | msg.put("channel", CHANNEL_NAME); |
| | | msg.put("event", "unsubscribe"); |
| | | JSONArray payload = new JSONArray(); |
| | | payload.add(INTERVAL); |
| | | payload.add(contract); |
| | | unsubscribeMsg.put("payload", payload); |
| | | ws.send(unsubscribeMsg.toJSONString()); |
| | | log.info("[{}] 已发送取消订阅请求,合约: {}, 周期: {}", CHANNEL_NAME, contract, INTERVAL); |
| | | msg.put("payload", payload); |
| | | ws.send(msg.toJSONString()); |
| | | log.info("[{}] unsubscribed", CHANNEL_NAME); |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleMessage(JSONObject response) { |
| | | String channel = response.getString("channel"); |
| | | if (!CHANNEL_NAME.equals(channel)) { |
| | | return false; |
| | | } |
| | | if (!CHANNEL_NAME.equals(response.getString("channel"))) return false; |
| | | try { |
| | | JSONArray resultArray = response.getJSONArray("result"); |
| | | if (resultArray == null || resultArray.isEmpty()) { |
| | | log.warn("[{}] 数据为空", CHANNEL_NAME); |
| | | return true; |
| | | } |
| | | if (resultArray == null || resultArray.isEmpty()) { log.warn("[{}] empty", CHANNEL_NAME); return true; } |
| | | JSONObject data = resultArray.getJSONObject(0); |
| | | BigDecimal closePx = new BigDecimal(data.getString("c")); |
| | | long t = data.getLong("t"); |
| | | boolean windowClosed = data.getBooleanValue("w"); |
| | | |
| | | log.info("========== Gate K线数据 =========="); |
| | | log.info("名称(n): {}", data.getString("n")); |
| | | log.info("时间 : {}", DateUtil.TimeStampToDateTime(t)); |
| | | log.info("开盘(o): {}", data.getString("o")); |
| | | log.info("最高(h): {}", data.getString("h")); |
| | | log.info("最低(l): {}", data.getString("l")); |
| | | log.info("收盘(c): {}", data.getString("c")); |
| | | log.info("成交量(v): {}", data.getString("v")); |
| | | log.info("成交额(a): {}", data.getString("a")); |
| | | log.info("K线完结(w): {}", windowClosed); |
| | | log.info("=================================="); |
| | | log.info("========== Gate K线 =========="); |
| | | log.info("n:{} t:{} o:{} h:{} l:{} c:{} v:{} a:{} w:{}", |
| | | data.getString("n"), DateUtil.TimeStampToDateTime(data.getLong("t")), |
| | | data.getString("o"), data.getString("h"), data.getString("l"), |
| | | data.getString("c"), data.getString("v"), data.getString("a"), |
| | | data.getBooleanValue("w")); |
| | | log.info("=============================="); |
| | | |
| | | if (gridTradeService != null) { |
| | | gridTradeService.onKline(closePx); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[{}] 处理数据失败", CHANNEL_NAME, e); |
| | | } |
| | | if (gridTradeService != null) gridTradeService.onKline(closePx); |
| | | } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); } |
| | | return true; |
| | | } |
| | | } |
| | |
| | | |
| | | /** |
| | | * 平仓频道处理器。 |
| | | * 私有频道,需认证。解析平仓推送后回调 {@link GateGridTradeService#onPositionClose}。 |
| | | * |
| | | * <h3>数据用途</h3> |
| | | * 每笔平仓发生时推送 pnl(盈亏金额),累加到 {@code cumulativePnl} 用于判断策略停止条件: |
| | | * cumulativePnl ≥ overallTp(达到止盈目标)或 ≤ -maxLoss(超过亏损上限)。 |
| | | * |
| | | * <h3>推送字段</h3> |
| | | * contract, side(long / short), pnl(该次平仓的盈亏,如 "+0.2" 或 "-0.1") |
| | | * |
| | | * <h3>注意</h3> |
| | | * 平仓盈亏来自服务器端,不受本地计算误差影响。这是策略停止的唯一盈亏判断来源。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | |
| | | @Override |
| | | public boolean handleMessage(JSONObject response) { |
| | | String channel = response.getString("channel"); |
| | | if (!CHANNEL_NAME.equals(channel)) { |
| | | return false; |
| | | } |
| | | if (!CHANNEL_NAME.equals(response.getString("channel"))) return false; |
| | | try { |
| | | JSONArray resultArray = response.getJSONArray("result"); |
| | | if (resultArray == null || resultArray.isEmpty()) { |
| | | return true; |
| | | } |
| | | if (resultArray == null || resultArray.isEmpty()) return true; |
| | | for (int i = 0; i < resultArray.size(); i++) { |
| | | JSONObject item = resultArray.getJSONObject(i); |
| | | if (!getContract().equals(item.getString("contract"))) { |
| | | continue; |
| | | } |
| | | if (!getContract().equals(item.getString("contract"))) continue; |
| | | BigDecimal pnl = new BigDecimal(item.getString("pnl")); |
| | | String side = item.getString("side"); |
| | | log.info("[{}] 推送: contract={}, side={}, pnl={}", |
| | | CHANNEL_NAME, getContract(), side, pnl); |
| | | log.info("[{}] side:{}, pnl:{}", CHANNEL_NAME, side, pnl); |
| | | if (getGridTradeService() != null) { |
| | | getGridTradeService().onPositionClose(getContract(), side, pnl); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[{}] 处理数据失败", CHANNEL_NAME, e); |
| | | } |
| | | } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); } |
| | | return true; |
| | | } |
| | | } |
| | |
| | | |
| | | /** |
| | | * 仓位频道处理器。 |
| | | * 私有频道,需认证。解析仓位推送后回调 {@link GateGridTradeService#onPositionUpdate}。 |
| | | * |
| | | * <h3>数据用途</h3> |
| | | * 监控仓位数量(size)。当 size 从有变为 0 时,表示该方向被止盈条件单平仓, |
| | | * 触发补仓(reopenLongPosition / reopenShortPosition)。 |
| | | * |
| | | * <h3>推送字段</h3> |
| | | * contract, mode(dual_long / dual_short), size(正=持有,0=无仓位), entry_price |
| | | * |
| | | * <h3>注意</h3> |
| | | * 双向持仓模式下空头 size 为负数,使用 {@code size.abs()} 判断是否有仓位。 |
| | | * 累计盈亏不由本频道计算,而是由 {@link PositionClosesChannelHandler} 独立处理。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | |
| | | |
| | | @Override |
| | | public boolean handleMessage(JSONObject response) { |
| | | String channel = response.getString("channel"); |
| | | if (!CHANNEL_NAME.equals(channel)) { |
| | | return false; |
| | | } |
| | | if (!CHANNEL_NAME.equals(response.getString("channel"))) return false; |
| | | try { |
| | | JSONArray resultArray = response.getJSONArray("result"); |
| | | if (resultArray == null || resultArray.isEmpty()) { |
| | | return true; |
| | | } |
| | | if (resultArray == null || resultArray.isEmpty()) return true; |
| | | for (int i = 0; i < resultArray.size(); i++) { |
| | | JSONObject pos = resultArray.getJSONObject(i); |
| | | if (!getContract().equals(pos.getString("contract"))) { |
| | | continue; |
| | | } |
| | | if (!getContract().equals(pos.getString("contract"))) continue; |
| | | String mode = pos.getString("mode"); |
| | | BigDecimal size = new BigDecimal(pos.getString("size")); |
| | | BigDecimal entryPrice = new BigDecimal(pos.getString("entry_price")); |
| | | log.info("[{}] 推送: contract={}, mode={}, size={}, entry_price={}", |
| | | CHANNEL_NAME, getContract(), mode, size, entryPrice); |
| | | log.info("[{}] mode:{}, size:{}, entry:{}", CHANNEL_NAME, mode, size, entryPrice); |
| | | if (getGridTradeService() != null) { |
| | | getGridTradeService().onPositionUpdate(getContract(), mode, size, entryPrice); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[{}] 处理数据失败", CHANNEL_NAME, e); |
| | | } |
| | | } catch (Exception e) { log.error("[{}] handle fail", CHANNEL_NAME, e); } |
| | | return true; |
| | | } |
| | | } |