feat(indicator): 添加15分钟交易策略和指标详细说明
- 为AdvancedMA、BOLL、KDJ、MA等指标添加详细作用说明和参数类型说明
- 添加推荐时间粒度及优缺点分析
- 实现15分钟交易策略FifteenMinuteTradingStrategy
- 添加FifteenMinuteStrategyExample和FifteenMinuteTradingExample示例
- 在CaoZuoService中添加caoZuoStrategy方法
- 实现操作策略逻辑,包括止损抗压判断和仓位管理
- 修改默认开仓张数从0.1调整为0.5
- 添加Kline参数类用于K线数据处理
- 扩展IndicatorBase添加ATR和波动率计算功能
11 files modified
7 files added
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.celue.CaoZuoService; |
| | | import com.xcong.excoin.modules.okxNewPrice.indicator.TradingStrategy; |
| | | 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.okxWs.param.Kline; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.param.TradeRequestParam; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.wanggeList.WangGeListEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.wanggeList.WangGeListService; |
| | | 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.WsParamBuild; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | import org.java_websocket.handshake.ServerHandshake; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.concurrent.*; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * OKX 新价格 WebSocket 客户端类,用于连接 OKX 的 WebSocket 接口, |
| | | * 实时获取并处理标记价格(mark price)数据,并将价格信息存储到 Redis 中。 |
| | | * 同时支持心跳检测、自动重连以及异常恢复机制。 |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class OkxKlineWebSocketClient { |
| | | private final RedisUtils redisUtils; |
| | | private final CaoZuoService caoZuoService; |
| | | private final OkxWebSocketClientManager clientManager; |
| | | private final WangGeListService wangGeListService; |
| | | |
| | | private WebSocketClient webSocketClient; |
| | | private ScheduledExecutorService heartbeatExecutor; |
| | | private volatile ScheduledFuture<?> pongTimeoutFuture; |
| | | private final AtomicReference<Long> lastMessageTime = new AtomicReference<>(System.currentTimeMillis()); |
| | | |
| | | // 连接状态标志 |
| | | private final AtomicBoolean isConnected = new AtomicBoolean(false); |
| | | private final AtomicBoolean isConnecting = new AtomicBoolean(false); |
| | | private final AtomicBoolean isInitialized = new AtomicBoolean(false); |
| | | |
| | | private static final String CHANNEL = "candle15m"; |
| | | |
| | | // 心跳超时时间(秒),小于30秒 |
| | | private static final int HEARTBEAT_TIMEOUT = 10; |
| | | |
| | | // 共享线程池用于重连等异步任务 |
| | | private final ExecutorService sharedExecutor = Executors.newCachedThreadPool(r -> { |
| | | Thread t = new Thread(r, "okx-ws-kline-worker"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | public OkxKlineWebSocketClient(RedisUtils redisUtils, |
| | | CaoZuoService caoZuoService, OkxWebSocketClientManager clientManager, |
| | | WangGeListService wangGeListService) { |
| | | this.redisUtils = redisUtils; |
| | | this.caoZuoService = caoZuoService; |
| | | this.clientManager = clientManager; |
| | | this.wangGeListService = wangGeListService; |
| | | } |
| | | |
| | | /** |
| | | * 初始化方法,创建并初始化WebSocket客户端实例 |
| | | */ |
| | | public void init() { |
| | | if (!isInitialized.compareAndSet(false, true)) { |
| | | log.warn("OkxKlineWebSocketClient 已经初始化过,跳过重复初始化"); |
| | | return; |
| | | } |
| | | connect(); |
| | | startHeartbeat(); |
| | | } |
| | | |
| | | /** |
| | | * 销毁方法,关闭WebSocket连接和相关资源 |
| | | */ |
| | | public void destroy() { |
| | | log.info("开始销毁OkxKlineWebSocketClient"); |
| | | |
| | | // 设置关闭标志,避免重连 |
| | | if (sharedExecutor != null && !sharedExecutor.isShutdown()) { |
| | | sharedExecutor.shutdown(); |
| | | } |
| | | |
| | | if (webSocketClient != null && webSocketClient.isOpen()) { |
| | | try { |
| | | webSocketClient.closeBlocking(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("关闭WebSocket连接时被中断"); |
| | | } |
| | | } |
| | | |
| | | shutdownExecutorGracefully(heartbeatExecutor); |
| | | if (pongTimeoutFuture != null) { |
| | | pongTimeoutFuture.cancel(true); |
| | | } |
| | | shutdownExecutorGracefully(sharedExecutor); |
| | | |
| | | log.info("OkxKlineWebSocketClient销毁完成"); |
| | | } |
| | | |
| | | private static final String WS_URL_MONIPAN = "wss://wspap.okx.com:8443/ws/v5/business"; |
| | | private static final String WS_URL_SHIPAN = "wss://ws.okx.com:8443/ws/v5/business"; |
| | | private static final boolean isAccountType = true; |
| | | |
| | | /** |
| | | * 建立与 OKX WebSocket 服务器的连接。 |
| | | * 设置回调函数以监听连接打开、接收消息、关闭和错误事件。 |
| | | */ |
| | | private void connect() { |
| | | // 避免重复连接 |
| | | if (isConnecting.get()) { |
| | | log.info("连接已在进行中,跳过重复连接请求"); |
| | | return; |
| | | } |
| | | |
| | | if (!isConnecting.compareAndSet(false, true)) { |
| | | log.info("连接已在进行中,跳过重复连接请求"); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | SSLConfig.configureSSL(); |
| | | System.setProperty("https.protocols", "TLSv1.2,TLSv1.3"); |
| | | String WS_URL = WS_URL_MONIPAN; |
| | | if (isAccountType){ |
| | | WS_URL = WS_URL_SHIPAN; |
| | | } |
| | | URI uri = new URI(WS_URL); |
| | | |
| | | // 关闭之前的连接(如果存在) |
| | | if (webSocketClient != null) { |
| | | try { |
| | | webSocketClient.closeBlocking(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.warn("关闭之前连接时被中断"); |
| | | } |
| | | } |
| | | |
| | | webSocketClient = new WebSocketClient(uri) { |
| | | @Override |
| | | public void onOpen(ServerHandshake handshake) { |
| | | log.info("OKX kline WebSocket连接成功"); |
| | | isConnected.set(true); |
| | | isConnecting.set(false); |
| | | |
| | | // 检查应用是否正在关闭 |
| | | if (sharedExecutor != null && !sharedExecutor.isShutdown()) { |
| | | resetHeartbeatTimer(); |
| | | subscribeChannels(); |
| | | } else { |
| | | log.warn("应用正在关闭,忽略WebSocket连接成功回调"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void onMessage(String message) { |
| | | lastMessageTime.set(System.currentTimeMillis()); |
| | | handleWebSocketMessage(message); |
| | | resetHeartbeatTimer(); |
| | | } |
| | | |
| | | @Override |
| | | public void onClose(int code, String reason, boolean remote) { |
| | | log.warn("OKX kline WebSocket连接关闭: code={}, reason={}", code, reason); |
| | | isConnected.set(false); |
| | | isConnecting.set(false); |
| | | cancelPongTimeout(); |
| | | |
| | | if (sharedExecutor != null && !sharedExecutor.isShutdown() && !sharedExecutor.isTerminated()) { |
| | | sharedExecutor.execute(() -> { |
| | | try { |
| | | reconnectWithBackoff(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.error("重连线程被中断", e); |
| | | } catch (Exception e) { |
| | | log.error("重连失败", e); |
| | | } |
| | | }); |
| | | } else { |
| | | log.warn("共享线程池已关闭,无法执行重连任务"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void onError(Exception ex) { |
| | | log.error("OKX New Price WebSocket发生错误", ex); |
| | | isConnected.set(false); |
| | | } |
| | | }; |
| | | |
| | | webSocketClient.connect(); |
| | | } catch (URISyntaxException e) { |
| | | log.error("WebSocket URI格式错误", e); |
| | | isConnecting.set(false); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 订阅指定交易对的价格通道。 |
| | | * 构造订阅请求并发送给服务端。 |
| | | */ |
| | | private void subscribeChannels() { |
| | | JSONObject subscribeMsg = new JSONObject(); |
| | | subscribeMsg.put("op", "subscribe"); |
| | | |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject arg = new JSONObject(); |
| | | arg.put("channel", CHANNEL); |
| | | arg.put("instId", CoinEnums.HE_YUE.getCode()); |
| | | argsArray.add(arg); |
| | | |
| | | subscribeMsg.put("args", argsArray); |
| | | webSocketClient.send(subscribeMsg.toJSONString()); |
| | | log.info("已发送 K线频道订阅请求,订阅通道数: {}", argsArray.size()); |
| | | } |
| | | |
| | | /** |
| | | * 处理从 WebSocket 收到的消息。 |
| | | * 包括订阅确认、错误响应、心跳响应以及实际的数据推送。 |
| | | * |
| | | * @param message 来自 WebSocket 的原始字符串消息 |
| | | */ |
| | | private void handleWebSocketMessage(String message) { |
| | | try { |
| | | JSONObject response = JSON.parseObject(message); |
| | | String event = response.getString("event"); |
| | | |
| | | if ("subscribe".equals(event)) { |
| | | log.info(" K线频道订阅成功: {}", response.getJSONObject("arg")); |
| | | } else if ("error".equals(event)) { |
| | | log.error(" K线频道订阅错误: code={}, msg={}", |
| | | response.getString("code"), response.getString("msg")); |
| | | } else if ("pong".equals(event)) { |
| | | log.debug("收到pong响应"); |
| | | cancelPongTimeout(); |
| | | } else { |
| | | processPushData(response); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理WebSocket消息失败: {}", message, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 解析并处理价格推送数据。 |
| | | * 将最新的标记价格存入 Redis 并触发后续业务逻辑比较处理。 |
| | | * 当价格变化时,调用CaoZuoService的caoZuo方法,触发所有账号的量化操作 |
| | | * |
| | | * @param response 包含价格数据的 JSON 对象 |
| | | */ |
| | | private void processPushData(JSONObject response) { |
| | | try { |
| | | /** |
| | | * { |
| | | * "arg": { |
| | | * "channel": "candle1D", |
| | | * "instId": "BTC-USDT" |
| | | * }, |
| | | * "data": [ |
| | | * [ |
| | | * "1629993600000", |
| | | * "42500", |
| | | * "48199.9", |
| | | * "41006.1", |
| | | * "41006.1", |
| | | * "3587.41204591", |
| | | * "166741046.22583129", |
| | | * "166741046.22583129", |
| | | * "0" |
| | | * ] |
| | | * ] |
| | | * } |
| | | */ |
| | | JSONObject arg = response.getJSONObject("arg"); |
| | | if (arg == null) { |
| | | log.warn("{}: 无效的推送数据,缺少 'arg' 字段", response); |
| | | return; |
| | | } |
| | | |
| | | String channel = arg.getString("channel"); |
| | | if (channel == null) { |
| | | log.warn("{}: 无效的推送数据,缺少 'channel' 字段", response); |
| | | return; |
| | | } |
| | | |
| | | String instId = arg.getString("instId"); |
| | | if (instId == null) { |
| | | log.warn("{}: 无效的推送数据,缺少 'instId' 字段", response); |
| | | return; |
| | | } |
| | | |
| | | if (CHANNEL.equals(channel) && CoinEnums.HE_YUE.getCode().equals(instId)) { |
| | | JSONArray dataArray = response.getJSONArray("data"); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("K线频道数据为空"); |
| | | return; |
| | | } |
| | | JSONArray data = dataArray.getJSONArray(0); |
| | | BigDecimal openPx = new BigDecimal(data.getString(1)); |
| | | BigDecimal highPx = new BigDecimal(data.getString(2)); |
| | | BigDecimal lowPx = new BigDecimal(data.getString(3)); |
| | | BigDecimal closePx = new BigDecimal(data.getString(4)); |
| | | BigDecimal vol = new BigDecimal(data.getString(5)); |
| | | /** |
| | | * K线状态 |
| | | * 0:K线未完结 |
| | | * 1:K线已完结 |
| | | */ |
| | | String confirm = data.getString(8); |
| | | if ("1".equals(confirm)){ |
| | | //调用策略 |
| | | // 创建交易策略 |
| | | TradingStrategy tradingStrategy = new TradingStrategy(); |
| | | |
| | | // 生成100个15分钟价格数据点 |
| | | List<Kline> kline15MinuteData = getKlineDataByInstIdAndBar(instId, "15m"); |
| | | //stream流获取kline15MinuteData中的o数据的集合 |
| | | List<BigDecimal> prices = kline15MinuteData.stream() |
| | | .map(Kline::getO) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 生成对应的高、低、收盘价数据 |
| | | List<BigDecimal> high = kline15MinuteData.stream() |
| | | .map(Kline::getH) |
| | | .collect(Collectors.toList()); |
| | | List<BigDecimal> low = kline15MinuteData.stream() |
| | | .map(Kline::getL) |
| | | .collect(Collectors.toList()); |
| | | List<BigDecimal> close = kline15MinuteData.stream() |
| | | .map(Kline::getC) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 生成成交量数据 |
| | | List<BigDecimal> volume = kline15MinuteData.stream() |
| | | .map(Kline::getVol) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 获取最新价格 |
| | | BigDecimal currentPrice = closePx; |
| | | |
| | | // 生成多周期价格数据(5分钟、1小时、4小时) |
| | | List<Kline> kline5MinuteData = getKlineDataByInstIdAndBar(instId, "5m"); |
| | | List<BigDecimal> fiveMinPrices = kline5MinuteData.stream() |
| | | .map(Kline::getC) |
| | | .collect(Collectors.toList()); |
| | | List<Kline> kline60MinuteData = getKlineDataByInstIdAndBar(instId, "1H"); |
| | | List<BigDecimal> oneHourPrices = kline60MinuteData.stream() |
| | | .map(Kline::getC) |
| | | .collect(Collectors.toList()); |
| | | List<Kline> kline240MinuteData = getKlineDataByInstIdAndBar(instId, "4H"); |
| | | List<BigDecimal> fourHourPrices = kline240MinuteData.stream() |
| | | .map(Kline::getC) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 其他参数 |
| | | BigDecimal fundingRate = new BigDecimal("0.001"); // 正常资金费率 |
| | | boolean hasLargeTransfer = false; // 无大额转账 |
| | | boolean hasUpcomingEvent = false; // 无即将到来的重大事件 |
| | | // 确定市场方向 |
| | | TradingStrategy.Direction direction = tradingStrategy.getDirection(prices, high, low, close, currentPrice); |
| | | System.out.println("市场方向(15分钟): " + direction); |
| | | if (direction == TradingStrategy.Direction.RANGING){ |
| | | return; |
| | | } |
| | | |
| | | /** |
| | | * 获取当前网格信息 |
| | | * 根据当前网格的持仓方向获取反方向是否存在持仓 |
| | | * 如果持有,直接止损 |
| | | */ |
| | | Collection<OkxQuantWebSocketClient> allClients = clientManager.getAllClients(); |
| | | //如果为空,则直接返回 |
| | | if (allClients.isEmpty()) { |
| | | return; |
| | | } |
| | | // 获取所有OkxQuantWebSocketClient实例 |
| | | for (OkxQuantWebSocketClient client : clientManager.getAllClients()) { |
| | | String accountName = client.getAccountName(); |
| | | if (accountName != null) { |
| | | TradingStrategy.SignalType signal = TradingStrategy.SignalType.NONE; |
| | | TradeRequestParam tradeRequestParam = new TradeRequestParam(); |
| | | // 检查当前持仓状态 |
| | | boolean hasLongPosition = false; // 示例:无当前做多持仓 |
| | | boolean hasShortPosition = false; // 示例:无当前做空持仓 |
| | | //先判断账户是否有持多仓 |
| | | String positionLongAccountName = PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode()); |
| | | BigDecimal imrLong = PositionsWs.getAccountMap(positionLongAccountName).get("imr"); |
| | | if (imrLong != null && imrLong.compareTo(BigDecimal.ZERO) > 0){ |
| | | log.info("账户{}有持多仓", accountName); |
| | | hasLongPosition = true; |
| | | |
| | | } |
| | | //先判断账户是否有持空仓 |
| | | String positionShortAccountName = PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode()); |
| | | BigDecimal imrShort = PositionsWs.getAccountMap(positionShortAccountName).get("imr"); |
| | | if (imrShort != null && imrShort.compareTo(BigDecimal.ZERO) > 0){ |
| | | log.info("账户{}有持空仓", accountName); |
| | | hasShortPosition = true; |
| | | } |
| | | signal = tradingStrategy.generateSignal(prices, high, low, close, volume, currentPrice, |
| | | hasLongPosition, hasShortPosition, |
| | | fiveMinPrices, oneHourPrices, fourHourPrices, |
| | | fundingRate, hasLargeTransfer, hasUpcomingEvent); |
| | | log.info("账户{}交易信号: " + signal, accountName); |
| | | if (TradingStrategy.SignalType.NONE == signal) { |
| | | continue; |
| | | }else if (TradingStrategy.SignalType.BUY == signal){ |
| | | tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_LONG.getCode()); |
| | | tradeRequestParam.setSide(CoinEnums.SIDE_BUY.getCode()); |
| | | String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_BUY.getCode()); |
| | | tradeRequestParam.setClOrdId(clOrdId); |
| | | String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); |
| | | tradeRequestParam.setSz(sz); |
| | | TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); |
| | | }else if (TradingStrategy.SignalType.SELL == signal){ |
| | | tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_SHORT.getCode()); |
| | | tradeRequestParam.setSide(CoinEnums.SIDE_SELL.getCode()); |
| | | String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_SELL.getCode()); |
| | | tradeRequestParam.setClOrdId(clOrdId); |
| | | String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); |
| | | tradeRequestParam.setSz(sz); |
| | | TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); |
| | | }else if (TradingStrategy.SignalType.CLOSE_BUY == signal){ |
| | | tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_LONG.getCode()); |
| | | tradeRequestParam.setSide(CoinEnums.SIDE_SELL.getCode()); |
| | | String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_SELL.getCode()); |
| | | tradeRequestParam.setClOrdId(clOrdId); |
| | | BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode())).get("pos"); |
| | | if (BigDecimal.ZERO.compareTo( pos) >= 0) { |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | } |
| | | tradeRequestParam.setSz(String.valueOf( pos)); |
| | | TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); |
| | | }else if (TradingStrategy.SignalType.CLOSE_SELL == signal){ |
| | | tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_SHORT.getCode()); |
| | | tradeRequestParam.setSide(CoinEnums.SIDE_BUY.getCode()); |
| | | String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_BUY.getCode()); |
| | | tradeRequestParam.setClOrdId(clOrdId); |
| | | BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_SHORT.getCode())).get("pos"); |
| | | if (BigDecimal.ZERO.compareTo( pos) >= 0) { |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | } |
| | | tradeRequestParam.setSz(String.valueOf( pos)); |
| | | TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理 K线频道推送数据失败", e); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 触发所有账号的量化操作 |
| | | * @param markPx 当前标记价格 |
| | | */ |
| | | private void triggerQuantOperations(String markPx) { |
| | | try { |
| | | // 1. 判断当前价格属于哪个网格 |
| | | WangGeListEnum gridByPriceNew = WangGeListEnum.getGridByPrice(new BigDecimal(markPx)); |
| | | if (gridByPriceNew == null) { |
| | | log.error("当前 K线频道{}不在任何网格范围内,无法触发量化操作", markPx); |
| | | return; |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | log.error("触发量化操作失败", e); |
| | | } |
| | | } |
| | | |
| | | private List<Kline> getKlineDataByInstIdAndBar(String instId, String bar) { |
| | | |
| | | |
| | | LinkedHashMap<String, Object> requestParam = new LinkedHashMap<>(); |
| | | requestParam.put("instId",instId); |
| | | requestParam.put("bar",bar); |
| | | requestParam.put("limit","100"); |
| | | String result = ExchangeLoginService.getInstance(ExchangeInfoEnum.OKX_UAT.name()).lineHistory(requestParam); |
| | | log.info("加载OKX-KLINE,{}", result); |
| | | JSONObject json = JSON.parseObject(result); |
| | | String data = json.getString("data"); |
| | | List<String[]> klinesList = JSON.parseArray(data, String[].class); |
| | | if(CollUtil.isEmpty(klinesList)){ |
| | | return null; |
| | | } |
| | | ArrayList<Kline> objects = new ArrayList<>(); |
| | | for(String[] s : klinesList) { |
| | | Kline kline = new Kline(); |
| | | kline.setTs(s[0]); |
| | | kline.setO(new BigDecimal(s[1])); |
| | | kline.setH(new BigDecimal(s[2])); |
| | | kline.setL(new BigDecimal(s[3])); |
| | | kline.setC(new BigDecimal(s[4])); |
| | | kline.setVol(new BigDecimal(s[5])); |
| | | kline.setConfirm(s[8]); |
| | | objects.add(kline); |
| | | } |
| | | return objects; |
| | | } |
| | | |
| | | /** |
| | | * 构建 Redis Key |
| | | */ |
| | | private String buildRedisKey(String instId) { |
| | | return "PRICE_" + instId.replace("-", ""); |
| | | } |
| | | |
| | | /** |
| | | * 启动心跳检测任务。 |
| | | * 使用 ScheduledExecutorService 定期检查是否需要发送 ping 请求来维持连接。 |
| | | */ |
| | | private void startHeartbeat() { |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isTerminated()) { |
| | | heartbeatExecutor.shutdownNow(); |
| | | } |
| | | |
| | | heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> { |
| | | Thread t = new Thread(r, "okx-kline-heartbeat"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | heartbeatExecutor.scheduleWithFixedDelay(this::checkHeartbeatTimeout, 25, 25, TimeUnit.SECONDS); |
| | | } |
| | | |
| | | /** |
| | | * 重置心跳计时器。 |
| | | * 当收到新消息或发送 ping 后取消当前超时任务并重新安排下一次超时检查。 |
| | | */ |
| | | private synchronized void resetHeartbeatTimer() { |
| | | cancelPongTimeout(); |
| | | |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) { |
| | | pongTimeoutFuture = heartbeatExecutor.schedule(this::checkHeartbeatTimeout, |
| | | HEARTBEAT_TIMEOUT, TimeUnit.SECONDS); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 检查心跳超时情况。 |
| | | * 若长时间未收到任何消息则主动发送 ping 请求保持连接活跃。 |
| | | */ |
| | | private void checkHeartbeatTimeout() { |
| | | // 只有在连接状态下才检查心跳 |
| | | if (!isConnected.get()) { |
| | | return; |
| | | } |
| | | |
| | | long currentTime = System.currentTimeMillis(); |
| | | long lastTime = lastMessageTime.get(); |
| | | |
| | | if (currentTime - lastTime >= HEARTBEAT_TIMEOUT * 1000L) { |
| | | sendPing(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 发送 ping 请求至 WebSocket 服务端。 |
| | | * 用于维持长连接有效性。 |
| | | */ |
| | | private void sendPing() { |
| | | try { |
| | | if (webSocketClient != null && webSocketClient.isOpen()) { |
| | | JSONObject ping = new JSONObject(); |
| | | ping.put("op", "ping"); |
| | | webSocketClient.send(ping.toJSONString()); |
| | | log.debug("发送ping请求"); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("发送ping失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 取消当前的心跳超时任务。 |
| | | * 在收到 pong 或其他有效消息时调用此方法避免不必要的断开重连。 |
| | | */ |
| | | private synchronized void cancelPongTimeout() { |
| | | if (pongTimeoutFuture != null && !pongTimeoutFuture.isDone()) { |
| | | pongTimeoutFuture.cancel(true); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 执行 WebSocket 重连操作。 |
| | | * 在连接意外中断后尝试重新建立连接。 |
| | | */ |
| | | private void reconnectWithBackoff() throws InterruptedException { |
| | | int attempt = 0; |
| | | int maxAttempts = 3; |
| | | long delayMs = 5000; |
| | | |
| | | while (attempt < maxAttempts) { |
| | | try { |
| | | Thread.sleep(delayMs); |
| | | connect(); |
| | | return; |
| | | } catch (Exception e) { |
| | | log.warn("第{}次重连失败", attempt + 1, e); |
| | | delayMs *= 2; |
| | | attempt++; |
| | | } |
| | | } |
| | | |
| | | log.error("超过最大重试次数({})仍未连接成功", maxAttempts); |
| | | } |
| | | |
| | | /** |
| | | * 优雅关闭线程池 |
| | | */ |
| | | private void shutdownExecutorGracefully(ExecutorService executor) { |
| | | if (executor == null || executor.isTerminated()) { |
| | | return; |
| | | } |
| | | try { |
| | | executor.shutdown(); |
| | | if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { |
| | | executor.shutdownNow(); |
| | | } |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | executor.shutdownNow(); |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.xcong.excoin.modules.okxNewPrice.celue.CaoZuoService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.ExchangeInfoEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.wanggeList.WangGeListService; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeService; |
| | | import com.xcong.excoin.rabbit.pricequeue.WebsocketPriceService; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | private final Map<String, OkxQuantWebSocketClient> quantClientMap = new ConcurrentHashMap<>(); |
| | | |
| | | // 存储OkxNewPriceWebSocketClient实例 |
| | | private OkxNewPriceWebSocketClient newPriceClient; |
| | | private OkxKlineWebSocketClient klinePriceClient; |
| | | |
| | | |
| | | /** |
| | | * 初始化方法,在Spring Bean构造完成后执行 |
| | |
| | | |
| | | // 初始化价格WebSocket客户端 |
| | | try { |
| | | newPriceClient = new OkxNewPriceWebSocketClient(redisUtils, caoZuoService, this, wangGeListService); |
| | | newPriceClient.init(); |
| | | klinePriceClient = new OkxKlineWebSocketClient(redisUtils, caoZuoService, this, wangGeListService); |
| | | klinePriceClient.init(); |
| | | log.info("已初始化OkxNewPriceWebSocketClient"); |
| | | } catch (Exception e) { |
| | | log.error("初始化OkxNewPriceWebSocketClient失败", e); |
| | |
| | | log.info("开始销毁OkxWebSocketClientManager"); |
| | | |
| | | // 关闭价格WebSocket客户端 |
| | | if (newPriceClient != null) { |
| | | if (klinePriceClient != null) { |
| | | try { |
| | | newPriceClient.destroy(); |
| | | klinePriceClient.destroy(); |
| | | log.info("已销毁OkxNewPriceWebSocketClient"); |
| | | } catch (Exception e) { |
| | | log.error("销毁OkxNewPriceWebSocketClient失败", e); |
| | |
| | | * 获取OkxNewPriceWebSocketClient实例 |
| | | * @return 价格WebSocket客户端实例 |
| | | */ |
| | | public OkxNewPriceWebSocketClient getNewPriceClient() { |
| | | return newPriceClient; |
| | | public OkxKlineWebSocketClient getKlineWebSocketClient() { |
| | | return klinePriceClient; |
| | | } |
| | | } |
| | |
| | | */ |
| | | public interface CaoZuoService { |
| | | |
| | | TradeRequestParam caoZuoStrategy(String accountName, String markPx, String posSide); |
| | | |
| | | TradeRequestParam caoZuoHandler(String accountName, String markPx, String posSide); |
| | | |
| | | /** |
| | |
| | | private final RedisUtils redisUtils; |
| | | private final TechnicalIndicatorStrategy technicalIndicatorStrategy; |
| | | |
| | | @Override |
| | | public TradeRequestParam caoZuoStrategy(String accountName, String markPx, String posSide) { |
| | | TradeRequestParam tradeRequestParam = new TradeRequestParam(); |
| | | tradeRequestParam.setAccountName(accountName); |
| | | tradeRequestParam.setInstId(CoinEnums.HE_YUE.getCode()); |
| | | tradeRequestParam.setTdMode(CoinEnums.CROSS.getCode()); |
| | | tradeRequestParam.setPosSide(posSide); |
| | | tradeRequestParam.setOrdType(CoinEnums.ORDTYPE_MARKET.getCode()); |
| | | |
| | | log.info("操作账户:{},当前价格: {},仓位方向: {}", accountName,markPx,posSide); |
| | | /** |
| | | * 准备工作 |
| | | * 1、准备好下单的基本信息 |
| | | */ |
| | | // 系统设置的开关,等于冷静中,则代表不开仓 |
| | | String outStr = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.OUT.name()); |
| | | if (OrderParamEnums.OUT_YES.getValue().equals(outStr)){ |
| | | log.error("冷静中,不允许下单......"); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | return tradeRequestParam; |
| | | } |
| | | BigDecimal cashBal = WsMapBuild.parseBigDecimalSafe(AccountWs.getAccountMap(accountName).get("cashBal")); |
| | | /** |
| | | * 判断止损抗压 |
| | | */ |
| | | BigDecimal realKuiSunAmount = WsMapBuild.parseBigDecimalSafe(AccountWs.getAccountMap(accountName).get("upl")); |
| | | log.info("实际盈亏金额: {}", realKuiSunAmount); |
| | | String zhiSunPercent = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.ZHI_SUN.name()); |
| | | BigDecimal zhiSunAmount = cashBal.multiply(new BigDecimal(zhiSunPercent)); |
| | | log.info("预期亏损金额: {}", zhiSunAmount); |
| | | String kangYaPercent = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.KANG_CANG.name()); |
| | | BigDecimal kangYaAmount = cashBal.multiply(new BigDecimal(kangYaPercent)); |
| | | log.info("预期抗仓金额: {}", kangYaAmount); |
| | | |
| | | if (realKuiSunAmount.compareTo(BigDecimal.ZERO) < 0){ |
| | | realKuiSunAmount = realKuiSunAmount.multiply(new BigDecimal("-1")); |
| | | // 账户预期亏损金额比这个还小时,立即止损 |
| | | if (realKuiSunAmount.compareTo(zhiSunAmount) > 0){ |
| | | log.error("账户冷静止损......"); |
| | | WsMapBuild.saveStringToMap(InstrumentsWs.getAccountMap(accountName), CoinEnums.OUT.name(), OrderParamEnums.OUT_YES.getValue()); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_YES.getValue()); |
| | | return caoZuoZhiSunEvent(accountName, markPx, posSide); |
| | | } |
| | | // 判断抗压 |
| | | if (realKuiSunAmount.compareTo(kangYaAmount) > 0 && realKuiSunAmount.compareTo(zhiSunAmount) <= 0){ |
| | | log.error("账户紧张扛仓......"); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | return tradeRequestParam; |
| | | } |
| | | } |
| | | |
| | | String positionAccountName = PositionsWs.initAccountName(accountName, posSide); |
| | | // 判断是否保证金超标 |
| | | if (PositionsWs.getAccountMap(positionAccountName).get("imr") == null){ |
| | | log.error("没有获取到持仓信息,等待初始化......"); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | return tradeRequestParam; |
| | | } |
| | | BigDecimal ordFrozImr = PositionsWs.getAccountMap(positionAccountName).get("imr"); |
| | | BigDecimal totalOrderUsdt = WsMapBuild.parseBigDecimalSafe(AccountWs.getAccountMap(accountName).get(CoinEnums.TOTAL_ORDER_USDT.name())) |
| | | .divide(new BigDecimal("2"), RoundingMode.DOWN); |
| | | if (ordFrozImr.compareTo(totalOrderUsdt) >= 0){ |
| | | log.error("已满仓......"); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | return tradeRequestParam; |
| | | } |
| | | |
| | | if (PositionsWs.getAccountMap(positionAccountName).get("pos") == null){ |
| | | log.error("没有获取到持仓信息,等待初始化......"); |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); |
| | | return tradeRequestParam; |
| | | } |
| | | tradeRequestParam.setTradeType(OrderParamEnums.TRADE_YES.getValue()); |
| | | return chooseEvent(tradeRequestParam); |
| | | } |
| | | |
| | | /** |
| | | * 执行主要的操作逻辑,包括读取合约状态、获取市场价格信息, |
| | | * 并根据当前持仓均价和标记价格决定是否执行买卖操作。 |
| | |
| | | /** |
| | | * Advanced MA (Moving Average) 指标实现 |
| | | * 支持扩展周期的指数移动平均线(EMA),用于三重EMA交叉系统 |
| | | * |
| | | * 作用: |
| | | * 1. 基于三重EMA交叉系统识别趋势方向和强度 |
| | | * 2. 当9EMA > 21EMA > 55EMA时形成多头排列,提示上涨趋势 |
| | | * 3. 当9EMA < 21EMA < 55EMA时形成空头排列,提示下跌趋势 |
| | | * 4. 计算三线粘合度,自动过滤震荡行情 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少1个价格数据点用于计算,根据不同周期需求更多数据点 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 5分钟(5m): |
| | | * - 优点:适合短线三重EMA交叉策略 |
| | | * - 缺点:需要频繁监控,容易受短期波动影响 |
| | | * 2. 15分钟(15m): |
| | | * - 优点:平衡了信号可靠性和反应速度 |
| | | * - 缺点:仍有一定噪音 |
| | | * 3. 1小时(1h): |
| | | * - 优点:信号较为可靠,适合中期趋势跟踪 |
| | | * - 缺点:反应较慢 |
| | | * 4. 4小时(4h)及以上: |
| | | * - 优点:趋势信号明确,适合长期持仓 |
| | | * - 缺点:反应滞后,入场点较晚 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| | |
| | | * 1. 中轨(MB)= N日移动平均线 |
| | | * 2. 上轨(UP)= 中轨 + K倍标准差 |
| | | * 3. 下轨(DN)= 中轨 - K倍标准差 |
| | | * |
| | | * 作用: |
| | | * 1. 测量价格波动范围和市场宽度 |
| | | * 2. 价格突破上轨,提示超买或趋势加速 |
| | | * 3. 价格跌破下轨,提示超卖或趋势加速 |
| | | * 4. 轨道收窄,提示即将发生剧烈波动 |
| | | * 5. 价格回归轨道内,提示趋势可能反转 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少20个(默认周期)价格数据点用于计算 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线突破策略 |
| | | * - 缺点:布林带宽度窄,假突破多 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:布林带宽度适中,突破信号相对可靠 |
| | | * - 缺点:仍有一定假突破 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:适合日内交易,突破信号较为可靠 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:布林带宽度稳定,突破信号可靠 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.indicator; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 15分钟交易策略使用示例 |
| | | * 展示如何使用FifteenMinuteTradingStrategy处理100个15分钟价格数据点 |
| | | */ |
| | | public class FifteenMinuteStrategyExample { |
| | | |
| | | public static void main(String[] args) { |
| | | // 1. 创建策略实例 |
| | | FifteenMinuteTradingStrategy strategy = new FifteenMinuteTradingStrategy(); |
| | | |
| | | // 2. 准备100个15分钟价格数据(这里使用模拟数据,用户可以替换为真实数据) |
| | | List<BigDecimal> prices = generateSampleFifteenMinuteData(); |
| | | System.out.println("已加载 " + prices.size() + " 个15分钟价格数据点"); |
| | | |
| | | // 3. 获取当前价格 |
| | | BigDecimal currentPrice = prices.get(prices.size() - 1); |
| | | System.out.println("当前价格: " + currentPrice); |
| | | |
| | | // 4. 示例1:获取多空方向 |
| | | System.out.println("\n=== 多空方向分析 ==="); |
| | | FifteenMinuteTradingStrategy.Direction direction = strategy.getDirection(prices); |
| | | System.out.println("当前市场方向: " + direction); |
| | | |
| | | // 5. 示例2:获取开仓平仓信号(假设当前没有持仓) |
| | | System.out.println("\n=== 开仓平仓信号分析(无持仓)==="); |
| | | FifteenMinuteTradingStrategy.PositionSignal signal1 = |
| | | strategy.getPositionSignal(prices, false, false); |
| | | System.out.println("无持仓时的信号: " + signal1); |
| | | |
| | | // 6. 示例3:获取开仓平仓信号(假设当前持有多仓) |
| | | System.out.println("\n=== 开仓平仓信号分析(持有多仓)==="); |
| | | FifteenMinuteTradingStrategy.PositionSignal signal2 = |
| | | strategy.getPositionSignal(prices, true, false); |
| | | System.out.println("持有多仓时的信号: " + signal2); |
| | | |
| | | // 7. 示例4:获取开仓平仓信号(假设当前持有空仓) |
| | | System.out.println("\n=== 开仓平仓信号分析(持有空仓)==="); |
| | | FifteenMinuteTradingStrategy.PositionSignal signal3 = |
| | | strategy.getPositionSignal(prices, false, true); |
| | | System.out.println("持有空仓时的信号: " + signal3); |
| | | |
| | | // 8. 示例5:获取完整交易结果 |
| | | System.out.println("\n=== 完整交易结果分析 ==="); |
| | | FifteenMinuteTradingStrategy.TradingResult result = |
| | | strategy.getTradingResult(prices, false, false); |
| | | System.out.println("市场方向: " + result.getDirection()); |
| | | System.out.println("交易信号: " + result.getSignal()); |
| | | System.out.println("\n指标状态详情:"); |
| | | System.out.println(result.getIndicatorStatus()); |
| | | } |
| | | |
| | | /** |
| | | * 生成模拟的15分钟价格数据(100个数据点) |
| | | * 用户可以替换为真实的价格数据 |
| | | * @return 15分钟价格数据列表 |
| | | */ |
| | | private static List<BigDecimal> generateSampleFifteenMinuteData() { |
| | | List<BigDecimal> prices = new ArrayList<>(); |
| | | |
| | | // 模拟ETH价格数据(从2400开始,有一定波动) |
| | | BigDecimal basePrice = new BigDecimal(2400); |
| | | |
| | | for (int i = 0; i < 100; i++) { |
| | | // 添加一些随机波动,但保持整体上升趋势 |
| | | double random = (Math.random() - 0.48) * 10; // -5 到 5 的随机波动,略微偏向上行 |
| | | BigDecimal price = basePrice.add(new BigDecimal(random)); |
| | | prices.add(price.setScale(2, BigDecimal.ROUND_HALF_UP)); |
| | | |
| | | // 整体缓慢上升 |
| | | basePrice = basePrice.add(new BigDecimal(0.2)); |
| | | } |
| | | |
| | | return prices; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.indicator; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Random; |
| | | |
| | | /** |
| | | * 15分钟交易策略示例 |
| | | * 演示如何使用交易策略与15分钟时间框架数据 |
| | | * 展示如何获取方向信号和交易信号 |
| | | */ |
| | | public class FifteenMinuteTradingExample { |
| | | |
| | | public static void main(String[] args) { |
| | | // 创建交易策略 |
| | | TradingStrategy tradingStrategy = new TradingStrategy(); |
| | | |
| | | // 生成100个15分钟价格数据点 |
| | | List<BigDecimal> prices = generateSampleFifteenMinuteData(100); |
| | | |
| | | // 生成对应的高、低、收盘价数据 |
| | | List<BigDecimal> high = generateHighPrices(prices); |
| | | List<BigDecimal> low = generateLowPrices(prices); |
| | | List<BigDecimal> close = new ArrayList<>(prices); // 使用价格作为收盘价 |
| | | |
| | | // 生成成交量数据 |
| | | List<BigDecimal> volume = generateVolumeData(prices.size()); |
| | | |
| | | // 获取最新价格 |
| | | BigDecimal currentPrice = prices.get(prices.size() - 1); |
| | | |
| | | // 生成多周期价格数据(5分钟、1小时、4小时) |
| | | List<BigDecimal> fiveMinPrices = generateSampleFifteenMinuteData(100); |
| | | List<BigDecimal> oneHourPrices = generateSampleFifteenMinuteData(100); |
| | | List<BigDecimal> fourHourPrices = generateSampleFifteenMinuteData(100); |
| | | |
| | | // 其他参数 |
| | | BigDecimal fundingRate = new BigDecimal("0.001"); // 正常资金费率 |
| | | boolean hasLargeTransfer = false; // 无大额转账 |
| | | boolean hasUpcomingEvent = false; // 无即将到来的重大事件 |
| | | |
| | | // 确定市场方向 |
| | | TradingStrategy.Direction direction = tradingStrategy.getDirection(prices, high, low, close, currentPrice); |
| | | System.out.println("市场方向(15分钟): " + direction); |
| | | |
| | | // 检查当前持仓状态 |
| | | boolean hasLongPosition = false; // 示例:无当前做多持仓 |
| | | boolean hasShortPosition = false; // 示例:无当前做空持仓 |
| | | |
| | | // 生成交易信号(开仓/平仓) |
| | | TradingStrategy.SignalType signal = tradingStrategy.generateSignal(prices, high, low, close, volume, currentPrice, |
| | | hasLongPosition, hasShortPosition, |
| | | fiveMinPrices, oneHourPrices, fourHourPrices, |
| | | fundingRate, hasLargeTransfer, hasUpcomingEvent); |
| | | System.out.println("交易信号(15分钟): " + signal); |
| | | |
| | | // 显示指标状态用于分析 |
| | | System.out.println("\n指标状态:"); |
| | | System.out.println(tradingStrategy.getIndicatorStatus()); |
| | | |
| | | // 计算动态杠杆 |
| | | BigDecimal dynamicLeverage = tradingStrategy.calculateDynamicLeverage(high, low, close); |
| | | System.out.println("\n动态杠杆倍数: " + dynamicLeverage); |
| | | |
| | | // 基于信号模拟持仓变化 |
| | | if (signal == TradingStrategy.SignalType.BUY) { |
| | | System.out.println("\n=== 执行开多操作 ==="); |
| | | hasLongPosition = true; |
| | | |
| | | // 演示三段式止盈策略 |
| | | BigDecimal entryPrice = currentPrice; |
| | | BigDecimal positionSize = new BigDecimal(100); |
| | | TradingStrategy.ProfitTakingResult profitTakingResult = |
| | | tradingStrategy.calculateThreeStepProfitTaking(entryPrice, currentPrice, direction, positionSize); |
| | | System.out.println("三段式止盈信号: " + profitTakingResult.getSignal()); |
| | | System.out.println("应平仓仓位大小: " + profitTakingResult.getClosePositionSize()); |
| | | } else if (signal == TradingStrategy.SignalType.SELL) { |
| | | System.out.println("\n=== 执行开空操作 ==="); |
| | | hasShortPosition = true; |
| | | |
| | | // 演示三段式止盈策略 |
| | | BigDecimal entryPrice = currentPrice; |
| | | BigDecimal positionSize = new BigDecimal(100); |
| | | TradingStrategy.ProfitTakingResult profitTakingResult = |
| | | tradingStrategy.calculateThreeStepProfitTaking(entryPrice, currentPrice, direction, positionSize); |
| | | System.out.println("三段式止盈信号: " + profitTakingResult.getSignal()); |
| | | System.out.println("应平仓仓位大小: " + profitTakingResult.getClosePositionSize()); |
| | | } else if (signal == TradingStrategy.SignalType.CLOSE_BUY) { |
| | | System.out.println("\n=== 执行平多操作 ==="); |
| | | hasLongPosition = false; |
| | | } else if (signal == TradingStrategy.SignalType.CLOSE_SELL) { |
| | | System.out.println("\n=== 执行平空操作 ==="); |
| | | hasShortPosition = false; |
| | | } else { |
| | | System.out.println("\n无需交易操作。"); |
| | | } |
| | | |
| | | // 现有做多持仓的模拟示例 |
| | | System.out.println("\n=== 现有做多持仓的模拟 ==="); |
| | | hasLongPosition = true; |
| | | hasShortPosition = false; |
| | | signal = tradingStrategy.generateSignal(prices, high, low, close, volume, currentPrice, |
| | | hasLongPosition, hasShortPosition, |
| | | fiveMinPrices, oneHourPrices, fourHourPrices, |
| | | fundingRate, hasLargeTransfer, hasUpcomingEvent); |
| | | System.out.println("有做多持仓时的交易信号: " + signal); |
| | | |
| | | // 现有做空持仓的模拟示例 |
| | | System.out.println("\n=== 现有做空持仓的模拟 ==="); |
| | | hasLongPosition = false; |
| | | hasShortPosition = true; |
| | | signal = tradingStrategy.generateSignal(prices, high, low, close, volume, currentPrice, |
| | | hasLongPosition, hasShortPosition, |
| | | fiveMinPrices, oneHourPrices, fourHourPrices, |
| | | fundingRate, hasLargeTransfer, hasUpcomingEvent); |
| | | System.out.println("有做空持仓时的交易信号: " + signal); |
| | | |
| | | // 模拟盈利场景演示三段式止盈 |
| | | System.out.println("\n=== 三段式止盈盈利场景演示 ==="); |
| | | BigDecimal entryPrice = new BigDecimal(2500.0); |
| | | BigDecimal currentPriceProfit = new BigDecimal(2700.0); // 模拟盈利价格 |
| | | BigDecimal positionSize = new BigDecimal(100); |
| | | TradingStrategy.ProfitTakingResult profitTakingResult = |
| | | tradingStrategy.calculateThreeStepProfitTaking(entryPrice, currentPriceProfit, TradingStrategy.Direction.LONG, positionSize); |
| | | System.out.println("入场价格: " + entryPrice); |
| | | System.out.println("当前价格: " + currentPriceProfit); |
| | | System.out.println("三段式止盈信号: " + profitTakingResult.getSignal()); |
| | | System.out.println("应平仓仓位大小: " + profitTakingResult.getClosePositionSize()); |
| | | } |
| | | |
| | | /** |
| | | * 生成具有真实波动的15分钟价格数据 |
| | | * @param size 要生成的数据点数量 |
| | | * @return 价格数据列表 |
| | | */ |
| | | private static List<BigDecimal> generateSampleFifteenMinuteData(int size) { |
| | | List<BigDecimal> prices = new ArrayList<>(); |
| | | Random random = new Random(); |
| | | |
| | | // 以基础价格开始(ETH示例价格) |
| | | BigDecimal basePrice = new BigDecimal(2500.0); |
| | | prices.add(basePrice); |
| | | |
| | | // 生成具有真实波动的后续价格 |
| | | for (int i = 1; i < size; i++) { |
| | | // 创建价格趋势(轻微上升偏向) |
| | | BigDecimal trend = new BigDecimal(0.1).multiply(new BigDecimal(i)); |
| | | // 添加随机波动(每个周期±2%) |
| | | BigDecimal volatility = new BigDecimal(random.nextDouble() * 0.04 - 0.02); |
| | | // 计算新价格 |
| | | BigDecimal newPrice = basePrice.add(trend).multiply(BigDecimal.ONE.add(volatility)); |
| | | // 四舍五入到2位小数 |
| | | newPrice = newPrice.setScale(2, BigDecimal.ROUND_HALF_UP); |
| | | prices.add(newPrice); |
| | | } |
| | | |
| | | return prices; |
| | | } |
| | | |
| | | /** |
| | | * 生成最高价数据 |
| | | * @param prices 价格数据 |
| | | * @return 最高价数据列表 |
| | | */ |
| | | private static List<BigDecimal> generateHighPrices(List<BigDecimal> prices) { |
| | | List<BigDecimal> high = new ArrayList<>(); |
| | | Random random = new Random(); |
| | | |
| | | for (BigDecimal price : prices) { |
| | | // 最高价比当前价格高0-2% |
| | | BigDecimal highPrice = price.multiply(BigDecimal.ONE.add(new BigDecimal(random.nextDouble() * 0.02))); |
| | | high.add(highPrice.setScale(2, BigDecimal.ROUND_HALF_UP)); |
| | | } |
| | | |
| | | return high; |
| | | } |
| | | |
| | | /** |
| | | * 生成最低价数据 |
| | | * @param prices 价格数据 |
| | | * @return 最低价数据列表 |
| | | */ |
| | | private static List<BigDecimal> generateLowPrices(List<BigDecimal> prices) { |
| | | List<BigDecimal> low = new ArrayList<>(); |
| | | Random random = new Random(); |
| | | |
| | | for (BigDecimal price : prices) { |
| | | // 最低价比当前价格低0-2% |
| | | BigDecimal lowPrice = price.multiply(BigDecimal.ONE.subtract(new BigDecimal(random.nextDouble() * 0.02))); |
| | | low.add(lowPrice.setScale(2, BigDecimal.ROUND_HALF_UP)); |
| | | } |
| | | |
| | | return low; |
| | | } |
| | | |
| | | /** |
| | | * 生成成交量数据 |
| | | * @param size 数据点数量 |
| | | * @return 成交量数据列表 |
| | | */ |
| | | private static List<BigDecimal> generateVolumeData(int size) { |
| | | List<BigDecimal> volume = new ArrayList<>(); |
| | | Random random = new Random(); |
| | | |
| | | for (int i = 0; i < size; i++) { |
| | | // 生成1000-10000之间的随机成交量 |
| | | BigDecimal vol = new BigDecimal(random.nextInt(9001) + 1000); |
| | | volume.add(vol); |
| | | } |
| | | |
| | | return volume; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.indicator; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 15分钟时间粒度的交易策略实现 |
| | | * 专门针对100个15分钟数据点设计的策略,包含明确的多空方向选择和开仓平仓方法 |
| | | */ |
| | | @Slf4j |
| | | public class FifteenMinuteTradingStrategy { |
| | | |
| | | @Getter |
| | | @Setter |
| | | public static class TradingResult { |
| | | private Direction direction; // 多空方向 |
| | | private PositionSignal signal; // 开仓平仓信号 |
| | | private String indicatorStatus; // 指标状态 |
| | | |
| | | public TradingResult(Direction direction, PositionSignal signal, String indicatorStatus) { |
| | | this.direction = direction; |
| | | this.signal = signal; |
| | | this.indicatorStatus = indicatorStatus; |
| | | } |
| | | } |
| | | |
| | | public enum Direction { |
| | | LONG, // 多头方向 |
| | | SHORT, // 空头方向 |
| | | RANGING // 震荡行情 |
| | | } |
| | | |
| | | public enum PositionSignal { |
| | | OPEN_LONG, // 开多仓 |
| | | OPEN_SHORT, // 开空仓 |
| | | CLOSE_LONG, // 平多仓 |
| | | CLOSE_SHORT, // 平空仓 |
| | | HOLD, // 持有 |
| | | STAY_OUT // 观望 |
| | | } |
| | | |
| | | private final MA ma; |
| | | private final AdvancedMA advancedMA; |
| | | private final BOLL boll; |
| | | private final KDJ kdj; |
| | | private final MACD macd; |
| | | private final RSI rsi; |
| | | |
| | | public FifteenMinuteTradingStrategy() { |
| | | // 15分钟数据优化的参数配置 |
| | | this.ma = new MA(); |
| | | this.advancedMA = new AdvancedMA(); |
| | | this.boll = new BOLL(20, 2.0); // BOLL使用默认20周期 |
| | | this.kdj = new KDJ(9); // KDJ使用默认9周期 |
| | | this.macd = new MACD(); // MACD使用默认12/26/9周期 |
| | | this.rsi = new RSI(14); // RSI使用默认14周期 |
| | | } |
| | | |
| | | /** |
| | | * 计算所有指标 |
| | | * @param prices 15分钟价格数据(至少100个数据点) |
| | | */ |
| | | private void calculateIndicators(List<BigDecimal> prices) { |
| | | ma.calculate(prices); |
| | | advancedMA.calculateTripleEMA(prices); |
| | | boll.calculate(prices); |
| | | kdj.calculate(prices); |
| | | macd.calculate(prices); |
| | | rsi.calculate(prices); |
| | | } |
| | | |
| | | /** |
| | | * 判断市场是否处于震荡行情 |
| | | * @return 是否为震荡行情 |
| | | */ |
| | | private boolean isRangeMarket() { |
| | | // AdvancedMA三线粘合 + RSI(40-60) + BOLL带宽收窄 |
| | | boolean isMaConverged = advancedMA.calculatePercent().compareTo(new BigDecimal(2)) < 0; |
| | | boolean isRsiNeutral = rsi.getRsi().compareTo(new BigDecimal(40)) > 0 && |
| | | rsi.getRsi().compareTo(new BigDecimal(60)) < 0; |
| | | boolean isBollNarrow = boll.calculateBandWidth().compareTo(new BigDecimal(0.05)) < 0; |
| | | |
| | | return isMaConverged && isRsiNeutral && isBollNarrow; |
| | | } |
| | | |
| | | /** |
| | | * 获取多空方向选择 |
| | | * @param prices 15分钟价格数据(100个数据点) |
| | | * @return 多空方向 |
| | | */ |
| | | public Direction getDirection(List<BigDecimal> prices) { |
| | | if (prices == null || prices.size() < 100) { |
| | | throw new IllegalArgumentException("需要至少100个15分钟价格数据点"); |
| | | } |
| | | |
| | | calculateIndicators(prices); |
| | | |
| | | // 震荡过滤 |
| | | if (isRangeMarket()) { |
| | | return Direction.RANGING; |
| | | } |
| | | |
| | | BigDecimal currentPrice = prices.get(prices.size() - 1); |
| | | |
| | | // 多头信号判断:MA多头排列 + MACD金叉 + RSI(30-70) + BOLL价格在上轨与中轨之间 |
| | | boolean isLongSignal = |
| | | ma.getEma5().compareTo(ma.getEma10()) > 0 && // MA5 > MA10 |
| | | ma.getEma10().compareTo(ma.getEma20()) > 0 && // MA10 > MA20 |
| | | macd.getDif().compareTo(macd.getDea()) > 0 && // MACD金叉 |
| | | rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && rsi.getRsi().compareTo(new BigDecimal(70)) < 0 && // RSI(30-70) |
| | | currentPrice.compareTo(boll.getMid()) > 0 && currentPrice.compareTo(boll.getUpper()) < 0; // BOLL价格在上轨与中轨之间 |
| | | |
| | | // 空头信号判断:MA空头排列 + MACD死叉 + RSI(30-70) + BOLL价格在下轨与中轨之间 |
| | | boolean isShortSignal = |
| | | ma.getEma5().compareTo(ma.getEma10()) < 0 && // MA5 < MA10 |
| | | ma.getEma10().compareTo(ma.getEma20()) < 0 && // MA10 < MA20 |
| | | macd.getDif().compareTo(macd.getDea()) < 0 && // MACD死叉 |
| | | rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && rsi.getRsi().compareTo(new BigDecimal(70)) < 0 && // RSI(30-70) |
| | | currentPrice.compareTo(boll.getMid()) < 0 && currentPrice.compareTo(boll.getLower()) > 0; // BOLL价格在下轨与中轨之间 |
| | | |
| | | if (isLongSignal) { |
| | | return Direction.LONG; |
| | | } else if (isShortSignal) { |
| | | return Direction.SHORT; |
| | | } else { |
| | | return Direction.RANGING; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取开仓平仓策略信号 |
| | | * @param prices 15分钟价格数据(100个数据点) |
| | | * @param hasLongPosition 当前是否持有多仓 |
| | | * @param hasShortPosition 当前是否持有空仓 |
| | | * @return 开仓平仓信号 |
| | | */ |
| | | public PositionSignal getPositionSignal(List<BigDecimal> prices, boolean hasLongPosition, boolean hasShortPosition) { |
| | | if (prices == null || prices.size() < 100) { |
| | | throw new IllegalArgumentException("需要至少100个15分钟价格数据点"); |
| | | } |
| | | |
| | | calculateIndicators(prices); |
| | | |
| | | // 震荡过滤 |
| | | if (isRangeMarket()) { |
| | | return PositionSignal.STAY_OUT; |
| | | } |
| | | |
| | | BigDecimal currentPrice = prices.get(prices.size() - 1); |
| | | |
| | | // 开多信号:MA金叉 + MACD金叉 + KDJ金叉 + RSI中性 + 价格在BOLL中轨上方 |
| | | boolean shouldOpenLong = |
| | | ma.getEma5().compareTo(ma.getEma20()) > 0 && // MA金叉(5日EMA上穿20日EMA) |
| | | macd.getDif().compareTo(macd.getDea()) > 0 && macd.getMacdBar().compareTo(BigDecimal.ZERO) > 0 && // MACD金叉且柱状图为正 |
| | | kdj.isGoldenCross() && // KDJ金叉 |
| | | rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && rsi.getRsi().compareTo(new BigDecimal(70)) < 0 && // RSI中性 |
| | | currentPrice.compareTo(boll.getMid()) > 0; // 价格在BOLL中轨上方 |
| | | |
| | | // 开空信号:MA死叉 + MACD死叉 + KDJ死叉 + RSI中性 + 价格在BOLL中轨下方 |
| | | boolean shouldOpenShort = |
| | | ma.getEma5().compareTo(ma.getEma20()) < 0 && // MA死叉(5日EMA下穿20日EMA) |
| | | macd.getDif().compareTo(macd.getDea()) < 0 && macd.getMacdBar().compareTo(BigDecimal.ZERO) < 0 && // MACD死叉且柱状图为负 |
| | | kdj.isDeathCross() && // KDJ死叉 |
| | | rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && rsi.getRsi().compareTo(new BigDecimal(70)) < 0 && // RSI中性 |
| | | currentPrice.compareTo(boll.getMid()) < 0; // 价格在BOLL中轨下方 |
| | | |
| | | // 平多信号:MA死叉 + MACD死叉 + RSI超买 + 价格跌破BOLL中轨 |
| | | boolean shouldCloseLong = |
| | | (ma.getEma5().compareTo(ma.getEma20()) < 0 && // MA死叉 |
| | | macd.getDif().compareTo(macd.getDea()) < 0 && // MACD死叉 |
| | | (rsi.isOverbought() || rsi.isExtremelyOverbought())) || // RSI超买 |
| | | currentPrice.compareTo(boll.getMid()) < 0; // 价格跌破BOLL中轨 |
| | | |
| | | // 平空信号:MA金叉 + MACD金叉 + RSI超卖 + 价格突破BOLL中轨 |
| | | boolean shouldCloseShort = |
| | | (ma.getEma5().compareTo(ma.getEma20()) > 0 && // MA金叉 |
| | | macd.getDif().compareTo(macd.getDea()) > 0 && // MACD金叉 |
| | | (rsi.isOversold() || rsi.isExtremelyOversold())) || // RSI超卖 |
| | | currentPrice.compareTo(boll.getMid()) > 0; // 价格突破BOLL中轨 |
| | | |
| | | // 确定开仓信号 |
| | | if (shouldOpenLong && !hasLongPosition && !hasShortPosition) { |
| | | return PositionSignal.OPEN_LONG; |
| | | } else if (shouldOpenShort && !hasLongPosition && !hasShortPosition) { |
| | | return PositionSignal.OPEN_SHORT; |
| | | } |
| | | |
| | | // 确定平仓信号 |
| | | if (shouldCloseLong && hasLongPosition) { |
| | | return PositionSignal.CLOSE_LONG; |
| | | } else if (shouldCloseShort && hasShortPosition) { |
| | | return PositionSignal.CLOSE_SHORT; |
| | | } |
| | | |
| | | // 无信号 |
| | | return hasLongPosition || hasShortPosition ? PositionSignal.HOLD : PositionSignal.STAY_OUT; |
| | | } |
| | | |
| | | /** |
| | | * 综合获取交易结果 |
| | | * @param prices 15分钟价格数据(100个数据点) |
| | | * @param hasLongPosition 当前是否持有多仓 |
| | | * @param hasShortPosition 当前是否持有空仓 |
| | | * @return 包含多空方向和开仓平仓信号的完整交易结果 |
| | | */ |
| | | public TradingResult getTradingResult(List<BigDecimal> prices, boolean hasLongPosition, boolean hasShortPosition) { |
| | | Direction direction = getDirection(prices); |
| | | PositionSignal signal = getPositionSignal(prices, hasLongPosition, hasShortPosition); |
| | | String indicatorStatus = getIndicatorStatus(); |
| | | |
| | | return new TradingResult(direction, signal, indicatorStatus); |
| | | } |
| | | |
| | | /** |
| | | * 获取当前指标状态 |
| | | * @return 指标状态字符串 |
| | | */ |
| | | private String getIndicatorStatus() { |
| | | return String.format("MA5: %s, MA20: %s, " + |
| | | "MACD-DIF: %s, MACD-DEA: %s, MACD-BAR: %s, " + |
| | | "KDJ-K: %s, KDJ-D: %s, KDJ-J: %s, " + |
| | | "RSI: %s, " + |
| | | "BOLL-UP: %s, BOLL-MID: %s, BOLL-DN: %s, " + |
| | | "AdvancedMA-Bullish: %s, Bearish: %s, Percent: %s", |
| | | ma.getEma5(), ma.getEma20(), |
| | | macd.getDif(), macd.getDea(), macd.getMacdBar(), |
| | | kdj.getK(), kdj.getD(), kdj.getJ(), |
| | | rsi.getRsi(), |
| | | boll.getUpper(), boll.getMid(), boll.getLower(), |
| | | advancedMA.isBullish(), advancedMA.isBearish(), advancedMA.calculatePercent()); |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | /** |
| | | * 技术指标基础类,提供通用计算方法 |
| | | * |
| | | * 指标组合策略: |
| | | * 1. 趋势判断(MA/AdvancedMA/MACD):判断价格的整体走势方向 |
| | | * 2. 动量确认(RSI/KDJ):确认当前趋势的强度和可持续性 |
| | | * 3. 波动参考(BOLL):确定价格的合理波动范围和突破时机 |
| | | * |
| | | * 多空方向选择逻辑: |
| | | * - 多头信号:MA多头排列 + MACD金叉 + RSI(30-70) + BOLL价格在上轨与中轨之间 |
| | | * - 空头信号:MA空头排列 + MACD死叉 + RSI(30-70) + BOLL价格在下轨与中轨之间 |
| | | * - 震荡信号:AdvancedMA三线粘合 + RSI(40-60) + BOLL带宽收窄 |
| | | * |
| | | * 开仓和平仓策略: |
| | | * - 开多:MA金叉 + MACD金叉 + KDJ金叉 + RSI(30-70) + 价格突破BOLL中轨 |
| | | * - 开空:MA死叉 + MACD死叉 + KDJ死叉 + RSI(30-70) + 价格跌破BOLL中轨 |
| | | * - 平多:MA死叉 + MACD死叉 + RSI超买(>70) + 价格跌破BOLL中轨 |
| | | * - 平空:MA金叉 + MACD金叉 + RSI超卖(<30) + 价格突破BOLL中轨 |
| | | */ |
| | | public abstract class IndicatorBase { |
| | | |
| | |
| | | int startIndex = Math.max(0, prices.size() - period); |
| | | return prices.subList(startIndex, prices.size()); |
| | | } |
| | | |
| | | /** |
| | | * 计算ATR(Average True Range) |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | * @param period 周期 |
| | | * @return ATR值 |
| | | */ |
| | | protected BigDecimal calculateATR(List<BigDecimal> high, List<BigDecimal> low, List<BigDecimal> close, int period) { |
| | | if (high == null || low == null || close == null || |
| | | high.size() < period || low.size() < period || close.size() < period) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | List<BigDecimal> trList = new ArrayList<>(); |
| | | for (int i = 1; i < high.size(); i++) { |
| | | BigDecimal trueRange = calculateTrueRange(high.get(i), low.get(i), close.get(i - 1)); |
| | | trList.add(trueRange); |
| | | } |
| | | |
| | | // 使用简单移动平均计算ATR |
| | | return calculateMA(trList, Math.min(period, trList.size())); |
| | | } |
| | | |
| | | /** |
| | | * 计算真实波幅(True Range) |
| | | * @param high 当前最高价 |
| | | * @param low 当前最低价 |
| | | * @param prevClose 前收盘价 |
| | | * @return 真实波幅 |
| | | */ |
| | | protected BigDecimal calculateTrueRange(BigDecimal high, BigDecimal low, BigDecimal prevClose) { |
| | | BigDecimal h1 = high.subtract(low); |
| | | BigDecimal h2 = high.subtract(prevClose).abs(); |
| | | BigDecimal h3 = low.subtract(prevClose).abs(); |
| | | return h1.max(h2).max(h3); |
| | | } |
| | | |
| | | /** |
| | | * 计算标准化波动率(基于ATR) |
| | | * @param close 收盘价列表 |
| | | * @param atr ATR值 |
| | | * @return 标准化波动率(百分比) |
| | | */ |
| | | protected BigDecimal normalizeVolatility(List<BigDecimal> close, BigDecimal atr) { |
| | | if (close == null || close.size() == 0 || atr.compareTo(BigDecimal.ZERO) == 0) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return atr.divide(close.get(close.size() - 1), 4, RoundingMode.HALF_UP).multiply(new BigDecimal(100)); |
| | | } |
| | | } |
| | |
| | | * 2. K = 2/3 * 前一日K值 + 1/3 * 当日RSV |
| | | * 3. D = 2/3 * 前一日D值 + 1/3 * 当日K值 |
| | | * 4. J = 3*K - 2*D |
| | | * |
| | | * 作用: |
| | | * 1. 衡量价格的超买超卖状态(K值>80超买,K值<20超卖) |
| | | * 2. K线上穿D线形成金叉,提示买入信号 |
| | | * 3. K线下穿D线形成死叉,提示卖出信号 |
| | | * 4. J值反映市场的极端状态,J值>100或J值<0为极端行情 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少9个(默认周期)价格数据点用于计算 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线交易 |
| | | * - 缺点:K值波动剧烈,信号频繁且可靠性低 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:K值波动相对稳定,适合短线交易 |
| | | * - 缺点:仍有一定虚假信号 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:信号较为可靠,适合日内交易 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:超买超卖信号明确,适合中期交易 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| | |
| | | /** |
| | | * MA (Moving Average) 指标实现 |
| | | * 支持不同周期的简单移动平均线(SMA)和指数移动平均线(EMA) |
| | | * |
| | | * 作用: |
| | | * 1. 平滑价格波动,识别趋势方向 |
| | | * 2. 短周期MA上穿长周期MA形成金叉,提示买入信号 |
| | | * 3. 短周期MA下穿长周期MA形成死叉,提示卖出信号 |
| | | * 4. 价格上穿/下穿MA线,也可作为买卖参考 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少1个价格数据点用于计算,根据不同周期需求更多数据点 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线交易 |
| | | * - 缺点:噪音多,容易产生虚假信号 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:平衡了反应速度和噪音过滤 |
| | | * - 缺点:仍有一定噪音 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:适合日内交易,信号相对可靠 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:趋势信号明确,虚假信号少 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| | | @Setter |
| | | public class MA extends IndicatorBase { |
| | | |
| | | // 常用周期 |
| | | public static final int MA5 = 5; |
| | | public static final int MA10 = 10; |
| | | public static final int MA20 = 20; |
| | | public static final int MA30 = 30; |
| | | public static final int MA60 = 60; |
| | | // 默认周期 |
| | | public static final int DEFAULT_MA5 = 5; |
| | | public static final int DEFAULT_MA10 = 10; |
| | | public static final int DEFAULT_MA20 = 20; |
| | | public static final int DEFAULT_MA30 = 30; |
| | | public static final int DEFAULT_MA60 = 60; |
| | | |
| | | // 动态周期参数 |
| | | private int ma5Period; |
| | | private int ma10Period; |
| | | private int ma20Period; |
| | | private int ma30Period; |
| | | private int ma60Period; |
| | | |
| | | private BigDecimal ma5 = BigDecimal.ZERO; |
| | | private BigDecimal ma10 = BigDecimal.ZERO; |
| | |
| | | private BigDecimal prevEma30 = null; |
| | | private BigDecimal prevEma60 = null; |
| | | |
| | | // 构造函数使用默认周期 |
| | | public MA() { |
| | | this.ma5Period = DEFAULT_MA5; |
| | | this.ma10Period = DEFAULT_MA10; |
| | | this.ma20Period = DEFAULT_MA20; |
| | | this.ma30Period = DEFAULT_MA30; |
| | | this.ma60Period = DEFAULT_MA60; |
| | | } |
| | | |
| | | /** |
| | | * 计算所有周期的MA指标 |
| | | * 计算所有周期的MA指标(使用当前周期设置) |
| | | * @param prices 价格列表 |
| | | */ |
| | | public void calculate(List<BigDecimal> prices) { |
| | | calculate(prices, null); |
| | | } |
| | | |
| | | /** |
| | | * 计算所有周期的MA指标,并支持动态周期调整 |
| | | * @param prices 价格列表 |
| | | * @param volatility 标准化波动率(ATR百分比),用于动态调整周期 |
| | | */ |
| | | public void calculate(List<BigDecimal> prices, BigDecimal volatility) { |
| | | if (prices == null || prices.size() < 1) { |
| | | return; |
| | | } |
| | | |
| | | // 如果提供了波动率,则动态调整周期 |
| | | if (volatility != null) { |
| | | adjustPeriodsByVolatility(volatility); |
| | | } |
| | | |
| | | // 计算SMA |
| | | if (prices.size() >= MA5) { |
| | | ma5 = calculateMA(prices, MA5); |
| | | if (prices.size() >= ma5Period) { |
| | | ma5 = calculateMA(prices, ma5Period); |
| | | } |
| | | if (prices.size() >= MA10) { |
| | | ma10 = calculateMA(prices, MA10); |
| | | if (prices.size() >= ma10Period) { |
| | | ma10 = calculateMA(prices, ma10Period); |
| | | } |
| | | if (prices.size() >= MA20) { |
| | | ma20 = calculateMA(prices, MA20); |
| | | if (prices.size() >= ma20Period) { |
| | | ma20 = calculateMA(prices, ma20Period); |
| | | } |
| | | if (prices.size() >= MA30) { |
| | | ma30 = calculateMA(prices, MA30); |
| | | if (prices.size() >= ma30Period) { |
| | | ma30 = calculateMA(prices, ma30Period); |
| | | } |
| | | if (prices.size() >= MA60) { |
| | | ma60 = calculateMA(prices, MA60); |
| | | if (prices.size() >= ma60Period) { |
| | | ma60 = calculateMA(prices, ma60Period); |
| | | } |
| | | |
| | | // 计算EMA |
| | | prevEma5 = calculateEMA(prices, MA5, prevEma5); |
| | | prevEma5 = calculateEMA(prices, ma5Period, prevEma5); |
| | | ema5 = prevEma5; |
| | | |
| | | prevEma10 = calculateEMA(prices, MA10, prevEma10); |
| | | prevEma10 = calculateEMA(prices, ma10Period, prevEma10); |
| | | ema10 = prevEma10; |
| | | |
| | | prevEma20 = calculateEMA(prices, MA20, prevEma20); |
| | | prevEma20 = calculateEMA(prices, ma20Period, prevEma20); |
| | | ema20 = prevEma20; |
| | | |
| | | prevEma30 = calculateEMA(prices, MA30, prevEma30); |
| | | prevEma30 = calculateEMA(prices, ma30Period, prevEma30); |
| | | ema30 = prevEma30; |
| | | |
| | | prevEma60 = calculateEMA(prices, MA60, prevEma60); |
| | | prevEma60 = calculateEMA(prices, ma60Period, prevEma60); |
| | | ema60 = prevEma60; |
| | | |
| | | log.debug("MA计算结果 - MA5: {}, MA10: {}, MA20: {}, MA30: {}, MA60: {}", |
| | | ma5, ma10, ma20, ma30, ma60); |
| | | log.debug("EMA计算结果 - EMA5: {}, EMA10: {}, EMA20: {}, EMA30: {}, EMA60: {}", |
| | | ema5, ema10, ema20, ema30, ema60); |
| | | log.debug("MA计算结果 - MA5({}): {}, MA10({}): {}, MA20({}): {}, MA30({}): {}, MA60({}): {}", |
| | | ma5Period, ma5, ma10Period, ma10, ma20Period, ma20, ma30Period, ma30, ma60Period, ma60); |
| | | log.debug("EMA计算结果 - EMA5({}): {}, EMA10({}): {}, EMA20({}): {}, EMA30({}): {}, EMA60({}): {}", |
| | | ma5Period, ema5, ma10Period, ema10, ma20Period, ema20, ma30Period, ema30, ma60Period, ema60); |
| | | } |
| | | |
| | | /** |
| | | * 根据波动率调整MA周期 |
| | | * @param volatility 标准化波动率(ATR百分比) |
| | | */ |
| | | private void adjustPeriodsByVolatility(BigDecimal volatility) { |
| | | // 根据波动率缩放均线周期 |
| | | // 3%、5%、8%作为ATR阈值 |
| | | BigDecimal lowVolatility = new BigDecimal(3); |
| | | BigDecimal midVolatility = new BigDecimal(5); |
| | | BigDecimal highVolatility = new BigDecimal(8); |
| | | |
| | | // 快速MA周期 (ma5) |
| | | ma5Period = volatility.compareTo(lowVolatility) < 0 ? 10 : 6; |
| | | |
| | | // 中期MA周期 (ma10, ma20) |
| | | ma10Period = volatility.compareTo(midVolatility) < 0 ? 10 : 8; |
| | | ma20Period = volatility.compareTo(midVolatility) < 0 ? 21 : 13; |
| | | |
| | | // 长期MA周期 (ma30, ma60) |
| | | ma30Period = volatility.compareTo(highVolatility) < 0 ? 30 : 24; |
| | | ma60Period = volatility.compareTo(highVolatility) < 0 ? 50 : 34; |
| | | |
| | | log.debug("根据波动率{}调整MA周期: ma5={}, ma10={}, ma20={}, ma30={}, ma60={}", |
| | | volatility, ma5Period, ma10Period, ma20Period, ma30Period, ma60Period); |
| | | } |
| | | |
| | | /** |
| | |
| | | * 1. DIF = EMA(12) - EMA(26) |
| | | * 2. DEA = EMA(DIF, 9) |
| | | * 3. MACD柱状图 = (DIF - DEA) * 2 |
| | | * |
| | | * 作用: |
| | | * 1. 识别趋势方向和动能变化 |
| | | * 2. DIF上穿DEA形成金叉,提示买入信号 |
| | | * 3. DIF下穿DEA形成死叉,提示卖出信号 |
| | | * 4. MACD柱状图由负转正,提示多头力量增强 |
| | | * 5. MACD柱状图由正转负,提示空头力量增强 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少2个价格数据点用于计算,数据越多计算越准确 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线交易 |
| | | * - 缺点:MACD柱状图波动剧烈,信号频繁 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:平衡了反应速度和信号可靠性 |
| | | * - 缺点:仍有一定噪音 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:适合日内交易,信号较为可靠 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:趋势信号明确,MACD柱状图变化稳定 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| | | @Setter |
| | | public class MACD extends IndicatorBase { |
| | | |
| | | private static final int FAST_PERIOD = 12; |
| | | private static final int SLOW_PERIOD = 26; |
| | | private static final int SIGNAL_PERIOD = 9; |
| | | // 默认周期参数 |
| | | public static final int DEFAULT_FAST_PERIOD = 12; |
| | | public static final int DEFAULT_SLOW_PERIOD = 26; |
| | | public static final int DEFAULT_SIGNAL_PERIOD = 9; |
| | | |
| | | // 动态周期参数 |
| | | private int fastPeriod; |
| | | private int slowPeriod; |
| | | private int signalPeriod; |
| | | |
| | | private BigDecimal dif = BigDecimal.ZERO; |
| | | private BigDecimal dea = BigDecimal.ZERO; |
| | |
| | | private BigDecimal prevSlowEMA = null; |
| | | private BigDecimal prevDea = null; |
| | | |
| | | // 构造函数使用默认周期 |
| | | public MACD() { |
| | | this.fastPeriod = DEFAULT_FAST_PERIOD; |
| | | this.slowPeriod = DEFAULT_SLOW_PERIOD; |
| | | this.signalPeriod = DEFAULT_SIGNAL_PERIOD; |
| | | } |
| | | |
| | | /** |
| | | * 计算MACD指标 |
| | | * 计算MACD指标(使用当前周期设置) |
| | | * @param prices 价格列表 |
| | | */ |
| | | public void calculate(List<BigDecimal> prices) { |
| | | calculate(prices, null); |
| | | } |
| | | |
| | | /** |
| | | * 计算MACD指标,并支持动态周期调整 |
| | | * @param prices 价格列表 |
| | | * @param volatility 标准化波动率(百分比),用于动态调整周期 |
| | | */ |
| | | public void calculate(List<BigDecimal> prices, BigDecimal volatility) { |
| | | if (prices == null || prices.size() < 2) { |
| | | return; |
| | | } |
| | | |
| | | // 如果提供了波动率,则动态调整周期 |
| | | if (volatility != null) { |
| | | adjustPeriodsByVolatility(volatility); |
| | | } |
| | | |
| | | // 计算快速EMA |
| | | prevFastEMA = calculateEMA(prices, FAST_PERIOD, prevFastEMA); |
| | | prevFastEMA = calculateEMA(prices, fastPeriod, prevFastEMA); |
| | | |
| | | // 计算慢速EMA |
| | | prevSlowEMA = calculateEMA(prices, SLOW_PERIOD, prevSlowEMA); |
| | | prevSlowEMA = calculateEMA(prices, slowPeriod, prevSlowEMA); |
| | | |
| | | // 计算DIF |
| | | dif = prevFastEMA.subtract(prevSlowEMA).setScale(8, RoundingMode.HALF_UP); |
| | |
| | | // 计算DEA |
| | | List<BigDecimal> difList = new ArrayList<>(); |
| | | difList.add(dif); |
| | | prevDea = calculateEMA(difList, SIGNAL_PERIOD, prevDea); |
| | | prevDea = calculateEMA(difList, signalPeriod, prevDea); |
| | | dea = prevDea.setScale(8, RoundingMode.HALF_UP); |
| | | |
| | | // 计算MACD柱状图 |
| | | macdBar = dif.subtract(dea).multiply(new BigDecimal(2)).setScale(8, RoundingMode.HALF_UP); |
| | | |
| | | log.debug("MACD计算结果 - DIF: {}, DEA: {}, MACD柱状图: {}", dif, dea, macdBar); |
| | | log.debug("MACD计算结果 - DIF: {}, DEA: {}, MACD柱状图: {}, 参数: fast={}, slow={}, signal={}", |
| | | dif, dea, macdBar, fastPeriod, slowPeriod, signalPeriod); |
| | | } |
| | | |
| | | /** |
| | | * 根据波动率调整MACD周期参数 |
| | | * @param volatility 标准化波动率(百分比) |
| | | */ |
| | | private void adjustPeriodsByVolatility(BigDecimal volatility) { |
| | | // 波动率阈值 |
| | | BigDecimal volatilityThreshold = new BigDecimal(15); |
| | | |
| | | // 根据波动率调整MACD参数 |
| | | if (volatility.compareTo(volatilityThreshold) < 0) { |
| | | // 低波动率环境,使用默认参数 |
| | | fastPeriod = DEFAULT_FAST_PERIOD; |
| | | slowPeriod = DEFAULT_SLOW_PERIOD; |
| | | signalPeriod = DEFAULT_SIGNAL_PERIOD; |
| | | } else { |
| | | // 高波动率环境,使用更灵敏的参数 |
| | | fastPeriod = 8; |
| | | slowPeriod = 17; |
| | | signalPeriod = 5; |
| | | } |
| | | |
| | | log.debug("根据波动率{}调整MACD周期: fast={}, slow={}, signal={}", |
| | | volatility, fastPeriod, slowPeriod, signalPeriod); |
| | | } |
| | | |
| | | /** |
| | |
| | | * 1. 计算N天内的上涨幅度和下跌幅度 |
| | | * 2. 计算平均上涨幅度和平均下跌幅度 |
| | | * 3. RSI = 100 - (100 / (1 + (平均上涨幅度 / 平均下跌幅度))) |
| | | * |
| | | * 作用: |
| | | * 1. 衡量市场的相对强弱程度(0-100) |
| | | * 2. 超买信号:RSI>70表示市场超买,可能回调 |
| | | * 3. 超卖信号:RSI<30表示市场超卖,可能反弹 |
| | | * 4. 极端超买:RSI>80表示市场极度超买 |
| | | * 5. 极端超卖:RSI<20表示市场极度超卖 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少15个(默认周期+1)价格数据点用于计算 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线交易 |
| | | * - 缺点:RSI波动剧烈,频繁进入超买超卖区域 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:RSI波动相对稳定,适合短线交易 |
| | | * - 缺点:仍有一定虚假超买超卖信号 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:超买超卖信号较为可靠,适合日内交易 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:超买超卖信号明确,适合中期交易 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.indicator; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 交易策略实现 |
| | | * 展示如何为ETH合约交易(开仓/平仓)组合所有指标 |
| | | */ |
| | | @Slf4j |
| | | public class TradingStrategy extends IndicatorBase { |
| | | |
| | | @Getter |
| | | @Setter |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | public static class StrategyConfig { |
| | | private int maShortPeriod = 5; // 短期移动平均周期 |
| | | private int maLongPeriod = 20; // 长期移动平均周期 |
| | | private int rsiPeriod = 14; // RSI指标周期 |
| | | private int kdjPeriod = 9; // KDJ指标周期 |
| | | private int bollPeriod = 20; // 布林带周期 |
| | | private double bollK = 2.0; // 布林带标准差倍数 |
| | | private int atrPeriod = 14; // ATR计算周期 |
| | | private boolean enableDynamicParams = true; // 是否启用动态参数优化 |
| | | private boolean enableMultiTimeframeConfirm = true; // 是否启用多周期确认 |
| | | private int volumeMaPeriod = 20; // 成交量移动平均周期 |
| | | private boolean enableVolumeConfirm = true; // 是否启用成交量验证 |
| | | |
| | | // 风险控制参数 |
| | | private BigDecimal baseLeverage = new BigDecimal(3); // 基础杠杆倍数 |
| | | private int volatilityThresholdPeriod = 30; // 波动率阈值计算周期(用于动态杠杆) |
| | | private boolean enableDynamicLeverage = true; // 是否启用动态杠杆 |
| | | private boolean enableThreeStepProfitTaking = true; // 是否启用三段式止盈 |
| | | private boolean enableBlackSwanFilter = true; // 是否启用黑天鹅事件过滤 |
| | | } |
| | | |
| | | public enum Direction { |
| | | LONG, // 做多方向信号 |
| | | SHORT, // 做空方向信号 |
| | | RANGING // 震荡市场 |
| | | } |
| | | |
| | | public enum SignalType { |
| | | NONE, // 无信号 |
| | | BUY, // 开多信号 |
| | | SELL, // 开空信号 |
| | | CLOSE_BUY, // 平多信号 |
| | | CLOSE_SELL // 平空信号 |
| | | } |
| | | |
| | | private final StrategyConfig config; |
| | | private final MA ma; |
| | | private final AdvancedMA advancedMA; |
| | | private final BOLL boll; |
| | | private final KDJ kdj; |
| | | private final MACD macd; |
| | | private final RSI rsi; |
| | | |
| | | public TradingStrategy() { |
| | | this(new StrategyConfig()); |
| | | } |
| | | |
| | | public TradingStrategy(StrategyConfig config) { |
| | | this.config = config; |
| | | this.ma = new MA(); |
| | | this.advancedMA = new AdvancedMA(); |
| | | this.boll = new BOLL(config.getBollPeriod(), config.getBollK()); |
| | | this.kdj = new KDJ(config.getKdjPeriod()); |
| | | this.macd = new MACD(); |
| | | this.rsi = new RSI(config.getRsiPeriod()); |
| | | } |
| | | |
| | | /** |
| | | * 计算所有指标并生成交易信号 |
| | | * @param prices 价格数据 |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | * @param volume 成交量列表 |
| | | * @param currentPrice 当前价格 |
| | | * @param hasLongPosition 是否当前持有做多仓位 |
| | | * @param hasShortPosition 是否当前持有做空仓位 |
| | | * @param fiveMinPrices 5分钟价格数据(多周期确认) |
| | | * @param oneHourPrices 1小时价格数据(多周期确认) |
| | | * @param fourHourPrices 4小时价格数据(多周期确认) |
| | | * @param fundingRate 当前资金费率(用于黑天鹅过滤) |
| | | * @param hasLargeTransfer 是否有大额转账(用于黑天鹅过滤) |
| | | * @param hasUpcomingEvent 是否有即将到来的重大事件(用于黑天鹅过滤) |
| | | * @return 交易信号 |
| | | */ |
| | | public SignalType generateSignal(List<BigDecimal> prices, List<BigDecimal> high, |
| | | List<BigDecimal> low, List<BigDecimal> close, |
| | | List<BigDecimal> volume, BigDecimal currentPrice, |
| | | boolean hasLongPosition, boolean hasShortPosition, |
| | | List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices, |
| | | BigDecimal fundingRate, |
| | | boolean hasLargeTransfer, |
| | | boolean hasUpcomingEvent) { |
| | | // 计算所有指标 |
| | | calculateIndicators(prices, high, low, close); |
| | | |
| | | // 检查是否为震荡市场,如果是,则无信号 |
| | | if (isRangeMarket()) { |
| | | log.debug("当前市场为震荡行情,不产生信号"); |
| | | return SignalType.NONE; |
| | | } |
| | | |
| | | // 黑天鹅事件过滤 |
| | | if (blackSwanFilter(fundingRate, hasLargeTransfer, hasUpcomingEvent)) { |
| | | log.debug("黑天鹅事件过滤触发,不产生信号"); |
| | | return SignalType.NONE; |
| | | } |
| | | |
| | | // 开多信号 |
| | | if (shouldOpenLong(currentPrice, prices, volume, fiveMinPrices, oneHourPrices, fourHourPrices) && !hasLongPosition && !hasShortPosition) { |
| | | log.debug("生成买入信号"); |
| | | return SignalType.BUY; |
| | | } |
| | | |
| | | // 开空信号 |
| | | if (shouldOpenShort(currentPrice, volume, fiveMinPrices, oneHourPrices, fourHourPrices) && !hasLongPosition && !hasShortPosition) { |
| | | log.debug("生成卖出信号"); |
| | | return SignalType.SELL; |
| | | } |
| | | |
| | | // 平多信号 |
| | | if (shouldCloseLong(currentPrice, volume, fiveMinPrices, oneHourPrices, fourHourPrices) && hasLongPosition) { |
| | | log.debug("生成平多信号"); |
| | | return SignalType.CLOSE_BUY; |
| | | } |
| | | |
| | | // 平空信号 |
| | | if (shouldCloseShort(currentPrice, volume, fiveMinPrices, oneHourPrices, fourHourPrices) && hasShortPosition) { |
| | | log.debug("生成平空信号"); |
| | | return SignalType.CLOSE_SELL; |
| | | } |
| | | |
| | | log.debug("未生成信号"); |
| | | return SignalType.NONE; |
| | | } |
| | | |
| | | /** |
| | | * 多周期确认辅助方法(看涨) |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否有足够的多周期确认 |
| | | */ |
| | | private boolean multiTimeframeConfirm(List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | if (!config.isEnableMultiTimeframeConfirm()) { |
| | | return true; // 如果未启用多周期确认,则默认返回true |
| | | } |
| | | |
| | | int confirmCount = 0; |
| | | |
| | | // 检查5分钟周期 |
| | | if (hasBullishTrend(fiveMinPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 检查1小时周期 |
| | | if (hasBullishTrend(oneHourPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 检查4小时周期 |
| | | if (hasBullishTrend(fourHourPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 至少需要2个周期确认 |
| | | return confirmCount >= 2; |
| | | } |
| | | |
| | | /** |
| | | * 多周期确认辅助方法(看跌) |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否有足够的多周期确认 |
| | | */ |
| | | private boolean multiTimeframeBearishConfirm(List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | if (!config.isEnableMultiTimeframeConfirm()) { |
| | | return true; // 如果未启用多周期确认,则默认返回true |
| | | } |
| | | |
| | | int confirmCount = 0; |
| | | |
| | | // 检查5分钟周期 |
| | | if (hasBearishTrend(fiveMinPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 检查1小时周期 |
| | | if (hasBearishTrend(oneHourPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 检查4小时周期 |
| | | if (hasBearishTrend(fourHourPrices)) { |
| | | confirmCount++; |
| | | } |
| | | |
| | | // 至少需要2个周期确认 |
| | | return confirmCount >= 2; |
| | | } |
| | | |
| | | /** |
| | | * 检查指定周期是否有看涨趋势 |
| | | * @param prices 价格数据 |
| | | * @return 是否有看涨趋势 |
| | | */ |
| | | private boolean hasBullishTrend(List<BigDecimal> prices) { |
| | | if (prices == null || prices.size() < 20) { |
| | | return false; // 数据不足 |
| | | } |
| | | |
| | | // 创建临时MA指标用于判断趋势 |
| | | MA tempMA = new MA(); |
| | | tempMA.calculate(prices); |
| | | |
| | | // 简单的趋势判断:短期MA > 长期MA |
| | | return tempMA.getEma5().compareTo(tempMA.getEma20()) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 检查指定周期是否有看跌趋势 |
| | | * @param prices 价格数据 |
| | | * @return 是否有看跌趋势 |
| | | */ |
| | | private boolean hasBearishTrend(List<BigDecimal> prices) { |
| | | if (prices == null || prices.size() < 20) { |
| | | return false; // 数据不足 |
| | | } |
| | | |
| | | // 创建临时MA指标用于判断趋势 |
| | | MA tempMA = new MA(); |
| | | tempMA.calculate(prices); |
| | | |
| | | // 简单的趋势判断:短期MA < 长期MA |
| | | return tempMA.getEma5().compareTo(tempMA.getEma20()) < 0; |
| | | } |
| | | |
| | | /** |
| | | * 成交量验证辅助方法 |
| | | * @param volume 成交量列表 |
| | | * @return 是否通过成交量验证 |
| | | */ |
| | | private boolean volumeConfirm(List<BigDecimal> volume) { |
| | | if (!config.isEnableVolumeConfirm() || volume == null || volume.size() < config.getVolumeMaPeriod()) { |
| | | return true; // 如果未启用成交量验证或数据不足,则默认返回true |
| | | } |
| | | |
| | | // 计算成交量移动平均 |
| | | BigDecimal volumeMA = calculateMA(volume, config.getVolumeMaPeriod()); |
| | | BigDecimal currentVolume = volume.get(volume.size() - 1); |
| | | |
| | | // 成交量需要大于均线 |
| | | return currentVolume.compareTo(volumeMA) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 量价背离检测 |
| | | * 检测价格上涨/下跌但成交量萎缩的情况,或价格和成交量趋势不一致 |
| | | * @param prices 价格列表 |
| | | * @param volume 成交量列表 |
| | | * @return 是否存在量价背离 |
| | | */ |
| | | private boolean hasPriceVolumeDivergence(List<BigDecimal> prices, List<BigDecimal> volume) { |
| | | if (!config.isEnableVolumeConfirm() || prices == null || volume == null || prices.size() < 3 || volume.size() < 3) { |
| | | return false; // 如果未启用成交量验证或数据不足,则默认返回false |
| | | } |
| | | |
| | | // 获取最近3个周期的价格和成交量 |
| | | BigDecimal currentPrice = prices.get(prices.size() - 1); |
| | | BigDecimal prevPrice1 = prices.get(prices.size() - 2); |
| | | BigDecimal prevPrice2 = prices.get(prices.size() - 3); |
| | | |
| | | BigDecimal currentVolume = volume.get(volume.size() - 1); |
| | | BigDecimal prevVolume1 = volume.get(volume.size() - 2); |
| | | BigDecimal prevVolume2 = volume.get(volume.size() - 3); |
| | | |
| | | // 计算价格趋势 |
| | | boolean priceTrendUp = currentPrice.compareTo(prevPrice1) > 0 && prevPrice1.compareTo(prevPrice2) > 0; |
| | | boolean priceTrendDown = currentPrice.compareTo(prevPrice1) < 0 && prevPrice1.compareTo(prevPrice2) < 0; |
| | | |
| | | // 计算成交量趋势 |
| | | boolean volumeTrendUp = currentVolume.compareTo(prevVolume1) > 0 && prevVolume1.compareTo(prevVolume2) > 0; |
| | | boolean volumeTrendDown = currentVolume.compareTo(prevVolume1) < 0 && prevVolume1.compareTo(prevVolume2) < 0; |
| | | |
| | | // 检测量价背离 |
| | | // 价格上涨但成交量萎缩 |
| | | boolean bullishDivergence = priceTrendUp && volumeTrendDown; |
| | | // 价格下跌但成交量萎缩(通常是强势信号,不视为背离) |
| | | // 价格下跌但成交量放大(可能是恐慌性抛售,视为背离) |
| | | boolean bearishDivergence = priceTrendDown && volumeTrendUp; |
| | | |
| | | return bullishDivergence || bearishDivergence; |
| | | } |
| | | |
| | | /** |
| | | * 计算所有指标 |
| | | * @param prices 价格数据 |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | */ |
| | | private void calculateIndicators(List<BigDecimal> prices, List<BigDecimal> high, |
| | | List<BigDecimal> low, List<BigDecimal> close) { |
| | | // 计算ATR和波动率 |
| | | BigDecimal atr = calculateATR(high, low, close, config.getAtrPeriod()); |
| | | BigDecimal volatility = normalizeVolatility(close, atr); |
| | | |
| | | // 使用动态参数计算指标 |
| | | if (config.isEnableDynamicParams()) { |
| | | log.debug("使用动态参数计算指标,波动率: {}", volatility); |
| | | ma.calculate(prices, volatility); |
| | | macd.calculate(prices, volatility); |
| | | } else { |
| | | ma.calculate(prices); |
| | | macd.calculate(prices); |
| | | } |
| | | |
| | | // 其他指标计算 |
| | | advancedMA.calculateTripleEMA(prices); |
| | | boll.calculate(prices); |
| | | kdj.calculate(prices); |
| | | rsi.calculate(prices); |
| | | } |
| | | |
| | | /** |
| | | * 检查是否为震荡市场 |
| | | * @return 是否为震荡市场 |
| | | */ |
| | | private boolean isRangeMarket() { |
| | | // 高级MA线收敛 + RSI(40-60) + 布林带收窄 |
| | | boolean isMaConverged = advancedMA.calculatePercent().compareTo(new BigDecimal(2)) < 0; |
| | | boolean isRsiNeutral = rsi.getRsi().compareTo(new BigDecimal(40)) > 0 && |
| | | rsi.getRsi().compareTo(new BigDecimal(60)) < 0; |
| | | boolean isBollNarrow = boll.calculateBandWidth().compareTo(new BigDecimal(0.05)) < 0; |
| | | |
| | | return isMaConverged && isRsiNeutral && isBollNarrow; |
| | | } |
| | | |
| | | /** |
| | | * 根据15分钟时间框架指标确定市场方向(做多/做空/震荡) |
| | | * @param prices 价格数据列表 |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | * @param currentPrice 当前价格 |
| | | * @return 市场方向 |
| | | */ |
| | | public Direction getDirection(List<BigDecimal> prices, List<BigDecimal> high, |
| | | List<BigDecimal> low, List<BigDecimal> close, |
| | | BigDecimal currentPrice) { |
| | | // 计算所有指标 |
| | | calculateIndicators(prices, high, low, close); |
| | | |
| | | // 检查是否为震荡市场 |
| | | if (isRangeMarket()) { |
| | | return Direction.RANGING; |
| | | } |
| | | |
| | | // 检查做多方向条件:MA多头排列 + MACD金叉 + RSI中性(30-70) + BOLL价格在上轨和中轨之间 |
| | | boolean isMaBullish = ma.getEma5().compareTo(ma.getEma10()) > 0 && |
| | | ma.getEma10().compareTo(ma.getEma20()) > 0; |
| | | boolean isMacdGoldenCross = macd.getDif().compareTo(macd.getDea()) > 0; |
| | | boolean isRsiNeutral = rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && |
| | | rsi.getRsi().compareTo(new BigDecimal(70)) < 0; |
| | | boolean isPriceInUpperMid = currentPrice.compareTo(boll.getMid()) > 0 && |
| | | currentPrice.compareTo(boll.getUpper()) < 0; |
| | | |
| | | if (isMaBullish && isMacdGoldenCross && isRsiNeutral && isPriceInUpperMid) { |
| | | return Direction.LONG; |
| | | } |
| | | |
| | | // 检查做空方向条件:MA空头排列 + MACD死叉 + RSI中性(30-70) + BOLL价格在下轨和中轨之间 |
| | | boolean isMaBearish = ma.getEma5().compareTo(ma.getEma10()) < 0 && |
| | | ma.getEma10().compareTo(ma.getEma20()) < 0; |
| | | boolean isMacdDeathCross = macd.getDif().compareTo(macd.getDea()) < 0; |
| | | boolean isPriceInLowerMid = currentPrice.compareTo(boll.getLower()) > 0 && |
| | | currentPrice.compareTo(boll.getMid()) < 0; |
| | | |
| | | if (isMaBearish && isMacdDeathCross && isRsiNeutral && isPriceInLowerMid) { |
| | | return Direction.SHORT; |
| | | } |
| | | |
| | | // 如果没有明确方向,默认为震荡 |
| | | return Direction.RANGING; |
| | | } |
| | | |
| | | /** |
| | | * 根据用户要求检查是否应该开多仓位 |
| | | * 条件:MA金叉 + MACD金叉 + KDJ金叉 + RSI中性 + 价格高于BOLL中轨 + 多周期确认 + 成交量验证 |
| | | * @param currentPrice 当前价格 |
| | | * @param prices 价格数据 |
| | | * @param volume 成交量列表 |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否应该开多 |
| | | */ |
| | | private boolean shouldOpenLong(BigDecimal currentPrice, List<BigDecimal> prices, List<BigDecimal> volume, |
| | | List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | // MA金叉 (5EMA > 20EMA) |
| | | boolean isMaGoldenCross = ma.getEma5().compareTo(ma.getEma20()) > 0; |
| | | // MACD金叉 (DIF > DEA) |
| | | boolean isMacdGoldenCross = macd.getDif().compareTo(macd.getDea()) > 0; |
| | | // KDJ金叉 (K线上穿D线) |
| | | boolean isKdjGoldenCross = kdj.isGoldenCross(); |
| | | // RSI中性 (30-70) |
| | | boolean isRsiNeutral = rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && |
| | | rsi.getRsi().compareTo(new BigDecimal(70)) < 0; |
| | | // 价格高于BOLL中轨 |
| | | boolean isPriceAboveBollMid = currentPrice.compareTo(boll.getMid()) > 0; |
| | | // 成交量验证 |
| | | boolean isVolumeConfirmed = volumeConfirm(volume); |
| | | // 量价背离检测 |
| | | boolean isPriceVolumeDivergence = hasPriceVolumeDivergence(prices, volume); |
| | | // 多周期确认 |
| | | boolean isMultiTimeframeConfirmed = multiTimeframeConfirm(fiveMinPrices, oneHourPrices, fourHourPrices); |
| | | |
| | | return isMaGoldenCross && isMacdGoldenCross && isKdjGoldenCross && |
| | | isRsiNeutral && isPriceAboveBollMid && isVolumeConfirmed && |
| | | !isPriceVolumeDivergence && isMultiTimeframeConfirmed; |
| | | } |
| | | |
| | | /** |
| | | * 根据用户要求检查是否应该开空仓位 |
| | | * 条件:MA死叉 + MACD死叉 + KDJ死叉 + RSI中性 + 价格低于BOLL中轨 + 多周期确认 + 成交量验证 |
| | | * @param currentPrice 当前价格 |
| | | * @param volume 成交量列表 |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否应该开空 |
| | | */ |
| | | private boolean shouldOpenShort(BigDecimal currentPrice, List<BigDecimal> volume, |
| | | List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | // MA死叉 (5EMA < 20EMA) |
| | | boolean isMaDeathCross = ma.getEma5().compareTo(ma.getEma20()) < 0; |
| | | // MACD死叉 (DIF < DEA) |
| | | boolean isMacdDeathCross = macd.getDif().compareTo(macd.getDea()) < 0; |
| | | // KDJ死叉 (K线下穿D线) |
| | | boolean isKdjDeathCross = kdj.isDeathCross(); |
| | | // RSI中性 (30-70) |
| | | boolean isRsiNeutral = rsi.getRsi().compareTo(new BigDecimal(30)) > 0 && |
| | | rsi.getRsi().compareTo(new BigDecimal(70)) < 0; |
| | | // 价格低于BOLL中轨 |
| | | boolean isPriceBelowBollMid = currentPrice.compareTo(boll.getMid()) < 0; |
| | | // 成交量验证 |
| | | boolean isVolumeConfirmed = volumeConfirm(volume); |
| | | // 多周期确认(看跌) |
| | | boolean isMultiTimeframeConfirmed = multiTimeframeBearishConfirm(fiveMinPrices, oneHourPrices, fourHourPrices); |
| | | |
| | | return isMaDeathCross && isMacdDeathCross && isKdjDeathCross && |
| | | isRsiNeutral && isPriceBelowBollMid && isVolumeConfirmed && isMultiTimeframeConfirmed; |
| | | } |
| | | |
| | | /** |
| | | * 根据用户要求检查是否应该平多仓位 |
| | | * 条件:MA死叉 + MACD死叉 + RSI超买 + 价格低于BOLL中轨 + 多周期确认 |
| | | * @param currentPrice 当前价格 |
| | | * @param volume 成交量列表 |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否应该平多 |
| | | */ |
| | | private boolean shouldCloseLong(BigDecimal currentPrice, List<BigDecimal> volume, |
| | | List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | // MA死叉 (5EMA < 20EMA) |
| | | boolean isMaDeathCross = ma.getEma5().compareTo(ma.getEma20()) < 0; |
| | | // MACD死叉 (DIF < DEA) |
| | | boolean isMacdDeathCross = macd.getDif().compareTo(macd.getDea()) < 0; |
| | | // RSI超买 (>70) |
| | | boolean isRsiOverbought = rsi.isOverbought(); |
| | | // 价格低于BOLL中轨 |
| | | boolean isPriceBelowBollMid = currentPrice.compareTo(boll.getMid()) < 0; |
| | | // 多周期确认(看跌) |
| | | boolean isMultiTimeframeConfirmed = multiTimeframeBearishConfirm(fiveMinPrices, oneHourPrices, fourHourPrices); |
| | | |
| | | return isMaDeathCross && isMacdDeathCross && isRsiOverbought && isPriceBelowBollMid && isMultiTimeframeConfirmed; |
| | | } |
| | | |
| | | /** |
| | | * 根据用户要求检查是否应该平空仓位 |
| | | * 条件:MA金叉 + MACD金叉 + RSI超卖 + 价格高于BOLL中轨 + 多周期确认 |
| | | * @param currentPrice 当前价格 |
| | | * @param volume 成交量列表 |
| | | * @param fiveMinPrices 5分钟价格数据 |
| | | * @param oneHourPrices 1小时价格数据 |
| | | * @param fourHourPrices 4小时价格数据 |
| | | * @return 是否应该平空 |
| | | */ |
| | | private boolean shouldCloseShort(BigDecimal currentPrice, List<BigDecimal> volume, |
| | | List<BigDecimal> fiveMinPrices, |
| | | List<BigDecimal> oneHourPrices, |
| | | List<BigDecimal> fourHourPrices) { |
| | | // MA金叉 (5EMA > 20EMA) |
| | | boolean isMaGoldenCross = ma.getEma5().compareTo(ma.getEma20()) > 0; |
| | | // MACD金叉 (DIF > DEA) |
| | | boolean isMacdGoldenCross = macd.getDif().compareTo(macd.getDea()) > 0; |
| | | // RSI超卖 (<30) |
| | | boolean isRsiOversold = rsi.isOversold(); |
| | | // 价格高于BOLL中轨 |
| | | boolean isPriceAboveBollMid = currentPrice.compareTo(boll.getMid()) > 0; |
| | | // 多周期确认(看涨) |
| | | boolean isMultiTimeframeConfirmed = multiTimeframeConfirm(fiveMinPrices, oneHourPrices, fourHourPrices); |
| | | |
| | | return isMaGoldenCross && isMacdGoldenCross && isRsiOversold && isPriceAboveBollMid && isMultiTimeframeConfirmed; |
| | | } |
| | | |
| | | /** |
| | | * 获取所有指标的当前状态 |
| | | * @return 指标状态字符串 |
| | | */ |
| | | public String getIndicatorStatus() { |
| | | return String.format("MA5: %s, MA20: %s, ", ma.getEma5(), ma.getEma20()) + |
| | | String.format("MACD-DIF: %s, MACD-DEA: %s, MACD-BAR: %s, ", macd.getDif(), macd.getDea(), macd.getMacdBar()) + |
| | | String.format("KDJ-K: %s, KDJ-D: %s, KDJ-J: %s, ", kdj.getK(), kdj.getD(), kdj.getJ()) + |
| | | String.format("RSI: %s, ", rsi.getRsi()) + |
| | | String.format("BOLL-MID: %s, BOLL-UP: %s, BOLL-DN: %s, ", boll.getMid(), boll.getUpper(), boll.getLower()) + |
| | | String.format("AdvancedMA-Bullish: %s, Bearish: %s, Percent: %s", |
| | | advancedMA.isBullish(), advancedMA.isBearish(), advancedMA.calculatePercent()); |
| | | } |
| | | |
| | | /** |
| | | * 计算动态杠杆倍数 |
| | | * 杠杆倍数 = 基础杠杆 * (波动率阈值/当前波动率) |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | * @return 动态杠杆倍数 |
| | | */ |
| | | public BigDecimal calculateDynamicLeverage(List<BigDecimal> high, List<BigDecimal> low, List<BigDecimal> close) { |
| | | if (!config.isEnableDynamicLeverage()) { |
| | | return config.getBaseLeverage(); |
| | | } |
| | | |
| | | // 计算当前ATR和波动率 |
| | | BigDecimal currentAtr = calculateATR(high, low, close, config.getAtrPeriod()); |
| | | BigDecimal currentVolatility = normalizeVolatility(close, currentAtr); |
| | | |
| | | // 计算30日ATR移动中位数作为波动率阈值 |
| | | BigDecimal volatilityThreshold = calculateVolatilityThreshold(high, low, close); |
| | | |
| | | // 动态计算杠杆倍数 |
| | | BigDecimal leverage = config.getBaseLeverage().multiply(volatilityThreshold).divide(currentVolatility, 2, BigDecimal.ROUND_HALF_UP); |
| | | |
| | | // 限制杠杆范围在1x-10x之间 |
| | | leverage = leverage.min(new BigDecimal(10)).max(BigDecimal.ONE); |
| | | |
| | | log.debug("动态杠杆计算 - 基础杠杆: {}, 波动率阈值: {}, 当前波动率: {}, 计算杠杆: {}", |
| | | config.getBaseLeverage(), volatilityThreshold, currentVolatility, leverage); |
| | | |
| | | return leverage; |
| | | } |
| | | |
| | | /** |
| | | * 计算波动率阈值(30日ATR移动中位数) |
| | | * @param high 最高价列表 |
| | | * @param low 最低价列表 |
| | | * @param close 收盘价列表 |
| | | * @return 波动率阈值 |
| | | */ |
| | | private BigDecimal calculateVolatilityThreshold(List<BigDecimal> high, List<BigDecimal> low, List<BigDecimal> close) { |
| | | if (high == null || low == null || close == null || close.size() < config.getVolatilityThresholdPeriod()) { |
| | | return new BigDecimal(5); // 默认阈值 |
| | | } |
| | | |
| | | List<BigDecimal> volatilityList = new ArrayList<>(); |
| | | for (int i = close.size() - config.getVolatilityThresholdPeriod(); i < close.size(); i++) { |
| | | List<BigDecimal> recentHigh = high.subList(Math.max(0, i - config.getAtrPeriod()), i + 1); |
| | | List<BigDecimal> recentLow = low.subList(Math.max(0, i - config.getAtrPeriod()), i + 1); |
| | | List<BigDecimal> recentClose = close.subList(Math.max(0, i - config.getAtrPeriod()), i + 1); |
| | | |
| | | BigDecimal atr = calculateATR(recentHigh, recentLow, recentClose, config.getAtrPeriod()); |
| | | BigDecimal volatility = normalizeVolatility(recentClose, atr); |
| | | volatilityList.add(volatility); |
| | | } |
| | | |
| | | // 计算中位数 |
| | | volatilityList.sort(BigDecimal::compareTo); |
| | | int midIndex = volatilityList.size() / 2; |
| | | return volatilityList.get(midIndex); |
| | | } |
| | | |
| | | /** |
| | | * 三段式止盈策略 |
| | | * 第一目标:BOLL上轨(30%仓位) |
| | | * 第二目标:斐波那契161.8%(50%仓位) |
| | | * 第三目标:趋势线破位(20%仓位) |
| | | * @param entryPrice 入场价格 |
| | | * @param currentPrice 当前价格 |
| | | * @param direction 交易方向 |
| | | * @param positionSize 当前仓位大小 |
| | | * @return 止盈信号和应该平仓的仓位比例 |
| | | */ |
| | | public ProfitTakingResult calculateThreeStepProfitTaking(BigDecimal entryPrice, BigDecimal currentPrice, |
| | | Direction direction, BigDecimal positionSize) { |
| | | if (!config.isEnableThreeStepProfitTaking()) { |
| | | return new ProfitTakingResult(SignalType.NONE, BigDecimal.ZERO); |
| | | } |
| | | |
| | | // 计算三个止盈目标 |
| | | BigDecimal firstTarget = calculateFirstProfitTarget(entryPrice, currentPrice, direction); |
| | | BigDecimal secondTarget = calculateSecondProfitTarget(entryPrice, direction); |
| | | BigDecimal thirdTarget = calculateThirdProfitTarget(currentPrice, direction); |
| | | |
| | | // 判断当前价格是否达到目标 |
| | | if (direction == Direction.LONG) { |
| | | if (currentPrice.compareTo(thirdTarget) >= 0) { |
| | | // 达到第三目标,平全部仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_BUY, positionSize); |
| | | } else if (currentPrice.compareTo(secondTarget) >= 0) { |
| | | // 达到第二目标,平50%仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_BUY, positionSize.multiply(new BigDecimal("0.5"))); |
| | | } else if (currentPrice.compareTo(firstTarget) >= 0) { |
| | | // 达到第一目标,平30%仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_BUY, positionSize.multiply(new BigDecimal("0.3"))); |
| | | } |
| | | } else if (direction == Direction.SHORT) { |
| | | if (currentPrice.compareTo(thirdTarget) <= 0) { |
| | | // 达到第三目标,平全部仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_SELL, positionSize); |
| | | } else if (currentPrice.compareTo(secondTarget) <= 0) { |
| | | // 达到第二目标,平50%仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_SELL, positionSize.multiply(new BigDecimal("0.5"))); |
| | | } else if (currentPrice.compareTo(firstTarget) <= 0) { |
| | | // 达到第一目标,平30%仓位 |
| | | return new ProfitTakingResult(SignalType.CLOSE_SELL, positionSize.multiply(new BigDecimal("0.3"))); |
| | | } |
| | | } |
| | | |
| | | return new ProfitTakingResult(SignalType.NONE, BigDecimal.ZERO); |
| | | } |
| | | |
| | | /** |
| | | * 计算第一止盈目标:BOLL上轨 |
| | | */ |
| | | private BigDecimal calculateFirstProfitTarget(BigDecimal entryPrice, BigDecimal currentPrice, Direction direction) { |
| | | return direction == Direction.LONG ? boll.getUpper() : boll.getLower(); |
| | | } |
| | | |
| | | /** |
| | | * 计算第二止盈目标:斐波那契161.8% |
| | | */ |
| | | private BigDecimal calculateSecondProfitTarget(BigDecimal entryPrice, Direction direction) { |
| | | BigDecimal fibonacciRatio = new BigDecimal("1.618"); |
| | | if (direction == Direction.LONG) { |
| | | return entryPrice.multiply(BigDecimal.ONE.add(fibonacciRatio.divide(new BigDecimal(100)))); |
| | | } else { |
| | | return entryPrice.multiply(BigDecimal.ONE.subtract(fibonacciRatio.divide(new BigDecimal(100)))); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算第三止盈目标:简单趋势线破位(这里简化为MA5下穿MA20) |
| | | */ |
| | | private BigDecimal calculateThirdProfitTarget(BigDecimal currentPrice, Direction direction) { |
| | | // 这里使用简化的趋势线破位判断 |
| | | // 实际应用中可以使用更复杂的趋势线计算 |
| | | return direction == Direction.LONG ? ma.getEma20() : ma.getEma20(); |
| | | } |
| | | |
| | | /** |
| | | * 黑天鹅事件过滤 |
| | | * 规避重大事件前后30分钟、链上大额转账、异常资金费率 |
| | | * @param fundingRate 当前资金费率 |
| | | * @param hasLargeTransfer 是否有大额转账 |
| | | * @param hasUpcomingEvent 是否有即将到来的重大事件 |
| | | * @return 是否应该规避交易 |
| | | */ |
| | | public boolean blackSwanFilter(BigDecimal fundingRate, boolean hasLargeTransfer, boolean hasUpcomingEvent) { |
| | | if (!config.isEnableBlackSwanFilter()) { |
| | | return false; |
| | | } |
| | | |
| | | // 资金费率绝对值大于0.2% |
| | | boolean isAbnormalFundingRate = fundingRate != null && |
| | | fundingRate.abs().compareTo(new BigDecimal("0.002")) > 0; |
| | | |
| | | // 大额转账监控(这里简化为外部传入) |
| | | // 重大事件监控(这里简化为外部传入) |
| | | |
| | | boolean shouldAvoid = isAbnormalFundingRate || hasLargeTransfer || hasUpcomingEvent; |
| | | |
| | | if (shouldAvoid) { |
| | | log.debug("黑天鹅事件过滤触发 - 资金费率异常: {}, 大额转账: {}, 即将发生重大事件: {}", |
| | | isAbnormalFundingRate, hasLargeTransfer, hasUpcomingEvent); |
| | | } |
| | | |
| | | return shouldAvoid; |
| | | } |
| | | |
| | | /** |
| | | * 止盈结果类 |
| | | */ |
| | | public static class ProfitTakingResult { |
| | | private SignalType signal; |
| | | private BigDecimal closePositionSize; |
| | | |
| | | public ProfitTakingResult(SignalType signal, BigDecimal closePositionSize) { |
| | | this.signal = signal; |
| | | this.closePositionSize = closePositionSize; |
| | | } |
| | | |
| | | public SignalType getSignal() { |
| | | return signal; |
| | | } |
| | | |
| | | public BigDecimal getClosePositionSize() { |
| | | return closePositionSize; |
| | | } |
| | | } |
| | | } |
| | |
| | | KANG_CANG("抗压比例KANG_CANG","0.9"), |
| | | ZHI_SUN("止损比例ZHI_SUN","0.8"), |
| | | //每次下单的张数 |
| | | BUY_CNT("每次开仓的张数buyCnt","0.1"), |
| | | BUY_CNT_INIT("每次初始化开仓张数的基础值buyCntInit","0.1"), |
| | | BUY_CNT("每次开仓的张数buyCnt","0.5"), |
| | | BUY_CNT_INIT("每次初始化开仓张数的基础值buyCntInit","0.5"), |
| | | BUY_CNT_TIME("每次开仓张数的倍数基础值buyCntTime","20"), |
| | | OUT("是否允许下单out","操作中"), |
| | | CTVAL("合约面值ctVal","0.1"), |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs.param; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | public class Kline{ |
| | | private String ts; |
| | | private BigDecimal o; |
| | | private BigDecimal h; |
| | | private BigDecimal l; |
| | | private BigDecimal c; |
| | | private BigDecimal vol; |
| | | private String confirm; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs.param; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Data |
| | | public class StrategyParam { |
| | | private String accountName; |
| | | } |