Administrator
10 hours ago 0d9c31cc7be76229cf71e141444598992758ebf6
feat(gateApi): 更新网格交易逻辑文档和实现

- 修改回调设计,为placeGridLimitOrder添加Consumer<String>接收orderId参数
- 新增placeConditionalEntryOrder和cancelConditionalOrder方法支持条件单操作
- 添加多仓和空仓止盈队列以及当前订单ID状态管理
- 重构网格核心逻辑,增加止盈队列管理和条件单挂单机制
- 更新初始化流程,改用条件单替代限价单进行网格开仓
- 完善异常处理和保证金安全检查机制
3 files modified
152 ■■■■ changed files
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateTradeExecutor.java 72 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/gateApi-logic.md 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -513,11 +513,13 @@
            log.info("[Gate] 多止盈队列:{}", longTakeProfitQueue);
            log.info("[Gate] 空止盈队列:{}", shortTakeProfitQueue);
            executor.placeGridLimitOrder(longPriceQueue.get(0), config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 初始限价多单已挂, id:{}", orderId); },
            executor.placeConditionalEntryOrder(longPriceQueue.get(0),
                    FuturesPriceTrigger.RuleEnum.NUMBER_1, config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 初始条件多单已挂, id:{}, trigger:{}", orderId, longPriceQueue.get(0)); },
                    null);
            executor.placeGridLimitOrder(shortPriceQueue.get(0), negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 初始限价空单已挂, id:{}", orderId); },
            executor.placeConditionalEntryOrder(shortPriceQueue.get(0),
                    FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 初始条件空单已挂, id:{}, trigger:{}", orderId, shortPriceQueue.get(0)); },
                    null);
            state = StrategyState.ACTIVE;
@@ -638,15 +640,17 @@
        log.info("[Gate] 空止盈队列增加:{}, 现止盈队列:{}", stpElem, shortTakeProfitQueue);
        if (!isMarginSafe()) {
            log.warn("[Gate] 保证金超限,跳过挂限价单");
            log.warn("[Gate] 保证金超限,跳过挂条件单");
        } else {
            String oldLongId = currentLongOrderId;
            executor.cancelOrder(oldLongId);
            executor.placeGridLimitOrder(newShortFirst, negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 新限价空单, id:{}, price:{}", orderId, newShortFirst); },
            executor.cancelConditionalOrder(oldLongId);
            executor.placeConditionalEntryOrder(newShortFirst,
                    FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 新条件空单, id:{}, trigger:{}", orderId, newShortFirst); },
                    null);
            executor.placeGridLimitOrder(newLongFirst, config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 新限价多单, id:{}, price:{}", orderId, newLongFirst); },
            executor.placeConditionalEntryOrder(newLongFirst,
                    FuturesPriceTrigger.RuleEnum.NUMBER_1, config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 新条件多单, id:{}, trigger:{}", orderId, newLongFirst); },
                    null);
        }
@@ -736,15 +740,17 @@
        log.info("[Gate] 多止盈队列增加:{}, 现止盈队列:{}", ltpElem, longTakeProfitQueue);
        if (!isMarginSafe()) {
            log.warn("[Gate] 保证金超限,跳过挂限价单");
            log.warn("[Gate] 保证金超限,跳过挂条件单");
        } else {
            String oldShortId = currentShortOrderId;
            executor.cancelOrder(oldShortId);
            executor.placeGridLimitOrder(newLongFirst, config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 新限价多单, id:{}, price:{}", orderId, newLongFirst); },
            executor.cancelConditionalOrder(oldShortId);
            executor.placeConditionalEntryOrder(newLongFirst,
                    FuturesPriceTrigger.RuleEnum.NUMBER_1, config.getQuantity(),
                    orderId -> { currentLongOrderId = orderId; log.info("[Gate] 新条件多单, id:{}, trigger:{}", orderId, newLongFirst); },
                    null);
            executor.placeGridLimitOrder(newShortFirst, negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 新限价空单, id:{}, price:{}", orderId, newShortFirst); },
            executor.placeConditionalEntryOrder(newShortFirst,
                    FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(config.getQuantity()),
                    orderId -> { currentShortOrderId = orderId; log.info("[Gate] 新条件空单, id:{}, trigger:{}", orderId, newShortFirst); },
                    null);
        }
src/main/java/com/xcong/excoin/modules/gateApi/GateTradeExecutor.java
@@ -260,6 +260,78 @@
    }
    /**
     * 异步创建条件开仓单(价格触发开仓)。
     *
     * <p>服务器监控价格,达到触发价后以市价 IOC 开仓。与止盈单不同,不设 order_type(默认开仓),
     * reduce_only=false。适用于"价格到达 X 才买入 / 跌到 Y 才卖出"的场景。
     *
     * @param triggerPrice 触发价格
     * @param rule         触发规则(NUMBER_1: 最新价≥触发价时执行;NUMBER_2: 最新价≤触发价时执行)
     * @param size         开仓张数(正=开多,负=开空)
     * @param onSuccess    成功回调,接收 conditionOrderId
     * @param onFailure    失败回调
     */
    public void placeConditionalEntryOrder(BigDecimal triggerPrice,
                                            FuturesPriceTrigger.RuleEnum rule,
                                            String size,
                                            Consumer<String> onSuccess,
                                            Runnable onFailure) {
        executor.execute(() -> {
            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(Long.parseLong(size));
                initial.setPrice("0");
                initial.setTif(FuturesInitialOrder.TifEnum.IOC);
                initial.setReduceOnly(false);
                FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder();
                order.setTrigger(trigger);
                order.setInitial(initial);
                TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order);
                String orderId = String.valueOf(response.getId());
                log.info("[TradeExec] 条件开仓单已创建, trigger:{}, rule:{}, size:{}, id:{}",
                        triggerPrice, rule, size, orderId);
                if (onSuccess != null) {
                    onSuccess.accept(orderId);
                }
            } catch (Exception e) {
                log.error("[TradeExec] 条件开仓单创建失败, trigger:{}, size:{}", triggerPrice, size, e);
                if (onFailure != null) {
                    onFailure.run();
                }
            }
        });
    }
    /**
     * 异步取消单个条件单。
     *
     * @param orderId 条件单 ID,为 null 时跳过
     */
    public void cancelConditionalOrder(String orderId) {
        if (orderId == null) {
            return;
        }
        executor.execute(() -> {
            try {
                futuresApi.cancelPriceTriggeredOrder(SETTLE, Long.parseLong(orderId));
                log.info("[TradeExec] 条件单已取消, id:{}", orderId);
            } catch (Exception e) {
                log.warn("[TradeExec] 取消条件单失败(可能已触发), id:{}", orderId);
            }
        });
    }
    /**
     * 构建 FuturesPriceTriggeredOrder 对象。
     *
     * <p>策略=0(价格触发),price_type=0(最新价),expiration=0(永不过期),
src/main/java/com/xcong/excoin/modules/gateApi/gateApi-logic.md
@@ -411,14 +411,17 @@
- allowCoreThreadTimeOut: 60s 空闲后线程回收
**回调设计**:
- 每个下单方法接受 `onSuccess` 和 `onFailure` 两个 `Runnable`
- REST 调用成功 → 执行 `onSuccess`(标记基底已开等)
- `openLong`/`openShort`/`placeTakeProfit` 接受 `onSuccess` 和 `onFailure` 两个 `Runnable`
- `placeGridLimitOrder` 的 `onSuccess` 为 `Consumer<String>`(接收 orderId)
- REST 调用成功 → 执行 `onSuccess`(标记基底已开、记录 orderId 等)
- REST 调用失败 → 执行 `onFailure`(当前版本多为 null,依赖 position 推送修正)
| 方法 | 说明 |
|------|------|
| `openLong(qty, onSuccess, onFailure)` | 异步 IOC 市价开多,双回调 |
| `openShort(qty, onSuccess, onFailure)` | 异步 IOC 市价开空,双回调 |
| `placeGridLimitOrder(price, size, onSuccess, onFailure)` | 异步 GTC 限价单(网格开仓用),onSuccess 接收 orderId |
| `cancelOrder(orderId)` | 异步取消指定挂单(orderId 为 null 时跳过) |
| `placeTakeProfit(trigger, rule, type, size)` | 异步止盈条件单(plan-close-*-position)。size 为显式平仓张数(正=平空,负=平多),多次调用互不影响 |
| `cancelAllPriceTriggeredOrders()` | 清除所有条件单 |
| `shutdown()` | 等待10秒,超时强制关闭 |
@@ -453,6 +456,10 @@
|------|------|------|
| shortPriceQueue | List\<BigDecimal\> | 空仓价格队列,降序(大→小),容量 gridQueueSize |
| longPriceQueue | List\<BigDecimal\> | 多仓价格队列,升序(小→大),容量 gridQueueSize |
| longTakeProfitQueue | List\<BigDecimal\> | 多仓止盈队列,升序(小→大),仓位推送时消费 |
| shortTakeProfitQueue | List\<BigDecimal\> | 空仓止盈队列,降序(大→小),仓位推送时消费 |
| currentLongOrderId | String | 当前多仓限价单 ID(用于取消旧单) |
| currentShortOrderId | String | 当前空仓限价单 ID(用于取消旧单) |
| shortBaseEntryPrice | BigDecimal | 基底空头入场价(仅首次记录,用于生成队列) |
| longBaseEntryPrice | BigDecimal | 基底多头入场价(仅首次记录,用于生成队列) |
| shortEntryPrice | BigDecimal | 当前空仓入场价(推送实时更新,加权均价) |
@@ -468,8 +475,8 @@
| initialPrincipal | BigDecimal | 初始本金(启动时账户总资产) |
**回调方法**:
- `onKline(closePrice)`: 更新 lastKlinePrice → 计算 unrealizedPnl → WAITING_KLINE 时触发基底双开,ACTIVE 时驱动 processShortGrid+processLongGrid
- `onPositionUpdate(contract, mode, size, entryPrice)`: 记录当前入场价和持仓量 → 基底:首次成交记录入场价、生成队列;非基底:按 quantity 张数创建独立止盈条件单(plan-close-*-position)。无仓位时清空持仓量并标记不活跃
- `onKline(closePrice)`: 更新 lastKlinePrice → 计算 unrealizedPnl → WAITING_KLINE 时触发基底双开,ACTIVE 时方向区分(closePrice>longPriceQueue[0]→processLongGrid,closePrice<shortPriceQueue[0]→processShortGrid,其余跳过)
- `onPositionUpdate(contract, mode, size, entryPrice)`: 记录当前入场价和持仓量 → 基底:首次成交记录入场价、生成队列+止盈队列+挂限价单;非基底(仓位增加):按 quantity 为单位计算增量,逐个从止盈队列消费止盈价(队列不足时 entryPrice ± step 兜底),每批挂独立止盈条件单。无仓位时清空持仓量并标记不活跃
- `onPositionClose(contract, side, pnl)`: 累加已实现盈亏,检查停止条件
**processShortGrid / processLongGrid 核心逻辑**:
@@ -477,9 +484,11 @@
| 步骤 | processShortGrid | processLongGrid |
|------|-----------------|-----------------|
| 匹配 | 收集 shortPriceQueue 中 > currentPrice 的元素 | 收集 longPriceQueue 中 < currentPrice 的元素 |
| 本队补充 | 尾价 − step 循环递减 | 尾价 + step 循环递增 |
| 对方转移 | 以多仓首元素为种子,递减 step | 以空仓首元素为种子,递增 step |
| 贴近过滤 | 新增与 longEntryPrice 太近 → 跳过 | 新增与 shortEntryPrice 太近 → 跳过 |
| 本队补充 | 尾价 − step 循环递减 × matched.size() 次 | 尾价 + step 循环递增 × matched.size() 次 |
| 对方转移 | 以多仓首元素为种子,递减 step × i | 以空仓首元素为种子,递增 step × i |
| 止盈队列 | shortTakeProfitQueue.add(新 short[0] − step),降序 | longTakeProfitQueue.add(新 long[0] + step),升序 |
| 下单 | 取消旧多仓限价单 → 挂新空单(新 short[0]) + 新多单(新 long[0]) | 取消旧空仓限价单 → 挂新多单(新 long[0]) + 新空单(新 short[0]) |
| 额外反向 | closePrice在[空均价,多均价]间 且 多>空 → openLong | closePrice在[空均价,多均价]间 且 多>空 → openShort |
**未实现盈亏计算** (`updateUnrealizedPnl()`):
@@ -505,10 +514,11 @@
| 方向 | 公式 | order_type | size(平仓张数) | rule |
|------|------|------------|-----------|------|
| 多头 TP | longPriceQueue[0](多仓队列首元素) | `plan-close-long-position` | `-quantity`(负=平多) | NUMBER_1(≥触发价) |
| 空头 TP | shortPriceQueue[0](空仓队列首元素) | `plan-close-short-position` | `+quantity`(正=平空) | NUMBER_2(≤触发价) |
| 多头 TP | longTakeProfitQueue.remove(0),空时兜底 longEntryPrice + step | `plan-close-long-position` | `-quantity`(负=平多) | NUMBER_1(≥触发价) |
| 空头 TP | shortTakeProfitQueue.remove(0),空时兜底 shortEntryPrice − step | `plan-close-short-position` | `+quantity`(正=平空) | NUMBER_2(≤触发价) |
> 止盈价取自对应方向队列的首元素(多仓队列升序首=最小价,空仓队列降序首=最高价)。止盈单使用显式张数而非 autoSize。每次网格触发开仓 quantity 张,只为该批张数创建独立的条件单,多个止盈单之间互不覆盖。
> 止盈价从止盈队列消费。止盈队列由 K线触发时新增元素(新 queue[0] ± step),仓位推送时按每批 quantity 张数逐个消费。
> 止盈队列空时兜底用当前入场价 ± step。每批挂独立止盈条件单,互不覆盖。
**REST API 调用**:
@@ -522,6 +532,8 @@
| 查账户 | `GET /futures/usdt/accounts` | `FuturesApi.listFuturesAccounts()` | 获取初始本金和保证金 |
| 清除条件单 | `DELETE /futures/usdt/price_orders` | `FuturesApi.cancelPriceTriggeredOrderList()` | |
| 市价单 | `POST /futures/usdt/orders` | `FuturesApi.createFuturesOrder()` | price=0, tif=IOC |
| 限价单 | `POST /futures/usdt/orders` | `FuturesApi.createFuturesOrder()` | price=具体价, tif=GTC(网格开仓用) |
| 取消订单 | `DELETE /futures/usdt/orders/{order_id}` | `FuturesApi.cancelFuturesOrder()` | 取消指定挂单 |
| 条件单 | `POST /futures/usdt/price_orders` | `FuturesApi.createPriceTriggeredOrder()` | strategy=0, price_type=0, expiration=0, order_type=plan-close-*-position, size=显式张数,多次调用互不冲突 |
**初始化顺序** (`init()`):
@@ -529,11 +541,11 @@
1. 获取用户 ID
2. 查账户 → 记录初始本金 → 如需要切持仓模式
3. 清除旧的止盈止损条件单
4. 查当前合约所有仓位 → 逐个市价平仓
   - 单向持仓(single): size=相反数, reduce_only=true
   - 双向持仓(dual): size=0, close=false, autoSize=LONG/SHORT, reduce_only=true
5. 设杠杆
6. 打印账户余额
4. 设多/空方向杠杆
5. 重置策略状态 + 清空队列 + 清空止盈队列 + 清除 orderId
6. 等待 K 线回调 → 双开基底(市价开多+市价开空,IOC)
7. 双基底成交 → 生成网格队列 + 止盈队列初始化 + 挂限价多单+空单(GTC) → state=ACTIVE
8. 异常回退: 10s 内任一基底未成交 → state=STOPPED
```
---