https://github.com/SongHeeJae/cointrader
전체 소스코드는 위 링크에서 찾아볼 수 있습니다.
전반적인 구조만 제시할 뿐이고, 알고리즘 전략 등은 직접 개선할 수 있습니다.
임의로 수정하며 사용했다보니, 코드 정리가 안된 부분은 양해 바랍니다.
업비트 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);
}
이를 이용하여 종목과 캔들을 조회하고, 주문을 넣을 수 있습니다.
각 값에 대한 세세한 설명은 문서에서 찾아볼 수 있습니다.
요청/응답에 필요한클래스도 작성해주고,
매매 전략 설정에 대한 상수 클래스를 선언해줍시다.
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시간 자동매매를 수행할 수 있을 것입니다.
나만의 알고리즘을 실현해봅시다.
전체 코드는 리포지토리를 참고하시길 바랍니다.
'코인 자동매매 매크로' 카테고리의 다른 글
스프링부트 코인 자동 매매 매크로(4) - 마치며 (1) | 2023.01.07 |
---|---|
스프링부트 코인 자동 매매 매크로(2) - Binance (0) | 2023.01.07 |
스프링부트 코인 자동 매매 매크로(1) - 프로젝트 설정 (1) | 2023.01.07 |