diff --git a/backend/src/main/java/org/dgu/backend/BackendApplication.java b/backend/src/main/java/org/dgu/backend/BackendApplication.java index a2400f6..aff20fb 100644 --- a/backend/src/main/java/org/dgu/backend/BackendApplication.java +++ b/backend/src/main/java/org/dgu/backend/BackendApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class BackendApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/org/dgu/backend/controller/TradingController.java b/backend/src/main/java/org/dgu/backend/controller/TradingController.java index 944cfa5..7913e99 100644 --- a/backend/src/main/java/org/dgu/backend/controller/TradingController.java +++ b/backend/src/main/java/org/dgu/backend/controller/TradingController.java @@ -5,6 +5,7 @@ import org.dgu.backend.common.constant.SuccessStatus; import org.dgu.backend.dto.TradingDto; import org.dgu.backend.service.TradingService; +import org.dgu.backend.service.UpbitAutoTrader; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -13,6 +14,7 @@ @RequiredArgsConstructor public class TradingController { private final TradingService tradingService; + private final UpbitAutoTrader upbitAutoTrader; // 자동매매 등록 API @PostMapping @@ -33,4 +35,10 @@ public ResponseEntity> removeAutoTrading( tradingService.removeAutoTrading(authorizationHeader, portfolioId); return ApiResponse.onSuccess(SuccessStatus.SUCCESS_DELETE_TRADING); } + + // 자동매매 수동 테스트 API + @GetMapping("/test") + public void test() { + upbitAutoTrader.performAutoTrading(); + } } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/domain/TradingLog.java b/backend/src/main/java/org/dgu/backend/domain/TradingLog.java index f5e6807..eb51d8f 100644 --- a/backend/src/main/java/org/dgu/backend/domain/TradingLog.java +++ b/backend/src/main/java/org/dgu/backend/domain/TradingLog.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import org.dgu.backend.common.BaseEntity; +import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @@ -33,7 +34,7 @@ public class TradingLog extends BaseEntity { private Long capital; @Column(name = "coin", nullable = false) - private Double coin; + private BigDecimal coin; @Column(name = "coin_price", nullable = false) private Long coinPrice; @@ -42,7 +43,7 @@ public class TradingLog extends BaseEntity { private Double rate; @Builder - public TradingLog(Portfolio portfolio, String type, LocalDateTime date, Long capital, Double coin, Long coinPrice, Double rate){ + public TradingLog(Portfolio portfolio, String type, LocalDateTime date, Long capital, BigDecimal coin, Long coinPrice, Double rate){ this.portfolio = portfolio; this.type = type; this.date = date; diff --git a/backend/src/main/java/org/dgu/backend/domain/TradingOption.java b/backend/src/main/java/org/dgu/backend/domain/TradingOption.java index a6f5c28..2bb4166 100644 --- a/backend/src/main/java/org/dgu/backend/domain/TradingOption.java +++ b/backend/src/main/java/org/dgu/backend/domain/TradingOption.java @@ -28,17 +28,26 @@ public class TradingOption extends BaseEntity { @JoinColumn(name = "portfolios_id", foreignKey = @ForeignKey(name = "trading_options_fk_portfolios_id")) private Portfolio portfolio; - @Column(name = "start_capital", nullable = false) - private BigDecimal startCapital; + @Column(name = "initial_capital", nullable = false) + private Long initialCapital; + + @Column(name = "current_capital", nullable = false) + private Long currentCapital; @Column(name = "avg_price") - private BigDecimal avgPrice; + private Double avgPrice; @Column(name = "trading_unit_price", nullable = false) - private BigDecimal tradingUnitPrice; + private Long tradingUnitPrice; + + @Column(name = "coin_count", nullable = false, scale = 10) + private BigDecimal coinCount; + + @Column(name = "trading_count", nullable = false) + private int tradingCount; - @Column(name = "remained_buying_count", nullable = false) - private int remainedBuyingCount; + @Column(name = "buying_count", nullable = false) + private int buyingCount; @Column(name = "start_date", nullable = false) private LocalDateTime startDate; @@ -47,17 +56,24 @@ public class TradingOption extends BaseEntity { private LocalDateTime endDate; @Builder - public TradingOption(User user, Portfolio portfolio, BigDecimal startCapital, BigDecimal tradingUnitPrice, int remainedBuyingCount, LocalDateTime startDate, LocalDateTime endDate) { + public TradingOption(User user, Portfolio portfolio, Long initialCapital, Long currentCapital, Long tradingUnitPrice, int tradingCount, int buyingCount, LocalDateTime startDate, LocalDateTime endDate) { this.user = user; this.portfolio = portfolio; - this.startCapital = startCapital; + this.initialCapital = initialCapital; + this.currentCapital = currentCapital; this.tradingUnitPrice = tradingUnitPrice; - this.remainedBuyingCount = remainedBuyingCount; + this.coinCount = BigDecimal.ZERO; + this.tradingCount = tradingCount; + this.buyingCount = buyingCount; this.startDate = startDate; this.endDate = endDate; } - public void updateAvgPrice(BigDecimal avgPrice) { + public void updateAvgPrice(Double avgPrice) { this.avgPrice = avgPrice; } + + public void updateCoinCount(BigDecimal coinCount) { + this.coinCount = coinCount; + } } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/dto/BackTestingDto.java b/backend/src/main/java/org/dgu/backend/dto/BackTestingDto.java index abf2931..e3b0d0b 100644 --- a/backend/src/main/java/org/dgu/backend/dto/BackTestingDto.java +++ b/backend/src/main/java/org/dgu/backend/dto/BackTestingDto.java @@ -9,6 +9,8 @@ import lombok.Getter; import org.dgu.backend.domain.*; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -69,7 +71,7 @@ public static class BackTestingResult { private LocalDateTime date; private String action; private Double coinPrice; - private Double coin; + private BigDecimal coin; private Long capital; private Double rate; private Long income; @@ -79,12 +81,12 @@ public void updateRate(Double rate) { this.rate = rate; } - public static BackTestingDto.BackTestingResult of(LocalDateTime date, String action, Double coinPrice, Double coin, Long capital, Double rate, Long income, Integer tradingPeriod) { + public static BackTestingDto.BackTestingResult of(LocalDateTime date, String action, Double coinPrice, BigDecimal coin, Long capital, Double rate, Long income, Integer tradingPeriod) { return BackTestingDto.BackTestingResult.builder() .date(date) .action(action) .coinPrice(coinPrice) - .coin(coin) + .coin(coin.setScale(3, RoundingMode.HALF_UP)) .capital(capital) .rate(rate) .income(income) @@ -222,7 +224,7 @@ public static class TradingLog { private String type; private LocalDateTime date; private Long capital; - private Double coin; + private BigDecimal coin; private Long coinPrice; private Double rate; diff --git a/backend/src/main/java/org/dgu/backend/dto/DashBoardDto.java b/backend/src/main/java/org/dgu/backend/dto/DashBoardDto.java index fe31b9f..b61536b 100644 --- a/backend/src/main/java/org/dgu/backend/dto/DashBoardDto.java +++ b/backend/src/main/java/org/dgu/backend/dto/DashBoardDto.java @@ -78,7 +78,7 @@ public static DashBoardDto.RepresentativeCoinResponse of(UpbitDto.Ticker ticker, .marketName(ticker.getMarket()) .koreanName(koreanName) .englishName(englishName) - .changePrice(BigDecimal.valueOf(ticker.getPrice())) + .changePrice(ticker.getPrice()) .changeRate(BigDecimal.valueOf(ticker.getChangeRate()).setScale(5, RoundingMode.HALF_UP)) .isIncrease(ticker.getChange().equals("RISE")) .build(); diff --git a/backend/src/main/java/org/dgu/backend/dto/TradingDto.java b/backend/src/main/java/org/dgu/backend/dto/TradingDto.java index 8658a64..6f17784 100644 --- a/backend/src/main/java/org/dgu/backend/dto/TradingDto.java +++ b/backend/src/main/java/org/dgu/backend/dto/TradingDto.java @@ -11,7 +11,6 @@ import org.dgu.backend.domain.TradingOption; import org.dgu.backend.domain.User; -import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.UUID; @@ -31,9 +30,11 @@ public TradingOption to(User user, Portfolio portfolio, PortfolioOption portfoli return TradingOption.builder() .user(user) .portfolio(portfolio) - .startCapital(BigDecimal.valueOf(fund)) - .tradingUnitPrice(BigDecimal.valueOf(fund / portfolioOption.getTradingUnit())) - .remainedBuyingCount(portfolioOption.getTradingUnit()) + .initialCapital(fund) + .currentCapital(fund) + .tradingUnitPrice(fund / portfolioOption.getTradingUnit()) + .tradingCount(portfolioOption.getTradingUnit()) + .buyingCount(0) .startDate(startDate) .endDate(endDate) .build(); diff --git a/backend/src/main/java/org/dgu/backend/dto/UpbitDto.java b/backend/src/main/java/org/dgu/backend/dto/UpbitDto.java index 06ff080..f215f2b 100644 --- a/backend/src/main/java/org/dgu/backend/dto/UpbitDto.java +++ b/backend/src/main/java/org/dgu/backend/dto/UpbitDto.java @@ -8,6 +8,7 @@ import org.dgu.backend.domain.Market; import java.math.BigDecimal; +import java.time.LocalDateTime; public class UpbitDto { @Builder @@ -86,8 +87,8 @@ public static class Account { private String currency; @JsonProperty("balance") private BigDecimal coinCount; - private BigDecimal locked; - private BigDecimal avgBuyPrice; + private Double locked; + private Double avgBuyPrice; private String unitCurrency; } @@ -99,11 +100,62 @@ public static class Account { public static class Ticker { private String market; @JsonProperty("trade_price") - private Double price; + private BigDecimal price; private String change; @JsonProperty("signed_change_rate") private Double changeRate; @JsonProperty("signed_change_price") private Double changePrice; } + + @Getter + @Builder + @AllArgsConstructor + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class OrderRequest { + private String market; + private String side; + private BigDecimal volume; + private Double price; + private String ordType; + + public static OrderRequest of(String market, String side, BigDecimal volume, Double price, String ordType) { + return OrderRequest.builder() + .market(market) + .side(side) + .volume(volume) + .price(price) + .ordType(ordType) + .build(); + } + } + + @Getter + @Builder + @AllArgsConstructor + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class OrderResponse { + private String uuid; + private String side; + private String ordType; + private String price; + private String state; + private String market; + @JsonProperty("created_at") + private LocalDateTime createdAt; + private String volume; + @JsonProperty("remaining_volume") + private String remainingVolume; + @JsonProperty("reserved_fee") + private String reservedFee; + @JsonProperty("remaining_fee") + private String remainingFee; + @JsonProperty("paid_fee") + private String paidFee; + private String locked; + @JsonProperty("executed_volume") + private String executedVolume; + @JsonProperty("trades_count") + private Integer tradesCount; + } } diff --git a/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java b/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java index 0123b9e..936dec1 100644 --- a/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java +++ b/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java @@ -8,6 +8,8 @@ import org.dgu.backend.util.NumberUtil; import org.springframework.stereotype.Component; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -22,7 +24,7 @@ public class BackTestingCalculator { private static Double buyingPoint; private static Double sellingPoint; private static Double stopLossPoint; - private static Double coin; + private static BigDecimal coin; private static LocalDateTime startDate; private static int buyingCnt; private static int positiveTradeCount; @@ -102,8 +104,8 @@ public List run(List candles, Back // 거래를 실행하는 메서드 private void executeTrade(List candles, int startIndex, int tradingCnt, List backTestingResults) { // 초기 세팅 - coin = 0.0; - executeBuy(candles.get(startIndex).getDateTime(), candles.get(startIndex).getTradePrice(), backTestingResults); + coin = BigDecimal.ZERO; + executeBuy("BUY", candles.get(startIndex).getDateTime(), candles.get(startIndex).getTradePrice(), backTestingResults); buyingCnt = 1; List tradePrices = new ArrayList<>(); tradePrices.add(candles.get(startIndex).getTradePrice()); @@ -118,56 +120,80 @@ private void executeTrade(List candles, int startIndex, int tradingC startDate = currentDate; } - Long initialCapital = capital + tradingUnit * (buyingCnt); + Long initialCapital = capital + tradingUnit * buyingCnt; Double curRate = calculateRate(capital, initialCapital, currentPrice, coin); + String action = determineAction(currentPrice, avgPrice, tradingCnt, buyingCnt, buyingPoint, sellingPoint, stopLossPoint, curRate); // 매수 처리 - if (buyingCnt < tradingCnt && currentPrice < avgPrice * (100 - buyingPoint) / 100) { - executeBuy(currentDate, currentPrice, backTestingResults); + if (action.equals("BUY")) { + executeBuy(action, currentDate, currentPrice, backTestingResults); buyingCnt++; tradePrices.add(currentPrice); avgPrice = tradePrices.stream().mapToDouble(Double::doubleValue).sum() / buyingCnt; } - // 전체 자본 대비 수익률을 기준으로 한 익절 처리 - else if (curRate > sellingPoint) { - executeSell(currentDate, currentPrice, backTestingResults); + // 익절 처리 + else if (action.equals("SELL")) { + executeSell(action, currentDate, currentPrice, backTestingResults); break; } - // 전체 자본 대비 수익률을 기준으로 한 손절 처리 - else if (curRate < -stopLossPoint) { - executeStopLoss(currentDate, currentPrice, backTestingResults); + // 손절 처리 + else if (action.equals("STOP_LOSS")) { + executeStopLoss(action, currentDate, currentPrice, backTestingResults); break; } + + // 마지막 남은 코인 매도 처리 + if (i == candles.size() - 1 && !Objects.equals(coin, BigDecimal.ZERO)) { + executeSell(action, currentDate, currentPrice, backTestingResults); + } + } + } + + // 액션을 판단하는 메서드 + public String determineAction(Double currentPrice, Double avgPrice, int tradingCnt, int buyingCnt, Double buyingPoint, Double sellingPoint, Double stopLossPoint, Double curRate) { + // 매수 조건 판단 + if (buyingCnt < tradingCnt && currentPrice < avgPrice * (100 - buyingPoint) / 100) { + return "BUY"; + } + // 익절 조건 판단 + else if (curRate > sellingPoint) { + return "SELL"; } + // 손절 조건 판단 + else if (curRate < -stopLossPoint) { + return "STOP_LOSS"; + } + + return "STAY"; } // 매수 처리 메서드 - private void executeBuy(LocalDateTime currentDate, Double currentPrice, List backTestingResults) { - coin += tradingUnit / currentPrice; + private void executeBuy(String action, LocalDateTime currentDate, Double currentPrice, List backTestingResults) { + coin = coin.add(BigDecimal.valueOf(tradingUnit).divide(BigDecimal.valueOf(currentPrice), 10, RoundingMode.HALF_UP)); capital -= tradingUnit; - backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, "BUY", currentPrice, coin, capital, 0.0, 0L, null)); + backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, action, currentPrice, coin, capital, 0.0, 0L, null)); } // 익절 처리 메서드 - private void executeSell(LocalDateTime currentDate, Double currentPrice, List backTestingResults) { + private void executeSell(String action, LocalDateTime currentDate, Double currentPrice, List backTestingResults) { Long initialCapital = capital + tradingUnit * buyingCnt; - capital += (long) (currentPrice * coin); - coin = 0.0; + capital += (BigDecimal.valueOf(currentPrice).multiply(coin)).longValue(); + coin = BigDecimal.ZERO; Long income = capital - initialCapital; Double rate = ((double) income / initialCapital) * 100; - backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, "SELL", currentPrice, coin, capital, rate, income, currentDate.compareTo(startDate))); + backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, action, currentPrice, coin, capital, rate, income, currentDate.compareTo(startDate))); } // 손절 처리 메서드 - private void executeStopLoss(LocalDateTime currentDate, Double currentPrice, List backTestingResults) { + private void executeStopLoss(String action, LocalDateTime currentDate, Double currentPrice, List backTestingResults) { Long initialCapital = capital + tradingUnit * buyingCnt; - capital += (long) (currentPrice * coin); - coin = 0.0; + capital += (BigDecimal.valueOf(currentPrice).multiply(coin)).longValue(); + coin = BigDecimal.ZERO; Long income = capital - initialCapital; Double rate = ((double) income / initialCapital) * 100; - backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, "STOP_LOSS", currentPrice, coin, capital, rate, income, currentDate.compareTo(startDate))); + backTestingResults.add(BackTestingDto.BackTestingResult.of(currentDate, action, currentPrice, coin, capital, rate, income, currentDate.compareTo(startDate))); } // 시작 인덱스를 찾는 메서드 @@ -284,8 +310,8 @@ private BackTestingDto.TradingLog createTradingLog(BackTestingDto.BackTestingRes } // 수익률을 계산하는 메서드 - private Double calculateRate(Long capital, Long initialCapital, Double currentPrice, Double coin) { - Double currentAssetValue = capital + (currentPrice * coin); + public Double calculateRate(Long capital, Long initialCapital, Double currentPrice, BigDecimal coin) { + Double currentAssetValue = (double) (capital + (BigDecimal.valueOf(currentPrice).multiply(coin)).longValue()); return ((currentAssetValue - initialCapital) / initialCapital) * 100; } diff --git a/backend/src/main/java/org/dgu/backend/service/UpbitApiClient.java b/backend/src/main/java/org/dgu/backend/service/UpbitApiClient.java index f57c53c..e26eba0 100644 --- a/backend/src/main/java/org/dgu/backend/service/UpbitApiClient.java +++ b/backend/src/main/java/org/dgu/backend/service/UpbitApiClient.java @@ -1,5 +1,6 @@ package org.dgu.backend.service; +import com.nimbusds.jose.shaded.gson.Gson; import lombok.RequiredArgsConstructor; import org.dgu.backend.dto.UpbitDto; import org.dgu.backend.exception.UpbitErrorResult; @@ -9,6 +10,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -44,17 +46,16 @@ private T sendHttpGetRequest(String url, Class responseType, Optional T sendHttpPostRequest(String url, Class responseType, String token, Object requestBody) { + private T sendHttpPostRequest(String url, Class responseType, String token, Map params) { HttpHeaders headers = new HttpHeaders(); - headers.set("accept", MediaType.APPLICATION_JSON_VALUE); - headers.add("Authorization", "Bearer " + token); - headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Content-Type", MediaType.APPLICATION_JSON_VALUE); + headers.set("Authorization", "Bearer " + token); try { ResponseEntity responseEntity = restTemplate.exchange( url, HttpMethod.POST, - new HttpEntity<>(requestBody, headers), + new HttpEntity<>(new Gson().toJson(params), headers), responseType ); if (Objects.isNull(responseEntity.getBody())) { @@ -82,7 +83,7 @@ public UpbitDto.MarketResponse[] getAllMarketsAtUpbit(String url) { // 전체 계좌 조회 업비트 API와 통신하는 메서드 public UpbitDto.Account[] getUserAccountsAtUpbit(String url, String token) { - return sendHttpGetRequest(url, UpbitDto.Account[].class, Optional.ofNullable(token)); + return sendHttpGetRequest(url, UpbitDto.Account[].class, Optional.of(token)); } // 시세 현재가 조회 업비트 API와 통신하는 메서드 @@ -91,7 +92,7 @@ public UpbitDto.Ticker[] getTickerPriceAtUpbit(String url) { } // 주문 생성 업비트 API와 통신하는 메서드 - public UpbitDto.OrderResponse[] createNewOrder(String url, String token, UpbitDto.OrderRequest request) { - return sendHttpPostRequest(url, UpbitDto.OrderResponse[].class, token, request); + public UpbitDto.OrderResponse[] createNewOrder(String url, String token, Map params) { + return sendHttpPostRequest(url, UpbitDto.OrderResponse[].class, token, params); } } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/service/UpbitAutoTrader.java b/backend/src/main/java/org/dgu/backend/service/UpbitAutoTrader.java index 41ccc97..0fdfc28 100644 --- a/backend/src/main/java/org/dgu/backend/service/UpbitAutoTrader.java +++ b/backend/src/main/java/org/dgu/backend/service/UpbitAutoTrader.java @@ -12,12 +12,14 @@ import org.dgu.backend.repository.UpbitKeyRepository; import org.dgu.backend.util.JwtUtil; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; -import java.util.List; -import java.util.Objects; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; @Component @RequiredArgsConstructor @@ -34,7 +36,7 @@ public class UpbitAutoTrader { private final PortfolioOptionRepository portfolioOptionRepository; private final UpbitKeyRepository upbitKeyRepository; - @Scheduled(fixedRate = 60000) // 1분마다 실행 + //@Scheduled(fixedRate = 6000) // 1분마다 실행 public void performAutoTrading() { System.out.println("자동매매 로직 실행 중..."); @@ -46,54 +48,89 @@ public void performAutoTrading() { .orElseThrow(() -> new PortfolioException(PortfolioErrorResult.NOT_FOUND_PORTFOLIO_OPTIONS)); // 현재가 조회 UpbitDto.Ticker[] ticker = upbitApiClient.getTickerPriceAtUpbit(UPBIT_URL_TICKER + BIT_COIN_MARKET_NAME); - BigDecimal curPrice = ticker[0].getPrice(); + Double curPrice = ticker[0].getPrice().doubleValue(); + if (Objects.isNull(tradingOption.getAvgPrice())) { + tradingOption.updateAvgPrice(curPrice); + tradingOptionRepository.save(tradingOption); + } executeTrade(user, portfolioOption, tradingOption, curPrice); } } // 매매 조건을 검토하고 거래를 실행하는 메서드 - public void executeTrade(User user, PortfolioOption portfolioOption, TradingOption tradingOption, BigDecimal curPrice) { + public void executeTrade(User user, PortfolioOption portfolioOption, TradingOption tradingOption, Double curPrice) { System.out.println("매매 조건을 검토하고 거래를 실행합니다..."); - BigDecimal curRate = backTestingCalculator.calculateRate(tradingOption.getCurrentCapital(), tradingOption.getInitialCapital(), curPrice, tradingOption.getCoinCount()); + Double curRate = backTestingCalculator.calculateRate(tradingOption.getCurrentCapital(), tradingOption.getInitialCapital(), curPrice, tradingOption.getCoinCount()); String action = backTestingCalculator.determineAction(curPrice, tradingOption.getAvgPrice(), tradingOption.getTradingCount(), tradingOption.getBuyingCount(), portfolioOption.getBuyingPoint(), portfolioOption.getSellingPoint(), portfolioOption.getStopLossPoint(), curRate); UpbitKey upbitKey = upbitKeyRepository.findByUser(user); if (Objects.isNull(upbitKey)) { throw new UserException(UserErrorResult.NOT_FOUND_KEY); } - String token = jwtUtil.generateUpbitToken(upbitKey); - - // 매수 처리 - if (action.equals("BUY")) { - BigDecimal numCoins = curPrice.divide(tradingOption.getTradingUnitPrice()); - UpbitDto.OrderRequest orderRequest = UpbitDto.OrderRequest.of(BIT_COIN_MARKET_NAME, "bid", numCoins, curPrice, "limit"); - executeBuy(action, token, orderRequest); + Map params; + try { + switch (action) { + case "BUY": + params = createParams(BIT_COIN_MARKET_NAME, "bid", null, tradingOption.getTradingUnitPrice(), "price"); + executeOrder(params, upbitKey); + break; + case "SELL": + case "STOP_LOSS": + params = createParams(BIT_COIN_MARKET_NAME, "ask", tradingOption.getCoinCount(), null, "market"); + executeOrder(params, upbitKey); + break; + default: + System.out.println("매매 조건에 맞는 액션이 없습니다."); + break; + } + } catch (RuntimeException e) { + System.err.println("주문 생성 실패: " + e.getMessage()); } - // 익절 처리 - else if (action.equals("SELL")) { - UpbitDto.OrderRequest orderRequest = UpbitDto.OrderRequest.of(BIT_COIN_MARKET_NAME, "ask", tradingOption.getCoinCount(), curPrice, "limit"); - executeSell(action, token, orderRequest); + } + + // 주문 생성 및 실행 메서드 + private void executeOrder(Map params, UpbitKey upbitKey) { + String queryString = buildQueryString(params); + String queryHash = generateQueryHash(queryString); + String authenticationToken = jwtUtil.generateUpbitOrderToken(upbitKey, queryHash); + upbitApiClient.createNewOrder(UPBIT_URL_ORDER, authenticationToken, params); + } + + // 파라미터 생성 + public Map createParams(String market, String side, BigDecimal volume, Long price, String ordType) { + Map params = new HashMap<>(); + params.put("market", market); + params.put("side", side); + if (!Objects.isNull(volume)) { + params.put("volume", volume.toString()); } - // 손절 처리 - else if (action.equals("STOP_LOSS")) { - UpbitDto.OrderRequest orderRequest = UpbitDto.OrderRequest.of(BIT_COIN_MARKET_NAME, "ask", tradingOption.getCoinCount(), curPrice, "limit"); - executeStopLoss(action, token, orderRequest); + if (!Objects.isNull(price)) { + params.put("price", price.toString()); } - } + params.put("ord_type", ordType); - // 매수 처리 메서드 - private void executeBuy(String action, String token, UpbitDto.OrderRequest orderRequest) { - upbitApiClient.createNewOrder(UPBIT_URL_ORDER, token, orderRequest); + return params; } - // 익절 처리 메서드 - private void executeSell(String action, String token, UpbitDto.OrderRequest orderRequest) { - upbitApiClient.createNewOrder(UPBIT_URL_ORDER, token, orderRequest); + // 쿼리 문자열 생성 + private static String buildQueryString(Map params) { + List queryElements = new ArrayList<>(); + for (Map.Entry entry : params.entrySet()) { + queryElements.add(entry.getKey() + "=" + entry.getValue()); + } + return String.join("&", queryElements); } - // 손절 처리 메서드 - private void executeStopLoss(String action, String token, UpbitDto.OrderRequest orderRequest) { - upbitApiClient.createNewOrder(UPBIT_URL_ORDER, token, orderRequest); + // 쿼리 해시 생성 + private static String generateQueryHash(String queryString) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + md.update(queryString.getBytes("UTF-8")); + byte[] hashBytes = md.digest(); + return String.format("%0128x", new BigInteger(1, hashBytes)); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new RuntimeException("SHA-512 해시 생성 실패: " + e.getMessage(), e); + } } } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/util/JwtUtil.java b/backend/src/main/java/org/dgu/backend/util/JwtUtil.java index b8cce07..08854ac 100644 --- a/backend/src/main/java/org/dgu/backend/util/JwtUtil.java +++ b/backend/src/main/java/org/dgu/backend/util/JwtUtil.java @@ -78,6 +78,23 @@ public String generateUpbitToken(UpbitKey upbitKey) { .compact(); } + // 업비트 주문 요청 위한 토큰을 발급하는 메서드 + public String generateUpbitOrderToken(UpbitKey upbitKey, String queryHash) { + PrivateKey privateKey = encryptionUtil.getDecryptedPrivateKey(upbitKey.getPrivateKey()); + String accessKey = encryptionUtil.decryptAndEncode(upbitKey.getAccessKey(), privateKey); + String secretKey = encryptionUtil.decryptAndEncode(upbitKey.getSecretKey(), privateKey); + + log.info("업비트 주문 요청 API 토큰이 발행되었습니다."); + + return Jwts.builder() + .claim("access_key", accessKey) + .claim("nonce", UUID.randomUUID().toString()) + .claim("query_hash", queryHash) + .claim("query_hash_alg", "SHA512") + .signWith(getUpbitSigningKey(secretKey)) + .compact(); + } + // 응답 헤더에서 액세스 토큰을 반환하는 메서드 public String getTokenFromHeader(String authorizationHeader) { return authorizationHeader.substring(7);