From 5a53a21e97311d239f57266d58d2d7b6f55c42a6 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Thu, 25 Dec 2025 15:20:24 +0800
Subject: [PATCH] feat(indicator): 更新技术指标基础类和MACD实现
---
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACDTest.java | 118 ++++++++++++++++
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACD.java | 182 +++++++++++++++++--------
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/IndicatorBase.java | 100 +++++++-------
3 files changed, 288 insertions(+), 112 deletions(-)
diff --git a/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/IndicatorBase.java b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/IndicatorBase.java
index 055e3fc..a3b80b7 100644
--- a/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/IndicatorBase.java
+++ b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/IndicatorBase.java
@@ -6,31 +6,31 @@
import java.util.List;
/**
- * 技术指标基础类,提供通用计算方法
+ * Technical indicators base class, provides common calculation methods
*
- * 指标组合策略:
- * 1. 趋势判断(MA/AdvancedMA/MACD):判断价格的整体走势方向
- * 2. 动量确认(RSI/KDJ):确认当前趋势的强度和可持续性
- * 3. 波动参考(BOLL):确定价格的合理波动范围和突破时机
+ * Indicator combination strategy:
+ * 1. Trend judgment (MA/AdvancedMA/MACD): Determine the overall trend direction of prices
+ * 2. Momentum confirmation (RSI/KDJ): Confirm the strength and sustainability of the current trend
+ * 3. Volatility reference (BOLL): Determine reasonable price volatility range and breakthrough timing
*
- * 多空方向选择逻辑:
- * - 多头信号:MA多头排列 + MACD金叉 + RSI(30-70) + BOLL价格在上轨与中轨之间
- * - 空头信号:MA空头排列 + MACD死叉 + RSI(30-70) + BOLL价格在下轨与中轨之间
- * - 震荡信号:AdvancedMA三线粘合 + RSI(40-60) + BOLL带宽收窄
+ * Long/Short direction selection logic:
+ * - Long signal: MA bullish arrangement + MACD golden cross + RSI(30-70) + BOLL price between upper and middle band
+ * - Short signal: MA bearish arrangement + MACD death cross + RSI(30-70) + BOLL price between lower and middle band
+ * - Consolidation signal: AdvancedMA three-line convergence + RSI(40-60) + BOLL bandwidth narrowing
*
- * 开仓和平仓策略:
- * - 开多:MA金叉 + MACD金叉 + KDJ金叉 + RSI(30-70) + 价格突破BOLL中轨
- * - 开空:MA死叉 + MACD死叉 + KDJ死叉 + RSI(30-70) + 价格跌破BOLL中轨
- * - 平多:MA死叉 + MACD死叉 + RSI超买(>70) + 价格跌破BOLL中轨
- * - 平空:MA金叉 + MACD金叉 + RSI超卖(<30) + 价格突破BOLL中轨
+ * Open and close position strategies:
+ * - Open long: MA golden cross + MACD golden cross + KDJ golden cross + RSI(30-70) + price breaks through BOLL middle band
+ * - Open short: MA death cross + MACD death cross + KDJ death cross + RSI(30-70) + price breaks below BOLL middle band
+ * - Close long: MA death cross + MACD death cross + RSI overbought(>70) + price breaks below BOLL middle band
+ * - Close short: MA golden cross + MACD golden cross + RSI oversold(<30) + price breaks through BOLL middle band
*/
public abstract class IndicatorBase {
/**
- * 计算移动平均值
- * @param prices 价格列表
- * @param period 周期
- * @return 移动平均值
+ * Calculate moving average
+ * @param prices Price list
+ * @param period Period
+ * @return Moving average value
*/
protected BigDecimal calculateMA(List<BigDecimal> prices, int period) {
if (prices == null || prices.size() < period) {
@@ -44,11 +44,11 @@
}
/**
- * 计算指数移动平均值
- * @param prices 价格列表
- * @param period 周期
- * @param prevEMA 前一个EMA值
- * @return 指数移动平均值
+ * Calculate exponential moving average
+ * @param prices Price list
+ * @param period Period
+ * @param prevEMA Previous EMA value
+ * @return Exponential moving average value
*/
protected BigDecimal calculateEMA(List<BigDecimal> prices, int period, BigDecimal prevEMA) {
if (prices == null || prices.size() == 0) {
@@ -63,10 +63,10 @@
}
/**
- * 计算标准差
- * @param prices 价格列表
- * @param period 周期
- * @return 标准差
+ * Calculate standard deviation
+ * @param prices Price list
+ * @param period Period
+ * @return Standard deviation
*/
protected BigDecimal calculateStdDev(List<BigDecimal> prices, int period) {
if (prices == null || prices.size() < period) {
@@ -83,9 +83,9 @@
}
/**
- * 计算平方根(简化实现)
- * @param value 输入值
- * @return 平方根
+ * Calculate square root (simplified implementation)
+ * @param value Input value
+ * @return Square root
*/
protected BigDecimal sqrt(BigDecimal value) {
if (value.compareTo(BigDecimal.ZERO) < 0) {
@@ -95,10 +95,10 @@
}
/**
- * 获取最近的价格数据
- * @param prices 所有价格数据
- * @param period 周期
- * @return 最近period个价格数据
+ * Get recent price data
+ * @param prices All price data
+ * @param period Period
+ * @return Recent period price data
*/
protected List<BigDecimal> getRecentPrices(List<BigDecimal> prices, int period) {
if (prices == null || prices.size() == 0) {
@@ -109,12 +109,12 @@
}
/**
- * 计算ATR(Average True Range)
- * @param high 最高价列表
- * @param low 最低价列表
- * @param close 收盘价列表
- * @param period 周期
- * @return ATR值
+ * Calculate ATR (Average True Range)
+ * @param high High price list
+ * @param low Low price list
+ * @param close Close price list
+ * @param period Period
+ * @return ATR value
*/
protected BigDecimal calculateATR(List<BigDecimal> high, List<BigDecimal> low, List<BigDecimal> close, int period) {
if (high == null || low == null || close == null ||
@@ -128,16 +128,16 @@
trList.add(trueRange);
}
- // 使用简单移动平均计算ATR
+ // Use simple moving average to calculate ATR
return calculateMA(trList, Math.min(period, trList.size()));
}
/**
- * 计算真实波幅(True Range)
- * @param high 当前最高价
- * @param low 当前最低价
- * @param prevClose 前收盘价
- * @return 真实波幅
+ * Calculate True Range
+ * @param high Current high price
+ * @param low Current low price
+ * @param prevClose Previous close price
+ * @return True range
*/
protected BigDecimal calculateTrueRange(BigDecimal high, BigDecimal low, BigDecimal prevClose) {
BigDecimal h1 = high.subtract(low);
@@ -147,10 +147,10 @@
}
/**
- * 计算标准化波动率(基于ATR)
- * @param close 收盘价列表
- * @param atr ATR值
- * @return 标准化波动率(百分比)
+ * Calculate normalized volatility (based on ATR)
+ * @param close Close price list
+ * @param atr ATR value
+ * @return Normalized volatility (percentage)
*/
protected BigDecimal normalizeVolatility(List<BigDecimal> close, BigDecimal atr) {
if (close == null || close.size() == 0 || atr.compareTo(BigDecimal.ZERO) == 0) {
diff --git a/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACD.java b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACD.java
index 0be9fea..9b63ee2 100644
--- a/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACD.java
+++ b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACD.java
@@ -2,48 +2,35 @@
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 {
@@ -52,28 +39,43 @@
public static final int DEFAULT_FAST_PERIOD = 12;
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;
}
/**
@@ -90,7 +92,7 @@
* @param volatility 标准化波动率(百分比),用于动态调整周期
*/
public void calculate(List<BigDecimal> prices, BigDecimal volatility) {
- if (prices == null || prices.size() < 2) {
+ if (prices == null || prices.isEmpty()) {
return;
}
@@ -99,45 +101,81 @@
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柱状图
+ * @return 平滑后的MACD柱状图值
+ */
+ private BigDecimal smoothMacdBars() {
+ int historyCount = rawMacdBarHistory.size();
- // 计算MACD柱状图
- macdBar = dif.subtract(dea).multiply(new BigDecimal(2)).setScale(8, RoundingMode.HALF_UP);
+ // 如果没有足够的历史数据,返回最新的原始值
+ if (historyCount < macdBarsSmoothingPeriod) {
+ return rawMacdBarHistory.get(historyCount - 1);
+ }
- log.info("MACD计算结果 - DIF: {}, DEA: {}, MACD柱状图: {}, 参数: fast={}, slow={}, signal={}",
- dif, dea, macdBar, fastPeriod, slowPeriod, signalPeriod);
+ // 使用简单移动平均平滑MACD柱状图
+ List<BigDecimal> recentMacdBars = rawMacdBarHistory.subList(historyCount - macdBarsSmoothingPeriod, historyCount);
+ return calculateMA(recentMacdBars, macdBarsSmoothingPeriod);
}
/**
@@ -161,23 +199,43 @@
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();
+
}
}
diff --git a/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACDTest.java b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACDTest.java
new file mode 100644
index 0000000..ec93614
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/MACDTest.java
@@ -0,0 +1,118 @@
+package com.xcong.excoin.modules.okxNewPrice.indicator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MACD Indicator Test Class
+ * Used to verify the correctness of MACD calculation logic
+ */
+public class MACDTest {
+
+ public static void main(String[] args) {
+ // Create MACD instance
+ MACD macd = new MACD();
+
+ // Set MACD bars parameters (optional, using default values here)
+ macd.setMacdBarsMultiplier(2); // Default multiplier
+ macd.setMacdBarsSmoothingPeriod(0); // No smoothing
+
+ // Generate test price data (simple upward trend)
+ List<BigDecimal> prices = generateTestPrices(30);
+
+ System.out.println("=== MACD Indicator Test Start ===");
+ System.out.println("Price count: " + prices.size());
+ System.out.println("MACD parameters: fast=" + macd.getFastPeriod() + ", slow=" + macd.getSlowPeriod() + ", signal=" + macd.getSignalPeriod());
+ System.out.println("MACD bars parameters: multiplier=" + macd.getMacdBarsMultiplier() + ", smoothing=" + macd.getMacdBarsSmoothingPeriod());
+ System.out.println();
+
+ // Calculate MACD
+ macd.calculate(prices);
+
+ // Output results
+ System.out.println("=== MACD Calculation Results ===");
+ System.out.println("DIF: " + macd.getDif());
+ System.out.println("DEA: " + macd.getDea());
+ System.out.println("MACD Bars: " + macd.getMacdBar());
+ System.out.println();
+
+ // Trend judgment
+ System.out.println("=== Trend Judgment ===");
+ if (macd.getDif().compareTo(BigDecimal.ZERO) > 0) {
+ System.out.println("DIFF > 0: Bullish trend");
+ } else if (macd.getDif().compareTo(BigDecimal.ZERO) < 0) {
+ System.out.println("DIFF < 0: Bearish trend");
+ } else {
+ System.out.println("DIFF = 0: No trend");
+ }
+
+ // Test smoothing function
+ testSmoothingFunction();
+
+ System.out.println("=== MACD Indicator Test End ===");
+ }
+
+ /**
+ * Generate test price data
+ * @param count Number of data points
+ * @return Price list
+ */
+ private static List<BigDecimal> generateTestPrices(int count) {
+ List<BigDecimal> prices = new ArrayList<>();
+ // Start from 100, simple upward trend with some random fluctuations
+ BigDecimal basePrice = new BigDecimal(100);
+ for (int i = 0; i < count; i++) {
+ // Add random fluctuation between 0.1 and 0.5
+ BigDecimal price = basePrice.add(new BigDecimal(Math.random() * 0.4 + 0.1));
+ prices.add(price.setScale(2, BigDecimal.ROUND_HALF_UP));
+ basePrice = price;
+ }
+ return prices;
+ }
+
+ /**
+ * Test MACD bars smoothing function
+ */
+ private static void testSmoothingFunction() {
+ System.out.println("=== MACD Bars Smoothing Function Test ===");
+
+ MACD macd = new MACD();
+ macd.setMacdBarsMultiplier(2);
+ macd.setMacdBarsSmoothingPeriod(3); // 3-day smoothing
+
+ // Generate test price data with more fluctuations
+ List<BigDecimal> prices = generateVolatileTestPrices(30);
+
+ macd.calculate(prices);
+
+ System.out.println("Price count: " + prices.size());
+ System.out.println("MACD parameters: fast=" + macd.getFastPeriod() + ", slow=" + macd.getSlowPeriod() + ", signal=" + macd.getSignalPeriod());
+ System.out.println("MACD bars parameters: multiplier=" + macd.getMacdBarsMultiplier() + ", smoothing=" + macd.getMacdBarsSmoothingPeriod());
+ System.out.println();
+
+ System.out.println("Smoothed MACD Results:");
+ System.out.println("DIF: " + macd.getDif());
+ System.out.println("DEA: " + macd.getDea());
+ System.out.println("MACD Bars: " + macd.getMacdBar());
+ System.out.println();
+ }
+
+ /**
+ * Generate test price data with more fluctuations
+ * @param count Number of data points
+ * @return Price list
+ */
+ private static List<BigDecimal> generateVolatileTestPrices(int count) {
+ List<BigDecimal> prices = new ArrayList<>();
+ // Start from 100 with more random fluctuations
+ BigDecimal basePrice = new BigDecimal(100);
+ for (int i = 0; i < count; i++) {
+ // Add random fluctuation between -1.0 and 1.0
+ BigDecimal price = basePrice.add(new BigDecimal(Math.random() * 2 - 1));
+ prices.add(price.setScale(2, BigDecimal.ROUND_HALF_UP));
+ basePrice = price;
+ }
+ return prices;
+ }
+}
\ No newline at end of file
--
Gitblit v1.9.1