diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0ff94df --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Files excluded from `composer archive` / Packagist distribution. +# Consumers installing via `composer require` only get src/ + composer.json +# + LICENSE + README + CHANGELOG — dev tooling stays in the repo. + +/tests export-ignore +/examples export-ignore +/.github export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/.php-cs-fixer.dist.php export-ignore +/.php-version export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1e95005 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,199 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + build: + name: PHP ${{ matrix.php }} — install + cs-check + phpstan + phpunit + smoke + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Matches composer.json `php: >=8.1`. 8.1 is our floor; 8.3 is + # current stable; 8.4 is latest. + php: ['8.1', '8.2', '8.3', '8.4'] + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring, curl, openssl + coverage: none + tools: composer:v2 + + - name: composer.json → Packagist only (no private-registry drift) + run: | + # composer.json must not declare arbitrary `repositories` — that + # bypasses Packagist. `type: composer` at packagist.org is the + # single default we allow. + if command -v jq >/dev/null 2>&1; then + bad=$(jq -c '.repositories // [] | map(select(.type != null and .type != "composer"))' composer.json) + if [ "$bad" != "[]" ] && [ -n "$bad" ]; then + echo "::error::composer.json has non-Packagist repositories: $bad" + exit 1 + fi + fi + + - name: Cache composer deps + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: vendor + key: composer-${{ matrix.php }}-${{ hashFiles('composer.json', 'composer.lock') }} + restore-keys: | + composer-${{ matrix.php }}- + + - name: composer install + run: composer install --no-interaction --no-progress --prefer-dist + + - name: PHP-CS-Fixer (dry-run) + run: vendor/bin/php-cs-fixer fix --dry-run --diff --no-interaction + + - name: PHPStan + run: vendor/bin/phpstan analyse --no-progress + + - name: PHPUnit + run: vendor/bin/phpunit --testdox --no-coverage + + # Smoke: autoload the package and construct a Client. Catches broken + # PSR-4 mapping / missing class / class-name typo BEFORE any consumer + # sees them. Analogous to the JS / Ruby equivalents. + - name: Load smoke (autoload + Client construction) + run: | + php -r ' + require "vendor/autoload.php"; + if (!defined("Bybit\\Bybit::VERSION")) { + throw new RuntimeException("Bybit\\Bybit::VERSION not defined"); + } + $cli = new Bybit\Client(new Bybit\Configuration()); + if (!$cli instanceof Bybit\Client) { + throw new RuntimeException("Client construction failed"); + } + printf("smoke OK: bybit-connector-php %s on PHP %s\n", Bybit\Bybit::VERSION, PHP_VERSION); + ' + + lowest-deps: + # Verify the SDK still builds and tests pass against the LOWEST versions + # of every `require` constraint (guzzle ^7.5 → 7.5.0, etc.). The regular + # `build` job resolves highest-compatible and would silently start using + # a Guzzle 7.6+-only API without bumping the constraint, which would break + # users pinned to 7.5. + name: PHP 8.1 — prefer-lowest dep resolve + phpunit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.1' + extensions: json, mbstring, curl, openssl + coverage: none + tools: composer:v2 + + - name: composer update --prefer-lowest + run: composer update --no-interaction --no-progress --prefer-dist --prefer-lowest --prefer-stable + + - name: PHPUnit + run: vendor/bin/phpunit --testdox --no-coverage + + audit: + name: composer audit (advisories on runtime deps) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + tools: composer:v2 + + - name: composer install + run: composer install --no-interaction --no-progress --prefer-dist + + # `composer audit` (Composer 2.4+) queries the FriendsOfPHP / Packagist + # advisory database and exits non-zero on any advisory. Runtime deps + # only (not require-dev) via `--no-dev`. + - name: composer audit --no-dev + run: composer audit --no-dev + + secret-scan: + name: gitleaks (CLI) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + + # Run the gitleaks binary directly. The `gitleaks/gitleaks-action@v2` + # wrapper requires a paid license for organizations and pins a deprecated + # node20 runtime — the underlying MIT-licensed CLI has neither restriction. + - name: Install gitleaks + env: + GITLEAKS_VERSION: '8.21.2' + run: | + set -eu + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + run: gitleaks detect --source . --redact --verbose --exit-code 1 --no-banner + + pack-check: + name: composer archive dry-run (LICENSE / README / src present, no test leak) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + tools: composer:v2 + + - name: composer install + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Build archive + run: composer archive --format=tar --dir=/tmp/pack --file=pack + + - name: Verify archive contents + run: | + set -eu + echo "Inspecting /tmp/pack/pack.tar" + tar -tf /tmp/pack/pack.tar | sort > /tmp/pack-files.txt + echo "--- packed files ---" + cat /tmp/pack-files.txt + + # Required entries + for need in LICENSE README.md CHANGELOG.md composer.json src/Bybit.php src/Configuration.php src/Client.php src/Session.php src/Authentication.php src/RestApi/BaseService.php; do + if ! grep -qx "$need" /tmp/pack-files.txt; then + echo "::error::Missing from archive: $need" + exit 1 + fi + done + + # Files that must NEVER ship in the distributed package. `composer archive` + # respects `.gitattributes` `export-ignore`; this is a belt-and-suspenders + # regression guard. + for bad in tests phpunit.xml.dist phpstan.neon.dist .php-cs-fixer.dist.php .github .git; do + if grep -qE "^${bad}(/|$)" /tmp/pack-files.txt; then + echo "::error::Archive contains unexpected entry: $bad" + exit 1 + fi + done + echo "pack check OK" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..73647c5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,157 @@ +name: Publish to Packagist + +on: + push: + tags: + # Accept both `1.2.3` and conventional `v1.2.3`. Strip the leading `v` + # before comparing against Bybit\Bybit::VERSION below. + - 'v?[0-9]+.[0-9]+.[0-9]+' + +# Serialize tag pushes: two tags in quick succession would otherwise race on +# `gh release create` and Packagist re-crawl. cancel-in-progress: false so a +# late tag never aborts a valid publish already underway. +concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write # for creating the GitHub Release + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + # Do NOT persist GITHUB_TOKEN into .git/config — nothing in this job + # runs `git push`, and `composer install` executes third-party code + # under contents:write scope. `gh release create` gets its token + # explicitly via env below. + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.3' + extensions: json, mbstring, curl, openssl + coverage: none + tools: composer:v2 + + - name: Validate tag is on main branch + run: | + set -eu + if ! git merge-base --is-ancestor "$GITHUB_SHA" "$(git rev-parse origin/main)"; then + echo "Error: tag '$GITHUB_REF_NAME' is not reachable from origin/main" + exit 1 + fi + echo "Branch check passed: tag is on main" + + - name: Validate tag matches src/Bybit.php VERSION constant + run: | + set -eu + composer install --no-interaction --no-progress --prefer-dist + PKG_VERSION=$(php -r 'require "vendor/autoload.php"; echo Bybit\Bybit::VERSION;') + # Strip an optional leading `v` (e.g. `v0.1.0` → `0.1.0`) so both tag + # conventions produce the same comparison against the VERSION constant. + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "Error: tag '$GITHUB_REF_NAME' (parsed as '$TAG_VERSION') does not match Bybit\\Bybit::VERSION '$PKG_VERSION'" + exit 1 + fi + echo "Version check passed: $TAG_VERSION" + + - name: PHP-CS-Fixer + PHPStan + PHPUnit must pass before publish + run: | + vendor/bin/php-cs-fixer fix --dry-run --diff --no-interaction + vendor/bin/phpstan analyse --no-progress + vendor/bin/phpunit --no-coverage + + - name: composer audit --no-dev (no live advisories on runtime deps) + run: composer audit --no-dev + + - name: Verify archive builds cleanly + run: | + set -eu + composer archive --format=tar --dir=/tmp/pack --file=pack + tar -tf /tmp/pack/pack.tar | sort > /tmp/files.txt + for need in LICENSE README.md composer.json src/Bybit.php src/Client.php src/Session.php src/Authentication.php; do + if ! grep -qx "$need" /tmp/files.txt; then + echo "::error::Missing from release archive: $need" + exit 1 + fi + done + # Mirror ci.yml's forbidden-entry check as a last-gate belt-and-suspenders + # before the tarball ships to Packagist. + for bad in tests phpunit.xml.dist phpstan.neon.dist .php-cs-fixer.dist.php .github .git; do + if grep -qE "^${bad}(/|$)" /tmp/files.txt; then + echo "::error::Archive contains unexpected entry: $bad" + exit 1 + fi + done + echo "archive OK" + + # Order matters: create the GitHub Release FIRST, then poke Packagist. + # If Packagist accepts but the Release step later fails, users can + # `composer require` a version whose GitHub Release 404s — split state + # that requires manual reconciliation. Reverse order keeps the visible + # (GitHub Release) side ahead of the searchable (Packagist) side. + - name: Create GitHub Release with CHANGELOG excerpt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#v}" + # Extract the section for THIS version from CHANGELOG.md — everything + # between the "## " header and the next "## " header. Falls + # back to a placeholder if the section isn't found (e.g. author forgot + # to update CHANGELOG.md — publish should still succeed). + if grep -q "^## ${VERSION}\b" CHANGELOG.md; then + NOTES=$(awk -v tag="^## ${VERSION}\\b" ' + $0 ~ tag { flag=1; print; next } + flag && /^## / { exit } + flag { print } + ' CHANGELOG.md) + else + NOTES="See CHANGELOG.md for details." + fi + # `gh release create` fails if the release already exists; that's + # fine — a re-push of the same tag is either intentional (in which + # case delete the release first) or a mistake (in which case + # failing is correct). + printf '%s' "$NOTES" | gh release create "$TAG" --title "$TAG" --notes-file - + + # Packagist auto-mirrors public GitHub tags when the repo is registered + # AND the Packagist webhook is configured on the GitHub repo. This step + # is a manual "poke" that forces Packagist to re-crawl immediately + # rather than waiting for the next scheduled sync — same idea as + # `pod trunk push` for CocoaPods. + # + # Requires TWO secrets on the repo settings: + # - PACKAGIST_USERNAME (your packagist.org login) + # - PACKAGIST_API_TOKEN (from packagist.org → Profile → Show API Token) + # If either secret is missing, this step logs a note and skips — the + # tag itself is still what triggers Packagist's automatic mirroring. + - name: Notify Packagist to re-crawl + env: + PACKAGIST_USERNAME: ${{ secrets.PACKAGIST_USERNAME }} + PACKAGIST_API_TOKEN: ${{ secrets.PACKAGIST_API_TOKEN }} + run: | + set -eu + if [ -z "${PACKAGIST_USERNAME:-}" ] || [ -z "${PACKAGIST_API_TOKEN:-}" ]; then + echo "::notice::PACKAGIST_USERNAME / PACKAGIST_API_TOKEN not set — skipping manual poke. Packagist will still auto-mirror on the next scheduled crawl." + exit 0 + fi + REPO_URL="https://github.com/${GITHUB_REPOSITORY}" + RESP=$(curl -s -o /tmp/packagist.out -w '%{http_code}' \ + -XPOST -H 'content-type:application/json' \ + "https://packagist.org/api/update-package?username=${PACKAGIST_USERNAME}&apiToken=${PACKAGIST_API_TOKEN}" \ + -d "{\"repository\":{\"url\":\"${REPO_URL}\"}}") + echo "Packagist API responded ${RESP}" + cat /tmp/packagist.out || true + if [ "$RESP" != "202" ] && [ "$RESP" != "200" ]; then + echo "::error::Packagist re-crawl request failed with HTTP ${RESP}" + exit 1 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9ec713 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/vendor/ +/coverage/ +/build/ +/tmp/ +/log/ +composer.lock +.phpunit.result.cache +.phpunit.cache/ +.php-cs-fixer.cache +.phpstan.cache +.DS_Store +.env +.env.local +.idea/ +.vscode/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..66bf1a5 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,22 @@ +in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ->in(__DIR__ . '/examples') + ->name('*.php'); + +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + 'declare_strict_types' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_unused_imports' => true, + 'ordered_imports' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + ]) + ->setFinder($finder); diff --git a/.php-version b/.php-version new file mode 100644 index 0000000..cf02201 --- /dev/null +++ b/.php-version @@ -0,0 +1 @@ +8.3 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e1d0cb8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.1.0 + +Initial public release of the official Bybit V5 REST connector for PHP. + +### Added +- Full Bybit V5 REST surface across 14 service classes (market, trade, position, account, asset, user, affiliate, broker, cryptoLoan, rfq, spotMargin, earn, p2p, bot) with ~240 typed endpoint methods. +- HMAC-SHA256 request signing (`X-BAPI-SIGN` / `X-BAPI-TIMESTAMP` / `X-BAPI-API-KEY` / `X-BAPI-RECV-WINDOW`) with a byte-identity invariant between the payload signed and the query string on the wire. +- Typed exception hierarchy rooted at `Bybit\Exception\BybitException`: transport-layer (`Timeout`, `Network`, `Server`, `Client`, `Parse`, `Transport`) and API-body (`Api`, `Auth`, `RateLimit`) branches. HTTP `401/403` route to `AuthException`, `429` to `RateLimitException`. +- `Bybit\Configuration` snapshot with credential redaction on every default PHP serialization pathway (`__toString`, `__debugInfo`, `jsonSerialize`); `baseUrl` / `testnet` / `timeout` / `httpClient` are readonly and set via constructor. +- Optional snake_case → camelCase key translation for callers via `Bybit\Util\WireKeys`, wired into every signed and public request. +- `Bybit\Client::raw()` escape hatch for endpoints not yet wrapped by a service class. +- Legacy P2P snake_case envelope (`ret_code` / `ret_msg` / `time_now` / `ext_info`) normalized to the V5 camelCase shape at the response boundary. +- Guzzle-based HTTP transport with `allow_redirects: false` to prevent `X-BAPI-*` header replay via WAF/proxy 302s. +- PHP 8.1 / 8.2 / 8.3 / 8.4 CI matrix, PHPStan level 6, PHP-CS-Fixer, PHPUnit strict mode (`failOnRisky` + `failOnWarning`), Composer archive dry-run, `gitleaks` secret scan. +- Runnable end-to-end example at `examples/quickstart.php`; full pagination + error-handling docs in `README.md`. diff --git a/LICENSE b/LICENSE index 74e50c5..685b70c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 bybit-exchange +Copyright (c) 2026-present bybit-exchange Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7611c83..22baac0 100644 --- a/README.md +++ b/README.md @@ -1 +1,299 @@ -# bybit.php.api \ No newline at end of file +# bybit-connector-php + +Official lightweight PHP connector for the [Bybit V5 REST API](https://bybit-exchange.github.io/docs/v5/intro). + +`bybit-connector-php` wraps the Bybit V5 HTTP endpoints as a set of typed PHP methods with explicit required arguments plus an `array $options = []` catch-all for optional parameters. Its goal is the same as [`pybit`](https://github.com/bybit-exchange/pybit) on the Python side and [`bybit-connector-ruby`](https://github.com/bybit-exchange/bybit.ruby.api) on the Ruby side: an easy-to-use, high-performance connector with a small dependency footprint. + +## Prerequisites + +Before you write any code you need a Bybit API key. Two accounts to know +about: + +- **Testnet** — [testnet.bybit.com](https://testnet.bybit.com) → sign up → + API Management → *Create New Key*. This is where you should point every + new integration until you're confident about behavior. Testnet balances + are virtual; nothing you do here touches real funds. +- **Mainnet** — [bybit.com](https://www.bybit.com) → API Management. Real + money. Enable only the permissions you actually need (spot / derivatives / + withdrawals) and prefer IP-restricted keys. + +Testnet and mainnet keys are **separate** — an API key issued on one won't +work against the other. The SDK selects the environment via the +`testnet: true|false` constructor argument on `Configuration` (or an explicit +`baseUrl:` override). + +## Installation + +PHP >= 8.1 is required (PHP 8.3.x recommended). Composer 2.x. + +``` +composer require bybit-exchange/bybit-connector-php +``` + +## Quick Start + +```php +market->getServerTime()); + +// 2. Signed endpoint — apiKey + apiSecret required. +$wallet = $client->account->getWalletBalance('UNIFIED'); +print_r($wallet['result']['list']); + +// 3. Place a LIMIT order well below market so it sits on the book and does +// NOT fill (safe to run repeatedly). Adjust `price` if BTC ever trades +// at $10k again — otherwise this stays a resting order you can cancel. +$order = $client->trade->createOrder( + 'linear', 'BTCUSDT', 'Buy', 'Limit', '0.01', + ['price' => '10000', 'timeInForce' => 'GTC'] +); +$orderId = $order['result']['orderId']; +echo "orderId: {$orderId}" . PHP_EOL; + +// 4. Cancel it before moving on. +$client->trade->cancelOrder('linear', 'BTCUSDT', ['orderId' => $orderId]); +``` + +> ⚠️ **Before switching `testnet = false`**: verify the `price` / `qty` in +> `createOrder` won't cross the top of the book — a Limit Buy at $10k on +> mainnet becomes a market fill instantly (if BTC ever drops that low), +> and a Limit Sell at $1M does the reverse. + +See `examples/quickstart.php` for a runnable script. + +## Configuration + +All options live on `Bybit\Configuration`. Instantiate with named arguments +and pass to `Client`: + +```php +use Bybit\Configuration; + +$config = new Configuration( + apiKey: getenv('BYBIT_TESTNET_KEY') ?: null, + apiSecret: getenv('BYBIT_TESTNET_SECRET') ?: null, + recvWindow: '5000', // milliseconds — Bybit rejects requests whose signed + // timestamp is older than this window. Bump to + // 10000+ if your clock drifts or the network is noisy. + testnet: true, // false selects mainnet (default) + timeout: 10, // Guzzle timeout, seconds +); +``` + +`testnet`, `baseUrl`, `timeout`, and `httpClient` are **readonly** — set them +via the constructor and Configuration is a snapshot the Session captures at +Client construction. `apiKey`, `apiSecret`, and `recvWindow` remain writable +on the object so callers can rotate credentials mid-run without rebuilding +the Client. + +Bring your own Guzzle client to inject retries / logging / middleware: + +```php +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; + +$stack = HandlerStack::create(); +$stack->push(Middleware::retry( + fn($retries, $req, $resp, $err) => $retries < 3 && ($err !== null || ($resp && $resp->getStatusCode() >= 500)), + fn($retries) => (int) pow(2, $retries) * 1000 // 1s, 2s, 4s (ms) +)); + +$config = new Configuration( + apiKey: getenv('BYBIT_TESTNET_KEY') ?: null, + apiSecret: getenv('BYBIT_TESTNET_SECRET') ?: null, + httpClient: new GuzzleClient([ + 'base_uri' => 'https://api-testnet.bybit.com', + 'handler' => $stack, + 'timeout' => 5, + ]), +); +``` + +> ⚠️ When you inject `httpClient`, the SDK does **not** re-apply +> `Configuration::$baseUrl` / `$testnet` / `$timeout` onto your client — the +> injected Guzzle instance is used as-is. Set `base_uri` and `timeout` on the +> Guzzle client yourself (as shown above), and pick `api.bybit.com` vs +> `api-testnet.bybit.com` explicitly. + +Base URLs (exported constants): + +- `Bybit\Bybit::BASE_URL_MAINNET` — `https://api.bybit.com` +- `Bybit\Bybit::BASE_URL_TESTNET` — `https://api-testnet.bybit.com` + +## Services + +Each API group is a readonly property on `Bybit\Client`: + +- `$client->market` — public market data (kline, tickers, orderbook, instruments-info, ...) +- `$client->trade` — orders (create / amend / cancel / batch / history) +- `$client->position` — positions, leverage, TP/SL, move-position +- `$client->account` — wallet, margin, collateral, fee-rate, transaction log +- `$client->asset` — coin balance, funding history +- `$client->user` — sub-accounts, API-key management +- `$client->affiliate` — sub-affiliate lists +- `$client->broker` — broker earnings, distributions +- `$client->cryptoLoan` — flexible / fixed crypto loans +- `$client->rfq` — request-for-quote (block trades) +- `$client->spotMargin` — UTA spot margin +- `$client->earn` — earn, liquidity mining, RWA, PWM, hold-to-earn +- `$client->p2p` — P2P advertise / order / chat +- `$client->bot` — DCA / grid / futures-combo / futures-grid / martingale + +## Error Handling + +Every failure is a subclass of `Bybit\Exception\BybitException`: + +```php +use Bybit\Exception\{ + AuthException, RateLimitException, TimeoutException, NetworkException, + ServerException, ClientException, ParseException, ApiException, +}; + +try { + $client->trade->createOrder('linear', 'BTCUSDT', 'Buy', 'Limit', '0.01', ['price' => '10000']); +} catch (AuthException $e) { // retCode 10002/10003/10004/10005/10007/10009/10010/10029, or HTTP 401/403 + // bad key / bad sign / permission +} catch (RateLimitException $e) { // retCode 10006/10018, or HTTP 429 + sleep(1); +} catch (TimeoutException $e) { // Guzzle ConnectException / RequestException(timeout) +} catch (NetworkException $e) { // Guzzle ConnectException (connection refused / DNS / SSL) +} catch (ServerException $e) { // HTTP 5xx w/ non-JSON body +} catch (ClientException $e) { // non-auth 4xx w/ non-JSON body (WAF, CDN, etc.) +} catch (ParseException $e) { // unrecognized body shape; $e->getBody() holds raw payload +} catch (ApiException $e) { // any other retCode != 0 — catch-all API error +} +``` + +Full hierarchy: + +- `Bybit\Exception\BybitException` (`\RuntimeException`) + - `Bybit\Exception\ConfigurationException` — missing apiKey / conflicting options + - `Bybit\Exception\TransportException` + - `Bybit\Exception\TimeoutException` + - `Bybit\Exception\NetworkException` + - `Bybit\Exception\ServerException` (5xx w/o body) + - `Bybit\Exception\ClientException` (non-auth 4xx w/o body) + - `Bybit\Exception\ParseException` — body did not parse or shape mismatch (has `getBody()`, `getHttpStatus()`) + - `Bybit\Exception\ApiException` — Bybit V5 body with retCode != 0 + - `Bybit\Exception\AuthException` + - `Bybit\Exception\RateLimitException` + +Every `ApiException` exposes `getRetCode()`, `getRetMsg()`, `getResult()`, `getTime()`, `getHttpStatus()`. See the [Bybit V5 error-code list](https://bybit-exchange.github.io/docs/v5/error) for meanings. + +> The `sleep(1)` on rate-limit shown above is fine for exploration, **not** +> for production — Bybit will escalate throttling on tight retry loops. For +> real workloads, wire a Guzzle `Middleware::retry` with exponential backoff +> via the "bring your own Guzzle client" hook in [Configuration](#configuration) +> above. + +## Return Value + +Every service method returns the raw parsed JSON as an associative `array`: + +```php +$response = $client->market->getKline('spot', 'BTCUSDT', '1'); +$response['retCode']; // => 0 +$response['retMsg']; // => 'OK' +$response['result']; // => ['category' => 'spot', 'symbol' => 'BTCUSDT', 'list' => [...]] +$response['time']; // => 1234567890000 +``` + +> P2P endpoints return a legacy snake_case envelope (`ret_code` / `ret_msg` / +> `time_now` / `ext_info`) instead of the V5 standard. The SDK normalizes +> these into `retCode` / `retMsg` / `time` / `retExtInfo` automatically — +> `$response['retCode']` works uniformly across every service. The original +> snake_case keys are preserved on the array for callers that want the wire +> shape verbatim. + +## Method Reference + +Method names follow the Bybit V5 endpoint slug in camelCase, grouped by +domain. Required path/body params are explicit typed arguments; every optional +parameter goes into the final `array $options = []`. A few illustrative +mappings: + +| Bybit V5 path | HTTP | SDK method | +| -------------------------------------- | ----- | ------------------------------------------------------------------- | +| `/v5/market/kline` | GET | `$client->market->getKline($category, $symbol, $interval, $options)` | +| `/v5/market/tickers` | GET | `$client->market->getTickers($category, $options)` | +| `/v5/order/create` | POST | `$client->trade->createOrder($category, $symbol, $side, $orderType, $qty, $options)` | +| `/v5/order/cancel` | POST | `$client->trade->cancelOrder($category, $symbol, $options)` | +| `/v5/position/list` | GET | `$client->position->getInfo($category, $options)` | +| `/v5/account/wallet-balance` | GET | `$client->account->getWalletBalance($accountType, $options)` | + +For the full list of 240+ endpoints, browse the service class source under +`src/RestApi/` — every method carries a PHPDoc block naming its HTTP verb, +path, params, and a `@see` link to the Bybit docs page. + +## Pagination + +Bybit V5 uses **opaque cursor pagination** — the response's +`result.nextPageCursor` (empty string when the page is the last one) feeds +back in as the `cursor` option on the next call: + +```php +$cursor = null; +do { + $opts = ['limit' => 50]; + if ($cursor !== null && $cursor !== '') { + $opts['cursor'] = $cursor; + } + $resp = $client->trade->getOrderHistory('linear', $opts); + foreach ($resp['result']['list'] as $order) { + // process($order); + } + $cursor = $resp['result']['nextPageCursor'] ?? null; +} while ($cursor !== null && $cursor !== ''); +``` + +The same pattern works for `getClosedPnl`, `getTransactionLog`, +`getExecutionList`, and every other paginated endpoint. + +## Signing Invariant (for the curious) + +`Session` guarantees that the query string signed matches the query string +sent on the wire byte-for-byte. It does this by: + +1. Sorting `params` keys and encoding via `rawurlencode` into one canonical + string (`sortAndEncode()`). +2. Feeding that exact string into `Authentication::signV5()`. +3. Building the request URL manually as `$path . '?' . $queryStr` rather + than letting Guzzle re-serialize `params` — Guzzle's default query + handler can reorder keys and array-bracket lists differently, which + would break the HMAC. + +Booleans are wire-serialized as literal `'true'` / `'false'`, floats are +formatted non-scientifically (so `1.0e-9` becomes `0.000000001`) — both +avoid Bybit's parameter validators rejecting the PHP default casts. + +## Development + +``` +composer install +composer test # PHPUnit +composer stan # PHPStan level 6 +composer cs-check # PHP-CS-Fixer dry-run +composer cs-fix # PHP-CS-Fixer auto-fix +``` + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..21361a7 --- /dev/null +++ b/composer.json @@ -0,0 +1,56 @@ +{ + "name": "bybit-exchange/bybit-connector-php", + "description": "Official Bybit V5 REST API connector for PHP — typed method signatures, HMAC-SHA256 signing, typed exception hierarchy, Guzzle-based transport.", + "type": "library", + "license": "MIT", + "keywords": [ + "bybit", + "crypto", + "exchange", + "trading", + "api", + "v5", + "rest" + ], + "homepage": "https://github.com/bybit-exchange/bybit.php.api", + "authors": [ + { + "name": "Bybit", + "homepage": "https://www.bybit.com" + } + ], + "require": { + "php": ">=8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.5" + }, + "require-dev": { + "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^1.10", + "friendsofphp/php-cs-fixer": "^3.50", + "mockery/mockery": "^1.6" + }, + "autoload": { + "psr-4": { + "Bybit\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Bybit\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit", + "test-coverage": "phpunit --coverage-text", + "stan": "phpstan analyse", + "cs-fix": "php-cs-fixer fix", + "cs-check": "php-cs-fixer fix --dry-run --diff" + }, + "config": { + "sort-packages": true, + "allow-plugins": {} + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/examples/quickstart.php b/examples/quickstart.php new file mode 100644 index 0000000..7eb2da1 --- /dev/null +++ b/examples/quickstart.php @@ -0,0 +1,57 @@ +market->getServerTime()); + +echo '--- kline (public) ---' . PHP_EOL; +print_r($client->market->getKline('spot', 'BTCUSDT', '1', ['limit' => 5])); + +echo '--- fee rate (signed) ---' . PHP_EOL; +try { + $fee = $client->account->getFeeRate('linear', ['symbol' => 'BTCUSDT']); + print_r($fee['result']['list']); +} catch (AuthException $e) { + fwrite(STDERR, sprintf("auth failed: [%s] %s\n", (string) $e->getRetCode(), (string) $e->getRetMsg())); +} catch (RateLimitException $e) { + fwrite(STDERR, sprintf("rate-limited: %s\n", (string) $e->getRetMsg())); +} catch (TimeoutException $e) { + fwrite(STDERR, sprintf("timeout: %s\n", $e->getMessage())); +} catch (NetworkException $e) { + fwrite(STDERR, sprintf("network: %s\n", $e->getMessage())); +} catch (ServerException $e) { + fwrite(STDERR, sprintf("server: %s\n", $e->getMessage())); +} catch (ClientException $e) { + fwrite(STDERR, sprintf("client: %s\n", $e->getMessage())); +} catch (ParseException $e) { + fwrite(STDERR, sprintf("parse: %s\n", $e->getMessage())); +} catch (ApiException $e) { + fwrite(STDERR, sprintf("api error [%s]: %s\n", (string) $e->getRetCode(), (string) $e->getRetMsg())); +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..09e0a09 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,19 @@ +parameters: + level: 6 + paths: + - src + excludePaths: + - vendor + # Reusable type aliases. The Bybit V5 response envelope (retCode / retMsg / + # result / retExtInfo / time) is the return shape of every service method — + # promote it to a name so docblocks stay in sync with the actual contract. + # `Bybit\Session::normalizeLegacyEnvelope` guarantees this shape even for + # snake_case P2P responses. `result` and `retExtInfo` are `mixed` because + # Bybit's spec varies per-endpoint (array / object / empty stdClass). + typeAliases: + BybitEnvelope: 'array{retCode:int, retMsg:string, result:mixed, retExtInfo:mixed, time:int}' + # A future pass could annotate every service method's `array $options` and + # `array` return with `array`; until then, silence the generic + # complaint uniformly. Runtime `array` typehints still enforce array-ness. + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..397a1ca --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + tests + + + + + src + + + + + + + + diff --git a/src/Authentication.php b/src/Authentication.php new file mode 100644 index 0000000..d71694f --- /dev/null +++ b/src/Authentication.php @@ -0,0 +1,27 @@ +config = $config ?? new Configuration(); + $this->session = new Session($this->config); + + // gen-sdk-php:client-inits:start + $this->account = new AccountService($this->session); + $this->affiliate = new AffiliateService($this->session); + $this->asset = new AssetService($this->session); + $this->bot = new BotService($this->session); + $this->broker = new BrokerService($this->session); + $this->cryptoLoan = new CryptoLoanService($this->session); + $this->earn = new EarnService($this->session); + $this->market = new MarketService($this->session); + $this->p2p = new P2pService($this->session); + $this->position = new PositionService($this->session); + $this->rfq = new RfqService($this->session); + $this->spotMargin = new SpotMarginService($this->session); + $this->trade = new TradeService($this->session); + $this->user = new UserService($this->session); + // gen-sdk-php:client-inits:end + } + + /** + * Escape hatch for endpoints not yet wrapped by a service class. Signs and + * dispatches a raw request via the same Session pipeline the services use. + * + * @param array|null $params + * @return BybitEnvelope + */ + public function raw(string $method, string $path, ?array $params = null, bool $signed = true): array + { + return $signed + ? $this->session->signRequest($method, $path, $params) + : $this->session->publicRequest($method, $path, $params); + } + + public function __toString(): string + { + return sprintf( + '', + $this->config->testnet ? 'true' : 'false', + $this->config->resolvedBaseUrl() + ); + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return [ + 'testnet' => $this->config->testnet, + 'baseUrl' => $this->config->resolvedBaseUrl(), + ]; + } +} diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 0000000..f297b85 --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,90 @@ +baseUrl !== null) { + return $this->baseUrl; + } + return $this->testnet ? Bybit::BASE_URL_TESTNET : Bybit::BASE_URL_MAINNET; + } + + /** + * Serialization-safe snapshot used by __debugInfo / jsonSerialize / __toString. + * + * @return array + */ + private function toSafeArray(): array + { + return [ + 'apiKey' => $this->redact($this->apiKey), + 'apiSecret' => $this->redact($this->apiSecret), + 'testnet' => $this->testnet, + 'baseUrl' => $this->resolvedBaseUrl(), + 'recvWindow' => $this->recvWindow, + 'timeout' => $this->timeout, + ]; + } + + /** + * @return array + */ + public function __debugInfo(): array + { + return $this->toSafeArray(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toSafeArray(); + } + + public function __toString(): string + { + return sprintf( + '', + $this->redact($this->apiKey), + $this->redact($this->apiSecret), + $this->testnet ? 'true' : 'false', + $this->resolvedBaseUrl() + ); + } + + private function redact(?string $v): string + { + return ($v === null || $v === '') ? '(unset)' : '[REDACTED]'; + } +} diff --git a/src/Exception/ApiException.php b/src/Exception/ApiException.php new file mode 100644 index 0000000..aad064b --- /dev/null +++ b/src/Exception/ApiException.php @@ -0,0 +1,84 @@ + $response Bybit V5 ApiResponse envelope. + */ + public function __construct(array $response, ?int $httpStatus = null) + { + $this->retCode = isset($response['retCode']) ? (int) $response['retCode'] : null; + $this->retMsg = isset($response['retMsg']) ? (string) $response['retMsg'] : null; + $this->result = $response['result'] ?? null; + $this->time = isset($response['time']) ? (int) $response['time'] : null; + $this->httpStatus = $httpStatus; + parent::__construct(sprintf('[%s] %s', (string) $this->retCode, (string) $this->retMsg)); + } + + public function getRetCode(): ?int + { + return $this->retCode; + } + + public function getRetMsg(): ?string + { + return $this->retMsg; + } + + /** @return mixed */ + public function getResult() + { + return $this->result; + } + + public function getTime(): ?int + { + return $this->time; + } + + public function getHttpStatus(): ?int + { + return $this->httpStatus; + } + + /** + * Route a V5 response with retCode != 0 to the most specific subclass. + * + * @param array $response + */ + public static function fromResponse(array $response, ?int $httpStatus = null): ApiException + { + $code = isset($response['retCode']) ? (int) $response['retCode'] : 0; + // Auth/permission family per https://bybit-exchange.github.io/docs/v5/error : + // 10002 sync-window, 10003 invalid-key, 10004 sign-mismatch, + // 10005 permission-denied, 10007 user-auth-failed, 10008 expired-token, + // 10009 ip-blocked, 10010 unmatched-ip, 10017 gateway-401/403, + // 10022 blocked-jurisdiction, 10024 compliance-check-failed, + // 10028 forbidden, 10029 access-denied. + // -2015 legacy account-service auth failure (observed on + // /v5/account/query-instruments — Bybit's account-service pathway + // still surfaces code alongside 10005). + if (in_array($code, [-2015, 10002, 10003, 10004, 10005, 10007, 10008, 10009, 10010, 10017, 10022, 10024, 10028, 10029], true)) { + return new AuthException($response, $httpStatus); + } + if (in_array($code, [10006, 10018], true)) { + return new RateLimitException($response, $httpStatus); + } + return new ApiException($response, $httpStatus); + } +} diff --git a/src/Exception/AuthException.php b/src/Exception/AuthException.php new file mode 100644 index 0000000..caa8ae2 --- /dev/null +++ b/src/Exception/AuthException.php @@ -0,0 +1,9 @@ +rawBody = $body; + $this->httpStatus = $httpStatus; + } + + public function getBody(): ?string + { + return $this->rawBody; + } + + public function getHttpStatus(): ?int + { + return $this->httpStatus; + } +} diff --git a/src/Exception/RateLimitException.php b/src/Exception/RateLimitException.php new file mode 100644 index 0000000..9b6bc85 --- /dev/null +++ b/src/Exception/RateLimitException.php @@ -0,0 +1,9 @@ +session->signRequest( + 'POST', + '/v5/account/set-collateral-switch-batch', + array_merge($options, ['request' => $request]) + ); + } + + /** + * Get Account Info. + * + * GET /v5/account/info + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/account-info + */ + public function getInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/info', $options); + } + + /** + * Get Account Instruments. + * + * GET /v5/account/instruments-info + * + * @param string $category Product type + * @param array{symbol?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getInstruments(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/account/instruments-info', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get Borrow History. + * + * GET /v5/account/borrow-history + * + * @param array{currency?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/borrow-history + */ + public function getBorrowHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/borrow-history', $options); + } + + /** + * Get Collateral Info. + * + * GET /v5/account/collateral-info + * + * @param array{currency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/collateral-info + */ + public function getCollateralInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/collateral-info', $options); + } + + /** + * Get DCP Info. + * + * GET /v5/account/query-dcp-info + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/dcp-info + */ + public function getDcpInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/query-dcp-info', $options); + } + + /** + * Get Fee Rate. + * + * GET /v5/account/fee-rate + * + * @param string $category Product type + * @param array{symbol?:string, baseCoin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/fee-rate + */ + public function getFeeRate(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/account/fee-rate', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get MMP State. + * + * GET /v5/account/mmp-state + * + * @param string $baseCoin Base coin + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/get-mmp-state + */ + public function getMmpState(string $baseCoin, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/account/mmp-state', + array_merge($options, ['baseCoin' => $baseCoin]) + ); + } + + /** + * Get SMP Group. + * + * GET /v5/account/smp-group + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/smp-group + */ + public function getSmpGroup(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/smp-group', $options); + } + + /** + * Get Transaction Log. + * + * GET /v5/account/transaction-log + * + * @param array{accountType?:string, category?:string, currency?:string, baseCoin?:string, type?:string, transSubType?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/transaction-log + */ + public function getTransactionLog(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/transaction-log', $options); + } + + /** + * Get Wallet Balance for the given account type. Returns coin balances, + * total equity, and available margin figures. + * + * GET /v5/account/wallet-balance + * + * NOTE: This endpoint is not currently exposed in the local OpenAPI spec — + * it lives in the docs (https://bybit-exchange.github.io/docs/v5/account/wallet-balance) + * and is hand-registered via the workflow's EXTRA_ENDPOINTS table so it + * survives regeneration. + * + * @param string $accountType UNIFIED | CONTRACT | SPOT + * @param array{coin?:string} $options coin — comma-separated coin filter (e.g. 'BTC,ETH,USDT'). + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/wallet-balance + */ + public function getWalletBalance(string $accountType, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/account/wallet-balance', + array_merge($options, ['accountType' => $accountType]) + ); + } + + /** + * Get Transferable Amount. + * + * GET /v5/account/withdrawal + * + * @param string $coinName Coin name + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTransferableAmount(string $coinName, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/account/withdrawal', + array_merge($options, ['coinName' => $coinName]) + ); + } + + /** + * Get User Settings. + * + * GET /v5/account/user-setting-config + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getUserSettings(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/account/user-setting-config', $options); + } + + /** + * Manual Borrow. + * + * POST /v5/account/borrow + * + * @param string $coin Coin name + * @param string $amount Amount to borrow + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function manualBorrow(string $coin, string $amount, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/borrow', + array_merge($options, ['coin' => $coin, 'amount' => $amount]) + ); + } + + /** + * Manual Repay. + * + * POST /v5/account/repay + * + * @param array{coin?:string, amount?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function manualRepay(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/account/repay', $options); + } + + /** + * No-Convert Repay. + * + * POST /v5/account/no-convert-repay + * + * @param string $coin Coin name + * @param array{amount?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/no-convert-repay + */ + public function noConvertRepay(string $coin, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/no-convert-repay', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * One-Click Repay: repay outstanding liabilities in one click for UTA accounts. + * + * POST /v5/account/quick-repayment + * + * @param array{coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/repay-liability + */ + public function oneClickRepay(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/account/quick-repayment', $options); + } + + /** + * Reset MMP: reset the market maker protection state for the specified base coin. + * + * POST /v5/account/mmp-reset + * + * @param string $baseCoin Base coin + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/reset-mmp + */ + public function resetMmp(string $baseCoin, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/mmp-reset', + array_merge($options, ['baseCoin' => $baseCoin]) + ); + } + + /** + * Set Collateral Coin: enable or disable a coin as collateral for USDC contract trading. + * + * POST /v5/account/set-collateral-switch + * + * @param string $coin Coin name + * @param string $collateralSwitch ON or OFF collateral switch + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/set-collateral + */ + public function setCollateralCoin(string $coin, string $collateralSwitch, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/set-collateral-switch', + array_merge($options, ['coin' => $coin, 'collateralSwitch' => $collateralSwitch]) + ); + } + + /** + * Set Margin Mode: switch margin mode between ISOLATED_MARGIN, REGULAR_MARGIN and PORTFOLIO_MARGIN. + * + * POST /v5/account/set-margin-mode + * + * @param string $setMarginMode Target margin mode: ISOLATED_MARGIN, REGULAR_MARGIN or PORTFOLIO_MARGIN + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/set-margin-mode + */ + public function setMarginMode(string $setMarginMode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/set-margin-mode', + array_merge($options, ['setMarginMode' => $setMarginMode]) + ); + } + + /** + * Set MMP: configure the market maker protection parameters for options trading. + * + * POST /v5/account/mmp-modify + * + * @param string $baseCoin Base coin + * @param string $window Time window in milliseconds + * @param string $frozenPeriod MMP frozen period in milliseconds + * @param string $qtyLimit Quantity limit + * @param string $deltaLimit Delta limit + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/set-mmp + */ + public function setMmp(string $baseCoin, string $window, string $frozenPeriod, string $qtyLimit, string $deltaLimit, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/mmp-modify', + array_merge($options, ['baseCoin' => $baseCoin, 'window' => $window, 'frozenPeriod' => $frozenPeriod, 'qtyLimit' => $qtyLimit, 'deltaLimit' => $deltaLimit]) + ); + } + + /** + * Set Price Limit: enable or disable the modify-order price limit action for a category. + * + * POST /v5/account/set-limit-px-action + * + * @param string $category Product type + * @param bool $modifyEnable Whether to enable modify price limit action + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function setPriceLimit(string $category, bool $modifyEnable, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/set-limit-px-action', + array_merge($options, ['category' => $category, 'modifyEnable' => $modifyEnable]) + ); + } + + /** + * Set Spot Hedging: enable or disable spot hedging feature for the portfolio margin account. + * + * POST /v5/account/set-hedging-mode + * + * @param string $setHedgingMode ON or OFF spot hedging mode + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/set-spot-hedge + */ + public function setSpotHedging(string $setHedgingMode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/account/set-hedging-mode', + array_merge($options, ['setHedgingMode' => $setHedgingMode]) + ); + } + + /** + * Upgrade to UTA Pro: upgrade the current account to Unified Trading Account Pro. + * + * POST /v5/account/upgrade-to-uta + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account + */ + public function upgradeToUtaPro(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/account/upgrade-to-uta', $options); + } +} diff --git a/src/RestApi/AffiliateService.php b/src/RestApi/AffiliateService.php new file mode 100644 index 0000000..27d6d4e --- /dev/null +++ b/src/RestApi/AffiliateService.php @@ -0,0 +1,36 @@ +session->signRequest('GET', '/v5/affiliate/affiliate-sub-list', $options); + } + + /** + * Get affiliate user list. + * + * GET /v5/affiliate/aff-user-list + * + * @param array{cursor?:string, size?:int, needDeposit?:bool, need30?:bool, need365?:bool, startDate?:string, endDate?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getUserList(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/affiliate/aff-user-list', $options); + } +} diff --git a/src/RestApi/AssetService.php b/src/RestApi/AssetService.php new file mode 100644 index 0000000..ff6546a --- /dev/null +++ b/src/RestApi/AssetService.php @@ -0,0 +1,41 @@ +session->signRequest( + 'GET', + '/v5/asset/transfer/query-account-coins-balance', + array_merge($options, ['accountType' => $accountType]) + ); + } + + /** + * Get Funding History - Query the funding fee history record of a specified account. + * + * GET /v5/asset/fundinghistory + * + * @param array{createTimeFrom?:string, createTimeTo?:string, limit?:string, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryFundingDetail(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/asset/fundinghistory', $options); + } +} diff --git a/src/RestApi/BaseService.php b/src/RestApi/BaseService.php new file mode 100644 index 0000000..d9b32f8 --- /dev/null +++ b/src/RestApi/BaseService.php @@ -0,0 +1,17 @@ +session = $session; + } +} diff --git a/src/RestApi/BotService.php b/src/RestApi/BotService.php new file mode 100644 index 0000000..e58765b --- /dev/null +++ b/src/RestApi/BotService.php @@ -0,0 +1,388 @@ + $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function closeDcaBot(int $botId, int $closeMode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/dca/close-bot', + array_merge($options, ['botId' => $botId, 'closeMode' => $closeMode]) + ); + } + + /** + * Create a new DCA (Dollar-Cost Averaging) bot with custom parameters. + * + * POST /v5/dca/create-bot + * + * @param array $parameters DCA bot configuration parameters. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createDcaBot(array $parameters, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/dca/create-bot', + array_merge($options, ['parameters' => $parameters]) + ); + } + + /** + * Close a running futures combo bot by bot ID. + * + * POST /v5/fcombobot/close + * + * @param int $botId Bot ID of the futures combo bot to close. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function closeComboBot(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fcombobot/close', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Create a new futures combo bot with multi-symbol portfolio and rebalancing. + * + * POST /v5/fcombobot/create + * + * @param string $leverage Leverage value for the combo bot. + * @param string $initMargin Initial margin allocated to the bot. + * @param int $adjustPositionMode Position adjustment mode for rebalancing. + * @param list> $symbolSettings Symbol-level settings for the multi-symbol portfolio. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createComboBot(string $leverage, string $initMargin, int $adjustPositionMode, array $symbolSettings, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fcombobot/create', + array_merge($options, ['leverage' => $leverage, 'initMargin' => $initMargin, 'adjustPositionMode' => $adjustPositionMode, 'symbolSettings' => $symbolSettings]) + ); + } + + /** + * Get full details of a futures combo bot including PnL, positions, and status. + * + * POST /v5/fcombobot/detail + * + * @param int $botId Bot ID of the futures combo bot to query. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getComboDetail(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fcombobot/detail', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Validate combo bot input parameters and return allowable ranges. + * + * POST /v5/fcombobot/getlimit + * + * @param string $leverage Leverage value for the combo bot. + * @param string $initMargin Initial margin allocated to the bot. + * @param int $adjustPositionMode Position adjustment mode for rebalancing. + * @param list> $symbolSettings Symbol-level settings for the multi-symbol portfolio. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getComboLimit(string $leverage, string $initMargin, int $adjustPositionMode, array $symbolSettings, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fcombobot/getlimit', + array_merge($options, ['leverage' => $leverage, 'initMargin' => $initMargin, 'adjustPositionMode' => $adjustPositionMode, 'symbolSettings' => $symbolSettings]) + ); + } + + /** + * Close a running futures grid bot by bot ID. + * + * POST /v5/fgridbot/close + * + * @param int $botId Bot ID. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function closeFuturesGridBot(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fgridbot/close', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Create a new futures grid trading bot with specified parameters. + * + * POST /v5/fgridbot/create + * + * @param string $symbol Trading symbol. + * @param int $gridMode Grid mode. + * @param string $minPrice Minimum price of grid range. + * @param string $maxPrice Maximum price of grid range. + * @param int $cellNumber Number of grid cells. + * @param string $leverage Leverage. + * @param int $gridType Grid type. + * @param string $totalInvestment Total investment amount. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createFuturesGridBot(string $symbol, int $gridMode, string $minPrice, string $maxPrice, int $cellNumber, string $leverage, int $gridType, string $totalInvestment, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fgridbot/create', + array_merge($options, ['symbol' => $symbol, 'gridMode' => $gridMode, 'minPrice' => $minPrice, 'maxPrice' => $maxPrice, 'cellNumber' => $cellNumber, 'leverage' => $leverage, 'gridType' => $gridType, 'totalInvestment' => $totalInvestment]) + ); + } + + /** + * Get full details of a futures grid bot including PnL, positions, and status. + * + * POST /v5/fgridbot/detail + * + * @param int $botId Bot ID. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFuturesGridDetail(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fgridbot/detail', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Validate futures grid bot input parameters and return allowable ranges. + * + * POST /v5/fgridbot/validate + * + * @param string $symbol Trading symbol. + * @param int $cellNumber Number of grid cells. + * @param string $minPrice Minimum price of grid range. + * @param string $maxPrice Maximum price of grid range. + * @param string $leverage Leverage. + * @param int $gridType Grid type. + * @param int $gridMode Grid mode. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function validateFuturesGridInput(string $symbol, int $cellNumber, string $minPrice, string $maxPrice, string $leverage, int $gridType, int $gridMode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fgridbot/validate', + array_merge($options, ['symbol' => $symbol, 'cellNumber' => $cellNumber, 'minPrice' => $minPrice, 'maxPrice' => $maxPrice, 'leverage' => $leverage, 'gridType' => $gridType, 'gridMode' => $gridMode]) + ); + } + + /** + * Close a running futures Martingale bot by bot ID. + * + * POST /v5/fmartingalebot/close + * + * @param int $botId Bot ID to close. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function closeFuturesMartingaleBot(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fmartingalebot/close', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Create a new futures Martingale bot with DCA averaging strategy. + * + * POST /v5/fmartingalebot/create + * + * @param string $symbol Symbol. + * @param string $martingaleMode Martingale mode. + * @param string $leverage Leverage. + * @param string $priceFloatPercent Price float percent. + * @param string $addPositionPercent Add position percent. + * @param int $addPositionNum Number of add position rounds. + * @param string $initMargin Initial margin. + * @param string $roundTpPercent Round take-profit percent. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createFuturesMartingaleBot(string $symbol, string $martingaleMode, string $leverage, string $priceFloatPercent, string $addPositionPercent, int $addPositionNum, string $initMargin, string $roundTpPercent, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fmartingalebot/create', + array_merge($options, ['symbol' => $symbol, 'martingaleMode' => $martingaleMode, 'leverage' => $leverage, 'priceFloatPercent' => $priceFloatPercent, 'addPositionPercent' => $addPositionPercent, 'addPositionNum' => $addPositionNum, 'initMargin' => $initMargin, 'roundTpPercent' => $roundTpPercent]) + ); + } + + /** + * Get full details of a futures Martingale bot including PnL, positions, and round progress. + * + * POST /v5/fmartingalebot/detail + * + * @param int $botId Bot ID. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFuturesMartingaleDetail(int $botId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fmartingalebot/detail', + array_merge($options, ['botId' => $botId]) + ); + } + + /** + * Validate Martingale bot input parameters and return allowable ranges. + * + * POST /v5/fmartingalebot/getlimit + * + * @param string $symbol Symbol. + * @param string $martingaleMode Martingale mode. + * @param string $leverage Leverage. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFuturesMartingaleLimit(string $symbol, string $martingaleMode, string $leverage, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/fmartingalebot/getlimit', + array_merge($options, ['symbol' => $symbol, 'martingaleMode' => $martingaleMode, 'leverage' => $leverage]) + ); + } + + /** + * Close a running spot grid bot with a specified settlement mode. + * + * POST /v5/grid/close-grid + * + * @param int $gridId Grid bot identifier. + * @param int $closeMode Settlement mode used to close the grid bot. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function closeGridBot(int $gridId, int $closeMode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/grid/close-grid', + array_merge($options, ['gridId' => $gridId, 'closeMode' => $closeMode]) + ); + } + + /** + * Create a new spot grid trading bot. + * + * POST /v5/grid/create-grid + * + * @param string $symbol Trading pair symbol. + * @param string $maxPrice Upper bound price of the grid range. + * @param string $minPrice Lower bound price of the grid range. + * @param string $totalInvestment Total investment amount. + * @param int $cellNumber Number of grid cells. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createGridBot(string $symbol, string $maxPrice, string $minPrice, string $totalInvestment, int $cellNumber, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/grid/create-grid', + array_merge($options, ['symbol' => $symbol, 'maxPrice' => $maxPrice, 'minPrice' => $minPrice, 'totalInvestment' => $totalInvestment, 'cellNumber' => $cellNumber]) + ); + } + + /** + * Query full details of a specific grid bot by grid_id. + * + * POST /v5/grid/query-grid-detail + * + * @param int $gridId Grid bot identifier. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryGridDetail(int $gridId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/grid/query-grid-detail', + array_merge($options, ['gridId' => $gridId]) + ); + } + + /** + * Validate spot grid bot parameters before creation. + * + * POST /v5/grid/validate-input + * + * @param string $symbol Trading pair symbol. + * @param int $cellNumber Number of grid cells. + * @param string $minPrice Lower bound price of the grid range. + * @param string $maxPrice Upper bound price of the grid range. + * @param string $totalInvestment Total investment amount. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function validateGridInput(string $symbol, int $cellNumber, string $minPrice, string $maxPrice, string $totalInvestment, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/grid/validate-input', + array_merge($options, ['symbol' => $symbol, 'cellNumber' => $cellNumber, 'minPrice' => $minPrice, 'maxPrice' => $maxPrice, 'totalInvestment' => $totalInvestment]) + ); + } +} diff --git a/src/RestApi/BrokerService.php b/src/RestApi/BrokerService.php new file mode 100644 index 0000000..3a4a471 --- /dev/null +++ b/src/RestApi/BrokerService.php @@ -0,0 +1,141 @@ +session->signRequest( + 'POST', + '/v5/broker/award/distribute-award', + array_merge($options, ['accountId' => $accountId, 'awardId' => $awardId, 'specCode' => $specCode, 'amount' => $amount, 'brokerId' => $brokerId]) + ); + } + + /** + * Get voucher details. + * + * POST /v5/broker/award/info + * + * @param string $id Voucher spec ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAwardInfo(string $id, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/broker/award/info', + array_merge($options, ['id' => $id]) + ); + } + + /** + * Query voucher distribution record. + * + * POST /v5/broker/award/distribution-record + * + * @param string $accountId Sub account UID + * @param string $awardId Voucher spec ID + * @param string $specCode Voucher spec code + * @param array{withUsedAmount?:bool} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getDistributionRecord(string $accountId, string $awardId, string $specCode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/broker/award/distribution-record', + array_merge($options, ['accountId' => $accountId, 'awardId' => $awardId, 'specCode' => $specCode]) + ); + } + + /** + * Get Broker Account Info. + * + * GET /v5/broker/account-info + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryAccountInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/broker/account-info', $options); + } + + /** + * Query Broker All UID Rate Limits. + * + * GET /v5/broker/apilimit/query-all + * + * @param array{uids?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryAllUidDetails(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/broker/apilimit/query-all', $options); + } + + /** + * Query Broker Rate Limit Cap. + * + * GET /v5/broker/apilimit/query-cap + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryCap(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/broker/apilimit/query-cap', $options); + } + + /** + * Get Broker Earnings Info. + * + * GET /v5/broker/earnings-info + * + * @param array{bizType?:string, begin?:string, end?:string, uid?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryEarning(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/broker/earnings-info', $options); + } + + /** + * Set Broker API Rate Limit. + * + * POST /v5/broker/apilimit/set + * + * @param array{list?:array} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function setApiLimit(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/broker/apilimit/set', $options); + } +} diff --git a/src/RestApi/CryptoLoanService.php b/src/RestApi/CryptoLoanService.php new file mode 100644 index 0000000..2867ccf --- /dev/null +++ b/src/RestApi/CryptoLoanService.php @@ -0,0 +1,449 @@ +session->signRequest( + 'POST', + '/v5/crypto-loan-common/adjust-ltv', + array_merge($options, ['currency' => $currency, 'amount' => $amount, 'direction' => $direction]) + ); + } + + /** + * Get Collateral Adjustment History. + * + * GET /v5/crypto-loan-common/adjustment-history + * + * @param array{adjustId?:int, collateralCurrency?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAdjustmentHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-common/adjustment-history', $options); + } + + /** + * Get Collateral Currency Data. + * + * GET /v5/crypto-loan-common/collateral-data + * + * @param array{currency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getCollateralData(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/crypto-loan-common/collateral-data', $options); + } + + /** + * Get Loanable Currency Data. + * + * GET /v5/crypto-loan-common/loanable-data + * + * @param array{currency?:string, vipLevel?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLoanableData(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-common/loanable-data', $options); + } + + /** + * Get Max Collateral Redeem Amount. + * + * GET /v5/crypto-loan-common/max-collateral-amount + * + * @param string $currency Collateral currency + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getMaxCollateralAmount(string $currency, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/crypto-loan-common/max-collateral-amount', + array_merge($options, ['currency' => $currency]) + ); + } + + /** + * Get Crypto Loan Position. + * + * GET /v5/crypto-loan-common/position + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getPosition(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-common/position', $options); + } + + /** + * Calculate Max Borrowable Amount. + * + * POST /v5/crypto-loan-common/max-loan + * + * @param string $currency Loan currency + * @param array $collateralList List of collateral items + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function maxLoan(string $currency, array $collateralList, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-common/max-loan', + array_merge($options, ['currency' => $currency, 'collateralList' => $collateralList]) + ); + } + + /** + * Cancel Borrow Order. + * + * POST /v5/crypto-loan-fixed/borrow-order-cancel + * + * @param string $orderId Order ID to cancel + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelFixedBorrowOrder(string $orderId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/borrow-order-cancel', + array_merge($options, ['orderId' => $orderId]) + ); + } + + /** + * Cancel Supply Order. + * + * POST /v5/crypto-loan-fixed/supply-order-cancel + * + * @param string $orderId Order ID to cancel + * @param array{refundedAccount?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelFixedSupplyOrder(string $orderId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/supply-order-cancel', + array_merge($options, ['orderId' => $orderId]) + ); + } + + /** + * Create Fixed-Term Borrow Order. + * + * POST /v5/crypto-loan-fixed/borrow + * + * @param string $orderCurrency Loan currency + * @param string $orderAmount Loan amount + * @param string $annualRate Annual interest rate + * @param string $term Loan term + * @param array $collateralList List of collateral assets + * @param array{autoRepay?:string, repayType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createFixedBorrow(string $orderCurrency, string $orderAmount, string $annualRate, string $term, array $collateralList, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/borrow', + array_merge($options, ['orderCurrency' => $orderCurrency, 'orderAmount' => $orderAmount, 'annualRate' => $annualRate, 'term' => $term, 'collateralList' => $collateralList]) + ); + } + + /** + * Fully Repay Loan. + * + * POST /v5/crypto-loan-fixed/fully-repay + * + * @param string $loanId Loan ID to repay + * @param string $loanCurrency Loan currency + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function fullyRepayCryptoLoanFixed(string $loanId, string $loanCurrency, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/fully-repay', + array_merge($options, ['loanId' => $loanId, 'loanCurrency' => $loanCurrency]) + ); + } + + /** + * Get Borrow Contract Info. + * + * GET /v5/crypto-loan-fixed/borrow-contract-info + * + * @param array{orderId?:string, loanId?:string, orderCurrency?:string, term?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedBorrowContractInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-fixed/borrow-contract-info', $options); + } + + /** + * Get Borrow Order Info. + * + * GET /v5/crypto-loan-fixed/borrow-order-info + * + * @param array{orderId?:string, orderCurrency?:string, state?:string, term?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedBorrowOrderInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-fixed/borrow-order-info', $options); + } + + /** + * Get Borrow Market Quotes. + * + * GET /v5/crypto-loan-fixed/borrow-order-quote + * + * @param array{orderCurrency?:string, term?:string, orderBy?:string, sort?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedBorrowOrderQuote(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/crypto-loan-fixed/borrow-order-quote', $options); + } + + /** + * Get Renewal Information. + * + * GET /v5/crypto-loan-fixed/renew-info + * + * @param array{orderId?:string, orderCurrency?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedRenewInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-fixed/renew-info', $options); + } + + /** + * Get Supply Contract Info. + * + * GET /v5/crypto-loan-fixed/supply-contract-info + * + * @param array{orderId?:string, supplyId?:string, supplyCurrency?:string, term?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedSupplyContractInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-fixed/supply-contract-info', $options); + } + + /** + * Get Supply Order Info. + * + * GET /v5/crypto-loan-fixed/supply-order-info + * + * @param array{orderId?:string, orderCurrency?:string, state?:string, term?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedSupplyOrderInfo(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-fixed/supply-order-info', $options); + } + + /** + * Get Supply Market Quotes. + * + * GET /v5/crypto-loan-fixed/supply-order-quote + * + * @param array{orderCurrency?:string, term?:string, orderBy?:string, sort?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedSupplyOrderQuote(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/crypto-loan-fixed/supply-order-quote', $options); + } + + /** + * Renew Loan. + * + * POST /v5/crypto-loan-fixed/renew + * + * @param string $loanId Loan ID to renew + * @param array $collateralList List of collateral assets + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function renewFixed(string $loanId, array $collateralList, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/renew', + array_merge($options, ['loanId' => $loanId, 'collateralList' => $collateralList]) + ); + } + + /** + * Repay with Collateral. + * + * POST /v5/crypto-loan-fixed/repay-collateral + * + * @param string $loanId Loan ID (Bybit returns loan IDs as strings; matches renewFixed / fullyRepayCryptoLoanFixed). + * @param string $loanCurrency Loan currency. + * @param string $collateralCoin Collateral coin. + * @param string $amount Amount to repay with collateral. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function repayFixedCollateral(string $loanId, string $loanCurrency, string $collateralCoin, string $amount, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-fixed/repay-collateral', + array_merge($options, ['loanId' => $loanId, 'loanCurrency' => $loanCurrency, 'collateralCoin' => $collateralCoin, 'amount' => $amount]) + ); + } + + /** + * Get Flexible Borrow History. + * + * GET /v5/crypto-loan-flexible/borrow-history + * + * @param array{orderId?:string, loanCurrency?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFlexibleBorrowHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-flexible/borrow-history', $options); + } + + /** + * Get Ongoing Flexible Borrow Info. + * + * GET /v5/crypto-loan-flexible/ongoing-coin + * + * @param array{loanCurrency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFlexibleOngoingCoin(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-flexible/ongoing-coin', $options); + } + + /** + * Get Flexible Repayment History. + * + * GET /v5/crypto-loan-flexible/repayment-history + * + * @param array{repayId?:string, loanCurrency?:string, limit?:int, cursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFlexibleRepaymentHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/crypto-loan-flexible/repayment-history', $options); + } + + /** + * Create Flexible Borrow Order. + * + * POST /v5/crypto-loan-flexible/borrow + * + * @param string $loanCurrency Loan currency + * @param string $loanAmount Loan amount + * @param array $collateralList Collateral list + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function borrowFlexible(string $loanCurrency, string $loanAmount, array $collateralList, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-flexible/borrow', + array_merge($options, ['loanCurrency' => $loanCurrency, 'loanAmount' => $loanAmount, 'collateralList' => $collateralList]) + ); + } + + /** + * Repay Flexible Loan. + * + * POST /v5/crypto-loan-flexible/repay + * + * @param string $loanCurrency Loan currency + * @param string $amount Repay amount + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function repayFlexible(string $loanCurrency, string $amount, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-flexible/repay', + array_merge($options, ['loanCurrency' => $loanCurrency, 'amount' => $amount]) + ); + } + + /** + * Repay with Collateral. + * + * POST /v5/crypto-loan-flexible/repay-collateral + * + * @param string $loanCurrency Loan currency + * @param string $collateralCoin Collateral coin + * @param string $amount Repay amount + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function repayFlexibleWithCollateral(string $loanCurrency, string $collateralCoin, string $amount, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/crypto-loan-flexible/repay-collateral', + array_merge($options, ['loanCurrency' => $loanCurrency, 'collateralCoin' => $collateralCoin, 'amount' => $amount]) + ); + } +} diff --git a/src/RestApi/EarnService.php b/src/RestApi/EarnService.php new file mode 100644 index 0000000..dc56a4e --- /dev/null +++ b/src/RestApi/EarnService.php @@ -0,0 +1,1300 @@ +session->signRequest( + 'POST', + '/v5/earn/liquidity-mining/add-liquidity', + array_merge($options, ['productId' => $productId, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Add margin to a liquidity mining position. + * + * POST /v5/earn/liquidity-mining/add-margin + * + * @param string $productId Product ID + * @param string $orderLinkId User-defined order link ID + * @param string $positionId Position ID to add margin to + * @param string $amount Margin amount to add + * @param string $quoteAccountType Account type used for the quote coin + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function addMargin(string $productId, string $orderLinkId, string $positionId, string $amount, string $quoteAccountType, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/liquidity-mining/add-margin', + array_merge($options, ['productId' => $productId, 'orderLinkId' => $orderLinkId, 'positionId' => $positionId, 'amount' => $amount, 'quoteAccountType' => $quoteAccountType]) + ); + } + + /** + * Claim accrued interest from a liquidity mining product. + * + * POST /v5/earn/liquidity-mining/claim-interest + * + * @param string $productId Product ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function claimLiquidityInterest(string $productId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/liquidity-mining/claim-interest', + array_merge($options, ['productId' => $productId]) + ); + } + + /** + * Get advance earn order. + * + * GET /v5/earn/advance/order + * + * @param string $category Product category + * @param array{productId?:int, orderId?:string, orderLinkId?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAdvanceEarnOrder(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/advance/order', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get advance earn position. + * + * GET /v5/earn/advance/position + * + * @param string $category Product category + * @param array{productId?:int, coin?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAdvanceEarnPosition(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/advance/position', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get advance earn product info. + * + * GET /v5/earn/advance/product + * + * @param string $category Product category + * @param array{coin?:string, duration?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAdvanceEarnProduct(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/advance/product', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get advance earn product extra info. + * + * GET /v5/earn/advance/product-extra-info + * + * @param string $category Product category + * @param array{productId?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAdvanceEarnProductExtraInfo(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/advance/product-extra-info', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get double win leverage info. + * + * GET /v5/earn/advance/double-win-leverage + * + * @param string $productId Product ID (Bybit returns product IDs as strings). + * @param string $initialPrice Initial price + * @param string $lowerPrice Lower price bound + * @param string $upperPrice Upper price bound + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getDoubleWinLeverage(string $productId, string $initialPrice, string $lowerPrice, string $upperPrice, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/advance/double-win-leverage', + array_merge($options, ['productId' => $productId, 'initialPrice' => $initialPrice, 'lowerPrice' => $lowerPrice, 'upperPrice' => $upperPrice]) + ); + } + + /** + * Get earn product APR history. + * + * GET /v5/earn/apr-history + * + * @param string $category Product category + * @param string $productId Product ID + * @param int $startTime Start timestamp (ms) + * @param int $endTime End timestamp (ms) + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAprHistory(string $category, string $productId, int $startTime, int $endTime, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/apr-history', + array_merge($options, ['category' => $category, 'productId' => $productId, 'startTime' => $startTime, 'endTime' => $endTime]) + ); + } + + /** + * Get earn hourly yield history. + * + * GET /v5/earn/hourly-yield + * + * @param string $category Product category + * @param array{productId?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getHourlyYieldHistory(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/hourly-yield', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get stake or redeem order history. + * + * GET /v5/earn/order + * + * @param string $category Product category + * @param array{orderId?:string, orderLinkId?:string, productId?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getOrderHistory(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/order', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get staked position. + * + * GET /v5/earn/position + * + * @param string $category Product category + * @param array{productId?:string, coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getPosition(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/position', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get earn product info. + * + * GET /v5/earn/product + * + * @param string $category Product category + * @param array{coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getProduct(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/product', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get earn yield history. + * + * GET /v5/earn/yield + * + * @param string $category Product category + * @param array{productId?:int, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getYieldHistory(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/yield', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get fixed term earn order history. + * + * GET /v5/earn/fixed-term/order + * + * @param array{orderType?:string, productId?:string, category?:string, orderId?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedTermOrder(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/fixed-term/order', $options); + } + + /** + * Query fixed term earn positions. + * + * GET /v5/earn/fixed-term/position + * + * @param array{productId?:string, category?:string, coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedTermPosition(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/fixed-term/position', $options); + } + + /** + * Get the list of fixed term earn products. + * + * GET /v5/earn/fixed-term/product + * + * @param array{coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getFixedTermProduct(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/earn/fixed-term/product', $options); + } + + /** + * Get the list of Hold-to-Earn products. + * + * GET /v5/earn/hold-to-earn/product + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getHoldToEarnProduct(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/earn/hold-to-earn/product', $options); + } + + /** + * Get the yield history for Hold-to-Earn products. + * + * GET /v5/earn/hold-to-earn/yield-history + * + * @param int $limit Number of records to return + * @param array{timeStart?:int, timeEnd?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getHoldToEarnYieldHistory(int $limit, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/hold-to-earn/yield-history', + array_merge($options, ['limit' => $limit]) + ); + } + + /** + * Get liquidation records for Liquidity Mining positions. + * + * GET /v5/earn/liquidity-mining/liquidation-records + * + * @param array{baseCoin?:string, quoteCoin?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLiquidityMiningLiquidationRecords(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/liquidity-mining/liquidation-records', $options); + } + + /** + * Get the liquidity mining order history. + * + * GET /v5/earn/liquidity-mining/order + * + * @param array{orderId?:string, orderLinkId?:string, productId?:string, orderType?:string, status?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLiquidityMiningOrders(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/liquidity-mining/order', $options); + } + + /** + * Get the list of active liquidity mining positions. + * + * GET /v5/earn/liquidity-mining/position + * + * @param array{productId?:string, baseCoin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLiquidityMiningPositions(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/liquidity-mining/position', $options); + } + + /** + * Get the list of liquidity mining products. + * + * GET /v5/earn/liquidity-mining/product + * + * @param array{baseCoin?:string, quoteCoin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLiquidityMiningProducts(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/earn/liquidity-mining/product', $options); + } + + /** + * Get the liquidity mining yield claim records. + * + * GET /v5/earn/liquidity-mining/yield-records + * + * @param array{baseCoin?:string, quoteCoin?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getLiquidityMiningYieldRecords(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/liquidity-mining/yield-records', $options); + } + + /** + * Get the NAV chart data for an RWA product. + * + * GET /v5/earn/rwa/nav-chart + * + * @param string $productId Product ID (Bybit returns product IDs as strings). + * @param array{startTime?:int, endTime?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRwaNavChart(string $productId, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/rwa/nav-chart', + array_merge($options, ['productId' => $productId]) + ); + } + + /** + * Get the list of RWA orders. + * + * GET /v5/earn/rwa/order + * + * @param array{orderId?:string, orderLinkId?:string, orderType?:string, productId?:int, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRwaOrderList(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/rwa/order', $options); + } + + /** + * Get the list of RWA positions. + * + * GET /v5/earn/rwa/position + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRwaPositionList(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/rwa/position', $options); + } + + /** + * Get the list of RWA earn products. + * + * GET /v5/earn/rwa/product + * + * @param array{coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRwaProductList(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/earn/rwa/product', $options); + } + + /** + * Get smart leverage redeem estimation amount list. + * + * GET /v5/earn/advance/get-redeem-est-amount-list + * + * @param string $category Product category + * @param string $positionIds List of position IDs + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getSmartLeverageRedeemEstAmountList(string $category, string $positionIds, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/advance/get-redeem-est-amount-list', + array_merge($options, ['category' => $category, 'positionIds' => $positionIds]) + ); + } + + /** + * Get daily yield records for a token earn position. + * + * GET /v5/earn/token/yield + * + * @param string $coin Coin name + * @param array{startTime?:int, endTime?:int, cursor?:string, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenDailyYield(string $coin, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/token/yield', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * Get historical APR data for a token earn product. + * + * GET /v5/earn/token/history-apr + * + * @param string $coin Coin name + * @param int $range Time range + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenHistoricalApr(string $coin, int $range, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/token/history-apr', + array_merge($options, ['coin' => $coin, 'range' => $range]) + ); + } + + /** + * Get hourly yield records for a token earn position. + * + * GET /v5/earn/token/hourly-yield + * + * @param string $coin Coin name + * @param array{startTime?:int, endTime?:int, cursor?:string, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenHourlyYield(string $coin, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/token/hourly-yield', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * Query the list of token earn orders. + * + * GET /v5/earn/token/order + * + * @param string $coin Coin name + * @param array{orderLinkId?:string, orderId?:string, orderType?:string, startTime?:int, endTime?:int, cursor?:string, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenOrderList(string $coin, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/token/order', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * Query the current token earn position for a coin. + * + * GET /v5/earn/token/position + * + * @param string $coin Coin name + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenPosition(string $coin, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/token/position', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * Get token earn product information by coin. + * + * GET /v5/earn/token/product + * + * @param string $coin Coin name + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTokenProduct(string $coin, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/earn/token/product', + array_merge($options, ['coin' => $coin]) + ); + } + + /** + * List coupons for the specified category. + * + * GET /v5/earn/coupons + * + * @param string $category Product category + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function listCoupons(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/coupons', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Modify an earn position (e.g. toggle auto reinvest). + * + * POST /v5/earn/position/modify + * + * @param string $category Product category + * @param string $productId Product ID (Bybit returns product IDs as strings). + * @param string $positionId Position ID (Bybit returns position IDs as strings). + * @param int $autoReinvest Auto reinvest flag. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function modifyEarnPosition(string $category, string $productId, string $positionId, int $autoReinvest, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/position/modify', + array_merge($options, ['category' => $category, 'productId' => $productId, 'positionId' => $positionId, 'autoReinvest' => $autoReinvest]) + ); + } + + /** + * Place an advance earn order. + * + * POST /v5/earn/advance/place-order + * + * @param string $category Product category + * @param string $productId Product ID (Bybit returns product IDs as strings). + * @param string $orderType Order type + * @param string $amount Order amount + * @param string $accountType Account type + * @param string $coin Coin name + * @param string $orderLinkId User-defined order ID + * @param array{dualAssetsExtra?:array, interestCard?:array, smartLeverageStakeExtra?:array, smartLeverageRedeemExtra?:array, doubleWinStakeExtra?:array, doubleWinRedeemExtra?:array, discountBuyExtra?:array} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function placeAdvanceEarnOrder(string $category, string $productId, string $orderType, string $amount, string $accountType, string $coin, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/advance/place-order', + array_merge($options, ['category' => $category, 'productId' => $productId, 'orderType' => $orderType, 'amount' => $amount, 'accountType' => $accountType, 'coin' => $coin, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Stake or redeem an earn product. + * + * POST /v5/earn/place-order + * + * @param string $category Product category + * @param string $orderType Order type: Stake or Redeem + * @param string $accountType Account type + * @param string $amount Order amount + * @param string $coin Coin name + * @param string $productId Product ID + * @param string $orderLinkId User-defined order ID + * @param array{redeemPositionId?:string, toAccountType?:string, interestCard?:array} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function placeOrder(string $category, string $orderType, string $accountType, string $amount, string $coin, string $productId, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/place-order', + array_merge($options, ['category' => $category, 'orderType' => $orderType, 'accountType' => $accountType, 'amount' => $amount, 'coin' => $coin, 'productId' => $productId, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Place a fixed term earn order. + * + * POST /v5/earn/fixed-term/place-order + * + * @param string $productId Product ID + * @param string $category Product category + * @param string $coin Coin name + * @param string $amount Order amount + * @param string $accountType Account type + * @param string $orderLinkId User-customized order ID + * @param array{autoInvest?:bool} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function placeFixedTermOrder(string $productId, string $category, string $coin, string $amount, string $accountType, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/fixed-term/place-order', + array_merge($options, ['productId' => $productId, 'category' => $category, 'coin' => $coin, 'amount' => $amount, 'accountType' => $accountType, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Place a stake or redeem order for an RWA product. + * + * POST /v5/earn/rwa/place-order + * + * @param string $productId Product ID (Bybit returns product IDs as strings). + * @param string $orderType Order type (Stake or Redeem) + * @param string $coin Coin name + * @param string $orderLinkId User-defined order link ID + * @param array{stakeAmount?:string, redeemShares?:string, accountType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function placeRwaOrder(string $productId, string $orderType, string $coin, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/rwa/place-order', + array_merge($options, ['productId' => $productId, 'orderType' => $orderType, 'coin' => $coin, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Place a mint or redeem order for token products. + * + * POST /v5/earn/token/place-order + * + * @param string $coin Coin name + * @param string $orderLinkId User-customized order ID + * @param string $orderType Order type (Mint/Redeem) + * @param string $amount Order amount + * @param string $accountType Account type + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function placeTokenOrder(string $coin, string $orderLinkId, string $orderType, string $amount, string $accountType, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/token/place-order', + array_merge($options, ['coin' => $coin, 'orderLinkId' => $orderLinkId, 'orderType' => $orderType, 'amount' => $amount, 'accountType' => $accountType]) + ); + } + + /** + * Get Plan Asset Trend for a PWM investment plan. + * + * GET /v5/earn/pwm/investment-plan/asset-trend + * + * @param string $planId Plan ID + * @param array{startTime?:int, endTime?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmAssetTrend(string $planId, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/pwm/investment-plan/asset-trend', + array_merge($options, ['planId' => $planId]) + ); + } + + /** + * Claim Available Funds from a PWM investment plan. + * + * POST /v5/earn/pwm/investment-plan/claim + * + * @param string $planId Plan ID + * @param string $orderLinkId User-defined order link ID + * @param array{toAccountType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmClaim(string $planId, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/investment-plan/claim', + array_merge($options, ['planId' => $planId, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Create Custom Investment Plan (Direct Mode) for PWM. + * + * POST /v5/earn/pwm/customize-plan/create + * + * @param array $products List of products to include in the plan + * @param string $orderLinkId User-defined order link ID + * @param array{accountType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmCreateCustomPlan(array $products, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/customize-plan/create', + array_merge($options, ['products' => $products, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Get Fund Historical NAV for a PWM investment plan fund. + * + * GET /v5/earn/pwm/investment-plan/fund-nav + * + * @param string $fundId Fund ID + * @param array{startTime?:int, endTime?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmFundNav(string $fundId, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/pwm/investment-plan/fund-nav', + array_merge($options, ['fundId' => $fundId]) + ); + } + + /** + * Transfer funds between PWM custody sub-accounts. + * + * POST /v5/earn/pwm/fund-transfer + * + * @param string $transferId Client-supplied transfer id + * @param string $fromUserId Source user id (Bybit UIDs are strings). + * @param string $toUserId Destination user id (Bybit UIDs are strings). + * @param string $amount Transfer amount + * @param string $coin Coin to transfer + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmFundTransfer(string $transferId, string $fromUserId, string $toUserId, string $amount, string $coin, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/fund-transfer', + array_merge($options, ['transferId' => $transferId, 'fromUserId' => $fromUserId, 'toUserId' => $toUserId, 'amount' => $amount, 'coin' => $coin]) + ); + } + + /** + * Get pending-subscription PWM investment plan detail. + * + * GET /v5/earn/pwm/investment-plan/new-plan + * + * @param string $planId Investment plan id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmGetNewPlanDetail(string $planId, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/pwm/investment-plan/new-plan', + array_merge($options, ['planId' => $planId]) + ); + } + + /** + * Get PWM investment plan detail (active or closed). + * + * GET /v5/earn/pwm/investment-plan/detail + * + * @param string $planId Investment plan id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmGetPlanDetail(string $planId, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/earn/pwm/investment-plan/detail', + array_merge($options, ['planId' => $planId]) + ); + } + + /** + * Create a pending-subscription fund under the PWM asset manager. + * + * POST /v5/earn/pwm/asset-manager/create-fund + * + * @param string $fundName Fund name + * @param string $coin Underlying coin + * @param string $profitShareRate Profit-share rate + * @param string $managementFeeRate Management fee rate + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array{fundIntroduction?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstCreateFund(string $fundName, string $coin, string $profitShareRate, string $managementFeeRate, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/create-fund', + array_merge($options, ['fundName' => $fundName, 'coin' => $coin, 'profitShareRate' => $profitShareRate, 'managementFeeRate' => $managementFeeRate, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Create an investment plan for a PWM client. + * + * POST /v5/earn/pwm/asset-manager/create-investment-plan + * + * @param string $accountUid Client account UID + * @param string $planName Investment plan name + * @param string $planType Investment plan type + * @param array $investmentDistribution Fund distribution allocations for the plan + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstCreateInvestmentPlan(string $accountUid, string $planName, string $planType, array $investmentDistribution, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/create-investment-plan', + array_merge($options, ['accountUid' => $accountUid, 'planName' => $planName, 'planType' => $planType, 'investmentDistribution' => $investmentDistribution, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Create a fund sub-account under the PWM asset manager. + * + * POST /v5/earn/pwm/asset-manager/create-sub-account + * + * @param string $fundId Fund identifier + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstCreateSubAccount(string $fundId, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/create-sub-account', + array_merge($options, ['fundId' => $fundId, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Query the institution's investment plans under PWM asset manager. + * + * GET /v5/earn/pwm/asset-manager/get-investment-plan + * + * @param array{planId?:string, status?:string, subscriptionUid?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstGetInvestmentPlans(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/asset-manager/get-investment-plan', $options); + } + + /** + * Query the institution's managed funds under PWM asset manager. + * + * GET /v5/earn/pwm/asset-manager/all-funds + * + * @param array{fundId?:string, coin?:string, status?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstListFunds(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/asset-manager/all-funds', $options); + } + + /** + * Query fund subscription and redemption orders under PWM asset manager. + * + * GET /v5/earn/pwm/asset-manager/all-order + * + * @param array{fundId?:string, orderType?:string, status?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstListOrders(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/asset-manager/all-order', $options); + } + + /** + * Update investment plan status and/or its constituent funds. + * + * POST /v5/earn/pwm/asset-manager/manage-investment-plan + * + * @param string $planId Investment plan id + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array{updateStatus?:string, updateFunds?:array} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstManageInvestmentPlan(string $planId, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/manage-investment-plan', + array_merge($options, ['planId' => $planId, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Approve or reject a PWM fund subscription/redemption order. + * + * POST /v5/earn/pwm/asset-manager/manage-order + * + * @param string $orderId Order id to manage + * @param string $action Action to perform (approve/reject) + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstManageOrder(string $orderId, string $action, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/manage-order', + array_merge($options, ['orderId' => $orderId, 'action' => $action, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Execute profit settlement for a PWM asset-manager fund. + * + * POST /v5/earn/pwm/asset-manager/settle-profit + * + * @param string $fundId Fund identifier + * @param string $reqLinkId Client-supplied idempotency/request link id + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInstSettleProfit(string $fundId, string $reqLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/asset-manager/settle-profit', + array_merge($options, ['fundId' => $fundId, 'reqLinkId' => $reqLinkId]) + ); + } + + /** + * Invest More in an Active PWM investment plan. + * + * POST /v5/earn/pwm/investment-plan/invest-more + * + * @param string $planId Plan ID + * @param string $category Product category + * @param string $productId Product ID + * @param string $amount Amount to invest + * @param string $orderLinkId User-defined order link ID + * @param array{accountType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmInvestMore(string $planId, string $category, string $productId, string $amount, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/investment-plan/invest-more', + array_merge($options, ['planId' => $planId, 'category' => $category, 'productId' => $productId, 'amount' => $amount, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * List PWM investment plans. + * + * GET /v5/earn/pwm/investment-plan/all + * + * @param array{planId?:string, status?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmListInvestmentPlans(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/investment-plan/all', $options); + } + + /** + * List Investment Plan Orders for PWM. + * + * GET /v5/earn/pwm/investment-plan/order + * + * @param array{planId?:string, category?:string, type?:string, status?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string, orderLinkId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmListOrder(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/investment-plan/order', $options); + } + + /** + * List Available Product Cards (Direct Mode) for PWM customize plan. + * + * GET /v5/earn/pwm/customize-plan/product + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmListProductCards(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/earn/pwm/customize-plan/product', $options); + } + + /** + * Query PWM fund transfer records. + * + * GET /v5/earn/pwm/query-fund-transfer-result + * + * @param array{transferId?:string, fromUserId?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmQueryFundTransferResult(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/earn/pwm/query-fund-transfer-result', $options); + } + + /** + * Redeem from a PWM Investment Plan. + * + * POST /v5/earn/pwm/investment-plan/redeem + * + * @param string $planId Plan ID + * @param string $category Product category + * @param string $productId Product ID + * @param string $orderLinkId User-defined order link ID + * @param array{shares?:string, amount?:string, positionId?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmRedeem(string $planId, string $category, string $productId, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/investment-plan/redeem', + array_merge($options, ['planId' => $planId, 'category' => $category, 'productId' => $productId, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * One-Click Subscribe to Pending Plan for PWM investment. + * + * POST /v5/earn/pwm/investment-plan/subscribe + * + * @param string $planId Plan ID + * @param string $orderLinkId User-defined order link ID + * @param array{accountType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function pwmSubscribe(string $planId, string $orderLinkId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/pwm/investment-plan/subscribe', + array_merge($options, ['planId' => $planId, 'orderLinkId' => $orderLinkId]) + ); + } + + /** + * Redeem a fixed term earn position. + * + * POST /v5/earn/fixed-term/redeem + * + * @param string $productId Product ID + * @param string $category Product category + * @param string $positionId Position ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function redeemFixedTerm(string $productId, string $category, string $positionId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/fixed-term/redeem', + array_merge($options, ['productId' => $productId, 'category' => $category, 'positionId' => $positionId]) + ); + } + + /** + * Reinvest accrued interest into a liquidity mining position. + * + * POST /v5/earn/liquidity-mining/reinvest + * + * @param string $productId Product ID + * @param string $orderLinkId User-defined order link ID + * @param string $positionId Position ID to reinvest into + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function reinvestLiquidity(string $productId, string $orderLinkId, string $positionId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/liquidity-mining/reinvest', + array_merge($options, ['productId' => $productId, 'orderLinkId' => $orderLinkId, 'positionId' => $positionId]) + ); + } + + /** + * Remove liquidity from a liquidity mining position. + * + * POST /v5/earn/liquidity-mining/remove-liquidity + * + * @param string $productId Product ID + * @param string $orderLinkId User-defined order link ID + * @param string $positionId Position ID to remove liquidity from + * @param array{removeRate?:int, removeType?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function removeLiquidity(string $productId, string $orderLinkId, string $positionId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/liquidity-mining/remove-liquidity', + array_merge($options, ['productId' => $productId, 'orderLinkId' => $orderLinkId, 'positionId' => $positionId]) + ); + } + + /** + * Enable or disable auto-invest on a fixed term earn position. + * + * POST /v5/earn/fixed-term/position/auto-invest + * + * @param string $productId Product ID + * @param string $category Product category + * @param string $positionId Position ID + * @param string $status Auto-invest status + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function setFixedTermAutoInvest(string $productId, string $category, string $positionId, string $status, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/earn/fixed-term/position/auto-invest', + array_merge($options, ['productId' => $productId, 'category' => $category, 'positionId' => $positionId, 'status' => $status]) + ); + } +} diff --git a/src/RestApi/MarketService.php b/src/RestApi/MarketService.php new file mode 100644 index 0000000..56268e8 --- /dev/null +++ b/src/RestApi/MarketService.php @@ -0,0 +1,429 @@ +session->publicRequest('GET', '/v5/market/adlAlert', $options); + } + + /** + * Get the delivery price for delivery contracts. + * + * GET /v5/market/delivery-price + * + * @param string $category + * @param array{symbol?:string, baseCoin?:string, settleCoin?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/delivery-price + */ + public function getDeliveryPrice(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/delivery-price', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get the fee group structure for a given product type. + * + * GET /v5/market/fee-group-info + * + * @param string $productType + * @param array{groupId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/fee-group-info + */ + public function getFeeGroupInfo(string $productType, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/fee-group-info', + array_merge($options, ['productType' => $productType]) + ); + } + + /** + * Get the historical funding rate for a symbol. + * + * GET /v5/market/funding/history + * + * @param string $category + * @param string $symbol + * @param array{startTime?:int, endTime?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/history-fund-rate + */ + public function getFundingRateHistory(string $category, string $symbol, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/funding/history', + array_merge($options, ['category' => $category, 'symbol' => $symbol]) + ); + } + + /** + * Get historical volatility data for options. + * + * GET /v5/market/historical-volatility + * + * @param string $category + * @param array{baseCoin?:string, quoteCoin?:string, period?:int, startTime?:int, endTime?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/iv + */ + public function getHistoricalVolatility(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/historical-volatility', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get index price components for a given index name. + * + * GET /v5/market/index-price-components + * + * @param string $indexName + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getIndexPriceComponents(string $indexName, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/index-price-components', + array_merge($options, ['indexName' => $indexName]) + ); + } + + /** + * Get the index price kline for a given symbol. + * + * GET /v5/market/index-price-kline + * + * @param string $category Product type — linear / inverse. + * @param string $symbol + * @param string $interval + * @param array{start?:int, end?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/index-kline + */ + public function getIndexPriceKline(string $category, string $symbol, string $interval, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/index-price-kline', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'interval' => $interval]) + ); + } + + /** + * Get instruments information for the given category. + * + * GET /v5/market/instruments-info + * + * @param string $category + * @param array{symbol?:string, status?:string, baseCoin?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/instrument + */ + public function getInstrumentsInfo(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/instruments-info', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get the insurance pool balance data. + * + * GET /v5/market/insurance + * + * @param array{coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/insurance + */ + public function getInsurancePool(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/market/insurance', $options); + } + + /** + * Get the long/short ratio for a symbol. + * + * GET /v5/market/account-ratio + * + * @param string $category + * @param string $symbol + * @param string $period + * @param array{startTime?:string, endTime?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/long-short-ratio + */ + public function getLongShortRatio(string $category, string $symbol, string $period, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/account-ratio', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'period' => $period]) + ); + } + + /** + * Get the kline (candlestick) data for a symbol. + * + * GET /v5/market/kline + * + * @param string $category Product type — spot / linear / inverse. + * @param string $symbol + * @param string $interval + * @param array{start?:int, end?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/kline + */ + public function getKline(string $category, string $symbol, string $interval, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/kline', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'interval' => $interval]) + ); + } + + /** + * Get the mark price kline for a given symbol. + * + * GET /v5/market/mark-price-kline + * + * @param string $category Product type — linear / inverse. + * @param string $symbol + * @param string $interval + * @param array{start?:int, end?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/mark-kline + */ + public function getMarkPriceKline(string $category, string $symbol, string $interval, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/mark-price-kline', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'interval' => $interval]) + ); + } + + /** + * Get the latest delivery price for a base coin. + * + * GET /v5/market/new-delivery-price + * + * @param string $category + * @param string $baseCoin + * @param array{settleCoin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/new-delivery-price + */ + public function getNewDeliveryPrice(string $category, string $baseCoin, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/new-delivery-price', + array_merge($options, ['category' => $category, 'baseCoin' => $baseCoin]) + ); + } + + /** + * Get the open interest history of a symbol. + * + * GET /v5/market/open-interest + * + * @param string $category + * @param string $symbol + * @param string $intervalTime + * @param array{startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/open-interest + */ + public function getOpenInterest(string $category, string $symbol, string $intervalTime, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/open-interest', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'intervalTime' => $intervalTime]) + ); + } + + /** + * Get orderbook data for a given symbol and category. + * + * GET /v5/market/orderbook + * + * @param string $category + * @param string $symbol + * @param array{limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/orderbook + */ + public function getOrderbook(string $category, string $symbol, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/orderbook', + array_merge($options, ['category' => $category, 'symbol' => $symbol]) + ); + } + + /** + * Get the order price limit for a symbol. + * + * GET /v5/market/price-limit + * + * @param string $symbol + * @param array{category?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getOrderPriceLimit(string $symbol, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/price-limit', + array_merge($options, ['symbol' => $symbol]) + ); + } + + /** + * Get premium index price kline for a given symbol and interval. + * + * GET /v5/market/premium-index-price-kline + * + * @param string $category Product type — linear / inverse. + * @param string $symbol + * @param string $interval + * @param array{start?:int, end?:int, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/premium-index-kline + */ + public function getPremiumIndexPriceKline(string $category, string $symbol, string $interval, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/premium-index-price-kline', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'interval' => $interval]) + ); + } + + /** + * Get recent public trades for a given symbol or option base coin. + * + * GET /v5/market/recent-trade + * + * @param string $category + * @param array{symbol?:string, baseCoin?:string, optionType?:string, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/recent-trade + */ + public function getRecentPublicTrades(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/recent-trade', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get risk limit for linear and inverse contracts. + * + * GET /v5/market/risk-limit + * + * @param string $category + * @param array{symbol?:string, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/risk-limit + */ + public function getRiskLimit(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/risk-limit', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get RPI orderbook data for a given symbol. + * + * GET /v5/market/rpi_orderbook + * + * @param string $symbol + * @param int $limit + * @param array{category?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRpiOrderbook(string $symbol, int $limit, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/rpi_orderbook', + array_merge($options, ['symbol' => $symbol, 'limit' => $limit]) + ); + } + + /** + * Get Bybit server time. + * + * GET /v5/market/time + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/time + */ + public function getServerTime(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/market/time', $options); + } + + /** + * Get latest ticker information for symbols in a given category. + * + * GET /v5/market/tickers + * + * @param string $category + * @param array{symbol?:string, baseCoin?:string, expDate?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/market/tickers + */ + public function getTickers(string $category, array $options = []): array + { + return $this->session->publicRequest( + 'GET', + '/v5/market/tickers', + array_merge($options, ['category' => $category]) + ); + } +} diff --git a/src/RestApi/P2pService.php b/src/RestApi/P2pService.php new file mode 100644 index 0000000..fabd1a8 --- /dev/null +++ b/src/RestApi/P2pService.php @@ -0,0 +1,345 @@ +session->signRequest('POST', '/v5/p2p/user/personal/info', $options); + } + + /** + * Get Ads. + * + * POST /v5/p2p/item/online + * + * @param string $tokenId Token ID + * @param string $currencyId Currency ID + * @param string $side Side + * @param array{page?:string, size?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAds(string $tokenId, string $currencyId, string $side, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/item/online', + array_merge($options, ['tokenId' => $tokenId, 'currencyId' => $currencyId, 'side' => $side]) + ); + } + + /** + * Get All Orders. + * + * POST /v5/p2p/order/simplifyList + * + * @param int $page Page number + * @param int $size Page size + * @param array{status?:int, beginTime?:string, endTime?:string, tokenId?:string, side?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getAllOrders(int $page, int $size, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/simplifyList', + array_merge($options, ['page' => $page, 'size' => $size]) + ); + } + + /** + * Get Chat Message. + * + * POST /v5/p2p/order/message/listpage + * + * @param string $orderId Order ID + * @param string $size Page size + * @param array{currentPage?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getChatMessages(string $orderId, string $size, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/message/listpage', + array_merge($options, ['orderId' => $orderId, 'size' => $size]) + ); + } + + /** + * Get Counterparty User Info. + * + * POST /v5/p2p/user/order/personal/info + * + * @param array{originalUid?:string, orderId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getCounterpartyUserInfo(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/p2p/user/order/personal/info', $options); + } + + /** + * Get My Ad Details. + * + * POST /v5/p2p/item/info + * + * @param string $itemId Item ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getMyAdDetails(string $itemId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/item/info', + array_merge($options, ['itemId' => $itemId]) + ); + } + + /** + * Get My Ads. + * + * POST /v5/p2p/item/personal/list + * + * @param array{itemId?:string, status?:string, side?:string, tokenId?:string, page?:string, size?:string, currencyId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getMyAds(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/p2p/item/personal/list', $options); + } + + /** + * Get Order Detail. + * + * POST /v5/p2p/order/info + * + * @param string $orderId Order ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getOrderDetail(string $orderId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/info', + array_merge($options, ['orderId' => $orderId]) + ); + } + + /** + * Get Pending Orders. + * + * POST /v5/p2p/order/pending/simplifyList + * + * @param int $page Page number + * @param int $size Page size + * @param array{status?:int, beginTime?:string, endTime?:string, tokenId?:string, side?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getPendingOrders(int $page, int $size, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/pending/simplifyList', + array_merge($options, ['page' => $page, 'size' => $size]) + ); + } + + /** + * Get User Payment. + * + * POST /v5/p2p/user/payment/list + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getUserPayment(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/p2p/user/payment/list', $options); + } + + /** + * Mark Order as Paid. + * + * POST /v5/p2p/order/pay + * + * @param string $orderId Order ID + * @param string $paymentType Payment type + * @param string $paymentId Payment ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function markOrderAsPaid(string $orderId, string $paymentType, string $paymentId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/pay', + array_merge($options, ['orderId' => $orderId, 'paymentType' => $paymentType, 'paymentId' => $paymentId]) + ); + } + + /** + * Post Ad. + * + * POST /v5/p2p/item/create + * + * @param string $tokenId Token ID + * @param string $currencyId Currency ID + * @param string $side Side + * @param string $priceType Price type + * @param string $premium Premium + * @param string $price Price + * @param string $minAmount Minimum amount + * @param string $maxAmount Maximum amount + * @param string $remark Remark + * @param array $tradingPreferenceSet Trading preference set + * @param array $paymentIds Payment IDs + * @param string $quantity Quantity + * @param string $paymentPeriod Payment period + * @param string $itemType Item type + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createAd(string $tokenId, string $currencyId, string $side, string $priceType, string $premium, string $price, string $minAmount, string $maxAmount, string $remark, array $tradingPreferenceSet, array $paymentIds, string $quantity, string $paymentPeriod, string $itemType, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/item/create', + array_merge($options, ['tokenId' => $tokenId, 'currencyId' => $currencyId, 'side' => $side, 'priceType' => $priceType, 'premium' => $premium, 'price' => $price, 'minAmount' => $minAmount, 'maxAmount' => $maxAmount, 'remark' => $remark, 'tradingPreferenceSet' => $tradingPreferenceSet, 'paymentIds' => $paymentIds, 'quantity' => $quantity, 'paymentPeriod' => $paymentPeriod, 'itemType' => $itemType]) + ); + } + + /** + * Release Assets. + * + * POST /v5/p2p/order/finish + * + * @param string $orderId Order ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function releaseAssets(string $orderId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/finish', + array_merge($options, ['orderId' => $orderId]) + ); + } + + /** + * Remove Ad. + * + * POST /v5/p2p/item/cancel + * + * @param string $itemId Item ID + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function removeAd(string $itemId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/item/cancel', + array_merge($options, ['itemId' => $itemId]) + ); + } + + /** + * Send Chat Message. + * + * POST /v5/p2p/order/message/send + * + * @param string $message Message content + * @param string $contentType Content type + * @param string $orderId Order ID + * @param string $msgUuid Message UUID + * @param array{fileName?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function sendChatMessage(string $message, string $contentType, string $orderId, string $msgUuid, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/order/message/send', + array_merge($options, ['message' => $message, 'contentType' => $contentType, 'orderId' => $orderId, 'msgUuid' => $msgUuid]) + ); + } + + /** + * Update / Relist Ad. + * + * POST /v5/p2p/item/update + * + * @param string $id Ad ID + * @param string $priceType Price type + * @param string $premium Premium + * @param string $price Price + * @param string $minAmount Minimum amount + * @param string $maxAmount Maximum amount + * @param string $remark Remark + * @param array $tradingPreferenceSet Trading preference set + * @param array $paymentIds Payment IDs + * @param string $actionType Action type + * @param string $quantity Quantity + * @param string $paymentPeriod Payment period + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function updateAd(string $id, string $priceType, string $premium, string $price, string $minAmount, string $maxAmount, string $remark, array $tradingPreferenceSet, array $paymentIds, string $actionType, string $quantity, string $paymentPeriod, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/item/update', + array_merge($options, ['id' => $id, 'priceType' => $priceType, 'premium' => $premium, 'price' => $price, 'minAmount' => $minAmount, 'maxAmount' => $maxAmount, 'remark' => $remark, 'tradingPreferenceSet' => $tradingPreferenceSet, 'paymentIds' => $paymentIds, 'actionType' => $actionType, 'quantity' => $quantity, 'paymentPeriod' => $paymentPeriod]) + ); + } + + /** + * Upload Chat File. + * + * POST /v5/p2p/oss/upload_file + * + * @param string $uploadFile File to upload. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function uploadChatFile(string $uploadFile, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/p2p/oss/upload_file', + array_merge($options, ['upload_file' => $uploadFile]) + ); + } +} diff --git a/src/RestApi/PositionService.php b/src/RestApi/PositionService.php new file mode 100644 index 0000000..7180513 --- /dev/null +++ b/src/RestApi/PositionService.php @@ -0,0 +1,239 @@ +session->signRequest( + 'POST', + '/v5/position/add-margin', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'margin' => $margin]) + ); + } + + /** + * Confirm new risk limit to remove the reduce-only restriction. + * + * POST /v5/position/confirm-pending-mmr + * + * @param string $category Product type — linear / inverse. + * @param string $symbol Symbol name. + * @param array{positionIdx?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/confirm-mmr + */ + public function confirmNewRiskLimit(string $category, string $symbol, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/confirm-pending-mmr', + array_merge($options, ['category' => $category, 'symbol' => $symbol]) + ); + } + + /** + * Get closed profit and loss records. + * + * GET /v5/position/closed-pnl + * + * @param string $category Product type — linear / inverse / option. + * @param array{symbol?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/close-pnl + */ + public function getClosedPnl(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/position/closed-pnl', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get closed option position records. + * + * GET /v5/position/get-closed-positions + * + * @param string $category Product type — option. + * @param array{symbol?:string, expDate?:string, cursor?:string, limit?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getClosePosition(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/position/get-closed-positions', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get move position (block trade) history. + * + * GET /v5/position/move-history + * + * @param array{category?:string, symbol?:string, startTime?:int, endTime?:int, status?:string, blockTradeId?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/move-position-history + */ + public function getMovePositionHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/position/move-history', $options); + } + + /** + * Get position info (real-time). + * + * GET /v5/position/list + * + * @param string $category Product type — linear / inverse / option. + * @param array{symbol?:string, baseCoin?:string, settleCoin?:string, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position + */ + public function getInfo(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/position/list', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Move positions between UIDs via block trade. + * + * POST /v5/position/move-positions + * + * @param string $fromUid Source UID + * @param string $toUid Destination UID + * @param list> $list Position rows to move. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/move-position + */ + public function movePosition(string $fromUid, string $toUid, array $list, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/move-positions', + array_merge($options, ['fromUid' => $fromUid, 'toUid' => $toUid, 'list' => $list]) + ); + } + + /** + * Enable or disable auto-add-margin for a position. + * + * POST /v5/position/set-auto-add-margin + * + * @param string $category Product type — linear / inverse. + * @param string $symbol Symbol name. + * @param int $autoAddMargin 0: off, 1: on. + * @param array{positionIdx?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/auto-add-margin + */ + public function setAutoAddMargin(string $category, string $symbol, int $autoAddMargin, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/set-auto-add-margin', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'autoAddMargin' => $autoAddMargin]) + ); + } + + /** + * Set leverage for a position. + * + * POST /v5/position/set-leverage + * + * @param string $category Product type — linear / inverse. + * @param string $symbol Symbol name. + * @param string $buyLeverage Buy leverage (must equal sellLeverage in one-way mode). + * @param string $sellLeverage Sell leverage. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/leverage + */ + public function setLeverage(string $category, string $symbol, string $buyLeverage, string $sellLeverage, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/set-leverage', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'buyLeverage' => $buyLeverage, 'sellLeverage' => $sellLeverage]) + ); + } + + /** + * Set take profit, stop loss, and trailing stop for a position. + * + * POST /v5/position/trading-stop + * + * @param string $category Product type — linear / inverse. + * @param string $symbol Symbol name. + * @param string $tpslMode TP/SL mode: "Full" or "Partial". + * @param int $positionIdx Position index (0 one-way, 1 hedge-Buy, 2 hedge-Sell). + * @param array{ + * takeProfit?:string, + * stopLoss?:string, + * trailingStop?:string, + * tpTriggerBy?:string, + * slTriggerBy?:string, + * activePrice?:string, + * tpSize?:string, + * slSize?:string, + * tpLimitPrice?:string, + * slLimitPrice?:string, + * tpOrderType?:string, + * slOrderType?:string + * } $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/trading-stop + */ + public function setTradingStop(string $category, string $symbol, string $tpslMode, int $positionIdx, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/trading-stop', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'tpslMode' => $tpslMode, 'positionIdx' => $positionIdx]) + ); + } + + /** + * Switch position mode between one-way and hedge mode. + * + * POST /v5/position/switch-mode + * + * @param string $category Product type — linear / inverse. + * @param int $mode 0: one-way, 3: hedge. + * @param array{symbol?:string, coin?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/position/position-mode + */ + public function switchPositionMode(string $category, int $mode, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/position/switch-mode', + array_merge($options, ['category' => $category, 'mode' => $mode]) + ); + } +} diff --git a/src/RestApi/RfqService.php b/src/RestApi/RfqService.php new file mode 100644 index 0000000..c1a2114 --- /dev/null +++ b/src/RestApi/RfqService.php @@ -0,0 +1,241 @@ +session->signRequest( + 'POST', + '/v5/rfq/accept-other-quote', + array_merge($options, ['rfqId' => $rfqId]) + ); + } + + /** + * Cancel All Quotes. + * + * POST /v5/rfq/cancel-all-quotes + * + * @param array{rfqId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelAllQuotes(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/rfq/cancel-all-quotes', $options); + } + + /** + * Cancel All RFQs. + * + * POST /v5/rfq/cancel-all-rfq + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelAllRfqs(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/rfq/cancel-all-rfq', $options); + } + + /** + * Cancel Quote. + * + * POST /v5/rfq/cancel-quote + * + * @param array{quoteId?:string, rfqId?:string} $options One of the two required. + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelQuote(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/rfq/cancel-quote', $options); + } + + /** + * Cancel RFQ. + * + * POST /v5/rfq/cancel-rfq + * + * @param array{rfqId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function cancelRfq(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/rfq/cancel-rfq', $options); + } + + /** + * Create Quote. + * + * POST /v5/rfq/create-quote + * + * @param string $rfqId RFQ ID. + * @param array{list?:list>, quoteRfqType?:int, anonymous?:bool} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createQuote(string $rfqId, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/rfq/create-quote', + array_merge($options, ['rfqId' => $rfqId]) + ); + } + + /** + * Create RFQ. + * + * POST /v5/rfq/create-rfq + * + * @param array $counterparties List of counterparties. + * @param list> $list RFQ leg list. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function createRfq(array $counterparties, array $list, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/rfq/create-rfq', + array_merge($options, ['counterparties' => $counterparties, 'list' => $list]) + ); + } + + /** + * Execute Quote. + * + * POST /v5/rfq/execute-quote + * + * @param string $rfqId RFQ ID. + * @param string $quoteId Quote ID. + * @param string $quoteSide Quote side. + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function executeQuote(string $rfqId, string $quoteId, string $quoteSide, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/rfq/execute-quote', + array_merge($options, ['rfqId' => $rfqId, 'quoteId' => $quoteId, 'quoteSide' => $quoteSide]) + ); + } + + /** + * Get Public Trades. + * + * GET /v5/rfq/public-trades + * + * @param array{baseCoin?:string, symbol?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getPublicTrades(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/public-trades', $options); + } + + /** + * Get Quotes. + * + * GET /v5/rfq/quote-list + * + * @param array{rfqId?:string, quoteId?:string, status?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getQuotes(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/quote-list', $options); + } + + /** + * Get Quotes Realtime. + * + * GET /v5/rfq/quote-realtime + * + * @param array{rfqId?:string, quoteId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getQuotesRealtime(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/quote-realtime', $options); + } + + /** + * Get RFQ Config. + * + * GET /v5/rfq/config + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getConfig(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/config', $options); + } + + /** + * Get RFQs. + * + * GET /v5/rfq/rfq-list + * + * @param array{rfqId?:string, status?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRfqs(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/rfq-list', $options); + } + + /** + * Get RFQs Realtime. + * + * GET /v5/rfq/rfq-realtime + * + * @param array{rfqId?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getRfqsRealtime(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/rfq-realtime', $options); + } + + /** + * Get Trade History. + * + * GET /v5/rfq/trade-list + * + * @param array{tradeId?:string, symbol?:string, baseCoin?:string, startTime?:int, endTime?:int, limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTradeHistory(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/rfq/trade-list', $options); + } +} diff --git a/src/RestApi/SpotMarginService.php b/src/RestApi/SpotMarginService.php new file mode 100644 index 0000000..917c15d --- /dev/null +++ b/src/RestApi/SpotMarginService.php @@ -0,0 +1,69 @@ +session->signRequest( + 'GET', + '/v5/spot-margin-trade/interest-rate-history', + array_merge($options, ['currency' => $currency]) + ); + } + + /** + * Get Position Tiers. + * + * GET /v5/spot-margin-trade/position-tiers + * + * @param array{currency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getPositionTiers(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/spot-margin-trade/position-tiers', $options); + } + + /** + * Get Tiered Collateral Ratio. + * + * GET /v5/spot-margin-trade/collateral + * + * @param array{currency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getTieredCollateralRatio(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/spot-margin-trade/collateral', $options); + } + + /** + * Get VIP Margin Data. + * + * GET /v5/spot-margin-trade/data + * + * @param array{vipLevel?:string, currency?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getVipMarginData(array $options = []): array + { + return $this->session->publicRequest('GET', '/v5/spot-margin-trade/data', $options); + } +} diff --git a/src/RestApi/TradeService.php b/src/RestApi/TradeService.php new file mode 100644 index 0000000..9773602 --- /dev/null +++ b/src/RestApi/TradeService.php @@ -0,0 +1,316 @@ +session->signRequest( + 'GET', + '/v5/execution/list', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Amend Order. + * + * POST /v5/order/amend + * + * @param string $category Product type — spot / linear / inverse / option. + * @param string $symbol Symbol name. + * @param array{ + * orderId?:string, + * orderLinkId?:string, + * orderIv?:string, + * triggerPrice?:string, + * qty?:string, + * price?:string, + * tpslMode?:string, + * takeProfit?:string, + * stopLoss?:string, + * tpTriggerBy?:string, + * slTriggerBy?:string, + * triggerBy?:string, + * tpLimitPrice?:string, + * slLimitPrice?:string + * } $options One of orderId / orderLinkId is required. + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/amend-order + */ + public function amendOrder(string $category, string $symbol, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/amend', + array_merge($options, ['category' => $category, 'symbol' => $symbol]) + ); + } + + /** + * Batch Amend Orders. + * + * POST /v5/order/amend-batch + * + * @param string $category Product type + * @param array $request Array of order amend requests + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/batch-amend + */ + public function batchAmendOrders(string $category, array $request, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/amend-batch', + array_merge($options, ['category' => $category, 'request' => $request]) + ); + } + + /** + * Batch Cancel Orders. + * + * POST /v5/order/cancel-batch + * + * @param string $category Product type + * @param array $request Array of order cancel requests + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/batch-cancel + */ + public function batchCancelOrders(string $category, array $request, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/cancel-batch', + array_merge($options, ['category' => $category, 'request' => $request]) + ); + } + + /** + * Batch Create Orders. + * + * POST /v5/order/create-batch + * + * @param string $category Product type + * @param array $request Array of order create requests + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/batch-place + */ + public function batchCreateOrders(string $category, array $request, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/create-batch', + array_merge($options, ['category' => $category, 'request' => $request]) + ); + } + + /** + * Cancel All Orders. + * + * POST /v5/order/cancel-all + * + * @param string $category Product type + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/cancel-all + */ + public function cancelAllOrders(string $category, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/cancel-all', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Cancel Order. + * + * POST /v5/order/cancel + * + * @param string $category Product type — spot / linear / inverse / option. + * @param string $symbol Symbol name. + * @param array{ + * orderId?:string, + * orderLinkId?:string, + * orderFilter?:string + * } $options One of orderId / orderLinkId is required. + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/cancel-order + */ + public function cancelOrder(string $category, string $symbol, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/cancel', + array_merge($options, ['category' => $category, 'symbol' => $symbol]) + ); + } + + /** + * Create Order. + * + * POST /v5/order/create + * + * @param string $category Product type — spot / linear / inverse / option. + * @param string $symbol Symbol name. + * @param string $side "Buy" or "Sell". + * @param string $orderType "Market" or "Limit". + * @param string $qty Order quantity as a string (Bybit rejects scientific notation). + * @param array{ + * isLeverage?:int, + * marketUnit?:string, + * slippageToleranceType?:string, + * slippageTolerance?:string, + * price?:string, + * triggerDirection?:int, + * orderFilter?:string, + * triggerPrice?:string, + * triggerBy?:string, + * orderIv?:string, + * timeInForce?:string, + * positionIdx?:int, + * orderLinkId?:string, + * takeProfit?:string, + * stopLoss?:string, + * tpTriggerBy?:string, + * slTriggerBy?:string, + * reduceOnly?:bool, + * closeOnTrigger?:bool, + * smpType?:string, + * mmp?:bool, + * tpslMode?:string, + * tpLimitPrice?:string, + * slLimitPrice?:string, + * tpOrderType?:string, + * slOrderType?:string + * } $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/create-order + */ + public function createOrder(string $category, string $symbol, string $side, string $orderType, string $qty, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/create', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'side' => $side, 'orderType' => $orderType, 'qty' => $qty]) + ); + } + + /** + * Set DCP Time Window. + * + * POST /v5/order/disconnected-cancel-all + * + * @param int $timeWindow DCP trigger time window in seconds + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/dcp + */ + public function dcpSetTimeWindow(int $timeWindow, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/disconnected-cancel-all', + array_merge($options, ['timeWindow' => $timeWindow]) + ); + } + + /** + * Get Open Orders. + * + * GET /v5/order/realtime + * + * @param string $category Product type + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/open-order + */ + public function getOpenOrders(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/order/realtime', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get Order History. + * + * GET /v5/order/history + * + * @param string $category Product type + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/order-list + */ + public function getOrderHistory(string $category, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/order/history', + array_merge($options, ['category' => $category]) + ); + } + + /** + * Get Spot Borrow Quota. + * + * GET /v5/order/spot-borrow-check + * + * @param string $category Product type + * @param string $symbol Symbol name + * @param string $side Buy or Sell + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/spot-borrow-quota + */ + public function getSpotBorrowQuota(string $category, string $symbol, string $side, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/order/spot-borrow-check', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'side' => $side]) + ); + } + + /** + * Pre-check Order. + * + * POST /v5/order/pre-check + * + * @param string $category Product type + * @param string $symbol Symbol name + * @param string $side Buy or Sell + * @param string $orderType Market or Limit + * @param string $qty Order quantity + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/order/pre-check-order + */ + public function preCheckOrder(string $category, string $symbol, string $side, string $orderType, string $qty, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/order/pre-check', + array_merge($options, ['category' => $category, 'symbol' => $symbol, 'side' => $side, 'orderType' => $orderType, 'qty' => $qty]) + ); + } +} diff --git a/src/RestApi/UserService.php b/src/RestApi/UserService.php new file mode 100644 index 0000000..af4b310 --- /dev/null +++ b/src/RestApi/UserService.php @@ -0,0 +1,297 @@ +session->signRequest( + 'POST', + '/v5/user/create-sub-api', + array_merge($options, ['subuid' => $subuid, 'readOnly' => $readOnly, 'permissions' => $permissions]) + ); + } + + /** + * Create a new sub-account (Sub UID). + * + * POST /v5/user/create-sub-member + * + * @param string $username Sub-account username + * @param int $memberType 1: normal sub-account, 6: custodial sub-account + * @param array{password?:string, switch?:int, isUta?:bool, note?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/create-subuid + */ + public function createSubMember(string $username, int $memberType, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/create-sub-member', + array_merge($options, ['username' => $username, 'memberType' => $memberType]) + ); + } + + /** + * Delete the master account API key. + * + * POST /v5/user/delete-api + * + * @param array{apikey?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/rm-master-apikey + */ + public function deleteApiKey(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/user/delete-api', $options); + } + + /** + * Delete an API key of a sub-account. + * + * POST /v5/user/delete-sub-api + * + * @param int $subuid Sub-account user id + * @param array{apikey?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function deleteSubApiKey(int $subuid, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/delete-sub-api', + array_merge($options, ['subuid' => $subuid]) + ); + } + + /** + * Delete a sub-account. + * + * POST /v5/user/del-submember + * + * @param int $subuid Sub-account user id to be deleted + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function deleteSubMember(int $subuid, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/del-submember', + array_merge($options, ['subuid' => $subuid]) + ); + } + + /** + * Freeze or unfreeze a Sub UID. + * + * POST /v5/user/frozen-sub-member + * + * @param int $subuid Sub-account user id + * @param int $frozen 0: unfreeze, 1: freeze + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/froze-subuid + */ + public function frozenSubMember(int $subuid, int $frozen, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/frozen-sub-member', + array_merge($options, ['subuid' => $subuid, 'frozen' => $frozen]) + ); + } + + /** + * Get Affiliate User Info (extended with coin/business filters). + * + * GET /v5/user/aff-customer-info + * + * @param string $uid The user id of the direct affiliate client + * @param array{coin?:string, business?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/apikey-info + */ + public function getAffiliateCustomOpenInfo(string $uid, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/user/aff-customer-info', + array_merge($options, ['uid' => $uid]) + ); + } + + /** + * Get the account type of the master or specified member accounts. + * + * GET /v5/user/get-member-type + * + * @param array{memberIds?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function getMemberAccountType(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/get-member-type', $options); + } + + /** + * List all API keys of a sub-account. + * + * GET /v5/user/sub-apikeys + * + * @param int $subuid Sub-account user id + * @param array{limit?:int, cursor?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys + */ + public function listSubApiKeys(int $subuid, array $options = []): array + { + return $this->session->signRequest( + 'GET', + '/v5/user/sub-apikeys', + array_merge($options, ['subuid' => $subuid]) + ); + } + + /** + * Get information about the current API key in use. + * + * GET /v5/user/query-api + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/apikey-info + */ + public function queryApiKey(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/query-api', $options); + } + + /** + * Query the escrow (fund management) sub-accounts. + * + * GET /v5/user/escrow_sub_members + * + * @param array{nextCursor?:int, pageSize?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryEscrowSubMembers(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/escrow_sub_members', $options); + } + + /** + * Query referrals invited by the current affiliate account. + * + * GET /v5/user/invitation/referrals + * + * @param array{cursor?:string, size?:int, status?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function queryReferrals(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/invitation/referrals', $options); + } + + /** + * Query the Sub UID list of the master account. + * + * GET /v5/user/query-sub-members + * + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/subuid-list + */ + public function querySubMembers(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/query-sub-members', $options); + } + + /** + * Query the list of sub-accounts under the master account. + * + * GET /v5/user/submembers + * + * @param array{pageSize?:int, nextCursor?:int} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/intro + */ + public function querySubMembersGet(array $options = []): array + { + return $this->session->signRequest('GET', '/v5/user/submembers', $options); + } + + /** + * Sign Agreement — accept the Bybit user agreement for a given category. + * + * POST /v5/user/agreement + * + * @param int $category Agreement category identifier + * @param bool $agree Whether the user agrees to the agreement + * @param array $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/sign-agreement + */ + public function signAgreement(int $category, bool $agree, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/agreement', + array_merge($options, ['category' => $category, 'agree' => $agree]) + ); + } + + /** + * Modify Master API Key — update permissions, IP whitelist, or read-only flag of the master API key. + * + * POST /v5/user/update-api + * + * @param array{readOnly?:int, ips?:string, permissions?:array} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey + */ + public function updateApiKey(array $options = []): array + { + return $this->session->signRequest('POST', '/v5/user/update-api', $options); + } + + /** + * Modify Sub-account API Key — update permissions, IP whitelist, or read-only flag of a sub-account API key. + * + * POST /v5/user/update-sub-api + * + * @param int $subuid Sub-account UID. + * @param int $readOnly 0: read-write, 1: read-only. + * @param array{apikey?:string, ips?:string, permissions?:array, note?:string} $options + * @return array Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + * @see https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey + */ + public function updateSubApiKey(int $subuid, int $readOnly, array $options = []): array + { + return $this->session->signRequest( + 'POST', + '/v5/user/update-sub-api', + array_merge($options, ['subuid' => $subuid, 'readOnly' => $readOnly]) + ); + } +} diff --git a/src/Session.php b/src/Session.php new file mode 100644 index 0000000..2cbe055 --- /dev/null +++ b/src/Session.php @@ -0,0 +1,358 @@ + HTTP verbs that carry params on the query string. */ + private const PAYLOAD_QUERY_METHODS = ['GET', 'DELETE']; + + private Configuration $config; + private ClientInterface $http; + + public function __construct(Configuration $config) + { + $this->config = $config; + $this->http = $config->httpClient ?? new GuzzleClient([ + 'base_uri' => $config->resolvedBaseUrl(), + 'timeout' => $config->timeout, + 'http_errors' => false, + ]); + } + + /** + * Public unsigned endpoint — no X-BAPI-* headers attached. + * + * @param array|null $params Query params for GET/DELETE, body for POST/PUT/PATCH. + * @return BybitEnvelope + */ + public function publicRequest(string $method, string $path, ?array $params = null): array + { + return $this->dispatch($method, $path, $params, false); + } + + /** + * Signed endpoint — X-BAPI-* headers computed via Authentication. + * + * @param array|null $params Query params for GET/DELETE, body for POST/PUT/PATCH. + * @return BybitEnvelope + */ + public function signRequest(string $method, string $path, ?array $params = null): array + { + return $this->dispatch($method, $path, $params, true); + } + + /** + * @param array|null $params + * @return array + */ + private function dispatch(string $method, string $path, ?array $params, bool $signed): array + { + $method = strtoupper($method); + // WireKeys::camelize aliases snake_case caller keys (order_type => orderType) + // BEFORE we canonicalize for signing — Bybit V5 expects camelCase on the + // wire, and the payload signed must match the wire byte-for-byte. + $clean = WireKeys::camelize($this->compact($params)); + $usesQuery = in_array($method, self::PAYLOAD_QUERY_METHODS, true); + + $queryStr = ($clean && $usesQuery) ? $this->sortAndEncode($clean) : ''; + $bodyStr = ($clean && !$usesQuery) ? (json_encode($clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '') : ''; + + $headers = $this->buildHeaders($signed, $usesQuery, $queryStr, $bodyStr); + + $url = $queryStr !== '' ? ($path . '?' . $queryStr) : $path; + + try { + // allow_redirects: false — Bybit V5 never legitimately 3xx-redirects + // an API call. A 302 from a proxy / WAF / maintenance page would + // otherwise cause Guzzle to replay X-BAPI-SIGN + X-BAPI-API-KEY to + // an unintended host. Better to surface the 3xx as a + // ParseException / ClientException in parseResponse. + $options = [ + 'headers' => $headers, + 'http_errors' => false, + 'allow_redirects' => false, + ]; + if ($bodyStr !== '') { + $options['body'] = $bodyStr; + } + $response = $this->http->request($method, $url, $options); + } catch (ConnectException $e) { + $msg = $e->getMessage(); + if (stripos($msg, 'timeout') !== false || stripos($msg, 'timed out') !== false) { + throw new TimeoutException($msg, 0, $e); + } + throw new NetworkException($msg, 0, $e); + } catch (RequestException $e) { + $msg = $e->getMessage(); + if (stripos($msg, 'timeout') !== false || stripos($msg, 'timed out') !== false) { + throw new TimeoutException($msg, 0, $e); + } + throw new TransportException($msg, 0, $e); + } catch (TransferException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } catch (GuzzleException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + return $this->parseResponse($response); + } + + /** + * Deterministic `&`-joined encoding, keys sorted, values URL-escaped. + * Arrays become repeated keys (symbol=BTCUSDT&symbol=ETHUSDT) — this + * matches Bybit V5's flat-list expectation. + * + * @param array $params + */ + private function sortAndEncode(array $params): string + { + $flat = []; + foreach ($params as $k => $v) { + if (is_array($v)) { + // Only flat lists (repeated key form) are supported on Bybit V5 + // GET/DELETE. A nested associative array or a list-of-objects + // would silently mis-encode (inner keys dropped, or literal + // 'Array' emitted) — throw a clear error at the source instead. + if (!array_is_list($v)) { + throw new \InvalidArgumentException(sprintf( + 'Nested associative array under param "%s" is not supported on GET/DELETE — pass a flat list or move the payload to a POST body.', + (string) $k + )); + } + foreach ($v as $inner) { + if (is_array($inner)) { + throw new \InvalidArgumentException(sprintf( + 'List-of-arrays under param "%s" is not supported on GET/DELETE.', + (string) $k + )); + } + $flat[] = [(string) $k, self::scalarToWire($inner)]; + } + } else { + $flat[] = [(string) $k, self::scalarToWire($v)]; + } + } + usort($flat, static function ($a, $b) { + return strcmp($a[0], $b[0]); + }); + $parts = []; + foreach ($flat as [$k, $v]) { + $parts[] = rawurlencode($k) . '=' . rawurlencode($v); + } + return implode('&', $parts); + } + + /** + * Normalize a scalar to its wire-format string: + * - bool → 'true' / 'false' (PHP's default cast gives '1' / '' which + * Bybit either rejects or misinterprets). + * - float → non-scientific decimal, trailing zeros stripped ('1.0e-9' + * becomes '0.000000001'). Bybit rejects E-notation on numeric params. + * - int / string / stringable object → default `(string)` cast. + * - null → empty string (callers strip nulls before this, so unreachable + * in practice, but defensive). + * + * @param mixed $v + */ + private static function scalarToWire($v): string + { + if ($v === null) { + return ''; + } + if (is_bool($v)) { + return $v ? 'true' : 'false'; + } + if (is_float($v)) { + if (!is_finite($v)) { + return (string) $v; // 'INF' / 'NAN' — Bybit will reject; surfaces the bug. + } + // %.14F gives up to 14 fractional digits (double precision) with no E. + $s = rtrim(rtrim(sprintf('%.14F', $v), '0'), '.'); + return $s === '' || $s === '-' ? '0' : $s; + } + return (string) $v; + } + + /** + * @return array + */ + private function buildHeaders(bool $signed, bool $usesQuery, string $queryStr, string $bodyStr): array + { + $h = []; + // Body-carrying verbs (POST/PUT/PATCH) always advertise application/json + // even when the body is empty — matches sibling connectors and future-proofs + // against WAF hardening that rejects unlabeled POST bodies. + if (!$usesQuery) { + $h['Content-Type'] = 'application/json'; + } + if (!$signed) { + return $h; + } + if ($this->config->apiKey === null || $this->config->apiSecret === null) { + throw new ConfigurationException('signed endpoint requires apiKey + apiSecret'); + } + $ts = (string) (int) floor(microtime(true) * 1000); + $payload = $usesQuery ? $queryStr : $bodyStr; + + $h['X-BAPI-API-KEY'] = $this->config->apiKey; + $h['X-BAPI-TIMESTAMP'] = $ts; + $h['X-BAPI-RECV-WINDOW'] = (string) $this->config->recvWindow; + $h['X-BAPI-SIGN'] = Authentication::signV5( + $this->config->apiSecret, + $ts, + $this->config->apiKey, + (string) $this->config->recvWindow, + $payload + ); + $h['X-BAPI-SIGN-TYPE'] = '2'; + return $h; + } + + /** + * @return array + */ + private function parseResponse(ResponseInterface $response): array + { + $status = $response->getStatusCode(); + $raw = (string) $response->getBody(); + $body = null; + if ($raw !== '') { + $decoded = json_decode($raw, true); + if (json_last_error() === JSON_ERROR_NONE) { + $body = $decoded; + } + } + + // P2P endpoints (/v5/p2p/*) return legacy snake_case envelope keys + // (ret_code / ret_msg / time_now / ext_info) — a real spec drift + // vs the V5 standard camelCase (retCode / retMsg / time / retExtInfo). + // Alias into the canonical shape so downstream consumers see the SDK + // contract uniformly. + if (is_array($body) && !isset($body['retCode']) && isset($body['ret_code'])) { + $body = self::normalizeLegacyEnvelope($body); + } + + if (!is_array($body) || !isset($body['retCode']) || !is_int($body['retCode'])) { + $preview = $this->truncateForError($raw); + if ($status >= 500) { + throw new ServerException(sprintf('Bybit server error (status=%d): %s', $status, $preview)); + } + // Non-envelope 4xx (WAF / CDN / edge auth) — route by status so + // catch(AuthException) / catch(RateLimitException) callers still + // get the right class even when the response never reached Bybit's + // application layer. + if ($status === 401 || $status === 403) { + throw new AuthException( + ['retMsg' => sprintf('Bybit auth error (HTTP %d): %s', $status, $preview)], + $status + ); + } + if ($status === 429) { + throw new RateLimitException( + ['retMsg' => sprintf('Bybit rate limit (HTTP %d): %s', $status, $preview)], + $status + ); + } + if ($status >= 400) { + throw new BybitClientException(sprintf('Bybit client error (status=%d): %s', $status, $preview)); + } + throw new ParseException( + sprintf('Response is not a valid Bybit V5 ApiResponse (status=%d): %s', $status, $preview), + $raw, + $status + ); + } + if ($body['retCode'] === 0) { + /** @var array $body */ + return $body; + } + throw ApiException::fromResponse($body, $status); + } + + /** + * Convert a legacy snake_case Bybit envelope (P2P and a handful of other + * older endpoints still emit this shape) to the V5 camelCase standard. + * Preserves original keys so raw callers who care about the wire shape can + * still read them; adds canonical aliases for the SDK contract. + * + * @param array $body + * @return array + */ + private static function normalizeLegacyEnvelope(array $body): array + { + // ret_code / ret_msg → retCode / retMsg (int). + $body['retCode'] = (int) $body['ret_code']; + $body['retMsg'] = (string) ($body['ret_msg'] ?? ''); + + // ext_info → retExtInfo (V5 standard puts extension info here). + if (!isset($body['retExtInfo'])) { + $body['retExtInfo'] = $body['ext_info'] ?? new \stdClass(); + } + + // time_now (seconds w/ microseconds, string) → time (milliseconds int). + if (!isset($body['time']) && isset($body['time_now'])) { + $seconds = (float) $body['time_now']; + $body['time'] = (int) round($seconds * 1000); + } + + return $body; + } + + private function truncateForError(string $raw): string + { + if ($raw === '') { + return '(empty body)'; + } + return strlen($raw) > 2048 ? (substr($raw, 0, 2048) . '…(truncated)') : $raw; + } + + /** + * @param array|null $params + * @return array|null + */ + private function compact(?array $params): ?array + { + if ($params === null) { + return null; + } + $out = []; + foreach ($params as $k => $v) { + if ($v !== null) { + $out[$k] = $v; + } + } + return $out === [] ? null : $out; + } +} diff --git a/src/Util/WireKeys.php b/src/Util/WireKeys.php new file mode 100644 index 0000000..089c968 --- /dev/null +++ b/src/Util/WireKeys.php @@ -0,0 +1,102 @@ + camelCase key translation for wire payloads. + * + * PHP callers naturally supply camelCase array keys matching Bybit's + * spec-native shape, so this helper is a no-op for most services. It exists + * primarily so users who supply snake_case keys (idiomatic in some PHP + * codebases) still get camelCase on the wire. + * + * Recursion: batch endpoints (trade.batchAmendOrders, account.batchSetCollateral, + * etc.) carry list or nested array payloads whose inner keys must ALSO + * be camelized. + */ +final class WireKeys +{ + /** + * PHP-reserved-word aliases: end_ => end on the wire. Callers who avoid + * `end` as a parameter name (PHP does not treat `end` as a keyword but + * we mirror Ruby for consistency) can pass `end_` and get `end` sent. + */ + private const RESERVED_ALIASES = [ + 'end_' => 'end', + 'begin_' => 'begin', + 'class_' => 'class', + 'next_' => 'next', + 'return_' => 'return', + 'do_' => 'do', + 'if_' => 'if', + 'else_' => 'else', + 'list_' => 'list', + 'array_' => 'array', + 'match_' => 'match', + 'fn_' => 'fn', + ]; + + private function __construct() + { + } + + /** + * Convert every key of an associative array from snake_case to camelCase. + * Recurses into nested arrays. List-of-arrays are also handled. + * Non-associative arrays (list) pass through unchanged at the leaf. + * + * @param array|null $input + * @return array|null + */ + public static function camelize(?array $input): ?array + { + if ($input === null) { + return null; + } + $out = []; + foreach ($input as $k => $v) { + $key = is_string($k) ? self::toCamel(self::unalias($k)) : $k; + $out[$key] = self::camelizeValue($v); + } + return $out; + } + + /** + * @param mixed $v + * @return mixed + */ + private static function camelizeValue($v) + { + if (!is_array($v)) { + return $v; + } + // Detect list-of-assoc-array (batch endpoints): recurse into each + // element; keep numeric-list shape by resetting numeric keys. + if (array_is_list($v)) { + $out = []; + foreach ($v as $el) { + $out[] = is_array($el) ? self::camelize($el) : $el; + } + return $out; + } + return self::camelize($v); + } + + private static function unalias(string $key): string + { + return self::RESERVED_ALIASES[$key] ?? $key; + } + + private static function toCamel(string $key): string + { + if (strpos($key, '_') === false) { + return $key; + } + $parts = explode('_', $key); + $first = array_shift($parts); + $tail = implode('', array_map(static fn ($p) => ucfirst($p), array_filter($parts, static fn ($p) => $p !== ''))); + return $first . $tail; + } +} diff --git a/tests/AuthenticationTest.php b/tests/AuthenticationTest.php new file mode 100644 index 0000000..18f0308 --- /dev/null +++ b/tests/AuthenticationTest.php @@ -0,0 +1,81 @@ + + */ +final class AuthenticationTest extends TestCase +{ + private string $apiSecret = 'test-secret'; + private string $apiKey = 'test-key'; + private string $timestamp = '1700000000000'; + private string $recvWindow = '5000'; + + public function testKnownVectorGetQueryString(): void + { + $payload = 'category=spot&symbol=BTCUSDT'; + // sha256_hmac(key='test-secret', + // msg='1700000000000test-key5000category=spot&symbol=BTCUSDT') + $expected = '0048edf42c4979197cec265d4f090ffe6c30d7dec8782e4e6a26b51c2703cbf9'; + $this->assertSame( + $expected, + Authentication::signV5($this->apiSecret, $this->timestamp, $this->apiKey, $this->recvWindow, $payload) + ); + } + + public function testKnownVectorPostBody(): void + { + $payload = '{"category":"linear","symbol":"BTCUSDT"}'; + // sha256_hmac(key='test-secret', + // msg='1700000000000test-key5000{"category":"linear","symbol":"BTCUSDT"}') + $expected = '16378a8ca3caa3c068e2e74ef209dad5c036fec4047c7582ddcfcf13323a8275'; + $this->assertSame( + $expected, + Authentication::signV5($this->apiSecret, $this->timestamp, $this->apiKey, $this->recvWindow, $payload) + ); + } + + public function testKnownVectorEmptyPayload(): void + { + // sha256_hmac(key='test-secret', msg='1700000000000test-key5000') + $expected = 'd8d5e71d8f986368aa5c13405f059ab6adb4f41df59d2f11bb056226b63457d6'; + $this->assertSame( + $expected, + Authentication::signV5($this->apiSecret, $this->timestamp, $this->apiKey, $this->recvWindow, '') + ); + } + + public function testProduces64CharHex(): void + { + $sig = Authentication::signV5($this->apiSecret, $this->timestamp, $this->apiKey, $this->recvWindow, ''); + $this->assertMatchesRegularExpression('/\A[0-9a-f]{64}\z/', $sig); + } + + /** + * Belt-and-suspenders: also verify signV5 agrees with a fresh + * `hash_hmac` call — this catches accidental double-hashing or + * post-processing regressions that the known-vector tests would miss. + */ + public function testMatchesFreshHashHmac(): void + { + $payload = 'a=1&b=2'; + $msg = $this->timestamp . $this->apiKey . $this->recvWindow . $payload; + $this->assertSame( + hash_hmac('sha256', $msg, $this->apiSecret), + Authentication::signV5($this->apiSecret, $this->timestamp, $this->apiKey, $this->recvWindow, $payload) + ); + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..54a8aa0 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,49 @@ +assertNotEmpty(Bybit::VERSION); + } + + public function testBuildsWithDefaults(): void + { + $config = new Configuration(testnet: true); + $client = new Client($config); + $this->assertInstanceOf(Client::class, $client); + } + + public function testToStringRedacts(): void + { + $config = new Configuration(apiKey: 'super-secret-key'); + $client = new Client($config); + $s = (string) $client; + $this->assertStringNotContainsString('super-secret-key', $s); + } + + public function testConfigurationRedactsInDebugInfo(): void + { + $config = new Configuration(apiKey: 'super-secret-key', apiSecret: 'super-secret-value'); + $dump = print_r($config, true); + $this->assertStringNotContainsString('super-secret-key', $dump); + $this->assertStringNotContainsString('super-secret-value', $dump); + } + + public function testReadonlySnapshotFieldsCannotBeMutated(): void + { + $config = new Configuration(testnet: true); + $this->expectException(\Error::class); + // @phpstan-ignore-next-line — deliberately touching a readonly field. + $config->testnet = false; + } +} diff --git a/tests/RestApiSmokeTest.php b/tests/RestApiSmokeTest.php new file mode 100644 index 0000000..9d07a5d --- /dev/null +++ b/tests/RestApiSmokeTest.php @@ -0,0 +1,174 @@ + */ + private static function serviceClasses(): array + { + return [ + \Bybit\RestApi\AccountService::class, + \Bybit\RestApi\AffiliateService::class, + \Bybit\RestApi\AssetService::class, + \Bybit\RestApi\BotService::class, + \Bybit\RestApi\BrokerService::class, + \Bybit\RestApi\CryptoLoanService::class, + \Bybit\RestApi\EarnService::class, + \Bybit\RestApi\MarketService::class, + \Bybit\RestApi\P2pService::class, + \Bybit\RestApi\PositionService::class, + \Bybit\RestApi\RfqService::class, + \Bybit\RestApi\SpotMarginService::class, + \Bybit\RestApi\TradeService::class, + \Bybit\RestApi\UserService::class, + ]; + } + + /** + * @return iterable + */ + public static function serviceMethodProvider(): iterable + { + foreach (self::serviceClasses() as $class) { + $ref = new \ReflectionClass($class); + foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass()->getName() !== $class) { + continue; // skip inherited (BaseService constructor, etc.) + } + if ($method->isConstructor() || $method->isStatic()) { + continue; + } + $shortClass = $ref->getShortName(); + yield "{$shortClass}::{$method->getName()}" => [$class, $method->getName()]; + } + } + } + + /** + * @dataProvider serviceMethodProvider + * @param class-string $serviceClass + */ + public function testEveryServiceMethodDispatchesAv5Request(string $serviceClass, string $methodName): void + { + // 240+ service methods — one shared 200-OK mock queue per test call. + $history = []; + $client = $this->makeClient(new Response(200, ['Content-Type' => 'application/json'], self::OK_ENVELOPE), $history); + + $service = $this->serviceProperty($client, $serviceClass); + $method = new \ReflectionMethod($serviceClass, $methodName); + $args = $this->dummyArgs($method); + + // Some methods might complete via publicRequest without needing keys — + // Configuration below has apiKey/apiSecret set so signed calls also + // succeed. Any exception here is a real bug. + $method->invokeArgs($service, $args); + + $this->assertCount(1, $history, "{$serviceClass}::{$methodName} did not dispatch exactly one request"); + $req = $history[0]['request']; + $verb = $req->getMethod(); + $this->assertContains( + $verb, + self::V5_VERBS, + "{$serviceClass}::{$methodName} used non-V5 verb: {$verb}" + ); + $path = $req->getUri()->getPath(); + $this->assertStringStartsWith( + '/v5/', + $path, + "{$serviceClass}::{$methodName} dispatched to non-/v5/ path: {$path}" + ); + } + + private function makeClient(Response $response, array &$history): Client + { + $mock = new MockHandler([$response]); + $stack = HandlerStack::create($mock); + $stack->push(Middleware::history($history)); + + $config = new Configuration( + apiKey: 'test-key', + apiSecret: 'test-secret', + baseUrl: 'https://api-testnet.bybit.com', + httpClient: new GuzzleClient([ + 'handler' => $stack, + 'base_uri' => 'https://api-testnet.bybit.com', + 'http_errors' => false, + ]), + ); + + return new Client($config); + } + + /** + * Map a service class name to its property on Client (e.g. + * MarketService → $client->market, CryptoLoanService → $client->cryptoLoan). + */ + private function serviceProperty(Client $client, string $serviceClass): object + { + $short = (new \ReflectionClass($serviceClass))->getShortName(); // e.g. "CryptoLoanService" + $trimmed = substr($short, 0, -strlen('Service')); // "CryptoLoan" + $prop = lcfirst($trimmed); // "cryptoLoan" + /** @var object $svc */ + $svc = $client->{$prop}; + return $svc; + } + + /** + * Generate a dummy argument for every declared parameter based on type. + * Bybit service methods use string / int / bool / array — no unions or + * complex objects. + * + * @return list + */ + private function dummyArgs(\ReflectionMethod $method): array + { + $args = []; + foreach ($method->getParameters() as $p) { + if ($p->isOptional()) { + // Fall through to defaults — usually `[] $options`. + break; + } + $type = $p->getType(); + $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : 'string'; + $args[] = match ($typeName) { + 'int' => 1, + 'bool' => false, + 'array' => [], + 'float' => 1.0, + default => 'dummy', + }; + } + return $args; + } +} diff --git a/tests/SessionTest.php b/tests/SessionTest.php new file mode 100644 index 0000000..d71262d --- /dev/null +++ b/tests/SessionTest.php @@ -0,0 +1,612 @@ + $responses + * @param list> $history + */ + private function makeSession(array $responses, array &$history): Session + { + $mock = new MockHandler($responses); + $stack = HandlerStack::create($mock); + $stack->push(Middleware::history($history)); + + $config = new Configuration( + apiKey: 'test-key', + apiSecret: 'test-secret', + recvWindow: '5000', + baseUrl: 'https://api-testnet.bybit.com', + httpClient: new GuzzleClient([ + 'handler' => $stack, + 'base_uri' => 'https://api-testnet.bybit.com', + 'http_errors' => false, + ]), + ); + + return new Session($config); + } + + private function okBody(int $retCode = 0, string $retMsg = 'OK'): string + { + return json_encode([ + 'retCode' => $retCode, + 'retMsg' => $retMsg, + 'result' => new \stdClass(), + 'retExtInfo' => new \stdClass(), + 'time' => 0, + ]) ?: ''; + } + + public function testSignsGetWithBapiHeaders(): void + { + $history = []; + $session = $this->makeSession([new Response(200, ['Content-Type' => 'application/json'], $this->okBody())], $history); + $session->signRequest('GET', '/v5/account/wallet-balance', ['accountType' => 'UNIFIED']); + + $req = $history[0]['request']; + foreach (['X-BAPI-API-KEY', 'X-BAPI-TIMESTAMP', 'X-BAPI-RECV-WINDOW', 'X-BAPI-SIGN', 'X-BAPI-SIGN-TYPE'] as $h) { + $this->assertTrue($req->hasHeader($h), "missing header: $h"); + } + $this->assertStringContainsString('accountType=UNIFIED', (string) $req->getUri()); + } + + public function testSignsPostWithJsonBody(): void + { + $history = []; + $session = $this->makeSession([new Response(200, ['Content-Type' => 'application/json'], $this->okBody())], $history); + $session->signRequest('POST', '/v5/order/create', ['category' => 'linear', 'symbol' => 'BTCUSDT']); + + $req = $history[0]['request']; + $this->assertSame('POST', $req->getMethod()); + $this->assertStringNotContainsString('?', (string) $req->getUri()); + $this->assertSame('application/json', $req->getHeaderLine('Content-Type')); + } + + public function testSignsDeleteWithParams(): void + { + $history = []; + $session = $this->makeSession([new Response(200, ['Content-Type' => 'application/json'], $this->okBody())], $history); + $session->signRequest('DELETE', '/v5/order/cancel', ['orderId' => 'abc123']); + + $req = $history[0]['request']; + $this->assertSame('DELETE', $req->getMethod()); + $this->assertStringContainsString('orderId=abc123', (string) $req->getUri()); + } + + public function testMissingApiKeyThrows(): void + { + $config = new Configuration(); + $session = new Session($config); + $this->expectException(ConfigurationException::class); + $this->expectExceptionMessageMatches('/apiKey/'); + $session->signRequest('GET', '/v5/account/wallet-balance'); + } + + public function testAuthExceptionOnRetCode10004(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody(10004, 'error sign')), + ], $history); + $this->expectException(AuthException::class); + $session->signRequest('GET', '/v5/account/wallet-balance'); + } + + public function testRateLimitExceptionOnRetCode10006(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody(10006, 'too many')), + ], $history); + $this->expectException(RateLimitException::class); + $session->signRequest('GET', '/v5/account/wallet-balance'); + } + + public function testServerExceptionOn5xxNonJson(): void + { + $history = []; + $session = $this->makeSession([ + new Response(502, [], 'Bad Gateway'), + ], $history); + $this->expectException(ServerException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + public function testClientExceptionOn4xxNonJson(): void + { + $history = []; + $session = $this->makeSession([ + new Response(400, [], 'bad request'), + ], $history); + $this->expectException(BybitClientException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + // ── P1: transport-level exception mapping ──────────────────────────────── + // MockHandler accepts Exceptions in the queue — those bubble as if the + // underlying handler failed. This exercises the catch blocks that + // integration tests would otherwise never hit. + + public function testTimeoutExceptionOnCurlTimeout(): void + { + $history = []; + // cURL surfaces read/connect timeouts as ConnectException on Guzzle 7. + // The exact message string is what our dispatch() classifier greps. + $session = $this->makeSession([ + new ConnectException( + 'cURL error 28: Operation timed out after 5000 milliseconds', + new Request('GET', '/v5/market/time') + ), + ], $history); + $this->expectException(TimeoutException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + public function testNetworkExceptionOnConnectRefused(): void + { + $history = []; + $session = $this->makeSession([ + new ConnectException( + 'cURL error 7: Failed to connect to api-testnet.bybit.com port 443: Connection refused', + new Request('GET', '/v5/market/time') + ), + ], $history); + $this->expectException(NetworkException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + public function testTransportExceptionOnGenericTransferError(): void + { + $history = []; + // Non-timeout, non-connect TransferException — a lower-level Guzzle + // fault (e.g. cURL error 60 SSL) that isn't a Connect problem per se. + $session = $this->makeSession([ + new TransferException('cURL error 60: SSL certificate problem'), + ], $history); + $this->expectException(TransportException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + // ── P1: signing-invariant test ─────────────────────────────────────────── + // The single most important property of Session: whatever query string + // shows up on the wire must be the EXACT string that got fed into signV5. + // If sortAndEncode ever drifts from what Guzzle emits, every signed call + // goes 401. + + public function testSignedGetPayloadEqualsWireQuery(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + // Deliberately non-alphabetical input to lock the sort behavior. + $session->signRequest('GET', '/v5/account/wallet-balance', [ + 'symbol' => 'BTCUSDT', + 'accountType' => 'UNIFIED', + 'coin' => 'BTC', + ]); + + $req = $history[0]['request']; + $wireQuery = $req->getUri()->getQuery(); + $signOnWire = $req->getHeaderLine('X-BAPI-SIGN'); + $ts = $req->getHeaderLine('X-BAPI-TIMESTAMP'); + $rw = $req->getHeaderLine('X-BAPI-RECV-WINDOW'); + $apiKey = $req->getHeaderLine('X-BAPI-API-KEY'); + + // Recompute independently using the SAME query bytes the server will see. + $recomputed = Authentication::signV5('test-secret', $ts, $apiKey, $rw, $wireQuery); + $this->assertSame($signOnWire, $recomputed, 'X-BAPI-SIGN diverges from wire-query re-derivation'); + + // Lock the sort: alphabetical by key. + $this->assertSame('accountType=UNIFIED&coin=BTC&symbol=BTCUSDT', $wireQuery); + } + + public function testSignedPostBodyEqualsSignaturePayload(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $session->signRequest('POST', '/v5/order/create', [ + 'category' => 'linear', + 'symbol' => 'BTCUSDT', + 'side' => 'Buy', + ]); + + $req = $history[0]['request']; + $bodyOnWire = (string) $req->getBody(); + $signOnWire = $req->getHeaderLine('X-BAPI-SIGN'); + $ts = $req->getHeaderLine('X-BAPI-TIMESTAMP'); + $rw = $req->getHeaderLine('X-BAPI-RECV-WINDOW'); + $apiKey = $req->getHeaderLine('X-BAPI-API-KEY'); + + $recomputed = Authentication::signV5('test-secret', $ts, $apiKey, $rw, $bodyOnWire); + $this->assertSame($signOnWire, $recomputed, 'POST body diverges from signature payload'); + $this->assertStringNotContainsString('?', (string) $req->getUri(), 'POST must not leak params into query'); + } + + // ── P1: scalar-normalization on the query string ───────────────────────── + + public function testBooleanQueryValueSerializesAsWord(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + // signRequest is fine — we only care about the wire query encoding. + $session->signRequest('GET', '/v5/foo', [ + 'onlyOrderbook' => true, + 'includeArchived' => false, + ]); + + $wireQuery = $history[0]['request']->getUri()->getQuery(); + // 'true' / 'false' string form — Bybit rejects '1' / '' for boolean params. + $this->assertStringContainsString('includeArchived=false', $wireQuery); + $this->assertStringContainsString('onlyOrderbook=true', $wireQuery); + // Guard against a regression to PHP's '(string) false === ""' cast. + $this->assertStringNotContainsString('includeArchived=&', $wireQuery); + $this->assertStringNotContainsString('=1&onlyOrderbook', $wireQuery); + } + + public function testFloatQueryValueAvoidsScientificNotation(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $session->signRequest('GET', '/v5/foo', [ + 'tiny' => 1.0e-9, + 'big' => 12345.6789, + 'zero' => 0.0, + ]); + + $wireQuery = $history[0]['request']->getUri()->getQuery(); + // Must not emit 'E-' / 'e-' in numeric values — Bybit rejects. + $this->assertStringNotContainsString('E-', $wireQuery); + $this->assertStringNotContainsString('e-', $wireQuery); + $this->assertStringContainsString('big=12345.6789', $wireQuery); + // Lock the trailing significant digit — 'tiny=0.00000000' alone would + // pass under a precision regression that drops the final '1'. + $this->assertStringContainsString('tiny=0.000000001', $wireQuery); + $this->assertStringContainsString('zero=0', $wireQuery); + } + + // ── P2P legacy envelope normalization ──────────────────────────────── + // P2P endpoints (/v5/p2p/*) return snake_case fields (ret_code / ret_msg / + // time_now / ext_info) instead of the V5 standard camelCase. Session must + // recognize both shapes and normalize to the canonical form so downstream + // consumers can rely on $r['retCode']. + + public function testP2pSnakeCaseEnvelopeNormalizedOnSuccess(): void + { + $history = []; + $legacyBody = json_encode([ + 'ret_code' => 0, + 'ret_msg' => 'SUCCESS', + 'result' => ['nickName' => 'Test'], + 'ext_code' => '', + 'ext_info' => new \stdClass(), + 'time_now' => '1700000000.123456', + ]) ?: ''; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $legacyBody), + ], $history); + + $r = $session->signRequest('GET', '/v5/p2p/user/personal/info'); + $this->assertSame(0, $r['retCode']); + $this->assertSame('SUCCESS', $r['retMsg']); + // time_now is float-seconds; time is int-milliseconds — 1700000000.123 * 1000 ≈ 1700000000123. + $this->assertSame(1700000000123, $r['time']); + // Original keys preserved for callers who want them. + $this->assertSame(0, $r['ret_code']); + $this->assertSame('SUCCESS', $r['ret_msg']); + } + + public function testP2pSnakeCaseEnvelopeRoutesToApiExceptionOnError(): void + { + $history = []; + $legacyBody = json_encode([ + 'ret_code' => 912000004, + 'ret_msg' => '[912000004] Parameter exception', + 'result' => new \stdClass(), + 'ext_code' => '', + 'ext_info' => new \stdClass(), + 'time_now' => '1700000000.123456', + ]) ?: ''; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $legacyBody), + ], $history); + + $this->expectException(\Bybit\Exception\ApiException::class); + try { + $session->signRequest('GET', '/v5/p2p/item/online'); + } catch (\Bybit\Exception\ApiException $e) { + $this->assertSame(912000004, $e->getRetCode()); + throw $e; + } + } + + // ── P1: redirect-safety guard ──────────────────────────────────────────── + + public function testAutoRedirectsDisabledForSignedCalls(): void + { + $history = []; + $session = $this->makeSession([ + // 302 → Location. Even though target host is HTTPS-legit, we must + // NOT follow it and re-send X-BAPI-SIGN. Session should surface the + // 3xx as a ParseException (or similar transport-shaped error). + new Response(302, ['Location' => 'https://evil.example.com/leak'], ''), + ], $history); + try { + $session->signRequest('GET', '/v5/account/wallet-balance'); + $this->fail('expected an exception for a 3xx response'); + } catch (\Bybit\Exception\BybitException $e) { + // OK — any Bybit-family exception is acceptable here. + } + // Critical assertion: only ONE request was issued (the redirect wasn't + // followed). If Guzzle had followed, MockHandler would have gotten a + // second dequeue and thrown "queue is empty" — but that would raise a + // Guzzle exception, not a bybit-family one. + $this->assertCount(1, $history, 'redirect was followed — X-BAPI-SIGN would leak'); + } + + // ── ApiException dispatch table regression lock ────────────────────────── + // ApiException::fromResponse routes 10 retCodes to typed subclasses. If a + // future edit removes / reshuffles a code, downstream retry/backoff logic + // silently misclassifies — one of the most damaging regressions a signing + // SDK can ship. Iterate every code. + + /** + * @return list}> + */ + public static function apiExceptionDispatchProvider(): array + { + return [ + [-2015, AuthException::class], + [10002, AuthException::class], + [10003, AuthException::class], + [10004, AuthException::class], + [10005, AuthException::class], + [10007, AuthException::class], + [10008, AuthException::class], + [10009, AuthException::class], + [10010, AuthException::class], + [10017, AuthException::class], + [10022, AuthException::class], + [10024, AuthException::class], + [10028, AuthException::class], + [10029, AuthException::class], + [10006, RateLimitException::class], + [10018, RateLimitException::class], + // Unmapped code stays as the base class. + [12345, \Bybit\Exception\ApiException::class], + ]; + } + + /** + * @dataProvider apiExceptionDispatchProvider + * @param class-string<\Bybit\Exception\ApiException> $expected + */ + public function testApiExceptionDispatchTable(int $retCode, string $expected): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody($retCode, 'nope')), + ], $history); + try { + $session->signRequest('GET', '/v5/foo'); + $this->fail("expected {$expected} for retCode {$retCode}"); + } catch (\Bybit\Exception\ApiException $e) { + $this->assertInstanceOf($expected, $e); + $this->assertSame($retCode, $e->getRetCode()); + } + } + + // ── Header-shape locks: format of X-BAPI-TIMESTAMP + X-BAPI-RECV-WINDOW ── + // testSignedGetPayloadEqualsWireQuery re-derives the signature FROM the wire + // headers, so a regression to microtime()/ISO-8601 timestamps would still be + // internally consistent and pass. Assert the SHAPE explicitly. + + public function testTimestampHeaderIsThirteenDigitMsEpoch(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $session->signRequest('GET', '/v5/foo'); + + $req = $history[0]['request']; + $ts = $req->getHeaderLine('X-BAPI-TIMESTAMP'); + $this->assertMatchesRegularExpression('/^\d{13}$/', $ts, 'X-BAPI-TIMESTAMP must be a 13-digit ms-epoch'); + $nowMs = (int) floor(microtime(true) * 1000); + $this->assertLessThan(5000, abs($nowMs - (int) $ts), 'X-BAPI-TIMESTAMP drifted from now by >5s'); + + $this->assertSame('5000', $req->getHeaderLine('X-BAPI-RECV-WINDOW')); + } + + public function testRecvWindowDefaultAppliesWhenNotOverridden(): void + { + // Reach past makeSession() — that helper hardcodes recvWindow='5000', + // which would mask a default-value regression. Build Session directly. + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ]); + $history = []; + $stack = HandlerStack::create($mock); + $stack->push(Middleware::history($history)); + + $config = new Configuration( + apiKey: 'k', + apiSecret: 's', + baseUrl: 'https://api-testnet.bybit.com', + httpClient: new GuzzleClient([ + 'handler' => $stack, + 'base_uri' => 'https://api-testnet.bybit.com', + 'http_errors' => false, + ]), + ); + $session = new Session($config); + $session->signRequest('GET', '/v5/foo'); + + $this->assertSame(\Bybit\Bybit::DEFAULT_RECV_WINDOW, $history[0]['request']->getHeaderLine('X-BAPI-RECV-WINDOW')); + } + + // ── WireKeys wire-in ───────────────────────────────────────────────────── + // Session::dispatch calls WireKeys::camelize on caller params before + // signing / serializing. A caller passing snake_case must see camelCase on + // the wire — else the Bybit V5 server 10001-rejects. + + public function testSnakeCaseCallerKeysAreCamelizedOnWire(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $session->signRequest('POST', '/v5/order/create', [ + 'order_type' => 'Limit', + 'time_in_force' => 'GTC', + 'symbol' => 'BTCUSDT', + ]); + + $req = $history[0]['request']; + $bodyOnWire = (string) $req->getBody(); + $this->assertStringContainsString('"orderType":"Limit"', $bodyOnWire); + $this->assertStringContainsString('"timeInForce":"GTC"', $bodyOnWire); + // Original snake_case must NOT survive to the wire. + $this->assertStringNotContainsString('order_type', $bodyOnWire); + $this->assertStringNotContainsString('time_in_force', $bodyOnWire); + } + + public function testSnakeCaseCallerKeysAreCamelizedOnQueryString(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $session->publicRequest('GET', '/v5/market/kline', [ + 'category' => 'spot', + 'symbol' => 'BTCUSDT', + 'start_time' => 1700000000, + ]); + + $wireQuery = $history[0]['request']->getUri()->getQuery(); + $this->assertStringContainsString('startTime=1700000000', $wireQuery); + $this->assertStringNotContainsString('start_time', $wireQuery); + } + + // ── HTTP-status → exception mapping for non-envelope 4xx ───────────────── + // README documents 401/403 → AuthException, 429 → RateLimitException even + // when the response body is not a Bybit envelope (WAF / CDN / edge auth). + + public function testHttp401NonEnvelopeMapsToAuthException(): void + { + $history = []; + $session = $this->makeSession([ + new Response(401, [], 'Unauthorized'), + ], $history); + $this->expectException(AuthException::class); + $session->signRequest('GET', '/v5/account/wallet-balance'); + } + + public function testHttp403NonEnvelopeMapsToAuthException(): void + { + $history = []; + $session = $this->makeSession([ + new Response(403, [], 'Forbidden'), + ], $history); + $this->expectException(AuthException::class); + $session->signRequest('GET', '/v5/account/wallet-balance'); + } + + public function testHttp429NonEnvelopeMapsToRateLimitException(): void + { + $history = []; + $session = $this->makeSession([ + new Response(429, [], 'Too Many Requests'), + ], $history); + $this->expectException(RateLimitException::class); + $session->publicRequest('GET', '/v5/market/time'); + } + + // ── P2P legacy envelope → AuthException routing ────────────────────────── + // If normalizeLegacyEnvelope / fromResponse ordering regresses, a P2P + // ret_code=10004 would silently downgrade to base ApiException. Lock it. + + // ── sortAndEncode nested-assoc guard ───────────────────────────────────── + // GET/DELETE support only flat scalar + list params. A nested + // associative array or list-of-arrays would previously silently mis-encode + // (dropping inner keys, or emitting literal 'Array'). Session now rejects + // at the source so latent regressions surface immediately. + + public function testNestedAssocArrayOnQueryRejected(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/nested/i'); + $session->publicRequest('GET', '/v5/foo', ['filter' => ['side' => 'Buy']]); + } + + public function testListOfArraysOnQueryRejected(): void + { + $history = []; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $this->okBody()), + ], $history); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/list-of-arrays/i'); + $session->publicRequest('GET', '/v5/foo', ['batch' => [['a' => 1], ['a' => 2]]]); + } + + public function testP2pLegacyEnvelopeRoutesRetCode10004ToAuthException(): void + { + $history = []; + $legacyBody = json_encode([ + 'ret_code' => 10004, + 'ret_msg' => 'error sign', + 'result' => new \stdClass(), + 'ext_info' => new \stdClass(), + 'time_now' => '1700000000.000000', + ]) ?: ''; + $session = $this->makeSession([ + new Response(200, ['Content-Type' => 'application/json'], $legacyBody), + ], $history); + $this->expectException(AuthException::class); + $session->signRequest('POST', '/v5/p2p/order/simplifyList'); + } +} diff --git a/tests/WireKeysTest.php b/tests/WireKeysTest.php new file mode 100644 index 0000000..769f589 --- /dev/null +++ b/tests/WireKeysTest.php @@ -0,0 +1,55 @@ +assertSame( + ['orderType' => 'Limit', 'symbol' => 'BTCUSDT'], + WireKeys::camelize(['order_type' => 'Limit', 'symbol' => 'BTCUSDT']) + ); + } + + public function testRecursesIntoNestedArray(): void + { + $input = ['extra' => ['stake_amount' => '1', 'account_type' => 'UNIFIED']]; + $output = ['extra' => ['stakeAmount' => '1', 'accountType' => 'UNIFIED']]; + $this->assertSame($output, WireKeys::camelize($input)); + } + + public function testRecursesIntoListOfArrays(): void + { + $input = [ + 'request' => [ + ['order_type' => 'Limit', 'symbol' => 'BTCUSDT'], + ['order_type' => 'Market', 'symbol' => 'ETHUSDT'], + ], + ]; + $output = [ + 'request' => [ + ['orderType' => 'Limit', 'symbol' => 'BTCUSDT'], + ['orderType' => 'Market', 'symbol' => 'ETHUSDT'], + ], + ]; + $this->assertSame($output, WireKeys::camelize($input)); + } + + public function testPassesThroughNonHashAndAlreadyCamel(): void + { + $input = ['symbols' => ['BTCUSDT', 'ETHUSDT'], 'count' => 5]; + $this->assertSame($input, WireKeys::camelize($input)); + } + + public function testRewritesReservedAliases(): void + { + $this->assertSame(['end' => 123], WireKeys::camelize(['end_' => 123])); + $this->assertSame(['begin' => 1, 'end' => 2], WireKeys::camelize(['begin_' => 1, 'end_' => 2])); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..06fdcb4 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +