View Javadoc
1   package org.oxerr.okcoin.rest.service.web;
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.ArrayList;
8   import java.util.List;
9   
10  import org.apache.http.Consts;
11  import org.apache.http.NameValuePair;
12  import org.apache.http.client.entity.UrlEncodedFormEntity;
13  import org.apache.http.client.methods.HttpPost;
14  import org.apache.http.client.utils.URIBuilder;
15  import org.apache.http.client.utils.URIUtils;
16  import org.apache.http.entity.ContentType;
17  import org.apache.http.message.BasicNameValuePair;
18  import org.oxerr.okcoin.rest.dto.Depth;
19  import org.oxerr.okcoin.rest.dto.Funds;
20  import org.oxerr.okcoin.rest.dto.IcebergOrderHistory;
21  import org.oxerr.okcoin.rest.dto.Result;
22  import org.oxerr.okcoin.rest.dto.Ticker;
23  import org.oxerr.okcoin.rest.dto.TickerResponse;
24  import org.oxerr.okcoin.rest.dto.Trade;
25  import org.oxerr.okcoin.rest.dto.TradeParam;
26  import org.oxerr.okcoin.rest.dto.valuereader.IcebergOrdersReader;
27  import org.oxerr.okcoin.rest.dto.valuereader.IndexHtmlPageReader;
28  import org.oxerr.okcoin.rest.dto.valuereader.PlainTextReader;
29  import org.oxerr.okcoin.rest.dto.valuereader.ResultValueReader;
30  import org.oxerr.okcoin.rest.dto.valuereader.VoidValueReader;
31  import org.oxerr.okcoin.rest.service.OKCoinAccountService;
32  import org.oxerr.okcoin.rest.service.OKCoinMarketDataService;
33  import org.oxerr.okcoin.rest.service.OKCoinTradeService;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import com.fasterxml.jackson.core.type.TypeReference;
38  
39  public class OKCoinClient implements AutoCloseable {
40  
41  	public static final String ENCODING = "UTF-8";
42  
43  	public static final ContentType APPLICATION_FORM_URLENCODED = ContentType.create(
44  			"application/x-www-form-urlencoded", Consts.UTF_8);
45  
46  	private static final URI HTTPS_BASE = URI.create("https://www.okcoin.cn/");
47  	private static final URI API_BASE = URIUtils.resolve(HTTPS_BASE, "api/");
48  
49  	private static final URI TICKER_URI = URIUtils.resolve(API_BASE, "ticker.do");
50  	private static final URI DEPTH_URI = URIUtils.resolve(API_BASE, "depth.do");
51  	private static final URI TRADES_URI = URIUtils.resolve(API_BASE, "trades.do");
52  
53  	private static final URI LOGIN_URI = URIUtils.resolve(HTTPS_BASE, "login/index.do");
54  	private static final URI LOGOUT_URI = URIUtils.resolve(HTTPS_BASE, "user/logout.do");
55  
56  	private static final URI BUY_BTC_SUBMIT_URI = URIUtils.resolve(HTTPS_BASE, "trade/buyBtcSubmit.do");
57  	private static final URI SELL_BTC_SUBMIT_URI = URIUtils.resolve(HTTPS_BASE, "trade/sellBtcSubmit.do");
58  
59  	private static final URI SUBMIT_CONTINUOUS_ENTRUST_URI = URIUtils.resolve(HTTPS_BASE, "strategy/submitContinousEntrust.do");
60  	private static final URI CANCEL_CONTINUOUS_ENTRUST_URI = URIUtils.resolve(HTTPS_BASE, "strategy/cancelContinuousEntrust.do");
61  
62  	private static final String TRADE_BTC_REFERER_PREFIX = HTTPS_BASE.toString() + "trade/btc.do?tradeType=";
63  
64  	private static final BigDecimal MIN_TRADE_AMOUNT = new BigDecimal("0.01");
65  
66  	private final Logger log = LoggerFactory.getLogger(OKCoinClient.class);
67  
68  	private final HttpClient httpClient;
69  
70  	private String loginName;
71  
72  	private String password;
73  
74  	private String tradePwd;
75  
76  	public OKCoinClient(
77  			int socketTimeout,
78  			int connectTimeout,
79  			int connectionRequestTimeout) {
80  		this(null, null, null,
81  				socketTimeout, connectTimeout, connectionRequestTimeout);
82  	}
83  
84  	public OKCoinClient(String loginName, String password,
85  			int socketTimeout,
86  			int connectTimeout,
87  			int connectionRequestTimeout) {
88  		this(loginName, password, null,
89  				socketTimeout, connectTimeout, connectionRequestTimeout);
90  	}
91  
92  	public OKCoinClient(String loginName, String password, String tradePwd,
93  			int socketTimeout,
94  			int connectTimeout,
95  			int connectionRequestTimeout) {
96  		httpClient = new HttpClient(
97  				socketTimeout, connectTimeout, connectionRequestTimeout);
98  
99  		this.loginName = loginName;
100 		this.password = password;
101 		this.tradePwd = tradePwd;
102 	}
103 
104 	/**
105 	 * Gets ticker.
106 	 *
107 	 * @return the ticker.
108 	 * @throws IOException indicates I/O exception.
109 	 * @deprecated Use {@link OKCoinMarketDataService#getTicker} instead.
110 	 */
111 	@Deprecated
112 	public Ticker getTicker() throws IOException {
113 		return httpClient.get(TICKER_URI, TickerResponse.class).getTicker();
114 	}
115 
116 	/**
117 	 * @deprecated Use {@link OKCoinMarketDataService#getTicker} instead.
118 	 * @return the market order book.
119 	 * @throws IOException indicates I/O exception.
120 	 */
121 	@Deprecated
122 	public Depth getDepth() throws IOException {
123 		return httpClient.get(DEPTH_URI, Depth.class);
124 	}
125 
126 	/**
127 	 * Returns the recent 80 trades.
128 	 *
129 	 * @return the recent 80 trades.
130 	 * @throws IOException indicates I/O exception.
131 	 * @deprecated Use {@link OKCoinMarketDataService#getTrades(com.xeiam.xchange.currency.CurrencyPair, Object...)} instead.
132 	 */
133 	@Deprecated
134 	public List<Trade> getTrades() throws IOException {
135 		return httpClient.get(TRADES_URI, new TypeReference<List<Trade>>() {
136 		});
137 	}
138 
139 	/**
140 	 * Gets trades.
141 	 *
142 	 * @param since 0 based. When pass 0, will return the trades from the first trade.
143 	 * @return the trades.
144 	 * @throws IOException indicates I/O exception.
145 	 * @deprecated Use @link {@link OKCoinMarketDataService#getTrades(com.xeiam.xchange.currency.CurrencyPair, Object...)} instead.
146 	 */
147 	@Deprecated
148 	public List<Trade> getTrades(int since) throws IOException {
149 		URIBuilder builder = new URIBuilder(TRADES_URI);
150 		builder.addParameter("since", Integer.toString(since));
151 
152 		final URI uri = toURI(builder);
153 
154 		return httpClient.get(uri, new TypeReference<List<Trade>>() {
155 		});
156 	}
157 
158 	public void login() throws IOException {
159 		initLoginPage();
160 
161 		URIBuilder builder = new URIBuilder(LOGIN_URI);
162 		builder.setParameter("random", randomInString());
163 		URI uri = toURI(builder);
164 
165 		Result result = httpClient.post(
166 				uri,
167 				ResultValueReader.getInstance(),
168 				new BasicNameValuePair("loginName", loginName),
169 				new BasicNameValuePair("password", password));
170 
171 		log.debug("Login result: {}", result);
172 
173 		switch(result.getResultCode()) {
174 		case -1:
175 			throw new OKCoinClientException("用户名或密码错误");
176 		case -2:
177 			throw new OKCoinClientException("此ip登录频繁,请2小时后再试");
178 		case -3:
179 			if (result.getErrorNum() == 0) {
180 				throw new OKCoinClientException("此ip登录频繁,请2小时后再试");
181 			} else {
182 				throw new OKCoinClientException("用户名或密码错误,您还有" + result.getErrorNum() + "次机会");
183 			}
184 		case -4:
185 			throw new OKCoinClientException("请设置启用COOKIE功能");
186 		case 1:
187 			break;
188 		case 2:
189 			throw new OKCoinClientException("账户出现安全隐患被冻结,请尽快联系客服。");
190 		}
191 	}
192 
193 	public void logout() throws IOException {
194 		httpClient.get(LOGOUT_URI, VoidValueReader.getInstance());
195 	}
196 
197 	/**
198 	 * Gets the balance.
199 	 *
200 	 * @return the balance.
201 	 * @throws IOException indicates I/O exception.
202 	 * @deprecated Use {@link OKCoinAccountService#getAccountInfo()} instead.
203 	 */
204 	@Deprecated
205 	public Funds getBalance() throws IOException {
206 		return httpClient.get(URIUtils.resolve(HTTPS_BASE, "/trade/btc.do"),
207 				new IndexHtmlPageReader());
208 	}
209 
210 	public BigDecimal getMinTradeAmount() {
211 		return MIN_TRADE_AMOUNT;
212 	}
213 
214 	/**
215 	 * Places bid order.
216 	 *
217 	 * @param amount the order quantity.
218 	 * @param cnyPrice the order price in CNY.
219 	 * @throws IOException indicates I/O exception.
220 	 * @deprecated Use {@link OKCoinTradeService#placeLimitOrder(com.xeiam.xchange.dto.trade.LimitOrder)} instead.
221 	 */
222 	@Deprecated
223 	public void bid(BigDecimal amount, BigDecimal cnyPrice) throws IOException {
224 		final int tradeType = 0;
225 		final int symbol = 0;
226 		final int isopen = 0;
227 
228 		trade(amount, cnyPrice, tradeType, symbol, isopen);
229 	}
230 
231 	/**
232 	 * Places ask order.
233 	 *
234 	 * @param amount the order quantity.
235 	 * @param cnyPrice the order price in CNY.
236 	 * @throws IOException indicates I/O exception.
237 	 * @deprecated Use {@link OKCoinTradeService#placeLimitOrder(com.xeiam.xchange.dto.trade.LimitOrder)} instead.
238 	 */
239 	@Deprecated
240 	public void ask(BigDecimal amount, BigDecimal cnyPrice) throws IOException {
241 		final int tradeType = 1;
242 		final int symbol = 0;
243 		final int isopen = 0;
244 
245 		trade(amount, cnyPrice, tradeType, symbol, isopen);
246 	}
247 
248 	/**
249 	 * Submits continuous entrust.
250 	 *
251 	 * @param symbol 0: BTC, 1: LTC.
252 	 * @param type 1: Buy Iceberg Order, 2: Sell Iceberg Order.
253 	 * @param tradeValue total Order amount.
254 	 * @param singleAvg average order amount.
255 	 * @param depthRange price variance.
256 	 * @param protePrice highest buy price for buying, lowest sell price for selling.
257 	 * @return result of continuous entrust.
258 	 * @throws LoginRequiredException indicates the client is not logged in.
259 	 * @throws IOException indicates I/O exception.
260 	 */
261 	public Result submitContinuousEntrust(int symbol, int type,
262 			BigDecimal tradeValue, BigDecimal singleAvg, BigDecimal depthRange,
263 			BigDecimal protePrice) throws LoginRequiredException, IOException {
264 		URI uri = randomUri(SUBMIT_CONTINUOUS_ENTRUST_URI);
265 
266 		HttpPost post = new HttpPost(uri);
267 		post.setHeader("X-Requested-With", "XMLHttpRequest");
268 		String referer =  URIUtils.resolve(HTTPS_BASE, "trade/btc.do").toString();
269 		log.debug("Add referer header: {}", referer);
270 		post.setHeader("Referer", referer);
271 
272 		List<NameValuePair> params = new ArrayList<>(6);
273 		params.add(new BasicNameValuePair("symbol", String.valueOf(symbol)));
274 		params.add(new BasicNameValuePair("type", String.valueOf(type)));
275 		params.add(new BasicNameValuePair("tradeValue", tradeValue.toPlainString()));
276 		params.add(new BasicNameValuePair("singleAvg", singleAvg.toPlainString()));
277 		params.add(new BasicNameValuePair("depthRange", depthRange.toPlainString()));
278 		params.add(new BasicNameValuePair("protePrice", protePrice.toPlainString()));
279 		params.add(new BasicNameValuePair("tradePwd", tradePwd == null ? "" : tradePwd));
280 
281 		post.setEntity(new UrlEncodedFormEntity(params));
282 		return httpClient.execute(ResultValueReader.getInstance(), post);
283 	}
284 
285 	/**
286 	 * Cancel a continuous entrust.
287 	 *
288 	 * @param symbol 0: BTC, 1: LTC.
289 	 * @param id the order ID.
290 	 * @return true if canceled successfully.
291 	 * @throws LoginRequiredException indicates the client is not logged in.
292 	 * @throws IOException indicates I/O exception.
293 	 */
294 	public boolean cancelContinuousEntrust(int symbol, long id)
295 			throws LoginRequiredException, IOException {
296 		URI uri = randomUri(CANCEL_CONTINUOUS_ENTRUST_URI);
297 		HttpPost post = new HttpPost(uri);
298 		post.setHeader("X-Requested-With", "XMLHttpRequest");
299 		String referer =  URIUtils.resolve(HTTPS_BASE, "trade/btc.do").toString();
300 		log.debug("Add referer header: {}", referer);
301 		post.setHeader("Referer", referer);
302 
303 		List<NameValuePair> params = new ArrayList<>(2);
304 		params.add(new BasicNameValuePair("symbol", String.valueOf(symbol)));
305 		params.add(new BasicNameValuePair("id", String.valueOf(id)));
306 
307 		post.setEntity(new UrlEncodedFormEntity(params));
308 		String result = httpClient.execute(PlainTextReader.getInstance(), post);
309 		log.debug("result: {}", result);
310 		if (result.length() == 0) {
311 			throw new LoginRequiredException();
312 		}
313 		return Integer.parseInt(result) == 0;
314 	}
315 
316 	/**
317 	 * Returns iceberg orders.
318 	 *
319 	 * @param symbol 0: BTC, 1: LTC.
320 	 * @param type 5: ?
321 	 * @param sign 1: Open Orders, 2: Order History
322 	 * @param strategyType 2: ?
323 	 * @param page 1 based.
324 	 * @return iceberg orders.
325 	 * @throws LoginRequiredException indicates the client is not logged in.
326 	 * @throws IOException indicates I/O exception.
327 	 */
328 	public IcebergOrderHistory getIcebergOrders(int symbol, int type, int sign,
329 			int strategyType, Integer page) throws LoginRequiredException, IOException {
330 		URIBuilder builder = new URIBuilder(URIUtils.resolve(HTTPS_BASE, "strategy/refrushRecordNew.do"))
331 			.setParameter("symbol", String.valueOf(symbol))
332 			.setParameter("type", String.valueOf(type))
333 			.setParameter("sign", String.valueOf(sign))
334 			.setParameter("strategyType", String.valueOf(strategyType));
335 		if (page != null) {
336 			builder.setParameter("_data_ice", null);
337 			builder.setParameter("currentPage", page.toString());
338 			builder.setParameter("_", String.valueOf(System.currentTimeMillis()));
339 		}
340 		URI uri = toURI(builder);
341 		log.debug("uri: {}", uri);
342 		return httpClient.get(uri, IcebergOrdersReader.getInstance());
343 	}
344 
345 	/**
346 	 * {@inheritDoc}
347 	 */
348 	@Override
349 	public void close() throws IOException {
350 		logout();
351 		httpClient.close();
352 	}
353 
354 	/**
355 	 * Calls this method before doing login post is required, otherwise the
356 	 * server will complain "请设置启用COOKIE功能" as
357 	 * {@link Result#getResultCode()} = -4.
358 	 */
359 	private void initLoginPage() throws IOException {
360 		httpClient.get(HTTPS_BASE, VoidValueReader.getInstance());
361 	}
362 
363 	/**
364 	 *
365 	 * @param tradeAmount
366 	 * @param tradeCnyPrice
367 	 * @param tradeType 0 means buy, otherwise sell.
368 	 * @param symbol 0 means BTC, 1 means LTC.
369 	 * @param isopen
370 	 * @throws URISyntaxException
371 	 * @throws IOException
372 	 */
373 	private void trade(BigDecimal tradeAmount, BigDecimal tradeCnyPrice,
374 			int tradeType, int symbol, int isopen) throws IOException {
375 		if (tradeAmount.compareTo(MIN_TRADE_AMOUNT) < 0) {
376 			final String error;
377 			if (symbol == 1) {
378 				error = "最小交易数量为:0.1LTC!";
379 			} else {
380 				error = "最小交易数量为:0.01BTC!";
381 			}
382 			throw new IllegalArgumentException(error);
383 		}
384 
385 		final URI submitUri;
386 		if (tradeType == 0) {
387 			submitUri = BUY_BTC_SUBMIT_URI;
388 		} else {
389 			submitUri = SELL_BTC_SUBMIT_URI;
390 		}
391 
392 		final URI uri = randomUri(submitUri);
393 
394 		TradeParam tradeParam = new TradeParam(
395 				tradeAmount, tradeCnyPrice, tradePwd, symbol);
396 
397 		final HttpPost post = new HttpPost(uri);
398 		post.setHeader("X-Requested-With", "XMLHttpRequest");
399 		final String referer =  TRADE_BTC_REFERER_PREFIX + tradeType;
400 		log.debug("Add referer header: {}", referer);
401 		post.setHeader("Referer", referer);
402 		post.setEntity(new UrlEncodedFormEntity(tradeParam.toNameValurPairs()));
403 
404 		Result result = httpClient.execute(ResultValueReader.getInstance(), post);
405 
406 		final String error;
407 		if (result != null) {
408 			log.debug("Result: {}", result);
409 
410 			if (result.getResultCode() == -1) {
411 				if (tradeType == 0) {
412 					if (symbol == 1) {
413 						error = "最小购买数量为:0.1LTC!";
414 					} else {
415 						error = "最小购买数量为:0.01BTC!";
416 					}
417 				} else {
418 					if (symbol == 1) {
419 						error = "最小卖出数量为:0.1LTC!";
420 					} else {
421 						error = "最小卖出数量为:0.01BTC!";
422 					}
423 				}
424 			} else if (result.getResultCode() == -2) {
425 				if (result.getErrorNum() == 0) {
426 					error = "交易密码错误五次,请2小时后再试!";
427 				} else {
428 					error = "交易密码不正确!您还有" + result.getErrorNum() + "次机会";
429 				}
430 			} else if (result.getResultCode() == -3) {
431 				error = "出价不能为0!";
432 			} else if (result.getResultCode() == -4) {
433 				error = "余额不足!";
434 			} else if (result.getResultCode() == -5) {
435 				error = "您未设置交易密码,请设置交易密码。";
436 			} else if (result.getResultCode() == -6) {
437 				error = "您输入的价格与最新成交价相差太大,请检查是否输错";
438 			} else if (result.getResultCode() == -7) {
439 				error = "交易密码免输超时,请刷新页面输入交易密码后重新激活。";
440 			} else if (result.getResultCode() == -8) {
441 				error = "请输入交易密码";
442 			} else if (result.getResultCode() == 0) {
443 				// success
444 				error = null;
445 			} else if (result.getResultCode() == 2) {
446 				error = "账户出现安全隐患已被冻结,请尽快联系客服。";
447 			} else {
448 				error = null;
449 			}
450 		} else {
451 			error = null;
452 		}
453 
454 		if (error != null) {
455 			throw new OKCoinClientException(error);
456 		}
457 	}
458 
459 	private String randomInString() {
460 		return Long.toString(random());
461 	}
462 
463 	private long random() {
464 		long random = Math.round(Math.random() * 100);
465 		return random;
466 	}
467 
468 	/**
469 	 * @return
470 	 * @throws IOException
471 	 */
472 	private URI randomUri(URI uri) throws IOException {
473 		final URI ret = toURI(new URIBuilder(uri)
474 			.setParameter("random", randomInString()));
475 		return ret;
476 	}
477 
478 	private URI toURI(URIBuilder builder) {
479 		try {
480 			return builder.build();
481 		} catch (URISyntaxException e) {
482 			throw new IllegalArgumentException(e);
483 		}
484 	}
485 
486 }