12 changed files with 537 additions and 6 deletions
-
7pom.xml
-
125src/main/java/com/deepchart/Main.java
-
27src/main/java/com/deepchart/StockApiDemo.java
-
21src/main/java/com/deepchart/controller/IndexController.java
-
19src/main/java/com/deepchart/entity/KDJData.java
-
1src/main/java/com/deepchart/entity/StockDailyData.java
-
6src/main/java/com/deepchart/service/IndexService.java
-
18src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
-
102src/main/java/com/deepchart/utils/KDJUtil.java
-
108src/main/java/com/deepchart/utils/KDUtil.java
-
91src/main/java/com/deepchart/utils/RSIUtil.java
-
18src/main/java/com/deepchart/utils/StockDataUtil.java
@ -0,0 +1,125 @@ |
|||||
|
package com.deepchart; |
||||
|
|
||||
|
import com.alibaba.dashscope.aigc.generation.Generation; |
||||
|
import com.alibaba.dashscope.aigc.generation.GenerationParam; |
||||
|
import com.alibaba.dashscope.aigc.generation.GenerationResult; |
||||
|
import com.alibaba.dashscope.common.Message; |
||||
|
import com.alibaba.dashscope.common.Role; |
||||
|
import com.alibaba.dashscope.exception.InputRequiredException; |
||||
|
import com.alibaba.dashscope.exception.NoApiKeyException; |
||||
|
import com.alibaba.dashscope.tools.FunctionDefinition; |
||||
|
import com.alibaba.dashscope.tools.ToolCallBase; |
||||
|
import com.alibaba.dashscope.tools.ToolCallFunction; |
||||
|
import com.alibaba.dashscope.tools.ToolFunction; |
||||
|
import com.alibaba.dashscope.utils.JsonUtils; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
public class Main { |
||||
|
// 若使用新加坡地域的模型,请释放下列注释 |
||||
|
// static {Constants.baseHttpApiUrl="https://dashscope-intl.aliyuncs.com/api/v1";} |
||||
|
|
||||
|
/** |
||||
|
* 第一步:定义工具的本地实现。 |
||||
|
* @param arguments 模型传入的、包含工具所需参数的JSON字符串。 |
||||
|
* @return 工具执行后的结果字符串。 |
||||
|
*/ |
||||
|
public static String getCurrentWeather(String arguments) { |
||||
|
try { |
||||
|
// 模型提供的参数是JSON格式的,需要我们手动解析。 |
||||
|
ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
JsonNode argsNode = objectMapper.readTree(arguments); |
||||
|
String location = argsNode.get("location").asText(); |
||||
|
|
||||
|
// 用随机结果来模拟真实的API调用或业务逻辑。 |
||||
|
List<String> weatherConditions = Arrays.asList("晴天", "多云", "雨天"); |
||||
|
String randomWeather = weatherConditions.get(new Random().nextInt(weatherConditions.size())); |
||||
|
|
||||
|
return location + "今天是" + randomWeather + "。"; |
||||
|
} catch (Exception e) { |
||||
|
// 异常处理,确保程序健壮性。 |
||||
|
return "无法解析地点参数。"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
try { |
||||
|
// 第二步:向模型描述(注册)我们的工具。 |
||||
|
String weatherParamsSchema = |
||||
|
"{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"城市或县区,比如北京市、杭州市、余杭区等。\"}},\"required\":[\"location\"]}"; |
||||
|
|
||||
|
FunctionDefinition weatherFunction = FunctionDefinition.builder() |
||||
|
.name("get_current_weather") // 工具的唯一标识名,必须与本地实现对应。 |
||||
|
.description("当你想查询指定城市的天气时非常有用。") // 清晰的描述能帮助模型更好地决定何时使用该工具。 |
||||
|
.parameters(JsonUtils.parseString(weatherParamsSchema).getAsJsonObject()) |
||||
|
.build(); |
||||
|
Generation gen = new Generation(); |
||||
|
String userInput = "北京天气咋样"; |
||||
|
|
||||
|
List<Message> messages = new ArrayList<>(); |
||||
|
messages.add(Message.builder().role(Role.USER.getValue()).content(userInput).build()); |
||||
|
|
||||
|
// 第四步:首次调用模型。将用户的请求和我们定义好的工具列表一同发送给模型。 |
||||
|
GenerationParam param = GenerationParam.builder() |
||||
|
.model("qwen-plus") // 指定需要调用的模型。 |
||||
|
.apiKey("sk-5bf09a590daf408cb1126c2c6684e982") // 从环境变量中获取API Key。 |
||||
|
.messages(messages) // 传入当前的对话历史。 |
||||
|
.tools(Arrays.asList(ToolFunction.builder().function(weatherFunction).build())) // 传入可用的工具列表。 |
||||
|
.resultFormat(GenerationParam.ResultFormat.MESSAGE) |
||||
|
.build(); |
||||
|
|
||||
|
GenerationResult result = gen.call(param); |
||||
|
Message assistantOutput = result.getOutput().getChoices().get(0).getMessage(); |
||||
|
messages.add(assistantOutput); // 将模型的首次回复也加入到对话历史中。 |
||||
|
|
||||
|
// 第五步:检查模型的回复,判断它是否请求调用工具。 |
||||
|
if (assistantOutput.getToolCalls() == null || assistantOutput.getToolCalls().isEmpty()) { |
||||
|
// 情况A:模型没有调用工具,而是直接给出了回答。 |
||||
|
System.out.println("无需调用天气查询工具,直接回复:" + assistantOutput.getContent()); |
||||
|
} else { |
||||
|
// 情况B:模型决定调用工具。 |
||||
|
// 使用 while 循环可以处理模型连续调用多次工具的场景。 |
||||
|
while (assistantOutput.getToolCalls() != null && !assistantOutput.getToolCalls().isEmpty()) { |
||||
|
ToolCallBase toolCall = assistantOutput.getToolCalls().get(0); |
||||
|
|
||||
|
// 从模型的回复中解析出工具调用的具体信息(要调用的函数名、参数)。 |
||||
|
ToolCallFunction functionCall = (ToolCallFunction) toolCall; |
||||
|
String funcName = functionCall.getFunction().getName(); |
||||
|
String arguments = functionCall.getFunction().getArguments(); |
||||
|
System.out.println("正在调用工具 [" + funcName + "],参数:" + arguments); |
||||
|
|
||||
|
// 根据工具名,在本地执行对应的Java方法。 |
||||
|
String toolResult = getCurrentWeather(arguments); |
||||
|
|
||||
|
// 构造一个 role 为 "tool" 的消息,其中包含工具的执行结果。 |
||||
|
Message toolMessage = Message.builder() |
||||
|
.role("tool") |
||||
|
.toolCallId(toolCall.getId()) |
||||
|
.content(toolResult) |
||||
|
.build(); |
||||
|
System.out.println("工具返回:" + toolMessage.getContent()); |
||||
|
messages.add(toolMessage); // 将工具的返回结果也加入到对话历史中。 |
||||
|
|
||||
|
// 第六步:再次调用模型。 |
||||
|
param.setMessages(messages); |
||||
|
result = gen.call(param); |
||||
|
assistantOutput = result.getOutput().getChoices().get(0).getMessage(); |
||||
|
messages.add(assistantOutput); |
||||
|
} |
||||
|
|
||||
|
// 第七步:打印模型经过总结后,生成的最终回复。 |
||||
|
System.out.println("助手最终回复:" + assistantOutput.getContent()); |
||||
|
} |
||||
|
|
||||
|
} catch (NoApiKeyException | InputRequiredException e) { |
||||
|
System.err.println("错误: " + e.getMessage()); |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
package com.deepchart; |
||||
|
|
||||
|
import java.net.URI; |
||||
|
import java.net.http.HttpClient; |
||||
|
import java.net.http.HttpRequest; |
||||
|
import java.net.http.HttpResponse; |
||||
|
import java.net.URLEncoder; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
|
||||
|
public class StockApiDemo { |
||||
|
public static void main(String[] args) throws Exception { |
||||
|
String url = "https://www.stockapi.com.cn/v1/quota/macd2"; |
||||
|
// 构建参数 |
||||
|
String params = "code=" + URLEncoder.encode("002131", StandardCharsets.UTF_8) + |
||||
|
"&cycle=9&startDate=2025-10-10&endDate=2025-10-15&longCycle=26&shortCycle=12&calculationCycle=100"; |
||||
|
|
||||
|
HttpClient client = HttpClient.newHttpClient(); |
||||
|
HttpRequest request = HttpRequest.newBuilder() |
||||
|
.uri(URI.create(url + "?" + params)) |
||||
|
.build(); |
||||
|
|
||||
|
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) |
||||
|
.thenApply(HttpResponse::body) |
||||
|
.thenAccept(data -> System.out.println("日线行情数据:" + data)) |
||||
|
.join(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.deepchart.entity; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.time.LocalDate; |
||||
|
|
||||
|
/** |
||||
|
* KDJ指标计算结果实体类 |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class KDJData { |
||||
|
private LocalDate date; // 交易日日期 |
||||
|
private double rsv; // RSV |
||||
|
private double k; // K |
||||
|
private double d; // D |
||||
|
private double j; // J |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.KDJData; |
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.util.*; |
||||
|
|
||||
|
/** |
||||
|
* KDJ指标分析工具类 |
||||
|
*/ |
||||
|
public class KDJUtil { |
||||
|
|
||||
|
private static final int N = 9; // 默认周期 |
||||
|
private static final int M1 = 3; // 平滑周期 K |
||||
|
private static final int M2 = 3; // 平滑周期 D |
||||
|
|
||||
|
public static String calculateAndAnalyze(List<StockDailyData> dataList) { |
||||
|
if (dataList == null || dataList.size() < N) { |
||||
|
return "数据不足,无法计算KDJ指标。"; |
||||
|
} |
||||
|
|
||||
|
List<KDJData> kdjList = new ArrayList<>(); |
||||
|
Queue<Double> lowQueue = new LinkedList<>(); |
||||
|
Queue<Double> highQueue = new LinkedList<>(); |
||||
|
|
||||
|
for (int i = 0; i < dataList.size(); i++) { |
||||
|
StockDailyData today = dataList.get(i); |
||||
|
lowQueue.offer(today.getLowPrice()); |
||||
|
highQueue.offer(today.getHighPrice()); |
||||
|
|
||||
|
// 维持窗口大小为 N 天 |
||||
|
if (lowQueue.size() > N) { |
||||
|
lowQueue.poll(); |
||||
|
highQueue.poll(); |
||||
|
} |
||||
|
|
||||
|
// 不足N天跳过 |
||||
|
if (i < N - 1) continue; |
||||
|
|
||||
|
double lowestLow = Collections.min(lowQueue); |
||||
|
double highestHigh = Collections.max(highQueue); |
||||
|
|
||||
|
double rsv = ((today.getClosePrice() - lowestLow) / (highestHigh - lowestLow)) * 100; |
||||
|
|
||||
|
double k, d, j; |
||||
|
|
||||
|
if (kdjList.isEmpty()) { |
||||
|
k = rsv; |
||||
|
d = k; |
||||
|
} else { |
||||
|
KDJData prev = kdjList.get(kdjList.size() - 1); |
||||
|
k = (2.0 / 3) * prev.getK() + (1.0 / 3) * rsv; |
||||
|
d = (2.0 / 3) * prev.getD() + (1.0 / 3) * k; |
||||
|
} |
||||
|
|
||||
|
j = 3 * k - 2 * d; |
||||
|
|
||||
|
kdjList.add(new KDJData(today.getDate(), rsv, k, d, j)); |
||||
|
} |
||||
|
|
||||
|
return generateReport(kdjList); |
||||
|
} |
||||
|
|
||||
|
private static String generateReport(List<KDJData> kdjList) { |
||||
|
StringBuilder report = new StringBuilder("📈【KDJ技术分析报告】\n"); |
||||
|
|
||||
|
if (kdjList.isEmpty()) { |
||||
|
return report.append("❌ 数据不完整,无法提供有效分析。\n").toString(); |
||||
|
} |
||||
|
|
||||
|
KDJData latest = kdjList.get(kdjList.size() - 1); |
||||
|
|
||||
|
// 超买/超卖判断 |
||||
|
if (latest.getK() > 80 || latest.getD() > 80 || latest.getJ() > 100) { |
||||
|
report.append("🔴 当前处于超买区域,请注意回调风险!\n"); |
||||
|
} else if (latest.getK() < 20 || latest.getD() < 20 || latest.getJ() < 0) { |
||||
|
report.append("🟢 当前处于超卖区域,存在反弹机会!\n"); |
||||
|
} else { |
||||
|
report.append("🟡 当前KDJ指标运行平稳,无明显买卖信号。\n"); |
||||
|
} |
||||
|
|
||||
|
// 交叉信号检测(简单实现) |
||||
|
if (kdjList.size() >= 2) { |
||||
|
KDJData prev = kdjList.get(kdjList.size() - 2); |
||||
|
if (prev.getK() <= prev.getD() && latest.getK() > latest.getD()) { |
||||
|
report.append("✨ 发生金叉,建议关注买入时机(尤其在低位)。\n"); |
||||
|
} |
||||
|
if (prev.getK() >= prev.getD() && latest.getK() < latest.getD()) { |
||||
|
report.append("🔻 发生死叉,建议控制仓位或考虑卖出(尤其在高位)。\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// J 线强化提示 |
||||
|
if (latest.getJ() > 100) { |
||||
|
report.append("⚠️ J值超过100,进入极端超买状态,短期可能剧烈回调。\n"); |
||||
|
} else if (latest.getJ() < 0) { |
||||
|
report.append("⚠️ J值低于0,进入极端超卖状态,短期可能强势反弹。\n"); |
||||
|
} |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.KDJData; |
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.util.*; |
||||
|
|
||||
|
/** |
||||
|
* KD指标分析工具类 |
||||
|
*/ |
||||
|
public class KDUtil { |
||||
|
|
||||
|
private static final int N = 9; // 默认周期长度 |
||||
|
private static final double SMOOTH_K = 1.0 / 3; // K 平滑系数 |
||||
|
private static final double SMOOTH_D = 1.0 / 3; // D 平滑系数 |
||||
|
|
||||
|
private static List<KDJData> calculateKD(List<StockDailyData> dataList) { |
||||
|
if (dataList == null || dataList.size() < N) { |
||||
|
throw new IllegalArgumentException("数据不足,至少需要" + N + "条记录"); |
||||
|
} |
||||
|
|
||||
|
List<KDJData> kdList = new ArrayList<>(); |
||||
|
Deque<Double> highs = new ArrayDeque<>(N); |
||||
|
Deque<Double> lows = new ArrayDeque<>(N); |
||||
|
|
||||
|
Double prevK = null; |
||||
|
Double prevD = null; |
||||
|
|
||||
|
for (int i = 0; i < dataList.size(); i++) { |
||||
|
StockDailyData data = dataList.get(i); |
||||
|
highs.offerLast(data.getHighPrice()); |
||||
|
lows.offerLast(data.getLowPrice()); |
||||
|
|
||||
|
if (highs.size() > N) { |
||||
|
highs.pollFirst(); |
||||
|
lows.pollFirst(); |
||||
|
} |
||||
|
|
||||
|
if (i >= N - 1) { |
||||
|
double hhv = Collections.max(highs); |
||||
|
double llv = Collections.min(lows); |
||||
|
double rsv = ((data.getClosePrice() - llv) / (hhv - llv)) * 100; |
||||
|
|
||||
|
double kValue = (prevK == null ? rsv : prevK * (1 - SMOOTH_K) + rsv * SMOOTH_K); |
||||
|
double dValue = (prevD == null ? kValue : prevD * (1 - SMOOTH_D) + kValue * SMOOTH_D); |
||||
|
|
||||
|
kdList.add(new KDJData(data.getDate(), rsv, kValue, dValue, 0)); |
||||
|
prevK = kValue; |
||||
|
prevD = dValue; |
||||
|
} else { |
||||
|
kdList.add(new KDJData(data.getDate(), 0, 0, 0, 0)); // 占位符 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return kdList; |
||||
|
} |
||||
|
|
||||
|
public static String generateReport(List<StockDailyData> stockDataList) { |
||||
|
List<KDJData> kdList = calculateKD(stockDataList); |
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
|
||||
|
report.append("📈 股票KD指标分析报告\n"); |
||||
|
report.append("=======================\n"); |
||||
|
|
||||
|
if (kdList.isEmpty()) { |
||||
|
report.append("❌ 数据不足,无法生成分析。\n"); |
||||
|
return report.toString(); |
||||
|
} |
||||
|
|
||||
|
KDJData latest = kdList.get(kdList.size() - 1); |
||||
|
KDJData previous = kdList.size() >= 2 ? kdList.get(kdList.size() - 2) : null; |
||||
|
|
||||
|
double k = latest.getK(); |
||||
|
double d = latest.getD(); |
||||
|
|
||||
|
// 判断超买或超卖 |
||||
|
if (k > 80 && d > 80) { |
||||
|
report.append("🔴 当前处于【超买】状态,注意回调风险。\n"); |
||||
|
} else if (k < 20 && d < 20) { |
||||
|
report.append("🟢 当前处于【超卖】状态,关注反弹机会。\n"); |
||||
|
} else { |
||||
|
report.append("🟡 当前处于中性区域,暂无明显买卖信号。\n"); |
||||
|
} |
||||
|
|
||||
|
// 判断金叉/死叉 |
||||
|
if (previous != null) { |
||||
|
boolean isGoldenCross = previous.getK() <= previous.getD() && k > d; |
||||
|
boolean isDeathCross = previous.getK() >= previous.getD() && k < d; |
||||
|
|
||||
|
if (isGoldenCross && d < 30) { |
||||
|
report.append("✨ 出现金叉(K线上穿D线),特别是在低位,是潜在买入信号。\n"); |
||||
|
} else if (isDeathCross && d > 70) { |
||||
|
report.append("⚠️ 出现死叉(K线下穿D线),尤其是在高位,建议谨慎卖出。\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 多空趋势判断 |
||||
|
if (k > 50 && d > 50 && k > d) { |
||||
|
report.append("⬆️ 当前为多头趋势,可考虑持股观望。\n"); |
||||
|
} else if (k < 50 && d < 50 && k < d) { |
||||
|
report.append("⬇️ 当前为空头趋势,宜控制仓位。\n"); |
||||
|
} else { |
||||
|
report.append("🔄 当前为震荡行情,短线操作更适宜。\n"); |
||||
|
} |
||||
|
|
||||
|
return report.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
package com.deepchart.utils; |
||||
|
|
||||
|
import com.deepchart.entity.StockDailyData; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* RSI指标技术分析工具类 |
||||
|
*/ |
||||
|
public class RSIUtil { |
||||
|
|
||||
|
public static Double calculateRSI(List<StockDailyData> dataList, int period) { |
||||
|
if (dataList == null || dataList.size() <= period) { |
||||
|
throw new IllegalArgumentException("数据不足,无法计算" + period + "日RSI"); |
||||
|
} |
||||
|
|
||||
|
List<Double> ups = new ArrayList<>(); |
||||
|
List<Double> downs = new ArrayList<>(); |
||||
|
|
||||
|
for (int i = 1; i <= period; i++) { |
||||
|
StockDailyData today = dataList.get(dataList.size() - period + i - 1); |
||||
|
StockDailyData yesterday = dataList.get(dataList.size() - period + i - 2); |
||||
|
|
||||
|
double change = today.getClosePrice() - yesterday.getClosePrice(); |
||||
|
|
||||
|
if (change > 0) { |
||||
|
ups.add(change); |
||||
|
downs.add(0.0); |
||||
|
} else { |
||||
|
ups.add(0.0); |
||||
|
downs.add(-change); // 取正值 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
double avgUp = ups.stream().mapToDouble(Double::doubleValue).average().orElse(0); |
||||
|
double avgDown = downs.stream().mapToDouble(Double::doubleValue).average().orElse(0); |
||||
|
|
||||
|
if (avgDown == 0) { |
||||
|
return 100.0; |
||||
|
} |
||||
|
|
||||
|
return 100 - (100 / (1 + avgUp / avgDown)); |
||||
|
} |
||||
|
|
||||
|
public static String generateReport(List<StockDailyData> dataList, int period) { |
||||
|
try { |
||||
|
Double rsiValue = calculateRSI(dataList, period); |
||||
|
StringBuilder report = new StringBuilder(); |
||||
|
report.append("📊 【RSI技术分析报告】\n"); |
||||
|
report.append(String.format("📈 当前 %d 日 RSI 值为:%.2f\n", period, rsiValue)); |
||||
|
|
||||
|
if (rsiValue >= 80) { |
||||
|
report.append("🔴 状态:严重超买!警惕高位回调风险。\n"); |
||||
|
} else if (rsiValue >= 70) { |
||||
|
report.append("🟠 状态:轻度超买,短期有回调压力。\n"); |
||||
|
} else if (rsiValue <= 20) { |
||||
|
report.append("🟢 状态:严重超卖!关注反弹机会。\n"); |
||||
|
} else if (rsiValue <= 30) { |
||||
|
report.append("🟡 状态:轻度超卖,短期内可能反弹。\n"); |
||||
|
} else { |
||||
|
report.append("🔵 状态:处于中性区间,震荡行情延续。\n"); |
||||
|
} |
||||
|
|
||||
|
if (rsiValue > 50) { |
||||
|
report.append("📈 多方力量较强,适合逢低介入。\n"); |
||||
|
} else { |
||||
|
report.append("📉 空方占优,应谨慎操作或减仓观望。\n"); |
||||
|
} |
||||
|
|
||||
|
// 判断是否出现背离信号(简单比较最近两日收盘价与RSI变化) |
||||
|
if (dataList.size() >= period + 2) { |
||||
|
StockDailyData lastDay = dataList.get(dataList.size() - 1); |
||||
|
StockDailyData prevDay = dataList.get(dataList.size() - 2); |
||||
|
Double rsiLast = calculateRSI(dataList.subList(0, dataList.size()), period); |
||||
|
Double rsiPrev = calculateRSI(dataList.subList(0, dataList.size() - 1), period); |
||||
|
|
||||
|
if (lastDay.getClosePrice() > prevDay.getClosePrice() && rsiLast < rsiPrev) { |
||||
|
report.append("⚠️ 出现顶背离迹象,请注意上涨动能减弱风险。\n"); |
||||
|
} else if (lastDay.getClosePrice() < prevDay.getClosePrice() && rsiLast > rsiPrev) { |
||||
|
report.append("✅ 出现底背离迹象,下跌动能减弱,或迎来反弹。\n"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return report.toString(); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
return "❌ 分析失败:" + e.getMessage(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue