| | |
| | | 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 java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | |
| | | /** |
| | | * Gate 网格交易服务类。 |
| | | * |
| | | * <h3>策略概述</h3> |
| | | * 多空双开 → 各放止盈条件单 → 仓位推送检测平仓补仓 → 平仓推送累加 pnl → 判断停止 |
| | | * |
| | | * <h3>触发逻辑</h3> |
| | | * <pre> |
| | | * K线首次价格就绪 → dualOpenPositions() // 多空双开 + 止盈条件单 |
| | | * 仓位推送 size=0 → reopenXxxPosition() // 该方向被平仓 → 只补该方向 |
| | | * 平仓推送 pnl → cumulativePnl += pnl // 累计盈亏 |
| | | * cumulativePnl ≥ overallTp → 停止 |
| | | * cumulativePnl ≤ -maxLoss → 停止 |
| | | * </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 类提供。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class GateGridTradeService { |
| | | |
| | |
| | | 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 volatile BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | /** 用户 ID,用于 WebSocket 私有频道订阅 */ |
| | | private Long userId; |
| | | |
| | | private BigDecimal totalHistoryPnl = BigDecimal.ZERO; |
| | | |
| | | /** |
| | | * 构造函数,初始化 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, |
| | | BigDecimal maxLoss, |
| | | String quantity) { |
| | | this.contract = contract; |
| | | this.leverage = leverage; |
| | |
| | | this.futuresApi = new FuturesApi(apiClient); |
| | | } |
| | | |
| | | /** |
| | | * 初始化账户。顺序:切持仓模式 → 清旧条件单 → 设杠杆 → 查余额。 |
| | | */ |
| | | public void init() { |
| | | try { |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, contract, leverage, marginMode, positionMode, null); |
| | | log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); |
| | | AccountApi accountApi = new AccountApi(apiClient); |
| | | AccountDetail accountDetail = accountApi.getAccountDetail(); |
| | | this.userId = accountDetail.getUserId(); |
| | | log.info("[GateGrid] 用户ID: {}", userId); |
| | | |
| | | 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] 已设置双向持仓模式"); |
| | | log.info("[GateGrid] 已设置持仓模式: {}", positionMode); |
| | | |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | log.info("[GateGrid] 已取消所有既有止盈止损条件单"); |
| | | |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, contract, leverage, marginMode, positionMode, null); |
| | | log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); |
| | | |
| | | log.info("[GateGrid] 账户可用余额: {}, 总资产: {}", |
| | | account.getAvailable(), account.getTotal()); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 初始化失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (ApiException e) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动网格策略。策略激活后等待 K 线价格就绪,然后自动首次双开。 |
| | | */ |
| | | public void startGrid() { |
| | | if (strategyActive) { |
| | | log.warn("[GateGrid] 策略已在运行中"); |
| | | return; |
| | | } |
| | | strategyActive = true; |
| | | totalHistoryPnl = BigDecimal.ZERO; |
| | | cumulativePnl = BigDecimal.ZERO; |
| | | log.info("[GateGrid] 网格策略启动,等待K线价格..."); |
| | | } |
| | | |
| | | /** |
| | | * 停止网格策略。 |
| | | */ |
| | | public void stopGrid() { |
| | | strategyActive = false; |
| | | log.info("[GateGrid] 网格策略已停止, history_pnl: {}", totalHistoryPnl); |
| | | log.info("[GateGrid] 网格策略已停止, cumulativePnl: {}", cumulativePnl); |
| | | } |
| | | |
| | | /** |
| | | * K线回调:存储最新价格,首次价格就绪时双开 |
| | | * K 线回调入口。由 调用。 |
| | | * 首次收到价格时触发多空双开,后续仅缓存最新价格供补仓使用。 |
| | | * |
| | | * @param closePrice K 线收盘价 |
| | | */ |
| | | public void onKline(BigDecimal closePrice) { |
| | | lastKlinePrice = closePrice; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 仓位推送回调:检测止盈平仓 → 补仓,累加 history_pnl → 判断停止 |
| | | * 仓位推送回调入口。由 {@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 入场价格 |
| | | */ |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, |
| | | BigDecimal entryPrice, BigDecimal historyPnl, |
| | | BigDecimal realisedPnl) { |
| | | BigDecimal entryPrice) { |
| | | if (!strategyActive) { |
| | | return; |
| | | } |
| | | |
| | | boolean hasPosition = size.compareTo(BigDecimal.ZERO) > 0; |
| | | boolean hasPosition = size.abs().compareTo(BigDecimal.ZERO) > 0; |
| | | |
| | | if ("dual_long".equals(mode)) { |
| | | if (longActive && !hasPosition) { |
| | |
| | | shortEntryPrice = entryPrice; |
| | | } |
| | | } |
| | | } |
| | | |
| | | totalHistoryPnl = historyPnl; |
| | | /** |
| | | * 平仓推送回调入口。由 {@link GateKlineWebSocketClient} 调用。 |
| | | * 累加平仓盈亏,每次平仓推送后检查停止条件。 |
| | | * |
| | | * @param contract 合约名 |
| | | * @param side 平仓方向(long / short) |
| | | * @param pnl 该次平仓的盈亏 |
| | | */ |
| | | public void onPositionClose(String contract, String side, BigDecimal pnl) { |
| | | if (!strategyActive) { |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | log.info("[GateGrid] 平仓盈亏累计, side:{}, pnl:{}, cumulativePnl:{}", side, pnl, cumulativePnl); |
| | | checkStopConditions(); |
| | | } |
| | | |
| | | /** |
| | | * 检查策略停止条件。满足任一即置 strategyActive=false: |
| | | * <ul> |
| | | * <li>累计盈利 ≥ overallTp</li> |
| | | * <li>累计亏损 ≤ -maxLoss</li> |
| | | * </ul> |
| | | */ |
| | | private void checkStopConditions() { |
| | | if (totalHistoryPnl.compareTo(overallTp) >= 0) { |
| | | log.info("[GateGrid] 累计止盈 {} 达到 {} USDT,停止策略", totalHistoryPnl, overallTp); |
| | | if (cumulativePnl.compareTo(overallTp) >= 0) { |
| | | log.info("[GateGrid] 累计止盈 {} 达到 {} USDT,停止策略", cumulativePnl, overallTp); |
| | | strategyActive = false; |
| | | return; |
| | | } |
| | | if (totalHistoryPnl.compareTo(maxLoss.negate()) <= 0) { |
| | | log.info("[GateGrid] 累计亏损 {} 达到上限 {} USDT,停止策略", totalHistoryPnl, maxLoss); |
| | | if (cumulativePnl.compareTo(maxLoss.negate()) <= 0) { |
| | | log.info("[GateGrid] 累计亏损 {} 达到上限 {} USDT,停止策略", cumulativePnl, maxLoss); |
| | | strategyActive = false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 首次多空双开。使用当前 K 线价格以市价单同时开多和开空, |
| | | * 开仓成功后立即为每个方向创建止盈条件单。 |
| | | */ |
| | | private void dualOpenPositions() { |
| | | if (lastKlinePrice == null) { |
| | | log.warn("[GateGrid] K线价格未就绪,跳过双开"); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 补开多头仓位。多头被止盈平掉后调用,市价重新开多并创建止盈单。 |
| | | */ |
| | | private void reopenLongPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | return; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 补开空头仓位。空头被止盈平掉后调用,市价重新开空并创建止盈单。 |
| | | */ |
| | | private void reopenShortPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | return; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 创建多头止盈条件单。 |
| | | * 触发价 = 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) { |
| | | FuturesPriceTriggeredOrder order = buildTriggeredOrder(triggerPrice, rule, orderType, 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] 止盈条件单已存在,取消旧单后重试"); |
| | | 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 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; |
| | |
| | | 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 strategyActive; |
| | | } |
| | | |
| | | public BigDecimal getTotalHistoryPnl() { |
| | | return totalHistoryPnl; |
| | | public BigDecimal getCumulativePnl() { |
| | | return cumulativePnl; |
| | | } |
| | | |
| | | public Long getUserId() { |
| | | return userId; |
| | | } |
| | | } |