| New file |
| | |
| | | package com.xcong.excoin.modules.okxApi.wsHandler.handler; |
| | | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxApi.OkxConfig; |
| | | import com.xcong.excoin.modules.okxApi.OkxGridTradeService; |
| | | import com.xcong.excoin.modules.okxApi.wsHandler.AbstractOkxPrivateChannelHandler; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | /** |
| | | * OKX 订单频道处理器(orders),替代 orders-algo 追踪条件单成交。 |
| | | * |
| | | * <h3>为什么用 orders 替代 orders-algo</h3> |
| | | * 当 algo 订单(conditional/trigger)触发后,OKX 会生成对应的普通市价单并成交。 |
| | | * orders 频道的 fill 数据中包含 {@code algoId} 字段,可直接关联到原始条件单 ID。 |
| | | * 相比 orders-algo(只推送状态变更),orders 频道能拿到真实的 {@code fillPx/fillSz/tradeId}。 |
| | | * |
| | | * <h3>订阅格式</h3> |
| | | * 私有频道,标准格式无需 instFamily: |
| | | * <pre> |
| | | * {"op":"subscribe","args":[{"channel":"orders","instType":"SWAP"}]} |
| | | * </pre> |
| | | * |
| | | * <h3>数据推送格式(含 algoId 的 fill)</h3> |
| | | * <pre> |
| | | * { |
| | | * "arg": {"channel":"orders","instType":"SWAP"}, |
| | | * "data": [{ |
| | | * "instId": "ETH-USDT-SWAP", |
| | | * "ordId": "...", |
| | | * "algoId": "...", // 链接回原始条件单 |
| | | * "state": "filled", // 只处理 filled 状态 |
| | | * "side": "sell", |
| | | * "posSide": "long", |
| | | * "fillPx": "3000", |
| | | * "fillSz": "1", |
| | | * "tradeId": "...", |
| | | * "ordType": "market" |
| | | * }] |
| | | * } |
| | | * </pre> |
| | | * |
| | | * <h3>状态映射 — 仅处理 filled</h3> |
| | | * <ul> |
| | | * <li>"filled" → status="finished" → 回调 onAutoOrder()</li> |
| | | * <li>"canceled"/"live"/"partially_filled" → 忽略</li> |
| | | * </ul> |
| | | * |
| | | * <h3>订单类型映射(同 orders-algo 逻辑)</h3> |
| | | * <table> |
| | | * <tr><th>posSide</th><th>side</th><th>含义</th><th>orderType</th></tr> |
| | | * <tr><td>long</td><td>sell</td><td>平多仓(止盈/止损)</td><td>plan-close-long-position</td></tr> |
| | | * <tr><td>short</td><td>buy</td><td>平空仓(止盈/止损)</td><td>plan-close-short-position</td></tr> |
| | | * <tr><td>long</td><td>buy</td><td>开多仓(条件单触发)</td><td>entry-long</td></tr> |
| | | * <tr><td>short</td><td>sell</td><td>开空仓(条件单触发)</td><td>entry-short</td></tr> |
| | | * </table> |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class OrdersOkxChannelHandler extends AbstractOkxPrivateChannelHandler { |
| | | |
| | | /** OKX 订单频道名称 */ |
| | | private static final String CHANNEL_NAME = "orders"; |
| | | |
| | | /** OKX 配置,用于按合约名称过滤 */ |
| | | private final OkxConfig config; |
| | | |
| | | /** |
| | | * 构造订单频道处理器。 |
| | | * |
| | | * @param config OKX 配置实例 |
| | | * @param gridTradeService OKX 网格交易策略服务实例 |
| | | */ |
| | | public OrdersOkxChannelHandler(OkxConfig config, OkxGridTradeService gridTradeService) { |
| | | super(CHANNEL_NAME, |
| | | config.getApiKey(), config.getApiSecret(), config.getPassphrase(), |
| | | config.getContract(), |
| | | gridTradeService); |
| | | this.config = config; |
| | | } |
| | | |
| | | /** |
| | | * 处理订单推送消息。 |
| | | * |
| | | * <h3>处理流程</h3> |
| | | * <ol> |
| | | * <li>按 instId 精确过滤出目标合约</li> |
| | | * <li>仅处理 state="filled"(成交),忽略其余状态</li> |
| | | * <li>过滤无 algoId 的订单(非条件单触发的普通订单)</li> |
| | | * <li>从 posSide + side 推断 orderType</li> |
| | | * <li>回调 gridTradeService.onAutoOrder(algoId, "finished", "filled", orderType, tradeId)</li> |
| | | * </ol> |
| | | * |
| | | * @param response WebSocket 推送的完整 JSON |
| | | * @return true 表示已处理(匹配成功) |
| | | */ |
| | | @Override |
| | | public boolean handleMessage(JSONObject response) { |
| | | JSONObject arg = response.getJSONObject("arg"); |
| | | if (arg == null || !CHANNEL_NAME.equals(arg.getString("channel"))) { |
| | | return false; |
| | | } |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray("data"); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | return true; |
| | | } |
| | | |
| | | String contract = config.getContract(); |
| | | for (int i = 0; i < dataArray.size(); i++) { |
| | | JSONObject orderData = dataArray.getJSONObject(i); |
| | | |
| | | // 按合约名称精确过滤 |
| | | String dataInstId = orderData.getString("instId"); |
| | | if (dataInstId == null || !contract.equals(dataInstId)) { |
| | | continue; |
| | | } |
| | | |
| | | // 仅处理已成交 |
| | | String state = orderData.getString("state"); |
| | | if (!"filled".equals(state)) { |
| | | continue; |
| | | } |
| | | |
| | | // 必须有 algoId(否则不是条件单触发的订单) |
| | | String algoId = orderData.getString("algoId"); |
| | | if (algoId == null || algoId.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | String posSide = orderData.getString("posSide"); |
| | | String side = orderData.getString("side"); |
| | | String tradeId = orderData.getString("tradeId"); |
| | | String fillSz = orderData.getString("fillSz"); |
| | | |
| | | // 推断订单类型 |
| | | String orderType = mapOrderType(posSide, side); |
| | | |
| | | log.info("[OKX-WS] orders fill, algoId:{}, state:filled, orderType:{}, side:{}, posSide:{}, fillSz:{}, tradeId:{}", |
| | | algoId, orderType, side, posSide, fillSz, tradeId); |
| | | |
| | | if (getGridTradeService() != null) { |
| | | getGridTradeService().onAutoOrder(algoId, "finished", state, orderType, tradeId); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[OKX-WS] 处理 orders 数据失败", e); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 根据 posSide 和 side 推断订单类型。 |
| | | */ |
| | | private String mapOrderType(String posSide, String side) { |
| | | // 平仓方向(止盈/止损触发) |
| | | if ("long".equals(posSide) && "sell".equals(side)) { |
| | | return "plan-close-long-position"; |
| | | } else if ("short".equals(posSide) && "buy".equals(side)) { |
| | | return "plan-close-short-position"; |
| | | } |
| | | // 开仓方向 — 覆盖 long_short_mode 和 net_mode |
| | | if (("long".equals(posSide) || "net".equals(posSide)) && "buy".equals(side)) { |
| | | return "entry-long"; |
| | | } else if (("short".equals(posSide) || "net".equals(posSide)) && "sell".equals(side)) { |
| | | return "entry-short"; |
| | | } |
| | | log.warn("[OKX-WS] orders 未知订单类型, posSide:{}, side:{}", posSide, side); |
| | | return "unknown"; |
| | | } |
| | | } |