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 okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; 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 ** *
实时查询当前保证金占用额(positionInitialMargin),计算其占初始本金的比例。 * 比例 ≥ marginRatioLimit(默认 20%)时拒绝开仓,但仍照常更新队列。 * *
查询失败时默认放行(返回 true),避免因 REST API 异常导致策略完全停滞。 * * @return true=安全可开仓 / false=保证金超限跳过开仓 */ private boolean isMarginSafe() { try { FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); BigDecimal margin = new BigDecimal(account.getPositionInitialMargin()); BigDecimal ratio = margin.divide(initialPrincipal, 4, RoundingMode.HALF_UP); log.debug("[Gate] 保证金比例: {}/{}={}", margin, initialPrincipal, ratio); return ratio.compareTo(config.getMarginRatioLimit()) < 0; } catch (Exception e) { log.warn("[Gate] 查保证金失败,默认放行", e); return true; } } // ---- 工具 ---- /** * 取反字符串数字。如 "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) { gridElement.setHasLongOrder(false); gridElement.setLongOrderId(null); } else { gridElement.setHasShortOrder(false); gridElement.setShortOrderId(null); } 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; }
}