65fbbf04892c6864906670033a9bffec1f7911c7..d663147264d0fc97e9e7ddbd4aeee69e833ad025
2026-06-02 Administrator
fix(okxNewPrice): 修复合约面值配置错误并更新文档
d66314 diff | tree
2026-06-02 Administrator
docs(okx): 添加网格交易策略架构文档并更新钉钉配置
1d9ab0 diff | tree
2026-06-02 Administrator
docs(okx): 添加网格交易策略架构文档并更新钉钉配置
a2f240 diff | tree
2026-06-02 Administrator
docs(okx): 添加网格交易策略架构文档并更新钉钉配置
308f28 diff | tree
2026-06-02 Administrator
refactor(okxNewPrice): 统一WebSocket登录逻辑并优化频道处理器配置
867db0 diff | tree
2026-06-02 Administrator
refactor(okxNewPrice): 统一WebSocket登录逻辑并优化频道处理器配置
19ee15 diff | tree
2026-06-02 Administrator
refactor(okxNewPrice): 统一WebSocket登录逻辑并优化频道处理器配置
b78e68 diff | tree
2026-06-02 Administrator
fix(okxNewPrice): 解决WebSocket连接超时问题
b6bc83 diff | tree
2026-06-02 Administrator
fix(okxNewPrice): 解决WebSocket连接超时问题
304f66 diff | tree
2026-06-02 Administrator
fix(okx): 修复账户余额获取和API请求处理问题
8838c5 diff | tree
2026-06-02 Administrator
fix(okx): 修复账户余额获取和API请求处理问题
917000 diff | tree
2026-06-02 Administrator
fix(okx): 修复账户余额获取和API请求处理问题
85e167 diff | tree
2026-06-02 Administrator
fix(okx): 修复账户余额获取和API请求处理问题
0f5172 diff | tree
2026-06-02 Administrator
fix(gate): 修复网格交易数量计算和配置参数
1fb824 diff | tree
2026-06-02 Administrator
refactor(okxNewPrice): 更新配置并优化网格交易初始化逻辑
ae403b diff | tree
36 files deleted
4 files added
15 files modified
41970 ■■■■■ changed files
src/main/java/com/xcong/excoin/configurations/MybatisPlusConfig.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/RabbitMqConfig.java 289 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/RedisConfig.java 47 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/WebMvcConfig.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/interceptor/MybatisInterceptor.java 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/properties/AliOssProperties.java 22 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/properties/ApplicationProperties.java 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/properties/CustomRabbitProperties.java 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/ExchangeInfoEnum.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OKX网格策略架构文档.md 373 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxConfig.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridTradeService.java 109 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridWsClient.java 89 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxTradeExecutor.java 33 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxWebSocketClientManager.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxAlgoOrdersChannelHandler.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxOrdersChannelHandler.java 91 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxPositionsChannelHandler.java 11 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/RequestHandler.java 17 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/ResponseHandler.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/utils/OkHttpUtils.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okx文档.txt 39846 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java 10 ●●●● patch | view | raw | blame | history
src/main/resources/application-okx.yml 2 ●●●●● patch | view | raw | blame | history
src/main/resources/application-test.yml 41 ●●●●● patch | view | raw | blame | history
src/main/resources/application.yml 41 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/TestUserDao.xml 12 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/contract/ContractEntrustOrderDao.xml 34 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/contract/ContractHoldOrderDao.xml 48 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/contract/ContractOrderDao.xml 56 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/home/MemberQuickBuySaleDao.xml 16 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/AgentReturnDao.xml 18 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/AppVersionDao.xml 5 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberAccountFlowEntityDao.xml 5 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberAccountMoneyChangeDao.xml 67 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberAuthenticationDao.xml 9 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberCoinAddressDao.xml 70 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberCoinChargeDao.xml 26 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberDao.xml 47 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberLevelRateDao.xml 9 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberPaymentMethodDao.xml 8 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberSelectSymbolsDao.xml 13 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberSettingDao.xml 38 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberWalletAgentDao.xml 10 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberWalletCoinDao.xml 43 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberWalletContractDao.xml 29 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/member/MemberWalletContractSimulateDao.xml 5 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/PlatformCnyUsdtExchangeDao.xml 13 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/PlatformFeeSettingDao.xml 36 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/PlatformSymbolsCoinDao.xml 19 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/PlatformSymbolsContractDao.xml 9 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/PlatformSymbolsSkuDao.xml 8 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/platform/TradeSettingDao.xml 26 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/walletCoinOrder/OrderCoinDealDao.xml 46 ●●●●● patch | view | raw | blame | history
src/main/resources/mapper/walletCoinOrder/OrderCoinsDao.xml 29 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/configurations/MybatisPlusConfig.java
File was deleted
src/main/java/com/xcong/excoin/configurations/RabbitMqConfig.java
File was deleted
src/main/java/com/xcong/excoin/configurations/RedisConfig.java
File was deleted
src/main/java/com/xcong/excoin/configurations/WebMvcConfig.java
@@ -1,13 +1,11 @@
package com.xcong.excoin.configurations;
import com.xcong.excoin.configurations.properties.AliOssProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.List;
/**
@@ -17,10 +15,6 @@
@SpringBootConfiguration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
    @Resource
    private AliOssProperties aliOssProperties;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
src/main/java/com/xcong/excoin/configurations/interceptor/MybatisInterceptor.java
File was deleted
src/main/java/com/xcong/excoin/configurations/properties/AliOssProperties.java
File was deleted
src/main/java/com/xcong/excoin/configurations/properties/ApplicationProperties.java
File was deleted
src/main/java/com/xcong/excoin/configurations/properties/CustomRabbitProperties.java
File was deleted
src/main/java/com/xcong/excoin/modules/okxNewPrice/ExchangeInfoEnum.java
@@ -17,8 +17,8 @@
//            "B0C1CC8F39625B41140D93DC25039E33",
//            "Aa12345678@",
//            true);
    OKX_UAT_ceshi("ffb4e79f-fcf5-4afb-82c5-2fbb64123f61",
            "AA06C5ED1D7C7F5AFE6484052E231C55",
    OKX_UAT_ceshi("ac76252d-e717-4459-a6f9-80512aed5ea0",
            "A8168543EF4F08A6DBFE27AB23956898",
            "Aa12345678@",
            false);
//
src/main/java/com/xcong/excoin/modules/okxNewPrice/OKX网格策略架构文档.md
New file
@@ -0,0 +1,373 @@
# OKX 网格交易策略 — 架构文档
## 一、模块总览
```
okxNewPrice/
├── OkxConfig.java              # 全局配置(Builder模式,策略唯一参数入口)
├── OkxGridTradeService.java    # 核心策略引擎(状态机 + 事件驱动循环)
├── OkxTradeExecutor.java       # REST API 异步执行器(单线程池 + 背压)
├── OkxGridElement.java         # 网格价格层级 + 8个全局O(1)静态索引
├── OkxTraderParam.java         # 单笔挂单参数(方向/触发价/数量/订单ID)
├── OkxGridWsClient.java        # WebSocket 客户端(双连接:business + private)
├── OkxWebSocketClientManager.java  # Spring容器入口 + Bean组装 + 生命周期
├── ExchangeInfoEnum.java       # 账户枚举
├── gridWs/
│   ├── OkxGridChannelHandler.java        # 频道处理器接口
│   ├── OkxKlineChannelHandler.java       # candle1m → onKline(closePrice)
│   ├── OkxPositionsChannelHandler.java   # positions → onPositionUpdate()
│   ├── OkxOrdersChannelHandler.java      # orders → onOrderUpdate() (替代orders-algo)
│   └── OkxAlgoOrdersChannelHandler.java  # orders-algo(测试网60018不可用,备用)
└── okxpi/config/              # OKX V5 API 底层HTTP签名/请求工具链
    ├── Account.java / OKXAccount.java
    ├── RequestHandler.java / ResponseHandler.java
    └── utils/ (SignUtils, UrlBuilder, OkHttpUtils...)
```
## 二、Spring 容器入口
`OkxWebSocketClientManager` 是 `@Component + @PostConstruct` 驱动的组装点。
### 2.1 激活条件
```yaml
# application-test.yml
app:
  quant: true   # @ConditionalOnProperty 控制是否启动
```
### 2.2 初始化顺序 (@PostConstruct)
```
1. ExchangeInfoEnum 取首个账户
2. 构建 OkxAccount(baseUrl + apiKey/secretKey/passphrase)
3. OkxConfig.builder() 构建配置(BTC-USDT-SWAP, 100x, cross, gridRate=0.001)
4. OkxGridTradeService.init()
   ├── GET /api/v5/account/balance → 取 USDT details[].eq 作为初始本金
   ├── POST /api/v5/trade/cancel-algos → 清旧条件单
   ├── GET /api/v5/account/positions → 遍历平已有仓位
   └── POST /api/v5/account/set-leverage → 设置杠杆
5. 创建双 WS 客户端并注册频道处理器:
   ├── gridWsClientPublic  → /v5/business  → candle1m (无需登录)
   └── gridWsClientPrivate → /v5/private   → positions + orders (先登录)
6. gridTradeService.startGrid() → WAITING_KLINE
```
### 2.3 销毁顺序 (@PreDestroy)
```
stopGrid() → cancelAllAlgoOrders + shutdown → gridWsClientPublic.destroy() → gridWsClientPrivate.destroy()
```
### 2.4 当前活跃配置
| 参数 | 值 | 说明 |
|------|-----|------|
| `instId` | `BTC-USDT-SWAP` | 合约 |
| `leverage` | `100` | 杠杆 |
| `tdMode` | `cross` | 全仓 |
| `gridRate` | `0.001` | 网格间距 0.1% |
| `expectedProfit` | `20` USDT | 盈亏达标重置线 |
| `maxLoss` | `30` USDT | 亏损风控告警线 |
| `quantity` | `1` | 每格下单张数 |
| `baseQuantity` | `10` | 基底开仓张数 |
| `priceScale` | `2` | 价格精度 |
| `ctVal` | `0.1` | 合约面值 |
| `gridQueueSize` | `300` | 价格队列容量 |
## 三、WS 连接架构
```
┌──────────────────────────────────────────────────────────┐
│                 OkxWebSocketClientManager                │
│                                                          │
│  gridWsClientPublic (isPublic=true)                     │
│  ├── URL: wss://wspap.okx.com:8443/ws/v5/business       │
│  ├── 连接后立即 subscribeAllHandlers()(无需登录)       │
│  └── OkxKlineChannelHandler(candle1m, instId)            │
│                                                          │
│  gridWsClientPrivate (isPublic=false)                    │
│  ├── URL: wss://wspap.okx.com:8443/ws/v5/private        │
│  ├── 连接后 wsLogin() → 登录成功 → subscribeAllHandlers()│
│  ├── OkxPositionsChannelHandler(positions, instType:SWAP)│
│  └── OkxOrdersChannelHandler(orders, instType:SWAP)     │
└──────────────────────────────────────────────────────────┘
```
| 连接 | URL | 频道 | 订阅参数 | 回调方法 |
|------|-----|------|---------|---------|
| business | `/v5/business` | `candle1m` | `instId` | `onKline(closePrice)` |
| private | `/v5/private` | `positions` | `instType:SWAP` | `onPositionUpdate(instId, posSide, pos, avgPx)` |
| private | `/v5/private` | `orders` | `instType:SWAP` | `onOrderUpdate(algoId, state, ordType)` |
> **orders 频道替代 orders-algo**:`orders-algo` 在测试网不可用(60018),改订阅 `orders` 频道。
> algo 触发后生成普通市价单,fill 数据中 `algoId` 字段非空时可匹配回原始条件单。
> `OkxOrdersChannelHandler` 过滤逻辑:`state=filled` AND `algoId` 非空。
### 3.1 心跳机制
```
10s 无消息 → send("ping") → server reply "pong" → 重置计时器
25s 定时检查 → 超时则 send("ping")
LostConnectionChecker: setConnectionLostTimeout(0) 已关闭(协议级ping OKX不响应)
```
## 四、策略生命周期
### 4.1 状态机
| 状态 | 含义 | 进入条件 | 退出动作 |
|------|------|---------|---------|
| `WAITING_KLINE` | 等待首根K线 | startGrid() | 首根K线→OPENING |
| `OPENING` | 基底开仓中 | 首根K线 | 双基底成交→ACTIVE |
| `ACTIVE` | 策略运行 | 网格初始化完成 | 盈亏达标/持仓归零→STOPPED |
| `STOPPED` | 已停止 | 重置/达标 | 下根K线自动 startGrid() |
### 4.2 完整流程
```
┌──────────────────────────────────────────────────────────────┐
│  init() → startGrid() → WAITING_KLINE                       │
│    ↓                                                         │
│  onKline(首根) → OPENING → executor.openLong/Short(10张)     │
│    ↓                                                         │
│  onPositionUpdate → 基底成交 → tryGenerateQueues()           │
│    ├── generateShortQueue(): shortBasePrice - step 向下步进  │
│    ├── generateLongQueue():  shortBasePrice + step 向上步进  │
│    ├── updateGridElements(): 构建 601个 OkxGridElement       │
│    │   ├── ID≤-1: 空仓区(降序)  ID=0:基底  ID≥1:多仓区(升序)│
│    │   └── ID索引 + 价格索引 + 6个订单ID索引 (O(1))          │
│    ├── 多仓止损 -2~-11: POST order-algo                      │
│    │   ordType=conditional, side=sell, posSide=long,         │
│    │   slTriggerPx=网格价, slOrdPx=-1, sz=quantity           │
│    └── 空仓止损 +2~+11: POST order-algo                      │
│        ordType=conditional, side=buy, posSide=short,         │
│        slTriggerPx=网格价, slOrdPx=-1, sz=quantity           │
│    ↓                                                         │
│  state = ACTIVE ─────────────────────────────────────────────│
└──────────────────────────────────────────────────────────────┘
```
## 五、事件驱动循环 (ACTIVE 状态)
### 5.1 K线回调 `onKline(closePrice)`
```
lastKlinePrice = closePrice
updateUnrealizedPnl()
if STOPPED:
    cancelAllAlgoOrders() + closeExistingPositions() + startGrid() → WAITING_KLINE
if WAITING_KLINE:
    市价双开 baseQuantity 张 (openLong + openShort) → OPENING
if ACTIVE:
    checkProfitAndReset()  // 每根K线检查盈亏达标
```
### 5.2 仓位推送 `onPositionUpdate`
```
long 有仓位:
  首次(基底) → 记录 baseLongOpened + 均价 → tryGenerateQueues()
  后续       → 更新 positionSize / entryPrice
  归零       → handlePositionZeroAndReset("多仓")
short 有仓位:
  首次(基底) → 记录 baseShortOpened + 均价 → tryGenerateQueues()
  后续       → 更新 positionSize / entryPrice
  归零       → handlePositionZeroAndReset("空仓")
```
### 5.3 订单成交 `onOrderUpdate(algoId, state, ordType)` — 核心事件
触发条件:`state == "filled"` (来自 `orders` 频道的成交推送)
```
                    ┌─ findByLongStopLossOrderId(algoId)
                    │  → handleLongStopLossTriggered()
                    │    止损-ID=-N → 清空止损ID
                    │    → 在 -(N-1) 挂 count×qty 张多单
                    │       (ordType=trigger, triggerPx, orderPx=-1)
                    │    → N>2 时取消 -(N-2) 旧多单
                    │
                    ├─ findByShortStopLossOrderId(algoId)
                    │  → handleShortStopLossTriggered()
                    │    止损ID=N → 清空止损ID
                    │    → 在 N-1 挂 count×qty 张空单
                    │       (ordType=trigger, triggerPx, orderPx=-1)
                    │    → N>2 时取消 N-2 旧空单
algoId 匹配 ────────┤
                    ├─ findByShortOrderId(algoId) && hasShortOrder
                    │  → shortEntryTraderIdParam(null, false)
                    │  → extendShortStopLoss(filledQty)
                    │    到最远空仓止损外扩 stopLossCount 个网格
                    │    每格挂 quantity 张止损
                    │
                    └─ findByLongOrderId(algoId) && hasLongOrder
                       → longEntryTraderIdParam(null, false)
                       → extendLongStopLoss(filledQty)
                         到最远多仓止损外扩 stopLossCount 个网格
                         每格挂 quantity 张止损
```
### 5.4 平仓推送 `onPositionClose`
```
cumulativePnl += pnl
总盈亏 ≤ -maxLoss → 钉钉告警(仅通知,不停止)
```
## 六、网格ID体系
```
价格方向:  低 ←────────────── 基底价 ──────────────→ 高
ID:        ... -3  -2  -1    0    1   2   3 ...
用途:      多仓止损/追单区    基底    空仓止损/追单区
           初始化止损:-2~-11        初始化止损:2~11
链表: ... ← -3 ← -2 ← -1 ← 0 → 1 → 2 → 3 → ...
      (upId/downId + INDEX → O(1) 遍历)
```
### 6.1 GridElement 字段
| 类别 | 字段 | 说明 |
|------|------|------|
| 标识 | `id`, `gridPrice`, `upId`, `downId` | 编号、价格、双向链表指针 |
| 多仓 | `hasLongOrder`, `longOrderId`, `longTraderParam` | 多仓挂单状态 |
| 空仓 | `hasShortOrder`, `shortOrderId`, `shortTraderParam` | 空仓挂单状态 |
| 止盈 | `longTakeProfitOrderId`, `shortTakeProfitOrderId` | 止盈algoId |
| 止损 | `longStopLossOrderId`, `shortStopLossOrderId` | 止损algoId |
### 6.2 8个全局O(1)索引
```
INDEX                  → findById(int)
PRICE_INDEX            → findByPrice(BigDecimal)
LONG_ORDER_ID_INDEX    → findByLongOrderId(String)
SHORT_ORDER_ID_INDEX   → findByShortOrderId(String)
LONG_TP_ORDER_ID_INDEX → findByLongTakeProfitOrderId(String)
SHORT_TP_ORDER_ID_INDEX→ findByShortTakeProfitOrderId(String)
LONG_SL_ORDER_ID_INDEX → findByLongStopLossOrderId(String)
SHORT_SL_ORDER_ID_INDEX→ findByShortStopLossOrderId(String)
```
每次订单状态变更后调用 `OkxGridElement.refreshIndices()` 增量重建,同时 `logAll()` 打印全量网格数据。
## 七、关键公式
### 7.1 网格步长
```
step = shortBaseEntryPrice × gridRate    (priceScale 精度对齐)
```
### 7.2 止损触发 → 追单
```
priceDiff = |avgEntryPrice - newEntryGridPrice|.abs()
count     = priceDiff / step           (RoundingMode.DOWN, 最小1)
entryQty  = count × quantity           → 挂 ordType=trigger 条件单
```
### 7.3 挂单成交 → 追挂止损
```
stopLossCount = filledQty / quantity
从最远止损ID向外扩展 stopLossCount 个网格
每格挂 1 个 quantity 张止损 (ordType=conditional, slTriggerPx)
```
### 7.4 未实现盈亏
```
longPnl  = longPositionSize × ctVal × (lastKlinePrice - longEntryPrice)
shortPnl = shortPositionSize × ctVal × (shortEntryPrice - lastKlinePrice)
unrealizedPnl = longPnl + shortPnl
```
### 7.5 盈亏达标检查
```
GET balance → upl (未实现盈亏) + availEq (可用保证金)
if upl + availEq > initialPrincipal + expectedProfit → STOPPED → 平仓+清条件单+startGrid()
```
## 八、REST API 映射
| 方法 | OKX API | ordType | 触发参数 | 用途 |
|------|---------|---------|---------|------|
| `openLong(size)` | `POST /api/v5/trade/order` | `market` | — | 市价开多 |
| `openShort(size)` | `POST /api/v5/trade/order` | `market` | — | 市价开空 |
| `marketClose(s,p,sz)` | `POST /api/v5/trade/order` | `market` | reduceOnly | 市价平仓 |
| `placeConditionalEntryOrder` | `POST /api/v5/trade/order-algo` | **`trigger`** | `triggerPx` | 计划委托开仓 |
| `placeTakeProfit` | `POST /api/v5/trade/order-algo` | **`conditional`** | `slTriggerPx` | 止损/止盈平仓 |
| `cancelAlgoOrder(id)` | `POST /api/v5/trade/cancel-algos` | — | array body | 取消单个条件单 |
| `cancelAllAlgoOrders()` | `POST /api/v5/trade/cancel-algos` | — | array body | 清除全部条件单 |
| `getBalance()` | `GET /api/v5/account/balance` | — | ccy=USDT | 查询余额 |
| `getPositions()` | `GET /api/v5/account/positions` | — | instId | 查询持仓 |
| `setLeverage(l)` | `POST /api/v5/account/set-leverage` | — | lever+mgnMode | 设置杠杆 |
### 8.1 ordType 对照表
| ordType | OKX含义 | 我们用途 | 触发价参数 |
|---------|--------|---------|-----------|
| `trigger` | 计划委托 | **开仓挂单**(等价格到位开仓) | `triggerPx` |
| `conditional` | 单向止盈止损 | **止损单**(等价格到位平仓) | `slTriggerPx` |
## 九、网格ID示例(BTC-USDT, step≈70)
```
ID=-11  price=69309.67 ← 最远多仓止损
ID=-10  price=69379.97
ID= -9  price=69450.27
ID= -8  price=69520.57
ID= -7  price=69590.87
ID= -6  price=69661.17
ID= -5  price=69731.47
ID= -4  price=69801.77
ID= -3  price=69872.07
ID= -2  price=69942.37  ← 多仓止损起点
ID= -1  price=70012.67
ID=  0  price=70082.97  ← 基底 (shortBaseEntryPrice)
ID=  1  price=70153.27
ID=  2  price=70223.57  ← 空仓止损起点
ID=  3  price=70293.87
ID=  4  price=70364.17
ID=  5  price=70434.47
ID=  6  price=70504.77
ID=  7  price=70575.07
ID=  8  price=70645.37
ID=  9  price=70715.67
ID= 10  price=70785.97
ID= 11  price=70856.27  ← 最远空仓止损
```
## 十、线程模型
```
[ctReadThread-XX] WS回调线程(串行)     [okx-trade-worker] Executor(单线程池)
  │                                        │
  ├─ onKline(closePrice)                   ├─ openLong / openShort
  │   └─ updateUnrealizedPnl()             ├─ marketClose
  │   └─ checkProfitAndReset()(同步REST)   ├─ placeConditionalEntryOrder (trigger)
  │                                        ├─ placeTakeProfit (conditional)
  ├─ onPositionUpdate(...)                 └─ cancelAlgoOrder / cancelAllAlgoOrders
  │   └─ tryGenerateQueues() → 批量挂止损
  │   └─ handlePositionZeroAndReset()
  │
  └─ onOrderUpdate(algoId, state, ordType)
      ├─ handleLongStopLossTriggered → placeConditionalEntryOrder
      ├─ handleShortStopLossTriggered → placeConditionalEntryOrder
      ├─ extendLongStopLoss → placeTakeProfit
      └─ extendShortStopLoss → placeTakeProfit
```
- WS 回调线程串行执行,天然线程安全
- REST 提交到 `okx-trade-worker` 单线程异步执行,避免阻塞 WS
- `closeExistingPositions()` / `handlePositionZeroAndReset()` 在 WS 线程中同步调用 REST(IOC 市价单,秒级完成)
- `LostConnectionChecker(0)` 已禁用,由应用层 `send("ping")`/`handle "pong"` 接管
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxConfig.java
@@ -154,7 +154,7 @@
        private boolean isSimulate = false;
        private int gridQueueSize = 300;
        private BigDecimal marginRatioLimit = new BigDecimal("0.2");
        private BigDecimal ctVal = new BigDecimal("0.1");
        private BigDecimal ctVal = new BigDecimal("0.01");
        private int priceScale = 2;
        public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; }
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridTradeService.java
@@ -104,18 +104,27 @@
     */
    public void init() {
        try {
            // 1. 查询账户获取初始本金
            // 1. 查询账户获取初始本金(仅取 USDT 合约账户余额)
            String balanceResp = executor.getBalance();
            if (balanceResp != null) {
                JSONObject json = JSON.parseObject(balanceResp);
                if ("0".equals(json.getString("code"))) {
                    JSONArray data = json.getJSONArray("data");
                    if (data != null && !data.isEmpty()) {
                        JSONObject detail = data.getJSONObject(0);
                        String totalEq = detail.getString("totalEq");
                        if (totalEq != null) {
                            this.initialPrincipal = new BigDecimal(totalEq);
                            log.info("[OKX] 初始本金: {} USDT", initialPrincipal);
                        JSONObject accountData = data.getJSONObject(0);
                        JSONArray details = accountData.getJSONArray("details");
                        if (details != null) {
                            for (int i = 0; i < details.size(); i++) {
                                JSONObject detail = details.getJSONObject(i);
                                if ("USDT".equals(detail.getString("ccy"))) {
                                    String eq = detail.getString("eq");
                                    if (eq != null) {
                                        this.initialPrincipal = new BigDecimal(eq);
                                        log.info("[OKX] 初始本金(USDT合约): {} USDT", initialPrincipal);
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
@@ -190,7 +199,44 @@
        longPriceQueue.clear();
        currentLongOrderIds.clear();
        currentShortOrderIds.clear();
        log.info("[OKX] 网格策略已启动");
        // 每次重启重新获取当前本金,确保盈亏对比基准正确
        refreshInitialPrincipal();
        log.info("[OKX] 网格策略已启动, 当前本金: {} USDT", initialPrincipal);
    }
    /**
     * 重新获取当前账户权益作为初始本金。
     */
    private void refreshInitialPrincipal() {
        try {
            String balanceResp = executor.getBalance();
            if (balanceResp != null) {
                JSONObject json = JSON.parseObject(balanceResp);
                if ("0".equals(json.getString("code"))) {
                    JSONArray data = json.getJSONArray("data");
                    if (data != null && !data.isEmpty()) {
                        JSONObject accountData = data.getJSONObject(0);
                        JSONArray details = accountData.getJSONArray("details");
                        if (details != null) {
                            for (int i = 0; i < details.size(); i++) {
                                JSONObject detail = details.getJSONObject(i);
                                if ("USDT".equals(detail.getString("ccy"))) {
                                    String eq = detail.getString("eq");
                                    if (eq != null) {
                                        this.initialPrincipal = new BigDecimal(eq);
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.warn("[OKX] 获取初始化本金失败,使用旧值: {}", initialPrincipal);
        }
    }
    public void stopGrid() {
@@ -310,7 +356,7 @@
    // ---- 订单/条件单推送回调 ----
    public void onOrderUpdate(String algoId, String state, String ordType) {
        if (!"effective".equals(state) && !"canceled".equals(state)) {
        if (!"filled".equals(state) && !"canceled".equals(state)) {
            return;
        }
@@ -362,31 +408,33 @@
            baseGridElement.setShortOrderId(baseShortTp.getEntryOrderId());
            baseGridElement.setHasShortOrder(true);
            // 挂多仓止损 (id=-2 到 -11)
            // 挂多仓止损 (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", "1",
                executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
                        profitId -> {
                            elem.setLongStopLossOrderId(profitId);
                            OkxGridElement.refreshIndices();
                            log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId);
                            log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
                                    finalId, triggerPrice, config.getQuantity(), profitId);
                        });
            }
            // 挂空仓止损 (id=2 到 11)
            // 挂空仓止损 (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", "1",
                executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
                        profitId -> {
                            elem.setShortStopLossOrderId(profitId);
                            OkxGridElement.refreshIndices();
                            log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId);
                            log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
                                    finalId, triggerPrice, config.getQuantity(), profitId);
                        });
            }
@@ -515,10 +563,12 @@
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs();
        int entryQty = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
        entryQty = Math.max(1, entryQty);
        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:{}挂{}张多单", gridId, newEntryGridId, 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);
    }
@@ -555,15 +605,20 @@
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs();
        int entryQty = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
        entryQty = Math.max(1, entryQty);
        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:{}挂{}张空单", gridId, newEntryGridId, 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) {
@@ -571,14 +626,14 @@
            }
        }
        if (furthestSlId == 0) furthestSlId = -11;
        log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty);
        for (int i = 0; i < filledQty; i++) {
        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", "1",
            executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
                    profitId -> {
                        elem.setLongStopLossOrderId(profitId);
                        OkxGridElement.refreshIndices();
@@ -588,6 +643,8 @@
    }
    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) {
@@ -595,14 +652,14 @@
            }
        }
        if (furthestSlId == 0) furthestSlId = 11;
        log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty);
        for (int i = 0; i < filledQty; i++) {
        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", "1",
            executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
                    profitId -> {
                        elem.setShortStopLossOrderId(profitId);
                        OkxGridElement.refreshIndices();
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridWsClient.java
@@ -38,12 +38,18 @@
    private static final int HEARTBEAT_TIMEOUT = 10;
    /** 模拟盘 WS 地址 */
    private static final String WS_URL_SIM = "wss://wspap.okx.com:8443/ws/v5/private";
    /** 实盘 WS 地址 */
    private static final String WS_URL_PROD = "wss://ws.okx.com:8443/ws/v5/private";
    /** 模拟盘业务 WS 地址(K线等行情数据) */
    private static final String WS_BUSINESS_URL_SIM = "wss://wspap.okx.com:8443/ws/v5/business";
    /** 实盘业务 WS 地址(K线等行情数据) */
    private static final String WS_BUSINESS_URL_PROD = "wss://ws.okx.com:8443/ws/v5/business";
    /** 模拟盘私有 WS 地址 */
    private static final String WS_PRIVATE_URL_SIM = "wss://wspap.okx.com:8443/ws/v5/private";
    /** 实盘私有 WS 地址 */
    private static final String WS_PRIVATE_URL_PROD = "wss://ws.okx.com:8443/ws/v5/private";
    private final ExchangeInfoEnum account;
    private final boolean isPublic;
    private final String logPrefix;
    private WebSocketClient webSocketClient;
    private ScheduledExecutorService heartbeatExecutor;
    private volatile ScheduledFuture<?> pongTimeoutFuture;
@@ -63,8 +69,10 @@
        return t;
    });
    public OkxGridWsClient(ExchangeInfoEnum account) {
    public OkxGridWsClient(ExchangeInfoEnum account, boolean isPublic) {
        this.account = account;
        this.isPublic = isPublic;
        this.logPrefix = isPublic ? "[OKX-Grid-WS-PUB]" : "[OKX-Grid-WS-PRI]";
    }
    public void addChannelHandler(OkxGridChannelHandler handler) {
@@ -73,7 +81,7 @@
    public void init() {
        if (!isInitialized.compareAndSet(false, true)) {
            log.warn("[OKX-Grid-WS] 已初始化过,跳过重复初始化");
            log.warn("[{}] 已初始化过,跳过重复初始化", logPrefix);
            return;
        }
        connect();
@@ -81,7 +89,7 @@
    }
    public void destroy() {
        log.info("[OKX-Grid-WS] 开始销毁...");
        log.info("[{}] 开始销毁...", logPrefix);
        if (webSocketClient != null && webSocketClient.isOpen()) {
            for (OkxGridChannelHandler handler : channelHandlers) {
                handler.unsubscribe(webSocketClient);
@@ -98,18 +106,23 @@
        shutdownExecutorGracefully(heartbeatExecutor);
        if (pongTimeoutFuture != null) pongTimeoutFuture.cancel(true);
        shutdownExecutorGracefully(sharedExecutor);
        log.info("[OKX-Grid-WS] 销毁完成");
        log.info("[{}] 销毁完成", logPrefix);
    }
    private void connect() {
        if (isConnecting.get() || !isConnecting.compareAndSet(false, true)) {
            log.info("[OKX-Grid-WS] 连接进行中,跳过重复请求");
            log.info("[{}] 连接进行中,跳过重复请求", logPrefix);
            return;
        }
        try {
            SSLConfig.configureSSL();
            System.setProperty("https.protocols", "TLSv1.2,TLSv1.3");
            String wsUrl = account.isAccountType() ? WS_URL_PROD : WS_URL_SIM;
            String wsUrl;
            if (account.isAccountType()) {
                wsUrl = isPublic ? WS_BUSINESS_URL_PROD : WS_PRIVATE_URL_PROD;
            } else {
                wsUrl = isPublic ? WS_BUSINESS_URL_SIM : WS_PRIVATE_URL_SIM;
            }
            URI uri = new URI(wsUrl);
            if (webSocketClient != null) {
@@ -119,12 +132,16 @@
            webSocketClient = new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake handshake) {
                    log.info("[OKX-Grid-WS] 连接成功");
                    log.info("[{}] 连接成功", logPrefix);
                    isConnected.set(true);
                    isConnecting.set(false);
                    if (sharedExecutor != null && !sharedExecutor.isShutdown()) {
                        resetHeartbeatTimer();
                        wsLogin();
                        if (isPublic) {
                            subscribeAllHandlers();
                        } else {
                            wsLogin();
                        }
                    }
                }
@@ -137,26 +154,27 @@
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    log.warn("[OKX-Grid-WS] 连接关闭, code:{}, reason:{}", code, reason);
                    log.warn("[{}] 连接关闭, code:{}, reason:{}", logPrefix, code, reason);
                    isConnected.set(false);
                    isConnecting.set(false);
                    cancelPongTimeout();
                    if (sharedExecutor != null && !sharedExecutor.isShutdown() && !sharedExecutor.isTerminated()) {
                        sharedExecutor.execute(() -> {
                            try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("[OKX-Grid-WS] 重连失败", e); }
                            try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("[{}] 重连失败", logPrefix, e); }
                        });
                    }
                }
                @Override
                public void onError(Exception ex) {
                    log.error("[OKX-Grid-WS] 发生错误", ex);
                    log.error("[{}] 发生错误", logPrefix, ex);
                    isConnected.set(false);
                }
            };
            webSocketClient.setConnectionLostTimeout(0);
            webSocketClient.connect();
        } catch (URISyntaxException e) {
            log.error("[OKX-Grid-WS] URI格式错误", e);
            log.error("[{}] URI格式错误", logPrefix, e);
            isConnecting.set(false);
        }
    }
@@ -180,45 +198,50 @@
            args.add(loginArgs);
            msg.put("args", args);
            webSocketClient.send(msg.toJSONString());
            log.info("[OKX-Grid-WS] 发送登录请求");
            log.info("[{}] 发送登录请求", logPrefix);
        } catch (Exception e) {
            log.error("[OKX-Grid-WS] 登录请求构建失败", e);
            log.error("[{}] 登录请求构建失败", logPrefix, e);
        }
    }
    private void subscribeAllHandlers() {
        log.info("[{}] 开始订阅频道", logPrefix);
        for (OkxGridChannelHandler handler : channelHandlers) {
            handler.subscribe(webSocketClient);
        }
    }
    private void handleMessage(String message) {
        try {
            if ("pong".equals(message)) {
                log.debug("[{}] 收到 pong", logPrefix);
                return;
            }
            JSONObject response = JSON.parseObject(message);
            String event = response.getString("event");
            String op = response.getString("op");
            // 登录成功 → 订阅所有频道
            if ("login".equals(event) || ("login".equals(op))) {
                log.info("[OKX-Grid-WS] 登录成功, 开始订阅频道");
                for (OkxGridChannelHandler handler : channelHandlers) {
                    handler.subscribe(webSocketClient);
                }
                log.info("[{}] 登录成功, 开始订阅频道", logPrefix);
                subscribeAllHandlers();
                return;
            }
            // 订阅确认
            if ("subscribe".equals(event) || "unsubscribe".equals(event)) {
                log.info("[OKX-Grid-WS] {}事件: {}", event, response.getString("arg"));
                log.info("[{}] {}事件: {}", logPrefix, event, response.getString("arg"));
                return;
            }
            // 错误
            if ("error".equals(event)) {
                log.error("[OKX-Grid-WS] 错误: {}", message);
                log.error("[{}] 错误: {}", logPrefix, message);
                return;
            }
            // 数据推送 → 路由到 handler
            for (OkxGridChannelHandler handler : channelHandlers) {
                if (handler.handleMessage(response)) return;
            }
        } catch (Exception e) {
            log.error("[OKX-Grid-WS] 处理消息失败: {}", message, e);
            log.error("[{}] 处理消息失败: {}", logPrefix, message, e);
        }
    }
@@ -250,9 +273,9 @@
        try {
            if (webSocketClient != null && webSocketClient.isOpen()) {
                webSocketClient.send("ping");
                log.debug("[OKX-Grid-WS] 发送 ping");
                log.debug("[{}] 发送 ping", logPrefix);
            }
        } catch (Exception e) { log.warn("[OKX-Grid-WS] 发送 ping 失败", e); }
        } catch (Exception e) { log.warn("[{}] 发送 ping 失败", logPrefix, e); }
    }
    private synchronized void cancelPongTimeout() {
@@ -264,9 +287,9 @@
        long delayMs = 5000;
        while (attempt < maxAttempts) {
            try { Thread.sleep(delayMs); connect(); return; }
            catch (Exception e) { log.warn("[OKX-Grid-WS] 第{}次重连失败", attempt + 1, e); delayMs *= 2; attempt++; }
            catch (Exception e) { log.warn("[{}] 第{}次重连失败", logPrefix, attempt + 1, e); delayMs *= 2; attempt++; }
        }
        log.error("[OKX-Grid-WS] 超过最大重试次数({}),放弃重连", maxAttempts);
        log.error("[{}] 超过最大重试次数({}),放弃重连", logPrefix, maxAttempts);
    }
    private void shutdownExecutorGracefully(ExecutorService executor) {
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxTradeExecutor.java
@@ -86,7 +86,7 @@
     * @param onFailure 失败回调
     */
    public void openLong(String quantity, Consumer<String> onSuccess, Runnable onFailure) {
        submitOrder("buy", "long", quantity, "market", null, false, "t-okx-grid-long", onSuccess, onFailure);
        submitOrder("buy", "long", quantity, "market", null, false, null, onSuccess, onFailure);
    }
    /**
@@ -97,7 +97,7 @@
     * @param onFailure 失败回调
     */
    public void openShort(String quantity, Consumer<String> onSuccess, Runnable onFailure) {
        submitOrder("sell", "short", quantity, "market", null, false, "t-okx-grid-short", onSuccess, onFailure);
        submitOrder("sell", "short", quantity, "market", null, false, null, onSuccess, onFailure);
    }
    /**
@@ -141,11 +141,11 @@
                params.put("tdMode", tdMode);
                params.put("side", side);
                params.put("posSide", posSide);
                params.put("ordType", "conditional");
                params.put("ordType", "trigger");
                params.put("sz", size);
                params.put("triggerPx", triggerPrice);
                params.put("triggerPxType", "last");
                params.put("orderPx", "-1"); // 市价成交
                params.put("orderPx", "-1");
                String resp = okxAccount.requestHandler.sendSignedRequest(
                        okxAccount.baseUrl, "/api/v5/trade/order-algo", params, HttpMethod.POST, okxAccount.isSimluate());
@@ -186,9 +186,9 @@
                params.put("posSide", posSide);
                params.put("ordType", "conditional");
                params.put("sz", size);
                params.put("triggerPx", triggerPrice);
                params.put("triggerPxType", "last");
                params.put("orderPx", "-1"); // 市价成交
                params.put("slTriggerPx", triggerPrice);
                params.put("slTriggerPxType", "last");
                params.put("slOrdPx", "-1");
                String resp = okxAccount.requestHandler.sendSignedRequest(
                        okxAccount.baseUrl, "/api/v5/trade/order-algo", params, HttpMethod.POST, okxAccount.isSimluate());
@@ -219,11 +219,9 @@
        }
        executor.execute(() -> {
            try {
                LinkedHashMap<String, Object> params = new LinkedHashMap<>();
                params.put("instId", instId);
                params.put("algoId", algoId);
                String resp = okxAccount.requestHandler.sendSignedRequest(
                        okxAccount.baseUrl, "/api/v5/trade/cancel-algos", params, HttpMethod.POST, okxAccount.isSimluate());
                String body = "[{\"instId\":\"" + instId + "\",\"algoId\":\"" + algoId + "\"}]";
                String resp = okxAccount.requestHandler.sendSignedRequestRaw(
                        okxAccount.baseUrl, "/api/v5/trade/cancel-algos", body, HttpMethod.POST, okxAccount.isSimluate());
                log.info("[OkxExec] 条件单已取消, algoId:{}", algoId);
                if (onSuccess != null) {
                    onSuccess.accept(algoId);
@@ -235,18 +233,17 @@
    }
    /**
     * 异步取消所有未完成的 algo 订单。
     * 异步取消所有未完成的 algo 订单(best-effort,失败仅警告)。
     */
    public void cancelAllAlgoOrders() {
        executor.execute(() -> {
            try {
                LinkedHashMap<String, Object> params = new LinkedHashMap<>();
                params.put("instId", instId);
                String resp = okxAccount.requestHandler.sendSignedRequest(
                        okxAccount.baseUrl, "/api/v5/trade/cancel-algos", params, HttpMethod.POST, okxAccount.isSimluate());
                String body = "[{\"instId\":\"" + instId + "\",\"instType\":\"SWAP\"}]";
                String resp = okxAccount.requestHandler.sendSignedRequestRaw(
                        okxAccount.baseUrl, "/api/v5/trade/cancel-algos", body, HttpMethod.POST, okxAccount.isSimluate());
                log.info("[OkxExec] 已尝试清除条件单, resp:{}", resp);
            } catch (Exception e) {
                log.error("[OkxExec] 清除条件单失败", e);
                log.warn("[OkxExec] 清除条件单失败(若无挂单可忽略), msg:{}", e.getMessage());
            }
        });
    }
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxWebSocketClientManager.java
@@ -2,6 +2,7 @@
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;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.DefaultUrls;
@@ -44,8 +45,10 @@
@ConditionalOnProperty(prefix = "app", name = "quant", havingValue = "true")
public class OkxWebSocketClientManager {
    /** 网格交易 WS 客户端 */
    private OkxGridWsClient gridWsClient;
    /** 网格交易公共 WS 客户端(candle1m) */
    private OkxGridWsClient gridWsClientPublic;
    /** 网格交易私有 WS 客户端(positions / orders-algo) */
    private OkxGridWsClient gridWsClientPrivate;
    /** 网格交易策略服务 */
    private OkxGridTradeService gridTradeService;
    /** 统一配置 */
@@ -83,16 +86,16 @@
                    .apiKey(primaryAccount.getApiKey())
                    .secretKey(primaryAccount.getSecretKey())
                    .passphrase(primaryAccount.getPassphrase())
                    .instId("ETH-USDT-SWAP")
                    .instId("BTC-USDT-SWAP")
                    .leverage("100")
                    .tdMode("cross")
                    .gridRate(new BigDecimal("0.0025"))
                    .expectedProfit(new BigDecimal("2"))
                    .maxLoss(new BigDecimal("15"))
                    .gridRate(new BigDecimal("0.001"))
                    .expectedProfit(new BigDecimal("20"))
                    .maxLoss(new BigDecimal("30"))
                    .quantity("1")
                    .baseQuantity("10")
                    .priceScale(2)
                    .ctVal(new BigDecimal("0.1"))
                    .ctVal(new BigDecimal("0.01"))
                    .isSimulate(!primaryAccount.isAccountType())
                    .gridQueueSize(300)
                    .marginRatioLimit(new BigDecimal("0.2"))
@@ -102,13 +105,19 @@
            gridTradeService = new OkxGridTradeService(okxConfig, okxAccount);
            gridTradeService.init();
            // 4. 创建 WS 客户端并注册 3 个频道处理器
            gridWsClient = new OkxGridWsClient(primaryAccount);
            gridWsClient.addChannelHandler(new OkxKlineChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClient.addChannelHandler(new OkxPositionsChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClient.addChannelHandler(new OkxAlgoOrdersChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClient.init();
            log.info("[OKX-Manager] WS已连接, 已注册 3 个频道处理器: candle1m/positions/orders-algo");
            // 4. 创建 WS 客户端并注册频道处理器
            // 业务 WS(/v5/business):candle1m
            gridWsClientPublic = new OkxGridWsClient(primaryAccount, true);
            gridWsClientPublic.addChannelHandler(new OkxKlineChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClientPublic.init();
            // 私有 WS(/v5/private):positions + orders(algo触发后订单fill带algoId可匹配)
            gridWsClientPrivate = new OkxGridWsClient(primaryAccount, false);
            gridWsClientPrivate.addChannelHandler(new OkxPositionsChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClientPrivate.addChannelHandler(new OkxOrdersChannelHandler(okxConfig.getInstId(), gridTradeService));
            gridWsClientPrivate.init();
            log.info("[OKX-Manager] WS已连接, business: candle1m, private: positions/orders");
            // 5. 激活策略,等待首根 K 线触发基底双开
            gridTradeService.startGrid();
@@ -133,11 +142,18 @@
                log.error("[OKX-Manager] 停止策略失败", e);
            }
        }
        if (gridWsClient != null) {
        if (gridWsClientPublic != null) {
            try {
                gridWsClient.destroy();
                gridWsClientPublic.destroy();
            } catch (Exception e) {
                log.error("[OKX-Manager] 销毁WS客户端失败", e);
                log.error("[OKX-Manager] 销毁公共WS客户端失败", e);
            }
        }
        if (gridWsClientPrivate != null) {
            try {
                gridWsClientPrivate.destroy();
            } catch (Exception e) {
                log.error("[OKX-Manager] 销毁私有WS客户端失败", e);
            }
        }
@@ -146,8 +162,9 @@
    /** @return 网格交易策略服务实例 */
    public OkxGridTradeService getGridTradeService() { return gridTradeService; }
    /** @return 网格交易 WS 客户端实例 */
    public OkxGridWsClient getGridWsClient() { return gridWsClient; }
    /** @return 统一配置实例 */
    /** @return 网格交易公共 WS 客户端实例 */
    public OkxGridWsClient getGridWsClientPublic() { return gridWsClientPublic; }
    /** @return 网格交易私有 WS 客户端实例 */
    public OkxGridWsClient getGridWsClientPrivate() { return gridWsClientPrivate; }
    public OkxConfig getOkxConfig() { return okxConfig; }
}
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxAlgoOrdersChannelHandler.java
@@ -33,13 +33,13 @@
        JSONObject msg = new JSONObject();
        JSONObject arg = new JSONObject();
        arg.put("channel", CHANNEL_NAME);
        arg.put("instType", "SWAP");
        arg.put("instId", instId);
        msg.put("op", "subscribe");
        JSONArray args = new JSONArray();
        args.add(arg);
        msg.put("args", args);
        ws.send(msg.toJSONString());
        log.info("[OKX-WS] {} 订阅成功", CHANNEL_NAME);
        log.info("[OKX-WS] {} 订阅成功, instId:{}", CHANNEL_NAME, instId);
    }
    @Override
@@ -47,7 +47,7 @@
        JSONObject msg = new JSONObject();
        JSONObject arg = new JSONObject();
        arg.put("channel", CHANNEL_NAME);
        arg.put("instType", "SWAP");
        arg.put("instId", instId);
        msg.put("op", "unsubscribe");
        JSONArray args = new JSONArray();
        args.add(arg);
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxOrdersChannelHandler.java
New file
@@ -0,0 +1,91 @@
package com.xcong.excoin.modules.okxNewPrice.gridWs;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xcong.excoin.modules.okxNewPrice.OkxGridTradeService;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
/**
 * OKX 订单频道处理器 (orders),替代 orders-algo。
 * 当 algo 订单触发后生成普通市价单,其 fill 数据带 algoId,可匹配到原始条件单。
 */
@Slf4j
public class OkxOrdersChannelHandler implements OkxGridChannelHandler {
    private static final String CHANNEL_NAME = "orders";
    private final String instId;
    private final OkxGridTradeService gridTradeService;
    public OkxOrdersChannelHandler(String instId, OkxGridTradeService gridTradeService) {
        this.instId = instId;
        this.gridTradeService = gridTradeService;
    }
    @Override
    public String getChannelName() { return CHANNEL_NAME; }
    @Override
    public void subscribe(WebSocketClient ws) {
        JSONObject msg = new JSONObject();
        JSONObject arg = new JSONObject();
        arg.put("channel", CHANNEL_NAME);
        arg.put("instType", "SWAP");
        msg.put("op", "subscribe");
        JSONArray args = new JSONArray();
        args.add(arg);
        msg.put("args", args);
        ws.send(msg.toJSONString());
        log.info("[OKX-WS] {} 订阅成功", CHANNEL_NAME);
    }
    @Override
    public void unsubscribe(WebSocketClient ws) {
        JSONObject msg = new JSONObject();
        JSONObject arg = new JSONObject();
        arg.put("channel", CHANNEL_NAME);
        arg.put("instType", "SWAP");
        msg.put("op", "unsubscribe");
        JSONArray args = new JSONArray();
        args.add(arg);
        msg.put("args", args);
        ws.send(msg.toJSONString());
        log.info("[OKX-WS] {} 取消订阅成功", CHANNEL_NAME);
    }
    @Override
    public boolean handleMessage(JSONObject response) {
        JSONObject arg = response.getJSONObject("arg");
        if (arg == null || !CHANNEL_NAME.equals(arg.getString("channel"))) {
            return false;
        }
        try {
            JSONArray data = response.getJSONArray("data");
            if (data == null || data.isEmpty()) return true;
            for (int i = 0; i < data.size(); i++) {
                JSONObject order = data.getJSONObject(i);
                if (!instId.equals(order.getString("instId"))) continue;
                String algoId = order.getString("algoId");
                if (algoId == null || algoId.isEmpty()) continue;
                String state = order.getString("state");
                if (!"filled".equals(state)) continue;
                String ordType = order.getString("ordType");
                log.info("[OKX-WS] 订单成交(algo), algoId:{}, ordType:{}, fillPx:{}, fillSz:{}",
                        algoId, ordType,
                        order.getString("fillPx"),
                        order.getString("fillSz"));
                if (gridTradeService != null) {
                    gridTradeService.onOrderUpdate(algoId, state, ordType);
                }
            }
        } catch (Exception e) {
            log.error("[OKX-WS] {} 处理数据失败", CHANNEL_NAME, e);
        }
        return true;
    }
}
src/main/java/com/xcong/excoin/modules/okxNewPrice/gridWs/OkxPositionsChannelHandler.java
@@ -69,11 +69,16 @@
            if (data == null || data.isEmpty()) return true;
            for (int i = 0; i < data.size(); i++) {
                JSONObject pos = data.getJSONObject(i);
                if (!instId.equals(pos.getString("instId"))) continue;
                String posInstId = pos.getString("instId");
                if (posInstId == null || !instId.equals(posInstId)) continue;
                String posSide = pos.getString("posSide");
                BigDecimal posSize = new BigDecimal(pos.getString("pos"));
                BigDecimal avgPx = new BigDecimal(pos.getString("avgPx"));
                String posStr = pos.getString("pos");
                String avgPxStr = pos.getString("avgPx");
                if (posStr == null || posStr.isEmpty() || avgPxStr == null || avgPxStr.isEmpty()) continue;
                BigDecimal posSize = new BigDecimal(posStr);
                BigDecimal avgPx = new BigDecimal(avgPxStr);
                log.info("[OKX-WS] 持仓更新, instId:{}, posSide:{}, pos:{}, avgPx:{}",
                        instId, posSide, posSize, avgPx);
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/RequestHandler.java
@@ -118,9 +118,20 @@
        if (null == secretKey || secretKey.isEmpty() || null == apiKey || apiKey.isEmpty()) {
            throw new FebsException("[RequestHandler] Secret key/API key cannot be null or empty!");
        }
        //parameters.put("timestamp", UrlBuilder.buildTimestamp());
        //String queryString = UrlBuilder.joinQueryParameters(parameters);
        //String signature = SignatureGenerator.getSignature(queryString, secretKey);
        return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.SIGNED, isSimluate);
    }
    public String sendSignedRequestRaw(String baseUrl, String urlPath, String rawBody,
                                        HttpMethod httpMethod, boolean isSimluate) {
        if (null == secretKey || secretKey.isEmpty() || null == apiKey || apiKey.isEmpty()) {
            throw new FebsException("[RequestHandler] Secret key/API key cannot be null or empty!");
        }
        String fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, null, null);
        log.debug("{} {}", httpMethod, fullUrl);
        String timestamp = DateUtils.format(DateUtils.FORMAT_UTC_ISO8601, new Date(), 0);
        String queryString = urlPath;
        String sign = SignUtils.signRest(secretKey, timestamp, httpMethod.toString(), queryString, rawBody);
        Request request = RequestBuilder.buildApiKeyRequest(fullUrl, rawBody, passphrase, sign, timestamp, httpMethod, apiKey, isSimluate);
        return ResponseHandler.handleResponse(request, isSimluate);
    }
}
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/ResponseHandler.java
@@ -21,7 +21,7 @@
    }
    public static String handleResponse(Request request, boolean showLimitUsage) {
        try (Response response = OkHttpUtils.okHttpClient.newCall(request).execute()) {//OkHttpUtils.builder().okHttpClient
        try (Response response = OkHttpUtils.getClient().newCall(request).execute()) {
            String responseAsString = getResponseBodyAsString(response.body());
            if (response.code() >= HTTP_STATUS_CODE_400 && response.code() <= HTTP_STATUS_CODE_499) {
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/utils/OkHttpUtils.java
@@ -74,6 +74,16 @@
    }
    /**
     * 确保 OkHttpClient 在任何请求路径下都已初始化(包括直接使用 new Request.Builder() 的场景)。
     */
    public static OkHttpClient getClient() {
        if (okHttpClient == null) {
            new OkHttpUtils(); // 触发构造函数中的 DCL 初始化
        }
        return okHttpClient;
    }
    /**
     * 创建OkHttpUtils
     *
     * @return
src/main/java/com/xcong/excoin/modules/okxNewPrice/okx文档.txt
New file
Diff too large
src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java
@@ -49,12 +49,12 @@
        if (DEFAULT == null) {
            synchronized (DingTalkUtils.class) {
                if (DEFAULT == null) {
//                    DEFAULT = new DingTalkUtils(
//                            "57a3e695f78d7547fe20fb7aef82cf35a27de1846bbc6966e0194761976d7597",
//                            "SECd59a93c8939eeaef0d97b5b714639df4af95d922002d0a440bc82ad42710a89e");
                    DEFAULT = new DingTalkUtils(
                            "e357a3417991da86a5f79ea5bc8785b529c1da8b9d27458febed3b3d10c857c4",
                            "SECf2b819e930cb4b367cf599f11a30eb8a5d0f4b0b1c069a57aa15328a3feebf8c");
                            "57a3e695f78d7547fe20fb7aef82cf35a27de1846bbc6966e0194761976d7597",
                            "SECd59a93c8939eeaef0d97b5b714639df4af95d922002d0a440bc82ad42710a89e");
//                    DEFAULT = new DingTalkUtils(
//                            "e357a3417991da86a5f79ea5bc8785b529c1da8b9d27458febed3b3d10c857c4",
//                            "SECf2b819e930cb4b367cf599f11a30eb8a5d0f4b0b1c069a57aa15328a3feebf8c");
                }
            }
        }
src/main/resources/application-okx.yml
New file
@@ -0,0 +1,2 @@
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
src/main/resources/application-test.yml
@@ -45,47 +45,6 @@
  ## 国际化配置
  messages:
    basename: i18n/messages
  ## redis配置
  redis:
    # Redis数据库索引(默认为 0)
    database: 8
    # Redis服务器地址
    host: 120.27.238.55
    # Redis服务器连接端口
    port: 6479
    # Redis 密码
    password: d3y6dsdl;f.327
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 8
        # 连接池中的最大空闲连接
        max-idle: 500
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 2000
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 10000
    # 连接超时时间(毫秒)
    timeout: 500000
  rabbitmq:
    host: 120.27.238.55
    port: 5672
    username: ct_rabbit
    password: 123456
    publisher-confirm-type: correlated
#custom:
#  rabbitmq:
#    host: 120.27.238.55
#    port: 5672
#    username: ct_rabbit
#    password: 123456
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
app:
  debug: false
src/main/resources/application.yml
@@ -47,47 +47,6 @@
  ## 国际化配置
  messages:
    basename: i18n/messages
  ## redis配置
  redis:
    # Redis数据库索引(默认为 0)
    database: 13
    # Redis服务器地址
    host: 120.27.238.55
    # Redis服务器连接端口
    port: 6479
    # Redis 密码
    password: d3y6dsdl;f.327
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 8
        # 连接池中的最大空闲连接
        max-idle: 500
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 2000
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 10000
    # 连接超时时间(毫秒)
    timeout: 500000
  rabbitmq:
    host: 120.27.238.55
    port: 5672
    username: ct_rabbit
    password: 123456
    publisher-confirm-type: correlated
#custom:
#  rabbitmq:
#    host: 120.27.238.55
#    port: 5672
#    username: ct_rabbit
#    password: 123456
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
app:
  debug: false
src/main/resources/mapper/TestUserDao.xml
File was deleted
src/main/resources/mapper/contract/ContractEntrustOrderDao.xml
File was deleted
src/main/resources/mapper/contract/ContractHoldOrderDao.xml
File was deleted
src/main/resources/mapper/contract/ContractOrderDao.xml
File was deleted
src/main/resources/mapper/home/MemberQuickBuySaleDao.xml
File was deleted
src/main/resources/mapper/member/AgentReturnDao.xml
File was deleted
src/main/resources/mapper/member/AppVersionDao.xml
File was deleted
src/main/resources/mapper/member/MemberAccountFlowEntityDao.xml
File was deleted
src/main/resources/mapper/member/MemberAccountMoneyChangeDao.xml
File was deleted
src/main/resources/mapper/member/MemberAuthenticationDao.xml
File was deleted
src/main/resources/mapper/member/MemberCoinAddressDao.xml
File was deleted
src/main/resources/mapper/member/MemberCoinChargeDao.xml
File was deleted
src/main/resources/mapper/member/MemberDao.xml
File was deleted
src/main/resources/mapper/member/MemberLevelRateDao.xml
File was deleted
src/main/resources/mapper/member/MemberPaymentMethodDao.xml
File was deleted
src/main/resources/mapper/member/MemberSelectSymbolsDao.xml
File was deleted
src/main/resources/mapper/member/MemberSettingDao.xml
File was deleted
src/main/resources/mapper/member/MemberWalletAgentDao.xml
File was deleted
src/main/resources/mapper/member/MemberWalletCoinDao.xml
File was deleted
src/main/resources/mapper/member/MemberWalletContractDao.xml
File was deleted
src/main/resources/mapper/member/MemberWalletContractSimulateDao.xml
File was deleted
src/main/resources/mapper/platform/PlatformCnyUsdtExchangeDao.xml
File was deleted
src/main/resources/mapper/platform/PlatformFeeSettingDao.xml
File was deleted
src/main/resources/mapper/platform/PlatformSymbolsCoinDao.xml
File was deleted
src/main/resources/mapper/platform/PlatformSymbolsContractDao.xml
File was deleted
src/main/resources/mapper/platform/PlatformSymbolsSkuDao.xml
File was deleted
src/main/resources/mapper/platform/TradeSettingDao.xml
File was deleted
src/main/resources/mapper/walletCoinOrder/OrderCoinDealDao.xml
File was deleted
src/main/resources/mapper/walletCoinOrder/OrderCoinsDao.xml
File was deleted