Skip to content

Commit

Permalink
Merge pull request #71 from CSID-DGU/feature/#33/trading
Browse files Browse the repository at this point in the history
[feat] : 업비트 자동매매 기능 초기 구현
  • Loading branch information
bbbang105 authored Jun 15, 2024
2 parents 2b1f0d5 + 8090667 commit 4d29c38
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@
@Getter
@RequiredArgsConstructor
public enum UpbitErrorResult implements BaseErrorCode {
FAIL_ACCESS_USER_ACCOUNT(HttpStatus.NOT_FOUND, "404", "업비트에서 유저 잔고를 가져오는 데 실패했습니다."),
FAIL_ACCESS_COIN_INFO(HttpStatus.NOT_FOUND, "404", "업비트에서 코인 정보를 가져오는 데 실패했습니다."),
FAIL_GET_CANDLE_INFO(HttpStatus.NOT_FOUND, "404", "업비트에서 캔들 정보를 가져오는 데 실패했습니다."),
NOT_FOUND_UPBIT_KEY(HttpStatus.NOT_FOUND, "404", "업비트 키가 존재하지 않습니다."),
FAIL_GET_RESPONSE(HttpStatus.UNAUTHORIZED, "401", "업비트에서 데이터를 가져오는 데 실패했습니다."),
UNAUTHORIZED_IP(HttpStatus.UNAUTHORIZED, "401", "허용되지 않은 IP 주소입니다."),
UNAUTHORIZED_UPBIT_KEY(HttpStatus.UNAUTHORIZED, "401", "올바른 업비트 키가 아닙니다.");
UNAUTHORIZED_KEY(HttpStatus.UNAUTHORIZED, "401", "올바른 업비트 키가 아닙니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
@RequiredArgsConstructor
public enum UserErrorResult implements BaseErrorCode {
NOT_FOUND_USER(HttpStatus.NOT_FOUND, "404", "존재하지 않는 유저입니다."),
ALREADY_AGREED(HttpStatus.CONFLICT, "409", "이미 서비스 약관 동의를 한 유저입니다.");
ALREADY_AGREED(HttpStatus.CONFLICT, "409", "이미 서비스 약관 동의를 한 유저입니다."),
NOT_FOUND_KEY(HttpStatus.NOT_FOUND, "404", "업비트 키가 존재하지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@
import org.dgu.backend.domain.CandleInfo;
import org.dgu.backend.domain.Market;
import org.dgu.backend.dto.UpbitDto;
import org.dgu.backend.exception.UpbitErrorResult;
import org.dgu.backend.exception.UpbitException;
import org.dgu.backend.repository.CandleInfoRepository;
import org.dgu.backend.repository.CandleRepository;
import org.dgu.backend.repository.MarketRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

@Service
@Transactional
Expand All @@ -30,7 +27,7 @@ public class CandleInfoServiceImpl implements CandleInfoService {
private final CandleInfoRepository candleInfoRepository;
private final MarketRepository marketRepository;
private final CandleRepository candleRepository;
private final RestTemplate restTemplate;
private final UpbitApiClient upbitApiClient;

// 업비트 API를 통해 캔들 정보를 가져오는 메서드
@Override
Expand All @@ -49,30 +46,16 @@ public void getCandleInfo(String koreanName, LocalDateTime to, int count, String
url = String.format(UPBIT_URL_CANDLE_ETC, candleName, marketName, count);
}

if (to != null) {
if (!Objects.isNull(to)) {
// 마지막 캔들 시각도 지정한 경우
String formattedTo = to.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
url += ("&to=" + formattedTo);
}

HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);

ResponseEntity<UpbitDto.CandleInfoResponse[]> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
UpbitDto.CandleInfoResponse[].class
);

UpbitDto.CandleInfoResponse[] responseBody = responseEntity.getBody();
if (responseBody != null) {
for (UpbitDto.CandleInfoResponse candleInfoResponse : responseBody) {
CandleInfo candleInfo = CandleInfo.toEntity(candleInfoResponse, market, candle);
candleInfoRepository.save(candleInfo);
}
} else {
throw new UpbitException(UpbitErrorResult.FAIL_GET_CANDLE_INFO);
UpbitDto.CandleInfoResponse[] responseBody = upbitApiClient.getCandleInfoAtUpbit(url);
for (UpbitDto.CandleInfoResponse candleInfoResponse : responseBody) {
CandleInfo candleInfo = CandleInfo.toEntity(candleInfoResponse, market, candle);
candleInfoRepository.save(candleInfo);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,13 @@
import org.dgu.backend.domain.UserCoin;
import org.dgu.backend.dto.DashBoardDto;
import org.dgu.backend.dto.UpbitDto;
import org.dgu.backend.exception.MarketErrorResult;
import org.dgu.backend.exception.MarketException;
import org.dgu.backend.exception.UpbitErrorResult;
import org.dgu.backend.exception.UpbitException;
import org.dgu.backend.exception.*;
import org.dgu.backend.repository.MarketRepository;
import org.dgu.backend.repository.UpbitKeyRepository;
import org.dgu.backend.repository.UserCoinRepository;
import org.dgu.backend.util.JwtUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import java.math.BigDecimal;
import java.math.RoundingMode;
Expand All @@ -39,8 +33,8 @@ public class DashBoardServiceImpl implements DashBoardService {
private String UPBIT_URL_ACCOUNT;
@Value("${upbit.url.ticker}")
private String UPBIT_URL_TICKER;
private final RestTemplate restTemplate;
private final JwtUtil jwtUtil;
private final UpbitApiClient upbitApiClient;
private final UpbitKeyRepository upbitKeyRepository;
private final UserCoinRepository userCoinRepository;
private final MarketRepository marketRepository;
Expand Down Expand Up @@ -88,8 +82,8 @@ private DashBoardDto.UserCoinResponse processSingleCoin(UpbitDto.Account account
if (Objects.isNull(market)) {
throw new MarketException(MarketErrorResult.NOT_FOUND_MARKET);
}
UpbitDto.Ticker[] ticker = getTickerPriceAtUpbit(UPBIT_URL_TICKER + marketName);
BigDecimal curPrice = BigDecimal.valueOf(ticker[0].getPrice());
UpbitDto.Ticker[] ticker = upbitApiClient.getTickerPriceAtUpbit(UPBIT_URL_TICKER + marketName);
BigDecimal curPrice = ticker[0].getPrice();
BigDecimal curCoinCount = account.getCoinCount();
boolean isIncrease = false;
BigDecimal rate = BigDecimal.ZERO;
Expand Down Expand Up @@ -119,7 +113,7 @@ private DashBoardDto.UserCoinResponse processSingleCoin(UpbitDto.Account account
public List<DashBoardDto.RepresentativeCoinResponse> getRepresentativeCoins() {
List<DashBoardDto.RepresentativeCoinResponse> representativeCoinResponses = new ArrayList<>();
for (Coin coin : Coin.values()) {
UpbitDto.Ticker[] ticker = getTickerPriceAtUpbit(UPBIT_URL_TICKER + coin.getMarketName());
UpbitDto.Ticker[] ticker = upbitApiClient.getTickerPriceAtUpbit(UPBIT_URL_TICKER + coin.getMarketName());
representativeCoinResponses.add(DashBoardDto.RepresentativeCoinResponse.of(ticker[0], coin.getKoreanName(), coin.getEnglishName()));
}

Expand All @@ -135,8 +129,8 @@ private BigDecimal getAccountSum(UpbitDto.Account[] accounts) {
} else {
// 현재가를 가져옴
String marketName = "KRW-" + account.getCurrency();
UpbitDto.Ticker[] ticker = getTickerPriceAtUpbit(UPBIT_URL_TICKER + marketName);
BigDecimal curPrice = BigDecimal.valueOf(ticker[0].getPrice());
UpbitDto.Ticker[] ticker = upbitApiClient.getTickerPriceAtUpbit(UPBIT_URL_TICKER + marketName);
BigDecimal curPrice = ticker[0].getPrice();
BigDecimal userCoinCount = account.getCoinCount();
accountSum = accountSum.add(curPrice.multiply(userCoinCount));
}
Expand All @@ -158,63 +152,10 @@ private BigDecimal getCoinPriceIncreaseRate(UserCoin userCoin, BigDecimal curPri
private UpbitDto.Account[] getUpbitAccounts(User user) {
UpbitKey upbitKey = upbitKeyRepository.findByUser(user);
if (Objects.isNull(upbitKey)) {
throw new UpbitException(UpbitErrorResult.NOT_FOUND_UPBIT_KEY);
throw new UserException(UserErrorResult.NOT_FOUND_KEY);
}

String token = jwtUtil.generateUpbitToken(upbitKey);
UpbitDto.Account[] responseBody = getUserAccountsAtUpbit(UPBIT_URL_ACCOUNT, token);
if (Objects.isNull(responseBody)) {
throw new UpbitException(UpbitErrorResult.FAIL_ACCESS_USER_ACCOUNT);
}
return responseBody;
}

// 전체 계좌 조회 업비트 API와 통신하는 메서드
private UpbitDto.Account[] getUserAccountsAtUpbit(String url, String token) {
String authenticationToken = "Bearer " + token;
HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);
headers.add("Authorization", authenticationToken);

try {
ResponseEntity<UpbitDto.Account[]> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
UpbitDto.Account[].class
);
return responseEntity.getBody();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED && e.getResponseBodyAsString().contains("no_authorization_ip")) {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_IP);
} else {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_UPBIT_KEY);
}
}
}

// 시세 현재가 조회 업비트 API와 통신하는 메서드
private UpbitDto.Ticker[] getTickerPriceAtUpbit(String url) {
HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);

try {
ResponseEntity<UpbitDto.Ticker[]> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
UpbitDto.Ticker[].class
);
if (Objects.isNull(responseEntity.getBody()[0])) {
throw new UpbitException(UpbitErrorResult.FAIL_ACCESS_COIN_INFO);
}
return responseEntity.getBody();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED && e.getResponseBodyAsString().contains("no_authorization_ip")) {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_IP);
} else {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_UPBIT_KEY);
}
}
return upbitApiClient.getUserAccountsAtUpbit(UPBIT_URL_ACCOUNT, token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,18 @@
public class MarketServiceImpl implements MarketService {
@Value("${upbit.url.market}")
private String UPBIT_URL_MARKET;
private final RestTemplate restTemplate;
private final UpbitApiClient upbitApiClient;
private final MarketRepository marketRepository;

// 모든 가상화폐 데이터를 가져와 저장하는 메서드
@Override
public void getAllMarkets() {
HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);

ResponseEntity<UpbitDto.MarketResponse[]> responseEntity = restTemplate.exchange(
UPBIT_URL_MARKET,
HttpMethod.GET,
new HttpEntity<>(headers),
UpbitDto.MarketResponse[].class
);

UpbitDto.MarketResponse[] responseBody = responseEntity.getBody();
if (responseBody != null) {
for (UpbitDto.MarketResponse marketResponse : responseBody) {
// "KRW-"로 시작하는 가상화폐만 저장
if (marketResponse.getName().startsWith("KRW-")) {
marketRepository.save(marketResponse.to());
}
UpbitDto.MarketResponse[] responseBody = upbitApiClient.getAllMarketsAtUpbit(UPBIT_URL_MARKET);
for (UpbitDto.MarketResponse marketResponse : responseBody) {
// "KRW-"로 시작하는 가상화폐만 저장
if (marketResponse.getName().startsWith("KRW-")) {
marketRepository.save(marketResponse.to());
}
} else {
log.error("Failed to receive market info");
}
}
}
97 changes: 97 additions & 0 deletions backend/src/main/java/org/dgu/backend/service/UpbitApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.dgu.backend.service;

import lombok.RequiredArgsConstructor;
import org.dgu.backend.dto.UpbitDto;
import org.dgu.backend.exception.UpbitErrorResult;
import org.dgu.backend.exception.UpbitException;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import java.util.Objects;
import java.util.Optional;

@Component
@RequiredArgsConstructor
public class UpbitApiClient {
private final RestTemplate restTemplate;

// HTTP GET 요청을 보내고 결과를 처리하는 메서드
private <T> T sendHttpGetRequest(String url, Class<T> responseType, Optional<String> token) {
HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);
token.ifPresent(t -> headers.add("Authorization", "Bearer " + t));

try {
ResponseEntity<T> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
responseType
);
if (Objects.isNull(responseEntity.getBody())) {
throw new UpbitException(UpbitErrorResult.FAIL_GET_RESPONSE);
}
return responseEntity.getBody();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED && e.getResponseBodyAsString().contains("no_authorization_ip")) {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_IP);
} else {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_KEY);
}
}
}

