Compare commits

...

8 Commits

Author SHA1 Message Date
vpckiet
5b16f1f2c9 update crawl 2024-10-21 16:14:22 +07:00
vpckiet
b8d4e0d3f2 Merge branch 'bug/update_kline_format' into 'develop'
update

See merge request india/india_market_java!46
2024-10-10 03:50:50 +00:00
vpckiet
c19277e390 update 2024-10-10 10:50:01 +07:00
vpckiet
514875878f Merge branch 'bug/update_kline_format' into 'develop'
fix bug kline

See merge request india/india_market_java!45
2024-10-10 01:47:17 +00:00
vpckiet
4257541342 fix bug kline 2024-10-09 17:18:26 +07:00
quangnguyen202
441adac15a Merge branch 'feature/optional-stock' into 'develop'
Add api for optional stock

See merge request india/india_market_java!44
2024-10-01 07:07:59 +00:00
quangnguyen202
ba29ab98c7 Add api for optional stock 2024-10-01 14:06:24 +07:00
Gavin g
94ec73874b Merge branch 'feature/kline-alternate-source' into 'develop'
Add alternate source

See merge request india/india_market_java!43
2024-09-26 09:57:06 +00:00
11 changed files with 388 additions and 23 deletions

View File

@@ -185,6 +185,12 @@
<version>30.1-jre</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
<build>

View File

@@ -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<OptionalStockPO> {
private static final long serialVersionUID = 1161631810L;
public static final QOptionalStockPO optionalStockPO = new QOptionalStockPO("optionalStockPO");
public final StringPath company = createString("company");
public final NumberPath<Integer> id = createNumber("id", Integer.class);
public final StringPath symbol = createString("symbol");
public QOptionalStockPO(String variable) {
super(OptionalStockPO.class, forVariable(variable));
}
public QOptionalStockPO(Path<? extends OptionalStockPO> path) {
super(path.getType(), path.getMetadata());
}
public QOptionalStockPO(PathMetadata metadata) {
super(OptionalStockPO.class, metadata);
}
}

View File

@@ -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<OptionalStock, OptionalStockPO> {
public static OptionalStockConvert of() {
return SpringUtils.getBean(OptionalStockConvert.class);
}
}

View File

@@ -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 {
}

View File

@@ -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<OptionalStock, OptionalStockPO, Integer> {
final OptionalStockRepo repo;
final OptionalStockConvert convert;
@Override
public GenericJpaRepository<OptionalStockPO, Integer> repo() {
return repo;
}
@Override
public IEntityPOConvert<OptionalStock, OptionalStockPO> convert() {
return convert;
}
}

View File

@@ -249,10 +249,17 @@ public class StockService {
String result = "";
try {
// 使用Jsoup连接到网页
Document doc = Jsoup.connect("https://www.business-standard.com/markets/news")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36")
.header("Referer", "https://www.business-standard.com/")
// Document doc = Jsoup.connect("https://www.business-standard.com/markets/news")
// .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36")
// .header("Referer", "https://www.business-standard.com/")
// .header("Accept-Language", "en-US,en;q=0.9")
// .get();
String url = "https://www.business-standard.com/markets/news";
Document doc = Jsoup.connect(url)
.referrer("https://www.business-standard.com/")
.header("Accept-Language", "en-US,en;q=0.9")
.userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1")
.timeout(5000) // timeout 5 seconds
.get();
// result = doc.html().substring(doc.html().indexOf("<div class=\"listingstyle_shortvideoimg__0TWuX shortvideoimg\">"),doc.html().lastIndexOf("<div class=\"listingstyle_shortvideoimg__0TWuX shortvideoimg\">")+500);
Elements divElements = doc.select("div.listingstyle_cardlistlist__dfq57");
@@ -272,10 +279,16 @@ public class StockService {
List<String> list = new ArrayList<>();
try {
// 使用Jsoup连接到网页
// Document doc = Jsoup.connect(url)
// .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36")
// .header("Referer", "https://www.business-standard.com/")
// .header("Accept-Language", "en-US,en;q=0.9")
// .get();
Document doc = Jsoup.connect(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36")
.header("Referer", "https://www.business-standard.com/")
.referrer("https://www.business-standard.com/")
.header("Accept-Language", "en-US,en;q=0.9")
.userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1")
.timeout(5000) // timeout 5 seconds
.get();
result = doc.html().substring(doc.html().indexOf("articleBody") + 14, doc.html().indexOf(",\"author\":") - 1);
list.add(result);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<OptionalStockPO, Integer> {
}

View File

@@ -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({
@@ -692,24 +720,24 @@ public class MoneyApiController {
int countback = 5;
if(StringUtils.equals("H",resolution)){
to = (long) (System.currentTimeMillis() / 1000);
from = to - (60 * 60);
countback = 60;
request.setResolution("1");
from = to - (10 * 60 * 60 );
countback = 328;
request.setResolution("60");
}else if(StringUtils.equals("D",resolution)){
to = (long) (System.currentTimeMillis() / 1000);
from = to - (24 * 60 * 60);
countback = 390;
request.setResolution("1");
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");
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 {
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<StockChartDto> 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<String, List<StockChartDto>> 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<Long> timestamps = new ArrayList<>();
List<Double> opens = new ArrayList<>();
List<Double> closes = new ArrayList<>();
List<Double> highs = new ArrayList<>();
List<Double> lows = new ArrayList<>();
List<Long> 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,11 +839,15 @@ 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 && !response.getS().equals("error")) {
setResponse(response, resolution);
// setResponse(response, resolution);
// API request successful, return the response
return ResponseEntity.ok(response);
} else {
@@ -768,6 +874,33 @@ public class MoneyApiController {
// 返回响应
}
@GetMapping({"/market/api/market/stock/optional", "/api/market/stock/optional"})
@ResponseBody
@EncryptFilter(decryptRequest = false)
public ResponseEntity<List<Object>> getOptionalStock() {
List<OptionalStock> optionalStocks = optionalStockRepository.findAll();
List<Object> 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;