시뻘건 개발 도전기

#4. 코인 자동 매매 프로그램 만들기 본문

프로젝트/토이프로젝트

#4. 코인 자동 매매 프로그램 만들기

시뻘건볼때기 2022. 2. 28. 20:45
반응형

 

※ 본 '코인 자동 매매 프로그램 만들기' 시리즈 포스팅은 개인적인 학습용으로 개발하게 되는 프로그램입니다.
투자의 책임은 투자자 본인에게 있음을 알려드립니다.

 

다른건 제외하고 핵심인 매수/매도 중에서 포인트만 포스팅을 해볼까 한다.

각 투자자들은 자신만의 매수점과 매도점을 계산할때 사용하는 지표들이 다를 것이다.

나는 참고로 RSI를 좋아해서 RSI를 계산하여 매수점과 매도점을 예측한다.

 


if(rsi < BUY_RSI_STANDARD) {
    // 호가조회
    Map<String,String> orderBook = nonAuthAPI.getCoinOrderBook(coinName).get(0);
    List<Map<String,String>> orderBookUnits = JsonUtil.jsonString2List(String.valueOf(orderBook.get(APIKeyConst.ORDERBOOK_UNITS))
            .replaceAll("\\{", "{\"")
            .replaceAll("=", "\":\"")
            .replaceAll("[^}], ", "\", \"")
            .replaceAll("}", "\"}")
    );
    
    // 호가매수
    Map<String,String> orderBookUnit = Optional.ofNullable(orderBookUnits)
            .map(o -> o.get(2))
            .orElseThrow(() -> new NullPointerException(coinName + " - 호가조회 실패"));
    double buyPrice = Double.parseDouble(orderBookUnit.get(APIKeyConst.BID_PRICE));

    int enableBuyAmount = Calculate.getEnableIntAmount(String.valueOf(authAPI.getMyKRW()), BUY_PERCENTAGE);


    if(enableBuyAmount >= MIN_BUY_AMOUNT) {
        double count = enableBuyAmount / buyPrice;
        Map<String,String> buyResult = JsonUtil.jsonString2Map(authAPI.buy(coinName, String.valueOf(count), String.valueOf(buyPrice)));
        String uuid = Optional.ofNullable(buyResult)
                .map(b -> b.get(APIKeyConst.UUID))
                .orElseThrow(() -> new Exception(coinName + "매수 주문 실패"));

        message.append(coinName)
                .append(" - ")
                .append(Calculate.KRW_FORMAT.format( buyPrice))
                .append("원에 ")
                .append(count)
                .append("개 매수 주문 (총 ")
                .append(Calculate.KRW_FORMAT.format(count * buyPrice))
                .append("원 매수)\n");

        database.saveOrderTable(OrderCoins.builder()
                .coin(coinName.substring(4))
                .orderDate(new Date())
                .uuid(uuid)
                .build());
    } else {
        break;
    }
}

한 가지 아쉬운 점은 업비트 API의 response가 json 짭(?)이라는 것이다. 완전히 json 문법에 맞는 형식이 아니라서 body를 파싱해서 사용하려면 replaceAll()을 통해 맞춰줘야한다..

 

BUY_RSI_STANDARD라고하는 매수 기준 RSI 값을 설정하고 그 값보다 작다면 호가 조회를 통해 선호하는 호가로 매수 주문을 넣는다.

나는 MIN_BUY_AMOUNT보다 적다면 주문을 넣지 않는다. 만약 내가 투자금이 100만원인데 매수 총 금액이 1만원이라면, 의미가 없기 때문에 이 제한을 두었다.

매수 주문을 넣은 후에 데이터베이스에 내가 어떤 거래ID로 어떤 코인을 언제 주문을 넣었는지 저장을 할텐데, 데이터베이스 접근은 async하게 접근한다.

@Async
public void saveOrderTable(OrderCoins order) {
    orderRepository.save(order);
}

하나의 스레드 내에서 수행하기엔 불필요하고, 하나의 스레드 내에서 처리한다면 다음 코인의 매수 시점이 계속해서 느려지기 때문이다. (코인 투자를 해보신 분들은 진짜 1분, 1초가 소중하다는 것을 알 것이다.)

 


