Administrator
2025-12-10 93ac37e763872e5c2911daa343358ed0b1b29047
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package com.xcong.excoin.modules.okxNewPrice.okxWs;
 
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums;
import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums;
import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils;
import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild;
import com.xcong.excoin.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
 
/**
 * 账户 WebSocket 处理类,用于订阅 OKX 的账户频道并处理账户信息推送。
 * 包含账户资金状态判断、计算下单保证金及保存到 Redis 的逻辑。
 *
 * @author Administrator
 */
@Slf4j
public class AccountWs {
 
    /**
     * 账户频道名称常量
     */
    public static final String ACCOUNTWS_CHANNEL = "account";
 
    private static final String CCY_KEY = "ccy";
    private static final String AVAIL_BAL_KEY = "availBal";
    private static final String CASH_BAL_KEY = "cashBal";
    private static final String EQ_KEY = "eq";
    private static final String DETAILS_KEY = "details";
    private static final String DATA_KEY = "data";
 
    // 缓存常用 BigDecimal 常量
    private static final BigDecimal KANG_CANG_THRESHOLD = new BigDecimal(OrderParamEnums.KANG_CANG.getValue());
    private static final BigDecimal ZHI_SUN_THRESHOLD = new BigDecimal(OrderParamEnums.ZHI_SUN.getValue());
    private static final BigDecimal TOTAL_ORDER_USDT_FACTOR = new BigDecimal(OrderParamEnums.TOTAL_ORDER_USDT.getValue());
    private static final BigDecimal EVERY_TIME_USDT_FACTOR = new BigDecimal(OrderParamEnums.EVERY_TIME_USDT.getValue());
 
