Skip to content

Commit

Permalink
feature: add new exchange 'Huobi Global'
Browse files Browse the repository at this point in the history
  • Loading branch information
namjug-kim committed Jun 23, 2019
1 parent 61bc858 commit e052926
Show file tree
Hide file tree
Showing 21 changed files with 624 additions and 168 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Support public market feature (tickData, orderBook)
| --------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------- |--------|---|
| ![binance](https://user-images.githubusercontent.com/16334718/57194951-e5e88600-6f87-11e9-918e-74de5c58e883.jpg) | Binance | BINANCE | * | [ws](https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md) |
| ![upbit](https://user-images.githubusercontent.com/16334718/57194949-e54fef80-6f87-11e9-85b3-67b8f82db564.jpg) | Upbit | UPBIT | v1.0.3 | [ws](https://docs.upbit.com/docs/upbit-quotation-websocket) |
| ![huobi_global](https://user-images.githubusercontent.com/16334718/59974411-f19b1500-95e6-11e9-95e3-a68a34e65c68.jpg) | HuobiGlobal | HUOBI_GLOBAL | * | [ws](https://github.com/huobiapi/API_Docs_en/wiki/WS_api_reference_en) |
| ![huobi korea](https://user-images.githubusercontent.com/16334718/57194946-e4b75900-6f87-11e9-940a-08ceb98193e4.jpg) | HuobiKorea | HUOBI_KOREA | * | [ws](https://github.com/alphaex-api/BAPI_Docs_ko/wiki) |
| ![okex](https://user-images.githubusercontent.com/16334718/57195022-90f93f80-6f88-11e9-8aaa-f6a515d300ae.jpg) | Okex | OKEX | v3 | [ws](https://www.okex.com/docs/en/#spot_ws-all) |
| ![bithumb](https://user-images.githubusercontent.com/16334718/57194948-e54fef80-6f87-11e9-90d8-41f108789c77.jpg) | Bithumb | BITHUMB | ⚠️ | ⚠️ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import com.njkim.reactivecrypto.core.common.util.toCarmelCase
enum class ExchangeVendor {
UPBIT,
BINANCE,
HUOBI_GLOBAL,
HUOBI_JAPAN,
HUOBI_KOREA,
OKEX,
BITHUMB,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ enum class Currency {

M19,

KRW, USD,
KRW, USD, JPY,

USDT, TUSD;

Expand All @@ -53,7 +53,8 @@ enum class Currency {
USDT,
TUSD,
BTC,
ETH
ETH,
JPY
)
}
}
33 changes: 33 additions & 0 deletions reactive-crypto-huobiglobal/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2019 namjug-kim
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.kotlin.jvm'

version '1.0-SNAPSHOT'

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

compile project(':reactive-crypto-core')
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/

package com.njkim.reactivecrypto.huobikorea
package com.njkim.reactivecrypto.huobiglobal

import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
import com.njkim.reactivecrypto.core.common.util.CurrencyPairUtil
Expand All @@ -24,4 +24,4 @@ object HuobiCommonUtil {
val parse = CurrencyPairUtil.parse(rawValue)
return checkNotNull(parse)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2019 namjug-kim
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.njkim.reactivecrypto.huobiglobal

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.njkim.reactivecrypto.core.ExchangeJsonObjectMapper
import com.njkim.reactivecrypto.core.common.model.ExchangeVendor
import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
import com.njkim.reactivecrypto.core.common.model.order.OrderBook
import com.njkim.reactivecrypto.core.common.model.order.TickData
import com.njkim.reactivecrypto.core.common.util.toEpochMilli
import com.njkim.reactivecrypto.core.netty.HeartBeatHandler
import com.njkim.reactivecrypto.core.websocket.AbstractExchangeWebsocketClient
import com.njkim.reactivecrypto.huobiglobal.model.HuobiTickDataWrapper
import com.njkim.reactivecrypto.huobiglobal.model.HuobiOrderBook
import com.njkim.reactivecrypto.huobiglobal.model.HuobiSubscribeResponse
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.ByteToMessageDecoder
import io.netty.handler.codec.compression.JdkZlibDecoder
import io.netty.handler.codec.compression.ZlibWrapper
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame
import mu.KotlinLogging
import org.apache.commons.codec.Charsets
import org.apache.commons.lang3.StringUtils
import org.springframework.util.StreamUtils
import reactor.core.publisher.Flux
import reactor.netty.http.client.HttpClient
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit
import kotlin.streams.toList

@Suppress("IMPLICIT_CAST_TO_ANY")
open class HuobiGlobalWebsocketClient(
private val baseUri: String = "wss://api.huobi.pro/ws"
) : AbstractExchangeWebsocketClient() {
private val log = KotlinLogging.logger {}

private val objectMapper: ObjectMapper = createJsonObjectMapper().objectMapper()

override fun createJsonObjectMapper(): ExchangeJsonObjectMapper {
return HuobiJsonObjectMapper()
}

override fun createDepthSnapshot(subscribeTargets: List<CurrencyPair>): Flux<OrderBook> {
val subscribeMessages = subscribeTargets.stream()
.map { "${it.targetCurrency.name.toLowerCase()}${it.baseCurrency.name.toLowerCase()}" }
.map { "{\"sub\": \"market.$it.depth.step0\",\"id\": \"$it\"}" }
.toList()

return HttpClient.create()
.wiretap(log.isDebugEnabled)
.tcpConfiguration { tcp ->
tcp.doOnConnected { connection ->
connection.addHandler(JdkZlibDecoder(ZlibWrapper.GZIP, true))
connection.addHandler(PingPongHandler())
connection.addHandler(
"heartBeat",
HeartBeatHandler(
false,
2000,
TimeUnit.MILLISECONDS,
1000
) { "{\"ping\": ${ZonedDateTime.now().toEpochMilli()}}" }
)
}
}
.websocket()
.uri(baseUri)
.handle { inbound, outbound ->
outbound.sendString(Flux.fromIterable<String>(subscribeMessages))
.then()
.thenMany(inbound.receive().asString())
}
.doOnNext { log.debug { it } }
.filter { it.contains("\"ch\"") }
.map { objectMapper.readValue<HuobiSubscribeResponse<HuobiOrderBook>>(it) }
.map {
OrderBook(
"${it.currencyPair}${it.ts.toEpochMilli()}",
it.currencyPair,
ZonedDateTime.now(),
ExchangeVendor.HUOBI_GLOBAL,
it.tick.getBids(),
it.tick.getAsks()
)
}
}

override fun createTradeWebsocket(subscribeTargets: List<CurrencyPair>): Flux<TickData> {
val subscribeMessages = subscribeTargets.stream()
.map { "${it.targetCurrency.name.toLowerCase()}${it.baseCurrency.name.toLowerCase()}" }
.map { "{\"sub\": \"market.$it.trade.detail\",\"id\": \"$it\"}" }
.toList()

return HttpClient.create()
.wiretap(log.isDebugEnabled)
.tcpConfiguration { tcp ->
tcp.doOnConnected { connection ->
connection.addHandler(JdkZlibDecoder(ZlibWrapper.GZIP, true))
connection.addHandler(PingPongHandler())
connection.addHandler(
"heartBeat",
HeartBeatHandler(
false,
2000,
TimeUnit.MILLISECONDS,
1000
) { "{\"ping\": ${ZonedDateTime.now().toEpochMilli()}}" }
)
}
}
.websocket()
.uri(baseUri)
.handle { inbound, outbound ->
outbound.sendString(Flux.fromIterable<String>(subscribeMessages))
.then()
.thenMany(inbound.receive().asString())
}
.filter { it.contains("\"ch\"") }
.map { objectMapper.readValue<HuobiSubscribeResponse<HuobiTickDataWrapper>>(it) }
.flatMapIterable {
it.tick.data
.map { huobiKoreaTickData ->
TickData(
huobiKoreaTickData.id.toPlainString(),
huobiKoreaTickData.ts,
huobiKoreaTickData.price,
huobiKoreaTickData.amount,
it.currencyPair,
ExchangeVendor.HUOBI_GLOBAL,
huobiKoreaTickData.direction
)
}
.toList()
}
}

/**
* server sent ping {"ping" : $epochMilli }
* client response pong {"pong" : $epochMilli }
*/
private inner class PingPongHandler : ByteToMessageDecoder() {
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
ByteBufInputStream(msg).use {
val response = StreamUtils.copyToString(it, Charsets.UTF_8)
if (StringUtils.contains(response, "ping")) {
val replace = response.replace("ping", "pong")
ctx.channel().writeAndFlush(TextWebSocketFrame(replace))
} else {
val uncompressed = msg.alloc().buffer().writeBytes(response.toByteArray())
out.add(uncompressed)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/

package com.njkim.reactivecrypto.huobikorea
package com.njkim.reactivecrypto.huobiglobal

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException
Expand Down Expand Up @@ -55,4 +55,4 @@ class HuobiJsonObjectMapper : ExchangeJsonObjectMapper {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/

package com.njkim.reactivecrypto.huobikorea.model
package com.njkim.reactivecrypto.huobiglobal.model

import com.njkim.reactivecrypto.core.common.model.order.OrderBookUnit
import com.njkim.reactivecrypto.core.common.model.order.OrderSideType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
* under the License.
*/

package com.njkim.reactivecrypto.huobikorea.model
package com.njkim.reactivecrypto.huobiglobal.model

import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
import com.njkim.reactivecrypto.huobikorea.HuobiCommonUtil
import com.njkim.reactivecrypto.huobiglobal.HuobiCommonUtil
import java.time.ZonedDateTime
import java.util.regex.Pattern

Expand All @@ -37,4 +37,4 @@ data class HuobiSubscribeResponse<T>(

throw IllegalArgumentException()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,16 @@
* under the License.
*/

package com.njkim.reactivecrypto.huobikorea.model
package com.njkim.reactivecrypto.huobiglobal.model

import com.njkim.reactivecrypto.core.common.model.order.TradeSideType
import java.math.BigDecimal
import java.time.ZonedDateTime

data class HuobiKoreaTickDataWrapper(
val id: BigDecimal,
val ts: ZonedDateTime,
val data: List<HuobiKoreaTickData>
)

data class HuobiKoreaTickData(
data class HuobiTickData(
val id: BigDecimal,
val amount: BigDecimal,
val ts: ZonedDateTime,
val price: BigDecimal,
val direction: TradeSideType
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2019 namjug-kim
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.njkim.reactivecrypto.huobiglobal.model

import java.math.BigDecimal
import java.time.ZonedDateTime

data class HuobiTickDataWrapper(
val id: BigDecimal,
val ts: ZonedDateTime,
val data: List<HuobiTickData>
)

0 comments on commit e052926

Please sign in to comment.