반응형

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/

 

문서 링크를 보시면 알겠지만, future를 기반으로 작성된 내용입니다.

포지션을 잡고 매매하는 방식으로 동작합니다.

세세한 API 스펙은 문서에서 찾아볼 수 있습니다.

 

바이낸스 API 문서를 통해 종목을 조회하고 주문을 넣어보겠습니다.

binance.client 패키지에 BinanceClient를 작성해줍니다.

메소드명을 통해 각 기능에 대해서 확인할 수 있습니다.

package kukekyakya.cointrader.binance.client;

import java.util.List;

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

import kukekyakya.cointrader.config.LogConfig;
import kukekyakya.cointrader.binance.client.response.BinanceBalance;
import kukekyakya.cointrader.binance.client.response.BinanceOrder;
import kukekyakya.cointrader.binance.client.response.BinancePosition;
import kukekyakya.cointrader.binance.client.response.BinanceSimpleTicker;
import kukekyakya.cointrader.binance.client.response.ChangeLeverageResponse;
import kukekyakya.cointrader.binance.client.response.ChangeMarginResponse;

/**
 * BinanceClient
 *
 * @author heejae.song
 * @since 2022. 09. 07.
 */
@FeignClient(
	name = "BinanceClient",
	url = "https://fapi.binance.com",
	configuration = LogConfig.class
)
public interface BinanceClient {

	@GetMapping("/fapi/v1/klines")
	List<List<Object>> readCandles(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("interval") String interval,
		@RequestParam("limit") Integer limit
	);

	@GetMapping("/fapi/v2/balance")
	List<BinanceBalance> readAllUserBalances(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature);

	@GetMapping("/fapi/v1/ticker/price")
	List<BinanceSimpleTicker> readAllTickers(
		@RequestHeader("X-MBX-APIKEY") String apiKey);

	@GetMapping("/fapi/v1/ticker/price")
	BinanceSimpleTicker readTicker(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol);

	@PostMapping("/fapi/v1/order")
	BinanceOrder buyOrder(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("side") String side,
		@RequestParam("positionSide") String positionSide,
		@RequestParam("type") String type,
		@RequestParam("quantity") Double quantity,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature);

	@PostMapping("/fapi/v1/order")
	BinanceOrder sellOrder(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("side") String side,
		@RequestParam("positionSide") String positionSide,
		@RequestParam("type") String type,
		@RequestParam("closePosition") String closePosition,
		@RequestParam("stopPrice") String stopPrice,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature);

	@DeleteMapping("/fapi/v1/order")
	BinanceOrder cancelOrder(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature);

	@PostMapping("/fapi/v1/leverage")
	ChangeLeverageResponse changeLeverage(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("leverage") Integer leverage,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature
	);

	@PostMapping("/fapi/v1/marginType")
	ChangeMarginResponse changeMarginType(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("marginType") String marginType,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature
	);

	@PostMapping("/fapi/v1/positionSide/dual")
	void changePositionMode(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("dualSidePosition") String dualSidePosition,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature
	);

	@GetMapping("/fapi/v2/positionRisk")
	List<BinancePosition> readAllPositions(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature
	);

	@GetMapping("/fapi/v2/positionRisk")
	BinancePosition readPosition(
		@RequestHeader("X-MBX-APIKEY") String apiKey,
		@RequestParam("symbol") String symbol,
		@RequestParam("timestamp") String timestamp,
		@RequestParam("signature") String signature
	);
}

종목/캔들 등을 조회하고, 포지션/레버리지를 변경하며, 매수/매도 주문을 넣을 수 있는 API를 작성해주었습니다.

 

 

각 요청에 대한 응답 클래스들도 작성해줍니다.

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

위 링크를 참고 바랍니다.

 

 

키 저장, 매매 등에 사용할 상수 클래스도 선언해주겠습니다.

