개별 투자자들의 투자 트랜드를 분석하여 "최근 개인이 많이 매수한 종목, 매매동향, 관심종목, 관련 뉴스 등"을 보여주는 서비스
한국 거래소 - 투자자별 매매동향 , 네이버 금융 크롤링
Spring Boot를 사용한 RESTful API 형태로 개발 진행
vscode 에서 프로젝트 열고 src/main/resources/application.properties 수정
# MySQL 데이터베이스 설정
spring.datasource.url=jdbc:mysql://localhost:3306/stock_trend?useSSL=false&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
mysql 접속 후
CREATE DATABASE stock_trend;
터미널에
.\mvnw.cmd spring-boot:run
<Controller/TrendContrller.java>
package com.example.personal_investor_trend;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/trend")
public class TrendController {
@GetMapping("/top-buyers")
public List<Map<String, String>> getTopBuyers() {
return List.of(
Map.of("ticker", "005930", "name", "삼성전자", "net_buy", "150억"),
Map.of("ticker", "000660", "name", "SK하이닉스", "net_buy", "120억")
);
}
@GetMapping("/comparison")
public Map<String, String> getComparison() {
return Map.of(
"개인 투자자", "삼성전자 +150억",
"기관 투자자", "삼성전자 -80억",
"외국인 투자자", "삼성전자 -70억"
);
}
}
http://openapi.krx.co.kr/contents/OPP/MAIN/main/index.cmd 에서 인증키 받고 원하는 데이터 api 신청
<KrxService.java>
package com.example.personal_investor_trend.service;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.Map;
@Service
public class KrxService {
private final RestTemplate restTemplate = new RestTemplate();
private static final String API_URL = "http://data-dbg.krx.co.kr/svc/apis/sto/ksq_bydd_trd"; // KRX OpenAPI URL
public String getKrxData(String basDd) {
//요청 데이터(JSON)
Map<String, String> requestBody = Collections.singletonMap("basDd", basDd);
//HTTP 헤더
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer API인증키");
//HTTP 요청 객체
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
//API 호출
ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
}
}
<TrendController.java>
package com.example.personal_investor_trend.controller;
import org.springframework.web.bind.annotation.*;
import com.example.personal_investor_trend.service.KrxService;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/trend")
public class TrendController {
private final KrxService krxService;
public TrendController(KrxService krxService){
this.krxService = krxService;
}
@PostMapping("/krx")
public String getKrxData(@RequestBody Map<String, String> request) {
return krxService.getKrxData(request.get("basDd"));
}
}
결과 확인
Method : POST
Header : Content-Type: application/json , Authorization: Bearer YOUR_API_KEY
Body (raw JSON)
{
"basDd": "20250210"
}
<StockTransaction.java>
package com.example.personal_investor_trend.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "stock_transactions")
public class StockTransaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String basDd; // 기준일자
private String isuCd; // 종목코드
private String isuNm; // 종목명
private String mktNm; // 시장 구분
private Long accTrdVol; // 거래량
private Long mktCap; // 시가총액
}
<StockTransactionRepository.java>
package com.example.personal_investor_trend.repository;
import com.example.personal_investor_trend.model.StockTransaction;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StockTransactionRepository extends JpaRepository<StockTransaction, Long> {
List<StockTransaction> findByBasDd(String basDd);
}
<KrxService.java>
package com.example.personal_investor_trend.service;
import com.example.personal_investor_trend.model.StockTransaction;
import com.example.personal_investor_trend.repository.StockTransactionRepository;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Service
public class KrxService {
private final RestTemplate restTemplate = new RestTemplate();
private static final String API_URL = "http://data-dbg.krx.co.kr/svc/apis/sto/ksq_bydd_trd"; // KRX OpenAPI URL
private final StockTransactionRepository repository;
public KrxService(StockTransactionRepository repository){
this.repository = repository;
}
public String getKrxData(String basDd) {
//요청 데이터(JSON)
Map<String, String> requestBody = Collections.singletonMap("basDd", basDd);
//HTTP 헤더
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer D6274193FC6A4043B50121146C47226E67C50DB5");
//HTTP 요청 객체
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
//API 호출
ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, requestEntity, String.class);
// 데이터 저장 로직 실행
return saveKrxData(response.getBody(), basDd);
}
@Transactional
public String saveKrxData(String jsonResponse, String basDd){
try{
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(jsonResponse).get("OutBlock_1");
for (JsonNode node: rootNode){
StockTransaction transaction = new StockTransaction();
transaction.setBasDd(basDd);
// transaction.setBasDd(node.get("BAS_DD").asText());
transaction.setIsuCd(node.get("ISU_CD").asText());
transaction.setIsuNm(node.get("ISU_NM").asText());
transaction.setMktNm(node.get("MKT_NM").asText());
transaction.setAccTrdVol(node.get("ACC_TRDVOL").asLong());
transaction.setMktCap(node.get("MKTCAP").asLong());
repository.save(transaction);
}
return "🍩🍩🍩 KRX 데이터 저장 완료";
}catch (Exception e) {
e.printStackTrace();
return "💥💥💥 KRX 데이터 저장 실패!";
}
}
}
<TrenController.java>
package com.example.personal_investor_trend.controller;
import com.example.personal_investor_trend.model.StockTransaction;
import com.example.personal_investor_trend.repository.StockTransactionRepository;
import com.example.personal_investor_trend.service.KrxService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/trend")
public class TrendController {
private final KrxService krxService;
private final StockTransactionRepository repository;
public TrendController(KrxService krxService, StockTransactionRepository repository){
this.krxService = krxService;
this.repository = repository;
}
@PostMapping("/krx")
public String getKrxData(@RequestBody Map<String, String> request) {
return krxService.getKrxData(request.get("basDd"));
}
@GetMapping("/stored-data")
public List<StockTransaction> getStoredData(@RequestParam String basDd){
return repository.findByBasDd(basDd);
}
}
엔드포인트 : /api/trend/top-buyers
HTTP 메서드 : GET
요청 파라미터 : period = week / month (type:String)
@GetMapping("/top-buyers")
public List<Map<String, Object>> getTopBuyers(@RequestParam String period) {
// 조회 기간 설정 (7일 or 30일)
String startDate = period.equals("week")
? LocalDate.now().minusDays(7).toString()
: LocalDate.now().minusDays(30).toString();
// JPA Repository 호출
List<Object[]> results = repository.findTopBuyers(startDate);
// 응답 데이터를 JSON 형태로 가공
return results.stream().map(result -> Map.of(
"종목코드", result[0],
"종목명", result[1],
"순매수량", result[2]
)).toList();
}
/ 최근 7일 또는 30일 동안 개인 투자자 순매수 상위 10개 조회
@Query("SELECT s.isuCd, s.isuNm, SUM(s.accTrdVol) AS totalTrdVol " +
"FROM StockTransaction s " +
"WHERE s.basDd >= :startDate " +
"GROUP BY s.isuCd, s.isuNm " +
"ORDER BY totalTrdVol DESC")
List<Object[]> findTopBuyers(@Param("startDate") String startDate);
- 최근 7일 / 30일 기준으로 동안 개인 vs 기관 vs 외국인의 매수/매도 흐름을 비교
엔드포인트 : /api/trend/comparison
HTTP 메서드 : GET
요청 파라미터 : period = week / month (type:String)
private Long instTrdVol; // 기관 거래량
private Long foreignTrdVol; // 외국인 거래량
transaction.setInstTrdVol(node.get("INST_TRDVOL").asLong());
transaction.setForeignTrdVol(node.get("FOREIGN_TRDVOL").asLong());
@Query("SELECT s.isuCd, s.isuNm, SUM(s.accTrdVol) AS 개인, SUM(s.instTrdVol) AS 기관, SUM(s.foreignTrdVol) AS 외국인 " +
"FROM StockTransaction s " +
"WHERE s.basDd >= :startDate " +
"GROUP BY s.isuCd, s.isuNm")
List<Object[]> compareInvestorTrends(@Param("startDate") String startDate);
@GetMapping("/comparison")
public List<Map<String, Object>> getComparison(@RequestParam String period){
String startDate = period.equals("week")
? LocalDate.now().minusDays(7).toString()
: LocalDate.now().minusDays(30).toString();
List<Object[]> results = repository.compareInvestorTrends(startDate);
return results.stream().map(result -> Map.of(
"종목코드", result[0],
"종목명", result[1],
"개인 순매수량", result[2],
"기관 순매수량", result[3],
"외국인 순매수량", result[4]
)).toList();
}
최근 1개월 동안 개인 투자자가 많이 매수한 종목을 분석하여 비슷한 매매 패턴을 보이는 종목을 찾아 연관 종목 추천하는 기능
- 단순 추천 로직을 사용해 기본적인 종목 추천 기능 개발
- 추후 AI 모델을 추가할 수 있도록 확장 가능하게 설계
@Query("SELECT s.isuCd, s.isuNm, SUM(s.accTrdVol) AS totalTrdVol " +
"FROM StockTransaction s " +
"WHERE s.basDd >= :startDate " +
"GROUP BY s.isuCd, s.isuNm " +
"ORDER BY totalTrdVol DESC")
List<Object[]> findTopPersonalStocks(@Param("startDate") String startDate, Pageable pageable);
* 처음에 LIMIT 10 으로 쿼리 줬다니 ERROR -> JPA/HQL 에서는 LIMIT 은 직접 사용이 불가, 대신 Spring Data JPA의 Pageable 사용
package com.example.personal_investor_trend.service;
import com.example.personal_investor_trend.repository.StockTransactionRepository;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.PageRequest;
import java.time.LocalDate;
import java.util.*;
@Service
public class RecommendationService {
private final StockTransactionRepository repository;
public RecommendationService(StockTransactionRepository repository) {
this.repository = repository;
}
public List<Map<String, String>> getRecommendedStocks() {
String startDate = LocalDate.now().minusDays(30).toString();
List<Object[]> topStocks = repository.findTopPersonalStocks(startDate, PageRequest.of(0, 10));
// 연관 종목 ex
Map<String, String> similarStocks = Map.of(
"090710", "056080",
"027040", "080220",
"466100", "056080",
"452450", "108860"
);
List<Map<String, String>> recommendations = new ArrayList<>();
for (Object[] stock : topStocks) {
String isuCd = (String) stock[0];
String isuNm = (String) stock[1];
if (similarStocks.containsKey(isuCd)) {
recommendations.add(Map.of(
"추천 종목코드", similarStocks.get(isuCd),
"추천 종목명", getStockName(similarStocks.get(isuCd)),
"추천 이유", isuNm + "과 비슷한 매매 패턴을 보임"
));
}
}
return recommendations;
}
private String getStockName(String isuCd) {
// 종목 코드 → 종목명 매핑 (추후 DB 조회 가능)
Map<String, String> stockNames = Map.of(
"090710", "휴림로봇",
"056080", "유진로봇",
"027040", "서울전자통신",
"080220", "제주반도체",
"466100", "클로봇",
"108860", "셀바스AI",
"452450", "피아이이"
);
return stockNames.getOrDefault(isuCd, "알 수 없음");
}
}
@GetMapping("/recommendations")
public List<Map<String, String>> getRecommendations(){
return RecommendationService.getRecommendedStocks();
}
- 사용자가 관심있는 종목을 선택해서 저장
엔드포인트 : /api/user/watchlist
HTTP 메서드 : POST
Req Body : {
"userId": 1,
"stockCode": "005930",
"stockName": "삼성전자"
}
Res : {"message": "삼성전자 (005930) 관심 종목으로 저장 완료!"}
package com.example.personal_investor_trend.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "user_watchlist")
public class UserWatchlist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId; // 사용자 ID
private String stockCode; // 종목 코드
private String stockName; // 종목명
}
package com.example.personal_investor_trend.repository;
import com.example.personal_investor_trend.model.UserWatchlist;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserWatchlistRepository extends JpaRepository<UserWatchlist, Long> {
List<UserWatchlist> findByUserId(Long userId);
}
package com.example.personal_investor_trend.service;
import com.example.personal_investor_trend.model.UserWatchlist;
import com.example.personal_investor_trend.repository.UserWatchlistRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserWatchlistService {
private final UserWatchlistRepository repository;
public UserWatchlistService(UserWatchlistRepository repository) {
this.repository = repository;
}
public UserWatchlist addWatchlist(Long userId, String stockCode, String stockName) {
UserWatchlist watchlist = new UserWatchlist();
watchlist.setUserId(userId);
watchlist.setStockCode(stockCode);
watchlist.setStockName(stockName);
return repository.save(watchlist);
}
public List<UserWatchlist> getUserWatchlist(Long userId) {
return repository.findByUserId(userId);
}
}
package com.example.personal_investor_trend.controller;
import com.example.personal_investor_trend.model.UserWatchlist;
import com.example.personal_investor_trend.service.UserWatchlistService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserWatchlistService watchlistService;
public UserController(UserWatchlistService watchlistService) {
this.watchlistService = watchlistService;
}
// 관심 종목 저장 (POST)
@PostMapping("/watchlist")
public Map<String, String> addWatchlist(@RequestBody Map<String, String> request) {
Long userId = Long.parseLong(request.get("userId"));
String stockCode = request.get("stockCode");
String stockName = request.get("stockName");
watchlistService.addWatchlist(userId, stockCode, stockName);
return Map.of("message", stockName + " (" + stockCode + ") 관심 종목으로 저장 완료!");
}
// 사용자의 관심 종목 조회 (GET)
@GetMapping("/watchlist")
public List<UserWatchlist> getUserWatchlist(@RequestParam Long userId) {
return watchlistService.getUserWatchlist(userId);
}
}
- 사용자의 관심 종목(user_watchlist 테이블)에서 stockCode 목록을 가져와 뉴스 크롤링
엔드포인트 :
HTTP 메서드 : POST
List<UserWatchlist> findStockCodeByUserId(Long userId);
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>
package com.example.personal_investor_trend.service;
import com.example.personal_investor_trend.model.UserWatchList;
import com.example.personal_investor_trend.repository.UserWatchListRepository;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
public class NewsService {
private final UserWatchListRepository watchlistRepository;
public NewsService(UserWatchListRepository watchlistRepository) {
this.watchlistRepository = watchlistRepository;
}
public List<Map<String, Object>> getNewsForUser(Long userId) {
List<UserWatchList> watchlist = watchlistRepository.findByUserId(userId);
List<Map<String, Object>> newsData = new ArrayList<>();
for (UserWatchList stock : watchlist) {
String stockCode = stock.getStockCode();
String stockName = stock.getStockName();
List<Map<String, String>> newsList = getStockNews(stockName);
newsData.add(Map.of("종목코드", stockCode, "종목명", stockName, "관련 뉴스", newsList));
}
return newsData;
}
// 크롤링 메서드
private List<Map<String, String>> getStockNews(String stockName) {
List<Map<String, String>> newsList = new ArrayList<>();
try {
String encodedQuery = URLEncoder.encode(stockName, StandardCharsets.UTF_8);
//네이버 뉴스
String url = "https://search.naver.com/search.naver?where=news&ie=utf8&sm=nws_hty&query=" + encodedQuery;
Document doc = Jsoup.connect(url).get();
Elements newsElements = doc.select(".list_news .news_area");
for (Element news : newsElements) {
Element titleElement = news.selectFirst(".news_tit"); //제목
if (titleElement == null) continue;
String title = titleElement.text();
String link = titleElement.attr("href");
newsList.add(Map.of("제목", title, "링크", link));
if (newsList.size() >= 5) break; // 최대 5개
}
} catch (IOException e) {
e.printStackTrace();
}
return newsList;
}
}
package com.example.personal_investor_trend.controller;
import com.example.personal_investor_trend.service.NewsService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class NewsController {
private final NewsService newsService;
public NewsController(NewsService newsService) {
this.newsService = newsService;
}
// 사용자 관심 종목 뉴스 조회
@PostMapping("/news")
public List<Map<String, Object>> getUserNews(@RequestBody Map<String, String> request) {
Long userId = Long.parseLong(request.get("userId"));
return newsService.getNewsForUser(userId);
}
}
위의 두가지를 제일 신경 쓴 간단한 프로젝트였다!
추후에 딥하게 들어간다면 실시간으로 주식 시세나 알림을 사용자에게 제공하고 AI기반으로 투자를 추천해준다거나 더 많은 공공데이터를 함께 사용해서 종목에 대한 분석을 강화할 수 있지 않을까?!