From 4d2914ae3872d1e1cd963ef3c144be4c2c02a042 Mon Sep 17 00:00:00 2001 From: kleninmaxim Date: Sun, 16 Jul 2023 12:32:26 +0300 Subject: [PATCH] Init --- .gitignore | 7 + README.md | 1123 +++++++++++++++++ composer.json | 32 + src/Binance.php | 275 ++++ src/BinanceAliases.php | 97 ++ src/BinanceOriginal.php | 98 ++ src/Docs/Const/BinanceApi/Endpoint.php | 26 + .../Const/BinanceApi/HasQueryParameters.php | 17 + src/Docs/Const/BinanceApi/ProcessResponse.php | 16 + .../Exception/EndpointQueryException.php | 9 + src/Docs/Const/Parameter.php | 12 + src/Docs/Const/Response.php | 14 + src/Docs/GeneralInfo/Const/BanBased.php | 9 + .../Const/EndpointParameterType.php | 9 + src/Docs/GeneralInfo/Const/HttpCodeStatus.php | 11 + src/Docs/GeneralInfo/Const/HttpMethod.php | 11 + src/Docs/GeneralInfo/DataSources.php | 14 + src/Docs/GeneralInfo/EndpointSecurityType.php | 35 + src/Docs/GeneralInfo/Filters.php | 168 +++ .../GeneralInfo/GeneralApiInformation.php | 37 + src/Docs/GeneralInfo/Limits.php | 28 + src/Docs/GeneralInfo/PublicApiDefinitions.php | 268 ++++ src/Docs/GeneralInfo/Signed.php | 132 ++ src/Docs/Introduction/TestNet.php | 8 + .../MarketDataEndpoint/CheckServerTime.php | 51 + .../CompressedAggregateTradesList.php | 109 ++ .../CurrentAveragePrice.php | 53 + .../ExchangeInformation.php | 187 +++ .../KlineCandlestickData.php | 117 ++ .../MarketDataEndpoint/OldTradeLookup.php | 87 ++ src/Docs/MarketDataEndpoint/OrderBook.php | 92 ++ .../MarketDataEndpoint/RecentTradesList.php | 78 ++ .../RollingWindowPriceChangeStatistics.php | 142 +++ .../SymbolOrderBookTicker.php | 80 ++ .../MarketDataEndpoint/SymbolPriceTicker.php | 71 ++ .../MarketDataEndpoint/TestConnectivity.php | 39 + .../TickerPriceChangeStatistics24hr.php | 100 ++ src/Docs/MarketDataEndpoint/UIKlines.php | 117 ++ src/Exception/BinanceException.php | 9 + src/Exception/BinanceResponseException.php | 9 + src/Exception/MethodNotExistException.php | 9 + src/Helper/Carbon.php | 21 + src/Helper/Http.php | 58 + src/Helper/Math.php | 45 + .../ResponseHandler/CustomResponseHandler.php | 80 ++ .../GuzzlePsr7ResponseHandler.php | 14 + .../OriginalDecodedHandler.php | 14 + .../OriginalResponseHandler.php | 14 + .../ResponseHandler/ResponseHandler.php | 18 + tests/BinanceOriginalTest.php | 154 +++ tests/BinanceTest.php | 621 +++++++++ tests/Helper/CarbonTest.php | 19 + tests/Helper/HttpTest.php | 61 + tests/Helper/MathTest.php | 35 + 54 files changed, 4960 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/Binance.php create mode 100644 src/BinanceAliases.php create mode 100644 src/BinanceOriginal.php create mode 100644 src/Docs/Const/BinanceApi/Endpoint.php create mode 100644 src/Docs/Const/BinanceApi/HasQueryParameters.php create mode 100644 src/Docs/Const/BinanceApi/ProcessResponse.php create mode 100644 src/Docs/Const/Exception/EndpointQueryException.php create mode 100644 src/Docs/Const/Parameter.php create mode 100644 src/Docs/Const/Response.php create mode 100644 src/Docs/GeneralInfo/Const/BanBased.php create mode 100644 src/Docs/GeneralInfo/Const/EndpointParameterType.php create mode 100644 src/Docs/GeneralInfo/Const/HttpCodeStatus.php create mode 100644 src/Docs/GeneralInfo/Const/HttpMethod.php create mode 100644 src/Docs/GeneralInfo/DataSources.php create mode 100644 src/Docs/GeneralInfo/EndpointSecurityType.php create mode 100644 src/Docs/GeneralInfo/Filters.php create mode 100644 src/Docs/GeneralInfo/GeneralApiInformation.php create mode 100644 src/Docs/GeneralInfo/Limits.php create mode 100644 src/Docs/GeneralInfo/PublicApiDefinitions.php create mode 100644 src/Docs/GeneralInfo/Signed.php create mode 100644 src/Docs/Introduction/TestNet.php create mode 100644 src/Docs/MarketDataEndpoint/CheckServerTime.php create mode 100644 src/Docs/MarketDataEndpoint/CompressedAggregateTradesList.php create mode 100644 src/Docs/MarketDataEndpoint/CurrentAveragePrice.php create mode 100644 src/Docs/MarketDataEndpoint/ExchangeInformation.php create mode 100644 src/Docs/MarketDataEndpoint/KlineCandlestickData.php create mode 100644 src/Docs/MarketDataEndpoint/OldTradeLookup.php create mode 100644 src/Docs/MarketDataEndpoint/OrderBook.php create mode 100644 src/Docs/MarketDataEndpoint/RecentTradesList.php create mode 100644 src/Docs/MarketDataEndpoint/RollingWindowPriceChangeStatistics.php create mode 100644 src/Docs/MarketDataEndpoint/SymbolOrderBookTicker.php create mode 100644 src/Docs/MarketDataEndpoint/SymbolPriceTicker.php create mode 100644 src/Docs/MarketDataEndpoint/TestConnectivity.php create mode 100644 src/Docs/MarketDataEndpoint/TickerPriceChangeStatistics24hr.php create mode 100644 src/Docs/MarketDataEndpoint/UIKlines.php create mode 100644 src/Exception/BinanceException.php create mode 100644 src/Exception/BinanceResponseException.php create mode 100644 src/Exception/MethodNotExistException.php create mode 100644 src/Helper/Carbon.php create mode 100644 src/Helper/Http.php create mode 100644 src/Helper/Math.php create mode 100644 src/Helper/ResponseHandler/CustomResponseHandler.php create mode 100644 src/Helper/ResponseHandler/GuzzlePsr7ResponseHandler.php create mode 100644 src/Helper/ResponseHandler/OriginalDecodedHandler.php create mode 100644 src/Helper/ResponseHandler/OriginalResponseHandler.php create mode 100644 src/Helper/ResponseHandler/ResponseHandler.php create mode 100644 tests/BinanceOriginalTest.php create mode 100644 tests/BinanceTest.php create mode 100644 tests/Helper/CarbonTest.php create mode 100644 tests/Helper/HttpTest.php create mode 100644 tests/Helper/MathTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ad6293 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.vscode +vendor +composer.lock +phpunit.xml +.phpunit.result.cache +.php-cs-fixer.cache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2da222e --- /dev/null +++ b/README.md @@ -0,0 +1,1123 @@ +# Binance PHP API Client +The purpose of this project is to assist you in creating your own projects that interact with the [Binance API](https://binance-docs.github.io/apidocs/spot/en/). + +## Introduction +This project requires [php](https://www.php.net/) version more or equal 8.2. Also it requires [bcmath](https://www.php.net/manual/en/book.bc.php) extension and [guzzle](https://docs.guzzlephp.org/en/stable/) dependency + +## Installation + +## Quick start +Every method's name in `\BinanceApi\Binance::class` created by name from url after prefix `v3`. For example, by table: + + + + + + + + +
Name of endpoint Method name
+ + [Order Book](https://binance-docs.github.io/apidocs/spot/en/#order-book): /api/v3/**depth** + + + + ```php + $binance->depth($symbol, $limit); + ``` +
+ + [Symbol Order Book Ticker](https://binance-docs.github.io/apidocs/spot/en/#symbol-order-book-ticker): /api/v3/**ticker/bookTicker** + + + + ```php + $binance->tickerBookTicker('BTCUSDT'); + ``` +
+ + [24hr Ticker Price Change Statistics](https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics): /api/v3/**ticker/24hr** + + + + ```php + $binance->ticker24hr($symbol, $symbols, $type); + ``` +
+ +All endpoints and their methods with parameters you can see in phpdoc in `\BinanceApi\Binance::class` + +You can start to execute requests in two ways: + +Custom handled Binance class + + ```php + $binance = new \BinanceApi\Binance(); + + $fullResult = $binance->depth('BTCUSDT', 2); + + $orderbook = $fullResult['response']['data']; + ``` +
+ View $fullResult variable + +``` +Array +( + [request] => Array + ( + [url] => /api/v3/depth + [headers] => Array + ( + ) + [query] => Array + ( + [symbol] => BTCUSDT + [limit] => 2 + ) + [body] => Array + ( + ) + ) + + [response] => Array + ( + [data] => Array + ( + [lastUpdateId] => 37910427874 + [bids] => Array + ( + [0] => Array + ( + [price] => 30319.99000000 + [amount] => 3.58155000 + ) + + [1] => Array + ( + [price] => 30319.98000000 + [amount] => 0.09091000 + ) + ) + [asks] => Array + ( + [0] => Array + ( + [price] => 30320.00000000 + [amount] => 21.24342000 + ) + + [1] => Array + ( + [price] => 30320.05000000 + [amount] => 0.00170000 + ) + ) + ) + + [info] => Array + ( + [statusCode] => 200 + [reasonPhrase] => OK + [headers] => Array + ( + [Content-Type] => Array + ( + [0] => application/json;charset=UTF-8 + ) + + ... + + [x-mbx-uuid] => Array + ( + [0] => ad6df6c5-903b-451b-904c-5ba90eb4576d + ) + + [x-mbx-used-weight] => Array + ( + [0] => 1 + ) + + [x-mbx-used-weight-1m] => Array + ( + [0] => 1 + ) + + ... + ) + ) + ) +) +``` +
+ +You can go to the `Example` topic to see more examples and use it +## Basic + +This topic all about `\BinanceApi\Binance::class` + +### Create a class with different endpoints (by default it: `https://api.binance.com`) +```php +$binanceOriginalEndpoint = new \BinanceApi\Binance(); +$binance = new \BinanceApi\Binance('https://api3.binance.com'); + +// Or through constants +$binance = new \BinanceApi\Binance(\BinanceApi\Docs\GeneralInfo\GeneralApiInformation::GCP_ENDPOINT); +$binance = new \BinanceApi\Binance(\BinanceApi\Docs\GeneralInfo\GeneralApiInformation::API1_ENDPOINT); +$binance = new \BinanceApi\Binance(\BinanceApi\Docs\GeneralInfo\GeneralApiInformation::API2_ENDPOINT); +$binance = new \BinanceApi\Binance(\BinanceApi\Docs\GeneralInfo\GeneralApiInformation::API3_ENDPOINT); +$binance = new \BinanceApi\Binance(\BinanceApi\Docs\GeneralInfo\GeneralApiInformation::API4_ENDPOINT); +``` +### Create a class to use [testnet](https://testnet.binance.vision/) +```php +$binanceTestNet = new \BinanceApi\Binance(TestNet::BASE_ENDPOINT); +``` + +### Set Guzzle Client +If you need to replace default Guzzle Client on yours, here an example: +```php +$client = new \GuzzleHttp\Client(['timeout' => 10]); +$binance = new \BinanceApi\Binance(client: $client); +``` +See about Guzzle Client [here](https://docs.guzzlephp.org/en/stable/) + +### Return result from each endpoint request + +Every request method returns next result: + +```php +print_r($binance->depth('BTCUSDT', 10)); +``` + +```text +Array +( + [request] => Array + ( + [url] => /api/v3/depth + [headers] => Array + ( + ... + ) + [query] => Array + ( + ... + ) + [body] => Array + ( + ... + ) + ) + + [response] => Array + ( + [data] => Array + ( + ... + ) + + [info] => Array + ( + [statusCode] => 200 + [reasonPhrase] => OK + [headers] => Array + ( + ... + ) + ) + ) +) +``` + +1) `request` - is an array with all request information to Binance +2) `response` - all about response, where `data` - processed result from Binance and `info` - response data (code, phrase and headers) + +### Filter every ending result from `\BinanceApi\Binance::class` +After each request, you get the result as an array. This array contains a lot of information and may be you don't want to use it. + +First example: +```php +$binance->setOutputCallback(function ($output) { + return $output['response']['data']; +}); + +print_r($binance->depth('BTCUSDT', 10)); +``` +```text +Array +( + [lastUpdateId] => 37910427874 + [bids] => Array + ( + ... + ) + [asks] => Array + ( + ... + ) +) +``` + +Second example: + +```php +$binance->setOutputCallback(function ($output) { + unset($output['response']['info']['headers']); + unset($output['request']); + + return $output; +}); +``` + +### Set Api Keys +Execute `setApiKeys` method: +```php +$binance->setApiKeys($apiKey, $secretKey); +``` +You also can set only Api Key, without Secret Key (Some endpoints need only Api Key): +```php +$binance->setApiKeys($apiKey); +``` + +### Exceptions +```php +try { + $result = $binance->depth('BTCUSDT', 2); +} catch (BinanceApi\Docs\Const\Exception\EndpointQueryException $e) { + // Here exception with endpoint related, + // For example, you don't set a symbol as a mandatory parameter into a method +} catch (BinanceApi\Exception\BinanceException $e) { + // Exception with Binance related, + // For example, don't set Api Keys to class +} catch (BinanceApi\Exception\BinanceResponseException $e) { + // This is exception throw, when binance return error message + // https://binance-docs.github.io/apidocs/spot/en/#error-codes +} catch (BinanceApi\Exception\MethodNotExistException $e) { + // If method doesn't exist it will throw such exception +} catch (\GuzzleHttp\Exception\GuzzleException $e) { + // It about Guzzle exception + // https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions +} +``` + +### Additional information + +`\BinanceApi\Binance::class` has `getAdditional()` method. + +After each request additional property updated by request and you can use it. + +```php +$binance->getAdditional(); +``` + +You can see, for example, how many limits you use in binance weights and last request that you made: +```php +print_r($binance->getAdditional()['limits'][BinanceApi\Docs\GeneralInfo\Const\BanBased::IP]['api']); +``` +```text +Array +( + [used] => 2 // By default maximum weight in minute is 1200 + [lastRequest] => Sat, 15 Jul 2023 14:19:01 GMT +) +``` + +### Carbon, Math and Http + + +## Digging deeper + +### Disable throw `BinanceApi\Exception\BinanceException` and `BinanceApi\Exception\BinanceResponseException` +```php +$binance->disableBinanceExceptions(); +``` +After to execute this method, you will not get `BinanceApi\Exception\BinanceException` and `BinanceApi\Exception\BinanceResponseException`, +but you still can get `BinanceApi\Docs\Const\Exception\EndpointQueryException`, `BinanceApi\Exception\MethodNotExistException` and `\GuzzleHttp\Exception\GuzzleException` + +If you also want to disable `BinanceApi\Docs\Const\Exception\EndpointQueryException`, you can rewrite Endpoint and add it to Binance class + +### Endpoint + +Endpoint is the main core of information on which the binance classes rely + +Let's look at one random Endpoint class: +```php + Carbon::getFullDate($item[0]), + 'openPrice' => $item[1], + 'highPrice' => $item[2], + 'lowPrice' => $item[3], + 'closePrice' => $item[4], + ]; + }, $response); + } +} + +// Replace default alias in 'klines'. See topic below about it +$binance->binanceOriginal->addAlias('klines', ExtendedKlineCandlestickData::class); + +$binance->klines('BTCUSDT', '1d', limit: 5); // and will handle the content of response in that format +``` + +### Add Endpoint to execute it as a function + +You can create your own Endpoint by previous topic or use already existing Endpoint + +```php +$binance = new \BinanceApi\Binance(); +$binance->binanceOriginal->addAlias('yourCustomFunction', YourCustomEndpoint::class); + +$binance->yourCustomFunction(); +$binance->yourCustomFunction(...$parameters); // If YourCustomEndpoint::class implements of BinanceApi\Docs\Const\BinanceApi\HasQueryParameters + +$binance->binanceOriginal->addAlias('yourCustomFunctionToGetOrderbook', BinanceApi\Docs\MarketDataEndpoint\OrderBook::class); + +// Now yourCustomFunctionToGetOrderbook() and depth() are identical and fully the same +$binance->yourCustomFunctionToGetOrderbook('BTCUSDT', 5); +$binance->depth('BTCUSDT', 5); +``` + +### Let's talk about `\BinanceApi\BinanceOriginal::class` + ```php + $binance = new \BinanceApi\BinanceOriginal(); + + $orderbook = $binance->depth(query: ['symbol' => 'BTCUSDT', 'limit' => 5]); + ``` + +
+ View $orderbook variable + +``` +Array +( + [lastUpdateId] => 37910484364 + + [bids] => Array + ( + [0] => Array + ( + [0] => 30331.38000000 + [1] => 14.74474000 + ) + [1] => Array + ( + [0] => 30331.36000000 + [1] => 0.01941000 + ) + ) + + [asks] => Array + ( + [0] => Array + ( + [0] => 30331.39000000 + [1] => 1.34772000 + ) + [1] => Array + ( + [0] => 30331.41000000 + [1] => 0.00131000 + ) + ) +) +``` +
+ +#### Inside of class: + +1) This class has only two core things: `__call(string $name, array $arguments)` and `\BinanceApi\BinanceAliases::class` trait +2) When we call any method, for example, `depth(...$parameters)` it resolve alias from property and execute http request. +In `\BinanceApi\BinanceAliases::class` trait we find the Endpoint class by the `$name`, for example `BinanceApi\Docs\MarketDataEndpoint\OrderBook::class`, anyway if not find alias, it will throw `BinanceApi\Exception\MethodNotExistException`. +3) Then execute http request + +Each method in Binance has fixed parameters: `depth(array $headers = [], array $query = [], array $body = []): mixed`. +All this parameters for http request. + +You can have access to `\BinanceApi\BinanceOriginal::class` through `\BinanceApi\Binance::class` +```php +$binance = new \BinanceApi\Binance(); + +$binance->binanceOriginal; // This is \BinanceApi\BinanceOriginal::class + +// And you can execute all methods +$binance->binanceOriginal->depth(['x-headers' => 'some-value'], ['symbol' => 'BTCUSDT', 'limit' => 10]); +``` + +#### The difference between `\BinanceApi\Binance::class` and `\BinanceApi\BinanceOriginal::class` + +**So, if you don't want to worry about the internal structure of classes and want to use ready-made methods use `\BinanceApi\Binance::class` and it will be nice.** + +**Use `\BinanceApi\BinanceOriginal::class` if you want to make any custom request with adding your parameters to the header, query and body bypassing the `\BinanceApi\Binance::class`** + +`\BinanceApi\Binance::class` +1) Return a full result and handle it +2) Every method has its own parameters to endpoint +3) Add to request necessary parameters, for example, api key to header and signature +4) Has additional property with some information +5) Handle the request from Binance (for example, in orderbook request you can see `price` and `amount`) +6) Throw Exception if a Binance return error message +7) And more another feature + +`\BinanceApi\BinanceOriginal::class` + +1) Return an original result from Binance as in documentation (for example [orderbook](https://binance-docs.github.io/apidocs/spot/en/#order-book)). +2) This class also will return an error result from Binance (for example, no mandatory parameter) as an original result from Binance. +3) Every method you can see in `\BinanceApi\BinanceAliases::class` and every method has three parameters: headers, query, body. So + you have yourself create signature, add an api key to header at those methods. + +## Examples and Analogs + +All methods you can find in `\BinanceApi\Binance::class` +```php +/** + * + * Original Methods: + * + * @method array ping() Test Connectivity + * @method array time() Check Server Time + * @method array exchangeInfo(null|string $symbol = null, null|array $symbols = null, null|string|array $permissions = null) Exchange Information + * @method array depth(null|string $symbol = null, int $limit = 100) Order Book. + * @method array trades(null|string $symbol = null, int $limit = 500) Recent Trades List. + * @method array historicalTrades(null|string $symbol = '', int $limit = 500, null|string $fromId = null) Old Trade Lookup (MARKET_DATA). + * @method array aggTrades(null|string $symbol = null, null|string $fromId = null, null|string $startTime = null, null|string $endTime = null, int $limit = 500) Compressed/Aggregate Trades List. + * @method array klines(null|string $symbol = null, null|string $interval = null, null|string $startTime = null, null|string $endTime = null, int $limit = 500) Kline/Candlestick Data. + * @method array uiKlines(null|string $symbol = null, null|string $interval = null, null|string $startTime = null, null|string $endTime = null, int $limit = 500) UIKlines. + * @method array avgPrice(null|string $symbol = null) Current Average Price. + * @method array ticker24hr(null|string $symbol = null, null|array $symbols = null, string $type = 'FULL') 24hr Ticker Price Change Statistics. + * @method array tickerPrice(null|string $symbol = null, null|array $symbols = null) Symbol Price Ticker. + * @method array tickerBookTicker(null|string $symbol = null, null|array $symbols = null) Symbol Order Book Ticker. + * @method array ticker(null|string $symbol = null, null|array $symbols = null, string $windowSize = '1d', string $type = 'FULL') Rolling window price change statistics. + * + * Analogs: + * + * @method array orderbook(string $symbol, int $limit = 100) depth() analog + * @method array orderbookBTCUSDT(int $limit = 100) depth() analog + */ +class Binance { + +} +``` + +### Common part for all next requests +```php +$binance = new \BinanceApi\Binance(); + +$binance->setOutputCallback(function ($output) { + return $output['response']['data']; +}); + +$binance->setApiKeys('apiKey', 'apiSecret'); +``` + +### [Test Connectivity](https://binance-docs.github.io/apidocs/spot/en/#test-connectivity) +```php +$binance->ping(); +``` + +
+View result + +```text +[] +``` +
+ +### [Check Server Time](https://binance-docs.github.io/apidocs/spot/en/#check-server-time) +```php +$binance->time(); +``` + +
+View result + +```text +[ + 'serverTime' => 1499827319559 + 'customAdditional' => [ + 'serverTimeDate' => 'Sat, 15 Jul 2023 17:02:47 UTC' + ] +] +``` +
+ +### [Exchange Information](https://binance-docs.github.io/apidocs/spot/en/#exchange-information) +```php +$binance->exchangeInfo('BTCUSDT'); +``` + +
+View result + +```text +[ + 'timezone' => 'UTC', + 'serverTime' => 1565246363776, + 'rateLimits' => [ + [ + 'rateLimitType' => 'REQUEST_WEIGHT', + 'interval' => 'MINUTE', + 'intervalNum' => 1, + 'limit' => 1200, + ], + [ + 'rateLimitType' => 'ORDERS', + 'interval' => 'SECOND', + 'intervalNum' => 10, + 'limit' => 50, + ], + [ + 'rateLimitType' => 'ORDERS', + 'interval' => 'DAY', + 'intervalNum' => 1, + 'limit' => 160000, + ], + [ + 'rateLimitType' => 'RAW_REQUESTS', + 'interval' => 'MINUTE', + 'intervalNum' => 5, + 'limit' => 6100, + ], + ], + 'exchangeFilters' => [], + 'symbols' => [ + [ + 'symbol' => 'ETHBTC', + 'status' => 'TRADING', + 'baseAsset' => 'ETH', + 'baseAssetPrecision' => 8, + 'quoteAsset' => 'BTC', + 'quotePrecision' => 8, + 'quoteAssetPrecision' => 8, + 'orderTypes' => [ + 'LIMIT', + 'LIMIT_MAKER', + 'MARKET', + 'STOP_LOSS', + 'STOP_LOSS_LIMIT', + 'TAKE_PROFIT', + 'TAKE_PROFIT_LIMIT' + ], + 'icebergAllowed' => true, + 'ocoAllowed' => true, + 'quoteOrderQtyMarketAllowed' => true, + 'allowTrailingStop' => false, + 'cancelReplaceAllowed' => false, + 'isSpotTradingAllowed' => true, + 'isMarginTradingAllowed' => true, + 'filters' => [ + [ + 'filterType' => 'PRICE_FILTER', + 'minPrice' => 0.00000100, + 'maxPrice' => 100.00000000, + 'tickSize' => 0.00000100, + ], + [ + 'filterType' => 'LOT_SIZE', + 'minPrice' => 0.00000100, + 'maxPrice' => 9000.00000000, + 'stepSize' => 0.00001000, + ], + [ + 'filterType' => 'ICEBERG_PARTS', + 'limit' => 10, + ], + [ + 'filterType' => 'MARKET_LOT_SIZE', + 'minPrice' => 0.00000000, + 'maxPrice' => 1000.00000000, + 'stepSize' => 0.00000000, + ], + [ + 'filterType' => 'TRAILING_DELTA', + 'minTrailingAboveDelta' => 10, + 'maxTrailingAboveDelta' => 2000, + 'minTrailingBelowDelta' => 10, + 'maxTrailingBelowDelta' => 2000, + ], + [ + 'filterType' => 'PERCENT_PRICE_BY_SIDE', + 'bidMultiplierUp' => 5, + 'bidMultiplierDown' => 0.2, + 'askMultiplierUp' => 5, + 'askMultiplierDown' => 0.2, + ], + [ + 'filterType' => 'NOTIONAL', + 'minNotional' => 0.00010000, + 'applyMinToMarket' => 1, + 'maxNotional' => 9000000.00000000, + 'applyMaxToMarket' => '', + 'avgPriceMins' => 1, + ], + [ + 'filterType' => 'MAX_NUM_ORDERS', + 'maxNumOrders' => 200, + ], + [ + 'filterType' => 'MAX_NUM_ALGO_ORDERS', + 'maxNumAlgoOrders' => 5, + ], + ], + 'permissions' => ['SPOT', 'MARGIN'], + 'defaultSelfTradePreventionMode' => 'NONE', + 'allowedSelfTradePreventionModes' => ['NONE'], + ] + ], + 'customAdditional' => [ + ['serverTimeDate'] => 'Sat, 15 Jul 2023 17:02:47 UTC' + ] +] +``` +
+ +### [Order Book](https://binance-docs.github.io/apidocs/spot/en/#order-book) +```php +$binance->depth('BTCUSDT', 5); +$binance->orderbook('BTCUSDT', 5); +$binance->orderbookBTCUSDT(5); // "BTCUSDT" you can replace with any market: "ETHUSDT", "BTCBUSD", ... +``` + +
+View result + +```text +[ + 'lastUpdateId' => 1027024, + 'bids' => [ + [ + 'amount' => '4.00000000', + 'price' => '431.00000000', + ], + ], + 'asks' => [ + [ + 'amount' => '4.00000200', + 'price' => '12.00000000', + ], + ] + ] +``` +
+ +### [Recent Trades List](https://binance-docs.github.io/apidocs/spot/en/#recent-trades-list) +```php +$binance->trades('BTCUSDT', 5); +``` + +
+View result + +```text +[ + [ + 'id' => 1454371, + 'price' => '30280.27000000', + 'qty' => '0.00100000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true, + 'customAdditional' => [ + 'timeDate' => 'Sun, 16 Jul 2023 08:17:01 UTC' + ], + ], + [ + 'id' => 28457, + 'price' => '4.00000100', + 'qty' => '12.00000000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true, + 'customAdditional' => [ + 'timeDate' => 'Sun, 16 Jul 2023 08:17:01 UTC' + ], + ] +] +``` +
+ +### [Old Trade Lookup](https://binance-docs.github.io/apidocs/spot/en/#old-trade-lookup) +```php +$binance->historicalTrades('BTCUSDT'); +``` + +
+View result + +```text +[ + [ + 'id' => 28457, + 'price' => '4.00000100', + 'qty' => '12.00000000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true + 'customAdditional' => [ + 'timeDate' => 'Sun, 16 Jul 2023 08:17:01 UTC' + ], + ], + [ + 'id' => 28457, + 'price' => '4.00000100', + 'qty' => '12.00000000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true + 'customAdditional' => [ + 'timeDate' => 'Sun, 16 Jul 2023 08:17:01 UTC' + ], + ] +] +``` +
+ +### [Compressed/Aggregate Trades List](https://binance-docs.github.io/apidocs/spot/en/#compressed-aggregate-trades-list) +```php +$binance->aggTrades('BTCUSDT', limit: 5); +``` + +
+View result + +```text +[ + [ + 'aggregateTradeId' => 26129, + 'price' => "0.01633102", + 'quantity' => "4.70443515", + 'firstTradeId' => 27781, + 'lastTradeId' => 27781, + 'timestamp' => 1498793709153, + 'wasTheBuyerTheMaker' => true, + 'wasTheTradeTheBestPriceMatch' => true, + 'customAdditional' => [ + 'timestampDate' => 'Sun, 16 Jul 2023 08:21:53 UTC' + ] + ], + [ + 'aggregateTradeId' => 26129, + 'price' => "0.01633102", + 'quantity' => "4.70443515", + 'firstTradeId' => 27781, + 'lastTradeId' => 27781, + 'timestamp' => 1498793709153, + 'wasTheBuyerTheMaker' => true, + 'wasTheTradeTheBestPriceMatch' => true, + 'customAdditional' => [ + 'timestampDate' => 'Sun, 16 Jul 2023 08:21:53 UTC' + ] + ] +] +``` +
+ +### [Kline/Candlestick Data](https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data) +```php +$binance->klines('BTCUSDT', '1d', limit: 5); +``` + +
+View result + +```text +[ + [ + 'klineOpenTime' => '1689465600000', + 'openPrice' => '30289.53000000', + 'highPrice' => '30335.16000000', + 'lowPrice' => '29984.02000000', + 'closePrice' => '30293.76000000', + 'volume' => '639.17429700', + 'klineCloseTime' => '1689551999999', + 'quoteAssetVolume' => '19323347.68137913', + 'numberOfTrades' => '45576', + 'takerBuyBaseAssetVolume' => '362.10226300', + 'takerBuyQuoteAssetVolume' => '10945758.72630827', + 'unusedField' => '0', + 'customAdditional' => [ + 'klineOpenTimeDate' => 'Sun, 16 Jul 2023 00:00:00 UTC', + 'klineCloseTimeDate' => 'Mon, 17 Jul 2023 00:00:00 UTC', + ], + ] +] +``` +
+ +### [UIKlines](https://binance-docs.github.io/apidocs/spot/en/#uiklines) +```php +$binance->uiKlines('BTCUSDT', '1d', limit: 5); +``` + +
+View result + +```text +[ + [ + 'klineOpenTime' => '1689465600000', + 'openPrice' => '30289.53000000', + 'highPrice' => '30335.16000000', + 'lowPrice' => '29984.02000000', + 'closePrice' => '30293.76000000', + 'volume' => '639.17429700', + 'klineCloseTime' => '1689551999999', + 'quoteAssetVolume' => '19323347.68137913', + 'numberOfTrades' => '45576', + 'takerBuyBaseAssetVolume' => '362.10226300', + 'takerBuyQuoteAssetVolume' => '10945758.72630827', + 'unusedField' => '0', + 'customAdditional' => [ + 'klineOpenTimeDate' => 'Sun, 16 Jul 2023 00:00:00 UTC', + 'klineCloseTimeDate' => 'Mon, 17 Jul 2023 00:00:00 UTC', + ], + ] +] +``` +
+ +### [Current Average Price](https://binance-docs.github.io/apidocs/spot/en/#current-average-price) +```php +$binance->avgPrice('BTCUSDT'); +``` + +
+View result + +```text +[ + 'mins' => 5, + 'price' => '9.35751834', +] +``` +
+ +### [24hr Ticker Price Change Statistics](https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics) +```php +$binance->ticker24hr('BTCUSDT'); +``` + +
+View result + +```text +[ + [ + 'symbol' => 'BNBBTC', + 'priceChange' => '-94.99999800', + 'priceChangePercent' => '-95.960', + 'weightedAvgPrice' => '0.29628482', + 'prevClosePrice' => '0.10002000', + 'lastPrice' => '4.00000200', + 'lastQty' => '200.00000000', + 'bidPrice' => '4.00000000', + 'bidQty' => '100.00000000', + 'askPrice' => '4.00000200', + 'askQty' => '100.00000000', + 'openPrice' => '99.00000000', + 'highPrice' => '100.00000000', + 'lowPrice' => '0.10000000', + 'volume' => '8913.30000000', + 'quoteVolume' => '15.30000000', + 'openTime' => 1499783499040, + 'closeTime' => 1499869899040, + 'firstId' => 28385, + 'lastId' => 28460, + 'count' => 76, + 'customAdditional' => [ + 'openTimeDate' => 'Sat, 15 Jul 2023 08:35:54 UTC', + 'closeTimeDate' => 'Sun, 16 Jul 2023 08:35:54 UTC' + ] + ] +] +``` +
+ +### [Symbol Price Ticker](https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker) +```php +$binance->tickerPrice(); +``` + +
+View result + +```text +[ + [ + 'symbol' => 'LTCBTC', + 'price' => '4.00000200', + ], + [ + 'symbol' => 'ETHBTC', + 'price' => '0.07946600', + ] +] +``` +
+ +### [Symbol Order Book Ticker](https://binance-docs.github.io/apidocs/spot/en/#symbol-order-book-ticker) +```php +$binance->tickerBookTicker('BTCUSDT'); +``` + +
+View result + +```text +[ + [ + 'symbol' => 'LTCBTC', + 'bidPrice' => '4.00000000', + 'bidQty' => '431.00000000', + 'askPrice' => '4.00000200', + 'askQty' => '9.00000000', + ], + [ + 'symbol' => 'ETHBTC', + 'bidPrice' => '0.07946700', + 'bidQty' => '9.00000000', + 'askPrice' => '100000.00000000', + 'askQty' => '1000.00000000', + ] +] +``` +
+ +### [Rolling window price change statistics](https://binance-docs.github.io/apidocs/spot/en/#rolling-window-price-change-statistics) +```php +$binance->ticker('BTCUSDT'); +``` + +
+View result + +```text +[ + [ + 'symbol' => 'BTCUSDT', + 'priceChange' => '-154.13000000', + 'priceChangePercent' => '-0.740', + 'weightedAvgPrice' => '20677.46305250', + 'openPrice' => '20825.27000000', + 'highPrice' => '20972.46000000', + 'lowPrice' => '20327.92000000', + 'lastPrice' => '20671.14000000', + 'volume' => '72.65112300', + 'quoteVolume' => '1502240.91155513', + 'openTime' => 1655432400000, + 'closeTime' => 1655446835460, + 'firstId' => 11147809, + 'lastId' => 11149775, + 'count' => 1967, + 'customAdditional' => [ + 'openTimeDate' => 'Sat, 15 Jul 2023 08:35:54 UTC', + 'closeTimeDate' => 'Sun, 16 Jul 2023 08:35:54 UTC' + ] + ], + [ + 'symbol' => 'BNBBTC', + 'priceChange' => '0.00008530', + 'priceChangePercent' => '0.823', + 'weightedAvgPrice' => '0.01043129', + 'openPrice' => '0.01036170', + 'highPrice' => '0.01049850', + 'lowPrice' => '0.01033870', + 'lastPrice' => '0.01044700', + 'volume' => '166.67000000', + 'quoteVolume' => '1.73858301', + 'openTime' => 1655432400000, + 'closeTime' => 1655446835460, + 'firstId' => 2351674, + 'lastId' => 2352034, + 'count' => 361, + 'customAdditional' => [ + 'openTimeDate' => 'Sat, 15 Jul 2023 08:35:54 UTC', + 'closeTimeDate' => 'Sun, 16 Jul 2023 08:35:54 UTC' + ] + ] +] +``` +
diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7c33dcd --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "kleninm/binance-api", + "description": "Binance API for php", + "keywords": ["Binance", "Cryptocurrency", "API"], + "license": "MIT", + "authors": [ + { + "name": "Klenin Maksim", + "email": "maks.klen.99@gmail.com", + "homepage": "https://github.com/kleninmaxim" + } + ], + "require": { + "php": ">=8.2", + "ext-bcmath": "*", + "guzzlehttp/guzzle": "^7.7.0" + }, + "require-dev": { + "phpunit/phpunit": "10.0.19", + "friendsofphp/php-cs-fixer": "^3.21" + }, + "autoload": { + "psr-4": { + "BinanceApi\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "BinanceApi\\Tests\\": "tests/" + } + } +} diff --git a/src/Binance.php b/src/Binance.php new file mode 100644 index 0000000..03e9945 --- /dev/null +++ b/src/Binance.php @@ -0,0 +1,275 @@ + [ + BanBased::IP => [ + 'api' => [ + 'used' => 0, + 'lastRequest' => null, + ], + 'sapi' => [ + 'used' => 0, + 'lastRequest' => null, + ], + ], + BanBased::ACCOUNT => [ + 'count' => 0, + 'lastRequest' => null, + ], + ] + ]; + + /** + * @var BinanceOriginal original binance class to work with Binance API + */ + public readonly BinanceOriginal $binanceOriginal; + + /** + * @var string public api key from binance + */ + protected string $apiKey = ''; + + /** + * @var string private api key from binance + */ + protected string $apiSecret = ''; + + /** + * @var Closure callback function to filter result data + */ + protected Closure $outputCallback; + + /** + * @var bool setting to throw exception or not + */ + protected bool $isThrowException = true; + + /** + * Create object + * + * @param string $endpoint + * @param Client $client + */ + public function __construct( + string $endpoint = GeneralApiInformation::BASE_ENDPOINT, + Client $client = new Client(['timeout' => 2]), + ) { + $this->binanceOriginal = new BinanceOriginal(new CustomResponseHandler(), $endpoint, $client); + } + + /** + * Return additional information + * + * @return array|array[] + */ + public function getAdditional(): array + { + return $this->addition; + } + + /** + * Set api keys as a property to use it + * + * @param string $apiKey + * @param string $apiSecret + * @return void + */ + public function setApiKeys(string $apiKey, string $apiSecret = ''): void + { + $this->apiKey = $apiKey; + $this->apiSecret = $apiSecret; + } + + /** + * Set a callback function to filter result data + * + * @param Closure $outputCallback + * @return void + */ + public function setOutputCallback(Closure $outputCallback): void + { + $this->outputCallback = $outputCallback; + } + + /** + * Disable throw exception for class and response handler + * + * @return void + */ + public function disableBinanceExceptions(): void + { + if ($this->binanceOriginal->responseHandler instanceof CustomResponseHandler) { + $this->binanceOriginal->responseHandler->disableException(); + } + $this->isThrowException = false; + } + + /** + * Original methods and analogs (see phpdoc in this class) + * + * @throws BinanceException|EndpointQueryException|BinanceResponseException + */ + public function __call($name, $arguments): array + { + // Change $name and $arguments to analog + $this->getAnalog($name, $arguments); + + $headers = $query = []; + + // Resolve necessary endpoint + $endpointObject = $this->binanceOriginal->resolveAlias($name); + + // If endpoint secure by api key, then add an api key to header + if (in_array($endpointObject->endpointType, EndpointSecurityType::SECURITY_TYPES_THROW_API_KEY)) { + if (! $this->apiKey) { + $this->throwException('This endpoint {'.$endpointObject->endpoint.'} need api key, you have to set non empty api key in authorize method'); + } + + $headers[EndpointSecurityType::SECURITY_API_KEY_HEADER] = $this->apiKey; + } + + // If endpoint has query parameters, then get a query as an array + if ($endpointObject instanceof HasQueryParameters) { + $query = $endpointObject->getQuery(...$arguments); + } + + // If endpoint secure by api secret, then create a timestamp parameter and signature + if ( + in_array($endpointObject->endpointType, EndpointSecurityType::SECURITY_TYPES_THROW_SIGNATURE) && + $endpointObject->encryption == Signed::SIGNED_SIGNATURE_ALGO + ) { + if (! $this->apiSecret) { + $this->throwException('This endpoint {'.$endpointObject->endpoint.'} need api secret, you have to set non empty api secret in authorize method'); + } + + $query[Signed::SIGNED_TIMING_SECURITY_PARAMETER] = Signed::binanceMicrotime(); + $query[Signed::SIGNED_PARAMETER] = Signed::signature(http_build_query($query), $this->apiSecret); + } + + // Form array to return + $output = [ + 'request' => [ + 'url' => $endpointObject->endpoint, + 'headers' => $headers, + 'query' => $query, + 'body' => $body ?? [], + ], + 'response' => $this->binanceOriginal->$name($headers, $query) + ]; + + // If a response handle is CustomResponseHandler, we know which format return and process it + if ($this->binanceOriginal->responseHandler instanceof CustomResponseHandler) { + $this->updateAddition($output); + } + + // if setOutputCallback call it and return, otherwise is simple return + return (isset($this->outputCallback)) ? ($this->outputCallback)($output) : $output; + } + + /** + * Get analogs of some original function and set it + * + * @param string $name + * @param array $arguments + * @return void + */ + protected function getAnalog(string &$name, array &$arguments): void + { + $name = match ($name) { + 'orderbook' => OrderBook::METHOD, + default => $name, + }; + + if (str_contains($name, 'orderbook')) { + array_unshift($arguments, strtoupper(str_replace('orderbook', '', $name))); + $name = OrderBook::METHOD; + } + } + + /** + * Update additional property after get response + * + * @param array $output + * @return void + */ + protected function updateAddition(array $output): void + { + $headers = $output['response']['info']['headers']; + + $usedWeightHeaderForApi = strtolower(Limits::API_IP_LIMIT_USED_WEIGHT_MINUTE); + if (isset($headers[$usedWeightHeaderForApi])) { + $this->addition['limits'][BanBased::IP]['api']['used'] = $headers[$usedWeightHeaderForApi][0]; + $this->addition['limits'][BanBased::IP]['api']['lastRequest'] = $headers['Date'][0]; + } + + $usedWeightHeaderForSapi = strtolower(Limits::SAPI_IP_LIMIT_USED_WEIGHT_MINUTE); + if (isset($headers[$usedWeightHeaderForSapi])) { + $this->addition['limits'][BanBased::IP]['sapi']['used'] = $headers[$usedWeightHeaderForSapi][0]; + $this->addition['limits'][BanBased::IP]['sapi']['lastRequest'] = $headers['Date'][0]; + } + + $limitOrderCount = strtolower(Limits::LIMIT_ORDER_COUNT_MINUTE); + if (isset($headers[$limitOrderCount])) { + $this->addition['limits'][BanBased::ACCOUNT]['used'] = $headers[$limitOrderCount][0]; + $this->addition['limits'][BanBased::ACCOUNT]['lastRequest'] = $headers['Date'][0]; + } + } + + /** + * Check if enable setting to throw exception then do it + * + * @param string $message + * @return void + */ + protected function throwException(string $message): void + { + if ($this->isThrowException) { + throw new BinanceException($message); + } + } +} diff --git a/src/BinanceAliases.php b/src/BinanceAliases.php new file mode 100644 index 0000000..2c47285 --- /dev/null +++ b/src/BinanceAliases.php @@ -0,0 +1,97 @@ + TestConnectivity::class, + CheckServerTime::METHOD => CheckServerTime::class, + ExchangeInformation::METHOD => ExchangeInformation::class, + OrderBook::METHOD => OrderBook::class, + RecentTradesList::METHOD => RecentTradesList::class, + OldTradeLookup::METHOD => OldTradeLookup::class, + CompressedAggregateTradesList::METHOD => CompressedAggregateTradesList::class, + KlineCandlestickData::METHOD => KlineCandlestickData::class, + UIKlines::METHOD => UIKlines::class, + CurrentAveragePrice::METHOD => CurrentAveragePrice::class, + TickerPriceChangeStatistics24hr::METHOD => TickerPriceChangeStatistics24hr::class, + SymbolPriceTicker::METHOD => SymbolPriceTicker::class, + SymbolOrderBookTicker::METHOD => SymbolOrderBookTicker::class, + RollingWindowPriceChangeStatistics::METHOD => RollingWindowPriceChangeStatistics::class, + ]; + + /** + * @var array Coded created fix Endpoint classes + */ + protected array $resolvedAliases = []; + + /** + * Method to resolve alias + * + * @param string $name name of method from $this->aliases + * @return Endpoint object of readonly Endpoint class + * @throws MethodNotExistException + */ + public function resolveAlias(string $name): Endpoint + { + if (isset($this->resolvedAliases[$name])) { + return $this->resolvedAliases[$name]; + } + + if (! isset($this->aliases[$name])) { + throw new MethodNotExistException('There is no such method or endpoint: '.$name); + } + + return $this->resolvedAliases[$name] ??= new $this->aliases[$name](); + } + + /** + * Method add new alias for endpoint and resolve it + * + * @param string $name + * @param string $endpointClass + * @return void + */ + public function addAlias(string $name, string $endpointClass): void + { + $this->aliases[$name] = $endpointClass; + + $this->resolveAlias($name); + } +} diff --git a/src/BinanceOriginal.php b/src/BinanceOriginal.php new file mode 100644 index 0000000..c1c7859 --- /dev/null +++ b/src/BinanceOriginal.php @@ -0,0 +1,98 @@ +http = new Http($client); + + // if (! in_array($endpoint, GeneralApiInformation::BASE_ENDPOINTS)) { + // throw new BinanceException('There is no such endpoint: {'.$endpoint. '}. Only has: '.implode(', ', GeneralApiInformation::BASE_ENDPOINTS)); + // } + + $this->endpoint = $endpoint; + } + + /** + * Handles all methods described in BinanceAliases + * + * First is resolve alias, do http request and handle this request + * + * @param string $name name of method + * @param array $arguments parameters of this method + * @return mixed handle response getting from binance + */ + public function __call(string $name, array $arguments): mixed + { + $endpointObject = $this->resolveAlias($name); + + return $this->responseHandler->get($this->httpRequest($endpointObject, ...$arguments), $endpointObject); + } + + /** + * Create http request for binance API + * + * @param Endpoint $endpointObject + * @param array $headers http header + * @param array $query http query parameters + * @param array $body http body + * @return ResponseInterface return guzzle ResponseInterface + * @throws Exception + */ + protected function httpRequest( + Endpoint $endpointObject, + array $headers = [], + array $query = [], + array $body = [] + ): ResponseInterface { + // Get full url by endpoint and api endpoint: https://api.binance.com/api/v3/exchangeInfo + $url = $this->endpoint.$endpointObject->endpoint; + + // create http request to binance + $response = $this->http->{strtolower($endpointObject->httpMethod)}($url, $headers, $query, $body); + + // something doesn't work with http request and/or http response + if (is_null($response)) { + throw new Exception('Can\'t made http request to this endpoint: '.$url); + } + + return $response; + } +} diff --git a/src/Docs/Const/BinanceApi/Endpoint.php b/src/Docs/Const/BinanceApi/Endpoint.php new file mode 100644 index 0000000..564692b --- /dev/null +++ b/src/Docs/Const/BinanceApi/Endpoint.php @@ -0,0 +1,26 @@ + Database'; +} diff --git a/src/Docs/GeneralInfo/EndpointSecurityType.php b/src/Docs/GeneralInfo/EndpointSecurityType.php new file mode 100644 index 0000000..5c74bfd --- /dev/null +++ b/src/Docs/GeneralInfo/EndpointSecurityType.php @@ -0,0 +1,35 @@ += minPrice + * - price <= maxPrice + * - price % tickSize == 0 + * + * @param float $price + * @param float $minPrice + * @param float $maxPrice + * @param float $ticketSize + * @return bool + */ + public static function priceFilterRule(float $price, float $minPrice, float $maxPrice, float $ticketSize): bool + { + return + static::priceFilterMinPriceRule($price, $minPrice) && + static::priceFilterMaxPriceRule($price, $maxPrice) && + static::priceFilterTickSizeRule($price, $ticketSize); + } + + /** + * PRICE_FILTER minPrice rule + * + * - price >= minPrice + * + * @param float $price + * @param float $minPrice + * @return bool + */ + public static function priceFilterMinPriceRule(float $price, float $minPrice): bool + { + return $price >= $minPrice || Math::isEqualFloats($price, $minPrice); + } + + /** + * PRICE_FILTER maxPrice rule + * + * - price <= maxPrice + * + * @param float $price + * @param float $maxPrice + * @return bool + */ + public static function priceFilterMaxPriceRule(float $price, float $maxPrice): bool + { + return $price <= $maxPrice || Math::isEqualFloats($price, $maxPrice); + } + + /** + * PRICE_FILTER tickSize rule + * + * - price % tickSize == 0 + * + * @param float $price + * @param float $ticketSize + * @return bool + */ + public static function priceFilterTickSizeRule(float $price, float $ticketSize): bool + { + return Math::isDivisionWithoutRemainder($price, $ticketSize); + } + + /** + * LOT_SIZE|MARKET_LOT_SIZE sum rules + * + * - quantity >= minQty + * - quantity <= maxQty + * - quantity % stepSize == 0 + * + * @param float $quantity + * @param float $minQty + * @param float $maxQty + * @param float $stepSize + * @return bool + */ + public static function lotSizeRule(float $quantity, float $minQty, float $maxQty, float $stepSize): bool + { + return + static::lotSizeMinQtyRule($quantity, $minQty) && + static::lotSizeMaxQtyRule($quantity, $maxQty) && + static::lotSizeStepSizeRule($quantity, $stepSize); + } + + /** + * LOT_SIZE|MARKET_LOT_SIZE minQty rule + * + * - quantity >= minQty + * + * @param float $quantity + * @param float $minQty + * @return bool + */ + public static function lotSizeMinQtyRule(float $quantity, float $minQty): bool + { + return $quantity >= $minQty || Math::isEqualFloats($quantity, $minQty); + } + + /** + * LOT_SIZE|MARKET_LOT_SIZE maxQty rule + * + * - quantity <= maxQty + * + * @param float $quantity + * @param float $maxQty + * @return bool + */ + public static function lotSizeMaxQtyRule(float $quantity, float $maxQty): bool + { + return $quantity <= $maxQty || Math::isEqualFloats($quantity, $maxQty); + } + + /** + * LOT_SIZE|MARKET_LOT_SIZE stepSize rule + * + * - quantity % stepSize == 0 + * + * @param float $quantity + * @param float $stepSize + * @return bool + */ + public static function lotSizeStepSizeRule(float $quantity, float $stepSize): bool + { + return Math::isDivisionWithoutRemainder($quantity, $stepSize); + } +} diff --git a/src/Docs/GeneralInfo/GeneralApiInformation.php b/src/Docs/GeneralInfo/GeneralApiInformation.php new file mode 100644 index 0000000..0dd587a --- /dev/null +++ b/src/Docs/GeneralInfo/GeneralApiInformation.php @@ -0,0 +1,37 @@ + 'base asset', 'quoteAsset' => 'quote asset']; + + + /** + * Symbol status (status): + */ + public const SYMBOL_STATUS_PRE_TRADING = 'PRE_TRADING'; + public const SYMBOL_STATUS_TRADING = 'TRADING'; + public const SYMBOL_STATUS_POST_TRADING = 'POST_TRADING'; + public const SYMBOL_STATUS_END_OF_DAY = 'END_OF_DAY'; + public const SYMBOL_STATUS_HALT = 'HALT'; + public const SYMBOL_STATUS_AUCTION_MATCH = 'AUCTION_MATCH'; + public const SYMBOL_STATUS_BREAK = 'BREAK'; + + public const SYMBOL_STATUSES = [ + self::SYMBOL_STATUS_PRE_TRADING, + self::SYMBOL_STATUS_TRADING, + self::SYMBOL_STATUS_POST_TRADING, + self::SYMBOL_STATUS_END_OF_DAY, + self::SYMBOL_STATUS_HALT, + self::SYMBOL_STATUS_AUCTION_MATCH, + self::SYMBOL_STATUS_BREAK, + ]; + + + /** + * Account and Symbol Permissions (permissions): + */ + public const ACCOUNT_AND_SYMBOL_PERMISSION_SPOT = 'ACCOUNT_AND_SYMBOL_PERMISSION_SPOT'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_MARGIN = 'ACCOUNT_AND_SYMBOL_PERMISSION_MARGIN'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_LEVERAGED = 'ACCOUNT_AND_SYMBOL_PERMISSION_LEVERAGED'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_002 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_002'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_003 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_003'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_004 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_004'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_005 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_005'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_006 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_006'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_007 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_007'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_008 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_008'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_009 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_009'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_010 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_010'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_011 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_011'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_012 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_012'; + public const ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_013 = 'ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_013'; + + public const ACCOUNT_AND_SYMBOL_PERMISSIONS = [ + self::ACCOUNT_AND_SYMBOL_PERMISSION_SPOT, + self::ACCOUNT_AND_SYMBOL_PERMISSION_MARGIN, + self::ACCOUNT_AND_SYMBOL_PERMISSION_LEVERAGED, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_002, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_003, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_004, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_005, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_006, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_007, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_008, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_009, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_010, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_011, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_012, + self::ACCOUNT_AND_SYMBOL_PERMISSION_TRD_GRP_013, + ]; + + + /** + * Order status (status): + */ + public const ORDER_STATUS_NEW = 'NEW'; + public const ORDER_STATUS_PARTIALLY_FILLED = 'PARTIALLY_FILLED'; + public const ORDER_STATUS_FILLED = 'FILLED'; + public const ORDER_STATUS_CANCELED = 'CANCELED'; + public const ORDER_STATUS_PENDING_CANCEL = 'PENDING_CANCEL'; + public const ORDER_STATUS_REJECTED = 'REJECTED'; + public const ORDER_STATUS_EXPIRED = 'EXPIRED'; + public const ORDER_STATUS_EXPIRED_IN_MATCH = 'EXPIRED_IN_MATCH'; + + public const ORDER_STATUSES = [ + self::ORDER_STATUS_NEW => 'The order has been accepted by the engine.', + self::ORDER_STATUS_PARTIALLY_FILLED => 'A part of the order has been filled.', + self::ORDER_STATUS_FILLED => 'The order has been completed.', + self::ORDER_STATUS_CANCELED => 'The order has been canceled by the user.', + self::ORDER_STATUS_PENDING_CANCEL => 'Currently unused', + self::ORDER_STATUS_REJECTED => 'The order was not accepted by the engine and not processed.', + self::ORDER_STATUS_EXPIRED => 'The order was canceled according to the order type\'s rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance)', + self::ORDER_STATUS_EXPIRED_IN_MATCH => 'The order was canceled by the exchange due to STP trigger. (e.g. an order with EXPIRE_TAKER will match with existing orders on the book with the same account or same tradeGroupId)', + ]; + + + /** + * OCO Status (listStatusType): + */ + public const OCO_STATUS_RESPONSE = 'RESPONSE'; + public const OCO_STATUS_EXEC_STARTED = 'EXEC_STARTED'; + public const OCO_STATUS_ALL_DONE = 'ALL_DONE'; + + public const OCO_STATUSES = [ + self::OCO_STATUS_RESPONSE => 'This is used when the ListStatus is responding to a failed action. (E.g. Orderlist placement or cancellation)', + self::OCO_STATUS_EXEC_STARTED => 'The order list has been placed or there is an update to the order list status.', + self::OCO_STATUS_ALL_DONE => 'The order list has finished executing and thus no longer active.', + ]; + + + /** + * OCO Order Status (listOrderStatus): + */ + public const OCO_ORDER_STATUS_EXECUTING = 'EXECUTING'; + public const OCO_ORDER_STATUS_ALL_DONE = 'ALL_DONE'; + public const OCO_ORDER_STATUS_REJECT = 'REJECT'; + + public const OCO_ORDER_STATUSES = [ + self::OCO_ORDER_STATUS_EXECUTING => 'Either an order list has been placed or there is an update to the status of the list.', + self::OCO_ORDER_STATUS_ALL_DONE => 'An order list has completed execution and thus no longer active.', + self::OCO_ORDER_STATUS_REJECT => 'The List Status is responding to a failed action either during order placement or order canceled.)', + ]; + + + /** + * ContingencyType + */ + public const CONTINGENCY_TYPE= 'ContingencyType'; + + + /** + * Order types (orderTypes, type): + */ + public const ORDER_TYPE_LIMIT = 'LIMIT'; + public const ORDER_TYPE_MARKET = 'MARKET'; + public const ORDER_TYPE_STOP_LOSS = 'STOP_LOSS'; + public const ORDER_TYPE_STOP_LOSS_LIMIT = 'STOP_LOSS_LIMIT'; + public const ORDER_TYPE_TAKE_PROFIT = 'TAKE_PROFIT'; + public const ORDER_TYPE_TAKE_PROFIT_LIMIT = 'TAKE_PROFIT_LIMIT'; + public const ORDER_TYPE_LIMIT_MAKER = 'LIMIT_MAKER'; + + public const ORDER_TYPES = [ + self::ORDER_TYPE_LIMIT, + self::ORDER_TYPE_MARKET, + self::ORDER_TYPE_STOP_LOSS, + self::ORDER_TYPE_STOP_LOSS_LIMIT, + self::ORDER_TYPE_TAKE_PROFIT, + self::ORDER_TYPE_TAKE_PROFIT_LIMIT, + self::ORDER_TYPE_LIMIT_MAKER, + ]; + + + /** + * Order Response Type (newOrderRespType): + */ + public const ORDER_RESPONSE_TYPE_ACK = 'ACK'; + public const ORDER_RESPONSE_TYPE_RESULT = 'RESULT'; + public const ORDER_RESPONSE_TYPE_FULL = 'FULL'; + + public const ORDER_RESPONSE_TYPES = [ + self::ORDER_RESPONSE_TYPE_ACK, + self::ORDER_RESPONSE_TYPE_RESULT, + self::ORDER_RESPONSE_TYPE_FULL, + ]; + + + /** + * Order side (side): + */ + public const ORDER_SIDE_BUY = 'BUY'; + public const ORDER_SIDE_SELL = 'SELL'; + + public const ORDER_SIDES = [ + self::ORDER_SIDE_BUY, + self::ORDER_SIDE_SELL, + ]; + + + /** + * Order Response Type (newOrderRespType): + */ + public const TIME_IN_FORCE_GTC = 'GTC'; + public const TIME_IN_FORCE_IOC = 'IOC'; + public const TIME_IN_FORCE_FOK = 'FOK'; + + public const TIME_IN_FORCES = [ + self::TIME_IN_FORCE_GTC => 'Good Til Canceled. An order will be on the book unless the order is canceled.', + self::TIME_IN_FORCE_IOC => 'Immediate Or Cancel. An order will try to fill the order as much as it can before the order expires.', + self::TIME_IN_FORCE_FOK => 'Fill or Kill. An order will expire if the full order cannot be filled upon execution.', + ]; + + + /** + * Kline/Candlestick chart intervals: + */ + public const KLINE_CHART_INTERVAL_ABBREVIATION = [ + 's' => 'seconds', + 'm' => 'minutes', + 'h' => 'hours', + 'd' => 'days', + 'w' => 'weeks', + 'M' => 'months', + ]; + + public const KLINE_CHART_INTERVAL_ONE_SECOND = '1s'; + public const KLINE_CHART_INTERVAL_ONE_MINUTE = '1m'; + public const KLINE_CHART_INTERVAL_THREE_MINUTES = '3m'; + public const KLINE_CHART_INTERVAL_FIVE_MINUTES = '5m'; + public const KLINE_CHART_INTERVAL_FIFTEEN_MINUTES = '15m'; + public const KLINE_CHART_INTERVAL_THIRTY_MINUTES = '30m'; + public const KLINE_CHART_INTERVAL_ONE_HOUR = '1h'; + public const KLINE_CHART_INTERVAL_TWO_HOURS = '2h'; + public const KLINE_CHART_INTERVAL_FOUR_HOURS = '4h'; + public const KLINE_CHART_INTERVAL_SIX_HOURS = '6h'; + public const KLINE_CHART_INTERVAL_EIGHT_HOURS = '8h'; + public const KLINE_CHART_INTERVAL_TWELVE_HOURS = '12h'; + public const KLINE_CHART_INTERVAL_ONE_DAY = '1d'; + public const KLINE_CHART_INTERVAL_THREE_DAYS = '3d'; + public const KLINE_CHART_INTERVAL_ONE_WEEK = '1w'; + public const KLINE_CHART_INTERVAL_ONE_MONTH = '1M'; + + public const KLINE_CHART_INTERVALS = [ + self::KLINE_CHART_INTERVAL_ONE_SECOND, + self::KLINE_CHART_INTERVAL_ONE_MINUTE, + self::KLINE_CHART_INTERVAL_THREE_MINUTES, + self::KLINE_CHART_INTERVAL_FIVE_MINUTES, + self::KLINE_CHART_INTERVAL_FIFTEEN_MINUTES, + self::KLINE_CHART_INTERVAL_THIRTY_MINUTES, + self::KLINE_CHART_INTERVAL_ONE_HOUR, + self::KLINE_CHART_INTERVAL_TWO_HOURS, + self::KLINE_CHART_INTERVAL_FOUR_HOURS, + self::KLINE_CHART_INTERVAL_SIX_HOURS, + self::KLINE_CHART_INTERVAL_EIGHT_HOURS, + self::KLINE_CHART_INTERVAL_TWELVE_HOURS, + self::KLINE_CHART_INTERVAL_ONE_DAY, + self::KLINE_CHART_INTERVAL_THREE_DAYS, + self::KLINE_CHART_INTERVAL_ONE_WEEK, + self::KLINE_CHART_INTERVAL_ONE_MONTH, + ]; + + + /** + * Rate limiters (rateLimitType) + */ + public const RATE_LIMITER_REQUEST_WEIGHT = 'REQUEST_WEIGHT'; + public const RATE_LIMITER_ORDERS = 'ORDERS'; + public const RATE_LIMITER_RAW_REQUESTS = 'RAW_REQUESTS'; + + public const RATE_LIMITERS = [ + self::RATE_LIMITER_REQUEST_WEIGHT, + self::RATE_LIMITER_ORDERS, + self::RATE_LIMITER_RAW_REQUESTS, + ]; + + + /** + * Rate limit intervals (interval) + */ + public const RATE_LIMITER_INTERVAL_SECOND = 'SECOND'; + public const RATE_LIMITER_INTERVAL_MINUTE = 'MINUTE'; + public const RATE_LIMITER_INTERVAL_DAY = 'DAY'; + + public const RATE_LIMITER_INTERVALS = [ + self::RATE_LIMITER_INTERVAL_SECOND, + self::RATE_LIMITER_INTERVAL_MINUTE, + self::RATE_LIMITER_INTERVAL_DAY, + ]; +} diff --git a/src/Docs/GeneralInfo/Signed.php b/src/Docs/GeneralInfo/Signed.php new file mode 100644 index 0000000..aa2aed9 --- /dev/null +++ b/src/Docs/GeneralInfo/Signed.php @@ -0,0 +1,132 @@ + 'LTCBTC', + 'side' => 'BUY', + 'type' => 'side', + 'timeInForce' => 'GTC', + 'quantity' => 1, + 'price' => 0.1, + 'recvWindow' => 5000, + 'timestamp' => 1499827319559, + ]; + + $totalParams = http_build_query($parameters); + + return [ + 'apiKey' => $apiKey, + 'secretKey' => $secretKey, + 'parameters' => $parameters, + 'totalParams' => $totalParams, + 'signature' => static::signature($totalParams, $secretKey), + ]; + } + + public static function exampleSignatureQueryString(): array + { + $apiKey = 'vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A'; + $secretKey = 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j'; + + $parameters = [ + 'symbol' => 'LTCBTC', + 'side' => 'BUY', + 'type' => 'side', + 'timeInForce' => 'GTC', + 'quantity' => 1, + 'price' => 0.1, + 'recvWindow' => 5000, + 'timestamp' => 1499827319559, + ]; + + $totalParams = http_build_query($parameters); + + return [ + 'apiKey' => $apiKey, + 'secretKey' => $secretKey, + 'parameters' => $parameters, + 'totalParams' => $totalParams, + 'signature' => static::signature($totalParams, $secretKey), + ]; + } + + public static function exampleMixedQueryStringAndRequestBody(): array + { + $apiKey = 'vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A'; + $secretKey = 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j'; + + $parameters = [ + 'queryString' => [ + 'symbol' => 'LTCBTC', + 'side' => 'BUY', + 'type' => 'side', + 'timeInForce' => 'GTC', + ], + 'requestBody' => [ + 'quantity' => 1, + 'price' => 0.1, + 'recvWindow' => 5000, + 'timestamp' => 1499827319559, + ], + ]; + + $totalParamsQueryString = http_build_query($parameters['queryString']); + $totalParamsRequestBody = http_build_query($parameters['requestBody']); + + return [ + 'apiKey' => $apiKey, + 'secretKey' => $secretKey, + 'parameters' => $parameters, + 'totalParamsQueryString' => $totalParamsQueryString, + 'totalParamsRequestBody' => $totalParamsRequestBody, + 'signature' => static::signatureMixedQueryStringAndRequestBody( + $totalParamsQueryString, + $totalParamsRequestBody, + $secretKey + ), + ]; + } + + // TODO: add SIGNED Endpoint Example for POST /api/v3/order - RSA Keys (https://binance-docs.github.io/apidocs/spot/en/#signed-trade-user_data-and-margin-endpoint-security) +} diff --git a/src/Docs/Introduction/TestNet.php b/src/Docs/Introduction/TestNet.php new file mode 100644 index 0000000..5b113bf --- /dev/null +++ b/src/Docs/Introduction/TestNet.php @@ -0,0 +1,8 @@ + $response['serverTime'], + ProcessResponse::ADDITIONAL_FIELD => [ + 'serverTimeDate' => Carbon::getFullDate($response['serverTime']), + ], + ]; + } + + public static function exampleResponse(): string + { + return json_encode(['serverTime' => 1499827319559]); + } +} diff --git a/src/Docs/MarketDataEndpoint/CompressedAggregateTradesList.php b/src/Docs/MarketDataEndpoint/CompressedAggregateTradesList.php new file mode 100644 index 0000000..a4ff968 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/CompressedAggregateTradesList.php @@ -0,0 +1,109 @@ + 1000) { + $limit = 1000; + } + + $query = ['symbol' => $symbol, 'limit' => $limit]; + + if ($fromId) { + $query['fromId'] = $fromId; + } + + if ($startTime) { + $query['startTime'] = $startTime; + } + + if ($endTime) { + $query['endTime'] = $endTime; + } + + return $query; + } + + public function processResponse(array $response): array + { + return array_map(function ($item) { + return [ + 'aggregateTradeId' => $item['a'], + 'price' => $item['p'], + 'quantity' => $item['q'], + 'firstTradeId' => $item['f'], + 'lastTradeId' => $item['l'], + 'timestamp' => $item['T'], + 'wasTheBuyerTheMaker' => $item['m'], + 'wasTheTradeTheBestPriceMatch' => $item['M'], + ProcessResponse::ADDITIONAL_FIELD => [ + 'timestampDate' => Carbon::getFullDate($item['T']), + ], + ]; + }, $response); + } + + public static function exampleResponse(): string + { + return json_encode([ + [ + 'a' => 26129, + 'p' => "0.01633102", + 'q' => "4.70443515", + 'f' => 27781, + 'l' => 27781, + 'T' => 1498793709153, + 'm' => true, + 'M' => true, + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/CurrentAveragePrice.php b/src/Docs/MarketDataEndpoint/CurrentAveragePrice.php new file mode 100644 index 0000000..0912720 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/CurrentAveragePrice.php @@ -0,0 +1,53 @@ + $symbol]; + } + + public static function exampleResponse(): string + { + return json_encode([ + 'mins' => 5, + 'price' => '9.35751834', + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/ExchangeInformation.php b/src/Docs/MarketDataEndpoint/ExchangeInformation.php new file mode 100644 index 0000000..bd34abc --- /dev/null +++ b/src/Docs/MarketDataEndpoint/ExchangeInformation.php @@ -0,0 +1,187 @@ + 'UTC', + 'serverTime' => 1565246363776, + 'rateLimits' => [ + [ + 'rateLimitType' => 'REQUEST_WEIGHT', + 'interval' => 'MINUTE', + 'intervalNum' => 1, + 'limit' => 1200, + ], + [ + 'rateLimitType' => 'ORDERS', + 'interval' => 'SECOND', + 'intervalNum' => 10, + 'limit' => 50, + ], + [ + 'rateLimitType' => 'ORDERS', + 'interval' => 'DAY', + 'intervalNum' => 1, + 'limit' => 160000, + ], + [ + 'rateLimitType' => 'RAW_REQUESTS', + 'interval' => 'MINUTE', + 'intervalNum' => 5, + 'limit' => 6100, + ], + ], + 'exchangeFilters' => [], + 'symbols' => [ + [ + 'symbol' => 'ETHBTC', + 'status' => 'TRADING', + 'baseAsset' => 'ETH', + 'baseAssetPrecision' => 8, + 'quoteAsset' => 'BTC', + 'quotePrecision' => 8, + 'quoteAssetPrecision' => 8, + 'orderTypes' => [ + 'LIMIT', + 'LIMIT_MAKER', + 'MARKET', + 'STOP_LOSS', + 'STOP_LOSS_LIMIT', + 'TAKE_PROFIT', + 'TAKE_PROFIT_LIMIT' + ], + 'icebergAllowed' => true, + 'ocoAllowed' => true, + 'quoteOrderQtyMarketAllowed' => true, + 'allowTrailingStop' => false, + 'cancelReplaceAllowed' => false, + 'isSpotTradingAllowed' => true, + 'isMarginTradingAllowed' => true, + 'filters' => [ + [ + 'filterType' => 'PRICE_FILTER', + 'minPrice' => 0.00000100, + 'maxPrice' => 100.00000000, + 'tickSize' => 0.00000100, + ], + [ + 'filterType' => 'LOT_SIZE', + 'minPrice' => 0.00000100, + 'maxPrice' => 9000.00000000, + 'stepSize' => 0.00001000, + ], + [ + 'filterType' => 'ICEBERG_PARTS', + 'limit' => 10, + ], + [ + 'filterType' => 'MARKET_LOT_SIZE', + 'minPrice' => 0.00000000, + 'maxPrice' => 1000.00000000, + 'stepSize' => 0.00000000, + ], + [ + 'filterType' => 'TRAILING_DELTA', + 'minTrailingAboveDelta' => 10, + 'maxTrailingAboveDelta' => 2000, + 'minTrailingBelowDelta' => 10, + 'maxTrailingBelowDelta' => 2000, + ], + [ + 'filterType' => 'PERCENT_PRICE_BY_SIDE', + 'bidMultiplierUp' => 5, + 'bidMultiplierDown' => 0.2, + 'askMultiplierUp' => 5, + 'askMultiplierDown' => 0.2, + ], + [ + 'filterType' => 'NOTIONAL', + 'minNotional' => 0.00010000, + 'applyMinToMarket' => 1, + 'maxNotional' => 9000000.00000000, + 'applyMaxToMarket' => '', + 'avgPriceMins' => 1, + ], + [ + 'filterType' => 'MAX_NUM_ORDERS', + 'maxNumOrders' => 200, + ], + [ + 'filterType' => 'MAX_NUM_ALGO_ORDERS', + 'maxNumAlgoOrders' => 5, + ], + ], + 'permissions' => ['SPOT', 'MARGIN'], + 'defaultSelfTradePreventionMode' => 'NONE', + 'allowedSelfTradePreventionModes' => ['NONE'], + ] + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/KlineCandlestickData.php b/src/Docs/MarketDataEndpoint/KlineCandlestickData.php new file mode 100644 index 0000000..e9e068c --- /dev/null +++ b/src/Docs/MarketDataEndpoint/KlineCandlestickData.php @@ -0,0 +1,117 @@ + 1000) { + $limit = 1000; + } + + $query = ['symbol' => $symbol, 'interval' => $interval, 'limit' => $limit]; + + if ($startTime) { + $query['startTime'] = $startTime; + } + + if ($endTime) { + $query['endTime'] = $endTime; + } + + return $query; + } + + public function processResponse(array $response): array + { + return array_map(function ($item) { + return [ + 'klineOpenTime' => $item[0], + 'openPrice' => $item[1], + 'highPrice' => $item[2], + 'lowPrice' => $item[3], + 'closePrice' => $item[4], + 'volume' => $item[5], + 'klineCloseTime' => $item[6], + 'quoteAssetVolume' => $item[7], + 'numberOfTrades' => $item[8], + 'takerBuyBaseAssetVolume' => $item[9], + 'takerBuyQuoteAssetVolume' => $item[10], + 'unusedField' => $item[11], + ProcessResponse::ADDITIONAL_FIELD => [ + 'klineOpenTimeDate' => Carbon::getFullDate($item[0]), + 'klineCloseTimeDate' => Carbon::getFullDate($item[6]), + ], + ]; + }, $response); + } + + public static function exampleResponse(): string + { + return json_encode([ + [ + 1499040000000, + '0.01634790', + '0.80000000', + '0.01575800', + '0.01577100', + '148976.11427815', + 1499644799999, + '2434.19055334', + 308, + '1756.87402397', + '28.46694368', + '0', + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/OldTradeLookup.php b/src/Docs/MarketDataEndpoint/OldTradeLookup.php new file mode 100644 index 0000000..a5cb584 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/OldTradeLookup.php @@ -0,0 +1,87 @@ + 1000) { + $limit = 1000; + } + + $query = ['symbol' => $symbol, 'limit' => $limit]; + + if ($fromId) { + $query['fromId'] = $fromId; + } + + return $query; + } + + public function processResponse(array $response): array + { + return array_map(function ($item) { + $item[ProcessResponse::ADDITIONAL_FIELD]['timeDate'] = Carbon::getFullDate($item['time']); + + return $item; + }, $response); + } + + public static function exampleResponse(): string + { + return json_encode([ + [ + 'id' => 28457, + 'price' => '4.00000100', + 'qty' => '12.00000000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/OrderBook.php b/src/Docs/MarketDataEndpoint/OrderBook.php new file mode 100644 index 0000000..d7e1939 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/OrderBook.php @@ -0,0 +1,92 @@ + 5000) { + $limit = 5000; + } + + return ['symbol' => $symbol, 'limit' => $limit]; + } + + public function processResponse(array $response): array + { + $response['bids'] = array_map( + function ($bid) { + [$price, $amount] = $bid; + return ['price' => $price, 'amount' => $amount]; + }, + $response['bids'] + ); + + $response['asks'] = array_map( + function ($ask) { + [$price, $amount] = $ask; + return ['price' => $price, 'amount' => $amount]; + }, + $response['asks'] + ); + + return $response; + } + + public static function exampleResponse(): string + { + return json_encode([ + 'lastUpdateId' => 1027024, + 'bids' => [ + [ + '4.00000000', + '431.00000000', + ], + ], + 'asks' => [ + [ + '4.00000200', + '12.00000000', + ], + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/RecentTradesList.php b/src/Docs/MarketDataEndpoint/RecentTradesList.php new file mode 100644 index 0000000..bee9e67 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/RecentTradesList.php @@ -0,0 +1,78 @@ + 1000) { + $limit = 1000; + } + + return ['symbol' => $symbol, 'limit' => $limit]; + } + + public function processResponse(array $response): array + { + return array_map(function ($item) { + $item[ProcessResponse::ADDITIONAL_FIELD]['timeDate'] = Carbon::getFullDate($item['time']); + + return $item; + }, $response); + } + + public static function exampleResponse(): string + { + return json_encode([ + [ + 'id' => 28457, + 'price' => '4.00000100', + 'qty' => '12.00000000', + 'quoteQty' => '48.000012', + 'time' => 1499865549590, + 'isBuyerMaker' => true, + 'isBestMatch' => true + ] + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/RollingWindowPriceChangeStatistics.php b/src/Docs/MarketDataEndpoint/RollingWindowPriceChangeStatistics.php new file mode 100644 index 0000000..7752f65 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/RollingWindowPriceChangeStatistics.php @@ -0,0 +1,142 @@ + $windowSize, 'type' => $type]; + + if (!$symbol && !$symbols) { + throw new EndpointQueryException('Symbol or Symbols is mandatory parameter: https://binance-docs.github.io/apidocs/spot/en/#rolling-window-price-change-statistics'); + } + + if ($symbol) { + $query['symbol'] = $symbol; + } + + if ($symbols) { + $query['symbols'] = '["'.implode('","', $symbols).'"]'; + } + + return $query; + } + + public function processResponse(array $response): array + { + if (isset($response[0])) { + return array_map(function ($item) { + $item[ProcessResponse::ADDITIONAL_FIELD] = [ + 'openTimeDate' => Carbon::getFullDate($item['openTime']), + 'closeTimeDate' => Carbon::getFullDate($item['closeTime']), + ]; + + return $item; + }, $response); + } + + $response[ProcessResponse::ADDITIONAL_FIELD] = [ + 'openTimeDate' => Carbon::getFullDate($response['openTime']), + 'closeTimeDate' => Carbon::getFullDate($response['closeTime']), + ]; + + return $response; + } + + public static function exampleResponse(): array + { + return [ + 'firstVersion' => json_encode([ + 'symbol' => 'BNBBTC', + 'priceChange' => '-8.00000000', + 'priceChangePercent' => '-88.889', + 'weightedAvgPrice' => '2.60427807', + 'openPrice' => '9.00000000', + 'highPrice' => '9.00000000', + 'lowPrice' => '1.00000000', + 'lastPrice' => '1.00000000', + 'volume' => '187.00000000', + 'quoteVolume' => '487.00000000', + 'openTime' => 1641859200000, + 'closeTime' => 1642031999999, + 'firstId' => 0, + 'lastId' => 60, + 'count' => 61, + ]), + 'secondVersion' => json_encode([ + [ + 'symbol' => 'BTCUSDT', + 'priceChange' => '-154.13000000', + 'priceChangePercent' => '-0.740', + 'weightedAvgPrice' => '20677.46305250', + 'openPrice' => '20825.27000000', + 'highPrice' => '20972.46000000', + 'lowPrice' => '20327.92000000', + 'lastPrice' => '20671.14000000', + 'volume' => '72.65112300', + 'quoteVolume' => '1502240.91155513', + 'openTime' => 1655432400000, + 'closeTime' => 1655446835460, + 'firstId' => 11147809, + 'lastId' => 11149775, + 'count' => 1967, + ], + [ + 'symbol' => 'BNBBTC', + 'priceChange' => '0.00008530', + 'priceChangePercent' => '0.823', + 'weightedAvgPrice' => '0.01043129', + 'openPrice' => '0.01036170', + 'highPrice' => '0.01049850', + 'lowPrice' => '0.01033870', + 'lastPrice' => '0.01044700', + 'volume' => '166.67000000', + 'quoteVolume' => '1.73858301', + 'openTime' => 1655432400000, + 'closeTime' => 1655446835460, + 'firstId' => 2351674, + 'lastId' => 2352034, + 'count' => 361, + ], + ]), + ]; + } +} diff --git a/src/Docs/MarketDataEndpoint/SymbolOrderBookTicker.php b/src/Docs/MarketDataEndpoint/SymbolOrderBookTicker.php new file mode 100644 index 0000000..1ad5824 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/SymbolOrderBookTicker.php @@ -0,0 +1,80 @@ + json_encode([ + 'symbol' => 'LTCBTC', + 'bidPrice' => '4.00000000', + 'bidQty' => '431.00000000', + 'askPrice' => '4.00000200', + 'askQty' => '9.00000000', + ]), + 'secondVersion' => json_encode([ + [ + 'symbol' => 'LTCBTC', + 'bidPrice' => '4.00000000', + 'bidQty' => '431.00000000', + 'askPrice' => '4.00000200', + 'askQty' => '9.00000000', + ], + [ + 'symbol' => 'ETHBTC', + 'bidPrice' => '0.07946700', + 'bidQty' => '9.00000000', + 'askPrice' => '100000.00000000', + 'askQty' => '1000.00000000', + ], + ]), + ]; + } +} diff --git a/src/Docs/MarketDataEndpoint/SymbolPriceTicker.php b/src/Docs/MarketDataEndpoint/SymbolPriceTicker.php new file mode 100644 index 0000000..1ac16b7 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/SymbolPriceTicker.php @@ -0,0 +1,71 @@ + json_encode([ + 'symbol' => 'LTCBTC', + 'price' => '4.00000200', + ]), + 'secondVersion' => json_encode([ + [ + 'symbol' => 'LTCBTC', + 'price' => '4.00000200', + ], + [ + 'symbol' => 'ETHBTC', + 'price' => '0.07946600', + ], + ]), + ]; + } +} diff --git a/src/Docs/MarketDataEndpoint/TestConnectivity.php b/src/Docs/MarketDataEndpoint/TestConnectivity.php new file mode 100644 index 0000000..53f21b8 --- /dev/null +++ b/src/Docs/MarketDataEndpoint/TestConnectivity.php @@ -0,0 +1,39 @@ + $type]; + + if ($symbol) { + $query['symbol'] = $symbol; + } + + if ($symbols) { + $query['symbols'] = '["' . implode('","', $symbols) . '"]'; + } + + return $query; + } + + public function processResponse(array $response): array + { + if (isset($response[0])) { + return array_map(function ($item) { + $item[ProcessResponse::ADDITIONAL_FIELD] = [ + 'openTimeDate' => Carbon::getFullDate($item['openTime']), + 'closeTimeDate' => Carbon::getFullDate($item['closeTime']), + ]; + + return $item; + }, $response); + } + + $response[ProcessResponse::ADDITIONAL_FIELD] = [ + 'openTimeDate' => Carbon::getFullDate($response['openTime']), + 'closeTimeDate' => Carbon::getFullDate($response['closeTime']), + ]; + + return $response; + } + + public static function exampleResponse(): string + { + return json_encode([ + 'symbol' => 'BNBBTC', + 'priceChange' => '-94.99999800', + 'priceChangePercent' => '-95.960', + 'weightedAvgPrice' => '0.29628482', + 'prevClosePrice' => '0.10002000', + 'lastPrice' => '4.00000200', + 'lastQty' => '200.00000000', + 'bidPrice' => '4.00000000', + 'bidQty' => '100.00000000', + 'askPrice' => '4.00000200', + 'askQty' => '100.00000000', + 'openPrice' => '99.00000000', + 'highPrice' => '100.00000000', + 'lowPrice' => '0.10000000', + 'volume' => '8913.30000000', + 'quoteVolume' => '15.30000000', + 'openTime' => 1499783499040, + 'closeTime' => 1499869899040, + 'firstId' => 28385, + 'lastId' => 28460, + 'count' => 76, + ]); + } +} diff --git a/src/Docs/MarketDataEndpoint/UIKlines.php b/src/Docs/MarketDataEndpoint/UIKlines.php new file mode 100644 index 0000000..37c96cd --- /dev/null +++ b/src/Docs/MarketDataEndpoint/UIKlines.php @@ -0,0 +1,117 @@ + 1000) { + $limit = 1000; + } + + $query = ['symbol' => $symbol, 'interval' => $interval, 'limit' => $limit]; + + if ($startTime) { + $query['startTime'] = $startTime; + } + + if ($endTime) { + $query['endTime'] = $endTime; + } + + return $query; + } + + public function processResponse(array $response): array + { + return array_map(function ($item) { + return [ + 'klineOpenTime' => $item[0], + 'openPrice' => $item[1], + 'highPrice' => $item[2], + 'lowPrice' => $item[3], + 'closePrice' => $item[4], + 'volume' => $item[5], + 'klineCloseTime' => $item[6], + 'quoteAssetVolume' => $item[7], + 'numberOfTrades' => $item[8], + 'takerBuyBaseAssetVolume' => $item[9], + 'takerBuyQuoteAssetVolume' => $item[10], + 'unusedField' => $item[11], + ProcessResponse::ADDITIONAL_FIELD => [ + 'klineOpenTimeDate' => Carbon::getFullDate($item[0]), + 'klineCloseTimeDate' => Carbon::getFullDate($item[6]), + ], + ]; + }, $response); + } + + public static function exampleResponse(): string + { + return json_encode([ + [ + 1499040000000, + '0.01634790', + '0.80000000', + '0.01575800', + '0.01577100', + '148976.11427815', + 1499644799999, + '2434.19055334', + 308, + '1756.87402397', + '28.46694368', + '0', + ] + ]); + } +} diff --git a/src/Exception/BinanceException.php b/src/Exception/BinanceException.php new file mode 100644 index 0000000..001ee53 --- /dev/null +++ b/src/Exception/BinanceException.php @@ -0,0 +1,9 @@ +setTimezone(new DateTimeZone($timezone)) + ->format('D, d M Y H:i:s').' '.$timezone; + } +} diff --git a/src/Helper/Http.php b/src/Helper/Http.php new file mode 100644 index 0000000..cce6329 --- /dev/null +++ b/src/Helper/Http.php @@ -0,0 +1,58 @@ + $this->request(HttpMethod::GET, ...$arguments), + 'post' => $this->request(HttpMethod::POST, ...$arguments), + 'put' => $this->request(HttpMethod::PUT, ...$arguments), + 'delete' => $this->request(HttpMethod::DELETE, ...$arguments), + default => throw new Exception('There is no such method: {'.$name.'}'), + }; + } + + /** + * @throws GuzzleException + */ + protected function request(string $method, string $url, array $headers = [], array $query = [], array $body = []): ?ResponseInterface + { + $options = []; + + if (! empty($headers)) { + $options['headers'] = $headers; + } + + if (in_array($method, [HttpMethod::GET, HttpMethod::POST]) && ! empty($query)) { + $options['query'] = $query; + } + + if (in_array($method, [HttpMethod::POST, HttpMethod::PUT, HttpMethod::DELETE]) && ! empty($body)) { + $options['form_params'] = $body; + } + + return $this->client->request($method, $url, $options ?? []); + } +} diff --git a/src/Helper/Math.php b/src/Helper/Math.php new file mode 100644 index 0000000..47f25fc --- /dev/null +++ b/src/Helper/Math.php @@ -0,0 +1,45 @@ +getBody()->getContents(), true); + + if ($this->isThrowException) { + $this->handleError($data); + } + + return [ + 'data' => ($endpoint instanceof ProcessResponse && ! $this->isErrorMessage($data)) ? $endpoint->processResponse($data) : $data, + 'info' => [ + 'statusCode' => $response->getStatusCode(), + 'reasonPhrase' => $response->getReasonPhrase(), + 'headers' => $response->getHeaders(), + ], + ]; + } + + /** + * Method to disable throw exception + * + * @return void + */ + public function disableException(): void + { + $this->isThrowException = false; + } + + /** + * Handle response to check is error message and if it does then throw exception + * + * @throws BinanceResponseException + */ + protected function handleError(array $data): void + { + if ($this->isErrorMessage($data)) { + throw new BinanceResponseException( + $data[GeneralApiInformation::ERROR_CODE_AND_MESSAGES[1]], + $data[GeneralApiInformation::ERROR_CODE_AND_MESSAGES[0]] + ); + } + } + + /** + * Check that response data is error message or not + * + * @param array $data + * @return bool + */ + protected function isErrorMessage(array $data): bool + { + foreach (GeneralApiInformation::ERROR_CODE_AND_MESSAGES as $errorCode) { + if (! isset($data[$errorCode])) { + return false; + } + } + + return count(GeneralApiInformation::ERROR_CODE_AND_MESSAGES) == count($data); + } +} diff --git a/src/Helper/ResponseHandler/GuzzlePsr7ResponseHandler.php b/src/Helper/ResponseHandler/GuzzlePsr7ResponseHandler.php new file mode 100644 index 0000000..d47d99e --- /dev/null +++ b/src/Helper/ResponseHandler/GuzzlePsr7ResponseHandler.php @@ -0,0 +1,14 @@ +getBody()->getContents(), true); + } +} diff --git a/src/Helper/ResponseHandler/OriginalResponseHandler.php b/src/Helper/ResponseHandler/OriginalResponseHandler.php new file mode 100644 index 0000000..8ae6b21 --- /dev/null +++ b/src/Helper/ResponseHandler/OriginalResponseHandler.php @@ -0,0 +1,14 @@ +getBody()->getContents(); + } +} diff --git a/src/Helper/ResponseHandler/ResponseHandler.php b/src/Helper/ResponseHandler/ResponseHandler.php new file mode 100644 index 0000000..1f83183 --- /dev/null +++ b/src/Helper/ResponseHandler/ResponseHandler.php @@ -0,0 +1,18 @@ +resolveAlias('ping'); + + $this->assertInstanceOf(TestConnectivity::class, $resolvedAlias); + } + + public function test_check_class_is_throw_exception_when_resolved_with_non_existing_alias() + { + $binance = new BinanceOriginal(); + + $this->expectException(MethodNotExistException::class); + + $binance->resolveAlias('nonExistingResolvedAlias'); + } + + /** + * @throws Exception + */ + public function test_add_new_alias_for_class() + { + $binance = new BinanceOriginal(); + + $nonExistingResolvedAlias = $this->createMock(Endpoint::class); + + $binance->addAlias('nonExistingResolvedAlias', $nonExistingResolvedAlias::class); + + $this->assertInstanceOf($nonExistingResolvedAlias::class, $binance->resolveAlias('nonExistingResolvedAlias')); + } + + public function test_expect_error_for_non_existing_method() + { + $binance = new BinanceOriginal(); + + $this->expectException(MethodNotExistException::class); + + $binance->nonExistingMethod(); + } + + public function test_it_make_http_request() + { + $container = []; + $handlerStack = HandlerStack::create(new MockHandler([ + new Response(body: json_encode([])), + ])); + $handlerStack->push(Middleware::history($container)); + + $binance = new BinanceOriginal(client: new Client(['handler' => $handlerStack])); + + $result = $binance->ping(); + + $this->assertEmpty($result); + $this->assertIsArray($result); + + foreach ($container as $transaction) { + $request = $transaction['request']; + + $this->assertEquals('api.binance.com', $request->getUri()->getHost()); + $this->assertEquals('/api/v3/ping', $request->getUri()->getPath()); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEmpty($request->getUri()->getQuery()); + $this->assertEmpty($request->getBody()->getContents()); + } + } + + public function test_it_return_result_in_guzzle_response() + { + $container = []; + $handlerStack = HandlerStack::create(new MockHandler([ + new Response(body: json_encode([])), + ])); + $handlerStack->push(Middleware::history($container)); + + $binance = new BinanceOriginal(new GuzzlePsr7ResponseHandler(), client: new Client(['handler' => $handlerStack])); + + $this->assertInstanceOf(ResponseInterface::class, $binance->ping()); + } + + public function test_binance_class_has_necessary_methods() + { + $methods = [ + 'ping' => json_decode(TestConnectivity::exampleResponse(), true), + 'time' => json_decode(CheckServerTime::exampleResponse(), true), + 'exchangeInfo' => json_decode(ExchangeInformation::exampleResponse(), true), + 'depth' => json_decode(OrderBook::exampleResponse(), true), + 'trades' => json_decode(RecentTradesList::exampleResponse(), true), + 'historicalTrades' => json_decode(OldTradeLookup::exampleResponse(), true), + 'aggTrades' => json_decode(CompressedAggregateTradesList::exampleResponse(), true), + 'klines' => json_decode(KlineCandlestickData::exampleResponse(), true), + 'avgPrice' => json_decode(UIKlines::exampleResponse(), true), + 'ticker24hr' => json_decode(CurrentAveragePrice::exampleResponse(), true), + 'tickerPrice' => json_decode(TickerPriceChangeStatistics24hr::exampleResponse(), true), + 'tickerBookTicker' => json_decode(SymbolPriceTicker::exampleResponse()['secondVersion'], true), + 'ticker' => json_decode(SymbolOrderBookTicker::exampleResponse()['secondVersion'], true), + ]; + + $handlerStack = HandlerStack::create(new MockHandler([ + new Response(body: TestConnectivity::exampleResponse()), + new Response(body: CheckServerTime::exampleResponse()), + new Response(body: ExchangeInformation::exampleResponse()), + new Response(body: OrderBook::exampleResponse()), + new Response(body: RecentTradesList::exampleResponse()), + new Response(body: OldTradeLookup::exampleResponse()), + new Response(body: CompressedAggregateTradesList::exampleResponse()), + new Response(body: KlineCandlestickData::exampleResponse()), + new Response(body: UIKlines::exampleResponse()), + new Response(body: CurrentAveragePrice::exampleResponse()), + new Response(body: TickerPriceChangeStatistics24hr::exampleResponse()), + new Response(body: SymbolPriceTicker::exampleResponse()['secondVersion']), + new Response(body: SymbolOrderBookTicker::exampleResponse()['secondVersion']), + ])); + + $binance = new BinanceOriginal(client: new Client(['handler' => $handlerStack])); + + foreach ($methods as $method => $response) { + $this->assertEquals($response, $binance->{$method}()); + } + } +} diff --git a/tests/BinanceTest.php b/tests/BinanceTest.php new file mode 100644 index 0000000..f69b273 --- /dev/null +++ b/tests/BinanceTest.php @@ -0,0 +1,621 @@ +client = $this->createMock(Client::class); + + $this->binance = new Binance(client: $this->client); + } + + public function test_throw_exception_when_get_error_message() + { + $this->client + ->expects($this->once()) + ->method('request') + ->will($this->returnValueMap([[ + 'GET', + 'https://api.binance.com/api/v3/ping', + [], + new Response(body: json_encode(['msg' => 'some error from binance', 'code' => '-1123'])) + ]])); + + $this->expectException(BinanceResponseException::class); + + $this->binance->ping(); + } + + public function test_return_message_when_get_error_message() + { + $this->client + ->expects($this->once()) + ->method('request') + ->will($this->returnValueMap([[ + 'GET', + 'https://api.binance.com/api/v3/ping', + [], + new Response(body: json_encode(['msg' => 'some error from binance', 'code' => '-1123'])) + ]])); + + $this->binance->disableBinanceExceptions(); + + $this->assertEquals( + ['msg' => 'some error from binance', 'code' => '-1123'], + $this->binance->ping()['response']['data'] + ); + } + + public function test_it_set_output_callback() + { + // TODO: add test for output callback + $this->markTestSkipped(); + } + + public function test_it_return_correct_full_result() + { + // TODO: add test for correct full result + $this->markTestSkipped(); + } + + public function test_http_test_connectivity() + { + $this->client + ->expects($this->once()) + ->method('request') + ->will($this->returnValueMap([[ + 'GET', + 'https://api.binance.com/api/v3/ping', + [], + new Response(body: TestConnectivity::exampleResponse()) + ]])); + + $this->assertEquals( + json_decode(TestConnectivity::exampleResponse(), true), + $this->binance->ping()['response']['data'] + ); + } + + public function test_http_check_server_time() + { + $this->client + ->expects($this->once()) + ->method('request') + ->will($this->returnValueMap([[ + 'GET', + 'https://api.binance.com/api/v3/time', + [], + new Response(body: CheckServerTime::exampleResponse()) + ]])); + + $result = json_decode(CheckServerTime::exampleResponse(), true); + $result['customAdditional'] = ['serverTimeDate' => 'Wed, 12 Jul 2017 02:42:00 UTC']; + + $this->assertEquals($result, $this->binance->time()['response']['data']); + } + + public function test_http_exchange_information() + { + $this->client + ->expects($this->exactly(5)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/exchangeInfo', + [], + new Response(body: ExchangeInformation::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/exchangeInfo', + ['query' => ['symbol' => 'BTCUSDT']], + new Response(body: ExchangeInformation::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/exchangeInfo', + ['query' => ['symbols' => '["BTCUSDT","ETHUSDT"]']], + new Response(body: ExchangeInformation::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/exchangeInfo', + ['query' => ['permissions' => 'SPOT']], + new Response(body: ExchangeInformation::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/exchangeInfo', + ['query' => ['permissions' => '["MARGIN","LEVERAGED"]']], + new Response(body: ExchangeInformation::exampleResponse()) + ], + ])); + + $result = json_decode(ExchangeInformation::exampleResponse(), true); + $result['customAdditional'] = ['serverTimeDate' => 'Thu, 08 Aug 2019 06:39:24 UTC']; + + $this->assertEquals($result, $this->binance->exchangeInfo()['response']['data']); + $this->assertEquals($result, $this->binance->exchangeInfo(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->exchangeInfo(symbols: ['BTCUSDT', 'ETHUSDT'])['response']['data']); + $this->assertEquals($result, $this->binance->exchangeInfo(permissions: 'SPOT')['response']['data']); + $this->assertEquals($result, $this->binance->exchangeInfo(permissions: ['MARGIN', 'LEVERAGED'])['response']['data']); + } + + public function test_http_order_book() + { + $this->client + ->expects($this->exactly(3)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/depth', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 100]], + new Response(body: OrderBook::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/depth', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 1]], + new Response(body: OrderBook::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/depth', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 5000]], + new Response(body: OrderBook::exampleResponse()) + ], + ])); + + $result = json_decode(OrderBook::exampleResponse(), true); + + $result['bids'] = array_map( + function ($bid) { + [$price, $amount] = $bid; + return ['price' => $price, 'amount' => $amount]; + }, + $result['bids'] + ); + + $result['asks'] = array_map( + function ($ask) { + [$price, $amount] = $ask; + return ['price' => $price, 'amount' => $amount]; + }, + $result['asks'] + ); + + $this->assertEquals($result, $this->binance->depth(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->depth(symbol: 'BTCUSDT', limit: -10)['response']['data']); + $this->assertEquals($result, $this->binance->depth(symbol: 'BTCUSDT', limit: 10000)['response']['data']); + + $this->expectException(EndpointQueryException::class); + $this->binance->depth(); + } + + public function test_http_recent_trade_list() + { + $this->client + ->expects($this->exactly(3)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/trades', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 500]], + new Response(body: RecentTradesList::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/trades', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 1]], + new Response(body: RecentTradesList::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/trades', + ['query' => ['symbol' => 'BTCUSDT', 'limit' => 1000]], + new Response(body: RecentTradesList::exampleResponse()) + ], + ])); + + $result = array_map(function ($item) { + $item['customAdditional']['timeDate'] = 'Wed, 12 Jul 2017 13:19:10 UTC'; + + return $item; + }, json_decode(RecentTradesList::exampleResponse(), true)); + + $this->assertEquals($result, $this->binance->trades(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->trades(symbol: 'BTCUSDT', limit: -10)['response']['data']); + $this->assertEquals($result, $this->binance->trades(symbol: 'BTCUSDT', limit: 10000)['response']['data']); + + $this->expectException(EndpointQueryException::class); + $this->binance->trades(); + } + + public function test_http_old_trade_lookup_without_keys() + { + $this->expectException(BinanceException::class); + $this->binance->historicalTrades(); + } + + public function test_http_old_trade_lookup() + { + $this->client + ->expects($this->exactly(4)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/historicalTrades', + ['headers' => ['X-MBX-APIKEY' => 'apiKey'], 'query' => ['symbol' => 'BTCUSDT', 'limit' => 500]], + new Response(body: OldTradeLookup::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/historicalTrades', + ['headers' => ['X-MBX-APIKEY' => 'apiKey'], 'query' => ['symbol' => 'BTCUSDT', 'limit' => 1]], + new Response(body: OldTradeLookup::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/historicalTrades', + ['headers' => ['X-MBX-APIKEY' => 'apiKey'], 'query' => ['symbol' => 'BTCUSDT', 'limit' => 1000]], + new Response(body: OldTradeLookup::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/historicalTrades', + ['headers' => ['X-MBX-APIKEY' => 'apiKey'], 'query' => ['symbol' => 'BTCUSDT', 'limit' => 10, 'fromId' => '19960']], + new Response(body: OldTradeLookup::exampleResponse()) + ], + ])); + + $this->binance->setApiKeys('apiKey'); + + $result = array_map(function ($item) { + $item['customAdditional']['timeDate'] = 'Wed, 12 Jul 2017 13:19:10 UTC'; + + return $item; + }, json_decode(OldTradeLookup::exampleResponse(), true)); + + $this->assertEquals($result, $this->binance->historicalTrades(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->historicalTrades(symbol: 'BTCUSDT', limit: -10)['response']['data']); + $this->assertEquals($result, $this->binance->historicalTrades(symbol: 'BTCUSDT', limit: 10000)['response']['data']); + $this->assertEquals($result, $this->binance->historicalTrades(symbol: 'BTCUSDT', limit: 10, fromId: 19960)['response']['data']); + + $this->expectException(EndpointQueryException::class); + $this->binance->historicalTrades(); + } + + public function test_http_klines_data() + { + $this->client + ->expects($this->exactly(4)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/klines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 500]], + new Response(body: KlineCandlestickData::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/klines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 1]], + new Response(body: KlineCandlestickData::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/klines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 1000]], + new Response(body: KlineCandlestickData::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/klines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 200, 'startTime' => '1689272940000', 'endTime' => '1689273059999']], + new Response(body: KlineCandlestickData::exampleResponse()) + ], + ])); + + $result = array_map(function ($item) { + return [ + 'klineOpenTime' => $item[0], + 'openPrice' => $item[1], + 'highPrice' => $item[2], + 'lowPrice' => $item[3], + 'closePrice' => $item[4], + 'volume' => $item[5], + 'klineCloseTime' => $item[6], + 'quoteAssetVolume' => $item[7], + 'numberOfTrades' => $item[8], + 'takerBuyBaseAssetVolume' => $item[9], + 'takerBuyQuoteAssetVolume' => $item[10], + 'unusedField' => $item[11], + 'customAdditional' => [ + 'klineOpenTimeDate' => Carbon::getFullDate($item[0]), + 'klineCloseTimeDate' => Carbon::getFullDate($item[6]), + ], + ]; + }, json_decode(KlineCandlestickData::exampleResponse(), true)); + + $this->assertEquals($result, $this->binance->klines(symbol: 'BTCUSDT', interval: '1m')['response']['data']); + $this->assertEquals($result, $this->binance->klines(symbol: 'BTCUSDT', interval: '1m', limit: -10)['response']['data']); + $this->assertEquals($result, $this->binance->klines(symbol: 'BTCUSDT', interval: '1m', limit: 10000)['response']['data']); + $this->assertEquals($result, $this->binance->klines(symbol: 'BTCUSDT', interval: '1m', startTime: '1689272940000', endTime: '1689273059999', limit: 200)['response']['data']); + } + + public function test_http_klines_data_mandatory_symbol() + { + + $this->expectException(EndpointQueryException::class); + $this->binance->klines(interval: '1m', startTime: '1689272940000', endTime: '1689273059999'); + } + + public function test_http_klines_data_mandatory_interval() + { + $this->expectException(EndpointQueryException::class); + $this->binance->klines(symbol: 'BTCUSDT'); + } + + public function test_http_uiklines_data() + { + $this->client + ->expects($this->exactly(4)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/uiKlines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 500]], + new Response(body: UIKlines::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/uiKlines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 1]], + new Response(body: UIKlines::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/uiKlines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 1000]], + new Response(body: UIKlines::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/uiKlines', + ['query' => ['symbol' => 'BTCUSDT', 'interval' => '1m', 'limit' => 200, 'startTime' => '1689272940000', 'endTime' => '1689273059999']], + new Response(body: UIKlines::exampleResponse()) + ], + ])); + + $result = array_map(function ($item) { + return [ + 'klineOpenTime' => $item[0], + 'openPrice' => $item[1], + 'highPrice' => $item[2], + 'lowPrice' => $item[3], + 'closePrice' => $item[4], + 'volume' => $item[5], + 'klineCloseTime' => $item[6], + 'quoteAssetVolume' => $item[7], + 'numberOfTrades' => $item[8], + 'takerBuyBaseAssetVolume' => $item[9], + 'takerBuyQuoteAssetVolume' => $item[10], + 'unusedField' => $item[11], + 'customAdditional' => [ + 'klineOpenTimeDate' => Carbon::getFullDate($item[0]), + 'klineCloseTimeDate' => Carbon::getFullDate($item[6]), + ], + ]; + }, json_decode(KlineCandlestickData::exampleResponse(), true)); + + $this->assertEquals($result, $this->binance->uiKlines(symbol: 'BTCUSDT', interval: '1m')['response']['data']); + $this->assertEquals($result, $this->binance->uiKlines(symbol: 'BTCUSDT', interval: '1m', limit: -10)['response']['data']); + $this->assertEquals($result, $this->binance->uiKlines(symbol: 'BTCUSDT', interval: '1m', limit: 10000)['response']['data']); + $this->assertEquals($result, $this->binance->uiKlines(symbol: 'BTCUSDT', interval: '1m', startTime: '1689272940000', endTime: '1689273059999', limit: 200)['response']['data']); + } + + public function test_http_uiklines_data_mandatory_symbol() + { + + $this->expectException(EndpointQueryException::class); + $this->binance->uiKlines(interval: '1m', startTime: '1689272940000', endTime: '1689273059999'); + } + + public function test_http_uiklines_data_mandatory_interval() + { + $this->expectException(EndpointQueryException::class); + $this->binance->uiKlines(symbol: 'BTCUSDT'); + } + + public function test_http_current_average_price() + { + $this->client + ->expects($this->exactly(1)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/avgPrice', + ['query' => ['symbol' => 'BTCUSDT']], + new Response(body: CurrentAveragePrice::exampleResponse()) + ], + ])); + + $this->assertEquals( + json_decode(CurrentAveragePrice::exampleResponse(), true), + $this->binance->avgPrice(symbol: 'BTCUSDT')['response']['data'] + ); + + $this->expectException(EndpointQueryException::class); + $this->binance->avgPrice(); + } + + public function test_http_24_hr_ticker_price_change_statistic() + { + $this->client + ->expects($this->exactly(3)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/24hr', + ['query' => ['type' => 'FULL', 'symbol' => 'BTCUSDT']], + new Response(body: TickerPriceChangeStatistics24hr::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/24hr', + ['query' => ['type' => 'FULL', 'symbols' => '["BTCUSDT","ETHUSDT"]']], + new Response(body: TickerPriceChangeStatistics24hr::exampleResponse()) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/24hr', + ['query' => ['type' => 'MINI', 'symbol' => 'BTCUSDT']], + new Response(body: TickerPriceChangeStatistics24hr::exampleResponse()) + ], + ])); + + $result = json_decode(TickerPriceChangeStatistics24hr::exampleResponse(), true); + $result['customAdditional'] = [ + 'openTimeDate' => 'Tue, 11 Jul 2017 14:31:39 UTC', + 'closeTimeDate' => 'Wed, 12 Jul 2017 14:31:39 UTC', + ]; + + $this->assertEquals($result, $this->binance->ticker24hr(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->ticker24hr(symbols: ['BTCUSDT', 'ETHUSDT'])['response']['data']); + $this->assertEquals($result, $this->binance->ticker24hr(symbol: 'BTCUSDT', type: 'MINI')['response']['data']); + } + + public function test_http_symbol_price_ticker() + { + $this->client + ->expects($this->exactly(2)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/price', + ['query' => ['symbol' => 'BTCUSDT']], + new Response(body: SymbolPriceTicker::exampleResponse()['firstVersion']) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/price', + ['query' => ['symbols' => '["BTCUSDT","ETHUSDT"]']], + new Response(body: SymbolPriceTicker::exampleResponse()['firstVersion']) + ], + ])); + + $result = json_decode(SymbolPriceTicker::exampleResponse()['firstVersion'], true); + + $this->assertEquals($result, $this->binance->tickerPrice(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->tickerPrice(symbols: ['BTCUSDT', 'ETHUSDT'])['response']['data']); + } + + public function test_http_symbol_order_book_ticker() + { + $this->client + ->expects($this->exactly(2)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/bookTicker', + ['query' => ['symbol' => 'BTCUSDT']], + new Response(body: SymbolOrderBookTicker::exampleResponse()['firstVersion']) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/ticker/bookTicker', + ['query' => ['symbols' => '["BTCUSDT","ETHUSDT"]']], + new Response(body: SymbolOrderBookTicker::exampleResponse()['firstVersion']) + ], + ])); + + $result = json_decode(SymbolOrderBookTicker::exampleResponse()['firstVersion'], true); + + $this->assertEquals($result, $this->binance->tickerBookTicker(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->tickerBookTicker(symbols: ['BTCUSDT', 'ETHUSDT'])['response']['data']); + } + + public function test_http_rolling_window_price_change_statistic() + { + $this->client + ->expects($this->exactly(2)) + ->method('request') + ->will($this->returnValueMap([ + [ + 'GET', + 'https://api.binance.com/api/v3/ticker', + ['query' => ['windowSize' => '1d', 'type' => 'FULL', 'symbol' => 'BTCUSDT']], + new Response(body: RollingWindowPriceChangeStatistics::exampleResponse()['secondVersion']) + ], + [ + 'GET', + 'https://api.binance.com/api/v3/ticker', + ['query' => ['windowSize' => '1h', 'type' => 'MINI', 'symbols' => '["BTCUSDT","ETHUSDT"]']], + new Response(body: RollingWindowPriceChangeStatistics::exampleResponse()['secondVersion']) + ], + ])); + + $result = array_map(function ($item) { + $item[ProcessResponse::ADDITIONAL_FIELD] = [ + 'openTimeDate' => Carbon::getFullDate($item['openTime']), + 'closeTimeDate' => Carbon::getFullDate($item['closeTime']), + ]; + + return $item; + }, json_decode(RollingWindowPriceChangeStatistics::exampleResponse()['secondVersion'], true)); + + $this->assertEquals($result, $this->binance->ticker(symbol: 'BTCUSDT')['response']['data']); + $this->assertEquals($result, $this->binance->ticker(symbols: ['BTCUSDT', 'ETHUSDT'], windowSize: '1h', type: 'MINI')['response']['data']); + } + + public function test_http_rolling_window_price_change_statistic_mandatory_parameters() + { + $this->expectException(EndpointQueryException::class); + $this->binance->ticker(); + } + + public function test_analogs() + { + // TODO: add tests for analogs + $this->markTestSkipped(); + } +} diff --git a/tests/Helper/CarbonTest.php b/tests/Helper/CarbonTest.php new file mode 100644 index 0000000..d053016 --- /dev/null +++ b/tests/Helper/CarbonTest.php @@ -0,0 +1,19 @@ +assertEquals('Tue, 11 Jul 2023 19:05:03 UTC', Carbon::getFullDate('1689102303379')); + } + + public function test_it_format_from_microtime_to_date_correct_with_another_timezone() + { + $this->assertEquals('Tue, 11 Jul 2023 15:07:30 America/New_York', Carbon::getFullDate('1689102449921', 'America/New_York')); + } +} diff --git a/tests/Helper/HttpTest.php b/tests/Helper/HttpTest.php new file mode 100644 index 0000000..e8ab64b --- /dev/null +++ b/tests/Helper/HttpTest.php @@ -0,0 +1,61 @@ +push(Middleware::history($container)); + + $http = new Http(new Client(['handler' => $handlerStack])); + + $http->get('https://example.com/get', ['HEADER' => 'header-get'], ['query' => 'get']); + $http->post('https://example.com/post', ['HEADER' => 'header-post'], body: ['body' => 'post']); + $http->put('https://example.com/put', ['HEADER' => 'header-put'], body: ['body' => 'put']); + $http->delete('https://example.com/delete', ['HEADER' => 'header-delete'], body: ['body' => 'delete']); + + $expected = [ + ['host' => 'example.com', 'path' => '/get', 'method' => 'GET', 'header' => ['HEADER' => ['header-get']], 'query' => 'query=get', 'body' => ''], + ['host' => 'example.com', 'path' => '/post', 'method' => 'POST', 'header' => ['HEADER' => ['header-post']], 'query' => '', 'body' => 'body=post'], + ['host' => 'example.com', 'path' => '/put', 'method' => 'PUT', 'header' => ['HEADER' => ['header-put']], 'query' => '', 'body' => 'body=put'], + ['host' => 'example.com', 'path' => '/delete', 'method' => 'DELETE', 'header' => ['HEADER' => ['header-delete']], 'query' => '', 'body' => 'body=delete'], + ]; + + foreach ($container as $key => $transaction) { + $request = $transaction['request']; + + $this->assertEquals($expected[$key]['host'], $request->getUri()->getHost()); + $this->assertEquals($expected[$key]['path'], $request->getUri()->getPath()); + $this->assertEquals($expected[$key]['method'], $request->getMethod()); + $this->assertEquals($expected[$key]['header']['HEADER'], $request->getHeader('HEADER')); + $this->assertEquals($expected[$key]['query'], $request->getUri()->getQuery()); + $this->assertEquals($expected[$key]['body'], $request->getBody()->getContents()); + } + } + + public function test_it_throw_error_for_unaccounted_requests() + { + $http = new Http(new Client(['handler' => HandlerStack::create(new MockHandler([new Response(200)]))])); + + $this->expectException(Exception::class); + + $http->patch('https://example.com/post', ['HEADER' => 'header-post'], body: ['body' => 'post']); + } +} diff --git a/tests/Helper/MathTest.php b/tests/Helper/MathTest.php new file mode 100644 index 0000000..c9c1169 --- /dev/null +++ b/tests/Helper/MathTest.php @@ -0,0 +1,35 @@ +assertTrue(Math::isEqualFloats(12 + 1000.21212, 1012.21212)); + $this->assertTrue(Math::isEqualFloats(17.3 * 29.5, 510.35)); + $this->assertTrue(Math::isEqualFloats(30 / 1200, 0.025)); + $this->assertTrue(Math::isEqualFloats(1000.2121298, 1000.2121298)); + } + + public function test_it_increment_number_correctly() + { + $this->assertTrue(Math::isEqualFloats(12.55, Math::incrementNumber(12.559, 0.01))); + $this->assertTrue(Math::isEqualFloats(12.3, Math::incrementNumber(12.559, 0.3))); + $this->assertTrue(Math::isEqualFloats(10, Math::incrementNumber(12.559, 5))); + } + + public function test_it_is_division_without_reminder_correctly() + { + $this->assertTrue(Math::isDivisionWithoutRemainder(18, 4.5)); + $this->assertTrue(Math::isDivisionWithoutRemainder(6, 2)); + } + + public function test_it_is_division_without_reminder_not_correctly() + { + $this->assertFalse(Math::isDivisionWithoutRemainder(10.1, 0.1)); + } +}