| | |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | private final GateConfig config; |
| | | private final StatsEventProducer statsProducer; |
| | | private String apiKeyMd5; |
| | | private final GateTradeExecutor executor; |
| | | private final FuturesApi futuresApi; |
| | | private static final String SETTLE = "usdt"; |
| | |
| | | private volatile BigDecimal shortPositionSize = BigDecimal.ZERO; |
| | | private Long userId; |
| | | private volatile BigDecimal initialPrincipal = BigDecimal.ZERO; |
| | | /** 上次 PNL 快照时间(毫秒),用于控制 PNL_SNAPSHOT 埋点频率 */ |
| | | private volatile long lastPnlSnapshotTime = 0; |
| | | private volatile GateKlineWebSocketClient wsClient; |
| | | |
| | | public GateGridTradeService(GateConfig config) { |
| | | public GateGridTradeService(GateConfig config, StatsEventProducer statsProducer) { |
| | | this.config = config; |
| | | this.statsProducer = statsProducer; |
| | | ApiClient apiClient = new ApiClient(); |
| | | apiClient.setBasePath(config.getRestBasePath()); |
| | | apiClient.setApiKeySecret(config.getApiKey(), config.getApiSecret()); |
| | |
| | | } |
| | | } |
| | | |
| | | // ---- 埋点 ---- |
| | | |
| | | private String apiKeyMd5() { |
| | | if (apiKeyMd5 == null) { |
| | | try { |
| | | java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); |
| | | byte[] digest = md.digest(config.getApiKey().getBytes(java.nio.charset.StandardCharsets.UTF_8)); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (byte b : digest) sb.append(String.format("%02x", b)); |
| | | apiKeyMd5 = sb.toString(); |
| | | } catch (Exception e) { |
| | | apiKeyMd5 = Integer.toHexString(config.getApiKey().hashCode()); |
| | | } |
| | | } |
| | | return apiKeyMd5; |
| | | } |
| | | |
| | | private void emitStats(String type, Object payload) { |
| | | if (statsProducer == null) return; |
| | | try { |
| | | statsProducer.sendStats(statsProducer.newStats(type, apiKeyMd5(), payload)); |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 埋点发送失败, type={}", type, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Java 8 兼容的 Map 构造工具(Map.of 为 Java 9 API,此处手动实现)。 |
| | | * 接受偶数个参数:key1, value1, key2, value2, ... |
| | | */ |
| | | private static Map<String, Object> mapOf(Object... kv) { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | for (int i = 0; i < kv.length; i += 2) { |
| | | m.put((String) kv[i], kv[i + 1]); |
| | | } |
| | | return m; |
| | | } |
| | | |
| | | // ---- 启动/停止 ---- |
| | | |
| | | /** |
| | |
| | | currentRound = 0; |
| | | |
| | | log.info("[Gate] 网格策略已启动, 当前本金: {} USDT", initialPrincipal); |
| | | |
| | | // 埋点: STRATEGY_START — 附全量配置快照 |
| | | GateConfigDTO snapshot = GateConfigDTO.from(config); |
| | | Map<String, Object> params = snapshot.toParamsMap(); |
| | | params.put("principal", initialPrincipal.toPlainString()); |
| | | params.put("contract", config.getContract()); |
| | | emitStats("STRATEGY_START", params); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public void stopGrid() { |
| | | state = StrategyState.STOPPED; |
| | | |
| | | // 埋点: STRATEGY_STOP |
| | | emitStats("STRATEGY_STOP", mapOf( |
| | | "reason", "manual", |
| | | "rounds", currentRound, |
| | | "pnl", cumulativePnl.toPlainString() |
| | | )); |
| | | |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | closeExistingPositions(); |
| | | executor.shutdown(); |
| | |
| | | .add(new BigDecimal(account.getUnrealisedPnl())) |
| | | .subtract(estimatedCloseFee); |
| | | |
| | | // 埋点: PNL_SNAPSHOT — 每60秒发射一次 |
| | | long now = System.currentTimeMillis(); |
| | | if (now - lastPnlSnapshotTime >= 60_000) { |
| | | lastPnlSnapshotTime = now; |
| | | BigDecimal total = new BigDecimal(account.getTotal()); |
| | | emitStats("PNL_SNAPSHOT", mapOf( |
| | | "cumulativePnl", cumulativePnl.toPlainString(), |
| | | "unrealizedPnl", new BigDecimal(account.getUnrealisedPnl()).toPlainString(), |
| | | "totalEquity", total.toPlainString(), |
| | | "markPrice", markPrice.toPlainString() |
| | | )); |
| | | } |
| | | |
| | | if (totalEquity.compareTo(target) > 0) { |
| | | currentRound++; |
| | | |
| | | // 埋点: ROUND_COMPLETE |
| | | emitStats("ROUND_COMPLETE", mapOf( |
| | | "roundNum", currentRound, |
| | | "totalEquity", totalEquity.toPlainString() |
| | | )); |
| | | |
| | | int maxRounds = config.getRounds(); |
| | | log.info("[Gate] 盈亏达标(净权益{}→含手续费-{}=实际{}>目标{}),第{}轮完成", |
| | | new BigDecimal(account.getTotal()).add(new BigDecimal(account.getUnrealisedPnl())), |
| | |
| | | int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity()); |
| | | shortEntryTraderIdParam(shortGridElement, orderId, false); |
| | | |
| | | // 埋点: ENTRY_FILLED — 空仓加仓成交 |
| | | emitStats("ENTRY_FILLED", mapOf( |
| | | "direction", "short", |
| | | "gridId", shortGridElement.getId(), |
| | | "filledQty", filledQty |
| | | )); |
| | | |
| | | // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend, |
| | | // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。 |
| | | if (shortGridElement.isExtendStopLossInProgress()) { |
| | |
| | | |
| | | int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity()); |
| | | longEntryTraderIdParam(longGridElement, orderId, false); |
| | | |
| | | // 埋点: ENTRY_FILLED — 多仓加仓成交 |
| | | emitStats("ENTRY_FILLED", mapOf( |
| | | "direction", "long", |
| | | "gridId", longGridElement.getId(), |
| | | "filledQty", filledQty |
| | | )); |
| | | |
| | | // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend, |
| | | // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。 |
| | |
| | | accumulatedLongLossCount++; |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单", |
| | | gridId, accumulatedLongLossCount, sameGrid ? "(同网格)" : ""); |
| | | |
| | | // 埋点: STOP_LOSS_TRIGGERED |
| | | emitStats("STOP_LOSS_TRIGGERED", mapOf( |
| | | "direction", "long", |
| | | "gridId", gridId, |
| | | "lossCount", accumulatedLongLossCount |
| | | )); |
| | | int newEntryGridId = gridId + 1; |
| | | |
| | | GridElement newEntryGrid = GridElement.findById(newEntryGridId); |
| | |
| | | accumulatedShortLossCount++; |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单", |
| | | gridId, accumulatedShortLossCount, sameGrid ? "(同网格)" : ""); |
| | | |
| | | // 埋点: STOP_LOSS_TRIGGERED |
| | | emitStats("STOP_LOSS_TRIGGERED", mapOf( |
| | | "direction", "short", |
| | | "gridId", gridId, |
| | | "lossCount", accumulatedShortLossCount |
| | | )); |
| | | int newEntryGridId = gridId - 1; |
| | | |
| | | GridElement newEntryGrid = GridElement.findById(newEntryGridId); |
| | |
| | | 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 int getCurrentRound() { return currentRound; } |
| | | /** @return 累计已实现盈亏(平仓推送驱动累加) */ |
| | | public BigDecimal getCumulativePnl() { return cumulativePnl; } |
| | | /** @return 初始本金 */ |
| | | public BigDecimal getInitialPrincipal() { return initialPrincipal; } |
| | | /** @return 当前未实现盈亏(每根 K 线实时计算) */ |
| | | public BigDecimal getUnrealizedPnl() { return unrealizedPnl; } |
| | | /** @return Gate 用户 ID(用于私有频道订阅 payload) */ |