Administrator
17 hours ago 025c66091b6b6903b5e830c5bde981fdbacbc744
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package com.xcong.excoin.modules.okxApi;
 
import com.xcong.excoin.modules.okxApi.enums.OkxEnums;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
@Slf4j
public class OkxGridTradeService {
 
    public enum StrategyState {
        WAITING_KLINE, OPENING, ACTIVE, STOPPED
    }
 
    private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position";
    private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position";
 
    private final OkxConfig config;
    private final OkxTradeExecutor executor;
 
    private volatile StrategyState state = StrategyState.WAITING_KLINE;
 
    private final List<BigDecimal> shortPriceQueue = Collections.synchronizedList(new ArrayList<>());
    private final List<BigDecimal> longPriceQueue = Collections.synchronizedList(new ArrayList<>());
 
    private BigDecimal shortBaseEntryPrice;
    private BigDecimal longBaseEntryPrice;
    private volatile boolean baseLongOpened = false;
    private volatile boolean baseShortOpened = false;
    private volatile boolean shortActive = false;
    private volatile boolean longActive = false;
 
    private volatile BigDecimal lastKlinePrice;
    private volatile BigDecimal cumulativePnl = BigDecimal.ZERO;
    private volatile BigDecimal unrealizedPnl = BigDecimal.ZERO;
    private volatile BigDecimal longEntryPrice = BigDecimal.ZERO;
    private volatile BigDecimal shortEntryPrice = BigDecimal.ZERO;
    private volatile BigDecimal longPositionSize = BigDecimal.ZERO;
    private volatile BigDecimal shortPositionSize = BigDecimal.ZERO;
 
