반응형

https://github.com/SongHeeJae/cointrader

 

GitHub - SongHeeJae/cointrader

Contribute to SongHeeJae/cointrader development by creating an account on GitHub.

github.com

전체 소스코드는 위 링크에서 찾아볼 수 있습니다.

전반적인 구조만 제시할 뿐이고, 알고리즘 전략 등은 직접 개선할 수 있습니다.

임의로 수정하며 사용했다보니, 코드 정리가 안된 부분은 양해 바랍니다.

 

업비트 API 문서

https://docs.upbit.com/reference/

 

바이낸스 API 문서

https://binance-docs.github.io/apidocs/futures/en/

 

 

지난 시간에는 바이낸스 자동 매매에 대해서 알아보았습니다.

업비트도 바이낸스와 동일하게 문서를 참고하면 쉽게 만들어낼 수 있습니다.

 

이번에도 몇 가지 액션에 대해 업비트 API들을 요청해보겠습니다.

 

이번에는 upbit.client 패키지에 UpbitClient를 작성해줍니다.

package kukekyakya.cointrader.upbit.client;

import java.util.List;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import feign.Headers;
import kukekyakya.cointrader.config.LogConfig;
import kukekyakya.cointrader.upbit.client.response.Account;
import kukekyakya.cointrader.upbit.client.response.Market;
import kukekyakya.cointrader.upbit.client.response.MinuteCandle;
import kukekyakya.cointrader.upbit.client.response.OrderResponse;
import kukekyakya.cointrader.upbit.client.response.Ticker;

/**
 * UpbitClient
 *
 * @author heejae.song
 * @since 2022. 08. 27.
 */
@FeignClient(
	name = "UpbitClient",
	url = "https://api.upbit.com",
	configuration = LogConfig.class
)
public interface UpbitClient {

	@GetMapping("/v1/market/all?isDetails=true")
	List<Market> readAllMarkets(@RequestHeader("Authorization") String authorization);

	@GetMapping("/v1/candles/minutes/{unit}")
	List<MinuteCandle> readAllMinuteCandle(@RequestHeader("Authorization") String authorization,
		@PathVariable Integer unit, @RequestParam String market, @RequestParam Integer count);

	@GetMapping("/v1/accounts")
	List<Account> readAllAccounts(@RequestHeader("Authorization") String authorization);

	@PostMapping(value = "/v1/orders")
	@Headers("content-type: application/json; charset=utf-8")
	OrderResponse order(@RequestHeader("Authorization") String authorization,
		@RequestParam(value = "market", required = false) String market,
		@RequestParam(value = "side", required = false) String side,
		@RequestParam(value = "volume", required = false) String volume,
		@RequestParam(value = "price", required = false) String price,
		@RequestParam(value = "ord_type", required = false) String ordType);

	@GetMapping("/v1/ticker") // 여러건은 반점 구분
	List<Ticker> readTickers(@RequestHeader("Authorization") String authorization,
		@RequestParam("markets") String market);
}

이를 이용하여 종목과 캔들을 조회하고, 주문을 넣을 수 있습니다.

각 값에 대한 세세한 설명은 문서에서 찾아볼 수 있습니다.

 

https://github.com/SongHeeJae/cointrader/tree/master/src/main/java/kukekyakya/cointrader/upbit/client

요청/응답에 필요한클래스도 작성해주고,

매매 전략 설정에 대한 상수 클래스를 선언해줍시다.

package kukekyakya.cointrader.upbit;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * UpbitConstants
 *
 * @author heejae.song
 * @since 2022. 09. 10.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class UpbitConstants {
	public static final String ACCESS_KEY = "";
	public static final String SECRET_KEY = "";
	public static final String TOKEN_TYPE = "Bearer ";
}

필요한 key는 업비트 사이트에서 발급받을 수 있습니다.

바이낸스와 동일하게 이에 대한 부분은 생략하겠습니다.

 

 

 

UbpitClient를 이용하여 실질적인 처리를 담당하는 UpblitClientService를 작성해보겠습니다.

package kukekyakya.cointrader.upbit.client;

import static java.util.function.Function.*;
import static java.util.stream.Collectors.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

import com.fasterxml.jackson.databind.ObjectMapper;

import kukekyakya.cointrader.upbit.client.response.Account;
import kukekyakya.cointrader.upbit.client.response.Market;
import kukekyakya.cointrader.upbit.client.response.MinuteCandle;
import kukekyakya.cointrader.upbit.client.response.OrderResponse;
import kukekyakya.cointrader.upbit.client.response.Ticker;
import kukekyakya.cointrader.upbit.service.TokenService;
import lombok.RequiredArgsConstructor;

/**
 * UpbitClientService
 *
 * @author heejae.song
 * @since 2022. 08. 27.
 */
// @Service
@RequiredArgsConstructor
public class UpbitClientService {
	private final UpbitClient upbitClient;
	private final TokenService tokenService;