if(rsi > SELL_RSI_STANDARD) {
    Map<String,String> orderBook = nonAuthAPI.getCoinOrderBook("KRW-" + coin).get(0);
    List<Map<String,String>> orderBookUnits = JsonUtil.jsonString2List(String.valueOf(orderBook.get(APIKeyConst.ORDERBOOK_UNITS))
                .replaceAll("\\{", "{\"")
                .replaceAll("=", "\":\"")
                .replaceAll("[^}], ", "\", \"")
                .replaceAll("}", "\"}")
        );

    Map<String,String> orderBookUnit = Optional.ofNullable(orderBookUnits)
            .map(o -> o.get(2))
            .orElseThrow(() -> new NullPointerException(coin + " - 호가조회 실패"));

    String sellPrice = orderBookUnit.get(APIKeyConst.ASK_PRICE);
    double count = Calculate.getEnableDoubleAmount(wallet.get(APIKeyConst.BALANCE), SELL_PERCENTAGE);

    Map<String,String> sellResult = JsonUtil.jsonString2Map(authAPI.sell("KRW-" + coin, String.valueOf(count), sellPrice));
    String uuid = Optional.ofNullable(sellResult)
            .map(b -> b.get(APIKeyConst.UUID))
            .orElseThrow(() -> new Exception(coin + "매수 주문 실패"));

    Map<String,String> market = nonAuthAPI.getCoinDetail(coin).get(0);
    double rateOfReturn = Calculate.getRateOfReturn(wallet, market);

    message.append(coin)
                .append(" - ")
                .append(Calculate.KRW_FORMAT.format( Integer.parseInt(sellPrice)))
                .append("원에 ")
                .append(count)
                .append("개 매도 (수익률 : ")
                .append(rateOfReturn)
                .append("%)\n");

    database.saveOrderTable(OrderCoins.builder()
            .coin(coin)
            .orderDate(new Date())
            .uuid(uuid)
            .build());
}

SELL_PERCENTAGE라는 녀석으로 가진거에서 몇 퍼센트를 매도할지 계산해서 매도 주문을 넣는다. 예를들어, 내가 100코인을 가지고 있는데 난 항상 매도할떄 30%만 팔겠다고한다면 30코인만 매도가 되고 나머지 60코인은 두 번쨰 매도 스케줄러가 돌때 또 30%로 처리된다. (3번째 매도시에는 전량 매도를 하려 했지만 구현하지 않았다.)

 


 

List<OrderCoins> orders = orderRepository.findAll();
for(OrderCoins order : orders) {
    try {
        Map<String,String> orderDetail = JsonUtil.jsonString2Map(authAPI.getOrderSignDetail(order.getUuid()));

        Date orderDate = order.getOrderDate();
        Date now = new Date();

        long diffSec = (orderDate.getTime() - now.getTime()) / 1000;
        final int DELETE_STANDARD = 86400 * 3;    // 3일
        if(diffSec >= DELETE_STANDARD) {
            authAPI.deleteOrder(order.getUuid());
            orderRepository.delete(order);
            SlackClient.sendSlack(order.getCoin() + " 3일 경과로 인한 주문 취소.");
            continue;
        }

        String remainVolume = Optional.ofNullable(orderDetail).map(o -> o.get(APIKeyConst.REMAINING_VOLUME)).orElseThrow(() -> new NullPointerException());
        if(Double.parseDouble(String.valueOf(remainVolume)) == 0.0) {
            orderRepository.delete(order);
        }

        if(orderDetail.get(APIKeyConst.SIDE).equals("bid")) {
            // 매수건
            MyCoins myCoin = walletRepository.findByCoin(order.getCoin());
            if(Objects.isNull(myCoin)) {
                // buy 건
                if(Objects.isNull(orderDetail.get(APIKeyConst.TRADES))) {
                    // 체결된게 없는 것
                    continue;
                } else {
                    // 하나라도 체결이 되었다면
                    database.saveWalletTable(MyCoins.builder()
                            .coin(order.getCoin())
                            .buyCount(1)
                            .firstBuyKRW((long) (Double.parseDouble(orderDetail.get(APIKeyConst.PRICE)) * Double.parseDouble(orderDetail.get(APIKeyConst.VOLUME))))
                            .build());
                }
            } else {
                // reBuy 건
                myCoin.setBuyCount(myCoin.getBuyCount() + 1);
                database.saveWalletTable(myCoin);
            }
        } else {
            // 매도건
            MyCoins myCoin = walletRepository.findByCoin(order.getCoin());
            database.deleteWalletTable(myCoin);
        }
    } catch (Exception e) {
        log.error ("exception msg", e);
        SlackClient.sendSlack(e.toString());
    }
}

매수주문과 매도주문 시에 데이터베이스에 쌓은 내용을 기반으로 주문이 체결되었는지 주기적으로 확인이 필요하다.

그래야 진짜 체결이 된 것일테고 내가 코인을 사거나 판게 되니까.

DB에서 나의 매수/매도 주문건을 UUID에 맵핑이 되어 있을 것이다. 체결이 안된 리스트를 전부 READ해와서 각각 매수건인지 재매수건인지 매도건인지 등 분기할 수밖에 없다. 이 부분에 대해서 SQL query(JSP) where절로 다른 리스트를 가지고와서 각각 다른 스레드에서 돌릴까도 생각해보았지만 그러기엔 1-2line을 수행만 해도 되는 부분이라 그냥 분기치는 지저분한 스타일로 개발했다ㅠㅠ(전혀 클린하지 않은 코드 ㅋㅋ)

반응형
Comments