    public OkxGridTradeService(OkxConfig config, String accountName) {
        this.config = config;
        this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName);
    }
 
    public void setWebSocketClient(WebSocketClient wsClient) {
        this.executor.setWebSocketClient(wsClient);
    }
 
    public void startGrid() {
        if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) {
            log.warn("[{}] 策略已在运行中, state:{}", config.getContract(), state);
            return;
        }
        state = StrategyState.WAITING_KLINE;
        cumulativePnl = BigDecimal.ZERO;
        unrealizedPnl = BigDecimal.ZERO;
        longEntryPrice = BigDecimal.ZERO;
        shortEntryPrice = BigDecimal.ZERO;
        longPositionSize = BigDecimal.ZERO;
        shortPositionSize = BigDecimal.ZERO;
        baseLongOpened = false;
        baseShortOpened = false;
        longActive = false;
        shortActive = false;
        shortPriceQueue.clear();
        longPriceQueue.clear();
        log.info("[{}] 网格策略已启动", config.getContract());
    }
 
    public void stopGrid() {
        state = StrategyState.STOPPED;
        executor.cancelAllPriceTriggeredOrders();
        executor.shutdown();
        log.info("[{}] 策略已停止, 累计盈亏: {}", config.getContract(), cumulativePnl);
    }
 
    public void onKline(BigDecimal closePrice) {
        lastKlinePrice = closePrice;
        updateUnrealizedPnl();
        if (state == StrategyState.STOPPED) {
            return;
        }
 
        if (state == StrategyState.WAITING_KLINE) {
            state = StrategyState.OPENING;
            log.info("[{}] 首根K线到达,开基底仓位...", config.getContract());
            executor.openLong(config.getQuantity(), () -> {
                log.info("[{}] 基底多单已提交", config.getContract());
            }, null);
            executor.openShort(config.getQuantity(), () -> {
                log.info("[{}] 基底空单已提交", config.getContract());
            }, null);
            return;
        }
 
        if (state != StrategyState.ACTIVE) {
            return;
        }
        processShortGrid(closePrice);
        processLongGrid(closePrice);
    }
 
    public void onPositionUpdate(String posSide, BigDecimal size, BigDecimal entryPrice) {
        if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) {
            return;
        }
 
        boolean hasPosition = size.compareTo(BigDecimal.ZERO) > 0;
 
        if (OkxEnums.POSSIDE_LONG.equals(posSide)) {
            if (hasPosition) {
                longActive = true;
                longEntryPrice = entryPrice;
                if (!baseLongOpened) {
                    longPositionSize = size;
                    longBaseEntryPrice = entryPrice;
                    baseLongOpened = true;
                    log.info("[{}] 基底多成交价: {}", config.getContract(), longBaseEntryPrice);
                    tryGenerateQueues();
                } else if (size.compareTo(longPositionSize) > 0) {
                    longPositionSize = size;
                    if (longPriceQueue.isEmpty()) {
                        log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract());
                    } else {
                        BigDecimal tpPrice = longPriceQueue.get(0);
                        executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_LONG, config.getQuantity());
                        log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity());
                    }
                } else {
                    longPositionSize = size;
                }
            } else {
                longActive = false;
                longPositionSize = BigDecimal.ZERO;
            }
        } else if (OkxEnums.POSSIDE_SHORT.equals(posSide)) {
            if (hasPosition) {
                shortActive = true;
                shortEntryPrice = entryPrice;
                if (!baseShortOpened) {
                    shortPositionSize = size;
                    shortBaseEntryPrice = entryPrice;
                    baseShortOpened = true;
                    log.info("[{}] 基底空成交价: {}", config.getContract(), shortBaseEntryPrice);
                    tryGenerateQueues();
                } else if (size.compareTo(shortPositionSize) > 0) {
                    shortPositionSize = size;
                    if (shortPriceQueue.isEmpty()) {
                        log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract());
                    } else {
                        BigDecimal tpPrice = shortPriceQueue.get(0);
                        executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_SHORT, config.getQuantity());
                        log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity());
                    }
                } else {
                    shortPositionSize = size;
                }
            } else {
                shortActive = false;
                shortPositionSize = BigDecimal.ZERO;
            }
        }
    }
 
    public void onOrderFilled(String posSide, BigDecimal fillSz, BigDecimal pnl) {
        if (state == StrategyState.STOPPED) {
            return;
        }
        cumulativePnl = cumulativePnl.add(pnl);
        log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", config.getContract(), pnl, posSide, cumulativePnl);
 
        if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) {
            log.info("[{}] 已达止盈目标 {}→已停止", config.getContract(), cumulativePnl);
            state = StrategyState.STOPPED;
        } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) {
            log.info("[{}] 已达亏损上限 {}→已停止", config.getContract(), cumulativePnl);
            state = StrategyState.STOPPED;
        }
    }
 
    private void tryGenerateQueues() {
        if (baseLongOpened && baseShortOpened) {
            generateShortQueue();
            generateLongQueue();
            state = StrategyState.ACTIVE;
            log.info("[{}] 网格队列已生成, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活",
                    config.getContract(),
                    shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(0),
                    shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(shortPriceQueue.size() - 1),
                    longPriceQueue.isEmpty() ? "N/A" : longPriceQueue.get(0),
                    longPriceQueue.isEmpty() ? "N/A" : longPriceQueue.get(longPriceQueue.size() - 1));
        }
    }
 
    private void generateShortQueue() {
        shortPriceQueue.clear();
        BigDecimal step = config.getGridRate();
        for (int i = 1; i <= config.getGridQueueSize(); i++) {
            shortPriceQueue.add(shortBaseEntryPrice.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP));
        }
        shortPriceQueue.sort((a, b) -> b.compareTo(a));
        log.info("[{}] 空队列:{}", config.getContract(), shortPriceQueue);
    }
 
    private void generateLongQueue() {
        longPriceQueue.clear();
        BigDecimal step = config.getGridRate();
        for (int i = 1; i <= config.getGridQueueSize(); i++) {
            longPriceQueue.add(longBaseEntryPrice.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP));
        }
        longPriceQueue.sort(BigDecimal::compareTo);
        log.info("[{}] 多队列:{}", config.getContract(), longPriceQueue);
    }
 
    /**
     * 空仓网格处理(价格跌破空仓队列中的高价)。
     *
     * <h3>匹配规则</h3>
     * 遍历空仓队列(降序),收集所有大于当前价的元素为 matched。
     * 队列为降序排列,一旦遇 price ≤ currentPrice 即停止遍历。
     *
     * <h3>执行流程</h3>
     * <ol>
     *   <li>匹配队列元素 → 为空则直接返回</li>
     *   <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li>
     *   <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li>
     *   <li>保证金检查 → 安全则开空一次</li>
     *   <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li>
     * </ol>
     *
     * <h3>多仓队列转移过滤</h3>
     * 新增元素若与多仓持仓均价差距小于 gridRate,则跳过该元素(避免在持仓成本附近生成无效网格线)。
     */
    private void processShortGrid(BigDecimal currentPrice) {
        List<BigDecimal> matched = new ArrayList<>();
        synchronized (shortPriceQueue) {
            for (BigDecimal p : shortPriceQueue) {
                if (p.compareTo(currentPrice) > 0) {
                    matched.add(p);
                } else {
                    break;
                }
            }
        }
        log.info("[{}] 原空队列:{}", config.getContract(), shortPriceQueue);
        if (matched.isEmpty()) {
            log.info("[{}] 空仓队列未触发, 当前价:{}", config.getContract(), currentPrice);
            return;
        }
        log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice);
 
        synchronized (shortPriceQueue) {
            shortPriceQueue.removeAll(matched);
            BigDecimal min = shortPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : shortPriceQueue.get(shortPriceQueue.size() - 1);
            BigDecimal step = config.getGridRate();
            for (int i = 0; i < matched.size(); i++) {
                min = min.multiply(BigDecimal.ONE.subtract(step)).setScale(1, RoundingMode.HALF_UP);
                shortPriceQueue.add(min);
                log.info("[{}] 空队列增加:{}", config.getContract(), min);
            }
            shortPriceQueue.sort((a, b) -> b.compareTo(a));
            log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue);
        }
 
        synchronized (longPriceQueue) {
            BigDecimal first = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(0);
            BigDecimal step = config.getGridRate();
            for (int i = 1; i <= matched.size(); i++) {
                BigDecimal elem = first.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP);
                if (longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                        && currentPrice.subtract(longEntryPrice).abs().compareTo(longEntryPrice.multiply(step)) < 0) {
                    log.info("[{}] 多队列跳过(price≈longEntry):{}", config.getContract(), elem);
                    continue;
                }
                longPriceQueue.add(elem);
                log.info("[{}] 多队列增加:{}", config.getContract(), elem);
            }
            longPriceQueue.sort(BigDecimal::compareTo);
            while (longPriceQueue.size() > config.getGridQueueSize()) {
                longPriceQueue.remove(longPriceQueue.size() - 1);
            }
            log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue);
        }
 
        if (!isMarginSafe()) {
            log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract());
        } else {
            executor.openShort(config.getQuantity(), null, null);
            if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) {
                executor.openLong(config.getQuantity(), null, null);
                log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice);
            }
        }
    }
 
    /**
     * 多仓网格处理(价格涨破多仓队列中的低价)。
     *
     * <h3>匹配规则</h3>
     * 遍历多仓队列(升序),收集所有小于当前价的元素为 matched。
     * 队列为升序排列,一旦遇 price ≥ currentPrice 即停止遍历。
     *
     * <h3>执行流程</h3>
     * <ol>
     *   <li>匹配队列元素 → 为空则直接返回</li>
     *   <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li>
     *   <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li>
     *   <li>保证金检查 → 安全则开多一次</li>
     *   <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li>
     * </ol>
     *
     * <h3>空仓队列转移过滤</h3>
     * 新增元素若与空仓持仓均价差距小于 gridRate,则跳过该元素(避免在持仓成本附近生成无效网格线)。
     */
    private void processLongGrid(BigDecimal currentPrice) {
        List<BigDecimal> matched = new ArrayList<>();
        synchronized (longPriceQueue) {
            for (BigDecimal p : longPriceQueue) {
                if (p.compareTo(currentPrice) < 0) {
                    matched.add(p);
                } else {
                    break;
                }
            }
        }
        log.info("[{}] 原多队列:{}", config.getContract(), longPriceQueue);
        if (matched.isEmpty()) {
            log.info("[{}] 多仓队列未触发, 当前价:{}", config.getContract(), currentPrice);
            return;
        }
        log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice);
 
        synchronized (longPriceQueue) {
            longPriceQueue.removeAll(matched);
            BigDecimal max = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(longPriceQueue.size() - 1);
            BigDecimal step = config.getGridRate();
            for (int i = 0; i < matched.size(); i++) {
                max = max.multiply(BigDecimal.ONE.add(step)).setScale(1, RoundingMode.HALF_UP);
                longPriceQueue.add(max);
                log.info("[{}] 多队列增加:{}", config.getContract(), max);
            }
            longPriceQueue.sort(BigDecimal::compareTo);
            log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue);
        }
 
        synchronized (shortPriceQueue) {
            BigDecimal first = shortPriceQueue.isEmpty() ? matched.get(0) : shortPriceQueue.get(0);
            BigDecimal step = config.getGridRate();
            for (int i = 1; i <= matched.size(); i++) {
                BigDecimal elem = first.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP);
                if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0
                        && currentPrice.subtract(shortEntryPrice).abs().compareTo(shortEntryPrice.multiply(step)) < 0) {
                    log.info("[{}] 空队列跳过(price≈shortEntry):{}", config.getContract(), elem);
                    continue;
                }
                shortPriceQueue.add(elem);
                log.info("[{}] 空队列增加:{}", config.getContract(), elem);
            }
            shortPriceQueue.sort((a, b) -> b.compareTo(a));
            while (shortPriceQueue.size() > config.getGridQueueSize()) {
                shortPriceQueue.remove(shortPriceQueue.size() - 1);
            }
            log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue);
        }
 
        if (!isMarginSafe()) {
            log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract());
        } else {
            executor.openLong(config.getQuantity(), null, null);
            if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(BigDecimal.ZERO) > 0
                    && longEntryPrice.compareTo(shortEntryPrice) > 0
                    && currentPrice.compareTo(shortEntryPrice.multiply(BigDecimal.ONE.add(config.getGridRate()))) > 0
                    && currentPrice.compareTo(longEntryPrice) < 0) {
                executor.openShort(config.getQuantity(), null, null);
                log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice);
            }
        }
    }
 
    /**
     * 保证金安全阀检查。
     *
     * <p>当前版本通过 wsHandler 推送的账户/持仓数据间接判断保证金状态。
     * 后续可通过 OKX REST API 实时查询保证金占用比例,超 marginRatioLimit 时拒绝开仓。
     *
     * @return true=安全可开仓 / false=保证金超限跳过开仓
     */
    private boolean isMarginSafe() {
        return true;
    }
 
    private void updateUnrealizedPnl() {
        BigDecimal price = lastKlinePrice;
        if (price == null || price.compareTo(BigDecimal.ZERO) == 0) {
            return;
        }
        BigDecimal multiplier = config.getContractMultiplier();
        BigDecimal longPnl = BigDecimal.ZERO;
        BigDecimal shortPnl = BigDecimal.ZERO;
        if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
            longPnl = longPositionSize.multiply(multiplier).multiply(price.subtract(longEntryPrice));
        }
        if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
            shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price));
        }
        unrealizedPnl = longPnl.add(shortPnl);
        log.info("[{}] 未实现盈亏: {}", config.getContract(), unrealizedPnl);
    }
 
    public BigDecimal getLastKlinePrice() { return lastKlinePrice; }
    public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; }
    public BigDecimal getCumulativePnl() { return cumulativePnl; }
    public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
    public StrategyState getState() { return state; }
    public String getAccountName() { return config.getContract(); }
}