goldenChat base source add
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.goldenchart.upbit;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 업비트 Open API JWT (query_hash + HS512).
|
||||
*/
|
||||
public final class UpbitJwtSigner {
|
||||
|
||||
private UpbitJwtSigner() {}
|
||||
|
||||
public static String sign(String accessKey, String secretKey, Map<String, String> params) {
|
||||
String nonce = UUID.randomUUID().toString();
|
||||
var builder = JWT.create()
|
||||
.withClaim("access_key", accessKey)
|
||||
.withClaim("nonce", nonce);
|
||||
|
||||
if (params != null && !params.isEmpty()) {
|
||||
String query = params.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("&"));
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-512");
|
||||
byte[] digest = md.digest(query.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
String queryHash = String.format("%0128x", new BigInteger(1, digest));
|
||||
builder.withClaim("query_hash", queryHash);
|
||||
builder.withClaim("query_hash_alg", "SHA512");
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("query_hash 생성 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.sign(Algorithm.HMAC512(secretKey));
|
||||
}
|
||||
|
||||
/** GET 등 쿼리 파라미터 없는 요청 */
|
||||
public static String signNoQuery(String accessKey, String secretKey) {
|
||||
return sign(accessKey, secretKey, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.goldenchart.upbit;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* 업비트 주문·계좌 REST API (실거래).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class UpbitOrderApiClient {
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${upbit.api.base-url:https://api.upbit.com}")
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* 시장가 매수 (KRW 금액 지정).
|
||||
*/
|
||||
public JsonNode placeMarketBuy(String accessKey, String secretKey,
|
||||
String market, double krwAmount) {
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("market", market);
|
||||
params.put("side", "bid");
|
||||
params.put("price", String.valueOf((long) Math.floor(krwAmount)));
|
||||
params.put("ord_type", "price");
|
||||
return postOrder(accessKey, secretKey, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시장가 매도 (수량 지정).
|
||||
*/
|
||||
public JsonNode placeMarketSell(String accessKey, String secretKey,
|
||||
String market, double volume) {
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("market", market);
|
||||
params.put("side", "ask");
|
||||
params.put("volume", formatVolume(volume));
|
||||
params.put("ord_type", "market");
|
||||
return postOrder(accessKey, secretKey, params);
|
||||
}
|
||||
|
||||
/** 보유 KRW 잔고 */
|
||||
public double getKrwBalance(String accessKey, String secretKey) {
|
||||
JsonNode accounts = getAccounts(accessKey, secretKey);
|
||||
if (accounts == null || !accounts.isArray()) return 0;
|
||||
for (JsonNode a : accounts) {
|
||||
if ("KRW".equals(a.path("currency").asText())) {
|
||||
return a.path("balance").asDouble(0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 종목 보유 수량 */
|
||||
public double getCoinBalance(String accessKey, String secretKey, String market) {
|
||||
String currency = market.startsWith("KRW-") ? market.substring(4) : market;
|
||||
JsonNode accounts = getAccounts(accessKey, secretKey);
|
||||
if (accounts == null || !accounts.isArray()) return 0;
|
||||
for (JsonNode a : accounts) {
|
||||
if (currency.equals(a.path("currency").asText())) {
|
||||
return a.path("balance").asDouble(0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public JsonNode getAccounts(String accessKey, String secretKey) {
|
||||
String token = UpbitJwtSigner.signNoQuery(accessKey, secretKey);
|
||||
try {
|
||||
String body = webClientBuilder.build()
|
||||
.get()
|
||||
.uri(baseUrl + "/v1/accounts")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
return objectMapper.readTree(body);
|
||||
} catch (WebClientResponseException e) {
|
||||
log.warn("[UpbitAPI] accounts {}: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new UpbitApiException(e.getStatusCode().value(), e.getResponseBodyAsString());
|
||||
} catch (Exception e) {
|
||||
throw new UpbitApiException(0, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode postOrder(String accessKey, String secretKey, Map<String, String> params) {
|
||||
String token = UpbitJwtSigner.sign(accessKey, secretKey, params);
|
||||
Map<String, Object> body = new LinkedHashMap<>(params);
|
||||
try {
|
||||
String response = webClientBuilder.build()
|
||||
.post()
|
||||
.uri(baseUrl + "/v1/orders")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(body)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
return objectMapper.readTree(response);
|
||||
} catch (WebClientResponseException e) {
|
||||
log.warn("[UpbitAPI] order {}: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new UpbitApiException(e.getStatusCode().value(), e.getResponseBodyAsString());
|
||||
} catch (Exception e) {
|
||||
throw new UpbitApiException(0, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatVolume(double v) {
|
||||
if (v >= 1) return String.format("%.8f", v).replaceAll("0+$", "").replaceAll("\\.$", "");
|
||||
return String.format("%.8f", v);
|
||||
}
|
||||
|
||||
public static class UpbitApiException extends RuntimeException {
|
||||
public final int status;
|
||||
public UpbitApiException(int status, String message) {
|
||||
super("Upbit API " + status + ": " + message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user