diff --git a/pom.xml b/pom.xml index 870d991..bd70d8f 100644 --- a/pom.xml +++ b/pom.xml @@ -185,6 +185,12 @@ 30.1-jre + + org.apache.httpcomponents + httpclient + 4.5.13 + + diff --git a/src/main/generated/cn/stock/market/infrastructure/db/po/QOptionalStockPO.java b/src/main/generated/cn/stock/market/infrastructure/db/po/QOptionalStockPO.java new file mode 100644 index 0000000..7c210e9 --- /dev/null +++ b/src/main/generated/cn/stock/market/infrastructure/db/po/QOptionalStockPO.java @@ -0,0 +1,41 @@ +package cn.stock.market.infrastructure.db.po; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.Generated; +import com.querydsl.core.types.Path; + + +/** + * QOptionalStockPO is a Querydsl query type for OptionalStockPO + */ +@Generated("com.querydsl.codegen.EntitySerializer") +public class QOptionalStockPO extends EntityPathBase { + + private static final long serialVersionUID = 1161631810L; + + public static final QOptionalStockPO optionalStockPO = new QOptionalStockPO("optionalStockPO"); + + public final StringPath company = createString("company"); + + public final NumberPath id = createNumber("id", Integer.class); + + public final StringPath symbol = createString("symbol"); + + public QOptionalStockPO(String variable) { + super(OptionalStockPO.class, forVariable(variable)); + } + + public QOptionalStockPO(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QOptionalStockPO(PathMetadata metadata) { + super(OptionalStockPO.class, metadata); + } + +} + diff --git a/src/main/java/cn/stock/market/domain/basic/convert/OptionalStockConvert.java b/src/main/java/cn/stock/market/domain/basic/convert/OptionalStockConvert.java new file mode 100644 index 0000000..22445c8 --- /dev/null +++ b/src/main/java/cn/stock/market/domain/basic/convert/OptionalStockConvert.java @@ -0,0 +1,16 @@ +package cn.stock.market.domain.basic.convert; + +import cn.qutaojing.common.domain.convert.SimpleEntityPOConvert; +import cn.qutaojing.common.utils.SpringUtils; +import cn.stock.market.domain.basic.entity.OptionalStock; +import cn.stock.market.infrastructure.db.po.OptionalStockPO; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Component +@Lazy +public class OptionalStockConvert extends SimpleEntityPOConvert { + public static OptionalStockConvert of() { + return SpringUtils.getBean(OptionalStockConvert.class); + } +} diff --git a/src/main/java/cn/stock/market/domain/basic/entity/OptionalStock.java b/src/main/java/cn/stock/market/domain/basic/entity/OptionalStock.java new file mode 100644 index 0000000..6bb2483 --- /dev/null +++ b/src/main/java/cn/stock/market/domain/basic/entity/OptionalStock.java @@ -0,0 +1,16 @@ +package cn.stock.market.domain.basic.entity; + +import cn.stock.market.infrastructure.db.po.OptionalStockPO; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@NoArgsConstructor +@SuperBuilder +@EqualsAndHashCode( + callSuper = false +) +public class OptionalStock extends OptionalStockPO { +} diff --git a/src/main/java/cn/stock/market/domain/basic/repository/OptionalStockRepository.java b/src/main/java/cn/stock/market/domain/basic/repository/OptionalStockRepository.java new file mode 100644 index 0000000..5e315b4 --- /dev/null +++ b/src/main/java/cn/stock/market/domain/basic/repository/OptionalStockRepository.java @@ -0,0 +1,32 @@ +package cn.stock.market.domain.basic.repository; + +import cn.qutaojing.common.domain.convert.IEntityPOConvert; +import cn.qutaojing.common.domain.respostory.SimplePoConvertEntityRepository; +import cn.stock.market.domain.basic.convert.OptionalStockConvert; +import cn.stock.market.domain.basic.entity.OptionalStock; +import cn.stock.market.infrastructure.db.po.OptionalStockPO; +import cn.stock.market.infrastructure.db.repo.OptionalStockRepo; +import com.rp.spring.jpa.GenericJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor( + onConstructor = @__(@Autowired) +) +public class OptionalStockRepository extends SimplePoConvertEntityRepository { + final OptionalStockRepo repo; + + final OptionalStockConvert convert; + + @Override + public GenericJpaRepository repo() { + return repo; + } + + @Override + public IEntityPOConvert convert() { + return convert; + } +} diff --git a/src/main/java/cn/stock/market/dto/OptionalStockResponse.java b/src/main/java/cn/stock/market/dto/OptionalStockResponse.java new file mode 100644 index 0000000..0a5cd89 --- /dev/null +++ b/src/main/java/cn/stock/market/dto/OptionalStockResponse.java @@ -0,0 +1,18 @@ +package cn.stock.market.dto; + +import lombok.Data; + +@Data +public class OptionalStockResponse { + private String message; + private Integer code; + private DataResponse data = new DataResponse(); + + @Data + public static class DataResponse { + private String symbol; + private String company; + private Double pricecurrent; + private Float pricepercentchange; + } +} diff --git a/src/main/java/cn/stock/market/dto/query/StockChartDto.java b/src/main/java/cn/stock/market/dto/query/StockChartDto.java new file mode 100644 index 0000000..08f2263 --- /dev/null +++ b/src/main/java/cn/stock/market/dto/query/StockChartDto.java @@ -0,0 +1,48 @@ +package cn.stock.market.dto.query; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class StockChartDto { + private Long timestamp; + private double open; + private double high; + private double low; + private double close; + private double volume; + + public StockChartDto(Long date, double open, double high, double low, double close, double volume) { + this.timestamp = date; + this.open = open; + this.high = high; + this.low = low; + this.close = close; + this.volume = volume; + } + + public double getVolume() { + return volume; + } + + public Long getTimestamp() { + return timestamp; + } + + public double getOpen() { + return open; + } + + public double getHigh() { + return high; + } + + public double getLow() { + return low; + } + + public double getClose() { + return close; + } +} diff --git a/src/main/java/cn/stock/market/infrastructure/db/po/MoneyStockPO.java b/src/main/java/cn/stock/market/infrastructure/db/po/MoneyStockPO.java index 8e73f8e..959bdbe 100644 --- a/src/main/java/cn/stock/market/infrastructure/db/po/MoneyStockPO.java +++ b/src/main/java/cn/stock/market/infrastructure/db/po/MoneyStockPO.java @@ -60,6 +60,10 @@ public class MoneyStockPO { * NSE India的id */ String nseIndiaId; + /** + * NSE India Chart的id */ + String nseIndiaChartId; + /** * 自有self_url */ String selfUrl; diff --git a/src/main/java/cn/stock/market/infrastructure/db/po/OptionalStockPO.java b/src/main/java/cn/stock/market/infrastructure/db/po/OptionalStockPO.java new file mode 100644 index 0000000..6197e75 --- /dev/null +++ b/src/main/java/cn/stock/market/infrastructure/db/po/OptionalStockPO.java @@ -0,0 +1,35 @@ +package cn.stock.market.infrastructure.db.po; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +@SuperBuilder +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@DynamicInsert +@DynamicUpdate +@Table( + name = "optional_stock" +) +public class OptionalStockPO { + @Id + @GeneratedValue( + strategy = javax.persistence.GenerationType.IDENTITY + ) + Integer id; + + String symbol; + + String company; +} diff --git a/src/main/java/cn/stock/market/infrastructure/db/repo/OptionalStockRepo.java b/src/main/java/cn/stock/market/infrastructure/db/repo/OptionalStockRepo.java new file mode 100644 index 0000000..d61d258 --- /dev/null +++ b/src/main/java/cn/stock/market/infrastructure/db/repo/OptionalStockRepo.java @@ -0,0 +1,7 @@ +package cn.stock.market.infrastructure.db.repo; + +import cn.stock.market.infrastructure.db.po.OptionalStockPO; +import com.rp.spring.jpa.GenericJpaRepository; + +public interface OptionalStockRepo extends GenericJpaRepository { +} diff --git a/src/main/java/cn/stock/market/utils/NseIndiaRequest.java b/src/main/java/cn/stock/market/utils/NseIndiaRequest.java index 326eb18..9868600 100644 --- a/src/main/java/cn/stock/market/utils/NseIndiaRequest.java +++ b/src/main/java/cn/stock/market/utils/NseIndiaRequest.java @@ -1,17 +1,22 @@ package cn.stock.market.utils; +import cn.stock.market.dto.StockHistoryRequest; +import cn.stock.market.dto.StockHistoryResponse; import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; +import org.apache.commons.lang.StringUtils; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class NseIndiaRequest { private static final String NSE_INDIA_URL = "https://www.nseindia.com"; + private static final String NSE_INDIA_CHART_URL = "https://charting.nseindia.com"; private static final OkHttpClient client; + private static final ObjectMapper objectMapper = new ObjectMapper(); static { client = new OkHttpClient.Builder() @@ -45,8 +50,8 @@ public class NseIndiaRequest { return request; } - private static void initCookie() { - Request request = createRequest(NSE_INDIA_URL); + private static void initCookie(String url) { + Request request = createRequest(url); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("Failed to fetch initial cookies"); @@ -56,8 +61,35 @@ public class NseIndiaRequest { } } + private static Integer getCode(String symbol) { + Request request = createRequest(NSE_INDIA_CHART_URL + "//Charts/GetEQMasters").newBuilder() + .addHeader("referer", NSE_INDIA_CHART_URL) + .addHeader("origin", NSE_INDIA_CHART_URL) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get EQ code"); + } + + String result = response.body().string(); + + String regex = "(\\d+)\\|" + symbol + "\\|.*"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(result); + + if (matcher.find()) { + return Integer.valueOf(matcher.group(1)); + } + throw new IOException("No data found"); + + } catch (IOException e) { + throw new RuntimeException("Failed to get EQ code", e); + } + } + public static JSONObject stockByJYSFromHttp(String stockType, String symbol, String nseIndiaId) { - initCookie(); + initCookie(NSE_INDIA_URL); String url = NSE_INDIA_URL + "/api/quote-equity?symbol=" + nseIndiaId; Request request = createRequest(url).newBuilder() @@ -93,4 +125,57 @@ public class NseIndiaRequest { throw new RuntimeException("Failed to fetch data", e); } } + + public static StockHistoryResponse stockKLineFromHttp(StockHistoryRequest stockHistoryRequest, String resolution) { + initCookie(NSE_INDIA_CHART_URL); + + Integer code = getCode(stockHistoryRequest.getSymbol()); + + int interval = 1; + if (StringUtils.equals("H", resolution)) { + resolution = "I"; + interval = 60; + } + + Map body = new HashMap<>(); + body.put("chartPeriod", resolution); + body.put("chartStart", 0); + body.put("exch", "N"); + body.put("fromDate", 0); + body.put("instrType", "C"); + body.put("scripCode", code); + body.put("timeInterval", interval); + body.put("toDate", stockHistoryRequest.getTo() + 18000); + body.put("ulToken", code); + + String payload; + try { + payload = objectMapper.writeValueAsString(body); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize body", e); + } + + RequestBody requestBody = RequestBody.create( + MediaType.get("application/json; charset=utf-8"), + payload + ); + + Request request = createRequest(NSE_INDIA_CHART_URL + "//Charts/symbolhistoricaldata/").newBuilder() + .addHeader("referer", NSE_INDIA_CHART_URL) + .addHeader("origin", NSE_INDIA_CHART_URL) + .post(requestBody) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Request failed with code: " + response.code()); + } + + StockHistoryResponse result = objectMapper.readValue(response.body().string(), StockHistoryResponse.class); + + return result; + } catch (IOException e) { + throw new RuntimeException("Failed to fetch data", e); + } + } } diff --git a/src/main/java/cn/stock/market/web/MoneyApiController.java b/src/main/java/cn/stock/market/web/MoneyApiController.java index 399db83..adfc339 100644 --- a/src/main/java/cn/stock/market/web/MoneyApiController.java +++ b/src/main/java/cn/stock/market/web/MoneyApiController.java @@ -3,14 +3,21 @@ package cn.stock.market.web; import cn.hutool.core.date.DateUtil; import cn.stock.market.MoneyStockSuggestDTO; import cn.stock.market.domain.basic.entity.MoneyStock; +import cn.stock.market.domain.basic.entity.OptionalStock; import cn.stock.market.domain.basic.repository.MoneyStockRepository; +import cn.stock.market.domain.basic.repository.OptionalStockRepository; +import cn.stock.market.dto.OptionalStockResponse; import cn.stock.market.dto.StockHistoryRequest; import cn.stock.market.dto.StockHistoryResponse; +import cn.stock.market.dto.query.StockChartDto; import cn.stock.market.infrastructure.db.po.QMoneyStockPO; +import cn.stock.market.utils.HttpRequest; import cn.stock.market.utils.NseIndiaRequest; import cn.stock.market.utils.ServerResponse; import cn.stock.market.web.annotations.EncryptFilter; import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; @@ -23,6 +30,12 @@ import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -36,8 +49,16 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -58,7 +79,14 @@ public class MoneyApiController { @Autowired private MoneyStockRepository moneyStockRepository; + @Autowired + private OptionalStockRepository optionalStockRepository; + + @Autowired + private ObjectMapper objectMapper; + private static final String EXTERNAL_API_URL = "https://priceapi.moneycontrol.com/techCharts/indianMarket/stock/history"; + private static final String OPTIONAL_STOCK_MONEYCONTROL_URL = "https://priceapi.moneycontrol.com/pricefeed/notapplicable/inidicesindia/"; @ApiOperation(value = "股票详情信息", httpMethod = "GET") @ApiImplicitParams({ @@ -690,26 +718,26 @@ public class MoneyApiController { Long to = null; Long from = null; int countback = 5; - if (StringUtils.equals("H", resolution)) { - to = (long) (System.currentTimeMillis() / 1000); - from = to - (60 * 60); - countback = 60; - request.setResolution("1"); - } else if (StringUtils.equals("D", resolution)) { - to = (long) (System.currentTimeMillis() / 1000); - from = to - (24 * 60 * 60); - countback = 390; - request.setResolution("1"); + if(StringUtils.equals("H",resolution)){ + to = (long) (System.currentTimeMillis() / 1000); + from = to - (10 * 60 * 60 ); + countback = 328; + request.setResolution("60"); + }else if(StringUtils.equals("D",resolution)){ + to = (long) (System.currentTimeMillis() / 1000); + from = to - (2 * 30 * 24 * 60 * 60 ); + countback = 730; + request.setResolution("1D"); } else if (StringUtils.equals("W", resolution)) { to = (long) (System.currentTimeMillis() / 1000); from = to - (7 * 24 * 60 * 60); - countback = 471; - request.setResolution("5"); + countback = 730; + request.setResolution("1W"); } else if (StringUtils.equals("M", resolution)) { - to = (long) (System.currentTimeMillis() / 1000); - from = to - (35 * 24 * 60 * 60); - countback = 328; - request.setResolution("30"); + to = (long) (System.currentTimeMillis() / 1000); + from = to - (15 * 30 * 24 * 60 * 60); + countback = 730; + request.setResolution("1D"); } request.setFrom(from); @@ -724,7 +752,81 @@ public class MoneyApiController { while (response == null && retryCount < maxRetries) { try { - response = restTemplate.getForObject(apiUrl, StockHistoryResponse.class); + if (StringUtils.equals("M", resolution)) { + CloseableHttpClient client = HttpClients.createDefault(); + HttpGet req = new HttpGet(apiUrl); + HttpResponse resp = client.execute(req); + String jsonResponse = EntityUtils.toString(resp.getEntity(), "UTF-8"); + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(jsonResponse); + + JsonNode timeNode = rootNode.get("t"); + JsonNode lowNode = rootNode.get("l"); + JsonNode highNode = rootNode.get("h"); + JsonNode openNode = rootNode.get("o"); + JsonNode closeNode = rootNode.get("c"); + JsonNode volumeNote = rootNode.get("v"); + + List stocks = new ArrayList<>(); + for (int i = 0; i < timeNode.size(); i++) { + long timestamp = timeNode.get(i).asLong(); + double closePrice = closeNode.get(i).asDouble(); + double openPrice = openNode.get(i).asDouble(); + double volume = volumeNote.get(i).asDouble(); + double low = lowNode.get(i).asDouble(); + double high = highNode.get(i).asDouble(); + + stocks.add(new StockChartDto(timestamp, openPrice, high, low, closePrice, volume)); + } + + Map> groupedByMonth = stocks.stream() + .collect(Collectors.groupingBy( + sp -> Instant.ofEpochSecond(sp.getTimestamp()) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy-MM")), + LinkedHashMap::new, + Collectors.toList() + )); + + List timestamps = new ArrayList<>(); + List opens = new ArrayList<>(); + List closes = new ArrayList<>(); + List highs = new ArrayList<>(); + List lows = new ArrayList<>(); + List volumes = new ArrayList<>(); + + response = new StockHistoryResponse(); + groupedByMonth.forEach((month, prices) -> { + double open = prices.get(0).getOpen(); + double close = prices.get(prices.size() - 1).getClose(); + double high = prices.stream().mapToDouble(StockChartDto::getHigh).max().orElse(0); + double low = prices.stream().mapToDouble(StockChartDto::getLow).min().orElse(0); + double volume = prices.stream().mapToDouble(StockChartDto::getVolume).sum(); + + long timestamp = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")).atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() / 1000; + timestamps.add(timestamp); + opens.add(open); + closes.add(close); + highs.add(high); + lows.add(low); + volumes.add((long) volume); + + System.out.println("Month: " + month); + System.out.println("Open: " + open + ", Close: " + close + ", High: " + high + ", Low: " + low); + }); + + response.setS("ok"); + response.setT(timestamps); + response.setL(lows); + response.setH(highs); + response.setO(opens); + response.setC(closes); + response.setV(volumes); + + } else { + response = restTemplate.getForObject(apiUrl, StockHistoryResponse.class); + } } catch (RestClientException e) { // Log the exception or perform any other error handling log.error("Error while making API request. Retrying... (Retry count: {})", retryCount + 1); @@ -737,14 +839,34 @@ public class MoneyApiController { } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } + } catch (ClientProtocolException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); } } - if (response != null) { - setResponse(response, resolution); + if (response != null && !response.getS().equals("error")) { +// setResponse(response, resolution); // API request successful, return the response return ResponseEntity.ok(response); } else { + try { + MoneyStock moneyStock = moneyStockRepository.findOne((QMoneyStockPO.moneyStockPO.moneyScId.eq(symbol)) + .and(QMoneyStockPO.moneyStockPO.isLock.eq(0)) + .and(QMoneyStockPO.moneyStockPO.isShow.eq(0))) + .orElse(null); + + if (moneyStock != null && moneyStock.getNseIndiaChartId() != null && !moneyStock.getNseIndiaChartId().isEmpty()) { + request.setSymbol(moneyStock.getNseIndiaChartId()); + response = NseIndiaRequest.stockKLineFromHttp(request, resolution); + return ResponseEntity.ok(response); + } + } catch (Exception e) { + log.error("Failed to get data from nseindia.", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + // All retries failed, return an error response log.error("Failed to get a successful response after {} retries.", maxRetries); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); @@ -752,6 +874,33 @@ public class MoneyApiController { // 返回响应 } + @GetMapping({"/market/api/market/stock/optional", "/api/market/stock/optional"}) + @ResponseBody + @EncryptFilter(decryptRequest = false) + public ResponseEntity> getOptionalStock() { + List optionalStocks = optionalStockRepository.findAll(); + List data = optionalStocks.stream().map(stock -> { + try { + String responseStr = HttpRequest.doGrabGet(OPTIONAL_STOCK_MONEYCONTROL_URL + URLEncoder.encode(stock.getSymbol(), "UTF-8")); + OptionalStockResponse response = objectMapper.readValue(responseStr, OptionalStockResponse.class); + if (response != null && response.getCode().equals(200)) { + return response.getData(); + } + } + catch (Exception e) { + log.error("Failed to get optional stock from moneycontrol for " + stock.getSymbol(), e); + } + + OptionalStockResponse response = new OptionalStockResponse(); + response.getData().setSymbol(stock.getSymbol()); + response.getData().setSymbol(stock.getCompany()); + + return response.getData(); + }).collect(Collectors.toList()); + + return ResponseEntity.ok(data); + } + private void setResponse(StockHistoryResponse response, String resolution) { if (!"ok".equals(response.getS())) { return;