package com.xcong.excoin.modules.gateApi; import io.gate.gateapi.ApiClient; import io.gate.gateapi.ApiException; import io.gate.gateapi.GateApiException; import io.gate.gateapi.api.AccountApi; 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 网格交易服务类。 * *

策略概述

* 多空双开 → 各放止盈条件单 → 仓位推送驱动补仓 → history_pnl 判断停止 * *

触发逻辑

*
 *   K线首次价格就绪 → dualOpenPositions()     // 多空双开 + 止盈条件单
 *   仓位推送 size=0  → reopenXxxPosition()     // 该方向被止盈平掉 → 补开
 *   仓位推送 history_pnl ≥ overallTp → 停止
 *   仓位推送 history_pnl ≤ -maxLoss → 停止
 * 
* *

止盈计算

* 多头止盈价 = entryPrice × (1 + gridRate)
* 空头止盈价 = entryPrice × (1 - gridRate) * *

依赖

* 使用 {@code io.gate:gate-api (7.2.71)} SDK 通过 REST API 下单, * 市场数据由 {@link GateKlineWebSocketClient} 通过 WebSocket 提供。 * * @author Administrator */ @Slf4j public class GateGridTradeService { private final ApiClient apiClient; 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 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 BigDecimal totalHistoryPnl = 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, int maxCycles, 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; this.apiClient = new ApiClient(); this.apiClient.setBasePath("https://api-testnet.gateapi.io/api/v4"); this.apiClient.setApiKeySecret(apiKey, apiSecret); this.futuresApi = new FuturesApi(apiClient); } /** * 初始化账户。设置杠杆、查询余额、切换持仓模式。 */ public void init() { try { AccountApi accountApi = new AccountApi(apiClient); AccountDetail accountDetail = accountApi.getAccountDetail(); this.userId = accountDetail.getUserId(); log.info("[GateGrid] 用户ID: {}", userId); futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); log.info("[GateGrid] 已取消所有既有止盈止损条件单"); futuresApi.updateContractPositionLeverageCall( SETTLE, contract, leverage, marginMode, positionMode, null); log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); log.info("[GateGrid] 账户可用余额: {}, 总资产: {}", account.getAvailable(), account.getTotal()); String positionModeSet = account.getPositionMode(); if (!positionMode.equals(positionModeSet)) { futuresApi.setPositionMode(SETTLE, positionMode); } log.info("[GateGrid] 已设置双向持仓模式"); } catch (GateApiException e) { log.error("[GateGrid] 初始化失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); } catch (ApiException e) { log.error("[GateGrid] 初始化API调用失败, code: {}", e.getCode()); } } /** * 启动网格策略。策略激活后等待 K 线价格就绪,然后自动首次双开。 */ public void startGrid() { if (strategyActive) { log.warn("[GateGrid] 策略已在运行中"); return; } strategyActive = true; totalHistoryPnl = BigDecimal.ZERO; log.info("[GateGrid] 网格策略启动,等待K线价格..."); } /** * 停止网格策略。 */ public void stopGrid() { strategyActive = false; log.info("[GateGrid] 网格策略已停止, history_pnl: {}", totalHistoryPnl); } /** * K 线回调入口。由 调用。 * 首次收到价格时触发多空双开,后续仅缓存最新价格供补仓使用。 * * @param closePrice K 线收盘价 */ public void onKline(BigDecimal closePrice) { lastKlinePrice = closePrice; if (!strategyActive) { return; } if (!dualOpened) { dualOpened = true; dualOpenPositions(); } } /** * 仓位推送回调入口。由 {@link GateKlineWebSocketClient} 调用。 * 根据仓位模式(dual_long/dual_short)和 size 判断: * * 每次推送更新 history_pnl 并检查停止条件。 * * @param contract 合约名 * @param mode 仓位模式(dual_long / dual_short) * @param size 仓位数量(0 表示无仓位) * @param entryPrice 入场价格 * @param historyPnl 已实现累计盈亏 * @param realisedPnl 已实现盈亏 */ public void onPositionUpdate(String contract, String mode, BigDecimal size, BigDecimal entryPrice, BigDecimal historyPnl, BigDecimal realisedPnl) { if (!strategyActive) { return; } boolean hasPosition = size.abs().compareTo(BigDecimal.ZERO) > 0; if ("dual_long".equals(mode)) { if (longActive && !hasPosition) { log.info("[GateGrid] 多头被平仓(止盈触发), 重新开多"); longActive = false; reopenLongPosition(); } else if (hasPosition) { longActive = true; longEntryPrice = entryPrice; } } else if ("dual_short".equals(mode)) { if (shortActive && !hasPosition) { log.info("[GateGrid] 空头被平仓(止盈触发), 重新开空"); shortActive = false; reopenShortPosition(); } else if (hasPosition) { shortActive = true; shortEntryPrice = entryPrice; } } totalHistoryPnl = historyPnl; checkStopConditions(); } /** * 检查策略停止条件。满足任一即置 strategyActive=false: * */ private void checkStopConditions() { if (totalHistoryPnl.compareTo(overallTp) >= 0) { log.info("[GateGrid] 累计止盈 {} 达到 {} USDT,停止策略", totalHistoryPnl, overallTp); strategyActive = false; return; } if (totalHistoryPnl.compareTo(maxLoss.negate()) <= 0) { log.info("[GateGrid] 累计亏损 {} 达到上限 {} USDT,停止策略", totalHistoryPnl, maxLoss); strategyActive = false; } } /** * 首次多空双开。使用当前 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); 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) { 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()); longActive = true; log.info("[GateGrid] 补开多成功, price: {}, id: {}", longEntryPrice, longResult.getId()); placeLongTp(longEntryPrice); } catch (Exception e) { log.error("[GateGrid] 补开多失败", e); } } /** * 补开空头仓位。空头被止盈平掉后调用,市价重新开空并创建止盈单。 */ private void reopenShortPosition() { if (lastKlinePrice == null || !strategyActive) { 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()); shortActive = true; log.info("[GateGrid] 补开空成功, price: {}, id: {}", shortEntryPrice, shortResult.getId()); placeShortTp(shortEntryPrice); } catch (Exception e) { log.error("[GateGrid] 补开空失败", e); } } /** * 创建多头止盈条件单。 * 触发价 = 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); } /** * 创建空头止盈条件单。 * 触发价 = 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); } /** * 通过 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) { try { 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); 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] 止盈条件单已存在,取消旧单后重试, label:{}", e.getErrorLabel()); try { futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, createPriceTriggeredOrderBean(triggerPrice, rule, orderType, autoSize)); 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 FuturesPriceTriggeredOrder createPriceTriggeredOrderBean(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 getTotalHistoryPnl() { return totalHistoryPnl; } public Long getUserId() { return userId; } }