feat(newPrice): 新增OKX交易所相关功能模块
- 添加CoinTypeConvert工具类,支持币种符号转换为K线键值
- 创建DateUtils日期处理工具类,支持多种日期格式化与解析
- 新增DefaultUrls枚举类,定义交易所相关URL常量
- 添加ExchangeInfoEnum枚举类,管理交易所账户信息配置
- 创建ExchangeLoginEventService接口及实现类,处理交易所登录事件
- 新增ExchangeLoginService单例类,统一管理交易所服务实例
- 添加FebsException自定义异常类,处理系统内部错误
- 创建HttpMethod枚举类,定义HTTP请求方法类型
- 添加JSONParser工具类,提供JSON数据解析功能
- 新增KlineVo实体类,封装K线请求参数
- 创建OkHttpUtils工具类,封装OkHttp客户端请求逻辑
- 添加OKXAccount账户管理类,处理OKX账户相关操作
- 新增OKXContants常量类,定义OKX接口路径常量
- 创建ParameterChecker参数检查工具类,验证接口参数合法性
- 添加RequestBuilder请求构建工具类,生成带签名的请求头
20 files added
4 files modified
| New file |
| | |
| | | package com.xcong.excoin.common.exception; |
| | | |
| | | /** |
| | | * FEBS系统内部异常 |
| | | * |
| | | * @author MrBird |
| | | */ |
| | | public class FebsException extends RuntimeException { |
| | | |
| | | private static final long serialVersionUID = -994962710559017255L; |
| | | |
| | | public FebsException(String message) { |
| | | super(message); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | @Getter |
| | | public enum ExchangeInfoEnum { |
| | | |
| | | /** |
| | | * 模拟盘 |
| | | * String apiKey = "16c882b0-2853-46c5-8af0-5269e3218d72"; |
| | | * String passphrase = "Zh12345@@"; |
| | | * String secretKey = "A13F7BC5721DB6D1F492B0FC333D4D9C"; |
| | | */ |
| | | OKX_UAT("0769b50c-2c36-4310-8bd9-cad6bc6c9d8f", |
| | | "7AF4A574BC44907CE76BBFF91F53852D", |
| | | "Aa123456@", |
| | | false); |
| | | |
| | | private final String apiKey; |
| | | |
| | | private final String secretKey; |
| | | |
| | | private final String passphrase; |
| | | |
| | | // @Setting(label = "账户类型", order = 40, type = FieldType.SELECT, options = {"实盘账户", "模拟账户"}, optionsVal = {"true", "false"}) |
| | | private final boolean accountType; |
| | | |
| | | ExchangeInfoEnum(String apiKey, String secretKey, String passphrase, boolean accountType) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | this.accountType = accountType; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | public interface ExchangeLoginEventService { |
| | | /** |
| | | * 获取交易产品基础信息 |
| | | * 获取所有可交易产品的信息列表。 |
| | | * <br><br> |
| | | * GET /api/v5/public/instruments |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 是 产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br> |
| | | * uly -- String 可选 标的指数,仅适用于交割/永续/期权,期权必填 <br> |
| | | * instFamily -- String 否 交易品种,仅适用于交割/永续/期权 <br> |
| | | * instId -- String 否 产品ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a> |
| | | */ |
| | | String exchangeInfo(LinkedHashMap<String, Object> parameters); |
| | | |
| | | String lineHistory(LinkedHashMap<String, Object> parameters); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | |
| | | import com.xcong.excoin.common.exception.FebsException; |
| | | import com.xcong.excoin.modules.newPrice.impl.ExchangeLoginEventServiceImpl; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | public class ExchangeLoginService { |
| | | private final static Map<String, ExchangeLoginEventService> eventMap = new HashMap<>(); |
| | | |
| | | static { |
| | | for (ExchangeInfoEnum infoEnum : ExchangeInfoEnum.values()) { |
| | | eventMap.put(infoEnum.name(), new ExchangeLoginEventServiceImpl( |
| | | infoEnum.getApiKey(), |
| | | infoEnum.getSecretKey(), |
| | | infoEnum.getPassphrase(), |
| | | infoEnum.isAccountType())); |
| | | } |
| | | } |
| | | |
| | | private ExchangeLoginService() { |
| | | } |
| | | |
| | | public final static ExchangeLoginService INSTANCE = new ExchangeLoginService(); |
| | | |
| | | public static ExchangeLoginEventService getInstance(String exchangeType) { |
| | | ExchangeLoginEventService exchange = eventMap.get(exchangeType); |
| | | if (exchange == null) { |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | return exchange; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotNull; |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | @ApiModel(value = "KlineVo", description = "请求K线类") |
| | | public class KlineVo implements Serializable { |
| | | |
| | | |
| | | @NotNull |
| | | @ApiModelProperty(value = "币种", example = "BTC/USDT") |
| | | private String instId; |
| | | |
| | | @NotNull |
| | | @ApiModelProperty(value = "类型 1-币币2-合约", example = "1") |
| | | private Integer type; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class OKXAccount { |
| | | |
| | | String passPhrase; |
| | | boolean isSimluate; |
| | | public String baseUrl; |
| | | public RequestHandler requestHandler; |
| | | public boolean showLimitUsage; |
| | | |
| | | public OKXAccount(){} |
| | | |
| | | public OKXAccount(String baseUrl, String apiKey, String secretKey, String passPhrase,boolean isSimluate) { |
| | | this.baseUrl = baseUrl; |
| | | this.requestHandler = new RequestHandler(apiKey, secretKey,passPhrase); |
| | | this.isSimluate = isSimluate; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.xcong.excoin.common.exception.FebsException; |
| | | import com.xcong.excoin.modules.newPrice.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.newPrice.enums.RequestType; |
| | | import com.xcong.excoin.modules.newPrice.utils.DateUtils; |
| | | import com.xcong.excoin.modules.newPrice.utils.RequestBuilder; |
| | | import com.xcong.excoin.modules.newPrice.utils.SignUtils; |
| | | import com.xcong.excoin.modules.newPrice.utils.UrlBuilder; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import okhttp3.Request; |
| | | |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Slf4j |
| | | public class RequestHandler { |
| | | private final String apiKey; |
| | | private final String secretKey; |
| | | private final String passphrase; |
| | | |
| | | public RequestHandler(String apiKey) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = null; |
| | | this.passphrase = null; |
| | | } |
| | | |
| | | public RequestHandler(String apiKey, String secretKey, String passphrase) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | String queryString = UrlBuilder.joinQueryParameters(new StringBuilder("/api/v5/account/balance"), balanceParameters).toString(); |
| | | String balanceParameters1 = UrlBuilder.buildFullUrl("/api/v5/account/balance","" , balanceParameters, null); |
| | | System.out.println(queryString); |
| | | System.out.println(balanceParameters1); |
| | | } |
| | | |
| | | /** |
| | | * Build request based on request type and send the requests to server. |
| | | * |
| | | * @param baseUrl |
| | | * @param urlPath |
| | | * @param parameters |
| | | * @param httpMethod |
| | | * @param requestType |
| | | * @return String - response from server |
| | | */ |
| | | private String sendApiRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, RequestType requestType, boolean isSimluate) { |
| | | String fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, parameters, null); |
| | | log.debug("{} {}", httpMethod, fullUrl); |
| | | //System.out.println("sendApiRequest:fullUrl"+fullUrl); |
| | | Request request; |
| | | switch (requestType) { |
| | | case PUBLIC: |
| | | request = RequestBuilder.buildPublicRequest(fullUrl, httpMethod, isSimluate).build(); |
| | | break; |
| | | case WITH_API_KEY: |
| | | case SIGNED: |
| | | // 获取签名 |
| | | String timestamp = DateUtils.format(DateUtils.FORMAT_UTC_ISO8601, new Date(), 0); |
| | | String queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), parameters).toString(); |
| | | // String timestamp = System.currentTimeMillis()+""; |
| | | // System.out.println("timestamp:"+timestamp); |
| | | // System.out.println("timestamp:"+timestamp); |
| | | // System.out.println("secretKey:"+secretKey); |
| | | // System.out.println("httpMethod.toString():"+httpMethod.toString()); |
| | | // System.out.println("queryString:"+queryString); |
| | | // System.out.println("passphrase:"+passphrase); |
| | | // 组装body |
| | | String body = ""; |
| | | if (HttpMethod.POST.equals(httpMethod)) { |
| | | body = JSON.toJSONString(parameters); |
| | | queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), null).toString(); |
| | | fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, null, null); |
| | | } |
| | | if (HttpMethod.GET.equals(httpMethod)) { |
| | | queryString = UrlBuilder.buildFullUrl(urlPath,"" , parameters, null); |
| | | // queryString = UrlBuilder.buildFullUrl(null, urlPath, parameters, null); |
| | | } |
| | | String sign = SignUtils.signRest(secretKey, |
| | | timestamp, |
| | | httpMethod.toString(), |
| | | queryString, body); |
| | | |
| | | |
| | | request = RequestBuilder.buildApiKeyRequest(fullUrl, body, passphrase, sign, timestamp, httpMethod, apiKey,isSimluate); |
| | | |
| | | |
| | | break; |
| | | default: |
| | | throw new FebsException("[RequestHandler] Invalid request type: " + requestType); |
| | | } |
| | | return ResponseHandler.handleResponse(request, isSimluate); |
| | | } |
| | | |
| | | public String sendPublicRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.PUBLIC, isSimluate); |
| | | } |
| | | |
| | | public String sendWithApiKeyRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | if (null == apiKey || apiKey.isEmpty()) { |
| | | throw new FebsException("[RequestHandler] API key cannot be null or empty!"); |
| | | } |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.WITH_API_KEY, isSimluate); |
| | | } |
| | | |
| | | public String sendSignedRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | if (null == secretKey || secretKey.isEmpty() || null == apiKey || apiKey.isEmpty()) { |
| | | throw new FebsException("[RequestHandler] Secret key/API key cannot be null or empty!"); |
| | | } |
| | | //parameters.put("timestamp", UrlBuilder.buildTimestamp()); |
| | | //String queryString = UrlBuilder.joinQueryParameters(parameters); |
| | | //String signature = SignatureGenerator.getSignature(queryString, secretKey); |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.SIGNED, isSimluate); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | |
| | | import cn.hutool.json.JSONException; |
| | | import com.xcong.excoin.common.exception.FebsException; |
| | | import com.xcong.excoin.modules.newPrice.utils.JSONParser; |
| | | import com.xcong.excoin.modules.newPrice.utils.OkHttpUtils; |
| | | import okhttp3.Request; |
| | | import okhttp3.Response; |
| | | import okhttp3.ResponseBody; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | public final class ResponseHandler { |
| | | |
| | | private static final int HTTP_STATUS_CODE_400 = 400; |
| | | private static final int HTTP_STATUS_CODE_499 = 499; |
| | | private static final int HTTP_STATUS_CODE_500 = 500; |
| | | |
| | | private ResponseHandler() { |
| | | } |
| | | |
| | | public static String handleResponse(Request request, boolean showLimitUsage) { |
| | | try { |
| | | OkHttpUtils.builder(); |
| | | try (Response response = OkHttpUtils.okHttpClient.newCall(request).execute()) {//OkHttpUtils.builder().okHttpClient |
| | | String responseAsString = getResponseBodyAsString(response.body()); |
| | | |
| | | if (response.code() >= HTTP_STATUS_CODE_400 && response.code() <= HTTP_STATUS_CODE_499) { |
| | | throw handleErrorResponse(responseAsString, response.code()); |
| | | } else if (response.code() >= HTTP_STATUS_CODE_500) { |
| | | System.out.println("handleResponse:"+response.code()); |
| | | throw new FebsException("responseAsString-"+responseAsString+";handleResponse-"+response.code()); |
| | | } |
| | | return responseAsString; |
| | | // if (showLimitUsage) { |
| | | // return getlimitUsage(response, responseAsString); |
| | | // } else { |
| | | // return responseAsString; |
| | | // } |
| | | } |
| | | } catch (IOException | IllegalStateException e) { |
| | | e.printStackTrace(); |
| | | throw new FebsException("[ResponseHandler] OKHTTP Error: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | |
| | | private static FebsException handleErrorResponse(String responseBody, int responseCode) { |
| | | try { |
| | | String errorMsg = JSONParser.getJSONStringValue(responseBody, "msg"); |
| | | int errorCode = JSONParser.getJSONIntValue(responseBody, "code"); |
| | | return new FebsException("responseBody-"+responseBody+";errorMsg-"+errorMsg+";responseCode-"+responseCode+";errorCode-"+errorCode); |
| | | } catch (JSONException e) { |
| | | throw new FebsException("responseBody-"+responseBody+";responseCode-"+responseCode); |
| | | } |
| | | } |
| | | |
| | | private static String getResponseBodyAsString(ResponseBody body) throws IOException { |
| | | if (null != body) { |
| | | return body.string(); |
| | | } else { |
| | | return ""; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.enums; |
| | | |
| | | public final class DefaultUrls { |
| | | public static final String USDM_UAT_URL = "https://www.okx.com"; |
| | | public static final String USDM_UAT_WSS_URL = "wss://wspap.okx.com:8443"; |
| | | //public static final String USDM_UAT_WSS_URL = "wss://ws.okx.com:8443"; |
| | | //USD-M Futures |
| | | public static final String USDM_PROD_URL = "https://aws.okx.com"; |
| | | public static final String USDM_PROD_WS_URL = "wss://ws.okx.com:8443"; |
| | | //比特币买入数量 |
| | | public static final String BTC_BUYNUMBER = "0.001"; |
| | | //以太坊买入数量 |
| | | public static final String ETH_BUYNUMBER = "0.01"; |
| | | //狗狗币买入数量 |
| | | public static final String DOGE_BUYNUMBER = "100"; |
| | | /** |
| | | * 全部卖出 |
| | | */ |
| | | public static final String OPERATION_ALLSOLD = "allsold"; |
| | | |
| | | /** |
| | | * 卖出 |
| | | */ |
| | | public static final String OPERATION_SOLD = "sell"; |
| | | |
| | | /** |
| | | * 买入 |
| | | */ |
| | | public static final String OPERATION_BUY = "buy"; |
| | | |
| | | /** |
| | | * 不买入 |
| | | */ |
| | | public static final String OPERATION_NOBUY = "nobuy"; |
| | | |
| | | private DefaultUrls() { |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.enums; |
| | | |
| | | public enum HttpMethod { |
| | | POST, |
| | | GET, |
| | | PUT, |
| | | DELETE, |
| | | INVALID |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.enums; |
| | | |
| | | public enum RequestType { |
| | | PUBLIC, |
| | | WITH_API_KEY, |
| | | SIGNED |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.newPrice.ExchangeLoginEventService; |
| | | import com.xcong.excoin.modules.newPrice.OKXAccount; |
| | | import com.xcong.excoin.modules.newPrice.enums.DefaultUrls; |
| | | import com.xcong.excoin.modules.newPrice.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.newPrice.utils.OKXContants; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.*; |
| | | |
| | | @Slf4j |
| | | public class ExchangeLoginEventServiceImpl implements ExchangeLoginEventService { |
| | | |
| | | |
| | | private final com.xcong.excoin.modules.newPrice.OKXAccount OKXAccount; |
| | | private final String apiKey; |
| | | private final String secretKey; |
| | | private final String passphrase; |
| | | private final boolean accountType; |
| | | |
| | | public ExchangeLoginEventServiceImpl(String apiKey, String secretKey, String passphrase, boolean accountType) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | this.accountType = accountType; |
| | | OKXAccount = new OKXAccount( |
| | | accountType ? DefaultUrls.USDM_PROD_URL : DefaultUrls.USDM_UAT_URL, |
| | | apiKey, |
| | | secretKey, |
| | | passphrase, |
| | | !accountType); |
| | | } |
| | | |
| | | @Override |
| | | public String exchangeInfo(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.INSTRUMENTS,parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String lineHistory(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.K_LINE_HISTORY,parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.time.ZoneId; |
| | | import java.util.Date; |
| | | import java.util.TimeZone; |
| | | |
| | | public class DateUtils { |
| | | |
| | | public static final String FORMAT_Y = "yyyy"; |
| | | public static final String FORMAT_D_1 = "yyyy/MM/dd"; |
| | | public static final String FORMAT_D_2 = "yyyy-MM-dd"; |
| | | public static final String FORMAT_D_3 = "yyyyMMdd"; |
| | | public static final String FORMAT_D_4 = "yyyy.MM.dd"; |
| | | public static final String FORMAT_D = "dd"; |
| | | public static final String FORMAT_DT_1 = "yyyy/MM/dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_2 = "yyyy-MM-dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_3 = "yyyyMMdd HH:mm:ss"; |
| | | public static final String FORMAT_DT_4 = "yyyy-MM-dd HH:mm"; |
| | | public static final String FORMAT_DT_5 = "yyyy.MM.dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_6 = "yyyyMMddHHmmss"; |
| | | public static final String FORMAT_DT_7 = "yyyyMMddHH"; |
| | | public static final String FORMAT_M_1 = "yyyy/MM"; |
| | | public static final String FORMAT_M_2 = "yyyy-MM"; |
| | | public static final String FORMAT_M_3 = "yyyyMM"; |
| | | public static final String FORMAT_M = "MM"; |
| | | public static final String FORMAT_MD_1 = "MM/dd"; |
| | | public static final String FORMAT_MD_2 = "MM-dd"; |
| | | public static final String FORMAT_MD_3 = "MMdd"; |
| | | public static final String FORMAT_T_1 = "HH:mm:ss"; |
| | | public static final String FORMAT_T_2 = "HH:mm"; |
| | | public static final String FORMAT_TH = "HH"; |
| | | public static final String FORMAT_TM = "mm"; |
| | | public static final String FORMAT_TS = "ss"; |
| | | public static final String FORMAT_UTC_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; |
| | | |
| | | public static String format(String format, Date date) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | return sdf.format(date); |
| | | } |
| | | |
| | | /** |
| | | * @param format format |
| | | * @param date date |
| | | * @param timeZone 时区数字 -8, 0, 8 等 |
| | | * @return date string |
| | | */ |
| | | public static String format(String format, Date date, int timeZone) { |
| | | timeZone = timeZone % 13; |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone); |
| | | TimeZone tz = TimeZone.getTimeZone(zoneId); |
| | | sdf.setTimeZone(tz); |
| | | return sdf.format(date); |
| | | } |
| | | |
| | | public static Date parse(String dateString, String format) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | |
| | | try { |
| | | return sdf.parse(dateString); |
| | | } catch (ParseException var4) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public static Date parse(String dateString, String format, int timeZone) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone); |
| | | TimeZone tz = TimeZone.getTimeZone(zoneId); |
| | | sdf.setTimeZone(tz); |
| | | try { |
| | | return sdf.parse(dateString); |
| | | } catch (ParseException var4) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import org.json.JSONArray; |
| | | import org.json.JSONException; |
| | | import org.json.JSONObject; |
| | | |
| | | import java.util.ArrayList; |
| | | |
| | | public final class JSONParser { |
| | | |
| | | private JSONParser() { |
| | | } |
| | | |
| | | public static String getJSONStringValue(String json, String key) { |
| | | try { |
| | | JSONObject obj = new JSONObject(json); |
| | | return obj.getString(key); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key)); |
| | | } |
| | | } |
| | | |
| | | public static int getJSONIntValue(String json, String key) { |
| | | try { |
| | | JSONObject obj = new JSONObject(json); |
| | | return obj.getInt(key); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key)); |
| | | } |
| | | } |
| | | |
| | | public static String getJSONArray(ArrayList<?> symbols, String key) { |
| | | try { |
| | | JSONArray arr = new JSONArray(symbols); |
| | | return arr.toString(); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to convert \"%s\" to JSON array", key)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | public class OKXContants { |
| | | /** |
| | | * 获取交易产品基础信息 |
| | | * 获取所有可交易产品的信息列表。 |
| | | * <br><br> |
| | | * GET /api/v5/public/instruments |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 是 产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br> |
| | | * uly -- String 可选 标的指数,仅适用于交割/永续/期权,期权必填 <br> |
| | | * instFamily -- String 否 交易品种,仅适用于交割/永续/期权 <br> |
| | | * instId -- String 否 产品ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a> |
| | | */ |
| | | public static final String INSTRUMENTS = "/api/v5/public/instruments"; |
| | | // public static final String K_LINE_HISTORY = "/api/v5/market/history-candles"; |
| | | public static final String K_LINE_HISTORY = "/api/v5/market/history-mark-price-candles"; |
| | | /** |
| | | * 查看账户余额 |
| | | * 获取交易账户中资金余额信息。 |
| | | * <br><br> |
| | | * GET /api/v5/account/balance |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * ccy -- String 否 币种,如 BTC 支持多币种查询(不超过20个),币种之间半角逗号分隔 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance</a> |
| | | */ |
| | | public static final String BALANCE = "/api/v5/account/balance"; |
| | | /** |
| | | * 查看持仓信息 |
| | | * 获取该账户下拥有实际持仓的信息。账户为单向持仓模式会显示净持仓(net),账户为双向持仓模式下会分别返回多头(long)或空头(short)的仓位。按照仓位创建时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 |
| | | * instType和instId同时传入的时候会校验instId与instType是否一致。<br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-190927-5000-C |
| | | * 支持多个instId查询(不超过10个),半角逗号分隔<br> |
| | | * posId -- String 否 持仓ID |
| | | * 支持多个posId查询(不超过20个),半角逗号分割<br> |
| | | * @return String <br> |
| | | * note: 如果该 instId 拥有过仓位且当前持仓量为0,传 instId 时,会返回仓位信息;不传 instId 时,仓位信息不返回。 |
| | | * 逐仓交易设置中,如果设置为自主划转模式,逐仓转入保证金后,会生成一个持仓量为0的仓位 <br> |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions</a> |
| | | */ |
| | | public static final String POSITIONS = "/api/v5/account/positions"; |
| | | /** |
| | | * 查看历史持仓信息 |
| | | * 获取最近3个月有更新的仓位信息,按照仓位更新时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions-history |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 <br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-SWAP <br> |
| | | * mgnMode -- String 否 保证金模式 |
| | | * cross:全仓,isolated:逐仓 |
| | | * type -- String 否 平仓类型 |
| | | * 1:部分平仓;2:完全平仓;3:强平;4:强减; 5:ADL自动减仓; |
| | | * 状态叠加时,以最新的平仓类型为准状态为准。 <br> |
| | | * posId -- String 否 持仓ID <br> |
| | | * after -- String 否 查询仓位更新 (uTime) 之前的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * before -- String 否 查询仓位更新 (uTime) 之后的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * limit -- String 否 分页返回结果的数量,最大为100,默认100条 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history</a> |
| | | */ |
| | | public static final String POSITIONS_HISTORY = "/api/v5/account/positions-history"; |
| | | /** |
| | | * 撤单 |
| | | * 撤销之前下的未完成订单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/cancel-order |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927 <br> |
| | | * ordId -- String 可选 订单ID, ordId和clOrdId必须传一个,若传两个,以ordId为主 <br> |
| | | * clOrdId -- String 可选 用户自定义ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order</a> |
| | | */ |
| | | public static final String CANCEL_ORDER = "/api/v5/trade/cancel-order"; |
| | | /** |
| | | * 下单 |
| | | * 只有当您的账户有足够的资金才能下单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/order |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927-5000-C <br> |
| | | * tdMode -- String 是 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 <br> |
| | | * ccy -- String 否 保证金币种,仅适用于单币种保证金模式下的全仓杠杆订单 <br> |
| | | * clOrdId -- String 否 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。<br> |
| | | * tag -- String 否 订单标签 |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-16位之间。 <br> |
| | | * side -- String 是 订单方向 |
| | | * buy:买, sell:卖 <br> |
| | | * posSide -- String 可选 持仓方向 |
| | | * 在双向持仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 <br> |
| | | * ordType -- String 是 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) <br> |
| | | * sz -- String 是 委托数量 <br> |
| | | * px -- String 可选 委托价格,仅适用于limit、post_only、fok、ioc类型的订单 <br> |
| | | * reduceOnly -- Boolean 否 是否只减仓,true 或 false,默认false |
| | | * 仅适用于币币杠杆,以及买卖模式下的交割/永续 |
| | | * 仅适用于单币种保证金模式和跨币种保证金模式 <br> |
| | | * tgtCcy -- String 否 市价单委托数量sz的单位,仅适用于币币市价订单 |
| | | * base_ccy: 交易货币 ;quote_ccy:计价货币 |
| | | * 买单默认quote_ccy, 卖单默认base_ccy <br> |
| | | * banAmend -- Boolean 否 是否禁止币币市价改单,true 或 false,默认false |
| | | * 为true时,余额不足时,系统不会改单,下单会失败,仅适用于币币市价单 <br> |
| | | * tpTriggerPx -- String 否 止盈触发价,如果填写此参数,必须填写 止盈委托价 <br> |
| | | * tpOrdPx -- String 否 止盈委托价,如果填写此参数,必须填写 止盈触发价 |
| | | * 委托价格为-1时,执行市价止盈 <br> |
| | | * slTriggerPx -- String 否 止损触发价,如果填写此参数,必须填写 止损委托价 <br> |
| | | * slOrdPx -- String 否 止损委托价,如果填写此参数,必须填写 止损触发价 |
| | | * 委托价格为-1时,执行市价止损 <br> |
| | | * tpTriggerPxType -- String 否 止盈触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * slTriggerPxType -- String 否 止损触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order</a> |
| | | */ |
| | | public static final String ORDER = "/api/v5/trade/order"; |
| | | |
| | | /** |
| | | * 获取币种价格信息 |
| | | */ |
| | | public static final String TICKER = "/api/v5/market/ticker"; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import okhttp3.*; |
| | | import org.json.JSONObject; |
| | | |
| | | import javax.net.ssl.SSLContext; |
| | | import javax.net.ssl.SSLSocketFactory; |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.X509TrustManager; |
| | | import java.io.IOException; |
| | | import java.net.URLEncoder; |
| | | import java.security.SecureRandom; |
| | | import java.security.cert.X509Certificate; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.concurrent.Semaphore; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * OkHttp请求工具封装 |
| | | * 参考:https://blog.csdn.net/DwZ735660836/article/details/119976068 |
| | | */ |
| | | @Slf4j |
| | | public class OkHttpUtils { |
| | | public static volatile OkHttpClient okHttpClient = null; |
| | | private static volatile Semaphore semaphore = null; |
| | | private Map<String, String> headerMap; |
| | | private Map<String, String> paramMap; |
| | | private String url; |
| | | private Request.Builder request; |
| | | // 开发环境用的 ShadowsocksR-dotnet4.0 免费版本 正式环境得使用外网服务器 |
| | | // 安易代理 http://127.0.0.1:10809/ http://127.0.0.1:10808/ |
| | | |
| | | /** |
| | | * 初始化okHttpClient,并且允许https访问 |
| | | */ |
| | | private OkHttpUtils() { |
| | | if (okHttpClient == null) { |
| | | synchronized (OkHttpUtils.class) { |
| | | if (okHttpClient == null) { |
| | | TrustManager[] trustManagers = buildTrustManagers(); |
| | | okHttpClient = new OkHttpClient.Builder() |
| | | .connectTimeout(30, TimeUnit.SECONDS) |
| | | .writeTimeout(20, TimeUnit.SECONDS) |
| | | .readTimeout(60, TimeUnit.SECONDS) |
| | | .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0]) |
| | | //.hostnameVerifier((hostName, session) -> true) |
| | | //配置自定义连接池参数 |
| | | .connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS)) |
| | | .retryOnConnectionFailure(true) |
| | | .build(); |
| | | addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"); |
| | | addHeader("Connection", "close"); |
| | | addHeader("Accept-Encoding", "identity"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 用于异步请求时,控制访问线程数,返回结果 |
| | | * |
| | | * @return |
| | | */ |
| | | private static Semaphore getSemaphoreInstance() { |
| | | //只能1个线程同时访问 |
| | | synchronized (OkHttpUtils.class) { |
| | | if (semaphore == null) { |
| | | semaphore = new Semaphore(0); |
| | | } |
| | | } |
| | | return semaphore; |
| | | } |
| | | |
| | | /** |
| | | * 创建OkHttpUtils |
| | | * |
| | | * @return |
| | | */ |
| | | public static OkHttpUtils builder() { |
| | | return new OkHttpUtils(); |
| | | } |
| | | |
| | | /** |
| | | * 添加url |
| | | * |
| | | * @param url |
| | | * @return |
| | | */ |
| | | public OkHttpUtils url(String url) { |
| | | this.url = url; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 添加参数 |
| | | * |
| | | * @param key 参数名 |
| | | * @param value 参数值 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils addParam(String key, String value) { |
| | | if (paramMap == null) { |
| | | paramMap = new LinkedHashMap<>(16); |
| | | } |
| | | paramMap.put(key, value); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 添加请求头 |
| | | * |
| | | * @param key 参数名 |
| | | * @param value 参数值 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils addHeader(String key, String value) { |
| | | if (headerMap == null) { |
| | | headerMap = new LinkedHashMap<>(16); |
| | | } |
| | | headerMap.put(key, value); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 初始化get方法 |
| | | * |
| | | * @return |
| | | */ |
| | | public OkHttpUtils get() { |
| | | request = new Request.Builder().get(); |
| | | StringBuilder urlBuilder = new StringBuilder(url); |
| | | if (paramMap != null) { |
| | | urlBuilder.append("?"); |
| | | try { |
| | | for (Map.Entry<String, String> entry : paramMap.entrySet()) { |
| | | urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")). |
| | | append("="). |
| | | append(URLEncoder.encode(entry.getValue(), "utf-8")). |
| | | append("&"); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | urlBuilder.deleteCharAt(urlBuilder.length() - 1); |
| | | } |
| | | request.url(urlBuilder.toString()); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 初始化post方法 |
| | | * |
| | | * @param isJsonPost true等于json的方式提交数据,类似postman里post方法的raw |
| | | * false等于普通的表单提交 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils post(boolean isJsonPost) { |
| | | RequestBody requestBody; |
| | | if (isJsonPost) { |
| | | String json = ""; |
| | | if (paramMap != null) { |
| | | json = JSONObject.valueToString(paramMap); |
| | | } |
| | | requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); |
| | | } else { |
| | | FormBody.Builder formBody = new FormBody.Builder(); |
| | | if (paramMap != null) { |
| | | paramMap.forEach(formBody::add); |
| | | } |
| | | requestBody = formBody.build(); |
| | | } |
| | | request = new Request.Builder().post(requestBody).url(url); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 同步请求 |
| | | * |
| | | * @return |
| | | */ |
| | | public Request.Builder sync() { |
| | | return setHeader(request); |
| | | } |
| | | |
| | | /** |
| | | * 同步请求 |
| | | * |
| | | * @return |
| | | */ |
| | | public String syncStr() { |
| | | setHeader(request); |
| | | try { |
| | | Response response = okHttpClient.newCall(request.build()).execute(); |
| | | assert response.body() != null; |
| | | return response.body().string(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | return "请求失败:" + e.getMessage(); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 异步请求,有返回值 |
| | | */ |
| | | public String async() { |
| | | StringBuilder buffer = new StringBuilder(); |
| | | setHeader(request); |
| | | okHttpClient.newCall(request.build()).enqueue(new Callback() { |
| | | @Override |
| | | public void onFailure(Call call, IOException e) { |
| | | buffer.append("请求出错:").append(e.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public void onResponse(Call call, Response response) throws IOException { |
| | | assert response.body() != null; |
| | | buffer.append(response.body().string()); |
| | | getSemaphoreInstance().release(); |
| | | } |
| | | }); |
| | | try { |
| | | getSemaphoreInstance().acquire(); |
| | | } catch (InterruptedException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 异步请求,带有接口回调 |
| | | * |
| | | * @param callBack |
| | | */ |
| | | public void async(ICallBack callBack) { |
| | | setHeader(request); |
| | | okHttpClient.newCall(request.build()).enqueue(new Callback() { |
| | | @Override |
| | | public void onFailure(Call call, IOException e) { |
| | | callBack.onFailure(call, e.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public void onResponse(Call call, Response response) throws IOException { |
| | | assert response.body() != null; |
| | | callBack.onSuccessful(call, response.body().string()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 为request添加请求头 |
| | | * |
| | | * @param request |
| | | */ |
| | | private Request.Builder setHeader(Request.Builder request) { |
| | | if (headerMap != null) { |
| | | try { |
| | | for (Map.Entry<String, String> entry : headerMap.entrySet()) { |
| | | request.addHeader(entry.getKey(), entry.getValue()); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | return request; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 生成安全套接字工厂,用于https请求的证书跳过 |
| | | * |
| | | * @return |
| | | */ |
| | | private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) { |
| | | SSLSocketFactory ssfFactory = null; |
| | | try { |
| | | SSLContext sc = SSLContext.getInstance("SSL"); |
| | | sc.init(null, trustAllCerts, new SecureRandom()); |
| | | ssfFactory = sc.getSocketFactory(); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return ssfFactory; |
| | | } |
| | | |
| | | private static TrustManager[] buildTrustManagers() { |
| | | return new TrustManager[]{ |
| | | new X509TrustManager() { |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getAcceptedIssuers() { |
| | | return new X509Certificate[]{}; |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * 自定义一个接口回调 |
| | | */ |
| | | public interface ICallBack { |
| | | |
| | | void onSuccessful(Call call, String data); |
| | | |
| | | void onFailure(Call call, String errorMsg); |
| | | |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | String url = "https://api2.binance.com/api/v3/ticker/24hr?symbol=BNBUSDT&type=MINI"; |
| | | String result = OkHttpUtils.builder() |
| | | .url(url) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .get() |
| | | .syncStr(); |
| | | System.out.println(result); |
| | | } |
| | | } |
| | | |
| | | |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import cc.mrbird.febs.common.exception.FebsException; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | public final class ParameterChecker { |
| | | |
| | | private ParameterChecker() { |
| | | } |
| | | |
| | | public static void checkParameter(LinkedHashMap<String, Object> parameters, String parameter, Class t) { |
| | | checkRequiredParameter(parameters, parameter); |
| | | checkParameterType(parameters.get(parameter), t, parameter); |
| | | } |
| | | |
| | | public static void checkOrParameters(LinkedHashMap<String, Object> parameters, String parameter, String parameter2) { |
| | | if (!parameters.containsKey(parameter) && (!parameters.containsKey(parameter2))) { |
| | | throw new FebsException(String.format("Either \"%s\" or \"%s\" is required!", parameter, parameter2)); |
| | | } |
| | | } |
| | | |
| | | public static void checkRequiredParameter(LinkedHashMap<String, Object> parameters, String parameter) { |
| | | if (!parameters.containsKey(parameter)) { |
| | | throw new FebsException(String.format("\"%s\" is a mandatory parameter!", parameter)); |
| | | } |
| | | } |
| | | |
| | | public static void checkParameterType(Object parameter, Class t, String name) { |
| | | if (!t.isInstance(parameter)) { |
| | | throw new FebsException(String.format("\"%s\" must be of %s type.", name, t)); |
| | | } else if (t == String.class && parameter.toString().trim().equals("")) { |
| | | throw new FebsException(String.format("\"%s\" must not be empty.", name)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | |
| | | import com.xcong.excoin.common.exception.FebsException; |
| | | import com.xcong.excoin.modules.newPrice.enums.HttpMethod; |
| | | import okhttp3.MediaType; |
| | | import okhttp3.Request; |
| | | import okhttp3.RequestBody; |
| | | |
| | | public final class RequestBuilder { |
| | | private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8"); |
| | | |
| | | private RequestBuilder() { |
| | | } |
| | | |
| | | public static Request.Builder buildPublicRequest(String fullUrl, HttpMethod httpMethod, boolean issimulated) { |
| | | try { |
| | | final Request.Builder result; |
| | | switch (httpMethod) { |
| | | case POST: |
| | | OkHttpUtils builder = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder |
| | | .url(fullUrl) |
| | | .post(true) |
| | | .sync(); |
| | | break; |
| | | case GET: |
| | | OkHttpUtils builder1 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder1.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder1 |
| | | .url(fullUrl) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .get() |
| | | .sync(); |
| | | break; |
| | | case PUT: |
| | | OkHttpUtils builder2 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder2.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder2 |
| | | .url(fullUrl) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .post(false) |
| | | .sync(); |
| | | break; |
| | | case DELETE: |
| | | OkHttpUtils builder3 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder3.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder3 |
| | | .url(fullUrl) |
| | | .post(false) |
| | | .sync(); |
| | | break; |
| | | default: |
| | | throw new FebsException("Invalid HTTP method: " + httpMethod); |
| | | } |
| | | return result; |
| | | } catch (IllegalArgumentException e) { |
| | | throw new FebsException("Invalid URL: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | public static Request buildApiKeyRequest(String fullUrl,String body,String passphrase,String sign,String timeStamp, HttpMethod httpMethod, String apiKey,boolean issimulate) { |
| | | try { |
| | | final Request request; |
| | | switch (httpMethod) { |
| | | case POST: |
| | | Request.Builder builder = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder |
| | | .url(fullUrl) |
| | | .post(RequestBody.create(JSON_TYPE, body)) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case GET: |
| | | Request.Builder builder1 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder1.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder1 |
| | | .url(fullUrl) |
| | | .get() |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case PUT: |
| | | Request.Builder builder2 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder2.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder2 |
| | | .url(fullUrl) |
| | | .put(RequestBody.create(JSON_TYPE, "")) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case DELETE: |
| | | Request.Builder builder3 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder3.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder3 |
| | | .url(fullUrl) |
| | | .delete() |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | default: |
| | | throw new FebsException("Invalid HTTP method: " + httpMethod); |
| | | } |
| | | return request; |
| | | } catch (IllegalArgumentException e) { |
| | | throw new FebsException("Invalid URL: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | public static Request buildWebsocketRequest(String fullUrl) { |
| | | return new Request.Builder().url(fullUrl).build(); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.util.Base64; |
| | | |
| | | |
| | | @Slf4j |
| | | public class SignUtils { |
| | | |
| | | public static String signRest(String secretKey, String timestamp, String method, String path, String body) { |
| | | String str = String.format("%s%s%s%s", |
| | | timestamp, // timestamp |
| | | method, // method GET/POST |
| | | path, // requestPath |
| | | body // body |
| | | ); |
| | | try { |
| | | return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes())); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * HmacSHA256算法,返回的结果始终是32位 |
| | | * |
| | | * @param key 加密的键,可以是任何数据 |
| | | * @param content 待加密的内容 |
| | | * @return 加密后的内容 |
| | | * @throws Exception ex |
| | | */ |
| | | public static byte[] hmacSHA256(byte[] key, byte[] content) throws Exception { |
| | | Mac hmacSha256 = Mac.getInstance("HmacSHA256"); |
| | | hmacSha256.init(new SecretKeySpec(key, 0, key.length, "HmacSHA256")); |
| | | return hmacSha256.doFinal(content); |
| | | } |
| | | |
| | | public static String signWebsocket(String timestamp, String secretKey) { |
| | | String str = String.format("%s%s%s", |
| | | timestamp, |
| | | "GET", |
| | | "/users/self/verify"); |
| | | try { |
| | | return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes())); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice.utils; |
| | | |
| | | import java.io.UnsupportedEncodingException; |
| | | import java.net.URLEncoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.text.DecimalFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | |
| | | public final class UrlBuilder { |
| | | private static final int MAX_DECIMAL_DIGITS = 30; |
| | | private static DecimalFormat df; |
| | | |
| | | |
| | | private UrlBuilder() { |
| | | } |
| | | |
| | | public static String buildFullUrl(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, String signature) { |
| | | if (parameters != null && !parameters.isEmpty()) { |
| | | StringBuilder sb = new StringBuilder(baseUrl); |
| | | sb.append(urlPath).append('?'); |
| | | joinQueryParameters(sb, parameters); |
| | | if (null != signature) { |
| | | sb.append("&signature=").append(signature); |
| | | } |
| | | return sb.toString(); |
| | | } else { |
| | | return baseUrl + urlPath; |
| | | } |
| | | } |
| | | |
| | | public static String buildStreamUrl(String baseUrl, ArrayList<String> streams) { |
| | | StringBuilder sb = new StringBuilder(baseUrl); |
| | | sb.append("?streams="); |
| | | return joinStreamUrls(sb, streams); |
| | | } |
| | | |
| | | //concatenate query parameters |
| | | public static String joinQueryParameters(LinkedHashMap<String, Object> parameters) { |
| | | return joinQueryParameters(new StringBuilder(), parameters).toString(); |
| | | } |
| | | |
| | | public static StringBuilder joinQueryParameters(StringBuilder urlPath, LinkedHashMap<String, Object> parameters) { |
| | | if (parameters == null || parameters.isEmpty()) { |
| | | return urlPath; |
| | | } |
| | | |
| | | boolean isFirst = true; |
| | | for (Map.Entry<String, Object> mapElement : parameters.entrySet()) { |
| | | |
| | | if (mapElement.getValue() instanceof Double) { |
| | | parameters.replace(mapElement.getKey(), getFormatter().format(mapElement.getValue())); |
| | | } else if (mapElement.getValue() instanceof ArrayList) { |
| | | if (((ArrayList<?>) mapElement.getValue()).isEmpty()) { |
| | | continue; |
| | | } |
| | | String key = mapElement.getKey(); |
| | | joinArrayListParameters(key, urlPath, (ArrayList<?>) mapElement.getValue(), isFirst); |
| | | isFirst = false; |
| | | continue; |
| | | } |
| | | |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('&'); |
| | | } |
| | | |
| | | urlPath.append(mapElement.getKey()) |
| | | .append('=') |
| | | .append(urlEncode(mapElement.getValue().toString())); |
| | | } |
| | | return urlPath; |
| | | } |
| | | |
| | | private static void joinArrayListParameters(String key, StringBuilder urlPath, ArrayList<?> values, boolean isFirst) { |
| | | for (Object value: values) { |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('&'); |
| | | } |
| | | |
| | | urlPath.append(key) |
| | | .append('=') |
| | | .append(urlEncode(value.toString())); |
| | | } |
| | | } |
| | | |
| | | private static String joinStreamUrls(StringBuilder urlPath, ArrayList<String> streams) { |
| | | boolean isFirst = true; |
| | | for (String stream: streams) { |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('/'); |
| | | } |
| | | urlPath.append(stream); |
| | | } |
| | | return urlPath.toString(); |
| | | } |
| | | |
| | | |
| | | public static String urlEncode(String s) { |
| | | try { |
| | | return URLEncoder.encode(s, StandardCharsets.UTF_8.name()); |
| | | } catch (UnsupportedEncodingException e) { |
| | | // UTF-8 being unsuppored is unlikely |
| | | // Replace with a unchecked exception to tidy up exception handling |
| | | throw new RuntimeException(StandardCharsets.UTF_8.name() + " is unsupported", e); |
| | | } |
| | | } |
| | | |
| | | private static DecimalFormat getFormatter() { |
| | | if (null == df) { |
| | | df = new DecimalFormat(); |
| | | df.setMaximumFractionDigits(MAX_DECIMAL_DIGITS); |
| | | df.setGroupingUsed(false); |
| | | } |
| | | return df; |
| | | } |
| | | |
| | | public static String buildTimestamp() { |
| | | return String.valueOf(System.currentTimeMillis()); |
| | | } |
| | | } |
| | |
| | | |
| | | import com.huobi.client.model.Candlestick; |
| | | import com.xcong.excoin.common.response.Result; |
| | | import com.xcong.excoin.modules.newPrice.KlineVo; |
| | | import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto; |
| | | import com.xcong.excoin.modules.symbols.parameter.vo.HomeSymbolsVo; |
| | | import com.xcong.excoin.modules.symbols.parameter.vo.KlineDataVo; |
| | |
| | | return symbolsService.findKlineDetails(klineDetailDto); |
| | | } |
| | | |
| | | @ApiOperation(value = "查询历史OKXK线数据", notes = "查询历史OKXK线数据") |
| | | @ApiResponses({ |
| | | @ApiResponse(code = 0, message = "success", response = KlineDataVo.class) |
| | | }) |
| | | @PostMapping(value = "/klineDetail") |
| | | public Result klineDetail(@RequestBody @Valid KlineVo klineDetailDto) { |
| | | return symbolsService.findKlineList(klineDetailDto); |
| | | } |
| | | |
| | | @ApiOperation(value = "查询当日最高最低价") |
| | | @GetMapping(value = "/getDayHighAndLow") |
| | | public Result getDayHighAndLow(@ApiParam(name = "symbol", value = "币种", required = true, example = "BTC/USDT") @RequestParam(value = "symbol") String symbol) { |
| | |
| | | |
| | | |
| | | import com.xcong.excoin.common.response.Result; |
| | | import com.xcong.excoin.modules.newPrice.KlineVo; |
| | | import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto; |
| | | |
| | | /** |
| | |
| | | |
| | | public Result findKlineDetails(KlineDetailDto klineDetailDto); |
| | | |
| | | public Result findKlineList(KlineVo klineDetailDto); |
| | | } |
| | |
| | | package com.xcong.excoin.modules.symbols.service.impl; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.date.DateUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.huobi.client.model.Candlestick; |
| | | import com.xcong.excoin.common.contants.AppContants; |
| | | import com.xcong.excoin.common.enumerates.SymbolEnum; |
| | | import com.xcong.excoin.common.response.Result; |
| | | import com.xcong.excoin.common.system.mapper.CandlestickMapper; |
| | | import com.xcong.excoin.modules.newPrice.ExchangeInfoEnum; |
| | | import com.xcong.excoin.modules.newPrice.ExchangeLoginService; |
| | | import com.xcong.excoin.modules.newPrice.KlineVo; |
| | | import com.xcong.excoin.modules.platform.dao.PlatformCnyUsdtExchangeDao; |
| | | import com.xcong.excoin.modules.platform.entity.PlatformCnyUsdtExchangeEntity; |
| | | import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto; |
| | |
| | | import com.xcong.excoin.modules.symbols.parameter.vo.KlineDataVo; |
| | | import com.xcong.excoin.modules.symbols.service.SymbolsService; |
| | | import com.xcong.excoin.utils.CoinTypeConvert; |
| | | import com.xcong.excoin.utils.MessageSourceUtils; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import com.xcong.excoin.utils.api.ApiClient; |
| | | import com.xcong.excoin.utils.api.response.Kline; |
| | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | |
| | | /** |
| | |
| | | } |
| | | return Result.fail("获取数据失败"); |
| | | } |
| | | |
| | | @Override |
| | | public Result findKlineList(KlineVo klineDetailDto) { |
| | | Integer type = klineDetailDto.getType(); |
| | | String instId = null; |
| | | if (AppContants.HOME_SYMBOLS_COIN == type){ |
| | | instId = CoinTypeConvert.convertToCoinKlineKey(klineDetailDto.getInstId()); |
| | | }else if (AppContants.HOME_SYMBOLS_CONTRACT == type){ |
| | | instId = CoinTypeConvert.convertToKlineKey(klineDetailDto.getInstId()); |
| | | }else{ |
| | | return Result.fail("参数错误"); |
| | | } |
| | | LinkedHashMap<String, Object> requestParam = new LinkedHashMap<>(); |
| | | requestParam.put("instId",instId); |
| | | String result = ExchangeLoginService.getInstance(ExchangeInfoEnum.OKX_UAT.name()).lineHistory(requestParam); |
| | | log.info("加载OKX-KLINE,{}", result); |
| | | JSONObject json = JSON.parseObject(result); |
| | | String data = json.getString("data"); |
| | | List<String[]> klinesList = JSON.parseArray(data, String[].class); |
| | | return Result.ok(klinesList); |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | public static String convertToCoinKlineKey(String symbol) { |
| | | switch (symbol) { |
| | | case "BTC/USDT": |
| | | return "BTC-USDT"; |
| | | case "ETH/USDT": |
| | | return "ETH-USDT"; |
| | | case "XRP/USDT": |
| | | return "XRP-USDT"; |
| | | case "LTC/USDT": |
| | | return "LTC-USDT"; |
| | | case "BCH/USDT": |
| | | return "BCH-USDT"; |
| | | case "ETC/USDT": |
| | | return "ETC-USDT"; |
| | | default: |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public static String convertToKlineKey(String symbol) { |
| | | switch (symbol) { |
| | | case "BTC/USDT": |
| | | return "BTC-USDT-SWAP"; |
| | | case "ETH/USDT": |
| | | return "ETH-USDT-SWAP"; |
| | | case "XRP/USDT": |
| | | return "XRP-USDT-SWAP"; |
| | | case "LTC/USDT": |
| | | return "LTC-USDT-SWAP"; |
| | | case "BCH/USDT": |
| | | return "BCH-USDT-SWAP"; |
| | | case "ETC/USDT": |
| | | return "ETC-USDT-SWAP"; |
| | | default: |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | public static String convertToOpenKey(String symbol) { |