    /**
     * 订阅账户频道
     *
     * @param webSocketClient WebSocket 客户端实例
     * @param option          请求选项(如 unsubscribe 或 subscribe)
     */
    public static void subscribeAccountChannel(WebSocketClient webSocketClient, String option) {
        try {
            JSONArray argsArray = new JSONArray();
            JSONObject args = new JSONObject();
            args.put("channel", ACCOUNTWS_CHANNEL);
            args.put(CCY_KEY, CoinEnums.USDT.getCode());
            argsArray.add(args);
 
            String connId = MallUtils.getOrderNum(ACCOUNTWS_CHANNEL);
            JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, option, argsArray);
            webSocketClient.send(jsonObject.toJSONString());
            log.info("发送账户频道:{}", option);
        } catch (Exception e) {
            log.error("订阅账户频道构建失败", e);
        }
    }
 
    /**
     * 处理账户频道推送的数据
     *
     * @param response   推送的 JSON 数据对象
     * @param redisUtils Redis 工具类实例,用于存储账户相关信息
     */
    public static void handleEvent(JSONObject response, RedisUtils redisUtils) {
 
 
        log.info("开始执行AccountWs......");
        try {
            JSONArray dataArray = response.getJSONArray(DATA_KEY);
            if (dataArray == null || dataArray.isEmpty()) {
                log.warn("账户频道数据为空");
                return;
            }
 
            for (int i = 0; i < dataArray.size(); i++) {
                try {
                    JSONObject accountData = dataArray.getJSONObject(i);
                    JSONArray detailsArray = accountData.getJSONArray(DETAILS_KEY);
                    if (detailsArray == null || detailsArray.isEmpty()) {
                        log.warn("账户频道{}数据为空",CoinEnums.USDT.getCode());
                        continue;
                    }
 
                    for (int j = 0; j < detailsArray.size(); j++) {
                        JSONObject detail = detailsArray.getJSONObject(j);
 
                        String ccy = detail.getString(CCY_KEY);
                        String availBalStr = detail.getString(AVAIL_BAL_KEY);
                        String cashBalStr = detail.getString(CASH_BAL_KEY);
                        String eq = detail.getString(EQ_KEY);
 
                        if (StrUtil.isBlank(ccy) || StrUtil.isBlank(availBalStr) || StrUtil.isBlank(cashBalStr)) {
                            log.warn("账户频道缺失必要字段,跳过处理");
                            continue;
                        }
 
                        BigDecimal availBal = parseBigDecimalSafe(availBalStr);
                        BigDecimal cashBal = parseBigDecimalSafe(cashBalStr);
 
                        if (availBal == null || cashBal == null || cashBal.compareTo(BigDecimal.ZERO) == 0) {
                            log.warn("账户频道无效的账户余额数据,跳过处理");
                            continue;
                        }
 
                        // 可用余额 / 现金余额 比例判断是否允许开仓
                        BigDecimal divide = availBal.divide(cashBal, 4, RoundingMode.DOWN);
 
                        String state = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state");
                        String out = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":out");
                        if (OrderParamEnums.STATE_4.getValue().equals(state)){
                            log.info(OrderParamEnums.STATE_4.getName());
                            state = OrderParamEnums.STATE_4.getValue();
                        }else if(OrderParamEnums.STATE_3.getValue().equals(state) && OrderParamEnums.OUT_YES.getValue().equals(out)){
                            log.info(OrderParamEnums.STATE_3.getName());
                            state = OrderParamEnums.STATE_3.getValue();
                        }else{
                            if (divide.compareTo(KANG_CANG_THRESHOLD) > 0) {
                                log.info(OrderParamEnums.STATE_1.getName());
                                state = OrderParamEnums.STATE_1.getValue();
                            } else if (divide.compareTo(ZHI_SUN_THRESHOLD) > 0) {
                                log.warn(OrderParamEnums.STATE_2.getName());
                                state = OrderParamEnums.STATE_2.getValue();
                            } else {
                                log.error(OrderParamEnums.STATE_0.getName());
                                state = OrderParamEnums.STATE_0.getValue();
                            }
                        }
 
                        // 根据可用余额计算下单总保证金与每次下单金额
                        BigDecimal totalOrderUsdt = availBal.multiply(TOTAL_ORDER_USDT_FACTOR).setScale(4, RoundingMode.DOWN);
                        BigDecimal everyTimeUsdt = totalOrderUsdt.divide(EVERY_TIME_USDT_FACTOR, 4, RoundingMode.DOWN);
 
                        log.info(
                                "账户详情-币种: {}, 可用余额: {}, 现金余额: {}, 余额: {}, 下单总保证金: {}, 每次下单保证金: {}, 是否允许开仓: {}",
                                ccy, availBalStr, cashBalStr, eq, totalOrderUsdt, everyTimeUsdt, state
                        );
 
                        saveToRedis(redisUtils, availBalStr, cashBalStr, totalOrderUsdt.toString(), everyTimeUsdt.toString(), state);
                    }
                } catch (Exception innerEx) {
                    log.warn("处理账户频道数据失败", innerEx);
                }
            }
        } catch (Exception e) {
            log.error("处理账户频道推送数据失败", e);
        }
    }
 
    /**
     * 安全地将字符串解析为 BigDecimal 类型
     *
     * @param value 字符串数值
     * @return 解析后的 BigDecimal 对象,若解析失败则返回 null
     */
    private static BigDecimal parseBigDecimalSafe(String value) {
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException e) {
            log.warn("无法转换为 BigDecimal: {}", value);
            return null;
        }
    }
 
    /**
     * 将账户相关数据保存至 Redis 中
     *
     * @param redisUtils       Redis 工具类实例
     * @param availBal         可用余额
     * @param cashBal          现金余额
     * @param totalOrderUsdt   总下单保证金
     * @param everyTimeUsdt    每次下单保证金
     * @param state            当前账户状态(是否可开仓)
     */
    private static void saveToRedis(RedisUtils redisUtils, String availBal, String cashBal,
                                   String totalOrderUsdt, String everyTimeUsdt, String state) {
        try {
            boolean setResult =
                    redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":availBal", availBal, 0)
                            && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":cashBal", cashBal, 0)
                            && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":totalOrderUsdt", totalOrderUsdt, 0)
                            && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":everyTimeUsdt", everyTimeUsdt, 0)
                            && redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", state, 0);
 
            if (!setResult) {
                log.warn("Redis set operation failed for key: account:{}", CoinEnums.USDT.getCode());
            }
        } catch (Exception e) {
            log.error("Redis操作异常,key: account:{}, error: {}", CoinEnums.USDT.getCode(), e.getMessage(), e);
        }
    }
}