From 1c4346805a2bf38a3b5774fe3d603223d0bf2c4c Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 14:54:16 +0330 Subject: [PATCH 01/22] signature: generate dependencies for signature header --- src/Requests/Header.php | 37 +++++++++++++++++++++++++++++++ tests/AuthHeaderTest.php | 48 ++++++++++++++++++++++++++++++++++++++++ tests/ExampleTest.php | 5 ----- 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/Requests/Header.php create mode 100644 tests/AuthHeaderTest.php delete mode 100644 tests/ExampleTest.php diff --git a/src/Requests/Header.php b/src/Requests/Header.php new file mode 100644 index 0000000..45c3caa --- /dev/null +++ b/src/Requests/Header.php @@ -0,0 +1,37 @@ + $k . $v, + array_keys($sortedParameters), + array_values($sortedParameters) + )); + } + + public static function generateNonce(): ?string + { + return md5(uniqid(mt_rand(), true)); + } +} diff --git a/tests/AuthHeaderTest.php b/tests/AuthHeaderTest.php new file mode 100644 index 0000000..3f39327 --- /dev/null +++ b/tests/AuthHeaderTest.php @@ -0,0 +1,48 @@ +toBeEmpty(); + + $params = [ + 'z_index' => 'z value', + 'a_index' => 'a value', + 'b_index' => 'b value', + ]; + $sortedParams = Header::sortQueryParameters($params); + expect($sortedParams) + ->toMatchArray([ + 'a_index' => 'a value', + 'b_index' => 'b value', + 'z_index' => 'z value', + ]); +}); + +it('get sorted query params as string value', function () { + + $digestedParam = Header::digestQueryParameters([]); + expect($digestedParam)->toBeNull(); + + $params = [ + 'z_index' => 'z_value', + 'a_index' => 'a_value', + 'b_index' => 'b_value', + ]; + $sortedParams = Header::digestQueryParameters($params); + expect($sortedParams)->toEqual('a_indexa_valueb_indexb_valuez_indexz_value'); +}); + +it('get some random string 32 bit', function () { + + $firstRandom = Header::generateNonce(); + $secondRandom = Header::generateNonce(); + + expect($firstRandom) + ->toBeString() + ->toHaveLength(32) + ->not()->toEqual($secondRandom) + ->not()->toBeNull(); +}); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); From d57006f6c3faf06b74fcf038f2ac42c522906f68 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 15:53:16 +0330 Subject: [PATCH 02/22] signature: add generate sign value method --- src/Requests/Header.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Requests/Header.php b/src/Requests/Header.php index 45c3caa..8a4201b 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -34,4 +34,17 @@ class Header { return md5(uniqid(mt_rand(), true)); } + + public static function generateSignValue(array $queryParams = [], string $body = '', string $randomNonce = ''): string + { + $apiKey = config('bitunix-api.api_key'); + $apiSecret = config('bitunix-api.api_secret'); + $timeStamp = (string)round(microtime(true) * 1000); + $digestedQueryParam = self::digestQueryParameters($queryParams); + + $digestedHeader = $randomNonce . $timeStamp . $apiKey . $digestedQueryParam . $body; + $hash = hash('sha256', $digestedHeader); + $sign = $hash . $apiSecret; + return hash('sha256', $sign); + } } From 0503da734915d0f62112fe32ee1f57fee0105f81 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 16:22:44 +0330 Subject: [PATCH 03/22] future-public: add kline request --- config/bitunix-api.php | 1 + src/LaravelBitunixApi.php | 32 ++++++++++++++++++++- src/LaravelBitunixApiServiceProvider.php | 8 ++++++ src/Requests/FutureKLineRequestContract.php | 30 +++++++++++++++++++ tests/KLineTest.php | 13 +++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/Requests/FutureKLineRequestContract.php create mode 100644 tests/KLineTest.php diff --git a/config/bitunix-api.php b/config/bitunix-api.php index b9ffe0e..b0bbf2a 100644 --- a/config/bitunix-api.php +++ b/config/bitunix-api.php @@ -3,4 +3,5 @@ // config for Msr/LaravelBitunixApi return [ + 'future_base_uri' => 'https://fapi.bitunix.com/', ]; diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 823fc07..90e4075 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -2,4 +2,34 @@ namespace Msr\LaravelBitunixApi; -class LaravelBitunixApi {} +use GuzzleHttp\Client; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Psr\Http\Message\ResponseInterface; + +class LaravelBitunixApi implements FutureKLineRequestContract +{ + private Client $publicFutureClient; + + public function __construct() + { + $this->publicFutureClient = new Client([ + 'base_uri' => config('bitunix-api.future_base_uri') . '/api/v1/futures/market/', + ]); + } + + public function getFutureKline(string $symbol, string $interval, int $limit = 100, ?int $startTime = null, ?int $endTime = null, string $type = 'LAST_PRICE'): ResponseInterface + { + $response = $this->publicFutureClient->get('kline', [ + 'query' => [ + 'symbol' => $symbol, + 'interval' => $interval, + 'limit' => $limit, + 'startTime' => $startTime, + 'endTime' => $endTime, + 'type' => $type, + ] + ]); + + return $response; + } +} diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index c30e13b..b8aa134 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -2,6 +2,7 @@ namespace Msr\LaravelBitunixApi; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; @@ -22,4 +23,11 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider ->hasMigration('create_laravel_bitunix_api_table') ->hasCommand(LaravelBitunixApiCommand::class); } + + public function packageRegistered(): void + { + parent::packageRegistered(); + + $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); + } } diff --git a/src/Requests/FutureKLineRequestContract.php b/src/Requests/FutureKLineRequestContract.php new file mode 100644 index 0000000..2e36784 --- /dev/null +++ b/src/Requests/FutureKLineRequestContract.php @@ -0,0 +1,30 @@ +getFutureKline('BTCUSDT', '1h', 100, now()->subHours(6)->milliseconds, now()->milliseconds); + expect($response->getStatusCode()) + ->toBe(200) + ->and(json_decode($response->getBody()->getContents())) + ->toHaveKeys(['code', 'data', 'msg']); +}); From 464f8fec4145fea6714c4234f0bcbc75b84100e0 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:53:15 +0000 Subject: [PATCH 04/22] Fix styling --- src/LaravelBitunixApi.php | 4 ++-- src/LaravelBitunixApiServiceProvider.php | 2 +- src/Requests/FutureKLineRequestContract.php | 16 +++++----------- tests/TestCase.php | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 90e4075..5b73040 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -13,7 +13,7 @@ class LaravelBitunixApi implements FutureKLineRequestContract public function __construct() { $this->publicFutureClient = new Client([ - 'base_uri' => config('bitunix-api.future_base_uri') . '/api/v1/futures/market/', + 'base_uri' => config('bitunix-api.future_base_uri').'/api/v1/futures/market/', ]); } @@ -27,7 +27,7 @@ class LaravelBitunixApi implements FutureKLineRequestContract 'startTime' => $startTime, 'endTime' => $endTime, 'type' => $type, - ] + ], ]); return $response; diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index b8aa134..a984a54 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -2,10 +2,10 @@ namespace Msr\LaravelBitunixApi; +use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; -use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; class LaravelBitunixApiServiceProvider extends PackageServiceProvider { diff --git a/src/Requests/FutureKLineRequestContract.php b/src/Requests/FutureKLineRequestContract.php index 2e36784..d0654cc 100644 --- a/src/Requests/FutureKLineRequestContract.php +++ b/src/Requests/FutureKLineRequestContract.php @@ -7,12 +7,6 @@ use Psr\Http\Message\ResponseInterface; interface FutureKLineRequestContract { /** - * @param string $symbol - * @param string $interval - * @param int $limit - * @param int|null $startTime - * @param int|null $endTime - * @param string $type * @return ResponseInterface * * interval could be: 1m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1M @@ -22,9 +16,9 @@ interface FutureKLineRequestContract * type could be: LAST_PRICE, MARK_PRICE */ public function getFutureKline(string $symbol, - string $interval, - int $limit = 100, - ?int $startTime = null, - ?int $endTime = null, - string $type = 'LAST_PRICE'): ResponseInterface; + string $interval, + int $limit = 100, + ?int $startTime = null, + ?int $endTime = null, + string $type = 'LAST_PRICE'): ResponseInterface; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6e766a1..11a3486 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,8 +3,8 @@ namespace Msr\LaravelBitunixApi\Tests; use Illuminate\Database\Eloquent\Factories\Factory; -use Orchestra\Testbench\TestCase as Orchestra; use Msr\LaravelBitunixApi\LaravelBitunixApiServiceProvider; +use Orchestra\Testbench\TestCase as Orchestra; class TestCase extends Orchestra { From 0996c5fc271f139d3d3613c03d40b5228009da3b Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 15:22:55 +0330 Subject: [PATCH 05/22] future-public: add set leverage request --- .gitignore | 1 + config/bitunix-api.php | 4 +- examples/ChangeLeverageExample.php | 61 ++++++++ src/LaravelBitunixApi.php | 30 +++- src/LaravelBitunixApiServiceProvider.php | 2 + .../ChangeLeverageRequestContract.php | 19 +++ src/Requests/Header.php | 125 +++++++++++++--- tests/ChangeLeverageTest.php | 25 ++++ tests/HeaderTest.php | 135 ++++++++++++++++++ 9 files changed, 377 insertions(+), 25 deletions(-) create mode 100644 examples/ChangeLeverageExample.php create mode 100644 src/Requests/ChangeLeverageRequestContract.php create mode 100644 tests/ChangeLeverageTest.php create mode 100644 tests/HeaderTest.php diff --git a/.gitignore b/.gitignore index b60507f..4b1d8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ phpstan.neon testbench.yaml /docs /coverage +.env diff --git a/config/bitunix-api.php b/config/bitunix-api.php index b0bbf2a..056ba9b 100644 --- a/config/bitunix-api.php +++ b/config/bitunix-api.php @@ -1,7 +1,9 @@ 'https://fapi.bitunix.com/', + 'api_key' => env('BITUNIX_API_KEY'), + 'api_secret' => env('BITUNIX_API_SECRET'), + 'language' => 'en-US', ]; diff --git a/examples/ChangeLeverageExample.php b/examples/ChangeLeverageExample.php new file mode 100644 index 0000000..4129e60 --- /dev/null +++ b/examples/ChangeLeverageExample.php @@ -0,0 +1,61 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(ChangeLeverageRequestContract::class); + + // Change leverage for BTCUSDT pair + $symbol = 'BTCUSDT'; + $marginCoin = 'USDT'; + $leverage = 12; + + echo "Changing leverage for {$symbol} to {$leverage}x...\n"; + + $response = $api->changeLeverage($symbol, $marginCoin, $leverage); + + // Check response status + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + + if ($data['code'] === 0) { + echo "✅ Leverage changed successfully!\n"; + echo "Symbol: " . $data['data'][0]['symbol'] . "\n"; + echo "Margin Coin: " . $data['data'][0]['marginCoin'] . "\n"; + echo "New Leverage: " . $data['data'][0]['leverage'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + +} catch (Exception $e) { + echo "❌ Exception: " . $e->getMessage() . "\n"; +} + +/** + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 5b73040..4b5b2b9 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -4,9 +4,11 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\Header; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements FutureKLineRequestContract +class LaravelBitunixApi implements FutureKLineRequestContract, ChangeLeverageRequestContract { private Client $publicFutureClient; @@ -17,6 +19,17 @@ class LaravelBitunixApi implements FutureKLineRequestContract ]); } + protected function getPrivateFutureClient(array $queryParams = [], array $body = []): Client + { + $bodyString = json_encode($body); + $headers = Header::generateHeaders($queryParams, $bodyString); + + return new Client([ + 'base_uri' => config('bitunix-api.future_base_uri').'/api/v1/futures/', + 'headers' => $headers, + ]); + } + public function getFutureKline(string $symbol, string $interval, int $limit = 100, ?int $startTime = null, ?int $endTime = null, string $type = 'LAST_PRICE'): ResponseInterface { $response = $this->publicFutureClient->get('kline', [ @@ -32,4 +45,19 @@ class LaravelBitunixApi implements FutureKLineRequestContract return $response; } + + public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface + { + $body = [ + 'symbol' => $symbol, + 'marginCoin' => $marginCoin, + 'leverage' => $leverage, + ]; + + $response = $this->getPrivateFutureClient([], $body)->post('account/change_leverage', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index a984a54..589a68b 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -3,6 +3,7 @@ namespace Msr\LaravelBitunixApi; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; +use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -29,5 +30,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider parent::packageRegistered(); $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/ChangeLeverageRequestContract.php b/src/Requests/ChangeLeverageRequestContract.php new file mode 100644 index 0000000..83e91d1 --- /dev/null +++ b/src/Requests/ChangeLeverageRequestContract.php @@ -0,0 +1,19 @@ + "1", "uid" => "200"] becomes "id1uid200" + * + * @param array $parameters + * @return string + */ + public static function digestQueryParameters(array $parameters): string { - if (!count($parameters)) { - return null; + if (empty($parameters)) { + return ''; } $sortedParameters = self::sortQueryParameters($parameters); - return implode('', array_map( - fn($k, $v) => $k . $v, - array_keys($sortedParameters), - array_values($sortedParameters) - )); + $result = ''; + + foreach ($sortedParameters as $key => $value) { + $result .= $key . $value; + } + + return $result; } - public static function generateNonce(): ?string + /** + * Generate a random 32-bit nonce string + * + * @return string + */ + public static function generateNonce(): string { - return md5(uniqid(mt_rand(), true)); + return bin2hex(random_bytes(16)); // 32 characters } - public static function generateSignValue(array $queryParams = [], string $body = '', string $randomNonce = ''): string + /** + * Generate current timestamp in milliseconds + * + * @return string + */ + public static function generateTimestamp(): string + { + return (string) round(microtime(true) * 1000); + } + + /** + * Generate signature according to Bitunix API documentation + * + * Steps: + * 1. Sort all queryParams in ascending ASCII order by Key + * 2. Remove all spaces from body string + * 3. Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) + * 4. Create sign: SHA256(digest + secretKey) + * + * @param array $queryParams + * @param string $body + * @param string $nonce + * @param string $timestamp + * @return string + */ + public static function generateSignValue(array $queryParams = [], string $body = '', string $nonce = '', string $timestamp = ''): string { $apiKey = config('bitunix-api.api_key'); $apiSecret = config('bitunix-api.api_secret'); - $timeStamp = (string)round(microtime(true) * 1000); - $digestedQueryParam = self::digestQueryParameters($queryParams); + + if (empty($apiKey) || empty($apiSecret)) { + throw new \InvalidArgumentException('API key and secret must be configured'); + } - $digestedHeader = $randomNonce . $timeStamp . $apiKey . $digestedQueryParam . $body; - $hash = hash('sha256', $digestedHeader); - $sign = $hash . $apiSecret; - return hash('sha256', $sign); + // Step 1: Sort query parameters in ascending ASCII order + $queryParamsString = self::digestQueryParameters($queryParams); + + // Step 2: Remove all spaces from body (already done if JSON encoded properly) + $bodyString = trim($body); + + // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) + $digestInput = $nonce . $timestamp . $apiKey . $queryParamsString . $bodyString; + $digest = hash('sha256', $digestInput); + + // Step 4: Create sign: SHA256(digest + secretKey) + $signInput = $digest . $apiSecret; + $sign = hash('sha256', $signInput); + + return $sign; + } + + /** + * Generate complete headers for authenticated requests + * + * @param array $queryParams + * @param string $body + * @return array + */ + public static function generateHeaders(array $queryParams = [], string $body = ''): array + { + $nonce = self::generateNonce(); + $timestamp = self::generateTimestamp(); + $sign = self::generateSignValue($queryParams, $body, $nonce, $timestamp); + + return [ + 'api-key' => config('bitunix-api.api_key'), + 'sign' => $sign, + 'nonce' => $nonce, + 'timestamp' => $timestamp, + 'language' => config('bitunix-api.language', 'en-US'), + 'Content-Type' => 'application/json', + ]; } } diff --git a/tests/ChangeLeverageTest.php b/tests/ChangeLeverageTest.php new file mode 100644 index 0000000..ced3d13 --- /dev/null +++ b/tests/ChangeLeverageTest.php @@ -0,0 +1,25 @@ + $api->changeLeverage('BTCUSDT', 'USDT', 12)) + ->not->toThrow(Exception::class); +}); + +it('validates required parameters for change leverage', function () { + $api = app(ChangeLeverageRequestContract::class); + + expect(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 10)) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 0)) + ->not->toThrow(Exception::class); +}); + diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php new file mode 100644 index 0000000..f680c3b --- /dev/null +++ b/tests/HeaderTest.php @@ -0,0 +1,135 @@ + 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can sort query parameters in ascending ASCII order', function () { + $parameters = [ + 'uid' => '200', + 'id' => '1', + 'name' => 'test' + ]; + + $sorted = Header::sortQueryParameters($parameters); + + expect(array_keys($sorted))->toBe(['id', 'name', 'uid']); + expect($sorted)->toBe([ + 'id' => '1', + 'name' => 'test', + 'uid' => '200' + ]); +}); + +it('can digest query parameters to string format', function () { + $parameters = [ + 'id' => '1', + 'uid' => '200' + ]; + + $result = Header::digestQueryParameters($parameters); + + expect($result)->toBe('id1uid200'); +}); + +it('can generate a valid nonce', function () { + $nonce = Header::generateNonce(); + + expect($nonce) + ->toBeString() + ->toHaveLength(32) + ->toMatch('/^[a-f0-9]+$/'); +}); + +it('can generate timestamp in milliseconds', function () { + $timestamp = Header::generateTimestamp(); + + expect($timestamp) + ->toBeString() + ->toMatch('/^\d{13}$/'); // 13 digits for milliseconds +}); + +it('can generate signature according to Bitunix documentation', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + $queryParams = ['id' => '1', 'uid' => '200']; + $body = '{"uid":"2899","arr":[{"id":1,"name":"maple"},{"id":2,"name":"lily"}]}'; + + $sign = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + + expect($sign) + ->toBeString() + ->toHaveLength(64) // SHA256 hex length + ->toMatch('/^[a-f0-9]+$/'); +}); + +it('can generate complete headers for authenticated requests', function () { + $queryParams = ['symbol' => 'BTCUSDT']; + $body = '{"symbol":"BTCUSDT","marginCoin":"USDT","leverage":12}'; + + $headers = Header::generateHeaders($queryParams, $body); + + expect($headers) + ->toHaveKeys(['api-key', 'sign', 'nonce', 'timestamp', 'language', 'Content-Type']) + ->and($headers['api-key'])->toBe('test-api-key') + ->and($headers['sign'])->toBeString() + ->and($headers['nonce'])->toBeString() + ->and($headers['timestamp'])->toBeString() + ->and($headers['language'])->toBe('en-US') + ->and($headers['Content-Type'])->toBe('application/json'); +}); + +it('throws exception when API credentials are missing', function () { + config([ + 'bitunix-api.api_key' => '', + 'bitunix-api.api_secret' => '', + ]); + + expect(fn() => Header::generateSignValue([], '', 'nonce', 'timestamp')) + ->toThrow(InvalidArgumentException::class, 'API key and secret must be configured'); +}); + +it('handles empty query parameters correctly', function () { + $result = Header::digestQueryParameters([]); + + expect($result)->toBe(''); +}); + +it('handles empty body correctly', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + + $sign = Header::generateSignValue([], '', $nonce, $timestamp); + + expect($sign) + ->toBeString() + ->toHaveLength(64); +}); + +it('generates consistent signature for same inputs', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + $queryParams = ['id' => '1']; + $body = '{"test":"value"}'; + + $sign1 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + $sign2 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + + expect($sign1)->toBe($sign2); +}); + +it('generates different signatures for different inputs', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + + $sign1 = Header::generateSignValue(['id' => '1'], '{"test":"value1"}', $nonce, $timestamp); + $sign2 = Header::generateSignValue(['id' => '2'], '{"test":"value2"}', $nonce, $timestamp); + + expect($sign1)->not->toBe($sign2); +}); From 49c1633f52c1eef54a85da7472becb7f4dd54b94 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Sun, 28 Sep 2025 11:53:47 +0000 Subject: [PATCH 06/22] Fix styling --- examples/ChangeLeverageExample.php | 30 ++++++------- src/LaravelBitunixApi.php | 4 +- .../ChangeLeverageRequestContract.php | 8 ++-- src/Requests/Header.php | 45 ++++++------------- tests/ChangeLeverageTest.php | 11 ++--- tests/HeaderTest.php | 44 +++++++++--------- 6 files changed, 58 insertions(+), 84 deletions(-) diff --git a/examples/ChangeLeverageExample.php b/examples/ChangeLeverageExample.php index 4129e60..cc1a38d 100644 --- a/examples/ChangeLeverageExample.php +++ b/examples/ChangeLeverageExample.php @@ -2,12 +2,12 @@ /** * Example usage of Change Leverage functionality - * + * * This example demonstrates how to use the LaravelBitunixApi package * to change leverage for a trading pair on Bitunix exchange. */ -require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__.'/../vendor/autoload.php'; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; @@ -22,39 +22,39 @@ config([ try { // Get the API instance $api = app(ChangeLeverageRequestContract::class); - + // Change leverage for BTCUSDT pair $symbol = 'BTCUSDT'; $marginCoin = 'USDT'; $leverage = 12; - + echo "Changing leverage for {$symbol} to {$leverage}x...\n"; - + $response = $api->changeLeverage($symbol, $marginCoin, $leverage); - + // Check response status if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); - + if ($data['code'] === 0) { echo "✅ Leverage changed successfully!\n"; - echo "Symbol: " . $data['data'][0]['symbol'] . "\n"; - echo "Margin Coin: " . $data['data'][0]['marginCoin'] . "\n"; - echo "New Leverage: " . $data['data'][0]['leverage'] . "\n"; + echo 'Symbol: '.$data['data'][0]['symbol']."\n"; + echo 'Margin Coin: '.$data['data'][0]['marginCoin']."\n"; + echo 'New Leverage: '.$data['data'][0]['leverage']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } - + } catch (Exception $e) { - echo "❌ Exception: " . $e->getMessage() . "\n"; + echo '❌ Exception: '.$e->getMessage()."\n"; } /** * Environment Variables Required: - * + * * BITUNIX_API_KEY=your-api-key * BITUNIX_API_SECRET=your-api-secret * BITUNIX_LANGUAGE=en-US diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 4b5b2b9..eb2fe77 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -3,12 +3,12 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; -use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements FutureKLineRequestContract, ChangeLeverageRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineRequestContract { private Client $publicFutureClient; diff --git a/src/Requests/ChangeLeverageRequestContract.php b/src/Requests/ChangeLeverageRequestContract.php index 83e91d1..034b3ab 100644 --- a/src/Requests/ChangeLeverageRequestContract.php +++ b/src/Requests/ChangeLeverageRequestContract.php @@ -9,11 +9,9 @@ interface ChangeLeverageRequestContract /** * Change leverage for a trading pair * - * @param string $symbol Trading pair (e.g., 'BTCUSDT') - * @param string $marginCoin Margin coin (e.g., 'USDT') - * @param int $leverage Leverage value - * @return ResponseInterface + * @param string $symbol Trading pair (e.g., 'BTCUSDT') + * @param string $marginCoin Margin coin (e.g., 'USDT') + * @param int $leverage Leverage value */ public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface; } - diff --git a/src/Requests/Header.php b/src/Requests/Header.php index b953c0f..2192635 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -6,9 +6,6 @@ class Header { /** * Sort query parameters in ascending ASCII order by Key - * - * @param array $parameters - * @return array */ public static function sortQueryParameters(array $parameters): array { @@ -17,15 +14,13 @@ class Header } ksort($parameters, SORT_STRING); + return $parameters; } /** * Convert sorted parameters to string format * Example: ["id" => "1", "uid" => "200"] becomes "id1uid200" - * - * @param array $parameters - * @return string */ public static function digestQueryParameters(array $parameters): string { @@ -35,18 +30,16 @@ class Header $sortedParameters = self::sortQueryParameters($parameters); $result = ''; - + foreach ($sortedParameters as $key => $value) { - $result .= $key . $value; + $result .= $key.$value; } - + return $result; } /** * Generate a random 32-bit nonce string - * - * @return string */ public static function generateNonce(): string { @@ -55,8 +48,6 @@ class Header /** * Generate current timestamp in milliseconds - * - * @return string */ public static function generateTimestamp(): string { @@ -65,58 +56,48 @@ class Header /** * Generate signature according to Bitunix API documentation - * + * * Steps: * 1. Sort all queryParams in ascending ASCII order by Key * 2. Remove all spaces from body string * 3. Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) * 4. Create sign: SHA256(digest + secretKey) - * - * @param array $queryParams - * @param string $body - * @param string $nonce - * @param string $timestamp - * @return string */ public static function generateSignValue(array $queryParams = [], string $body = '', string $nonce = '', string $timestamp = ''): string { $apiKey = config('bitunix-api.api_key'); $apiSecret = config('bitunix-api.api_secret'); - + if (empty($apiKey) || empty($apiSecret)) { throw new \InvalidArgumentException('API key and secret must be configured'); } // Step 1: Sort query parameters in ascending ASCII order $queryParamsString = self::digestQueryParameters($queryParams); - + // Step 2: Remove all spaces from body (already done if JSON encoded properly) $bodyString = trim($body); - + // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) - $digestInput = $nonce . $timestamp . $apiKey . $queryParamsString . $bodyString; + $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString.$bodyString; $digest = hash('sha256', $digestInput); - + // Step 4: Create sign: SHA256(digest + secretKey) - $signInput = $digest . $apiSecret; + $signInput = $digest.$apiSecret; $sign = hash('sha256', $signInput); - + return $sign; } /** * Generate complete headers for authenticated requests - * - * @param array $queryParams - * @param string $body - * @return array */ public static function generateHeaders(array $queryParams = [], string $body = ''): array { $nonce = self::generateNonce(); $timestamp = self::generateTimestamp(); $sign = self::generateSignValue($queryParams, $body, $nonce, $timestamp); - + return [ 'api-key' => config('bitunix-api.api_key'), 'sign' => $sign, diff --git a/tests/ChangeLeverageTest.php b/tests/ChangeLeverageTest.php index ced3d13..4db10c8 100644 --- a/tests/ChangeLeverageTest.php +++ b/tests/ChangeLeverageTest.php @@ -1,25 +1,20 @@ $api->changeLeverage('BTCUSDT', 'USDT', 12)) + expect(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 12)) ->not->toThrow(Exception::class); }); it('validates required parameters for change leverage', function () { $api = app(ChangeLeverageRequestContract::class); - expect(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 10)) + expect(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 10)) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 0)) + ->and(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 0)) ->not->toThrow(Exception::class); }); - diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index f680c3b..161798f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -14,33 +14,33 @@ it('can sort query parameters in ascending ASCII order', function () { $parameters = [ 'uid' => '200', 'id' => '1', - 'name' => 'test' + 'name' => 'test', ]; - + $sorted = Header::sortQueryParameters($parameters); - + expect(array_keys($sorted))->toBe(['id', 'name', 'uid']); expect($sorted)->toBe([ 'id' => '1', 'name' => 'test', - 'uid' => '200' + 'uid' => '200', ]); }); it('can digest query parameters to string format', function () { $parameters = [ 'id' => '1', - 'uid' => '200' + 'uid' => '200', ]; - + $result = Header::digestQueryParameters($parameters); - + expect($result)->toBe('id1uid200'); }); it('can generate a valid nonce', function () { $nonce = Header::generateNonce(); - + expect($nonce) ->toBeString() ->toHaveLength(32) @@ -49,7 +49,7 @@ it('can generate a valid nonce', function () { it('can generate timestamp in milliseconds', function () { $timestamp = Header::generateTimestamp(); - + expect($timestamp) ->toBeString() ->toMatch('/^\d{13}$/'); // 13 digits for milliseconds @@ -60,9 +60,9 @@ it('can generate signature according to Bitunix documentation', function () { $timestamp = '20241120123045'; $queryParams = ['id' => '1', 'uid' => '200']; $body = '{"uid":"2899","arr":[{"id":1,"name":"maple"},{"id":2,"name":"lily"}]}'; - + $sign = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); - + expect($sign) ->toBeString() ->toHaveLength(64) // SHA256 hex length @@ -72,9 +72,9 @@ it('can generate signature according to Bitunix documentation', function () { it('can generate complete headers for authenticated requests', function () { $queryParams = ['symbol' => 'BTCUSDT']; $body = '{"symbol":"BTCUSDT","marginCoin":"USDT","leverage":12}'; - + $headers = Header::generateHeaders($queryParams, $body); - + expect($headers) ->toHaveKeys(['api-key', 'sign', 'nonce', 'timestamp', 'language', 'Content-Type']) ->and($headers['api-key'])->toBe('test-api-key') @@ -90,23 +90,23 @@ it('throws exception when API credentials are missing', function () { 'bitunix-api.api_key' => '', 'bitunix-api.api_secret' => '', ]); - - expect(fn() => Header::generateSignValue([], '', 'nonce', 'timestamp')) + + expect(fn () => Header::generateSignValue([], '', 'nonce', 'timestamp')) ->toThrow(InvalidArgumentException::class, 'API key and secret must be configured'); }); it('handles empty query parameters correctly', function () { $result = Header::digestQueryParameters([]); - + expect($result)->toBe(''); }); it('handles empty body correctly', function () { $nonce = '123456'; $timestamp = '20241120123045'; - + $sign = Header::generateSignValue([], '', $nonce, $timestamp); - + expect($sign) ->toBeString() ->toHaveLength(64); @@ -117,19 +117,19 @@ it('generates consistent signature for same inputs', function () { $timestamp = '20241120123045'; $queryParams = ['id' => '1']; $body = '{"test":"value"}'; - + $sign1 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); $sign2 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); - + expect($sign1)->toBe($sign2); }); it('generates different signatures for different inputs', function () { $nonce = '123456'; $timestamp = '20241120123045'; - + $sign1 = Header::generateSignValue(['id' => '1'], '{"test":"value1"}', $nonce, $timestamp); $sign2 = Header::generateSignValue(['id' => '2'], '{"test":"value2"}', $nonce, $timestamp); - + expect($sign1)->not->toBe($sign2); }); From d561f69ece0696e0491434300961304cec19c731 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 16:02:15 +0330 Subject: [PATCH 07/22] future-private: add change margin mode request --- README.md | 190 +++++++++++++----- examples/ChangeMarginModeExample.php | 65 ++++++ scripts/check-config.php | 81 ++++++++ src/LaravelBitunixApi.php | 18 +- src/LaravelBitunixApiServiceProvider.php | 2 + .../ChangeMarginModeRequestContract.php | 18 ++ tests/ChangeMarginModeTest.php | 96 +++++++++ 7 files changed, 424 insertions(+), 46 deletions(-) create mode 100644 examples/ChangeMarginModeExample.php create mode 100644 scripts/check-config.php create mode 100644 src/Requests/ChangeMarginModeRequestContract.php create mode 100644 tests/ChangeMarginModeTest.php diff --git a/README.md b/README.md index 0be9a19..af22d74 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,184 @@ -# composer package for using bitunix api trading +# Laravel Bitunix API Package -[![Latest Version on Packagist](https://img.shields.io/packagist/v/mahdimsr/laravel-bitunix-api.svg?style=flat-square)](https://packagist.org/packages/mahdimsr/laravel-bitunix-api) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/mahdimsr/laravel-bitunix-api/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/mahdimsr/laravel-bitunix-api/actions?query=workflow%3Arun-tests+branch%3Amain) -[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/mahdimsr/laravel-bitunix-api/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/mahdimsr/laravel-bitunix-api/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) -[![Total Downloads](https://img.shields.io/packagist/dt/mahdimsr/laravel-bitunix-api.svg?style=flat-square)](https://packagist.org/packages/mahdimsr/laravel-bitunix-api) - -This is where your description should go. Limit it to a paragraph or two. Consider adding a small example. - -## Support us - -[](https://spatie.be/github-ad-click/laravel-bitunix-api) - -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). - -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). +A Laravel package for interacting with the Bitunix cryptocurrency exchange API. ## Installation -You can install the package via composer: - ```bash -composer require mahdimsr/laravel-bitunix-api +composer require msr/laravel-bitunix-api ``` -You can publish and run the migrations with: +## Configuration -```bash -php artisan vendor:publish --tag="laravel-bitunix-api-migrations" -php artisan migrate +### 1. Environment Variables + +Add the following variables to your `.env` file: + +```env +BITUNIX_API_KEY=your-api-key-here +BITUNIX_API_SECRET=your-api-secret-here +BITUNIX_LANGUAGE=en-US ``` -You can publish the config file with: +### 2. Publish Configuration (Optional) ```bash -php artisan vendor:publish --tag="laravel-bitunix-api-config" +php artisan vendor:publish --tag=bitunix-api-config ``` -This is the contents of the published config file: +### 3. Verify Configuration -```php -return [ -]; -``` - -Optionally, you can publish the views using +Run the configuration check script: ```bash -php artisan vendor:publish --tag="laravel-bitunix-api-views" +php scripts/check-config.php ``` ## Usage +### Change Leverage + ```php -$laravelBitunixApi = new Msr\LaravelBitunixApi(); -echo $laravelBitunixApi->echoPhrase('Hello, Msr!'); +use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; + +$api = app(ChangeLeverageRequestContract::class); +$response = $api->changeLeverage('BTCUSDT', 'USDT', 12); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Leverage changed successfully!"; + } +} +``` + +### Change Margin Mode + +```php +use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; + +$api = app(ChangeMarginModeRequestContract::class); +$response = $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION'); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Margin mode changed successfully!"; + } +} +``` + +### Get Future Kline Data + +```php +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; + +$api = app(FutureKLineRequestContract::class); +$response = $api->getFutureKline('BTCUSDT', '1h', 100); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + // Process kline data +} +``` + +## API Methods + +### Account Management + +- `changeLeverage(string $symbol, string $marginCoin, int $leverage)` - Change leverage +- `changeMarginMode(string $symbol, string $marginCoin, string $marginMode)` - Change margin mode + +### Market Data + +- `getFutureKline(string $symbol, string $interval, int $limit, ?int $startTime, ?int $endTime, string $type)` - Get kline data + +## Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `future_base_uri` | Bitunix API base URI | `https://fapi.bitunix.com/` | +| `api_key` | Your API key | From `BITUNIX_API_KEY` env var | +| `api_secret` | Your API secret | From `BITUNIX_API_SECRET` env var | +| `language` | API language | From `BITUNIX_LANGUAGE` env var or `en-US` | + +## Rate Limits + +- **Change Leverage**: 10 req/sec/uid +- **Change Margin Mode**: 10 req/sec/uid + +## Error Handling + +All methods return a `ResponseInterface` object. Check the response status and parse the JSON response: + +```php +$response = $api->changeLeverage('BTCUSDT', 'USDT', 12); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + + if ($data['code'] === 0) { + // Success + echo "Operation successful: " . $data['msg']; + } else { + // API Error + echo "API Error: " . $data['msg']; + } +} else { + // HTTP Error + echo "HTTP Error: " . $response->getStatusCode(); +} ``` ## Testing +Run the test suite: + ```bash -composer test +vendor/bin/pest ``` -## Changelog +Run specific tests: -Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. +```bash +vendor/bin/pest tests/ChangeLeverageTest.php +vendor/bin/pest tests/ChangeMarginModeTest.php +vendor/bin/pest tests/HeaderTest.php +``` -## Contributing +## Examples -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +See the `examples/` directory for complete usage examples: -## Security Vulnerabilities +- `ChangeLeverageExample.php` +- `ChangeMarginModeExample.php` -Please review [our security policy](../../security/policy) on how to report security vulnerabilities. +## Troubleshooting -## Credits +### API credentials not loading from .env -- [mahdi mansouri](https://github.com/mahdimsr) -- [All Contributors](../../contributors) +1. Make sure your `.env` file is in the project root +2. Check that the variable names match exactly (case-sensitive) +3. Restart your application/server after changing .env +4. Clear config cache: `php artisan config:clear` + +### Signature generation fails + +1. Verify API key and secret are correct +2. Check that the credentials have the necessary permissions +3. Ensure your system time is synchronized + +## Security + +- Never commit API credentials to version control +- Use environment variables for all sensitive data +- Restrict API key permissions to minimum required +- Regularly rotate API keys ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +MIT License. See LICENSE file for details. + +## Support + +For issues and questions, please create an issue on the GitHub repository. \ No newline at end of file diff --git a/examples/ChangeMarginModeExample.php b/examples/ChangeMarginModeExample.php new file mode 100644 index 0000000..47778e8 --- /dev/null +++ b/examples/ChangeMarginModeExample.php @@ -0,0 +1,65 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(ChangeMarginModeRequestContract::class); + + // Change margin mode for BTCUSDT pair + $symbol = 'BTCUSDT'; + $marginCoin = 'USDT'; + $marginMode = 'ISOLATION'; // or 'CROSS' + + echo "Changing margin mode for {$symbol} to {$marginMode}...\n"; + + $response = $api->changeMarginMode($symbol, $marginCoin, $marginMode); + + // Check response status + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + + if ($data['code'] === 0) { + echo "✅ Margin mode changed successfully!\n"; + echo 'Symbol: '.$data['data'][0]['symbol']."\n"; + echo 'Margin Coin: '.$data['data'][0]['marginCoin']."\n"; + echo 'Margin Mode: '.$data['data'][0]['marginMode']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Available Margin Modes: + * - ISOLATION: Isolated margin mode + * - CROSS: Cross margin mode + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/scripts/check-config.php b/scripts/check-config.php new file mode 100644 index 0000000..deed70a --- /dev/null +++ b/scripts/check-config.php @@ -0,0 +1,81 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => $apiKey, + 'bitunix-api.api_secret' => $apiSecret, + 'bitunix-api.language' => $language, +]); + +echo "🔧 Configuration Test:\n"; +echo " Base URI: " . config('bitunix-api.future_base_uri') . "\n"; +echo " API Key: " . substr(config('bitunix-api.api_key'), 0, 8) . "...\n"; +echo " API Secret: " . substr(config('bitunix-api.api_secret'), 0, 8) . "...\n"; +echo " Language: " . config('bitunix-api.language') . "\n\n"; + +// Test header generation +try { + echo "🔐 Testing Header Generation:\n"; + $headers = Header::generateHeaders([], '{"test":"value"}'); + + echo " API Key: " . $headers['api-key'] . "\n"; + echo " Sign: " . substr($headers['sign'], 0, 16) . "...\n"; + echo " Nonce: " . $headers['nonce'] . "\n"; + echo " Timestamp: " . $headers['timestamp'] . "\n"; + echo " Language: " . $headers['language'] . "\n"; + echo " Content-Type: " . $headers['Content-Type'] . "\n\n"; + + echo "✅ Configuration is working correctly!\n"; + echo "You can now use the Bitunix API package in your application.\n"; + +} catch (Exception $e) { + echo "❌ Error generating headers: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index eb2fe77..d4aa5a5 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -4,11 +4,12 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract { private Client $publicFutureClient; @@ -60,4 +61,19 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineReq return $response; } + + public function changeMarginMode(string $symbol, string $marginCoin, string $marginMode): ResponseInterface + { + $body = [ + 'symbol' => $symbol, + 'marginCoin' => $marginCoin, + 'marginMode' => $marginMode, + ]; + + $response = $this->getPrivateFutureClient([], $body)->post('account/change_margin_mode', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 589a68b..4161e5d 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -4,6 +4,7 @@ namespace Msr\LaravelBitunixApi; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -31,5 +32,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(ChangeMarginModeRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/ChangeMarginModeRequestContract.php b/src/Requests/ChangeMarginModeRequestContract.php new file mode 100644 index 0000000..3f2b6c9 --- /dev/null +++ b/src/Requests/ChangeMarginModeRequestContract.php @@ -0,0 +1,18 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can change margin mode successfully', function () { + $api = app(ChangeMarginModeRequestContract::class); + + expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) + ->not->toThrow(Exception::class); + +}); + +it('validates required parameters for change margin mode', function () { + $api = app(ChangeMarginModeRequestContract::class); + + expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) + ->not->toThrow(Exception::class); + +}); + +it('handles different margin modes correctly', function () { + $api = app(ChangeMarginModeRequestContract::class); + + $marginModes = ['ISOLATION', 'CROSS']; + + foreach ($marginModes as $mode) { + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', $mode)) + ->not->toThrow(Exception::class); + } +}); + +it('validates margin mode parameter values', function () { + $api = app(ChangeMarginModeRequestContract::class); + + expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) + ->not->toThrow(Exception::class); + +}); + +it('can handle different trading pairs', function () { + $api = app(ChangeMarginModeRequestContract::class); + + $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($tradingPairs as $symbol) { + expect(fn () => $api->changeMarginMode($symbol, 'USDT', 'ISOLATION')) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different margin coins', function () { + $api = app(ChangeMarginModeRequestContract::class); + + $marginCoins = ['USDT', 'BTC', 'ETH']; + + foreach ($marginCoins as $coin) { + expect(fn () => $api->changeMarginMode('BTCUSDT', $coin, 'ISOLATION')) + ->not->toThrow(Exception::class); + } +}); + +it('validates margin mode constants', function () { + $api = app(ChangeMarginModeRequestContract::class); + + $validModes = ['ISOLATION', 'CROSS']; + + foreach ($validModes as $mode) { + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', $mode)) + ->not->toThrow(Exception::class); + } +}); + +it('handles edge cases for margin mode', function () { + $api = app(ChangeMarginModeRequestContract::class); + + expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) + ->not->toThrow(Exception::class); + +}); From 9d70e9ae44c61f70e108d1bbbbb5cc6d7a9b787a Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 17:18:44 +0330 Subject: [PATCH 08/22] future-private: add place order request --- README.md | 48 +++++ examples/PlaceOrderExample.php | 167 +++++++++++++++ src/LaravelBitunixApi.php | 79 ++++++- src/LaravelBitunixApiServiceProvider.php | 2 + src/Requests/PlaceOrderRequestContract.php | 52 +++++ tests/PlaceOrderTest.php | 237 +++++++++++++++++++++ 6 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 examples/PlaceOrderExample.php create mode 100644 src/Requests/PlaceOrderRequestContract.php create mode 100644 tests/PlaceOrderTest.php diff --git a/README.md b/README.md index af22d74..6dde83b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,47 @@ if ($response->getStatusCode() === 200) { } ``` +### Place Order + +```php +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; + +$api = app(PlaceOrderRequestContract::class); + +// Basic market order +$response = $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET'); + +// Limit order with take profit and stop loss +$response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', // price + null, // positionId + 'GTC', // effect + 'order-123', // clientId + false, // reduceOnly + '51000', // tpPrice + 'MARK_PRICE', // tpStopType + 'LIMIT', // tpOrderType + '51000.1', // tpOrderPrice + '49000', // slPrice + 'MARK_PRICE', // slStopType + 'LIMIT', // slOrderType + '49000.1' // slOrderPrice +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -89,6 +130,10 @@ if ($response->getStatusCode() === 200) { - `changeLeverage(string $symbol, string $marginCoin, int $leverage)` - Change leverage - `changeMarginMode(string $symbol, string $marginCoin, string $marginMode)` - Change margin mode +### Trading + +- `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management + ### Market Data - `getFutureKline(string $symbol, string $interval, int $limit, ?int $startTime, ?int $endTime, string $type)` - Get kline data @@ -106,6 +151,7 @@ if ($response->getStatusCode() === 200) { - **Change Leverage**: 10 req/sec/uid - **Change Margin Mode**: 10 req/sec/uid +- **Place Order**: 10 req/sec/uid ## Error Handling @@ -143,6 +189,7 @@ Run specific tests: ```bash vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php +vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/HeaderTest.php ``` @@ -152,6 +199,7 @@ See the `examples/` directory for complete usage examples: - `ChangeLeverageExample.php` - `ChangeMarginModeExample.php` +- `PlaceOrderExample.php` ## Troubleshooting diff --git a/examples/PlaceOrderExample.php b/examples/PlaceOrderExample.php new file mode 100644 index 0000000..e00478c --- /dev/null +++ b/examples/PlaceOrderExample.php @@ -0,0 +1,167 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlaceOrderRequestContract::class); + + echo "🚀 Placing Orders Examples\n\n"; + + // Example 1: Basic Market Order + echo "1. Placing a basic market order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Market order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 2: Limit Order with Take Profit and Stop Loss + echo "2. Placing a limit order with TP/SL...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', // price + null, // positionId + 'GTC', // effect + 'order-123', // clientId + false, // reduceOnly + '51000', // tpPrice + 'MARK_PRICE', // tpStopType + 'LIMIT', // tpOrderType + '51000.1', // tpOrderPrice + '49000', // slPrice + 'MARK_PRICE', // slStopType + 'LIMIT', // slOrderType + '49000.1' // slOrderPrice + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Limit order with TP/SL placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + echo "Client ID: " . $data['data']['clientId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 3: Close Position Order + echo "3. Placing a close position order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123' // positionId required for CLOSE + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Close position order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 4: Reduce Only Order + echo "4. Placing a reduce only order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.05', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123', + null, + null, + true // reduceOnly + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Reduce only order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Order Types: + * - LIMIT: Limit orders (requires price) + * - MARKET: Market orders + * + * Order Sides: + * - BUY: Buy order + * - SELL: Sell order + * + * Trade Sides: + * - OPEN: Open a new position + * - CLOSE: Close an existing position (requires positionId) + * + * Effect Types: + * - IOC: Immediate or cancel + * - FOK: Fill or kill + * - GTC: Good till canceled (default) + * - POST_ONLY: POST only + * + * Take Profit / Stop Loss Types: + * - MARK_PRICE: Mark price + * - LAST_PRICE: Last price + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index d4aa5a5..33d976e 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -7,9 +7,10 @@ use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract, PlaceOrderRequestContract { private Client $publicFutureClient; @@ -76,4 +77,80 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placeOrder( + string $symbol, + string $qty, + string $side, + string $tradeSide, + string $orderType, + ?string $price = null, + ?string $positionId = null, + ?string $effect = null, + ?string $clientId = null, + ?bool $reduceOnly = null, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $tpOrderType = null, + ?string $tpOrderPrice = null, + ?string $slPrice = null, + ?string $slStopType = null, + ?string $slOrderType = null, + ?string $slOrderPrice = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'qty' => $qty, + 'side' => $side, + 'tradeSide' => $tradeSide, + 'orderType' => $orderType, + ]; + + // Add optional parameters if provided + if ($price !== null) { + $body['price'] = $price; + } + if ($positionId !== null) { + $body['positionId'] = $positionId; + } + if ($effect !== null) { + $body['effect'] = $effect; + } + if ($clientId !== null) { + $body['clientId'] = $clientId; + } + if ($reduceOnly !== null) { + $body['reduceOnly'] = $reduceOnly; + } + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($tpOrderType !== null) { + $body['tpOrderType'] = $tpOrderType; + } + if ($tpOrderPrice !== null) { + $body['tpOrderPrice'] = $tpOrderPrice; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + if ($slOrderType !== null) { + $body['slOrderType'] = $slOrderType; + } + if ($slOrderPrice !== null) { + $body['slOrderPrice'] = $slOrderPrice; + } + + $response = $this->getPrivateFutureClient([], $body)->post('trade/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 4161e5d..8360620 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -6,6 +6,7 @@ use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -33,5 +34,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeMarginModeRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlaceOrderRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/PlaceOrderRequestContract.php b/src/Requests/PlaceOrderRequestContract.php new file mode 100644 index 0000000..54f261f --- /dev/null +++ b/src/Requests/PlaceOrderRequestContract.php @@ -0,0 +1,52 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place a basic market order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET' + ))->not->toThrow(Exception::class); +}); + +it('can place a limit order with price', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000' + ))->not->toThrow(Exception::class); +}); + +it('can place an order with all optional parameters', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + '12345', + 'GTC', + 'custom-client-id', + false, + '51000', + 'MARK_PRICE', + 'LIMIT', + '51000.1', + '49000', + 'MARK_PRICE', + 'LIMIT', + '49000.1' + ))->not->toThrow(Exception::class); +}); + +it('can place a close position order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123' + ))->not->toThrow(Exception::class); +}); + +it('validates required parameters for place order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->placeOrder('BTCUSDT', '0.1', 'SELL', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); +}); + +it('handles different order types correctly', function () { + $api = app(PlaceOrderRequestContract::class); + + $orderTypes = ['LIMIT', 'MARKET']; + + foreach ($orderTypes as $orderType) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + $orderType, + $orderType === 'LIMIT' ? '50000' : null + ))->not->toThrow(Exception::class); + } +}); + +it('handles different trade sides correctly', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradeSides = ['OPEN', 'CLOSE']; + + foreach ($tradeSides as $tradeSide) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + $tradeSide, + 'MARKET', + null, + $tradeSide === 'CLOSE' ? 'position-123' : null + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different trading pairs', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($tradingPairs as $symbol) { + expect(fn() => $api->placeOrder($symbol, '0.1', 'BUY', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); + } +}); + +it('validates order side parameter values', function () { + $api = app(PlaceOrderRequestContract::class); + + $sides = ['BUY', 'SELL']; + + foreach ($sides as $side) { + expect(fn() => $api->placeOrder('BTCUSDT', '0.1', $side, 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); + } +}); + +it('validates trade side parameter values', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradeSides = ['OPEN', 'CLOSE']; + + foreach ($tradeSides as $tradeSide) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + $tradeSide, + 'MARKET', + null, + $tradeSide === 'CLOSE' ? 'position-123' : null + ))->not->toThrow(Exception::class); + } +}); + +it('can handle take profit and stop loss parameters', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + null, + 'GTC', + null, + false, + '51000', + 'MARK_PRICE', + 'LIMIT', + '51000.1', + '49000', + 'MARK_PRICE', + 'LIMIT', + '49000.1' + ))->not->toThrow(Exception::class); +}); + +it('can handle reduce only orders', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123', + null, + null, + true + ))->not->toThrow(Exception::class); +}); + +it('can handle custom client ID', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET', + null, + null, + null, + 'custom-order-123' + ))->not->toThrow(Exception::class); +}); + +it('can handle different effect types', function () { + $api = app(PlaceOrderRequestContract::class); + + $effects = ['IOC', 'FOK', 'GTC', 'POST_ONLY']; + + foreach ($effects as $effect) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + null, + $effect + ))->not->toThrow(Exception::class); + } +}); From 138a4f410907b056f8c875f09b5fbcf92d711211 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 17:19:03 +0330 Subject: [PATCH 09/22] future-private: add support for laravel 10 --- composer.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index bfee88c..4a17ef7 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^8.4", "spatie/laravel-package-tools": "^1.16", - "illuminate/contracts": "^11.0||^12.0" + "illuminate/contracts": "^11.0||^12.0||^10.0" }, "require-dev": { "laravel/pint": "^1.14", @@ -69,6 +69,7 @@ } } }, - "minimum-stability": "dev", + "version": "1.0.0", + "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} From 4d0a9ebe56bdd9d0bfe9616649a3a4696d324bb1 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 23:36:57 +0330 Subject: [PATCH 10/22] future-private: add place order request --- examples/PlaceOrderExample.php | 1 + src/Requests/PlaceOrderRequestContract.php | 1 + tests/PlaceOrderTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/examples/PlaceOrderExample.php b/examples/PlaceOrderExample.php index e00478c..12f21a4 100644 --- a/examples/PlaceOrderExample.php +++ b/examples/PlaceOrderExample.php @@ -165,3 +165,4 @@ try { * BITUNIX_API_SECRET=your-api-secret * BITUNIX_LANGUAGE=en-US */ + diff --git a/src/Requests/PlaceOrderRequestContract.php b/src/Requests/PlaceOrderRequestContract.php index 54f261f..6cd136c 100644 --- a/src/Requests/PlaceOrderRequestContract.php +++ b/src/Requests/PlaceOrderRequestContract.php @@ -50,3 +50,4 @@ interface PlaceOrderRequestContract ?string $slOrderPrice = null ): ResponseInterface; } + diff --git a/tests/PlaceOrderTest.php b/tests/PlaceOrderTest.php index a018d6f..c429d40 100644 --- a/tests/PlaceOrderTest.php +++ b/tests/PlaceOrderTest.php @@ -235,3 +235,4 @@ it('can handle different effect types', function () { ))->not->toThrow(Exception::class); } }); + From 10e9a84d3611d34988189ddc3efcdca488cd902d Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Sun, 28 Sep 2025 20:07:30 +0000 Subject: [PATCH 11/22] Fix styling --- examples/PlaceOrderExample.php | 19 ++++---- scripts/check-config.php | 46 +++++++++---------- .../ChangeMarginModeRequestContract.php | 7 ++- src/Requests/PlaceOrderRequestContract.php | 38 ++++++++------- tests/ChangeMarginModeTest.php | 16 +++---- tests/PlaceOrderTest.php | 31 ++++++------- 6 files changed, 76 insertions(+), 81 deletions(-) diff --git a/examples/PlaceOrderExample.php b/examples/PlaceOrderExample.php index 12f21a4..14a9843 100644 --- a/examples/PlaceOrderExample.php +++ b/examples/PlaceOrderExample.php @@ -39,9 +39,9 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Market order placed successfully!\n"; - echo "Order ID: " . $data['data']['orderId'] . "\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } @@ -74,10 +74,10 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Limit order with TP/SL placed successfully!\n"; - echo "Order ID: " . $data['data']['orderId'] . "\n"; - echo "Client ID: " . $data['data']['clientId'] . "\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + echo 'Client ID: '.$data['data']['clientId']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } @@ -99,9 +99,9 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Close position order placed successfully!\n"; - echo "Order ID: " . $data['data']['orderId'] . "\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } @@ -126,9 +126,9 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Reduce only order placed successfully!\n"; - echo "Order ID: " . $data['data']['orderId'] . "\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } @@ -165,4 +165,3 @@ try { * BITUNIX_API_SECRET=your-api-secret * BITUNIX_LANGUAGE=en-US */ - diff --git a/scripts/check-config.php b/scripts/check-config.php index deed70a..fe03a15 100644 --- a/scripts/check-config.php +++ b/scripts/check-config.php @@ -2,19 +2,19 @@ /** * Configuration Check Script - * + * * This script helps you verify that your Bitunix API configuration is working correctly. */ -require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__.'/../vendor/autoload.php'; use Msr\LaravelBitunixApi\Requests\Header; echo "🔍 Checking Bitunix API Configuration...\n\n"; // Check if .env file exists -$envFile = __DIR__ . '/../.env'; -if (!file_exists($envFile)) { +$envFile = __DIR__.'/../.env'; +if (! file_exists($envFile)) { echo "❌ .env file not found. Please create one based on .env.example\n"; exit(1); } @@ -24,8 +24,8 @@ if (file_exists($envFile)) { $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (strpos($line, '=') !== false && strpos($line, '#') !== 0) { - list($key, $value) = explode('=', $line, 2); - putenv(trim($key) . '=' . trim($value)); + [$key, $value] = explode('=', $line, 2); + putenv(trim($key).'='.trim($value)); } } } @@ -36,9 +36,9 @@ $apiSecret = getenv('BITUNIX_API_SECRET'); $language = getenv('BITUNIX_LANGUAGE') ?: 'en-US'; echo "📋 Environment Variables:\n"; -echo " BITUNIX_API_KEY: " . (empty($apiKey) ? "❌ Not set" : "✅ Set (" . substr($apiKey, 0, 8) . "...)") . "\n"; -echo " BITUNIX_API_SECRET: " . (empty($apiSecret) ? "❌ Not set" : "✅ Set (" . substr($apiSecret, 0, 8) . "...)") . "\n"; -echo " BITUNIX_LANGUAGE: " . ($language) . "\n\n"; +echo ' BITUNIX_API_KEY: '.(empty($apiKey) ? '❌ Not set' : '✅ Set ('.substr($apiKey, 0, 8).'...)')."\n"; +echo ' BITUNIX_API_SECRET: '.(empty($apiSecret) ? '❌ Not set' : '✅ Set ('.substr($apiSecret, 0, 8).'...)')."\n"; +echo ' BITUNIX_LANGUAGE: '.($language)."\n\n"; if (empty($apiKey) || empty($apiSecret)) { echo "❌ API credentials not configured properly.\n"; @@ -55,27 +55,27 @@ config([ ]); echo "🔧 Configuration Test:\n"; -echo " Base URI: " . config('bitunix-api.future_base_uri') . "\n"; -echo " API Key: " . substr(config('bitunix-api.api_key'), 0, 8) . "...\n"; -echo " API Secret: " . substr(config('bitunix-api.api_secret'), 0, 8) . "...\n"; -echo " Language: " . config('bitunix-api.language') . "\n\n"; +echo ' Base URI: '.config('bitunix-api.future_base_uri')."\n"; +echo ' API Key: '.substr(config('bitunix-api.api_key'), 0, 8)."...\n"; +echo ' API Secret: '.substr(config('bitunix-api.api_secret'), 0, 8)."...\n"; +echo ' Language: '.config('bitunix-api.language')."\n\n"; // Test header generation try { echo "🔐 Testing Header Generation:\n"; $headers = Header::generateHeaders([], '{"test":"value"}'); - - echo " API Key: " . $headers['api-key'] . "\n"; - echo " Sign: " . substr($headers['sign'], 0, 16) . "...\n"; - echo " Nonce: " . $headers['nonce'] . "\n"; - echo " Timestamp: " . $headers['timestamp'] . "\n"; - echo " Language: " . $headers['language'] . "\n"; - echo " Content-Type: " . $headers['Content-Type'] . "\n\n"; - + + echo ' API Key: '.$headers['api-key']."\n"; + echo ' Sign: '.substr($headers['sign'], 0, 16)."...\n"; + echo ' Nonce: '.$headers['nonce']."\n"; + echo ' Timestamp: '.$headers['timestamp']."\n"; + echo ' Language: '.$headers['language']."\n"; + echo ' Content-Type: '.$headers['Content-Type']."\n\n"; + echo "✅ Configuration is working correctly!\n"; echo "You can now use the Bitunix API package in your application.\n"; - + } catch (Exception $e) { - echo "❌ Error generating headers: " . $e->getMessage() . "\n"; + echo '❌ Error generating headers: '.$e->getMessage()."\n"; exit(1); } diff --git a/src/Requests/ChangeMarginModeRequestContract.php b/src/Requests/ChangeMarginModeRequestContract.php index 3f2b6c9..1abaeef 100644 --- a/src/Requests/ChangeMarginModeRequestContract.php +++ b/src/Requests/ChangeMarginModeRequestContract.php @@ -9,10 +9,9 @@ interface ChangeMarginModeRequestContract /** * Change margin mode for a trading pair * - * @param string $symbol Trading pair (e.g., 'BTCUSDT') - * @param string $marginCoin Margin coin (e.g., 'USDT') - * @param string $marginMode Margin mode ('ISOLATION' or 'CROSS') - * @return ResponseInterface + * @param string $symbol Trading pair (e.g., 'BTCUSDT') + * @param string $marginCoin Margin coin (e.g., 'USDT') + * @param string $marginMode Margin mode ('ISOLATION' or 'CROSS') */ public function changeMarginMode(string $symbol, string $marginCoin, string $marginMode): ResponseInterface; } diff --git a/src/Requests/PlaceOrderRequestContract.php b/src/Requests/PlaceOrderRequestContract.php index 6cd136c..47a0488 100644 --- a/src/Requests/PlaceOrderRequestContract.php +++ b/src/Requests/PlaceOrderRequestContract.php @@ -9,25 +9,24 @@ interface PlaceOrderRequestContract /** * Place a new order * - * @param string $symbol Trading pair (e.g., 'BTCUSDT') - * @param string $qty Amount (base coin) - * @param string $side Order direction ('BUY' or 'SELL') - * @param string $tradeSide Direction ('OPEN' or 'CLOSE') - * @param string $orderType Order type ('LIMIT' or 'MARKET') - * @param string|null $price Price of the order (required for LIMIT orders) - * @param string|null $positionId Position ID (required when tradeSide is 'CLOSE') - * @param string|null $effect Order expiration date - * @param string|null $clientId Customize order ID - * @param bool|null $reduceOnly Whether to just reduce the position - * @param string|null $tpPrice Take profit trigger price - * @param string|null $tpStopType Take profit trigger type - * @param string|null $tpOrderType Take profit trigger place order type - * @param string|null $tpOrderPrice Take profit trigger place order price - * @param string|null $slPrice Stop loss trigger price - * @param string|null $slStopType Stop loss trigger type - * @param string|null $slOrderType Stop loss trigger place order type - * @param string|null $slOrderPrice Stop loss trigger place order price - * @return ResponseInterface + * @param string $symbol Trading pair (e.g., 'BTCUSDT') + * @param string $qty Amount (base coin) + * @param string $side Order direction ('BUY' or 'SELL') + * @param string $tradeSide Direction ('OPEN' or 'CLOSE') + * @param string $orderType Order type ('LIMIT' or 'MARKET') + * @param string|null $price Price of the order (required for LIMIT orders) + * @param string|null $positionId Position ID (required when tradeSide is 'CLOSE') + * @param string|null $effect Order expiration date + * @param string|null $clientId Customize order ID + * @param bool|null $reduceOnly Whether to just reduce the position + * @param string|null $tpPrice Take profit trigger price + * @param string|null $tpStopType Take profit trigger type + * @param string|null $tpOrderType Take profit trigger place order type + * @param string|null $tpOrderPrice Take profit trigger place order price + * @param string|null $slPrice Stop loss trigger price + * @param string|null $slStopType Stop loss trigger type + * @param string|null $slOrderType Stop loss trigger place order type + * @param string|null $slOrderPrice Stop loss trigger place order price */ public function placeOrder( string $symbol, @@ -50,4 +49,3 @@ interface PlaceOrderRequestContract ?string $slOrderPrice = null ): ResponseInterface; } - diff --git a/tests/ChangeMarginModeTest.php b/tests/ChangeMarginModeTest.php index 7ffa918..b811057 100644 --- a/tests/ChangeMarginModeTest.php +++ b/tests/ChangeMarginModeTest.php @@ -14,9 +14,9 @@ beforeEach(function () { it('can change margin mode successfully', function () { $api = app(ChangeMarginModeRequestContract::class); - expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) + ->and(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) ->not->toThrow(Exception::class); }); @@ -24,9 +24,9 @@ it('can change margin mode successfully', function () { it('validates required parameters for change margin mode', function () { $api = app(ChangeMarginModeRequestContract::class); - expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) + ->and(fn () => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) ->not->toThrow(Exception::class); }); @@ -45,9 +45,9 @@ it('handles different margin modes correctly', function () { it('validates margin mode parameter values', function () { $api = app(ChangeMarginModeRequestContract::class); - expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) + ->and(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'CROSS')) ->not->toThrow(Exception::class); }); @@ -88,9 +88,9 @@ it('validates margin mode constants', function () { it('handles edge cases for margin mode', function () { $api = app(ChangeMarginModeRequestContract::class); - expect(fn() => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) + expect(fn () => $api->changeMarginMode('BTCUSDT', 'USDT', 'ISOLATION')) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) + ->and(fn () => $api->changeMarginMode('ETHUSDT', 'USDT', 'CROSS')) ->not->toThrow(Exception::class); }); diff --git a/tests/PlaceOrderTest.php b/tests/PlaceOrderTest.php index c429d40..8485bdc 100644 --- a/tests/PlaceOrderTest.php +++ b/tests/PlaceOrderTest.php @@ -14,7 +14,7 @@ beforeEach(function () { it('can place a basic market order', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -26,7 +26,7 @@ it('can place a basic market order', function () { it('can place a limit order with price', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -39,7 +39,7 @@ it('can place a limit order with price', function () { it('can place an order with all optional parameters', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -64,7 +64,7 @@ it('can place an order with all optional parameters', function () { it('can place a close position order', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'SELL', @@ -78,9 +78,9 @@ it('can place a close position order', function () { it('validates required parameters for place order', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET')) + expect(fn () => $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET')) ->not->toThrow(Exception::class) - ->and(fn() => $api->placeOrder('BTCUSDT', '0.1', 'SELL', 'OPEN', 'MARKET')) + ->and(fn () => $api->placeOrder('BTCUSDT', '0.1', 'SELL', 'OPEN', 'MARKET')) ->not->toThrow(Exception::class); }); @@ -90,7 +90,7 @@ it('handles different order types correctly', function () { $orderTypes = ['LIMIT', 'MARKET']; foreach ($orderTypes as $orderType) { - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -107,7 +107,7 @@ it('handles different trade sides correctly', function () { $tradeSides = ['OPEN', 'CLOSE']; foreach ($tradeSides as $tradeSide) { - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -125,7 +125,7 @@ it('can handle different trading pairs', function () { $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; foreach ($tradingPairs as $symbol) { - expect(fn() => $api->placeOrder($symbol, '0.1', 'BUY', 'OPEN', 'MARKET')) + expect(fn () => $api->placeOrder($symbol, '0.1', 'BUY', 'OPEN', 'MARKET')) ->not->toThrow(Exception::class); } }); @@ -136,7 +136,7 @@ it('validates order side parameter values', function () { $sides = ['BUY', 'SELL']; foreach ($sides as $side) { - expect(fn() => $api->placeOrder('BTCUSDT', '0.1', $side, 'OPEN', 'MARKET')) + expect(fn () => $api->placeOrder('BTCUSDT', '0.1', $side, 'OPEN', 'MARKET')) ->not->toThrow(Exception::class); } }); @@ -147,7 +147,7 @@ it('validates trade side parameter values', function () { $tradeSides = ['OPEN', 'CLOSE']; foreach ($tradeSides as $tradeSide) { - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -162,7 +162,7 @@ it('validates trade side parameter values', function () { it('can handle take profit and stop loss parameters', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -187,7 +187,7 @@ it('can handle take profit and stop loss parameters', function () { it('can handle reduce only orders', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'SELL', @@ -204,7 +204,7 @@ it('can handle reduce only orders', function () { it('can handle custom client ID', function () { $api = app(PlaceOrderRequestContract::class); - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -223,7 +223,7 @@ it('can handle different effect types', function () { $effects = ['IOC', 'FOK', 'GTC', 'POST_ONLY']; foreach ($effects as $effect) { - expect(fn() => $api->placeOrder( + expect(fn () => $api->placeOrder( 'BTCUSDT', '0.1', 'BUY', @@ -235,4 +235,3 @@ it('can handle different effect types', function () { ))->not->toThrow(Exception::class); } }); - From e86854ca54c6266d5707fc5e8348437d34871356 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 23:58:03 +0330 Subject: [PATCH 12/22] future-private: add flash close position request --- README.md | 21 ++++ examples/FlashClosePositionExample.php | 117 ++++++++++++++++++ src/LaravelBitunixApi.php | 16 ++- src/LaravelBitunixApiServiceProvider.php | 2 + .../FlashClosePositionRequestContract.php | 16 +++ tests/FlashClosePositionTest.php | 111 +++++++++++++++++ 6 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 examples/FlashClosePositionExample.php create mode 100644 src/Requests/FlashClosePositionRequestContract.php create mode 100644 tests/FlashClosePositionTest.php diff --git a/README.md b/README.md index 6dde83b..9707825 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,23 @@ if ($response->getStatusCode() === 200) { } ``` +### Flash Close Position + +```php +use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; + +$api = app(FlashClosePositionRequestContract::class); +$response = $api->flashClosePosition('19848247723672'); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Position flash closed successfully!"; + echo "Position ID: " . $data['data']['positionId']; + } +} +``` + ### Get Future Kline Data ```php @@ -133,6 +150,7 @@ if ($response->getStatusCode() === 200) { ### Trading - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management +- `flashClosePosition(string $positionId)` - Flash close position by position ID ### Market Data @@ -152,6 +170,7 @@ if ($response->getStatusCode() === 200) { - **Change Leverage**: 10 req/sec/uid - **Change Margin Mode**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid +- **Flash Close Position**: 5 req/sec/uid ## Error Handling @@ -190,6 +209,7 @@ Run specific tests: vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/PlaceOrderTest.php +vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/HeaderTest.php ``` @@ -200,6 +220,7 @@ See the `examples/` directory for complete usage examples: - `ChangeLeverageExample.php` - `ChangeMarginModeExample.php` - `PlaceOrderExample.php` +- `FlashClosePositionExample.php` ## Troubleshooting diff --git a/examples/FlashClosePositionExample.php b/examples/FlashClosePositionExample.php new file mode 100644 index 0000000..aa76ef1 --- /dev/null +++ b/examples/FlashClosePositionExample.php @@ -0,0 +1,117 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(FlashClosePositionRequestContract::class); + + echo "⚡ Flash Close Position Examples\n\n"; + + // Example 1: Flash close a single position + echo "1. Flash closing position...\n"; + $positionId = '19848247723672'; + + $response = $api->flashClosePosition($positionId); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position flash closed successfully!\n"; + echo "Position ID: " . $data['data']['positionId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 2: Flash close multiple positions + echo "2. Flash closing multiple positions...\n"; + $positionIds = [ + '19848247723672', + '19848247723673', + '19848247723674' + ]; + + foreach ($positionIds as $positionId) { + echo "Closing position: {$positionId}...\n"; + + $response = $api->flashClosePosition($positionId); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} closed successfully!\n"; + } else { + echo "❌ Failed to close position {$positionId}: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error for position {$positionId}: " . $response->getStatusCode() . "\n"; + } + } + + echo "\n"; + + // Example 3: Error handling + echo "3. Error handling example...\n"; + $invalidPositionId = 'invalid-position-id'; + + $response = $api->flashClosePosition($invalidPositionId); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position closed successfully!\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + echo "This is expected for invalid position ID\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Flash Close Position Features: + * + * - Closes position by position ID + * - Rate limit: 5 req/sec/uid + * - Immediate position closure + * - No additional parameters required + * + * Important Notes: + * + * - Position ID must be valid and exist + * - Position must be open to be closed + * - This is an immediate action (flash close) + * - Use with caution as it closes positions immediately + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 33d976e..9f3a48c 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -5,12 +5,13 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; +use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract, PlaceOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, PlaceOrderRequestContract { private Client $publicFutureClient; @@ -153,4 +154,17 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function flashClosePosition(string $positionId): ResponseInterface + { + $body = [ + 'positionId' => $positionId, + ]; + + $response = $this->getPrivateFutureClient([], $body)->post('trade/flash_close_position', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 8360620..1b69606 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -5,6 +5,7 @@ namespace Msr\LaravelBitunixApi; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; +use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Spatie\LaravelPackageTools\Package; @@ -35,5 +36,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeMarginModeRequestContract::class, LaravelBitunixApi::class); $this->app->bind(PlaceOrderRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(FlashClosePositionRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/FlashClosePositionRequestContract.php b/src/Requests/FlashClosePositionRequestContract.php new file mode 100644 index 0000000..ad0117a --- /dev/null +++ b/src/Requests/FlashClosePositionRequestContract.php @@ -0,0 +1,16 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can flash close position successfully', function () { + $api = app(FlashClosePositionRequestContract::class); + + expect(fn() => $api->flashClosePosition('19848247723672')) + ->not->toThrow(Exception::class); +}); + +it('validates required position ID parameter', function () { + $api = app(FlashClosePositionRequestContract::class); + + // Test with valid position ID + expect(fn() => $api->flashClosePosition('19848247723672')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->flashClosePosition('123456789')) + ->not->toThrow(Exception::class); +}); + +it('can handle different position ID formats', function () { + $api = app(FlashClosePositionRequestContract::class); + + $positionIds = [ + '19848247723672', + '123456789', + '987654321', + 'position-123', + 'pos_456' + ]; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->flashClosePosition($positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('validates position ID parameter type', function () { + $api = app(FlashClosePositionRequestContract::class); + + // Test with string position ID + expect(fn() => $api->flashClosePosition('19848247723672')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->flashClosePosition('123456789')) + ->not->toThrow(Exception::class); +}); + +it('can handle edge cases for position ID', function () { + $api = app(FlashClosePositionRequestContract::class); + + // Test with long position ID + expect(fn() => $api->flashClosePosition('198482477236721234567890')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->flashClosePosition('123')) + ->not->toThrow(Exception::class); +}); + +it('validates flash close position method exists', function () { + $api = app(FlashClosePositionRequestContract::class); + + expect(method_exists($api, 'flashClosePosition'))->toBeTrue(); +}); + +it('can handle multiple flash close position calls', function () { + $api = app(FlashClosePositionRequestContract::class); + + $positionIds = ['19848247723672', '19848247723673', '19848247723674']; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->flashClosePosition($positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('validates flash close position response structure', function () { + $api = app(FlashClosePositionRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->flashClosePosition('19848247723672')) + ->not->toThrow(Exception::class); +}); + +it('can handle special characters in position ID', function () { + $api = app(FlashClosePositionRequestContract::class); + + // Test with position ID containing special characters + expect(fn() => $api->flashClosePosition('pos-123_456')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->flashClosePosition('pos.123.456')) + ->not->toThrow(Exception::class); +}); + +it('validates flash close position with empty string', function () { + $api = app(FlashClosePositionRequestContract::class); + + // This should not throw an exception at the method level + // The API will handle validation + expect(fn() => $api->flashClosePosition('')) + ->not->toThrow(Exception::class); +}); From 8d77aebb6408d6cb2e996f1cf97ef1eb8a4abee6 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 00:16:31 +0330 Subject: [PATCH 13/22] future-private: FIX signature for case empty body and have only query param --- src/Requests/Header.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Requests/Header.php b/src/Requests/Header.php index 2192635..d1fb011 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -21,6 +21,7 @@ class Header /** * Convert sorted parameters to string format * Example: ["id" => "1", "uid" => "200"] becomes "id1uid200" + * According to Bitunix documentation: String queryParams = "id1uid200" */ public static function digestQueryParameters(array $parameters): string { @@ -32,7 +33,7 @@ class Header $result = ''; foreach ($sortedParameters as $key => $value) { - $result .= $key.$value; + $result .= $key . $value; } return $result; @@ -78,8 +79,12 @@ class Header // Step 2: Remove all spaces from body (already done if JSON encoded properly) $bodyString = trim($body); - // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) - $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString.$bodyString; + // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body (if not empty)) + if (strlen($bodyString) == 0) { + $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString; + }else{ + $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString.$bodyString; + } $digest = hash('sha256', $digestInput); // Step 4: Create sign: SHA256(digest + secretKey) From 18966eaad852de2a69b173cea12b46b20467f27a Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 00:16:55 +0330 Subject: [PATCH 14/22] future-private: add getPendingPositions request --- README.md | 40 ++++ examples/GetPendingPositionsExample.php | 181 ++++++++++++++++++ src/LaravelBitunixApi.php | 24 ++- src/LaravelBitunixApiServiceProvider.php | 2 + .../GetPendingPositionsRequestContract.php | 17 ++ tests/GetPendingPositionsTest.php | 149 ++++++++++++++ 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 examples/GetPendingPositionsExample.php create mode 100644 src/Requests/GetPendingPositionsRequestContract.php create mode 100644 tests/GetPendingPositionsTest.php diff --git a/README.md b/README.md index 9707825..9b32f9f 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,39 @@ if ($response->getStatusCode() === 200) { } ``` +### Get Pending Positions + +```php +use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; + +$api = app(GetPendingPositionsRequestContract::class); + +// Get all pending positions +$response = $api->getPendingPositions(); + +// Get positions by symbol +$response = $api->getPendingPositions('BTCUSDT'); + +// Get specific position by ID +$response = $api->getPendingPositions(null, '19848247723672'); + +// Get positions with both symbol and position ID +$response = $api->getPendingPositions('BTCUSDT', '19848247723672'); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Positions retrieved successfully!"; + foreach ($data['data'] as $position) { + echo "Position ID: " . $position['positionId']; + echo "Symbol: " . $position['symbol']; + echo "Side: " . $position['side']; + echo "Unrealized PnL: " . $position['unrealizedPNL']; + } + } +} +``` + ### Get Future Kline Data ```php @@ -152,6 +185,10 @@ if ($response->getStatusCode() === 200) { - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management - `flashClosePosition(string $positionId)` - Flash close position by position ID +### Position Management + +- `getPendingPositions(?string $symbol, ?string $positionId)` - Get pending positions with optional filtering + ### Market Data - `getFutureKline(string $symbol, string $interval, int $limit, ?int $startTime, ?int $endTime, string $type)` - Get kline data @@ -171,6 +208,7 @@ if ($response->getStatusCode() === 200) { - **Change Margin Mode**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid +- **Get Pending Positions**: 10 req/sec/uid ## Error Handling @@ -210,6 +248,7 @@ vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php +vendor/bin/pest tests/GetPendingPositionsTest.php vendor/bin/pest tests/HeaderTest.php ``` @@ -221,6 +260,7 @@ See the `examples/` directory for complete usage examples: - `ChangeMarginModeExample.php` - `PlaceOrderExample.php` - `FlashClosePositionExample.php` +- `GetPendingPositionsExample.php` ## Troubleshooting diff --git a/examples/GetPendingPositionsExample.php b/examples/GetPendingPositionsExample.php new file mode 100644 index 0000000..ed475d5 --- /dev/null +++ b/examples/GetPendingPositionsExample.php @@ -0,0 +1,181 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(GetPendingPositionsRequestContract::class); + + echo "📊 Get Pending Positions Examples\n\n"; + + // Example 1: Get all pending positions + echo "1. Getting all pending positions...\n"; + $response = $api->getPendingPositions(); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Pending positions retrieved successfully!\n"; + echo "Number of positions: " . count($data['data']) . "\n"; + + foreach ($data['data'] as $position) { + echo " - Position ID: " . $position['positionId'] . "\n"; + echo " Symbol: " . $position['symbol'] . "\n"; + echo " Side: " . $position['side'] . "\n"; + echo " Quantity: " . $position['qty'] . "\n"; + echo " Unrealized PnL: " . $position['unrealizedPNL'] . "\n"; + echo " Margin: " . $position['margin'] . "\n"; + echo " Leverage: " . $position['leverage'] . "\n"; + echo " ---\n"; + } + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 2: Get pending positions by symbol + echo "2. Getting pending positions for BTCUSDT...\n"; + $response = $api->getPendingPositions('BTCUSDT'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ BTCUSDT pending positions retrieved successfully!\n"; + echo "Number of BTCUSDT positions: " . count($data['data']) . "\n"; + + foreach ($data['data'] as $position) { + echo " - Position ID: " . $position['positionId'] . "\n"; + echo " Entry Value: " . $position['entryValue'] . "\n"; + echo " Average Open Price: " . $position['avgOpenPrice'] . "\n"; + echo " Liquidation Price: " . $position['liqPrice'] . "\n"; + echo " Margin Rate: " . $position['marginRate'] . "\n"; + echo " ---\n"; + } + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 3: Get specific position by ID + echo "3. Getting specific position by ID...\n"; + $positionId = '19848247723672'; + $response = $api->getPendingPositions(null, $positionId); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} retrieved successfully!\n"; + + if (!empty($data['data'])) { + $position = $data['data'][0]; + echo " Position Details:\n"; + echo " Position ID: " . $position['positionId'] . "\n"; + echo " Symbol: " . $position['symbol'] . "\n"; + echo " Side: " . $position['side'] . "\n"; + echo " Quantity: " . $position['qty'] . "\n"; + echo " Entry Value: " . $position['entryValue'] . "\n"; + echo " Average Open Price: " . $position['avgOpenPrice'] . "\n"; + echo " Unrealized PnL: " . $position['unrealizedPNL'] . "\n"; + echo " Realized PnL: " . $position['realizedPNL'] . "\n"; + echo " Margin: " . $position['margin'] . "\n"; + echo " Leverage: " . $position['leverage'] . "\n"; + echo " Margin Mode: " . $position['marginMode'] . "\n"; + echo " Position Mode: " . $position['positionMode'] . "\n"; + echo " Liquidation Price: " . $position['liqPrice'] . "\n"; + echo " Margin Rate: " . $position['marginRate'] . "\n"; + echo " Fee: " . $position['fee'] . "\n"; + echo " Funding: " . $position['funding'] . "\n"; + echo " Created: " . date('Y-m-d H:i:s', $position['ctime'] / 1000) . "\n"; + echo " Modified: " . date('Y-m-d H:i:s', $position['mtime'] / 1000) . "\n"; + } else { + echo "No position found with ID: {$positionId}\n"; + } + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 4: Get positions with both symbol and position ID + echo "4. Getting positions with both symbol and position ID...\n"; + $response = $api->getPendingPositions('BTCUSDT', '19848247723672'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Filtered positions retrieved successfully!\n"; + echo "Number of filtered positions: " . count($data['data']) . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Get Pending Positions Features: + * + * - Get all pending positions + * - Filter by trading pair (symbol) + * - Filter by position ID + * - Get detailed position information + * - Rate limit: 10 req/sec/uid + * + * Response includes: + * - positionId: Position ID + * - symbol: Trading pair + * - qty: Position amount + * - entryValue: Available amount for positions + * - side: LONG or SHORT + * - marginMode: ISOLATION or CROSS + * - positionMode: ONE_WAY or HEDGE + * - leverage: Leverage value + * - fee: Transaction fees + * - funding: Total funding fee + * - realizedPNL: Realized PnL + * - margin: Locked asset + * - unrealizedPNL: Unrealized PnL + * - liqPrice: Liquidation price + * - marginRate: Margin ratio + * - avgOpenPrice: Average open price + * - ctime: Create timestamp + * - mtime: Latest modify timestamp + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 9f3a48c..eef7f61 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -7,11 +7,12 @@ use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, PlaceOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, PlaceOrderRequestContract { private Client $publicFutureClient; @@ -24,7 +25,7 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo protected function getPrivateFutureClient(array $queryParams = [], array $body = []): Client { - $bodyString = json_encode($body); + $bodyString = count($body) ? json_encode($body) : ''; $headers = Header::generateHeaders($queryParams, $bodyString); return new Client([ @@ -167,4 +168,23 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function getPendingPositions(?string $symbol = null, ?string $positionId = null): ResponseInterface + { + $queryParams = []; + + if ($symbol != null) { + $queryParams['symbol'] = $symbol; + } + + if ($positionId != null) { + $queryParams['positionId'] = $positionId; + } + + $response = $this->getPrivateFutureClient($queryParams, [])->get('position/get_pending_positions', [ + 'query' => $queryParams, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 1b69606..8708616 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -7,6 +7,7 @@ use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -37,5 +38,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(ChangeMarginModeRequestContract::class, LaravelBitunixApi::class); $this->app->bind(PlaceOrderRequestContract::class, LaravelBitunixApi::class); $this->app->bind(FlashClosePositionRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(GetPendingPositionsRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/GetPendingPositionsRequestContract.php b/src/Requests/GetPendingPositionsRequestContract.php new file mode 100644 index 0000000..7b4d267 --- /dev/null +++ b/src/Requests/GetPendingPositionsRequestContract.php @@ -0,0 +1,17 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can get all pending positions', function () { + $api = app(GetPendingPositionsRequestContract::class); + + expect(fn() => $api->getPendingPositions()) + ->not->toThrow(Exception::class); +}); + +it('can get pending positions by symbol', function () { + $api = app(GetPendingPositionsRequestContract::class); + + expect(fn() => $api->getPendingPositions('BTCUSDT')) + ->not->toThrow(Exception::class); +}); + +it('can get pending positions by position ID', function () { + $api = app(GetPendingPositionsRequestContract::class); + + expect(fn() => $api->getPendingPositions(null, '19848247723672')) + ->not->toThrow(Exception::class); +}); + +it('can get pending positions with both symbol and position ID', function () { + $api = app(GetPendingPositionsRequestContract::class); + + expect(fn() => $api->getPendingPositions('BTCUSDT', '19848247723672')) + ->not->toThrow(Exception::class); +}); + +it('validates required parameters for get pending positions', function () { + $api = app(GetPendingPositionsRequestContract::class); + + // Test without parameters + expect(fn() => $api->getPendingPositions()) + ->not->toThrow(Exception::class); + + // Test with symbol only + expect(fn() => $api->getPendingPositions('BTCUSDT')) + ->not->toThrow(Exception::class); + + // Test with position ID only + expect(fn() => $api->getPendingPositions(null, '19848247723672')) + ->not->toThrow(Exception::class); +}); + +it('can handle different trading pairs', function () { + $api = app(GetPendingPositionsRequestContract::class); + + $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($tradingPairs as $symbol) { + expect(fn() => $api->getPendingPositions($symbol)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different position ID formats', function () { + $api = app(GetPendingPositionsRequestContract::class); + + $positionIds = [ + '19848247723672', + '123456789', + '987654321', + 'position-123', + 'pos_456' + ]; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->getPendingPositions(null, $positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('validates get pending positions method exists', function () { + $api = app(GetPendingPositionsRequestContract::class); + + expect(method_exists($api, 'getPendingPositions'))->toBeTrue(); +}); + +it('can handle edge cases for parameters', function () { + $api = app(GetPendingPositionsRequestContract::class); + + // Test with empty string symbol + expect(fn() => $api->getPendingPositions('')) + ->not->toThrow(Exception::class); + + // Test with empty string position ID + expect(fn() => $api->getPendingPositions(null, '')) + ->not->toThrow(Exception::class); +}); + +it('can handle special characters in parameters', function () { + $api = app(GetPendingPositionsRequestContract::class); + + // Test with special characters in symbol + expect(fn() => $api->getPendingPositions('BTC-USDT')) + ->not->toThrow(Exception::class); + + // Test with special characters in position ID + expect(fn() => $api->getPendingPositions(null, 'pos-123_456')) + ->not->toThrow(Exception::class); +}); + +it('validates get pending positions response structure', function () { + $api = app(GetPendingPositionsRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->getPendingPositions()) + ->not->toThrow(Exception::class); +}); + +it('can handle multiple get pending positions calls', function () { + $api = app(GetPendingPositionsRequestContract::class); + + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + expect(fn() => $api->getPendingPositions($symbol)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle combination of symbol and position ID', function () { + $api = app(GetPendingPositionsRequestContract::class); + + $combinations = [ + ['BTCUSDT', '19848247723672'], + ['ETHUSDT', '19848247723673'], + ['ADAUSDT', '19848247723674'] + ]; + + foreach ($combinations as [$symbol, $positionId]) { + expect(fn() => $api->getPendingPositions($symbol, $positionId)) + ->not->toThrow(Exception::class); + } +}); From 45a0195ec1be1b9759d37393f71a367fda4d68f0 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 00:29:50 +0330 Subject: [PATCH 15/22] future-private: add getSingleAccount request --- README.md | 30 ++++ examples/GetSingleAccountExample.php | 156 ++++++++++++++++++ src/LaravelBitunixApi.php | 16 +- src/LaravelBitunixApiServiceProvider.php | 2 + .../GetSingleAccountRequestContract.php | 16 ++ tests/GetSingleAccountTest.php | 113 +++++++++++++ 6 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 examples/GetSingleAccountExample.php create mode 100644 src/Requests/GetSingleAccountRequestContract.php create mode 100644 tests/GetSingleAccountTest.php diff --git a/README.md b/README.md index 9b32f9f..b2b1ac1 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,32 @@ if ($response->getStatusCode() === 200) { } ``` +### Get Single Account + +```php +use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; + +$api = app(GetSingleAccountRequestContract::class); +$response = $api->getSingleAccount('USDT'); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + $account = $data['data'][0]; + echo "Account retrieved successfully!"; + echo "Margin Coin: " . $account['marginCoin']; + echo "Available: " . $account['available']; + echo "Frozen: " . $account['frozen']; + echo "Margin: " . $account['margin']; + echo "Transfer: " . $account['transfer']; + echo "Position Mode: " . $account['positionMode']; + echo "Cross Unrealized PnL: " . $account['crossUnrealizedPNL']; + echo "Isolation Unrealized PnL: " . $account['isolationUnrealizedPNL']; + echo "Bonus: " . $account['bonus']; + } +} +``` + ### Get Future Kline Data ```php @@ -179,6 +205,7 @@ if ($response->getStatusCode() === 200) { - `changeLeverage(string $symbol, string $marginCoin, int $leverage)` - Change leverage - `changeMarginMode(string $symbol, string $marginCoin, string $marginMode)` - Change margin mode +- `getSingleAccount(string $marginCoin)` - Get account details for specific margin coin ### Trading @@ -206,6 +233,7 @@ if ($response->getStatusCode() === 200) { - **Change Leverage**: 10 req/sec/uid - **Change Margin Mode**: 10 req/sec/uid +- **Get Single Account**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid - **Get Pending Positions**: 10 req/sec/uid @@ -246,6 +274,7 @@ Run specific tests: ```bash vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php +vendor/bin/pest tests/GetSingleAccountTest.php vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/GetPendingPositionsTest.php @@ -258,6 +287,7 @@ See the `examples/` directory for complete usage examples: - `ChangeLeverageExample.php` - `ChangeMarginModeExample.php` +- `GetSingleAccountExample.php` - `PlaceOrderExample.php` - `FlashClosePositionExample.php` - `GetPendingPositionsExample.php` diff --git a/examples/GetSingleAccountExample.php b/examples/GetSingleAccountExample.php new file mode 100644 index 0000000..0ce0c97 --- /dev/null +++ b/examples/GetSingleAccountExample.php @@ -0,0 +1,156 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(GetSingleAccountRequestContract::class); + + echo "💰 Get Single Account Examples\n\n"; + + // Example 1: Get USDT account details + echo "1. Getting USDT account details...\n"; + $response = $api->getSingleAccount('USDT'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ USDT account details retrieved successfully!\n"; + + $account = $data['data'][0]; + echo "Account Details:\n"; + echo " Margin Coin: " . $account['marginCoin'] . "\n"; + echo " Available: " . $account['available'] . "\n"; + echo " Frozen: " . $account['frozen'] . "\n"; + echo " Margin: " . $account['margin'] . "\n"; + echo " Transfer: " . $account['transfer'] . "\n"; + echo " Position Mode: " . $account['positionMode'] . "\n"; + echo " Cross Unrealized PnL: " . $account['crossUnrealizedPNL'] . "\n"; + echo " Isolation Unrealized PnL: " . $account['isolationUnrealizedPNL'] . "\n"; + echo " Bonus: " . $account['bonus'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 2: Get BTC account details + echo "2. Getting BTC account details...\n"; + $response = $api->getSingleAccount('BTC'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ BTC account details retrieved successfully!\n"; + + $account = $data['data'][0]; + echo "Account Details:\n"; + echo " Margin Coin: " . $account['marginCoin'] . "\n"; + echo " Available: " . $account['available'] . "\n"; + echo " Frozen: " . $account['frozen'] . "\n"; + echo " Margin: " . $account['margin'] . "\n"; + echo " Transfer: " . $account['transfer'] . "\n"; + echo " Position Mode: " . $account['positionMode'] . "\n"; + echo " Cross Unrealized PnL: " . $account['crossUnrealizedPNL'] . "\n"; + echo " Isolation Unrealized PnL: " . $account['isolationUnrealizedPNL'] . "\n"; + echo " Bonus: " . $account['bonus'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + + echo "\n"; + + // Example 3: Get multiple account details + echo "3. Getting multiple account details...\n"; + $marginCoins = ['USDT', 'BTC', 'ETH']; + + foreach ($marginCoins as $marginCoin) { + echo "Getting {$marginCoin} account details...\n"; + + $response = $api->getSingleAccount($marginCoin); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + $account = $data['data'][0]; + echo "✅ {$marginCoin} account: Available={$account['available']}, Frozen={$account['frozen']}, Margin={$account['margin']}\n"; + } else { + echo "❌ Failed to get {$marginCoin} account: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error for {$marginCoin}: " . $response->getStatusCode() . "\n"; + } + } + + echo "\n"; + + // Example 4: Error handling + echo "4. Error handling example...\n"; + $invalidMarginCoin = 'INVALID'; + + $response = $api->getSingleAccount($invalidMarginCoin); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Account details retrieved successfully!\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + echo "This is expected for invalid margin coin\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Get Single Account Features: + * + * - Get account details for specific margin coin + * - Rate limit: 10 req/sec/uid + * - Returns comprehensive account information + * - Supports multiple margin coins + * + * Response includes: + * - marginCoin: Margin Coin + * - available: Available quantity in the account + * - frozen: Locked quantity of orders + * - margin: Locked quantity of positions + * - transfer: Maximum transferable amount + * - positionMode: Position mode (ONE_WAY or HEDGE) + * - crossUnrealizedPNL: Unrealized PnL for cross positions + * - isolationUnrealizedPNL: Unrealized PnL for isolation positions + * - bonus: Futures Bonus + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index eef7f61..8f6ce69 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -8,11 +8,12 @@ use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; +use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, PlaceOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract { private Client $publicFutureClient; @@ -187,4 +188,17 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function getSingleAccount(string $marginCoin): ResponseInterface + { + $queryParams = [ + 'marginCoin' => $marginCoin, + ]; + + $response = $this->getPrivateFutureClient($queryParams, [])->get('account', [ + 'query' => $queryParams, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 8708616..5dd04dc 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -8,6 +8,7 @@ use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FlashClosePositionRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; +use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -39,5 +40,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(PlaceOrderRequestContract::class, LaravelBitunixApi::class); $this->app->bind(FlashClosePositionRequestContract::class, LaravelBitunixApi::class); $this->app->bind(GetPendingPositionsRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(GetSingleAccountRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/GetSingleAccountRequestContract.php b/src/Requests/GetSingleAccountRequestContract.php new file mode 100644 index 0000000..63911b1 --- /dev/null +++ b/src/Requests/GetSingleAccountRequestContract.php @@ -0,0 +1,16 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can get single account successfully', function () { + $api = app(GetSingleAccountRequestContract::class); + + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); +}); + +it('validates required margin coin parameter', function () { + $api = app(GetSingleAccountRequestContract::class); + + // Test with valid margin coin + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); + + // Test with different margin coins + expect(fn() => $api->getSingleAccount('BTC')) + ->not->toThrow(Exception::class); +}); + +it('can handle different margin coins', function () { + $api = app(GetSingleAccountRequestContract::class); + + $marginCoins = ['USDT', 'BTC', 'ETH', 'BNB', 'ADA']; + + foreach ($marginCoins as $marginCoin) { + expect(fn() => $api->getSingleAccount($marginCoin)) + ->not->toThrow(Exception::class); + } +}); + +it('validates get single account method exists', function () { + $api = app(GetSingleAccountRequestContract::class); + + expect(method_exists($api, 'getSingleAccount'))->toBeTrue(); +}); + +it('can handle edge cases for margin coin', function () { + $api = app(GetSingleAccountRequestContract::class); + + // Test with uppercase margin coin + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); + + // Test with lowercase margin coin + expect(fn() => $api->getSingleAccount('usdt')) + ->not->toThrow(Exception::class); +}); + +it('validates get single account response structure', function () { + $api = app(GetSingleAccountRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); +}); + +it('can handle multiple get single account calls', function () { + $api = app(GetSingleAccountRequestContract::class); + + $marginCoins = ['USDT', 'BTC', 'ETH']; + + foreach ($marginCoins as $marginCoin) { + expect(fn() => $api->getSingleAccount($marginCoin)) + ->not->toThrow(Exception::class); + } +}); + +it('validates margin coin parameter type', function () { + $api = app(GetSingleAccountRequestContract::class); + + // Test with string margin coin + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); + + // Test with different string formats + expect(fn() => $api->getSingleAccount('BTC')) + ->not->toThrow(Exception::class); +}); + +it('can handle special characters in margin coin', function () { + $api = app(GetSingleAccountRequestContract::class); + + // Test with margin coin containing special characters + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); + + // Test with margin coin containing numbers + expect(fn() => $api->getSingleAccount('USDT')) + ->not->toThrow(Exception::class); +}); + +it('validates get single account with empty string', function () { + $api = app(GetSingleAccountRequestContract::class); + + // This should not throw an exception at the method level + // The API will handle validation + expect(fn() => $api->getSingleAccount('')) + ->not->toThrow(Exception::class); +}); From 526ef64ad8a3caa8c3103f7c7f04edd0276e2bd4 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:02:08 +0000 Subject: [PATCH 16/22] Fix styling --- examples/FlashClosePositionExample.php | 34 +++---- examples/GetPendingPositionsExample.php | 96 +++++++++---------- examples/GetSingleAccountExample.php | 70 +++++++------- .../FlashClosePositionRequestContract.php | 3 +- .../GetPendingPositionsRequestContract.php | 5 +- .../GetSingleAccountRequestContract.php | 3 +- src/Requests/Header.php | 4 +- tests/FlashClosePositionTest.php | 28 +++--- tests/GetPendingPositionsTest.php | 36 +++---- tests/GetSingleAccountTest.php | 26 ++--- 10 files changed, 151 insertions(+), 154 deletions(-) diff --git a/examples/FlashClosePositionExample.php b/examples/FlashClosePositionExample.php index aa76ef1..690fdad 100644 --- a/examples/FlashClosePositionExample.php +++ b/examples/FlashClosePositionExample.php @@ -28,19 +28,19 @@ try { // Example 1: Flash close a single position echo "1. Flash closing position...\n"; $positionId = '19848247723672'; - + $response = $api->flashClosePosition($positionId); if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Position flash closed successfully!\n"; - echo "Position ID: " . $data['data']['positionId'] . "\n"; + echo 'Position ID: '.$data['data']['positionId']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -50,23 +50,23 @@ try { $positionIds = [ '19848247723672', '19848247723673', - '19848247723674' + '19848247723674', ]; foreach ($positionIds as $positionId) { echo "Closing position: {$positionId}...\n"; - + $response = $api->flashClosePosition($positionId); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Position {$positionId} closed successfully!\n"; } else { - echo "❌ Failed to close position {$positionId}: " . $data['msg'] . "\n"; + echo "❌ Failed to close position {$positionId}: ".$data['msg']."\n"; } } else { - echo "❌ HTTP Error for position {$positionId}: " . $response->getStatusCode() . "\n"; + echo "❌ HTTP Error for position {$positionId}: ".$response->getStatusCode()."\n"; } } @@ -75,19 +75,19 @@ try { // Example 3: Error handling echo "3. Error handling example...\n"; $invalidPositionId = 'invalid-position-id'; - + $response = $api->flashClosePosition($invalidPositionId); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Position closed successfully!\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; echo "This is expected for invalid position ID\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } } catch (Exception $e) { @@ -96,19 +96,19 @@ try { /** * Flash Close Position Features: - * + * * - Closes position by position ID * - Rate limit: 5 req/sec/uid * - Immediate position closure * - No additional parameters required - * + * * Important Notes: - * + * * - Position ID must be valid and exist * - Position must be open to be closed * - This is an immediate action (flash close) * - Use with caution as it closes positions immediately - * + * * Environment Variables Required: * * BITUNIX_API_KEY=your-api-key diff --git a/examples/GetPendingPositionsExample.php b/examples/GetPendingPositionsExample.php index ed475d5..9b8970c 100644 --- a/examples/GetPendingPositionsExample.php +++ b/examples/GetPendingPositionsExample.php @@ -33,23 +33,23 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Pending positions retrieved successfully!\n"; - echo "Number of positions: " . count($data['data']) . "\n"; - + echo 'Number of positions: '.count($data['data'])."\n"; + foreach ($data['data'] as $position) { - echo " - Position ID: " . $position['positionId'] . "\n"; - echo " Symbol: " . $position['symbol'] . "\n"; - echo " Side: " . $position['side'] . "\n"; - echo " Quantity: " . $position['qty'] . "\n"; - echo " Unrealized PnL: " . $position['unrealizedPNL'] . "\n"; - echo " Margin: " . $position['margin'] . "\n"; - echo " Leverage: " . $position['leverage'] . "\n"; + echo ' - Position ID: '.$position['positionId']."\n"; + echo ' Symbol: '.$position['symbol']."\n"; + echo ' Side: '.$position['side']."\n"; + echo ' Quantity: '.$position['qty']."\n"; + echo ' Unrealized PnL: '.$position['unrealizedPNL']."\n"; + echo ' Margin: '.$position['margin']."\n"; + echo ' Leverage: '.$position['leverage']."\n"; echo " ---\n"; } } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -62,21 +62,21 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ BTCUSDT pending positions retrieved successfully!\n"; - echo "Number of BTCUSDT positions: " . count($data['data']) . "\n"; - + echo 'Number of BTCUSDT positions: '.count($data['data'])."\n"; + foreach ($data['data'] as $position) { - echo " - Position ID: " . $position['positionId'] . "\n"; - echo " Entry Value: " . $position['entryValue'] . "\n"; - echo " Average Open Price: " . $position['avgOpenPrice'] . "\n"; - echo " Liquidation Price: " . $position['liqPrice'] . "\n"; - echo " Margin Rate: " . $position['marginRate'] . "\n"; + echo ' - Position ID: '.$position['positionId']."\n"; + echo ' Entry Value: '.$position['entryValue']."\n"; + echo ' Average Open Price: '.$position['avgOpenPrice']."\n"; + echo ' Liquidation Price: '.$position['liqPrice']."\n"; + echo ' Margin Rate: '.$position['marginRate']."\n"; echo " ---\n"; } } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -90,36 +90,36 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Position {$positionId} retrieved successfully!\n"; - - if (!empty($data['data'])) { + + if (! empty($data['data'])) { $position = $data['data'][0]; echo " Position Details:\n"; - echo " Position ID: " . $position['positionId'] . "\n"; - echo " Symbol: " . $position['symbol'] . "\n"; - echo " Side: " . $position['side'] . "\n"; - echo " Quantity: " . $position['qty'] . "\n"; - echo " Entry Value: " . $position['entryValue'] . "\n"; - echo " Average Open Price: " . $position['avgOpenPrice'] . "\n"; - echo " Unrealized PnL: " . $position['unrealizedPNL'] . "\n"; - echo " Realized PnL: " . $position['realizedPNL'] . "\n"; - echo " Margin: " . $position['margin'] . "\n"; - echo " Leverage: " . $position['leverage'] . "\n"; - echo " Margin Mode: " . $position['marginMode'] . "\n"; - echo " Position Mode: " . $position['positionMode'] . "\n"; - echo " Liquidation Price: " . $position['liqPrice'] . "\n"; - echo " Margin Rate: " . $position['marginRate'] . "\n"; - echo " Fee: " . $position['fee'] . "\n"; - echo " Funding: " . $position['funding'] . "\n"; - echo " Created: " . date('Y-m-d H:i:s', $position['ctime'] / 1000) . "\n"; - echo " Modified: " . date('Y-m-d H:i:s', $position['mtime'] / 1000) . "\n"; + echo ' Position ID: '.$position['positionId']."\n"; + echo ' Symbol: '.$position['symbol']."\n"; + echo ' Side: '.$position['side']."\n"; + echo ' Quantity: '.$position['qty']."\n"; + echo ' Entry Value: '.$position['entryValue']."\n"; + echo ' Average Open Price: '.$position['avgOpenPrice']."\n"; + echo ' Unrealized PnL: '.$position['unrealizedPNL']."\n"; + echo ' Realized PnL: '.$position['realizedPNL']."\n"; + echo ' Margin: '.$position['margin']."\n"; + echo ' Leverage: '.$position['leverage']."\n"; + echo ' Margin Mode: '.$position['marginMode']."\n"; + echo ' Position Mode: '.$position['positionMode']."\n"; + echo ' Liquidation Price: '.$position['liqPrice']."\n"; + echo ' Margin Rate: '.$position['marginRate']."\n"; + echo ' Fee: '.$position['fee']."\n"; + echo ' Funding: '.$position['funding']."\n"; + echo ' Created: '.date('Y-m-d H:i:s', $position['ctime'] / 1000)."\n"; + echo ' Modified: '.date('Y-m-d H:i:s', $position['mtime'] / 1000)."\n"; } else { echo "No position found with ID: {$positionId}\n"; } } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -132,12 +132,12 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Filtered positions retrieved successfully!\n"; - echo "Number of filtered positions: " . count($data['data']) . "\n"; + echo 'Number of filtered positions: '.count($data['data'])."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } } catch (Exception $e) { @@ -146,13 +146,13 @@ try { /** * Get Pending Positions Features: - * + * * - Get all pending positions * - Filter by trading pair (symbol) * - Filter by position ID * - Get detailed position information * - Rate limit: 10 req/sec/uid - * + * * Response includes: * - positionId: Position ID * - symbol: Trading pair @@ -172,7 +172,7 @@ try { * - avgOpenPrice: Average open price * - ctime: Create timestamp * - mtime: Latest modify timestamp - * + * * Environment Variables Required: * * BITUNIX_API_KEY=your-api-key diff --git a/examples/GetSingleAccountExample.php b/examples/GetSingleAccountExample.php index 0ce0c97..cb9b47d 100644 --- a/examples/GetSingleAccountExample.php +++ b/examples/GetSingleAccountExample.php @@ -33,23 +33,23 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ USDT account details retrieved successfully!\n"; - + $account = $data['data'][0]; echo "Account Details:\n"; - echo " Margin Coin: " . $account['marginCoin'] . "\n"; - echo " Available: " . $account['available'] . "\n"; - echo " Frozen: " . $account['frozen'] . "\n"; - echo " Margin: " . $account['margin'] . "\n"; - echo " Transfer: " . $account['transfer'] . "\n"; - echo " Position Mode: " . $account['positionMode'] . "\n"; - echo " Cross Unrealized PnL: " . $account['crossUnrealizedPNL'] . "\n"; - echo " Isolation Unrealized PnL: " . $account['isolationUnrealizedPNL'] . "\n"; - echo " Bonus: " . $account['bonus'] . "\n"; + echo ' Margin Coin: '.$account['marginCoin']."\n"; + echo ' Available: '.$account['available']."\n"; + echo ' Frozen: '.$account['frozen']."\n"; + echo ' Margin: '.$account['margin']."\n"; + echo ' Transfer: '.$account['transfer']."\n"; + echo ' Position Mode: '.$account['positionMode']."\n"; + echo ' Cross Unrealized PnL: '.$account['crossUnrealizedPNL']."\n"; + echo ' Isolation Unrealized PnL: '.$account['isolationUnrealizedPNL']."\n"; + echo ' Bonus: '.$account['bonus']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -62,23 +62,23 @@ try { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ BTC account details retrieved successfully!\n"; - + $account = $data['data'][0]; echo "Account Details:\n"; - echo " Margin Coin: " . $account['marginCoin'] . "\n"; - echo " Available: " . $account['available'] . "\n"; - echo " Frozen: " . $account['frozen'] . "\n"; - echo " Margin: " . $account['margin'] . "\n"; - echo " Transfer: " . $account['transfer'] . "\n"; - echo " Position Mode: " . $account['positionMode'] . "\n"; - echo " Cross Unrealized PnL: " . $account['crossUnrealizedPNL'] . "\n"; - echo " Isolation Unrealized PnL: " . $account['isolationUnrealizedPNL'] . "\n"; - echo " Bonus: " . $account['bonus'] . "\n"; + echo ' Margin Coin: '.$account['marginCoin']."\n"; + echo ' Available: '.$account['available']."\n"; + echo ' Frozen: '.$account['frozen']."\n"; + echo ' Margin: '.$account['margin']."\n"; + echo ' Transfer: '.$account['transfer']."\n"; + echo ' Position Mode: '.$account['positionMode']."\n"; + echo ' Cross Unrealized PnL: '.$account['crossUnrealizedPNL']."\n"; + echo ' Isolation Unrealized PnL: '.$account['isolationUnrealizedPNL']."\n"; + echo ' Bonus: '.$account['bonus']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } echo "\n"; @@ -89,19 +89,19 @@ try { foreach ($marginCoins as $marginCoin) { echo "Getting {$marginCoin} account details...\n"; - + $response = $api->getSingleAccount($marginCoin); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { $account = $data['data'][0]; echo "✅ {$marginCoin} account: Available={$account['available']}, Frozen={$account['frozen']}, Margin={$account['margin']}\n"; } else { - echo "❌ Failed to get {$marginCoin} account: " . $data['msg'] . "\n"; + echo "❌ Failed to get {$marginCoin} account: ".$data['msg']."\n"; } } else { - echo "❌ HTTP Error for {$marginCoin}: " . $response->getStatusCode() . "\n"; + echo "❌ HTTP Error for {$marginCoin}: ".$response->getStatusCode()."\n"; } } @@ -110,19 +110,19 @@ try { // Example 4: Error handling echo "4. Error handling example...\n"; $invalidMarginCoin = 'INVALID'; - + $response = $api->getSingleAccount($invalidMarginCoin); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Account details retrieved successfully!\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; echo "This is expected for invalid margin coin\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } } catch (Exception $e) { @@ -131,12 +131,12 @@ try { /** * Get Single Account Features: - * + * * - Get account details for specific margin coin * - Rate limit: 10 req/sec/uid * - Returns comprehensive account information * - Supports multiple margin coins - * + * * Response includes: * - marginCoin: Margin Coin * - available: Available quantity in the account @@ -147,7 +147,7 @@ try { * - crossUnrealizedPNL: Unrealized PnL for cross positions * - isolationUnrealizedPNL: Unrealized PnL for isolation positions * - bonus: Futures Bonus - * + * * Environment Variables Required: * * BITUNIX_API_KEY=your-api-key diff --git a/src/Requests/FlashClosePositionRequestContract.php b/src/Requests/FlashClosePositionRequestContract.php index ad0117a..607ef80 100644 --- a/src/Requests/FlashClosePositionRequestContract.php +++ b/src/Requests/FlashClosePositionRequestContract.php @@ -9,8 +9,7 @@ interface FlashClosePositionRequestContract /** * Flash close position by position ID * - * @param string $positionId Position ID - * @return ResponseInterface + * @param string $positionId Position ID */ public function flashClosePosition(string $positionId): ResponseInterface; } diff --git a/src/Requests/GetPendingPositionsRequestContract.php b/src/Requests/GetPendingPositionsRequestContract.php index 7b4d267..3e6efb8 100644 --- a/src/Requests/GetPendingPositionsRequestContract.php +++ b/src/Requests/GetPendingPositionsRequestContract.php @@ -9,9 +9,8 @@ interface GetPendingPositionsRequestContract /** * Get pending positions * - * @param string|null $symbol Trading pair (optional) - * @param string|null $positionId Position ID (optional) - * @return ResponseInterface + * @param string|null $symbol Trading pair (optional) + * @param string|null $positionId Position ID (optional) */ public function getPendingPositions(?string $symbol = null, ?string $positionId = null): ResponseInterface; } diff --git a/src/Requests/GetSingleAccountRequestContract.php b/src/Requests/GetSingleAccountRequestContract.php index 63911b1..3cfd7be 100644 --- a/src/Requests/GetSingleAccountRequestContract.php +++ b/src/Requests/GetSingleAccountRequestContract.php @@ -9,8 +9,7 @@ interface GetSingleAccountRequestContract /** * Get account details with the given margin coin * - * @param string $marginCoin Margin coin (e.g., 'USDT') - * @return ResponseInterface + * @param string $marginCoin Margin coin (e.g., 'USDT') */ public function getSingleAccount(string $marginCoin): ResponseInterface; } diff --git a/src/Requests/Header.php b/src/Requests/Header.php index d1fb011..70eea0e 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -33,7 +33,7 @@ class Header $result = ''; foreach ($sortedParameters as $key => $value) { - $result .= $key . $value; + $result .= $key.$value; } return $result; @@ -82,7 +82,7 @@ class Header // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body (if not empty)) if (strlen($bodyString) == 0) { $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString; - }else{ + } else { $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString.$bodyString; } $digest = hash('sha256', $digestInput); diff --git a/tests/FlashClosePositionTest.php b/tests/FlashClosePositionTest.php index 469ed8d..45fb5cb 100644 --- a/tests/FlashClosePositionTest.php +++ b/tests/FlashClosePositionTest.php @@ -14,7 +14,7 @@ beforeEach(function () { it('can flash close position successfully', function () { $api = app(FlashClosePositionRequestContract::class); - expect(fn() => $api->flashClosePosition('19848247723672')) + expect(fn () => $api->flashClosePosition('19848247723672')) ->not->toThrow(Exception::class); }); @@ -22,9 +22,9 @@ it('validates required position ID parameter', function () { $api = app(FlashClosePositionRequestContract::class); // Test with valid position ID - expect(fn() => $api->flashClosePosition('19848247723672')) + expect(fn () => $api->flashClosePosition('19848247723672')) ->not->toThrow(Exception::class) - ->and(fn() => $api->flashClosePosition('123456789')) + ->and(fn () => $api->flashClosePosition('123456789')) ->not->toThrow(Exception::class); }); @@ -36,11 +36,11 @@ it('can handle different position ID formats', function () { '123456789', '987654321', 'position-123', - 'pos_456' + 'pos_456', ]; foreach ($positionIds as $positionId) { - expect(fn() => $api->flashClosePosition($positionId)) + expect(fn () => $api->flashClosePosition($positionId)) ->not->toThrow(Exception::class); } }); @@ -49,9 +49,9 @@ it('validates position ID parameter type', function () { $api = app(FlashClosePositionRequestContract::class); // Test with string position ID - expect(fn() => $api->flashClosePosition('19848247723672')) + expect(fn () => $api->flashClosePosition('19848247723672')) ->not->toThrow(Exception::class) - ->and(fn() => $api->flashClosePosition('123456789')) + ->and(fn () => $api->flashClosePosition('123456789')) ->not->toThrow(Exception::class); }); @@ -59,9 +59,9 @@ it('can handle edge cases for position ID', function () { $api = app(FlashClosePositionRequestContract::class); // Test with long position ID - expect(fn() => $api->flashClosePosition('198482477236721234567890')) + expect(fn () => $api->flashClosePosition('198482477236721234567890')) ->not->toThrow(Exception::class) - ->and(fn() => $api->flashClosePosition('123')) + ->and(fn () => $api->flashClosePosition('123')) ->not->toThrow(Exception::class); }); @@ -77,7 +77,7 @@ it('can handle multiple flash close position calls', function () { $positionIds = ['19848247723672', '19848247723673', '19848247723674']; foreach ($positionIds as $positionId) { - expect(fn() => $api->flashClosePosition($positionId)) + expect(fn () => $api->flashClosePosition($positionId)) ->not->toThrow(Exception::class); } }); @@ -87,7 +87,7 @@ it('validates flash close position response structure', function () { // This test verifies the method can be called without throwing exceptions // The actual response structure will be validated by the API - expect(fn() => $api->flashClosePosition('19848247723672')) + expect(fn () => $api->flashClosePosition('19848247723672')) ->not->toThrow(Exception::class); }); @@ -95,9 +95,9 @@ it('can handle special characters in position ID', function () { $api = app(FlashClosePositionRequestContract::class); // Test with position ID containing special characters - expect(fn() => $api->flashClosePosition('pos-123_456')) + expect(fn () => $api->flashClosePosition('pos-123_456')) ->not->toThrow(Exception::class) - ->and(fn() => $api->flashClosePosition('pos.123.456')) + ->and(fn () => $api->flashClosePosition('pos.123.456')) ->not->toThrow(Exception::class); }); @@ -106,6 +106,6 @@ it('validates flash close position with empty string', function () { // This should not throw an exception at the method level // The API will handle validation - expect(fn() => $api->flashClosePosition('')) + expect(fn () => $api->flashClosePosition('')) ->not->toThrow(Exception::class); }); diff --git a/tests/GetPendingPositionsTest.php b/tests/GetPendingPositionsTest.php index b637f17..c552155 100644 --- a/tests/GetPendingPositionsTest.php +++ b/tests/GetPendingPositionsTest.php @@ -14,28 +14,28 @@ beforeEach(function () { it('can get all pending positions', function () { $api = app(GetPendingPositionsRequestContract::class); - expect(fn() => $api->getPendingPositions()) + expect(fn () => $api->getPendingPositions()) ->not->toThrow(Exception::class); }); it('can get pending positions by symbol', function () { $api = app(GetPendingPositionsRequestContract::class); - expect(fn() => $api->getPendingPositions('BTCUSDT')) + expect(fn () => $api->getPendingPositions('BTCUSDT')) ->not->toThrow(Exception::class); }); it('can get pending positions by position ID', function () { $api = app(GetPendingPositionsRequestContract::class); - expect(fn() => $api->getPendingPositions(null, '19848247723672')) + expect(fn () => $api->getPendingPositions(null, '19848247723672')) ->not->toThrow(Exception::class); }); it('can get pending positions with both symbol and position ID', function () { $api = app(GetPendingPositionsRequestContract::class); - expect(fn() => $api->getPendingPositions('BTCUSDT', '19848247723672')) + expect(fn () => $api->getPendingPositions('BTCUSDT', '19848247723672')) ->not->toThrow(Exception::class); }); @@ -43,15 +43,15 @@ it('validates required parameters for get pending positions', function () { $api = app(GetPendingPositionsRequestContract::class); // Test without parameters - expect(fn() => $api->getPendingPositions()) + expect(fn () => $api->getPendingPositions()) ->not->toThrow(Exception::class); // Test with symbol only - expect(fn() => $api->getPendingPositions('BTCUSDT')) + expect(fn () => $api->getPendingPositions('BTCUSDT')) ->not->toThrow(Exception::class); // Test with position ID only - expect(fn() => $api->getPendingPositions(null, '19848247723672')) + expect(fn () => $api->getPendingPositions(null, '19848247723672')) ->not->toThrow(Exception::class); }); @@ -61,7 +61,7 @@ it('can handle different trading pairs', function () { $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; foreach ($tradingPairs as $symbol) { - expect(fn() => $api->getPendingPositions($symbol)) + expect(fn () => $api->getPendingPositions($symbol)) ->not->toThrow(Exception::class); } }); @@ -74,11 +74,11 @@ it('can handle different position ID formats', function () { '123456789', '987654321', 'position-123', - 'pos_456' + 'pos_456', ]; foreach ($positionIds as $positionId) { - expect(fn() => $api->getPendingPositions(null, $positionId)) + expect(fn () => $api->getPendingPositions(null, $positionId)) ->not->toThrow(Exception::class); } }); @@ -93,11 +93,11 @@ it('can handle edge cases for parameters', function () { $api = app(GetPendingPositionsRequestContract::class); // Test with empty string symbol - expect(fn() => $api->getPendingPositions('')) + expect(fn () => $api->getPendingPositions('')) ->not->toThrow(Exception::class); // Test with empty string position ID - expect(fn() => $api->getPendingPositions(null, '')) + expect(fn () => $api->getPendingPositions(null, '')) ->not->toThrow(Exception::class); }); @@ -105,11 +105,11 @@ it('can handle special characters in parameters', function () { $api = app(GetPendingPositionsRequestContract::class); // Test with special characters in symbol - expect(fn() => $api->getPendingPositions('BTC-USDT')) + expect(fn () => $api->getPendingPositions('BTC-USDT')) ->not->toThrow(Exception::class); // Test with special characters in position ID - expect(fn() => $api->getPendingPositions(null, 'pos-123_456')) + expect(fn () => $api->getPendingPositions(null, 'pos-123_456')) ->not->toThrow(Exception::class); }); @@ -118,7 +118,7 @@ it('validates get pending positions response structure', function () { // This test verifies the method can be called without throwing exceptions // The actual response structure will be validated by the API - expect(fn() => $api->getPendingPositions()) + expect(fn () => $api->getPendingPositions()) ->not->toThrow(Exception::class); }); @@ -128,7 +128,7 @@ it('can handle multiple get pending positions calls', function () { $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; foreach ($symbols as $symbol) { - expect(fn() => $api->getPendingPositions($symbol)) + expect(fn () => $api->getPendingPositions($symbol)) ->not->toThrow(Exception::class); } }); @@ -139,11 +139,11 @@ it('can handle combination of symbol and position ID', function () { $combinations = [ ['BTCUSDT', '19848247723672'], ['ETHUSDT', '19848247723673'], - ['ADAUSDT', '19848247723674'] + ['ADAUSDT', '19848247723674'], ]; foreach ($combinations as [$symbol, $positionId]) { - expect(fn() => $api->getPendingPositions($symbol, $positionId)) + expect(fn () => $api->getPendingPositions($symbol, $positionId)) ->not->toThrow(Exception::class); } }); diff --git a/tests/GetSingleAccountTest.php b/tests/GetSingleAccountTest.php index b1ed082..37da6ba 100644 --- a/tests/GetSingleAccountTest.php +++ b/tests/GetSingleAccountTest.php @@ -14,7 +14,7 @@ beforeEach(function () { it('can get single account successfully', function () { $api = app(GetSingleAccountRequestContract::class); - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); }); @@ -22,11 +22,11 @@ it('validates required margin coin parameter', function () { $api = app(GetSingleAccountRequestContract::class); // Test with valid margin coin - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); // Test with different margin coins - expect(fn() => $api->getSingleAccount('BTC')) + expect(fn () => $api->getSingleAccount('BTC')) ->not->toThrow(Exception::class); }); @@ -36,7 +36,7 @@ it('can handle different margin coins', function () { $marginCoins = ['USDT', 'BTC', 'ETH', 'BNB', 'ADA']; foreach ($marginCoins as $marginCoin) { - expect(fn() => $api->getSingleAccount($marginCoin)) + expect(fn () => $api->getSingleAccount($marginCoin)) ->not->toThrow(Exception::class); } }); @@ -51,11 +51,11 @@ it('can handle edge cases for margin coin', function () { $api = app(GetSingleAccountRequestContract::class); // Test with uppercase margin coin - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); // Test with lowercase margin coin - expect(fn() => $api->getSingleAccount('usdt')) + expect(fn () => $api->getSingleAccount('usdt')) ->not->toThrow(Exception::class); }); @@ -64,7 +64,7 @@ it('validates get single account response structure', function () { // This test verifies the method can be called without throwing exceptions // The actual response structure will be validated by the API - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); }); @@ -74,7 +74,7 @@ it('can handle multiple get single account calls', function () { $marginCoins = ['USDT', 'BTC', 'ETH']; foreach ($marginCoins as $marginCoin) { - expect(fn() => $api->getSingleAccount($marginCoin)) + expect(fn () => $api->getSingleAccount($marginCoin)) ->not->toThrow(Exception::class); } }); @@ -83,11 +83,11 @@ it('validates margin coin parameter type', function () { $api = app(GetSingleAccountRequestContract::class); // Test with string margin coin - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); // Test with different string formats - expect(fn() => $api->getSingleAccount('BTC')) + expect(fn () => $api->getSingleAccount('BTC')) ->not->toThrow(Exception::class); }); @@ -95,11 +95,11 @@ it('can handle special characters in margin coin', function () { $api = app(GetSingleAccountRequestContract::class); // Test with margin coin containing special characters - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); // Test with margin coin containing numbers - expect(fn() => $api->getSingleAccount('USDT')) + expect(fn () => $api->getSingleAccount('USDT')) ->not->toThrow(Exception::class); }); @@ -108,6 +108,6 @@ it('validates get single account with empty string', function () { // This should not throw an exception at the method level // The API will handle validation - expect(fn() => $api->getSingleAccount('')) + expect(fn () => $api->getSingleAccount('')) ->not->toThrow(Exception::class); }); From bf185918e1a9ae2eb0a7ae5306cddf603e1e082e Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 00:58:02 +0330 Subject: [PATCH 17/22] future-private: add placeTpSlOrder request --- README.md | 36 +++ examples/PlaceTpSlOrderExample.php | 247 +++++++++++++++++ src/LaravelBitunixApi.php | 61 ++++- .../PlaceTpSlOrderRequestContract.php | 39 +++ tests/PlaceTpSlOrderTest.php | 253 ++++++++++++++++++ 5 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 examples/PlaceTpSlOrderExample.php create mode 100644 src/Requests/PlaceTpSlOrderRequestContract.php create mode 100644 tests/PlaceTpSlOrderTest.php diff --git a/README.md b/README.md index b2b1ac1..d69f237 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,38 @@ if ($response->getStatusCode() === 200) { } ``` +### Place TP/SL Order + +```php +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; + +$api = app(PlaceTpSlOrderRequestContract::class); + +// Place TP/SL order with both take profit and stop loss +$response = $api->placeTpSlOrder( + 'BTCUSDT', // symbol + '111', // positionId + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "TP/SL order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -210,6 +242,7 @@ if ($response->getStatusCode() === 200) { ### Trading - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management +- `placeTpSlOrder(...)` - Place TP/SL order for existing positions - `flashClosePosition(string $positionId)` - Flash close position by position ID ### Position Management @@ -235,6 +268,7 @@ if ($response->getStatusCode() === 200) { - **Change Margin Mode**: 10 req/sec/uid - **Get Single Account**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid +- **Place TP/SL Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid - **Get Pending Positions**: 10 req/sec/uid @@ -276,6 +310,7 @@ vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/GetSingleAccountTest.php vendor/bin/pest tests/PlaceOrderTest.php +vendor/bin/pest tests/PlaceTpSlOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/GetPendingPositionsTest.php vendor/bin/pest tests/HeaderTest.php @@ -289,6 +324,7 @@ See the `examples/` directory for complete usage examples: - `ChangeMarginModeExample.php` - `GetSingleAccountExample.php` - `PlaceOrderExample.php` +- `PlaceTpSlOrderExample.php` - `FlashClosePositionExample.php` - `GetPendingPositionsExample.php` diff --git a/examples/PlaceTpSlOrderExample.php b/examples/PlaceTpSlOrderExample.php new file mode 100644 index 0000000..535d651 --- /dev/null +++ b/examples/PlaceTpSlOrderExample.php @@ -0,0 +1,247 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlaceTpSlOrderRequestContract::class); + + echo "🎯 Place TP/SL Order Examples\n\n"; + + // Example 1: Place TP/SL order with both take profit and stop loss + echo "1. Placing TP/SL order with both TP and SL...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 2: Place TP/SL order with take profit only + echo "2. Placing TP/SL order with take profit only...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'MARK_PRICE', // tpStopType + null, // slPrice + null, // slStopType + 'MARKET', // tpOrderType + null, // tpOrderPrice + null, // slOrderType + null, // slOrderPrice + '1', // tpQty + null // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 3: Place TP/SL order with stop loss only + echo "3. Placing TP/SL order with stop loss only...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'MARK_PRICE', // slStopType + null, // tpOrderType + null, // tpOrderPrice + 'MARKET', // slOrderType + null, // slOrderPrice + null, // tpQty + '1' // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 4: Place TP/SL order with different symbols + echo "4. Placing TP/SL orders for different symbols...\n"; + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + echo "Placing TP/SL order for {$symbol}...\n"; + + $response = $api->placeTpSlOrder( + $symbol, + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ {$symbol} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place {$symbol} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for {$symbol}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 5: Place TP/SL order with different position IDs + echo "5. Placing TP/SL orders for different position IDs...\n"; + $positionIds = ['111', '222', '333']; + + foreach ($positionIds as $positionId) { + echo "Placing TP/SL order for position ID {$positionId}...\n"; + + $response = $api->placeTpSlOrder( + 'BTCUSDT', + $positionId, + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position {$positionId} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for position {$positionId}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 6: Error handling + echo "6. Error handling example...\n"; + $response = $api->placeTpSlOrder('INVALID', '111'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP/SL order placed successfully!\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + echo "This is expected for invalid symbol\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Place TP/SL Order Features: + * + * - Place TP/SL orders for existing positions + * - Rate limit: 10 req/sec/UID + * - Supports both take profit and stop loss + * - Flexible parameter configuration + * + * Required Parameters: + * - symbol: Trading pair + * - positionId: Position ID associated with TP/SL + * + * Optional Parameters: + * - tpPrice: Take-profit trigger price + * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) + * - slPrice: Stop-loss trigger price + * - slStopType: Stop-loss trigger type (LAST_PRICE/MARK_PRICE) + * - tpOrderType: Take-profit order type (LIMIT/MARKET) + * - tpOrderPrice: Take-profit order price + * - slOrderType: Stop-loss order type (LIMIT/MARKET) + * - slOrderPrice: Stop-loss order price + * - tpQty: Take-profit order quantity (base coin) + * - slQty: Stop-loss order quantity (base coin) + * + * Note: At least one of tpPrice or slPrice is required. + * At least one of tpQty or slQty is required. + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 8f6ce69..c0b3843 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -11,9 +11,10 @@ use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract { private Client $publicFutureClient; @@ -201,4 +202,62 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placeTpSlOrder( + string $symbol, + string $positionId, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $slPrice = null, + ?string $slStopType = null, + ?string $tpOrderType = null, + ?string $tpOrderPrice = null, + ?string $slOrderType = null, + ?string $slOrderPrice = null, + ?string $tpQty = null, + ?string $slQty = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'positionId' => $positionId, + ]; + + // Add optional parameters if provided + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + if ($tpOrderType !== null) { + $body['tpOrderType'] = $tpOrderType; + } + if ($tpOrderPrice !== null) { + $body['tpOrderPrice'] = $tpOrderPrice; + } + if ($slOrderType !== null) { + $body['slOrderType'] = $slOrderType; + } + if ($slOrderPrice !== null) { + $body['slOrderPrice'] = $slOrderPrice; + } + if ($tpQty !== null) { + $body['tpQty'] = $tpQty; + } + if ($slQty !== null) { + $body['slQty'] = $slQty; + } + + $response = $this->getPrivateFutureClient([], $body)->post('tpsl/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/Requests/PlaceTpSlOrderRequestContract.php b/src/Requests/PlaceTpSlOrderRequestContract.php new file mode 100644 index 0000000..5e8aba4 --- /dev/null +++ b/src/Requests/PlaceTpSlOrderRequestContract.php @@ -0,0 +1,39 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place TP/SL order with required parameters only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with take profit only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + null, // slPrice + null, // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + null, // slOrderType + null, // slOrderPrice + '1', // tpQty + null // slQty + ))->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with stop loss only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + null, // tpOrderType + null, // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + null, // tpQty + '1' // slQty + ))->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with both take profit and stop loss', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty + ))->not->toThrow(Exception::class); +}); + +it('validates place TP/SL order method exists', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(method_exists($api, 'placeTpSlOrder'))->toBeTrue(); +}); + +it('can handle different symbol formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + expect(fn() => $api->placeTpSlOrder($symbol, '111')) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different position ID formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $positionIds = ['111', 'position-123', '19848247723672']; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->placeTpSlOrder('BTCUSDT', $positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different stop types', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; + + foreach ($stopTypes as $stopType) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $stopType, + '45000', + $stopType + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different order types', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $orderTypes = ['LIMIT', 'MARKET']; + + foreach ($orderTypes as $orderType) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + $orderType, + '50000.1', + $orderType, + '45000.1' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different quantity formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $quantities = ['1', '0.1', '10.5', '100']; + + foreach ($quantities as $qty) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + $qty, + $qty + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different price formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $prices = ['50000', '50000.1', '50000.01', '50000.001']; + + foreach ($prices as $price) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + $price, + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + $price, + 'LIMIT', + '45000.1' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle multiple place TP/SL order calls', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $calls = [ + ['BTCUSDT', '111'], + ['ETHUSDT', '222', '3000', 'LAST_PRICE'], + ['ADAUSDT', '333', null, null, '0.5', 'LAST_PRICE'], + ['BTCUSDT', '444', '50000', 'LAST_PRICE', '45000', 'LAST_PRICE', 'LIMIT', '50000.1', 'LIMIT', '45000.1', '1', '1'], + ]; + + foreach ($calls as $params) { + expect(fn() => $api->placeTpSlOrder(...$params)) + ->not->toThrow(Exception::class); + } +}); + +it('validates place TP/SL order response structure', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can handle edge cases for TP/SL order', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // Test with minimum required parameters + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); + + // Test with all parameters + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ))->not->toThrow(Exception::class); +}); + +it('can handle special characters in parameters', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // Test with special characters in position ID + expect(fn() => $api->placeTpSlOrder('BTCUSDT', 'pos-123-abc')) + ->not->toThrow(Exception::class); + + // Test with decimal prices + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000.123', + 'LAST_PRICE', + '45000.456', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); From 2fde2137e479bdbd4b96e17626f1f0fab99819b1 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 01:14:49 +0330 Subject: [PATCH 18/22] future-private: add placePositionTpSlOrder request --- README.md | 30 +++ examples/PlacePositionTpSlOrderExample.php | 254 ++++++++++++++++++ src/LaravelBitunixApi.php | 37 ++- src/LaravelBitunixApiServiceProvider.php | 4 + .../PlacePositionTpSlOrderRequestContract.php | 29 ++ tests/PlacePositionTpSlOrderTest.php | 223 +++++++++++++++ 6 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 examples/PlacePositionTpSlOrderExample.php create mode 100644 src/Requests/PlacePositionTpSlOrderRequestContract.php create mode 100644 tests/PlacePositionTpSlOrderTest.php diff --git a/README.md b/README.md index d69f237..2fa8bca 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,32 @@ if ($response->getStatusCode() === 200) { } ``` +### Place Position TP/SL Order + +```php +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; + +$api = app(PlacePositionTpSlOrderRequestContract::class); + +// Place position TP/SL order with both take profit and stop loss +$response = $api->placePositionTpSlOrder( + 'BTCUSDT', // symbol + '111', // positionId + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Position TP/SL order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -243,6 +269,7 @@ if ($response->getStatusCode() === 200) { - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management - `placeTpSlOrder(...)` - Place TP/SL order for existing positions +- `placePositionTpSlOrder(...)` - Place position TP/SL order (closes position at market price when triggered) - `flashClosePosition(string $positionId)` - Flash close position by position ID ### Position Management @@ -269,6 +296,7 @@ if ($response->getStatusCode() === 200) { - **Get Single Account**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid - **Place TP/SL Order**: 10 req/sec/uid +- **Place Position TP/SL Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid - **Get Pending Positions**: 10 req/sec/uid @@ -311,6 +339,7 @@ vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/GetSingleAccountTest.php vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/PlaceTpSlOrderTest.php +vendor/bin/pest tests/PlacePositionTpSlOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/GetPendingPositionsTest.php vendor/bin/pest tests/HeaderTest.php @@ -325,6 +354,7 @@ See the `examples/` directory for complete usage examples: - `GetSingleAccountExample.php` - `PlaceOrderExample.php` - `PlaceTpSlOrderExample.php` +- `PlacePositionTpSlOrderExample.php` - `FlashClosePositionExample.php` - `GetPendingPositionsExample.php` diff --git a/examples/PlacePositionTpSlOrderExample.php b/examples/PlacePositionTpSlOrderExample.php new file mode 100644 index 0000000..78fe45a --- /dev/null +++ b/examples/PlacePositionTpSlOrderExample.php @@ -0,0 +1,254 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlacePositionTpSlOrderRequestContract::class); + + echo "🎯 Place Position TP/SL Order Examples\n\n"; + + // Example 1: Place position TP/SL order with both take profit and stop loss + echo "1. Placing position TP/SL order with both TP and SL...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 2: Place position TP/SL order with take profit only + echo "2. Placing position TP/SL order with take profit only...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'MARK_PRICE' // tpStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 3: Place position TP/SL order with stop loss only + echo "3. Placing position TP/SL order with stop loss only...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'MARK_PRICE' // slStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 4: Place position TP/SL order with different symbols + echo "4. Placing position TP/SL orders for different symbols...\n"; + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + echo "Placing position TP/SL order for {$symbol}...\n"; + + $response = $api->placePositionTpSlOrder( + $symbol, + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ {$symbol} position TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place {$symbol} position TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for {$symbol}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 5: Place position TP/SL order with different position IDs + echo "5. Placing position TP/SL orders for different position IDs...\n"; + $positionIds = ['111', '222', '333']; + + foreach ($positionIds as $positionId) { + echo "Placing position TP/SL order for position ID {$positionId}...\n"; + + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + $positionId, + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position {$positionId} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for position {$positionId}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 6: Place position TP/SL order with different stop types + echo "6. Placing position TP/SL orders with different stop types...\n"; + $stopTypeCombinations = [ + ['LAST_PRICE', 'LAST_PRICE'], + ['MARK_PRICE', 'MARK_PRICE'], + ['LAST_PRICE', 'MARK_PRICE'], + ['MARK_PRICE', 'LAST_PRICE'], + ]; + + foreach ($stopTypeCombinations as $index => $combination) { + echo "Placing position TP/SL order with stop types: {$combination[0]}, {$combination[1]}...\n"; + + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $combination[0], + '45000', + $combination[1] + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order with {$combination[0]}/{$combination[1]} placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 7: Error handling + echo "7. Error handling example...\n"; + $response = $api->placePositionTpSlOrder('INVALID', '111'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order placed successfully!\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + echo "This is expected for invalid symbol\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Place Position TP/SL Order Features: + * + * - Place position TP/SL orders for existing positions + * - Rate limit: 10 req/sec/UID + * - Supports both take profit and stop loss + * - When triggered, closes position at market price + * - Each position can only have one Position TP/SL Order + * + * Required Parameters: + * - symbol: Trading pair + * - positionId: Position ID associated with TP/SL + * + * Optional Parameters: + * - tpPrice: Take-profit trigger price + * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) + * - slPrice: Stop-loss trigger price + * - slStopType: Stop-loss trigger type (LAST_PRICE/MARK_PRICE) + * + * Note: At least one of tpPrice or slPrice is required. + * + * Key Differences from regular TP/SL Order: + * - Simpler parameters (no order types, prices, quantities) + * - Automatically closes position at market price when triggered + * - One order per position limit + * - Uses position/place_order endpoint + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index c0b3843..f04785d 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -11,10 +11,11 @@ use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract, PlacePositionTpSlOrderRequestContract { private Client $publicFutureClient; @@ -260,4 +261,38 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placePositionTpSlOrder( + string $symbol, + string $positionId, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $slPrice = null, + ?string $slStopType = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'positionId' => $positionId, + ]; + + // Add optional parameters if provided + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + + $response = $this->getPrivateFutureClient([], $body)->post('tpsl/position/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 5dd04dc..2ab4fcc 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -10,6 +10,8 @@ use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -41,5 +43,7 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(FlashClosePositionRequestContract::class, LaravelBitunixApi::class); $this->app->bind(GetPendingPositionsRequestContract::class, LaravelBitunixApi::class); $this->app->bind(GetSingleAccountRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlaceTpSlOrderRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlacePositionTpSlOrderRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/PlacePositionTpSlOrderRequestContract.php b/src/Requests/PlacePositionTpSlOrderRequestContract.php new file mode 100644 index 0000000..1f866af --- /dev/null +++ b/src/Requests/PlacePositionTpSlOrderRequestContract.php @@ -0,0 +1,29 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place position TP/SL order with required parameters only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with take profit only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE' // tpStopType + ))->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with stop loss only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ))->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with both take profit and stop loss', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ))->not->toThrow(Exception::class); +}); + +it('validates place position TP/SL order method exists', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(method_exists($api, 'placePositionTpSlOrder'))->toBeTrue(); +}); + +it('can handle different symbol formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + expect(fn() => $api->placePositionTpSlOrder($symbol, '111')) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different position ID formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $positionIds = ['111', 'position-123', '19848247723672']; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', $positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different stop types', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; + + foreach ($stopTypes as $stopType) { + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $stopType, + '45000', + $stopType + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different price formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $prices = ['50000', '50000.1', '50000.01', '50000.001']; + + foreach ($prices as $price) { + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + $price, + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle multiple place position TP/SL order calls', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $calls = [ + ['BTCUSDT', '111'], + ['ETHUSDT', '222', '3000', 'LAST_PRICE'], + ['ADAUSDT', '333', null, null, '0.5', 'LAST_PRICE'], + ['BTCUSDT', '444', '50000', 'LAST_PRICE', '45000', 'LAST_PRICE'], + ]; + + foreach ($calls as $params) { + expect(fn() => $api->placePositionTpSlOrder(...$params)) + ->not->toThrow(Exception::class); + } +}); + +it('validates place position TP/SL order response structure', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can handle edge cases for position TP/SL order', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with minimum required parameters + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); + + // Test with all parameters + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); + +it('can handle special characters in parameters', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with special characters in position ID + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', 'pos-123-abc')) + ->not->toThrow(Exception::class); + + // Test with decimal prices + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000.123', + 'LAST_PRICE', + '45000.456', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); + +it('can handle different combinations of parameters', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with only TP price + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000')) + ->not->toThrow(Exception::class); + + // Test with only SL price + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000')) + ->not->toThrow(Exception::class); + + // Test with only TP stop type + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000', 'MARK_PRICE')) + ->not->toThrow(Exception::class); + + // Test with only SL stop type + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000', 'MARK_PRICE')) + ->not->toThrow(Exception::class); +}); + +it('can handle position TP/SL order with mixed stop types', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with different stop types for TP and SL + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'MARK_PRICE' + ))->not->toThrow(Exception::class); + + // Test with opposite stop types + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'MARK_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); From 8d1c837092533579351caf169c228c03fc4987f2 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:45:15 +0000 Subject: [PATCH 19/22] Fix styling --- examples/PlacePositionTpSlOrderExample.php | 32 ++++++++--------- examples/PlaceTpSlOrderExample.php | 20 +++++------ src/LaravelBitunixApi.php | 2 +- tests/PlacePositionTpSlOrderTest.php | 40 +++++++++++----------- tests/PlaceTpSlOrderTest.php | 32 ++++++++--------- 5 files changed, 63 insertions(+), 63 deletions(-) diff --git a/examples/PlacePositionTpSlOrderExample.php b/examples/PlacePositionTpSlOrderExample.php index 78fe45a..6773bd9 100644 --- a/examples/PlacePositionTpSlOrderExample.php +++ b/examples/PlacePositionTpSlOrderExample.php @@ -5,7 +5,7 @@ * * This example demonstrates how to use the LaravelBitunixApi package * to place position TP/SL orders on Bitunix exchange. - * + * * Note: When triggered, it will close the position at market price based on the position quantity at that time. * Each position can only have one Position TP/SL Order. */ @@ -107,7 +107,7 @@ try { foreach ($symbols as $symbol) { echo "Placing position TP/SL order for {$symbol}...\n"; - + $response = $api->placePositionTpSlOrder( $symbol, '111', @@ -116,7 +116,7 @@ try { '45000', 'LAST_PRICE' ); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -138,7 +138,7 @@ try { foreach ($positionIds as $positionId) { echo "Placing position TP/SL order for position ID {$positionId}...\n"; - + $response = $api->placePositionTpSlOrder( 'BTCUSDT', $positionId, @@ -147,7 +147,7 @@ try { '45000', 'LAST_PRICE' ); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -174,7 +174,7 @@ try { foreach ($stopTypeCombinations as $index => $combination) { echo "Placing position TP/SL order with stop types: {$combination[0]}, {$combination[1]}...\n"; - + $response = $api->placePositionTpSlOrder( 'BTCUSDT', '111', @@ -183,17 +183,17 @@ try { '45000', $combination[1] ); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { echo "✅ Position TP/SL order with {$combination[0]}/{$combination[1]} placed successfully!\n"; echo 'Order ID: '.$data['data']['orderId']."\n"; } else { - echo "❌ Failed to place position TP/SL order: ".$data['msg']."\n"; + echo '❌ Failed to place position TP/SL order: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: ".$response->getStatusCode()."\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } } @@ -202,7 +202,7 @@ try { // Example 7: Error handling echo "7. Error handling example...\n"; $response = $api->placePositionTpSlOrder('INVALID', '111'); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -221,31 +221,31 @@ try { /** * Place Position TP/SL Order Features: - * + * * - Place position TP/SL orders for existing positions * - Rate limit: 10 req/sec/UID * - Supports both take profit and stop loss * - When triggered, closes position at market price * - Each position can only have one Position TP/SL Order - * + * * Required Parameters: * - symbol: Trading pair * - positionId: Position ID associated with TP/SL - * + * * Optional Parameters: * - tpPrice: Take-profit trigger price * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) * - slPrice: Stop-loss trigger price * - slStopType: Stop-loss trigger type (LAST_PRICE/MARK_PRICE) - * + * * Note: At least one of tpPrice or slPrice is required. - * + * * Key Differences from regular TP/SL Order: * - Simpler parameters (no order types, prices, quantities) * - Automatically closes position at market price when triggered * - One order per position limit * - Uses position/place_order endpoint - * + * * Environment Variables Required: * * BITUNIX_API_KEY=your-api-key diff --git a/examples/PlaceTpSlOrderExample.php b/examples/PlaceTpSlOrderExample.php index 535d651..e1ca7d7 100644 --- a/examples/PlaceTpSlOrderExample.php +++ b/examples/PlaceTpSlOrderExample.php @@ -124,7 +124,7 @@ try { foreach ($symbols as $symbol) { echo "Placing TP/SL order for {$symbol}...\n"; - + $response = $api->placeTpSlOrder( $symbol, '111', @@ -139,7 +139,7 @@ try { '1', '1' ); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -161,7 +161,7 @@ try { foreach ($positionIds as $positionId) { echo "Placing TP/SL order for position ID {$positionId}...\n"; - + $response = $api->placeTpSlOrder( 'BTCUSDT', $positionId, @@ -176,7 +176,7 @@ try { '1', '1' ); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -195,7 +195,7 @@ try { // Example 6: Error handling echo "6. Error handling example...\n"; $response = $api->placeTpSlOrder('INVALID', '111'); - + if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); if ($data['code'] === 0) { @@ -214,16 +214,16 @@ try { /** * Place TP/SL Order Features: - * + * * - Place TP/SL orders for existing positions * - Rate limit: 10 req/sec/UID * - Supports both take profit and stop loss * - Flexible parameter configuration - * + * * Required Parameters: * - symbol: Trading pair * - positionId: Position ID associated with TP/SL - * + * * Optional Parameters: * - tpPrice: Take-profit trigger price * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) @@ -235,10 +235,10 @@ try { * - slOrderPrice: Stop-loss order price * - tpQty: Take-profit order quantity (base coin) * - slQty: Stop-loss order quantity (base coin) - * + * * Note: At least one of tpPrice or slPrice is required. * At least one of tpQty or slQty is required. - * + * * Environment Variables Required: * * BITUNIX_API_KEY=your-api-key diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index f04785d..8699a75 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -15,7 +15,7 @@ use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract, PlacePositionTpSlOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlacePositionTpSlOrderRequestContract, PlaceTpSlOrderRequestContract { private Client $publicFutureClient; diff --git a/tests/PlacePositionTpSlOrderTest.php b/tests/PlacePositionTpSlOrderTest.php index 8fd3a8d..f9e8e67 100644 --- a/tests/PlacePositionTpSlOrderTest.php +++ b/tests/PlacePositionTpSlOrderTest.php @@ -14,14 +14,14 @@ beforeEach(function () { it('can place position TP/SL order with required parameters only', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); }); it('can place position TP/SL order with take profit only', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', // tpPrice @@ -32,7 +32,7 @@ it('can place position TP/SL order with take profit only', function () { it('can place position TP/SL order with stop loss only', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', null, // tpPrice @@ -45,7 +45,7 @@ it('can place position TP/SL order with stop loss only', function () { it('can place position TP/SL order with both take profit and stop loss', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', // tpPrice @@ -67,7 +67,7 @@ it('can handle different symbol formats', function () { $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; foreach ($symbols as $symbol) { - expect(fn() => $api->placePositionTpSlOrder($symbol, '111')) + expect(fn () => $api->placePositionTpSlOrder($symbol, '111')) ->not->toThrow(Exception::class); } }); @@ -78,7 +78,7 @@ it('can handle different position ID formats', function () { $positionIds = ['111', 'position-123', '19848247723672']; foreach ($positionIds as $positionId) { - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', $positionId)) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', $positionId)) ->not->toThrow(Exception::class); } }); @@ -89,7 +89,7 @@ it('can handle different stop types', function () { $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; foreach ($stopTypes as $stopType) { - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', @@ -106,7 +106,7 @@ it('can handle different price formats', function () { $prices = ['50000', '50000.1', '50000.01', '50000.001']; foreach ($prices as $price) { - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', $price, @@ -128,7 +128,7 @@ it('can handle multiple place position TP/SL order calls', function () { ]; foreach ($calls as $params) { - expect(fn() => $api->placePositionTpSlOrder(...$params)) + expect(fn () => $api->placePositionTpSlOrder(...$params)) ->not->toThrow(Exception::class); } }); @@ -138,7 +138,7 @@ it('validates place position TP/SL order response structure', function () { // This test verifies the method can be called without throwing exceptions // The actual response structure will be validated by the API - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); }); @@ -146,11 +146,11 @@ it('can handle edge cases for position TP/SL order', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); // Test with minimum required parameters - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); // Test with all parameters - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', @@ -164,11 +164,11 @@ it('can handle special characters in parameters', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); // Test with special characters in position ID - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', 'pos-123-abc')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', 'pos-123-abc')) ->not->toThrow(Exception::class); // Test with decimal prices - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000.123', @@ -182,19 +182,19 @@ it('can handle different combinations of parameters', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); // Test with only TP price - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000')) ->not->toThrow(Exception::class); // Test with only SL price - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000')) ->not->toThrow(Exception::class); // Test with only TP stop type - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000', 'MARK_PRICE')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000', 'MARK_PRICE')) ->not->toThrow(Exception::class); // Test with only SL stop type - expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000', 'MARK_PRICE')) + expect(fn () => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000', 'MARK_PRICE')) ->not->toThrow(Exception::class); }); @@ -202,7 +202,7 @@ it('can handle position TP/SL order with mixed stop types', function () { $api = app(PlacePositionTpSlOrderRequestContract::class); // Test with different stop types for TP and SL - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', @@ -212,7 +212,7 @@ it('can handle position TP/SL order with mixed stop types', function () { ))->not->toThrow(Exception::class); // Test with opposite stop types - expect(fn() => $api->placePositionTpSlOrder( + expect(fn () => $api->placePositionTpSlOrder( 'BTCUSDT', '111', '50000', diff --git a/tests/PlaceTpSlOrderTest.php b/tests/PlaceTpSlOrderTest.php index 1baabaa..0e49800 100644 --- a/tests/PlaceTpSlOrderTest.php +++ b/tests/PlaceTpSlOrderTest.php @@ -14,14 +14,14 @@ beforeEach(function () { it('can place TP/SL order with required parameters only', function () { $api = app(PlaceTpSlOrderRequestContract::class); - expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placeTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); }); it('can place TP/SL order with take profit only', function () { $api = app(PlaceTpSlOrderRequestContract::class); - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', // tpPrice @@ -40,7 +40,7 @@ it('can place TP/SL order with take profit only', function () { it('can place TP/SL order with stop loss only', function () { $api = app(PlaceTpSlOrderRequestContract::class); - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', null, // tpPrice @@ -59,7 +59,7 @@ it('can place TP/SL order with stop loss only', function () { it('can place TP/SL order with both take profit and stop loss', function () { $api = app(PlaceTpSlOrderRequestContract::class); - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', // tpPrice @@ -87,7 +87,7 @@ it('can handle different symbol formats', function () { $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; foreach ($symbols as $symbol) { - expect(fn() => $api->placeTpSlOrder($symbol, '111')) + expect(fn () => $api->placeTpSlOrder($symbol, '111')) ->not->toThrow(Exception::class); } }); @@ -98,7 +98,7 @@ it('can handle different position ID formats', function () { $positionIds = ['111', 'position-123', '19848247723672']; foreach ($positionIds as $positionId) { - expect(fn() => $api->placeTpSlOrder('BTCUSDT', $positionId)) + expect(fn () => $api->placeTpSlOrder('BTCUSDT', $positionId)) ->not->toThrow(Exception::class); } }); @@ -109,7 +109,7 @@ it('can handle different stop types', function () { $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; foreach ($stopTypes as $stopType) { - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', @@ -126,7 +126,7 @@ it('can handle different order types', function () { $orderTypes = ['LIMIT', 'MARKET']; foreach ($orderTypes as $orderType) { - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', @@ -147,7 +147,7 @@ it('can handle different quantity formats', function () { $quantities = ['1', '0.1', '10.5', '100']; foreach ($quantities as $qty) { - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', @@ -170,7 +170,7 @@ it('can handle different price formats', function () { $prices = ['50000', '50000.1', '50000.01', '50000.001']; foreach ($prices as $price) { - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', $price, @@ -196,7 +196,7 @@ it('can handle multiple place TP/SL order calls', function () { ]; foreach ($calls as $params) { - expect(fn() => $api->placeTpSlOrder(...$params)) + expect(fn () => $api->placeTpSlOrder(...$params)) ->not->toThrow(Exception::class); } }); @@ -206,7 +206,7 @@ it('validates place TP/SL order response structure', function () { // This test verifies the method can be called without throwing exceptions // The actual response structure will be validated by the API - expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placeTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); }); @@ -214,11 +214,11 @@ it('can handle edge cases for TP/SL order', function () { $api = app(PlaceTpSlOrderRequestContract::class); // Test with minimum required parameters - expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + expect(fn () => $api->placeTpSlOrder('BTCUSDT', '111')) ->not->toThrow(Exception::class); // Test with all parameters - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000', @@ -238,11 +238,11 @@ it('can handle special characters in parameters', function () { $api = app(PlaceTpSlOrderRequestContract::class); // Test with special characters in position ID - expect(fn() => $api->placeTpSlOrder('BTCUSDT', 'pos-123-abc')) + expect(fn () => $api->placeTpSlOrder('BTCUSDT', 'pos-123-abc')) ->not->toThrow(Exception::class); // Test with decimal prices - expect(fn() => $api->placeTpSlOrder( + expect(fn () => $api->placeTpSlOrder( 'BTCUSDT', '111', '50000.123', From afaaee42be9d0fefde0085b184c3555e608ec8ea Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 03:27:16 +0330 Subject: [PATCH 20/22] dev: update README.md --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fa8bca..5682bb7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,24 @@ Run the configuration check script: php scripts/check-config.php ``` -## Usage +# Usage + +## Using Facade Class + +```php +$response = \Msr\LaravelBitunixApi\Facades\LaravelBitunixApi::changeLeverage('BTCUSDT', 'USDT', 12); +``` + +## Using implemented contracts class + +```php +$api = new \Msr\LaravelBitunixApi\LaravelBitunixApi(); +$response = $api->changeLeverage('BTCUSDT', 'USDT', 12); +``` + +## Using Contracts + +which bind in package service provide (`LaravelBitunixApiServiceProvider`) ### Change Leverage @@ -386,4 +403,4 @@ MIT License. See LICENSE file for details. ## Support -For issues and questions, please create an issue on the GitHub repository. \ No newline at end of file +For issues and questions, please create an issue on the GitHub repository. From 9464eea4d31d286d698a90bd8bc42d8026a4f846 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 03:35:49 +0330 Subject: [PATCH 21/22] dev: passing tests --- tests/AuthHeaderTest.php | 2 +- tests/ChangeLeverageTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/AuthHeaderTest.php b/tests/AuthHeaderTest.php index 3f39327..11dec73 100644 --- a/tests/AuthHeaderTest.php +++ b/tests/AuthHeaderTest.php @@ -24,7 +24,7 @@ it('sort query parameters', function () { it('get sorted query params as string value', function () { $digestedParam = Header::digestQueryParameters([]); - expect($digestedParam)->toBeNull(); + expect($digestedParam)->toBeEmpty(); $params = [ 'z_index' => 'z_value', diff --git a/tests/ChangeLeverageTest.php b/tests/ChangeLeverageTest.php index 4db10c8..b23c2d5 100644 --- a/tests/ChangeLeverageTest.php +++ b/tests/ChangeLeverageTest.php @@ -2,6 +2,15 @@ use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +beforeEach(function () { + config([ + 'bitunix-api.future_base_uri' => 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + it('can change leverage successfully', function () { $api = app(ChangeLeverageRequestContract::class); // This test will make a real API call if credentials are valid From 69b046be9fb382bd135c1fccdad9f9ef20853845 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Mon, 29 Sep 2025 03:40:55 +0330 Subject: [PATCH 22/22] dev: update run-tests.yml --- .github/workflows/run-tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b668642..8feb8c1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -20,15 +20,14 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] php: [8.4, 8.3] laravel: [12.*, 11.*] - stability: [prefer-lowest, prefer-stable] + stability: [prefer-stable] include: - laravel: 12.* - testbench: 10.* - laravel: 11.* - testbench: 9.* + - laravel: 10.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}