| | |
| | | |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * MACD (Moving Average Convergence Divergence) 指标实现 |
| | | * 计算逻辑: |
| | | * 1. DIF = EMA(12) - EMA(26) |
| | | * 2. DEA = EMA(DIF, 9) |
| | | * 3. MACD柱状图 = (DIF - DEA) * 2 |
| | | * 1. 快线DIFF = EMA(Close, 12) - EMA(Close, 26) |
| | | * 2. 慢线DEA = EMA(DIFF, 9) |
| | | * 3. MACD柱状图 = (DIFF - DEA) * macdBarsMultiplier |
| | | * |
| | | * 作用: |
| | | * 1. 识别趋势方向和动能变化 |
| | | * 2. DIF上穿DEA形成金叉,提示买入信号 |
| | | * 3. DIF下穿DEA形成死叉,提示卖出信号 |
| | | * 4. MACD柱状图由负转正,提示多头力量增强 |
| | | * 5. MACD柱状图由正转负,提示空头力量增强 |
| | | * 核心概念: |
| | | * 1. DIFF是EMA12与EMA26之间的距离,反映短期与长期趋势的差异 |
| | | * 2. 当DIFF > 0表示EMA12在EMA26上方,代表多头趋势 |
| | | * 3. 当DIFF < 0表示EMA12在EMA26下方,代表空头趋势 |
| | | * 4. DEA是DIFF的EMA平滑线,用于过滤DIFF的波动 |
| | | * 5. MACD柱状图通过放大倍数展示DIFF与DEA之间的关系,便于观察趋势变化 |
| | | * |
| | | * 价格参数类型: |
| | | * - 参数名称:prices |
| | | * - 参数类型:List<BigDecimal> |
| | | * - 参数说明:需要至少2个价格数据点用于计算,数据越多计算越准确 |
| | | * 多空判断: |
| | | * - 多头机会:DIFF在0轴上且MACD柱状图向上,股价同步上涨 |
| | | * - 空头机会:DIFF在0轴下且MACD柱状图向下,股价同步下跌 |
| | | * |
| | | * 推荐时间粒度及优缺点: |
| | | * 1. 1分钟(1m): |
| | | * - 优点:反应迅速,适合超短线交易 |
| | | * - 缺点:MACD柱状图波动剧烈,信号频繁 |
| | | * 2. 5分钟(5m): |
| | | * - 优点:平衡了反应速度和信号可靠性 |
| | | * - 缺点:仍有一定噪音 |
| | | * 3. 15分钟(15m): |
| | | * - 优点:适合日内交易,信号较为可靠 |
| | | * - 缺点:反应速度较慢 |
| | | * 4. 1小时(1h)及以上: |
| | | * - 优点:趋势信号明确,MACD柱状图变化稳定 |
| | | * - 缺点:反应滞后,不适合短线交易 |
| | | * 信号过滤: |
| | | * - 可通过设置macdBarsSmoothingPeriod启用MACD柱状图平滑处理 |
| | | * - 平滑公式:MACD柱状图 = MA((DIFF - DEA) * macdBarsMultiplier, macdBarsSmoothingPeriod) |
| | | * - 作用:消除柱状图杂讯,使信号更加清晰 |
| | | */ |
| | | @Slf4j |
| | | @Getter |
| | | @Setter |
| | | public class MACD extends IndicatorBase { |
| | |
| | | public static final int DEFAULT_SLOW_PERIOD = 26; |
| | | public static final int DEFAULT_SIGNAL_PERIOD = 9; |
| | | |
| | | // 动态周期参数 |
| | | // 默认MACD柱状图放大倍数 |
| | | public static final int DEFAULT_MACDBARS_MULTIPLIER = 2; |
| | | // 默认MACD柱状图平滑周期(0表示不平滑) |
| | | public static final int DEFAULT_MACDBARS_SMOOTHING_PERIOD = 0; |
| | | |
| | | // 周期参数 |
| | | private int fastPeriod; |
| | | private int slowPeriod; |
| | | private int signalPeriod; |
| | | |
| | | // MACD柱状图参数 |
| | | private int macdBarsMultiplier; // MACD柱状图放大倍数 |
| | | private int macdBarsSmoothingPeriod; // MACD柱状图平滑周期(0表示不平滑) |
| | | |
| | | // MACD计算结果 |
| | | private BigDecimal dif = BigDecimal.ZERO; |
| | | private BigDecimal dea = BigDecimal.ZERO; |
| | | private BigDecimal macdBar = BigDecimal.ZERO; |
| | | |
| | | // 历史值缓存 |
| | | private BigDecimal prevFastEMA = null; |
| | | private BigDecimal prevSlowEMA = null; |
| | | private BigDecimal prevDea = null; |
| | | // 用于保存历史DIF值,用于计算DEA |
| | | private List<BigDecimal> difHistory = new ArrayList<>(); |
| | | // 最大保存的DIF历史值数量,至少为signalPeriod |
| | | private static final int MAX_DIF_HISTORY = 30; |
| | | private List<BigDecimal> difHistory = new ArrayList<>(); // 保存历史DIF值,用于计算DEA |
| | | private List<BigDecimal> rawMacdBarHistory = new ArrayList<>(); // 保存原始MACD柱状图值,用于平滑处理 |
| | | |
| | | // 构造函数使用默认周期 |
| | | // 最大保存的历史值数量 |
| | | private static final int MAX_HISTORY_SIZE = 50; |
| | | |
| | | // 构造函数使用默认参数 |
| | | public MACD() { |
| | | this.fastPeriod = DEFAULT_FAST_PERIOD; |
| | | this.slowPeriod = DEFAULT_SLOW_PERIOD; |
| | | this.signalPeriod = DEFAULT_SIGNAL_PERIOD; |
| | | this.macdBarsMultiplier = DEFAULT_MACDBARS_MULTIPLIER; |
| | | this.macdBarsSmoothingPeriod = DEFAULT_MACDBARS_SMOOTHING_PERIOD; |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param volatility 标准化波动率(百分比),用于动态调整周期 |
| | | */ |
| | | public void calculate(List<BigDecimal> prices, BigDecimal volatility) { |
| | | if (prices == null || prices.size() < 2) { |
| | | if (prices == null || prices.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | |
| | | adjustPeriodsByVolatility(volatility); |
| | | } |
| | | |
| | | // 计算快速EMA |
| | | |
| | | |
| | | // 计算快速EMA (12日) |
| | | prevFastEMA = calculateEMA(prices, fastPeriod, prevFastEMA); |
| | | |
| | | // 计算慢速EMA |
| | | // 计算慢速EMA (26日) |
| | | prevSlowEMA = calculateEMA(prices, slowPeriod, prevSlowEMA); |
| | | |
| | | // 计算DIF |
| | | // 计算DIF = EMA(12) - EMA(26) |
| | | dif = prevFastEMA.subtract(prevSlowEMA).setScale(8, RoundingMode.HALF_UP); |
| | | |
| | | // 将新的DIF值添加到历史记录 |
| | | difHistory.add(dif); |
| | | // 保持历史记录在合理范围内 |
| | | if (difHistory.size() > MAX_DIF_HISTORY) { |
| | | if (difHistory.size() > MAX_HISTORY_SIZE) { |
| | | difHistory.remove(0); |
| | | } |
| | | |
| | | // 计算DEA:使用足够数量的历史DIF值 |
| | | // 如果历史DIF值不足,使用当前可用的所有值 |
| | | int difCount = difHistory.size(); |
| | | List<BigDecimal> deaCalculationList; |
| | | if (difCount >= signalPeriod) { |
| | | // 使用最近的signalPeriod个DIF值 |
| | | deaCalculationList = difHistory.subList(difCount - signalPeriod, difCount); |
| | | } else if (difCount > 0) { |
| | | // 使用所有可用的DIF值 |
| | | deaCalculationList = new ArrayList<>(difHistory); |
| | | } else { |
| | | // 没有DIF历史值,使用当前DIF |
| | | deaCalculationList = Collections.singletonList(dif); |
| | | // 计算DEA = EMA(DIFF, 9) |
| | | calculateDEA(); |
| | | |
| | | // 计算原始MACD柱状图值 = (DIF - DEA) * 放大倍数 |
| | | BigDecimal rawMacdBar = dif.subtract(dea).multiply(new BigDecimal(macdBarsMultiplier)).setScale(8, RoundingMode.HALF_UP); |
| | | |
| | | // 将原始MACD柱状图值添加到历史记录 |
| | | rawMacdBarHistory.add(rawMacdBar); |
| | | // 保持历史记录在合理范围内 |
| | | if (rawMacdBarHistory.size() > MAX_HISTORY_SIZE) { |
| | | rawMacdBarHistory.remove(0); |
| | | } |
| | | |
| | | prevDea = calculateEMA(deaCalculationList, signalPeriod, prevDea); |
| | | // 如果启用了平滑处理,则计算平滑后的MACD柱状图 |
| | | if (macdBarsSmoothingPeriod > 0) { |
| | | macdBar = smoothMacdBars().setScale(8, RoundingMode.HALF_UP); |
| | | } else { |
| | | macdBar = rawMacdBar; |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 计算DEA指标 |
| | | */ |
| | | private void calculateDEA() { |
| | | int difCount = difHistory.size(); |
| | | |
| | | // 如果没有足够的DIF历史值,无法计算有效的DEA |
| | | if (difCount == 0) { |
| | | dea = BigDecimal.ZERO; |
| | | prevDea = null; |
| | | return; |
| | | } |
| | | |
| | | // 计算DEA = EMA(DIFF, signalPeriod) |
| | | // 使用所有DIF历史值来计算初始EMA,然后使用最新值更新 |
| | | prevDea = calculateEMA(difHistory, signalPeriod, prevDea); |
| | | dea = prevDea.setScale(8, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | // 计算MACD柱状图 |
| | | macdBar = dif.subtract(dea).multiply(new BigDecimal(2)).setScale(8, RoundingMode.HALF_UP); |
| | | /** |
| | | * 平滑MACD柱状图 |
| | | * @return 平滑后的MACD柱状图值 |
| | | */ |
| | | private BigDecimal smoothMacdBars() { |
| | | int historyCount = rawMacdBarHistory.size(); |
| | | |
| | | log.info("MACD计算结果 - DIF: {}, DEA: {}, MACD柱状图: {}, 参数: fast={}, slow={}, signal={}", |
| | | dif, dea, macdBar, fastPeriod, slowPeriod, signalPeriod); |
| | | // 如果没有足够的历史数据,返回最新的原始值 |
| | | if (historyCount < macdBarsSmoothingPeriod) { |
| | | return rawMacdBarHistory.get(historyCount - 1); |
| | | } |
| | | |
| | | // 使用简单移动平均平滑MACD柱状图 |
| | | List<BigDecimal> recentMacdBars = rawMacdBarHistory.subList(historyCount - macdBarsSmoothingPeriod, historyCount); |
| | | return calculateMA(recentMacdBars, macdBarsSmoothingPeriod); |
| | | } |
| | | |
| | | /** |
| | |
| | | signalPeriod = 5; |
| | | } |
| | | |
| | | log.info("根据波动率{}调整MACD周期: fast={}, slow={}, signal={}", |
| | | volatility, fastPeriod, slowPeriod, signalPeriod); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 判断金叉信号(DIF上穿DEA) |
| | | * @param previousDIF 上一个DIF值 |
| | | * @param previousDEA 上一个DEA值 |
| | | * @return 是否形成金叉 |
| | | */ |
| | | public boolean isGoldenCross(BigDecimal previousDIF, BigDecimal previousDEA) { |
| | | return previousDIF.compareTo(previousDEA) < 0 && dif.compareTo(dea) > 0; |
| | | return previousDIF != null && previousDEA != null && |
| | | previousDIF.compareTo(previousDEA) < 0 && dif.compareTo(dea) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 判断死叉信号(DIF下穿DEA) |
| | | * @param previousDIF 上一个DIF值 |
| | | * @param previousDEA 上一个DEA值 |
| | | * @return 是否形成死叉 |
| | | */ |
| | | public boolean isDeathCross(BigDecimal previousDIF, BigDecimal previousDEA) { |
| | | return previousDIF.compareTo(previousDEA) > 0 && dif.compareTo(dea) < 0; |
| | | return previousDIF != null && previousDEA != null && |
| | | previousDIF.compareTo(previousDEA) > 0 && dif.compareTo(dea) < 0; |
| | | } |
| | | |
| | | /** |
| | | * 重置MACD指标状态 |
| | | */ |
| | | public void reset() { |
| | | dif = BigDecimal.ZERO; |
| | | dea = BigDecimal.ZERO; |
| | | macdBar = BigDecimal.ZERO; |
| | | prevFastEMA = null; |
| | | prevSlowEMA = null; |
| | | prevDea = null; |
| | | difHistory.clear(); |
| | | rawMacdBarHistory.clear(); |
| | | |
| | | } |
| | | } |