920927331afecb67e85268eecb567a91b53f1e61..d84dcaac5c3722074d21739054dda74b23c37435
2026-01-06 Administrator
fix(okxWs): 修复批量下单频道连接ID生成问题
d84dca diff | tree
2026-01-06 Administrator
fix(okxWs): 修复订单信息处理中的参数遗漏问题
ac4107 diff | tree
2026-01-06 Administrator
feat(okxWs): 添加批量订单WebSocket支持
bccc54 diff | tree
2026-01-06 Administrator
refactor(okxWs): 优化交易订单WebSocket参数构建逻辑
db14cd diff | tree
2026-01-06 Administrator
refactor(okxWs): 优化下单参数构建逻辑
fe4d54 diff | tree
2026-01-06 Administrator
refactor(okxNewPrice): 重构交易订单处理逻辑
39252e diff | tree
2026-01-06 Administrator
feat(okxNewPrice): 添加账户盈亏监控和自动止损功能
f139af diff | tree
2026-01-06 Administrator
fix(okx): 修复平仓收益比例配置和订单处理逻辑
60396e diff | tree
2026-01-06 Administrator
fix(okxNewPrice): 修复开仓日志记录位置错误
37240c diff | tree
2026-01-06 Administrator
fix(strategy): 修复MACD策略日志输出和网格止损逻辑
36c1ee diff | tree
2026-01-06 Administrator
refactor(okxNewPrice): 优化MACD策略日志输出和交易逻辑
de704d diff | tree
2026-01-06 Administrator
refactor(MacdEmaStrategy): 调整多头和空头开仓条件的逻辑顺序
97b7f9 diff | tree
2026-01-06 Administrator
``` chore(okxNewPrice): 注释平仓逻辑代码
f414d5 diff | tree
2026-01-06 Administrator
``` chore(okxNewPrice): 注释平仓逻辑代码
1b4c52 diff | tree
2026-01-06 Administrator
feat(okxNewPrice): 添加MACD和EMA组合交易策略实现
7903e7 diff | tree
6 files modified
1 files added
759 ■■■■ changed files
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxKlineWebSocketClient.java 109 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxQuantWebSocketClient.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdEmaStrategy.java 463 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/OrderInfoWs.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/TradeOrderWs.java 116 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/enums/CoinEnums.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/ExchangeInfoEnum.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxKlineWebSocketClient.java
@@ -10,6 +10,7 @@
import com.xcong.excoin.modules.blackchain.service.DateUtil;
import com.xcong.excoin.modules.okxNewPrice.celue.CaoZuoService;
import com.xcong.excoin.modules.okxNewPrice.indicator.TradingStrategy;
import com.xcong.excoin.modules.okxNewPrice.indicator.macdAndMatrategy.MacdEmaStrategy;
import com.xcong.excoin.modules.okxNewPrice.indicator.macdAndMatrategy.MacdMaStrategy;
import com.xcong.excoin.modules.okxNewPrice.okxWs.*;
import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums;
@@ -21,6 +22,7 @@
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeInfoEnum;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeLoginService;
import com.xcong.excoin.modules.okxNewPrice.utils.SSLConfig;
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;
@@ -343,10 +345,9 @@
                 */
                String confirm = data.getString(8);
                if ("1".equals(confirm)){
                    log.info("{}开仓{}:{}",time,closePx,instId);
                    //调用策略
                    // 创建策略实例
                    MacdMaStrategy strategy = new MacdMaStrategy();
                    MacdEmaStrategy strategy = new MacdEmaStrategy();
                    // 生成200个1m价格数据点
                    List<Kline> kline1MinuteData = getKlineDataByInstIdAndBar(instId, "1m");
@@ -354,14 +355,8 @@
                            .map(Kline::getC)
                            .collect(Collectors.toList());
                    // 生成200个1D价格数据点
                    List<Kline> kline1DayData = getKlineDataByInstIdAndBar(instId, "1D");
                    List<BigDecimal> historicalPrices1D = kline1DayData.stream()
                            .map(Kline::getC)
                            .collect(Collectors.toList());
                    log.info("1D:{}", JSONUtil.parse( historicalPrices1D));
                    // 使用策略分析最新价格数据
                    MacdMaStrategy.TradingOrder tradingOrderOpenOpen = strategy.generateTradingOrder(historicalPrices1M,historicalPrices1D,MacdMaStrategy.OperationType.open.name());
                    MacdEmaStrategy.TradingOrder tradingOrderOpenOpen = strategy.generateTradingOrder(historicalPrices1M, MacdMaStrategy.OperationType.open.name());
                    if (tradingOrderOpenOpen == null){
                        return;
                    }
@@ -376,80 +371,8 @@
                        String accountName = client.getAccountName();
                        if (accountName != null) {
                            if (ObjectUtil.isNotEmpty(tradingOrderOpenOpen)){
                                // 根据信号执行交易操作
                                TradeRequestParam tradeRequestParam = new TradeRequestParam();
                                String posSide = tradingOrderOpenOpen.getPosSide();
                                tradeRequestParam.setPosSide(posSide);
                                String currentPrice = String.valueOf(closePx);
                                tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide);
                                String side = tradingOrderOpenOpen.getSide();
                                tradeRequestParam.setSide(side);
                                String clOrdId = WsParamBuild.getOrderNum(side);
                                tradeRequestParam.setClOrdId(clOrdId);
                                String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name());
                                tradeRequestParam.setSz(sz);
                                TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam);
                            }
                        }
                    }
                }else{
                    log.info("{}平仓{}:{}",time,closePx,instId);
                    //调用策略
                    // 创建策略实例
                    MacdMaStrategy strategy = new MacdMaStrategy();
                    // 生成200个1m价格数据点
                    List<Kline> kline1MinuteData = getKlineDataByInstIdAndBar(instId, "1m");
                    List<BigDecimal> historicalPrices1M = kline1MinuteData.stream()
                            .map(Kline::getC)
                            .collect(Collectors.toList());
                    // 生成200个1D价格数据点
                    List<Kline> kline1DayData = getKlineDataByInstIdAndBar(instId, "1D");
                    List<BigDecimal> historicalPrices1D = kline1DayData.stream()
                            .map(Kline::getC)
                            .collect(Collectors.toList());
                    // 使用策略分析最新价格数据
                    MacdMaStrategy.TradingOrder tradingOrderOpenClose = strategy.generateTradingOrder(historicalPrices1M,historicalPrices1D,MacdMaStrategy.OperationType.close.name());
                    if (tradingOrderOpenClose == null){
                        return;
                    }
                    Collection<OkxQuantWebSocketClient> allClients = clientManager.getAllClients();
                    //如果为空,则直接返回
                    if (allClients.isEmpty()) {
                        return;
                    }
                    // 获取所有OkxQuantWebSocketClient实例
                    for (OkxQuantWebSocketClient client : clientManager.getAllClients()) {
                        String accountName = client.getAccountName();
                        if (accountName != null) {
                            if (ObjectUtil.isNotEmpty(tradingOrderOpenClose)){
                                // 根据信号执行交易操作
                                TradeRequestParam tradeRequestParam = new TradeRequestParam();
                                tradeRequestParam.setAccountName(accountName);
                                tradeRequestParam.setMarkPx(String.valueOf(closePx));
                                tradeRequestParam.setInstId(CoinEnums.HE_YUE.getCode());
                                tradeRequestParam.setTdMode(CoinEnums.CROSS.getCode());
                                tradeRequestParam.setOrdType(CoinEnums.ORDTYPE_MARKET.getCode());
                                String posSide = tradingOrderOpenClose.getPosSide();
                                tradeRequestParam.setPosSide(posSide);
                                String side = tradingOrderOpenClose.getSide();
                                tradeRequestParam.setSide(side);
                                String clOrdId = WsParamBuild.getOrderNum(side);
                                tradeRequestParam.setClOrdId(clOrdId);
                                String positionAccountName = PositionsWs.initAccountName(accountName, posSide);
                                BigDecimal pos = PositionsWs.getAccountMap(positionAccountName).get("pos");
                                tradeRequestParam.setSz(String.valueOf(pos));
                                TradeOrderWs.orderZhiYingZhiSunEventNoState(client.getWebSocketClient(), tradeRequestParam);
                                log.info("{}开仓{}:{}",instId,tradingOrderOpenOpen.getPosSide(),tradingOrderOpenOpen.getSide());
                                doOpen(client.getWebSocketClient(),accountName, tradingOrderOpenOpen, closePx);
                            }
                        }
                    }
@@ -460,6 +383,26 @@
        }
    }
    private void doOpen(WebSocketClient webSocketClient, String accountName, MacdEmaStrategy.TradingOrder tradingOrderOpenOpen, BigDecimal closePx) {
        // 根据信号执行交易操作
        TradeRequestParam tradeRequestParam = new TradeRequestParam();
        String posSide = tradingOrderOpenOpen.getPosSide();
        tradeRequestParam.setPosSide(posSide);
        String currentPrice = String.valueOf(closePx);
        tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide);
        String side = tradingOrderOpenOpen.getSide();
        tradeRequestParam.setSide(side);
        String clOrdId = WsParamBuild.getOrderNum(side);
        tradeRequestParam.setClOrdId(clOrdId);
        String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name());
        tradeRequestParam.setSz(sz);
        TradeOrderWs.orderEvent(webSocketClient, tradeRequestParam);
    }
    private List<Kline> getKlineDataByInstIdAndBar(String instId, String bar) {
        List<Kline> klineList = new ArrayList<>();
        try {
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxQuantWebSocketClient.java
@@ -18,6 +18,7 @@
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -383,9 +384,9 @@
        // 注意:当前实现中,OrderInfoWs等类使用静态Map存储数据
        // 这会导致多账号之间的数据冲突。需要进一步修改这些类的设计,让数据存储与特定账号关联
        if (OrderInfoWs.ORDERINFOWS_CHANNEL.equals(channel)) {
            OrderInfoWs.handleEvent(response, redisUtils, account.name());
//            TradeRequestParam tradeRequestParam = OrderInfoWs.handleEvent(response, redisUtils, account.name());
//            TradeOrderWs.orderZhiYingZhiSunEventNoState(webSocketClient, tradeRequestParam);
//            OrderInfoWs.handleEvent(response, redisUtils, account.name());
            List<TradeRequestParam> tradeRequestParams = OrderInfoWs.handleEvent(response, redisUtils, account.name());
            TradeOrderWs.orderZhiYingZhiSunEventNoState(webSocketClient, tradeRequestParams);
        }else if (AccountWs.ACCOUNTWS_CHANNEL.equals(channel)) {
            AccountWs.handleEvent(response, account.name());
        } else if (PositionsWs.POSITIONSWS_CHANNEL.equals(channel)) {
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdEmaStrategy.java
New file
@@ -0,0 +1,463 @@
/**
 * MACD和MA组合交易策略实现类
 * 基于多时间粒度K线数据生成交易信号并确定持仓方向
 *
 * 该策略综合考虑了EMA指标、MACD指标、价格突破信号和波动率因素,
 * 形成了一套完整的开仓、平仓和持仓管理机制。
 * 支持1分钟(K线)和1分钟(K线)级别的数据输入,数据顺序要求从新到旧排列。
 */
package com.xcong.excoin.modules.okxNewPrice.indicator.macdAndMatrategy;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * MACD和MA组合交易策略实现
 * <p>
 * 该策略利用EMA交叉、MACD指标、价格突破信号和波动率过滤,
 * 为多时间粒度K线级别交易提供综合决策支持。
 * <p>
 * 数据输入要求:
 * - historicalPrices1M:1分钟K线收盘价列表,顺序从新到旧
 * - historicalPrices1D:日线K线收盘价列表,顺序从新到旧
 */
@Slf4j
public class MacdEmaStrategy {
    /** 操作类型枚举 */
    public enum OperationType {
        /** 开仓 */
        open,
        /** 平仓 */
        close
    }
    /** 持仓状态枚举 */
    public enum PositionType {
        /** 多头开仓 */
        LONG_BUY,
        /** 多头平仓 */
        LONG_SELL,
        /** 空头开仓 */
        SHORT_SELL,
        /** 空头平仓 */
        SHORT_BUY,
        /** 空仓 */
        NONE
    }
    /** 交易指令类,封装side和posSide的组合 */
    public static class TradingOrder {
        private String side;    // buy或sell
        private String posSide; // long或short
        public TradingOrder(String side, String posSide) {
            this.side = side;
            this.posSide = posSide;
        }
        public String getSide() {
            return side;
        }
        public String getPosSide() {
            return posSide;
        }
        @Override
        public String toString() {
            return String.format("TradingOrder{side='%s', posSide='%s'}", side, posSide);
        }
    }
    // 策略参数
    private int shortPeriod;      // 短期EMA周期
    private int longPeriod;       // 长期EMA周期
    private int signalPeriod;     // MACD信号线周期
    private int volatilityPeriod; // 波动率计算周期
    private int trendPeriod = 200; // 趋势过滤EMA周期(200日)
    private BigDecimal stopLossRatio;   // 止损比例
    private BigDecimal takeProfitRatio; // 止盈比例
    /**
     * 默认构造函数,使用标准MACD参数
     * 短期周期=12, 长期周期=26, 信号线周期=9, 波动率周期=20
     * 止损比例=1%, 止盈比例=2%
     */
    public MacdEmaStrategy() {
        this(12, 26, 9, 20, new BigDecimal("0.01"), new BigDecimal("0.02"));
    }
    /**
     * 自定义参数构造函数,使用默认趋势周期200
     *
     * @param shortPeriod 短期EMA周期
     * @param longPeriod 长期EMA周期
     * @param signalPeriod MACD信号线周期
     * @param volatilityPeriod 波动率计算周期
     * @param stopLossRatio 止损比例
     * @param takeProfitRatio 止盈比例
     */
    public MacdEmaStrategy(int shortPeriod, int longPeriod, int signalPeriod, int volatilityPeriod,
                           BigDecimal stopLossRatio, BigDecimal takeProfitRatio) {
        this(shortPeriod, longPeriod, signalPeriod, volatilityPeriod, 200, stopLossRatio, takeProfitRatio);
    }
    /**
     * 自定义参数构造函数
     *
     * @param shortPeriod 短期EMA周期
     * @param longPeriod 长期EMA周期
     * @param signalPeriod MACD信号线周期
     * @param volatilityPeriod 波动率计算周期
     * @param trendPeriod 趋势过滤EMA周期(200日)
     * @param stopLossRatio 止损比例
     * @param takeProfitRatio 止盈比例
     */
    public MacdEmaStrategy(int shortPeriod, int longPeriod, int signalPeriod, int volatilityPeriod, int trendPeriod,
                           BigDecimal stopLossRatio, BigDecimal takeProfitRatio) {
        this.shortPeriod = shortPeriod;
        this.longPeriod = longPeriod;
        this.signalPeriod = signalPeriod;
        this.volatilityPeriod = volatilityPeriod;
        this.trendPeriod = trendPeriod;
        this.stopLossRatio = stopLossRatio;
        this.takeProfitRatio = takeProfitRatio;
    }
    // 主流程方法
    /**
     * 分析历史价格数据并生成交易指令
     *
     * @param historicalPrices 历史价格序列(1分钟K线收盘价),顺序从新到旧
     * @param operation 操作类型(open/close)
     * @return 交易指令(包含side和posSide),如果没有交易信号则返回null
     */
    public TradingOrder generateTradingOrder(List<BigDecimal> historicalPrices, String operation) {
        PositionType signal = null;
        if (OperationType.open.name().equals(operation)) {
            signal = analyzeOpen(historicalPrices);
        } else if (OperationType.close.name().equals(operation)) {
            signal = analyzeClose(historicalPrices);
        }
        // 根据信号生成交易指令
        return convertSignalToTradingOrder(signal);
    }
    /**
     * 分析最新价格数据并生成开仓信号
     *
     * @param closePrices 1分钟K线收盘价序列,顺序从新到旧
     * @return 生成的交易信号(LONG_BUY、SHORT_SELL或NONE)
     */
    public PositionType analyzeOpen(List<BigDecimal> closePrices) {
        // 数据检查:确保有足够的数据点进行计算(需要足够数据计算200日EMA)
        if (closePrices == null || closePrices.size() < Math.max(34, trendPeriod)) {
            return PositionType.NONE; // 数据不足,无法生成信号
        }
        // 计算MACD指标
        MACDResult macdResult = MACDCalculator.calculateMACD(
                closePrices, shortPeriod, longPeriod, signalPeriod);
         log.info("MACD计算结果:{}", macdResult.getMacdData().get(0));
        // 多头开仓条件检查
        if (isLongEntryCondition(macdResult, closePrices)) {
            log.info("多头开仓信号,价格:{}", closePrices.get(0));
            return PositionType.LONG_BUY;
        }
        // 空头开仓条件检查
        if (isShortEntryCondition(macdResult, closePrices)) {
            log.info("空头开仓信号,价格:{}", closePrices.get(0));
            return PositionType.SHORT_SELL;
        }
        // 无信号
        return PositionType.NONE;
    }
    /**
     * 分析最新价格数据并生成平仓信号
     *
     * @param closePrices 1分钟K线收盘价序列,顺序从新到旧
     * @return 生成的交易信号(LONG_SELL、SHORT_BUY或NONE)
     */
    public PositionType analyzeClose(List<BigDecimal> closePrices) {
        // 数据检查:确保有足够的数据点进行计算
        if (closePrices == null || closePrices.size() < Math.max(34, trendPeriod)) {
            return PositionType.NONE; // 数据不足,无法生成信号
        }
        // 计算MACD指标
        MACDResult macdResult = MACDCalculator.calculateMACD(
                closePrices, shortPeriod, longPeriod, signalPeriod);
        // 最新收盘价
        BigDecimal latestPrice = closePrices.get(0);
        if (isLongExitCondition(macdResult, latestPrice)) {
            log.info("多头平仓信号,价格:{}", latestPrice);
            return PositionType.LONG_SELL;
        }
        if (isShortExitCondition(macdResult, latestPrice)) {
            log.info("空头平仓信号,价格:{}", latestPrice);
            return PositionType.SHORT_BUY;
        }
        // 无信号
        return PositionType.NONE;
    }
    // 信号转换方法
    /**
     * 将持仓信号转换为交易指令
     *
     * @param signal 持仓信号
     * @return 交易指令,无信号则返回null
     */
    private TradingOrder convertSignalToTradingOrder(PositionType signal) {
        if (signal == null) {
            return null;
        }
        switch (signal) {
            case LONG_BUY:
                // 开多:买入开多(side 填写 buy; posSide 填写 long )
                return new TradingOrder("buy", "long");
            case LONG_SELL:
                // 平多:卖出平多(side 填写 sell; posSide 填写 long )
                return new TradingOrder("sell", "long");
            case SHORT_SELL:
                // 开空:卖出开空(side 填写 sell; posSide 填写 short )
                return new TradingOrder("sell", "short");
            case SHORT_BUY:
                // 平空:买入平空(side 填写 buy; posSide 填写 short )
                return new TradingOrder("buy", "short");
            default:
                // 无信号
                return null;
        }
    }
    // 开仓条件检查方法
    /**
     * 多头开仓条件检查
     *
     * @param macdResult MACD计算结果
     * @param closePrices 1分钟K线收盘价序列,顺序从新到旧
     * @return 是否满足多头开仓条件
     */
    private boolean isLongEntryCondition(MACDResult macdResult, List<BigDecimal> closePrices) {
        // 1. 计算200日EMA(趋势过滤)
        // 复制并反转日线数据,确保从旧到新计算EMA
        List<BigDecimal> reversed1DPrices = new ArrayList<>(closePrices);
        Collections.reverse(reversed1DPrices);
        List<BigDecimal> trendEma = EMACalculator.calculateEMA(reversed1DPrices, trendPeriod, true);
        BigDecimal latestTrendEma = trendEma.get(trendEma.size() - 1);
        BigDecimal latestPrice = closePrices.get(0);
        // 2. 价格必须位于200日EMA上方(多头趋势确认)
        boolean isAboveTrend = latestPrice.compareTo(latestTrendEma) > 0;
        // 3. MACD金叉检查
        boolean isGoldenCross = isGoldenCross(macdResult);
        // 4. MACD柱状线由负转正(动量转变)
        boolean isMacdHistTurningPositive = isMacdHistTurningPositive(macdResult);
        // 5. 底背离检查(增强多头信号可靠性)
        boolean isBottomDivergence = MACDCalculator.isBottomDivergence(closePrices, macdResult);
        log.info("多头信号检查, 200日EMA价格{}位于上方: {}, 金叉: {}, MACD柱状线由负转正: {}, 底背离: {}",
                latestTrendEma,isAboveTrend, isGoldenCross, isMacdHistTurningPositive, isBottomDivergence);
        // 多头开仓条件:柱状线转强 + 金叉 + (趋势向上或底背离)
        return isMacdHistTurningPositive && isGoldenCross && ( isAboveTrend|| isBottomDivergence);
    }
    /**
     * 空头开仓条件检查
     *
     * @param macdResult MACD计算结果
     * @param closePrices 1分钟K线收盘价序列,顺序从新到旧
     * @return 是否满足空头开仓条件
     */
    private boolean isShortEntryCondition(MACDResult macdResult, List<BigDecimal> closePrices) {
        // 1. 计算200日EMA(趋势过滤)
        // 复制并反转日线数据,确保从旧到新计算EMA
        List<BigDecimal> reversed1DPrices = new ArrayList<>(closePrices);
        Collections.reverse(reversed1DPrices);
        List<BigDecimal> trendEma = EMACalculator.calculateEMA(reversed1DPrices, trendPeriod, true);
        BigDecimal latestTrendEma = trendEma.get(trendEma.size() - 1);
        BigDecimal latestPrice = closePrices.get(0);
        // 2. 价格必须位于200日EMA下方(空头趋势确认)
        boolean isBelowTrend = latestPrice.compareTo(latestTrendEma) < 0;
        // 3. MACD死叉检查
        boolean isDeathCross = isDeathCross(macdResult);
        // 4. MACD柱状线由正转负(动量转变)
        boolean isMacdHistTurningNegative = isMacdHistTurningNegative(macdResult);
        // 5. 顶背离检查(增强空头信号可靠性)
        boolean isTopDivergence = MACDCalculator.isTopDivergence(closePrices, macdResult);
        log.info("空头信号检查, 200日EMA价格{}位于下方: {}, 死叉: {}, MACD柱状线由正转负: {}, 顶背离: {}",
                latestTrendEma,isBelowTrend, isDeathCross, isMacdHistTurningNegative, isTopDivergence);
        // 空头开仓条件:柱状线转弱 + 死叉 + (趋势向下或顶背离)
        return isMacdHistTurningNegative && isDeathCross && ( isBelowTrend || isTopDivergence);
    }
    // 平仓条件检查方法
    /**
     * 多头平仓条件检查
     *
     * @param macdResult MACD计算结果
     * @param currentPrice 当前价格
     * @return 是否满足多头平仓条件
     */
    private boolean isLongExitCondition(MACDResult macdResult, BigDecimal currentPrice) {
        // 多头平仓条件:MACD柱状线动量减弱(由正转弱)
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() >= 2) {
            PriceData latest = macdData.get(0); // 最新数据
            PriceData previous = macdData.get(1); // 前一个数据
            // 柱状线由正转弱:前一根为正,当前绝对值减小
            boolean momentumWeakening = previous.getMacdHist().compareTo(BigDecimal.ZERO) >= 0 &&
                                      latest.getMacdHist().abs().compareTo(previous.getMacdHist().abs()) < 0;
            return momentumWeakening;
        }
        return false;
    }
    /**
     * 空头平仓条件检查
     *
     * @param macdResult MACD计算结果
     * @param currentPrice 当前价格
     * @return 是否满足空头平仓条件
     */
    private boolean isShortExitCondition(MACDResult macdResult, BigDecimal currentPrice) {
        // 空头平仓条件:MACD柱状线动量减弱(由负转弱)
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() >= 2) {
            PriceData latest = macdData.get(0); // 最新数据
            PriceData previous = macdData.get(1); // 前一个数据
            // 柱状线由负转弱:前一根为负,当前绝对值减小
            boolean momentumWeakening = previous.getMacdHist().compareTo(BigDecimal.ZERO) <= 0 &&
                                      latest.getMacdHist().abs().compareTo(previous.getMacdHist().abs()) < 0;
            return momentumWeakening;
        }
        return false;
    }
    // MACD信号辅助方法
    /**
     * 简单金叉判断
     * <p>
     * 条件:DIF线从下往上穿过DEA线
     *
     * @param macdResult MACD计算结果
     * @return 是否形成金叉
     */
    private boolean isGoldenCross(MACDResult macdResult) {
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() < 2) {
            return false;
        }
        PriceData latest = macdData.get(0);
        PriceData previous = macdData.get(1);
        // 金叉判断:DIF从下往上穿过DEA
        return previous.getDif().compareTo(previous.getDea()) < 0 &&
               latest.getDif().compareTo(latest.getDea()) > 0;
    }
    /**
     * 简单死叉判断
     * <p>
     * 条件:DIF线从上往下穿过DEA线
     *
     * @param macdResult MACD计算结果
     * @return 是否形成死叉
     */
    private boolean isDeathCross(MACDResult macdResult) {
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() < 2) {
            return false;
        }
        PriceData latest = macdData.get(0);
        PriceData previous = macdData.get(1);
        // 死叉判断:DIF从上往下穿过DEA
        return previous.getDif().compareTo(previous.getDea()) > 0 &&
               latest.getDif().compareTo(latest.getDea()) < 0;
    }
    /**
     * MACD柱状线由负转正判断
     * <p>
     * 条件:前一根柱状线为负,当前柱状线为正
     *
     * @param macdResult MACD计算结果
     * @return 是否由负转正
     */
    private boolean isMacdHistTurningPositive(MACDResult macdResult) {
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() < 2) {
            return false;
        }
        PriceData latest = macdData.get(0); // 最新数据
        PriceData previous = macdData.get(1); // 前一个数据
        // 柱状线由负转正:前一根为负,当前为正
        return previous.getMacdHist().compareTo(BigDecimal.ZERO) <= 0 &&
               latest.getMacdHist().compareTo(BigDecimal.ZERO) > 0;
    }
    /**
     * MACD柱状线由正转负判断
     * <p>
     * 条件:前一根柱状线为正,当前柱状线为负
     *
     * @param macdResult MACD计算结果
     * @return 是否由正转负
     */
    private boolean isMacdHistTurningNegative(MACDResult macdResult) {
        List<PriceData> macdData = macdResult.getMacdData();
        if (macdData.size() < 2) {
            return false;
        }
        PriceData latest = macdData.get(0); // 最新数据
        PriceData previous = macdData.get(1); // 前一个数据
        // 柱状线由正转负:前一根为正,当前为负
        return previous.getMacdHist().compareTo(BigDecimal.ZERO) >= 0 &&
               latest.getMacdHist().compareTo(BigDecimal.ZERO) < 0;
    }
}
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/OrderInfoWs.java
@@ -16,6 +16,8 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -69,7 +71,7 @@
    private static final String STATE_KEY = "state";
    private static final String FILLFEE_KEY = "fillFee";
    private static final String POSSIDE_KEY = "posSide";
    public static TradeRequestParam handleEvent(JSONObject response, RedisUtils redisUtils, String accountName) {
    public static List<TradeRequestParam> handleEvent(JSONObject response, RedisUtils redisUtils, String accountName) {
        log.info("开始执行OrderInfoWs......");
        try {
@@ -116,6 +118,7 @@
                    log.info("{}: 订单详情已完成: {}, 自定义编号: {}", accountName, CoinEnums.HE_YUE.getCode(), clOrdId);
                    ArrayList<TradeRequestParam> tradeRequestParams = new ArrayList<>();
                    TradeRequestParam tradeRequestParam = new TradeRequestParam();
                    tradeRequestParam.setAccountName(accountName);
                    BigDecimal zhiYingPx = getZhiYingPx(
@@ -137,7 +140,31 @@
                    tradeRequestParam.setSide(CoinEnums.POSSIDE_LONG.getCode().equals(posSide) ? CoinEnums.SIDE_SELL.getCode() : CoinEnums.SIDE_BUY.getCode());
                    tradeRequestParam.setClOrdId(WsParamBuild.getOrderNum(tradeRequestParam.getSide()));
                    tradeRequestParam.setSz(accFillSz);
                    return tradeRequestParam;
                    tradeRequestParams.add(tradeRequestParam);
                    TradeRequestParam tradeRequestParamZhiSun = new TradeRequestParam();
                    tradeRequestParamZhiSun.setAccountName(accountName);
                    BigDecimal zhiSunPx = getZhiSunPx(
                            accountName,
                            posSide,
                            fillFee,
                            WsMapBuild.parseBigDecimalSafe(InstrumentsWs.getAccountMap(accountName).get(CoinEnums.CTVAL.name())),
                            WsMapBuild.parseBigDecimalSafe(accFillSz),
                            WsMapBuild.parseBigDecimalSafe(InstrumentsWs.getAccountMap(accountName).get(CoinEnums.CONTRACTMULTIPLIER.name())),
                            WsMapBuild.parseBigDecimalSafe(avgPx),
                            WsMapBuild.parseBigDecimalSafe(CoinEnums.LEVERAGE.getCode())
                    );
                    tradeRequestParamZhiSun.setMarkPx(String.valueOf(zhiSunPx));
                    tradeRequestParamZhiSun.setInstId(CoinEnums.HE_YUE.getCode());
                    tradeRequestParamZhiSun.setTdMode(CoinEnums.CROSS.getCode());
                    tradeRequestParamZhiSun.setPosSide(posSide);
                    tradeRequestParamZhiSun.setOrdType(CoinEnums.ORDTYPE_LIMIT.getCode());
                    tradeRequestParamZhiSun.setTradeType(OrderParamEnums.TRADE_YES.getValue());
                    tradeRequestParamZhiSun.setSide(CoinEnums.POSSIDE_LONG.getCode().equals(posSide) ? CoinEnums.SIDE_SELL.getCode() : CoinEnums.SIDE_BUY.getCode());
                    tradeRequestParamZhiSun.setClOrdId(WsParamBuild.getOrderNum(tradeRequestParamZhiSun.getSide()));
                    tradeRequestParamZhiSun.setSz(accFillSz);
                    tradeRequestParams.add(tradeRequestParamZhiSun);
                    return tradeRequestParams;
                }
                return null;
@@ -164,6 +191,23 @@
    /**
     * 计算预期收益
     */
    public static BigDecimal getZhiSunPx(
            String accountName, String posSide, String fillFee, BigDecimal coinValue, BigDecimal coinNum,
            BigDecimal contractMultiplier, BigDecimal avgPx, BigDecimal leverage
    ) {
        BigDecimal initMargin = getInitMargin(coinValue, coinNum, contractMultiplier, avgPx, leverage);
        String pingCangImr = StrUtil.isEmpty(InstrumentsWs.getAccountMap(accountName).get(CoinEnums.PING_CANG_SHOUYI.name())) ? "0.2" : InstrumentsWs.getAccountMap(accountName).get(CoinEnums.PING_CANG_SHOUYI.name());
        BigDecimal expectProfit = (initMargin)
                .multiply(new BigDecimal(pingCangImr))
                .add(new BigDecimal(fillFee).abs())
                .multiply(new BigDecimal("-1"))
                .setScale(4, RoundingMode.DOWN);
        log.info("{}: 订单详情-预期收益: {}", accountName, expectProfit);
        return getMarkPrice(expectProfit,posSide, coinValue, coinNum, contractMultiplier, avgPx, leverage);
    }
    /**
     * 计算预期收益
     */
    public static BigDecimal getZhiYingPx(
            String accountName, String posSide, String fillFee, BigDecimal coinValue, BigDecimal coinNum,
            BigDecimal contractMultiplier, BigDecimal avgPx, BigDecimal leverage
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/TradeOrderWs.java
@@ -13,6 +13,7 @@
import org.java_websocket.client.WebSocketClient;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +34,7 @@
    }
    public static final String ORDERWS_CHANNEL = "order";
    public static final String BATCH_ORDERSWS_CHANNEL = "batch-orders";
    public static void orderEvent(WebSocketClient webSocketClient, TradeRequestParam tradeRequestParam) {
@@ -136,64 +138,65 @@
        }
    }
    public static void orderZhiYingZhiSunEventNoState(WebSocketClient webSocketClient, TradeRequestParam tradeRequestParam) {
    public static void orderZhiYingZhiSunEventNoState(WebSocketClient webSocketClient, List<TradeRequestParam> tradeRequestParams) {
        log.info("开始执行TradeOrderWs......");
        if (tradeRequestParam == null){
        if (tradeRequestParams == null){
            log.warn("下单{}参数缺失,取消发送",tradeRequestParam);
            return;
        }
        String accountName = tradeRequestParam.getAccountName();
        String markPx = tradeRequestParam.getMarkPx();
        String instId = tradeRequestParam.getInstId();
        String tdMode = tradeRequestParam.getTdMode();
        String posSide = tradeRequestParam.getPosSide();
        String ordType = tradeRequestParam.getOrdType();
        String tradeType = tradeRequestParam.getTradeType();
        String clOrdId = tradeRequestParam.getClOrdId();
        String side = tradeRequestParam.getSide();
        String sz = tradeRequestParam.getSz();
        /**
         * 校验必要参数
         * 验证下单参数是否存在空值
         */
        if (
                StrUtil.isBlank(accountName)
                        || StrUtil.isBlank(instId)
                        || StrUtil.isBlank(tdMode)
                        || StrUtil.isBlank(posSide)
                        || StrUtil.isBlank(ordType)
                        || StrUtil.isBlank(clOrdId)
                        || StrUtil.isBlank(side)
                        || StrUtil.isBlank(sz)
                        || StrUtil.isBlank(markPx)
        ){
            log.warn("下单参数缺失,取消发送");
            return;
        }
        log.info("账户:{},类型:{},触发价格:{},币种:{},方向:{},买卖:{},数量:{},是否允许下单:{},编号:{},",
                accountName,ordType, markPx, instId, posSide,side,  sz, tradeType, clOrdId);
        //验证是否允许下单
        if (StrUtil.isNotEmpty(tradeType) && OrderParamEnums.TRADE_NO.getValue().equals(tradeType)) {
            log.warn("账户{}不允许下单,取消发送", accountName);
            log.warn("下单{}参数缺失,取消发送",tradeRequestParams);
            return;
        }
        /**
         * 检验账户和仓位是否准备就绪
         * 开多:买入开多(side 填写 buy; posSide 填写 long )
         * 开空:卖出开空(side 填写 sell; posSide 填写 short ) 需要检验账户通道是否准备就绪
         * 平多:卖出平多(side 填写 sell;posSide 填写 long )
         * 平空:买入平空(side 填写 buy; posSide 填写 short ) 需要检验仓位通道是否准备就绪
         */
        JSONArray argsArray = new JSONArray();
        for (TradeRequestParam tradeRequestParam : tradeRequestParams){
            String accountName = tradeRequestParam.getAccountName();
            String markPx = tradeRequestParam.getMarkPx();
            String instId = tradeRequestParam.getInstId();
            String tdMode = tradeRequestParam.getTdMode();
            String posSide = tradeRequestParam.getPosSide();
            String ordType = tradeRequestParam.getOrdType();
        try {
            JSONArray argsArray = new JSONArray();
            String tradeType = tradeRequestParam.getTradeType();
            String clOrdId = tradeRequestParam.getClOrdId();
            String side = tradeRequestParam.getSide();
            String sz = tradeRequestParam.getSz();
            /**
             * 校验必要参数
             * 验证下单参数是否存在空值
             */
            if (
                    StrUtil.isBlank(accountName)
                            || StrUtil.isBlank(instId)
                            || StrUtil.isBlank(tdMode)
                            || StrUtil.isBlank(posSide)
                            || StrUtil.isBlank(ordType)
                            || StrUtil.isBlank(clOrdId)
                            || StrUtil.isBlank(side)
                            || StrUtil.isBlank(sz)
                            || StrUtil.isBlank(markPx)
            ){
                log.warn("下单参数缺失,取消发送");
                continue;
            }
            log.info("账户:{},类型:{},触发价格:{},币种:{},方向:{},买卖:{},数量:{},是否允许下单:{},编号:{},",
                    accountName,ordType, markPx, instId, posSide,side,  sz, tradeType, clOrdId);
            //验证是否允许下单
            if (StrUtil.isNotEmpty(tradeType) && OrderParamEnums.TRADE_NO.getValue().equals(tradeType)) {
                log.warn("账户{}不允许下单,取消发送", accountName);
                continue;
            }
            /**
             * 检验账户和仓位是否准备就绪
             * 开多:买入开多(side 填写 buy; posSide 填写 long )
             * 开空:卖出开空(side 填写 sell; posSide 填写 short ) 需要检验账户通道是否准备就绪
             * 平多:卖出平多(side 填写 sell;posSide 填写 long )
             * 平空:买入平空(side 填写 buy; posSide 填写 short ) 需要检验仓位通道是否准备就绪
             */
            JSONObject args = new JSONObject();
            args.put("instId", instId);
            args.put("tdMode", tdMode);
@@ -205,15 +208,12 @@
            args.put("sz", sz);
            args.put("px", markPx);
            argsArray.add(args);
            String connId = WsParamBuild.getOrderNum(ORDERWS_CHANNEL);
            JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, ORDERWS_CHANNEL, argsArray);
            webSocketClient.send(jsonObject.toJSONString());
            log.info("发送下单频道:{},数量:{}", side, sz);
        } catch (Exception e) {
            log.error("下单构建失败", e);
        }
        String connId = WsParamBuild.getOrderNum(null);
        JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, BATCH_ORDERSWS_CHANNEL, argsArray);
        webSocketClient.send(jsonObject.toJSONString());
        log.info("发送止盈止损批量下单频道:{}",argsArray);
    }
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/enums/CoinEnums.java
@@ -43,7 +43,7 @@
    READY_STATE_YES("准备就绪ready_state", "1"),
    READY_STATE_NO("未准备就绪ready_state", "0"),
    PING_CANG_SHOUYI("平仓收益比例", "0.5"),
    PING_CANG_SHOUYI("平仓收益比例", "1"),
    //下单的总保障金为账户总金额cashBal * TOTAL_ORDER_USDT用来做保证金
    TOTAL_ORDER_USDTPECENT("总保证金比例total_order_usdtpecent","0.06"),
    TOTAL_ORDER_USDT("总保证金totalOrderUsdt","0"),
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/ExchangeInfoEnum.java
@@ -13,14 +13,14 @@
     * 模拟盘账户信息
     * 存储了模拟盘交易所需的API密钥、秘钥和通过码
     */
    OKX_UAT("f512673b-2685-4fcb-9bb1-2ae8db745d62",
            "B0C1CC8F39625B41140D93DC25039E33",
            "Aa12345678@",
            true);
//    OKX_UAT("ffb4e79f-fcf5-4afb-82c5-2fbb64123f61",
//            "AA06C5ED1D7C7F5AFE6484052E231C55",
//    OKX_UAT("f512673b-2685-4fcb-9bb1-2ae8db745d62",
//            "B0C1CC8F39625B41140D93DC25039E33",
//            "Aa12345678@",
//            false);
//            true);
    OKX_UAT("ffb4e79f-fcf5-4afb-82c5-2fbb64123f61",
            "AA06C5ED1D7C7F5AFE6484052E231C55",
            "Aa12345678@",
            false);
//    /**
//     * 模拟盘账户信息