Compare commits
merge into: majun:master
majun:dev
majun:liruiqiang
majun:majun
majun:master
pull from: majun:liruiqiang
majun:dev
majun:liruiqiang
majun:majun
majun:master
6 Commits
master
...
liruiqiang
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
829913e5ec |
WR指标
|
1 week ago |
|
|
48aa7dafbc |
EXPMA指标
|
1 week ago |
|
|
14cbc45601 |
MA指标与CCI指标
|
1 week ago |
|
|
c350de9937 |
MA工具版本1
|
2 weeks ago |
|
|
bf36f084b4 |
11.18 22:22
完成MACD指标的插件 |
2 weeks ago |
|
|
423959272a |
11.18 16:50
完成连续交易日股票基本数据的工具类 |
2 weeks ago |
14 changed files with 1107 additions and 12 deletions
-
12pom.xml
-
46src/main/java/com/deepchart/controller/IndexController.java
-
22src/main/java/com/deepchart/entity/MACDData.java
-
17src/main/java/com/deepchart/entity/StockDailyData.java
-
9src/main/java/com/deepchart/entity/StockInfo.java
-
16src/main/java/com/deepchart/service/IndexService.java
-
39src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
-
118src/main/java/com/deepchart/utils/CCIUtil.java
-
156src/main/java/com/deepchart/utils/EXPMAUtil.java
-
227src/main/java/com/deepchart/utils/MACDUtil.java
-
102src/main/java/com/deepchart/utils/MAUtil.java
-
24src/main/java/com/deepchart/utils/Result.java
-
100src/main/java/com/deepchart/utils/StockDataUtil.java
-
231src/main/java/com/deepchart/utils/WRUtil.java
@ -1,10 +1,56 @@ |
|||||
package com.deepchart.controller; |
package com.deepchart.controller; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
import com.deepchart.entity.StockInfo; |
||||
|
import com.deepchart.service.IndexService; |
||||
|
import com.deepchart.utils.Result; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
import org.springframework.web.bind.annotation.RestController; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
@RestController |
@RestController |
||||
@RequestMapping(value = "/api/index", produces = "application/json") |
@RequestMapping(value = "/api/index", produces = "application/json") |
||||
public class IndexController { |
public class IndexController { |
||||
|
|
||||
|
@Autowired |
||||
|
private IndexService indexService; |
||||
|
|
||||
|
@PostMapping("/macd") |
||||
|
public Result macd(@RequestBody StockInfo stock) { |
||||
|
List<StockDailyData> list = indexService.getStockData(stock); |
||||
|
String result = indexService.macd(list); |
||||
|
return Result.success("success", result); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/ma") |
||||
|
public Result ma(@RequestBody StockInfo stock) { |
||||
|
List<StockDailyData> list = indexService.getStockData(stock); |
||||
|
String result = indexService.ma(list); |
||||
|
return Result.success("success", result); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/cci") |
||||
|
public Result cci(@RequestBody StockInfo stock) { |
||||
|
List<StockDailyData> list = indexService.getStockData(stock); |
||||
|
String result = indexService.cci(list); |
||||
|
return Result.success("success", result); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/expma") |
||||
|
public Result expma(@RequestBody StockInfo stock) { |
||||
|
List<StockDailyData> list = indexService.getStockData(stock); |
||||
|
String result = indexService.expma(list); |
||||
|
return Result.success("success", result); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/wr") |
||||
|
public Result wr(@RequestBody StockInfo stock) { |
||||
|
List<StockDailyData> list = indexService.getStockData(stock); |
||||
|
String result = indexService.wr(list); |
||||
|
return Result.success("success", result); |
||||
|
} |
||||
} |
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.deepchart.entity; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
|
||||
|
/** |
||||
|
* MACD指标计算结果实体类 |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class MACDData { |
||||
|
private LocalDate date; // 交易日日期 |
||||
|
private double dif; // 差离值 |
||||
|
private double dea; // 信号线 |
||||
|
private double macdBar; // MACD柱状图 |
||||
|
|
||||
|
// 判断MACD柱颜色(红/绿) |
||||
|
public boolean isRedBar() { return macdBar > 0; } |
||||
|
public boolean isGreenBar() { return macdBar < 0; } |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
package com.deepchart.entity; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
|
||||
|
/** |
||||
|
* 股票单日行情基本数据实体类 |
||||
|
*/ |
||||
|
@Data |
||||
|
public class StockDailyData { |
||||
|
private LocalDate date; // 交易日日期 |
||||
|
private double openPrice; // 开盘价 |
||||
|
private double closePrice; // 收盘价 |
||||
|
private double lowPrice; // 最低价 |
||||
|
private double highPrice; // 最高价 |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
package com.deepchart.entity; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class StockInfo { |
||||
|
private String market; |
||||
|
private String code; |
||||
|
} |
||||
@ -1,4 +1,20 @@ |
|||||
package com.deepchart.service; |
package com.deepchart.service; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
import com.deepchart.entity.StockInfo; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
public interface IndexService { |
public interface IndexService { |
||||
|
List<StockDailyData> getStockData(StockInfo stock); |
||||
|
|
||||
|
String macd(List<StockDailyData> list); |
||||
|
|
||||
|
String ma(List<StockDailyData> list); |
||||
|
|
||||
|
String cci(List<StockDailyData> list); |
||||
|
|
||||
|
String expma(List<StockDailyData> list); |
||||
|
|
||||
|
String wr(List<StockDailyData> list); |
||||
} |
} |
||||
@ -1,8 +1,47 @@ |
|||||
package com.deepchart.service.impl; |
package com.deepchart.service.impl; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
import com.deepchart.entity.StockInfo; |
||||
import com.deepchart.service.IndexService; |
import com.deepchart.service.IndexService; |
||||
|
import com.deepchart.utils.*; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
@Service |
@Service |
||||
public class IndexServiceImpl implements IndexService { |
public class IndexServiceImpl implements IndexService { |
||||
|
|
||||
|
@Autowired |
||||
|
private StockDataUtil stockDataUtil; |
||||
|
|
||||
|
@Override |
||||
|
public List<StockDailyData> getStockData(StockInfo stock) { |
||||
|
return stockDataUtil.getStockData(stock); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String macd(List<StockDailyData> list) { |
||||
|
return MACDUtil.analyzeStock(list); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String ma(List<StockDailyData> list) { |
||||
|
return MAUtil.analyzeMA(list); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String cci(List<StockDailyData> list) { |
||||
|
return CCIUtil.analyzeCCI(list); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String expma(List<StockDailyData> list) { |
||||
|
return EXPMAUtil.analyzeEXPMA(list,12,40); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String wr(List<StockDailyData> list) { |
||||
|
return WRUtil.analyzeWR(list); |
||||
|
} |
||||
} |
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class CCIUtil { |
||||
|
|
||||
|
private static final int CCI_PERIOD = 14; // CCI默认周期 |
||||
|
|
||||
|
public static String analyzeCCI(List<StockDailyData> stockDataList) { |
||||
|
if (stockDataList == null || stockDataList.size() < CCI_PERIOD) { |
||||
|
return "CCI分析失败:数据不足,至少需要" + CCI_PERIOD + "个交易日"; |
||||
|
} |
||||
|
|
||||
|
// 1. 计算CCI指标值 |
||||
|
double currentCCI = calculateCCI(stockDataList); |
||||
|
double prevCCI = calculateCCI(stockDataList.subList(0, stockDataList.size() - 1)); |
||||
|
|
||||
|
// 2. 计算20日均线(用于判断价格位置) |
||||
|
double twentyDayMA = calculateMA(stockDataList, 20); |
||||
|
double currentClose = stockDataList.get(stockDataList.size() - 1).getClosePrice(); |
||||
|
String pricePosition = (currentClose > twentyDayMA) ? "20日均线之上" : "20日均线之下"; |
||||
|
|
||||
|
// 3. 判断信号类型 |
||||
|
String signalType = "无信号"; |
||||
|
String signalDescription = "CCI值未触发标准信号"; |
||||
|
|
||||
|
if (prevCCI <= -100 && currentCCI > -100) { |
||||
|
signalType = "超卖反弹买入信号"; |
||||
|
signalDescription = "CCI从超卖区上穿-100线,进入常态区间"; |
||||
|
} else if (prevCCI >= 100 && currentCCI < 100) { |
||||
|
signalType = "超买回调卖出信号"; |
||||
|
signalDescription = "CCI从超买区下穿+100线,进入常态区间"; |
||||
|
} else if (currentCCI > 100) { |
||||
|
signalType = "强势上涨信号"; |
||||
|
signalDescription = "CCI持续在+100上方,处于强势上涨趋势"; |
||||
|
} else if (currentCCI < -100) { |
||||
|
signalType = "强势下跌信号"; |
||||
|
signalDescription = "CCI持续在-100下方,处于强势下跌趋势"; |
||||
|
} |
||||
|
|
||||
|
// 4. 生成分析报告 |
||||
|
return generateReport(currentCCI, signalType, signalDescription, pricePosition, currentClose, twentyDayMA); |
||||
|
} |
||||
|
|
||||
|
private static double calculateCCI(List<StockDailyData> stockDataList) { |
||||
|
int n = stockDataList.size(); |
||||
|
List<Double> typicalPrices = new ArrayList<>(); |
||||
|
|
||||
|
// 计算典型价格 (H+L+C)/3 |
||||
|
for (StockDailyData data : stockDataList) { |
||||
|
double typicalPrice = (data.getHighPrice() + data.getLowPrice() + data.getClosePrice()) / 3; |
||||
|
typicalPrices.add(typicalPrice); |
||||
|
} |
||||
|
|
||||
|
// 计算N期平均典型价格 |
||||
|
double avgTypicalPrice = calculateMAFromDoubleList(typicalPrices, CCI_PERIOD); |
||||
|
|
||||
|
// 计算平均偏差 |
||||
|
double sumAbsDev = 0.0; |
||||
|
for (int i = n - CCI_PERIOD; i < n; i++) { |
||||
|
sumAbsDev += Math.abs(typicalPrices.get(i) - avgTypicalPrice); |
||||
|
} |
||||
|
double avgDev = sumAbsDev / CCI_PERIOD; |
||||
|
|
||||
|
// 计算CCI |
||||
|
return (typicalPrices.get(n - 1) - avgTypicalPrice) / (0.015 * avgDev); |
||||
|
} |
||||
|
|
||||
|
private static double calculateMA(List<StockDailyData> stockDataList, int period) { |
||||
|
if (stockDataList.size() < period) { |
||||
|
throw new IllegalArgumentException("数据不足,无法计算" + period + "日均线"); |
||||
|
} |
||||
|
double sum = 0.0; |
||||
|
for (int i = stockDataList.size() - period; i < stockDataList.size(); i++) { |
||||
|
sum += stockDataList.get(i).getClosePrice(); |
||||
|
} |
||||
|
return sum / period; |
||||
|
} |
||||
|
|
||||
|
private static double calculateMAFromDoubleList(List<Double> prices, int period) { |
||||
|
if (prices.size() < period) { |
||||
|
throw new IllegalArgumentException("数据不足,无法计算" + period + "日均线"); |
||||
|
} |
||||
|
double sum = 0.0; |
||||
|
for (int i = prices.size() - period; i < prices.size(); i++) { |
||||
|
sum += prices.get(i); |
||||
|
} |
||||
|
return sum / period; |
||||
|
} |
||||
|
|
||||
|
private static String generateReport(double cciValue, String signalType, String signalDescription, |
||||
|
String pricePosition, double currentClose, double twentyDayMA) { |
||||
|
return String.format( |
||||
|
"CCI分析报告(14日周期)\n" + |
||||
|
"当前CCI值: %.1f | 信号类型: %s | 信号描述: %s\n" + |
||||
|
"价格位置: %s | 当前价格: %.2f | 20日均线: %.2f\n" + |
||||
|
"核心逻辑: %s", |
||||
|
cciValue, signalType, signalDescription, |
||||
|
pricePosition, currentClose, twentyDayMA, |
||||
|
getSignalLogic(signalType, cciValue) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private static String getSignalLogic(String signalType, double cciValue) { |
||||
|
if (signalType.contains("买入信号")) { |
||||
|
return "CCI上穿-100线 + 价格站上20日均线 + 市场情绪从超卖转为常态"; |
||||
|
} else if (signalType.contains("卖出信号")) { |
||||
|
return "CCI下穿+100线 + 价格回调至常态区间 + 市场情绪从超买转为常态"; |
||||
|
} else if (signalType.contains("强势上涨")) { |
||||
|
return "CCI持续在+100以上,趋势强劲,应持有多头仓位"; |
||||
|
} else if (signalType.contains("强势下跌")) { |
||||
|
return "CCI持续在-100以下,趋势疲软,应持币观望"; |
||||
|
} |
||||
|
return "CCI在常态区间(-100~100),无明确信号"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.util.*; |
||||
|
|
||||
|
public class EXPMAUtil { |
||||
|
|
||||
|
public static String analyzeEXPMA(List<StockDailyData> stockData, int shortPeriod, int longPeriod) { |
||||
|
if (stockData == null || stockData.size() < Math.max(shortPeriod, longPeriod)) { |
||||
|
return "数据不足,无法进行 EXPMA 分析。请至少提供 " + Math.max(shortPeriod, longPeriod) + " 天数据。"; |
||||
|
} |
||||
|
|
||||
|
// 1. 计算 EXPMA |
||||
|
List<Double> expmaShort = calculateSingleExpma(stockData, shortPeriod); |
||||
|
List<Double> expmaLong = calculateSingleExpma(stockData, longPeriod); |
||||
|
|
||||
|
int n = stockData.size(); |
||||
|
LocalDate latestDate = stockData.get(n - 1).getDate(); |
||||
|
double latestPrice = stockData.get(n - 1).getClosePrice(); |
||||
|
double currentShort = expmaShort.get(n - 1); |
||||
|
double currentLong = expmaLong.get(n - 1); |
||||
|
|
||||
|
StringBuilder analysis = new StringBuilder(); |
||||
|
analysis.append("📊 EXPMA 趋势分析(截至 ").append(latestDate).append(")\n"); |
||||
|
analysis.append(" • 短期EXPMA(").append(shortPeriod).append("): ").append(String.format("%.2f", currentShort)).append("\n"); |
||||
|
analysis.append(" • 长期EXPMA(").append(longPeriod).append("): ").append(String.format("%.2f", currentLong)).append("\n"); |
||||
|
analysis.append(" • 当前收盘价: ").append(String.format("%.2f", latestPrice)).append("\n\n"); |
||||
|
|
||||
|
// 2. 判断多头/空头排列 |
||||
|
boolean isBullish = currentShort > currentLong; |
||||
|
boolean shortUp = n >= 2 && expmaShort.get(n - 1) > expmaShort.get(n - 2); |
||||
|
boolean longUp = n >= 2 && expmaLong.get(n - 1) > expmaLong.get(n - 2); |
||||
|
|
||||
|
if (isBullish && shortUp && longUp) { |
||||
|
analysis.append("📈 趋势状态:【多头排列】\n"); |
||||
|
analysis.append(" → 市场处于上升趋势,短期均线在长期均线上方且双双上行。\n"); |
||||
|
if (latestPrice >= currentShort) { |
||||
|
analysis.append(" → 股价站稳短期EXPMA之上,强势格局。\n"); |
||||
|
} else { |
||||
|
analysis.append(" → 股价回踩短期EXPMA,若不破可视为加仓机会。\n"); |
||||
|
} |
||||
|
} else if (!isBullish && !shortUp && !longUp) { |
||||
|
analysis.append("📉 趋势状态:【空头排列】\n"); |
||||
|
analysis.append(" → 市场处于下降趋势,短期均线在长期均线下方且双双下行。\n"); |
||||
|
if (latestPrice <= currentShort) { |
||||
|
analysis.append(" → 股价受压于短期EXPMA,弱势明显,建议观望。\n"); |
||||
|
} else { |
||||
|
analysis.append(" → 股价反弹至短期EXPMA附近,可能是做空或减仓时机。\n"); |
||||
|
} |
||||
|
} else { |
||||
|
analysis.append("🔄 趋势状态:【震荡整理】\n"); |
||||
|
analysis.append(" → 均线缠绕或方向不一致,市场缺乏明确趋势。\n"); |
||||
|
} |
||||
|
|
||||
|
// 3. 检测最近是否发生金叉/死叉(过去5日内) |
||||
|
boolean goldenCross = false; |
||||
|
boolean deathCross = false; |
||||
|
int crossDayIndex = -1; |
||||
|
String crossType = ""; |
||||
|
|
||||
|
// 从后往前找最近一次交叉(最多看最近10天) |
||||
|
int lookback = Math.min(10, n - 1); |
||||
|
for (int i = n - 1; i >= lookback; i--) { |
||||
|
double shortToday = expmaShort.get(i); |
||||
|
double longToday = expmaLong.get(i); |
||||
|
double shortYesterday = expmaShort.get(i - 1); |
||||
|
double longYesterday = expmaLong.get(i - 1); |
||||
|
|
||||
|
// 金叉:昨日 short <= long,今日 short > long |
||||
|
if (shortYesterday <= longYesterday && shortToday > longToday) { |
||||
|
goldenCross = true; |
||||
|
crossDayIndex = i; |
||||
|
crossType = "黄金交叉"; |
||||
|
break; |
||||
|
} |
||||
|
// 死叉:昨日 short >= long,今日 short < long |
||||
|
if (shortYesterday >= longYesterday && shortToday < longToday) { |
||||
|
deathCross = true; |
||||
|
crossDayIndex = i; |
||||
|
crossType = "死亡交叉"; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (goldenCross || deathCross) { |
||||
|
LocalDate crossDate = stockData.get(crossDayIndex).getDate(); |
||||
|
analysis.append("\n🔔 近期信号:") |
||||
|
.append(crossType) |
||||
|
.append("(").append(crossDate).append(")\n"); |
||||
|
if (goldenCross) { |
||||
|
analysis.append(" → 短期动能转强,可视为潜在买入信号。\n"); |
||||
|
if (currentLong > expmaLong.get(Math.max(0, crossDayIndex - 5))) { |
||||
|
analysis.append(" → 且长期EXPMA已走平或向上,信号可靠性较高。\n"); |
||||
|
} |
||||
|
} else { |
||||
|
analysis.append(" → 短期动能转弱,可视为潜在卖出信号。\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 4. 支撑/阻力提示 |
||||
|
if (isBullish) { |
||||
|
double support = currentShort; |
||||
|
if (Math.abs(latestPrice - support) / support < 0.01) { // 价格接近短期EXPMA(1%内) |
||||
|
analysis.append("\n🛡️ 支撑观察:当前股价接近短期EXPMA(") |
||||
|
.append(String.format("%.2f", support)) |
||||
|
.append("),若在此企稳,可能延续升势。\n"); |
||||
|
} |
||||
|
} else if (!isBullish) { |
||||
|
double resistance = currentShort; |
||||
|
if (Math.abs(latestPrice - resistance) / resistance < 0.01) { |
||||
|
analysis.append("\n⚠️ 阻力观察:当前股价接近短期EXPMA(") |
||||
|
.append(String.format("%.2f", resistance)) |
||||
|
.append("),若遇阻回落,可能延续跌势。\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 5. 总结建议 |
||||
|
analysis.append("\n📌 操作建议:\n"); |
||||
|
if (isBullish && shortUp) { |
||||
|
analysis.append(" • 多头趋势中,持股待涨;回调至短期EXPMA附近可考虑低吸。\n"); |
||||
|
} else if (!isBullish && !shortUp) { |
||||
|
analysis.append(" • 空头趋势中,避免抄底;反弹至短期EXPMA附近可考虑离场。\n"); |
||||
|
} else { |
||||
|
analysis.append(" • 市场方向不明,建议结合成交量或震荡指标(如KDJ)辅助决策。\n"); |
||||
|
} |
||||
|
analysis.append("\n❗ 注意:EXPMA为趋势跟踪指标,在横盘震荡市中易产生假信号,请勿单独依赖。"); |
||||
|
|
||||
|
return analysis.toString(); |
||||
|
} |
||||
|
|
||||
|
// 辅助方法:计算单周期 EXPMA |
||||
|
private static List<Double> calculateSingleExpma(List<StockDailyData> data, int period) { |
||||
|
List<Double> expma = new ArrayList<>(data.size()); |
||||
|
double alpha = 2.0 / (period + 1); |
||||
|
double value = 0.0; |
||||
|
|
||||
|
for (int i = 0; i < data.size(); i++) { |
||||
|
double price = data.get(i).getClosePrice(); |
||||
|
if (i == 0) { |
||||
|
// 初始值用 SMA(period) |
||||
|
double sum = 0; |
||||
|
int count = Math.min(period, data.size()); |
||||
|
for (int j = 0; j < count; j++) { |
||||
|
sum += data.get(j).getClosePrice(); |
||||
|
} |
||||
|
value = sum / count; |
||||
|
} else { |
||||
|
value = alpha * price + (1 - alpha) * value; |
||||
|
} |
||||
|
expma.add(value); |
||||
|
} |
||||
|
return expma; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,227 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.MACDData; |
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* MACD指标分析工具类 |
||||
|
* 实现五大核心分析方法 |
||||
|
*/ |
||||
|
public class MACDUtil { |
||||
|
|
||||
|
public static String analyzeStock(List<StockDailyData> prices) { |
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
|
||||
|
List<MACDData> macdData = calculateMACD(prices); |
||||
|
|
||||
|
// 1. 交叉信号分析 |
||||
|
report.append("===== 交叉信号分析 =====\n"); |
||||
|
analyzeCrossovers(macdData, report); |
||||
|
|
||||
|
// 2. 柱状图动能分析 |
||||
|
report.append("\n===== 柱状图动能分析 =====\n"); |
||||
|
analyzeBarMomentum(macdData, report); |
||||
|
|
||||
|
// 3. 零轴趋势判断 |
||||
|
report.append("\n===== 零轴趋势判断 =====\n"); |
||||
|
analyzeZeroAxisTrend(macdData, report); |
||||
|
|
||||
|
// 4. 背离现象检测 |
||||
|
report.append("\n===== 背离现象分析 =====\n"); |
||||
|
analyzeDivergence(prices, macdData, report); |
||||
|
|
||||
|
// 5. 参数调整建议 |
||||
|
// report.append("\n===== 参数调整建议 =====\n"); |
||||
|
// report.append(" 当前参数:EMA(12,26,9)\n"); |
||||
|
// report.append("• 短线交易可调整为EMA(6,13,5)提升灵敏度\n"); |
||||
|
// report.append("• 长线投资可调整为EMA(24,52,9)过滤噪音\n"); |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
|
||||
|
// 核心MACD计算逻辑 |
||||
|
private static List<MACDData> calculateMACD(List<StockDailyData> stockData) { |
||||
|
List<MACDData> result = new ArrayList<>(); |
||||
|
double ema12; |
||||
|
double ema26; |
||||
|
double prevEma12 = 0.0; |
||||
|
double prevEma26 = 0.0; |
||||
|
double prevDea = 0.0; |
||||
|
boolean firstDay = true; |
||||
|
|
||||
|
for (StockDailyData daily : stockData) { |
||||
|
double close = daily.getClosePrice(); |
||||
|
LocalDate date = daily.getDate(); |
||||
|
|
||||
|
// 首日初始化 |
||||
|
if (firstDay) { |
||||
|
firstDay = false; |
||||
|
MACDData data = new MACDData(date, 0.0, 0.0, 0.0); |
||||
|
result.add(data); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 计算EMA(12)和EMA(26) |
||||
|
ema12 = calculateEMA(close, prevEma12, 12); |
||||
|
ema26 = calculateEMA(close, prevEma26, 26); |
||||
|
|
||||
|
// 计算DIF |
||||
|
double dif = ema12 - ema26; |
||||
|
|
||||
|
// 计算DEA(DIF的9日EMA) |
||||
|
double dea = calculateEMA(dif, prevDea, 9); |
||||
|
|
||||
|
// 计算MACD柱状图 |
||||
|
double macdBar = 2 * (dif - dea); |
||||
|
|
||||
|
// 保存当前值供下个交易日使用 |
||||
|
prevEma12 = ema12; |
||||
|
prevEma26 = ema26; |
||||
|
prevDea = dea; |
||||
|
|
||||
|
result.add(new MACDData(date, dif, dea, macdBar)); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
// 指数移动平均(EMA)计算公式 |
||||
|
private static double calculateEMA(double currentPrice, double prevEMA, int period) { |
||||
|
return currentPrice * (2.0 / (period + 1)) + prevEMA * ((period - 1.0) / (period + 1)); |
||||
|
} |
||||
|
|
||||
|
// 金叉/死叉检测 |
||||
|
private static void analyzeCrossovers(List<MACDData> macdData, StringBuilder report) { |
||||
|
int goldenCrossCount = 0; |
||||
|
int deathCrossCount = 0; |
||||
|
boolean aboveZeroGolden = false; |
||||
|
boolean belowZeroDeath = false; |
||||
|
|
||||
|
for (int i = 1; i < macdData.size(); i++) { |
||||
|
MACDData prev = macdData.get(i-1); |
||||
|
MACDData curr = macdData.get(i); |
||||
|
|
||||
|
// 金叉:DIF上穿DEA |
||||
|
if (prev.getDif() < prev.getDea() && curr.getDif() > curr.getDea()) { |
||||
|
goldenCrossCount++; |
||||
|
if (curr.getDif() > 0) { |
||||
|
aboveZeroGolden = true; |
||||
|
report.append(String.format("• %tF:强势金叉信号(零轴上方),买入机会\n", curr.getDate())); |
||||
|
} else { |
||||
|
report.append(String.format("• %tF:弱势金叉信号(零轴下方),谨慎参与\n", curr.getDate())); |
||||
|
} |
||||
|
} |
||||
|
// 死叉:DIF下穿DEA |
||||
|
else if (prev.getDif() > prev.getDea() && curr.getDif() < curr.getDea()) { |
||||
|
deathCrossCount++; |
||||
|
if (curr.getDif() < 0) { |
||||
|
belowZeroDeath = true; |
||||
|
report.append(String.format("• %tF:强势死叉信号(零轴下方),卖出警示\n", curr.getDate())); |
||||
|
} else { |
||||
|
report.append(String.format("• %tF:弱势死叉信号(零轴上方),部分止盈\n", curr.getDate())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
report.append("\n 交叉信号统计:\n"); |
||||
|
report.append("• 近期出现金叉次数:").append(goldenCrossCount).append("次\n"); |
||||
|
report.append("• 近期出现死叉次数:").append(deathCrossCount).append("次\n"); |
||||
|
if (aboveZeroGolden) report.append("▶ 存在零轴上方的强势金叉,多头动能强劲\n"); |
||||
|
if (belowZeroDeath) report.append("▶ 存在零轴下方的强势死叉,空头风险显著\n"); |
||||
|
} |
||||
|
|
||||
|
// 柱状图动能分析 |
||||
|
private static void analyzeBarMomentum(List<MACDData> macdData, StringBuilder report) { |
||||
|
if (macdData.size() < 5) return; |
||||
|
|
||||
|
// 获取最近5个交易日的柱状图数据 |
||||
|
MACDData curr = macdData.get(macdData.size() - 1); |
||||
|
MACDData prev1 = macdData.get(macdData.size() - 2); |
||||
|
MACDData prev2 = macdData.get(macdData.size() - 3); |
||||
|
|
||||
|
report.append(" 最新交易日数据:\n"); |
||||
|
report.append(String.format("• MACD柱状图数值:%.4f (%s)\n", |
||||
|
curr.getMacdBar(), curr.isRedBar() ? "红柱" : "绿柱")); |
||||
|
|
||||
|
// 红柱分析(零轴上方) |
||||
|
if (curr.isRedBar()) { |
||||
|
report.append("▶ 当前处于多头市场(红柱区域)\n"); |
||||
|
if (curr.getMacdBar() > prev1.getMacdBar() && prev1.getMacdBar() > prev2.getMacdBar()) { |
||||
|
report.append("▶ 红柱连续放大(3日),上涨动能加速\n"); |
||||
|
} else if (curr.getMacdBar() < prev1.getMacdBar()) { |
||||
|
report.append("▶ 红柱开始缩短,警惕上涨动能衰减\n"); |
||||
|
} |
||||
|
} |
||||
|
// 绿柱分析(零轴下方) |
||||
|
else { |
||||
|
report.append("▶ 当前处于空头市场(绿柱区域)\n"); |
||||
|
if (curr.getMacdBar() < prev1.getMacdBar() && prev1.getMacdBar() < prev2.getMacdBar()) { |
||||
|
report.append("▶ 绿柱连续放大(3日),下跌动能加速\n"); |
||||
|
} else if (curr.getMacdBar() > prev1.getMacdBar()) { |
||||
|
report.append("▶ 绿柱开始缩短,空头力量减弱\n"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 零轴趋势分析 |
||||
|
private static void analyzeZeroAxisTrend(List<MACDData> macdData, StringBuilder report) { |
||||
|
int aboveZeroDays = 0; |
||||
|
int belowZeroDays = 0; |
||||
|
|
||||
|
for (MACDData data : macdData) { |
||||
|
if (data.getDif() > 0) aboveZeroDays++; |
||||
|
else belowZeroDays++; |
||||
|
} |
||||
|
|
||||
|
double aboveRatio = 100.0 * aboveZeroDays / macdData.size(); |
||||
|
report.append(String.format(" 历史交易日统计(共%d天):\n", macdData.size())); |
||||
|
report.append(String.format("• DIF在零轴上方天数:%d天 (占比%.1f%%)\n", aboveZeroDays, aboveRatio)); |
||||
|
report.append(String.format("• DIF在零轴下方天数:%d天 (占比%.1f%%)\n", belowZeroDays, 100 - aboveRatio)); |
||||
|
|
||||
|
MACDData last = macdData.get(macdData.size() - 1); |
||||
|
if (last.getDif() > 0 && last.getDea() > 0) { |
||||
|
report.append("▶ 当前趋势:多头市场(DIF与DEA均在零轴上方)\n"); |
||||
|
} else if (last.getDif() < 0 && last.getDea() < 0) { |
||||
|
report.append("▶ 当前趋势:空头市场(DIF与DEA均在零轴下方)\n"); |
||||
|
} else { |
||||
|
report.append("▶ 当前趋势:震荡行情(多空分歧明显)\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 背离分析 |
||||
|
private static void analyzeDivergence(List<StockDailyData> prices, List<MACDData> macdData, StringBuilder report) { |
||||
|
boolean hasTopDivergence = false; |
||||
|
boolean hasBottomDivergence = false; |
||||
|
|
||||
|
// 寻找最近30个交易日内的背离 |
||||
|
int lookback = Math.min(30, prices.size() - 1); |
||||
|
for (int i = prices.size() - 2; i > prices.size() - lookback; i--) { |
||||
|
// 顶背离检测(股价新高但MACD低点) |
||||
|
if (prices.get(i).getClosePrice() > prices.get(i+1).getClosePrice() && |
||||
|
prices.get(i).getClosePrice() > prices.get(i-1).getClosePrice() && |
||||
|
macdData.get(i).getDif() < macdData.get(i+1).getDif() && |
||||
|
macdData.get(i).getDif() < macdData.get(i-1).getDif()) { |
||||
|
|
||||
|
hasTopDivergence = true; |
||||
|
report.append(String.format("• %tF:检测到顶背离(股价新高但MACD走弱),警示下跌风险\n", |
||||
|
prices.get(i).getDate())); |
||||
|
} |
||||
|
// 底背离检测(股价新低但MACD高点) |
||||
|
else if (prices.get(i).getClosePrice() < prices.get(i+1).getClosePrice() && |
||||
|
prices.get(i).getClosePrice() < prices.get(i-1).getClosePrice() && |
||||
|
macdData.get(i).getDif() > macdData.get(i+1).getDif() && |
||||
|
macdData.get(i).getDif() > macdData.get(i-1).getDif()) { |
||||
|
|
||||
|
hasBottomDivergence = true; |
||||
|
report.append(String.format("• %tF:检测到底背离(股价新低但MACD走强),预示反弹机会\n", |
||||
|
prices.get(i).getDate())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!hasTopDivergence) report.append("• 近期未检测到显著顶背离现象\n"); |
||||
|
if (!hasBottomDivergence) report.append("• 近期未检测到显著底背离现象\n"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.util.*; |
||||
|
|
||||
|
public class MAUtil { |
||||
|
|
||||
|
// 周期定义:短期5日、中期20日、长期40日 |
||||
|
private static final int SHORT_PERIOD = 5; |
||||
|
private static final int MID_PERIOD = 20; |
||||
|
private static final int LONG_PERIOD = 40; |
||||
|
|
||||
|
/** |
||||
|
* 对股票日线数据进行MA指标分析,返回JSON格式的中文字符串结果 |
||||
|
* @param stockDataList 至少包含40个交易日的StockDailyData列表(按时间升序排列) |
||||
|
* @return JSON格式的分析结果字符串(全中文内容) |
||||
|
*/ |
||||
|
public static String analyzeMA(List<StockDailyData> stockDataList) { |
||||
|
if (stockDataList == null || stockDataList.size() < LONG_PERIOD) { |
||||
|
return buildJsonResult("错误", "无信号", "数据不足,至少需要40个交易日", 0.0, 0.0, null); |
||||
|
} |
||||
|
|
||||
|
List<Double> closePrices = new ArrayList<>(); |
||||
|
for (StockDailyData data : stockDataList) { |
||||
|
closePrices.add(data.getClosePrice()); |
||||
|
} |
||||
|
|
||||
|
int n = closePrices.size(); // 应为40 |
||||
|
|
||||
|
double shortMA = calculateMA(closePrices, SHORT_PERIOD); |
||||
|
double midMA = calculateMA(closePrices, MID_PERIOD); |
||||
|
Double longMA = calculateMA(closePrices, LONG_PERIOD); |
||||
|
|
||||
|
// 1. 趋势判断 |
||||
|
String trend = "震荡趋势"; |
||||
|
if (shortMA > midMA && midMA > longMA) { |
||||
|
trend = "上升趋势"; |
||||
|
} else if (shortMA < midMA && midMA < longMA) { |
||||
|
trend = "下降趋势"; |
||||
|
} |
||||
|
|
||||
|
// 2. 金叉/死叉判断(5日上穿20日为金叉,下穿为死叉) |
||||
|
String crossSignal = "无交叉信号"; |
||||
|
if (n >= Math.max(SHORT_PERIOD, MID_PERIOD) + 1) { |
||||
|
double prevShortMA = calculateMA(closePrices.subList(0, n - 1), SHORT_PERIOD); |
||||
|
double prevMidMA = calculateMA(closePrices.subList(0, n - 1), MID_PERIOD); |
||||
|
|
||||
|
if (prevShortMA <= prevMidMA && shortMA > midMA) { |
||||
|
crossSignal = "金叉信号"; |
||||
|
} else if (prevShortMA >= prevMidMA && shortMA < midMA) { |
||||
|
crossSignal = "死叉信号"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 3. 均线排列状态 |
||||
|
String arrangement = "均线粘合"; |
||||
|
if (Math.abs(shortMA - midMA) / midMA < 0.005 && Math.abs(midMA - longMA) / longMA < 0.005) { |
||||
|
arrangement = "均线粘合"; |
||||
|
} else if (shortMA > midMA && midMA > longMA) { |
||||
|
arrangement = "多头排列"; |
||||
|
} else if (shortMA < midMA && midMA < longMA) { |
||||
|
arrangement = "空头排列"; |
||||
|
} |
||||
|
|
||||
|
return buildJsonResult(trend, crossSignal, arrangement, shortMA, midMA, longMA); |
||||
|
} |
||||
|
|
||||
|
private static double calculateMA(List<Double> prices, int period) { |
||||
|
if (prices.size() < period) { |
||||
|
throw new IllegalArgumentException("价格数据长度不足,无法计算" + period + "日均线"); |
||||
|
} |
||||
|
double sum = 0.0; |
||||
|
int n = prices.size(); |
||||
|
for (int i = n - period; i < n; i++) { |
||||
|
sum += prices.get(i); |
||||
|
} |
||||
|
return sum / period; |
||||
|
} |
||||
|
|
||||
|
// 手动构建轻量级 JSON 字符串(全中文值) |
||||
|
private static String buildJsonResult(String trend, String crossSignal, String arrangement, |
||||
|
double shortMA, double midMA, Double longMA) { |
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
sb.append("趋势:").append(escapeJson(trend)).append(","); |
||||
|
sb.append("交叉信号:").append(escapeJson(crossSignal)).append(","); |
||||
|
sb.append("均线排列:").append(escapeJson(arrangement)).append(","); |
||||
|
sb.append("五日均线:").append(String.format("%.4f", shortMA)).append(","); |
||||
|
sb.append("二十日均线:").append(String.format("%.4f", midMA)).append(","); |
||||
|
if (longMA != null) { |
||||
|
sb.append("四十日均线:").append(String.format("%.4f", longMA)); |
||||
|
} else { |
||||
|
sb.append("四十日均线:无"); |
||||
|
} |
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
private static String escapeJson(String str) { |
||||
|
if (str == null) return ""; |
||||
|
return str.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* 请求响应返回结果工具类 |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class Result<T> { |
||||
|
private Integer code; //状态码 |
||||
|
private String msg; //返回信息 |
||||
|
private T data; //返回数据 |
||||
|
|
||||
|
public static <T> Result<T> success(String message, T data) { |
||||
|
return new Result<>(200, message, data); |
||||
|
} |
||||
|
|
||||
|
public static <T> Result<T> error(Integer resultCode, String message) { |
||||
|
return new Result<>(resultCode, message, null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
import com.deepchart.entity.StockInfo; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.io.BufferedReader; |
||||
|
import java.io.InputStreamReader; |
||||
|
import java.io.OutputStream; |
||||
|
import java.net.HttpURLConnection; |
||||
|
import java.net.URL; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.time.LocalDate; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 股票行情基本数据工具类 |
||||
|
*/ |
||||
|
@Component |
||||
|
public class StockDataUtil { |
||||
|
private static final String STOCK_DATA_PREFIX = "https://api.homilychart.com/link"; |
||||
|
private static final String TOKEN = "8nkj4QBV1RPIb4CzoRTnbZi0+fEeMx8pywnIlrmTxdwROKkuwWqAWu9orpkpeXVqL98DPfeonNYpHv+mucA"; |
||||
|
private static final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
public List<StockDailyData> getStockData(StockInfo stock) { |
||||
|
|
||||
|
List<List<Object>> kLine20 = List.of(); |
||||
|
|
||||
|
try { |
||||
|
// 1. 构建请求URL |
||||
|
String url = STOCK_DATA_PREFIX + "/api/superBrainData"; |
||||
|
|
||||
|
// 2. 使用Map构造请求体参数 |
||||
|
Map<String, Object> requestBody = new HashMap<>(); |
||||
|
requestBody.put("brainPrivilegeState", 1); |
||||
|
requestBody.put("marketList", "can,usa,hk,vi,sg,th,in,cn,gb,my"); |
||||
|
requestBody.put("market", stock.getMarket()); |
||||
|
requestBody.put("code", stock.getCode()); |
||||
|
|
||||
|
// 3. 将Map转换为JSON字符串 |
||||
|
String jsonInputString = objectMapper.writeValueAsString(requestBody); |
||||
|
|
||||
|
// 4. 创建HTTP连接 |
||||
|
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); |
||||
|
connection.setRequestMethod("POST"); |
||||
|
connection.setRequestProperty("Content-Type", "application/json"); |
||||
|
connection.setRequestProperty("token", TOKEN); |
||||
|
connection.setDoOutput(true); |
||||
|
|
||||
|
// 5. 发送请求数据 |
||||
|
try (OutputStream os = connection.getOutputStream()) { |
||||
|
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); |
||||
|
os.write(input, 0, input.length); |
||||
|
} |
||||
|
|
||||
|
// 6. 解析响应 |
||||
|
if (connection.getResponseCode() == 200) { |
||||
|
try (BufferedReader br = new BufferedReader( |
||||
|
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { |
||||
|
StringBuilder response = new StringBuilder(); |
||||
|
String responseLine; |
||||
|
while ((responseLine = br.readLine()) != null) { |
||||
|
response.append(responseLine.trim()); |
||||
|
} |
||||
|
|
||||
|
// 解析JSON响应 |
||||
|
Map<String, Object> jsonResponse = objectMapper.readValue(response.toString(), Map.class); |
||||
|
Map<String, Object> data = (Map<String, Object>) jsonResponse.get("data"); |
||||
|
Map<String, Object> brain = (Map<String, Object>) data.get("Brain"); |
||||
|
kLine20 = (List<List<Object>>) brain.get("KLine20"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 7. 关闭连接 |
||||
|
connection.disconnect(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
|
||||
|
List<StockDailyData> list = new ArrayList<>(); |
||||
|
|
||||
|
for (List<Object> v : kLine20) { |
||||
|
StockDailyData s = new StockDailyData(); |
||||
|
s.setDate(LocalDate.parse((String) v.get(0), DateTimeFormatter.ofPattern("yyyy/MM/dd"))); |
||||
|
s.setOpenPrice((double)v.get(1)); |
||||
|
s.setClosePrice((double)v.get(2)); |
||||
|
s.setLowPrice((double)v.get(3)); |
||||
|
s.setHighPrice((double)v.get(4)); |
||||
|
list.add(s); |
||||
|
} |
||||
|
|
||||
|
return list; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
import java.util.*; |
||||
|
|
||||
|
public class WRUtil { |
||||
|
|
||||
|
/** |
||||
|
* 对40日股票数据进行WR(威廉指标)分析 |
||||
|
* |
||||
|
* @param stockData 按时间升序排列的40天股票日线数据(最早在前) |
||||
|
* @return WR分析报告文本 |
||||
|
*/ |
||||
|
public static String analyzeWR(List<StockDailyData> stockData) { |
||||
|
if (stockData == null || stockData.size() < 14) { |
||||
|
return "❌ 数据不足:WR分析至少需要14天K线数据。"; |
||||
|
} |
||||
|
|
||||
|
int n = stockData.size(); |
||||
|
// 常用双周期:快线 N=6,慢线 N=10 |
||||
|
List<Double> wrFast = calculateWr(stockData, 6); |
||||
|
List<Double> wrSlow = calculateWr(stockData, 10); |
||||
|
List<Double> prices = new ArrayList<>(); |
||||
|
for (var d : stockData) prices.add(d.getClosePrice()); |
||||
|
|
||||
|
LocalDate latestDate = stockData.get(n - 1).getDate(); |
||||
|
double latestPrice = prices.get(n - 1); |
||||
|
double currentFast = wrFast.get(n - 1); |
||||
|
double currentSlow = wrSlow.get(n - 1); |
||||
|
|
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
report.append("📊 威廉指标(WR)分析报告(截至 ").append(latestDate).append(")\n"); |
||||
|
report.append(" • 当前收盘价: ").append(String.format("%.2f", latestPrice)).append("\n"); |
||||
|
report.append(" • WR(6): ").append(String.format("%.1f", currentFast)).append("\n"); |
||||
|
report.append(" • WR(10): ").append(String.format("%.1f", currentSlow)).append("\n\n"); |
||||
|
|
||||
|
// 区域判断 |
||||
|
boolean fastInOversold = currentFast >= 80; |
||||
|
boolean slowInOversold = currentSlow >= 80; |
||||
|
boolean fastInOverbought = currentFast <= 20; |
||||
|
boolean slowInOverbought = currentSlow <= 20; |
||||
|
|
||||
|
if (fastInOversold || slowInOversold) { |
||||
|
report.append("📉 市场状态:【超卖区域】\n"); |
||||
|
report.append(" → 价格接近近期低点,短期下跌动能可能衰竭。\n"); |
||||
|
} else if (fastInOverbought || slowInOverbought) { |
||||
|
report.append("📈 市场状态:【超买区域】\n"); |
||||
|
report.append(" → 价格接近近期高点,短期上涨动能可能衰竭。\n"); |
||||
|
} else { |
||||
|
report.append("⚖️ 市场状态:【常态波动区(20~80)】\n"); |
||||
|
report.append(" → 无极端情绪信号,WR参考价值有限。\n"); |
||||
|
} |
||||
|
|
||||
|
// 双线交叉信号(仅当有足够历史数据) |
||||
|
String crossSignal = detectCross(wrFast, wrSlow, n); |
||||
|
if (!crossSignal.isEmpty()) { |
||||
|
report.append("\n").append(crossSignal); |
||||
|
} |
||||
|
|
||||
|
// 背离检测 |
||||
|
boolean topDiv = detectTopDivergence(prices, wrFast, n); |
||||
|
boolean bottomDiv = detectBottomDivergence(prices, wrFast, n); |
||||
|
|
||||
|
if (topDiv) { |
||||
|
report.append("\n🚨 顶背离信号!\n"); |
||||
|
report.append(" → 股价创新高,但WR强势减弱,上涨动能不足。\n"); |
||||
|
report.append(" → 强烈建议减仓或止盈。\n"); |
||||
|
} |
||||
|
if (bottomDiv) { |
||||
|
report.append("\n💎 底背离信号!\n"); |
||||
|
report.append(" → 股价创新低,但WR弱势减弱,下跌动能衰竭。\n"); |
||||
|
report.append(" → 可视为短线抄底机会。\n"); |
||||
|
} |
||||
|
if (!topDiv && !bottomDiv) { |
||||
|
report.append("\n🔍 背离状态:无显著背离\n"); |
||||
|
} |
||||
|
|
||||
|
// 综合操作建议 |
||||
|
report.append("\n📌 操作建议:\n"); |
||||
|
if (bottomDiv || (fastInOversold && currentFast < wrFast.get(n - 2))) { |
||||
|
report.append(" • WR从超卖区回升,结合底背离,可考虑短线买入。\n"); |
||||
|
} else if (topDiv || (fastInOverbought && currentFast > wrFast.get(n - 2))) { |
||||
|
report.append(" • WR从超买区回落,结合顶背离,建议逢高减仓。\n"); |
||||
|
} else if (currentFast > 50 && currentSlow > 50) { |
||||
|
report.append(" • WR处于中性偏弱区域,观望为主。\n"); |
||||
|
} else if (currentFast < 50 && currentSlow < 50) { |
||||
|
report.append(" • WR处于中性偏强区域,持股待涨。\n"); |
||||
|
} else { |
||||
|
report.append(" • 市场方向不明,建议结合趋势指标(如EXPMA)综合判断。\n"); |
||||
|
} |
||||
|
|
||||
|
report.append("\n❗ 注意:WR在单边行情中易钝化(长期超买/超卖),震荡市中效果最佳。"); |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
|
||||
|
// 计算WR(N) |
||||
|
private static List<Double> calculateWr(List<StockDailyData> data, int period) { |
||||
|
List<Double> wr = new ArrayList<>(Collections.nCopies(data.size(), 100.0)); |
||||
|
if (data.size() < period) return wr; |
||||
|
|
||||
|
for (int i = period - 1; i < data.size(); i++) { |
||||
|
double highest = Double.MIN_VALUE; |
||||
|
double lowest = Double.MAX_VALUE; |
||||
|
|
||||
|
// 找过去period天(含当日)的最高价和最低价 |
||||
|
for (int j = i - period + 1; j <= i; j++) { |
||||
|
highest = Math.max(highest, data.get(j).getHighPrice()); |
||||
|
lowest = Math.min(lowest, data.get(j).getLowPrice()); |
||||
|
} |
||||
|
|
||||
|
double close = data.get(i).getClosePrice(); |
||||
|
if (highest == lowest) { |
||||
|
wr.set(i, 0.0); // 防止除零 |
||||
|
} else { |
||||
|
double value = (highest - close) / (highest - lowest) * 100.0; |
||||
|
wr.set(i, Math.max(0.0, Math.min(100.0, value))); // 限制在[0,100] |
||||
|
} |
||||
|
} |
||||
|
return wr; |
||||
|
} |
||||
|
|
||||
|
// 检测双线交叉(仅看最近3天) |
||||
|
private static String detectCross(List<Double> fast, List<Double> slow, int n) { |
||||
|
if (n < 3) return ""; |
||||
|
|
||||
|
double f0 = fast.get(n - 1), f1 = fast.get(n - 2); |
||||
|
double s0 = slow.get(n - 1), s1 = slow.get(n - 2); |
||||
|
|
||||
|
// 金叉:快线上穿慢线(且在超卖区附近) |
||||
|
if (f1 <= s1 && f0 > s0 && f0 >= 70) { |
||||
|
return "✨ 买入信号:WR(6)在超卖区上穿WR(10),形成金叉,短期反弹可期。\n"; |
||||
|
} |
||||
|
|
||||
|
// 死叉:快线下穿慢线(且在超买区附近) |
||||
|
if (f1 >= s1 && f0 < s0 && f0 <= 30) { |
||||
|
return "⚠️ 卖出信号:WR(6)在超买区下穿WR(10),形成死叉,短期回调风险加大。\n"; |
||||
|
} |
||||
|
|
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
// 顶背离:价格新高,WR高点降低(WR是反向指标,高点=弱势) |
||||
|
private static boolean detectTopDivergence(List<Double> prices, List<Double> wr, int n) { |
||||
|
if (n < 20) return false; |
||||
|
|
||||
|
// 找最近一个价格高点(局部最大) |
||||
|
int recentHighIdx = findRecentPeak(prices, n, 5); |
||||
|
if (recentHighIdx == -1 || recentHighIdx < 10) return false; |
||||
|
|
||||
|
// 找上一个价格高点 |
||||
|
int prevHighIdx = findPreviousPeak(prices, recentHighIdx - 5, recentHighIdx - 15); |
||||
|
if (prevHighIdx == -1) return false; |
||||
|
|
||||
|
double recentPriceHigh = prices.get(recentHighIdx); |
||||
|
double prevPriceHigh = prices.get(prevHighIdx); |
||||
|
double recentWrHigh = wr.get(recentHighIdx); // WR高值 = 弱势 |
||||
|
double prevWrHigh = wr.get(prevHighIdx); |
||||
|
|
||||
|
// 价格新高 + WR高点更低(即弱势更弱)→ 顶背离 |
||||
|
return (recentPriceHigh > prevPriceHigh) && (recentWrHigh < prevWrHigh); |
||||
|
} |
||||
|
|
||||
|
// 底背离:价格新低,WR低点抬高(WR低值=强势) |
||||
|
private static boolean detectBottomDivergence(List<Double> prices, List<Double> wr, int n) { |
||||
|
if (n < 20) return false; |
||||
|
|
||||
|
int recentLowIdx = findRecentTrough(prices, n, 5); |
||||
|
if (recentLowIdx == -1 || recentLowIdx < 10) return false; |
||||
|
|
||||
|
int prevLowIdx = findPreviousTrough(prices, recentLowIdx - 5, recentLowIdx - 15); |
||||
|
if (prevLowIdx == -1) return false; |
||||
|
|
||||
|
double recentPriceLow = prices.get(recentLowIdx); |
||||
|
double prevPriceLow = prices.get(prevLowIdx); |
||||
|
double recentWrLow = wr.get(recentLowIdx); // WR低值 = 强势 |
||||
|
double prevWrLow = wr.get(prevLowIdx); |
||||
|
|
||||
|
// 价格新低 + WR低点更高(即强势更强)→ 底背离 |
||||
|
return (recentPriceLow < prevPriceLow) && (recentWrLow > prevWrLow); |
||||
|
} |
||||
|
|
||||
|
// 辅助:找最近局部高点 |
||||
|
private static int findRecentPeak(List<Double> prices, int n, int window) { |
||||
|
for (int i = n - 1; i >= n - window && i >= 2; i--) { |
||||
|
if (prices.get(i) >= prices.get(i - 1) && prices.get(i) >= prices.get(i + 1)) { |
||||
|
return i; |
||||
|
} |
||||
|
} |
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
private static int findPreviousPeak(List<Double> prices, int end, int start) { |
||||
|
start = Math.max(0, start); |
||||
|
end = Math.min(end, prices.size() - 1); |
||||
|
double max = -1; |
||||
|
int idx = -1; |
||||
|
for (int i = start; i <= end; i++) { |
||||
|
if (prices.get(i) > max) { |
||||
|
max = prices.get(i); |
||||
|
idx = i; |
||||
|
} |
||||
|
} |
||||
|
return idx; |
||||
|
} |
||||
|
|
||||
|
private static int findRecentTrough(List<Double> prices, int n, int window) { |
||||
|
for (int i = n - 2; i >= n - window && i >= 2; i--) { |
||||
|
if (prices.get(i) <= prices.get(i - 1) && prices.get(i) <= prices.get(i + 1)) { |
||||
|
return i; |
||||
|
} |
||||
|
} |
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
private static int findPreviousTrough(List<Double> prices, int end, int start) { |
||||
|
start = Math.max(0, start); |
||||
|
end = Math.min(end, prices.size() - 1); |
||||
|
double min = Double.MAX_VALUE; |
||||
|
int idx = -1; |
||||
|
for (int i = start; i <= end; i++) { |
||||
|
if (prices.get(i) < min) { |
||||
|
min = prices.get(i); |
||||
|
idx = i; |
||||
|
} |
||||
|
} |
||||
|
return idx; |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue