diff --git a/README.md b/README.md index 0be9a19..2fa8bca 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,389 @@ -# 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!"; + } +} +``` + +### 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']; + } +} +``` + +### 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 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 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']; + } +} +``` + +### 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']; + } +} +``` + +### 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 +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 +- `getSingleAccount(string $marginCoin)` - Get account details for specific margin coin + +### 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 +- `placePositionTpSlOrder(...)` - Place position TP/SL order (closes position at market price when triggered) +- `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 + +## 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 +- **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 + +## 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/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 +``` -## Contributing +## Examples -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +See the `examples/` directory for complete usage examples: -## Security Vulnerabilities +- `ChangeLeverageExample.php` +- `ChangeMarginModeExample.php` +- `GetSingleAccountExample.php` +- `PlaceOrderExample.php` +- `PlaceTpSlOrderExample.php` +- `PlacePositionTpSlOrderExample.php` +- `FlashClosePositionExample.php` +- `GetPendingPositionsExample.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/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 +} 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/examples/FlashClosePositionExample.php b/examples/FlashClosePositionExample.php new file mode 100644 index 0000000..690fdad --- /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/examples/GetPendingPositionsExample.php b/examples/GetPendingPositionsExample.php new file mode 100644 index 0000000..9b8970c --- /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/examples/GetSingleAccountExample.php b/examples/GetSingleAccountExample.php new file mode 100644 index 0000000..cb9b47d --- /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/examples/PlaceOrderExample.php b/examples/PlaceOrderExample.php new file mode 100644 index 0000000..14a9843 --- /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/examples/PlacePositionTpSlOrderExample.php b/examples/PlacePositionTpSlOrderExample.php new file mode 100644 index 0000000..6773bd9 --- /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/examples/PlaceTpSlOrderExample.php b/examples/PlaceTpSlOrderExample.php new file mode 100644 index 0000000..e1ca7d7 --- /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/scripts/check-config.php b/scripts/check-config.php new file mode 100644 index 0000000..fe03a15 --- /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..8699a75 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -4,11 +4,18 @@ 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\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, FutureKLineRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlacePositionTpSlOrderRequestContract, PlaceTpSlOrderRequestContract { private Client $publicFutureClient; @@ -21,7 +28,7 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineReq 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([ @@ -60,4 +67,232 @@ 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; + } + + 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; + } + + public function flashClosePosition(string $positionId): ResponseInterface + { + $body = [ + 'positionId' => $positionId, + ]; + + $response = $this->getPrivateFutureClient([], $body)->post('trade/flash_close_position', [ + 'json' => $body, + ]); + + 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; + } + + public function getSingleAccount(string $marginCoin): ResponseInterface + { + $queryParams = [ + 'marginCoin' => $marginCoin, + ]; + + $response = $this->getPrivateFutureClient($queryParams, [])->get('account', [ + 'query' => $queryParams, + ]); + + 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; + } + + 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 589a68b..2ab4fcc 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -4,7 +4,14 @@ 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\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; @@ -31,5 +38,12 @@ 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); + $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/ChangeMarginModeRequestContract.php b/src/Requests/ChangeMarginModeRequestContract.php new file mode 100644 index 0000000..1abaeef --- /dev/null +++ b/src/Requests/ChangeMarginModeRequestContract.php @@ -0,0 +1,17 @@ + "1", "uid" => "200"] becomes "id1uid200" + * According to Bitunix documentation: String queryParams = "id1uid200" */ public static function digestQueryParameters(array $parameters): string { @@ -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) diff --git a/src/Requests/PlaceOrderRequestContract.php b/src/Requests/PlaceOrderRequestContract.php new file mode 100644 index 0000000..47a0488 --- /dev/null +++ b/src/Requests/PlaceOrderRequestContract.php @@ -0,0 +1,51 @@ + '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); + +}); diff --git a/tests/FlashClosePositionTest.php b/tests/FlashClosePositionTest.php new file mode 100644 index 0000000..45fb5cb --- /dev/null +++ b/tests/FlashClosePositionTest.php @@ -0,0 +1,111 @@ + '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); +}); diff --git a/tests/GetPendingPositionsTest.php b/tests/GetPendingPositionsTest.php new file mode 100644 index 0000000..c552155 --- /dev/null +++ b/tests/GetPendingPositionsTest.php @@ -0,0 +1,149 @@ + '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); + } +}); diff --git a/tests/GetSingleAccountTest.php b/tests/GetSingleAccountTest.php new file mode 100644 index 0000000..37da6ba --- /dev/null +++ b/tests/GetSingleAccountTest.php @@ -0,0 +1,113 @@ + '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); +}); diff --git a/tests/PlaceOrderTest.php b/tests/PlaceOrderTest.php new file mode 100644 index 0000000..8485bdc --- /dev/null +++ b/tests/PlaceOrderTest.php @@ -0,0 +1,237 @@ + '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); + } +}); diff --git a/tests/PlacePositionTpSlOrderTest.php b/tests/PlacePositionTpSlOrderTest.php new file mode 100644 index 0000000..f9e8e67 --- /dev/null +++ b/tests/PlacePositionTpSlOrderTest.php @@ -0,0 +1,223 @@ + '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); +}); diff --git a/tests/PlaceTpSlOrderTest.php b/tests/PlaceTpSlOrderTest.php new file mode 100644 index 0000000..0e49800 --- /dev/null +++ b/tests/PlaceTpSlOrderTest.php @@ -0,0 +1,253 @@ + '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); +});