java를 이용한 업비트 WebSocket 클라이언트 예제(okhttp, websocket client)

안녕하세요.

 

오늘은 웹소켓으로 제공되는 데이터를 수신하기 위한 자바 클라이언트 예제를 구현해보려 합니다.

 

https://docs.upbit.com/docs/upbit-quotation-websocket

 

업비트 개발자 센터

업비트 Open API 사용을 위한 개발 문서를 제공 합니다.업비트 Open API 사용하여 다양한 앱과 프로그램을 제작해보세요.

docs.upbit.com

 

현재까지 구현된 소스코드는 여기에서 확인하실 수 있습니다.

 


 

스프링을 이용하며 데모 프로젝트를 개발중에 있지만 우선은 main 메소드를 활용하여 작성한 예제코드를 제공하려 합니다.

 

현재 해당 WebSocket 예제를 작성하기까지 제가 필요했던 부분은 바로 체결강도라는 수치입니다.

 

체결강도란 현재 매수세가 강한지 매도세가 강한지에 대해 수초 혹은 수분동안의 단기간의 시세를 파악하기에 용이합니다. 예를 들어서, 거래량이 붙고 체결강도가 100%가 넘게 치솟으면 현재 과열구간으로 단기간 RSI가 70이상을 넘기는 과매수 구간으로 이해할 수 있습니다.

 

따라서 특정 조건식에 따라 매수가 되었고 이후 해당 종목의 체결강도가 100%이상에 도달했다면 적절히 과매수구간에 매도하여 정점에 매도할 수 있도록하는 조건을 구현해보려하였습니다.

 

하지만 해당 내용의 대해서는 매우 욕심이 많은 매매기법으로 우선 가볍게 예제만 짜놓았고 현재 업비트측에서 문서상에 내놓은 체결강도 계산식이 맞는지에 확인하기에 앞서 데이터를 확보할 수 있는 기술이 우선 선행되어야 합니다.

 

따라서 모든 지표에 대한 계산은 차후로 생각하고 데이터 확보를 위한 예제코드를 먼저 구현해보려 합니다.

 


 

(웹소켓) 현재 실시간 체결정보 가져오기

우선 필요한 라이브러리를 추가하겠습니다.

// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.3'

 

우선 해당 웹소켓의 파라미터는 다음과 같이 구성됩니다.

 

[{"ticket":"test"},{"type":"ticker","codes":["KRW-BTC"]}]

 

별도의 ticket에 대해서 UUID로 호출하면 됩니다.(문서에 나옴), 뒤에 나오는 파라미터 객체는 어떤 타입(현재가, 체결, 호가)으로 호출하고 어떤 코드(마켓)에 해당하는 데이터를 수신할 것인지를 전달해주어야 합니다.

 

main 메소드

public class UpbitWsClient {
    public static void main(String[] args) throws InterruptedException {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("wss://api.upbit.com/websocket/v1")
                .build();

        UpbitWebSocketListener webSocketListener = new UpbitWebSocketListener();
        webSocketListener.setParameter(SiseType.TRADE, List.of("KRW-BTC"));

        client.newWebSocket(request, webSocketListener);
        client.dispatcher().executorService().shutdown();
    }
}

 

 

전체적인 흐름은 위와 같습니다. 간단히 웹소켓 연결을 맺고 데이터를 출력하려 합니다.(출력문은 내부 listener)

 

SiseType (시세타입)

public enum SiseType implements EnumInterface {

    TICKER("ticker", "현재가"),
    TRADE("trade", "체결"),
    ORDERBOOK("orderbook", "호가");

    private String type;
    private String name;

}

websocket의 필요한 파라미터입니다. 저는 여기서 체결정보를 가져와보기 위해 trade를 파라미터로 넘겼습니다.

 

UpbitWebSocketListener 클래스

public class UpbitWebSocketListener extends WebSocketListener {

    private static final int NORMAL_CLOSURE_STATUS = 1000;
    private String json;
    private SiseType siseType;

    @Override
    public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
        System.out.printf("Socket Closed : %s / %s\r\n", code, reason);
    }

    @Override
    public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
        System.out.printf("Socket Closing : %s / %s\n", code, reason);
        webSocket.close(NORMAL_CLOSURE_STATUS, null);
        webSocket.cancel();
    }

    @Override
    public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
        System.out.println("Socket Error : " + t.getMessage());
    }

    @Override
    public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
        JsonNode jsonNode = JsonUtil.fromJson(text, JsonNode.class);
        System.out.println(jsonNode.toPrettyString());
    }

    @Override
    public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
        System.out.println(JsonUtil.fromJson(bytes.string(StandardCharsets.UTF_8), JsonNode.class).toPrettyString());
        switch(siseType) {
            case TRADE:
                TradeResult tradeResult = JsonUtil.fromJson(bytes.string(StandardCharsets.UTF_8), TradeResult.class);
                System.out.println(tradeResult);
                break;
            case TICKER:
                TickerResult result = JsonUtil.fromJson(bytes.string(StandardCharsets.UTF_8), TickerResult.class);
                System.out.println(result);
                break;
            case ORDERBOOK:
                System.out.println(JsonUtil.fromJson(bytes.string(StandardCharsets.UTF_8), OrderBookResult.class));
                break;
            default:
                throw new RuntimeException("지원하지 않는 웹소켓 조회 유형입니다. : " + siseType.getType());
        }

    }

    @Override
    public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
        webSocket.send(getParameter());
//        webSocket.close(NORMAL_CLOSURE_STATUS, null); // 없을 경우 끊임없이 서버와 통신함
    }

    public void setParameter(SiseType siseType, List<String> codes) {
        this.siseType = siseType;
        this.json = JsonUtil.toJson(List.of(Ticket.of(UUID.randomUUID().toString()), Type.of(siseType, codes)));
    }

    private String getParameter() {
        return this.json;
    }

    @Data(staticConstructor = "of")
    private static class Ticket {
        private final String ticket;
    }

    @Data(staticConstructor = "of")
    private static class Type {
        private final SiseType type;
        private final List<String> codes; // market
        private Boolean isOnlySnapshot = false;
        private Boolean isOnlyRealtime = true;
    }
}

해당 클래스를 구현할 때 주의점은 반드시 

public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes)

해당 메소드를 재정의해주어야 한다는 점입니다. 내부적으로 해당 메소드가 호출되어서 재정의해주지 않으면 정상동작하지 않을 수 있습니다. 실제로 웹소켓 데이터는 해당 메소드를 통해 수신하고 있으므로 반드시 재정의가 필요합니다.

 

아래쪽에 파라미터 클래스들인 Type과 Ticket을 정의해주었습니다. 해당 클래스 내부에서만 쓰일 것이기 때문에 내부 이너클래스로 작성해보았습니다.

 

onOpen 메소드에서 웹소켓에 대한 연결을 시작하려할 때 파라미터 셋팅을 해줍니다. setParameter를 통해 사전에 바인딩이 된 내부 클래스변수에 들어있는 값을 활용합니다. 따라서 main 메소드에서 setParameter를 통해서 값을 셋팅해주지 않으면 정상동작하지 않습니다.

 

데이터 수신 결과

{
  "type" : "trade",
  "code" : "KRW-BTC",
  "timestamp" : 1645350390896,
  "trade_date" : "2022-02-20",
  "trade_time" : "09:46:30",
  "trade_timestamp" : 1645350390000,
  "trade_price" : 4.679E7,
  "trade_volume" : 0.024,
  "ask_bid" : "BID",
  "prev_closing_price" : 4.9035E7,
  "change" : "FALL",
  "change_price" : 2245000.0,
  "sequential_id" : 1645350390000013,
  "stream_type" : "REALTIME"
}

 

해당 결과는 하나의 체결데이터를 수신한 것입니다. 예를 들어, 업비트 화면에서 호가탭의 왼쪽 아래 체결강도와 체결량이 있는 화면이 보일 것 입니다. 

거기서 하나의 데이터가 쌓일 때마다 해당 웹소켓에서 데이터를 수신할 수 있습니다. 근데 체결강도에 대한 계산은 업비트측에서 24시마다 초기화하고 있으므로 그에 대해서 정확한 계산은 웹소켓에서 24시간 혹은 자정부터 모든 체결데이터를 수신하여 직접

 

매수체결량 / 매도체결량 * 100

 

위 공식을 매번 구성해야 하므로 실질적으로는 (자정부터 현재시각까지의 전체 매수체결량) / 자정부터 현재시각까지의 전체 매도체결량) * 100의 공식이 필요하게 됩니다.

 

따라서 별도로 매번 웹소켓을 수신하여 시스템을 구성하기에는 아직까지는 많은 갈길이 있어보이기 때문에 데이터를 수신하는 예제만 구상을 해보았고 실질적으로 해당 체결강도 수신만을 위해서는 사실 그렇게 유용성이 많은 기능은 아니라서 폐기될 수도 있는 기능이긴 합니다.

 


 

자동매매에서 가장 중요한 부분은 전략과 백테스팅이라고 생각합니다. 훌륭한 전략을 백테스팅을 통해서 직접 검증을 해봐야 더 좋은 결과를 얻어낼 수 있다고 생각합니다. 따라서 전체적인 시스템이 어떤식으로 자리잡히게 되고 어떤 데이터가 저한테 좀더 유리한 결과를 도출해줄 수 있을지에 따라 이 기능이 유용할지 안할지 모르겠습니다.ㅎㅎ

 

다음번 예제는 분/일/월/년 의 평균을 알 수 있는 캔들을 호출해보는 예제를 따보겠습니다.

댓글

Designed by JB FACTORY