| | |
| | | import io.gate.gateapi.api.FuturesApi; |
| | | import io.gate.gateapi.models.AccountDetail; |
| | | import io.gate.gateapi.models.FuturesAccount; |
| | | import io.gate.gateapi.models.FuturesInitialOrder; |
| | | import io.gate.gateapi.models.FuturesOrder; |
| | | import io.gate.gateapi.models.FuturesPriceTrigger; |
| | | import io.gate.gateapi.models.FuturesPriceTriggeredOrder; |
| | | import io.gate.gateapi.models.TriggerOrderResponse; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | |
| | | /** |
| | | * Gate 网格交易服务类。 |
| | | * Gate 网格交易服务类。使用 Gate SDK 通过 REST API 下单。 |
| | | * |
| | | * <h3>策略概述</h3> |
| | | * 多空双开 → 各放止盈条件单 → 仓位推送驱动补仓 → history_pnl 判断停止 |
| | | * |
| | | * <h3>触发逻辑</h3> |
| | | * <h3>状态机</h3> |
| | | * <pre> |
| | | * K线首次价格就绪 → dualOpenPositions() // 多空双开 + 止盈条件单 |
| | | * 仓位推送 size=0 → reopenXxxPosition() // 该方向被止盈平掉 → 补开 |
| | | * 仓位推送 history_pnl ≥ overallTp → 停止 |
| | | * 仓位推送 history_pnl ≤ -maxLoss → 停止 |
| | | * WAITING_KLINE → (首次 K 线) → OPENING → ACTIVE |
| | | * 双开失败 → STOPPED |
| | | * |
| | | * ACTIVE: |
| | | * ├─ 仓位 size=0 且方向活跃 → REOPENING_L/S → ACTIVE |
| | | * │ 补仓失败 → 重试 → 仍失败 → STOPPED |
| | | * └─ cumulativePnl ≥ overallTp 或 ≤ -maxLoss → STOPPED |
| | | * </pre> |
| | | * |
| | | * <h3>止盈计算</h3> |
| | | * 多头止盈价 = entryPrice × (1 + gridRate)<br> |
| | | * 空头止盈价 = entryPrice × (1 - gridRate) |
| | | * |
| | | * <h3>依赖</h3> |
| | | * 使用 {@code io.gate:gate-api (7.2.71)} SDK 通过 REST API 下单, |
| | | * 市场数据由 {@link GateKlineWebSocketClient} 通过 WebSocket 提供。 |
| | | * <h3>架构</h3> |
| | | * REST 下单委派给 {@link GateTradeExecutor}(独立线程池,避免阻塞 WS 回调线程)。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class GateGridTradeService { |
| | | |
| | | private final ApiClient apiClient; |
| | | 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 final String contract; |
| | | private final String leverage; |
| | | private final String marginMode; |
| | | private final BigDecimal gridRate; |
| | | private final BigDecimal overallTp; |
| | | private final BigDecimal maxLoss; |
| | | private final String quantity; |
| | | private final String positionMode; |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | | /** 策略是否处于运行状态 */ |
| | | private volatile boolean strategyActive = false; |
| | | /** 是否已完成首次双开 */ |
| | | private volatile boolean dualOpened = false; |
| | | /** 多头仓位是否活跃 */ |
| | | /** 多头是否活跃(有仓位) */ |
| | | private volatile boolean longActive = false; |
| | | /** 空头仓位是否活跃 */ |
| | | /** 空头是否活跃(有仓位) */ |
| | | private volatile boolean shortActive = false; |
| | | |
| | | /** 多头入场价 */ |
| | | private BigDecimal longEntryPrice; |
| | | /** 空头入场价 */ |
| | | private BigDecimal shortEntryPrice; |
| | | /** WebSocket 推送的最新 K 线收盘价 */ |
| | | private volatile BigDecimal lastKlinePrice; |
| | | /** 服务器返回的累计已实现盈亏 */ |
| | | private BigDecimal totalHistoryPnl = BigDecimal.ZERO; |
| | | /** 用户 ID,用于 WebSocket 私有频道订阅 */ |
| | | private volatile BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 构造函数,初始化 Gate 期货 API 客户端。 |
| | | * |
| | | * @param contract 合约名称(如 XAU_USDT) |
| | | * @param leverage 杠杆倍数 |
| | | * @param marginMode 保证金模式(cross/isolated) |
| | | * @param positionMode 持仓模式(single/dual/dual_plus) |
| | | * @param gridRate 网格间距比例(如 0.0035) |
| | | * @param overallTp 整体止盈阈值(USDT) |
| | | * @param maxLoss 最大亏损阈值(USDT) |
| | | * @param quantity 下单数量(合约张数) |
| | | */ |
| | | public GateGridTradeService(String apiKey, String apiSecret, |
| | | String contract, String leverage, |
| | | String marginMode, String positionMode, |
| | | BigDecimal gridRate, BigDecimal overallTp, |
| | | int maxCycles, BigDecimal maxLoss, |
| | | String quantity) { |
| | | this.contract = contract; |
| | | this.leverage = leverage; |
| | | this.marginMode = marginMode; |
| | | this.gridRate = gridRate; |
| | | this.overallTp = overallTp; |
| | | this.maxLoss = maxLoss; |
| | | this.quantity = quantity; |
| | | this.positionMode = positionMode; |
| | | private int longReopenFails = 0; |
| | | private int shortReopenFails = 0; |
| | | |
| | | this.apiClient = new ApiClient(); |
| | | this.apiClient.setBasePath("https://api-testnet.gateapi.io/api/v4"); |
| | | this.apiClient.setApiKeySecret(apiKey, apiSecret); |
| | | 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 { |
| | | AccountApi accountApi = new AccountApi(apiClient); |
| | | AccountDetail accountDetail = accountApi.getAccountDetail(); |
| | | this.userId = accountDetail.getUserId(); |
| | | log.info("[GateGrid] 用户ID: {}", userId); |
| | | |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | log.info("[GateGrid] 已取消所有既有止盈止损条件单"); |
| | | |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, contract, leverage, marginMode, positionMode, null); |
| | | log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); |
| | | 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); |
| | | log.info("[GateGrid] 账户可用余额: {}, 总资产: {}", |
| | | account.getAvailable(), account.getTotal()); |
| | | String positionModeSet = account.getPositionMode(); |
| | | if (!positionMode.equals(positionModeSet)) { |
| | | futuresApi.setPositionMode(SETTLE, positionMode); |
| | | if (!config.getPositionMode().equals(account.getPositionMode())) { |
| | | futuresApi.setPositionMode(SETTLE, config.getPositionMode()); |
| | | } |
| | | log.info("[GateGrid] 已设置双向持仓模式"); |
| | | 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("[GateGrid] 初始化失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | log.error("[Gate] init fail, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (ApiException e) { |
| | | log.error("[GateGrid] 初始化API调用失败, code: {}", e.getCode()); |
| | | log.error("[Gate] init fail, code:{}", e.getCode()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动网格策略。策略激活后等待 K 线价格就绪,然后自动首次双开。 |
| | | */ |
| | | public void startGrid() { |
| | | if (strategyActive) { |
| | | log.warn("[GateGrid] 策略已在运行中"); |
| | | if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { |
| | | log.warn("[Gate] already running, state:{}", state); |
| | | return; |
| | | } |
| | | strategyActive = true; |
| | | totalHistoryPnl = BigDecimal.ZERO; |
| | | log.info("[GateGrid] 网格策略启动,等待K线价格..."); |
| | | state = StrategyState.WAITING_KLINE; |
| | | cumulativePnl = BigDecimal.ZERO; |
| | | longActive = false; |
| | | shortActive = false; |
| | | longReopenFails = 0; |
| | | shortReopenFails = 0; |
| | | log.info("[Gate] grid started"); |
| | | } |
| | | |
| | | /** |
| | | * 停止网格策略。 |
| | | */ |
| | | public void stopGrid() { |
| | | strategyActive = false; |
| | | log.info("[GateGrid] 网格策略已停止, history_pnl: {}", totalHistoryPnl); |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | executor.shutdown(); |
| | | log.info("[Gate] stopped, pnl:{}", cumulativePnl); |
| | | } |
| | | |
| | | /** |
| | | * K 线回调入口。由 调用。 |
| | | * 首次收到价格时触发多空双开,后续仅缓存最新价格供补仓使用。 |
| | | * |
| | | * @param closePrice K 线收盘价 |
| | | * K 线回调。首次价格就绪 → 异步双开。 |
| | | */ |
| | | public void onKline(BigDecimal closePrice) { |
| | | lastKlinePrice = closePrice; |
| | | if (!strategyActive) { |
| | | if (state != StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | if (!dualOpened) { |
| | | dualOpened = true; |
| | | dualOpenPositions(); |
| | | } |
| | | |
| | | 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()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 仓位推送回调入口。由 {@link GateKlineWebSocketClient} 调用。 |
| | | * 根据仓位模式(dual_long/dual_short)和 size 判断: |
| | | * <ul> |
| | | * <li>size=0 且之前活跃 → 该方向被平仓 → 补开</li> |
| | | * <li>size>0 → 确认仓位活跃,更新入场价</li> |
| | | * </ul> |
| | | * 每次推送更新 history_pnl 并检查停止条件。 |
| | | * |
| | | * @param contract 合约名 |
| | | * @param mode 仓位模式(dual_long / dual_short) |
| | | * @param size 仓位数量(0 表示无仓位) |
| | | * @param entryPrice 入场价格 |
| | | * @param historyPnl 已实现累计盈亏 |
| | | * @param realisedPnl 已实现盈亏 |
| | | * 仓位推送回调。检测 size=0 触发补仓。 |
| | | */ |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, |
| | | BigDecimal entryPrice, BigDecimal historyPnl, |
| | | BigDecimal realisedPnl) { |
| | | if (!strategyActive) { |
| | | public void onPositionUpdate(String contract, String mode, BigDecimal size, BigDecimal entryPrice) { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | |
| | |
| | | |
| | | if ("dual_long".equals(mode)) { |
| | | if (longActive && !hasPosition) { |
| | | log.info("[GateGrid] 多头被平仓(止盈触发), 重新开多"); |
| | | log.info("[Gate] long closed"); |
| | | longActive = false; |
| | | reopenLongPosition(); |
| | | tryReopenLong(0); |
| | | } else if (hasPosition) { |
| | | longActive = true; |
| | | longEntryPrice = entryPrice; |
| | | } |
| | | } else if ("dual_short".equals(mode)) { |
| | | if (shortActive && !hasPosition) { |
| | | log.info("[GateGrid] 空头被平仓(止盈触发), 重新开空"); |
| | | log.info("[Gate] short closed"); |
| | | shortActive = false; |
| | | reopenShortPosition(); |
| | | tryReopenShort(0); |
| | | } else if (hasPosition) { |
| | | shortActive = true; |
| | | shortEntryPrice = entryPrice; |
| | | } |
| | | } |
| | | |
| | | totalHistoryPnl = historyPnl; |
| | | checkStopConditions(); |
| | | } |
| | | |
| | | /** |
| | | * 检查策略停止条件。满足任一即置 strategyActive=false: |
| | | * <ul> |
| | | * <li>累计盈利 ≥ overallTp</li> |
| | | * <li>累计亏损 ≤ -maxLoss</li> |
| | | * </ul> |
| | | * 平仓推送回调。累加 pnl 并检查停止条件。 |
| | | */ |
| | | private void checkStopConditions() { |
| | | if (totalHistoryPnl.compareTo(overallTp) >= 0) { |
| | | log.info("[GateGrid] 累计止盈 {} 达到 {} USDT,停止策略", totalHistoryPnl, overallTp); |
| | | strategyActive = false; |
| | | public void onPositionClose(String contract, String side, BigDecimal pnl) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (totalHistoryPnl.compareTo(maxLoss.negate()) <= 0) { |
| | | log.info("[GateGrid] 累计亏损 {} 达到上限 {} USDT,停止策略", totalHistoryPnl, maxLoss); |
| | | strategyActive = false; |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 首次多空双开。使用当前 K 线价格以市价单同时开多和开空, |
| | | * 开仓成功后立即为每个方向创建止盈条件单。 |
| | | */ |
| | | private void dualOpenPositions() { |
| | | if (lastKlinePrice == null) { |
| | | log.warn("[GateGrid] K线价格未就绪,跳过双开"); |
| | | dualOpened = false; |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder longOrder = new FuturesOrder(); |
| | | longOrder.setContract(contract); |
| | | longOrder.setSize(quantity); |
| | | longOrder.setPrice("0"); |
| | | longOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | longOrder.setText("t-grid-long-init"); |
| | | FuturesOrder longResult = futuresApi.createFuturesOrder(SETTLE, longOrder, null); |
| | | longEntryPrice = safeDecimal(longResult.getFillPrice()); |
| | | longActive = true; |
| | | log.info("[GateGrid] 开多成功, price: {}, id: {}", longEntryPrice, longResult.getId()); |
| | | placeLongTp(longEntryPrice); |
| | | // ---- reopen with retry ---- |
| | | |
| | | FuturesOrder shortOrder = new FuturesOrder(); |
| | | shortOrder.setContract(contract); |
| | | shortOrder.setSize(negateQuantity(quantity)); |
| | | shortOrder.setPrice("0"); |
| | | shortOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | shortOrder.setText("t-grid-short-init"); |
| | | FuturesOrder shortResult = futuresApi.createFuturesOrder(SETTLE, shortOrder, null); |
| | | shortEntryPrice = safeDecimal(shortResult.getFillPrice()); |
| | | shortActive = true; |
| | | log.info("[GateGrid] 开空成功, price: {}, id: {}", shortEntryPrice, shortResult.getId()); |
| | | placeShortTp(shortEntryPrice); |
| | | |
| | | printGridInfo(); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 双开失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | strategyActive = false; |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 双开异常", e); |
| | | strategyActive = false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 补开多头仓位。多头被止盈平掉后调用,市价重新开多并创建止盈单。 |
| | | */ |
| | | private void reopenLongPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | private void tryReopenLong(int retry) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (longActive) { |
| | | log.warn("[GateGrid] 多头已存在,跳过补开"); |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder longOrder = new FuturesOrder(); |
| | | longOrder.setContract(contract); |
| | | longOrder.setSize(quantity); |
| | | longOrder.setPrice("0"); |
| | | longOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | longOrder.setText("t-grid-long-reopen"); |
| | | FuturesOrder longResult = futuresApi.createFuturesOrder(SETTLE, longOrder, null); |
| | | longEntryPrice = safeDecimal(longResult.getFillPrice()); |
| | | longActive = true; |
| | | log.info("[GateGrid] 补开多成功, price: {}, id: {}", longEntryPrice, longResult.getId()); |
| | | placeLongTp(longEntryPrice); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 补开多失败", e); |
| | | } |
| | | |
| | | 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 reopenShortPosition() { |
| | | if (lastKlinePrice == null || !strategyActive) { |
| | | private void tryReopenShort(int retry) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | if (shortActive) { |
| | | log.warn("[GateGrid] 空头已存在,跳过补开"); |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder shortOrder = new FuturesOrder(); |
| | | shortOrder.setContract(contract); |
| | | shortOrder.setSize(negateQuantity(quantity)); |
| | | shortOrder.setPrice("0"); |
| | | shortOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | shortOrder.setText("t-grid-short-reopen"); |
| | | FuturesOrder shortResult = futuresApi.createFuturesOrder(SETTLE, shortOrder, null); |
| | | shortEntryPrice = safeDecimal(shortResult.getFillPrice()); |
| | | shortActive = true; |
| | | log.info("[GateGrid] 补开空成功, price: {}, id: {}", shortEntryPrice, shortResult.getId()); |
| | | placeShortTp(shortEntryPrice); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 补开空失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 创建多头止盈条件单。 |
| | | * 触发价 = entryPrice × (1 + gridRate),价格 ≥ 触发价时平多。 |
| | | */ |
| | | private void placeLongTp(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-long-position", "close_long"); |
| | | log.info("[GateGrid] 多头止盈已设置, TP:{}", tpPrice); |
| | | } |
| | | |
| | | /** |
| | | * 创建空头止盈条件单。 |
| | | * 触发价 = entryPrice × (1 - gridRate),价格 ≤ 触发价时平空。 |
| | | */ |
| | | private void placeShortTp(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-short-position", "close_short"); |
| | | log.info("[GateGrid] 空头止盈已设置, TP:{}", tpPrice); |
| | | } |
| | | |
| | | /** |
| | | * 通过 Gate REST API 创建止盈条件单。 |
| | | * |
| | | * @param triggerPrice 触发价格 |
| | | * @param rule 触发规则(1: ≥, 2: ≤) |
| | | * @param orderType 止盈止损类型(close-long-position / close-short-position) |
| | | * @param autoSize 双仓平仓方向(close_long / close_short) |
| | | */ |
| | | private void placePriceTriggeredOrder(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | try { |
| | | FuturesPriceTrigger trigger = new FuturesPriceTrigger(); |
| | | trigger.setStrategyType(FuturesPriceTrigger.StrategyTypeEnum.NUMBER_0); |
| | | trigger.setPriceType(FuturesPriceTrigger.PriceTypeEnum.NUMBER_0); |
| | | trigger.setPrice(triggerPrice.toString()); |
| | | trigger.setRule(rule); |
| | | trigger.setExpiration(0); |
| | | |
| | | FuturesInitialOrder initial = new FuturesInitialOrder(); |
| | | initial.setContract(contract); |
| | | initial.setSize(0L); |
| | | initial.setPrice("0"); |
| | | initial.setTif(FuturesInitialOrder.TifEnum.IOC); |
| | | initial.setReduceOnly(true); |
| | | initial.setAutoSize(autoSize); |
| | | |
| | | FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder(); |
| | | order.setTrigger(trigger); |
| | | order.setInitial(initial); |
| | | order.setOrderType(orderType); |
| | | |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[GateGrid] 止盈条件单已创建, triggerPrice:{}, orderType:{}, autoSize:{}, id:{}", |
| | | triggerPrice, orderType, autoSize, response.getId()); |
| | | } catch (GateApiException e) { |
| | | if ("AUTO_USER_EXIST_POSITION_ORDER".equals(e.getErrorLabel())) { |
| | | log.warn("[GateGrid] 止盈条件单已存在,取消旧单后重试, label:{}", e.getErrorLabel()); |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, contract); |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, createPriceTriggeredOrderBean(triggerPrice, rule, orderType, autoSize)); |
| | | log.info("[GateGrid] 止盈条件单重试成功, triggerPrice:{}, orderType:{}, autoSize:{}, id:{}", |
| | | triggerPrice, orderType, autoSize, response.getId()); |
| | | } catch (Exception retryEx) { |
| | | log.error("[GateGrid] 止盈条件单重试失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, retryEx); |
| | | } |
| | | } else { |
| | | log.error("[GateGrid] 止盈条件单创建失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, e); |
| | | state = StrategyState.REOPENING_SHORT; |
| | | executor.openShort(negate(config.getQuantity()), () -> { |
| | | synchronized (this) { |
| | | shortEntryPrice = lastKlinePrice; |
| | | shortActive = true; |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 止盈条件单创建失败, triggerPrice:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, orderType, autoSize, e); |
| | | } |
| | | 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); |
| | | }); |
| | | } |
| | | |
| | | private FuturesPriceTriggeredOrder createPriceTriggeredOrderBean(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | FuturesPriceTrigger trigger = new FuturesPriceTrigger(); |
| | | trigger.setStrategyType(FuturesPriceTrigger.StrategyTypeEnum.NUMBER_0); |
| | | trigger.setPriceType(FuturesPriceTrigger.PriceTypeEnum.NUMBER_0); |
| | | trigger.setPrice(triggerPrice.toString()); |
| | | trigger.setRule(rule); |
| | | trigger.setExpiration(0); |
| | | // ---- util ---- |
| | | |
| | | FuturesInitialOrder initial = new FuturesInitialOrder(); |
| | | initial.setContract(contract); |
| | | initial.setSize(0L); |
| | | initial.setPrice("0"); |
| | | initial.setTif(FuturesInitialOrder.TifEnum.IOC); |
| | | initial.setReduceOnly(true); |
| | | initial.setAutoSize(autoSize); |
| | | |
| | | FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder(); |
| | | order.setTrigger(trigger); |
| | | order.setInitial(initial); |
| | | order.setOrderType(orderType); |
| | | return order; |
| | | private BigDecimal longTpPrice() { |
| | | return lastKlinePrice.multiply(BigDecimal.ONE.add(config.getGridRate())) |
| | | .setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 打印当前网格配置和入场信息。 |
| | | */ |
| | | private void printGridInfo() { |
| | | BigDecimal longTp = BigDecimal.ZERO; |
| | | BigDecimal shortTp = BigDecimal.ZERO; |
| | | if (longEntryPrice != null) { |
| | | longTp = longEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | if (shortEntryPrice != null) { |
| | | shortTp = shortEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | log.info("========== Gate 网格开仓 =========="); |
| | | log.info("合约: {} 杠杆: {}x {}", contract, leverage, marginMode); |
| | | log.info("多头入场: {} TP: {}", longEntryPrice, longTp); |
| | | log.info("空头入场: {} TP: {}", shortEntryPrice, shortTp); |
| | | log.info("数量: {} 网格间距: {}%", quantity, gridRate.multiply(new BigDecimal("100"))); |
| | | log.info("整体止盈: {} USDT 最大亏损: {} USDT", overallTp, maxLoss); |
| | | log.info("====================================="); |
| | | private BigDecimal shortTpPrice() { |
| | | return lastKlinePrice.multiply(BigDecimal.ONE.subtract(config.getGridRate())) |
| | | .setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** 对数量取反(开多用正数,开空用负数) */ |
| | | private String negateQuantity(String qty) { |
| | | if (qty.startsWith("-")) { |
| | | return qty.substring(1); |
| | | } |
| | | return "-" + qty; |
| | | private String negate(String qty) { |
| | | return qty.startsWith("-") ? qty.substring(1) : "-" + qty; |
| | | } |
| | | |
| | | /** 安全转换字符串为 BigDecimal,null 返回 0 */ |
| | | private BigDecimal safeDecimal(String val) { |
| | | if (val == null || val.isEmpty()) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return new BigDecimal(val); |
| | | } |
| | | |
| | | public BigDecimal getLastKlinePrice() { |
| | | return lastKlinePrice; |
| | | } |
| | | |
| | | public boolean isStrategyActive() { |
| | | return strategyActive; |
| | | } |
| | | |
| | | public BigDecimal getTotalHistoryPnl() { |
| | | return totalHistoryPnl; |
| | | } |
| | | |
| | | public Long getUserId() { |
| | | return userId; |
| | | } |
| | | 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; } |
| | | } |