Administrator
5 days ago 3f2a5a15d15052833f1eb864d9f04bc02ecd6cc0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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";
    }
}