| | |
| | | package com.xcong.excoin.modules.gateApi; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.xcong.excoin.utils.dingtalk.DingTalkUtils; |
| | | import io.gate.gateapi.ApiClient; |
| | | import io.gate.gateapi.ApiException; |
| | | import io.gate.gateapi.GateApiException; |
| | | import io.gate.gateapi.api.AccountApi; |
| | | import io.gate.gateapi.api.FuturesApi; |
| | | import io.gate.gateapi.models.FuturesAccount; |
| | | import io.gate.gateapi.models.FuturesInitialOrder; |
| | | import io.gate.gateapi.models.FuturesOrder; |
| | | import io.gate.gateapi.models.FuturesPriceTrigger; |
| | | import io.gate.gateapi.models.FuturesPriceTriggeredOrder; |
| | | import io.gate.gateapi.models.TriggerOrderResponse; |
| | | import io.gate.gateapi.models.*; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.io.IOException; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.xcong.excoin.modules.gateApi.wsHandler.handler.CandlestickChannelHandler; |
| | | import com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionClosesChannelHandler; |
| | | import com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionsChannelHandler; |
| | | |
| | | /** |
| | | * Gate 网格交易服务类,使用 gate-api SDK 进行合约下单。 |
| | | * 策略:多空双开 → 设置止盈止损点位 → 网格循环交易 |
| | | * 网格交易策略引擎 — 多空对冲网格。 |
| | | * |
| | | * 测试参数: |
| | | * 品种: XAU_USDT(黄金) |
| | | * 杠杆: 100x(全仓) |
| | | * 数量: 0.01 XAU |
| | | * 网格: 0.0035(千分之三点五) |
| | | * 整体止盈: 0.5 USDT |
| | | * 循环次数: 3 |
| | | * 报警: 本金亏损 15%(初始本金 50 USDT) |
| | | * <h3>策略原理</h3> |
| | | * 以空仓基底入场价(shortBaseEntryPrice)为价格基准,向上/向下各生成一个价格网格队列。 |
| | | * 价格触发网格层级时挂条件单,成交后自动挂止盈单。每笔止盈盈利 = step - minTick。 |
| | | * |
| | | * <h3>完整生命周期</h3> |
| | | * <pre> |
| | | * init() → startGrid() → WAITING_KLINE |
| | | * ↓ |
| | | * onKline(首根K线) → OPENING → 异步市价双开基底(开多+开空) |
| | | * ↓ |
| | | * onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened |
| | | * ↓ |
| | | * tryGenerateQueues() |
| | | * ├── generateShortQueue() ← 空仓价格队列(降序,从 shortBaseEntryPrice-step 向下) |
| | | * ├── generateLongQueue() ← 多仓价格队列(升序,从 shortBaseEntryPrice+step 向上) |
| | | * ├── updateGridElements() ← 构建 GridElement 列表 + TraderParam + 全局索引 |
| | | * ├── 挂基座止盈单(ID=0 的 long/short takeProfit) |
| | | * └── 挂初始条件单(up=-1 多单, down=1 空单) |
| | | * ↓ |
| | | * state = ACTIVE(每根K线反复执行以下循环) |
| | | * ↓ |
| | | * onKline() → processLongGrid() + processShortGrid() |
| | | * ├── 匹配队列元素 → 队列补偿 → 保证金检查 |
| | | * ├── 首元素方向:挂条件开仓单 → 订单ID + GridElement状态同步 |
| | | * └── 反向守卫:在 downGrid 位置挂对向单(价格区间+trigger方向校验) |
| | | * ↓ |
| | | * onOrderUpdate() ← futures.orders / futures.autoorders 推送 |
| | | * ├── 匹配止盈单ID → 清空止盈状态(已成交) |
| | | * └── 匹配挂单ID → 挂止盈条件单 → 止盈ID + GridElement状态同步 |
| | | * ↓ |
| | | * onPositionClose() → cumulativePnl 累加 |
| | | * ├── ≥ overallTp → STOPPED |
| | | * └── ≤ -maxLoss → STOPPED |
| | | * </pre> |
| | | * |
| | | * <h3>仓位线动态调整</h3> |
| | | * <pre> |
| | | * onPositionUpdate() 中仓位均价变化后: |
| | | * longEntryPrice ↑ → 取消 高于 longEntryPrice 的空仓挂单(避免逆势空单) |
| | | * shortEntryPrice ↓ → 取消 低于 shortEntryPrice 的多仓挂单(避免逆势多单) |
| | | * </pre> |
| | | * |
| | | * <h3>关键公式</h3> |
| | | * <pre> |
| | | * step = shortBaseEntryPrice × gridRate ← 网格绝对步长 |
| | | * minTick = 10^(-priceScale) ← 交易所最小价格单位 |
| | | * 多止盈 = gridPrice + (step - minTick) ← 多仓止盈价 |
| | | * 空止盈 = gridPrice - (step - minTick) ← 空仓止盈价 |
| | | * 单笔盈利 = (step - minTick) × contractMultiplier × quantity ← USDT |
| | | * </pre> |
| | | * |
| | | * <h3>线程模型</h3> |
| | | * 所有 WS 回调(onKline/onPositionUpdate/onOrderUpdate 等)在 WS 回调线程中串行执行。 |
| | | * 下单/撤单操作提交到 GateTradeExecutor 的单线程池异步执行,避免阻塞 WS 线程。 |
| | | * stopGrid() 会将 state 设为 STOPPED,后续所有 WS 回调直接返回不再处理。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class GateGridTradeService { |
| | | |
| | | private final ApiClient apiClient; |
| | | public enum StrategyState { |
| | | WAITING_KLINE, OPENING, ACTIVE, STOPPED |
| | | } |
| | | |
| | | /** |
| | | * 止盈条件单 order_type:仓位计划止盈止损 — 平多仓(支持部分平仓,size<0)。 |
| | | * 注意:不能用 close-long-position(仅支持全平且双仓需 auto_size), |
| | | * 必须用 plan-close-long-position 以支持指定张数部分平仓。 |
| | | */ |
| | | private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position"; |
| | | /** |
| | | * 止盈条件单 order_type:仓位计划止盈止损 — 平空仓(支持部分平仓,size>0)。 |
| | | * 注意:不能用 close-short-position(仅支持全平且双仓需 auto_size), |
| | | * 必须用 plan-close-short-position 以支持指定张数部分平仓。 |
| | | */ |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | private final GateConfig config; |
| | | private final GateTradeExecutor executor; |
| | | private final FuturesApi futuresApi; |
| | | private static final String SETTLE = "usdt"; |
| | | |
| | | private final String contract; |
| | | private final String leverage; |
| | | private final String marginMode; |
| | | private final BigDecimal gridRate; |
| | | private final BigDecimal overallTp; |
| | | private final int maxCycles; |
| | | private final BigDecimal maxLoss; |
| | | private final String quantity; |
| | | private final String positionMode; |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | | private volatile boolean strategyActive = false; |
| | | private int currentCycle = 0; |
| | | private BigDecimal totalProfit = BigDecimal.ZERO; |
| | | private BigDecimal longEntryPrice; |
| | | private BigDecimal shortEntryPrice; |
| | | private Long longOrderId; |
| | | private Long shortOrderId; |
| | | private Long longTpOrderId; |
| | | private Long longSlOrderId; |
| | | private Long shortTpOrderId; |
| | | private Long shortSlOrderId; |
| | | /** 空仓价格队列,降序排列(大→小),容量 gridQueueSize */ |
| | | private final List<BigDecimal> shortPriceQueue = Collections.synchronizedList(new ArrayList<>()); |
| | | /** 多仓价格队列,升序排列(小→大),容量 gridQueueSize */ |
| | | private final List<BigDecimal> longPriceQueue = Collections.synchronizedList(new ArrayList<>()); |
| | | |
| | | private volatile BigDecimal lastClosePrice; |
| | | /** 当前多仓条件单映射:订单ID → 止盈价格,订单成交后通过订单订阅推送匹配止盈 */ |
| | | private final Map<String, BigDecimal> currentLongOrderIds = Collections.synchronizedMap(new LinkedHashMap<>()); |
| | | /** 当前空仓条件单映射:订单ID → 止盈价格,订单成交后通过订单订阅推送匹配止盈 */ |
| | | private final Map<String, BigDecimal> currentShortOrderIds = Collections.synchronizedMap(new LinkedHashMap<>()); |
| | | |
| | | public GateGridTradeService(String apiKey, String apiSecret, |
| | | String contract, String leverage, |
| | | String marginMode,String positionMode, |
| | | BigDecimal gridRate, BigDecimal overallTp, |
| | | int maxCycles, BigDecimal maxLoss, |
| | | String quantity) { |
| | | this.contract = contract; |
| | | this.leverage = leverage; |
| | | this.marginMode = marginMode; |
| | | this.gridRate = gridRate; |
| | | this.overallTp = overallTp; |
| | | this.maxCycles = maxCycles; |
| | | this.maxLoss = maxLoss; |
| | | this.quantity = quantity; |
| | | this.positionMode = positionMode; |
| | | /** 基底空头入场价 */ |
| | | private BigDecimal shortBaseEntryPrice; |
| | | /** 基底多头入场价(仅记录,当前未被业务逻辑消费,保留以备后续使用) */ |
| | | private BigDecimal longBaseEntryPrice; |
| | | /** 基底多头是否已开 */ |
| | | private volatile boolean baseLongOpened = false; |
| | | /** 基底空头是否已开 */ |
| | | private volatile boolean baseShortOpened = false; |
| | | |
| | | this.apiClient = new ApiClient(); |
| | | this.apiClient.setBasePath("https://api-testnet.gateapi.io/api/v4"); |
| | | this.apiClient.setApiKeySecret(apiKey, apiSecret); |
| | | /** 空头是否活跃(有仓位) */ |
| | | private volatile boolean shortActive = false; |
| | | /** 多头是否活跃(有仓位) */ |
| | | private volatile boolean longActive = false; |
| | | |
| | | private volatile BigDecimal lastKlinePrice; |
| | | private volatile BigDecimal markPrice = BigDecimal.ZERO; |
| | | 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; |
| | | private Long userId; |
| | | private volatile BigDecimal initialPrincipal = BigDecimal.ZERO; |
| | | private volatile GateKlineWebSocketClient wsClient; |
| | | |
| | | /** 多仓挂单张数计数器:止损触发时用当前值挂单,随后+1;挂单成交后重置为1 */ |
| | | private volatile int longEntryQty = 1; |
| | | /** 空仓挂单张数计数器:止损触发时用当前值挂单,随后+1;挂单成交后重置为1 */ |
| | | private volatile int shortEntryQty = 1; |
| | | |
| | | public GateGridTradeService(GateConfig config) { |
| | | this.config = config; |
| | | ApiClient apiClient = new ApiClient(); |
| | | apiClient.setBasePath(config.getRestBasePath()); |
| | | apiClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | | this.futuresApi = new FuturesApi(apiClient); |
| | | this.executor = new GateTradeExecutor(apiClient, config.getContract()); |
| | | } |
| | | |
| | | // ---- 初始化 ---- |
| | | |
| | | /** |
| | | * 初始化账户:设置持仓模式 + 杠杆 |
| | | * 初始化策略环境。 |
| | | * |
| | | * <h3>执行顺序</h3> |
| | | * <ol> |
| | | * <li>获取用户 ID(用于私有频道订阅 payload)</li> |
| | | * <li>获取账户信息 → 记录初始本金</li> |
| | | * <li>如需要,切换为双向持仓模式</li> |
| | | * <li>如需要,调整持仓模式(single/dual)</li> |
| | | * <li>清除旧的止盈止损条件单</li> |
| | | * <li>平掉所有已有仓位</li> |
| | | * <li>设置杠杆倍数</li> |
| | | * </ol> |
| | | */ |
| | | public void init() { |
| | | try { |
| | | futuresApi.updateContractPositionLeverageCall( |
| | | SETTLE, contract, leverage, marginMode, positionMode, null); |
| | | log.info("[GateGrid] 已设置杠杆: {}x, 保证金模式: {}", leverage, marginMode); |
| | | ApiClient detailClient = new ApiClient(); |
| | | detailClient.setBasePath(config.getRestBasePath()); |
| | | detailClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | | AccountDetail detail = new AccountApi(detailClient).getAccountDetail(); |
| | | this.userId = detail.getUserId(); |
| | | log.info("[Gate] 用户ID: {}", userId); |
| | | |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | log.info("[GateGrid] 账户可用余额: {}, 总资产: {}", |
| | | account.getAvailable(), account.getTotal()); |
| | | String positionModeSet = account.getPositionMode(); |
| | | if (!positionMode.equals(positionModeSet)){ |
| | | futuresApi.setPositionMode(SETTLE, positionMode); |
| | | this.initialPrincipal = new BigDecimal(account.getTotal()); |
| | | log.info("[Gate] 初始本金: {} USDT", initialPrincipal); |
| | | |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | log.info("[Gate] 旧条件单已清除"); |
| | | closeExistingPositions(); |
| | | |
| | | //设置持仓模式为双向持仓 |
| | | Boolean inDualMode = account.getInDualMode(); |
| | | if (!inDualMode) { |
| | | try { |
| | | futuresApi.setDualModeCall(SETTLE,true,null).execute(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | log.info("[GateGrid] 已设置双向持仓模式"); |
| | | |
| | | try { |
| | | futuresApi.updateDualModePositionLeverageCall( |
| | | SETTLE, config.getContract(), config.getLeverage(), |
| | | null, null).execute(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | |
| | | if (!config.getMarginMode().equals(account.getMarginMode())) { |
| | | |
| | | UpdateDualCompPositionCrossModeRequest updateDualCompPositionCrossModeRequest = new UpdateDualCompPositionCrossModeRequest(); |
| | | updateDualCompPositionCrossModeRequest.setMode(config.getMarginMode()); |
| | | updateDualCompPositionCrossModeRequest.setContract(config.getContract()); |
| | | try { |
| | | futuresApi.updateDualCompPositionCrossModeCall(SETTLE, updateDualCompPositionCrossModeRequest, null).execute(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | log.info("[Gate] 持仓模式: {} 余额: {}", config.getPositionMode(), account.getAvailable()); |
| | | log.info("[Gate] 杠杆: {}x {}", config.getLeverage(), config.getMarginMode()); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 初始化失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | log.error("[Gate] 初始化失败, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (ApiException e) { |
| | | log.error("[GateGrid] 初始化API调用失败, code: {}", e.getCode()); |
| | | log.error("[Gate] 初始化失败, code:{}", e.getCode()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动网格策略 |
| | | * 平掉当前合约的所有已有仓位。 |
| | | * |
| | | * <h3>平仓策略</h3> |
| | | * <ul> |
| | | * <li>单向持仓:size=相反数,reduceOnly=true,市价 IOC 平仓</li> |
| | | * <li>双向持仓:size=0,close=false,autoSize=LONG/SHORT,reduceOnly=true,市价 IOC 全平</li> |
| | | * </ul> |
| | | * |
| | | * <h3>注意事项</h3> |
| | | * 双向持仓模式下必须使用 autoSize 参数,不能直接传负数 size, |
| | | * 否则 Gate API 会拒绝(双向模式下空头 size 为负是正常的持仓方向)。 |
| | | */ |
| | | private void closeExistingPositions() { |
| | | try { |
| | | List<Position> positions = futuresApi.listPositions(SETTLE).execute(); |
| | | if (positions == null || positions.isEmpty()) { log.info("[Gate] 无已有仓位"); return; } |
| | | for (Position pos : positions) { |
| | | if (!config.getContract().equals(pos.getContract())) { |
| | | continue; |
| | | } |
| | | String sizeStr = pos.getSize(); |
| | | long size = Long.parseLong(sizeStr); |
| | | if (size == 0) { |
| | | continue; |
| | | } |
| | | String closeSize = size > 0 ? String.valueOf(-size) : String.valueOf(Math.abs(size)); |
| | | Position.ModeEnum mode = pos.getMode(); |
| | | FuturesOrder closeOrder = new FuturesOrder(); |
| | | closeOrder.setContract(config.getContract()); |
| | | closeOrder.setPrice("0"); |
| | | closeOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | closeOrder.setReduceOnly(true); |
| | | if (mode != null && mode.getValue() != null && mode.getValue().contains("dual")) { |
| | | closeOrder.setSize("0"); |
| | | closeOrder.setClose(false); |
| | | closeOrder.setAutoSize(size > 0 ? FuturesOrder.AutoSizeEnum.LONG : FuturesOrder.AutoSizeEnum.SHORT); |
| | | } else { |
| | | closeOrder.setSize(closeSize); |
| | | } |
| | | closeOrder.setText("t-grid-init-close"); |
| | | futuresApi.createFuturesOrder(SETTLE, closeOrder, null); |
| | | log.info("[Gate] 平已有仓位, 方向:{}, size:{}, mode:{}", size > 0 ? "多" : "空", sizeStr, mode); |
| | | } |
| | | } catch (GateApiException e) { |
| | | log.warn("[Gate] 平仓位失败, label:{}, msg:{}", e.getErrorLabel(), e.getMessage()); |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 平仓位异常", e); |
| | | } |
| | | } |
| | | |
| | | // ---- 启动/停止 ---- |
| | | |
| | | /** |
| | | * 启动网格策略。重置所有状态变量和队列,进入 WAITING_KLINE 等待首根 K 线。 |
| | | * 仅当当前状态为 WAITING_KLINE 或 STOPPED 时才允许启动。 |
| | | */ |
| | | public void startGrid() { |
| | | if (strategyActive) { |
| | | log.warn("[GateGrid] 策略已在运行中"); |
| | | if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { |
| | | log.warn("[Gate] 策略已在运行中, state:{}", state); |
| | | return; |
| | | } |
| | | strategyActive = true; |
| | | currentCycle = 0; |
| | | totalProfit = BigDecimal.ZERO; |
| | | log.info("[GateGrid] 网格策略启动, cycle: {}", currentCycle + 1); |
| | | dualOpenPositions(); |
| | | state = StrategyState.WAITING_KLINE; |
| | | cumulativePnl = BigDecimal.ZERO; |
| | | unrealizedPnl = BigDecimal.ZERO; |
| | | markPrice = 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(); |
| | | currentLongOrderIds.clear(); |
| | | currentShortOrderIds.clear(); |
| | | longEntryQty = 1; |
| | | shortEntryQty = 1; |
| | | |
| | | // 每次重启重新获取当前本金 |
| | | refreshInitialPrincipal(); |
| | | |
| | | log.info("[Gate] 网格策略已启动, 当前本金: {} USDT", initialPrincipal); |
| | | } |
| | | |
| | | /** |
| | | * 停止网格策略 |
| | | * 重新获取当前账户权益作为初始本金。 |
| | | */ |
| | | private void refreshInitialPrincipal() { |
| | | try { |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | this.initialPrincipal = new BigDecimal(account.getTotal()); |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 获取初始化本金失败,使用旧值: {}", initialPrincipal); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 停止网格策略。取消所有条件单 → 关闭交易线程池。 |
| | | * 状态设为 STOPPED,K 线回调将直接返回不再处理。 |
| | | */ |
| | | public void stopGrid() { |
| | | strategyActive = false; |
| | | cancelTpSl(longTpOrderId); |
| | | cancelTpSl(longSlOrderId); |
| | | cancelTpSl(shortTpOrderId); |
| | | cancelTpSl(shortSlOrderId); |
| | | closeAllPositions(); |
| | | log.info("[GateGrid] 网格策略已停止, 总盈亏: {}, 循环: {}", totalProfit, currentCycle); |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | executor.shutdown(); |
| | | log.info("[Gate] 策略已停止, 累计盈亏: {}", cumulativePnl); |
| | | } |
| | | |
| | | // ---- K线回调 ---- |
| | | |
| | | /** |
| | | * K线回调:收到新的收盘价 |
| | | * K 线回调入口。由 {@link CandlestickChannelHandler} 在收到 WebSocket K 线推送时调用。 |
| | | * |
| | | * <h3>处理流程</h3> |
| | | * <ol> |
| | | * <li>更新 lastKlinePrice → 计算 unrealizedPnl(浮动盈亏)</li> |
| | | * <li>STOPPED → 直接返回(仅保留盈亏更新)</li> |
| | | * <li>WAITING_KLINE → 切换为 OPENING → 异步提交基底双开(开多+开空)</li> |
| | | * <li>OPENING → 等待仓位推送回调生成队列,此处返回</li> |
| | | * <li>ACTIVE → 执行 processShortGrid + processLongGrid</li> |
| | | * </ol> |
| | | * |
| | | * <h3>注意</h3> |
| | | * 基底双开下单提交到 GateTradeExecutor 的独立线程池中异步执行, |
| | | * 成交状态由 onPositionUpdate 回调驱动,不阻塞 WS 回调线程。 |
| | | * |
| | | * @param closePrice K 线收盘价(即当前最新成交价) |
| | | */ |
| | | public void onKline(BigDecimal closePrice, boolean isKlineClosed) { |
| | | lastClosePrice = closePrice; |
| | | if (!strategyActive || !isKlineClosed) { |
| | | public void onKline(BigDecimal closePrice) { |
| | | lastKlinePrice = closePrice; |
| | | updateUnrealizedPnl(); |
| | | if (state == StrategyState.STOPPED) { |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | } catch (ApiException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | closeExistingPositions(); |
| | | |
| | | BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl); |
| | | log.info("[Gate] 已实现:{}, 未实现:{}, 合计:{}", |
| | | cumulativePnl, unrealizedPnl, totalPnl); |
| | | |
| | | startGrid(); |
| | | return; |
| | | } |
| | | checkPositions(closePrice); |
| | | |
| | | //初始化0位置的开仓,并且用空的开仓价格,作为价格基准来划分网格 |
| | | if (state == StrategyState.WAITING_KLINE) { |
| | | if (wsClient == null || !wsClient.areAllSubscribed()) { |
| | | return; |
| | | } |
| | | state = StrategyState.OPENING; |
| | | log.info("[Gate] 首根K线到达,开基底仓位 多空各{}张...", config.getBaseQuantity()); |
| | | executor.openLong(config.getBaseQuantity(), (orderId) -> { |
| | | TraderParam baseLongTp = TraderParam.builder() |
| | | .entryOrderId(orderId) |
| | | .build(); |
| | | config.setBaseLongTraderParam(baseLongTp); |
| | | }, null); |
| | | executor.openShort(negate(config.getBaseQuantity()), (orderId) -> { |
| | | TraderParam baseShortTp = TraderParam.builder() |
| | | .entryOrderId(orderId) |
| | | .build(); |
| | | config.setBaseShortTraderParam(baseShortTp); |
| | | }, null); |
| | | |
| | | return; |
| | | } |
| | | |
| | | if (state != StrategyState.ACTIVE) { |
| | | return; |
| | | } |
| | | checkProfitAndReset(); |
| | | } |
| | | |
| | | // ---- 仓位推送回调 ---- |
| | | |
| | | /** |
| | | * 多空双开 |
| | | * 仓位推送回调。由 {@link PositionsChannelHandler} 在收到 WebSocket 仓位更新时调用。 |
| | | * |
| | | * <h3>处理逻辑</h3> |
| | | * <ul> |
| | | * <li><b>有仓位 (size ≠ 0)</b>: |
| | | * <ul> |
| | | * <li>首次开仓(基底):标记 baseOpened=true,记录基底入场价,双基底都成交后生成网格队列</li> |
| | | * <li>仓位净减少(size.abs() < 之前记录值):止盈平仓后 → 检查反向条件单条件 → |
| | | * 满足时以 entryPrice ± step 为止盈价挂反向市价单(订单ID + 止盈价存入 Map)</li> |
| | | * <li>仓位净增加或不变:仅更新 positionSize,止盈由 {@link #onOrderUpdate} 通过订单订阅匹配处理</li> |
| | | * </ul> |
| | | * </li> |
| | | * <li><b>无仓位 (size = 0)</b>:清空活跃标记和持仓量</li> |
| | | * <li><b>Map 截断</b>:currentLongOrderIds / currentShortOrderIds 超过 5 个时, |
| | | * 从 LinkedHashMap 头部删除最旧条目,保留最新 5 个</li> |
| | | * </ul> |
| | | * |
| | | * @param contract 合约名称 |
| | | * @param mode 持仓模式(DUAL_LONG / DUAL_SHORT) |
| | | * @param size 持仓张数(多头为正、空头为负) |
| | | * @param entryPrice 当前持仓加权均价(交易所计算) |
| | | */ |
| | | private void dualOpenPositions() { |
| | | try { |
| | | FuturesOrder longOrder = new FuturesOrder(); |
| | | longOrder.setContract(contract); |
| | | longOrder.setSize(quantity); |
| | | longOrder.setPrice("0"); |
| | | longOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | longOrder.setText("t-grid-long-" + (currentCycle + 1)); |
| | | FuturesOrder longResult = futuresApi.createFuturesOrder(SETTLE, longOrder, null); |
| | | longOrderId = longResult.getId(); |
| | | longEntryPrice = safeDecimal(longResult.getFillPrice()); |
| | | log.info("[GateGrid] 开多成功, price: {}, id: {}", longEntryPrice, longOrderId); |
| | | placeLongTpSl(longEntryPrice); |
| | | |
| | | FuturesOrder shortOrder = new FuturesOrder(); |
| | | shortOrder.setContract(contract); |
| | | shortOrder.setSize(negateQuantity(quantity)); |
| | | shortOrder.setPrice("0"); |
| | | shortOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | shortOrder.setText("t-grid-short-" + (currentCycle + 1)); |
| | | FuturesOrder shortResult = futuresApi.createFuturesOrder(SETTLE, shortOrder, null); |
| | | shortOrderId = shortResult.getId(); |
| | | shortEntryPrice = safeDecimal(shortResult.getFillPrice()); |
| | | log.info("[GateGrid] 开空成功, price: {}, id: {}", shortEntryPrice, shortOrderId); |
| | | placeShortTpSl(shortEntryPrice); |
| | | |
| | | printGridInfo(); |
| | | } catch (GateApiException e) { |
| | | log.error("[GateGrid] 双开失败, label: {}, msg: {}", e.getErrorLabel(), e.getMessage()); |
| | | strategyActive = false; |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 双开异常", e); |
| | | strategyActive = false; |
| | | } |
| | | } |
| | | |
| | | private void placeLongTpSl(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal slPrice = entryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | longTpOrderId = placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-long-position", "close_long"); |
| | | longSlOrderId = placePriceTriggeredOrder(slPrice, FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-long-position", "close_long"); |
| | | log.info("[GateGrid] 多头TP/SL已设置, TP:{}, SL:{}, tpId:{}, slId:{}", tpPrice, slPrice, longTpOrderId, longSlOrderId); |
| | | } |
| | | |
| | | private void placeShortTpSl(BigDecimal entryPrice) { |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal slPrice = entryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | shortTpOrderId = placePriceTriggeredOrder(tpPrice, FuturesPriceTrigger.RuleEnum.NUMBER_2, "close-short-position", "close_short"); |
| | | shortSlOrderId = placePriceTriggeredOrder(slPrice, FuturesPriceTrigger.RuleEnum.NUMBER_1, "close-short-position", "close_short"); |
| | | log.info("[GateGrid] 空头TP/SL已设置, TP:{}, SL:{}, tpId:{}, slId:{}", tpPrice, slPrice, shortTpOrderId, shortSlOrderId); |
| | | } |
| | | |
| | | private Long placePriceTriggeredOrder(BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String orderType, |
| | | String autoSize) { |
| | | try { |
| | | FuturesPriceTrigger trigger = new FuturesPriceTrigger(); |
| | | trigger.setStrategyType(FuturesPriceTrigger.StrategyTypeEnum.NUMBER_0); |
| | | trigger.setPriceType(FuturesPriceTrigger.PriceTypeEnum.NUMBER_0); |
| | | trigger.setPrice(triggerPrice.toString()); |
| | | trigger.setRule(rule); |
| | | trigger.setExpiration(0); |
| | | |
| | | FuturesInitialOrder initial = new FuturesInitialOrder(); |
| | | initial.setContract(contract); |
| | | initial.setSize(0L); |
| | | initial.setPrice("0"); |
| | | initial.setTif(FuturesInitialOrder.TifEnum.IOC); |
| | | initial.setReduceOnly(true); |
| | | initial.setAutoSize(autoSize); |
| | | |
| | | FuturesPriceTriggeredOrder order = new FuturesPriceTriggeredOrder(); |
| | | order.setTrigger(trigger); |
| | | order.setInitial(initial); |
| | | order.setOrderType(orderType); |
| | | |
| | | TriggerOrderResponse response = futuresApi.createPriceTriggeredOrder(SETTLE, order); |
| | | log.info("[GateGrid] 条件单已创建, triggerPrice:{}, rule:{}, orderType:{}, autoSize:{}, id:{}", |
| | | triggerPrice, rule, orderType, autoSize, response.getId()); |
| | | return response.getId(); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 创建条件单失败, triggerPrice:{}, rule:{}, orderType:{}, autoSize:{}", |
| | | triggerPrice, rule, orderType, autoSize, e); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private void cancelTpSl(Long orderId) { |
| | | if (orderId == null) { |
| | | public void onPositionUpdate(String contract, Position.ModeEnum mode, BigDecimal size, |
| | | BigDecimal entryPrice) { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrder(SETTLE, orderId); |
| | | log.info("[GateGrid] 已取消条件单, id:{}", orderId); |
| | | } catch (Exception e) { |
| | | log.warn("[GateGrid] 取消条件单失败, id:{}", orderId, e); |
| | | |
| | | boolean hasPosition = size.abs().compareTo(BigDecimal.ZERO) > 0; |
| | | |
| | | if (Position.ModeEnum.DUAL_LONG == mode) { |
| | | if (hasPosition) { |
| | | longActive = true; |
| | | longEntryPrice = entryPrice; |
| | | if (!baseLongOpened) { |
| | | longPositionSize = size; |
| | | longBaseEntryPrice = entryPrice; |
| | | baseLongOpened = true; |
| | | log.info("[Gate] 基底多成交价: {}", longBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | }else { |
| | | longPositionSize = size; |
| | | // checkShortEntryOrderToCancel(); |
| | | // checkLongEntryOrderToCancel(); |
| | | } |
| | | } else { |
| | | if (longActive && state == StrategyState.ACTIVE) { |
| | | log.info("[Gate] 多仓持仓归零,重置策略"); |
| | | handlePositionZeroAndReset("多仓"); |
| | | } |
| | | longActive = false; |
| | | longPositionSize = BigDecimal.ZERO; |
| | | } |
| | | } else if (Position.ModeEnum.DUAL_SHORT == mode) { |
| | | if (hasPosition) { |
| | | shortActive = true; |
| | | shortEntryPrice = entryPrice; |
| | | if (!baseShortOpened) { |
| | | shortPositionSize = size.abs(); |
| | | shortBaseEntryPrice = entryPrice; |
| | | baseShortOpened = true; |
| | | log.info("[Gate] 基底空成交价: {}", shortBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | }else { |
| | | shortPositionSize = size.abs(); |
| | | // checkShortEntryOrderToCancel(); |
| | | // checkLongEntryOrderToCancel(); |
| | | } |
| | | } else { |
| | | if (shortActive && state == StrategyState.ACTIVE) { |
| | | log.info("[Gate] 空仓持仓归零,重置策略"); |
| | | handlePositionZeroAndReset("空仓"); |
| | | } |
| | | shortActive = false; |
| | | shortPositionSize = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void checkShortEntryOrderToCancel() { |
| | | List<GridElement> allLongOrders = GridElement.findAllShortOrders(shortEntryPrice); |
| | | if (CollUtil.isNotEmpty(allLongOrders)){ |
| | | GridElement keep = allLongOrders.stream() |
| | | .min((a, b) -> a.getGridPrice().compareTo(b.getGridPrice())) |
| | | .orElse(null); |
| | | for (GridElement e : allLongOrders) { |
| | | if (e == keep) { |
| | | continue; |
| | | } |
| | | executor.cancelConditionalOrder( |
| | | e.getShortOrderId(), |
| | | orderId -> { |
| | | shortEntryTraderIdParam( |
| | | e, |
| | | null, |
| | | false |
| | | ); |
| | | } |
| | | ); |
| | | if (e.getShortTakeProfitOrderId() != null){ |
| | | executor.cancelConditionalOrder( |
| | | e.getShortTakeProfitOrderId(), |
| | | orderId -> { |
| | | shortTakeProfitTraderIdParam( |
| | | e, |
| | | null, |
| | | false |
| | | ); |
| | | } |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void checkLongEntryOrderToCancel() { |
| | | List<GridElement> allShortOrders = GridElement.findAllLongOrders(longEntryPrice); |
| | | if (CollUtil.isNotEmpty(allShortOrders)){ |
| | | GridElement keep = allShortOrders.stream() |
| | | .max((a, b) -> a.getGridPrice().compareTo(b.getGridPrice())) |
| | | .orElse(null); |
| | | for (GridElement e : allShortOrders) { |
| | | if (e == keep) { |
| | | continue; |
| | | } |
| | | executor.cancelConditionalOrder( |
| | | e.getLongOrderId(), |
| | | orderId -> { |
| | | longEntryTraderIdParam( |
| | | e, |
| | | null, |
| | | false |
| | | ); |
| | | } |
| | | ); |
| | | |
| | | if (e.getLongTakeProfitOrderId() != null){ |
| | | executor.cancelConditionalOrder( |
| | | e.getLongTakeProfitOrderId(), |
| | | orderId -> { |
| | | longTakeProfitTraderIdParam( |
| | | e, |
| | | null, |
| | | false |
| | | ); |
| | | } |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ---- 平仓推送回调 ---- |
| | | |
| | | /** |
| | | * 平仓推送回调。由 {@link PositionClosesChannelHandler} 在收到平仓推送时调用。 |
| | | * |
| | | * <h3>累加规则</h3> |
| | | * cumulativePnl += pnl。止盈平仓时 pnl > 0,止损平仓时 pnl < 0。 |
| | | * 累加后检查停止条件:≥ overallTp 或 ≤ -maxLoss。 |
| | | * |
| | | * @param contract 合约名称 |
| | | * @param side 平仓方向("long" / "short") |
| | | * @param pnl 本次平仓的盈亏金额 |
| | | */ |
| | | public void onPositionClose(String contract, String side, BigDecimal pnl) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | updateUnrealizedPnl(); |
| | | BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl); |
| | | log.info("[Gate] 已实现:{}, 未实现:{}, 合计:{}", |
| | | cumulativePnl, unrealizedPnl, totalPnl); |
| | | if(totalPnl.compareTo(config.getMaxLoss().negate()) <= 0) { |
| | | String logMessage = StrUtil.format("[Gate] 已达亏损风险值(合计{}), 已实现:{}, 未实现:{}", |
| | | totalPnl, cumulativePnl, unrealizedPnl); |
| | | log.info(logMessage); |
| | | |
| | | DingTalkUtils.getDefault().sendActionCard("风险提醒", logMessage, config.getApiKey(), ""); |
| | | } |
| | | } |
| | | |
| | | // ---- 订单推送回调 ---- |
| | | |
| | | /** |
| | | * 订单推送回调。由 OrdersChannelHandler 在收到订单更新推送时调用。 |
| | | * |
| | | * <h3>处理逻辑</h3> |
| | | * 当订单状态为 finished 且 finish_as 为 filled 时, |
| | | * 从 {@link #currentLongOrderIds} / {@link #currentShortOrderIds} 中匹配订单ID, |
| | | * 取出止盈价格并挂止盈单。匹配成功后从 Map 中移除该条目,防止重复挂单。 |
| | | * |
| | | * @param orderId 订单 ID |
| | | * @param status 订单状态(open / finished) |
| | | * @param finishAs 订单结束方式(filled / cancelled / ioc 等) |
| | | */ |
| | | public void onOrderUpdate(String orderId, String status, String finishAs) { |
| | | if (!"finished".equals(status) || !"filled".equals(finishAs)) { |
| | | return; |
| | | } |
| | | |
| | | /** |
| | | * 匹配止盈单止盈 |
| | | */ |
| | | GridElement byLongTakeProfitOrderId = GridElement.findByLongTakeProfitOrderId(orderId); |
| | | if (byLongTakeProfitOrderId != null){ |
| | | longTakeProfitTraderIdParam( |
| | | byLongTakeProfitOrderId, |
| | | null, |
| | | false |
| | | ); |
| | | // longEntryTraderIdParam( |
| | | // byLongTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | } |
| | | GridElement byShortTakeProfitOrderId = GridElement.findByShortTakeProfitOrderId(orderId); |
| | | if (byShortTakeProfitOrderId != null){ |
| | | shortTakeProfitTraderIdParam( |
| | | byShortTakeProfitOrderId, |
| | | null, |
| | | false |
| | | ); |
| | | // shortEntryTraderIdParam( |
| | | // byShortTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | } |
| | | |
| | | /** |
| | | * 匹配挂单 |
| | | */ |
| | | GridElement longGridElement = GridElement.findByLongOrderId(orderId); |
| | | if (longGridElement != null) { |
| | | if (longGridElement.isHasLongOrder()){ |
| | | longEntryTraderIdParam( |
| | | longGridElement, |
| | | null, |
| | | false |
| | | ); |
| | | if (longGridElement.getLongTakeProfitOrderId() == null){ |
| | | BigDecimal longTp = longGridElement.getLongTraderParam().getTakeProfitPrice(); |
| | | if (longTp != null) { |
| | | executor.placeTakeProfit(longTp, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | ORDER_TYPE_CLOSE_LONG, |
| | | negate(config.getQuantity()), |
| | | (profitId) -> { |
| | | longTakeProfitTraderIdParam( |
| | | longGridElement, |
| | | profitId, |
| | | true |
| | | ); |
| | | }); |
| | | log.info("[Gate] 多单成交匹配止盈, orderId:{}, 止盈价:{}, size:{}", orderId, longTp, negate(config.getQuantity())); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | GridElement shortGridElement = GridElement.findByShortOrderId(orderId); |
| | | if (shortGridElement != null) { |
| | | if (shortGridElement.isHasShortOrder()){ |
| | | shortEntryTraderIdParam( |
| | | shortGridElement, |
| | | null, |
| | | false |
| | | ); |
| | | if (shortGridElement.getShortTakeProfitOrderId() == null){ |
| | | BigDecimal shortTp = shortGridElement.getShortTraderParam().getTakeProfitPrice(); |
| | | if (shortTp != null) { |
| | | executor.placeTakeProfit(shortTp, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | ORDER_TYPE_CLOSE_SHORT, |
| | | config.getQuantity(), |
| | | (profitId) -> { |
| | | shortTakeProfitTraderIdParam( |
| | | shortGridElement, |
| | | profitId, |
| | | true |
| | | ); |
| | | }); |
| | | log.info("[Gate] 空单成交匹配止盈, orderId:{}, 止盈价:{}, size:{}", orderId, shortTp, config.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 检查多空仓位是否触及止盈止损 |
| | | * 用户私有成交回调。由 {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.UserTradesChannelHandler} |
| | | * 在收到 {@code futures.usertrades} 推送时调用。 |
| | | * |
| | | * @param contract 合约名称 |
| | | * @param orderId 订单 ID |
| | | * @param price 成交价格 |
| | | * @param size 成交数量 |
| | | * @param role 用户角色(maker / taker) |
| | | * @param fee 手续费 |
| | | */ |
| | | private void checkPositions(BigDecimal currentPrice) { |
| | | if (longEntryPrice == null || shortEntryPrice == null) { |
| | | public void onUserTrade(String contract, String orderId, BigDecimal price, String size, String role, BigDecimal fee) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | log.info("[Gate] 成交明细, 合约:{}, 订单ID:{}, 价格:{}, 数量:{}, 角色:{}, 手续费:{}", |
| | | contract, orderId, price, size, role, fee); |
| | | } |
| | | |
| | | /** |
| | | * 自动订单(条件单)状态变更回调。 |
| | | * 由 {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.AutoOrdersChannelHandler} |
| | | * 在收到 {@code futures.autoorders} 推送时调用。 |
| | | * |
| | | * @param orderId 条件单 ID |
| | | * @param status 订单状态(open / finished / cancelled) |
| | | * @param reason 变更原因 |
| | | * @param orderType 订单类型(plan-close-long-position 等) |
| | | */ |
| | | public void onAutoOrder(String orderId, String status, String reason, String orderType, String tradeId) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | log.info("[Gate] 条件单状态变更, id:{}, status:{}, reason:{}, order_type:{}", |
| | | orderId, status, reason, orderType); |
| | | if (!"finished".equals(status)) { |
| | | return; |
| | | } |
| | | |
| | | BigDecimal longTp = longEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal longSl = longEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal shortTp = shortEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal shortSl = shortEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | GridElement longStopLossElem = GridElement.findByLongStopLossOrderId(orderId); |
| | | if (longStopLossElem != null) { |
| | | handleLongStopLossTriggered(longStopLossElem); |
| | | return; |
| | | } |
| | | GridElement shortStopLossElem = GridElement.findByShortStopLossOrderId(orderId); |
| | | if (shortStopLossElem != null) { |
| | | handleShortStopLossTriggered(shortStopLossElem); |
| | | return; |
| | | } |
| | | |
| | | System.out.println("========== Gate 网格状态 =========="); |
| | | System.out.println("当前价格: " + currentPrice); |
| | | System.out.println("多头入场: " + longEntryPrice + " TP: " + longTp + " SL: " + longSl); |
| | | System.out.println("空头入场: " + shortEntryPrice + " TP: " + shortTp + " SL: " + shortSl); |
| | | System.out.println("累计盈亏: " + totalProfit + " 循环: " + currentCycle + "/" + maxCycles); |
| | | System.out.println("==================================="); |
| | | // GridElement byShortTakeProfitOrderId = GridElement.findByShortTakeProfitOrderId(orderId); |
| | | // if (byShortTakeProfitOrderId != null){ |
| | | // shortTakeProfitTraderIdParam( |
| | | // byShortTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | // shortEntryTraderIdParam( |
| | | // byShortTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | // TPonUserTradeShortEntry(byShortTakeProfitOrderId); |
| | | // } |
| | | // GridElement byLongTakeProfitOrderId = GridElement.findByLongTakeProfitOrderId(orderId); |
| | | // if (byLongTakeProfitOrderId != null){ |
| | | // longTakeProfitTraderIdParam( |
| | | // byLongTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | // longEntryTraderIdParam( |
| | | // byLongTakeProfitOrderId, |
| | | // null, |
| | | // false |
| | | // ); |
| | | // TPonUserTradeLongEntry(byLongTakeProfitOrderId); |
| | | // } |
| | | |
| | | // 多头止盈 |
| | | if (currentPrice.compareTo(longTp) >= 0) { |
| | | log.info("[GateGrid] 多头止盈触发! entry:{}, current:{}", longEntryPrice, currentPrice); |
| | | BigDecimal cnt = new BigDecimal(quantity); |
| | | BigDecimal profit = currentPrice.subtract(longEntryPrice).multiply(cnt); |
| | | totalProfit = totalProfit.add(profit); |
| | | log.info("[GateGrid] 多头止盈 profit:{}, totalProfit:{}", profit, totalProfit); |
| | | closeLongPosition(); |
| | | closeShortPosition(); |
| | | currentCycle++; |
| | | checkStopConditions(); |
| | | return; |
| | | GridElement shortGridElement = GridElement.findByShortOrderId(orderId); |
| | | if (shortGridElement != null) { |
| | | if (shortGridElement.isHasShortOrder() && !tradeId.equals("0")){ |
| | | int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity()); |
| | | shortEntryTraderIdParam(shortGridElement, null, false); |
| | | shortEntryQty = 1; |
| | | extendShortStopLoss(filledQty); |
| | | log.info("[Gate] 空单成交 gridId:{}, qty:{}, 追挂止损", shortGridElement.getId(), filledQty); |
| | | checkMaxPositionAndPlaceTakeProfit(false, shortGridElement.getId()); |
| | | } |
| | | } |
| | | // 多头止损 |
| | | if (currentPrice.compareTo(longSl) <= 0) { |
| | | log.info("[GateGrid] 多头止损触发! entry:{}, current:{}", longEntryPrice, currentPrice); |
| | | BigDecimal cnt = new BigDecimal(quantity); |
| | | BigDecimal loss = longEntryPrice.subtract(currentPrice).multiply(cnt); |
| | | totalProfit = totalProfit.subtract(loss); |
| | | log.info("[GateGrid] 多头止损 loss:{}, totalProfit:{}", loss, totalProfit); |
| | | closeLongPosition(); |
| | | currentCycle++; |
| | | checkStopConditions(); |
| | | return; |
| | | } |
| | | // 空头止盈 |
| | | if (currentPrice.compareTo(shortTp) <= 0) { |
| | | log.info("[GateGrid] 空头止盈触发! entry:{}, current:{}", shortEntryPrice, currentPrice); |
| | | BigDecimal cnt = new BigDecimal(quantity); |
| | | BigDecimal profit = shortEntryPrice.subtract(currentPrice).multiply(cnt); |
| | | totalProfit = totalProfit.add(profit); |
| | | log.info("[GateGrid] 空头止盈 profit:{}, totalProfit:{}", profit, totalProfit); |
| | | closeShortPosition(); |
| | | closeLongPosition(); |
| | | currentCycle++; |
| | | checkStopConditions(); |
| | | return; |
| | | } |
| | | // 空头止损 |
| | | if (currentPrice.compareTo(shortSl) >= 0) { |
| | | log.info("[GateGrid] 空头止损触发! entry:{}, current:{}", shortEntryPrice, currentPrice); |
| | | BigDecimal cnt = new BigDecimal(quantity); |
| | | BigDecimal loss = currentPrice.subtract(shortEntryPrice).multiply(cnt); |
| | | totalProfit = totalProfit.subtract(loss); |
| | | log.info("[GateGrid] 空头止损 loss:{}, totalProfit:{}", loss, totalProfit); |
| | | closeShortPosition(); |
| | | currentCycle++; |
| | | checkStopConditions(); |
| | | GridElement longGridElement = GridElement.findByLongOrderId(orderId); |
| | | if (longGridElement != null) { |
| | | if (longGridElement.isHasLongOrder() && !tradeId.equals("0")){ |
| | | int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity()); |
| | | longEntryTraderIdParam(longGridElement, null, false); |
| | | longEntryQty = 1; |
| | | extendLongStopLoss(filledQty); |
| | | log.info("[Gate] 多单成交 gridId:{}, qty:{}, 追挂止损", longGridElement.getId(), filledQty); |
| | | checkMaxPositionAndPlaceTakeProfit(true, longGridElement.getId()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void checkStopConditions() { |
| | | if (totalProfit.compareTo(overallTp) >= 0) { |
| | | log.info("[GateGrid] 达到整体止盈 {} USDT,停止策略", overallTp); |
| | | strategyActive = false; |
| | | return; |
| | | private void TPonUserTradeShortEntry(GridElement gridElement) { |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | // 判断网格是否能开多仓,如果不能则跳过 |
| | | GridElement upGridElement = GridElement.findById(gridElement.getUpId()); |
| | | if (upGridElement != null){ |
| | | BigDecimal upGridPrice = upGridElement.getGridPrice(); |
| | | TraderParam upLongTraderParam = upGridElement.getLongTraderParam(); |
| | | if ( |
| | | !upGridElement.isHasLongOrder() && |
| | | upGridPrice.compareTo(longEntryPrice) <= 0 |
| | | ){ |
| | | placeEntryOrderWithPreFlag(upGridElement, true, |
| | | upLongTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | upLongTraderParam.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | if (BigDecimal.ZERO.subtract(totalProfit).compareTo(maxLoss) >= 0) { |
| | | log.info("[GateGrid] 亏损 {} 达到上限 {} USDT,停止策略", totalProfit.negate(), maxLoss); |
| | | strategyActive = false; |
| | | return; |
| | | } |
| | | if (currentCycle >= maxCycles) { |
| | | log.info("[GateGrid] 达到最大循环次数 {},停止策略", maxCycles); |
| | | strategyActive = false; |
| | | return; |
| | | } |
| | | log.info("[GateGrid] 进入下一轮循环: {}", currentCycle + 1); |
| | | dualOpenPositions(); |
| | | } |
| | | |
| | | private void closeLongPosition() { |
| | | if (longEntryPrice == null) { |
| | | private void TPonUserTradeLongEntry(GridElement gridElement) { |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | // 判断网格是否能开空仓,如果不能则跳过 |
| | | GridElement downGridElement = GridElement.findById(gridElement.getDownId()); |
| | | if (downGridElement != null){ |
| | | |
| | | BigDecimal downGridPrice = downGridElement.getGridPrice(); |
| | | |
| | | TraderParam shortTraderParam = downGridElement.getShortTraderParam(); |
| | | if ( |
| | | !downGridElement.isHasShortOrder() && |
| | | downGridPrice.compareTo(shortEntryPrice) >= 0 |
| | | ){ |
| | | placeEntryOrderWithPreFlag(downGridElement, false, |
| | | shortTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | negate(config.getQuantity())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void onUserTradeShortEntry(GridElement gridElement) { |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | //下一个开仓位置 |
| | | GridElement UpGridElement = GridElement.findById(gridElement.getDownId()); |
| | | BigDecimal newLongFirst = UpGridElement.getGridPrice(); |
| | | |
| | | // 判断网格是否能开空仓,如果不能则跳过 |
| | | if (UpGridElement != null) { |
| | | |
| | | if (!UpGridElement.isHasShortOrder() && shortEntryPrice.compareTo(newLongFirst) > 0) { |
| | | |
| | | TraderParam upShortTraderParam = UpGridElement.getShortTraderParam(); |
| | | placeEntryOrderWithPreFlag(UpGridElement, false, |
| | | upShortTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | negate(upShortTraderParam.getQuantity())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void onUserTradeLongEntry(GridElement gridElement) { |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | //下一个开仓位置 |
| | | GridElement UpGridElement = GridElement.findById(gridElement.getUpId()); |
| | | BigDecimal newLongFirst = UpGridElement.getGridPrice() ; |
| | | |
| | | // 判断网格是否能开多仓,如果不能则跳过 |
| | | if (UpGridElement != null) { |
| | | |
| | | if (!UpGridElement.isHasLongOrder() && longEntryPrice.compareTo(newLongFirst) < 0) { |
| | | TraderParam upLongTraderParam = UpGridElement.getLongTraderParam(); |
| | | placeEntryOrderWithPreFlag(UpGridElement, true, |
| | | upLongTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | config.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ---- 网格队列处理 ---- |
| | | |
| | | /** |
| | | * 尝试生成网格队列。双基底(多+空)都成交后才触发: |
| | | * <ol> |
| | | * <li>生成空仓价格队列(降序)和多仓价格队列(升序)</li> |
| | | * <li>挂初始多仓条件单(触发价 = 多仓队列首元素,rule=NUMBER_1 ≥触发价时开多), |
| | | * 止盈价 = 触发价 + step,通过 onSuccess 回调将 orderId → 止盈价存入 currentLongOrderIds</li> |
| | | * <li>挂初始空仓条件单(触发价 = 空仓队列首元素,rule=NUMBER_2 ≤触发价时开空), |
| | | * 止盈价 = 触发价 − step,通过 onSuccess 回调将 orderId → 止盈价存入 currentShortOrderIds</li> |
| | | * <li>状态切换为 ACTIVE</li> |
| | | * </ol> |
| | | * 条件单成交后由 {@link #onOrderUpdate} 匹配止盈价并挂止盈条件单。 |
| | | */ |
| | | private void tryGenerateQueues() { |
| | | if (baseLongOpened && baseShortOpened) { |
| | | generateShortQueue(); |
| | | generateLongQueue(); |
| | | updateGridElements(); |
| | | |
| | | GridElement baseGridElement = GridElement.findById(0); |
| | | TraderParam baseLongTraderParam = config.getBaseLongTraderParam(); |
| | | baseGridElement.setLongOrderId(baseLongTraderParam.getEntryOrderId()); |
| | | baseGridElement.setHasLongOrder(true); |
| | | TraderParam baseShortTraderParam = config.getBaseShortTraderParam(); |
| | | baseGridElement.setShortOrderId(baseShortTraderParam.getEntryOrderId()); |
| | | baseGridElement.setHasShortOrder(true); |
| | | |
| | | for (int id = 2; id <= 11; id++) { |
| | | GridElement elem = GridElement.findById(id); |
| | | if (elem == null) { |
| | | continue; |
| | | } |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalId = id; |
| | | executor.placeTakeProfit( |
| | | triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | ORDER_TYPE_CLOSE_SHORT, |
| | | config.getQuantity(), |
| | | profitId -> { |
| | | elem.setShortStopLossOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | log.info("[Gate] 空仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | for (int id = -2; id >= -11; id--) { |
| | | GridElement elem = GridElement.findById(id); |
| | | if (elem == null) { |
| | | continue; |
| | | } |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalId = id; |
| | | executor.placeTakeProfit( |
| | | triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | ORDER_TYPE_CLOSE_LONG, |
| | | negate(config.getQuantity()), |
| | | profitId -> { |
| | | elem.setLongStopLossOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | log.info("[Gate] 多仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | log.info("[Gate] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11"); |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 更新基座止盈信息,将止盈价、订单ID等写入 TraderParam 并回填到 ID=0 的网格元素中。 |
| | | */ |
| | | private void longTakeProfitTraderIdParam( |
| | | GridElement baseElement,String profitId, boolean flag |
| | | ) { |
| | | TraderParam tp = baseElement.getLongTraderParam(); |
| | | tp.setTakeProfitOrderId(profitId); |
| | | tp.setTakeProfitPlaced(flag); |
| | | baseElement.setLongTakeProfitOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | } |
| | | private void shortTakeProfitTraderIdParam( |
| | | GridElement baseElement,String profitId, boolean flag |
| | | ) { |
| | | TraderParam tp = baseElement.getShortTraderParam(); |
| | | tp.setTakeProfitOrderId(profitId); |
| | | tp.setTakeProfitPlaced(flag); |
| | | baseElement.setShortTakeProfitOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void longEntryTraderIdParam( |
| | | GridElement baseElement,String entryId,boolean flag |
| | | ) { |
| | | TraderParam tp = baseElement.getLongTraderParam(); |
| | | tp.setEntryOrderId(entryId); |
| | | tp.setEntryOrderPlaced(flag); |
| | | baseElement.setHasLongOrder(flag); |
| | | baseElement.setLongOrderId(entryId); |
| | | GridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void shortEntryTraderIdParam( |
| | | GridElement baseElement, String entryId, boolean flag |
| | | ) { |
| | | TraderParam tp = baseElement.getShortTraderParam(); |
| | | tp.setEntryOrderId(entryId); |
| | | tp.setEntryOrderPlaced(flag); |
| | | baseElement.setHasShortOrder(flag); |
| | | baseElement.setShortOrderId(entryId); |
| | | GridElement.refreshIndices(); |
| | | } |
| | | |
| | | /** |
| | | * 生成空仓价格队列。 |
| | | * 以 shortBaseEntryPrice × gridRate 作为绝对步长 step,存到 config。 |
| | | * 第1个元素 = shortBaseEntryPrice − step,后续依次递减 step,共 gridQueueSize 个。 |
| | | * 队列降序排列(大→小),方便 processShortGrid 中从头遍历。 |
| | | */ |
| | | private void generateShortQueue() { |
| | | shortPriceQueue.clear(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP); |
| | | config.setStep(step); |
| | | BigDecimal elem = shortBaseEntryPrice.subtract(step).setScale(prec, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | shortPriceQueue.add(elem); |
| | | elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP); |
| | | if (elem.compareTo(BigDecimal.ZERO) <= 0) { |
| | | break; |
| | | } |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[Gate] 空队列:{}", shortPriceQueue); |
| | | } |
| | | |
| | | /** |
| | | * 生成多仓价格队列。 |
| | | * 以 shortBaseEntryPrice + step 为首元素,后续依次递增 step,共 gridQueueSize 个。 |
| | | * 队列升序排列(小→大),方便 processLongGrid 中从头遍历。 |
| | | */ |
| | | private void generateLongQueue() { |
| | | longPriceQueue.clear(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = config.getStep(); |
| | | BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | longPriceQueue.add(elem); |
| | | elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[Gate] 多队列:{}", longPriceQueue); |
| | | } |
| | | |
| | | /** |
| | | * 根据当前多空价格队列同步构建网格元素列表,写入 config。 |
| | | * |
| | | * <h3>ID 分配规则</h3> |
| | | * <ul> |
| | | * <li>空仓队列:id 从 -1 自减(-1, -2, -3...),第一个元素 upId=0,最后一个 downId=null</li> |
| | | * <li>位置 0:gridPrice=shortBaseEntryPrice,upId=-1,downId=1,其数据在基座开仓时更新</li> |
| | | * <li>多仓队列:id 从 1 自增(1, 2, 3...),第一个元素 upId=0,最后一个 downId=null</li> |
| | | * </ul> |
| | | * |
| | | * <h3>链表关系</h3> |
| | | * 所有元素通过 upId/downId 串成一条双向链表: |
| | | * ... → -3 → -2 → -1 → 0 → 1 → 2 → 3 → ... |
| | | */ |
| | | private void updateGridElements() { |
| | | List<GridElement> elements = new ArrayList<>(); |
| | | int shortSize = shortPriceQueue.size(); |
| | | int longSize = longPriceQueue.size(); |
| | | //根据精度转换成小数 |
| | | int prec = config.getPriceScale(); |
| | | // BigDecimal minTick = BigDecimal.ONE.scaleByPowerOfTen(-prec); |
| | | // BigDecimal step = config.getStep().subtract(minTick); |
| | | BigDecimal step = config.getStep(); |
| | | String qty = config.getQuantity(); |
| | | |
| | | // 空仓队列:id 从 -1 自减, shortPriceQueue[i] → id=-(i+1) |
| | | for (int i = 0; i < shortSize; i++) { |
| | | int id = -(i + 1); |
| | | Integer upId = (i == 0) ? 0 : id + 1; |
| | | Integer downId = (i == shortSize - 1) ? null : id - 1; |
| | | BigDecimal price = shortPriceQueue.get(i); |
| | | TraderParam longParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.LONG) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | TraderParam shortParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.SHORT) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | elements.add(GridElement.builder() |
| | | .id(id) |
| | | .gridPrice(price) |
| | | .upId(upId) |
| | | .downId(downId) |
| | | .longTraderParam(longParam) |
| | | .shortTraderParam(shortParam) |
| | | .build()); |
| | | } |
| | | |
| | | // 位置 0:基底价格,数据在基座开仓时更新 |
| | | { |
| | | BigDecimal price = shortBaseEntryPrice; |
| | | TraderParam longParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.LONG) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | TraderParam shortParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.SHORT) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | elements.add(GridElement.builder() |
| | | .id(0) |
| | | .gridPrice(price) |
| | | .upId(shortSize > 0 ? 1 : null) |
| | | .downId(longSize > 0 ? -1 : null) |
| | | .longTraderParam(longParam) |
| | | .shortTraderParam(shortParam) |
| | | .build()); |
| | | } |
| | | |
| | | // 多仓队列:id 从 1 自增, longPriceQueue[i] → id=i+1 |
| | | for (int i = 0; i < longSize; i++) { |
| | | int id = i + 1; |
| | | Integer downId = (i == 0) ? 0 : id - 1; |
| | | Integer upId = (i == longSize - 1) ? null : id + 1; |
| | | BigDecimal price = longPriceQueue.get(i); |
| | | TraderParam longParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.LONG) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | TraderParam shortParam = TraderParam.builder() |
| | | .direction(TraderParam.Direction.SHORT) |
| | | .entryPrice(price) |
| | | .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)) |
| | | .quantity(qty) |
| | | .build(); |
| | | elements.add(GridElement.builder() |
| | | .id(id) |
| | | .gridPrice(price) |
| | | .upId(upId) |
| | | .downId(downId) |
| | | .longTraderParam(longParam) |
| | | .shortTraderParam(shortParam) |
| | | .build()); |
| | | } |
| | | |
| | | config.setGridElements(elements); |
| | | log.info("[Gate] 网格元素列表已构建, 共{}个元素 (空仓:{} 位置:0 多仓:{})", elements.size(), shortSize, longSize); |
| | | } |
| | | |
| | | private void processShortGrid(BigDecimal currentPrice) { |
| | | int prec = config.getPriceScale(); |
| | | List<BigDecimal> matched = new ArrayList<>(); |
| | | synchronized (shortPriceQueue) { |
| | | for (BigDecimal p : shortPriceQueue) { |
| | | if (p.compareTo(currentPrice) >= 0) { |
| | | matched.add(p); |
| | | } else { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | if (matched.isEmpty()) { |
| | | return; |
| | | } |
| | | log.info("[Gate] 空仓队列触发, 匹配{}个元素, 当前价:{}", matched.size(), currentPrice); |
| | | |
| | | synchronized (shortPriceQueue) { |
| | | shortPriceQueue.removeAll(matched); |
| | | BigDecimal min = shortPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : shortPriceQueue.get(shortPriceQueue.size() - 1); |
| | | BigDecimal gridStep = config.getStep(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | min = min.subtract(gridStep).setScale(prec, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(min); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | } |
| | | |
| | | synchronized (longPriceQueue) { |
| | | BigDecimal first = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(0); |
| | | BigDecimal gridStep = config.getStep(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.subtract(gridStep.multiply(BigDecimal.valueOf(i))).setScale(prec, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(elem); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | while (longPriceQueue.size() > config.getGridQueueSize()) { |
| | | longPriceQueue.remove(longPriceQueue.size() - 1); |
| | | } |
| | | } |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | |
| | | /** |
| | | * 下一个开仓位置 |
| | | * 获取队列第一个元素的价格对应的网格 |
| | | * 判断网格是否能开空仓,如果不能则跳过 |
| | | * 前进方向挂空仓条件单 |
| | | * 后置方向挂多空条件单 |
| | | */ |
| | | //下一个开仓位置 |
| | | BigDecimal newLongFirst = shortPriceQueue.get(0); |
| | | GridElement UpGridElement = GridElement.findByPrice(newLongFirst); |
| | | |
| | | // 判断网格是否能开空仓,如果不能则跳过 |
| | | if (UpGridElement != null) { |
| | | |
| | | // if (!UpGridElement.isHasShortOrder() && shortEntryPrice.compareTo(newLongFirst) > 0) { |
| | | // |
| | | // TraderParam upShortTraderParam = UpGridElement.getShortTraderParam(); |
| | | // placeEntryOrderWithPreFlag(UpGridElement, false, |
| | | // upShortTraderParam.getEntryPrice(), |
| | | // FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | // negate(upShortTraderParam.getQuantity())); |
| | | // } |
| | | int i = UpGridElement.getId() + 2; |
| | | GridElement downGridElement = GridElement.findById(i); |
| | | if (downGridElement != null){ |
| | | |
| | | BigDecimal downGridPrice = downGridElement.getGridPrice(); |
| | | |
| | | // TraderParam downShortTraderParam = downGridElement.getShortTraderParam(); |
| | | // if ( |
| | | // !downGridElement.isHasShortOrder() && |
| | | // downGridPrice.compareTo(longEntryPrice) <= 0 && |
| | | // downGridPrice.compareTo(shortEntryPrice) >= 0 |
| | | // ){ |
| | | // placeEntryOrderWithPreFlag(downGridElement, false, |
| | | // downShortTraderParam.getEntryPrice(), |
| | | // FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | // negate(downShortTraderParam.getQuantity())); |
| | | // |
| | | // } |
| | | |
| | | TraderParam downLongTraderParam = downGridElement.getLongTraderParam(); |
| | | if ( |
| | | !downGridElement.isHasLongOrder() && |
| | | downGridPrice.compareTo(longEntryPrice) <= 0 |
| | | ){ |
| | | placeEntryOrderWithPreFlag(downGridElement, true, |
| | | downLongTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | downLongTraderParam.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void processLongGrid(BigDecimal currentPrice) { |
| | | int prec = config.getPriceScale(); |
| | | List<BigDecimal> matched = new ArrayList<>(); |
| | | synchronized (longPriceQueue) { |
| | | for (BigDecimal p : longPriceQueue) { |
| | | if (p.compareTo(currentPrice) <= 0) { |
| | | matched.add(p); |
| | | } else { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | if (matched.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | log.info("[Gate] 多仓队列触发, 匹配{}个元素, 当前价:{}", matched.size(), currentPrice); |
| | | |
| | | /** |
| | | * 匹配到元素后, |
| | | * 多仓队列更新 |
| | | * 空仓队列更新 |
| | | */ |
| | | synchronized (longPriceQueue) { |
| | | longPriceQueue.removeAll(matched); |
| | | BigDecimal max = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(longPriceQueue.size() - 1); |
| | | BigDecimal gridStep = config.getStep(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | max = max.add(gridStep).setScale(prec, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(max); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | } |
| | | synchronized (shortPriceQueue) { |
| | | BigDecimal first = shortPriceQueue.isEmpty() ? matched.get(0) : shortPriceQueue.get(0); |
| | | BigDecimal gridStep = config.getStep(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.add(gridStep.multiply(BigDecimal.valueOf(i))).setScale(prec, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(elem); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | while (shortPriceQueue.size() > config.getGridQueueSize()) { |
| | | shortPriceQueue.remove(shortPriceQueue.size() - 1); |
| | | } |
| | | } |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[Gate] 保证金超限,跳过挂条件单"); |
| | | } else { |
| | | |
| | | /** |
| | | * 下一个开仓位置 |
| | | * 获取队列第一个元素的价格对应的网格 |
| | | * 判断网格是否能开多仓,如果不能则跳过 |
| | | * 前进方向挂多仓条件单 |
| | | * 后置方向挂多空条件单 |
| | | */ |
| | | //下一个开仓位置 |
| | | BigDecimal newLongFirst = longPriceQueue.get(0); |
| | | GridElement UpGridElement = GridElement.findByPrice(newLongFirst); |
| | | |
| | | // 判断网格是否能开多仓,如果不能则跳过 |
| | | if (UpGridElement != null) { |
| | | |
| | | // if (!UpGridElement.isHasLongOrder() && longEntryPrice.compareTo(newLongFirst) < 0) { |
| | | // TraderParam upLongTraderParam = UpGridElement.getLongTraderParam(); |
| | | // placeEntryOrderWithPreFlag(UpGridElement, true, |
| | | // upLongTraderParam.getEntryPrice(), |
| | | // FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | // config.getQuantity()); |
| | | // } |
| | | |
| | | int i = UpGridElement.getId() - 2; |
| | | GridElement downGridElement = GridElement.findById(i); |
| | | if (downGridElement != null){ |
| | | |
| | | BigDecimal downGridPrice = downGridElement.getGridPrice(); |
| | | |
| | | // TraderParam downLongTraderParam = downGridElement.getLongTraderParam(); |
| | | // if ( |
| | | // !downGridElement.isHasLongOrder() && |
| | | // downGridPrice.compareTo(shortEntryPrice) >= 0 && |
| | | // downGridPrice.compareTo(longEntryPrice) <= 0 |
| | | // ){ |
| | | // placeEntryOrderWithPreFlag(downGridElement, true, |
| | | // downLongTraderParam.getEntryPrice(), |
| | | // FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | // config.getQuantity()); |
| | | // |
| | | // } |
| | | |
| | | TraderParam shortTraderParam = downGridElement.getShortTraderParam(); |
| | | if ( |
| | | !downGridElement.isHasShortOrder() && |
| | | downGridPrice.compareTo(shortEntryPrice) >= 0 |
| | | ){ |
| | | |
| | | placeEntryOrderWithPreFlag(downGridElement, false, |
| | | shortTraderParam.getEntryPrice(), |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | negate(config.getQuantity())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void handleLongStopLossTriggered(GridElement gridElement) { |
| | | int gridId = gridElement.getId(); |
| | | int N = Math.abs(gridId); |
| | | gridElement.setLongStopLossOrderId(null); |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 开始追单", gridId); |
| | | |
| | | int newEntryGridId = -(N - 1); |
| | | |
| | | GridElement newEntryGrid = GridElement.findById(newEntryGridId); |
| | | if (newEntryGrid == null) { |
| | | log.warn("[Gate] 多仓止损触发 but gridId:{} 不存在", newEntryGridId); |
| | | GridElement.refreshIndices(); |
| | | return; |
| | | } |
| | | |
| | | if (N > 2) { |
| | | int cancelGridId = -(N - 2); |
| | | GridElement cancelGrid = GridElement.findById(cancelGridId); |
| | | if (cancelGrid != null && cancelGrid.isHasLongOrder()) { |
| | | executor.cancelConditionalOrder(cancelGrid.getLongOrderId(), oid -> { |
| | | longEntryTraderIdParam(cancelGrid, null, false); |
| | | log.info("[Gate] 多仓止损触发, 取消gridId:{}的多单", cancelGridId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | longEntryQty++; |
| | | int entryQty = longEntryQty; |
| | | String size = new BigDecimal(String.valueOf(entryQty)).multiply(new BigDecimal(config.getQuantity())).toString(); |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 在gridId:{}挂{}基础张多单(计数器:{}, size:{})", |
| | | gridId, newEntryGridId, entryQty, longEntryQty, size); |
| | | newEntryGrid.getLongTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, size); |
| | | } |
| | | |
| | | private void handleShortStopLossTriggered(GridElement gridElement) { |
| | | int gridId = gridElement.getId(); |
| | | int N = gridId; |
| | | gridElement.setShortStopLossOrderId(null); |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 开始追单", gridId); |
| | | |
| | | int newEntryGridId = N - 1; |
| | | |
| | | GridElement newEntryGrid = GridElement.findById(newEntryGridId); |
| | | if (newEntryGrid == null) { |
| | | log.warn("[Gate] 空仓止损触发 but gridId:{} 不存在", newEntryGridId); |
| | | GridElement.refreshIndices(); |
| | | return; |
| | | } |
| | | |
| | | if (N > 2) { |
| | | int cancelGridId = N - 2; |
| | | GridElement cancelGrid = GridElement.findById(cancelGridId); |
| | | if (cancelGrid != null && cancelGrid.isHasShortOrder()) { |
| | | executor.cancelConditionalOrder(cancelGrid.getShortOrderId(), oid -> { |
| | | shortEntryTraderIdParam(cancelGrid, null, false); |
| | | log.info("[Gate] 空仓止损触发, 取消gridId:{}的空单", cancelGridId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | shortEntryQty++; |
| | | int entryQty = shortEntryQty; |
| | | String size = new BigDecimal(String.valueOf(entryQty)).multiply(new BigDecimal(config.getQuantity())).toString(); |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 在gridId:{}挂{}基础张空单(计数器:{}, size:{})", |
| | | gridId, newEntryGridId, entryQty, shortEntryQty, size); |
| | | newEntryGrid.getShortTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size)); |
| | | } |
| | | |
| | | private void extendLongStopLoss(int filledQty) { |
| | | int furthestSlId = 0; |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) { |
| | | furthestSlId = e.getId(); |
| | | } |
| | | } |
| | | if (furthestSlId == 0) { |
| | | furthestSlId = -11; |
| | | } |
| | | log.info("[Gate] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty); |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newSlId = furthestSlId - i - 1; |
| | | GridElement elem = GridElement.findById(newSlId); |
| | | if (elem == null) { |
| | | continue; |
| | | } |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalSlId = newSlId; |
| | | executor.placeTakeProfit( |
| | | triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | ORDER_TYPE_CLOSE_LONG, |
| | | negate(config.getQuantity()), |
| | | profitId -> { |
| | | elem.setLongStopLossOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | log.info("[Gate] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); |
| | | } |
| | | ); |
| | | } |
| | | } |
| | | |
| | | private void extendShortStopLoss(int filledQty) { |
| | | int furthestSlId = 0; |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) { |
| | | furthestSlId = e.getId(); |
| | | } |
| | | } |
| | | if (furthestSlId == 0) { |
| | | furthestSlId = 11; |
| | | } |
| | | log.info("[Gate] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty); |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newSlId = furthestSlId + i + 1; |
| | | GridElement elem = GridElement.findById(newSlId); |
| | | if (elem == null) { |
| | | continue; |
| | | } |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalSlId = newSlId; |
| | | executor.placeTakeProfit( |
| | | triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | ORDER_TYPE_CLOSE_SHORT, |
| | | config.getQuantity(), |
| | | profitId -> { |
| | | elem.setShortStopLossOrderId(profitId); |
| | | GridElement.refreshIndices(); |
| | | log.info("[Gate] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); |
| | | } |
| | | ); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 通过 REST API 查询实时持仓,超限则在下一网格挂止盈单。 |
| | | * 使用异步执行避免阻塞 WS 回调线程。 |
| | | * |
| | | * @param isLong 是否为多仓方向 |
| | | * @param gridId 当前挂单成交的网格 ID |
| | | */ |
| | | private void checkMaxPositionAndPlaceTakeProfit(boolean isLong, int gridId) { |
| | | int maxPos = config.getMaxPositionSize(); |
| | | if (maxPos <= 0) return; |
| | | |
| | | executor.submitTask(() -> { |
| | | try { |
| | | List<Position> positions = futuresApi.listPositions(SETTLE).execute(); |
| | | if (positions == null) return; |
| | | |
| | | long actualPosSize = 0; |
| | | String targetMode = isLong ? "dual_long" : "dual_short"; |
| | | for (Position pos : positions) { |
| | | if (!config.getContract().equals(pos.getContract())) continue; |
| | | Position.ModeEnum mode = pos.getMode(); |
| | | if (mode != null && mode.getValue() != null && targetMode.equals(mode.getValue())) { |
| | | actualPosSize = Math.abs(Long.parseLong(pos.getSize())); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (actualPosSize <= maxPos) { |
| | | log.debug("[Gate] 持仓检查 {}方向 实际持仓:{} ≤ 上限:{}, 无需止盈", targetMode, actualPosSize, maxPos); |
| | | return; |
| | | } |
| | | |
| | | // 下一网格 ID:空仓 N→N-1(向基底方向,更低价),多仓 -N→-(N-1)(向基底方向,更高价) |
| | | int nextGridId = isLong ? -(Math.abs(gridId) - 1) : gridId - 1; |
| | | GridElement nextGrid = GridElement.findById(nextGridId); |
| | | if (nextGrid == null) { |
| | | log.warn("[Gate] 持仓超限 but 下一网格{}不存在", nextGridId); |
| | | return; |
| | | } |
| | | |
| | | BigDecimal tpPrice = nextGrid.getGridPrice(); |
| | | final long finalPosSize = actualPosSize; |
| | | final int finalNextGridId = nextGridId; |
| | | if (isLong) { |
| | | executor.placeTakeProfit(tpPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, |
| | | ORDER_TYPE_CLOSE_LONG, |
| | | negate(config.getQuantity()), |
| | | profitId -> log.info("[Gate] 多仓超限止盈已挂(持仓:{})>, gridId:{}, tpPrice:{}, id:{}", |
| | | finalPosSize, finalNextGridId, tpPrice, profitId)); |
| | | } else { |
| | | executor.placeTakeProfit(tpPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, |
| | | ORDER_TYPE_CLOSE_SHORT, |
| | | config.getQuantity(), |
| | | profitId -> log.info("[Gate] 空仓超限止盈已挂(持仓:{})>, gridId:{}, tpPrice:{}, id:{}", |
| | | finalPosSize, finalNextGridId, tpPrice, profitId)); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 通过API查询持仓超限检查失败", e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** Gate 永续合约 taker 费率 0.05% */ |
| | | private static final BigDecimal TAKER_FEE_RATE = new BigDecimal("0.0005"); |
| | | /** ETH_USDT 合约面值(每张=0.01 ETH) */ |
| | | private static final BigDecimal CT_VAL = new BigDecimal("0.01"); |
| | | |
| | | private void checkProfitAndReset() { |
| | | try { |
| | | FuturesOrder closeOrder = new FuturesOrder(); |
| | | closeOrder.setContract(contract); |
| | | closeOrder.setSize(negateQuantity(quantity)); |
| | | closeOrder.setPrice("0"); |
| | | closeOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | closeOrder.setReduceOnly(true); |
| | | closeOrder.setText("t-grid-close-long"); |
| | | FuturesOrder result = futuresApi.createFuturesOrder(SETTLE, closeOrder, null); |
| | | log.info("[GateGrid] 平多成功, id: {}, fillPrice: {}", result.getId(), result.getFillPrice()); |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | BigDecimal unrealisedPnl = new BigDecimal(account.getCrossUnrealisedPnl()); |
| | | BigDecimal available = new BigDecimal(account.getCrossAvailable()); |
| | | BigDecimal totalEquity = unrealisedPnl.add(available); |
| | | |
| | | // 估算平仓手续费:(多仓张数+空仓张数) × 合约面值 × 当前价 × taker费率 |
| | | BigDecimal closeContractValue = longPositionSize.add(shortPositionSize) |
| | | .multiply(CT_VAL).multiply(lastKlinePrice != null ? lastKlinePrice : BigDecimal.ZERO); |
| | | BigDecimal estimatedFee = closeContractValue.multiply(TAKER_FEE_RATE); |
| | | BigDecimal netEquity = totalEquity.subtract(estimatedFee); |
| | | |
| | | BigDecimal target = initialPrincipal.add(config.getExpectedProfit()); |
| | | log.info("[Gate] 盈亏检查 upl:{}, avail:{}, 合计:{}, 估手续费:{}, 净权益:{}, 目标:{}", |
| | | unrealisedPnl, available, totalEquity, estimatedFee, netEquity, target); |
| | | if (netEquity.compareTo(target) > 0) { |
| | | log.info("[Gate] 盈亏达标(净权益{}>目标{}),重置策略", netEquity, target); |
| | | state = StrategyState.STOPPED; |
| | | closeExistingPositions(); |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | |
| | | // 提交到 executor 末尾:单线程FIFO保证前面所有平仓/取消任务完成后才重置 |
| | | executor.submitTask(() -> { |
| | | try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } |
| | | startGrid(); |
| | | }); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 平多失败", e); |
| | | log.warn("[Gate] 盈亏检查失败", e); |
| | | } |
| | | longEntryPrice = null; |
| | | longOrderId = null; |
| | | cancelTpSl(longTpOrderId); |
| | | cancelTpSl(longSlOrderId); |
| | | longTpOrderId = null; |
| | | longSlOrderId = null; |
| | | } |
| | | |
| | | private void closeShortPosition() { |
| | | if (shortEntryPrice == null) { |
| | | private void handlePositionZeroAndReset(String direction) { |
| | | state = StrategyState.STOPPED; |
| | | try { |
| | | futuresApi.cancelPriceTriggeredOrderList(SETTLE, config.getContract()); |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] {}持仓归零后取消条件单失败", direction, e); |
| | | } |
| | | closeExistingPositions(); |
| | | // 提交到 executor 末尾:单线程FIFO保证前面所有平仓/取消任务完成后才重置 |
| | | executor.submitTask(() -> { |
| | | try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } |
| | | startGrid(); |
| | | }); |
| | | } |
| | | |
| | | // ---- 保证金安全阀 ---- |
| | | |
| | | /** |
| | | * 保证金安全阀检查。 |
| | | * |
| | | * <p>实时查询当前保证金占用额(positionInitialMargin),计算其占初始本金的比例。 |
| | | * 比例 ≥ marginRatioLimit(默认 20%)时拒绝开仓,但仍照常更新队列。 |
| | | * |
| | | * <p>查询失败时默认放行(返回 true),避免因 REST API 异常导致策略完全停滞。 |
| | | * |
| | | * @return true=安全可开仓 / false=保证金超限跳过开仓 |
| | | */ |
| | | private boolean isMarginSafe() { |
| | | try { |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | BigDecimal margin = new BigDecimal(account.getPositionInitialMargin()); |
| | | BigDecimal ratio = margin.divide(initialPrincipal, 4, RoundingMode.HALF_UP); |
| | | log.debug("[Gate] 保证金比例: {}/{}={}", margin, initialPrincipal, ratio); |
| | | return ratio.compareTo(config.getMarginRatioLimit()) < 0; |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 查保证金失败,默认放行", e); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | // ---- 工具 ---- |
| | | |
| | | /** |
| | | * 取反字符串数字。如 "1" → "-1","-2" → "2"。 |
| | | * 用于开空单时将正数张数转为负数。 |
| | | */ |
| | | private String negate(String qty) { |
| | | return qty.startsWith("-") ? qty.substring(1) : "-" + qty; |
| | | } |
| | | |
| | | /** |
| | | * 预设标志位后提交条件开仓单,防止异步回调导致的竞态重复挂单。 |
| | | * |
| | | * <p>在调用 {@link GateTradeExecutor#placeConditionalEntryOrder} 之前同步设置 |
| | | * {@code isHasLongOrder / isHasShortOrder},关闭 WS 线程与 Executor 线程之间的 |
| | | * 检查-下单时间窗口。API 失败时自动回滚标志位。 |
| | | * |
| | | * @param gridElement 目标网格元素 |
| | | * @param isLong true=多仓下单,false=空仓下单 |
| | | * @param triggerPrice 触发价 |
| | | * @param rule 触发规则 |
| | | * @param size 开仓张数 |
| | | */ |
| | | private void placeEntryOrderWithPreFlag(GridElement gridElement, boolean isLong, |
| | | BigDecimal triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum rule, |
| | | String size) { |
| | | if (isLong) { |
| | | gridElement.setHasLongOrder(true); |
| | | } else { |
| | | gridElement.setHasShortOrder(true); |
| | | } |
| | | executor.placeConditionalEntryOrder(triggerPrice, rule, size, |
| | | orderId -> { |
| | | if (isLong) { |
| | | longEntryTraderIdParam(gridElement, orderId, true); |
| | | } else { |
| | | shortEntryTraderIdParam(gridElement, orderId, true); |
| | | } |
| | | }, |
| | | () -> { |
| | | if (isLong) { |
| | | gridElement.setHasLongOrder(false); |
| | | gridElement.setLongOrderId(null); |
| | | } else { |
| | | gridElement.setHasShortOrder(false); |
| | | gridElement.setShortOrderId(null); |
| | | } |
| | | GridElement.refreshIndices(); |
| | | log.warn("[Gate] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 根据持仓和当前价格计算未实现盈亏。 |
| | | * |
| | | * <h3>正向合约公式</h3> |
| | | * <pre> |
| | | * 多仓: 持仓量 × 合约乘数 × (计价价格 − 开仓均价) |
| | | * 空仓: 持仓量 × 合约乘数 × (开仓均价 − 计价价格) |
| | | * </pre> |
| | | * 计价价格由 {@link GateConfig.PnLPriceMode} 决定:LAST_PRICE 用最新成交价,MARK_PRICE 用标记价格。 |
| | | */ |
| | | private void updateUnrealizedPnl() { |
| | | BigDecimal price = resolvePnlPrice(); |
| | | if (price == null || price.compareTo(BigDecimal.ZERO) == 0) { |
| | | return; |
| | | } |
| | | try { |
| | | FuturesOrder closeOrder = new FuturesOrder(); |
| | | closeOrder.setContract(contract); |
| | | closeOrder.setSize(quantity); |
| | | closeOrder.setPrice("0"); |
| | | closeOrder.setTif(FuturesOrder.TifEnum.IOC); |
| | | closeOrder.setReduceOnly(true); |
| | | closeOrder.setText("t-grid-close-short"); |
| | | FuturesOrder result = futuresApi.createFuturesOrder(SETTLE, closeOrder, null); |
| | | log.info("[GateGrid] 平空成功, id: {}, fillPrice: {}", result.getId(), result.getFillPrice()); |
| | | } catch (Exception e) { |
| | | log.error("[GateGrid] 平空失败", e); |
| | | 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)); |
| | | } |
| | | shortEntryPrice = null; |
| | | shortOrderId = null; |
| | | cancelTpSl(shortTpOrderId); |
| | | cancelTpSl(shortSlOrderId); |
| | | shortTpOrderId = null; |
| | | shortSlOrderId = null; |
| | | } |
| | | |
| | | private void closeAllPositions() { |
| | | closeLongPosition(); |
| | | closeShortPosition(); |
| | | } |
| | | |
| | | private void printGridInfo() { |
| | | BigDecimal longTp = BigDecimal.ZERO; |
| | | BigDecimal longSl = BigDecimal.ZERO; |
| | | BigDecimal shortTp = BigDecimal.ZERO; |
| | | BigDecimal shortSl = BigDecimal.ZERO; |
| | | if (longEntryPrice != null) { |
| | | longTp = longEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | longSl = longEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) { |
| | | shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price)); |
| | | } |
| | | if (shortEntryPrice != null) { |
| | | shortTp = shortEntryPrice.multiply(BigDecimal.ONE.subtract(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | shortSl = shortEntryPrice.multiply(BigDecimal.ONE.add(gridRate)).setScale(1, RoundingMode.HALF_UP); |
| | | unrealizedPnl = longPnl.add(shortPnl); |
| | | |
| | | log.info("[Gate] 未实现盈亏: {}", unrealizedPnl); |
| | | } |
| | | |
| | | /** |
| | | * 根据配置的 PnLPriceMode 返回计价价格。 |
| | | * MARK_PRICE 模式优先使用标记价格(外部注入),未注入时回退到最新成交价。 |
| | | * |
| | | * @return 计价价格,可能为 null |
| | | */ |
| | | private BigDecimal resolvePnlPrice() { |
| | | if (config.getUnrealizedPnlPriceMode() == GateConfig.PnLPriceMode.MARK_PRICE |
| | | && markPrice.compareTo(BigDecimal.ZERO) > 0) { |
| | | return markPrice; |
| | | } |
| | | System.out.println("========== Gate 网格开仓 =========="); |
| | | System.out.println("合约: " + contract + " 杠杆: " + leverage + "x " + marginMode); |
| | | System.out.println("多头入场: " + longEntryPrice + " TP: " + longTp + " SL: " + longSl); |
| | | System.out.println("空头入场: " + shortEntryPrice + " TP: " + shortTp + " SL: " + shortSl); |
| | | System.out.println("数量: " + quantity + " 网格间距: " + gridRate.multiply(new BigDecimal("100")) + "%"); |
| | | System.out.println("整体止盈: " + overallTp + " USDT 最大循环: " + maxCycles); |
| | | System.out.println("最大亏损: " + maxLoss + " USDT"); |
| | | System.out.println("====================================="); |
| | | return lastKlinePrice; |
| | | } |
| | | |
| | | private String negateQuantity(String qty) { |
| | | if (qty.startsWith("-")) { |
| | | return qty.substring(1); |
| | | } |
| | | return "-" + qty; |
| | | } |
| | | |
| | | private BigDecimal safeDecimal(String val) { |
| | | if (val == null || val.isEmpty()) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return new BigDecimal(val); |
| | | } |
| | | |
| | | public BigDecimal getLastClosePrice() { |
| | | return lastClosePrice; |
| | | } |
| | | |
| | | public boolean isStrategyActive() { |
| | | return strategyActive; |
| | | } |
| | | |
| | | public BigDecimal getTotalProfit() { |
| | | return totalProfit; |
| | | } |
| | | |
| | | public int getCurrentCycle() { |
| | | return currentCycle; |
| | | } |
| | | /** @return 最新 K 线价格(每次 onKline 更新) */ |
| | | public BigDecimal getLastKlinePrice() { return lastKlinePrice; } |
| | | /** 设置标记价格(外部注入,MARK_PRICE 模式时用于盈亏计算) */ |
| | | public void setMarkPrice(BigDecimal markPrice) { this.markPrice = markPrice; } |
| | | /** @return 策略是否处于活跃状态(非 STOPPED 且非 WAITING_KLINE) */ |
| | | public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; } |
| | | /** @return 累计已实现盈亏(平仓推送驱动累加) */ |
| | | public BigDecimal getCumulativePnl() { return cumulativePnl; } |
| | | /** @return 当前未实现盈亏(每根 K 线实时计算) */ |
| | | public BigDecimal getUnrealizedPnl() { return unrealizedPnl; } |
| | | /** @return Gate 用户 ID(用于私有频道订阅 payload) */ |
| | | public Long getUserId() { return userId; } |
| | | /** @return 当前策略状态 */ |
| | | public StrategyState getState() { return state; } |
| | | /** 注入WS客户端,用于订阅状态检查 */ |
| | | public void setWsClient(GateKlineWebSocketClient wsClient) { this.wsClient = wsClient; } |
| | | } |