View Javadoc
1   package org.oxerr.chbtc;
2   
3   import java.io.IOException;
4   import java.math.BigDecimal;
5   import java.net.URI;
6   import java.net.URISyntaxException;
7   import java.util.Collections;
8   import java.util.HashMap;
9   import java.util.List;
10  import java.util.Map;
11  
12  import org.apache.http.NameValuePair;
13  import org.apache.http.client.utils.URIBuilder;
14  import org.apache.http.client.utils.URIUtils;
15  import org.apache.http.message.BasicNameValuePair;
16  import org.oxerr.chbtc.dto.AccountInfo;
17  import org.oxerr.chbtc.dto.CHBTCError;
18  import org.oxerr.chbtc.dto.Depth;
19  import org.oxerr.chbtc.dto.Order;
20  import org.oxerr.chbtc.dto.OrderResponse;
21  import org.oxerr.chbtc.dto.Ticker;
22  import org.oxerr.chbtc.dto.TickerResponse;
23  import org.oxerr.chbtc.dto.Trade;
24  import org.oxerr.chbtc.dto.Type;
25  import org.oxerr.chbtc.dto.valuereader.DepthReader;
26  import org.oxerr.chbtc.dto.valuereader.ErrorableJsonValueTypeRefReader;
27  import org.oxerr.chbtc.dto.valuereader.ValueReader;
28  import org.oxerr.chbtc.service.polling.CHBTCMarketDataService;
29  import org.oxerr.chbtc.util.EncryDigestUtil;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import com.fasterxml.jackson.core.type.TypeReference;
34  import com.fasterxml.jackson.databind.ObjectMapper;
35  import com.xeiam.xchange.currency.Currencies;
36  import com.xeiam.xchange.currency.CurrencyPair;
37  
38  public class CHBTCClient implements AutoCloseable {
39  
40  	public static final String ENCODING = "UTF-8";
41  
42  	@Deprecated
43  	private static final URI API_BASE_URI = URI.create("http://api.chbtc.com/");
44  
45  	@Deprecated
46  	private static final URI DATA_BASE_URI = URIUtils.resolve(API_BASE_URI, "data/");
47  
48  	@Deprecated
49  	private static final URI TICKER_URI = URIUtils.resolve(DATA_BASE_URI, "ticker");
50  
51  	@Deprecated
52  	private static final URI DEPTH_URI = URIUtils.resolve(DATA_BASE_URI, "depth");
53  
54  	@Deprecated
55  	private static final URI TRADES_URI = URIUtils.resolve(DATA_BASE_URI, "trades");
56  
57  	@Deprecated
58  	private static final URI LTC_DATA_BASE_URI = URIUtils.resolve(DATA_BASE_URI, "ltc");
59  
60  	@Deprecated
61  	private static final URI LTC_TICKER_URI = URIUtils.resolve(LTC_DATA_BASE_URI, "ticker");
62  
63  	@Deprecated
64  	private static final URI LTC_DEPTH_URI = URIUtils.resolve(LTC_DATA_BASE_URI, "depth");
65  
66  	@Deprecated
67  	private static final URI LTC_TRADES_URI = URIUtils.resolve(LTC_DATA_BASE_URI, "trades");
68  
69  	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
70  
71  	@Deprecated
72  	private static final DepthReader DEPTH_READER = new DepthReader(OBJECT_MAPPER);
73  
74  	@Deprecated
75  	private static final TypeReference<List<Trade>> TRADE_LIST_TYPE_REFERENCE = new TypeReference<List<Trade>>() {
76  	};
77  
78  	private static final TypeReference<List<Order>> ORDER_LIST_TYPE_REFERENCE = new TypeReference<List<Order>>() {
79  	};
80  
81  	private static final ValueReader<List<Order>> ORDER_LIST_READER = new ErrorableJsonValueTypeRefReader<>(OBJECT_MAPPER, ORDER_LIST_TYPE_REFERENCE);
82  
83  	private final Logger log = LoggerFactory.getLogger(CHBTCClient.class);
84  
85  	private final Map<String, Long> lastReqTime = new HashMap<>(8);
86  
87  	private final HttpClient httpClient;
88  
89  	private final String tradeApiUrl;
90  
91  	private final String accessKey;
92  
93  	private final String secret;
94  
95  	public CHBTCClient(
96  			final String accessKey,
97  			final String secretKey,
98  			final int socketTimeout,
99  			final int connectTimeout,
100 			final int connectionRequestTimeout) {
101 		this("https://trade.chbtc.com/api", accessKey, secretKey,
102 				socketTimeout, connectTimeout, connectionRequestTimeout);
103 	}
104 
105 	public CHBTCClient(
106 			final String tradeApiUrl,
107 			final String accessKey,
108 			final String secretKey,
109 			final int socketTimeout,
110 			final int connectTimeout,
111 			final int connectionRequestTimeout) {
112 		this.tradeApiUrl = tradeApiUrl;
113 
114 		httpClient = new HttpClient(
115 				socketTimeout,
116 				connectTimeout,
117 				connectionRequestTimeout);
118 
119 		this.accessKey = accessKey;
120 		this.secret = EncryDigestUtil.digest(secretKey);
121 	}
122 
123 	/**
124 	 * Gets BTC ticker.
125 	 *
126 	 * @return the ticker of the BTC market.
127 	 * @throws IOException indicates I/O exception.
128 	 * @deprecated Use {@link CHBTCMarketDataService#getTicker(CurrencyPair)} instead.
129 	 */
130 	@Deprecated
131 	public Ticker getTicker() throws IOException {
132 		return httpClient.get(TICKER_URI, TickerResponse.class).getTicker();
133 	}
134 
135 	/**
136 	 * Gets ticker of the specified market.
137 	 * 
138 	 * @param currency the currency symbol of the tradable, could be BTC or LTC.
139 	 * @return the ticker.
140 	 * @throws IOException indicates I/O exception.
141 	 * @deprecated Use {@link CHBTCMarketDataService#getTicker(CurrencyPair)} instead.
142 	 */
143 	@Deprecated
144 	public Ticker getTicker(String currency) throws IOException {
145 		if (currency.equalsIgnoreCase(Currencies.LTC)) {
146 			return httpClient.get(LTC_TICKER_URI, TickerResponse.class)
147 					.getTicker();
148 		} else {
149 			return getTicker();
150 		}
151 	}
152 
153 	/**
154 	 * Gets BTC market order book.
155 	 *
156 	 * @return the order book of BTC market.
157 	 * @throws IOException indicates I/O exception.
158 	 * @deprecated Use {@link CHBTCMarketDataService#getOrderBook(CurrencyPair, Object...)} instead.
159 	 */
160 	@Deprecated
161 	public Depth getDepth() throws IOException {
162 		return httpClient.get(DEPTH_URI, DEPTH_READER);
163 	}
164 
165 	/**
166 	 * Gets order book of the specified market.
167 	 *
168 	 * @param currency the currency symbol of the tradable, could be BTC or LTC.
169 	 * @return the order book of the specified market.
170 	 * @throws IOException indicates I/O exception.
171 	 * @deprecated Use {@link CHBTCMarketDataService#getOrderBook(CurrencyPair, Object...)} instead.
172 	 */
173 	@Deprecated
174 	public Depth getDepth(String currency) throws IOException {
175 		if (currency.equalsIgnoreCase(Currencies.LTC)) {
176 			return httpClient.get(LTC_DEPTH_URI, DEPTH_READER);
177 		} else {
178 			return getDepth();
179 		}
180 	}
181 
182 	/**
183 	 * Gets trades of BTC market.
184 	 *
185 	 * @return the trades of BTC market.
186 	 * @throws IOException indicates I/O exception.
187 	 * @deprecated Use {@link CHBTCMarketDataService#getTrades(CurrencyPair, Object...)} instead.
188 	 */
189 	@Deprecated
190 	public List<Trade> getTrades() throws IOException {
191 		return httpClient.get(TRADES_URI, TRADE_LIST_TYPE_REFERENCE);
192 	}
193 
194 	/**
195 	 * Gets trades of BTC market.
196 	 *
197 	 * @param since 1 based. When pass 1, will get trades from the first trade of CHBTC.
198 	 * @return the trades in BTC market.
199 	 * @throws IOException indicates I/O exception.
200 	 * @deprecated Use {@link CHBTCMarketDataService#getTrades(CurrencyPair, Object...)} instead.
201 	 */
202 	@Deprecated
203 	public List<Trade> getTrades(int since) throws IOException {
204 		final URI uri;
205 		try {
206 			uri = new URIBuilder(TRADES_URI)
207 				.setParameter("since", String.valueOf(since))
208 				.build();
209 		} catch (URISyntaxException e) {
210 			throw new IllegalArgumentException(e);
211 		}
212 
213 		return httpClient.get(uri, TRADE_LIST_TYPE_REFERENCE);
214 	}
215 
216 	/**
217 	 * Gets recent trades of the specified market.
218 	 *
219 	 * @param currency the currency symbol of the tradable, could be BTC or LTC.
220 	 * @return the trades.
221 	 * @throws IOException indicates I/O exception.
222 	 * @deprecated Use {@link CHBTCMarketDataService#getTrades(CurrencyPair, Object...)} instead.
223 	 */
224 	@Deprecated
225 	public List<Trade> getTrades(String currency) throws IOException {
226 		if (currency.equalsIgnoreCase(Currencies.LTC)) {
227 			return httpClient.get(LTC_TRADES_URI, TRADE_LIST_TYPE_REFERENCE);
228 		} else {
229 			return getTrades();
230 		}
231 	}
232 
233 	/**
234 	 * Gets trades.
235 	 *
236 	 * @param currency the currency symbol of the tradable, could be BTC or LTC.
237 	 * @param since 1 based. When pass 1, will get trades from the first trade of CHBTC.
238 	 * @return trades.
239 	 * @throws IOException indicates I/O exception.
240 	 * @deprecated Use {@link CHBTCMarketDataService#getTrades(CurrencyPair, Object...)} instead.
241 	 */
242 	@Deprecated
243 	public List<Trade> getTrades(
244 			String currency,
245 			int since
246 			) throws IOException {
247 		if (currency.equalsIgnoreCase(Currencies.LTC)) {
248 			final URI uri;
249 			try {
250 				uri = new URIBuilder(LTC_TRADES_URI)
251 					.setParameter("since", String.valueOf(since))
252 					.build();
253 			} catch (URISyntaxException e) {
254 				throw new IllegalArgumentException(e);
255 			}
256 
257 			return httpClient.get(uri, TRADE_LIST_TYPE_REFERENCE);
258 		} else {
259 			return getTrades(since);
260 		}
261 	}
262 
263 	/**
264 	 * {@inheritDoc}
265 	 */
266 	@Override
267 	public void close() throws IOException {
268 		log.debug("Closing HTTP Client...");
269 		httpClient.close();
270 	}
271 
272 	/**
273 	 * 委托 BTC/LTC 买单或卖单。
274 	 *
275 	 * @param price 单价。
276 	 * @param amount 交易数量。
277 	 * @param tradeType 交易类型 1/0[buy/sell]
278 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
279 	 * @return 挂单 ID。
280 	 * @throws IOException indicates I/O exception.
281 	 */
282 	public long order(
283 			final BigDecimal price,
284 			final BigDecimal amount,
285 			final Type tradeType,
286 			final String currency
287 			) throws IOException {
288 		OrderResponse orderResponse = get(
289 				CHBTC.METHOD_ORDER,
290 				OrderResponse.class,
291 				new BasicNameValuePair("price", price.toPlainString()),
292 				new BasicNameValuePair("amount", amount.toPlainString()),
293 				new BasicNameValuePair("tradeType",
294 						String.valueOf(tradeType.getTradeType())),
295 				new BasicNameValuePair("currency", currency));
296 		if (orderResponse.getCode() == CHBTCError.SUCCESS) {
297 			return orderResponse.getId();
298 		} else {
299 			throw new CHBTCClientException(orderResponse);
300 		}
301 	}
302 
303 	/**
304 	 * 取消委托买单或卖单。
305 	 *
306 	 * @param id 挂单 ID。
307 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
308 	 * @throws IOException indicates I/O exception.
309 	 */
310 	public void cancelOrder(
311 			final long id,
312 			final String currency
313 			) throws IOException {
314 		CHBTCError error = get(
315 				CHBTC.METHOD_CANCEL_ORDER,
316 				CHBTCError.class,
317 				new BasicNameValuePair("id", String.valueOf(id)),
318 				new BasicNameValuePair("currency", currency));
319 
320 		if (error.getCode() != CHBTCError.SUCCESS) {
321 			throw new CHBTCClientException(error);
322 		}
323 	}
324 
325 	/**
326 	 * 获取委托买单或卖单。
327 	 *
328 	 * @param id 挂单 ID。
329 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
330 	 * @return 委托买单或卖单。
331 	 * @throws IOException indicates I/O exception.
332 	 */
333 	public Order getOrder(
334 			final long id,
335 			final String currency
336 			) throws IOException {
337 		return get(
338 				CHBTC.METHOD_GET_ORDER,
339 				Order.class,
340 				new BasicNameValuePair("id", String.valueOf(id)),
341 				new BasicNameValuePair("currency", currency));
342 	}
343 
344 	/**
345 	 * 获取多个委托买单或卖单,每次请求返回 10 条记录。
346 	 *
347 	 * @param tradeType 交易类型 1/0[buy/sell]。
348 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
349 	 * @param pageIndex 当前页数。
350 	 * @return 多个委托买单或卖单,最多 10 条记录。
351 	 * @throws IOException indicates I/O exception.
352 	 */
353 	public List<Order> getOrders(
354 			final Type tradeType,
355 			final String currency,
356 			final int pageIndex
357 			) throws IOException {
358 		return get(
359 				CHBTC.METHOD_GET_ORDERS,
360 				ORDER_LIST_READER,
361 				new BasicNameValuePair("tradeType",
362 						String.valueOf(tradeType.getTradeType())),
363 				new BasicNameValuePair("currency", currency),
364 				new BasicNameValuePair("pageIndex", String.valueOf(pageIndex)));
365 	}
366 
367 	/**
368 	 * 获取多个委托买单或卖单,每次请求返回 {@literal pageSize<=100} 条记录。
369 	 *
370 	 * @param tradeType 交易类型 1/0[buy/sell]。
371 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
372 	 * @param pageIndex 当前页数。
373 	 * @param pageSize 每页数量。
374 	 * @return 多个委托买单或卖单。
375 	 * @throws IOException indicates I/O exception.
376 	 */
377 	public List<Order> getOrdersNew(
378 			final Type tradeType,
379 			final String currency,
380 			final int pageIndex,
381 			final int pageSize
382 			) throws IOException {
383 		return get(
384 				CHBTC.METHOD_GET_ORDERS_NEW,
385 				ORDER_LIST_READER,
386 				new BasicNameValuePair("tradeType",
387 						String.valueOf(tradeType.getTradeType())),
388 				new BasicNameValuePair("currency", currency),
389 				new BasicNameValuePair("pageIndex", String.valueOf(pageIndex)),
390 				new BasicNameValuePair("pageSize", String.valueOf(pageSize)));
391 	}
392 
393 	/**
394 	 * 与 gerOrders 的区别是取消 tradeType 字段过滤,可同时获取买单和卖单。
395 	 *
396 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
397 	 * @param pageIndex 当前页数。
398 	 * @param pageSize 每页数量。
399 	 * @return 多个委托买单和卖单。
400 	 * @throws IOException indicates I/O exception.
401 	 */
402 	public List<Order> getOrdersIgnoreTradeType(
403 			final String currency,
404 			final int pageIndex,
405 			final int pageSize
406 			) throws IOException {
407 		return get(
408 				CHBTC.METHOD_GET_ORDERS_IGNORE_TRADE_TYPE,
409 				ORDER_LIST_READER,
410 				new BasicNameValuePair("currency", currency),
411 				new BasicNameValuePair("pageIndex", String.valueOf(pageIndex)),
412 				new BasicNameValuePair("pageSize", String.valueOf(pageSize)));
413 	}
414 
415 	/**
416 	 * 获取未成交或部份成交的买单和卖单,每次请求返回 {@literal pageSize<=100} 条记录。
417 	 *
418 	 * @param currency 交易类型(目前仅支持 BTC/LTC)。
419 	 * @param pageIndex 当前页数。
420 	 * @param pageSize 每页数量。
421 	 * @return 未成交或部份成交的买单和卖单。
422 	 * @throws IOException indicates I/O exception.
423 	 */
424 	public List<Order> getUnfinishedOrdersIgnoreTradeType(
425 			final String currency,
426 			final int pageIndex,
427 			final int pageSize
428 			) throws IOException {
429 		try {
430 			return getUnfinishedOrdersIgnoreTradeTypeInternal(currency,
431 					pageIndex, pageSize);
432 		} catch (CHBTCClientException e) {
433 			if (e.getErrorCode() == CHBTCError.NOT_FOUND_ORDER) {
434 				return Collections.emptyList();
435 			} else {
436 				throw e;
437 			}
438 		}
439 	}
440 
441 	/**
442 	 * 获取用户信息。
443 	 * @return 用户信息。
444 	 * @throws IOException indicates I/O exception.
445 	 */
446 	public AccountInfo getAccountInfo() throws IOException {
447 		return get(CHBTC.METHOD_GET_ACCOUNT_INFO, AccountInfo.class);
448 	}
449 
450 	private List<Order> getUnfinishedOrdersIgnoreTradeTypeInternal(
451 			final String currency, final int pageIndex, final int pageSize)
452 			throws IOException {
453 		return get(
454 				CHBTC.METHOD_GET_UNFINISHED_ORDERS_IGNORE_TRADE_TYPE,
455 				ORDER_LIST_READER,
456 				new BasicNameValuePair("currency", currency),
457 				new BasicNameValuePair("pageIndex", String.valueOf(pageIndex)),
458 				new BasicNameValuePair("pageSize", String.valueOf(pageSize)));
459 	}
460 
461 	private <T> T get(
462 			String method,
463 			Class<T> valueType,
464 			NameValuePair... params
465 			) throws IOException {
466 		sleepIfRequired(method);
467 
468 		URI uri = buildTradeUri(method, params);
469 		T t = httpClient.get(uri, valueType);
470 
471 		this.lastReqTime.put(method, System.currentTimeMillis());
472 
473 		return t;
474 	}
475 
476 	private <T> T get(
477 			String method,
478 			ValueReader<T> valueReader,
479 			NameValuePair... params
480 			) throws IOException {
481 		sleepIfRequired(method);
482 
483 		URI uri = buildTradeUri(method, params);
484 		T t = httpClient.get(uri, valueReader);
485 
486 		this.lastReqTime.put(method, System.currentTimeMillis());
487 
488 		return t;
489 	}
490 
491 	private URI buildTradeUri(String method, NameValuePair... nameValuePairs) {
492 		final StringBuilder paramsBuilder = new StringBuilder("method=")
493 			.append(method)
494 			.append("&accesskey=").append(accessKey);
495 		for (NameValuePair pair : nameValuePairs) {
496 			paramsBuilder.append("&")
497 				.append(pair.getName()).append("=").append(pair.getValue());
498 		}
499 
500 		final String params = paramsBuilder.toString();
501 
502 		final String sign = EncryDigestUtil.hmacSign(params, secret);
503 
504 		final String uri = new StringBuilder(tradeApiUrl)
505 			.append("/")
506 			.append(method)
507 			.append("?")
508 			.append(params)
509 			.append("&sign=").append(sign)
510 			.append("&reqTime=").append(System.currentTimeMillis())
511 			.toString();
512 
513 		return URI.create(uri);
514 	}
515 
516 	/**
517 	 * 因为请求同一个方法,1 秒内只能请求一次,所以必须控制请求频率。
518 	 */
519 	private void sleepIfRequired(String method) {
520 		Long last = lastReqTime.get(method);
521 		long now = System.currentTimeMillis();
522 		if (last != null && now - last.longValue() < 1000) {
523 			sleepQuietly(1000 - (now - last.longValue()));
524 		}
525 	}
526 
527 	private void sleepQuietly(long millis) {
528 		try {
529 			log.debug("Sleeping {} ms.", millis);
530 			Thread.sleep(millis);
531 		} catch (InterruptedException e) {
532 			// Ignore.
533 		}
534 	}
535 
536 }