diff --git a/src/main/java/com/deepchart/controller/IndexController.java b/src/main/java/com/deepchart/controller/IndexController.java index ebfb05c..5771118 100644 --- a/src/main/java/com/deepchart/controller/IndexController.java +++ b/src/main/java/com/deepchart/controller/IndexController.java @@ -46,4 +46,11 @@ public class IndexController { String result = indexService.expma(list); return Result.success("success", result); } + + @PostMapping("/wr") + public Result wr(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.wr(list); + return Result.success("success", result); + } } diff --git a/src/main/java/com/deepchart/service/IndexService.java b/src/main/java/com/deepchart/service/IndexService.java index 3dc69a8..df03feb 100644 --- a/src/main/java/com/deepchart/service/IndexService.java +++ b/src/main/java/com/deepchart/service/IndexService.java @@ -15,4 +15,6 @@ public interface IndexService { String cci(List list); String expma(List list); + + String wr(List list); } diff --git a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java index cf8b947..c292a97 100644 --- a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java +++ b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java @@ -39,4 +39,9 @@ public class IndexServiceImpl implements IndexService { public String expma(List list) { return EXPMAUtil.analyzeEXPMA(list,12,40); } + + @Override + public String wr(List list) { + return WRUtil.analyzeWR(list); + } } diff --git a/src/main/java/com/deepchart/utils/WRUtil.java b/src/main/java/com/deepchart/utils/WRUtil.java new file mode 100644 index 0000000..b3968fb --- /dev/null +++ b/src/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 stockData) { + if (stockData == null || stockData.size() < 14) { + return "❌ 数据不足:WR分析至少需要14天K线数据。"; + } + + int n = stockData.size(); + // 常用双周期:快线 N=6,慢线 N=10 + List wrFast = calculateWr(stockData, 6); + List wrSlow = calculateWr(stockData, 10); + List 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 calculateWr(List data, int period) { + List 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 fast, List 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 prices, List 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 prices, List 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 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 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 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 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; + } +}