package kukekyakya.cointrader.binance;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import kukekyakya.cointrader.binance.client.CandleInterval;
import kukekyakya.cointrader.binance.client.MarginType;
import kukekyakya.cointrader.binance.client.PositionSide;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * BinaneConstants
 *
 * @author heejae.song
 * @since 2022. 09. 10.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class BinaneConstants {
	public static final String API_KEY = "";
	public static final String SECRET_KEY = "";

	public static final CandleInterval CANDLE_INTERVAL = CandleInterval.MINUTE_15;
	public static final int COUNT = 10; // {CANDLE_INTERVAL}봉 COUNT개 조회

	public static final double DIFF_PERCENT = 0.015; // 봉의 시가, 현재 종가 기준으로 이 정도 이상 차이나는 경우
	public static final double SELL_PERCENT_P = 0.02; // 양(이득)일 때 판매 퍼센트 설정
	public static final double SELL_PERCENT_M = 0.012; // 음(손해)일 때 판매 퍼센트 설정
	public static final long MINUS_CANDLE_COUNT = 12; // COUNT 보다 작아야한다.

	public static final double START_BUY_MONEY = 300; // USDT
	public static final double MINIMUM_PRICE = 0; // 구매하고자 하는 코인의 최소 개당 금액

	public static final MarginType MARGIN_TYPE = MarginType.ISOLATED;
	public static final AtomicInteger LEVERAGE = new AtomicInteger(10);
	public static final PositionSide POSITION_SIDE = PositionSide.LONG;

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

	public static final String MY_CURRENCY = "USDT";

	public static final AtomicBoolean IS_SHORT = new AtomicBoolean(false);
}

 

API_KEY, SECRET_KEY는 바이낸스 사이트에서 발급받을 수 있습니다.

이에 대한 부분은 생략하겠습니다.

필요한 상수 값들은, 본인의 알고리즘 전략에 알맞게 추가 정의할 수 있습니다.

제 기준으로 정의된 점 참고 부탁드립니다.

 

MarginType, PositionSide 등에 대한 정의는,

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

위 링크에서 찾아볼 수 있습니다.

 

 

BinanceClient를 이용하여, 실질적인 조회/주문 등을 처리하는 BinanceClientService를 살펴보겠습니다.

package kukekyakya.cointrader.binance.client;

import static kukekyakya.cointrader.binance.BinaneConstants.*;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.springframework.stereotype.Service;

