diff --git a/README.md b/README.md index b2b1ac1..d69f237 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,38 @@ if ($response->getStatusCode() === 200) { } ``` +### Place TP/SL Order + +```php +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; + +$api = app(PlaceTpSlOrderRequestContract::class); + +// Place TP/SL order with both take profit and stop loss +$response = $api->placeTpSlOrder( + 'BTCUSDT', // symbol + '111', // positionId + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "TP/SL order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -210,6 +242,7 @@ if ($response->getStatusCode() === 200) { ### Trading - `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management +- `placeTpSlOrder(...)` - Place TP/SL order for existing positions - `flashClosePosition(string $positionId)` - Flash close position by position ID ### Position Management @@ -235,6 +268,7 @@ if ($response->getStatusCode() === 200) { - **Change Margin Mode**: 10 req/sec/uid - **Get Single Account**: 10 req/sec/uid - **Place Order**: 10 req/sec/uid +- **Place TP/SL Order**: 10 req/sec/uid - **Flash Close Position**: 5 req/sec/uid - **Get Pending Positions**: 10 req/sec/uid @@ -276,6 +310,7 @@ vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php vendor/bin/pest tests/GetSingleAccountTest.php vendor/bin/pest tests/PlaceOrderTest.php +vendor/bin/pest tests/PlaceTpSlOrderTest.php vendor/bin/pest tests/FlashClosePositionTest.php vendor/bin/pest tests/GetPendingPositionsTest.php vendor/bin/pest tests/HeaderTest.php @@ -289,6 +324,7 @@ See the `examples/` directory for complete usage examples: - `ChangeMarginModeExample.php` - `GetSingleAccountExample.php` - `PlaceOrderExample.php` +- `PlaceTpSlOrderExample.php` - `FlashClosePositionExample.php` - `GetPendingPositionsExample.php` diff --git a/examples/PlaceTpSlOrderExample.php b/examples/PlaceTpSlOrderExample.php new file mode 100644 index 0000000..535d651 --- /dev/null +++ b/examples/PlaceTpSlOrderExample.php @@ -0,0 +1,247 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlaceTpSlOrderRequestContract::class); + + echo "🎯 Place TP/SL Order Examples\n\n"; + + // Example 1: Place TP/SL order with both take profit and stop loss + echo "1. Placing TP/SL order with both TP and SL...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 2: Place TP/SL order with take profit only + echo "2. Placing TP/SL order with take profit only...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'MARK_PRICE', // tpStopType + null, // slPrice + null, // slStopType + 'MARKET', // tpOrderType + null, // tpOrderPrice + null, // slOrderType + null, // slOrderPrice + '1', // tpQty + null // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 3: Place TP/SL order with stop loss only + echo "3. Placing TP/SL order with stop loss only...\n"; + $response = $api->placeTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'MARK_PRICE', // slStopType + null, // tpOrderType + null, // tpOrderPrice + 'MARKET', // slOrderType + null, // slOrderPrice + null, // tpQty + '1' // slQty + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + + echo "\n"; + + // Example 4: Place TP/SL order with different symbols + echo "4. Placing TP/SL orders for different symbols...\n"; + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + echo "Placing TP/SL order for {$symbol}...\n"; + + $response = $api->placeTpSlOrder( + $symbol, + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ {$symbol} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place {$symbol} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for {$symbol}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 5: Place TP/SL order with different position IDs + echo "5. Placing TP/SL orders for different position IDs...\n"; + $positionIds = ['111', '222', '333']; + + foreach ($positionIds as $positionId) { + echo "Placing TP/SL order for position ID {$positionId}...\n"; + + $response = $api->placeTpSlOrder( + 'BTCUSDT', + $positionId, + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Position {$positionId} TP/SL order placed successfully!\n"; + echo 'Order ID: '.$data['data']['orderId']."\n"; + } else { + echo "❌ Failed to place position {$positionId} TP/SL order: ".$data['msg']."\n"; + } + } else { + echo "❌ HTTP Error for position {$positionId}: ".$response->getStatusCode()."\n"; + } + } + + echo "\n"; + + // Example 6: Error handling + echo "6. Error handling example...\n"; + $response = $api->placeTpSlOrder('INVALID', '111'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ TP/SL order placed successfully!\n"; + } else { + echo '❌ API Error: '.$data['msg']."\n"; + echo "This is expected for invalid symbol\n"; + } + } else { + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Place TP/SL Order Features: + * + * - Place TP/SL orders for existing positions + * - Rate limit: 10 req/sec/UID + * - Supports both take profit and stop loss + * - Flexible parameter configuration + * + * Required Parameters: + * - symbol: Trading pair + * - positionId: Position ID associated with TP/SL + * + * Optional Parameters: + * - tpPrice: Take-profit trigger price + * - tpStopType: Take-profit trigger type (LAST_PRICE/MARK_PRICE) + * - slPrice: Stop-loss trigger price + * - slStopType: Stop-loss trigger type (LAST_PRICE/MARK_PRICE) + * - tpOrderType: Take-profit order type (LIMIT/MARKET) + * - tpOrderPrice: Take-profit order price + * - slOrderType: Stop-loss order type (LIMIT/MARKET) + * - slOrderPrice: Stop-loss order price + * - tpQty: Take-profit order quantity (base coin) + * - slQty: Stop-loss order quantity (base coin) + * + * Note: At least one of tpPrice or slPrice is required. + * At least one of tpQty or slQty is required. + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 8f6ce69..c0b3843 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -11,9 +11,10 @@ use Msr\LaravelBitunixApi\Requests\GetPendingPositionsRequestContract; use Msr\LaravelBitunixApi\Requests\GetSingleAccountRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceTpSlOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FlashClosePositionRequestContract, FutureKLineRequestContract, GetPendingPositionsRequestContract, GetSingleAccountRequestContract, PlaceOrderRequestContract, PlaceTpSlOrderRequestContract { private Client $publicFutureClient; @@ -201,4 +202,62 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placeTpSlOrder( + string $symbol, + string $positionId, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $slPrice = null, + ?string $slStopType = null, + ?string $tpOrderType = null, + ?string $tpOrderPrice = null, + ?string $slOrderType = null, + ?string $slOrderPrice = null, + ?string $tpQty = null, + ?string $slQty = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'positionId' => $positionId, + ]; + + // Add optional parameters if provided + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + if ($tpOrderType !== null) { + $body['tpOrderType'] = $tpOrderType; + } + if ($tpOrderPrice !== null) { + $body['tpOrderPrice'] = $tpOrderPrice; + } + if ($slOrderType !== null) { + $body['slOrderType'] = $slOrderType; + } + if ($slOrderPrice !== null) { + $body['slOrderPrice'] = $slOrderPrice; + } + if ($tpQty !== null) { + $body['tpQty'] = $tpQty; + } + if ($slQty !== null) { + $body['slQty'] = $slQty; + } + + $response = $this->getPrivateFutureClient([], $body)->post('tpsl/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/Requests/PlaceTpSlOrderRequestContract.php b/src/Requests/PlaceTpSlOrderRequestContract.php new file mode 100644 index 0000000..5e8aba4 --- /dev/null +++ b/src/Requests/PlaceTpSlOrderRequestContract.php @@ -0,0 +1,39 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place TP/SL order with required parameters only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with take profit only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + null, // slPrice + null, // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + null, // slOrderType + null, // slOrderPrice + '1', // tpQty + null // slQty + ))->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with stop loss only', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + null, // tpPrice + null, // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + null, // tpOrderType + null, // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + null, // tpQty + '1' // slQty + ))->not->toThrow(Exception::class); +}); + +it('can place TP/SL order with both take profit and stop loss', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', // tpPrice + 'LAST_PRICE', // tpStopType + '45000', // slPrice + 'LAST_PRICE', // slStopType + 'LIMIT', // tpOrderType + '50000.1', // tpOrderPrice + 'LIMIT', // slOrderType + '45000.1', // slOrderPrice + '1', // tpQty + '1' // slQty + ))->not->toThrow(Exception::class); +}); + +it('validates place TP/SL order method exists', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + expect(method_exists($api, 'placeTpSlOrder'))->toBeTrue(); +}); + +it('can handle different symbol formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $symbols = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($symbols as $symbol) { + expect(fn() => $api->placeTpSlOrder($symbol, '111')) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different position ID formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $positionIds = ['111', 'position-123', '19848247723672']; + + foreach ($positionIds as $positionId) { + expect(fn() => $api->placeTpSlOrder('BTCUSDT', $positionId)) + ->not->toThrow(Exception::class); + } +}); + +it('can handle different stop types', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $stopTypes = ['LAST_PRICE', 'MARK_PRICE']; + + foreach ($stopTypes as $stopType) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + $stopType, + '45000', + $stopType + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different order types', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $orderTypes = ['LIMIT', 'MARKET']; + + foreach ($orderTypes as $orderType) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + $orderType, + '50000.1', + $orderType, + '45000.1' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different quantity formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $quantities = ['1', '0.1', '10.5', '100']; + + foreach ($quantities as $qty) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + $qty, + $qty + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different price formats', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $prices = ['50000', '50000.1', '50000.01', '50000.001']; + + foreach ($prices as $price) { + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + $price, + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + $price, + 'LIMIT', + '45000.1' + ))->not->toThrow(Exception::class); + } +}); + +it('can handle multiple place TP/SL order calls', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + $calls = [ + ['BTCUSDT', '111'], + ['ETHUSDT', '222', '3000', 'LAST_PRICE'], + ['ADAUSDT', '333', null, null, '0.5', 'LAST_PRICE'], + ['BTCUSDT', '444', '50000', 'LAST_PRICE', '45000', 'LAST_PRICE', 'LIMIT', '50000.1', 'LIMIT', '45000.1', '1', '1'], + ]; + + foreach ($calls as $params) { + expect(fn() => $api->placeTpSlOrder(...$params)) + ->not->toThrow(Exception::class); + } +}); + +it('validates place TP/SL order response structure', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // This test verifies the method can be called without throwing exceptions + // The actual response structure will be validated by the API + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); +}); + +it('can handle edge cases for TP/SL order', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // Test with minimum required parameters + expect(fn() => $api->placeTpSlOrder('BTCUSDT', '111')) + ->not->toThrow(Exception::class); + + // Test with all parameters + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000', + 'LAST_PRICE', + '45000', + 'LAST_PRICE', + 'LIMIT', + '50000.1', + 'LIMIT', + '45000.1', + '1', + '1' + ))->not->toThrow(Exception::class); +}); + +it('can handle special characters in parameters', function () { + $api = app(PlaceTpSlOrderRequestContract::class); + + // Test with special characters in position ID + expect(fn() => $api->placeTpSlOrder('BTCUSDT', 'pos-123-abc')) + ->not->toThrow(Exception::class); + + // Test with decimal prices + expect(fn() => $api->placeTpSlOrder( + 'BTCUSDT', + '111', + '50000.123', + 'LAST_PRICE', + '45000.456', + 'LAST_PRICE' + ))->not->toThrow(Exception::class); +});