diff --git a/README.md b/README.md index d69f237..2fa8bca 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,32 @@ if ($response->getStatusCode() === 200) { } ``` +### Place Position TP/SL Order + +```php +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; + +$api = app(PlacePositionTpSlOrderRequestContract::class); + +// Place position TP/SL order with both take profit and stop loss +$response = $api->placePositionTpSlOrder( + 'BTCUSDT', // symbol + '111', // positionId + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Position TP/SL order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -243,6 +269,7 @@ if ($response->getStatusCode() === 200) { - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management - `placeTpSlOrder(...)` - Place TP/SL order for existing positions +- `placePositionTpSlOrder(...)` - Place position TP/SL order (closes position at market price when triggered) - `flashClosePosition(string $positionId)` - Flash close position by position ID ### Position Management @@ -269,6 +296,7 @@ if ($response->getStatusCode() === 200) { - **Get Single Account**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid - **Place TP/SL Order**: 10 req/sec/uid +- **Place Position TP/SL Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid - **Get Pending Positions**: 10 req/sec/uid @@ -311,6 +339,7 @@ vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/GetSingleAccountTest.php vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/PlaceTpSlOrderTest.php +vendor/bin/pest tests/PlacePositionTpSlOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/GetPendingPositionsTest.php vendor/bin/pest tests/HeaderTest.php @@ -325,6 +354,7 @@ See the `examples/` directory for complete usage examples: - `GetSingleAccountExample.php` - `PlaceOrderExample.php` - `PlaceTpSlOrderExample.php` +- `PlacePositionTpSlOrderExample.php` - `FlashClosePositionExample.php` - `GetPendingPositionsExample.php` diff --git a/examples/PlacePositionTpSlOrderExample.php b/examples/PlacePositionTpSlOrderExample.php new file mode 100644 index 0000000..78fe45a --- /dev/null +++ b/examples/PlacePositionTpSlOrderExample.php @@ -0,0 +1,254 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlacePositionTpSlOrderRequestContract::class); + + echo "🎯 Place Position TP/SL Order Examples\n\n"; + + // Example 1: Place position TP/SL order with both take profit and stop loss + echo "1. Placing position TP/SL order with both TP and SL...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 2: Place position TP/SL order with take profit only + echo "2. Placing position TP/SL order with take profit only...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'MARK_PRICE' // tpStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 3: Place position TP/SL order with stop loss only + echo "3. Placing position TP/SL order with stop loss only...\n"; + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'MARK_PRICE' // slStopType + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 4: Place position TP/SL order with different symbols + echo "4. Placing position TP/SL orders for different symbols...\n"; + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + echo "Placing position TP/SL order for {$symbol}...\n"; + + $response = $api->placePositionTpSlOrder( + $symbol, + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ {$symbol} position TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place {$symbol} position TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for {$symbol}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 5: Place position TP/SL order with different position IDs + echo "5. Placing position TP/SL orders for different position IDs...\n"; + $positionIds = ['111', '222', '333']; + + foreach ($positionIds as $positionId) { + echo "Placing position TP/SL order for position ID {$positionId}...\n"; + + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + $positionId, + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position {$positionId} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for position {$positionId}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 6: Place position TP/SL order with different stop types + echo "6. Placing position TP/SL orders with different stop types...\n"; + $stopTypeCombinations = [ + ['LAST_PRICE', 'LAST_PRICE'], + ['MARK_PRICE', 'MARK_PRICE'], + ['LAST_PRICE', 'MARK_PRICE'], + ['MARK_PRICE', 'LAST_PRICE'], + ]; + + foreach ($stopTypeCombinations as $index => $combination) { + echo "Placing position TP/SL order with stop types: {$combination[0]}, {$combination[1]}...\n"; + + $response = $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $combination[0], + '45000', + $combination[1] + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order with {$combination[0]}/{$combination[1]} placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 7: Error handling + echo "7. Error handling example...\n"; + $response = $api->placePositionTpSlOrder('INVALID', '111'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position TP/SL order placed successfully!\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + echo "This is expected for invalid symbol\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Place Position TP/SL Order Features: + * + * - Place position TP/SL orders for existing positions + * - Rate limit: 10 req/sec/UID + * - Supports both take profit and stop loss + * - When triggered, closes position at market price + * - Each position can only have one Position TP/SL Order + * + * Required Parameters: + * - symbol: Trading pair + * - positionId: Position ID associated with TP/SL + * + * Optional Parameters: + * - tpPrice: Take-profit trigger price + * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) + * - slPrice: Stop-loss trigger price + * - slStopType: Stop-loss trigger type (LAST_PRICE/MARK_PRICE) + * + * Note: At least one of tpPrice or slPrice is required. + * + * Key Differences from regular TP/SL Order: + * - Simpler parameters (no order types, prices, quantities) + * - Automatically closes position at market price when triggered + * - One order per position limit + * - Uses position/place_order endpoint + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index c0b3843..f04785d 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -11,10 +11,11 @@ use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract, PlacePositionTpSlOrderRequestContract { private Client $publicFutureClient; @@ -260,4 +261,38 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placePositionTpSlOrder( + string $symbol, + string $positionId, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $slPrice = null, + ?string $slStopType = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'positionId' => $positionId, + ]; + + // Add optional parameters if provided + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + + $response = $this->getPrivateFutureClient([], $body)->post('tpsl/position/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 5dd04dc..2ab4fcc 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -10,6 +10,8 @@ use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlacePositionTpSlOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -41,5 +43,7 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(FlashClosePositionRequestContract::class, LaravelBitunixApi::class); $this->app->bind(GetPendingPositionsRequestContract::class, LaravelBitunixApi::class); $this->app->bind(GetSingleAccountRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlaceTpSlOrderRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlacePositionTpSlOrderRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/PlacePositionTpSlOrderRequestContract.php b/src/Requests/PlacePositionTpSlOrderRequestContract.php new file mode 100644 index 0000000..1f866af --- /dev/null +++ b/src/Requests/PlacePositionTpSlOrderRequestContract.php @@ -0,0 +1,29 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place position TP/SL order with required parameters only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with take profit only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE' // tpStopType + ))->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with stop loss only', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ))->not->toThrow(Exception::class); +}); + +it('can place position TP/SL order with both take profit and stop loss', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE' // slStopType + ))->not->toThrow(Exception::class); +}); + +it('validates place position TP/SL order method exists', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + expect(method_exists($api, 'placePositionTpSlOrder'))->toBeTrue(); +}); + +it('can handle different symbol formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + expect(fn() => $api->placePositionTpSlOrder($symbol, '111')) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different position ID formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $positionIds = ['111', 'position-123', '19848247723672']; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', $positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different stop types', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; + + foreach ($stopTypes as $stopType) { + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $stopType, + '45000', + $stopType + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different price formats', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $prices = ['50000', '50000.1', '50000.01', '50000.001']; + + foreach ($prices as $price) { + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + $price, + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle multiple place position TP/SL order calls', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + $calls = [ + ['BTCUSDT', '111'], + ['ETHUSDT', '222', '3000', 'LAST_PRICE'], + ['ADAUSDT', '333', null, null, '0.5', 'LAST_PRICE'], + ['BTCUSDT', '444', '50000', 'LAST_PRICE', '45000', 'LAST_PRICE'], + ]; + + foreach ($calls as $params) { + expect(fn() => $api->placePositionTpSlOrder(...$params)) + ->not->toThrow(Exception::class); + } +}); + +it('validates place position TP/SL order response structure', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can handle edge cases for position TP/SL order', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with minimum required parameters + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); + + // Test with all parameters + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); + +it('can handle special characters in parameters', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with special characters in position ID + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', 'pos-123-abc')) + ->not->toThrow(Exception::class); + + // Test with decimal prices + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000.123', + 'LAST_PRICE', + '45000.456', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +}); + +it('can handle different combinations of parameters', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with only TP price + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000')) + ->not->toThrow(Exception::class); + + // Test with only SL price + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000')) + ->not->toThrow(Exception::class); + + // Test with only TP stop type + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', '50000', 'MARK_PRICE')) + ->not->toThrow(Exception::class); + + // Test with only SL stop type + expect(fn() => $api->placePositionTpSlOrder('BTCUSDT', '111', null, null, '45000', 'MARK_PRICE')) + ->not->toThrow(Exception::class); +}); + +it('can handle position TP/SL order with mixed stop types', function () { + $api = app(PlacePositionTpSlOrderRequestContract::class); + + // Test with different stop types for TP and SL + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'MARK_PRICE' + ))->not->toThrow(Exception::class); + + // Test with opposite stop types + expect(fn() => $api->placePositionTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'MARK_PRICE', + '45000', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +});