4 changed files with 245 additions and 0 deletions
-
7src/main/java/com/deepchart/controller/IndexController.java
-
2src/main/java/com/deepchart/service/IndexService.java
-
5src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
-
231src/main/java/com/deepchart/utils/WRUtil.java
@ -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