| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
| 2026-06-05 | Administrator | ![]() |
pom.xml
@@ -6,65 +6,78 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> <relativePath/> </parent> <groupId>com.xcong</groupId> <artifactId>excoin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>excoin</name> <description>Demo project for Spring Boot</description> <description>OKX Grid Trading Strategy</description> <properties> <java.version>1.8</java.version> <mysql-driver.version>8.0.17</mysql-driver.version> <alibaba-druid.version>1.1.18</alibaba-druid.version> <mybatis.version>2.0.1</mybatis.version> <mybatis-plus.version>3.3.1.tmp</mybatis-plus.version> <validation-api.version>2.0.1.Final</validation-api.version> <hibernate-validator.version>6.1.0.Final</hibernate-validator.version> <swagger.version>2.9.2</swagger.version> <io-swagger.version>1.5.23</io-swagger.version> <mapstruct.version>1.3.1.Final</mapstruct.version> <hutool.version>5.3.1</hutool.version> <fastjson.version>1.2.61</fastjson.version> <netty.version>4.1.33.Final</netty.version> <dom4j.version>1.6.1</dom4j.version> <m2e.apt.activation>jdt_apt</m2e.apt.activation> <okhttp.version>3.6.0</okhttp.version> <aliyun-oss.version>3.8.0</aliyun-oss.version> </properties> <dependencies> <!-- Spring Boot Web (嵌入式Tomcat) --> <dependency> <groupId>ripple</groupId> <artifactId>ripple</artifactId> <version>0.0.1</version> <scope>system</scope> <systemPath>${basedir}/lib/ripple-core-0.0.1-SNAPSHOT.jar</systemPath> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- OKX REST API 用 OkHttp3 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <!-- OKX WebSocket 连接 --> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.5.3</version> </dependency> <!-- JSON 解析 (FastJSON) --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- JSON 解析 (org.json) --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20230618</version> </dependency> <!-- Hutool 工具库 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <!-- <version>3.6.0</version>--> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor --> <!-- Apache Commons Codec (钉钉Base64签名) --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>logging-interceptor</artifactId> <version>3.6.0</version> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> <!-- 钉钉机器人 SDK (本地Jar) --> <dependency> <groupId>taobao</groupId> <artifactId>taobao-sdk</artifactId> @@ -73,54 +86,7 @@ <systemPath>${basedir}/lib/taobao-sdk-java.jar</systemPath> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.security</groupId>--> <!-- <artifactId>spring-security-test</artifactId>--> <!-- <scope>test</scope>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> @@ -132,156 +98,6 @@ </exclusion> </exclusions> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${alibaba-druid.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-driver.version}</version> </dependency> <!-- 参数校验 start --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>${validation-api.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate-validator.version}</version> </dependency> <!-- 参数校验 end --> <!-- swagger2 start --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>${io-swagger.version}</version> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-models</artifactId> <version>${io-swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!-- swagger2 end --> <!-- bean映射转化 --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>${dom4j.version}</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.5.3</version> </dependency> <!-- submail邮件 start --> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>net.sf.ezmorph</groupId> <artifactId>ezmorph</artifactId> <version>1.0.3</version> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.2.3</version> <classifier>jdk15</classifier> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpmime</artifactId> <version>4.3.5</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- submail邮件 end --> </dependencies> <build> @@ -302,11 +118,6 @@ <source>${java.version}</source> <target>${java.version}</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> src/main/java/com/xcong/excoin/ExcoinApplication.java
@@ -1,18 +1,16 @@ package com.xcong.excoin; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * OKX 网格交易策略启动入口 * * @author helius */ @EnableScheduling @EnableSwagger2 @SpringBootApplication @MapperScan("com.xcong.excoin.modules.*.dao") public class ExcoinApplication { public static void main(String[] args) { src/main/java/com/xcong/excoin/common/annotations/SubmitRepeat.java
File was deleted src/main/java/com/xcong/excoin/common/annotations/UserAuth.java
File was deleted src/main/java/com/xcong/excoin/common/aop/SubmitRepeatAspect.java
File was deleted src/main/java/com/xcong/excoin/common/contants/AppContants.java
File was deleted src/main/java/com/xcong/excoin/common/enumerates/CoinTypeEnum.java
File was deleted src/main/java/com/xcong/excoin/common/enumerates/MemberWalletCoinEnum.java
File was deleted src/main/java/com/xcong/excoin/common/enumerates/OrderClosingTypeEnum.java
File was deleted src/main/java/com/xcong/excoin/common/enumerates/RabbitPriceTypeEnum.java
File was deleted src/main/java/com/xcong/excoin/common/enumerates/SymbolEnum.java
File was deleted src/main/java/com/xcong/excoin/common/exception/GlobalException.java
File was deleted src/main/java/com/xcong/excoin/common/response/Result.java
File was deleted src/main/java/com/xcong/excoin/common/response/ResultCode.java
File was deleted src/main/java/com/xcong/excoin/configurations/GlobalExceptionHandler.java
File was deleted src/main/java/com/xcong/excoin/configurations/WebMvcConfig.java
File was deleted src/main/java/com/xcong/excoin/configurations/i18n/CustomLocaleResolver.java
File was deleted src/main/java/com/xcong/excoin/configurations/i18n/LocaleResolverConfig.java
File was deleted src/main/java/com/xcong/excoin/configurations/properties/SecurityProperties.java
File was deleted src/main/java/com/xcong/excoin/configurations/security/CustomAccessDeniedHandler.java
File was deleted src/main/java/com/xcong/excoin/configurations/security/CustomAuthenticationEntryPoint.java
File was deleted src/main/java/com/xcong/excoin/configurations/security/WebSecurityConfig.java
File was deleted src/main/java/com/xcong/excoin/modules/okxNewPrice/GridQueueBuilder.java
New file @@ -0,0 +1,152 @@ package com.xcong.excoin.modules.okxNewPrice; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; /** * 网格价格队列构建器 — 从基底入场价生成双向价格队列和 OkxGridElement 列表。 * * <h3>职责</h3> * <ul> * <li>根据基底空头入场价,向下递减排空仓队列</li> * <li>向上递增排多仓队列</li> * <li>整合两队列为 OkxGridElement 列表并注入 {@link OkxConfig}</li> * </ul> * * <h3>线程安全</h3> * 本类为纯函数式(除 config.setStep / config.setGridElements 副作用外), * 每次调用返回新构建的对象,不持有可变状态。 * * @author Administrator */ @Slf4j public final class GridQueueBuilder { private GridQueueBuilder() { /* utility class */ } /** * 从基底入场价向下递减生成空仓价格队列,降序排列(大→小)。 * * @param config 全局配置(step 会被计算并写入) * @param basePrice 空仓基底入场价 * @return 空仓价格队列,按降序排列 */ public static List<BigDecimal> buildShortQueue(OkxConfig config, BigDecimal basePrice) { int prec = config.getPriceScale(); BigDecimal step = basePrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP); config.setStep(step); // 其它方法依赖此 step List<BigDecimal> queue = new ArrayList<>(); BigDecimal elem = basePrice.subtract(step).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { queue.add(elem); elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP); if (elem.compareTo(BigDecimal.ZERO) <= 0) break; } queue.sort((a, b) -> b.compareTo(a)); // 降序 log.info("[OKX] 空队列:{}", queue); return queue; } /** * 从基底入场价向上递增生成多仓价格队列,升序排列(小→大)。 * * @param config 全局配置(使用已设置的 step) * @param basePrice 空仓基底入场价(对齐 Gate 版本,多仓队列也以此为基准) * @return 多仓价格队列,按升序排列 */ public static List<BigDecimal> buildLongQueue(OkxConfig config, BigDecimal basePrice) { int prec = config.getPriceScale(); BigDecimal step = config.getStep(); List<BigDecimal> queue = new ArrayList<>(); BigDecimal elem = basePrice.add(step).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { queue.add(elem); elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP); } queue.sort(BigDecimal::compareTo); // 升序 log.info("[OKX] 多队列:{}", queue); return queue; } /** * 将空/多仓队列整合为 OkxGridElement 列表。 * <ul> * <li>空仓网格: id = -1, -2, -3 …</li> * <li>基底网格: id = 0(短仓入场价位置)</li> * <li>多仓网格: id = +1, +2, +3 …</li> * </ul> * 每个网格元素包含两个方向的 {@link OkxTraderParam}(入场价 / 止盈价 / 数量)。 * * @param config 全局配置 * @param shortQueue 空仓价格队列 * @param longQueue 多仓价格队列 * @param basePrice 空仓基底入场价 * @return 构建好的网格元素列表(已注入 config) */ public static List<OkxGridElement> buildGridElements(OkxConfig config, List<BigDecimal> shortQueue, List<BigDecimal> longQueue, BigDecimal basePrice) { List<OkxGridElement> elements = new ArrayList<>(); int shortSize = shortQueue.size(); int longSize = longQueue.size(); int prec = config.getPriceScale(); BigDecimal step = config.getStep(); String qty = config.getQuantity(); // ---- 空仓网格: id = -1, -2, … ---- 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 = shortQueue.get(i); OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } // ---- 基底网格: id = 0 ---- { BigDecimal price = basePrice; OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(0).gridPrice(price) .upId(shortSize > 0 ? 1 : null).downId(longSize > 0 ? -1 : null) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } // ---- 多仓网格: id = 1, 2, … ---- 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 = longQueue.get(i); OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } config.setGridElements(elements); log.info("[OKX] 网格元素列表已构建, 共{}个元素", elements.size()); return elements; } } src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridTradeService.java
@@ -57,6 +57,7 @@ private final OkxConfig config; private final OkxTradeExecutor executor; private final OKXAccount okxAccount; private final StopLossManager stopLossManager; private volatile StrategyState state = StrategyState.WAITING_KLINE; @@ -79,6 +80,16 @@ /** 基底空头是否已开 */ private volatile boolean baseShortOpened = false; // ---- WS 订阅就绪标志 ---- /** candle1m (Business WS) 订阅已确认 */ private volatile boolean candle1mSubscribed = false; /** positions (Private WS) 订阅已确认 */ private volatile boolean positionsSubscribed = false; /** orders (Private WS) 订阅已确认 */ private volatile boolean ordersSubscribed = false; /** 等待所有订阅就绪期间缓存的最新 K 线价格 */ private volatile BigDecimal pendingKlinePrice = null; private volatile boolean shortActive = false; private volatile boolean longActive = false; @@ -95,6 +106,7 @@ this.config = config; this.okxAccount = okxAccount; this.executor = new OkxTradeExecutor(okxAccount, config.getInstId(), config.getTdMode()); this.stopLossManager = new StopLossManager(config, executor); } // ---- 初始化 ---- @@ -195,10 +207,15 @@ baseShortOpened = false; longActive = false; shortActive = false; candle1mSubscribed = false; positionsSubscribed = false; ordersSubscribed = false; pendingKlinePrice = null; shortPriceQueue.clear(); longPriceQueue.clear(); currentLongOrderIds.clear(); currentShortOrderIds.clear(); stopLossManager.resetAllEntryQuantities(); // 每次重启重新获取当前本金,确保盈亏对比基准正确 refreshInitialPrincipal(); @@ -253,25 +270,23 @@ updateUnrealizedPnl(); if (state == StrategyState.STOPPED) { executor.cancelAllAlgoOrders(); closeExistingPositions(); // stopGrid() 已做清理,仅打印日志不重复操作 BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl); log.info("[OKX] 已实现:{}, 未实现:{}, 合计:{}", cumulativePnl, unrealizedPnl, totalPnl); startGrid(); return; } if (state == StrategyState.WAITING_KLINE) { // 等待所有 WS 订阅就绪后再开仓 if (!allSubscriptionsReady()) { pendingKlinePrice = closePrice; log.info("[OKX] 等待所有 WS 订阅就绪(candle1m={}, positions={}, orders={}), 当前价: {}", candle1mSubscribed, positionsSubscribed, ordersSubscribed, closePrice); return; } state = StrategyState.OPENING; log.info("[OKX] 首根K线到达,开基底仓位 多空各{}张...", config.getBaseQuantity()); executor.openLong(config.getBaseQuantity(), (orderId) -> { OkxTraderParam baseLongTp = OkxTraderParam.builder().entryOrderId(orderId).build(); config.setBaseLongTraderParam(baseLongTp); }, null); executor.openShort(config.getBaseQuantity(), (orderId) -> { OkxTraderParam baseShortTp = OkxTraderParam.builder().entryOrderId(orderId).build(); config.setBaseShortTraderParam(baseShortTp); }, null); log.info("[OKX] 首根K线到达,所有订阅就绪,开基底仓位 多空各{}张...", config.getBaseQuantity()); openBasePositions(); return; } @@ -279,6 +294,47 @@ return; } checkProfitAndReset(); } /** * WS 订阅成功确认回调,由各 {@link OkxGridChannelHandler#onSubscribed()} 触发。 * 当所有订阅就绪且已有缓存的 K 线价格时,自动触发开仓。 */ public void onSubscriptionConfirmed(String channel) { if ("candle1m".equals(channel)) candle1mSubscribed = true; else if ("positions".equals(channel)) positionsSubscribed = true; else if ("orders".equals(channel)) ordersSubscribed = true; log.info("[OKX] 订阅就绪: {}, 全部就绪: {}", channel, allSubscriptionsReady()); // 所有订阅就绪 + 有缓存 K 线价格 + 仍处于等待状态 → 触发开仓 if (allSubscriptionsReady() && pendingKlinePrice != null && state == StrategyState.WAITING_KLINE) { BigDecimal price = pendingKlinePrice; pendingKlinePrice = null; log.info("[OKX] 所有 WS 订阅就绪,触发开仓, 价格: {}", price); state = StrategyState.OPENING; log.info("[OKX] 首根K线到达,所有订阅就绪,开基底仓位 多空各{}张...", config.getBaseQuantity()); openBasePositions(); } } /** @return true 表示 candle1m + positions + orders 三个频道均已订阅成功 */ private boolean allSubscriptionsReady() { return candle1mSubscribed && positionsSubscribed && ordersSubscribed; } /** 市价双开基底仓位(多 + 空),对齐 Gate 版本逻辑 */ private void openBasePositions() { executor.openLong(config.getBaseQuantity(), (orderId) -> { OkxTraderParam baseLongTp = OkxTraderParam.builder().entryOrderId(orderId).build(); config.setBaseLongTraderParam(baseLongTp); tryGenerateQueues(); }, null); executor.openShort(config.getBaseQuantity(), (orderId) -> { OkxTraderParam baseShortTp = OkxTraderParam.builder().entryOrderId(orderId).build(); config.setBaseShortTraderParam(baseShortTp); tryGenerateQueues(); }, null); } // ---- 仓位推送回调 ---- @@ -300,12 +356,12 @@ baseLongOpened = true; log.info("[OKX] 基底多成交价: {}", longBaseEntryPrice); tryGenerateQueues(); } else { longPositionSize = posSize; } } else { longPositionSize = posSize; tryGenerateQueues(); // 后续 WS 推送触发重试,兜底此前 NPE 失败的情况 } } else { if (longActive && state == StrategyState.ACTIVE) { log.info("[OKX] 多仓持仓归零,重置策略"); handlePositionZeroAndReset("多仓"); } longActive = false; @@ -321,12 +377,12 @@ baseShortOpened = true; log.info("[OKX] 基底空成交价: {}", shortBaseEntryPrice); tryGenerateQueues(); } else { shortPositionSize = posSize; } } else { shortPositionSize = posSize; tryGenerateQueues(); // 后续 WS 推送触发重试,兜底此前 NPE 失败的情况 } } else { if (shortActive && state == StrategyState.ACTIVE) { log.info("[OKX] 空仓持仓归零,重置策略"); handlePositionZeroAndReset("空仓"); } shortActive = false; @@ -360,32 +416,34 @@ return; } // 匹配止损单 // 匹配止损单 → 委托 StopLossManager OkxGridElement byLongStopLoss = OkxGridElement.findByLongStopLossOrderId(algoId); if (byLongStopLoss != null) { handleLongStopLossTriggered(byLongStopLoss); stopLossManager.handleLongStopLossTriggered(byLongStopLoss, longEntryPrice); return; } OkxGridElement byShortStopLoss = OkxGridElement.findByShortStopLossOrderId(algoId); if (byShortStopLoss != null) { handleShortStopLossTriggered(byShortStopLoss); stopLossManager.handleShortStopLossTriggered(byShortStopLoss, shortEntryPrice); return; } // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 + 挂止盈单 // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 OkxGridElement shortGridElement = OkxGridElement.findByShortOrderId(algoId); if (shortGridElement != null && shortGridElement.isHasShortOrder()) { int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity()); shortEntryTraderIdParam(shortGridElement, null, false); extendShortStopLoss(filledQty); stopLossManager.clearShortEntryState(shortGridElement); stopLossManager.resetShortEntryQty(); stopLossManager.extendShortStopLoss(filledQty); log.info("[OKX] 空单成交 gridId:{}, qty:{}, 追挂止损", shortGridElement.getId(), filledQty); return; } OkxGridElement longGridElement = OkxGridElement.findByLongOrderId(algoId); if (longGridElement != null && longGridElement.isHasLongOrder()) { int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity()); longEntryTraderIdParam(longGridElement, null, false); extendLongStopLoss(filledQty); stopLossManager.clearLongEntryState(longGridElement); stopLossManager.resetLongEntryQty(); stopLossManager.extendLongStopLoss(filledQty); log.info("[OKX] 多单成交 gridId:{}, qty:{}, 追挂止损", longGridElement.getId(), filledQty); return; } @@ -394,346 +452,48 @@ // ---- 网格队列处理 ---- private void tryGenerateQueues() { // 防止重复执行:一旦已进入 ACTIVE 状态不再重复初始化 if (state == StrategyState.ACTIVE) { return; } if (baseLongOpened && baseShortOpened) { generateShortQueue(); generateLongQueue(); updateGridElements(); // 防御异步竞态:openLong/openShort 的回调可能还未设置 trader param OkxTraderParam baseLongTp = config.getBaseLongTraderParam(); OkxTraderParam baseShortTp = config.getBaseShortTraderParam(); if (baseLongTp == null || baseShortTp == null) { log.warn("[OKX] tryGenerateQueues 等待异步回调: longTp={}, shortTp={}", baseLongTp != null, baseShortTp != null); return; } // 委托 GridQueueBuilder 构建价格队列 + GridElements List<BigDecimal> tmpShort = GridQueueBuilder.buildShortQueue(config, shortBaseEntryPrice); List<BigDecimal> tmpLong = GridQueueBuilder.buildLongQueue(config, shortBaseEntryPrice); synchronized (shortPriceQueue) { shortPriceQueue.clear(); shortPriceQueue.addAll(tmpShort); } synchronized (longPriceQueue) { longPriceQueue.clear(); longPriceQueue.addAll(tmpLong); } GridQueueBuilder.buildGridElements(config, shortPriceQueue, longPriceQueue, shortBaseEntryPrice); // 标记基座挂单 OkxGridElement baseGridElement = OkxGridElement.findById(0); OkxTraderParam baseLongTp = config.getBaseLongTraderParam(); baseGridElement.setLongOrderId(baseLongTp.getEntryOrderId()); baseGridElement.setHasLongOrder(true); OkxTraderParam baseShortTp = config.getBaseShortTraderParam(); baseGridElement.setShortOrderId(baseShortTp.getEntryOrderId()); baseGridElement.setHasShortOrder(true); // 挂多仓止损 (id=-2 到 -11),每格 quantity 张 for (int id = -2; id >= -11; id--) { OkxGridElement elem = OkxGridElement.findById(id); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalId = id; executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(), profitId -> { elem.setLongStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}", finalId, triggerPrice, config.getQuantity(), profitId); }); } // 挂空仓止损 (id=2 到 11),每格 quantity 张 for (int id = 2; id <= 11; id++) { OkxGridElement elem = OkxGridElement.findById(id); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalId = id; executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(), profitId -> { elem.setShortStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}", finalId, triggerPrice, config.getQuantity(), profitId); }); } log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11"); // 委托 StopLossManager 挂止损单 stopLossManager.setupBaseStopLosses(); state = StrategyState.ACTIVE; } } 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("[OKX] 空队列:{}", shortPriceQueue); } 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("[OKX] 多队列:{}", longPriceQueue); } private void updateGridElements() { List<OkxGridElement> elements = new ArrayList<>(); int shortSize = shortPriceQueue.size(); int longSize = longPriceQueue.size(); int prec = config.getPriceScale(); BigDecimal step = config.getStep(); String qty = config.getQuantity(); // 空仓队列: id=-1, -2, ... 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); OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } // 位置 0: 基底价格 { BigDecimal price = shortBaseEntryPrice; OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(0).gridPrice(price) .upId(shortSize > 0 ? 1 : null).downId(longSize > 0 ? -1 : null) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } // 多仓队列: id=1, 2, ... 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); OkxTraderParam longParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.LONG) .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); OkxTraderParam shortParam = OkxTraderParam.builder() .direction(OkxTraderParam.Direction.SHORT) .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) .longTraderParam(longParam).shortTraderParam(shortParam).build()); } config.setGridElements(elements); log.info("[OKX] 网格元素列表已构建, 共{}个元素", elements.size()); } // ---- 止损触发处理 ---- /** * 多仓止损触发处理(Gate 模式逐步缩进)。 * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N>2,先取消上一步的旧挂单。 */ private void handleLongStopLossTriggered(OkxGridElement gridElement) { int gridId = gridElement.getId(); int N = Math.abs(gridId); gridElement.setLongStopLossOrderId(null); log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId); int newEntryGridId = -(N - 1); OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); if (newEntryGrid == null) { OkxGridElement.refreshIndices(); log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); return; } if (N > 2) { int cancelGridId = -(N - 2); OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); if (cancelGrid != null && cancelGrid.isHasLongOrder()) { executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> { longEntryTraderIdParam(cancelGrid, null, false); log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs(); int count = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue(); count = Math.max(1, count); int entryQty = count * Integer.parseInt(config.getQuantity()); String size = String.valueOf(entryQty); log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(价差:{},步长:{},count:{},qty:{})", gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity()); newEntryGrid.getLongTraderParam().setQuantity(size); placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size); } /** * 空仓止损触发处理(Gate 模式逐步缩进)。 * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N>2,先取消上一步的旧挂单。 */ private void handleShortStopLossTriggered(OkxGridElement gridElement) { int gridId = gridElement.getId(); int N = gridId; gridElement.setShortStopLossOrderId(null); log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId); int newEntryGridId = N - 1; OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); if (newEntryGrid == null) { OkxGridElement.refreshIndices(); log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); return; } if (N > 2) { int cancelGridId = N - 2; OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); if (cancelGrid != null && cancelGrid.isHasShortOrder()) { executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> { shortEntryTraderIdParam(cancelGrid, null, false); log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs(); int count = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue(); count = Math.max(1, count); int entryQty = count * Integer.parseInt(config.getQuantity()); String size = String.valueOf(entryQty); log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(价差:{},步长:{},count:{},qty:{})", gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity()); newEntryGrid.getShortTraderParam().setQuantity(size); placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size); } private void extendLongStopLoss(int filledQty) { // filledQty 为本次新增止损张数 = count * quantity, 需要按 quantity 为粒度拆分为 count 个止损单 int qty = Integer.parseInt(config.getQuantity()); int stopLossCount = filledQty / qty; int furthestSlId = 0; for (OkxGridElement e : config.getGridElements()) { if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) { furthestSlId = e.getId(); } } if (furthestSlId == 0) furthestSlId = -11; log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty); for (int i = 0; i < stopLossCount; i++) { int newSlId = furthestSlId - i - 1; OkxGridElement elem = OkxGridElement.findById(newSlId); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalSlId = newSlId; executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(), profitId -> { elem.setLongStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); }); } } private void extendShortStopLoss(int filledQty) { int qty = Integer.parseInt(config.getQuantity()); int stopLossCount = filledQty / qty; int furthestSlId = 0; for (OkxGridElement e : config.getGridElements()) { if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) { furthestSlId = e.getId(); } } if (furthestSlId == 0) furthestSlId = 11; log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty); for (int i = 0; i < stopLossCount; i++) { int newSlId = furthestSlId + i + 1; OkxGridElement elem = OkxGridElement.findById(newSlId); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalSlId = newSlId; executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(), profitId -> { elem.setShortStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); }); } } // ---- 辅助方法 ---- private void longTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) { OkxTraderParam tp = baseElement.getLongTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); baseElement.setLongTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } private void shortTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) { OkxTraderParam tp = baseElement.getShortTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); baseElement.setShortTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } private void longEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) { OkxTraderParam tp = baseElement.getLongTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); baseElement.setHasLongOrder(flag); baseElement.setLongOrderId(entryId); OkxGridElement.refreshIndices(); } private void shortEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) { OkxTraderParam tp = baseElement.getShortTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); baseElement.setHasShortOrder(flag); baseElement.setShortOrderId(entryId); OkxGridElement.refreshIndices(); } private void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong, BigDecimal triggerPrice, String size) { if (isLong) { gridElement.setHasLongOrder(true); } else { gridElement.setHasShortOrder(true); } String side = isLong ? "buy" : "sell"; String posSide = isLong ? "long" : "short"; executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, 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); } OkxGridElement.refreshIndices(); log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong); } ); } // ---- 盈亏计算 ---- private void updateUnrealizedPnl() { if (lastKlinePrice == null || lastKlinePrice.compareTo(BigDecimal.ZERO) == 0) return; @@ -794,7 +554,11 @@ state = StrategyState.STOPPED; closeExistingPositions(); executor.cancelAllAlgoOrders(); startGrid(); // 提交到 executor 末尾:单线程FIFO保证前面所有平仓/取消任务完成后才重置 executor.submitTask(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } startGrid(); }); } } catch (Exception e) { log.warn("[OKX] 盈亏检查失败", e); @@ -802,10 +566,15 @@ } private void handlePositionZeroAndReset(String direction) { log.info("[OKX] {}持仓归零,重置策略", direction); state = StrategyState.STOPPED; executor.cancelAllAlgoOrders(); closeExistingPositions(); startGrid(); // 提交到 executor 末尾:FIFO保证平仓完成后再重置 executor.submitTask(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } startGrid(); }); } // ---- getters ---- src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridWsClient.java
@@ -229,6 +229,21 @@ if ("subscribe".equals(event) || "unsubscribe".equals(event)) { log.info("[{}] {}事件: {}", logPrefix, event, response.getString("arg")); // 订阅成功确认:解析频道名并通知对应 handler if ("subscribe".equals(event)) { JSONObject argObj = response.getJSONObject("arg"); if (argObj != null) { String channel = argObj.getString("channel"); if (channel != null) { for (OkxGridChannelHandler handler : channelHandlers) { if (channel.equals(handler.getChannelName())) { handler.onSubscribed(); break; } } } } } return; } src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxTradeExecutor.java
@@ -79,6 +79,14 @@ } /** * 提交一个通用任务到交易线程池末尾。 * 利用单线程池的 FIFO 特性确保任务按提交顺序执行。 */ public void submitTask(Runnable task) { executor.execute(task); } /** * 异步 IOC 市价开多。 * * @param quantity 开仓张数(正数) src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxWebSocketClientManager.java
@@ -1,6 +1,5 @@ package com.xcong.excoin.modules.okxNewPrice; import com.xcong.excoin.modules.okxNewPrice.gridWs.OkxAlgoOrdersChannelHandler; import com.xcong.excoin.modules.okxNewPrice.gridWs.OkxKlineChannelHandler; import com.xcong.excoin.modules.okxNewPrice.gridWs.OkxOrdersChannelHandler; import com.xcong.excoin.modules.okxNewPrice.gridWs.OkxPositionsChannelHandler; @@ -86,14 +85,14 @@ .apiKey(primaryAccount.getApiKey()) .secretKey(primaryAccount.getSecretKey()) .passphrase(primaryAccount.getPassphrase()) .instId("BTC-USDT-SWAP") .instId("ETH-USDT-SWAP") .leverage("100") .tdMode("cross") .gridRate(new BigDecimal("0.001")) .expectedProfit(new BigDecimal("20")) .gridRate(new BigDecimal("0.002")) .expectedProfit(new BigDecimal("180")) .maxLoss(new BigDecimal("30")) .quantity("1") .baseQuantity("10") .quantity("5") .baseQuantity("50") .priceScale(2) .ctVal(new BigDecimal("0.01")) .isSimulate(!primaryAccount.isAccountType()) src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
New file @@ -0,0 +1,311 @@ package com.xcong.excoin.modules.okxNewPrice; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; /** * 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。 * * <h3>止损策略(对齐 Gate 版本)</h3> * <ol> * <li>基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity</li> * <li>条件单成交后以成交量为粒度追挂止损(extend 系列)</li> * <li>止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N>2 时先取消旧挂单</li> * </ol> * * <h3>线程安全</h3> * 本类通过 executor 的异步回调串行化下单操作;对 OkxGridElement 的读写依赖调用方保证有序。 * * @author Administrator */ @Slf4j public class StopLossManager { private final OkxConfig config; private final OkxTradeExecutor executor; /** 多仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */ private volatile int longEntryQty = 1; /** 空仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */ private volatile int shortEntryQty = 1; public StopLossManager(OkxConfig config, OkxTradeExecutor executor) { this.config = config; this.executor = executor; } // ========== 基底止损挂载 ========== /** * 在基底开仓完成后挂载初始止损单。 * 多仓止损: id=-2 到 -11,每格 quantity 张,方向 sell/long * 空仓止损: id=2 到 11,每格 quantity 张,方向 buy/short */ public void setupBaseStopLosses() { // 挂多仓止损 (id=-2 到 -11) for (int id = -2; id >= -11; id--) { OkxGridElement elem = OkxGridElement.findById(id); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalId = id; executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(), profitId -> { elem.setLongStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}", finalId, triggerPrice, config.getQuantity(), profitId); }); } // 挂空仓止损 (id=2 到 11) for (int id = 2; id <= 11; id++) { OkxGridElement elem = OkxGridElement.findById(id); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalId = id; executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(), profitId -> { elem.setShortStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}", finalId, triggerPrice, config.getQuantity(), profitId); }); } log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11"); } // ========== 止损触发处理 ========== /** * 多仓止损触发处理。 * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N > 2,先取消上一步的旧挂单。 */ public void handleLongStopLossTriggered(OkxGridElement gridElement, BigDecimal longEntryPrice) { int gridId = gridElement.getId(); int N = Math.abs(gridId); gridElement.setLongStopLossOrderId(null); log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId); int newEntryGridId = -(N - 1); OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); if (newEntryGrid == null) { OkxGridElement.refreshIndices(); log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); return; } if (N > 2) { int cancelGridId = -(N - 2); OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); if (cancelGrid != null && cancelGrid.isHasLongOrder()) { executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> { clearLongEntryState(cancelGrid); log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); longEntryQty++; int entryQty = longEntryQty * Integer.parseInt(config.getQuantity()); String size = String.valueOf(entryQty); log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(计数器:{}, qty:{})", gridId, newEntryGridId, entryQty, longEntryQty, config.getQuantity()); newEntryGrid.getLongTraderParam().setQuantity(size); placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size); } /** * 空仓止损触发处理。 * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N > 2,先取消上一步的旧挂单。 */ public void handleShortStopLossTriggered(OkxGridElement gridElement, BigDecimal shortEntryPrice) { int gridId = gridElement.getId(); int N = gridId; gridElement.setShortStopLossOrderId(null); log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId); int newEntryGridId = N - 1; OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); if (newEntryGrid == null) { OkxGridElement.refreshIndices(); log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); return; } if (N > 2) { int cancelGridId = N - 2; OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); if (cancelGrid != null && cancelGrid.isHasShortOrder()) { executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> { clearShortEntryState(cancelGrid); log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); shortEntryQty++; int entryQty = shortEntryQty * Integer.parseInt(config.getQuantity()); String size = String.valueOf(entryQty); log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(计数器:{}, qty:{})", gridId, newEntryGridId, entryQty, shortEntryQty, config.getQuantity()); newEntryGrid.getShortTraderParam().setQuantity(size); placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size); } // ========== 止损追挂 ========== /** * 多仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。 */ public void extendLongStopLoss(int filledQty) { int qty = Integer.parseInt(config.getQuantity()); int stopLossCount = filledQty / qty; int furthestSlId = 0; for (OkxGridElement e : config.getGridElements()) { if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) { furthestSlId = e.getId(); } } if (furthestSlId == 0) furthestSlId = -11; log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty); for (int i = 0; i < stopLossCount; i++) { int newSlId = furthestSlId - i - 1; OkxGridElement elem = OkxGridElement.findById(newSlId); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalSlId = newSlId; executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(), profitId -> { elem.setLongStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); }); } } /** * 空仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。 */ public void extendShortStopLoss(int filledQty) { int qty = Integer.parseInt(config.getQuantity()); int stopLossCount = filledQty / qty; int furthestSlId = 0; for (OkxGridElement e : config.getGridElements()) { if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) { furthestSlId = e.getId(); } } if (furthestSlId == 0) furthestSlId = 11; log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty); for (int i = 0; i < stopLossCount; i++) { int newSlId = furthestSlId + i + 1; OkxGridElement elem = OkxGridElement.findById(newSlId); if (elem == null) continue; BigDecimal triggerPrice = elem.getGridPrice(); int finalSlId = newSlId; executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(), profitId -> { elem.setShortStopLossOrderId(profitId); OkxGridElement.refreshIndices(); log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); }); } } // ========== 辅助:条件单创建 & 状态回写 ========== /** * 预置标志位后发送条件入单请求,成功/失败均通过回调写入 GridElement 状态。 */ void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong, BigDecimal triggerPrice, String size) { if (isLong) { gridElement.setHasLongOrder(true); } else { gridElement.setHasShortOrder(true); } String side = isLong ? "buy" : "sell"; String posSide = isLong ? "long" : "short"; executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, size, orderId -> { if (isLong) { setLongEntryState(gridElement, orderId, true); } else { setShortEntryState(gridElement, orderId, true); } }, () -> { if (isLong) { gridElement.setHasLongOrder(false); gridElement.setLongOrderId(null); } else { gridElement.setHasShortOrder(false); gridElement.setShortOrderId(null); } OkxGridElement.refreshIndices(); log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong); } ); } // ---- GridElement 状态修改(package-private,OkxGridTradeService 也可调用) ---- void setLongEntryState(OkxGridElement gridElement, String entryId, boolean flag) { OkxTraderParam tp = gridElement.getLongTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); gridElement.setHasLongOrder(flag); gridElement.setLongOrderId(entryId); OkxGridElement.refreshIndices(); } void setShortEntryState(OkxGridElement gridElement, String entryId, boolean flag) { OkxTraderParam tp = gridElement.getShortTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); gridElement.setHasShortOrder(flag); gridElement.setShortOrderId(entryId); OkxGridElement.refreshIndices(); } void clearLongEntryState(OkxGridElement gridElement) { setLongEntryState(gridElement, null, false); } void clearShortEntryState(OkxGridElement gridElement) { setShortEntryState(gridElement, null, false); } void setLongTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) { OkxTraderParam tp = gridElement.getLongTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); gridElement.setLongTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } void setShortTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) { OkxTraderParam tp = gridElement.getShortTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); gridElement.setShortTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } // ========== 计数器管理 ========== /** 重置多仓挂单张数计数器(挂单成交后调用) */ public void resetLongEntryQty() { longEntryQty = 1; } /** 重置空仓挂单张数计数器(挂单成交后调用) */ public void resetShortEntryQty() { shortEntryQty = 1; } /** 重置全部挂单张数计数器(startGrid时调用) */ public void resetAllEntryQuantities() { longEntryQty = 1; shortEntryQty = 1; } } src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxAlgoOrdersChannelHandler.java
File was deleted src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxGridChannelHandler.java
@@ -27,4 +27,10 @@ * @return true 表示已处理(循环停止),false 表示频道不匹配 */ boolean handleMessage(JSONObject response); /** * 订阅成功确认回调,收到 OKX 服务器 subscribe 事件时触发。 * 子类可重写以通知策略引擎订阅就绪。 */ default void onSubscribed() {} } src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxKlineChannelHandler.java
@@ -46,6 +46,14 @@ } @Override public void onSubscribed() { log.info("[OKX-WS] {} 订阅确认, instId:{}", CHANNEL_NAME, instId); if (gridTradeService != null) { gridTradeService.onSubscriptionConfirmed(CHANNEL_NAME); } } @Override public void unsubscribe(WebSocketClient ws) { JSONObject msg = new JSONObject(); JSONObject arg = new JSONObject(); src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxOrdersChannelHandler.java
@@ -41,6 +41,14 @@ } @Override public void onSubscribed() { log.info("[OKX-WS] {} 订阅确认", CHANNEL_NAME); if (gridTradeService != null) { gridTradeService.onSubscriptionConfirmed(CHANNEL_NAME); } } @Override public void unsubscribe(WebSocketClient ws) { JSONObject msg = new JSONObject(); JSONObject arg = new JSONObject(); src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxPositionsChannelHandler.java
@@ -45,6 +45,14 @@ } @Override public void onSubscribed() { log.info("[OKX-WS] {} 订阅确认", CHANNEL_NAME); if (gridTradeService != null) { gridTradeService.onSubscriptionConfirmed(CHANNEL_NAME); } } @Override public void unsubscribe(WebSocketClient ws) { JSONObject msg = new JSONObject(); JSONObject arg = new JSONObject(); src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/utils/RequestBuilder.java
@@ -96,7 +96,6 @@ .get() .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) .addHeader("OK-ACCESS-PASSPHRASE", passphrase) @@ -112,7 +111,6 @@ .put(RequestBody.create(JSON_TYPE, "")) .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) .addHeader("OK-ACCESS-PASSPHRASE", passphrase) @@ -127,7 +125,6 @@ .url(fullUrl) .delete() .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) src/main/java/com/xcong/excoin/utils/MessageSourceUtils.java
File was deleted src/main/java/com/xcong/excoin/utils/RedisUtils.java
File was deleted src/main/java/com/xcong/excoin/utils/SSLClient.java
File was deleted src/main/java/com/xcong/excoin/utils/ShareCodeUtil.java
File was deleted src/main/java/com/xcong/excoin/utils/SmsUtils.java
File was deleted src/main/java/com/xcong/excoin/utils/SpringContextHolder.java
File was deleted src/main/java/com/xcong/excoin/utils/TypeJudgeUtils.java
File was deleted src/main/java/com/xcong/excoin/utils/api/ApiClient.java
File was deleted src/main/java/com/xcong/excoin/utils/api/ApiException.java
File was deleted src/main/java/com/xcong/excoin/utils/api/request/CreateOrderRequest.java
File was deleted src/main/java/com/xcong/excoin/utils/api/request/DepthRequest.java
File was deleted src/main/java/com/xcong/excoin/utils/api/request/IntrustOrdersDetailRequest.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Account.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Accounts.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/AccountsResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/ApiResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Balance.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/BalanceBean.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/BalanceResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Batchcancel.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/BatchcancelBean.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/BatchcancelResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Currencys.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/CurrencysResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Depth.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/DepthResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/DetailResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Details.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/HistoryTrade.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/HistoryTradeResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/HistoryTradess.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/IntrustDetail.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/IntrustDetailResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Kline.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/KlineResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/KlineReturn.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/MatchresultsOrdersDetail.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/MatchresultsOrdersDetailResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Merged.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/MergedResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/OrdersDetail.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/OrdersDetailResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Place.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/SubmitcancelResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Symbol.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Symbols.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/SymbolsResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/TimestampResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/Trade.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/TradeBean.java
File was deleted src/main/java/com/xcong/excoin/utils/api/response/TradeResponse.java
File was deleted src/main/java/com/xcong/excoin/utils/mail/RequestEncoder.java
File was deleted src/main/java/com/xcong/excoin/utils/mail/Sms106Send.java
File was deleted src/main/java/com/xcong/excoin/utils/mail/SmsSend.java
File was deleted src/main/java/com/xcong/excoin/utils/mail/SubMailSend.java
File was deleted src/main/resources/application-app.yml
File was deleted src/main/resources/application-dayline.yml
File was deleted src/main/resources/application-loop.yml
File was deleted src/main/resources/application-newprice.yml
File was deleted src/main/resources/application-okx.yml
@@ -1,2 +1,4 @@ mybatis-plus: mapper-locations: classpath*:mapper/blackchain/*.xml, classpath*:mapper/ding/*.xml, classpath*:mapper/price/*.xml, classpath*:mapper/combom/*.xml, classpath*:mapper/record/*.xml, classpath*:mapper/uinfo/*.xml, classpath*:mapper/push/*.xml, classpath*:mapper/coin/*.xml, classpath*:mapper/user/*.xml, classpath*:mapper/demo/*.xml # OKX 网格策略专用配置 logging: level: com.xcong.excoin.modules.okxNewPrice: INFO src/main/resources/application-test.yml
File was deleted src/main/resources/application.yml
@@ -1,75 +1,17 @@ server: port: 8888 servlet: context-path: / spring: OKEX: baseurl: https://www.okex.com profiles: active: test datasource: url: jdbc:mysql://120.27.238.55:3406/db_base?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2b8 username: ct_test password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: ${spring_datasource_druid_initial_size:10} max-active: ${spring_datasource_druid_max_active:20} min-idle: ${spring_datasource_druid_min_idle:3} #配置获取连接等待超时的时间 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 validation-query: SELECT 'x' test-on-borrow: true test-on-return: true test-while-idle: true #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 #配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 #spring.datasource.druid.max-evguide.ftlictable-idle-time-millis= filters: stat,wall stat-view-servlet: # 默认true 内置监控页面首页/druid/index.html enabled: true url-pattern: /druid/* # 允许清空统计数据 reset-enable: true login-username: root login-password: 123456 # IP白名单 多个逗号分隔 allow: ${spring_datasource_stat_view_servlet_allow:} # IP黑名单 deny: ${spring_datasource_stat_view_servlet_deny:} ## 国际化配置 messages: basename: i18n/messages active: okx autoconfigure: exclude: # 移除不需要的自动配置 - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration - org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration app: debug: false redis_expire: 3000 # k线更新任务控制 kline-update-job: false #最新价任务控制 newest-price-update-job: false #日线 该任务不能与最新价处于同一个服务器 day-line: false #其他任务控制 other-job: false loop-job: false rabbit-consumer: false block-job: false aliyun: oss: end-point: https://oss-cn-hangzhou.aliyuncs.com bucket-name: https://excoin.oss-cn-hangzhou.aliyuncs.com access-key-id: LTAI4GBuydqbJ5bTsDP97Lpd access-key-secret: vbCjQtPxABWjqtUlQfzjlA0qAY96fh rsa: public_key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCCf8UFZK54AiK4PRu7tNd+Z8qZ77o/QXCnk25DRmygVpOEu5mGNSAvfnWmKp2pEV2RljeXq3Rid/+LQkonaebMJeXKSF0yxL/VgyeT8JaQ5gNbOrdfdlc+mFkXJyzyJt8YkvApEdPRNSU2ENBn7mgRfD0BYPM4vZ6/rv+de38FJwIDAQAB private_key: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIJ/xQVkrngCIrg9G7u0135nypnvuj9BcKeTbkNGbKBWk4S7mYY1IC9+daYqnakRXZGWN5erdGJ3/4tCSidp5swl5cpIXTLEv9WDJ5PwlpDmA1s6t192Vz6YWRcnLPIm3xiS8CkR09E1JTYQ0GfuaBF8PQFg8zi9nr+u/517fwUnAgMBAAECgYBhPt9NvpI4wbanvnndLczr2GJkxfzvSE+vwLCJF4C5FusFHVsxZINggQcg1V75bwRgCiXRMyYefreCSdrCditS43PzTOmE4RRrpxLlm8oubJc0C98LQ2qlN9AsUqL5IHpVGgbHDyWAwjc1GBID6nwXKpxq1/VodFqhahG9D5EZsQJBALnkb+5VTxQbiyQI4Uc9NIvAyVcNY1OisbvY6tvNgdBbJkADgAb78M1HWxxYjUqsvzggNHc7cWqWBHMgpnJaqm8CQQCztze4D7uAk7OC9MJHY5eE980J8Kk+GEZKxz4LahzU6V6dcb9GFac3wEtgilj/tOAn9y0/Q8sm9vvCIbMDzgzJAkEAqRYcqhF26LdVDOX25DHMBgLKISDQZFbsjA13M4/usHL4i+mjHrc0BcUOHu59NpuDI65HitzLAUSLr5zXSdUmiQJAW77wOg4GCejdXsB3IhzMsHwU97sdm26nC+vVV9xvJZ6Rx8zW+f9543NOx9U5BCmhuaVtOvvwDU9PTVcI3atmSQJAXAIJ5gGdtXx0DXiX4VvzNFHqgaqHMGvXyjNVkU2FYQbSAd2A6app4uRO+BkZu9dSjh14m+oXMnV2HzAN2rRnjA== # OKX 量化策略开关 quant: true src/main/resources/i18n/messages.properties
src/main/resources/i18n/messages_en_US.properties
File was deleted src/main/resources/i18n/messages_zh_CN.properties
File was deleted