From 5a2c3b8966269f77b60a698af602ffcfefc459ce Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Mon, 10 Aug 2026 13:53:01 +0800
Subject: [PATCH] feat(gate): 添加加仓止损配置功能
---
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java | 229 +++++++++----------------------------
src/main/resources/static/gate-config.html | 27 ++++
src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java | 12 ++
src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java | 4
src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java | 36 ++++++
5 files changed, 133 insertions(+), 175 deletions(-)
diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java
index 31cc1f8..5f9ab69 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java
@@ -111,6 +111,14 @@
private final boolean priceDriveEnabled;
/** 策略运行轮数:达到盈利后重启算一轮,达到上限后不再重启。0=不限轮数 */
private final int rounds;
+ /** 止损次数统计方式:"single"=单向分别统计 / "dual"=双向统一统计,默认 "dual" */
+ private final String stopLossCountMode;
+ /** 加仓间隔:每隔多少次止损触发一次加仓,默认 3 */
+ private final int addPositionInterval;
+ /** 加仓数量:每次加仓追加的张数,默认 1 */
+ private final int addPositionQuantity;
+ /** 单边最大仓位量:单向持仓张数上限,超出则按上限挂单,0=不限制 */
+ private final int maxPositionPerSide;
/** 网格绝对步长(shortBaseEntryPrice × gridRate),运行时由队列生成逻辑设置 */
private BigDecimal step;
/** 网格元素列表,由队列初始化时同步填充,包含完整的多空仓挂单状态 */
@@ -146,6 +154,10 @@
this.takeProfitGridSpan = builder.takeProfitGridSpan;
this.priceDriveEnabled = builder.priceDriveEnabled;
this.rounds = builder.rounds;
+ this.stopLossCountMode = builder.stopLossCountMode;
+ this.addPositionInterval = builder.addPositionInterval;
+ this.addPositionQuantity = builder.addPositionQuantity;
+ this.maxPositionPerSide = builder.maxPositionPerSide;
}
// ==================== REST/WS 地址 ====================
@@ -241,6 +253,14 @@
public boolean isPriceDriveEnabled() { return priceDriveEnabled; }
/** @return 策略运行轮数上限,0=不限 */
public int getRounds() { return rounds; }
+ /** @return 止损次数统计方式:"single"=单向分别统计 / "dual"=双向统一统计 */
+ public String getStopLossCountMode() { return stopLossCountMode; }
+ /** @return 加仓间隔:每隔多少次止损触发一次加仓 */
+ public int getAddPositionInterval() { return addPositionInterval; }
+ /** @return 加仓数量:每次加仓追加的张数 */
+ public int getAddPositionQuantity() { return addPositionQuantity; }
+ /** @return 单边最大仓位量:单向持仓张数上限,0=不限制 */
+ public int getMaxPositionPerSide() { return maxPositionPerSide; }
// ==================== 运行时参数 ====================
@@ -341,6 +361,14 @@
private boolean priceDriveEnabled = true;
/** 策略运行轮数上限,0=不限轮数。默认 0 */
private int rounds = 0;
+ /** 止损次数统计方式:"single"=单向分别统计 / "dual"=双向统一统计,默认 "dual" */
+ private String stopLossCountMode = "dual";
+ /** 加仓间隔:每隔多少次止损触发一次加仓,默认 3 */
+ private int addPositionInterval = 3;
+ /** 加仓数量:每次加仓追加的张数,默认 1 */
+ private int addPositionQuantity = 1;
+ /** 单边最大仓位量:单向持仓张数上限,超出则按上限挂单,0=不限制 */
+ private int maxPositionPerSide = 0;
/** 设置 API Key */
public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; }
@@ -392,6 +420,14 @@
public Builder priceDriveEnabled(boolean priceDriveEnabled) { this.priceDriveEnabled = priceDriveEnabled; return this; }
/** 设置策略运行轮数上限:达到盈利后重启计数,达到此值后不再重启。0=不限 */
public Builder rounds(int rounds) { this.rounds = rounds; return this; }
+ /** 设置止损次数统计方式:"single"=单向分别统计 / "dual"=双向统一统计 */
+ public Builder stopLossCountMode(String stopLossCountMode) { this.stopLossCountMode = stopLossCountMode; return this; }
+ /** 设置加仓间隔:每隔多少次止损触发一次加仓 */
+ public Builder addPositionInterval(int addPositionInterval) { this.addPositionInterval = addPositionInterval; return this; }
+ /** 设置加仓数量:每次加仓追加的张数 */
+ public Builder addPositionQuantity(int addPositionQuantity) { this.addPositionQuantity = addPositionQuantity; return this; }
+ /** 设置单边最大仓位量:单向持仓张数上限,0=不限制 */
+ public Builder maxPositionPerSide(int maxPositionPerSide) { this.maxPositionPerSide = maxPositionPerSide; return this; }
public GateConfig build() {
return new GateConfig(this);
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 bd054b4..7a897dd 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
@@ -57,6 +57,14 @@
private Boolean priceDriveEnabled;
/** 策略运行轮数上限:达到盈利后重启计数,达到此值后不再重启。0=不限 */
private int rounds;
+ /** 止损次数统计方式:"single"=单向分别统计 / "dual"=双向统一统计,默认 "dual" */
+ private String stopLossCountMode;
+ /** 加仓间隔:每隔多少次止损触发一次加仓,默认 3 */
+ private int addPositionInterval;
+ /** 加仓数量:每次加仓追加的张数,默认 1 */
+ private int addPositionQuantity;
+ /** 单边最大仓位量:单向持仓张数上限,超出则按上限挂单,0=不限制 */
+ private int maxPositionPerSide;
/**
* 从 GateConfig 构建 DTO(不暴露 apiSecret)。
@@ -84,6 +92,10 @@
.isProduction(config.isProduction())
.priceDriveEnabled(config.isPriceDriveEnabled())
.rounds(config.getRounds())
+ .stopLossCountMode(config.getStopLossCountMode())
+ .addPositionInterval(config.getAddPositionInterval())
+ .addPositionQuantity(config.getAddPositionQuantity())
+ .maxPositionPerSide(config.getMaxPositionPerSide())
.build();
}
}
diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java
index f6e7000..abc0bc7 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java
@@ -106,6 +106,10 @@
.takeProfitGridSpan(dto.getTakeProfitGridSpan())
.priceDriveEnabled(nvl(dto.getPriceDriveEnabled(), true))
.rounds(dto.getRounds())
+ .stopLossCountMode(nvl(dto.getStopLossCountMode(), "dual"))
+ .addPositionInterval(dto.getAddPositionInterval() > 0 ? dto.getAddPositionInterval() : 3)
+ .addPositionQuantity(dto.getAddPositionQuantity() > 0 ? dto.getAddPositionQuantity() : 1)
+ .maxPositionPerSide(dto.getMaxPositionPerSide())
.build();
}
diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
index 589498a..11b3e00 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -676,7 +676,6 @@
log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
- checkShortStopLongProfit(accumulatedShortLossCount, shortGridElement.getId() + 2);
}
}
@@ -706,8 +705,6 @@
// [Gate] 止盈挂单:超出基础仓位的部分,挂在空仓第一止损位
// placeExcessTakeProfit(posSize, true);
log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
-
- checkLongStopShortProfit(accumulatedLongLossCount, longGridElement.getId() -2);
}
}
@@ -1274,6 +1271,56 @@
}
}
+ // ========== 加仓计算 ==========
+
+ /**
+ * 根据 {@code stopLossCountMode} 计算当前有效的止损次数。
+ * <ul>
+ * <li>{@code "single"}(单向):返回该方向的累计止损次数</li>
+ * <li>{@code "dual"}(双向):返回多空双向累计总次数</li>
+ * </ul>
+ *
+ * @param isLong {@code true}=多仓方向,{@code false}=空仓方向
+ * @return 有效止损次数
+ */
+ private int getEffectiveStopLossCount(boolean isLong) {
+ if ("single".equals(config.getStopLossCountMode())) {
+ return isLong ? accumulatedLongLossCount : accumulatedShortLossCount;
+ }
+ // "dual" — 双向总次数
+ return accumulatedLongLossCount + accumulatedShortLossCount;
+ }
+
+ /**
+ * 根据加仓配置计算止损追单时的实际下单量。
+ * <p>公式:</p>
+ * <pre>
+ * addMultiplier = floor(effectiveStopLossCount / addPositionInterval)
+ * addQty = addMultiplier × addPositionQuantity
+ * finalQty = min(baseQuantity + addQty, maxPositionPerSide > 0 ? maxPositionPerSide : ∞)
+ * </pre>
+ *
+ * @param isLong {@code true}=多仓方向,{@code false}=空仓方向
+ * @return 实际下单张数(字符串)
+ */
+ private String calculateEntryQuantity(boolean isLong) {
+ int baseQty = Integer.parseInt(config.getBaseQuantity());
+ int interval = config.getAddPositionInterval();
+ int addQtyPerUnit = config.getAddPositionQuantity();
+ int maxPerSide = config.getMaxPositionPerSide();
+
+ int effectiveCount = getEffectiveStopLossCount(isLong);
+ int addMultiplier = interval > 0 ? effectiveCount / interval : 0;
+ int addQty = addMultiplier * addQtyPerUnit;
+ int totalQty = baseQty + addQty;
+
+ if (maxPerSide > 0 && totalQty > maxPerSide) {
+ totalQty = maxPerSide;
+ }
+
+ return String.valueOf(totalQty);
+ }
+
private void handleLongStopLossTriggered(GridElement gridElement, String orderId) {
gridElement.removeLongStopLossOrderId(orderId);
@@ -1300,8 +1347,9 @@
// 止损追单:同一网格可有多笔挂单,不判断 isHasLongOrder,直接挂单
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
-// String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()) + Integer.parseInt(config.getQuantity()));
- String size = String.valueOf(config.getBaseQuantity());
+ String size = calculateEntryQuantity(true);
+ log.info("[Gate] 多仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
+ getEffectiveStopLossCount(true), config.getBaseQuantity(), size, config.getStopLossCountMode());
newEntryGrid.getLongTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice,
FuturesPriceTrigger.RuleEnum.NUMBER_1, size);
@@ -1317,172 +1365,6 @@
});
}
}
-
- }
-
- // ========== 止损触发操作表 ==========
- /**
- * value = int[2]: {对手满仓止盈%, 基础仓位阈值%}
- * <ul>
- * <li>[0] 对手满仓:挂对手仓位此%做止盈;0=不挂</li>
- * <li>[1] 基础仓位阈值(占maxPos的%):对手/己方仓位超出此阈值部分做止盈;0=不挂</li>
- * </ul>
- */
- private static final Map<Integer, int[]> STOP_LOSS_RULES = new LinkedHashMap<>();
- private static final int DEFAULT_STOP_LOSS_PERCENT = 5;
-
- static {
- // times {满仓%, 阈值%}
- put(1, new int[]{0, 0});
- put(2, new int[]{0, 0});
- put(3, new int[]{30, 0});
- put(4, new int[]{0, 0});
- put(5, new int[]{0, 0});
- put(6, new int[]{40, 40});
- put(7, new int[]{0, 0});
- put(8, new int[]{0, 0});
- put(9, new int[]{50, 50});
- put(10, new int[]{0, 0});
- put(11, new int[]{0, 0});
- put(12, new int[]{55, 60});
- put(13, new int[]{0, 0});
- put(14, new int[]{0, 0});
- put(15, new int[]{60, 60});
- put(16, new int[]{0, 0});
- put(17, new int[]{0, 0});
- put(18, new int[]{65, 65});
- put(19, new int[]{0, 0});
- put(20, new int[]{0, 0});
- put(21, new int[]{70, 70});
- put(22, new int[]{0, 0});
- put(23, new int[]{0, 0});
- put(24, new int[]{75, 75});
- put(25, new int[]{0, 0});
- put(26, new int[]{0, 0});
- put(27, new int[]{80, 80});
- put(28, new int[]{0, 0});
- put(29, new int[]{0, 0});
- put(30, new int[]{85, 85});
- put(31, new int[]{0, 0});
- put(32, new int[]{0, 0});
- }
-
- private static void put(int times, int[] rule) { STOP_LOSS_RULES.put(times, rule); }
-
- private static int[] ruleOf(int times) {
- int[] r = STOP_LOSS_RULES.get(times);
- return r != null ? r : STOP_LOSS_RULES.get(32);
- }
-
- /**
- * 多仓止损触发后,查表处理对手盘(空仓)盈止 + 己方盈止 + 激活。
- */
- private void checkLongStopShortProfit(int times, int gridId) {
- int[] r = ruleOf(times);
- int maxPos = Integer.parseInt(config.getBaseQuantity());
- if (maxPos <= 0) {
- return;
- }
-
- int fullTpPct = r[0]; // 对手满仓 → 止盈%
- int fullTpPctNum = maxPos * (100- fullTpPct)/ 100;
- int thresholdPct = r[1]; // 基础仓位阈值%
- int thresholdPosNum = maxPos * (100- thresholdPct)/ 100;
-
- // ---------- 对手盘(空仓)盈止 ----------
- int oppPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
- boolean isFullTp = oppPos > fullTpPctNum;
- if (isFullTp) {
- // 对手仓位超出阈值:超额整个挂一笔止盈,位置在触发格 - 1
- int excess = oppPos - fullTpPctNum;
- GridElement tpElem = GridElement.findById(gridId);
- if (tpElem != null) {
- placeTakeProfitAtGrid(tpElem, false, excess, times);
- }
- }
-
- // ---------- 己方(多仓)盈止(≥6次) ----------
-// if (times >= 6 && thresholdPct > 0) {
-// int selfPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
-// int excess = selfPos - thresholdPosNum;
-// if (excess > 0) {
-// int perOrderQty = maxPos * DEFAULT_STOP_LOSS_PERCENT / 100;
-// if (perOrderQty <= 0) {
-// perOrderQty = 1;
-// }
-// int totalOrders = excess / perOrderQty;
-// if (totalOrders <= 0) {
-// totalOrders = 1;
-// }
-// int entryGridId = 0; // 多仓建仓格
-// for (int i = 0; i < totalOrders; i++) {
-// int tpGridId = entryGridId + 2 * (i + 1); // 朝盈利方向(向上)间隔2格
-// GridElement tpElem = GridElement.findById(tpGridId);
-// if (tpElem == null) {
-// break;
-// }
-// placeTakeProfitAtGrid(tpElem, true, perOrderQty, times);
-// }
-// log.info("[Gate] 止损{}→己方多仓止盈, excess:{}, 挂{}单, 每单{}张",
-// times, excess, totalOrders, perOrderQty);
-// }
-// }
-
- }
-
- /**
- * 空仓止损触发后,查表处理对手盘(多仓)盈止 + 己方盈止。
- */
- private void checkShortStopLongProfit(int times, int gridId) {
- int[] r = ruleOf(times);
- int maxPos = Integer.parseInt(config.getBaseQuantity());
- if (maxPos <= 0) {
- return;
- }
-
- int fullTpPct = r[0]; // 对手满仓 → 止盈%
- int fullTpPctNum = maxPos * (100 - fullTpPct) / 100;
- int thresholdPct = r[1]; // 基础仓位阈值%
- int thresholdPosNum = maxPos * (100 - thresholdPct) / 100;
-
- // ---------- 对手盘(多仓)盈止 ----------
- int oppPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
- boolean isFullTp = oppPos > fullTpPctNum;
- if (isFullTp) {
- // 对手仓位超出阈值:超额整个挂一笔止盈,位置在触发格
- int excess = oppPos - fullTpPctNum;
- GridElement tpElem = GridElement.findById(gridId);
- if (tpElem != null) {
- placeTakeProfitAtGrid(tpElem, true, excess, times);
- }
- }
-
- // ---------- 己方(空仓)盈止(≥6次) ----------
-// if (times >= 6 && thresholdPct > 0) {
-// int selfPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
-// int excess = selfPos - thresholdPosNum;
-// if (excess > 0) {
-// int perOrderQty = maxPos * DEFAULT_STOP_LOSS_PERCENT / 100;
-// if (perOrderQty <= 0) {
-// perOrderQty = 1;
-// }
-// int totalOrders = excess / perOrderQty;
-// if (totalOrders <= 0) {
-// totalOrders = 1;
-// }
-// int entryGridId = 0; // 空仓建仓格
-// for (int i = 0; i < totalOrders; i++) {
-// int tpGridId = entryGridId - 2 * (i + 1); // 朝盈利方向(向下)间隔2格
-// GridElement tpElem = GridElement.findById(tpGridId);
-// if (tpElem == null) {
-// break;
-// }
-// placeTakeProfitAtGrid(tpElem, false, perOrderQty, times);
-// }
-// log.info("[Gate] 止损{}→己方空仓止盈, excess:{}, 挂{}单, 每单{}张",
-// times, excess, totalOrders, perOrderQty);
-// }
-// }
}
@@ -1510,8 +1392,9 @@
// 止损追单:同一网格可有多笔挂单,不判断 isHasShortOrder,直接挂单
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
-// String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()) + Integer.parseInt(config.getQuantity()));
- String size = String.valueOf(config.getBaseQuantity());
+ String size = calculateEntryQuantity(false);
+ log.info("[Gate] 空仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
+ getEffectiveStopLossCount(false), config.getBaseQuantity(), size, config.getStopLossCountMode());
newEntryGrid.getShortTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice,
FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size));
diff --git a/src/main/resources/static/gate-config.html b/src/main/resources/static/gate-config.html
index 1b41299..d3582e4 100644
--- a/src/main/resources/static/gate-config.html
+++ b/src/main/resources/static/gate-config.html
@@ -117,6 +117,20 @@
<div class="form-group"><label>止盈网格跨度</label><input id="takeProfitGridSpan" placeholder="2"></div>
<div class="form-group"><label>价格驱动开关</label><select id="priceDriveEnabled"><option value="true" selected>开启</option><option value="false">关闭</option></select></div>
<div class="form-group"><label>运行轮数(0=不限)</label><input id="rounds" type="number" placeholder="0"></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="stopLossCountMode">
+ <option value="dual" selected>双向统一统计</option>
+ <option value="single">单向分别统计</option>
+ </select>
+ </div>
+ <div class="form-group"><label>加仓间隔(次)</label><input id="addPositionInterval" type="number" placeholder="3"></div>
+ <div class="form-group"><label>加仓数量(张)</label><input id="addPositionQuantity" type="number" placeholder="1"></div>
+ <div class="form-group"><label>单边最大仓位量(张)</label><input id="maxPositionPerSide" type="number" placeholder="0(不限制)"></div>
</form>
</div>
@@ -192,7 +206,8 @@
} catch(e) {
// 文件不存在 → 用默认值填充,用户可编辑后保存
fillForm({ gridRate:0.005, expectedProfit:0.15, maxLoss:1.5,
- baseQuantity:'2', quantity:'2', maxPositionSize:4, stopLossCount:0, takeProfitGridSpan:2, priceDriveEnabled: true, rounds: 0 });
+ baseQuantity:'2', quantity:'2', maxPositionSize:4, stopLossCount:0, takeProfitGridSpan:2, priceDriveEnabled: true, rounds: 0,
+ stopLossCountMode:'dual', addPositionInterval:3, addPositionQuantity:1, maxPositionPerSide:0 });
toast('未找到配置,已加载默认值,编辑后请保存', 'success');
}
currentApiKey = key;
@@ -212,6 +227,10 @@
byId('takeProfitGridSpan').value = d.takeProfitGridSpan ?? '';
byId('priceDriveEnabled').value = (d.priceDriveEnabled === true || d.priceDriveEnabled === 'true') ? 'true' : 'false';
byId('rounds').value = d.rounds ?? 0;
+ byId('stopLossCountMode').value = d.stopLossCountMode || 'dual';
+ byId('addPositionInterval').value = d.addPositionInterval ?? 3;
+ byId('addPositionQuantity').value = d.addPositionQuantity ?? 1;
+ byId('maxPositionPerSide').value = d.maxPositionPerSide ?? 0;
}
function collectForm() {
@@ -226,7 +245,11 @@
stopLossCount: parseInt(byId('stopLossCount').value) || 0,
takeProfitGridSpan: parseInt(byId('takeProfitGridSpan').value) || 2,
priceDriveEnabled: byId('priceDriveEnabled').value === 'true',
- rounds: parseInt(byId('rounds').value) || 0
+ rounds: parseInt(byId('rounds').value) || 0,
+ stopLossCountMode: byId('stopLossCountMode').value,
+ addPositionInterval: parseInt(byId('addPositionInterval').value) || 3,
+ addPositionQuantity: parseInt(byId('addPositionQuantity').value) || 1,
+ maxPositionPerSide: parseInt(byId('maxPositionPerSide').value) || 0
};
}
--
Gitblit v1.9.1