Administrator
2026-06-16 8e39888664499d524bed90909334f22ef11ec68e
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
package com.xcong.excoin.modules.gateApi.simulation;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
 
/**
 * 网格策略模拟器 — 模拟 GateGridTradeService 完整生命周期并发现逻辑 BUG。
 *
 * <h3>预设参数</h3>
 * shortBaseEntryPrice = 100.0, gridRate = 0.005 (0.5%), step = 0.5, priceScale = 1, baseQuantity = 10, quantity = 1
 */
public class GridSimulator {
 
    static int SIM_PRICE_PREC = 1;
    static BigDecimal BASE_PRICE = new BigDecimal("100.0");
    static BigDecimal STEP = BASE_PRICE.multiply(new BigDecimal("0.005")).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); // 0.5
    static int BASE_QTY = 10;
    static int QTY = 1;
    static int GRID_QUEUE_SIZE = 10; // 模拟用缩减队列
 
    // ---- 网格数据结构 ----
    static class Grid {
        int id;
        BigDecimal price;
        boolean hasLongOrder, hasShortOrder;
        String longOrderId, shortOrderId;
        String longTpOrderId, shortTpOrderId;   // 止盈
        String longSlOrderId, shortSlOrderId;   // 止损
        BigDecimal longTpPrice, shortTpPrice;
        int longFilledQty = QTY, shortFilledQty = QTY;
 
        Grid(int id, BigDecimal price) {
            this.id = id; this.price = price;
            this.longTpPrice = price.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
            this.shortTpPrice = price.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
        }
        @Override public String toString() {
            return String.format("G[%2d] p=%.1f | L:o=%s tp=%s sl=%s | S:o=%s tp=%s sl=%s",
                id, price,
                hasLongOrder ? "Y" : "N", longTpOrderId != null ? longTpOrderId : "-", longSlOrderId != null ? longSlOrderId : "-",
                hasShortOrder ? "Y" : "N", shortTpOrderId != null ? shortTpOrderId : "-", shortSlOrderId != null ? shortSlOrderId : "-");
        }
    }
 
    static Map<Integer, Grid> INDEX = new LinkedHashMap<>();
    static List<Grid> gridElements = new ArrayList<>();
 
    // ---- 队列 ----
    static List<BigDecimal> shortQueue = new ArrayList<>(); // 降序
    static List<BigDecimal> longQueue = new ArrayList<>();  // 升序
    static List<BigDecimal> totalShortQ = new ArrayList<>(); // 降序
    static List<BigDecimal> totalLongQ = new ArrayList<>();  // 升序
 
    // ---- 状态 ----
    enum State { WAITING_KLINE, OPENING, ACTIVE, STOPPED }
    static State state = State.WAITING_KLINE;
    static boolean baseLongOpened = false, baseShortOpened = false;
    static boolean longActive = false, shortActive = false;
    static BigDecimal longPositionSize = BigDecimal.ZERO;
    static BigDecimal shortPositionSize = BigDecimal.ZERO;
    static BigDecimal longEntryPrice = BigDecimal.ZERO;
    static BigDecimal shortEntryPrice = BigDecimal.ZERO;
    static BigDecimal cumulativePnl = BigDecimal.ZERO;
    static BigDecimal unrealizedPnl = BigDecimal.ZERO;
    static BigDecimal shortBaseEntryPrice = BigDecimal.ZERO;
    static BigDecimal longBaseEntryPrice = BigDecimal.ZERO;
 
    static int orderIdCounter = 1000;
    static List<String> eventLog = new ArrayList<>();
    static List<String> bugLog = new ArrayList<>();
 
    static void log(String msg) { eventLog.add(msg); System.out.println("  " + msg); }
    static void bug(String msg) { String s = "  [BUG] " + msg; bugLog.add(s); System.out.println(s); }
    static String newOrderId() { return "O-" + (orderIdCounter++); }
    static String repeat(String s, int n) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) sb.append(s);
        return sb.toString();
    }
 
    // ================================================================
    // 步骤1: 初始化
    // ================================================================
    static void init() {
        log("=== Phase 1: 初始化 ===");
        log("参数: basePrice=" + BASE_PRICE + " step=" + STEP + " baseQty=" + BASE_QTY + " qty=" + QTY);
        state = State.WAITING_KLINE;
    }
 
    // ================================================================
    // 步骤2: 首根K线 → 市价双开基底
    // ================================================================
    static void onFirstKline() {
        log("\n=== Phase 2: 首根K线到达,市价双开基底 ===");
        state = State.OPENING;
        log("市价开多 " + BASE_QTY + " 张,开空 " + BASE_QTY + " 张");
 
        // 模拟成交
        longPositionSize = new BigDecimal(BASE_QTY);
        shortPositionSize = new BigDecimal(BASE_QTY);
        longEntryPrice = BASE_PRICE;
        shortEntryPrice = BASE_PRICE;
        longActive = true;
        shortActive = true;
 
        // 模拟 onPositionUpdate → 基底成交
        log("基底多成交价: " + BASE_PRICE);
        longBaseEntryPrice = BASE_PRICE;
        baseLongOpened = true;
        log("基底空成交价: " + BASE_PRICE);
        shortBaseEntryPrice = BASE_PRICE;
        baseShortOpened = true;
 
        tryGenerateQueues();
    }
 
    // ================================================================
    // 步骤3: 生成队列和网格
    // ================================================================
    static void tryGenerateQueues() {
        if (!baseLongOpened || !baseShortOpened) return;
        log("\n=== Phase 3: 生成网格队列 ===");
 
        // 生成队列
        generateShortQueue();
        generateLongQueue();
        updateGridElements();
 
        // 标记基底元素 (模拟 baseLongTraderParam/baseShortTraderParam)
        Grid baseG = INDEX.get(0);
        baseG.hasLongOrder = true;
        baseG.longOrderId = "BASE-LONG-IOC";
        baseG.hasShortOrder = true;
        baseG.shortOrderId = "BASE-SHORT-IOC";
        log("基底元素 ID=0 标记 hasLongOrder=true, hasShortOrder=true");
 
        // 初始化止损单
        initStopLossOrders();
 
        state = State.ACTIVE;
        log("状态切换为 ACTIVE");
        printGridState();
    }
 
    static void generateShortQueue() {
        shortQueue.clear();
        BigDecimal elem = BASE_PRICE.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
        for (int i = 0; i < GRID_QUEUE_SIZE; i++) {
            shortQueue.add(elem);
            totalLongQ.add(elem);
            totalShortQ.add(elem);
            elem = elem.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
            if (elem.compareTo(BigDecimal.ZERO) <= 0) break;
        }
        shortQueue.sort((a, b) -> b.compareTo(a));
        log("空仓队列(降序): " + shortQueue);
    }
 
    static void generateLongQueue() {
        longQueue.clear();
        BigDecimal elem = BASE_PRICE.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
        for (int i = 0; i < GRID_QUEUE_SIZE; i++) {
            longQueue.add(elem);
            totalLongQ.add(elem);
            totalShortQ.add(elem);
            elem = elem.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP);
        }
        longQueue.sort(BigDecimal::compareTo);
        log("多仓队列(升序): " + longQueue);
    }
 
    static void updateGridElements() {
        gridElements.clear();
        INDEX.clear();
        int shortSize = shortQueue.size();
        int longSize = longQueue.size();
 
        // 空仓区域: id 从 -1 自减
        for (int i = 0; i < shortSize; i++) {
            int id = -(i + 1);
            BigDecimal price = shortQueue.get(i);
            Grid g = new Grid(id, price);
            g.longFilledQty = QTY;
            g.shortFilledQty = QTY;
            gridElements.add(g);
            INDEX.put(id, g);
        }
 
        // 位置 0
        Grid g0 = new Grid(0, BASE_PRICE);
        g0.longFilledQty = QTY;
        g0.shortFilledQty = QTY;
        gridElements.add(g0);
        INDEX.put(0, g0);
 
        // 多仓区域: id 从 1 自增
        for (int i = 0; i < longSize; i++) {
            int id = i + 1;
            BigDecimal price = longQueue.get(i);
            Grid g = new Grid(id, price);
            g.longFilledQty = QTY;
            g.shortFilledQty = QTY;
            gridElements.add(g);
            INDEX.put(id, g);
        }
 
        log("网格元素: " + gridElements.size() + " 个");
    }
 
    static void initStopLossOrders() {
        log("\n--- 初始化止损单 ---");
        // 空仓止损: ID 2 到 BASE_QTY+1
        int shortTime = BASE_QTY + 1;
        for (int id = 2; id <= shortTime; id++) {
            Grid g = INDEX.get(id);
            if (g == null) continue;
            String slId = newOrderId();
            g.shortSlOrderId = slId;
            log(String.format("  空仓止损 gridId:%d price:%.1f slId:%s (CLOSE_SHORT NUMBER_1)", id, g.price, slId));
        }
 
        // 多仓止损: ID -2 到 -(BASE_QTY+1)
        int longTime = BASE_QTY + 1;
        for (int id = -2; id >= -longTime; id--) {
            Grid g = INDEX.get(id);
            if (g == null) continue;
            String slId = newOrderId();
            g.longSlOrderId = slId;
            log(String.format("  多仓止损 gridId:%d price:%.1f slId:%s (CLOSE_LONG NUMBER_2)", id, g.price, slId));
        }
        log("止损单初始化完成");
    }
 
    // ================================================================
    // 步骤4: onKline ACTIVE 状态下的网格处理
    // ================================================================
    static void onKline(BigDecimal closePrice) {
        if (state != State.ACTIVE) return;
 
        if (!longActive && longPositionSize.compareTo(BigDecimal.ZERO) == 0) {
            processShortGrid(closePrice);
        }
        if (!shortActive && shortPositionSize.compareTo(BigDecimal.ZERO) == 0) {
            processLongGrid(closePrice);
        }
    }
 
    static void processShortGrid(BigDecimal currentPrice) {
        log("\n--- processShortGrid (多仓归零, 触发空仓队列) 当前价:" + currentPrice + " ---");
        BigDecimal matched = BigDecimal.ZERO;
        for (BigDecimal p : totalLongQ) {
            if (p.compareTo(currentPrice) >= 0) {
                matched = p;
                break;
            }
        }
        if (matched.compareTo(BigDecimal.ZERO) == 0) {
            log("  未匹配到价格");
            return;
        }
        log("  匹配价格: " + matched);
        Grid matchedG = findByPrice(matched);
        if (matchedG == null) { log("  Grid不存在"); return; }
 
        if (!matchedG.hasLongOrder) {
            // 在 upId 位置挂多单
            int upId = matchedG.id + 1; // upId逻辑简化
            Grid newEntryG = INDEX.get(upId);
            if (newEntryG != null && !newEntryG.hasLongOrder) {
                String oid = newOrderId();
                newEntryG.hasLongOrder = true;
                newEntryG.longOrderId = oid;
                newEntryG.longFilledQty = BASE_QTY;
                log(String.format("  在gridId:%d挂多单(size=%d) orderId:%s", upId, BASE_QTY, oid));
            }
 
            // 取消upId+1位置的多单
            int cancelId = upId + 1;
            Grid cancelG = INDEX.get(cancelId);
            if (cancelG != null && cancelG.hasLongOrder) {
                log(String.format("  取消gridId:%d的多单 orderId:%s", cancelId, cancelG.longOrderId));
                cancelG.hasLongOrder = false;
                cancelG.longOrderId = null;
            }
 
            // BUG: 这里新挂的单 size = BASE_QTY,但如果之前已有其他追单
            // 可能导致持仓远超预期
        }
    }
 
    static void processLongGrid(BigDecimal currentPrice) {
        log("\n--- processLongGrid (空仓归零, 触发多仓队列) 当前价:" + currentPrice + " ---");
        BigDecimal matched = BigDecimal.ZERO;
        for (BigDecimal p : totalShortQ) {
            if (p.compareTo(currentPrice) <= 0) {
                matched = p;
                break;
            }
        }
        if (matched.compareTo(BigDecimal.ZERO) == 0) {
            log("  未匹配到价格");
            return;
        }
        log("  匹配价格: " + matched);
        Grid matchedG = findByPrice(matched);
        if (matchedG == null) { log("  Grid不存在"); return; }
 
        if (!matchedG.hasShortOrder) {
            int downId = matchedG.id - 1;
            Grid newEntryG = INDEX.get(downId);
            if (newEntryG != null && !newEntryG.hasShortOrder) {
                String oid = newOrderId();
                newEntryG.hasShortOrder = true;
                newEntryG.shortOrderId = oid;
                newEntryG.shortFilledQty = BASE_QTY;
                log(String.format("  在gridId:%d挂空单(size=%d) orderId:%s", downId, BASE_QTY, oid));
            }
 
            int cancelId = downId - 1;
            Grid cancelG = INDEX.get(cancelId);
            if (cancelG != null && cancelG.hasShortOrder) {
                log(String.format("  取消gridId:%d的空单 orderId:%s", cancelId, cancelG.shortOrderId));
                cancelG.hasShortOrder = false;
                cancelG.shortOrderId = null;
            }
        }
    }
 
    // ================================================================
    // 步骤5: onAutoOrder 条件单成交回调
    // ================================================================
    static void onAutoOrder(String orderId, String status, String orderType, String tradeId) {
        if (state == State.STOPPED) return;
        if (!"finished".equals(status)) return;
 
        log("\n--- onAutoOrder: id=" + orderId + " type=" + orderType + " tradeId=" + tradeId + " ---");
 
        // 1. 检查是否是止损单成交
        Grid longSl = findByLongSlOrderId(orderId);
        if (longSl != null && tradeId != null && !"0".equals(tradeId)) {
            handleLongStopLossTriggered(longSl);
            return;
        }
        Grid shortSl = findByShortSlOrderId(orderId);
        if (shortSl != null && tradeId != null && !"0".equals(tradeId)) {
            handleShortStopLossTriggered(shortSl);
            return;
        }
 
        // 2. 检查是否是空仓挂单成交
        Grid shortG = findByShortOrderId(orderId);
        if (shortG != null && shortG.hasShortOrder && tradeId != null && !"0".equals(tradeId)) {
            int filledQty = shortG.shortFilledQty;
            shortG.hasShortOrder = false;
            shortG.shortOrderId = null;
            extendShortStopLoss(filledQty);
            log(String.format("  空单成交 gridId:%d filledQty:%d", shortG.id, filledQty));
 
            // 新逻辑: 持仓超过baseQuantity时,从gridId-2开始追挂止盈
            BigDecimal baseQty = new BigDecimal(BASE_QTY);
            BigDecimal qty = new BigDecimal(QTY);
            if (shortPositionSize.compareTo(baseQty) > 0) {
                BigDecimal excess = shortPositionSize.subtract(baseQty);
                int excessCount = excess.divide(qty, 0, RoundingMode.DOWN).intValue();
                for (int i = 0; i < excessCount; i++) {
                    int tpId = shortG.id - 2 - i;
                    Grid tpG = INDEX.get(tpId);
                    if (tpG == null || tpG.shortTpOrderId != null) continue;
                    String tpIdStr = newOrderId();
                    tpG.shortTpOrderId = tpIdStr;
                    log(String.format("  空仓止盈挂单 gridId:%d price:%.1f tpId:%s", tpId, tpG.price, tpIdStr));
                }
            }
        }
 
        // 3. 检查是否是多仓挂单成交
        Grid longG = findByLongOrderId(orderId);
        if (longG != null && longG.hasLongOrder && tradeId != null && !"0".equals(tradeId)) {
            int filledQty = longG.longFilledQty;
            longG.hasLongOrder = false;
            longG.longOrderId = null;
            extendLongStopLoss(filledQty);
            log(String.format("  多单成交 gridId:%d filledQty:%d", longG.id, filledQty));
 
            BigDecimal baseQty = new BigDecimal(BASE_QTY);
            BigDecimal qty = new BigDecimal(QTY);
            if (longPositionSize.compareTo(baseQty) > 0) {
                BigDecimal excess = longPositionSize.subtract(baseQty);
                int excessCount = excess.divide(qty, 0, RoundingMode.DOWN).intValue();
                for (int i = 0; i < excessCount; i++) {
                    int tpId = longG.id + 2 + i;
                    Grid tpG = INDEX.get(tpId);
                    if (tpG == null || tpG.longTpOrderId != null) continue;
                    String tpIdStr = newOrderId();
                    tpG.longTpOrderId = tpIdStr;
                    log(String.format("  多仓止盈挂单 gridId:%d price:%.1f tpId:%s", tpId, tpG.price, tpIdStr));
                }
            }
        }
    }
 
    static void handleLongStopLossTriggered(Grid g) {
        log("  多仓止损触发 gridId:" + g.id);
        g.longSlOrderId = null;
        // 模拟平仓: longPositionSize 减少
        longPositionSize = longPositionSize.subtract(new BigDecimal(QTY));
        if (longPositionSize.compareTo(BigDecimal.ZERO) < 0) longPositionSize = BigDecimal.ZERO;
 
        // 在gridId+1位置挂新追单
        int newId = g.id + 1;
        Grid newG = INDEX.get(newId);
        if (newG == null) {
            log("  gridId:" + newId + " 不存在,止损追单失败");
            return;
        }
 
        // 计算size
        BigDecimal baseQty = new BigDecimal(BASE_QTY);
        BigDecimal subtract = baseQty.subtract(longPositionSize);
        int size = QTY + 1;
        if (subtract.compareTo(BigDecimal.ZERO) >= 0) {
            size = subtract.intValue() + 1;
        }
        log(String.format("  挂多单 gridId:%d size:%d (pos=%s)", newId, size, longPositionSize));
        newG.hasLongOrder = true;
        newG.longOrderId = newOrderId();
        newG.longFilledQty = size;
 
        // 取消gridId+2的多单
        int cancelId = g.id + 2;
        Grid cancelG = INDEX.get(cancelId);
        if (cancelG != null && cancelG.hasLongOrder) {
            log(String.format("  取消gridId:%d的多单 orderId:%s", cancelId, cancelG.longOrderId));
            cancelG.hasLongOrder = false;
            cancelG.longOrderId = null;
        }
 
        // 取消最远多仓止盈
        Grid farthest = null;
        for (Grid e : gridElements) {
            if (e.longTpOrderId != null) {
                if (farthest == null || e.price.compareTo(farthest.price) > 0) {
                    farthest = e;
                }
            }
        }
        if (farthest != null) {
            log(String.format("  取消最远多仓止盈 gridId:%d orderId:%s", farthest.id, farthest.longTpOrderId));
            farthest.longTpOrderId = null;
        }
 
        // 持仓归零检查
        if (longPositionSize.compareTo(BigDecimal.ZERO) == 0) {
            longActive = false;
            log("  多仓已全部止损,longActive=false");
        }
    }
 
    static void handleShortStopLossTriggered(Grid g) {
        log("  空仓止损触发 gridId:" + g.id);
        g.shortSlOrderId = null;
        shortPositionSize = shortPositionSize.subtract(new BigDecimal(QTY));
        if (shortPositionSize.compareTo(BigDecimal.ZERO) < 0) shortPositionSize = BigDecimal.ZERO;
 
        int newId = g.id - 1;
        Grid newG = INDEX.get(newId);
        if (newG == null) {
            log("  gridId:" + newId + " 不存在,止损追单失败");
            return;
        }
 
        BigDecimal baseQty = new BigDecimal(BASE_QTY);
        BigDecimal subtract = baseQty.subtract(shortPositionSize);
        int size = QTY + 1;
        if (subtract.compareTo(BigDecimal.ZERO) >= 0) {
            size = subtract.intValue() + 1;
        }
        log(String.format("  挂空单 gridId:%d size:%d (pos=%s)", newId, size, shortPositionSize));
        newG.hasShortOrder = true;
        newG.shortOrderId = newOrderId();
        newG.shortFilledQty = size;
 
        int cancelId = g.id - 2;
        Grid cancelG = INDEX.get(cancelId);
        if (cancelG != null && cancelG.hasShortOrder) {
            log(String.format("  取消gridId:%d的空单 orderId:%s", cancelId, cancelG.shortOrderId));
            cancelG.hasShortOrder = false;
            cancelG.shortOrderId = null;
        }
 
        Grid farthest = null;
        for (Grid e : gridElements) {
            if (e.shortTpOrderId != null) {
                if (farthest == null || e.price.compareTo(farthest.price) < 0) {
                    farthest = e;
                }
            }
        }
        if (farthest != null) {
            log(String.format("  取消最远空仓止盈 gridId:%d orderId:%s", farthest.id, farthest.shortTpOrderId));
            farthest.shortTpOrderId = null;
        }
 
        if (shortPositionSize.compareTo(BigDecimal.ZERO) == 0) {
            shortActive = false;
            log("  空仓已全部止损,shortActive=false");
        }
    }
 
    static void extendLongStopLoss(int filledQty) {
        // 找到最远止损
        int furthestId = 0;
        for (Grid e : gridElements) {
            if (e.longSlOrderId != null && e.id < furthestId) {
                furthestId = e.id;
            }
        }
        if (furthestId == 0) furthestId = -11;
        for (int i = 0; i < filledQty; i++) {
            int newId = furthestId - i - 1;
            Grid g = INDEX.get(newId);
            if (g == null) continue;
            String slId = newOrderId();
            g.longSlOrderId = slId;
            log(String.format("  多仓止损追加 gridId:%d slId:%s", newId, slId));
        }
    }
 
    static void extendShortStopLoss(int filledQty) {
        int furthestId = 0;
        for (Grid e : gridElements) {
            if (e.shortSlOrderId != null && e.id > furthestId) {
                furthestId = e.id;
            }
        }
        if (furthestId == 0) furthestId = 11;
        for (int i = 0; i < filledQty; i++) {
            int newId = furthestId + i + 1;
            Grid g = INDEX.get(newId);
            if (g == null) continue;
            String slId = newOrderId();
            g.shortSlOrderId = slId;
            log(String.format("  空仓止损追加 gridId:%d slId:%s", newId, slId));
        }
    }
 
    // ================================================================
    // 模拟 onPositionUpdate
    // ================================================================
    static void onPositionUpdate(boolean isLong, BigDecimal size, BigDecimal entryPrice) {
        if (isLong) {
            if (size.compareTo(BigDecimal.ZERO) > 0) {
                longActive = true;
                longPositionSize = size;
                longEntryPrice = entryPrice;
            } else {
                longActive = false;
                longPositionSize = BigDecimal.ZERO;
                longEntryPrice = BigDecimal.ZERO;
            }
        } else {
            if (size.compareTo(BigDecimal.ZERO) > 0) {
                shortActive = true;
                shortPositionSize = size;
                shortEntryPrice = entryPrice;
            } else {
                shortActive = false;
                shortPositionSize = BigDecimal.ZERO;
                shortEntryPrice = BigDecimal.ZERO;
            }
        }
    }
 
    // ================================================================
    // 查找方法
    // ================================================================
    static Grid findByPrice(BigDecimal price) {
        for (Grid g : gridElements) {
            if (g.price.compareTo(price) == 0) return g;
        }
        return null;
    }
    static Grid findByLongSlOrderId(String oid) {
        for (Grid g : gridElements) if (oid.equals(g.longSlOrderId)) return g;
        return null;
    }
    static Grid findByShortSlOrderId(String oid) {
        for (Grid g : gridElements) if (oid.equals(g.shortSlOrderId)) return g;
        return null;
    }
    static Grid findByLongOrderId(String oid) {
        for (Grid g : gridElements) if (oid.equals(g.longOrderId)) return g;
        return null;
    }
    static Grid findByShortOrderId(String oid) {
        for (Grid g : gridElements) if (oid.equals(g.shortOrderId)) return g;
        return null;
    }
 
    // ================================================================
    // 打印
    // ================================================================
    static void printGridState() {
        System.out.println("\n  === 网格状态 ===");
        System.out.println("  多仓: pos=" + longPositionSize + " active=" + longActive + " entryPrice=" + longEntryPrice);
        System.out.println("  空仓: pos=" + shortPositionSize + " active=" + shortActive + " entryPrice=" + shortEntryPrice);
        for (Grid g : gridElements) {
            System.out.println("  " + g);
        }
    }
 
    // ================================================================
    // 主模拟流程
    // ================================================================
    public static void main(String[] args) {
        System.out.println(repeat("=",70));
        System.out.println("Gate 网格策略模拟器");
        System.out.println("参数: basePrice=100.0, step=0.5, baseQty=10, qty=1, gridSize=10");
        System.out.println(repeat("=",70));
 
        init();
 
        // ---- 场景 A: 价格稳定 → 基底双开 → 进入ACTIVE ----
        System.out.println("\n\n◆◆◆ 场景A: 首根K线,基底双开 ◆◆◆");
        onFirstKline();
        // 模拟 onPositionUpdate 设置基底持仓
        onPositionUpdate(true, new BigDecimal(BASE_QTY), BASE_PRICE);
        onPositionUpdate(false, new BigDecimal(BASE_QTY), BASE_PRICE);
 
        printGridState();
 
        // ---- 场景 B: 价格下跌触发多仓止损 ----
        System.out.println("\n\n◆◆◆ 场景B: 价格跌至99.0,触发多仓止损(gridId:-2) ◆◆◆");
        Grid gMinus2 = INDEX.get(-2);
        String slOrderId = gMinus2.longSlOrderId;
        if (slOrderId != null) {
            // 模拟止损成交
            onAutoOrder(slOrderId, "finished", "plan-close-long-position", "T001");
            // 模拟 onPositionUpdate: 多仓从10降到9
            onPositionUpdate(true, new BigDecimal("9"), new BigDecimal("99.5"));
            printGridState();
        } else {
            log("  gridId:-2 无止损单!");
        }
 
        // ---- 场景 C: 价格持续下跌,再次触发止损 ----
        System.out.println("\n\n◆◆◆ 场景C: 价格跌至98.5,再次触发多仓止损(gridId:-3) ◆◆◆");
        Grid gMinus3 = INDEX.get(-3);
        slOrderId = gMinus3.longSlOrderId;
        if (slOrderId != null) {
            onAutoOrder(slOrderId, "finished", "plan-close-long-position", "T002");
            onPositionUpdate(true, new BigDecimal("8"), new BigDecimal("99.0"));
            printGridState();
        }
 
        // ---- 场景 D: 止损追单成交,测试止盈追挂 ----
        System.out.println("\n\n◆◆◆ 场景D: 追单成交,触发止盈追挂 ◆◆◆");
        // 找到最近挂的多单
        Grid pendingLong = null;
        for (Grid g : gridElements) {
            if (g.hasLongOrder) {
                pendingLong = g;
                break;
            }
        }
        if (pendingLong != null) {
            // 模拟追单成交,追了2张
            log("\n模拟追单成交: gridId:" + pendingLong.id + " orderId:" + pendingLong.longOrderId);
            longPositionSize = longPositionSize.add(new BigDecimal(pendingLong.longFilledQty));
            log("多仓持仓变为: " + longPositionSize);
            onAutoOrder(pendingLong.longOrderId, "finished", "", "T003");
            onPositionUpdate(true, longPositionSize, BASE_PRICE);
            printGridState();
        }
 
        // ---- 场景 E: 多仓全部止损归零 ----
        System.out.println("\n\n◆◆◆ 场景E: 价格继续暴跌,多仓全部止损归零 ◆◆◆");
        log("模拟多仓全部平仓...");
        onPositionUpdate(true, BigDecimal.ZERO, BigDecimal.ZERO);
        log("多仓归零, longActive=" + longActive + ", pos=" + longPositionSize);
 
        // ACTIVE状态下多仓归零 → 下一根K线触发processShortGrid
        System.out.println("\n  下一根K线触发 processShortGrid:");
        onKline(new BigDecimal("95.0"));
        printGridState();
 
        // ---- 场景 F: 空仓止损触发 ----
        System.out.println("\n\n◆◆◆ 场景F: 价格暴涨至101.0,触发空仓止损(gridId:2) ◆◆◆");
        Grid g2 = INDEX.get(2);
        if (g2 != null && g2.shortSlOrderId != null) {
            onAutoOrder(g2.shortSlOrderId, "finished", "plan-close-short-position", "T004");
            onPositionUpdate(false, new BigDecimal("9"), new BigDecimal("100.5"));
            printGridState();
        }
 
        // ================================================================
        // BUG 分析
        // ================================================================
        System.out.println("\n\n" + repeat("=",70));
        System.out.println("BUG 分析报告");
        System.out.println(repeat("=",70));
 
        int bugNum = 1;
 
        // BUG 1
        System.out.println("\n[BUG-" + (bugNum++) + "] 基座IOC成交不触发onAutoOrder,hasLongOrder/hasShortOrder永久为true");
        System.out.println("  位置: GateGridTradeService.onKline() + tryGenerateQueues()");
        System.out.println("  描述: 基底市价IOC开仓后,tryGenerateQueues中在ID=0设置了hasLongOrder=true/hasShortOrder=true。");
        System.out.println("        但IOC成交不会触发onAutoOrder(条件单回调),导致ID=0的挂单状态永远不会被清除。");
        System.out.println("  影响: ID=0的hasLongOrder永远为true。当止损触发在gridId=-2时,会尝试取消gridId=0的多单");
        System.out.println("        (cancelGridId = -2 + 2 = 0),由于IOC已成交,取消会失败但只产生warn日志,无严重后果。");
        System.out.println("        但状态不一致可能影响后续processShortGrid/processLongGrid的匹配逻辑。");
        System.out.println("  修复: tryGenerateQueues中不要用IOC的orderId标记hasLongOrder,或者增加onOrderUpdate回调处理普通订单。");
 
        // BUG 2
        System.out.println("\n[BUG-" + (bugNum++) + "] handleShortStopLossTriggered 使用了longPositionSize计算size");
        System.out.println("  位置: handleShortStopLossTriggered() line 1176");
        System.out.println("  代码: BigDecimal subtract = baseQuantity.subtract(longPositionSize);");
        System.out.println("  描述: 空仓止损触发时,用 longPositionSize 来计算追单size,应该用 shortPositionSize。");
        System.out.println("  影响: 空仓止损追单的size计算基于多头持仓而非空头持仓,导致补仓数量错误。");
        System.out.println("        例如多仓持仓5,空仓持仓9,空仓止损触发时: subtract = 10 - 5 = 5, size = 5+1 = 6.");
        System.out.println("        应该是: subtract = 10 - 9 = 1, size = 1+1 = 2.");
        System.out.println("  修复: 将 longPositionSize 改为 shortPositionSize。");
 
        // BUG 3
        System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中持仓判断存在竞态问题");
        System.out.println("  位置: onAutoOrder() 追挂止盈逻辑 (line 617/655)");
        System.out.println("  描述: onAutoOrder被调用时,longPositionSize/shortPositionSize可能尚未被onPositionUpdate更新。");
        System.out.println("        WebSocket消息顺序虽然是顺序的,但onAutoOrder基于orders/autoorders频道,");
        System.out.println("        onPositionUpdate基于positions频道,两者到达顺序不一定严格匹配。");
        System.out.println("  影响: 追挂止盈的excessCount计算可能偏小(持仓尚未更新),导致少挂止盈单。");
        System.out.println("        下次fill时会再次尝试追挂,重复计算可能产生多余止盈单。");
        System.out.println("  修复: 考虑在onPositionUpdate中做止盈追挂,或基于filledQty累加预估新持仓。");
 
        // BUG 4
        System.out.println("\n[BUG-" + (bugNum++) + "] 止盈追挂从gridId±2开始,可能覆盖已有止损单的网格位置");
        System.out.println("  位置: onAutoOrder() 止盈追挂逻辑");
        System.out.println("  描述: 止盈追挂从 filledGridId+2(多)/filledGridId-2(空) 开始。");
        System.out.println("        但这些位置可能已经被止损单占用(如initStopLossOrders在ID 2~11和-2~-11挂止损)。");
        System.out.println("        虽然止盈和止损是不同的订单(分别存在takeProfitOrderId和stopLossOrderId中),");
        System.out.println("        但同一个GridElement同时有止盈和止损,在状态追踪上可能模糊。");
        System.out.println("  影响: 同一个网格位置可能同时有止盈和止损订单,当订单成交回调时可能匹配错误。");
        System.out.println("  风险: 中低。onAutoOrder先用findByLongStopLossOrderId匹配止损,再匹配入口单,止盈不会被匹配。");
 
        // BUG 5 → 已确认为设计意图,移除
        System.out.println("\n[OK-5] processShortGrid/processLongGrid 只在对方持仓归零时触发 — 属于设计意图,非BUG");
        System.out.println("  位置: onKline() line 389-400");
        System.out.println("  描述: 策略有两套驱动机制:有持仓时走订单驱动(onAutoOrder处理挂单成交/止损追单),");
        System.out.println("        当某方向持仓归零后切换到价格驱动(onKline→processGrid),挂新入场单后回到订单驱动。");
        System.out.println("        两套机制交替运作,确保仓位归零后能基于当前价格重新入场。");
 
        // BUG 6
        System.out.println("\n[BUG-" + (bugNum++) + "] processShortGrid/processLongGrid 中 size 使用 BASE_QTY 而非 QTY");
        System.out.println("  位置: processShortGrid() line 1027, processLongGrid() line 1077");
        System.out.println("  描述: 仓位归零后重新挂单时,使用 baseQuantity 而不是 quantity。");
        System.out.println("        例如 baseQuantity=10,会在新位置挂10张,而其他网格层级只挂1张。");
        System.out.println("  影响: 每次仓位归零后的重新入场都是10张,可能导致持仓量远超预期。");
        System.out.println("  风险: 可能是设计意图(仓位归零后需要重建基底仓位),但需确认。");
 
        // BUG 7
        System.out.println("\n[BUG-" + (bugNum++) + "] extendLongStopLoss/extendShortStopLoss 不停向外扩展");
        System.out.println("  位置: extendLongStopLoss() line 1216, extendShortStopLoss() line 1249");
        System.out.println("  描述: 每次成交都调用extend,从当前最远止损位置继续向外挂filledQty个止损单。");
        System.out.println("        如果成交次数多,止损单会无限向外扩展(比如挂到gridId=-100)。");
        System.out.println("  影响: 大量止损单可能超出交易所限制(Gate条件单上限通常200个)。并且止损位置离当前价");
        System.out.println("        越来越远,实际作用不大但占用资源。");
        System.out.println("  风险: 低中。止损位置越来越远意味着回撤越来越大,风控上可能已经触发maxLoss停止策略。");
 
        // BUG 8 → 已确认为设计意图,移除
        System.out.println("\n[OK-8] onPositionUpdate 中两仓归零异步重启策略 — 属于设计意图,非BUG");
        System.out.println("  位置: onPositionUpdate() line 522-538");
        System.out.println("  描述: 当 shortActive==false && longActive==false 时,取消所有条件单并平仓,");
        System.out.println("        然后异步sleep 3s后调用startGrid重启;双仓归零后重新开启一轮新策略。");
 
        // BUG 9
        System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中stopLoss匹配缺少positionSize校验");
        System.out.println("  位置: onAutoOrder() line 595-604");
        System.out.println("  描述: 原始代码有 longPositionSize > 0 的校验但被注释掉了:");
        System.out.println("        // if (longStopLossElem != null && longPositionSize.compareTo(BigDecimal.ZERO) > 0 ...");
        System.out.println("       现在只要tradeId非空非0就触发handler。");
        System.out.println("  影响: 如果多仓已全部平仓但止损单尚未取消(异步延迟),止损单的finished回调");
        System.out.println("        仍会触发handleLongStopLossTriggered,产生多余的追单操作。");
        System.out.println("  修复: 恢复positionSize > 0的校验。");
 
        // BUG 10
        System.out.println("\n[BUG-" + (bugNum++) + "] tryGenerateQueues中上/下ID分配有误导性命名");
        System.out.println("  位置: updateGridElements() line 960-967");
        System.out.println("  描述: ID=0的upId=1(指向高价), downId=-1(指向低价)。");
        System.out.println("        在注释和文档中写的是 upId=-1, downId=1,但代码实际是反过来的。");
        System.out.println("        实际语义: upId=\"向上的价格\"(更大ID), downId=\"向下的价格\"(更小ID)。");
        System.out.println("  影响: 代码实际工作正确,但注释和代码不一致,容易误导维护者。");
        System.out.println("  状态: 可能是早期代码重构遗留,实际语义是\"up=价格向上, down=价格向下\"。");
 
        // BUG 11
        System.out.println("\n[BUG-" + (bugNum++) + "] currentLongOrderIds/currentShortOrderIds 声明但从未使用");
        System.out.println("  位置: GateGridTradeService line 119-121");
        System.out.println("  描述: currentLongOrderIds 和 currentShortOrderIds 是 synchronized LinkedHashMap,");
        System.out.println("        声明但代码中没有任何put操作,只有clear。");
        System.out.println("  影响: 死代码,占用内存但无实际作用。代码中对此Map的使用意图已过时。");
 
        // BUG 12
        System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中已成交的挂单仍通过isHasLongOrder检测");
        System.out.println("  位置: onAutoOrder() line 608/617");
        System.out.println("  描述: 当查找shortGridElement时,检查 isHasShortOrder() 为true才处理。");
        System.out.println("        但同一个orderId被匹配到说明它确实是挂单,检查 isHasShortOrder 可能因");
        System.out.println("        placeEntryOrderWithPreFlag的竞态导致问题(API失败→回滚→但WS已推送)。");
        System.out.println("  影响: 如果挂单API失败但WS已推送finished回调,isHasShortOrder可能已回滚为false,");
        System.out.println("        导致成交回调被忽略,持仓状态与实际不符。");
        System.out.println("  风险: 极低(需要API失败+WS推送的精确时序)。");
    }
}