refactor(okx): 重构账户与合约 WebSocket 数据处理逻辑
- 移除 RedisUtils 依赖,改用内存 Map 存储数据
- 新增 WsMapBuild 工具类统一处理数据解析与存储
- 调整账户、合约、订单等模块的数据结构与处理方式
- 优化止盈止损逻辑,使用 BigDecimal 进行精确计算
- 简化操作服务中的状态判断与交易决策流程
- 更新枚举类,增加更多配置项支持灵活策略调整
- 修复数据处理过程中的空指针异常风险
- 提升代码可读性与维护性,减少外部依赖耦合度
10 files modified
1 files added
| | |
| | | } |
| | | |
| | | try { |
| | | InstrumentsWs.handleEvent(redisUtils); |
| | | InstrumentsWs.handleEvent(); |
| | | wangGeService.initWangGe(); |
| | | SSLConfig.configureSSL(); |
| | | System.setProperty("https.protocols", "TLSv1.2,TLSv1.3"); |
| | |
| | | if (OrderInfoWs.ORDERINFOWS_CHANNEL.equals(channel)) { |
| | | OrderInfoWs.handleEvent(response, redisUtils); |
| | | }else if (AccountWs.ACCOUNTWS_CHANNEL.equals(channel)) { |
| | | AccountWs.handleEvent(response, redisUtils); |
| | | AccountWs.handleEvent(response); |
| | | String side = caoZuoService.caoZuo(); |
| | | TradeOrderWs.orderEvent(webSocketClient, redisUtils, side); |
| | | TradeOrderWs.orderEvent(webSocketClient, side); |
| | | } else if (PositionsWs.POSITIONSWS_CHANNEL.equals(channel)) { |
| | | PositionsWs.handleEvent(response, redisUtils); |
| | | PositionsWs.handleEvent(response); |
| | | } else if (BalanceAndPositionWs.CHANNEL_NAME.equals(channel)) { |
| | | BalanceAndPositionWs.handleEvent(response); |
| | | } |
| | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.*; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsMapBuild; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeQueue; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeService; |
| | | import com.xcong.excoin.rabbit.pricequeue.AscBigDecimal; |
| | |
| | | @RequiredArgsConstructor |
| | | public class CaoZuoServiceImpl implements CaoZuoService { |
| | | |
| | | private final RedisUtils redisUtils; |
| | | private final WangGeService wangGeService; |
| | | |
| | | |
| | | // 构造Redis键名 |
| | | final String coinCode = CoinEnums.HE_YUE.getCode(); |
| | | final String instrumentsStateKey = InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + coinCode + ":state"; |
| | | final String instrumentsOutKey = InstrumentsWs.INSTRUMENTSWS_CHANNEL+":" + coinCode+":out"; |
| | | final String positionsMarkPxKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":markPx"; |
| | | final String positionsAvgPxKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":avgPx"; |
| | | final String positionsOrderPriceKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":orderPrice"; |
| | | final String positionsUplKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":upl"; |
| | | final String positionsRealizedPnlKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":realizedPnl"; |
| | | final String positionsImrKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":imr"; |
| | | final String positionsPosKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":pos"; |
| | | |
| | | /** |
| | | * 执行主要的操作逻辑,包括读取合约状态、获取市场价格信息, |
| | | * 并根据当前持仓均价和标记价格决定是否执行买卖操作。 |
| | | * |
| | | * @return 返回操作类型字符串(如买入BUY、卖出SELL等) |
| | | * @return 返回操作类型字符串(如买入BUY、卖出SELL等),如果无有效操作则返回null |
| | | */ |
| | | @Override |
| | | public String caoZuo() { |
| | | String state = (String) redisUtils.get(instrumentsStateKey); |
| | | log.info("开始执行操作CaoZuoServiceImpl......{}",state); |
| | | log.info("开始执行操作CaoZuoServiceImpl......"); |
| | | BigDecimal cashBal = AccountWs.ACCOUNTWSMAP.get("cashBal"); |
| | | BigDecimal availBal = AccountWs.ACCOUNTWSMAP.get("availBal"); |
| | | |
| | | String live = (String) redisUtils.getWithDelay(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state"); |
| | | if (!CoinEnums.ORDER_LIVE.getCode().equals( live)){ |
| | | log.warn("正在下单中,等待下单结束..."); |
| | | } |
| | | // 获取合约执行操作状态 |
| | | String outStr = (String) redisUtils.get(instrumentsOutKey); |
| | | if (OrderParamEnums.OUT_YES.getValue().equals(outStr) && OrderParamEnums.STATE_3.getValue().equals(state)){ |
| | | log.error("止损过了......冷静一下,等待下次入场......"); |
| | | // 判断账户余额是否充足 |
| | | if (cashBal.compareTo(BigDecimal.ZERO) <= 0){ |
| | | log.error("账户没有钱,请充值......"); |
| | | return null; |
| | | } |
| | | if (OrderParamEnums.STATE_4.getValue().equals(state)) { |
| | | log.error("操作下单中,等待......"); |
| | | return OrderParamEnums.ORDERING.getValue(); |
| | | |
| | | // 系统设置的开关,等于冷静中,则代表不开仓 |
| | | String outStr = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.OUT.name()); |
| | | if (OrderParamEnums.OUT_YES.getValue().equals(outStr)){ |
| | | log.error("冷静中,不允许下单......"); |
| | | return null; |
| | | } |
| | | |
| | | // 判断当前是否有正在进行的订单操作 |
| | | String state = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.STATE.name()); |
| | | log.info(OrderParamEnums.getNameByValue(state)); |
| | | if (OrderParamEnums.STATE_4.getValue().equals(state)){ |
| | | log.warn("正在下单中,等待下单结束..."); |
| | | return null; |
| | | } |
| | | if (OrderParamEnums.STATE_3.getValue().equals(state)){ |
| | | log.error("持仓盈亏超过下单总保证金,冷静止损......"); |
| | | redisUtils.set(instrumentsOutKey, OrderParamEnums.OUT_YES.getValue(), 0); |
| | | return OrderParamEnums.OUT.getValue(); |
| | | log.error("冷静中,不允许下单......"); |
| | | return null; |
| | | } |
| | | if (OrderParamEnums.STATE_2.getValue().equals(state)){ |
| | | log.error("持仓盈亏抗压......"); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | log.error("账户紧张扛仓......"); |
| | | return null; |
| | | } |
| | | if (OrderParamEnums.STATE_0.getValue().equals(state)){ |
| | | log.error("请检查系统参数,不允许开仓......"); |
| | | log.error("参数异常,不允许开仓......"); |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 判断止损抗压 |
| | | */ |
| | | // 实际亏损金额 |
| | | BigDecimal realKuiSunAmount = cashBal.subtract(availBal); |
| | | log.info("实际亏损金额: {}", realKuiSunAmount); |
| | | String zhiSunPercent = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.ZHI_SUN.name()); |
| | | BigDecimal zhiSunAmount = cashBal.multiply(new BigDecimal(zhiSunPercent)); |
| | | log.info("预期亏损金额: {}", realKuiSunAmount); |
| | | String kangYaPercent = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.KANG_CANG.name()); |
| | | log.info("预期抗仓金额: {}", kangYaPercent); |
| | | BigDecimal kangYaAmount = cashBal.multiply(new BigDecimal(kangYaPercent)); |
| | | |
| | | // 账户预期亏损金额比这个还小时,立即止损 |
| | | if (realKuiSunAmount.compareTo(zhiSunAmount) > 0){ |
| | | log.error("账户冷静止损......"); |
| | | WsMapBuild.saveStringToMap(InstrumentsWs.INSTRUMENTSWSMAP, CoinEnums.OUT.name(), OrderParamEnums.OUT_YES.getValue()); |
| | | return OrderParamEnums.OUT.getValue(); |
| | | } |
| | | // 判断抗压 |
| | | if (realKuiSunAmount.compareTo(kangYaAmount) > 0 && realKuiSunAmount.compareTo(zhiSunAmount) <= 0){ |
| | | log.error("账户紧张扛仓......"); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | |
| | | String pos = (String) redisUtils.get(positionsPosKey); |
| | | if (StrUtil.isBlank(pos) || BigDecimal.ZERO.compareTo( new BigDecimal(pos)) >= 0) { |
| | | log.error("未获取到持仓数量"); |
| | | BigDecimal ordFroz = AccountWs.ACCOUNTWSMAP.get("ordFroz"); |
| | | if (BigDecimal.ZERO.compareTo( ordFroz) >= 0) { |
| | | log.error("占用保证金为零,进行初始化订单"); |
| | | return OrderParamEnums.INIT.getValue(); |
| | | } |
| | | |
| | | String uplStr = (String) redisUtils.get(positionsUplKey); |
| | | //可使用的总保证金 |
| | | String cashBalStrKey = AccountWs.ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":cashBal"; |
| | | String cashBalStr = (String) redisUtils.get(cashBalStrKey); |
| | | if (StrUtil.isBlank(cashBalStr) || StrUtil.isBlank(uplStr)){ |
| | | return OrderParamEnums.INIT.getValue(); |
| | | } |
| | | BigDecimal upl = new BigDecimal(uplStr); |
| | | if (BigDecimal.ZERO.compareTo(upl) >= 0){ |
| | | upl = upl.multiply(new BigDecimal("-1")); |
| | | BigDecimal bigDecimal = new BigDecimal(cashBalStr).multiply(new BigDecimal(OrderParamEnums.ZHI_SUN.getValue())); |
| | | if (upl.compareTo(bigDecimal) >= 0) { |
| | | log.error("持仓盈亏超过下单总保证金,止损冷静一天......"); |
| | | return OrderParamEnums.OUT.getValue(); |
| | | } |
| | | } |
| | | |
| | | log.info(OrderParamEnums.getNameByValue(state)); |
| | | |
| | | // 获取标记价格和平均持仓价格 |
| | | String markPxObj = (String) redisUtils.get(positionsMarkPxKey); |
| | | String avgPxObj = (String) redisUtils.get(positionsAvgPxKey); |
| | | |
| | | if (StrUtil.isBlank(markPxObj) || StrUtil.isBlank(avgPxObj)) { |
| | | return OrderParamEnums.INIT.getValue(); |
| | | // 判断是否保证金超标 |
| | | BigDecimal totalOrderUsdt = AccountWs.ACCOUNTWSMAP.get(CoinEnums.TOTAL_ORDER_USDT.name()); |
| | | if (ordFroz.compareTo(totalOrderUsdt) >= 0){ |
| | | log.error("已满仓......"); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | |
| | | try { |
| | | BigDecimal markPx = new BigDecimal( markPxObj); |
| | | BigDecimal avgPx = new BigDecimal( avgPxObj); |
| | | |
| | | // 获取标记价格和平均持仓价格 |
| | | BigDecimal markPx = PositionsWs.POSITIONSWSMAP.get("markPx"); |
| | | BigDecimal avgPx = PositionsWs.POSITIONSWSMAP.get("avgPx"); |
| | | log.info("开仓价格: {}, 当前价格:{},匹配队列中......", avgPx, markPx); |
| | | |
| | | // 初始化网格队列 |
| | |
| | | PriorityBlockingQueue<AscBigDecimal> queuePingCang = wangGeService.initPingCang(avgPx, queueAsc); |
| | | |
| | | // 处理订单价格在队列中的情况 |
| | | String orderPrice = (String) redisUtils.get(positionsOrderPriceKey); |
| | | String orderPrice = OrderInfoWs.ORDERINFOWSMAP.get("orderPrice"); |
| | | handleOrderPriceInQueues(orderPrice, queueKaiCang, queuePingCang); |
| | | |
| | | String side = OrderParamEnums.HOLDING.getValue(); |
| | | |
| | | // 判断是加仓还是减仓 |
| | | if (avgPx.compareTo(markPx) > 0) { |
| | |
| | | if (queueKaiCang.isEmpty()) { |
| | | // 队列为空 |
| | | log.info("开始加仓,但是超出了网格设置..."); |
| | | return side; |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | DescBigDecimal kaiCang = queueKaiCang.peek(); |
| | | if (kaiCang != null && markPx.compareTo(kaiCang.getValue()) <= 0 && avgPx.compareTo(kaiCang.getValue()) >= 0) { |
| | | log.info("开始加仓...开仓队列价格大于当前价格{}>{}", kaiCang.getValue(), markPx); |
| | | side = OrderParamEnums.BUY.getValue(); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(kaiCang.getValue()), 0); |
| | | WsMapBuild.saveStringToMap(OrderInfoWs.ORDERINFOWSMAP, "orderPrice",String.valueOf(kaiCang.getValue())); |
| | | return OrderParamEnums.BUY.getValue(); |
| | | } else { |
| | | //判断是否加仓(当前持仓过小,可以加仓) |
| | | boolean isAddCang = doAddCang(); |
| | | log.info("加仓过程中发现持仓过小 :{}",isAddCang); |
| | | if (isAddCang){ |
| | | log.info("触发加仓......,持仓过小"); |
| | | return OrderParamEnums.BUY.getValue(); |
| | | } |
| | | log.info("未触发加仓......,等待"); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | } else if (avgPx.compareTo(markPx) < 0) { |
| | | log.info("开始减仓..."); |
| | | if (queuePingCang.isEmpty()) { |
| | | // 队列为空 |
| | | log.info("开始减仓,但是超出了网格设置..."); |
| | | return side; |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | AscBigDecimal pingCang = queuePingCang.peek(); |
| | | if (pingCang != null && markPx.compareTo(pingCang.getValue()) >= 0 && avgPx.compareTo(pingCang.getValue()) < 0) { |
| | | log.info("开始减仓...平仓队列价格小于当前价格{}<={}", pingCang.getValue(), markPx); |
| | | //判断当前是否盈利 |
| | | String uplstr = (String) redisUtils.get(positionsUplKey); |
| | | String realizedPnl = (String) redisUtils.get(positionsRealizedPnlKey); |
| | | String imr = (String) redisUtils.get(positionsImrKey); |
| | | if (uplstr != null && realizedPnl != null && imr != null) { |
| | | BigDecimal uplValue = new BigDecimal(uplstr); |
| | | BigDecimal realizedPnlValue = new BigDecimal(realizedPnl); |
| | | BigDecimal imrValue = new BigDecimal(imr).multiply(new BigDecimal(OrderParamEnums.PING_CANG_SHOUYI.getValue())); |
| | | if (realizedPnlValue.compareTo(BigDecimal.ZERO) <= 0) { |
| | | BigDecimal realizedPnlValueZheng = realizedPnlValue.multiply(new BigDecimal("-1")); |
| | | if (uplValue.compareTo(realizedPnlValue) > 0 && uplValue.compareTo(imrValue.add(realizedPnlValueZheng)) >= 0) { |
| | | log.info("当前未实现盈亏:{}大于预计收益>{},赚钱咯", uplValue, imrValue); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(pingCang.getValue()), 0); |
| | | return OrderParamEnums.SELL.getValue(); |
| | | }else{ |
| | | //判断是否加仓(当前持仓过小,可以加仓) |
| | | boolean isAddCang = doAddCang(); |
| | | log.info("减仓过程中发现持仓过小 :{}",isAddCang); |
| | | if (isAddCang){ |
| | | log.info("触发加仓......,持仓过小"); |
| | | return OrderParamEnums.BUY.getValue(); |
| | | } |
| | | log.info("当前未实现盈亏:{}没有大于预计收益>{},钱在路上了", uplValue, imrValue); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(pingCang.getValue()), 0); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | WsMapBuild.saveStringToMap(OrderInfoWs.ORDERINFOWSMAP, "orderPrice",String.valueOf(pingCang.getValue())); |
| | | |
| | | }else { |
| | | if (uplValue.compareTo(imrValue) >= 0) { |
| | | log.info("当前未实现盈亏:{}大于预计收益>{},赚钱咯", uplValue, imrValue); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(pingCang.getValue()), 0); |
| | | return OrderParamEnums.SELL.getValue(); |
| | | }else{ |
| | | //判断是否加仓(当前持仓过小,可以加仓) |
| | | boolean isAddCang = doAddCang(); |
| | | log.info("减仓过程中发现持仓过小 :{}",isAddCang); |
| | | if (isAddCang){ |
| | | log.info("触发加仓......,持仓过小"); |
| | | return OrderParamEnums.BUY.getValue(); |
| | | } |
| | | log.info("当前未实现盈亏:{}没有大于预计收益>{},钱在路上了", uplValue, imrValue); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(pingCang.getValue()), 0); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | } |
| | | // 判断当前是否盈利 |
| | | BigDecimal uplValue = PositionsWs.POSITIONSWSMAP.get("upl"); |
| | | BigDecimal imr = PositionsWs.POSITIONSWSMAP.get("imr"); |
| | | BigDecimal realizedPnlValue = PositionsWs.POSITIONSWSMAP.get("realizedPnl"); |
| | | String pingCangImr = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.PING_CANG_SHOUYI.name()); |
| | | BigDecimal imrValue = imr.multiply(new BigDecimal(pingCangImr)); |
| | | |
| | | if (realizedPnlValue.compareTo(BigDecimal.ZERO) <= 0) { |
| | | BigDecimal realizedPnlValueZheng = realizedPnlValue.multiply(new BigDecimal("-1")); |
| | | if (uplValue.compareTo(realizedPnlValue) > 0 && uplValue.compareTo(imrValue.add(realizedPnlValueZheng)) >= 0) { |
| | | log.info("当前未实现盈亏:{}大于预计收益>{},赚钱咯", uplValue, imrValue); |
| | | return OrderParamEnums.SELL.getValue(); |
| | | }else{ |
| | | log.info("当前未实现盈亏:{}没有大于预计收益>{},钱在路上了", uplValue, imrValue); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | }else { |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | if (uplValue.compareTo(imrValue) >= 0) { |
| | | log.info("当前未实现盈亏:{}大于预计收益>{},赚钱咯", uplValue, imrValue); |
| | | return OrderParamEnums.SELL.getValue(); |
| | | }else{ |
| | | log.info("当前未实现盈亏:{}没有大于预计收益>{},钱在路上了", uplValue, imrValue); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | } |
| | | } else { |
| | | log.info("未触发减仓......,等待"); |
| | |
| | | } else { |
| | | log.info("价格波动较小......,等待"); |
| | | } |
| | | |
| | | return side; |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } catch (NumberFormatException e) { |
| | | log.error("解析价格失败,请检查Redis中的值是否合法", e); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | } |
| | | |
| | | private boolean doAddCang() { |
| | | String imr = (String) redisUtils.get(positionsImrKey); |
| | | BigDecimal imrValue = new BigDecimal(StrUtil.isBlank(imr) ? "0" : imr); |
| | | String everyTimeUsdt = (String) redisUtils.get(AccountWs.ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":everyTimeUsdt"); |
| | | BigDecimal everyTimeUsdtValue = new BigDecimal(everyTimeUsdt); |
| | | return everyTimeUsdtValue.compareTo(imrValue) >= 0; |
| | | } |
| | | |
| | | /** |
| | |
| | | log.warn("无效的价格格式: {}", orderPrice); |
| | | return; |
| | | } |
| | | |
| | | // 删除比该价格大的数据 |
| | | queueKaiCang.removeIf(item -> item.getValue().compareTo(priceDecimal) >= 0); |
| | | |
| | | // 打印开仓队列 |
| | | StringBuilder kaiCangStr = new StringBuilder(); |
| | | kaiCangStr.append("开仓队列: ["); |
| | |
| | | log.info(pingCangStr.toString()); |
| | | } |
| | | } |
| | | |
| | |
| | | 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.WsMapBuild; |
| | | 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; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 账户 WebSocket 处理类,用于订阅 OKX 的账户频道并处理账户信息推送。 |
| | |
| | | @Slf4j |
| | | public class AccountWs { |
| | | |
| | | public static final Map<String,BigDecimal> ACCOUNTWSMAP = new ConcurrentHashMap<>(); |
| | | /** |
| | | * 账户频道名称常量 |
| | | */ |
| | | 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()); |
| | | |
| | | /** |
| | | * 订阅账户频道 |
| | |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("channel", ACCOUNTWS_CHANNEL); |
| | | args.put(CCY_KEY, CoinEnums.USDT.getCode()); |
| | | args.put("ccy", CoinEnums.USDT.getCode()); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(ACCOUNTWS_CHANNEL); |
| | |
| | | * 处理账户频道推送的数据 |
| | | * |
| | | * @param response 推送的 JSON 数据对象 |
| | | * @param redisUtils Redis 工具类实例,用于存储账户相关信息 |
| | | */ |
| | | public static void handleEvent(JSONObject response, RedisUtils redisUtils) { |
| | | public static void handleEvent(JSONObject response) { |
| | | |
| | | |
| | | String state = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state"); |
| | | |
| | | log.info("开始执行AccountWs......{}",state); |
| | | log.info("开始执行AccountWs......{}",ACCOUNTWS_CHANNEL); |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray(DATA_KEY); |
| | | JSONArray dataArray = response.getJSONArray("data"); |
| | | 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); |
| | | JSONArray detailsArray = accountData.getJSONArray("details"); |
| | | 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 ccyKey = "ccy"; |
| | | String availBalKey = "availBal"; |
| | | String cashBalKey = "cashBal"; |
| | | String eqKey = "eq"; |
| | | String ordFrozKey = "ordFroz"; |
| | | |
| | | 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); |
| | | BigDecimal ccy = WsMapBuild.parseBigDecimalSafe(detail.getString(ccyKey)); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, ccyKey, ccy); |
| | | |
| | | if (StrUtil.isBlank(ccy) || StrUtil.isBlank(availBalStr) || StrUtil.isBlank(cashBalStr)) { |
| | | log.warn("账户频道缺失必要字段,跳过处理"); |
| | | continue; |
| | | } |
| | | BigDecimal availBal = WsMapBuild.parseBigDecimalSafe(detail.getString(availBalKey)); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, availBalKey, availBal); |
| | | |
| | | BigDecimal availBal = parseBigDecimalSafe(availBalStr); |
| | | BigDecimal cashBal = parseBigDecimalSafe(cashBalStr); |
| | | BigDecimal cashBal = WsMapBuild.parseBigDecimalSafe(detail.getString(cashBalKey)); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, cashBalKey, cashBal); |
| | | |
| | | if (availBal == null || cashBal == null || cashBal.compareTo(BigDecimal.ZERO) == 0) { |
| | | BigDecimal eq = WsMapBuild.parseBigDecimalSafe(detail.getString(eqKey)); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, eqKey, eq); |
| | | |
| | | BigDecimal ordFroz = WsMapBuild.parseBigDecimalSafe(detail.getString(ordFrozKey)); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, ordFrozKey, ordFroz); |
| | | |
| | | if (cashBal.compareTo(BigDecimal.ZERO) == 0) { |
| | | log.warn("账户频道无效的账户余额数据,跳过处理"); |
| | | continue; |
| | | } |
| | | |
| | | // 可用余额 / 现金余额 比例判断是否允许开仓 |
| | | BigDecimal divide = availBal.divide(cashBal, 4, RoundingMode.DOWN); |
| | | 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)){ |
| | | 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_3.getName()); |
| | | state = OrderParamEnums.STATE_3.getValue(); |
| | | } |
| | | } |
| | | |
| | | // 根据可用余额计算下单总保证金与每次下单金额 |
| | | BigDecimal totalOrderUsdt = availBal.multiply(TOTAL_ORDER_USDT_FACTOR).setScale(4, RoundingMode.DOWN); |
| | | BigDecimal everyTimeUsdt = totalOrderUsdt.divide(EVERY_TIME_USDT_FACTOR, 4, RoundingMode.DOWN); |
| | | // 根据可用余额计算下单总保证金 |
| | | String total_order_usdtpecent = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.TOTAL_ORDER_USDTPECENT.name()); |
| | | BigDecimal total_order_usdt_factor = WsMapBuild.parseBigDecimalSafe(total_order_usdtpecent); |
| | | BigDecimal totalOrderUsdt = availBal.divide(total_order_usdt_factor, 4, RoundingMode.DOWN); |
| | | WsMapBuild.saveBigDecimalToMap(ACCOUNTWSMAP, CoinEnums.TOTAL_ORDER_USDT.name(), totalOrderUsdt); |
| | | |
| | | log.info( |
| | | "账户详情-币种: {}, 可用余额: {}, 现金余额: {}, 余额: {}, 下单总保证金: {}, 每次下单保证金: {}, 是否允许开仓: {}", |
| | | ccy, availBalStr, cashBalStr, eq, totalOrderUsdt, everyTimeUsdt, state |
| | | "账户详情-币种: {}, 可用余额: {}, 现金余额: {}, 余额: {}, 占用保证金: {}, 下单总保证金: {}", |
| | | ccy, availBal, cashBal, eq, ordFroz, totalOrderUsdt |
| | | ); |
| | | |
| | | saveToRedis(redisUtils, availBalStr, cashBalStr, totalOrderUsdt.toString(), everyTimeUsdt.toString(), state); |
| | | } |
| | | } catch (Exception innerEx) { |
| | | log.warn("处理账户频道数据失败", innerEx); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全地将字符串解析为 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsMapBuild; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * @author Administrator |
| | |
| | | @Slf4j |
| | | public class InstrumentsWs { |
| | | |
| | | public static final Map<String, String> INSTRUMENTSWSMAP = new ConcurrentHashMap<>(); |
| | | |
| | | public static final String INSTRUMENTSWS_CHANNEL = "instruments"; |
| | | |
| | | public static void handleEvent(RedisUtils redisUtils) { |
| | | public static void handleEvent() { |
| | | log.info("开始执行InstrumentsWs......"); |
| | | // 将账户数据保存到Redis中,设置过期时间为30分钟 |
| | | try { |
| | | boolean setResult = |
| | | redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":instId", CoinEnums.HE_YUE.getCode(), 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":ctVal", "0.01", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":tickSz", "2", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":minSz", "2", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":instIdCode", CoinEnums.HE_YUE.getCode(), 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":state", OrderParamEnums.STATE_0.getValue(), 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":out", OrderParamEnums.OUT_NO.getValue(), 0) |
| | | ; |
| | | if (!setResult) { |
| | | log.warn("Redis set operation failed for key: account:{}", CoinEnums.HE_YUE.getCode()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Redis操作异常,key: account:{}, error: {}", CoinEnums.HE_YUE.getCode(), e.getMessage(), e); |
| | | } |
| | | |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.HE_YUE.name(), CoinEnums.HE_YUE.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.CTVAL.name(), CoinEnums.CTVAL.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.TICKSZ.name(), CoinEnums.TICKSZ.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.MINSZ.name(), CoinEnums.MINSZ.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.STATE.name(), OrderParamEnums.STATE_1.getValue()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.OUT.name(), OrderParamEnums.OUT_NO.getValue()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.LEVERAGE.name(), CoinEnums.LEVERAGE.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.BUY_CNT.name(), CoinEnums.BUY_CNT.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.ZHI_SUN.name(), CoinEnums.ZHI_SUN.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.KANG_CANG.name(), CoinEnums.KANG_CANG.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.PING_CANG_SHOUYI.name(), CoinEnums.PING_CANG_SHOUYI.getCode()); |
| | | WsMapBuild.saveStringToMap(INSTRUMENTSWSMAP, CoinEnums.TOTAL_ORDER_USDTPECENT.name(), CoinEnums.TOTAL_ORDER_USDTPECENT.getCode()); |
| | | } |
| | | } |
| | |
| | | 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.WsMapBuild; |
| | | 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.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class OrderInfoWs { |
| | | |
| | | public static final Map<String,String> ORDERINFOWSMAP = new ConcurrentHashMap<>(); |
| | | |
| | | public static final String ORDERINFOWS_CHANNEL = "orders"; |
| | | |
| | |
| | | accFillSz, avgPx,state |
| | | ); |
| | | |
| | | String clOrdIdStr = (String) redisUtils.get(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":clOrdId"); |
| | | String stateStr = (String) redisUtils.get(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state"); |
| | | String clOrdIdStr = TradeOrderWs.TRADEORDERWSMAP.get("clOrdId"); |
| | | String stateStr = TradeOrderWs.TRADEORDERWSMAP.get("state"); |
| | | if ( |
| | | StrUtil.isNotBlank(clOrdIdStr) |
| | | && clOrdId.equals(clOrdIdStr) |
| | |
| | | && state.equals(stateStr) |
| | | && !CoinEnums.ORDER_LIVE.getCode().equals(state) |
| | | ){ |
| | | boolean setResult = false; |
| | | String outStr = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":out"); |
| | | //记录成交均价 |
| | | if (StrUtil.isBlank(ORDERINFOWSMAP.get("orderPrice"))){ |
| | | WsMapBuild.saveStringToMap(ORDERINFOWSMAP, "orderPrice",avgPx); |
| | | } |
| | | |
| | | WsMapBuild.saveStringToMap(TradeOrderWs.TRADEORDERWSMAP, "state", CoinEnums.ORDER_LIVE.getCode()); |
| | | |
| | | String outStr = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.OUT.name()); |
| | | if (OrderParamEnums.OUT_YES.getValue().equals(outStr)){ |
| | | setResult = redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", OrderParamEnums.STATE_3.getValue(), 0); |
| | | WsMapBuild.saveStringToMap(InstrumentsWs.INSTRUMENTSWSMAP, CoinEnums.STATE.name(), OrderParamEnums.STATE_3.getValue()); |
| | | }else{ |
| | | setResult = redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", OrderParamEnums.STATE_1.getValue(), 0); |
| | | WsMapBuild.saveStringToMap(InstrumentsWs.INSTRUMENTSWSMAP, CoinEnums.STATE.name(), OrderParamEnums.STATE_1.getValue()); |
| | | } |
| | | if (setResult){ |
| | | redisUtils.set(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", CoinEnums.ORDER_LIVE.getCode(), 0); |
| | | log.info("订单详情-币种: {}, 自定义编号: {}, 订单状态: {}", CoinEnums.HE_YUE.getCode(), clOrdId, OrderParamEnums.STATE_1.getValue()); |
| | | } |
| | | log.info("订单详情已完成: {}, 自定义编号: {}", CoinEnums.HE_YUE.getCode(), clOrdId); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | |
| | | 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.WsMapBuild; |
| | | 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 org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Optional; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * @author Administrator |
| | |
| | | @Slf4j |
| | | public class PositionsWs { |
| | | |
| | | public static final Map<String,BigDecimal> POSITIONSWSMAP = new ConcurrentHashMap<>(); |
| | | |
| | | public static final String POSITIONSWS_CHANNEL = "positions"; |
| | | private static final String REDIS_KEY_PREFIX = POSITIONSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode(); |
| | | |
| | | public static void subscribePositionChannel(WebSocketClient webSocketClient, String option) { |
| | | try { |
| | |
| | | } |
| | | } |
| | | |
| | | public static void handleEvent(JSONObject response, RedisUtils redisUtils) { |
| | | public static void handleEvent(JSONObject response) { |
| | | |
| | | |
| | | log.info("开始执行PositionsWs......"); |
| | |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.info("账户持仓频道数据为空,已当前价买入,并且初始化网格"); |
| | | JSONObject posData = new JSONObject(); |
| | | processPositionData(posData, redisUtils); |
| | | processPositionData(posData); |
| | | return; |
| | | } |
| | | |
| | |
| | | last, idxPx, bePx, realizedPnl, settledPnl, |
| | | markPx |
| | | ); |
| | | processPositionData(posData, redisUtils); |
| | | |
| | | processPositionData(posData); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | |
| | | } |
| | | } |
| | | |
| | | private static void processPositionData(JSONObject posData, RedisUtils redisUtils) { |
| | | try { |
| | | String avgPx = safeGetString(posData, "avgPx"); |
| | | String pos = safeGetString(posData, "pos"); |
| | | String upl = safeGetString(posData, "upl"); |
| | | String imr = safeGetString(posData, "imr"); |
| | | String mgnRatio = safeGetString(posData, "mgnRatio"); |
| | | String markPx = safeGetString(posData, "markPx"); |
| | | String bePx = safeGetString(posData, "bePx"); |
| | | String realizedPnl = safeGetString(posData, "realizedPnl"); |
| | | private static void processPositionData(JSONObject posData) { |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "avgPx", WsMapBuild.parseBigDecimalSafe(posData.getString("avgPx"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "pos", WsMapBuild.parseBigDecimalSafe(posData.getString("pos"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "upl", WsMapBuild.parseBigDecimalSafe(posData.getString("upl"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "imr", WsMapBuild.parseBigDecimalSafe(posData.getString("imr"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "mgnRatio", WsMapBuild.parseBigDecimalSafe(posData.getString("mgnRatio"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "markPx", WsMapBuild.parseBigDecimalSafe(posData.getString("markPx"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "bePx", WsMapBuild.parseBigDecimalSafe(posData.getString("bePx"))); |
| | | WsMapBuild.saveBigDecimalToMap(POSITIONSWSMAP, "realizedPnl", WsMapBuild.parseBigDecimalSafe(posData.getString("realizedPnl"))); |
| | | |
| | | boolean setResult = saveToRedis(redisUtils, avgPx, pos, upl, imr, mgnRatio, markPx, bePx,realizedPnl); |
| | | |
| | | if (setResult) { |
| | | calculateAndSaveBuyCount(redisUtils); |
| | | } else { |
| | | log.warn("Redis操作失败"); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Redis操作异常", e); |
| | | } |
| | | } |
| | | |
| | | private static boolean saveToRedis(RedisUtils redisUtils, |
| | | String avgPx, String pos, String upl, |
| | | String imr, String mgnRatio, String markPx, String bePx, String realizedPnl) { |
| | | return redisUtils.set(REDIS_KEY_PREFIX + ":avgPx", avgPx, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":pos", pos, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":upl", upl, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":imr", imr, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":mgnRatio", mgnRatio, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":markPx", markPx, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":bePx", bePx, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":realizedPnl", realizedPnl, 0); |
| | | } |
| | | |
| | | private static void calculateAndSaveBuyCount(RedisUtils redisUtils) { |
| | | try { |
| | | String ctValStr = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":ctVal"); |
| | | String markPxStr = (String) redisUtils.get(REDIS_KEY_PREFIX + ":markPx"); |
| | | String minSzStr = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":minSz"); |
| | | String everyTimeUsdt = (String) redisUtils.get(AccountWs.ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":everyTimeUsdt"); |
| | | |
| | | BigDecimal margin = parseBigDecimal(everyTimeUsdt, "0"); |
| | | BigDecimal leverage = parseBigDecimal(OrderParamEnums.LEVERAGE.getValue(), "1"); |
| | | BigDecimal faceValue = parseBigDecimal(ctValStr, "0"); |
| | | BigDecimal markPrice = parseBigDecimal(markPxStr, "0"); // 默认值需谨慎对待 |
| | | int minLotSz = parseInt(minSzStr, 0); |
| | | |
| | | BigDecimal buyCnt = buyCnt(margin, leverage, faceValue, markPrice, minLotSz); |
| | | redisUtils.set(REDIS_KEY_PREFIX + ":buyCnt", buyCnt.toString(), 0); |
| | | } catch (NumberFormatException | ArithmeticException e) { |
| | | log.error("计算购买数量时发生数字转换错误", e); |
| | | } |
| | | } |
| | | |
| | | private static String safeGetString(JSONObject obj, String key) { |
| | | return Optional.ofNullable(obj.getString(key)).orElse("0"); |
| | | } |
| | | |
| | | private static BigDecimal parseBigDecimal(String value, String defaultValue) { |
| | | return new BigDecimal(Optional.ofNullable(value).filter(s -> !s.isEmpty()).orElse(defaultValue)); |
| | | } |
| | | |
| | | private static int parseInt(String value, int defaultValue) { |
| | | try { |
| | | return Integer.parseInt(value); |
| | | } catch (NumberFormatException e) { |
| | | return defaultValue; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算购买合约的数量 |
| | | * |
| | | * USDT 币本位合约 |
| | | * 公式:张数 = 保证金 / (面值 * 标记价格 / 杠杆倍数) |
| | | * |
| | | * @param margin 用户的保证金金额 |
| | | * @param leverage 杠杆倍数 |
| | | * @param faceValue 合约面值 |
| | | * @param markPrice 标记价格 |
| | | * @param minLotSz 最小下单精度 |
| | | * @return 返回用户可以购买的合约数量 |
| | | */ |
| | | public static BigDecimal buyCnt(BigDecimal margin, BigDecimal leverage, BigDecimal faceValue, BigDecimal markPrice, int minLotSz) { |
| | | if (margin.compareTo(BigDecimal.ZERO) <= 0 || |
| | | leverage.compareTo(BigDecimal.ZERO) <= 0 || |
| | | faceValue.compareTo(BigDecimal.ZERO) <= 0 || |
| | | markPrice.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | BigDecimal divisor = markPrice.divide(leverage, 10, BigDecimal.ROUND_DOWN); |
| | | BigDecimal denominator = faceValue.multiply(divisor); |
| | | return margin.divide(denominator, minLotSz, BigDecimal.ROUND_DOWN); |
| | | } |
| | | } |
| | |
| | | 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.WsMapBuild; |
| | | 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.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 交易订单处理类,负责构建和发送订单请求到OKX WebSocket |
| | |
| | | @Slf4j |
| | | public class TradeOrderWs { |
| | | |
| | | public static final Map<String,String> TRADEORDERWSMAP = new ConcurrentHashMap<>(); |
| | | |
| | | public static final String ORDERWS_CHANNEL = "order"; |
| | | |
| | | public static void orderEvent(WebSocketClient webSocketClient, RedisUtils redisUtils, String side) { |
| | | public static void orderEvent(WebSocketClient webSocketClient, String side) { |
| | | |
| | | log.info("开始执行TradeOrderWs......"); |
| | | if (StrUtil.isBlank( side)){ |
| | | log.warn("止损了,下次再战..."); |
| | | return; |
| | | } |
| | | String live = (String) redisUtils.getWithDelay(ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state"); |
| | | if (!CoinEnums.ORDER_LIVE.getCode().equals( live)){ |
| | | log.warn("正在下单中,等待下单结束..."); |
| | | return; |
| | | } |
| | | |
| | | String buyCnt = null; |
| | | String ctval = getRedisValue(redisUtils, InstrumentsWs.INSTRUMENTSWS_CHANNEL, ":ctVal"); |
| | | String buyCntNormal = getRedisValue(redisUtils, PositionsWs.POSITIONSWS_CHANNEL, ":buyCnt"); |
| | | String pos = getRedisValue(redisUtils, PositionsWs.POSITIONSWS_CHANNEL, ":pos"); |
| | | if (OrderParamEnums.ORDERING.getValue().equals(side)) { |
| | | log.warn("正在下单中,等待下单结束..."); |
| | | return; |
| | | } else if (OrderParamEnums.HOLDING.getValue().equals(side)) { |
| | | return; |
| | | } else if (OrderParamEnums.INIT.getValue().equals(side)) { |
| | | side = OrderParamEnums.BUY.getValue(); |
| | | if (StrUtil.isNotBlank(buyCntNormal) && new BigDecimal(buyCntNormal).compareTo(BigDecimal.ZERO) > 0) { |
| | | buyCnt = buyCntNormal; |
| | | }else{ |
| | | buyCnt = ctval; |
| | | } |
| | | } else if (OrderParamEnums.OUT.getValue().equals(side)) { |
| | | log.info(OrderParamEnums.getNameByValue(OrderParamEnums.OUT.getValue())); |
| | | side = OrderParamEnums.SELL.getValue(); |
| | | buyCnt = pos; |
| | | } else if (OrderParamEnums.BUY.getValue().equals(side)){ |
| | | side = OrderParamEnums.BUY.getValue(); |
| | | if (StrUtil.isNotBlank(buyCntNormal) && new BigDecimal(buyCntNormal).compareTo(BigDecimal.ZERO) > 0) { |
| | | buyCnt = buyCntNormal; |
| | | }else{ |
| | | buyCnt = ctval; |
| | | } |
| | | }else if (OrderParamEnums.SELL.getValue().equals(side)){ |
| | | side = OrderParamEnums.SELL.getValue(); |
| | | buyCnt = getRedisValue(redisUtils, PositionsWs.POSITIONSWS_CHANNEL, ":pos"); |
| | | }else{ |
| | | log.warn("操作信号异常,请检查下单操作..."); |
| | | return; |
| | | } |
| | | |
| | | // 校验必要参数 |
| | | if (StrUtil.isBlank(side)) { |
| | | log.warn("下单参数 side 为空,取消发送"); |
| | | return; |
| | | } |
| | | String buyCnt = ""; |
| | | if (OrderParamEnums.HOLDING.getValue().equals(side)){ |
| | | log.info("当前状态为持仓中,取消发送"); |
| | | return; |
| | | }else if (OrderParamEnums.OUT.getValue().equals(side)){ |
| | | log.info("当前状态为止损"); |
| | | buyCnt = String.valueOf(PositionsWs.POSITIONSWSMAP.get("pos")); |
| | | }else if (OrderParamEnums.INIT.getValue().equals(side)){ |
| | | log.info("当前状态为初始化"); |
| | | buyCnt = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.BUY_CNT.name()); |
| | | }else if (OrderParamEnums.BUY.getValue().equals(side)){ |
| | | log.info("当前状态为加仓"); |
| | | buyCnt = InstrumentsWs.INSTRUMENTSWSMAP.get(CoinEnums.BUY_CNT.name()); |
| | | }else if (OrderParamEnums.SELL.getValue().equals(side)){ |
| | | log.info("当前状态为减仓"); |
| | | buyCnt = String.valueOf(PositionsWs.POSITIONSWSMAP.get("pos")); |
| | | } |
| | | |
| | | if (StrUtil.isBlank(buyCnt)) { |
| | |
| | | } |
| | | |
| | | try { |
| | | String clOrdId = MallUtils.getOrderNum(side); |
| | | String clOrdId = WsParamBuild.getOrderNum(side); |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", CoinEnums.HE_YUE.getCode()); |
| | |
| | | args.put("sz", buyCnt); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(ORDERWS_CHANNEL); |
| | | String connId = WsParamBuild.getOrderNum(ORDERWS_CHANNEL); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, ORDERWS_CHANNEL, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("发送下单频道:{},数量:{}", side, buyCnt); |
| | | boolean setResult = |
| | | redisUtils.set(ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":clOrdId", clOrdId, 0) |
| | | && redisUtils.set(ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", CoinEnums.ORDER_FILLED.getCode(), 0) |
| | | && redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", OrderParamEnums.STATE_4.getValue(), 0); |
| | | if (!setResult) { |
| | | log.warn("Redis set operation failed for key: order:{}", CoinEnums.HE_YUE.getCode()); |
| | | } |
| | | |
| | | WsMapBuild.saveStringToMap(TRADEORDERWSMAP, "clOrdId", connId); |
| | | WsMapBuild.saveStringToMap(TRADEORDERWSMAP, "state", CoinEnums.ORDER_FILLED.getCode()); |
| | | |
| | | WsMapBuild.saveStringToMap(InstrumentsWs.INSTRUMENTSWSMAP, CoinEnums.STATE.name(), OrderParamEnums.STATE_4.getValue()); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("下单构建失败", e); |
| | | } |
| | |
| | | return profit.setScale(minTickSz, BigDecimal.ROUND_DOWN); |
| | | } |
| | | |
| | | /** |
| | | * 统一封装 Redis Key 构建逻辑 |
| | | * |
| | | * @param redisUtils Redis 工具类实例 |
| | | * @param prefix 渠道前缀 |
| | | * @param suffix 字段后缀 |
| | | * @return Redis 中存储的值 |
| | | */ |
| | | private static String getRedisValue(RedisUtils redisUtils, String prefix, String suffix) { |
| | | String key = prefix + ":" + CoinEnums.HE_YUE.getCode() + suffix; |
| | | Object valueObj = redisUtils.get(key); |
| | | return valueObj == null ? null : String.valueOf(valueObj); |
| | | } |
| | | } |
| | |
| | | * |
| | | * 若不添加该字段或将其设置为上述合法值以外的其他值,数据将根据事件推送并大约每 5 秒定期推送一次 |
| | | */ |
| | | UPDATEINTERVAL("2000", |
| | | "2000"), |
| | | UPDATEINTERVAL("2000","2000"), |
| | | |
| | | ORDER_FILLED("filled","filled"), |
| | | ORDER_LIVE("live","live"), |
| | | |
| | | INSTTYPE_SWAP("SWAP","SWAP"), |
| | | |
| | | ORDTYPE_MARKET("market","market"), |
| | | |
| | | POSSIDE_SHORT("short","short"), |
| | | |
| | | POSSIDE_LONG("long","long"), |
| | | |
| | | SIDE_SELL("sell","sell"), |
| | | |
| | | SIDE_BUY("buy","buy"), |
| | | |
| | | CROSS("cross","cross"), |
| | | |
| | | USDT("USDT","USDT"), |
| | | |
| | | |
| | | ORDER_FILLED("filled", |
| | | "filled"), |
| | | ORDER_LIVE("live", |
| | | "live"), |
| | | |
| | | |
| | | INSTTYPE_SWAP("SWAP", |
| | | "SWAP"), |
| | | |
| | | |
| | | ORDTYPE_MARKET("market", |
| | | "market"), |
| | | |
| | | |
| | | POSSIDE_SHORT("short", |
| | | "short"), |
| | | |
| | | |
| | | POSSIDE_LONG("long", |
| | | "long"), |
| | | |
| | | |
| | | SIDE_SELL("sell", |
| | | "sell"), |
| | | |
| | | SIDE_BUY("buy", |
| | | "buy"), |
| | | |
| | | CROSS("cross", |
| | | "cross"), |
| | | |
| | | |
| | | USDT("USDT", |
| | | "USDT"), |
| | | |
| | | |
| | | HE_YUE("BTC-USDT-SWAP", |
| | | "BTC-USDT-SWAP"); |
| | | PING_CANG_SHOUYI("平仓收益比例", "0.2"), |
| | | //下单的总保障金为账户总金额cashBal * TOTAL_ORDER_USDT用来做保证金 |
| | | TOTAL_ORDER_USDTPECENT("总保证金比例total_order_usdtpecent","0.1"), |
| | | TOTAL_ORDER_USDT("总保证金totalOrderUsdt","0"), |
| | | KANG_CANG("抗压比例KANG_CANG","0.8"), |
| | | ZHI_SUN("止损比例ZHI_SUN","0.5"), |
| | | //每次下单的张数 |
| | | BUY_CNT("每次开仓的张数buyCnt","0.2"), |
| | | OUT("是否允许下单out","操作中"), |
| | | STATE("初始下单状态不允许下单state","0"), |
| | | CTVAL("合约面值ctVal","0.01"), |
| | | TICKSZ("下单价格精度tickSz","2"), |
| | | MINSZ("最小下单数小数位minSz","2"), |
| | | LEVERAGE("合约杠杆leverage","100"), |
| | | HE_YUE("合约instId","BTC-USDT-SWAP"); |
| | | |
| | | private String name; |
| | | |
| | |
| | | STATE_1("允许开仓", "1"), |
| | | STATE_0("不允许开仓", "0"), |
| | | |
| | | ZHI_SUN("止损......", "0.6"), |
| | | |
| | | KANG_CANG("抗压......", "0.8"), |
| | | |
| | | LEVERAGE("杠杆倍数", "100"), |
| | | |
| | | PING_CANG_SHOUYI("平仓收益比例", "0.2"), |
| | | |
| | | EVERY_TIME_USDT("总下单次数", "50"), |
| | | //下单的总保障金为账户总金额cashBal * TOTAL_ORDER_USDT用来做保证金 |
| | | TOTAL_ORDER_USDT("下单的总金额比例", "0.1"), |
| | | ; |
| | | |
| | | private String name; |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.utils; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class WsMapBuild { |
| | | |
| | | public static void saveBigDecimalToMap(Map<String,BigDecimal> accountMap, String key, BigDecimal value) { |
| | | try { |
| | | accountMap.put(key, value); |
| | | } catch (Exception e) { |
| | | log.error("保存账户数据到MAP 失败", e); |
| | | } |
| | | } |
| | | |
| | | public static void saveStringToMap(Map<String,String> accountMap, String key, String value) { |
| | | try { |
| | | accountMap.put(key, value); |
| | | } catch (Exception e) { |
| | | log.error("保存账户数据到MAP 失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全地将字符串解析为 BigDecimal 类型 |
| | | * |
| | | * @param value 字符串数值 |
| | | * @return 解析后的 BigDecimal 对象,若解析失败则返回 null |
| | | */ |
| | | public static BigDecimal parseBigDecimalSafe(String value) { |
| | | if (value == null || value.isEmpty()) { |
| | | return new BigDecimal(0); |
| | | } |
| | | return new BigDecimal(value); |
| | | } |
| | | } |
| | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.Random; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | public class WsParamBuild { |
| | | |
| | | public static String getOrderNum(String prefix) { |
| | | SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); |
| | | String dd=df.format(new Date()); |
| | | if (StrUtil.isNotBlank(prefix)) { |
| | | return prefix+dd+getRandomNum(5); |
| | | } |
| | | return dd+getRandomNum(5); |
| | | } |
| | | |
| | | public static String getRandomNum(int length) { |
| | | String str = "0123456789"; |
| | | Random random = new Random(); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int i = 0; i < length; ++i) { |
| | | int number = random.nextInt(str.length()); |
| | | sb.append(str.charAt(number)); |
| | | } |
| | | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | public static JSONObject buildJsonObject(String connId, String option, JSONArray argsArray) { |
| | | JSONObject jsonObject = new JSONObject(); |
| | |
| | | jsonObject.put("args", argsArray); |
| | | return jsonObject; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 计算购买合约的数量 |
| | | * |
| | | * USDT 币本位合约 |
| | | * 公式:张数 = 保证金 / (面值 * 标记价格 / 杠杆倍数) |
| | | * |
| | | * @param margin 用户的保证金金额 |
| | | * @param leverage 杠杆倍数 |
| | | * @param faceValue 合约面值 |
| | | * @param markPrice 标记价格 |
| | | * @param minLotSz 最小下单精度 |
| | | * @return 返回用户可以购买的合约数量 |
| | | */ |
| | | public static BigDecimal buyCnt(BigDecimal margin, BigDecimal leverage, BigDecimal faceValue, BigDecimal markPrice, int minLotSz) { |
| | | if (margin.compareTo(BigDecimal.ZERO) <= 0 || |
| | | leverage.compareTo(BigDecimal.ZERO) <= 0 || |
| | | faceValue.compareTo(BigDecimal.ZERO) <= 0 || |
| | | markPrice.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | BigDecimal divisor = markPrice.divide(leverage, 10, BigDecimal.ROUND_DOWN); |
| | | BigDecimal denominator = faceValue.multiply(divisor); |
| | | return margin.divide(denominator, minLotSz, BigDecimal.ROUND_DOWN); |
| | | } |
| | | } |