From ff1a51a49e02aa347463257eccfab61c8c0cf0bd Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Sun, 20 Sep 2026 15:49:14 +0800
Subject: [PATCH] feat(station): 添加双开止损触发配置功能

---
 src/main/resources/static/station-dashboard.html                                    |    8 ++++
 src/main/java/com/xcong/excoin/modules/station/entity/StrategyStatus.java           |    9 ++++
 src/main/resources/static/gate-config.html                                          |   18 ++++++++-
 src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java                   |   12 ++++++
 src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java |    3 +
 src/main/resources/db/station-schema.sql                                            |   11 +++++
 6 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
index 97be386..c083e4d 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
@@ -71,6 +71,12 @@
     private int addPositionStartThreshold;
     /** 超额止盈开关:true=挂单成交后将超出基础仓位的部分挂止盈单,默认 false */
     private boolean placeExcessTakeProfit;
+    /** 双开止损触发开关:true=按多空止损触发价初始化双向止损单,false=沿用现有逻辑 */
+    private Boolean dualStopLossEnabled;
+    /** 多头止损触发价 */
+    private String longStopLossPrice;
+    /** 空头止损触发价 */
+    private String shortStopLossPrice;
 
     /**
      * 从 GateConfig 构建 DTO(不暴露 apiSecret)。
@@ -131,6 +137,9 @@
         m.put("maxPositionPerSide", maxPositionPerSide);
         m.put("addPositionStartThreshold", addPositionStartThreshold);
         m.put("placeExcessTakeProfit", placeExcessTakeProfit);
+        m.put("dualStopLossEnabled", dualStopLossEnabled);
+        m.put("longStopLossPrice", longStopLossPrice);
+        m.put("shortStopLossPrice", shortStopLossPrice);
         return m;
     }
 
@@ -165,6 +174,9 @@
                 .maxPositionPerSide(0)
                 .addPositionStartThreshold(1)
                 .placeExcessTakeProfit(false)
+                .dualStopLossEnabled(false)
+                .longStopLossPrice(null)
+                .shortStopLossPrice(null)
                 .build();
     }
 }
diff --git a/src/main/java/com/xcong/excoin/modules/station/entity/StrategyStatus.java b/src/main/java/com/xcong/excoin/modules/station/entity/StrategyStatus.java
index db1c277..96942d4 100644
--- a/src/main/java/com/xcong/excoin/modules/station/entity/StrategyStatus.java
+++ b/src/main/java/com/xcong/excoin/modules/station/entity/StrategyStatus.java
@@ -73,6 +73,15 @@
     /** 价格驱动开关 */
     private Boolean priceDriveEnabled;
 
+    /** 双开止损触发开关:true=按多空止损触发价初始化双向止损单,false=沿用现有逻辑 */
+    private Boolean dualStopLossEnabled;
+
+    /** 多头止损触发价 */
+    private String longStopLossPrice;
+
+    /** 空头止损触发价 */
+    private String shortStopLossPrice;
+
     /** 当前轮次 */
     private Integer currentRound;
 
diff --git a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
index b2ce1c0..c84880d 100644
--- a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
+++ b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
@@ -243,6 +243,9 @@
         if (params.containsKey("addPositionStartThreshold")) status.setAddPositionStartThreshold(intVal(params.get("addPositionStartThreshold")));
         if (params.containsKey("placeExcessTakeProfit")) status.setPlaceExcessTakeProfit(boolVal(params.get("placeExcessTakeProfit")));
         if (params.containsKey("priceDriveEnabled")) status.setPriceDriveEnabled(boolVal(params.get("priceDriveEnabled")));
+        if (params.containsKey("dualStopLossEnabled")) status.setDualStopLossEnabled(boolVal(params.get("dualStopLossEnabled")));
+        if (params.containsKey("longStopLossPrice")) status.setLongStopLossPrice(str(params.get("longStopLossPrice")));
+        if (params.containsKey("shortStopLossPrice")) status.setShortStopLossPrice(str(params.get("shortStopLossPrice")));
         if (params.containsKey("rounds")) status.setTotalRounds(intVal(params.get("rounds")));
         if (params.containsKey("principal")) status.setPrincipal(str(params.get("principal")));
     }
