| | |
| | | import io.gate.gateapi.api.FuturesApi; |
| | | import io.gate.gateapi.models.AccountDetail; |
| | | import io.gate.gateapi.models.FuturesAccount; |
| | | import io.gate.gateapi.models.FuturesOrder; |
| | | import io.gate.gateapi.models.FuturesPriceTrigger; |
| | | import io.gate.gateapi.models.Position; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | |
| | | WAITING_KLINE, OPENING, ACTIVE, REOPENING_LONG, REOPENING_SHORT, STOPPED |
| | | } |
| | | |
| | | private static final String AUTO_SIZE_LONG = "close_long"; |
| | | private static final String AUTO_SIZE_SHORT = "close_short"; |
| | | private static final String ORDER_TYPE_CLOSE_LONG = "close-long-position"; |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "close-short-position"; |
| | | |
| | | private final GateConfig config; |
| | | private final GateTradeExecutor executor; |
| | | private final FuturesApi futuresApi; |
| | |
| | | private volatile BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | private Long userId; |
| | | |
| | | /** 多头补仓连续失败次数 */ |
| | | private int longReopenFails = 0; |
| | | /** 空头补仓连续失败次数 */ |
| | | private int shortReopenFails = 0; |
| | | |
| | | public GateGridTradeService(GateConfig config) { |
| | |
| | | detailClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | | AccountDetail detail = new AccountApi(detailClient).getAccountDetail(); |
| | | this.userId = detail.getUserId(); |
| | | log.info("[Gate] uid:{}", userId); |
| | | log.info("[Gate] 用户ID: {}", 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()); |
| | | log.info("[Gate] 持仓模式: {} 余额: {}", config.getPositionMode(), account.getAvailable()); |
| | | |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | log.info("[Gate] old orders cleared"); |
| | | log.info("[Gate] 旧条件单已清除"); |
| | | |
| | | closeExistingPositions(); |
| | | |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, config.getContract(), config.getLeverage(), |
| | | config.getMarginMode(), config.getPositionMode(), null); |
| | | log.info("[Gate] {}x {}", config.getLeverage(), config.getMarginMode()); |
| | | log.info("[Gate] 杠杆: {}x {}", config.getLeverage(), config.getMarginMode()); |
| | | } catch (GateApiException e) { |
| | | log.error("[Gate] init fail, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | log.error("[Gate] 初始化失败, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (ApiException e) { |
| | | log.error("[Gate] init fail, code:{}", e.getCode()); |
| | | log.error("[Gate] 初始化失败, code:{}", e.getCode()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 平掉当前合约所有已有仓位。 |
| | | * 策略启动前的准备工作,确保从零持仓状态开始运行。 |
| | | */ |
| | | private void closeExistingPositions() { |
| | | try { |
| | | java.util.List<Position> positions = futuresApi.listPositions(SETTLE).execute(); |
| | | if (positions == null || positions.isEmpty()) { |
| | | log.info("[Gate] 无已有仓位,无需平仓"); |
| | | return; |
| | | } |
| | | |
| | | for (Position pos : positions) { |
| | | if (!config.getContract().equals(pos.getContract())) { |
| | | continue; |
| | | } |
| | | String sizeStr = pos.getSize(); |
| | | long size = Long.parseLong(sizeStr); |
| | | if (size == 0) { |
| | | continue; |
| | | } |
| | | |
| | | String closeSize = size > 0 ? String.valueOf(-size) : String.valueOf(Math.abs(size)); |
| | | boolean isLong = size > 0; |
| | | Position.ModeEnum mode = pos.getMode(); |
| | | |
| | | FuturesOrder closeOrder = new FuturesOrder(); |
| | | closeOrder.setContract(config.getContract()); |
| | | closeOrder.setPrice("0"); |
| | | closeOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | closeOrder.setReduceOnly(true); |
| | | if (mode != null && mode.getValue() != null && mode.getValue().contains("dual")) { |
| | | closeOrder.setSize("0"); |
| | | closeOrder.setClose(false); |
| | | closeOrder.setAutoSize(isLong ? FuturesOrder.AutoSizeEnum.LONG : FuturesOrder.AutoSizeEnum.SHORT); |
| | | } else { |
| | | closeOrder.setSize(closeSize); |
| | | } |
| | | closeOrder.setText("t-grid-init-close"); |
| | | futuresApi.createFuturesOrder(SETTLE, closeOrder, null); |
| | | log.info("[Gate] 已平掉已有仓位, 方向:{}, sizes:{}, mode:{}", isLong ? "多头" : "空头", sizeStr, mode); |
| | | } |
| | | } catch (GateApiException e) { |
| | | log.warn("[Gate] 平已有仓位失败, label:{}, msg:{}, 可能无仓位", e.getErrorLabel(), e.getMessage()); |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 平已有仓位异常, 可能无仓位", e); |
| | | } |
| | | } |
| | | |
| | | public void startGrid() { |
| | | if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { |
| | | log.warn("[Gate] already running, state:{}", state); |
| | | log.warn("[Gate] 策略已在运行中, state:{}", state); |
| | | return; |
| | | } |
| | | state = StrategyState.WAITING_KLINE; |
| | |
| | | shortActive = false; |
| | | longReopenFails = 0; |
| | | shortReopenFails = 0; |
| | | log.info("[Gate] grid started"); |
| | | log.info("[Gate] 网格策略已启动"); |
| | | } |
| | | |
| | | public void stopGrid() { |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | executor.shutdown(); |
| | | log.info("[Gate] stopped, pnl:{}", cumulativePnl); |
| | | log.info("[Gate] 策略已停止, 累计盈亏: {}", cumulativePnl); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | state = StrategyState.OPENING; |
| | | log.info("[Gate] first kline, opening..."); |
| | | log.info("[Gate] 首根K线到达,开始双开..."); |
| | | |
| | | executor.openLong(config.getQuantity(), () -> { |
| | | synchronized (this) { |
| | |
| | | longActive = true; |
| | | } |
| | | executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | "close-long-position", "close_long"); |
| | | }); |
| | | ORDER_TYPE_CLOSE_LONG, AUTO_SIZE_LONG); |
| | | }, null); |
| | | executor.openShort(negate(config.getQuantity()), () -> { |
| | | synchronized (this) { |
| | | shortEntryPrice = lastKlinePrice; |
| | | shortActive = true; |
| | | } |
| | | executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | "close-short-position", "close_short"); |
| | | ORDER_TYPE_CLOSE_SHORT, AUTO_SIZE_SHORT); |
| | | if (longActive && shortActive && state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | log.info("[Gate] active, long:{}, short:{}, tpL:{}, tpS:{}", |
| | | log.info("[Gate] 已激活, 多头入场:{}, 空头入场:{}, 多头止盈:{}, 空头止盈:{}", |
| | | longEntryPrice, shortEntryPrice, longTpPrice(), shortTpPrice()); |
| | | } |
| | | }); |
| | | }, null); |
| | | } |
| | | |
| | | /** |
| | | * 仓位推送回调。检测 size=0 触发补仓。 |
| | | */ |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, BigDecimal entryPrice) { |
| | | public void onPositionUpdate(String contract, Position.ModeEnum 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 (Position.ModeEnum.DUAL_LONG == mode) { |
| | | if (longActive && !hasPosition) { |
| | | log.info("[Gate] long closed"); |
| | | log.info("[Gate] 多头已平仓"); |
| | | longActive = false; |
| | | tryReopenLong(0); |
| | | tryReopenLong(); |
| | | } else if (hasPosition) { |
| | | longActive = true; |
| | | longEntryPrice = entryPrice; |
| | | } |
| | | } else if ("dual_short".equals(mode)) { |
| | | } else if (Position.ModeEnum.DUAL_SHORT == mode) { |
| | | if (shortActive && !hasPosition) { |
| | | log.info("[Gate] short closed"); |
| | | log.info("[Gate] 空头已平仓"); |
| | | shortActive = false; |
| | | tryReopenShort(0); |
| | | tryReopenShort(); |
| | | } else if (hasPosition) { |
| | | shortActive = true; |
| | | shortEntryPrice = entryPrice; |
| | |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | log.info("[Gate] pnl+{}, side:{}, total:{}", pnl, side, cumulativePnl); |
| | | log.info("[Gate] 盈亏累加:{}, 方向:{}, 累计:{}", pnl, side, cumulativePnl); |
| | | |
| | | if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { |
| | | log.info("[Gate] TP reached {}→STOPPED", cumulativePnl); |
| | | log.info("[Gate] 已达止盈目标 {}→已停止", cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) { |
| | | log.info("[Gate] loss {}→STOPPED", cumulativePnl); |
| | | log.info("[Gate] 已达亏损上限 {}→已停止", cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } |
| | | } |
| | | |
| | | // ---- reopen with retry ---- |
| | | // ---- 补仓(含失败重试) ---- |
| | | |
| | | private void tryReopenLong(int retry) { |
| | | private void tryReopenLong() { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (longActive) { |
| | | return; |
| | | } |
| | | |
| | | longReopenFails++; |
| | | if (longReopenFails > config.getReopenMaxRetries()) { |
| | | log.warn("[Gate] 多头补仓连续失败{}次,停止策略", longReopenFails); |
| | | state = StrategyState.STOPPED; |
| | | return; |
| | | } |
| | | |
| | |
| | | longActive = true; |
| | | } |
| | | executor.placeTakeProfit(longTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | "close-long-position", "close_long"); |
| | | ORDER_TYPE_CLOSE_LONG, AUTO_SIZE_LONG); |
| | | longReopenFails = 0; |
| | | if (state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | log.info("[Gate] long reopened, price:{}", longEntryPrice); |
| | | }); |
| | | log.info("[Gate] 多头已补开, 价格:{}", longEntryPrice); |
| | | }, this::tryReopenLong); |
| | | } |
| | | |
| | | private void tryReopenShort(int retry) { |
| | | private void tryReopenShort() { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (shortActive) { |
| | | return; |
| | | } |
| | | |
| | | shortReopenFails++; |
| | | if (shortReopenFails > config.getReopenMaxRetries()) { |
| | | log.warn("[Gate] 空头补仓连续失败{}次,停止策略", shortReopenFails); |
| | | state = StrategyState.STOPPED; |
| | | return; |
| | | } |
| | | |
| | |
| | | shortActive = true; |
| | | } |
| | | executor.placeTakeProfit(shortTpPrice(), FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | "close-short-position", "close_short"); |
| | | ORDER_TYPE_CLOSE_SHORT, AUTO_SIZE_SHORT); |
| | | shortReopenFails = 0; |
| | | if (state != StrategyState.STOPPED) { |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | log.info("[Gate] short reopened, price:{}", shortEntryPrice); |
| | | }); |
| | | log.info("[Gate] 空头已补开, 价格:{}", shortEntryPrice); |
| | | }, this::tryReopenShort); |
| | | } |
| | | |
| | | // ---- util ---- |
| | | // ---- 止盈价格计算 ---- |
| | | |
| | | private BigDecimal longTpPrice() { |
| | | return lastKlinePrice.multiply(BigDecimal.ONE.add(config.getGridRate())) |
| | |
| | | .setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** 对数量取反(开多用正数,开空用负数) */ |
| | | private String negate(String qty) { |
| | | return qty.startsWith("-") ? qty.substring(1) : "-" + qty; |
| | | } |