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); + } +});