	public List<Market> readAllMarkets() {
		try {
			String token = tokenService.gen();
			return upbitClient.readAllMarkets(token);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return List.of();
	}

	public List<MinuteCandle> readAllMinuteCandles(Integer unit, String market, Integer count) {
		try {
			String token = tokenService.gen();
			return upbitClient.readAllMinuteCandle(token, unit, market, count);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return List.of();
	}

	public List<Account> readAllAccounts() {
		try {
			String token = tokenService.gen();
			return upbitClient.readAllAccounts(token);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return List.of();
	}

	public Map<String, Account> readAllAccountsToMap() {
		return readAllAccounts().stream()
			.collect(toMap(Account::getCurrency, identity()));
	}

	public List<Ticker> readTickers(String market) {
		try {
			Map<String, String> params = new HashMap<>();
			params.put("markets", market);

			String token = tokenService.gen(params);
			return upbitClient.readTickers(token, market);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return List.of();
	}

	public OrderResponse buy(String market, String price) {
		try {
			Map<String, String> params = new HashMap<>();
			params.put("market", market);
			params.put("side", OrderParam.BUY.getSide());
			params.put("price", price);
			params.put("ord_type", OrderParam.BUY.getOrdType());

			String token = tokenService.gen(params);
			return upbitClient.order(token, market, OrderParam.BUY.getSide(),
				null, price, OrderParam.BUY.getOrdType());
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public OrderResponse sell(String market, String volume) {
		Map<String, String> params = new HashMap<>();
		params.put("market", market);
		params.put("side", OrderParam.SELL.getSide());
		params.put("volume", volume);
		params.put("ord_type", OrderParam.SELL.getOrdType());

		String token = tokenService.gen(params);


		try {
			CloseableHttpClient client = HttpClientBuilder.create().build();
			HttpPost request = new HttpPost("https://api.upbit.com/v1/orders");
			request.setHeader("Content-Type", "application/json");
			request.addHeader("Authorization", token);
			request.setEntity(new StringEntity(new ObjectMapper().writeValueAsString(params)));

			CloseableHttpResponse response = client.execute(request);
			HttpEntity entity = response.getEntity();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}

각 동작을 조합하여 적절한 알고리즘 전략을 만들어낼 수 있을 것입니다.

 

업비트 API는 요청 Authorization 헤더에 토큰도 전달해줘야하는데요,

이를 생성해주는 TokenService도 살펴보겠습니다.

package kukekyakya.cointrader.upbit.service;

import static kukekyakya.cointrader.upbit.UpbitConstants.*;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.springframework.stereotype.Service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import lombok.extern.slf4j.Slf4j;

/**
 * TokenService
 *
 * @author heejae.song
 * @since 2022. 08. 27.
 */
@Slf4j
// @Service
public class TokenService {
	public String gen() {
		return TOKEN_TYPE + JWT.create()
			.withClaim("access_key", ACCESS_KEY)
			.withClaim("nonce", UUID.randomUUID().toString())
			.sign(Algorithm.HMAC256(SECRET_KEY));
	}

	public String gen(Map<String, String> params) {
		return TOKEN_TYPE + JWT.create()
			.withClaim("access_key", ACCESS_KEY)
			.withClaim("nonce", UUID.randomUUID().toString())
			.withClaim("query_hash", getQueryHash(params))
			.withClaim("query_hash_alg", "SHA512")
			.sign(Algorithm.HMAC256(SECRET_KEY));
	}

	private String getQueryHash(Map<String, String> params) {
		List<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]));

		try {
			MessageDigest md = MessageDigest.getInstance("SHA-512");
			md.update(queryString.getBytes("utf8"));
			return String.format("%0128x", new BigInteger(1, md.digest()));
		} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
			// empty
		}
		throw new RuntimeException("get query hash error.");
	}
}

토큰을 생성하는 법도 알게 되었습니다.

 

 

UpbitClientService를 이용하여 자동매매를 하는 예시 코드를 살펴보겠습니다.

다음 코드는 예시일 뿐이며,

액션을 적절하게 조립하고, 필요한 액션은 문서를 통해 추가 정의하여, 원하는 매매 전략을 수립할 수 있을 것입니다.

package kukekyakya.cointrader.upbit.service;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.springframework.scheduling.annotation.Async;

import kukekyakya.cointrader.upbit.client.UpbitClientService;
import kukekyakya.cointrader.upbit.client.response.Account;
import kukekyakya.cointrader.upbit.client.response.Market;
import kukekyakya.cointrader.upbit.client.response.MinuteCandle;
import kukekyakya.cointrader.upbit.client.response.Ticker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * CoinTradeService
 *
 * @author heejae.song
 * @since 2022. 08. 27.
 */
@Slf4j
// @Service
@RequiredArgsConstructor
public class UpbitCoinTradeService {
	private final UpbitClientService upbitClientService;

	private static final int MINUTE_UNIT = 5;
	private static final int COUNT = 1;
	private static final double DIFF_PERCENT = 0.01; // 봉의 시가, 현재 종가 기준으로 이 정도 이상 차이나는 경우
	private static final double SELL_PERCENT_P = 0.003; // 양일 떄 판매 퍼센트 설정
	private static final double SELL_PERCENT_M = 0.005; // 음일 떄 판매 퍼센트 설정
	private static final long MINUS_CANDLE_COUNT = 12; // COUNT 보다 작아야한다.

	private static final int START_BUY_MONEY = 10000;
	private static final double MINIMUM_PRICE = 5; // 구매하고자 하는 코인의 최소 개당 금액

	// private static final MyWallet MY_WALLET =

	private static final Map<String, LocalDateTime> BUY_LOCK_DATE_TIME = new ConcurrentHashMap<>();

	private static final AtomicBoolean FLAG = new AtomicBoolean(false);

	private static final String MY_CURRENCY = "KRW";

	@Async
	public void start() {
		if(FLAG.get()) {
			return;
		}

		// upbitClientService.sell("KRW-SAND", "7.0001");

		// if(true) return;

		log.info("coin trade start");

		FLAG.set(true);

		while(FLAG.get()) {
			Set<String> alreadyBuyCoin = new HashSet<>();

			List<Account> accounts = upbitClientService.readAllAccounts(); // 구매한 목록

			double myMoney = -1;
			for (Account account : accounts) {
				if(account.getCurrency().equals(MY_CURRENCY)) {
					myMoney = Double.valueOf(account.getBalance());
					break;
				}
			}

			log.info("현재 잔액 : {}", myMoney);

			for (Account account : accounts){
				if(account.getCurrency().equals(MY_CURRENCY)) continue;

				String market = account.getUnitCurrency() + "-" + account.getCurrency();
				Ticker ticker = upbitClientService.readTickers(market).get(0);
				Double tradePrice = ticker.getTradePrice();

				double percent = calPercent(Double.valueOf(account.getAvgBuyPrice()), tradePrice);
				if((percent > 0 && percent >= SELL_PERCENT_P) || (percent < 0 && percent <= -SELL_PERCENT_M)) {
					log.info("판매 완료 market = {}, percent = {}", market, percent);
					upbitClientService.sell(market, account.getBalance());
					BUY_LOCK_DATE_TIME.put(market, LocalDateTime.now().plusMinutes(MINUTE_UNIT + 1));
				} else {
					alreadyBuyCoin.add(market);
				}

				sleep(130);
			}

			if(myMoney <= START_BUY_MONEY) { // 판매 금액을 다시 추가하진 않는다.
				sleep(100);
				continue;
			}

			List<Market> markets = upbitClientService.readAllMarkets();
			for (Market market : markets) {
				if (market.getMarket().indexOf("KRW") == -1)
					continue;

				sleep(100);
				List<MinuteCandle> minuteCandles = upbitClientService.readAllMinuteCandles(MINUTE_UNIT,
					market.getMarket(), COUNT);
				if (minuteCandles.size() < COUNT)
					continue;

				MinuteCandle newestCandle = minuteCandles.get(0);
				MinuteCandle oldestCandle = minuteCandles.get(minuteCandles.size() - 1);
				double percent = calPercent(oldestCandle.getOpeningPrice(), newestCandle.getTradePrice());
				long count = countMinusCandle(minuteCandles);

				// 구매
				if (minuteCandles.get(0).getTradePrice() <= MINIMUM_PRICE)
					continue;
				if (percent >= DIFF_PERCENT) { // TODO 거래량도 확인
					LocalDateTime lockDateTime = BUY_LOCK_DATE_TIME.get(market.getMarket());
					if(lockDateTime != null) {
						if(lockDateTime.isBefore(LocalDateTime.now())) {
							BUY_LOCK_DATE_TIME.remove(market.getMarket());
						} else {
							continue;
						}
					}

					if(alreadyBuyCoin.contains(market.getMarket())) continue;

					try {
						log.info("구매 완료 market = {}, percent = {}", market.getMarket(), percent);
						upbitClientService.buy(market.getMarket(), String.valueOf(START_BUY_MONEY));
					} catch (Exception e) {
						log.error("구매 실패 market = {}, percent = {}", market.getMarket(), percent, e);
					}
					sleep(30);
				}
			}

			sleep(100);
		}

		log.info("coin trade end");
	}

	private double calPercent(double start, double end) {
		return end > start ? (end / start) - 1 : -((start / end) - 1);
	}

	private long countMinusCandle(List<MinuteCandle> minuteCandles) {
		return minuteCandles.stream().filter(MinuteCandle::isMinusCandle).count();
	}

	private void sleep(long milliseconds) {
		try {
			TimeUnit.MILLISECONDS.sleep(milliseconds);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void end() {
		FLAG.set(false);
	}

}

WinRate나 MyWallet은, 승률을 계산하거나 임의로 테스트해볼 수 있는 클래스인데요,

코드에 대한 자세한 설명은 생략하겠습니다.

 

 

매매를 시작할 수 있는 컨트롤러 클래스는 이전 글에서 다루었으므로 생략하겠습니다.

별도 서버에 프로그램을 올려두면, 24시간 자동매매를 수행할 수 있을 것입니다.

나만의 알고리즘을 실현해봅시다.

 

전체 코드는 리포지토리를 참고하시길 바랍니다.

반응형

+ Recent posts