package com.xcong.excoin.modules.gateApi; 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.FuturesPriceTrigger; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; /** * Gate 网格交易服务类。使用 Gate SDK 通过 REST API 下单。 * *

状态机

*
 *   WAITING_KLINE → (首次 K 线) → OPENING → ACTIVE
 *                             双开失败 → STOPPED
 *
 *   ACTIVE:
 *     ├─ 仓位 size=0 且方向活跃 → REOPENING_L/S → ACTIVE
 *     │   补仓失败 → 重试 → 仍失败 → STOPPED
 *     └─ cumulativePnl ≥ overallTp 或 ≤ -maxLoss → STOPPED
 * 
* *

架构

* REST 下单委派给 {@link GateTradeExecutor}(独立线程池,避免阻塞 WS 回调线程)。 * * @author Administrator */ @Slf4j public class GateGridTradeService { public enum StrategyState { WAITING_KLINE, OPENING, ACTIVE, REOPENING_LONG, REOPENING_SHORT, STOPPED } private final GateConfig config; private final GateTradeExecutor executor; private final FuturesApi futuresApi; private static final String SETTLE = "usdt"; private volatile StrategyState state = StrategyState.WAITING_KLINE; /** 多头是否活跃(有仓位) */ private volatile boolean longActive = false; /** 空头是否活跃(有仓位) */ private volatile boolean shortActive = false; private BigDecimal longEntryPrice; private BigDecimal shortEntryPrice; private volatile BigDecimal lastKlinePrice; private volatile BigDecimal cumulativePnl = BigDecimal.ZERO; private Long userId; private int longReopenFails = 0; private int shortReopenFails = 0; public GateGridTradeService(GateConfig config) { this.config = config; ApiClient apiClient = new ApiClient(); apiClient.setBasePath(config.getRestBasePath()); apiClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); this.futuresApi = new FuturesApi(apiClient); this.executor = new GateTradeExecutor(apiClient, config.getContract()); } public void init() { try { ApiClient detailClient = new ApiClient(); detailClient.setBasePath(config.getRestBasePath()); detailClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); AccountDetail detail = new AccountApi(detailClient).getAccountDetail(); this.userId = detail.getUserId(); log.info("[Gate] uid:{}", userId); FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); if (!config.getPositionMode().equals(account.getPositionMode())) { futuresApi.setPositionMode(SETTLE, config.getPositionMode()); } log.info("[Gate] mode:{} balance:{}", config.getPositionMode(), account.getAvailable()); futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); log.info("[Gate] old orders cleared"); futuresApi.updateContractPositionLeverageCall( SETTLE, config.getContract(), config.getLeverage(), config.getMarginMode(), config.getPositionMode(), null); log.info("[Gate] {}x {}", config.getLeverage(), config.getMarginMode()); } catch (GateApiException e) { log.error("[Gate] init fail, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); } catch (ApiException e) { log.error("[Gate] init fail, code:{}", e.getCode()); } } public void startGrid() { if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { log.warn("[Gate] already running, state:{}", state); return; } state = StrategyState.WAITING_KLINE; cumulativePnl = BigDecimal.ZERO; longActive = false; shortActive = false; longReopenFails = 0; shortReopenFails = 0; log.info("[Gate] grid started"); } public void stopGrid() { state = StrategyState.STOPPED; executor.cancelAllPriceTriggeredOrders(); executor.shutdown(); log.info("[Gate] stopped, pnl:{}", cumulativePnl); } /** * K 线回调。首次价格就绪 → 异步双开。 */ public void onKline(BigDecimal closePrice) { lastKlinePrice = closePrice; if (state != StrategyState.WAITING_KLINE) { return; } state = StrategyState.OPENING; log.info("[Gate] first kline, opening..."); executor.openLong(config.getQuantity(), () -> { synchronized (this) { longEntryPrice = lastKlinePrice; longActive = true; } executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-long-position", "close_long"); }); executor.openShort(negate(config.getQuantity()), () -> { synchronized (this) { shortEntryPrice = lastKlinePrice; shortActive = true; } executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-short-position", "close_short"); if (longActive && shortActive && state != StrategyState.STOPPED) { state = StrategyState.ACTIVE; log.info("[Gate] active, long:{}, short:{}, tpL:{}, tpS:{}", longEntryPrice, shortEntryPrice, longTpPrice(), shortTpPrice()); } }); } /** * 仓位推送回调。检测 size=0 触发补仓。 */ public void onPositionUpdate(String contract, String mode, BigDecimal size, BigDecimal entryPrice) { if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { return; } boolean hasPosition = size.abs().compareTo(BigDecimal.ZERO) > 0; if ("dual_long".equals(mode)) { if (longActive && !hasPosition) { log.info("[Gate] long closed"); longActive = false; tryReopenLong(0); } else if (hasPosition) { longActive = true; longEntryPrice = entryPrice; } } else if ("dual_short".equals(mode)) { if (shortActive && !hasPosition) { log.info("[Gate] short closed"); shortActive = false; tryReopenShort(0); } else if (hasPosition) { shortActive = true; shortEntryPrice = entryPrice; } } } /** * 平仓推送回调。累加 pnl 并检查停止条件。 */ public void onPositionClose(String contract, String side, BigDecimal pnl) { if (state == StrategyState.STOPPED) { return; } cumulativePnl = cumulativePnl.add(pnl); log.info("[Gate] pnl+{}, side:{}, total:{}", pnl, side, cumulativePnl); if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { log.info("[Gate] TP reached {}→STOPPED", cumulativePnl); state = StrategyState.STOPPED; } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) { log.info("[Gate] loss {}→STOPPED", cumulativePnl); state = StrategyState.STOPPED; } } // ---- reopen with retry ---- private void tryReopenLong(int retry) { if (state == StrategyState.STOPPED) { return; } if (longActive) { return; } state = StrategyState.REOPENING_LONG; executor.openLong(config.getQuantity(), () -> { synchronized (this) { longEntryPrice = lastKlinePrice; longActive = true; } executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-long-position", "close_long"); longReopenFails = 0; if (state != StrategyState.STOPPED) { state = StrategyState.ACTIVE; } log.info("[Gate] long reopened, price:{}", longEntryPrice); }); } private void tryReopenShort(int retry) { if (state == StrategyState.STOPPED) { return; } if (shortActive) { return; } state = StrategyState.REOPENING_SHORT; executor.openShort(negate(config.getQuantity()), () -> { synchronized (this) { shortEntryPrice = lastKlinePrice; shortActive = true; } executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-short-position", "close_short"); shortReopenFails = 0; if (state != StrategyState.STOPPED) { state = StrategyState.ACTIVE; } log.info("[Gate] short reopened, price:{}", shortEntryPrice); }); } // ---- util ---- private BigDecimal longTpPrice() { return lastKlinePrice.multiply(BigDecimal.ONE.add(config.getGridRate())) .setScale(1, RoundingMode.HALF_UP); } private BigDecimal shortTpPrice() { return lastKlinePrice.multiply(BigDecimal.ONE.subtract(config.getGridRate())) .setScale(1, RoundingMode.HALF_UP); } private String negate(String qty) { return qty.startsWith("-") ? qty.substring(1) : "-" + qty; } public BigDecimal getLastKlinePrice() { return lastKlinePrice; } public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; } public BigDecimal getCumulativePnl() { return cumulativePnl; } public Long getUserId() { return userId; } public StrategyState getState() { return state; } }