import kukekyakya.cointrader.binance.client.response.BinanceBalance;
import kukekyakya.cointrader.binance.client.response.BinanceCandle;
import kukekyakya.cointrader.binance.client.response.BinanceOrder;
import kukekyakya.cointrader.binance.client.response.BinancePosition;
import kukekyakya.cointrader.binance.client.response.BinanceSimpleTicker;
import kukekyakya.cointrader.binance.client.response.ChangeLeverageResponse;
import kukekyakya.cointrader.binance.client.response.ChangeMarginResponse;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * BinanceClientService
 *
 * @author heejae.song
 * @since 2022. 09. 07.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class BinanceClientService {
	private final BinanceClient binanceClient;

	public List<BinanceCandle> readCandles(String symbol, CandleInterval interval, Integer limit) {
		try {
			List<List<Object>> result = binanceClient.readCandles(API_KEY, symbol,
				interval.getValue(), limit);

			return result.stream()
				.map(list -> BinanceCandle.builder()
					.openTime((Long)list.get(0))
					.openPrice((String)list.get(1))
					.highPrice((String)list.get(2))
					.lowPrice((String)list.get(3))
					.closePrice((String)list.get(4))
					.volume((String)list.get(5))
					.closeTime((Long)list.get(6))
					.quoteAssetVolume((String)list.get(7))
					.numberOfTrades((Integer)list.get(8))
					.takerBuyBaseAssetVolume((String)list.get(9))
					.takerBuyQuoteAssetVolume((String)list.get(10))
					.ignoreField((String)list.get(11))
					.build())
				.collect(Collectors.toList());
		} catch (Exception e) {
			e.printStackTrace();
			return List.of();
		}
	}

	public List<BinanceBalance> readUserBalances() {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(new QueryParam("timestamp", timestamp));
		String queryString = getQueryString(params);
		try {
			return binanceClient.readAllUserBalances(API_KEY, timestamp, getSignature(queryString));
		} catch (Exception e) {
			e.printStackTrace();
			return List.of();
		}
	}

	public List<BinanceSimpleTicker> readAllTickers() {
		try {
			return binanceClient.readAllTickers(API_KEY);
		} catch (Exception e) {
			e.printStackTrace();
			return List.of();
		}
	}

	public BinanceSimpleTicker readTicker(String symbol) {
		try {
			return binanceClient.readTicker(API_KEY, symbol);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public List<BinancePosition> readAllPositions() {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(new QueryParam("timestamp", timestamp));

		String queryString = getQueryString(params);

		try {
			return binanceClient.readAllPositions(
				API_KEY,
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			e.printStackTrace();
			return List.of();
		}
	}

	public BinancePosition readPosition(String symbol) {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(new QueryParam("symbol", symbol), new QueryParam("timestamp", timestamp));
		String queryString = getQueryString(params);
		try {
			return binanceClient.readPosition(
				API_KEY,
				symbol,
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public BinanceOrder buyOrder(String symbol, OrderSide side, OrderType type, Double quantity) {
		quantity = Double.valueOf(quantity.intValue());
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(
			new QueryParam("symbol", symbol),
			new QueryParam("side", side.getValue()),
			new QueryParam("positionSide", PositionSide.BOTH.getValue()),
			new QueryParam("type", type.getValue()),
			new QueryParam("quantity", String.valueOf(quantity)),
			new QueryParam("timestamp", timestamp));

		String queryString = getQueryString(params);

		try {
			return binanceClient.buyOrder(
				API_KEY,
				symbol,
				side.getValue(),
				PositionSide.BOTH.getValue(),
				type.getValue(),
				quantity,
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public BinanceOrder cancelOrder(String symbol) {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(
			new QueryParam("symbol", symbol),
			new QueryParam("timestamp", timestamp));

		String queryString = getQueryString(params);

		try {
			return binanceClient.cancelOrder(
				API_KEY,
				symbol,
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			return null;
		}
	}

	// close position
	public BinanceOrder sellOrder(String symbol, OrderSide side, OrderType type, Double stopPrice, String tickSize) {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(
			new QueryParam("symbol", symbol),
			new QueryParam("side", side.getValue()),
			new QueryParam("positionSide", PositionSide.BOTH.getValue()),
			new QueryParam("type", type.getValue()),
			new QueryParam("closePosition", "true"),
			new QueryParam("stopPrice", String.format(tickSize, stopPrice)),
			new QueryParam("timestamp", timestamp));

		String queryString = getQueryString(params);

		try {
			return binanceClient.sellOrder(
				API_KEY,
				symbol,
				side.getValue(),
				PositionSide.BOTH.getValue(),
				type.getValue(),
				"true",
				String.format(tickSize, stopPrice),
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException ex) { }
			log.info("판매 주문 실패 symbol = {}, side = {}, type = {}, stopPrice = {}, tickSize = {}", symbol, side.getValue(),
				type.getValue(), stopPrice, tickSize);

			boolean precisionError = e.getMessage().indexOf("1111") != -1;
			String nextTickSize = precisionError ? nextTickSize(tickSize) : tickSize;

			if(OrderSide.BUY == side) { // short
				if(OrderType.TAKE_PROFIT_MARKET == type) { // TAKE_PROFIT_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 0.9999, nextTickSize);
				} else { // STOP_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 1.0001, nextTickSize);
				}
			} else { // long
				if(OrderType.TAKE_PROFIT_MARKET == type) { // TAKE_PROFIT_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 1.0001, nextTickSize);
				} else { // STOP_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 0.9999, nextTickSize);
				}
			}

		}
	}

	private String nextTickSize(String tickSize) {
		if("%.8f".equalsIgnoreCase(tickSize)) {
			return "%.7f";
		} else if("%.7f".equalsIgnoreCase(tickSize)) {
			return "%.6f";
		} else if("%.6f".equalsIgnoreCase(tickSize)) {
			return "%.5f";
		} else if("%.5f".equalsIgnoreCase(tickSize)) {
			return "%.4f";
		} else if("%.4f".equalsIgnoreCase(tickSize)) {
			return "%.3f";
		} else if("%.3f".equalsIgnoreCase(tickSize)) {
			return "%.2f";
		} else if("%.2f".equalsIgnoreCase(tickSize)) {
			return "%.1f";
		} else {
			return "%.0f";
		}
	}

	public ChangeLeverageResponse changeLeverage(String symbol, Integer leverage) {
		try {
			String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
			List<QueryParam> params = List.of(
				new QueryParam("symbol", symbol),
				new QueryParam("leverage", String.valueOf(leverage)),
				new QueryParam("timestamp", timestamp));

			String queryString = getQueryString(params);
			return binanceClient.changeLeverage(
				API_KEY,
				symbol,
				leverage,
				timestamp,
				getSignature(queryString));
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public ChangeMarginResponse changeMarginType(String symbol, MarginType marginType) {
		try {
			String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
			List<QueryParam> params = List.of(
				new QueryParam("symbol", symbol),
				new QueryParam("marginType", marginType.getValue()),
				new QueryParam("timestamp", timestamp));

			String queryString = getQueryString(params);
			return binanceClient.changeMarginType(
				API_KEY,
				symbol,
				marginType.getValue(),
				timestamp,
				getSignature(queryString));
		} catch (Exception e) {
			// e.printStackTrace();
			return null;
		}
	}

	public void changePositionMode(boolean dualSidePosition) {
		try {
			String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
			List<QueryParam> params = List.of(
				new QueryParam("dualSidePosition", dualSidePosition ? "true" : "false"),
				new QueryParam("timestamp", timestamp));

			String queryString = getQueryString(params);
			binanceClient.changePositionMode(
				API_KEY,
				dualSidePosition ? "true" : "false",
				timestamp,
				getSignature(queryString));
		} catch (Exception e) {
			// empty
		}
	}

	private String getSignature(String queryString) {
		try {
			Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
			SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256");
			hmacSHA256.init(secretKeySpec);
			return new String(Hex.encodeHex(hmacSHA256.doFinal(queryString.getBytes())));
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
		return "";
	}

	private String getQueryString(List<QueryParam> params) {
		return params.stream()
			.map(param -> param.getKey() + "=" + param.getValue())
			.collect(Collectors.joining("&"));
	}

	@Getter
	@NoArgsConstructor
	@AllArgsConstructor
	private static class QueryParam {
		private String key;
		private String value;
	}
}

전체 소스코드는 위와 같습니다.

주요한 부분만 추가로 살펴보겠습니다.

 

 

Binance는 API 요청할 때,

@RequestParam("timestamp") String timestamp,
@RequestParam("signature") String signature

위 파라미터를 전달해야합니다.

 

다음 메소드를 활용할 수 있습니다.

자동매매를 만드는 것이 목적이므로 세세한 내용은 생략합니다.

private String getSignature(String queryString) {
	try {
		Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
		SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256");
		hmacSHA256.init(secretKeySpec);
		return new String(Hex.encodeHex(hmacSHA256.doFinal(queryString.getBytes())));
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (InvalidKeyException e) {
		e.printStackTrace();
	}
	return "";
}

private String getQueryString(List<QueryParam> params) {
	return params.stream()
		.map(param -> param.getKey() + "=" + param.getValue())
		.collect(Collectors.joining("&"));
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
private static class QueryParam {
	private String key;
	private String value;
}

 

위 메소드를 활용하여 signature를 생성하는 예시를 살펴보겠습니다.

public BinanceOrder sellOrder(String symbol, OrderSide side, OrderType type, Double stopPrice, String tickSize) {
        String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
        List<QueryParam> params = List.of(
            new QueryParam("symbol", symbol),
            new QueryParam("side", side.getValue()),
            new QueryParam("positionSide", PositionSide.BOTH.getValue()),
            new QueryParam("type", type.getValue()),
            new QueryParam("closePosition", "true"),
            new QueryParam("stopPrice", String.format(tickSize, stopPrice)),
            new QueryParam("timestamp", timestamp));

        String queryString = getQueryString(params);
        ...
        String signature = getSignature(queryString);
        ...
}

API 전달될 파라미터와 타임스탬프를 이용하여 query string을 만들고, 이를 통해 signature를 생성해줍니다.

 

 

 

판매 주문에 대한 코드를 조금 더 살펴보겠습니다.

포지션에 대한 100% 매도를 API 상에서 지원하지않기 때문에 일부 편법을 사용하였습니다.

(지금은 어떨지 모르겠네요)

	// close position
	public BinanceOrder sellOrder(String symbol, OrderSide side, OrderType type, Double stopPrice, String tickSize) {
		String timestamp = String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime());
		List<QueryParam> params = List.of(
			new QueryParam("symbol", symbol),
			new QueryParam("side", side.getValue()),
			new QueryParam("positionSide", PositionSide.BOTH.getValue()),
			new QueryParam("type", type.getValue()),
			new QueryParam("closePosition", "true"),
			new QueryParam("stopPrice", String.format(tickSize, stopPrice)),
			new QueryParam("timestamp", timestamp));

		String queryString = getQueryString(params);

		try {
			return binanceClient.sellOrder(
				API_KEY,
				symbol,
				side.getValue(),
				PositionSide.BOTH.getValue(),
				type.getValue(),
				"true",
				String.format(tickSize, stopPrice),
				timestamp,
				getSignature(queryString)
			);
		} catch (Exception e) {
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException ex) { }
			log.info("판매 주문 실패 symbol = {}, side = {}, type = {}, stopPrice = {}, tickSize = {}", symbol, side.getValue(),
				type.getValue(), stopPrice, tickSize);

			boolean precisionError = e.getMessage().indexOf("1111") != -1;
			String nextTickSize = precisionError ? nextTickSize(tickSize) : tickSize;

			if(OrderSide.BUY == side) { // short
				if(OrderType.TAKE_PROFIT_MARKET == type) { // TAKE_PROFIT_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 0.9999, nextTickSize);
				} else { // STOP_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 1.0001, nextTickSize);
				}
			} else { // long
				if(OrderType.TAKE_PROFIT_MARKET == type) { // TAKE_PROFIT_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 1.0001, nextTickSize);
				} else { // STOP_MARKET
					return sellOrder(symbol, side, type, precisionError ? stopPrice : stopPrice * 0.9999, nextTickSize);
				}
			}

		}
	}

	private String nextTickSize(String tickSize) {
		if("%.8f".equalsIgnoreCase(tickSize)) {
			return "%.7f";
		} else if("%.7f".equalsIgnoreCase(tickSize)) {
			return "%.6f";
		} else if("%.6f".equalsIgnoreCase(tickSize)) {
			return "%.5f";
		} else if("%.5f".equalsIgnoreCase(tickSize)) {
			return "%.4f";
		} else if("%.4f".equalsIgnoreCase(tickSize)) {
			return "%.3f";
		} else if("%.3f".equalsIgnoreCase(tickSize)) {
			return "%.2f";
		} else if("%.2f".equalsIgnoreCase(tickSize)) {
			return "%.1f";
		} else {
			return "%.0f";
		}
	}

sellOrder는 특정 가격에 포지션을 닫는 주문을 걸어두는 것입니다.

코인 종목에 따라서 1111 에러 코드에 대해서 소수점 관련 에러가 오는데, 포지션에 따라 소수점 범위를 좁혀가며 재주문 요청을 넣게 됩니다.

짧은 시간에 튀는 종목의 경우, 소수점을 좁혀가는 재요청 딜레이에 따라,

요청 가격에 원하던 즉시 주문 처리하는건 어려울 수 있다는 점 인지해둡시다.

이에 대한 부분도 로직에 따라 풀어나갈 순 있을 것입니다.

 

 

지금까지 바이낸스에 조회/주문을 넣는 방법에 대해 알아보았습니다.

이러한 요청 외에도 바이낸스 문서에서 지원하는 API를 이용하여 여러 액션을 수행할 수 있습니다.

위 액션들을 조합하여, 자신만의 알고리즘 전략을 수립하여 자동매매에 활용할 수 있습니다.

 

예시로 만들었던 코드를 보겠습니다.

아래 내용은 참고 자료일 뿐이고, 적절하게 수정하여 사용하면 될 것 입니다.

package kukekyakya.cointrader.binance.service;

import static kukekyakya.cointrader.binance.BinaneConstants.*;

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

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import kukekyakya.cointrader.binance.client.BinanceClientService;
import kukekyakya.cointrader.binance.client.CandleInterval;
import kukekyakya.cointrader.binance.client.OrderSide;
import kukekyakya.cointrader.binance.client.OrderType;
import kukekyakya.cointrader.binance.client.response.BinanceBalance;
import kukekyakya.cointrader.binance.client.response.BinanceCandle;
import kukekyakya.cointrader.binance.client.response.BinanceOrder;
import kukekyakya.cointrader.binance.client.response.BinancePosition;
import kukekyakya.cointrader.binance.client.response.BinanceSimpleTicker;
import kukekyakya.cointrader.utils.WinRate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * BinanceCoinTradeSecondService
 *
 * @author heejae.song
 * @since 2022. 09. 10.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class BinanceCoinTradeSecondService {
	private final BinanceClientService binanceClientService;

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

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

		log.info("coin trade start");

		FLAG.set(true);

		binanceClientService.changePositionMode(false);

		WinRate winRate = WinRate.init(2);

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

			List<BinanceBalance> balances = binanceClientService.readUserBalances();

			double myMoney = 0;
			for (BinanceBalance binanceBalance : balances) {
				String assetName = binanceBalance.getAsset();
				double balance = Double.valueOf(binanceBalance.getBalance());
				if (assetName.equalsIgnoreCase(MY_CURRENCY)) {
					myMoney += balance;
				}
			}

			// 포지션 조회
			List<BinancePosition> positions = binanceClientService.readAllPositions();
			for (BinancePosition position : positions) {
				String symbol = position.getSymbol();
				double entryPrice = Double.valueOf(position.getEntryPrice());
				double markPrice = Double.valueOf(position.getMarkPrice());
				double percent = calPercent(entryPrice, markPrice) * LEVERAGE.get();
				if(entryPrice <= 0) continue;

				myMoney -= Double.valueOf(position.getIsolatedWallet());

				alreadyBuyCoin.add(symbol);

			}

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

			if(myMoney <= START_BUY_MONEY) {
				sleep(500);
				continue;
			}

			List<BinanceSimpleTicker> tickers = binanceClientService.readAllTickers();

			for (BinanceSimpleTicker ticker : tickers) {
				String symbol = ticker.getSymbol();
				if (!symbol.substring(symbol.length() - 4).equalsIgnoreCase(MY_CURRENCY)) {
					continue;
				}

				Double price = Double.valueOf(ticker.getPrice());
				if (price <= MINIMUM_PRICE)
					continue;

				if (alreadyBuyCoin.contains(symbol)) continue;

				LocalDateTime lockDateTime = BUY_LOCK_DATE_TIME.get(symbol);
				if (lockDateTime != null) {
					if (lockDateTime.isBefore(LocalDateTime.now())) {
						BUY_LOCK_DATE_TIME.remove(symbol);
					} else {
						continue;
					}
				}

				// binanceClientService.changeLeverage(symbol, LEVERAGE.get());

				List<BinanceCandle> binanceCandles = binanceClientService.readCandles(symbol, CANDLE_INTERVAL, COUNT);
				if(binanceCandles.size() < COUNT) continue;

				BinanceCandle newestCandle = binanceCandles.get(COUNT - 1); // 마지막 캔들이 최신
				BinanceCandle middleCandle = binanceCandles.get(COUNT - 2);
				BinanceCandle oldestCandle = binanceCandles.get(COUNT - 3);
				BinanceCandle maxOldestCandle = binanceCandles.get(COUNT - 4);
				BinanceCandle maxMaxOldestCandle = binanceCandles.get(COUNT - 5); // 인덱스 범위 조심
				//
				double oldestPercent = calPercent(Double.valueOf(oldestCandle.getOpenPrice()),
					Double.valueOf(oldestCandle.getClosePrice()));
				double middlePercent = calPercent(middleCandle.getOpenPrice(), middleCandle.getClosePrice());
				double newestPercent = calPercent(newestCandle.getOpenPrice(), newestCandle.getClosePrice());

				double maxPrice = binanceCandles.get(0).getHighPrice();
				for(int i=1; i<7; i++) {
					maxPrice = Math.max(maxPrice, binanceCandles.get(i).getHighPrice());
				}

				// 급락 후 급등하는 종목 방지. 최근 10개 봉의 최고가가 최신가보다 크면, continue;
				if(maxPrice >= Double.valueOf(newestCandle.getClosePrice())) {
					continue;
				}

				double highPercent = calPercent(Double.valueOf(newestCandle.getOpenPrice()),
					Double.valueOf(newestCandle.getHighPrice()));
				double lowPercent = calPercent(Double.valueOf(newestCandle.getOpenPrice()),
					Double.valueOf(newestCandle.getLowPrice()));
				double closePercent = calPercent(Double.valueOf(newestCandle.getOpenPrice()),
					Double.valueOf(newestCandle.getClosePrice()));

				double newestCloseAndHighDiffPercent = calPercent(Double.valueOf(newestCandle.getClosePrice()),
					Double.valueOf(newestCandle.getHighPrice()));
				double newestLowAndOpenDiffPercent = calPercent(Double.valueOf(newestCandle.getLowPrice()),
					Double.valueOf(newestCandle.getOpenPrice()));

				double middleCloseAndHighDiffPercent = calPercent(Double.valueOf(middleCandle.getClosePrice()),
					Double.valueOf(middleCandle.getHighPrice()));

				List<BinanceCandle> minuteCandles = binanceClientService.readCandles(symbol, CandleInterval.MINUTE_1, 1);

				double upPercent = calPercent(Double.valueOf(middleCandle.getLowPrice()), Double.valueOf(newestCandle.getClosePrice()));
				double downPercent = calPercent(Double.valueOf(middleCandle.getHighPrice()), Double.valueOf(newestCandle.getClosePrice()));

				if (
					(
						middlePercent >= DIFF_PERCENT
							// calPercent(middleCandle.getHighPrice(), middleCandle.getClosePrice()) + newestPercent <= -0.003 &&
							// oldestCandle.isPlusCandle()
							// minuteCandles.get(0).isMinusCandle()
					)
				// ||
				// 	(
				// 		newestPercent + middlePercent >= DIFF_PERCENT
				// 			// calPercent(newestCandle.getHighPrice(), newestCandle.getClosePrice()) <= -0.003 &&
				// 			// middleCandle.isPlusCandle()
				// 			// minuteCandles.get(0).isMinusCandle()
				// 	)
				) {
					// 급등 후 급락 숏 포지션

					double stopPriceWhenUp = price * (1 - SELL_PERCENT_P);
					double stopPriceWhenDown = price * (1 + SELL_PERCENT_M);

					// log.info("구매 완료 symbol = {}, upPercent = {}", symbol, String.format("%.4f", upPercent));
					log.info("찾기 완료 symbol = {}, upPercent = {}", symbol, String.format("%.4f", upPercent));

					BinanceOrder buyOrderResponse = binanceClientService.buyOrder(symbol, OrderSide.SELL, OrderType.MARKET,
						START_BUY_MONEY * LEVERAGE.get() / price);
					if(buyOrderResponse != null) {
						binanceClientService.sellOrder(symbol, OrderSide.BUY,
							OrderType.TAKE_PROFIT_MARKET, stopPriceWhenUp, "%.8f");
						binanceClientService.sellOrder(symbol, OrderSide.BUY,
							OrderType.STOP_MARKET, stopPriceWhenDown, "%.8f");
					}

					BUY_LOCK_DATE_TIME.put(symbol, LocalDateTime.now().plusMinutes(CANDLE_INTERVAL.getMinutes()));
				}
				else if (downPercent <= -DIFF_PERCENT) {
					// 급락 롱 포지션
					double stopPriceWhenUp = price * (1 + SELL_PERCENT_P);
					double stopPriceWhenDown = price * (1 - SELL_PERCENT_M);

					log.info("구매 완료 symbol = {}, downPercent = {}", symbol, String.format("%.4f", downPercent));

					BinanceOrder buyOrderResponse = binanceClientService.buyOrder(symbol,
						OrderSide.BUY, OrderType.MARKET, START_BUY_MONEY * LEVERAGE.get() / price);
					if(buyOrderResponse != null) {
						binanceClientService.sellOrder(symbol, OrderSide.SELL,
							OrderType.TAKE_PROFIT_MARKET, stopPriceWhenUp, "%.8f");
						binanceClientService.sellOrder(symbol, OrderSide.SELL,
							OrderType.STOP_MARKET, stopPriceWhenDown, "%.8f");
					}
				}
			}
		}

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

	public void changePosition(boolean isShort) {
		IS_SHORT.set(isShort);
	}

	public void changeLeverage(int leverage) {
		LEVERAGE.set(leverage);
	}

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

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

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

자동매매가 돌아가는 와중에, 롱/숏 포지션이나 레버리지를 수정하고,

급등 후 급락, 급락 후 급등, 연속 분봉 등에 대해 확인하면서 매수/매도 주문을 넣게 됩니다.

조건에 알맞으면 매수 포지션을 잡고, 매도 포지션 또한 즉시 잡아두는 전략입니다.

 

캔들 조회는 시가/종가/현재가 등의 정보로 전달되므로,

아래 수식을 이용하여 캔들 사이의 퍼센트도 계산해봅시다.

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

 

 

위 로직들을 실행 할 수 있는 컨트롤러 클래스도 확인해봅시다.

package kukekyakya.cointrader.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import kukekyakya.cointrader.binance.client.PositionSide;
import kukekyakya.cointrader.binance.service.BinanceCoinTradeSecondService;
import kukekyakya.cointrader.binance.service.BinanceCoinTradeService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

/**
 * CoinTradeController
 *
 * @author heejae.song
 * @since 2022. 08. 27.
 */
@RestController
@RequiredArgsConstructor
public class CoinTradeController {
	private final BinanceCoinTradeSecondService binanceCoinTradeService;

	@PostMapping("/trade/start")
	public void start() {
		new Thread(() -> binanceCoinTradeService.start()).start();
	}

	@PostMapping("/trade/end")
	public void end() {
		binanceCoinTradeService.end();
	}

	@PostMapping("/trade/change-position")
	public void changePosition(@RequestBody ChangePositionRequest request) {
		PositionSide position = request.getPosition();
		if(position == PositionSide.LONG) {
			binanceCoinTradeService.changePosition(false);
		} else {
			binanceCoinTradeService.changePosition(true);
		}
	}

	@PostMapping("/trade/change-leverage")
	public void changeLeverage(@RequestBody ChangeLeverageRequest request) {
		binanceCoinTradeService.changeLeverage(request.leverage);
	}

	@Getter
	@Setter
	private static class ChangePositionRequest {
		private PositionSide position;
	}

	@Getter
	@Setter
	private static class ChangeLeverageRequest {
		private int leverage;
	}
}

/trade/start API를 호출하면, 자동매매가 시작됩니다.

레버리지나 포지션을 적절하게 설정할 수 있습니다.

 

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

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

 

 

다음 글에서는 업비트에 대해 살펴보겠습니다.

반응형

+ Recent posts