From 9d70e9ae44c61f70e108d1bbbbb5cc6d7a9b787a Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 17:18:44 +0330 Subject: [PATCH] future-private: add place order request --- README.md | 48 +++++ examples/PlaceOrderExample.php | 167 +++++++++++++++ src/LaravelBitunixApi.php | 79 ++++++- src/LaravelBitunixApiServiceProvider.php | 2 + src/Requests/PlaceOrderRequestContract.php | 52 +++++ tests/PlaceOrderTest.php | 237 +++++++++++++++++++++ 6 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 examples/PlaceOrderExample.php create mode 100644 src/Requests/PlaceOrderRequestContract.php create mode 100644 tests/PlaceOrderTest.php diff --git a/README.md b/README.md index af22d74..6dde83b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,47 @@ if ($response->getStatusCode() === 200) { } ``` +### Place Order + +```php +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; + +$api = app(PlaceOrderRequestContract::class); + +// Basic market order +$response = $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET'); + +// Limit order with take profit and stop loss +$response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', // price + null, // positionId + 'GTC', // effect + 'order-123', // clientId + false, // reduceOnly + '51000', // tpPrice + 'MARK_PRICE', // tpStopType + 'LIMIT', // tpOrderType + '51000.1', // tpOrderPrice + '49000', // slPrice + 'MARK_PRICE', // slStopType + 'LIMIT', // slOrderType + '49000.1' // slOrderPrice +); + +if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "Order placed successfully!"; + echo "Order ID: " . $data['data']['orderId']; + } +} +``` + ### Get Future Kline Data ```php @@ -89,6 +130,10 @@ if ($response->getStatusCode() === 200) { - `changeLeverage(string $symbol, string $marginCoin, int $leverage)` - Change leverage - `changeMarginMode(string $symbol, string $marginCoin, string $marginMode)` - Change margin mode +### Trading + +- `placeOrder(...)` - Place a new order with full support for all order types, take profit, stop loss, and position management + ### Market Data - `getFutureKline(string $symbol, string $interval, int $limit, ?int $startTime, ?int $endTime, string $type)` - Get kline data @@ -106,6 +151,7 @@ if ($response->getStatusCode() === 200) { - **Change Leverage**: 10 req/sec/uid - **Change Margin Mode**: 10 req/sec/uid +- **Place Order**: 10 req/sec/uid ## Error Handling @@ -143,6 +189,7 @@ Run specific tests: ```bash vendor/bin/pest tests/ChangeLeverageTest.php vendor/bin/pest tests/ChangeMarginModeTest.php +vendor/bin/pest tests/PlaceOrderTest.php vendor/bin/pest tests/HeaderTest.php ``` @@ -152,6 +199,7 @@ See the `examples/` directory for complete usage examples: - `ChangeLeverageExample.php` - `ChangeMarginModeExample.php` +- `PlaceOrderExample.php` ## Troubleshooting diff --git a/examples/PlaceOrderExample.php b/examples/PlaceOrderExample.php new file mode 100644 index 0000000..e00478c --- /dev/null +++ b/examples/PlaceOrderExample.php @@ -0,0 +1,167 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(PlaceOrderRequestContract::class); + + echo "🚀 Placing Orders Examples\n\n"; + + // Example 1: Basic Market Order + echo "1. Placing a basic market order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET' + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Market order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 2: Limit Order with Take Profit and Stop Loss + echo "2. Placing a limit order with TP/SL...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', // price + null, // positionId + 'GTC', // effect + 'order-123', // clientId + false, // reduceOnly + '51000', // tpPrice + 'MARK_PRICE', // tpStopType + 'LIMIT', // tpOrderType + '51000.1', // tpOrderPrice + '49000', // slPrice + 'MARK_PRICE', // slStopType + 'LIMIT', // slOrderType + '49000.1' // slOrderPrice + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Limit order with TP/SL placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + echo "Client ID: " . $data['data']['clientId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 3: Close Position Order + echo "3. Placing a close position order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123' // positionId required for CLOSE + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Close position order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + + echo "\n"; + + // Example 4: Reduce Only Order + echo "4. Placing a reduce only order...\n"; + $response = $api->placeOrder( + 'BTCUSDT', + '0.05', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123', + null, + null, + true // reduceOnly + ); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + if ($data['code'] === 0) { + echo "✅ Reduce only order placed successfully!\n"; + echo "Order ID: " . $data['data']['orderId'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } + +} catch (Exception $e) { + echo '❌ Exception: '.$e->getMessage()."\n"; +} + +/** + * Order Types: + * - LIMIT: Limit orders (requires price) + * - MARKET: Market orders + * + * Order Sides: + * - BUY: Buy order + * - SELL: Sell order + * + * Trade Sides: + * - OPEN: Open a new position + * - CLOSE: Close an existing position (requires positionId) + * + * Effect Types: + * - IOC: Immediate or cancel + * - FOK: Fill or kill + * - GTC: Good till canceled (default) + * - POST_ONLY: POST only + * + * Take Profit / Stop Loss Types: + * - MARK_PRICE: Mark price + * - LAST_PRICE: Last price + * + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index d4aa5a5..33d976e 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -7,9 +7,10 @@ use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginModeRequestContract, FutureKLineRequestContract, PlaceOrderRequestContract { private Client $publicFutureClient; @@ -76,4 +77,80 @@ class LaravelBitunixApi implements ChangeLeverageRequestContract, ChangeMarginMo return $response; } + + public function placeOrder( + string $symbol, + string $qty, + string $side, + string $tradeSide, + string $orderType, + ?string $price = null, + ?string $positionId = null, + ?string $effect = null, + ?string $clientId = null, + ?bool $reduceOnly = null, + ?string $tpPrice = null, + ?string $tpStopType = null, + ?string $tpOrderType = null, + ?string $tpOrderPrice = null, + ?string $slPrice = null, + ?string $slStopType = null, + ?string $slOrderType = null, + ?string $slOrderPrice = null + ): ResponseInterface { + $body = [ + 'symbol' => $symbol, + 'qty' => $qty, + 'side' => $side, + 'tradeSide' => $tradeSide, + 'orderType' => $orderType, + ]; + + // Add optional parameters if provided + if ($price !== null) { + $body['price'] = $price; + } + if ($positionId !== null) { + $body['positionId'] = $positionId; + } + if ($effect !== null) { + $body['effect'] = $effect; + } + if ($clientId !== null) { + $body['clientId'] = $clientId; + } + if ($reduceOnly !== null) { + $body['reduceOnly'] = $reduceOnly; + } + if ($tpPrice !== null) { + $body['tpPrice'] = $tpPrice; + } + if ($tpStopType !== null) { + $body['tpStopType'] = $tpStopType; + } + if ($tpOrderType !== null) { + $body['tpOrderType'] = $tpOrderType; + } + if ($tpOrderPrice !== null) { + $body['tpOrderPrice'] = $tpOrderPrice; + } + if ($slPrice !== null) { + $body['slPrice'] = $slPrice; + } + if ($slStopType !== null) { + $body['slStopType'] = $slStopType; + } + if ($slOrderType !== null) { + $body['slOrderType'] = $slOrderType; + } + if ($slOrderPrice !== null) { + $body['slOrderPrice'] = $slOrderPrice; + } + + $response = $this->getPrivateFutureClient([], $body)->post('trade/place_order', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index 4161e5d..8360620 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -6,6 +6,7 @@ use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeMarginModeRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\PlaceOrderRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -33,5 +34,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); $this->app->bind(ChangeMarginModeRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(PlaceOrderRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/PlaceOrderRequestContract.php b/src/Requests/PlaceOrderRequestContract.php new file mode 100644 index 0000000..54f261f --- /dev/null +++ b/src/Requests/PlaceOrderRequestContract.php @@ -0,0 +1,52 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can place a basic market order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET' + ))->not->toThrow(Exception::class); +}); + +it('can place a limit order with price', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000' + ))->not->toThrow(Exception::class); +}); + +it('can place an order with all optional parameters', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + '12345', + 'GTC', + 'custom-client-id', + false, + '51000', + 'MARK_PRICE', + 'LIMIT', + '51000.1', + '49000', + 'MARK_PRICE', + 'LIMIT', + '49000.1' + ))->not->toThrow(Exception::class); +}); + +it('can place a close position order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123' + ))->not->toThrow(Exception::class); +}); + +it('validates required parameters for place order', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder('BTCUSDT', '0.1', 'BUY', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class) + ->and(fn() => $api->placeOrder('BTCUSDT', '0.1', 'SELL', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); +}); + +it('handles different order types correctly', function () { + $api = app(PlaceOrderRequestContract::class); + + $orderTypes = ['LIMIT', 'MARKET']; + + foreach ($orderTypes as $orderType) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + $orderType, + $orderType === 'LIMIT' ? '50000' : null + ))->not->toThrow(Exception::class); + } +}); + +it('handles different trade sides correctly', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradeSides = ['OPEN', 'CLOSE']; + + foreach ($tradeSides as $tradeSide) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + $tradeSide, + 'MARKET', + null, + $tradeSide === 'CLOSE' ? 'position-123' : null + ))->not->toThrow(Exception::class); + } +}); + +it('can handle different trading pairs', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradingPairs = ['BTCUSDT', 'ETHUSDT', 'ADAUSDT']; + + foreach ($tradingPairs as $symbol) { + expect(fn() => $api->placeOrder($symbol, '0.1', 'BUY', 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); + } +}); + +it('validates order side parameter values', function () { + $api = app(PlaceOrderRequestContract::class); + + $sides = ['BUY', 'SELL']; + + foreach ($sides as $side) { + expect(fn() => $api->placeOrder('BTCUSDT', '0.1', $side, 'OPEN', 'MARKET')) + ->not->toThrow(Exception::class); + } +}); + +it('validates trade side parameter values', function () { + $api = app(PlaceOrderRequestContract::class); + + $tradeSides = ['OPEN', 'CLOSE']; + + foreach ($tradeSides as $tradeSide) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + $tradeSide, + 'MARKET', + null, + $tradeSide === 'CLOSE' ? 'position-123' : null + ))->not->toThrow(Exception::class); + } +}); + +it('can handle take profit and stop loss parameters', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + null, + 'GTC', + null, + false, + '51000', + 'MARK_PRICE', + 'LIMIT', + '51000.1', + '49000', + 'MARK_PRICE', + 'LIMIT', + '49000.1' + ))->not->toThrow(Exception::class); +}); + +it('can handle reduce only orders', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'SELL', + 'CLOSE', + 'MARKET', + null, + 'position-123', + null, + null, + true + ))->not->toThrow(Exception::class); +}); + +it('can handle custom client ID', function () { + $api = app(PlaceOrderRequestContract::class); + + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'MARKET', + null, + null, + null, + 'custom-order-123' + ))->not->toThrow(Exception::class); +}); + +it('can handle different effect types', function () { + $api = app(PlaceOrderRequestContract::class); + + $effects = ['IOC', 'FOK', 'GTC', 'POST_ONLY']; + + foreach ($effects as $effect) { + expect(fn() => $api->placeOrder( + 'BTCUSDT', + '0.1', + 'BUY', + 'OPEN', + 'LIMIT', + '50000', + null, + $effect + ))->not->toThrow(Exception::class); + } +});