Skip to content

Commit dac13f7

Browse files
committed
feat: add exchange UnicornX
1 parent e4eb740 commit dac13f7

File tree

9 files changed

+414
-0
lines changed

9 files changed

+414
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Support public market feature (tickData, orderBook)
3030
| ![huobi_japan](https://user-images.githubusercontent.com/16334718/59976909-caa00b80-9605-11e9-9c5f-8b11aea70944.jpg) | Huobi Japan | HUOBI_JAPAN | * | [ws](https://api-doc.huobi.co.jp/#websocket) |
3131
| ![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) |
3232
| ![okex_korea](https://user-images.githubusercontent.com/16334718/57195022-90f93f80-6f88-11e9-8aaa-f6a515d300ae.jpg) | Okex Korea | OKEX_KOREA | v3 | [ws](https://www.okex.com/docs/en/#spot_ws-all) |
33+
| ![unicornx](https://user-images.githubusercontent.com/16334718/104712645-20974f80-5766-11eb-8791-707bdb8a3783.png) | UnicornX | UNICORNX | v3 | [ws](https://www.okex.com/docs/en/#spot_ws-all) |
3334
| ![hubi](https://user-images.githubusercontent.com/16334718/57194945-e4b75900-6f87-11e9-8fea-889fc93a7ba4.jpg) | Hubi | HUBI | * | [ws](https://www.hubi.com/docs/index-en.pdf) |
3435
| ![bitmex](https://user-images.githubusercontent.com/16334718/57194950-e54fef80-6f87-11e9-8b54-3f2192012306.jpg) | Bitmex | BITMEX | * | [ws](https://www.bitmex.com/app/wsAPI) |
3536
| ![kraken](https://user-images.githubusercontent.com/16334718/57220400-2dc5e680-7036-11e9-803c-18b14e82921a.jpg) | Kraken | KRAKEN | 0.1.1 | [ws](https://www.kraken.com/features/websocket-api) |

reactive-crypto-core/src/main/kotlin/com/njkim/reactivecrypto/core/common/model/ExchangeVendor.kt

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ data class ExchangeVendor(val name: String) {
6363
val BHEX = getInstance("BHEX")
6464
@JvmField
6565
val BITZ = getInstance("BITZ")
66+
@JvmField
67+
val UNICORNX = getInstance("UNICORNX")
6668

6769
@JvmStatic
6870
fun getInstance(value: String): ExchangeVendor {

reactive-crypto-unicornx/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-okex')
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,36 @@
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.unicornx
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.TickData
23+
import com.njkim.reactivecrypto.okex.OkexWebsocketClient
24+
import reactor.core.publisher.Flux
25+
26+
class UnicornxWebsocketClient : OkexWebsocketClient("wss://real.okex.com:8443/ws/v3?brokerId=185") {
27+
override fun createDepthSnapshot(subscribeTargets: List<CurrencyPair>): Flux<OrderBook> {
28+
return super.createDepthSnapshot(subscribeTargets)
29+
.map { it.copy(exchangeVendor = ExchangeVendor.UNICORNX) }
30+
}
31+
32+
override fun createTradeWebsocket(subscribeTargets: List<CurrencyPair>): Flux<TickData> {
33+
return super.createTradeWebsocket(subscribeTargets)
34+
.map { it.copy(exchangeVendor = ExchangeVendor.UNICORNX) }
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.unicornx;
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.TickData;
23+
import org.junit.Test;
24+
import reactor.core.publisher.Flux;
25+
import reactor.test.StepVerifier;
26+
27+
import java.math.BigDecimal;
28+
import java.util.Collections;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
public class UnicornxWebsocketClientJavaTest {
33+
@Test
34+
public void unicornx_tick_data_subscribe() {
35+
// given
36+
CurrencyPair targetCurrencyPair = CurrencyPair.parse("BTC", "USDT");
37+
Flux<TickData> tickDataFlux = new UnicornxWebsocketClient()
38+
.createTradeWebsocket(Collections.singletonList(targetCurrencyPair));
39+
40+
// when
41+
StepVerifier.create(tickDataFlux.limitRequest(2))
42+
// then
43+
.assertNext(tickData -> {
44+
assertThat(tickData).isNotNull();
45+
assertThat(tickData.getCurrencyPair())
46+
.isEqualTo(targetCurrencyPair);
47+
assertThat(tickData.getExchangeVendor())
48+
.isEqualTo(ExchangeVendor.UNICORNX);
49+
assertThat(tickData.getPrice())
50+
.isGreaterThan(BigDecimal.ZERO);
51+
assertThat(tickData.getQuantity())
52+
.isGreaterThan(BigDecimal.ZERO);
53+
})
54+
.assertNext(tickData -> {
55+
assertThat(tickData).isNotNull();
56+
assertThat(tickData.getCurrencyPair())
57+
.isEqualTo(targetCurrencyPair);
58+
assertThat(tickData.getExchangeVendor())
59+
.isEqualTo(ExchangeVendor.UNICORNX);
60+
assertThat(tickData.getPrice())
61+
.isGreaterThan(BigDecimal.ZERO);
62+
assertThat(tickData.getQuantity())
63+
.isGreaterThan(BigDecimal.ZERO);
64+
})
65+
.verifyComplete();
66+
}
67+
68+
@Test
69+
public void unicornx_orderBook_subscribe() {
70+
// given
71+
CurrencyPair targetCurrencyPair = CurrencyPair.parse("BTC", "USDT");
72+
Flux<OrderBook> orderBookFlux = new UnicornxWebsocketClient()
73+
.createDepthSnapshot(Collections.singletonList(targetCurrencyPair));
74+
75+
// when
76+
StepVerifier.create(orderBookFlux.limitRequest(2))
77+
// then
78+
.assertNext(orderBook -> {
79+
assertThat(orderBook).isNotNull();
80+
assertThat(orderBook.getCurrencyPair())
81+
.isEqualTo(targetCurrencyPair);
82+
assertThat(orderBook.getExchangeVendor())
83+
.isEqualTo(ExchangeVendor.UNICORNX);
84+
assertThat(orderBook.getAsks())
85+
.isNotEmpty();
86+
assertThat(orderBook.getBids())
87+
.isNotEmpty();
88+
89+
assertThat(orderBook.getAsks().get(0).getQuantity())
90+
.isGreaterThan(BigDecimal.ZERO);
91+
assertThat(orderBook.getBids().get(0).getQuantity())
92+
.isGreaterThan(BigDecimal.ZERO);
93+
94+
assertThat(orderBook.getAsks().get(0).getPrice())
95+
.withFailMessage("ask price must be bigger than bid price")
96+
.isGreaterThan(orderBook.getBids().get(0).getPrice());
97+
98+
assertThat(orderBook.getAsks().get(0).getPrice())
99+
.withFailMessage("asks must be sorted by price asc")
100+
.isLessThan(orderBook.getAsks().get(1).getPrice());
101+
assertThat(orderBook.getBids().get(0).getPrice())
102+
.withFailMessage("bids must be sorted by price desc")
103+
.isGreaterThan(orderBook.getBids().get(1).getPrice());
104+
})
105+
.assertNext(orderBook -> {
106+
assertThat(orderBook).isNotNull();
107+
assertThat(orderBook.getCurrencyPair())
108+
.isEqualTo(targetCurrencyPair);
109+
assertThat(orderBook.getExchangeVendor())
110+
.isEqualTo(ExchangeVendor.UNICORNX);
111+
assertThat(orderBook.getAsks())
112+
.isNotEmpty();
113+
assertThat(orderBook.getBids())
114+
.isNotEmpty();
115+
116+
assertThat(orderBook.getAsks().get(0).getQuantity())
117+
.isGreaterThan(BigDecimal.ZERO);
118+
assertThat(orderBook.getBids().get(0).getQuantity())
119+
.isGreaterThan(BigDecimal.ZERO);
120+
121+
assertThat(orderBook.getAsks().get(0).getPrice())
122+
.withFailMessage("ask price must be bigger than bid price")
123+
.isGreaterThan(orderBook.getBids().get(0).getPrice());
124+
125+
assertThat(orderBook.getAsks().get(0).getPrice())
126+
.withFailMessage("asks must be sorted by price asc")
127+
.isLessThan(orderBook.getAsks().get(1).getPrice());
128+
assertThat(orderBook.getBids().get(0).getPrice())
129+
.withFailMessage("bids must be sorted by price desc")
130+
.isGreaterThan(orderBook.getBids().get(1).getPrice());
131+
})
132+
.verifyComplete();
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.unicornx
18+
19+
import com.njkim.reactivecrypto.core.ExchangeClientFactory
20+
import com.njkim.reactivecrypto.core.common.model.ExchangeVendor
21+
import com.njkim.reactivecrypto.core.websocket.ExchangeWebsocketClient
22+
import org.assertj.core.api.Assertions.assertThat
23+
import org.junit.Test
24+
25+
class ExchangeClientFactoryTest {
26+
@Test
27+
fun `create unicornx websocket client`() {
28+
val exchangeWebsocketClient = ExchangeClientFactory.websocket(ExchangeVendor.UNICORNX)
29+
30+
assertThat(exchangeWebsocketClient).isNotNull
31+
assertThat(exchangeWebsocketClient).isInstanceOf(ExchangeWebsocketClient::class.java)
32+
assertThat(exchangeWebsocketClient).isExactlyInstanceOf(UnicornxWebsocketClient::class.java)
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.unicornx
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.util.toEpochMilli
22+
import mu.KotlinLogging
23+
import org.assertj.core.api.Assertions
24+
import org.junit.Test
25+
import reactor.test.StepVerifier
26+
import java.math.BigDecimal
27+
28+
class UnicornxWebsocketClientTest {
29+
private val log = KotlinLogging.logger {}
30+
31+
@Test
32+
fun `unicornx tick data subscribe`() {
33+
// given
34+
val targetCurrencyPair = CurrencyPair.parse("BTC", "USDT")
35+
val tickDataFlux = UnicornxWebsocketClient()
36+
.createTradeWebsocket(listOf(targetCurrencyPair))
37+
38+
// when
39+
StepVerifier.create(tickDataFlux.limitRequest(2))
40+
// then
41+
.assertNext {
42+
Assertions.assertThat(it).isNotNull
43+
Assertions.assertThat(it.currencyPair)
44+
.isEqualTo(targetCurrencyPair)
45+
Assertions.assertThat(it.exchangeVendor)
46+
.isEqualTo(ExchangeVendor.UNICORNX)
47+
Assertions.assertThat(it.price)
48+
.isGreaterThan(BigDecimal.ZERO)
49+
Assertions.assertThat(it.quantity)
50+
.isGreaterThan(BigDecimal.ZERO)
51+
}
52+
.assertNext {
53+
Assertions.assertThat(it).isNotNull
54+
Assertions.assertThat(it.currencyPair)
55+
.isEqualTo(targetCurrencyPair)
56+
Assertions.assertThat(it.exchangeVendor)
57+
.isEqualTo(ExchangeVendor.UNICORNX)
58+
Assertions.assertThat(it.price)
59+
.isGreaterThan(BigDecimal.ZERO)
60+
Assertions.assertThat(it.quantity)
61+
.isGreaterThan(BigDecimal.ZERO)
62+
}
63+
.verifyComplete()
64+
}
65+
66+
@Test
67+
fun `unicornx orderBook subscribe`() {
68+
// given
69+
val targetCurrencyPair = CurrencyPair.parse("BTC", "USDT")
70+
val orderBookFlux = UnicornxWebsocketClient()
71+
.createDepthSnapshot(listOf(targetCurrencyPair))
72+
var prevTimestamp = 0L
73+
74+
// when
75+
StepVerifier.create(orderBookFlux.limitRequest(5))
76+
.expectNextCount(3)
77+
// then
78+
.assertNext {
79+
prevTimestamp = it.eventTime.toEpochMilli()
80+
Assertions.assertThat(it).isNotNull
81+
Assertions.assertThat(it.currencyPair)
82+
.isEqualTo(targetCurrencyPair)
83+
Assertions.assertThat(it.exchangeVendor)
84+
.isEqualTo(ExchangeVendor.UNICORNX)
85+
Assertions.assertThat(it.asks)
86+
.isNotEmpty
87+
Assertions.assertThat(it.bids)
88+
.isNotEmpty
89+
90+
Assertions.assertThat(it.asks[0].quantity)
91+
.isGreaterThan(BigDecimal.ZERO)
92+
Assertions.assertThat(it.bids[0].quantity)
93+
.isGreaterThan(BigDecimal.ZERO)
94+
95+
Assertions.assertThat(it.asks[0].price)
96+
.withFailMessage("ask price must be bigger than bid price")
97+
.isGreaterThan(it.bids[0].price)
98+
99+
Assertions.assertThat(it.asks[0].price)
100+
.withFailMessage("asks must be sorted by price asc")
101+
.isLessThan(it.asks[1].price)
102+
Assertions.assertThat(it.bids[0].price)
103+
.withFailMessage("bids must be sorted by price desc")
104+
.isGreaterThan(it.bids[1].price)
105+
}
106+
.assertNext {
107+
Assertions.assertThat(prevTimestamp)
108+
.isNotEqualTo(it.eventTime.toEpochMilli())
109+
Assertions.assertThat(it).isNotNull
110+
Assertions.assertThat(it.currencyPair)
111+
.isEqualTo(targetCurrencyPair)
112+
Assertions.assertThat(it.exchangeVendor)
113+
.isEqualTo(ExchangeVendor.UNICORNX)
114+
Assertions.assertThat(it.asks)
115+
.isNotEmpty
116+
Assertions.assertThat(it.bids)
117+
.isNotEmpty
118+
119+
Assertions.assertThat(it.asks[0].quantity)
120+
.isGreaterThan(BigDecimal.ZERO)
121+
Assertions.assertThat(it.bids[0].quantity)
122+
.isGreaterThan(BigDecimal.ZERO)
123+
124+
Assertions.assertThat(it.asks[0].price)
125+
.withFailMessage("ask price must be bigger than bid price")
126+
.isGreaterThan(it.bids[0].price)
127+
128+
Assertions.assertThat(it.asks[0].price)
129+
.withFailMessage("asks must be sorted by price asc")
130+
.isLessThan(it.asks[1].price)
131+
Assertions.assertThat(it.bids[0].price)
132+
.withFailMessage("bids must be sorted by price desc")
133+
.isGreaterThan(it.bids[1].price)
134+
}
135+
.verifyComplete()
136+
}
137+
}

0 commit comments

Comments
 (0)