package com.yami.trading.huobi.rose;
|
|
import cn.hutool.core.thread.ThreadUtil;
|
import cn.hutool.http.HttpRequest;
|
import cn.hutool.http.HttpResponse;
|
import cn.hutool.http.HttpUtil;
|
import cn.hutool.json.JSON;
|
import cn.hutool.json.JSONArray;
|
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONUtil;
|
import lombok.Builder;
|
import lombok.Data;
|
import lombok.extern.slf4j.Slf4j;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.TimeUnit;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
import java.util.Optional;
|
|
/**
|
* @Author: TG:哪吒出海
|
* @Date: 2025-05-02-5:18
|
* @Description: 东方财富工具类
|
*/
|
@Slf4j
|
public class EastmoneyApi {
|
|
// 基础URL
|
private static final String BASE_URL = "https://push2.eastmoney.com/api/qt/clist/get";
|
|
// 市场代码映射
|
private static final String MARKET_ETF = "b:MK0021,b:MK0022,b:MK0023,b:MK0024,b:MK0827";
|
private static final String MARKET_HK = "b:MK0102,b:MK0104"; // 港股
|
private static final String MARKET_US = "b:MK0201,b:MK0202"; // 美股
|
|
// 默认配置
|
private static final int DEFAULT_TIMEOUT = 5000;
|
private static final int MAX_RETRY = 3;
|
|
@Data
|
@Builder
|
public static class QuoteConfig {
|
private String market; // 市场类型:ETF、HK、US
|
private int pageSize; // 每页条数
|
private int pageNumber; // 页码
|
private boolean useProxy; // 是否使用代理
|
private String proxyHost;
|
private int proxyPort;
|
private int timeout; // 超时时间(毫秒)
|
|
public static QuoteConfig defaultConfig() {
|
return QuoteConfig.builder()
|
.market("ETF")
|
.pageSize(20)
|
.pageNumber(1)
|
.useProxy(false)
|
.timeout(DEFAULT_TIMEOUT)
|
.build();
|
}
|
}
|
|
/**
|
* 异步获取行情数据
|
*
|
* @param config 行情配置
|
* @return CompletableFuture<QuoteResponse>
|
*/
|
public static CompletableFuture<QuoteResponse> quotesAsync(QuoteConfig config) {
|
return CompletableFuture.supplyAsync(() -> {
|
return quotes(config);
|
}, ThreadUtil.newExecutor());
|
}
|
|
/**
|
* 获取行情数据
|
*
|
* @param config 行情配置
|
* @return QuoteResponse
|
*/
|
public static QuoteResponse quotes(QuoteConfig config) {
|
String marketCode = getMarketCode(config.getMarket());
|
String url = buildUrl(marketCode, config.getPageSize(), config.getPageNumber());
|
|
int retryCount = 0;
|
while (retryCount < MAX_RETRY) {
|
try {
|
String jsonData = fetchData(url, config);
|
return parseResponse(jsonData);
|
} catch (Exception e) {
|
log.error("请求东方财富接口失败,重试次数: {}, 错误: {}", retryCount + 1, e.getMessage());
|
retryCount++;
|
if (retryCount < MAX_RETRY) {
|
ThreadUtil.sleep(1000 * retryCount);
|
}
|
}
|
}
|
throw new RuntimeException("请求东方财富接口失败,已重试" + MAX_RETRY + "次");
|
}
|
|
private static String fetchData(String url, QuoteConfig config) {
|
log.info("正在请求URL:{}", url);
|
HttpRequest request = HttpUtil.createGet(url)
|
.timeout(config.getTimeout())
|
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
|
if (config.isUseProxy() && config.getProxyHost() != null) {
|
request.setHttpProxy(config.getProxyHost(), config.getProxyPort());
|
}
|
|
try (HttpResponse response = request.execute()) {
|
if (!response.isOk()) {
|
throw new RuntimeException("API请求失败,状态码:" + response.getStatus());
|
}
|
return extractJsonFromJsonp(response.body());
|
}
|
}
|
|
private static QuoteResponse parseResponse(String jsonData) {
|
JSONObject jsonObject = JSONUtil.parseObj(jsonData);
|
QuoteResponse quoteResponse = new QuoteResponse();
|
|
JSONObject dataObj = jsonObject.getJSONObject("data");
|
if (dataObj == null) {
|
return quoteResponse;
|
}
|
|
quoteResponse.setTotal(dataObj.getInt("total", 0));
|
JSONArray diffArray = dataObj.getJSONArray("diff");
|
if (diffArray == null) {
|
return quoteResponse;
|
}
|
|
List<QuoteResponse.QuoteItem> items = new ArrayList<>();
|
for (Object item : diffArray) {
|
items.add(parseQuoteItem((JSONObject) item));
|
}
|
quoteResponse.setItems(items);
|
return quoteResponse;
|
}
|
|
private static QuoteResponse.QuoteItem parseQuoteItem(JSONObject quote) {
|
QuoteResponse.QuoteItem item = new QuoteResponse.QuoteItem();
|
|
// 使用hutool的类型转换工具,简化空值处理
|
item.setCode(quote.getStr("f12", ""));
|
item.setName(quote.getStr("f14", ""));
|
item.setCurrentPrice(quote.getDouble("f2", 0.0) / 100);
|
item.setPriceChange(quote.getDouble("f4", 0.0) / 1000);
|
item.setPriceChangePercent(quote.getDouble("f3", 0.0) / 100);
|
item.setOpenPrice(quote.getDouble("f17", 0.0));
|
item.setHighPrice(quote.getDouble("f15", 0.0));
|
item.setLowPrice(quote.getDouble("f16", 0.0));
|
item.setPreClosePrice(quote.getDouble("f18", 0.0));
|
item.setTradeAmount(quote.getLong("f6", 0L));
|
item.setVolume(quote.getLong("f5", 0L));
|
|
return item;
|
}
|
|
private static String getMarketCode(String market) {
|
switch (market.toUpperCase()) {
|
case "ETF":
|
return MARKET_ETF;
|
case "HK":
|
return MARKET_HK;
|
case "US":
|
return MARKET_US;
|
default:
|
throw new IllegalArgumentException("不支持的市场类型: " + market);
|
}
|
}
|
|
private static String buildUrl(String marketCode, int pageSize, int pageNumber) {
|
StringBuilder fields = new StringBuilder();
|
//
|
fields.append("f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f17,f18,f15,f16");
|
return String.format(
|
"%s?np=1&fltt=1&invt=2&fs=%s&fields=%s&fid=f2&pn=%d&pz=%d&po=1&dect=1",
|
BASE_URL, marketCode, fields, pageNumber, pageSize);
|
}
|
|
/**
|
* 从JSONP响应中提取JSON数据
|
*/
|
private static String extractJsonFromJsonp(String jsonp) {
|
Pattern pattern = Pattern.compile("jQuery[0-9_]+\\((.*)\\);?");
|
Matcher matcher = pattern.matcher(jsonp);
|
if (matcher.find()) {
|
return matcher.group(1);
|
}
|
return jsonp; // 如果不是JSONP格式,直接返回
|
}
|
}
|