| | |
| | | import com.google.common.collect.Lists; |
| | | import com.yami.trading.bean.cms.Infomation; |
| | | import com.yami.trading.bean.data.domain.*; |
| | | import com.yami.trading.bean.item.domain.Item; |
| | | import com.yami.trading.common.util.RedisUtil; |
| | | import com.yami.trading.huobi.data.DataCache; |
| | | import com.yami.trading.huobi.data.internal.DepthTimeObject; |
| | |
| | | import com.yami.trading.huobi.hobi.http.HttpMethodType; |
| | | import com.yami.trading.service.cms.InfomationService; |
| | | import com.yami.trading.service.item.ItemService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.apache.http.NameValuePair; |
| | | import org.apache.http.client.utils.URLEncodedUtils; |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.*; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | |
| | | * 完成需求k线图采集 |
| | | */ |
| | | @Component |
| | | @Slf4j |
| | | public class XueQiuDataServiceImpl { |
| | | // https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol=TSLA&begin=1682695800000&period=120m&type=before&count=-500&indicator=kline"; |
| | | public final static String klineUrl = "https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={}&begin={}&period={}&type=before&count=-500&indicator=kline"; |
| | |
| | | /** |
| | | * live |
| | | */ |
| | | public final static String live = "https://stock.xueqiu.com/v5/stock/quote.json"; |
| | | public final static String live = "https://www.tsanghi.com/api/fin/"; |
| | | public final static String markets = "https://stock.xueqiu.com/v5/stock/quote.json"; |
| | | |
| | | public final static String pankou = "https://stock.xueqiu.com/v5/stock/realtime/pankou.json"; |
| | |
| | | } |
| | | |
| | | public List<Realtime> realtimeSingle(String symbols) { |
| | | List<Realtime> list = new ArrayList<Realtime>(); |
| | | List<Realtime> list = new ArrayList<>(); |
| | | try { |
| | | List<String> strings = Arrays.asList(symbols.split(",")); |
| | | String cookie = HttpHelper.getCookie("https://xueqiu.com/"); |
| | | for (String symbol: strings) { |
| | | String result = HttpHelper.sendGetHttp(live, "symbol=" + symbol, cookie); |
| | | JSONObject resultJson = JSON.parseObject(result); |
| | | String code = resultJson.getString("error_code"); |
| | | if ("0".equals(code)) { |
| | | JSONObject jsonObject = resultJson.getJSONObject("data").getJSONObject("quote"); |
| | | Realtime realtime = new Realtime(); |
| | | String currency; |
| | | currency = symbol; |
| | | int decimal = itemService.getDecimal(currency); |
| | | realtime.setSymbol(currency); |
| | | realtime.setName(currency); |
| | | Long timestamp = jsonObject.getLong("timestamp"); |
| | | if (timestamp.toString().length() > 13) { |
| | | timestamp = timestamp / 1000; |
| | | List<String> symbolList = Arrays.asList(symbols.split(",")); |
| | | |
| | | for (String symbol : symbolList) { |
| | | try { |
| | | Item item = itemService.findBySymbol(symbol); |
| | | if (item == null) { |
| | | log.warn("未找到对应的item: {}", symbol); |
| | | continue; |
| | | } |
| | | realtime.setTs(timestamp); |
| | | realtime.setOpen(jsonObject.getBigDecimal("open").setScale(decimal, RoundingMode.HALF_UP)); |
| | | realtime.setClose(jsonObject.getBigDecimal("current").setScale(decimal, RoundingMode.HALF_UP)); |
| | | realtime.setHigh(jsonObject.getBigDecimal("high").setScale(decimal, RoundingMode.HALF_UP)); |
| | | realtime.setLow(jsonObject.getBigDecimal("low").setScale(decimal, RoundingMode.HALF_UP)); |
| | | realtime.setMarketCapital(jsonObject.getLong("market_capital")); |
| | | realtime.setFloatMarketCapital(jsonObject.getLong("float_market_capital")); |
| | | realtime.setPeForecast(jsonObject.getBigDecimal("pe_forecast")); |
| | | realtime.setVolumeRatio(jsonObject.getBigDecimal("volume_ratio")); |
| | | realtime.setTurnoverRate(jsonObject.getBigDecimal("turnover_rate")); |
| | | BigDecimal amount = jsonObject.getBigDecimal("amount"); |
| | | if (amount == null) { |
| | | amount = BigDecimal.ZERO; |
| | | |
| | | String type = ""; |
| | | String exchange = ""; |
| | | |
| | | // 根据类型确定API路径和交易所 |
| | | if ("US-stocks".equals(item.getType())) { |
| | | type = "stock"; |
| | | exchange = "XNAS"; // 美股纳斯达克 |
| | | } else if ("indices".equals(item.getType()) || "ETF".equals(item.getType())) { |
| | | type = "etf"; |
| | | exchange = "XNAS"; // ETF也在纳斯达克 |
| | | } else { |
| | | // 其他类型,默认为股票和上交所 |
| | | type = "stock"; |
| | | exchange = "XSHG"; |
| | | } |
| | | realtime.setAmount(amount.setScale(decimal, RoundingMode.HALF_UP)); |
| | | BigDecimal volume = jsonObject.getBigDecimal("volume"); |
| | | if (volume == null) { |
| | | volume = BigDecimal.ZERO; |
| | | |
| | | // 构建API URL |
| | | String url = String.format("https://www.tsanghi.com/api/fin/%s/%s/realtime?token=9668db3503214cd19a831a9f866923b9&ticker=%s", |
| | | type, exchange, symbol); |
| | | |
| | | String result = HttpHelper.sendGetHttp(url, null, null); |
| | | JSONObject resultJson = JSON.parseObject(result); |
| | | |
| | | String code = resultJson.getString("code"); |
| | | if ("200".equals(code)) { |
| | | JSONArray dataArray = resultJson.getJSONArray("data"); |
| | | |
| | | // 检查数据是否为空 |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("股票 {} 的实时数据为空", symbol); |
| | | continue; |
| | | } |
| | | |
| | | // 取第一个数据对象 |
| | | JSONObject dataObject = dataArray.getJSONObject(0); |
| | | |
| | | Realtime realtime = new Realtime(); |
| | | int decimal = itemService.getDecimal(symbol); |
| | | |
| | | realtime.setSymbol(symbol); |
| | | realtime.setName(item.getName() != null ? item.getName() : symbol); |
| | | |
| | | // 处理时间戳 |
| | | String dateStr = dataObject.getString("date"); |
| | | long timestamp = parseDateTimeToTimestamp(dateStr); |
| | | if (Long.toString(timestamp).length() > 13) { |
| | | timestamp = timestamp / 1000; |
| | | } |
| | | realtime.setTs(timestamp); |
| | | |
| | | // 设置价格数据 |
| | | realtime.setOpen(getBigDecimalValue(dataObject, "open", decimal)); |
| | | realtime.setClose(getBigDecimalValue(dataObject, "close", decimal)); |
| | | realtime.setHigh(getBigDecimalValue(dataObject, "high", decimal)); |
| | | realtime.setLow(getBigDecimalValue(dataObject, "low", decimal)); |
| | | |
| | | // 设置成交量和成交额 |
| | | realtime.setVolume(getBigDecimalValue(dataObject, "volume", decimal)); |
| | | realtime.setAmount(getBigDecimalValue(dataObject, "amount", decimal)); |
| | | |
| | | // 设置昨收价 |
| | | BigDecimal preClose = getBigDecimalValue(dataObject, "pre_close", decimal); |
| | | if (preClose != null) { |
| | | // 如果Realtime类有preClose字段,取消注释 |
| | | // realtime.setPreClose(preClose); |
| | | } |
| | | |
| | | // 处理盘口数据(如果存在) |
| | | JSONArray buyPriceArray = dataObject.getJSONArray("buy_price"); |
| | | JSONArray sellPriceArray = dataObject.getJSONArray("sell_price"); |
| | | JSONArray buyVolumeArray = dataObject.getJSONArray("buy_volume"); |
| | | JSONArray sellVolumeArray = dataObject.getJSONArray("sell_volume"); |
| | | |
| | | if (buyPriceArray != null && !buyPriceArray.isEmpty()) { |
| | | realtime.setBid(getBigDecimalFromArray(buyPriceArray, 0, decimal)); |
| | | } |
| | | if (sellPriceArray != null && !sellPriceArray.isEmpty()) { |
| | | realtime.setAsk(getBigDecimalFromArray(sellPriceArray, 0, decimal)); |
| | | } |
| | | |
| | | list.add(realtime); |
| | | } else { |
| | | log.warn("API返回错误代码: {}, 股票: {}", code, symbol); |
| | | } |
| | | realtime.setVolume(volume.setScale(decimal, RoundingMode.HALF_UP)); |
| | | // realtime.setAsk(realtimeJson.getBigDecimal("ask").setScale(decimal, RoundingMode.HALF_UP)); |
| | | // realtime.setBid(realtimeJson.getBigDecimal("pb").setScale(decimal, RoundingMode.HALF_UP)); |
| | | list.add(realtime); |
| | | } catch (Exception e) { |
| | | log.error("处理股票 {} 时发生错误", symbol, e); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("error", e); |
| | | log.error("获取实时数据失败", e); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * 将日期时间字符串转换为时间戳 |
| | | * 格式: "yyyy-mm-dd hh:mm:ss" |
| | | */ |
| | | private long parseDateTimeToTimestamp(String dateTimeStr) { |
| | | try { |
| | | if (dateTimeStr == null || dateTimeStr.isEmpty()) { |
| | | return System.currentTimeMillis(); |
| | | } |
| | | |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | Date date = sdf.parse(dateTimeStr); |
| | | return date.getTime(); |
| | | } catch (Exception e) { |
| | | log.error("日期时间解析失败: {}", dateTimeStr, e); |
| | | return System.currentTimeMillis(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全获取BigDecimal值 |
| | | */ |
| | | private BigDecimal getBigDecimalValue(JSONObject jsonObject, String key, int decimal) { |
| | | try { |
| | | BigDecimal value = jsonObject.getBigDecimal(key); |
| | | if (value == null) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return value.setScale(decimal, RoundingMode.HALF_UP); |
| | | } catch (Exception e) { |
| | | log.warn("获取字段 {} 失败", key, e); |
| | | return BigDecimal.ZERO; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从JSONArray中获取BigDecimal值 |
| | | */ |
| | | private BigDecimal getBigDecimalFromArray(JSONArray jsonArray, int index, int decimal) { |
| | | try { |
| | | if (jsonArray == null || jsonArray.size() <= index) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | BigDecimal value = jsonArray.getBigDecimal(index); |
| | | if (value == null) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return value.setScale(decimal, RoundingMode.HALF_UP); |
| | | } catch (Exception e) { |
| | | log.warn("从数组获取数据失败", e); |
| | | return BigDecimal.ZERO; |
| | | } |
| | | } |
| | | |
| | | public List<Realtime> realtime(String symbols) { |
| | |
| | | map.put(Kline.PERIOD_1MON, buildOneMonthPeriod(symbol)); |
| | | map.put(Kline.PERIOD_1DAY, buildOneDayPeriod(symbol)); |
| | | map.put(Kline.PERIOD_5DAY, buildFiveDayPeriod(symbol)); |
| | | map.put(Kline.PERIOD_QUARTER, buildOneQuarterPeriod(symbol)); |
| | | // map.put(Kline.PERIOD_QUARTER, buildOneQuarterPeriod(symbol)); |
| | | map.put(Kline.PERIOD_YEAR, buildOneYearPeriod(symbol)); |
| | | |
| | | return map; |
| | | } |
| | | |
| | | public List<Kline> buildOneDayPeriod(String currency) { |
| | | return getTimeseriesByPeriod(currency, "day", Kline.PERIOD_1DAY, 365); |
| | | return getTimeseriesByPeriod(currency, "daily", Kline.PERIOD_1DAY, 365); |
| | | |
| | | } |
| | | |
| | | |
| | | public List<Kline> buildOneWeekPeriod(String currency) { |
| | | return getTimeseriesByPeriod(currency, "week", Kline.PERIOD_1WEEK, 365 * 5); |
| | | return getTimeseriesByPeriod(currency, "weekly", Kline.PERIOD_1WEEK, 365 * 5); |
| | | |
| | | } |
| | | |
| | | public List<Kline> buildOneMonthPeriod(String currency) { |
| | | return getTimeseriesByPeriod(currency, "month", Kline.PERIOD_1MON, 365 * 5); |
| | | return getTimeseriesByPeriod(currency, "monthly", Kline.PERIOD_1MON, 365 * 5); |
| | | } |
| | | |
| | | public List<Kline> buildOneQuarterPeriod(String currency) { |
| | |
| | | } |
| | | |
| | | public List<Kline> buildOneYearPeriod(String currency) { |
| | | return getTimeseriesByPeriod(currency, "year", Kline.PERIOD_YEAR, 365 * 100); |
| | | return getTimeseriesByPeriod(currency, "yearly", Kline.PERIOD_YEAR, 365 * 100); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | public List<Kline> getTimeseriesByPeriod(String currency, String periodXieQiu, String sysPeriod, long limitDays) { |
| | | List<Kline> resList = new ArrayList<>(); |
| | | long nowTs = System.currentTimeMillis(); |
| | | long startTs = System.currentTimeMillis() - limitDays * 24 * 60 * 60 * 1000; |
| | | long begin = nowTs; |
| | | String cookie = HttpHelper.getCookie("https://xueqiu.com/"); |
| | | |
| | | Set<Long> tsSet = new HashSet<>(); |
| | | while (begin > startTs) { |
| | | String url = StrUtil.format(klineUrl, currency, begin, periodXieQiu); |
| | | String json = HttpHelper.sendGetHttp(url, null, cookie); |
| | | try { |
| | | // 获取商品信息判断类型 |
| | | Item itemData = itemService.findBySymbol(currency); |
| | | String type = "stock"; // 默认股票 |
| | | String exchange = "XNAS"; // 默认纳斯达克 |
| | | |
| | | if (itemData != null) { |
| | | if ("ETF".equals(itemData.getType()) || "etf".equals(itemData.getType()) || |
| | | "indices".equals(itemData.getType()) || currency.startsWith("51") || |
| | | currency.startsWith("15") || currency.startsWith("16")) { |
| | | type = "etf"; |
| | | // ETF可能在不同的交易所,根据实际情况调整 |
| | | exchange = "XNAS"; // 或者可能是XSHG等其他交易所 |
| | | } else if ("US-stocks".equals(itemData.getType())) { |
| | | type = "stock"; |
| | | exchange = "XNAS"; |
| | | } else { |
| | | // 其他类型默认为A股股票 |
| | | type = "stock"; |
| | | exchange = "XSHG"; |
| | | } |
| | | } else { |
| | | // 如果没有找到item信息,根据symbol特征猜测类型 |
| | | if (currency.startsWith("51") || currency.startsWith("15") || currency.startsWith("16")) { |
| | | type = "etf"; |
| | | exchange = "XSHG"; // A股ETF |
| | | } else if (currency.matches("[A-Z]+")) { |
| | | type = "stock"; |
| | | exchange = "XNAS"; // 美股 |
| | | } else { |
| | | type = "stock"; |
| | | exchange = "XSHG"; // 默认A股 |
| | | } |
| | | } |
| | | |
| | | log.debug("获取K线数据,symbol: {}, 类型: {}, 交易所: {}, 周期: {}", |
| | | currency, type, exchange, periodXieQiu); |
| | | |
| | | // 构建API URL |
| | | String url = StrUtil.format("https://www.tsanghi.com/api/fin/{}/{}/{}?token=9668db3503214cd19a831a9f866923b9&ticker={}&order=2", |
| | | type, exchange, periodXieQiu, currency); |
| | | |
| | | // 发送HTTP请求 |
| | | String json = HttpHelper.sendGetHttp(url, null, null); |
| | | JSONObject resultJson = JSON.parseObject(json); |
| | | JSONArray dataArray = resultJson.getJSONObject("data").getJSONArray("item"); |
| | | List<List> lists = dataArray.toJavaList(List.class); |
| | | long minTS = begin; |
| | | for (List result : lists) { |
| | | |
| | | // 检查API响应是否成功 |
| | | if (resultJson == null || !resultJson.containsKey("data")) { |
| | | log.error("API响应数据格式异常: {}", json); |
| | | return resList; |
| | | } |
| | | |
| | | JSONArray dataArray = resultJson.getJSONArray("data"); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("未获取到K线数据"); |
| | | return resList; |
| | | } |
| | | |
| | | // 计算时间范围 |
| | | long endTime = System.currentTimeMillis(); |
| | | long startTime = endTime - limitDays * 24 * 60 * 60 * 1000L; |
| | | |
| | | Set<String> dateSet = new HashSet<>(); |
| | | |
| | | // 解析数据 |
| | | for (int i = 0; i < dataArray.size(); i++) { |
| | | JSONObject item = dataArray.getJSONObject(i); |
| | | |
| | | // 解析日期 |
| | | String dateStr = item.getString("date"); |
| | | if (dateSet.contains(dateStr)) { |
| | | continue; |
| | | } |
| | | dateSet.add(dateStr); |
| | | |
| | | // 将日期转换为时间戳 |
| | | long ts = parseDateToTimestamp(dateStr); |
| | | |
| | | // 检查时间范围 |
| | | if (ts < startTime) { |
| | | continue; |
| | | } |
| | | |
| | | Kline kline = new Kline(); |
| | | kline.setSymbol(currency); |
| | | kline.setPeriod(sysPeriod); |
| | | // 毫秒 |
| | | long ts = Long.parseLong(result.get(0).toString()); |
| | | if (Long.toString(ts).length() > 13) { |
| | | ts = ts / 1000; |
| | | } |
| | | if (tsSet.contains(ts)) { |
| | | continue; |
| | | } else { |
| | | tsSet.add(ts); |
| | | } |
| | | kline.setTs(ts); |
| | | kline.setOpen(new BigDecimal(result.get(2).toString())); |
| | | kline.setClose(new BigDecimal(result.get(5).toString())); |
| | | kline.setHigh(new BigDecimal(result.get(3).toString())); |
| | | kline.setLow(new BigDecimal(result.get(4).toString())); |
| | | kline.setVolume(new BigDecimal(result.get(1).toString())); |
| | | kline.setAmount(new BigDecimal(result.get(9).toString())); |
| | | kline.setOpen(item.getBigDecimal("open")); |
| | | kline.setClose(item.getBigDecimal("close")); |
| | | kline.setHigh(item.getBigDecimal("high")); |
| | | kline.setLow(item.getBigDecimal("low")); |
| | | kline.setVolume(item.getBigDecimal("volume")); |
| | | |
| | | // 如果有amount字段就设置,没有就设为0 |
| | | if (item.containsKey("amount")) { |
| | | kline.setAmount(item.getBigDecimal("amount")); |
| | | } else { |
| | | kline.setAmount(BigDecimal.ZERO); |
| | | } |
| | | |
| | | // 修复K线数据(如果有修复逻辑) |
| | | if (klineService != null) { |
| | | klineService.repairKline(kline); |
| | | } |
| | | |
| | | resList.add(kline); |
| | | if (ts < minTS) { |
| | | minTS = ts; |
| | | } |
| | | } |
| | | |
| | | // 按时间戳升序排序 |
| | | Collections.sort(resList); |
| | | |
| | | // 如果需要设置开盘价为前一根的收盘价 |
| | | int len = resList.size(); |
| | | for (int i = 1; i < len; i++) { |
| | | resList.get(i).setOpen(resList.get(i - 1).getClose()); |
| | | } |
| | | if (minTS == begin) { |
| | | break; |
| | | } |
| | | begin = minTS; |
| | | if (begin < startTs) { |
| | | break; |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | log.error("获取K线数据失败: {}", currency, e); |
| | | } |
| | | Collections.sort(resList); |
| | | int len = resList.size(); |
| | | for (int i = 1; i < len; i++) { |
| | | resList.get(i).setOpen(resList.get(i - 1).getClose()); |
| | | } |
| | | |
| | | return resList; |
| | | } |
| | | |
| | | /** |
| | | * 将日期字符串转换为时间戳 |
| | | * 格式: "yyyy-mm-dd" |
| | | */ |
| | | private long parseDateToTimestamp(String dateStr) { |
| | | try { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | Date date = sdf.parse(dateStr); |
| | | return date.getTime(); |
| | | } catch (Exception e) { |
| | | log.error("日期解析失败: {}", dateStr, e); |
| | | return System.currentTimeMillis(); |
| | | } |
| | | } |
| | | |
| | | public List<Kline> getTimeseriesByMinute(String currency, int minute, long limitDays) { |