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 ** *
公式:
*
* divisor = addPositionInterval + 1
* addMultiplier = floor(effectiveStopLossCount / divisor)
* addQty = addMultiplier × addPositionQuantity
* finalQty = min(baseQuantity + addQty, maxPositionPerSide > 0 ? maxPositionPerSide : ∞)
*
*
* @param isLong {@code true}=多仓方向,{@code false}=空仓方向
* @return 实际下单张数(字符串)
*/
private String calculateEntryQuantity(boolean isLong) {
int baseQty = Integer.parseInt(config.getBaseQuantity());
int interval = config.getAddPositionInterval();
int addQtyPerUnit = config.getAddPositionQuantity();
int maxPerSide = config.getMaxPositionPerSide();
int effectiveCount = getEffectiveStopLossCount(isLong);
int startThreshold = config.getAddPositionStartThreshold();
if (startThreshold > 0) {
effectiveCount = Math.max(0, effectiveCount - startThreshold);
}
// divisor = interval + 1:interval=0→每次加仓, interval=1→每2次加仓(2,4,6...), interval=3→每4次加仓(4,8,12...)
int divisor = interval + 1;
int addMultiplier = interval >= 0 ? effectiveCount / divisor : 0;
int addQty = addMultiplier * addQtyPerUnit;
int totalQty = baseQty + addQty;
if (maxPerSide > 0 && totalQty > maxPerSide) {
totalQty = maxPerSide;
}
return String.valueOf(totalQty);
}
private void handleLongStopLossTriggered(GridElement gridElement, String orderId) {
gridElement.removeLongStopLossOrderId(orderId);
int gridId = gridElement.getId();
boolean sameGrid = (gridId == lastLongStopLossGridId);
if (sameGrid) {
log.info("[Gate] 多仓止损触发 同网格, 忽略");
return;
}
lastLongStopLossGridId = gridId;
accumulatedLongLossCount++;
log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
gridId, accumulatedLongLossCount, sameGrid ? "(同网格)" : "");
int newEntryGridId = gridId + 1;
GridElement newEntryGrid = GridElement.findById(newEntryGridId);
if (newEntryGrid == null) {
log.warn("[Gate] 多仓止损触发 but gridId:{} 不存在", newEntryGridId);
GridElement.refreshIndices();
return;
}
// 止损追单:同一网格可有多笔挂单,不判断 isHasLongOrder,直接挂单
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
String size = calculateEntryQuantity(true);
log.info("[Gate] 多仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
getEffectiveStopLossCount(true), config.getBaseQuantity(), size, config.getStopLossCountMode());
newEntryGrid.getLongTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice,
FuturesPriceTrigger.RuleEnum.NUMBER_1, size);
// 取消 gridId+2 位置的所有多单
int cancelGridId = gridId + 2;
GridElement cancelGrid = GridElement.findById(cancelGridId);
if (cancelGrid != null && cancelGrid.isHasLongOrder()) {
for (String cancelOrderId : new ArrayList<>(cancelGrid.getLongOrderIds())) {
executor.cancelConditionalOrder(cancelOrderId, oid -> {
longEntryTraderIdParam(cancelGrid, cancelOrderId, false);
log.info("[Gate] 多仓止损触发, 取消gridId:{}的多单,{}", cancelGridId, cancelOrderId);
});
}
}
}
private void handleShortStopLossTriggered(GridElement gridElement, String orderId) {
gridElement.removeShortStopLossOrderId(orderId);
int gridId = gridElement.getId();
boolean sameGrid = (gridId == lastShortStopLossGridId);
if (sameGrid) {
log.info("[Gate] 空仓止损触发 同网格, 忽略");
return;
}
lastShortStopLossGridId = gridId;
accumulatedShortLossCount++;
log.info("[Gate] 空仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
gridId, accumulatedShortLossCount, sameGrid ? "(同网格)" : "");
int newEntryGridId = gridId - 1;
GridElement newEntryGrid = GridElement.findById(newEntryGridId);
if (newEntryGrid == null) {
log.warn("[Gate] 空仓止损触发 but gridId:{} 不存在", newEntryGridId);
GridElement.refreshIndices();
return;
}
// 止损追单:同一网格可有多笔挂单,不判断 isHasShortOrder,直接挂单
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
String size = calculateEntryQuantity(false);
log.info("[Gate] 空仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
getEffectiveStopLossCount(false), config.getBaseQuantity(), size, config.getStopLossCountMode());
newEntryGrid.getShortTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice,
FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size));
// 取消 gridId-2 位置的所有空单
int cancelGridId = gridId - 2;
GridElement cancelGrid = GridElement.findById(cancelGridId);
if (cancelGrid != null && cancelGrid.isHasShortOrder()) {
for (String cancelOrderId : new ArrayList<>(cancelGrid.getShortOrderIds())) {
executor.cancelConditionalOrder(cancelOrderId, oid -> {
shortEntryTraderIdParam(cancelGrid, cancelOrderId, false);
log.info("[Gate] 空仓止损触发, 取消gridId:{}的空单,{}", cancelGridId, cancelOrderId);
});
}
}
}
// ========== 止盈/止损取消辅助方法 ==========
/**
* 止盈触发后检查跨度是否达至要求,满足条件则重启策略。
*
* 在调用 {@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; }
// ========== 止损查表辅助方法 ==========
/** 找到第一个有多仓止损单的网格(首个匹配即返回) */
private GridElement findFirstLongStopLossGrid() {
for (GridElement e : config.getGridElements()) {
if (!e.getLongStopLossOrderIds().isEmpty()) return e;
}
return null;
}
/** 找到第一个有空仓止损单的网格(首个匹配即返回) */
private GridElement findFirstShortStopLossGrid() {
for (GridElement e : config.getGridElements()) {
if (!e.getShortStopLossOrderIds().isEmpty()) return e;
}
return null;
}
/**
* 挂单成交后,将超出基础仓位的部分挂止盈单,挂在对向仓位的第一止损位上。
*
* 遍历所有 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); } ); } /** * 在指定网格挂一笔对手止盈单(非满仓超额止盈,挂在止损触发位的下一格)。 */ private void placeTakeProfitAtGrid(GridElement tpElem, boolean isLong, int qty, int times) { BigDecimal triggerPrice = tpElem.getGridPrice(); String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT; FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1 : FuturesPriceTrigger.RuleEnum.NUMBER_2; String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty); int gridId = tpElem.getId(); executor.placeTakeProfit(triggerPrice, rule, orderType, size, profitId -> { if (isLong) { longTakeProfitTraderIdParam(tpElem, profitId, true); } else { shortTakeProfitTraderIdParam(tpElem, profitId, true); } log.info("[Gate] 止损{}→对手超额止盈 gridId:{}, 量:{}, tpId:{}", times, gridId, qty, profitId); } ); } /** * 挂对手盘止盈单:在对向仓位第一止损位挂止盈。 * @param isLong true=挂多仓止盈(对空仓), false=挂空仓止盈(对多仓) */ private void placeOpponentTakeProfit(boolean isLong, int tpQty, int times, int gridId) { GridElement tpElem = GridElement.findById(isLong ? gridId + 1 : gridId - 1); if (tpElem == null) { log.warn("[Gate] 对手止盈挂单失败:未找到止损位"); return; } int tpGridId = tpElem.getId(); BigDecimal triggerPrice = tpElem.getGridPrice(); String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT; FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1 : FuturesPriceTrigger.RuleEnum.NUMBER_2; String size = isLong ? negate(String.valueOf(tpQty)) : String.valueOf(tpQty); executor.placeTakeProfit(triggerPrice, rule, orderType, size, profitId -> { if (isLong) longTakeProfitTraderIdParam(tpElem, profitId, true); else shortTakeProfitTraderIdParam(tpElem, profitId, true); log.info("[Gate] 止损次数{}→对手{}止盈 gridId:{}, 量:{}, tpId:{}", times, isLong ? "多仓" : "空仓", tpGridId, size, profitId); } ); } }