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 com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; import io.gate.gateapi.ApiClient; import io.gate.gateapi.ApiException; import io.gate.gateapi.GateApiException; import io.gate.gateapi.JSON; 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.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; 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() ← 空仓价格队列(降序,基价×(1−gridRate) 等比向下) * ├── generateLongQueue() ← 多仓价格队列(升序,基价×(1+gridRate) 等比向上) * ├── 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 ← 首档间距(0档与±1档的距离度量) * minTick = 10^(-priceScale) ← 交易所最小价格单位 * 等比网格: 下一档 = 本档 × (1 ± gridRate) ← 向下×(1−r),向上×(1+r) * 止盈价 = 本档价 × (1 ± gridRate)(与相邻网格价联动) ← 多仓向上,空仓向下 * 单笔盈利 ≈ 档价 × gridRate × contractMultiplier × quantity(随网格位置浮动) ** *
gate-api SDK 的枚举定义滞后于交易所实际返回值,反序列化未知枚举值时 * {@code fromValue} 会抛 {@link IllegalArgumentException},导致接口被误判为失败。 * 已确认业务不使用以下字段,因此通过 Gson 排除策略直接跳过其解析: *
每个网格价 = 上一档 × (1 − gridRate),即自 shortBaseEntryPrice 向下等比递减: *
* 第1档 = shortBaseEntryPrice × (1 − gridRate)
* 第2档 = 第1档 × (1 − gridRate)
* ...
*
* 档间距 = 该档价格 × gridRate,随价格下降而收窄。
*
* 同时将 step(= shortBaseEntryPrice × gridRate,即 0档 与 ±1档 的间距)注入 config, * 供跨度重启等需要固定距离度量的功能使用。 * *
队列降序排列(大→小),方便 processShortGrid 中从头遍历。 */ private void generateShortQueue() { shortPriceQueue.clear(); totalShortPriceQueue.clear(); totalLongPriceQueue.clear(); int prec = config.getPriceScale(); BigDecimal gridRate = config.getGridRate(); // 首档间距 = 基价 × gridRate(0档与±1档的间距),保留作跨度等固定距离度量 BigDecimal step = shortBaseEntryPrice.multiply(gridRate).setScale(prec, RoundingMode.HALF_UP); config.setStep(step); BigDecimal factor = BigDecimal.ONE.subtract(gridRate); BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { shortPriceQueue.add(elem); totalLongPriceQueue.add(elem); totalShortPriceQueue.add(elem); elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP); if (elem.compareTo(BigDecimal.ZERO) <= 0) { break; } } shortPriceQueue.sort((a, b) -> b.compareTo(a)); log.info("[Gate] 空队列(等比):{}", shortPriceQueue); } /** * 生成多仓价格队列(等比 / 复合百分比网格)。 * *
每个网格价 = 上一档 × (1 + gridRate),即自 shortBaseEntryPrice 向上等比递增: *
* 第1档 = shortBaseEntryPrice × (1 + gridRate)
* 第2档 = 第1档 × (1 + gridRate)
* ...
*
* 档间距 = 该档价格 × gridRate,随价格上升而放大;
* 与空仓队列(下方 ×(1−gridRate))关于基价对称,保证上下网格连续一致。
*
* 队列升序排列(小→大),方便 processLongGrid 中从头遍历。 */ private void generateLongQueue() { longPriceQueue.clear(); int prec = config.getPriceScale(); BigDecimal factor = BigDecimal.ONE.add(config.getGridRate()); BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { longPriceQueue.add(elem); totalLongPriceQueue.add(elem); totalShortPriceQueue.add(elem); elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP); } longPriceQueue.sort(BigDecimal::compareTo); log.info("[Gate] 多队列(等比):{}", longPriceQueue); totalShortPriceQueue.sort((a, b) -> b.compareTo(a)); log.info("[Gate] 队列从大到小:{}", totalShortPriceQueue); totalLongPriceQueue.sort(BigDecimal::compareTo); log.info("[Gate] 队列从小到大:{}", totalLongPriceQueue); } /** * 根据当前多空价格队列同步构建网格元素列表,写入 config。 * *
公式:
*
* divisor = addPositionInterval + 1
* addMultiplier = floor(effectiveStopLossCount / divisor)
* addQty = addMultiplier × addPositionQuantity
* finalQty = min(baseQuantity + addQty, upperLimit)
*
* 上限 upperLimit:
*在调用 {@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 int getCurrentRound() { return currentRound; }
/** @return 累计已实现盈亏(平仓推送驱动累加) */
public BigDecimal getCumulativePnl() { return cumulativePnl; }
/** @return 初始本金 */
public BigDecimal getInitialPrincipal() { return initialPrincipal; }
/** @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; }
// ========== 手动控制(Station 指令) ==========
/**
* 手动取消多仓止盈 + 止损挂单,并同步清理对应 GridElement 的挂单记录。
*/
public void cancelLongTakeProfitAndStopLoss() {
submitManualTask(this::cancelAllLongTakeProfitsAndStopLosses);
}
/**
* 手动取消空仓止盈 + 止损挂单,并同步清理对应 GridElement 的挂单记录。
*/
public void cancelShortTakeProfitAndStopLoss() {
submitManualTask(this::cancelAllShortTakeProfitsAndStopLosses);
}
/**
* 手动重新生成多仓止盈 + 止损挂单,逻辑与挂单成交(onAutoOrder)一致:
* 先撤多仓所有止盈止损,再按交易所最新持仓重挂止损阶梯,超额部分挂止盈到空仓第一止损位。
*/
public void rebuildLongTakeProfitAndStopLoss() {
submitManualTask(() -> {
cancelAllLongTakeProfitsAndStopLosses();
int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
if (posSize <= 0) {
log.warn("[Gate] 生成多仓止盈止损失败:无多仓持仓");
return;
}
extendLongStopLoss(posSize, 0);
if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
placeExcessTakeProfit(posSize, true);
} else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
placeDispersedTakeProfit(posSize, true, 0);
}
placeBaseTakeProfit(posSize, true, 0);
log.info("[Gate] 生成多仓止盈止损完成, 仓位:{}张", posSize);
});
}
/**
* 手动重新生成空仓止盈 + 止损挂单,逻辑与挂单成交(onAutoOrder)一致:
* 先撤空仓所有止盈止损,再按交易所最新持仓重挂止损阶梯,超额部分挂止盈到多仓第一止损位。
*/
public void rebuildShortTakeProfitAndStopLoss() {
submitManualTask(() -> {
cancelAllShortTakeProfitsAndStopLosses();
int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
if (posSize <= 0) {
log.warn("[Gate] 生成空仓止盈止损失败:无空仓持仓");
return;
}
extendShortStopLoss(posSize, 0);
if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
placeExcessTakeProfit(posSize, false);
} else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
placeDispersedTakeProfit(posSize, false, 0);
}
placeBaseTakeProfit(posSize, false, 0);
log.info("[Gate] 生成空仓止盈止损完成, 仓位:{}张", posSize);
});
}
/**
* 多单平仓:市价平掉多仓 → 撤多仓所有挂单并更新 GridElement → 交由价格驱动(onKline→processGrid)重新入场。
*/
public void closeLongPosition() {
submitManualTask(() -> closePositionByMode(Position.ModeEnum.DUAL_LONG, true));
}
/**
* 空单平仓:市价平掉空仓 → 撤空仓所有挂单并更新 GridElement → 交由价格驱动(onKline→processGrid)重新入场。
*/
public void closeShortPosition() {
submitManualTask(() -> closePositionByMode(Position.ModeEnum.DUAL_SHORT, false));
}
/**
* 统一提交手动操作任务。策略停止(executor 已关闭)时直接忽略,避免 RejectedExecutionException。
*/
private void submitManualTask(Runnable task) {
if (state == StrategyState.STOPPED) {
log.warn("[Gate] 策略已停止,忽略手动操作");
return;
}
executor.submitTask(task);
}
/**
* 按持仓模式市价平仓(reduceOnly + IOC),随后撤该方向所有挂单(止盈/止损/入场单)、
* 更新 GridElement,并清空 active 与持仓量标记,让价格驱动在下一根 K 线重新挂入场单。
*
* @param mode 持仓模式(DUAL_LONG / DUAL_SHORT)
* @param isLong true=多仓,false=空仓
*/
private void closePositionByMode(Position.ModeEnum mode, boolean isLong) {
String sideLabel = isLong ? "多" : "空";
// 1. 市价平掉对应方向仓位(价格保护拒绝时重试)
try {
Position pos = queryPosition(mode);
if (pos != null) {
String sizeStr = pos.getSize();
long size = Long.parseLong(sizeStr);
if (size != 0) {
Position.ModeEnum posMode = pos.getMode();
FuturesOrder closeOrder = new FuturesOrder();
closeOrder.setContract(config.getContract());
closeOrder.setPrice("0");
closeOrder.setTif(FuturesOrder.TifEnum.IOC);
closeOrder.setReduceOnly(true);
if (posMode != null && posMode.getValue() != null && posMode.getValue().contains("dual")) {
closeOrder.setSize("0");
closeOrder.setClose(false);
closeOrder.setAutoSize(size > 0 ? FuturesOrder.AutoSizeEnum.LONG : FuturesOrder.AutoSizeEnum.SHORT);
} else {
closeOrder.setSize(size > 0 ? String.valueOf(-size) : String.valueOf(Math.abs(size)));
}
closeOrder.setText("t-grid-manual-close");
for (int attempt = 1; attempt <= CLOSE_POSITION_RETRY_MAX_ATTEMPTS; attempt++) {
try {
futuresApi.createFuturesOrder(SETTLE, closeOrder, null);
log.info("[Gate] 手动平{}仓, size:{}, mode:{}", sideLabel, sizeStr, posMode);
break;
} catch (Exception e) {
if (attempt < CLOSE_POSITION_RETRY_MAX_ATTEMPTS && isPriceProtectionRejected(e)) {
log.warn("[Gate] 手动平{}仓遭遇价格保护, 第{}次失败, {}ms后重试: {}",
sideLabel, attempt, CLOSE_POSITION_RETRY_DELAY_MS, e.getMessage());
try {
Thread.sleep(CLOSE_POSITION_RETRY_DELAY_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
} else {
throw e;
}
}
}
}
}
} catch (Exception e) {
log.warn("[Gate] 手动平{}仓失败", sideLabel, e);
}
// 2. 撤该方向所有挂单 + 更新 GridElement
if (isLong) {
cancelAllLongTakeProfitsAndStopLosses();
cancelAllLongEntryOrders();
longActive = false;
longPositionSize = BigDecimal.ZERO;
} else {
cancelAllShortTakeProfitsAndStopLosses();
cancelAllShortEntryOrders();
shortActive = false;
shortPositionSize = BigDecimal.ZERO;
}
log.info("[Gate] {}仓已平仓并清理挂单,交由价格驱动重新入场", sideLabel);
}
/**
* 判断异常是否为市价单价格保护拒绝({@code MARKET_PRICE_TOO_DEVIATED})。
*
* 该错误是交易所对市价单的保护:当盘口价相对最新价偏离超过滑点比率时拒绝下单, * 属于瞬时行情错误,等待行情稳定后重试通常可成功。 */ private boolean isPriceProtectionRejected(Exception e) { if (e instanceof GateApiException) { String label = ((GateApiException) e).getErrorLabel(); return label != null && label.contains("PRICE_TOO_DEVIATED"); } return false; } /** * 取消所有多仓入场单(条件开仓单),并清空 GridElement 的多仓挂单记录。 */ private void cancelAllLongEntryOrders() { for (GridElement e : config.getGridElements()) { for (String oid : new ArrayList<>(e.getLongOrderIds())) { executor.cancelConditionalOrder(oid, o -> {}); } e.getLongTraderParam().setEntryOrderId(null); e.getLongTraderParam().setEntryOrderPlaced(false); e.setHasLongOrder(false); e.clearLongOrderIds(); } GridElement.refreshIndices(); log.info("[Gate] 已提交取消所有多仓入场单"); } /** * 取消所有空仓入场单(条件开仓单),并清空 GridElement 的空仓挂单记录。 */ private void cancelAllShortEntryOrders() { for (GridElement e : config.getGridElements()) { for (String oid : new ArrayList<>(e.getShortOrderIds())) { executor.cancelConditionalOrder(oid, o -> {}); } e.getShortTraderParam().setEntryOrderId(null); e.getShortTraderParam().setEntryOrderPlaced(false); e.setHasShortOrder(false); e.clearShortOrderIds(); } GridElement.refreshIndices(); log.info("[Gate] 已提交取消所有空仓入场单"); } // ========== 止损查表辅助方法 ========== /** 找到第一个有多仓止损单的网格(首个匹配即返回) */ 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); } ); } /** * 分散止盈:把超出基础仓位的部分,从入场网格下一格开始逐格挂止盈单, * 一直挂到对手盘止损位的前一格(不含止损位那一格)。 * *
数量分配:每个网格至少挂 1 张;当 excessQty < 网格数时,张数不够则从起点逐格各挂 1 张、 * 挂满 excessQty 张即停止;当 excessQty ≥ 网格数时,每格 excessQty÷n,余数堆最后一格(即止损位的前一格)。 * *
方向:多仓成交向价格更高方向(id 递增)铺到空仓第一止损位;
* 空仓成交向价格更低方向(id 递减)铺到多仓第一止损位。
*
* @param posSize 当前总持仓张数
* @param isLong true=多仓成交,false=空仓成交
* @param entryGridId 入场网格 ID
*/
private void placeDispersedTakeProfit(int posSize, boolean isLong, int entryGridId) {
int baseQty = Integer.parseInt(config.getBaseQuantity());
int excessQty = posSize - baseQty;
if (excessQty <= 0) {
return;
}
GridElement endElem = isLong ? findFirstShortStopLossGrid() : findFirstLongStopLossGrid();
if (endElem == null) {
log.warn("[Gate] {}分散止盈失败:未找到对手盘止损位", isLong ? "多仓" : "空仓");
return;
}
int dir = isLong ? 1 : -1;
int startId = entryGridId + dir;
// 分配范围到止损位的前一格截止(不含止损位那一格)
int endId = endElem.getId() - dir;
// 终点必须在起点前进方向上,否则退化为集中止盈(全部挂对手盘止损位)
boolean valid = isLong ? (endId >= startId) : (endId <= startId);
if (!valid) {
placeExcessTakeProfit(posSize, isLong);
return;
}
// 收集区间内全部网格(含两端)
List 规则:从入场网格下一格(entryGridId ± 1)开始,每格 baseTakeProfitPerGrid 张,
* 最多挂 baseTakeProfitGridCount 格,挂到止损位的前一格截止(与分散止盈同起点/同终点)。
*
* 边界:累计张数到 min(baseQuantity, 当前持仓) 即停(张数不足则最后一格挂剩余 = "挂几个算几个");
* 网格不足时挂到停止位置结束。
*
* @param posSize 当前总持仓张数
* @param isLong true=多仓止盈,false=空仓止盈
* @param entryGridId 入场网格 ID
*/
private void placeBaseTakeProfit(int posSize, boolean isLong, int entryGridId) {
int perGrid = config.getBaseTakeProfitPerGrid();
int gridCount = config.getBaseTakeProfitGridCount();
if (perGrid <= 0 || gridCount <= 0) {
return;
}
int baseQty = Integer.parseInt(config.getBaseQuantity());
if (baseQty <= 0) {
return;
}
// 张数硬上限 = min(基础张数, 当前持仓),避免持仓不足 baseQuantity 时超挂
int maxQty = Math.min(baseQty, posSize);
if (maxQty <= 0) {
return;
}
GridElement endElem = isLong ? findFirstShortStopLossGrid() : findFirstLongStopLossGrid();
if (endElem == null) {
log.warn("[Gate] {}基础止盈失败:未找到对手盘止损位", isLong ? "多仓" : "空仓");
return;
}
int dir = isLong ? 1 : -1;
int startId = entryGridId + dir;
// 分配范围到止损位的前一格截止(与分散止盈一致)
int endId = endElem.getId() - dir;
int remaining = maxQty;
int placed = 0;
int id = startId;
while (placed < gridCount && remaining > 0) {
if (isLong ? id > endId : id < endId) {
break;
}
GridElement elem = GridElement.findById(id);
if (elem == null) {
break;
}
int qty = Math.min(perGrid, remaining);
final GridElement fe = elem;
final int gid = id;
final BigDecimal triggerPrice = elem.getGridPrice();
final String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty);
final String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT;
final FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1
: FuturesPriceTrigger.RuleEnum.NUMBER_2;
executor.placeTakeProfit(triggerPrice, rule, orderType, size,
profitId -> {
if (isLong) {
longTakeProfitTraderIdParam(fe, profitId, true);
} else {
shortTakeProfitTraderIdParam(fe, profitId, true);
}
log.info("[Gate] 基础止盈 {} gridId:{}, 触发价:{}, 数量:{}, tpId:{}",
isLong ? "多仓" : "空仓", gid, triggerPrice, size, profitId);
}
);
remaining -= qty;
placed++;
id += dir;
}
log.info("[Gate] 基础止盈 {} 挂单完成, 入场gridId:{}, 挂{}格, 剩余{}张",
isLong ? "多仓" : "空仓", entryGridId, placed, remaining);
}
/**
* 在指定网格挂一笔对手止盈单(非满仓超额止盈,挂在止损触发位的下一格)。
*/
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);
}
);
}
}