// HTTP POST 주문 요청을 보내고 결과를 처리하는 메서드
private <T> T sendHttpPostRequest(String url, Class<T> responseType, String token, Object requestBody) {
HttpHeaders headers = new HttpHeaders();
headers.set("accept", MediaType.APPLICATION_JSON_VALUE);
headers.add("Authorization", "Bearer " + token);
headers.setContentType(MediaType.APPLICATION_JSON);

try {
ResponseEntity<T> responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
new HttpEntity<>(requestBody, headers),
responseType
);
if (Objects.isNull(responseEntity.getBody())) {
throw new UpbitException(UpbitErrorResult.FAIL_GET_RESPONSE);
}
return responseEntity.getBody();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED && e.getResponseBodyAsString().contains("no_authorization_ip")) {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_IP);
} else {
throw new UpbitException(UpbitErrorResult.UNAUTHORIZED_KEY);
}
}
}

// 캔들 차트 조회 업비트 API와 통신하는 메서드
public UpbitDto.CandleInfoResponse[] getCandleInfoAtUpbit(String url) {
return sendHttpGetRequest(url, UpbitDto.CandleInfoResponse[].class, Optional.empty());
}

// 가상화폐 조회 업비트 API와 통신하는 메서드
public UpbitDto.MarketResponse[] getAllMarketsAtUpbit(String url) {
return sendHttpGetRequest(url, UpbitDto.MarketResponse[].class, Optional.empty());
}

// 전체 계좌 조회 업비트 API와 통신하는 메서드
public UpbitDto.Account[] getUserAccountsAtUpbit(String url, String token) {
return sendHttpGetRequest(url, UpbitDto.Account[].class, Optional.ofNullable(token));
}

// 시세 현재가 조회 업비트 API와 통신하는 메서드
public UpbitDto.Ticker[] getTickerPriceAtUpbit(String url) {
return sendHttpGetRequest(url, UpbitDto.Ticker[].class, Optional.empty());
}

// 주문 생성 업비트 API와 통신하는 메서드
public UpbitDto.OrderResponse[] createNewOrder(String url, String token, UpbitDto.OrderRequest request) {
return sendHttpPostRequest(url, UpbitDto.OrderResponse[].class, token, request);
}
}
Loading

0 comments on commit 4d29c38

Please sign in to comment.