Skip to content

Commit 2243573

Browse files
authored
Merge pull request #5 from namjug-kim/feature/add_exchange_bithumb
Add exchange bithumb
2 parents 6122754 + 2840a98 commit 2243573

File tree

15 files changed

+592
-7
lines changed

15 files changed

+592
-7
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Reactive CryptoCurrency
2-
[![CircleCI](https://circleci.com/gh/namjug-kim/reactive-crypto.svg?style=svg&circle-token=aa6aa4ebd3956dd3e1a767d938c7e73869ffd6ab)](https://circleci.com/gh/namjug-kim/reactive-crypto)
2+
[![Kotlin](https://img.shields.io/badge/kotlin-1.3.x-blue.svg)](http://kotlinlang.org) [![CircleCI](https://circleci.com/gh/namjug-kim/reactive-crypto.svg?style=shield&circle-token=aa6aa4ebd3956dd3e1a767d938c7e73869ffd6ab)](https://circleci.com/gh/namjug-kim/reactive-crypto)
33

44
A Kotlin library for cryptocurrency trading.
55

@@ -10,10 +10,13 @@ Support public market feature (tickData, orderBook)
1010

1111
| Exchange | ver | doc |
1212
|----------------|---|---|
13-
| Binance | * | [ws](https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md)| Done |
14-
| Upbit | v1.0.3 | [ws](https://docs.upbit.com/docs/upbit-quotation-websocket) | Done |
15-
| HuobiKorea | * | [ws](https://github.com/alphaex-api/BAPI_Docs_ko/wiki) | Done |
16-
| Okex | v3 | [ws](https://www.okex.com/docs/en/#spot_ws-all) | Done |
13+
| Binance | * | [ws](https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md)|
14+
| Upbit | v1.0.3 | [ws](https://docs.upbit.com/docs/upbit-quotation-websocket) |
15+
| HuobiKorea | * | [ws](https://github.com/alphaex-api/BAPI_Docs_ko/wiki) |
16+
| Okex | v3 | [ws](https://www.okex.com/docs/en/#spot_ws-all) |
17+
| Bithumb⚠️ | - | - |
18+
19+
⚠️ : Uses endpoints that are used by the official web. This is not an official api and should be used with care.
1720

1821
### Api
1922
| Exchange | ver | doc |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apply plugin: 'kotlin'
2+
apply plugin: 'org.jetbrains.kotlin.jvm'
3+
4+
version '1.0-SNAPSHOT'
5+
6+
dependencies {
7+
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
8+
9+
compile project(':reactive-crypto-core')
10+
}
11+
12+
compileKotlin {
13+
kotlinOptions.jvmTarget = "1.8"
14+
}
15+
compileTestKotlin {
16+
kotlinOptions.jvmTarget = "1.8"
17+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.njkim.reactivecrypto.bithumb
2+
3+
import com.fasterxml.jackson.core.JsonParser
4+
import com.fasterxml.jackson.core.JsonProcessingException
5+
import com.fasterxml.jackson.databind.DeserializationContext
6+
import com.fasterxml.jackson.databind.DeserializationFeature
7+
import com.fasterxml.jackson.databind.JsonDeserializer
8+
import com.fasterxml.jackson.databind.ObjectMapper
9+
import com.fasterxml.jackson.databind.module.SimpleModule
10+
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
11+
import com.njkim.reactivecrypto.core.common.model.currency.Currency
12+
import mu.KotlinLogging
13+
import org.apache.commons.lang3.StringUtils
14+
import java.io.IOException
15+
import java.math.BigDecimal
16+
import java.time.LocalDateTime
17+
import java.time.ZoneOffset
18+
import java.time.ZonedDateTime
19+
import java.time.format.DateTimeFormatter
20+
21+
class BithumbJsonObjectMapper {
22+
private val log = KotlinLogging.logger {}
23+
24+
companion object {
25+
val instance = BithumbJsonObjectMapper().objectMapper()
26+
}
27+
28+
private fun objectMapper(): ObjectMapper {
29+
val simpleModule = SimpleModule()
30+
31+
simpleModule.addDeserializer(ZonedDateTime::class.java, object : JsonDeserializer<ZonedDateTime>() {
32+
@Throws(IOException::class, JsonProcessingException::class)
33+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ZonedDateTime {
34+
// Bithumb use KOR(+9) timezone without zone information
35+
val parsedKorLocalDateTime = LocalDateTime.parse(
36+
p.valueAsString,
37+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
38+
)
39+
return ZonedDateTime.of(parsedKorLocalDateTime, ZoneOffset.ofHours(9))
40+
}
41+
})
42+
simpleModule.addDeserializer(BigDecimal::class.java, object : JsonDeserializer<BigDecimal>() {
43+
@Throws(IOException::class, JsonProcessingException::class)
44+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BigDecimal? {
45+
val valueAsString = p.valueAsString
46+
return if (StringUtils.isBlank(valueAsString)) {
47+
null
48+
} else BigDecimal(valueAsString)
49+
}
50+
})
51+
simpleModule.addDeserializer(Currency::class.java, object : JsonDeserializer<Currency>() {
52+
@Throws(IOException::class, JsonProcessingException::class)
53+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Currency? {
54+
val rawValue = p.valueAsString
55+
return Currency.valueOf(rawValue)
56+
}
57+
})
58+
59+
val objectMapper = ObjectMapper().registerKotlinModule()
60+
objectMapper.registerModule(simpleModule)
61+
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
62+
return objectMapper
63+
}
64+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.njkim.reactivecrypto.bithumb
2+
3+
import com.fasterxml.jackson.module.kotlin.readValue
4+
import com.njkim.reactivecrypto.bithumb.model.BithumbOrderBook
5+
import com.njkim.reactivecrypto.bithumb.model.BithumbResponseWrapper
6+
import com.njkim.reactivecrypto.bithumb.model.BithumbTickData
7+
import com.njkim.reactivecrypto.core.ExchangeWebsocketClient
8+
import com.njkim.reactivecrypto.core.common.model.ExchangeVendor
9+
import com.njkim.reactivecrypto.core.common.model.currency.Currency
10+
import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
11+
import com.njkim.reactivecrypto.core.common.model.order.OrderBook
12+
import com.njkim.reactivecrypto.core.common.model.order.OrderBookUnit
13+
import com.njkim.reactivecrypto.core.common.model.order.OrderSideType
14+
import com.njkim.reactivecrypto.core.common.model.order.TickData
15+
import mu.KotlinLogging
16+
import reactor.core.publisher.Flux
17+
import reactor.core.publisher.toFlux
18+
import reactor.netty.http.client.HttpClient
19+
import java.time.ZonedDateTime
20+
21+
class BithumbWebsocketClient : ExchangeWebsocketClient {
22+
private val log = KotlinLogging.logger {}
23+
24+
private val baseUri = "wss://wss.bithumb.com/public"
25+
26+
override fun createTradeWebsocket(subscribeTargets: List<CurrencyPair>): Flux<TickData> {
27+
val subscribeRequests = subscribeTargets.stream()
28+
.map {
29+
if (it.baseCurrency == Currency.KRW) {
30+
"${it.targetCurrency}"
31+
} else {
32+
"${it.targetCurrency}${it.baseCurrency}"
33+
}
34+
}
35+
.map { "{\"currency\":\"$it\",\"tickDuration\":\"24H\",\"service\":\"transaction\"}" }
36+
.toFlux()
37+
38+
return HttpClient.create()
39+
.headers { it.add("Origin", "https://www.bithumb.com") }
40+
.wiretap(log.isDebugEnabled)
41+
.websocket()
42+
.uri(baseUri)
43+
.handle { inbound, outbound ->
44+
outbound.sendString(subscribeRequests)
45+
.then()
46+
.thenMany(inbound.receive().asString())
47+
}
48+
.map { BithumbJsonObjectMapper.instance.readValue<BithumbResponseWrapper<List<BithumbTickData>>>(it) }
49+
.flatMapIterable {
50+
it.data.map { bithumbTickData ->
51+
TickData(
52+
bithumbTickData.countNo.toString(),
53+
bithumbTickData.transactionDate,
54+
bithumbTickData.price,
55+
bithumbTickData.unitsTraded,
56+
CurrencyPair(it.header.currency, Currency.KRW), // Bithumb only have KRW market
57+
ExchangeVendor.BITHUMB
58+
)
59+
}
60+
}
61+
}
62+
63+
override fun createDepthSnapshot(subscribeTargets: List<CurrencyPair>): Flux<OrderBook> {
64+
val subscribeRequests = subscribeTargets.stream()
65+
.map {
66+
if (it.baseCurrency == Currency.KRW) {
67+
"${it.targetCurrency}"
68+
} else {
69+
"${it.targetCurrency}${it.baseCurrency}"
70+
}
71+
}
72+
.map { "{\"currency\":\"$it\",\"tickDuration\":\"24H\",\"service\":\"orderbook\"}" }
73+
.toFlux()
74+
75+
return HttpClient.create()
76+
.headers { it.add("Origin", "https://www.bithumb.com") }
77+
.wiretap(log.isDebugEnabled)
78+
.websocket()
79+
.uri(baseUri)
80+
.handle { inbound, outbound ->
81+
outbound.sendString(subscribeRequests)
82+
.then()
83+
.thenMany(inbound.receive().asString())
84+
}
85+
.map { BithumbJsonObjectMapper.instance.readValue<BithumbResponseWrapper<BithumbOrderBook>>(it) }
86+
.map {
87+
OrderBook(
88+
"${it.header.currency}${ZonedDateTime.now().toInstant().toEpochMilli()}",
89+
CurrencyPair(it.header.currency, Currency.KRW), // Bithumb only have KRW market
90+
ZonedDateTime.now(),
91+
ExchangeVendor.BITHUMB,
92+
it.data.bids.map { bithumbBid ->
93+
OrderBookUnit(
94+
bithumbBid.price,
95+
bithumbBid.quantity,
96+
OrderSideType.BID,
97+
null
98+
)
99+
},
100+
it.data.asks.map { bithumbAsk ->
101+
OrderBookUnit(
102+
bithumbAsk.price,
103+
bithumbAsk.quantity,
104+
OrderSideType.ASK,
105+
null
106+
)
107+
}
108+
)
109+
}
110+
111+
}
112+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.njkim.reactivecrypto.bithumb.model
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import java.math.BigDecimal
5+
6+
data class BithumbOrderBook(
7+
@get:JsonProperty("asks")
8+
val asks: List<BithumbOrderBookUnit>,
9+
10+
@get:JsonProperty("bids")
11+
val bids: List<BithumbOrderBookUnit>
12+
)
13+
14+
data class BithumbOrderBookUnit(
15+
@get:JsonProperty("price")
16+
val price: BigDecimal,
17+
18+
@get:JsonProperty("quantity")
19+
val quantity: BigDecimal
20+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.njkim.reactivecrypto.bithumb.model
2+
3+
import com.njkim.reactivecrypto.core.common.model.currency.Currency
4+
5+
data class BithumbResponseHeader(
6+
val currency: Currency,
7+
val service: String
8+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.njkim.reactivecrypto.bithumb.model
2+
3+
/**
4+
* {
5+
* "amount":"0",
6+
* "data":[
7+
* {
8+
* "cont_no":34601963,
9+
* "price":"6632000",
10+
* "total":"952355.2",
11+
* "transaction_date":"2019-05-04 22:05:49.530989",
12+
* "type":"up",
13+
* "units_traded":"0.1436"
14+
* }
15+
* ],
16+
* "header":{
17+
* "currency":"BTC",
18+
* "service":"transaction"
19+
* },
20+
* "status":"0000"
21+
* }
22+
*/
23+
data class BithumbResponseWrapper<T>(
24+
val data: T,
25+
val header: BithumbResponseHeader,
26+
val status: String
27+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.njkim.reactivecrypto.bithumb.model
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import java.math.BigDecimal
5+
import java.time.ZonedDateTime
6+
7+
/**
8+
* {
9+
* "cont_no":34601963,
10+
* "price":"6632000",
11+
* "total":"952355.2",
12+
* "transaction_date":"2019-05-04 22:05:49.530989",
13+
* "type":"up",
14+
* "units_traded":"0.1436"
15+
* }
16+
*/
17+
data class BithumbTickData(
18+
@get:JsonProperty("count_no")
19+
val countNo: Long,
20+
21+
@get:JsonProperty("price")
22+
val price: BigDecimal,
23+
24+
@get:JsonProperty("total")
25+
val total: BigDecimal,
26+
27+
@get:JsonProperty("transaction_date")
28+
val transactionDate: ZonedDateTime,
29+
30+
@get:JsonProperty("type")
31+
val type: String,
32+
33+
@get:JsonProperty("units_traded")
34+
val unitsTraded: BigDecimal
35+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration>
3+
<Properties>
4+
<Property name="LOG_PATTERN">
5+
%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex
6+
</Property>
7+
</Properties>
8+
9+
<Appenders>
10+
<Console name="ConsoleAppender" target="SYSTEM_OUT" follow="true">
11+
<PatternLayout pattern="${LOG_PATTERN}"/>
12+
</Console>
13+
</Appenders>
14+
15+
<Loggers>
16+
<Root level="info">
17+
<AppenderRef ref="ConsoleAppender"/>
18+
</Root>
19+
</Loggers>
20+
</Configuration>

0 commit comments

Comments
 (0)