프로젝트/SpringBoot 업비트 자동매매 프로젝트

[자동매매, SpringBoot] 3. 매도, 매수

브랜치 미정 2024. 5. 16. 00:47

2024.05.11 - [프로젝트/SpringBoot 업비트 자동매매 프로젝트] - [자동매매, SpringBoot] 2. 계좌 조회

 

이전 글에서 계좌 조회 기능을 만들었고, 매도 매수 기능을 추가 할 것이다.

 

업비트 개발자센터에서 API Reference를 보면 

 

https://api.upbit.com/v1/orders POST 요청을 보내 매도, 매수요청을 할 수 있고

 

Request Parameter 중 side 파라미터로 매수/매도 를 구분 한다.

 

나는 직관적으로 매수 / 매도 함수를 나눠서 만들것이다.

 

1. Util Class

매수 / 매도 요청을 보낼 때 필요한 Jwt Token에는 클레임에 query_hash, query_hash_alg 를 포함해야 한다.

 

기존에 만든 Util Class에 query_hash, query_hash_alg를 매개변수로 받는 generateAuthenticationToken를 추가해준다.

 

public String generateAuthenticationToken(String queryHash, String queryHashAlg) {
    String ACCESS_KEY = config.getACCESS_KEY();
    String SECRET_KEY = config.getSECRET_KEY();

    Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
    String jwtToken = JWT.create()
            .withClaim("access_key", ACCESS_KEY)
            .withClaim("nonce", UUID.randomUUID().toString())
            .withClaim("query_hash", queryHash)
            .withClaim("query_hash_alg", queryHashAlg)
            .sign(algorithm);

    return "Bearer " + jwtToken;
}

 

2. OrderService

주문 관련 서비스들은 OrderService에 추가 할 것이다.

 

매수 / 매도 둘 다 https://api.upbit.com/v1/ordersPOST 요청을 보내야하는데

 

Request Param 에 side 로 매수 / 매도를 구분한다. 함수 내에서 구분해도 되지만 

 

매수 함수인 bid, 매도 함수인 ask 로 나누어 추가했다.

 

매도는 시장가로 할 것이기 때문에 ord_type을 market으로 지정해주었다.

 

요청 header에는 위에서 추가한 Jwt Token을  담아서 날린다.

 

bid 함수 맨 위에 MIN_ORDER_AMOUNT는 주문 최소 금액을 Config에 정의해 놓은것인데 업비트는 주문 최소 금액이 5000원이다.

 

예외처리는 나중에 더 신경 쓰는 걸로..

 

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
    private final Util util;
    private final RestTemplate restTemplate;
    private final AccountService accountService;

	/**
    * 매수
    */
    public void bid(String market, String price) throws NoSuchAlgorithmException, UnsupportedEncodingException {

        if(Double.parseDouble(price) < Config.MIN_ORDER_AMOUNT) {
            log.info("최소 주문 금액은 " + Config.MIN_ORDER_AMOUNT + " 입니다.");
            return;
        }

        HashMap<String, String> params = new HashMap<>();
        params.put("market", market);
        params.put("side", "bid");
        params.put("price", price);
        params.put("ord_type", "price");

        ArrayList<String> queryElements = new ArrayList<>();

        for(Map.Entry<String, String> entity : params.entrySet()) {
            queryElements.add(entity.getKey() + "=" + entity.getValue());
        }

        String queryString = String.join("&", queryElements.toArray(new String[0]));

        MessageDigest md = MessageDigest.getInstance("SHA-512");

        md.update(queryString.getBytes("UTF-8"));

        String queryHash = String.format("%0128x", new BigInteger(1, md.digest()));

        String authenticationToken = util.generateAuthenticationToken(queryHash, "SHA512");

        HttpHeaders headers = new HttpHeaders();

        headers.set("Authorization", authenticationToken);
        headers.set("Content-Type", "application/json");

        HttpEntity request = new HttpEntity(new Gson().toJson(params), headers);

        ResponseEntity<String> response = restTemplate.exchange(SERVER_URL + "/v1/orders", HttpMethod.POST, request, String.class);

        log.info("[" + market + "] 매수, 금액 : " + price);
    }

	/**
    * 매도
    */
    public void ask(String market, String volume) throws NoSuchAlgorithmException, UnsupportedEncodingException, JsonProcessingException {
        double balance = accountService.getBalance(market);

        if(balance < Double.parseDouble(volume)) {
            log.info("매도 요청 수량이 보유 수량보다 많습니다. 보유 수량 : " + balance + ", 매도 요청 수량 : " + volume);
            return;
        }

        HashMap<String, String> params = new HashMap<>();
        params.put("market", market);
        params.put("side", "ask");
        params.put("ord_type", "market");
        params.put("volume", volume);

        ArrayList<String> queryElements = new ArrayList<>();

        for(Map.Entry<String, String> entity : params.entrySet()) {
            queryElements.add(entity.getKey() + "=" + entity.getValue());
        }

        String queryString = String.join("&", queryElements.toArray(new String[0]));

        MessageDigest md = MessageDigest.getInstance("SHA-512");

        md.update(queryString.getBytes("UTF-8"));

        String queryHash = String.format("%0128x", new BigInteger(1, md.digest()));

        String authenticationToken = util.generateAuthenticationToken(queryHash, "SHA512");

        HttpHeaders headers = new HttpHeaders();

        headers.set("Authorization", authenticationToken);
        headers.set("Content-Type", "application/json");

        HttpEntity request = new HttpEntity(new Gson().toJson(params), headers);

        ResponseEntity<String> response = restTemplate.exchange(SERVER_URL + "/v1/orders", HttpMethod.POST, request, String.class);

        log.info("[" + market + "] : 매도, 수량 : " + volume);
    }
}