From 52abe3700b3277bd7085b9b2da1bfbf9dd7c918a Mon Sep 17 00:00:00 2001 From: Taylor Bockman Date: Tue, 8 May 2018 19:51:56 -0700 Subject: [PATCH] Initial GET Work (#1) --- .circleci/config.yml | 42 +++ .github/ISSUE_TEMPLATE.md | 32 ++ .github/PULL_REQUEST_TEMPLATE.md | 27 ++ CONTRIBUTING.md | 86 +++++ LICENSE | 21 ++ README.md | 130 ++++++- build.gradle | 11 + src/main/java/com/sigmaflare/Binance.java | 17 - .../com/sigmaflare/binancej/BaseBinanceApi.java | 38 ++ .../java/com/sigmaflare/binancej/Constant.java | 11 + .../com/sigmaflare/binancej/GeneralUtilities.java | 148 ++++++++ src/main/java/com/sigmaflare/binancej/Helpers.java | 87 +++++ .../java/com/sigmaflare/binancej/MarketData.java | 382 +++++++++++++++++++++ .../sigmaflare/binancej/entities/Candlestick.java | 43 +++ .../binancej/entities/ExchangeFilter.java | 7 + .../sigmaflare/binancej/entities/ExchangeInfo.java | 37 ++ .../com/sigmaflare/binancej/entities/Interval.java | 34 ++ .../binancej/entities/LotSizeFilter.java | 30 ++ .../binancej/entities/MinNotionalFilter.java | 22 ++ .../binancej/entities/OrderBookDepth.java | 29 ++ .../binancej/entities/OrderBookPricing.java | 18 + .../sigmaflare/binancej/entities/OrderType.java | 11 + .../com/sigmaflare/binancej/entities/Ping.java | 7 + .../sigmaflare/binancej/entities/PriceFilter.java | 30 ++ .../sigmaflare/binancej/entities/RateLimit.java | 23 ++ .../binancej/entities/RateLimitInterval.java | 7 + .../binancej/entities/RateLimitType.java | 6 + .../sigmaflare/binancej/entities/ServiceError.java | 21 ++ .../com/sigmaflare/binancej/entities/Symbol.java | 51 +++ .../sigmaflare/binancej/entities/SymbolFilter.java | 22 ++ .../sigmaflare/binancej/entities/SymbolStatus.java | 11 + .../sigmaflare/binancej/entities/TickerPrice.java | 24 ++ .../com/sigmaflare/binancej/entities/Time.java | 16 + .../transform/CandlestickDeserializer.java | 46 +++ .../OrderBookDepthResponseDeserializer.java | 66 ++++ .../exceptions/BinanceServiceException.java | 12 + .../BinanceServiceUnreachableException.java | 11 + src/test/java/LibraryTest.java | 12 - .../binancej/CandlestickMethodTests.java | 289 ++++++++++++++++ .../binancej/ExchangeInfoMethodTests.java | 120 +++++++ .../sigmaflare/binancej/OrderBookDepthTests.java | 124 +++++++ .../com/sigmaflare/binancej/PingMethodTests.java | 105 ++++++ .../com/sigmaflare/binancej/TickerPriceTests.java | 207 +++++++++++ .../com/sigmaflare/binancej/TimeMethodTests.java | 108 ++++++ .../sigmaflare/binancej/matchers/GetMatcher.java | 29 ++ .../sigmaflare/binancej/matchers/PostMatcher.java | 37 ++ .../resources/candlestick_response_sample.json | 16 + .../resources/exchange_info_response_sample.json | 45 +++ .../resources/multiple_ticker_prices_response.json | 10 + src/test/resources/orderbook_depth_sample.json | 17 + .../resources/single_ticker_price_response.json | 4 + 51 files changed, 2697 insertions(+), 42 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE delete mode 100644 src/main/java/com/sigmaflare/Binance.java create mode 100644 src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java create mode 100644 src/main/java/com/sigmaflare/binancej/Constant.java create mode 100644 src/main/java/com/sigmaflare/binancej/GeneralUtilities.java create mode 100644 src/main/java/com/sigmaflare/binancej/Helpers.java create mode 100644 src/main/java/com/sigmaflare/binancej/MarketData.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/Candlestick.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/ExchangeFilter.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/ExchangeInfo.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/Interval.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/LotSizeFilter.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/MinNotionalFilter.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/OrderBookDepth.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/OrderBookPricing.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/OrderType.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/Ping.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/PriceFilter.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/RateLimit.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/RateLimitInterval.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/RateLimitType.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/ServiceError.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/Symbol.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/SymbolFilter.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/SymbolStatus.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/TickerPrice.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/Time.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/transform/CandlestickDeserializer.java create mode 100644 src/main/java/com/sigmaflare/binancej/entities/transform/OrderBookDepthResponseDeserializer.java create mode 100644 src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceException.java create mode 100644 src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java delete mode 100644 src/test/java/LibraryTest.java create mode 100644 src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/PingMethodTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/TickerPriceTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/TimeMethodTests.java create mode 100644 src/test/java/com/sigmaflare/binancej/matchers/GetMatcher.java create mode 100644 src/test/java/com/sigmaflare/binancej/matchers/PostMatcher.java create mode 100644 src/test/resources/candlestick_response_sample.json create mode 100644 src/test/resources/exchange_info_response_sample.json create mode 100644 src/test/resources/multiple_ticker_prices_response.json create mode 100644 src/test/resources/orderbook_depth_sample.json create mode 100644 src/test/resources/single_ticker_price_response.json diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..a59e106 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,42 @@ +# Java Gradle CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-java/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/openjdk:8-jdk + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/repo + + environment: + # Customize the JVM maximum heap limit + JVM_OPTS: -Xmx3200m + TERM: dumb + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: gradle dependencies + + - save_cache: + paths: + - ~/.gradle + key: v1-dependencies-{{ checksum "build.gradle" }} + + # run tests! + - run: gradle test diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..bcd3afa --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,32 @@ +Your issue may already be reported! +Please search on the [issue track](../) before creating one. + +## Expected Behavior + + + +## Current Behavior + + + +## Possible Solution + + + +## Steps to Reproduce (for bugs) + + +1. +2. +3. +4. + +## Context + + + +## Your Environment + +* Version used: +* Operating System and version (desktop or mobile): +* Link to your project: \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d3c302b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Description + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have updated the version of the library as required per the specifications of [Semantic Versioning](https://semver.org/) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..293097b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing to the BinanceJ Project + +### Welcome to BinanceJ! + +The goal of this project is to provide the de-facto standard Binance API in Java. + +--- +#### Sections + +> * [Getting Started](#getting-started) +> * [Testing](#testing) +> * [Code Standards](#code-standards) +> * [Versioning](#versioning) +> * [Submitting Pull Requests](#submitting-pull-requests) +> * [Submitting Issues](#submitting-issues) + +--- + +## Getting Started + +### Overall Requirements + +* Java 8 +* Gradle +* [Sonarlint](https://www.sonarlint.org/) + +It is highly suggested you use something like Intellij IDEA when developing this project though +there is no strict enforcement on that. + +To get started with the project, do as follows: + +```sh +git clone git@github.com:angrygoats/binancej.git +cd binancej +gradle clean && gradle build +``` + +## Testing + +We operate a strict TDD shop here. As a result, any PR submitted without tests for anything but _the most trivial_ of +features will be rejected immediately. 100% code coverage is not necessary (but desirable where possible). You should +aim to cover all major conditions your code can go through thoroughly. + +The tools we primarily use are Junit4 and Mockito. Since the API is so simple this all we really need. + +To run the tests, simple run: + +```sh +gradle test +``` + +from the root of the project. + + +## Code Standards + +Clean Java is good Java. As all of us know Java can get unnecessarily verbose at times, and in order to keep readability +in view at all times good code standards should be followed: + +1. Run Sonarlint and follow it's suggestions +2. 4 spaces (not tabs) +3. Keep a space between each member of a class for readability +4. Javadoc every function you write with the exception of already documented functions you are `@Override`'ing +5. Javadoc classes that are not obvious from their name +6. __Absolutely no wild card importing__ +7. Clean up your imports - do not leave unused imports in any code you write +8. You should make an effort to clean up all code you touch if the clean up will not take too long + * Any thing that will take long should be made into an issue +9. Make use of Lombok wherever you can to reduce boilerplate as much as possible +10. Take advantage of Java 8 features such as streams to simplify your code where possible +11. If you are using IntelliJ, take advantage of code formatting under `Code -> Reformat Code` + +## Versioning + +The BinanceJ project uses [Semantic Versioning](https://semver.org/). In the `build.gradle` you will find a line +to adjust the semantic versioning of the library. + + ## Submitting Pull Requests + + Please be sure to follow the supplied pull request template. PRs will be rejected if they lack tests or the test + coverage isn't sufficient. + + ## Submitting Issues + + Please be sure to follow the supplied issue template. If the template does not contain fields you need, feel free + to add them if they provide more context to the issue you are experiencing. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..837be67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Taylor Bockman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 8c6f464..5223345 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,22 @@ #BinanceJ -A Java 8 implementation of the [Binance API Specification](https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md). - +[![CircleCI](https://circleci.com/gh/angrygoats/binancej/tree/master.svg?style=svg&circle-token=ec4614038357b8ff4bcc4773bedd4a264a1947b7)](https://circleci.com/gh/angrygoats/binancej/tree/master) -## Rate Limiting +A Java 8 implementation of the [Binance API Specification](https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md). -BinanceJ does not perform rate limiting, so you are responsible for limiting your requests. +## Licensing -## TODO +BinanceJ is released under the MIT license. -* Only implement the GET methods for now -* Test everything with mocked http clients and such -* Use closable clients, etc +## Rate Limiting +BinanceJ does not perform rate limiting, so you are responsible for limiting your requests. Pay attention to the errors +you receive from Binance and check to see if they are a 429. If they are, you need to back off or face a temporary +ban. -## TO IMPLEMENT ON THIS RUN: +## API Coverage -1. Error code detection to make things easy (think like you did before) -2. All enums for each endpoint -3. Errors and Responses inheirit from the same base object and errors are returned conditionally with an EITHER type structure +The following endpoints are currently covered: 1. `GET /api/v1/ping` 2. `GET /api/v1/time` @@ -27,4 +25,110 @@ BinanceJ does not perform rate limiting, so you are responsible for limiting you 5. `GET /api/v1/klines` 6. `GET /api/v3/ticker/price` -for (5) you will need to implement all enums (such as candlestick hours, etc) as well as compose the various fields of the returned array into an object. +More will be added in future PRs as they become necessary to me or the people using the library. + +## Return Types + +All functions return an `Either` type. For those of you not in the know - an Either type represents a disjoint union. +Typically this means a "success" and "failure" case that must be handled uniquely. They work well with stream +processing and are a very natural way to delineate success and failure cleanly. Additionally it eliminates the pattern +of throwing exceptions on errors, and allows exceptions to be reserved for truly exceptional behavior as intended. + +By convention I adapted the Haskell Either type style to this code. What this means is that the Either type's Right +value is the correct one (mnemonic: "right" as in correct) and the Left value is the serviceError. + +### How do I extract the raw value from the Either type? + +There are a few ways to do this using Ambivalent, but the most prevalent way in BinanceJ's tests is: + +```java +Either val = clazz.getThing(); + +// Good path case +if(val.isRight()) { + TypeB thing = val.right().join(Function.identity(), Function.identity()); +} + +``` + +Alternatively you are welcome to use `Helpers.extractEitherValueSafely` method to make your code cleaner. + + +## Examples + +### Server alive check with ping + +```java +GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); +Either res = generalUtilities.ping(); +if(res.isRight()) { + // Successful ping +} + +``` + +### Getting current server time + +```java +GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); +Either res = generalUtilities.getServerTime(); + +if(res.isRight() { + ServerTime response = Helpers.extractEitherValueSafely(res.right()); + //... +} +``` + +### Getting Exchange Information + +```java +GeneralUtilities generalUtilities = GeneralUtilities.builder().apiKey("KEY").secretKey("KEY").build(); +Either res = generalUtilities.getExchangeInfo(); + +if(res.isRight() { + ExchangeInfo response = Helpers.extractEitherValueSafely(res.right()); + //... +} +``` + +### Getting Candlestick data + +```java +MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); +Either> res = marketData.getCandlestickData("ETHBTC", Interval.ONE_MINUTE); + +if(res.isRight() { + List data = Helpers.extractEitherValueSafely(res.right()); + //... +} +``` + +### Getting market depth + +```java +MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); +Either res = marketData.getOrderBookDepth("ETHBTC", 1000); + +if(res.isRight() { + OrderBookDepth orderBookDepth = Helpers.extractEitherValueSafely(res.right()); + //... +} +``` + +### Getting ticker price for an instrument + +```java +MarketData marketData = MarketData.builder().apiKey("KEY").secretKey("KEY").build(); +Either res = marketData.getTickerPriceForSymbol("ETHBTC"); + +if(res.isRight() { + TickerPrice tickerPrice = Helpers.extractEitherValueSafely(res.right()); + //... +} +``` + +## Contributing + +Head over to our [CONTRIBUTING.md](CONTRIBUTING.md) to get started. All features are welcome as long as they are +in scope of the API and following the contributing guide. + diff --git a/build.gradle b/build.gradle index 84431df..ad33797 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,29 @@ plugins { // Apply the java-library plugin to add support for Java Library id 'java-library' + id 'com.github.ethankhall.semantic-versioning' version "1.1.0" apply true } +project.version.with { major = 1; minor= 0; patch = 0 } + sourceCompatibility = 1.8 dependencies { compile group: 'org.projectlombok', name: 'lombok', version: '1.16.20' + compile group: 'com.codepoetics', name: 'ambivalence', version: '0.2' compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.5' compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.5' + + compile group: 'commons-io', name: 'commons-io', version: '2.6' compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.5' + compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' + compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.11.0' + + testCompile 'junit:junit:4.12' + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.18.3' } repositories { diff --git a/src/main/java/com/sigmaflare/Binance.java b/src/main/java/com/sigmaflare/Binance.java deleted file mode 100644 index d92c92d..0000000 --- a/src/main/java/com/sigmaflare/Binance.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sigmaflare; - - -public class Binance { - private final String apiKey; - private final String secretKey; - private static final String BASE_ENDPOINT = "https://api.binance.com"; - - public Binance(String apiKey, String secretKey) { - this.apiKey = apiKey; - this.secretKey = secretKey; - } - - public boolean someLibraryMethod() { - return true; - } -} diff --git a/src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java b/src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java new file mode 100644 index 0000000..e0cd54c --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/BaseBinanceApi.java @@ -0,0 +1,38 @@ +package com.sigmaflare.binancej; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +import java.io.IOException; + +public abstract class BaseBinanceApi { + protected final String apiKey; + protected final String secretKey; + protected final CloseableHttpClient closeableHttpClient; + + protected static final ObjectMapper mapper = Helpers.objectMapperBuilder(); + + public BaseBinanceApi(String apiKey, String secretKey) { + this.apiKey = apiKey; + this.secretKey = secretKey; + this.closeableHttpClient = HttpClientBuilder.create().build(); + } + + public BaseBinanceApi(String apiKey, String secretKey, CloseableHttpClient closeableHttpClient) { + this.apiKey = apiKey; + this.secretKey = secretKey; + this.closeableHttpClient = closeableHttpClient; + } + + + public void close() throws IOException { + closeableHttpClient.close(); + } + + @Override + protected void finalize() throws IOException { + close(); + } +} diff --git a/src/main/java/com/sigmaflare/binancej/Constant.java b/src/main/java/com/sigmaflare/binancej/Constant.java new file mode 100644 index 0000000..03adabf --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/Constant.java @@ -0,0 +1,11 @@ +package com.sigmaflare.binancej; + +final class Constant { + private Constant() {} + + static final String BASE_ENDPOINT = "https://api.binance.com"; + + static final String NO_RESPONSE_TEXT = "No response returned from {}"; + static final String NO_RESPONSE_TEXT_FORMATTED = "No response returned from %s"; + static final String SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED = "Symbol and interval must be supplied"; +} diff --git a/src/main/java/com/sigmaflare/binancej/GeneralUtilities.java b/src/main/java/com/sigmaflare/binancej/GeneralUtilities.java new file mode 100644 index 0000000..19aeac0 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/GeneralUtilities.java @@ -0,0 +1,148 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.sigmaflare.binancej.entities.ExchangeInfo; +import com.sigmaflare.binancej.entities.Ping; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.Time; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT; +import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED; + +/** + * GeneralUtilities is a container class for methods that interact with the infrastructure + * on the Binance side. Methods like server health and exchange information are found here. + */ +@Slf4j +public class GeneralUtilities extends BaseBinanceApi { + private static final String PING_URL = "/api/v1/ping"; + private static final String TIME_URL = "/api/v1/time"; + private static final String EXCHANGE_INFO_URL = "/api/v1/exchangeInfo"; + + @Builder + public GeneralUtilities(String apiKey, String secretKey) { + super(apiKey, secretKey); + } + + GeneralUtilities(String apiKey, String secretKey, CloseableHttpClient closeableHttpClient) { + super(apiKey, secretKey, closeableHttpClient); + } + + /** + * Hits the ping endpoint to check if the service is alive. + * + * @return empty Ping object if it returned 200, otherwise ServiceError + */ + public Either ping() throws BinanceServiceUnreachableException { + final String url = String.format("%s%s", BASE_ENDPOINT, PING_URL); + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException( + String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + return Either.ofRight(new Ping()); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + /** + * Gets the current server time on Binance's servers + * + * @return A Time object if successful, otherwise an ServiceError object + * @throws BinanceServiceUnreachableException Throws when the request fails + */ + public Either getServerTime() throws BinanceServiceUnreachableException { + final String url = String.format("%s%s", BASE_ENDPOINT, TIME_URL); + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException( + String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + return Either.ofRight(mapper.readValue(response, Time.class)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + /** + * Retrieves exchange information + * + * @return An ExchangeInfo when successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service cannot be reached + */ + public Either getExchangeInfo() throws BinanceServiceUnreachableException { + final String url = String.format("%s%s", BASE_ENDPOINT, EXCHANGE_INFO_URL); + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException( + String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + return Either.ofRight(mapper.readValue(response, ExchangeInfo.class)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + +} diff --git a/src/main/java/com/sigmaflare/binancej/Helpers.java b/src/main/java/com/sigmaflare/binancej/Helpers.java new file mode 100644 index 0000000..f27b058 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/Helpers.java @@ -0,0 +1,87 @@ +package com.sigmaflare.binancej; + + +import com.codepoetics.ambivalence.LeftProjection; +import com.codepoetics.ambivalence.RightProjection; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; + +import java.util.function.Function; + +public final class Helpers { + private Helpers() { + } + + /** + * Checks to insure the status code is ok. + * + * @param statusCode The status code + * @return True of the status code is acceptable (200-399), and false otherwise + */ + public static boolean statusCodeIsOk(int statusCode) { + return statusCode >= 200 && statusCode < 400; + } + + + /** + * Builds a configured HttpGet + * + * @param url The full URL to GET to + * @param apiKey The API key + * @return A configured HttpGet object + */ + public static HttpGet getBuilder(String url, String apiKey) { + HttpGet httpGet = new HttpGet(url); + httpGet.setHeader("X-MBX-APIKEY", apiKey); + return httpGet; + } + + /** + * Builds a configured HttpPost + * + * @param url The full URL to POST to + * @param apiKey The API key + * @return A configured HttpPost object + */ + public static HttpPost postBuilder(String url, String apiKey) { + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("X-MBX-APIKEY", apiKey); + return httpPost; + } + + /** + * Builds a fully configured ObjectMapper + * + * @return A fully configured ObjectMapper + */ + public static ObjectMapper objectMapperBuilder() { + return new ObjectMapper().findAndRegisterModules(); + } + + /** + * Safely extracts the value from a LeftProjection of an Either + * + * @param val The LeftProjection to perform the extraction on + * @param The Left type + * @param The Right type + * @return The unwrapped L type object + */ + @SuppressWarnings("unchecked") + public static L extractEitherValueSafely(LeftProjection val) { + return (L) val.join(Function.identity(), Function.identity()); + } + + /** + * Safely extracts the value from a RightProjection of an Either + * + * @param val The RightProjection to perform the extraction on + * @param The Left type + * @param The Right type + * @return The unwrapped R type object + */ + @SuppressWarnings("unchecked") + public static R extractEitherValueSafely(RightProjection val) { + return (R) val.join(Function.identity(), Function.identity()); + } +} diff --git a/src/main/java/com/sigmaflare/binancej/MarketData.java b/src/main/java/com/sigmaflare/binancej/MarketData.java new file mode 100644 index 0000000..4b67fd6 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/MarketData.java @@ -0,0 +1,382 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.JavaType; +import com.sigmaflare.binancej.entities.Candlestick; +import com.sigmaflare.binancej.entities.Interval; +import com.sigmaflare.binancej.entities.OrderBookDepth; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.TickerPrice; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.util.List; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT; +import static com.sigmaflare.binancej.Constant.NO_RESPONSE_TEXT_FORMATTED; +import static com.sigmaflare.binancej.Constant.SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED; + +@Slf4j +public class MarketData extends BaseBinanceApi { + private static final String ORDER_BOOK_URL = "/api/v1/depth"; + private static final String CANDLESTICK_URL = "/api/v1/klines"; + private static final String TICKER_PRICE_URL = "/api/v3/ticker/price"; + + @Builder + MarketData(String apiKey, String secretKey) { + super(apiKey, secretKey); + } + + MarketData(String apiKey, String secretKey, CloseableHttpClient closeableHttpClient) { + super(apiKey, secretKey, closeableHttpClient); + } + + /** + * Overloaded version of getOrderBookDepth that uses the default limit of 100 + * + * @param symbol The symbol + * @return A populated OrderBookDepth if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException In the case the service is unreachable + */ + public Either getOrderBookDepth(String symbol) + throws BinanceServiceUnreachableException { + return getOrderBookDepth(symbol, 100); + } + + /** + * Retrieves orderbook depth information + * + * @param symbol The symbol + * @param limit The record limit (default: 100, maximum 1000) + * @return A populated OrderBookDepth if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException In the case the service is unreachable + */ + public Either getOrderBookDepth(String symbol, int limit) + throws BinanceServiceUnreachableException { + String url = String.format("%s%s?symbol=%s&limit=%d", BASE_ENDPOINT, ORDER_BOOK_URL, symbol, limit); + + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException(String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + return Either.ofRight(mapper.readValue(response, OrderBookDepth.class)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + /** + * Retrieves candlestick data for the supplied symbol and interval + * + * @param symbol The symbol + * @param interval The interval + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied + */ + public Either> getCandleStickData(String symbol, Interval interval) + throws BinanceServiceUnreachableException { + + if (symbol == null || interval == null) { + throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); + } + + String url = String.format( + "%s%s?symbol=%s&interval=%s", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString()); + + return getCandleStickDataFromUrl(url); + } + + /** + * Retrieves candlestick data for the supplied symbol and interval + * + * @param symbol The symbol + * @param interval The interval + * @param limit The output limit + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied + */ + public Either> getCandleStickData(String symbol, Interval interval, int limit) + throws BinanceServiceUnreachableException { + + if (symbol == null || interval == null) { + throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); + } + + String url = String.format( + "%s%s?symbol=%s&interval=%s&limit=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + limit); + + return getCandleStickDataFromUrl(url); + } + + /** + * Retrieves candlestick data for the supplied symbol and interval + * + * @param symbol The symbol + * @param interval The interval + * @param time The start/end time + * @param isStartTime Indicates whether the time is a start or end time + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied + */ + public Either> + getCandleStickData(String symbol, Interval interval, long time, boolean isStartTime) + throws BinanceServiceUnreachableException { + + if (symbol == null || interval == null) { + throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); + } + + String url; + + if (isStartTime) { + url = String.format( + "%s%s?symbol=%s&interval=%s&startTime=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + time); + } else { + url = String.format( + "%s%s?symbol=%s&interval=%s&endTime=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + time); + } + + return getCandleStickDataFromUrl(url); + } + + /** + * Retrieves candlestick data for the supplied symbol and interval + * + * @param symbol The symbol + * @param interval The interval + * @param limit The output limit + * @param time The timeframe + * @param isStartTime indicates whether time is a startTime (true) or endTime (false) + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied + */ + public Either> + getCandleStickData(String symbol, Interval interval, int limit, long time, boolean isStartTime) + throws BinanceServiceUnreachableException { + + if (symbol == null || interval == null) { + throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); + } + + String url; + + if (isStartTime) { + url = String.format( + "%s%s?symbol=%s&interval=%s&limit=%d&startTime=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + limit, + time); + } else { + url = String.format( + "%s%s?symbol=%s&interval=%s&limit=%d&endTime=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + limit, + time); + } + + return getCandleStickDataFromUrl(url); + } + + /** + * Retrieves candlestick data for the supplied symbol and interval + * + * @param symbol The symbol + * @param interval The interval + * @param limit The output limit + * @param startTime The start timeframe + * @param endTime The end timeframe + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + * @throws IllegalArgumentException If the required arguments symbol and interval are not supplied + */ + public Either> + getCandleStickData(String symbol, Interval interval, int limit, long startTime, long endTime) + throws BinanceServiceUnreachableException { + + if (symbol == null || interval == null) { + throw new IllegalArgumentException(SYMBOL_AND_INTERVAL_MUST_BE_SUPPLIED); + } + + String url; + + url = String.format( + "%s%s?symbol=%s&interval=%s&limit=%d&startTime=%d&endTime=%d", + BASE_ENDPOINT, + CANDLESTICK_URL, + symbol, + interval.toString(), + limit, + startTime, + endTime); + + return getCandleStickDataFromUrl(url); + } + + /** + * Retrieves the current ticker price for the specified symbol + * + * @param symbol The symbol + * @return A TickerPrice if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + */ + public Either getTickerPriceForSymbol(String symbol) + throws BinanceServiceUnreachableException { + if (symbol == null) { + throw new IllegalArgumentException("Symbol must not be null"); + } + String url = String.format("%s%s?symbol=%s", BASE_ENDPOINT, TICKER_PRICE_URL, symbol); + + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException(String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + return Either.ofRight(mapper.readValue(response, TickerPrice.class)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + /** + * Retrieves ticker prices for all supported Binance symbols. This is good to use in terms of cost if you need + * more than one symbol's current ticker price. + * + * @return Either a list of TickerPrice objects if successful, or an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + */ + public Either> getTickerPrices() throws BinanceServiceUnreachableException { + String url = String.format("%s%s", BASE_ENDPOINT, TICKER_PRICE_URL); + + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException(String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, TickerPrice.class); + return Either.ofRight(mapper.readValue(response, type)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } + + /** + * Gets candlestick data from the provided URL, allowing all of the getCandleStickData functions to act + * as glorified URL builders. + * + * @param url The URL to use + * @return A list of candlesticks if successful, otherwise an ServiceError + * @throws BinanceServiceUnreachableException If the service is unreachable + */ + private Either> + getCandleStickDataFromUrl(String url) throws BinanceServiceUnreachableException { + final HttpGet request = Helpers.getBuilder(url, apiKey); + + try { + try (CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(request)) { + StatusLine sl = closeableHttpResponse.getStatusLine(); + + HttpEntity httpEntity = closeableHttpResponse.getEntity(); + + if (httpEntity == null) { + log.error(NO_RESPONSE_TEXT, url); + throw new BinanceServiceUnreachableException(String.format(NO_RESPONSE_TEXT_FORMATTED, url), null); + } + + String response = EntityUtils.toString(httpEntity); + + if (!Helpers.statusCodeIsOk(sl.getStatusCode())) { + return Either.ofLeft(mapper.readValue(response, ServiceError.class)); + } + + JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Candlestick.class); + return Either.ofRight(mapper.readValue(response, type)); + } + } catch (IOException e) { + throw new BinanceServiceUnreachableException(e.getMessage(), e.getCause()); + } + } +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/Candlestick.java b/src/main/java/com/sigmaflare/binancej/entities/Candlestick.java new file mode 100644 index 0000000..52c58ce --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/Candlestick.java @@ -0,0 +1,43 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sigmaflare.binancej.entities.transform.CandlestickDeserializer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * Represents a single candlestick/kline. No JsonProperty information is stored because we use a + * custom serializer to clean up the data that's given to us from the endpoint. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonDeserialize(using = CandlestickDeserializer.class) +public class Candlestick { + private long openTime; + + private BigDecimal open; + + private BigDecimal high; + + private BigDecimal low; + + private BigDecimal close; + + private BigDecimal volume; + + private long closeTime; + + private BigDecimal quoteAssetVolume; + + private long numberOfTrades; + + private BigDecimal takerBuyBaseAssetVolume; + + private BigDecimal takerBuyQuoteAssetVolume; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/ExchangeFilter.java b/src/main/java/com/sigmaflare/binancej/entities/ExchangeFilter.java new file mode 100644 index 0000000..d0900c9 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/ExchangeFilter.java @@ -0,0 +1,7 @@ +package com.sigmaflare.binancej.entities; + +/** + * The documents do not make it clear what is in this object so it is left blank. + */ +public class ExchangeFilter { +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/ExchangeInfo.java b/src/main/java/com/sigmaflare/binancej/entities/ExchangeInfo.java new file mode 100644 index 0000000..460b870 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/ExchangeInfo.java @@ -0,0 +1,37 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ExchangeInfo { + @NonNull + @JsonProperty("timezone") + private String timezone; + + @JsonProperty("serverTime") + private Long serverTime; + + @NonNull + @JsonProperty("rateLimits") + private Set rateLimits; + + @NonNull + @JsonProperty("exchangeFilters") + private Set exchangeFilters; + + @NonNull + @JsonProperty("symbols") + private List symbols; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/Interval.java b/src/main/java/com/sigmaflare/binancej/entities/Interval.java new file mode 100644 index 0000000..e8515e9 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/Interval.java @@ -0,0 +1,34 @@ +package com.sigmaflare.binancej.entities; + +public enum Interval { + ONE_MINUTE("1m"), + THREE_MINUTES("3m"), + FIVE_MINUTES("5m"), + FIFTEEN_MINUTES("15m"), + THIRTY_MINUTES("30m"), + ONE_HOUR("1h"), + TWO_HOURS("2h"), + FOUR_HOURS("4h"), + SIX_HOURS("6h"), + EIGHT_HOURS("8h"), + TWELVE_HOURS("12h"), + ONE_DAY("1d"), + THREE_DAYS("3d"), + ONE_WEEK("1w"), + ONE_MONTH("1M"); + + private final String representation; + + /** + * Constructor + * @param representation The string representation of the time interval to construct + */ + Interval(String representation) { + this.representation = representation; + } + + @Override + public String toString() { + return representation; + } +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/LotSizeFilter.java b/src/main/java/com/sigmaflare/binancej/entities/LotSizeFilter.java new file mode 100644 index 0000000..2bb2b79 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/LotSizeFilter.java @@ -0,0 +1,30 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@JsonTypeName("LOT_SIZE") +public class LotSizeFilter extends SymbolFilter { + @NonNull + @JsonProperty("minQty") + private String minQty; + + @NonNull + @JsonProperty("maxQty") + private String maxQty; + + @NonNull + @JsonProperty("stepSize") + private String stepSize; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/MinNotionalFilter.java b/src/main/java/com/sigmaflare/binancej/entities/MinNotionalFilter.java new file mode 100644 index 0000000..cb6c58e --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/MinNotionalFilter.java @@ -0,0 +1,22 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonTypeName("MIN_NOTIONAL") +@EqualsAndHashCode(callSuper = false) +public class MinNotionalFilter extends SymbolFilter { + @NonNull + @JsonProperty("minNotional") + private String minNotional; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/OrderBookDepth.java b/src/main/java/com/sigmaflare/binancej/entities/OrderBookDepth.java new file mode 100644 index 0000000..c43ea06 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/OrderBookDepth.java @@ -0,0 +1,29 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sigmaflare.binancej.entities.transform.OrderBookDepthResponseDeserializer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Represents the Order book depth for Binance's market. + * + * Order book depth is a mess coming from Binance's API, so this is handled purely with + * a custom deserializer to make using the resulting POJOs easier + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonDeserialize(using = OrderBookDepthResponseDeserializer.class) +public class OrderBookDepth { + private Long lastUpdateId; + + private List bids; + + private List asks; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/OrderBookPricing.java b/src/main/java/com/sigmaflare/binancej/entities/OrderBookPricing.java new file mode 100644 index 0000000..c13d0fc --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/OrderBookPricing.java @@ -0,0 +1,18 @@ +package com.sigmaflare.binancej.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OrderBookPricing { + private BigDecimal price; + + private BigDecimal quantity; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/OrderType.java b/src/main/java/com/sigmaflare/binancej/entities/OrderType.java new file mode 100644 index 0000000..ae031e2 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/OrderType.java @@ -0,0 +1,11 @@ +package com.sigmaflare.binancej.entities; + +public enum OrderType { + LIMIT, + MARKET, + STOP_LOSS, + STOP_LOSS_LIMIT, + TAKE_PROFIT, + TAKE_PROFIT_LIMIT, + LIMIT_MAKER +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/Ping.java b/src/main/java/com/sigmaflare/binancej/entities/Ping.java new file mode 100644 index 0000000..8e5429a --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/Ping.java @@ -0,0 +1,7 @@ +package com.sigmaflare.binancej.entities; + +/** + * Ping doesn't return anything, but we need to have this object to keep the API consistent. + */ +public class Ping { +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/PriceFilter.java b/src/main/java/com/sigmaflare/binancej/entities/PriceFilter.java new file mode 100644 index 0000000..8743c78 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/PriceFilter.java @@ -0,0 +1,30 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonTypeName("PRICE_FILTER") +@EqualsAndHashCode(callSuper = false) +public class PriceFilter extends SymbolFilter { + @NonNull + @JsonProperty("minPrice") + private String minPrice; + + @NonNull + @JsonProperty("maxPrice") + private String maxPrice; + + @NonNull + @JsonProperty("tickSize") + private String tickSize; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/RateLimit.java b/src/main/java/com/sigmaflare/binancej/entities/RateLimit.java new file mode 100644 index 0000000..116eb70 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/RateLimit.java @@ -0,0 +1,23 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RateLimit { + @NonNull + @JsonProperty("rateLimitType") + private RateLimitType rateLimitType; + + @NonNull + @JsonProperty("interval") + private RateLimitInterval rateLimitInterval; + + @JsonProperty("limit") + private Long limit; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/RateLimitInterval.java b/src/main/java/com/sigmaflare/binancej/entities/RateLimitInterval.java new file mode 100644 index 0000000..e7b8f4e --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/RateLimitInterval.java @@ -0,0 +1,7 @@ +package com.sigmaflare.binancej.entities; + +public enum RateLimitInterval { + SECOND, + MINUTE, + DAY +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/RateLimitType.java b/src/main/java/com/sigmaflare/binancej/entities/RateLimitType.java new file mode 100644 index 0000000..fe74863 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/RateLimitType.java @@ -0,0 +1,6 @@ +package com.sigmaflare.binancej.entities; + +public enum RateLimitType { + REQUESTS, + ORDERS +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/ServiceError.java b/src/main/java/com/sigmaflare/binancej/entities/ServiceError.java new file mode 100644 index 0000000..c821ced --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/ServiceError.java @@ -0,0 +1,21 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ServiceError { + @JsonProperty("code") + public int code; + + @NonNull + @JsonProperty("msg") + public String message; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/Symbol.java b/src/main/java/com/sigmaflare/binancej/entities/Symbol.java new file mode 100644 index 0000000..7a7d11d --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/Symbol.java @@ -0,0 +1,51 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Symbol { + @NonNull + @JsonProperty("symbol") + private String ticker; + + @NonNull + @JsonProperty("status") + private SymbolStatus symbolStatus; + + @NonNull + @JsonProperty("baseAsset") + private String baseAsset; + + @JsonProperty("baseAssetPrecision") + private int baseAssetPrecision; + + @NonNull + @JsonProperty("quoteAsset") + private String quoteAsset; + + @JsonProperty("quotePrecision") + private int quotePrecision; + + @NonNull + @JsonProperty("orderTypes") + private Set orderTypes; + + @JsonProperty("icebergAllowed") + private boolean icebergAllowed; + + @NonNull + @JsonProperty("filters") + private List symbolFilters; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/SymbolFilter.java b/src/main/java/com/sigmaflare/binancej/entities/SymbolFilter.java new file mode 100644 index 0000000..4e3add4 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/SymbolFilter.java @@ -0,0 +1,22 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.NonNull; + +// The "property" here refers to an identifying feature of the JSON that will be used to +// match to a @JsonTypeName +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "filterType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = PriceFilter.class, name = "PRICE_FILTER"), + @JsonSubTypes.Type(value = LotSizeFilter.class, name = "LOT_SIZE"), + @JsonSubTypes.Type(value = MinNotionalFilter.class, name = "MIN_NOTIONAL") +}) +public abstract class SymbolFilter { + // This seems like an enum, but the entire space of values is not enumerated + // so it is left as a string for now + @NonNull + @JsonProperty("filterType") + private String filterType; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/SymbolStatus.java b/src/main/java/com/sigmaflare/binancej/entities/SymbolStatus.java new file mode 100644 index 0000000..fc61167 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/SymbolStatus.java @@ -0,0 +1,11 @@ +package com.sigmaflare.binancej.entities; + +public enum SymbolStatus { + PRE_TRADING, + TRADING, + POST_TRADING, + END_OF_DAY, + HALT, + AUCTION_MATCH, + BREAK +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/TickerPrice.java b/src/main/java/com/sigmaflare/binancej/entities/TickerPrice.java new file mode 100644 index 0000000..c2f75b8 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/TickerPrice.java @@ -0,0 +1,24 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.math.BigDecimal; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TickerPrice { + @NonNull + @JsonProperty("symbol") + private String symbol; + + @NonNull + @JsonProperty("price") + private BigDecimal price; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/Time.java b/src/main/java/com/sigmaflare/binancej/entities/Time.java new file mode 100644 index 0000000..c559f19 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/Time.java @@ -0,0 +1,16 @@ +package com.sigmaflare.binancej.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Time { + @JsonProperty("serverTime") + private long serverTime; +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/transform/CandlestickDeserializer.java b/src/main/java/com/sigmaflare/binancej/entities/transform/CandlestickDeserializer.java new file mode 100644 index 0000000..8fc5a0b --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/transform/CandlestickDeserializer.java @@ -0,0 +1,46 @@ +package com.sigmaflare.binancej.entities.transform; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.sigmaflare.binancej.entities.Candlestick; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * A custom deserializer for OrderBookDepth. Binance returns some non-standard looking data + * in it's array, so we convert the entire thing into our own representation here to make + * it easier to use. + */ +public class CandlestickDeserializer extends StdDeserializer { + + public CandlestickDeserializer() { + this(null); + } + + public CandlestickDeserializer(Class vc) { + super(vc); + } + + @Override + public Candlestick deserialize(JsonParser jp, DeserializationContext context) throws IOException { + JsonNode n = jp.getCodec().readTree(jp); + + return Candlestick + .builder() + .openTime(n.get(0).asLong()) + .open(new BigDecimal(n.get(1).asText())) + .high(new BigDecimal(n.get(2).asText())) + .low(new BigDecimal(n.get(3).asText())) + .close(new BigDecimal(n.get(4).asText())) + .volume(new BigDecimal(n.get(5).asText())) + .closeTime(n.get(6).asLong()) + .quoteAssetVolume(new BigDecimal(n.get(7).asText())) + .numberOfTrades(n.get(8).asLong()) + .takerBuyBaseAssetVolume(new BigDecimal(n.get(9).asText())) + .takerBuyQuoteAssetVolume(new BigDecimal(n.get(10).asText())) + .build(); + } +} diff --git a/src/main/java/com/sigmaflare/binancej/entities/transform/OrderBookDepthResponseDeserializer.java b/src/main/java/com/sigmaflare/binancej/entities/transform/OrderBookDepthResponseDeserializer.java new file mode 100644 index 0000000..ec6d940 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/entities/transform/OrderBookDepthResponseDeserializer.java @@ -0,0 +1,66 @@ +package com.sigmaflare.binancej.entities.transform; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.sigmaflare.binancej.entities.OrderBookDepth; +import com.sigmaflare.binancej.entities.OrderBookPricing; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * A custom deserializer for OrderBookDepth. Binance returns some non-standard looking data + * in it's array, so we convert the entire thing into our own representation here to make + * it easier to use. + */ +public class OrderBookDepthResponseDeserializer extends StdDeserializer { + + public OrderBookDepthResponseDeserializer() { + this(null); + } + + public OrderBookDepthResponseDeserializer(Class vc) { + super(vc); + } + + @Override + public OrderBookDepth deserialize(JsonParser jp, DeserializationContext context) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + + Long lastUpdateId = node.get("lastUpdateId").asLong(); + List bids = new ArrayList<>(); + List asks = new ArrayList<>(); + + + // NOTE: Jackson nodes don't seem to support conversion to streams + + for (JsonNode n : node.get("bids")) { + bids.add(OrderBookPricing + .builder() + .price(new BigDecimal(n.get(0).asText())) + .quantity(new BigDecimal(n.get(1).asText())) + .build() + ); + } + + for (JsonNode n : node.get("asks")) { + asks.add(OrderBookPricing + .builder() + .price(new BigDecimal(n.get(0).asText())) + .quantity(new BigDecimal(n.get(1).asText())) + .build() + ); + } + + return OrderBookDepth + .builder() + .lastUpdateId(lastUpdateId) + .bids(bids) + .asks(asks) + .build(); + } +} diff --git a/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceException.java b/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceException.java new file mode 100644 index 0000000..e62c2ad --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceException.java @@ -0,0 +1,12 @@ +package com.sigmaflare.binancej.exceptions; + + +public class BinanceServiceException extends Exception { + public BinanceServiceException() { super(); } + + public BinanceServiceException(String message) { super(message); } + + public BinanceServiceException(String message, Throwable cause) { super(message, cause); } + + public BinanceServiceException(Throwable cause) { super(cause); } +} diff --git a/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java b/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java new file mode 100644 index 0000000..558f913 --- /dev/null +++ b/src/main/java/com/sigmaflare/binancej/exceptions/BinanceServiceUnreachableException.java @@ -0,0 +1,11 @@ +package com.sigmaflare.binancej.exceptions; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BinanceServiceUnreachableException extends BinanceServiceException { + public String message; + public Throwable cause; +} diff --git a/src/test/java/LibraryTest.java b/src/test/java/LibraryTest.java deleted file mode 100644 index 645740d..0000000 --- a/src/test/java/LibraryTest.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -import org.junit.Test; -import static org.junit.Assert.*; - -public class LibraryTest { - @Test public void testSomeLibraryMethod() { - Library classUnderTest = new Library(); - assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod()); - } -} diff --git a/src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java b/src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java new file mode 100644 index 0000000..c538324 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/CandlestickMethodTests.java @@ -0,0 +1,289 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.Candlestick; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.Interval; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.commons.io.FileUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CandlestickMethodTests { + private static ObjectMapper mapper = Helpers.objectMapperBuilder(); + private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/klines?symbol=TEST&interval=8h"); + private final String urlWithLimit = String.format("%s%s", url, "&limit=1000"); + private final String urlWithStartTime = String.format("%s%s", url, "&startTime=1234"); + private final String urlWithEndTime = String.format("%s%s", url, "&endTime=1234"); + private final String urlWithLimitAndStartTime = String.format("%s%s", urlWithLimit, "&startTime=1234"); + private final String urlWithLimitAndEndTime = String.format("%s%s", urlWithLimit, "&endTime=1234"); + + private List candlesticks; + + // This will just make mocking easier on us... + private String data; + + @Before + public void setUp() throws IOException { + ClassLoader classLoader = ExchangeInfoMethodTests.class.getClassLoader(); + File file = new File(classLoader.getResource("candlestick_response_sample.json").getFile()); + + JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Candlestick.class); + + data = FileUtils.readFileToString(file, "UTF-8"); + + candlesticks = mapper.readValue(data, type); + } + + /** + * testFieldsAreCorrect is a test method to insure field alignment. Since a custom deserializer + * is used to convert an array into a Candlestick object, it's important that field alignment + * is checked so numbers are guaranteed. + */ + @Test + public void testFieldsAreCorrect() { + Candlestick c = candlesticks.get(0); + + assertEquals(1499040000000L, c.getOpenTime()); + assertEquals(new BigDecimal("0.01634790"), c.getOpen()); + assertEquals(new BigDecimal("0.80000000"), c.getHigh()); + assertEquals(new BigDecimal("0.01575800"), c.getLow()); + assertEquals(new BigDecimal("0.01577100"), c.getClose()); + assertEquals(new BigDecimal("148976.11427815"), c.getVolume()); + assertEquals(1499644799999L, c.getCloseTime()); + assertEquals(new BigDecimal("2434.19055334"), c.getQuoteAssetVolume()); + assertEquals(308L, c.getNumberOfTrades()); + assertEquals(new BigDecimal("1756.87402397"), c.getTakerBuyBaseAssetVolume()); + assertEquals(new BigDecimal("28.46694368"), c.getTakerBuyQuoteAssetVolume()); + // There's a field marked ignore after TakerBuyQuoteAssetVolume... + } + + @Test + public void testCandlestickServiceReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test + public void testCandlestickServiceWithLimitReturnsSuccessfully() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimit, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test + public void testCandlestickServiceWithLimitAndStartTimeReturnsSuccessfully() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimitAndStartTime, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000, 1234L,true); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test + public void testCandlestickServiceWithLimitAndEndTimeReturnsSuccessfully() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimitAndEndTime, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1000, 1234L,false); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test + public void testCandlestickServiceWithStartTimeReturnsSuccessfully() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithStartTime, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L,true); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test + public void testCandlestickServiceWithEndTimeReturnsSuccessfully() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithEndTime, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS, 1234L,false); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), candlesticks); + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void testCandlestickServiceWithoutHttpEntityThrows() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS); + } + + @Test + public void TestCandlestickServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent( + new ByteArrayInputStream(mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = + marketData.getCandleStickData("TEST", Interval.EIGHT_HOURS); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java b/src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java new file mode 100644 index 0000000..cbfbc79 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/ExchangeInfoMethodTests.java @@ -0,0 +1,120 @@ +package com.sigmaflare.binancej; + + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.ExchangeInfo; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.commons.io.FileUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ExchangeInfoMethodTests { + private static ObjectMapper mapper = Helpers.objectMapperBuilder(); + private String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/exchangeInfo"); + + private ExchangeInfo exchangeInfo; + + @Before + public void setUp() throws IOException { + ClassLoader classLoader = ExchangeInfoMethodTests.class.getClassLoader(); + File file = new File(classLoader.getResource("exchange_info_response_sample.json").getFile()); + + exchangeInfo = + mapper.readValue(FileUtils.readFileToString(file, "UTF-8"), ExchangeInfo.class); + } + + @Test + public void TestExchangeInfoServiceReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream( + mapper.writeValueAsString(exchangeInfo).getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.getExchangeInfo(); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), exchangeInfo); + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void TestExchangeInfoServiceWithNoHttpEntityFails() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + generalUtilities.getExchangeInfo(); + } + + @Test + public void TestExchangeInfoServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "Bad API Key")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent( + new ByteArrayInputStream(mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.getExchangeInfo(); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java b/src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java new file mode 100644 index 0000000..133e6a1 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/OrderBookDepthTests.java @@ -0,0 +1,124 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.OrderBookDepth; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.commons.io.FileUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OrderBookDepthTests { + private static ObjectMapper mapper = Helpers.objectMapperBuilder(); + private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=100"); + private final String urlWithLimit = String.format("%s%s", BASE_ENDPOINT, "/api/v1/depth?symbol=ETHBTC&limit=1000"); + + private OrderBookDepth orderBookDepth; + private String orderBookDepthJson; + + @Before + public void setUp() throws IOException { + ClassLoader classLoader = ExchangeInfoMethodTests.class.getClassLoader(); + File file = new File(classLoader.getResource("orderbook_depth_sample.json").getFile()); + + orderBookDepthJson = FileUtils.readFileToString(file, "UTF-8"); + + orderBookDepth = + mapper.readValue(orderBookDepthJson, OrderBookDepth.class); + } + + @Test + public void TestOrderBookDepthReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + + // This changes from the usual way we do this because we do post-processing in the deserializer and the JSON + // will no longer match the stringified OrderBookDepth. + httpEntity.setContent(new ByteArrayInputStream(orderBookDepthJson.getBytes())); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either res = marketData.getOrderBookDepth("ETHBTC"); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), orderBookDepth); + + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void TestOrderBookDepthWithNoHttpEntityFails() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimit, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + marketData.getOrderBookDepth("ETHBTC", 1000); + } + + @Test + public void TestOrderBookDepthWithBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "Bad API Key")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(urlWithLimit, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream( + mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + Either res = marketData.getOrderBookDepth("ETHBTC", 1000); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/PingMethodTests.java b/src/test/java/com/sigmaflare/binancej/PingMethodTests.java new file mode 100644 index 0000000..bb919e3 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/PingMethodTests.java @@ -0,0 +1,105 @@ +package com.sigmaflare.binancej; + + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.Ping; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PingMethodTests { + private static ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/ping"); + + @Test + public void TestServiceAliveReturnsSuccess() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.ping(); + + assertTrue(res.isRight()); + + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void TestServiceFailsToReturnHttpEntity() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + generalUtilities.ping(); + } + + @Test + public void TestServiceBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "Bad API Key")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent( + new ByteArrayInputStream(mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities + = new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.ping(); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/TickerPriceTests.java b/src/test/java/com/sigmaflare/binancej/TickerPriceTests.java new file mode 100644 index 0000000..b2283f5 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/TickerPriceTests.java @@ -0,0 +1,207 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.TickerPrice; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.commons.io.FileUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TickerPriceTests { + private static final ObjectMapper mapper = Helpers.objectMapperBuilder(); + private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v3/ticker/price"); + private final String specificUrl = String.format("%s%s", url, "?symbol=TEST"); + + private String singleTickerPriceJson; + private String multipleTickerPricesJson; + + private TickerPrice tickerPrice; + private List tickerPrices; + + + @Before + public void setUp() throws IOException { + ClassLoader classLoader = ExchangeInfoMethodTests.class.getClassLoader(); + File singleTickerPriceFile = + new File(classLoader.getResource("single_ticker_price_response.json").getFile()); + + File multipleTickerPriceFile = + new File(classLoader.getResource("multiple_ticker_prices_response.json").getFile()); + + JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, TickerPrice.class); + + singleTickerPriceJson = FileUtils.readFileToString(singleTickerPriceFile, "UTF-8"); + multipleTickerPricesJson = FileUtils.readFileToString(multipleTickerPriceFile, "UTF-8"); + + tickerPrice = mapper.readValue(singleTickerPriceJson, TickerPrice.class); + + tickerPrices = mapper.readValue(multipleTickerPricesJson, type); + } + + @Test + public void testTickerPriceForSymbolReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(specificUrl, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(singleTickerPriceJson.getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either res = marketData.getTickerPriceForSymbol("TEST"); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), tickerPrice); + } + + @Test + public void testTickerPricesSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(multipleTickerPricesJson.getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = marketData.getTickerPrices(); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), tickerPrices); + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void testTickerPriceForSymbolWithoutHttpEntityThrows() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(specificUrl, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + marketData.getTickerPriceForSymbol("TEST"); + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void testTickerPricesWithoutHttpEntityThrows() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + marketData.getTickerPrices(); + } + + @Test + public void testTickerPriceForSymbolWithBadApiKeyReturns400() + throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(specificUrl, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent( + new ByteArrayInputStream(mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either res = marketData.getTickerPriceForSymbol("TEST"); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } + + @Test + public void testTickerPricesBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent( + new ByteArrayInputStream(mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + MarketData marketData = new MarketData("1234", "abcd", mockedCloseableHttpClient); + + Either> res = marketData.getTickerPrices(); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/TimeMethodTests.java b/src/test/java/com/sigmaflare/binancej/TimeMethodTests.java new file mode 100644 index 0000000..ea1c286 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/TimeMethodTests.java @@ -0,0 +1,108 @@ +package com.sigmaflare.binancej; + +import com.codepoetics.ambivalence.Either; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigmaflare.binancej.entities.ServiceError; +import com.sigmaflare.binancej.entities.Time; +import com.sigmaflare.binancej.exceptions.BinanceServiceUnreachableException; +import com.sigmaflare.binancej.matchers.GetMatcher; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.sigmaflare.binancej.Constant.BASE_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TimeMethodTests { + private static ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final String url = String.format("%s%s", BASE_ENDPOINT, "/api/v1/time"); + + @Test + public void TestTimeReturnsSuccessfully() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + Time time = Time.builder().serverTime(System.currentTimeMillis() / 1000L).build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream( + mapper.writeValueAsString(time).getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.getServerTime(); + + assertTrue(res.isRight()); + assertEquals(Helpers.extractEitherValueSafely(res.right()), time); + } + + @Test(expected = BinanceServiceUnreachableException.class) + public void TestTimeNoEntityReturnedFails() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 200, "test")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + generalUtilities.getServerTime(); + } + + @Test + public void TestTimeBadApiKeyReturns400() throws IOException, BinanceServiceUnreachableException { + CloseableHttpClient mockedCloseableHttpClient = mock(CloseableHttpClient.class); + CloseableHttpResponse mockedCloseableHttpResponse = mock(CloseableHttpResponse.class); + + when(mockedCloseableHttpResponse.getStatusLine()) + .thenReturn(new BasicStatusLine(new ProtocolVersion("TEST", 1, 0), + 400, "Bad API Key")); + + when(mockedCloseableHttpClient.execute(argThat(new GetMatcher(url, "1234")))) + .thenReturn(mockedCloseableHttpResponse); + + ServiceError serviceError = ServiceError.builder().code(400).message("Bad API Key").build(); + + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream( + mapper.writeValueAsString(serviceError).getBytes(StandardCharsets.UTF_8))); + + + when(mockedCloseableHttpResponse.getEntity()).thenReturn(httpEntity); + + GeneralUtilities generalUtilities = + new GeneralUtilities("1234", "abcd", mockedCloseableHttpClient); + + Either res = generalUtilities.getServerTime(); + + assertTrue(res.isLeft()); + assertEquals(Helpers.extractEitherValueSafely(res.left()), serviceError); + } +} diff --git a/src/test/java/com/sigmaflare/binancej/matchers/GetMatcher.java b/src/test/java/com/sigmaflare/binancej/matchers/GetMatcher.java new file mode 100644 index 0000000..8be8a4f --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/matchers/GetMatcher.java @@ -0,0 +1,29 @@ +package com.sigmaflare.binancej.matchers; + +import org.apache.http.client.methods.HttpGet; +import org.mockito.ArgumentMatcher; + +public class GetMatcher implements ArgumentMatcher { + private String url; + private String apiKey; + + public GetMatcher(String url, String apiKey) { + this.url = url; + this.apiKey = apiKey; + } + + /** + * Matches HttpGet objects based on "loose matching". We really only care that the URL and associated + * headers we care about are correct. + * @param httpGet The HttpGet object under test + * @return True if they are equal by our definition, false otherwise + */ + public boolean matches(HttpGet httpGet) { + final String url = httpGet.getURI().toASCIIString(); + final String apiKeyHeaderValue = httpGet.getFirstHeader("X-MBX-APIKEY").getValue(); + + + return url.equals(this.url) && apiKeyHeaderValue != null && apiKeyHeaderValue.equals(apiKey); + } +} + diff --git a/src/test/java/com/sigmaflare/binancej/matchers/PostMatcher.java b/src/test/java/com/sigmaflare/binancej/matchers/PostMatcher.java new file mode 100644 index 0000000..1e87da2 --- /dev/null +++ b/src/test/java/com/sigmaflare/binancej/matchers/PostMatcher.java @@ -0,0 +1,37 @@ +package com.sigmaflare.binancej.matchers; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.HttpPost; +import org.mockito.ArgumentMatcher; + +import java.io.IOException; + +public class PostMatcher implements ArgumentMatcher { + private String url; + private String apiKey; + private String requestBody; + + public PostMatcher(String url, String apiKey, String requestBody) { + this.url = url; + this.apiKey = apiKey; + this.requestBody = requestBody; + } + + /** + * Matches HttpPost objects based on "loose matching". We really only care that the URL and associated + * headers we care about are correct. + * @param httpPost The HttpPost object under test + * @return True if they are equal by our definition, false otherwise + */ + public boolean matches(HttpPost httpPost) { + final String url = httpPost.getURI().toASCIIString(); + final String apiKeyHeaderValue = httpPost.getFirstHeader("X-MBX-APIKEY").getValue(); + + try { + String testBody = IOUtils.toString(httpPost.getEntity().getContent(), "UTF-8"); + return this.url.equals(url) && apiKey.equals(apiKeyHeaderValue) && requestBody.equals(testBody); + } catch(IOException e) { + return false; + } + } +} diff --git a/src/test/resources/candlestick_response_sample.json b/src/test/resources/candlestick_response_sample.json new file mode 100644 index 0000000..56d0cd3 --- /dev/null +++ b/src/test/resources/candlestick_response_sample.json @@ -0,0 +1,16 @@ +[ + [ + 1499040000000, + "0.01634790", + "0.80000000", + "0.01575800", + "0.01577100", + "148976.11427815", + 1499644799999, + "2434.19055334", + 308, + "1756.87402397", + "28.46694368", + "17928899.62484339" + ] +] \ No newline at end of file diff --git a/src/test/resources/exchange_info_response_sample.json b/src/test/resources/exchange_info_response_sample.json new file mode 100644 index 0000000..85c7b6b --- /dev/null +++ b/src/test/resources/exchange_info_response_sample.json @@ -0,0 +1,45 @@ +{ + "timezone": "UTC", + "serverTime": 1508631584636, + "rateLimits": [{ + "rateLimitType": "REQUESTS", + "interval": "MINUTE", + "limit": 1200 + }, + { + "rateLimitType": "ORDERS", + "interval": "SECOND", + "limit": 10 + }, + { + "rateLimitType": "ORDERS", + "interval": "DAY", + "limit": 100000 + } + ], + "exchangeFilters": [], + "symbols": [{ + "symbol": "ETHBTC", + "status": "TRADING", + "baseAsset": "ETH", + "baseAssetPrecision": 8, + "quoteAsset": "BTC", + "quotePrecision": 8, + "orderTypes": ["LIMIT", "MARKET"], + "icebergAllowed": false, + "filters": [{ + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + }, { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + }, { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00100000" + }] + }] +} \ No newline at end of file diff --git a/src/test/resources/multiple_ticker_prices_response.json b/src/test/resources/multiple_ticker_prices_response.json new file mode 100644 index 0000000..1557fc1 --- /dev/null +++ b/src/test/resources/multiple_ticker_prices_response.json @@ -0,0 +1,10 @@ +[ + { + "symbol": "LTCBTC", + "price": "4.00000200" + }, + { + "symbol": "ETHBTC", + "price": "0.07946600" + } +] \ No newline at end of file diff --git a/src/test/resources/orderbook_depth_sample.json b/src/test/resources/orderbook_depth_sample.json new file mode 100644 index 0000000..248b882 --- /dev/null +++ b/src/test/resources/orderbook_depth_sample.json @@ -0,0 +1,17 @@ +{ + "lastUpdateId": 1027024, + "bids": [ + [ + "4.00000000", + "431.00000000", + [] + ] + ], + "asks": [ + [ + "4.00000200", + "12.00000000", + [] + ] + ] +} \ No newline at end of file diff --git a/src/test/resources/single_ticker_price_response.json b/src/test/resources/single_ticker_price_response.json new file mode 100644 index 0000000..ab64ec7 --- /dev/null +++ b/src/test/resources/single_ticker_price_response.json @@ -0,0 +1,4 @@ +{ + "symbol": "LTCBTC", + "price": "4.00000200" +} \ No newline at end of file