From d561f69ece0696e0491434300961304cec19c731 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 16:02:15 +0330 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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',