diff --git a/src/main/resources/db/station-schema.sql b/src/main/resources/db/station-schema.sql
index 9c18808..537b242 100644
--- a/src/main/resources/db/station-schema.sql
+++ b/src/main/resources/db/station-schema.sql
@@ -23,6 +23,9 @@
     `add_position_start_threshold` INT(11)     DEFAULT 0  COMMENT '加仓启动阈值',
     `place_excess_take_profit`    TINYINT(1)   DEFAULT 0  COMMENT '超额止盈开关',
     `price_drive_enabled`        TINYINT(1)   DEFAULT 1  COMMENT '价格驱动开关',
+    `dual_stop_loss_enabled`     TINYINT(1)   DEFAULT 0  COMMENT '双开止损触发开关',
+    `long_stop_loss_price`       VARCHAR(32)  DEFAULT NULL COMMENT '多头止损触发价',
+    `short_stop_loss_price`      VARCHAR(32)  DEFAULT NULL COMMENT '空头止损触发价',
     `current_round`  INT(11)      DEFAULT 0  COMMENT '当前轮次',
     `total_rounds`   INT(11)      DEFAULT 0  COMMENT '总轮数上限',
     `accumulated_long_loss`       INT(11)      DEFAULT 0  COMMENT '多头累计止损次数',
@@ -76,3 +79,11 @@
 -- ============================================================
 ALTER TABLE `strategy_status`
     ADD COLUMN `alias_name` VARCHAR(64) DEFAULT NULL COMMENT '实例别名(Station 端自定义显示名)' AFTER `api_key_md5`;
+
+-- ============================================================
+-- 升级脚本:双开止损触发价参数(2026-09 新增)
+-- ============================================================
+ALTER TABLE `strategy_status`
+    ADD COLUMN `dual_stop_loss_enabled` TINYINT(1)  DEFAULT 0    COMMENT '双开止损触发开关' AFTER `price_drive_enabled`,
+    ADD COLUMN `long_stop_loss_price`   VARCHAR(32) DEFAULT NULL COMMENT '多头止损触发价' AFTER `dual_stop_loss_enabled`,
+    ADD COLUMN `short_stop_loss_price`  VARCHAR(32) DEFAULT NULL COMMENT '空头止损触发价' AFTER `long_stop_loss_price`;
diff --git a/src/main/resources/static/gate-config.html b/src/main/resources/static/gate-config.html
index 7c3d40e..319c8f3 100644
--- a/src/main/resources/static/gate-config.html
+++ b/src/main/resources/static/gate-config.html
@@ -133,6 +133,13 @@
             <div class="form-group"><label>单边最大仓位量(张)</label><input id="maxPositionPerSide" type="number" placeholder="0(不限制)"></div>
             <div class="form-group"><label>加仓启动阈值(次)</label><input id="addPositionStartThreshold" type="number" placeholder="1(前N次止损不加仓)"></div>
             <div class="form-group"><label>超额止盈开关</label><select id="placeExcessTakeProfit"><option value="false" selected>关闭</option><option value="true">开启</option></select></div>
+            <!-- ===== 双开止损触发配置 ===== -->
+            <div class="form-group full" style="margin-top:6px;border-top:1px solid var(--border);padding-top:8px">
+                <label style="font-size:13px;font-weight:600;color:var(--text)">双开止损触发</label>
+            </div>
+            <div class="form-group"><label>双开止损触发开关</label><select id="dualStopLossEnabled"><option value="false" selected>关闭</option><option value="true">开启</option></select></div>
+            <div class="form-group"><label>多头止损触发价</label><input id="longStopLossPrice" placeholder="如 2540"></div>
+            <div class="form-group"><label>空头止损触发价</label><input id="shortStopLossPrice" placeholder="如 2650"></div>
         </form>
     </div>
 
