| 2026-08-10 | Administrator | ![]() |
| 2026-08-10 | Administrator | ![]() |
| 2026-08-10 | Administrator | ![]() |
src/main/java/com/xcong/excoin/modules/gateApi/GateConfig.java
@@ -111,6 +111,16 @@ 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; /** 加仓启动阈值:前N次止损不触发加仓,默认 1(第1次止损不加仓,第2次起按公式计算) */ private final int addPositionStartThreshold; /** 网格绝对步长(shortBaseEntryPrice × gridRate),运行时由队列生成逻辑设置 */ private BigDecimal step; /** 网格元素列表,由队列初始化时同步填充,包含完整的多空仓挂单状态 */ @@ -146,6 +156,11 @@ 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; this.addPositionStartThreshold = builder.addPositionStartThreshold; } // ==================== REST/WS 地址 ==================== @@ -241,6 +256,16 @@ 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; } /** @return 加仓启动阈值:前N次止损不触发加仓 */ public int getAddPositionStartThreshold() { return addPositionStartThreshold; } // ==================== 运行时参数 ==================== @@ -341,6 +366,16 @@ 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; /** 加仓启动阈值:前N次止损不触发加仓,调整后的 effectiveCount = max(0, count - threshold)。默认 1 */ private int addPositionStartThreshold = 1; /** 设置 API Key */ public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } @@ -392,6 +427,16 @@ 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; } /** 设置加仓启动阈值:前N次止损不触发加仓,0=立即启动 */ public Builder addPositionStartThreshold(int addPositionStartThreshold) { this.addPositionStartThreshold = addPositionStartThreshold; return this; } public GateConfig build() { return new GateConfig(this); src/main/java/com/xcong/excoin/modules/gateApi/GateConfigDTO.java
@@ -57,6 +57,16 @@ 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; /** 加仓启动阈值:前N次止损不触发加仓,默认 1 */ private int addPositionStartThreshold; /** * 从 GateConfig 构建 DTO(不暴露 apiSecret)。 @@ -84,6 +94,11 @@ .isProduction(config.isProduction()) .priceDriveEnabled(config.isPriceDriveEnabled()) .rounds(config.getRounds()) .stopLossCountMode(config.getStopLossCountMode()) .addPositionInterval(config.getAddPositionInterval()) .addPositionQuantity(config.getAddPositionQuantity()) .maxPositionPerSide(config.getMaxPositionPerSide()) .addPositionStartThreshold(config.getAddPositionStartThreshold()) .build(); } } src/main/java/com/xcong/excoin/modules/gateApi/GateConfigPersistenceService.java
@@ -106,6 +106,11 @@ .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()) .addPositionStartThreshold(dto.getAddPositionStartThreshold() >= 0 ? dto.getAddPositionStartThreshold() : 1) .build(); } 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,63 @@ } } // ========== 加仓计算 ========== /** * 根据 {@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> * divisor = addPositionInterval + 1 * addMultiplier = floor(effectiveStopLossCount / divisor) * 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 startThreshold = config.getAddPositionStartThreshold(); if (startThreshold > 0) { effectiveCount = Math.max(0, effectiveCount - startThreshold); } // divisor = interval + 1:interval=0→每次加仓, interval=1→每2次加仓(2,4,6...), interval=3→每4次加仓(4,8,12...) int divisor = interval + 1; int addMultiplier = interval >= 0 ? effectiveCount / divisor : 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 +1354,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 +1372,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 +1399,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)); src/main/resources/static/gate-config.html
@@ -117,6 +117,21 @@ <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> <div class="form-group"><label>加仓启动阈值(次)</label><input id="addPositionStartThreshold" type="number" placeholder="1(前N次止损不加仓)"></div> </form> </div> @@ -192,7 +207,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, addPositionStartThreshold:1 }); toast('未找到配置,已加载默认值,编辑后请保存', 'success'); } currentApiKey = key; @@ -212,6 +228,11 @@ 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; byId('addPositionStartThreshold').value = d.addPositionStartThreshold ?? 1; } function collectForm() { @@ -226,9 +247,18 @@ 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: intVal(byId('addPositionInterval').value, 3), addPositionQuantity: intVal(byId('addPositionQuantity').value, 1), maxPositionPerSide: intVal(byId('maxPositionPerSide').value, 0), addPositionStartThreshold: intVal(byId('addPositionStartThreshold').value, 1) }; } function intVal(str, fallback) { const v = parseInt(str); return isNaN(v) ? fallback : v; } async function saveConfig() { if (!currentApiKey) return toast('请先输入 API Key', 'error');