Skip to content

Commit 6e10e76

Browse files
authored
Merge pull request #75 from namjug-kim/add_bitstamp_exchange
feat: add bitstamp websocket client
2 parents 1c746a8 + fd6a38b commit 6e10e76

20 files changed

+1012
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Support public market feature (tickData, orderBook)
3535
| ![idax](https://user-images.githubusercontent.com/16334718/58029691-128bc880-7b58-11e9-9aaa-a331f394c8bd.jpg) | Idax | IDAX | * | [ws](https://github.com/idax-exchange/idax-official-api-docs/blob/master/open-ws_en.md) |
3636
| ![coineal](https://user-images.githubusercontent.com/16334718/58037062-7d90cb80-7b67-11e9-9278-e8b03c5ddd86.jpg) | Coineal | COINEAL | ⚠️ | ⚠️ |
3737
| ![poloniex](https://user-images.githubusercontent.com/16334718/59551277-335a0900-8fb2-11e9-9d1e-4ab2a7574148.jpg) | Poloniex | POLONIEX | * | [ws](https://docs.poloniex.com/#websocket-api) |
38+
| ![bitstamp](https://user-images.githubusercontent.com/16334718/59565122-2c062e80-908a-11e9-8a38-6264c26aa3c2.jpg) | Bitstamp | BITSTAMP | v2 | [ws](https://www.bitstamp.net/websocket/v2/) |
3839

3940
⚠️ : Uses endpoints that are used by the official web. This is not an official api and should be used with care.
4041

reactive-crypto-bitstamp/build.gradle

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2019 namjug-kim
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
apply plugin: 'kotlin'
18+
apply plugin: 'org.jetbrains.kotlin.jvm'
19+
20+
version '1.0-SNAPSHOT'
21+
22+
dependencies {
23+
compile project(':reactive-crypto-core')
24+
25+
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
26+
}
27+
28+
compileKotlin {
29+
kotlinOptions.jvmTarget = "1.8"
30+
}
31+
compileTestKotlin {
32+
kotlinOptions.jvmTarget = "1.8"
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2019 namjug-kim
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.njkim.reactivecrypto.bitstamp
18+
19+
import com.fasterxml.jackson.core.JsonParser
20+
import com.fasterxml.jackson.databind.DeserializationContext
21+
import com.fasterxml.jackson.databind.JsonDeserializer
22+
import com.fasterxml.jackson.databind.JsonNode
23+
import com.fasterxml.jackson.databind.ObjectMapper
24+
import com.fasterxml.jackson.databind.module.SimpleModule
25+
import com.njkim.reactivecrypto.bitstamp.model.BitstampDetailOrderBook
26+
import com.njkim.reactivecrypto.bitstamp.model.BitstampDetailOrderBookUnit
27+
import com.njkim.reactivecrypto.bitstamp.model.BitstampOrderBook
28+
import com.njkim.reactivecrypto.bitstamp.model.BitstampOrderBookUnit
29+
import com.njkim.reactivecrypto.core.ExchangeJsonObjectMapper
30+
import com.njkim.reactivecrypto.core.common.model.order.TradeSideType
31+
import java.math.BigDecimal
32+
import java.time.Instant
33+
import java.time.ZoneId
34+
import java.time.ZonedDateTime
35+
import java.util.concurrent.TimeUnit
36+
37+
class BitstampJsonObjectMapper : ExchangeJsonObjectMapper {
38+
39+
companion object {
40+
val instance: ObjectMapper = BitstampJsonObjectMapper().objectMapper()
41+
}
42+
43+
override fun bigDecimalDeserializer(): JsonDeserializer<BigDecimal> {
44+
return object : JsonDeserializer<BigDecimal>() {
45+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BigDecimal {
46+
return BigDecimal(p.valueAsString)
47+
}
48+
}
49+
}
50+
51+
// "1560675931555480"
52+
// "1560675931"
53+
override fun zonedDateTimeDeserializer(): JsonDeserializer<ZonedDateTime> {
54+
return object : JsonDeserializer<ZonedDateTime>() {
55+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ZonedDateTime {
56+
return if (p.valueAsLong > 1000000000000000) {
57+
Instant.ofEpochSecond(TimeUnit.MICROSECONDS.toSeconds(p.valueAsLong), p.valueAsLong % 1000000)
58+
.atZone(ZoneId.systemDefault())
59+
} else {
60+
Instant.ofEpochSecond(p.valueAsLong)
61+
.atZone(ZoneId.systemDefault())
62+
}
63+
}
64+
}
65+
}
66+
67+
override fun tradeSideTypeDeserializer(): JsonDeserializer<TradeSideType>? {
68+
// Trade type (0 - buy; 1 - sell).
69+
return object : JsonDeserializer<TradeSideType>() {
70+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): TradeSideType {
71+
return when (p.valueAsInt) {
72+
0 -> TradeSideType.BUY
73+
1 -> TradeSideType.SELL
74+
else -> throw IllegalArgumentException()
75+
}
76+
}
77+
}
78+
}
79+
80+
override fun customConfiguration(simpleModule: SimpleModule) {
81+
val orderBookUnitDeserializer = object : JsonDeserializer<BitstampOrderBookUnit>() {
82+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BitstampOrderBookUnit {
83+
val jsonNode: JsonNode = p.codec.readTree(p)
84+
return BitstampOrderBookUnit(
85+
instance.convertValue(jsonNode[0], BigDecimal::class.java),
86+
instance.convertValue(jsonNode[1], BigDecimal::class.java)
87+
)
88+
}
89+
}
90+
91+
val orderBookDeserializer = object : JsonDeserializer<BitstampOrderBook>() {
92+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BitstampOrderBook {
93+
val jsonNode: JsonNode = p.codec.readTree(p)
94+
val timestampNode = jsonNode.get("timestamp")
95+
val microtimestampNode = jsonNode.get("microtimestamp")
96+
val bidsNode = jsonNode.get("bids")
97+
val asksNode = jsonNode.get("asks")
98+
99+
return BitstampOrderBook(
100+
instance.convertValue(microtimestampNode, ZonedDateTime::class.java),
101+
instance.convertValue(timestampNode, ZonedDateTime::class.java),
102+
bidsNode.map { instance.convertValue(it, BitstampOrderBookUnit::class.java) },
103+
asksNode.map { instance.convertValue(it, BitstampOrderBookUnit::class.java) }
104+
)
105+
}
106+
}
107+
108+
val detailOrderBookUnitDeserializer = object : JsonDeserializer<BitstampDetailOrderBookUnit>() {
109+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BitstampDetailOrderBookUnit {
110+
val jsonNode: JsonNode = p.codec.readTree(p)
111+
return BitstampDetailOrderBookUnit(
112+
instance.convertValue(jsonNode[0], BigDecimal::class.java),
113+
instance.convertValue(jsonNode[1], BigDecimal::class.java),
114+
jsonNode[2].asText()
115+
)
116+
}
117+
}
118+
119+
val detailOrderBookDeserializer = object : JsonDeserializer<BitstampDetailOrderBook>() {
120+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): BitstampDetailOrderBook {
121+
val jsonNode: JsonNode = p.codec.readTree(p)
122+
val timestampNode = jsonNode.get("timestamp")
123+
val microtimestampNode = jsonNode.get("microtimestamp")
124+
val bidsNode = jsonNode.get("bids")
125+
val asksNode = jsonNode.get("asks")
126+
127+
return BitstampDetailOrderBook(
128+
instance.convertValue(microtimestampNode, ZonedDateTime::class.java),
129+
instance.convertValue(timestampNode, ZonedDateTime::class.java),
130+
bidsNode.map { instance.convertValue(it, BitstampDetailOrderBookUnit::class.java) },
131+
asksNode.map { instance.convertValue(it, BitstampDetailOrderBookUnit::class.java) }
132+
)
133+
}
134+
}
135+
136+
simpleModule.addDeserializer(BitstampOrderBookUnit::class.java, orderBookUnitDeserializer)
137+
simpleModule.addDeserializer(BitstampOrderBook::class.java, orderBookDeserializer)
138+
simpleModule.addDeserializer(BitstampDetailOrderBookUnit::class.java, detailOrderBookUnitDeserializer)
139+
simpleModule.addDeserializer(BitstampDetailOrderBook::class.java, detailOrderBookDeserializer)
140+
}
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2019 namjug-kim
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.njkim.reactivecrypto.bitstamp
18+
19+
import com.fasterxml.jackson.module.kotlin.readValue
20+
import com.njkim.reactivecrypto.bitstamp.model.*
21+
import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
22+
import reactor.core.publisher.Flux
23+
import reactor.core.publisher.toFlux
24+
import reactor.netty.http.client.HttpClient
25+
26+
/**
27+
* Bitstamp Websocket API v2
28+
* document : https://www.bitstamp.net/websocket/v2/
29+
*/
30+
class BitstampRawWebsocketClient {
31+
private val baseUrl: String = "wss://ws.bitstamp.net"
32+
33+
fun liveTicker(currencyPairs: List<CurrencyPair>): Flux<BitstampMessageFrame<BitstampTradeEvent>> {
34+
val subscribeMessages = currencyPairs.map { createSubscribeMessage(it, BitstampEventType.TRADE) }
35+
.toFlux()
36+
37+
return HttpClient.create()
38+
.websocket()
39+
.uri(baseUrl)
40+
.handle { inbound, outbound ->
41+
outbound.sendString(subscribeMessages)
42+
.then()
43+
.thenMany(inbound.receive().asString())
44+
}
45+
.filter { !it.contains("bts:subscription_succeeded") }
46+
.map { BitstampJsonObjectMapper.instance.readValue<BitstampMessageFrame<BitstampTradeEvent>>(it) }
47+
}
48+
49+
fun liveOrderBook(currencyPairs: List<CurrencyPair>): Flux<BitstampMessageFrame<BitstampOrderBook>> {
50+
val subscribeMessages = currencyPairs
51+
.map { createSubscribeMessage(it, BitstampEventType.ORDER_BOOK) }
52+
.toFlux()
53+
54+
return HttpClient.create()
55+
.websocket()
56+
.uri(baseUrl)
57+
.handle { inbound, outbound ->
58+
outbound.sendString(subscribeMessages)
59+
.then()
60+
.thenMany(inbound.aggregateFrames().receive().asString())
61+
}
62+
.filter { !it.contains("bts:subscription_succeeded") }
63+
.map { BitstampJsonObjectMapper.instance.readValue<BitstampMessageFrame<BitstampOrderBook>>(it) }
64+
}
65+
66+
fun liveDetailOrderBook(currencyPairs: List<CurrencyPair>): Flux<BitstampMessageFrame<BitstampDetailOrderBook>> {
67+
val subscribeMessages = currencyPairs
68+
.map { createSubscribeMessage(it, BitstampEventType.DETAIL_ORDER_BOOK) }
69+
.toFlux()
70+
71+
return HttpClient.create()
72+
.websocket()
73+
.uri(baseUrl)
74+
.handle { inbound, outbound ->
75+
outbound.sendString(subscribeMessages)
76+
.then()
77+
.thenMany(inbound.aggregateFrames().receive().asString())
78+
}
79+
.filter { !it.contains("bts:subscription_succeeded") }
80+
.map { BitstampJsonObjectMapper.instance.readValue<BitstampMessageFrame<BitstampDetailOrderBook>>(it) }
81+
}
82+
83+
private fun createSubscribeMessage(currencyPair: CurrencyPair, event: BitstampEventType): String {
84+
val channel =
85+
"${event.subscribeMessage}_${currencyPair.targetCurrency}${currencyPair.baseCurrency}".toLowerCase()
86+
return "{" +
87+
"\"event\": \"bts:subscribe\"," +
88+
"\"data\": {" +
89+
"\"channel\": \"$channel\"" +
90+
"}" +
91+
"}"
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2019 namjug-kim
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.njkim.reactivecrypto.bitstamp
18+
19+
import com.njkim.reactivecrypto.core.common.model.ExchangeVendor
20+
import com.njkim.reactivecrypto.core.common.model.currency.CurrencyPair
21+
import com.njkim.reactivecrypto.core.common.model.order.OrderBook
22+
import com.njkim.reactivecrypto.core.common.model.order.OrderBookUnit
23+
import com.njkim.reactivecrypto.core.common.model.order.OrderSideType
24+
import com.njkim.reactivecrypto.core.common.model.order.TickData
25+
import com.njkim.reactivecrypto.core.common.util.toEpochMilli
26+
import com.njkim.reactivecrypto.core.websocket.ExchangeWebsocketClient
27+
import reactor.core.publisher.Flux
28+
29+
/**
30+
* Bitstamp Websocket API v2
31+
* document : https://www.bitstamp.net/websocket/v2/
32+
*/
33+
class BitstampWebsocketClient : ExchangeWebsocketClient {
34+
private val bitstampRawWebsocketClient: BitstampRawWebsocketClient = BitstampRawWebsocketClient()
35+
36+
override fun createTradeWebsocket(subscribeTargets: List<CurrencyPair>): Flux<TickData> {
37+
return bitstampRawWebsocketClient
38+
.liveTicker(subscribeTargets)
39+
.map { bitstampMessageFrame ->
40+
TickData(
41+
"${bitstampMessageFrame.data.id}",
42+
bitstampMessageFrame.data.microTimestamp,
43+
bitstampMessageFrame.data.price,
44+
bitstampMessageFrame.data.amount,
45+
bitstampMessageFrame.currencyPair,
46+
ExchangeVendor.BITSTAMP,
47+
bitstampMessageFrame.data.type
48+
)
49+
}
50+
}
51+
52+
override fun createDepthSnapshot(subscribeTargets: List<CurrencyPair>): Flux<OrderBook> {
53+
return bitstampRawWebsocketClient
54+
.liveOrderBook(subscribeTargets)
55+
.map { bitstampMessageFrame ->
56+
val microTimestamp = bitstampMessageFrame.data.microTimestamp
57+
58+
OrderBook(
59+
"${microTimestamp.toEpochMilli()}",
60+
bitstampMessageFrame.currencyPair,
61+
microTimestamp,
62+
ExchangeVendor.BITSTAMP,
63+
bitstampMessageFrame.data.bids.map { OrderBookUnit(it.price, it.quantity, OrderSideType.BID) },
64+
bitstampMessageFrame.data.asks.map { OrderBookUnit(it.price, it.quantity, OrderSideType.ASK) }
65+
)
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2019 namjug-kim
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.njkim.reactivecrypto.bitstamp.model
18+
19+
import com.fasterxml.jackson.annotation.JsonProperty
20+
import java.time.ZonedDateTime
21+
22+
data class BitstampDetailOrderBook(
23+
@JsonProperty("microtimestamp")
24+
val microTimestamp: ZonedDateTime,
25+
@JsonProperty("timestamp")
26+
val timestamp: ZonedDateTime,
27+
@JsonProperty("bids")
28+
val bids: List<BitstampDetailOrderBookUnit>,
29+
@JsonProperty("asks")
30+
val asks: List<BitstampDetailOrderBookUnit>
31+
)

0 commit comments

Comments
 (0)