@@ -209,7 +216,8 @@
         // 文件不存在 → 用默认值填充,用户可编辑后保存
         fillForm({ gridRate:0.005, expectedProfit:0.15, maxLoss:1.5,
             baseQuantity:'2', quantity:'2', maxPositionSize:4, stopLossCount:0, takeProfitGridSpan:2, priceDriveEnabled: true, rounds: 0,
-            stopLossCountMode:'dual', addPositionInterval:3, addPositionQuantity:1, maxPositionPerSide:0, addPositionStartThreshold:1, placeExcessTakeProfit: false });
+            stopLossCountMode:'dual', addPositionInterval:3, addPositionQuantity:1, maxPositionPerSide:0, addPositionStartThreshold:1, placeExcessTakeProfit: false,
+            dualStopLossEnabled: false, longStopLossPrice: '', shortStopLossPrice: '' });
         toast('未找到配置,已加载默认值,编辑后请保存', 'success');
     }
     currentApiKey = key;
@@ -235,6 +243,9 @@
     byId('maxPositionPerSide').value = d.maxPositionPerSide ?? 0;
     byId('addPositionStartThreshold').value = d.addPositionStartThreshold ?? 1;
     byId('placeExcessTakeProfit').value = (d.placeExcessTakeProfit === true || d.placeExcessTakeProfit === 'true') ? 'true' : 'false';
+    byId('dualStopLossEnabled').value = (d.dualStopLossEnabled === true || d.dualStopLossEnabled === 'true') ? 'true' : 'false';
+    byId('longStopLossPrice').value = d.longStopLossPrice || '';
+    byId('shortStopLossPrice').value = d.shortStopLossPrice || '';
 }
 
 function collectForm() {
@@ -255,7 +266,10 @@
         addPositionQuantity: intVal(byId('addPositionQuantity').value, 1),
         maxPositionPerSide: intVal(byId('maxPositionPerSide').value, 0),
         addPositionStartThreshold: intVal(byId('addPositionStartThreshold').value, 1),
-        placeExcessTakeProfit: byId('placeExcessTakeProfit').value === 'true'
+        placeExcessTakeProfit: byId('placeExcessTakeProfit').value === 'true',
+        dualStopLossEnabled: byId('dualStopLossEnabled').value === 'true',
+        longStopLossPrice: byId('longStopLossPrice').value,
+        shortStopLossPrice: byId('shortStopLossPrice').value
     };
 }
 function intVal(str, fallback) {
diff --git a/src/main/resources/static/station-dashboard.html b/src/main/resources/static/station-dashboard.html
index c19fc8c..e694045 100644
--- a/src/main/resources/static/station-dashboard.html
+++ b/src/main/resources/static/station-dashboard.html
@@ -1038,6 +1038,9 @@
     addPositionStartThreshold: 1,
     placeExcessTakeProfit: false,
     priceDriveEnabled: true,
+    dualStopLossEnabled: false,
+    longStopLossPrice: '',
+    shortStopLossPrice: '',
 };
 
 async function loadConfig(md5) {
@@ -1084,6 +1087,10 @@
     { key: 'rounds', label: '运行轮数 (0=不限)', type: 'number', placeholder: '0', jtype: 'int', group: 'run', tip: '盈利重启达此轮数后停止' },
     { key: 'placeExcessTakeProfit', label: '超额止盈', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'run' },
     { key: 'priceDriveEnabled', label: '价格驱动', type: 'select', options: [{ v: 'true', l: '开启' }, { v: 'false', l: '关闭' }], group: 'run' },
+
+    { key: 'dualStopLossEnabled', label: '双开止损触发', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'dual-sl', tip: '开启后按下方多空止损触发价初始化双向止损单,任一触发后重挂另一侧止损' },
+    { key: 'longStopLossPrice', label: '多头止损触发价', type: 'number', step: 'any', placeholder: '如 2540', jtype: 'string', group: 'dual-sl', tip: '价格跌至该值时触发多头止损' },
+    { key: 'shortStopLossPrice', label: '空头止损触发价', type: 'number', step: 'any', placeholder: '如 2650', jtype: 'string', group: 'dual-sl', tip: '价格涨至该值时触发空头止损' },
 ];
 
 // 分组定义(渲染顺序)
@@ -1091,6 +1098,7 @@
     { id: 'grid', name: '📐 网格参数' },
     { id: 'risk', name: '🛡️ 风控参数' },
     { id: 'addon', name: '📈 加仓 & 止损' },
+    { id: 'dual-sl', name: '🎯 双开止损触发' },
     { id: 'run', name: '⚡ 运行控制' },
 ];
 

--
Gitblit v1.9.1