package com.xcong.excoin.modules.gateApi; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import com.xcong.excoin.utils.dingtalk.DingTalkUtils; 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.*; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import com.xcong.excoin.modules.gateApi.wsHandler.handler.CandlestickChannelHandler; import com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionClosesChannelHandler; import com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionsChannelHandler; /** * 网格交易策略引擎 — 多空对冲网格。 * *
* init() → startGrid() → WAITING_KLINE * ↓ * onKline(首根K线) → OPENING → 异步市价双开基底(开多+开空) * ↓ * onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened * ↓ * tryGenerateQueues() * ├── generateShortQueue() ← 空仓价格队列(降序,从 shortBaseEntryPrice-step 向下) * ├── generateLongQueue() ← 多仓价格队列(升序,从 shortBaseEntryPrice+step 向上) * ├── updateGridElements() ← 构建 GridElement 列表 + TraderParam + 全局索引 * ├── 挂基座止盈单(ID=0 的 long/short takeProfit) * └── 挂初始条件单(up=-1 多单, down=1 空单) * ↓ * state = ACTIVE(每根K线反复执行以下循环) * ↓ * onKline() → processLongGrid() + processShortGrid() * ├── 匹配队列元素 → 队列补偿 → 保证金检查 * ├── 首元素方向:挂条件开仓单 → 订单ID + GridElement状态同步 * └── 反向守卫:在 downGrid 位置挂对向单(价格区间+trigger方向校验) * ↓ * onOrderUpdate() ← futures.orders / futures.autoorders 推送 * ├── 匹配止盈单ID → 清空止盈状态(已成交) * └── 匹配挂单ID → 挂止盈条件单 → 止盈ID + GridElement状态同步 * ↓ * onPositionClose() → cumulativePnl 累加 * ├── ≥ overallTp → STOPPED * └── ≤ -maxLoss → STOPPED ** *
* onPositionUpdate() 中仓位均价变化后: * longEntryPrice ↑ → 取消 高于 longEntryPrice 的空仓挂单(避免逆势空单) * shortEntryPrice ↓ → 取消 低于 shortEntryPrice 的多仓挂单(避免逆势多单) ** *
* step = shortBaseEntryPrice × gridRate ← 网格绝对步长 * minTick = 10^(-priceScale) ← 交易所最小价格单位 * 多止盈 = gridPrice + (step - minTick) ← 多仓止盈价 * 空止盈 = gridPrice - (step - minTick) ← 空仓止盈价 * 单笔盈利 = (step - minTick) × contractMultiplier × quantity ← USDT ** *
遍历所有 GridElement,找到对向仓位第一个有止损单的网格作为止盈挂单位置。 * *
例:空仓成交后持仓 8 张,基础 4 张 → 超出 4 张, * 找到多仓第一止损位(如 gridId=-2)→ 在该位置挂空仓止盈单。 * * @param posSize 当前总持仓张数 * @param isLong true=多仓成交,false=空仓成交 */ private void placeExcessTakeProfit(int posSize, boolean isLong) { int baseQty = Integer.parseInt(config.getBaseQuantity()); int excessQty = posSize - baseQty; if (excessQty <= 0) { return; } // 遍历找到对向仓位第一个有止损单的网格 GridElement tpElem = isLong ? findFirstShortStopLossGrid() : findFirstLongStopLossGrid(); if (tpElem == null) { log.warn("[Gate] {}止盈挂单失败:未找到对向仓止损位", isLong ? "多仓" : "空仓"); return; } int tpGridId = tpElem.getId(); BigDecimal triggerPrice = tpElem.getGridPrice(); String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT; // 多仓止盈:价格≥触发价时平仓(NUMBER_1);空仓止盈:价格≤触发价时平仓(NUMBER_2) FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1 : FuturesPriceTrigger.RuleEnum.NUMBER_2; String size = isLong ? negate(String.valueOf(excessQty)) : String.valueOf(excessQty); // if (isLong && tpElem.getLongTakeProfitOrderId() != null) { // executor.cancelConditionalOrder(tpElem.getLongTakeProfitOrderId(), oid -> { // longTakeProfitTraderIdParam(tpElem, null, false); // log.info("[Gate] 取消旧止盈, gridId:{}, orderId:{}", tpGridId, oid); // }); // } else if (!isLong && tpElem.getShortTakeProfitOrderId() != null) { // executor.cancelConditionalOrder(tpElem.getShortTakeProfitOrderId(), oid -> { // shortTakeProfitTraderIdParam(tpElem, null, false); // log.info("[Gate] 取消旧止盈, gridId:{}, orderId:{}", tpGridId, oid); // }); // } String finalSize = size; int finalTpGridId = tpGridId; executor.placeTakeProfit(triggerPrice, rule, orderType, size, profitId -> { if (isLong) { longTakeProfitTraderIdParam(tpElem, profitId, true); } else { shortTakeProfitTraderIdParam(tpElem, profitId, true); } log.info("[Gate] {}止盈挂单, gridId:{}, 触发价:{}, 数量:{}, takeProfitId:{}", isLong ? "多仓" : "空仓", finalTpGridId, triggerPrice, finalSize, profitId); } ); } /** * 找到有长仓止损单且离 0 最近的网格(第一个多仓止损位)。 */ private GridElement findFirstLongStopLossGrid() { GridElement first = null; for (GridElement e : config.getGridElements()) { if (!e.getLongStopLossOrderIds().isEmpty()) { // 多仓止损在负数区,取 id 最大(最靠近 0)的 if (first == null || e.getId() > first.getId()) { first = e; } } } return first; } /** * 找到有空仓止损单且离 0 最近的网格(第一个空仓止损位)。 */ private GridElement findFirstShortStopLossGrid() { GridElement first = null; for (GridElement e : config.getGridElements()) { if (!e.getShortStopLossOrderIds().isEmpty()) { // 空仓止损在正数区,取 id 最小(最靠近 0)的 if (first == null || e.getId() < first.getId()) { first = e; } } } return first; } /** * 延展完成后重挂止损(处理被跳过的入场单成交)。 * 取消已有止损单并用最新仓位重新挂单,确保止损覆盖最新持仓数。 */ private void reExtendLongStopLoss(GridElement entryElem) { if (entryElem.isExtendStopLossInProgress()) { log.info("[Gate] 多仓重挂止损跳过, entryGridId:{}, 仍在进行中", entryElem.getId()); return; } entryElem.setExtendStopLossInProgress(true); cancelAllLongTakeProfitsAndStopLosses(); int latestPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); log.info("[Gate] 多仓重挂止损, entryGridId:{}, 最新仓位:{}张", entryElem.getId(), latestPos); extendLongStopLoss(latestPos, entryElem.getId()); } private void reExtendShortStopLoss(GridElement entryElem) { if (entryElem.isExtendStopLossInProgress()) { log.info("[Gate] 空仓重挂止损跳过, entryGridId:{}, 仍在进行中", entryElem.getId()); return; } entryElem.setExtendStopLossInProgress(true); cancelAllShortTakeProfitsAndStopLosses(); int latestPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); log.info("[Gate] 空仓重挂止损, entryGridId:{}, 最新仓位:{}张", entryElem.getId(), latestPos); extendShortStopLoss(latestPos, entryElem.getId()); } /** * 在指定网格位置挂 count 个独立止损单,每个 size 张。 */ private void placeStopLossOrders(int gridId, int count, int qty, FuturesPriceTrigger.RuleEnum rule, String orderType, boolean isLong, AtomicInteger remainCount, GridElement entryElement) { if (count <= 0) { return; } GridElement elem = GridElement.findById(gridId); if (elem == null) { log.warn("[Gate] 止损挂单位置不存在, gridId:{}", gridId); // 即使挂单位置不存在也需递减计数器,避免标志永不重置 if (remainCount != null && entryElement != null) { for (int i = 0; i < count; i++) { if (remainCount.decrementAndGet() == 0) { entryElement.setExtendStopLossInProgress(false); log.info("[Gate] {}止损追单全部完成(部分位置缺失), entryGridId:{}, 防重入标记已重置", isLong ? "多仓" : "空仓", entryElement.getId()); // 检查待重挂请求 if (entryElement.isPendingStopLossReExtend()) { entryElement.setPendingStopLossReExtend(false); if (isLong) { reExtendLongStopLoss(entryElement); } else { reExtendShortStopLoss(entryElement); } } } } } return; } BigDecimal triggerPrice = elem.getGridPrice(); for (int i = 0; i < count; i++) { String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty); int finalGridId = gridId; int finalI = i; executor.placeTakeProfit( triggerPrice, rule, orderType, size, profitId -> { if (isLong) { elem.addLongStopLossOrderId(profitId); } else { elem.addShortStopLossOrderId(profitId); } GridElement.refreshIndices(); log.info("[Gate] {}止损追加, gridId:{}, 触发价:{}, 第{}单, stopLossId:{}", isLong ? "多仓" : "空仓", finalGridId, triggerPrice, finalI + 1, profitId); // 计数器归零时重置防重入标记,并检查是否有待重挂请求 if (remainCount != null && remainCount.decrementAndGet() == 0 && entryElement != null) { entryElement.setExtendStopLossInProgress(false); log.info("[Gate] {}止损追单全部完成, entryGridId:{}, 防重入标记已重置", isLong ? "多仓" : "空仓", entryElement.getId()); // 如果有被跳过的入场单成交,用最新仓位重挂一次止损 if (entryElement.isPendingStopLossReExtend()) { entryElement.setPendingStopLossReExtend(false); if (isLong) { reExtendLongStopLoss(entryElement); } else { reExtendShortStopLoss(entryElement); } } } } ); } } // ---- 工具 ---- /** * 取反字符串数字。如 "1" → "-1","-2" → "2"。 * 用于开空单时将正数张数转为负数。 */ private String negate(String qty) { return qty.startsWith("-") ? qty.substring(1) : "-" + qty; } /** * 预设标志位后提交条件开仓单,防止异步回调导致的竞态重复挂单。 * *
在调用 {@link GateTradeExecutor#placeConditionalEntryOrder} 之前同步设置 * {@code isHasLongOrder / isHasShortOrder},关闭 WS 线程与 Executor 线程之间的 * 检查-下单时间窗口。API 失败时自动回滚标志位。 * * @param gridElement 目标网格元素 * @param isLong true=多仓下单,false=空仓下单 * @param triggerPrice 触发价 * @param rule 触发规则 * @param size 开仓张数 */ private void placeEntryOrderWithPreFlag(GridElement gridElement, boolean isLong, BigDecimal triggerPrice, FuturesPriceTrigger.RuleEnum rule, String size) { if (isLong) { gridElement.setHasLongOrder(true); } else { gridElement.setHasShortOrder(true); } executor.placeConditionalEntryOrder(triggerPrice, rule, size, orderId -> { if (isLong) { longEntryTraderIdParam(gridElement, orderId, true); } else { shortEntryTraderIdParam(gridElement, orderId, true); } }, () -> { // 仅当列表为空(无其他有效订单)时才清预置标志,避免误伤其他并发挂单 if (isLong) { if (!gridElement.hasLongOrderIds()) { gridElement.setHasLongOrder(false); } } else { if (!gridElement.hasShortOrderIds()) { gridElement.setHasShortOrder(false); } } GridElement.refreshIndices(); log.warn("[Gate] 条件单创建失败 gridId:{}, isLong:{}", gridElement.getId(), isLong); } ); } /** * 根据持仓和当前价格计算未实现盈亏。 * *
* 多仓: 持仓量 × 合约乘数 × (计价价格 − 开仓均价)
* 空仓: 持仓量 × 合约乘数 × (开仓均价 − 计价价格)
*
* 计价价格由 {@link GateConfig.PnLPriceMode} 决定:LAST_PRICE 用最新成交价,MARK_PRICE 用标记价格。
*/
private void updateUnrealizedPnl() {
BigDecimal price = resolvePnlPrice();
if (price == null || price.compareTo(BigDecimal.ZERO) == 0) {
return;
}
BigDecimal multiplier = config.getContractMultiplier();
BigDecimal longPnl = BigDecimal.ZERO;
BigDecimal shortPnl = BigDecimal.ZERO;
if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
longPnl = longPositionSize.multiply(multiplier).multiply(price.subtract(longEntryPrice));
}
if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price));
}
unrealizedPnl = longPnl.add(shortPnl);
log.info("[Gate] 未实现盈亏: {}", unrealizedPnl);
}
/**
* 根据配置的 PnLPriceMode 返回计价价格。
* MARK_PRICE 模式优先使用标记价格(外部注入),未注入时回退到最新成交价。
*
* @return 计价价格,可能为 null
*/
private BigDecimal resolvePnlPrice() {
if (config.getUnrealizedPnlPriceMode() == GateConfig.PnLPriceMode.MARK_PRICE
&& markPrice.compareTo(BigDecimal.ZERO) > 0) {
return markPrice;
}
return lastKlinePrice;
}
/** @return 最新 K 线价格(每次 onKline 更新) */
public BigDecimal getLastKlinePrice() { return lastKlinePrice; }
/** 设置标记价格(外部注入,MARK_PRICE 模式时用于盈亏计算) */
public void setMarkPrice(BigDecimal markPrice) { this.markPrice = markPrice; }
/** @return 策略是否处于活跃状态(非 STOPPED 且非 WAITING_KLINE) */
public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; }
/** @return 累计已实现盈亏(平仓推送驱动累加) */
public BigDecimal getCumulativePnl() { return cumulativePnl; }
/** @return 当前未实现盈亏(每根 K 线实时计算) */
public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
/** @return Gate 用户 ID(用于私有频道订阅 payload) */
public Long getUserId() { return userId; }
/** @return 当前策略状态 */
public StrategyState getState() { return state; }
/** 注入WS客户端,用于订阅状态检查 */
public void setWsClient(GateKlineWebSocketClient wsClient) { this.wsClient = wsClient; }
}