diff --git a/CONNECTOR_ARCHITECTURE_EXPLANATION.md b/CONNECTOR_ARCHITECTURE_EXPLANATION.md
new file mode 100644
index 00000000000..4f9147b1c1f
--- /dev/null
+++ b/CONNECTOR_ARCHITECTURE_EXPLANATION.md
@@ -0,0 +1,570 @@
+# Connector Architecture: Request Flow, Authentication, and Async Handling
+
+This document explains in detail how Hummingbot connectors handle API requests, authentication, throttling, and async operations.
+
+## Table of Contents
+1. [High-Level Architecture](#high-level-architecture)
+2. [Request Execution Flow](#request-execution-flow)
+3. [Authentication Flow](#authentication-flow)
+4. [Throttler Mechanism](#throttler-mechanism)
+5. [Async Task Handling](#async-task-handling)
+6. [Order Management Flow](#order-management-flow)
+7. [Position Management Flow](#position-management-flow)
+
+---
+
+## High-Level Architecture
+
+```mermaid
+graph TB
+ subgraph "Connector Layer"
+ Connector[OrderlyPerpetualDerivative]
+ Connector -->|creates| Factory[WebAssistantsFactory]
+ Connector -->|creates| Throttler[AsyncThrottler]
+ Connector -->|creates| Auth[OrderlyPerpetualAuth]
+ end
+
+ subgraph "Web Assistant Layer"
+ Factory -->|creates| RESTAssistant[RESTAssistant]
+ Factory -->|provides| Throttler
+ Factory -->|provides| Auth
+ RESTAssistant -->|uses| RESTConnection[RESTConnection]
+ RESTConnection -->|uses| AiohttpSession[aiohttp.ClientSession]
+ end
+
+ subgraph "Throttler Layer"
+ Throttler -->|manages| TaskLogs[TaskLog List]
+ Throttler -->|creates| RequestContext[AsyncRequestContext]
+ RequestContext -->|acquires| Lock[asyncio.Lock]
+ end
+
+ subgraph "External API"
+ AiohttpSession -->|HTTP Request| API[Orderly Network API]
+ API -->|HTTP Response| AiohttpSession
+ end
+
+ Connector -->|calls| RESTAssistant
+ RESTAssistant -->|rate limits via| Throttler
+ RESTAssistant -->|authenticates via| Auth
+```
+
+---
+
+## Request Execution Flow
+
+This diagram shows the complete flow from connector method call to API response:
+
+```mermaid
+sequenceDiagram
+ participant Connector as OrderlyPerpetualDerivative
+ participant APIReq as _api_request()
+ participant Factory as WebAssistantsFactory
+ participant RESTAssist as RESTAssistant
+ participant Throttler as AsyncThrottler
+ participant Auth as OrderlyPerpetualAuth
+ participant Conn as RESTConnection
+ participant API as Orderly Network API
+
+ Connector->>APIReq: _place_order() / _update_positions()
+ APIReq->>Factory: get_rest_assistant()
+ Factory->>RESTAssist: create RESTAssistant(connection, throttler, auth)
+ Factory-->>APIReq: RESTAssistant instance
+
+ APIReq->>RESTAssist: execute_request(url, throttler_limit_id, ...)
+
+ Note over RESTAssist: Build RESTRequest object
+ RESTAssist->>RESTAssist: Create RESTRequest(method, url, params, data, headers)
+
+ Note over RESTAssist,Throttler: Rate Limiting Phase
+ RESTAssist->>Throttler: execute_task(limit_id)
+ Throttler->>Throttler: Get rate limit for limit_id
+ Throttler->>Throttler: Create AsyncRequestContext
+ Throttler->>Throttler: acquire() - check capacity
+ alt Within Capacity
+ Throttler->>Throttler: Log task to TaskLogs
+ Throttler-->>RESTAssist: Enter context (proceed)
+ else Over Capacity
+ Throttler->>Throttler: await asyncio.sleep(retry_interval)
+ Throttler->>Throttler: Retry capacity check
+ Throttler-->>RESTAssist: Wait until capacity available
+ end
+
+ Note over RESTAssist,Auth: Authentication Phase
+ RESTAssist->>RESTAssist: _pre_process_request() - apply pre-processors
+ RESTAssist->>Auth: rest_authenticate(request) [if is_auth_required]
+ Auth->>Auth: Generate timestamp
+ Auth->>Auth: Create normalized string (timestamp + method + path + body/params)
+ Auth->>Auth: Sign with ed25519 private key
+ Auth->>Auth: Create auth headers (account-id, key, signature, timestamp)
+ Auth-->>RESTAssist: Request with auth headers
+
+ Note over RESTAssist,API: Network Request Phase
+ RESTAssist->>Conn: call(request)
+ Conn->>API: HTTP Request (aiohttp)
+ API-->>Conn: HTTP Response
+ Conn-->>RESTAssist: RESTResponse
+
+ Note over RESTAssist: Post-processing Phase
+ RESTAssist->>RESTAssist: _post_process_response()
+ RESTAssist-->>APIReq: Response JSON
+ APIReq-->>Connector: Parsed response data
+```
+
+---
+
+## Authentication Flow
+
+Detailed authentication process for Orderly Network (ed25519 signature-based):
+
+```mermaid
+sequenceDiagram
+ participant Connector as Connector Method
+ participant APIReq as _api_request()
+ participant RESTAssist as RESTAssistant
+ participant Auth as OrderlyPerpetualAuth
+ participant PrivateKey as Ed25519PrivateKey
+
+ Connector->>APIReq: _api_request(path, is_auth_required=True)
+ APIReq->>RESTAssist: execute_request(is_auth_required=True)
+
+ RESTAssist->>Auth: rest_authenticate(request)
+
+ Note over Auth: Extract Request Components
+ Auth->>Auth: Extract method (GET/POST/PUT/DELETE)
+ Auth->>Auth: Extract path from URL
+ Auth->>Auth: Get params (query) or data (body)
+
+ Note over Auth: Generate Signature
+ Auth->>Auth: Get current timestamp (milliseconds)
+ alt GET/DELETE Request
+ Auth->>Auth: Build query string from params
+ Auth->>Auth: message = timestamp + method + path + "?" + query_string
+ else POST/PUT Request
+ Auth->>Auth: Use request.data (JSON string) as-is
+ Auth->>Auth: message = timestamp + method + path + json_body
+ end
+
+ Auth->>PrivateKey: sign(message.encode('utf-8'))
+ PrivateKey-->>Auth: signature_bytes
+ Auth->>Auth: base64.encode(signature_bytes)
+
+ Note over Auth: Create Headers
+ Auth->>Auth: headers = {
"orderly-account-id": account_id,
"orderly-key": public_key,
"orderly-signature": base64_signature,
"orderly-timestamp": timestamp
}
+
+ Auth->>RESTAssist: request.headers.update(auth_headers)
+ Auth-->>RESTAssist: Authenticated request
+
+ RESTAssist->>RESTAssist: Send request to API
+```
+
+**Key Authentication Points:**
+- **Signature Format**: `{timestamp}{method}{path}{body_or_query}`
+- **Algorithm**: Ed25519 elliptic curve cryptography
+- **Headers Required**: account-id, public key, signature (base64), timestamp
+- **Timestamp Window**: Orderly validates timestamps within 300 seconds
+
+---
+
+## Throttler Mechanism
+
+How the throttler manages rate limits and task queuing:
+
+```mermaid
+stateDiagram-v2
+ [*] --> RequestReceived: execute_task(limit_id)
+
+ RequestReceived --> GetRateLimit: Get rate limit config
+ GetRateLimit --> CreateContext: Create AsyncRequestContext
+
+ CreateContext --> AcquireLock: Enter async context
+ AcquireLock --> FlushOldTasks: Acquire asyncio.Lock
+
+ FlushOldTasks --> CheckCapacity: Remove expired TaskLogs
+ CheckCapacity --> WithinCapacity: Check if within capacity
+
+ WithinCapacity --> LogTask: Add TaskLog to list
+ LogTask --> ReleaseLock: Release lock
+ ReleaseLock --> ExecuteRequest: Proceed with request
+ ExecuteRequest --> [*]
+
+ CheckCapacity --> OverCapacity: Not within capacity
+ OverCapacity --> ReleaseLock2: Release lock
+ ReleaseLock2 --> Wait: await asyncio.sleep(retry_interval)
+ Wait --> AcquireLock: Retry acquisition
+
+ note right of CheckCapacity
+ Capacity Check:
+ - Count tasks in time window
+ - Check against rate limit
+ - Consider safety margin (5%)
+ - Check related limits
+ end note
+
+ note right of LogTask
+ TaskLog contains:
+ - timestamp
+ - rate_limit reference
+ - weight (consumption)
+ end note
+```
+
+**Throttler Components:**
+
+```mermaid
+classDiagram
+ class AsyncThrottler {
+ -List[RateLimit] _rate_limits
+ -Dict[str, RateLimit] _id_to_limit_map
+ -List[TaskLog] _task_logs
+ -asyncio.Lock _lock
+ -float _retry_interval
+ -float _safety_margin_pct
+ +execute_task(limit_id) AsyncRequestContext
+ +get_related_limits(limit_id) Tuple
+ }
+
+ class AsyncRequestContext {
+ -List[TaskLog] _task_logs
+ -RateLimit _rate_limit
+ -List[Tuple[RateLimit, int]] _related_limits
+ -asyncio.Lock _lock
+ +acquire() async
+ +within_capacity() bool
+ +flush()
+ }
+
+ class RateLimit {
+ +str limit_id
+ +int limit
+ +float time_interval
+ +int weight
+ +List[LinkedLimitWeightPair] linked_limits
+ }
+
+ class TaskLog {
+ +float timestamp
+ +RateLimit rate_limit
+ +int weight
+ }
+
+ AsyncThrottler --> AsyncRequestContext : creates
+ AsyncRequestContext --> TaskLog : logs to
+ AsyncRequestContext --> RateLimit : uses
+ TaskLog --> RateLimit : references
+```
+
+**Rate Limit Example (Orderly Network):**
+```python
+RateLimit(
+ limit_id="/v1/order",
+ limit=100, # 100 requests
+ time_interval=1.0, # per second
+ weight=1, # consumes 1 unit
+ linked_limits=[ # Also consumes from general limit
+ LinkedLimitWeightPair(limit_id="general", weight=1)
+ ]
+)
+```
+
+---
+
+## Async Task Handling
+
+How async operations are coordinated:
+
+```mermaid
+graph TB
+ subgraph "Main Event Loop"
+ EventLoop[asyncio Event Loop]
+ end
+
+ subgraph "Connector Tasks"
+ StatusPolling[_status_polling_loop]
+ OrderUpdate[_update_order_status]
+ BalanceUpdate[_update_balances]
+ PositionUpdate[_update_positions]
+ UserStream[_user_stream_event_listener]
+ end
+
+ subgraph "Request Tasks"
+ Request1[Request 1: Place Order]
+ Request2[Request 2: Get Positions]
+ Request3[Request 3: Cancel Order]
+ end
+
+ subgraph "Throttler Coordination"
+ Throttler[AsyncThrottler]
+ Lock[asyncio.Lock]
+ TaskLogs[(TaskLogs)]
+ end
+
+ EventLoop --> StatusPolling
+ EventLoop --> UserStream
+ EventLoop --> Request1
+ EventLoop --> Request2
+ EventLoop --> Request3
+
+ StatusPolling --> OrderUpdate
+ StatusPolling --> BalanceUpdate
+ StatusPolling --> PositionUpdate
+
+ Request1 --> Throttler
+ Request2 --> Throttler
+ Request3 --> Throttler
+
+ Throttler --> Lock
+ Throttler --> TaskLogs
+
+ Lock -.->|serializes access| TaskLogs
+
+ style Lock fill:#ff9999
+ style TaskLogs fill:#99ff99
+```
+
+**Async Coordination Points:**
+
+1. **Throttler Lock**: `asyncio.Lock` ensures only one task can check/modify TaskLogs at a time
+2. **Context Manager**: `async with throttler.execute_task()` ensures proper acquisition/release
+3. **Non-blocking Wait**: Tasks sleep (`asyncio.sleep`) when over capacity, allowing other tasks to run
+4. **Concurrent Requests**: Multiple requests can be in-flight simultaneously, but throttler ensures rate limits
+
+**Example Async Flow:**
+```python
+# Multiple concurrent requests
+async def place_order():
+ async with throttler.execute_task("/v1/order"): # Acquires capacity
+ response = await rest_assistant.call(request) # Non-blocking HTTP call
+ return response
+
+# These can run concurrently, but throttler ensures rate limits
+task1 = asyncio.create_task(place_order())
+task2 = asyncio.create_task(get_positions())
+task3 = asyncio.create_task(cancel_order())
+
+results = await asyncio.gather(task1, task2, task3)
+```
+
+---
+
+## Order Management Flow
+
+Complete flow for placing and managing orders:
+
+```mermaid
+sequenceDiagram
+ participant Strategy as Trading Strategy
+ participant Connector as OrderlyPerpetualDerivative
+ participant OrderTracker as OrderTracker
+ participant APIReq as _api_request()
+ participant Throttler as AsyncThrottler
+ participant Auth as OrderlyPerpetualAuth
+ participant API as Orderly API
+
+ Strategy->>Connector: place_order(trading_pair, amount, price)
+ Connector->>Connector: Generate client_order_id
+ Connector->>OrderTracker: start_tracking_order(order_id, ...)
+ OrderTracker->>OrderTracker: Create InFlightOrder
+
+ Connector->>Connector: _place_order(order_id, trading_pair, ...)
+ Connector->>Connector: exchange_symbol_associated_to_pair()
+ Connector->>Connector: Build order_params
+
+ Connector->>APIReq: _api_request(CREATE_ORDER_URL, POST, data=order_params, is_auth_required=True)
+
+ Note over APIReq,Throttler: Rate Limiting
+ APIReq->>Throttler: execute_task("/v1/order")
+ Throttler-->>APIReq: Capacity acquired
+
+ Note over APIReq,Auth: Authentication
+ APIReq->>Auth: rest_authenticate(request)
+ Auth-->>APIReq: Request with headers
+
+ APIReq->>API: POST /v1/order
+ API-->>APIReq: {success: true, data: {order_id: "12345"}}
+
+ APIReq-->>Connector: Response with exchange_order_id
+ Connector->>OrderTracker: Update order with exchange_order_id
+
+ Note over Connector: Status Polling Loop
+ loop Every tick
+ Connector->>Connector: _update_order_status()
+ Connector->>APIReq: _api_request(GET_ORDER_URL, is_auth_required=True)
+ APIReq->>API: GET /v1/order/{order_id}
+ API-->>APIReq: Order status (OPEN/FILLED/CANCELLED)
+ APIReq-->>Connector: OrderUpdate
+ Connector->>OrderTracker: process_order_update()
+ OrderTracker->>Strategy: Order status event
+ end
+
+ Note over Connector: WebSocket Updates (Alternative)
+ API->>Connector: WebSocket: executionreport event
+ Connector->>Connector: _process_order_event()
+ Connector->>OrderTracker: process_order_update()
+ OrderTracker->>Strategy: Order status event
+```
+
+**Order Lifecycle States:**
+- `PENDING_CREATE`: Order created locally, waiting for exchange confirmation
+- `OPEN`: Order placed on exchange
+- `PARTIALLY_FILLED`: Order partially executed
+- `FILLED`: Order completely executed
+- `CANCELED`: Order cancelled
+- `FAILED`: Order failed
+
+---
+
+## Position Management Flow
+
+How positions are fetched and updated:
+
+```mermaid
+sequenceDiagram
+ participant Connector as OrderlyPerpetualDerivative
+ participant PerpetualTrading as PerpetualTrading
+ participant APIReq as _api_request()
+ participant Throttler as AsyncThrottler
+ participant Auth as OrderlyPerpetualAuth
+ participant API as Orderly API
+
+ Note over Connector: Status Polling Loop
+ loop Every tick
+ Connector->>Connector: _status_polling_loop_fetch_updates()
+ Connector->>Connector: _update_positions()
+
+ Connector->>APIReq: _api_request(POSITIONS_URL, GET, is_auth_required=True)
+
+ Note over APIReq,Throttler: Rate Limiting
+ APIReq->>Throttler: execute_task("/v1/positions")
+ Throttler-->>APIReq: Capacity acquired
+
+ Note over APIReq,Auth: Authentication
+ APIReq->>Auth: rest_authenticate(request)
+ Auth-->>APIReq: Request with headers
+
+ APIReq->>API: GET /v1/positions
+ API-->>APIReq: {success: true, data: {rows: [{symbol, position_qty, ...}]}}
+
+ APIReq-->>Connector: Positions data
+
+ loop For each position
+ Connector->>Connector: Parse position data
+ Connector->>Connector: trading_pair_associated_to_exchange_symbol()
+ Connector->>Connector: Calculate position_side (LONG/SHORT)
+
+ alt Position exists
+ Connector->>PerpetualTrading: get_position(trading_pair, side)
+ Connector->>PerpetualTrading: update_position(...)
+ else New position
+ Connector->>PerpetualTrading: set_position(pos_key, Position(...))
+ end
+
+ alt Position qty == 0
+ Connector->>PerpetualTrading: remove_position(pos_key)
+ end
+ end
+ end
+
+ Note over Connector: WebSocket Updates (Alternative)
+ API->>Connector: WebSocket: position event
+ Connector->>Connector: _process_position_event()
+ Connector->>Connector: _update_positions()
+ Connector->>PerpetualTrading: Update positions
+```
+
+**Position Update Process:**
+
+1. **Fetch Positions**: GET `/v1/positions` returns all positions
+2. **Parse Response**: Extract symbol, quantity, entry price, unrealized PnL
+3. **Map Symbols**: Convert exchange symbol (PERP_BTC_USDC) to trading pair (BTC-USDC)
+4. **Determine Side**: LONG if quantity > 0, SHORT if quantity < 0
+5. **Update State**: Update existing position or create new one
+6. **Remove Zero Positions**: If quantity == 0, remove from tracking
+
+---
+
+## Key Components Summary
+
+### 1. **Connector** (`OrderlyPerpetualDerivative`)
+- Inherits from `PerpetualDerivativePyBase`
+- Manages trading pairs, orders, positions
+- Creates `WebAssistantsFactory` with throttler and auth
+- Implements order placement, cancellation, status updates
+- Implements position fetching and updates
+
+### 2. **WebAssistantsFactory**
+- Factory pattern for creating REST/WS assistants
+- Injects throttler, auth, and pre/post processors
+- Singleton pattern for connection management
+
+### 3. **RESTAssistant**
+- Wraps REST connection with throttling and auth
+- Applies pre-processors (headers, time sync)
+- Applies post-processors (error handling, parsing)
+- Manages request lifecycle
+
+### 4. **AsyncThrottler**
+- Manages rate limits per endpoint
+- Tracks task execution in time windows
+- Uses `asyncio.Lock` for thread-safe access
+- Implements async context manager for capacity acquisition
+
+### 5. **OrderlyPerpetualAuth**
+- Implements `AuthBase` interface
+- Generates ed25519 signatures
+- Creates normalized strings for signing
+- Adds authentication headers to requests
+
+### 6. **RESTConnection**
+- Low-level HTTP client wrapper
+- Uses `aiohttp.ClientSession`
+- Handles actual network I/O
+
+---
+
+## Async Patterns Used
+
+1. **Async Context Managers**: `async with throttler.execute_task()` ensures proper resource management
+2. **Async Locks**: `asyncio.Lock` prevents race conditions in throttler
+3. **Async Sleep**: Non-blocking waits when over capacity
+4. **Task Gathering**: `asyncio.gather()` for concurrent operations
+5. **Event Loops**: Status polling runs in background tasks
+6. **WebSocket Streams**: Async iterators for real-time updates
+
+---
+
+## Rate Limiting Strategy
+
+The throttler uses a **sliding window** approach:
+
+1. **Task Logs**: Each request logs its execution time and weight
+2. **Capacity Check**: Before executing, count tasks within time window
+3. **Wait if Over**: If over capacity, wait and retry
+4. **Flush Old**: Remove expired task logs periodically
+5. **Safety Margin**: Apply 5% safety margin to prevent exceeding limits
+
+**Example:**
+- Rate limit: 100 requests/second
+- Safety margin: 5%
+- Effective limit: 95 requests/second
+- If 95 tasks executed in last second, next request waits
+
+---
+
+## Error Handling
+
+1. **Network Errors**: Retried by connection layer
+2. **Rate Limit Errors**: Handled by throttler (waits and retries)
+3. **Auth Errors**: Propagated to connector (invalid credentials)
+4. **API Errors**: Parsed and raised as `IOError` with details
+5. **Timeout Errors**: Handled by `wait_for()` with timeout parameter
+
+---
+
+This architecture ensures:
+- ✅ **Rate Limit Compliance**: Never exceeds exchange limits
+- ✅ **Secure Authentication**: Ed25519 signatures for all requests
+- ✅ **Concurrent Operations**: Multiple requests can be in-flight
+- ✅ **Efficient Resource Usage**: Shared connections and throttler
+- ✅ **Real-time Updates**: WebSocket + polling for order/position updates
+- ✅ **Error Resilience**: Proper error handling and retries
+
diff --git a/conf/scripts/Kodiak_btc.yml b/conf/scripts/Kodiak_btc.yml
new file mode 100644
index 00000000000..b2f39045403
--- /dev/null
+++ b/conf/scripts/Kodiak_btc.yml
@@ -0,0 +1,10 @@
+exchange: orderly_perpetual
+trading_pair: BTC-USDC
+order_amount_quote: [20]
+bid_spread_levels: [0.0005]
+ask_spread_levels: [0.0005]
+order_refresh_time: 10
+max_inventory: 0.002 # 200 usd
+max_price_adjustment: 0.001 # 10 bps
+order_cooldown: 10 # 10 seconds
+leverage: 100
diff --git a/conf/scripts/Kodiak_eth.yml b/conf/scripts/Kodiak_eth.yml
new file mode 100644
index 00000000000..2684ec0d0a4
--- /dev/null
+++ b/conf/scripts/Kodiak_eth.yml
@@ -0,0 +1,9 @@
+exchange: orderly_perpetual
+trading_pair: ETH-USDC
+order_amount_quote: 150
+bid_spread_levels: [0.0003]
+ask_spread_levels: [0.0003]
+order_refresh_time: 5
+max_inventory: 1
+max_price_adjustment: 0.0012
+leverage: 100
diff --git a/conf/scripts/pmm_kodiak_example.yml b/conf/scripts/pmm_kodiak_example.yml
new file mode 100644
index 00000000000..9b4fdda1d47
--- /dev/null
+++ b/conf/scripts/pmm_kodiak_example.yml
@@ -0,0 +1,10 @@
+exchange: orderly_perpetual
+trading_pair: BTC-USDC
+order_amount_quote: [20]
+order_cooldown: 10 # 10 seconds
+bid_spread_levels: [0.0005]
+ask_spread_levels: [0.0005]
+order_refresh_time: 10
+max_inventory: 0.002 # 200 usd
+max_price_adjustment: 0.001 # 10 bps
+leverage: 100
diff --git a/hummingbot/connector/derivative/orderly_perpetual/__init__.py b/hummingbot/connector/derivative/orderly_perpetual/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/hummingbot/connector/derivative/orderly_perpetual/dummy.pxd b/hummingbot/connector/derivative/orderly_perpetual/dummy.pxd
new file mode 100644
index 00000000000..8620ffaff4f
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/dummy.pxd
@@ -0,0 +1 @@
+# Dummy Cython header file for build system compatibility
diff --git a/hummingbot/connector/derivative/orderly_perpetual/dummy.pyx b/hummingbot/connector/derivative/orderly_perpetual/dummy.pyx
new file mode 100644
index 00000000000..1bf1fc0a336
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/dummy.pyx
@@ -0,0 +1 @@
+# Dummy Cython file for build system compatibility
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_api_order_book_data_source.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_api_order_book_data_source.py
new file mode 100644
index 00000000000..f1da387daee
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_api_order_book_data_source.py
@@ -0,0 +1,921 @@
+"""
+Order Book Data Source for Orderly Network Perpetual Connector
+
+This module handles:
+- Trading rules retrieval
+- Order book snapshots and updates via REST and WebSocket
+- Public trade data streaming
+- Funding rate information
+- Last traded prices
+
+Reference: Orderly Network EVM API
+https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/public
+https://orderly.network/docs/build-on-omnichain/evm-api/websocket-api/public
+"""
+
+import asyncio
+import time
+from collections import defaultdict
+from decimal import Decimal
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
+
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_constants as CONSTANTS
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_web_utils as web_utils
+from hummingbot.connector.trading_rule import TradingRule
+from hummingbot.core.data_type.common import TradeType
+from hummingbot.core.data_type.funding_info import FundingInfo, FundingInfoUpdate
+from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType
+from hummingbot.core.data_type.perpetual_api_order_book_data_source import PerpetualAPIOrderBookDataSource
+from hummingbot.core.web_assistant.connections.data_types import RESTMethod, WSJSONRequest
+from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
+from hummingbot.core.web_assistant.ws_assistant import WSAssistant
+from hummingbot.logger import HummingbotLogger
+
+if TYPE_CHECKING:
+ from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_derivative import (
+ OrderlyPerpetualDerivative,
+ )
+
+
+class OrderlyPerpetualAPIOrderBookDataSource(PerpetualAPIOrderBookDataSource):
+ """
+ Order book data source for Orderly Network Perpetual.
+
+ Fetches and maintains:
+ - Trading rules
+ - Order book snapshots and updates
+ - Public trades
+ - Funding rates
+ - Last traded prices
+ """
+
+ _logger: Optional[HummingbotLogger] = None
+ _trading_pair_symbol_map: Dict[str, Dict[str, str]] = {}
+ _mapping_initialization_lock = asyncio.Lock()
+
+ def __init__(
+ self,
+ trading_pairs: List[str],
+ connector: 'OrderlyPerpetualDerivative',
+ api_factory: WebAssistantsFactory,
+ domain: str = CONSTANTS.DOMAIN
+ ):
+ """
+ Initialize the order book data source.
+
+ Args:
+ trading_pairs: List of trading pairs to track
+ connector: Reference to the main connector instance
+ api_factory: Factory for creating REST/WS assistants
+ domain: Domain identifier (mainnet or testnet)
+ """
+ super().__init__(trading_pairs)
+ self._connector = connector
+ self._api_factory = api_factory
+ self._domain = domain
+ self._trading_pairs: List[str] = trading_pairs
+ self._message_queue: Dict[str, asyncio.Queue] = defaultdict(asyncio.Queue)
+ self._snapshot_messages_queue_key = "order_book_snapshot"
+ self._mark_price_messages_queue_key = "mark_price"
+
+ @property
+ def trading_rules_request_path(self) -> str:
+ """
+ Return the REST API path for trading rules.
+
+ Returns:
+ API endpoint path for futures/perpetual info
+ """
+ return CONSTANTS.EXCHANGE_INFO_URL
+
+ async def get_last_traded_prices(
+ self,
+ trading_pairs: List[str],
+ domain: Optional[str] = None
+ ) -> Dict[str, float]:
+ """
+ Fetch last traded prices for given trading pairs.
+
+ Args:
+ trading_pairs: List of trading pairs
+ domain: Domain identifier (unused, kept for interface compatibility)
+
+ Returns:
+ Dictionary mapping trading_pair to last traded price
+ """
+ return await self._connector.get_last_traded_prices(trading_pairs=trading_pairs)
+
+ async def get_funding_info(self, trading_pair: str) -> FundingInfo:
+ """
+ Fetch funding rate information for a trading pair.
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format (e.g., "BTC-USDC")
+
+ Returns:
+ FundingInfo object with current rates and timestamps
+ """
+ orderly_symbol = await self._connector.exchange_symbol_associated_to_pair(
+ trading_pair=trading_pair
+ )
+
+ # Fetch funding rate
+ funding_url = web_utils.public_rest_url(
+ CONSTANTS.FUNDING_RATE_URL.format(symbol=orderly_symbol),
+ domain=self._domain
+ )
+
+ # Fetch market info for index and mark prices
+ market_info_url = web_utils.public_rest_url(
+ CONSTANTS.SYMBOL_INFO_URL.format(symbol=orderly_symbol),
+ domain=self._domain
+ )
+
+ # Execute both requests in parallel
+ rest_assistant = await self._api_factory.get_rest_assistant()
+
+ funding_response, market_response = await asyncio.gather(
+ rest_assistant.execute_request(
+ url=funding_url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.FUNDING_RATE_URL,
+ ),
+ rest_assistant.execute_request(
+ url=market_info_url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.SYMBOL_INFO_URL,
+ ),
+ )
+
+ # Parse funding response
+ if not funding_response.get("success", False):
+ raise IOError(f"Failed to fetch funding info for {trading_pair}: {funding_response}")
+
+ funding_data = funding_response.get("data", {})
+
+ # Parse market info response for prices
+ index_price = Decimal("0")
+ mark_price = Decimal("0")
+
+ if market_response.get("success", False):
+ market_data = market_response.get("data", {})
+ index_price = Decimal(str(market_data.get("index_price", 0)))
+ mark_price = Decimal(str(market_data.get("mark_price", 0)))
+
+ funding_info = FundingInfo(
+ trading_pair=trading_pair,
+ index_price=index_price,
+ mark_price=mark_price,
+ next_funding_utc_timestamp=int(funding_data.get("next_funding_time", 0)) / 1000,
+ rate=Decimal(str(funding_data.get("est_funding_rate", 0))),
+ )
+
+ return funding_info
+
+ async def listen_for_funding_info(self, output: asyncio.Queue):
+ """
+ Poll for funding rate updates and push to output queue.
+
+ Orderly doesn't provide WebSocket funding updates, so we poll periodically.
+
+ Args:
+ output: Queue to push FundingInfoUpdate messages to
+ """
+ while True:
+ try:
+ for trading_pair in self._trading_pairs:
+ try:
+ funding_info = await self.get_funding_info(trading_pair)
+ funding_info_update = FundingInfoUpdate(
+ trading_pair=trading_pair,
+ index_price=funding_info.index_price,
+ mark_price=funding_info.mark_price,
+ next_funding_utc_timestamp=funding_info.next_funding_utc_timestamp,
+ rate=funding_info.rate,
+ )
+ output.put_nowait(funding_info_update)
+ except Exception as e:
+ self.logger().error(
+ f"Error fetching funding info for {trading_pair}: {e}",
+ exc_info=True
+ )
+
+ await self._sleep(CONSTANTS.FUNDING_RATE_UPDATE_INTERVAL_SECOND)
+
+ except asyncio.CancelledError:
+ raise
+ except Exception:
+ self.logger().exception(
+ "Unexpected error when processing public funding info updates from exchange"
+ )
+ await self._sleep(CONSTANTS.FUNDING_RATE_UPDATE_INTERVAL_SECOND)
+
+ async def listen_for_mark_price(self, output: asyncio.Queue):
+ """
+ Listen for mark price updates from WebSocket and push to funding info stream.
+ This is separate from funding rate polling and processes mark price updates independently.
+
+ Args:
+ output: Queue to push FundingInfoUpdate messages to
+ """
+ message_queue = self._message_queue[self._mark_price_messages_queue_key]
+ while True:
+ try:
+ mark_price_message = await message_queue.get()
+ await self._parse_mark_price_message(
+ raw_message=mark_price_message,
+ message_queue=output
+ )
+ except asyncio.CancelledError:
+ raise
+ except Exception:
+ self.logger().exception(
+ "Unexpected error when processing mark price updates from WebSocket"
+ )
+
+ async def _request_complete_trading_rules(self) -> List[Dict[str, Any]]:
+ """
+ Fetch complete trading rules from the exchange.
+
+ First fetches list of symbols from /v1/public/futures, then fetches
+ detailed trading rules for each symbol from /v1/public/info/{symbol}.
+
+ Returns:
+ List of trading rule dictionaries
+ """
+ # First, get list of all available symbols
+ symbols_url = web_utils.public_rest_url(
+ CONSTANTS.EXCHANGE_INFO_URL,
+ domain=self._domain
+ )
+
+ rest_assistant = await self._api_factory.get_rest_assistant()
+ symbols_response = await rest_assistant.execute_request(
+ url=symbols_url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.EXCHANGE_INFO_URL,
+ )
+
+ if not symbols_response.get("success", False):
+ raise IOError(f"Failed to fetch symbol list: {symbols_response}")
+
+ symbols_data = symbols_response.get("data", {})
+ symbols_list = symbols_data.get("rows", [])
+
+ # Now fetch trading rules for each symbol
+ trading_rules = []
+
+ # For efficiency, we can use /v1/public/info to get all rules at once
+ trading_rules_url = web_utils.public_rest_url(
+ CONSTANTS.TRADING_RULES_URL,
+ domain=self._domain
+ )
+
+ rules_response = await rest_assistant.execute_request(
+ url=trading_rules_url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.TRADING_RULES_URL,
+ )
+
+ if not rules_response.get("success", False):
+ # If batch request fails, fall back to individual requests
+ self.logger().warning(
+ f"Batch trading rules request failed: {rules_response}. "
+ f"Falling back to individual requests."
+ )
+
+ # Fetch rules for each symbol individually
+ tasks = []
+ for symbol_data in symbols_list:
+ symbol = symbol_data.get("symbol")
+ if not symbol:
+ continue
+
+ url = web_utils.public_rest_url(
+ CONSTANTS.TRADING_RULE_URL.format(symbol=symbol),
+ domain=self._domain
+ )
+
+ tasks.append(
+ rest_assistant.execute_request(
+ url=url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.TRADING_RULE_URL,
+ )
+ )
+
+ responses = await asyncio.gather(*tasks, return_exceptions=True)
+
+ for response in responses:
+ if isinstance(response, Exception):
+ self.logger().error(f"Failed to fetch trading rule: {response}")
+ continue
+
+ if response.get("success", False):
+ trading_rules.append(response.get("data", {}))
+
+ else:
+ # Successfully fetched all rules at once
+ rules_data = rules_response.get("data", {})
+ trading_rules = rules_data.get("rows", [])
+
+ return trading_rules
+
+ async def _format_trading_rules(
+ self,
+ exchange_info_dict: List[Dict[str, Any]]
+ ) -> Dict[str, TradingRule]:
+ """
+ Parse raw trading rules into TradingRule objects.
+
+ Args:
+ exchange_info_dict: List of raw trading rule dictionaries
+
+ Returns:
+ Dictionary mapping trading_pair to TradingRule
+ """
+ trading_rules = {}
+
+ for rule_data in exchange_info_dict:
+ try:
+ # Skip invalid entries
+ if not web_utils.is_exchange_information_valid(rule_data):
+ continue
+
+ orderly_symbol = rule_data["symbol"]
+
+ # Convert to Hummingbot trading pair format
+ trading_pair = web_utils.format_trading_pair(orderly_symbol)
+
+ # Parse trading rule parameters
+ trading_rule = TradingRule(
+ trading_pair=trading_pair,
+ min_order_size=Decimal(str(rule_data.get("base_min", "0"))),
+ max_order_size=Decimal(str(rule_data.get("base_max", "1000000"))),
+ min_price_increment=Decimal(str(rule_data.get("quote_tick", "0.01"))),
+ min_base_amount_increment=Decimal(str(rule_data.get("base_tick", "0.01"))),
+ min_notional_size=Decimal(str(rule_data.get("min_notional", "0"))),
+ supports_limit_orders=True,
+ supports_market_orders=True,
+ )
+
+ trading_rules[trading_pair] = trading_rule
+
+ except Exception as e:
+ self.logger().error(
+ f"Error parsing trading rule for {rule_data.get('symbol', 'unknown')}: {e}",
+ exc_info=True
+ )
+
+ return trading_rules
+
+ async def _request_order_book_snapshot(self, trading_pair: str) -> Dict[str, Any]:
+ """
+ Request order book snapshot via REST API.
+
+ NOTE: This is a PRIVATE endpoint that requires authentication.
+ According to official Orderly SDK (_market.py:228), /v1/orderbook/{symbol} uses _sign_request().
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format
+
+ Returns:
+ Raw order book snapshot data
+ """
+ orderly_symbol = await self._connector.exchange_symbol_associated_to_pair(
+ trading_pair=trading_pair
+ )
+ # Use private_rest_url since this endpoint requires authentication
+ url = web_utils.private_rest_url(
+ CONSTANTS.ORDERBOOK_SNAPSHOT_URL.format(symbol=orderly_symbol),
+ domain=self._domain
+ )
+
+ rest_assistant = await self._api_factory.get_rest_assistant()
+
+ # Create authenticated request
+ from hummingbot.core.web_assistant.connections.data_types import RESTRequest, RESTMethod
+ request = RESTRequest(
+ method=RESTMethod.GET,
+ url=url,
+ is_auth_required=True, # This endpoint requires authentication
+ throttler_limit_id=CONSTANTS.ORDERBOOK_SNAPSHOT_URL,
+ )
+
+ response = await rest_assistant.call(request=request)
+ response_json = await response.json()
+
+ # Expected response:
+ # {
+ # "success": true,
+ # "data": {
+ # "symbol": "PERP_BTC_USDC",
+ # "asks": [[50000.0, 1.5], [50100.0, 2.0]],
+ # "bids": [[49900.0, 1.2], [49800.0, 0.8]],
+ # "timestamp": 1698765432000
+ # }
+ # }
+
+ if not response_json.get("success", False):
+ raise IOError(f"Failed to fetch order book snapshot for {trading_pair}: {response_json}")
+
+ return response_json.get("data", {})
+
+ async def _order_book_snapshot(self, trading_pair: str) -> OrderBookMessage:
+ """
+ Fetch and parse order book snapshot into OrderBookMessage.
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format
+
+ Returns:
+ OrderBookMessage of type SNAPSHOT
+ """
+ snapshot_data = await self._request_order_book_snapshot(trading_pair)
+
+ timestamp = snapshot_data.get("timestamp", int(time.time() * 1000))
+
+ # Convert timestamp from milliseconds to seconds
+ timestamp_seconds = timestamp / 1000 if timestamp > 1e10 else timestamp
+
+ # Transform REST API orderbook format to array format
+ # REST API returns: [{"price": 10669.4, "quantity": 1.56}, ...]
+ # Hummingbot expects: [[10669.4, 1.56], ...]
+ raw_bids = snapshot_data.get("bids", [])
+ raw_asks = snapshot_data.get("asks", [])
+
+ bids = [[float(bid["price"]), float(bid["quantity"])] for bid in raw_bids]
+ asks = [[float(ask["price"]), float(ask["quantity"])] for ask in raw_asks]
+
+ snapshot_msg = OrderBookMessage(
+ OrderBookMessageType.SNAPSHOT,
+ {
+ "trading_pair": trading_pair,
+ "update_id": timestamp,
+ "bids": bids,
+ "asks": asks,
+ },
+ timestamp=timestamp_seconds
+ )
+
+ return snapshot_msg
+
+ async def _connected_websocket_assistant(self) -> WSAssistant:
+ """
+ Create and connect a WebSocket assistant.
+
+ Returns:
+ Connected WSAssistant instance
+ """
+ ws_url = web_utils.wss_url("public", self._domain, self._connector.authenticator.account_id) # account_id is a mandatory parameter for public WebSocket URL
+ self.logger().info(f"[WEBSOCKET] Attempting to connect to public WebSocket: {ws_url}")
+
+ try:
+ ws: WSAssistant = await self._api_factory.get_ws_assistant()
+ self.logger().debug(f"[WEBSOCKET] WSAssistant created, connecting to: {ws_url}")
+
+ await ws.connect(
+ ws_url=ws_url,
+ ping_timeout=CONSTANTS.HEARTBEAT_TIME_INTERVAL
+ )
+
+ self.logger().info(f"[WEBSOCKET] Successfully connected to public WebSocket: {ws_url}")
+ return ws
+
+ except Exception as e:
+ self.logger().error(
+ f"[WEBSOCKET] Failed to connect to public WebSocket: {ws_url}. "
+ f"Error type: {type(e).__name__}, Error: {str(e)}",
+ exc_info=True
+ )
+ raise
+
+ async def _subscribe_channels(self, ws: WSAssistant):
+ """
+ Subscribe to order book and trade channels via WebSocket.
+
+ Orderly WebSocket format:
+ {
+ "id": "unique-client-id",
+ "event": "subscribe",
+ "topic": "{symbol}@orderbook"
+ }
+
+ Args:
+ ws: Connected WebSocket assistant
+ """
+ try:
+ for trading_pair in self._trading_pairs:
+ orderly_symbol = await self._connector.exchange_symbol_associated_to_pair(
+ trading_pair=trading_pair
+ )
+
+ self.logger().debug(
+ f"[WEBSOCKET SUBSCRIBE] Subscribing to channels for trading pair: "
+ f"'{trading_pair}' (Orderly symbol: '{orderly_symbol}')"
+ )
+
+ # Subscribe to orderbook channel
+ # Orderly format: {symbol}@orderbook (e.g., "PERP_BTC_USDC@orderbook")
+ orderbook_payload = {
+ "id": f"{orderly_symbol}_orderbook",
+ "event": "subscribe",
+ "topic": f"{orderly_symbol}@{CONSTANTS.WS_ORDERBOOK_CHANNEL}"
+ }
+ subscribe_orderbook_request = WSJSONRequest(payload=orderbook_payload)
+
+ # Subscribe to trades channel
+ # Orderly format: {symbol}@trade (e.g., "PERP_BTC_USDC@trade")
+ trades_payload = {
+ "id": f"{orderly_symbol}_trade",
+ "event": "subscribe",
+ "topic": f"{orderly_symbol}@{CONSTANTS.WS_TRADES_CHANNEL}"
+ }
+ subscribe_trade_request = WSJSONRequest(payload=trades_payload)
+
+ # Subscribe to mark price channel
+ # Orderly format: {symbol}@markprice (e.g., "PERP_BTC_USDC@markprice")
+ mark_price_payload = {
+ "id": f"{orderly_symbol}_markprice",
+ "event": "subscribe",
+ "topic": f"{orderly_symbol}@{CONSTANTS.WS_MARKPRICE_CHANNEL}"
+ }
+ subscribe_mark_price_request = WSJSONRequest(payload=mark_price_payload)
+
+ self.logger().debug(
+ f"[WEBSOCKET SUBSCRIBE] Sending orderbook subscription: {orderbook_payload}"
+ )
+ await ws.send(subscribe_orderbook_request)
+
+ self.logger().debug(
+ f"[WEBSOCKET SUBSCRIBE] Sending trades subscription: {trades_payload}"
+ )
+ await ws.send(subscribe_trade_request)
+
+ self.logger().debug(
+ f"[WEBSOCKET SUBSCRIBE] Sending mark price subscription: {mark_price_payload}"
+ )
+ await ws.send(subscribe_mark_price_request)
+
+ self.logger().info(
+ f"[WEBSOCKET SUBSCRIBE] Subscribed to public order book, trade, and mark price channels for {trading_pair}"
+ )
+
+ except asyncio.CancelledError:
+ raise
+ except Exception as e:
+ self.logger().error(
+ f"[WEBSOCKET SUBSCRIBE] Unexpected error occurred subscribing to order book data streams. "
+ f"Error type: {type(e).__name__}, Error: {str(e)}",
+ exc_info=True
+ )
+ raise
+
+ def _channel_originating_message(self, event_message: Dict[str, Any]) -> str:
+ """
+ Identify the channel/queue for a WebSocket message.
+
+ Orderly message format:
+ {
+ "topic": "PERP_BTC_USDC@orderbook",
+ "ts": 1698765432000,
+ "data": {...}
+ }
+
+ Args:
+ event_message: WebSocket message
+
+ Returns:
+ Channel identifier string
+ """
+ channel = ""
+
+ # Check if this is a subscription confirmation or error
+ event_type = event_message.get("event")
+ if event_type in ["subscribe", "unsubscribe", "error"]:
+ # Subscription confirmation or error message, don't process as data
+ return channel
+
+ # Orderly uses 'topic' field for data messages
+ topic = event_message.get("topic", "")
+
+ if topic:
+ # Topic format: "{symbol}@{channel}" (e.g., "PERP_BTC_USDC@orderbook")
+ if "@" in topic:
+ channel_name = topic.split("@")[1]
+
+ if channel_name == CONSTANTS.WS_ORDERBOOK_CHANNEL or \
+ channel_name == CONSTANTS.WS_ORDERBOOK_UPDATE_CHANNEL:
+ channel = self._snapshot_messages_queue_key
+ elif channel_name == CONSTANTS.WS_TRADES_CHANNEL:
+ channel = self._trade_messages_queue_key
+ elif channel_name == CONSTANTS.WS_MARKPRICE_CHANNEL:
+ channel = self._mark_price_messages_queue_key
+
+ return channel
+
+ async def _parse_order_book_diff_message(
+ self,
+ raw_message: Dict[str, Any],
+ message_queue: asyncio.Queue
+ ):
+ """
+ Parse order book diff/update message and push to queue.
+
+ Orderly message format:
+ {
+ "topic": "PERP_BTC_USDC@orderbook",
+ "ts": 1698765432000,
+ "data": {
+ "symbol": "PERP_BTC_USDC",
+ "asks": [[50000.0, 1.5], ...],
+ "bids": [[49900.0, 1.2], ...],
+ "timestamp": 1698765432000
+ }
+ }
+
+ Args:
+ raw_message: Raw WebSocket message
+ message_queue: Queue to push parsed message to
+ """
+ try:
+ data = raw_message.get("data", {})
+
+ # Extract symbol from topic (e.g., "PERP_BTC_USDC@orderbook" -> "PERP_BTC_USDC")
+ topic = raw_message.get("topic", "")
+ symbol = topic.split("@")[0] if "@" in topic else data.get("symbol")
+
+ if not symbol:
+ self.logger().warning(f"No symbol in order book message: {raw_message}")
+ return
+
+ # Convert to Hummingbot trading pair
+ trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol)
+
+ # Use timestamp from data or message ts
+ timestamp = data.get("timestamp") or raw_message.get("ts", int(time.time() * 1000))
+ timestamp_seconds = timestamp / 1000 if timestamp > 1e10 else timestamp
+
+ order_book_message = OrderBookMessage(
+ OrderBookMessageType.DIFF,
+ {
+ "trading_pair": trading_pair,
+ "update_id": timestamp,
+ "bids": data.get("bids", []),
+ "asks": data.get("asks", []),
+ },
+ timestamp=timestamp_seconds
+ )
+
+ message_queue.put_nowait(order_book_message)
+
+ except Exception as e:
+ self.logger().error(
+ f"Error parsing order book diff message: {e}",
+ exc_info=True
+ )
+
+ async def _parse_order_book_snapshot_message(
+ self,
+ raw_message: Dict[str, Any],
+ message_queue: asyncio.Queue
+ ):
+ """
+ Parse order book snapshot message and push to queue.
+
+ This handles full order book snapshots from WebSocket.
+
+ Args:
+ raw_message: Raw WebSocket message
+ message_queue: Queue to push parsed message to
+ """
+ try:
+ data = raw_message.get("data", {})
+
+ # Extract symbol from topic
+ topic = raw_message.get("topic", "")
+ symbol = topic.split("@")[0] if "@" in topic else data.get("symbol")
+
+ if not symbol:
+ self.logger().warning(f"No symbol in order book snapshot: {raw_message}")
+ return
+
+ trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol)
+
+ timestamp = data.get("timestamp") or raw_message.get("ts", int(time.time() * 1000))
+ timestamp_seconds = timestamp / 1000 if timestamp > 1e10 else timestamp
+
+ order_book_message = OrderBookMessage(
+ OrderBookMessageType.SNAPSHOT,
+ {
+ "trading_pair": trading_pair,
+ "update_id": timestamp,
+ "bids": data.get("bids", []),
+ "asks": data.get("asks", []),
+ },
+ timestamp=timestamp_seconds
+ )
+
+ message_queue.put_nowait(order_book_message)
+
+ except Exception as e:
+ self.logger().error(
+ f"Error parsing order book snapshot message: {e}",
+ exc_info=True
+ )
+
+ async def _parse_trade_message(
+ self,
+ raw_message: Dict[str, Any],
+ message_queue: asyncio.Queue
+ ):
+ """
+ Parse trade message and push to queue.
+
+ Orderly trade message format:
+ {
+ "topic": "PERP_BTC_USDC@trade",
+ "ts": 1698765432000,
+ "data": [
+ {
+ "symbol": "PERP_BTC_USDC",
+ "price": 50000.0,
+ "quantity": 0.5,
+ "side": "BUY",
+ "trade_id": 12345,
+ "timestamp": 1698765432000
+ }
+ ]
+ }
+
+ Args:
+ raw_message: Raw WebSocket message
+ message_queue: Queue to push parsed message to
+ """
+ try:
+ data = raw_message.get("data", {})
+
+ # Extract symbol from topic
+ topic = raw_message.get("topic", "")
+ symbol = topic.split("@")[0] if "@" in topic else None
+
+ # Handle both single trade and array of trades
+ trades = data if isinstance(data, list) else [data]
+
+ for trade_data in trades:
+ # Use symbol from topic, fallback to trade data
+ trade_symbol = symbol or trade_data.get("symbol")
+ if not trade_symbol:
+ continue
+
+ trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(trade_symbol)
+
+ # Determine trade type
+ side = trade_data.get("side", "").upper()
+ trade_type = TradeType.BUY if side == "BUY" else TradeType.SELL
+
+ timestamp = trade_data.get("timestamp") or raw_message.get("ts", int(time.time() * 1000))
+ timestamp_seconds = timestamp / 1000 if timestamp > 1e10 else timestamp
+
+ trade_message = OrderBookMessage(
+ OrderBookMessageType.TRADE,
+ {
+ "trading_pair": trading_pair,
+ "trade_type": float(trade_type.value),
+ "trade_id": trade_data.get("trade_id", trade_data.get("id", 0)),
+ "price": float(trade_data.get("price", 0)),
+ "amount": float(trade_data.get("quantity", trade_data.get("qty", 0))),
+ },
+ timestamp=timestamp_seconds
+ )
+
+ message_queue.put_nowait(trade_message)
+
+ except Exception as e:
+ self.logger().error(
+ f"Error parsing trade message: {e}",
+ exc_info=True
+ )
+
+ async def _parse_funding_info_message(
+ self,
+ raw_message: Dict[str, Any],
+ message_queue: asyncio.Queue
+ ):
+ """
+ Parse funding info message (placeholder for future WebSocket support).
+
+ Currently, Orderly doesn't provide WebSocket funding updates,
+ so we poll via listen_for_funding_info() instead.
+
+ Args:
+ raw_message: Raw WebSocket message
+ message_queue: Queue to push parsed message to
+ """
+ pass
+
+ async def _parse_mark_price_message(
+ self,
+ raw_message: Dict[str, Any],
+ message_queue: asyncio.Queue
+ ):
+ """
+ Parse mark price message from WebSocket and push FundingInfoUpdate to queue.
+
+ Orderly mark price message format (inferred from REST API structure):
+ {
+ "topic": "PERP_BTC_USDC@markprice",
+ "ts": 1698765432000,
+ "data": {
+ "symbol": "PERP_BTC_USDC",
+ "mark_price": 50000.0,
+ "index_price": 50001.0, # Optional
+ "timestamp": 1698765432000
+ }
+ }
+
+ Args:
+ raw_message: Raw WebSocket message
+ message_queue: Queue to push FundingInfoUpdate to
+ """
+ try:
+ data = raw_message.get("data", {})
+ # Handle both dict and list formats (Orderly may send either)
+ if isinstance(data, list) and len(data) > 0:
+ data = data[0]
+
+ # Extract symbol from topic (e.g., "PERP_BTC_USDC@markprice" -> "PERP_BTC_USDC")
+ topic = raw_message.get("topic", "")
+ symbol = topic.split("@")[0] if "@" in topic else data.get("symbol")
+
+ if not symbol:
+ self.logger().warning(f"No symbol in mark price message: {raw_message}")
+ return
+
+ # Convert to Hummingbot trading pair
+ trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol)
+
+ # Extract mark price from data
+ mark_price = Decimal(str(data.get("price", 0)))
+
+ if mark_price == 0:
+ self.logger().warning(f"Invalid mark price in message: {raw_message}")
+ return
+
+ # Extract index_price if available (some exchanges send both)
+ index_price = None
+ if "index_price" in data:
+ index_price = Decimal(str(data.get("index_price", 0)))
+
+ # Create FundingInfoUpdate with mark_price (and optionally index_price) updated
+ # Other fields (rate, next_funding_utc_timestamp) remain None
+ # and will be preserved from existing FundingInfo
+ funding_info_update = FundingInfoUpdate(
+ trading_pair=trading_pair,
+ mark_price=mark_price,
+ index_price=index_price if index_price else None,
+ )
+
+ message_queue.put_nowait(funding_info_update)
+
+ except Exception as e:
+ self.logger().error(
+ f"Error parsing mark price message: {e}",
+ exc_info=True
+ )
+
+ async def _make_network_check_request(self) -> bool:
+ """
+ Make a simple network check request to verify connectivity.
+
+ Uses GET /v1/public/system_info to check system status.
+
+ Returns:
+ True if connection is successful and system is operational
+ """
+ try:
+ url = web_utils.public_rest_url(
+ CONSTANTS.SYSTEM_INFO_URL,
+ domain=self._domain
+ )
+
+ rest_assistant = await self._api_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ method=RESTMethod.GET,
+ throttler_limit_id=CONSTANTS.SYSTEM_INFO_URL,
+ )
+
+ # Check if response is successful and system is operational
+ # status = 0 means system is functioning properly
+ # status = 2 means system is under maintenance
+ if response.get("success", False):
+ data = response.get("data", {})
+ status = data.get("status", -1)
+ return status == 0 # Return True only if system is operational
+
+ return False
+
+ except Exception as e:
+ self.logger().warning(f"Network check failed: {e}")
+ return False
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_auth.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_auth.py
new file mode 100644
index 00000000000..42085ddc603
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_auth.py
@@ -0,0 +1,317 @@
+"""
+Orderly Network Perpetual Authentication
+
+This module handles authentication for Orderly Network API using ed25519 cryptographic signatures.
+
+Authentication Flow:
+1. User provides pre-generated credentials (account_id, orderly_key, orderly_secret)
+2. For each API request, generate a signature using ed25519 private key
+3. Add authentication headers to the request
+
+Orderly uses ed25519 elliptic curve cryptography for request authentication.
+Each request must include headers:
+- orderly-account-id: Account identifier
+- orderly-key: Public key (ed25519:BASE58_ENCODED)
+- orderly-signature: Request signature (BASE64_ENCODED)
+- orderly-timestamp: Unix milliseconds
+
+For WebSocket connections, authentication is included in the connection URL.
+
+Reference:
+- Orderly Auth Documentation: https://orderly.network/docs/build-on-omnichain/evm-api/api-authentication
+- SDK Implementation: orderly-evm-connector-python/orderly_evm_connector/lib/utils.py
+"""
+
+import base64
+import json
+import time
+from typing import Any, Dict, Optional
+from urllib.parse import urlparse
+
+import base58
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+from hummingbot.connector.derivative.orderly_perpetual import orderly_perpetual_constants as CONSTANTS
+from hummingbot.core.web_assistant.auth import AuthBase
+from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSRequest
+
+
+class OrderlyPerpetualAuth(AuthBase):
+ """
+ Authentication handler for Orderly Network Perpetual API.
+
+ Uses ed25519 signature-based authentication for all API requests.
+ Assumes credentials are pre-generated (does not handle account registration or key generation).
+
+ Args:
+ account_id: Orderly account ID (hex string, e.g., "0x1234...")
+ orderly_key: Public key in Orderly format (e.g., "ed25519:BASE58_ENCODED_KEY")
+ orderly_secret: Private key in Orderly format (e.g., "ed25519:BASE58_ENCODED_KEY")
+ """
+
+ def __init__(
+ self,
+ account_id: str,
+ orderly_key: str,
+ orderly_secret: str,
+ ):
+ self._account_id = account_id
+ self._orderly_key = orderly_key
+ self._orderly_secret = orderly_secret
+ self._private_key = self._parse_private_key(orderly_secret)
+
+ def _parse_private_key(self, orderly_secret: str) -> Ed25519PrivateKey:
+ """
+ Parse ed25519 private key from Orderly secret format.
+
+ Orderly secret format: "ed25519:BASE58_ENCODED_PRIVATE_KEY"
+
+ Args:
+ orderly_secret: Private key in Orderly format
+
+ Returns:
+ Ed25519PrivateKey instance
+
+ Raises:
+ ValueError: If the secret format is invalid
+ """
+ try:
+ # Remove "ed25519:" prefix and decode from BASE58
+ if not orderly_secret.startswith("ed25519:"):
+ raise ValueError("Orderly secret must start with 'ed25519:'")
+
+ secret_b58 = orderly_secret.split(":", 1)[1]
+ private_bytes = base58.b58decode(secret_b58)
+
+ # Ed25519 private keys are 32 bytes
+ if len(private_bytes) < 32:
+ raise ValueError("Invalid private key length")
+
+ return Ed25519PrivateKey.from_private_bytes(private_bytes[:32])
+
+ except Exception as e:
+ raise ValueError(f"Failed to parse Orderly secret: {e}")
+
+ def _get_timestamp(self) -> int:
+ """
+ Get current timestamp in milliseconds.
+
+ Returns:
+ Unix timestamp in milliseconds
+ """
+ return int(time.time() * 1000)
+
+ def _create_normalized_string(
+ self,
+ timestamp: int,
+ method: str,
+ path: str,
+ params: Optional[Dict[str, Any]] = None
+ ) -> str:
+ """
+ Create normalized string for signature generation.
+
+ Format: {timestamp}{method}{path}{body or query_string}
+
+ For GET/DELETE with query params: timestamp + method + path + "?" + query_string
+ For POST/PUT with body: timestamp + method + path + json_body
+ For GET/DELETE without params: timestamp + method + path
+
+ Args:
+ timestamp: Unix milliseconds
+ method: HTTP method (GET, POST, PUT, DELETE)
+ path: Request path (e.g., "/v1/order")
+ params: Request parameters (query params for GET, body for POST)
+
+ Returns:
+ Normalized string for signing
+ """
+ normalized = f"{timestamp}{method}{path}"
+
+ if params:
+ if method in ["GET", "DELETE"]:
+ # Query string format: key1=value1&key2=value2
+ # Match SDK: use insertion order, not sorted
+ query_string = "&".join([f"{k}={v}" for k, v in params.items()])
+ if query_string:
+ normalized += f"?{query_string}"
+ elif method in ["POST", "PUT"]:
+ # JSON body (no spaces, sorted keys for consistency)
+ body_json = json.dumps(params, separators=(",", ":"), sort_keys=True)
+ normalized += body_json
+
+ return normalized
+
+ def generate_signature(
+ self,
+ timestamp: int,
+ method: str,
+ path: str,
+ params: Optional[Dict[str, Any]] = None
+ ) -> str:
+ """
+ Generate ed25519 signature for API request.
+
+ Args:
+ timestamp: Unix milliseconds
+ method: HTTP method
+ path: Request path
+ params: Request parameters
+
+ Returns:
+ BASE64-encoded signature
+ """
+ normalized_string = self._create_normalized_string(timestamp, method, path, params)
+ signature_bytes = self._private_key.sign(normalized_string.encode("utf-8"))
+ return base64.b64encode(signature_bytes).decode("utf-8")
+
+ def get_headers(
+ self,
+ method: str,
+ path: str,
+ params: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, str]:
+ """
+ Generate authentication headers for API request.
+
+ Args:
+ method: HTTP method
+ path: Request path
+ params: Request parameters
+
+ Returns:
+ Dictionary of authentication headers
+ """
+ timestamp = self._get_timestamp()
+ signature = self.generate_signature(timestamp, method, path, params)
+
+ return {
+ "orderly-account-id": self._account_id,
+ "orderly-key": self._orderly_key,
+ "orderly-signature": signature,
+ "orderly-timestamp": str(timestamp),
+ }
+
+ async def rest_authenticate(self, request: RESTRequest) -> RESTRequest:
+ """
+ Add authentication headers to REST request.
+
+ This method is called by Hummingbot's web assistant before sending the request.
+
+ IMPORTANT: Matches SDK behavior exactly:
+ - For GET/DELETE: params are appended to url_path as query string BEFORE signing
+ - For POST/PUT: JSON body is used as-is for signing (exact string from request.data)
+
+ Args:
+ request: REST request to authenticate
+
+ Returns:
+ Authenticated REST request
+ """
+ # Extract method and path
+ method = request.method.name # Convert RESTMethod enum to string
+ path = urlparse(str(request.url)).path
+
+ # Generate timestamp
+ timestamp = self._get_timestamp()
+
+ # Build the message to sign based on method - MATCH SDK EXACTLY
+ if method in ["GET", "DELETE"]:
+ if request.params:
+ query_string = "&".join([f"{k}={v}" for k, v in request.params.items()])
+ url_path_with_query = f"{path}?{query_string}"
+ else:
+ url_path_with_query = path
+
+ # SDK format: {timestamp}{method}{url_path_with_query}
+ message_to_sign = f"{timestamp}{method}{url_path_with_query}"
+
+ elif method in ["POST", "PUT"]:
+ # For POST/PUT, use the exact JSON string from request.data
+ # DO NOT parse and re-encode - it will create a different JSON string!
+ body_string = request.data if request.data else ""
+ message_to_sign = f"{timestamp}{method}{path}{body_string}"
+ else:
+ message_to_sign = f"{timestamp}{method}{path}"
+
+ # Sign the message
+ signature_bytes = self._private_key.sign(message_to_sign.encode("utf-8"))
+ signature = base64.b64encode(signature_bytes).decode("utf-8")
+
+ # Create authentication headers
+ auth_headers = {
+ "orderly-account-id": self._account_id,
+ "orderly-key": self._orderly_key,
+ "orderly-signature": signature,
+ "orderly-timestamp": str(timestamp),
+ }
+
+ # Add headers to request
+ if request.headers is None:
+ request.headers = {}
+ request.headers.update(auth_headers)
+
+ return request
+
+ async def ws_authenticate(self, request: WSRequest) -> WSRequest:
+ """
+ Authenticate WebSocket request.
+
+ For Orderly, authentication is handled in the WebSocket URL path
+ (includes account_id) and through subscription messages.
+
+ Args:
+ request: WebSocket request
+
+ Returns:
+ WebSocket request (pass-through, auth handled elsewhere)
+ """
+ # WebSocket authentication for private channels is handled
+ # in the user stream data source via subscription messages
+ return request
+
+ @property
+ def account_id(self) -> str:
+ """Get account ID"""
+ return self._account_id
+
+ async def get_ws_auth_payload(self) -> Dict[str, Any]:
+ """
+ Generate authentication payload for WebSocket subscription.
+
+ Orderly WebSocket authentication format:
+ {
+ "id": "auth",
+ "event": "auth",
+ "params": {
+ "orderly_key": "ed25519:BASE58_PUBLIC_KEY",
+ "sign": "BASE64_SIGNATURE",
+ "timestamp": 1683270060000
+ }
+ }
+
+ The signature is: sign(str(timestamp)) using ed25519 private key, BASE64 encoded.
+
+ Used by the user stream data source to authenticate private channels.
+
+ Returns:
+ Dictionary with authentication fields in Orderly format
+ """
+ timestamp = self._get_timestamp()
+
+ # Sign the timestamp string
+ message = str(timestamp)
+ signature_bytes = self._private_key.sign(message.encode("utf-8"))
+
+ # Orderly expects BASE64 encoded signature for WebSocket auth
+ signature = base64.b64encode(signature_bytes).decode("utf-8")
+
+ return {
+ "id": "auth",
+ "event": "auth",
+ "params": {
+ "orderly_key": self._orderly_key,
+ "sign": signature,
+ "timestamp": timestamp,
+ }
+ }
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_constants.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_constants.py
new file mode 100644
index 00000000000..fa10c8fddae
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_constants.py
@@ -0,0 +1,428 @@
+"""
+Constants for Orderly Network Perpetual Connector
+
+Based on Orderly Network EVM API documentation:
+https://orderly.network/docs/build-on-omnichain/evm-api/introduction
+"""
+from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit
+from hummingbot.core.data_type.in_flight_order import OrderState
+
+# Exchange Information
+EXCHANGE_NAME = "orderly_perpetual"
+BROKER_ID = "kodiak" # Will be configured by user
+MAX_ORDER_ID_LEN = 36 # Orderly supports up to 36 character client_order_id
+
+DOMAIN = EXCHANGE_NAME
+TESTNET_DOMAIN = "orderly_perpetual_testnet"
+
+# Base URLs
+PERPETUAL_BASE_URL = "https://api.orderly.org"
+TESTNET_BASE_URL = "https://testnet-api.orderly.org"
+
+# WebSocket URLs
+PERPETUAL_WS_PUBLIC_URL = "wss://ws-evm.orderly.org/ws/stream"
+TESTNET_WS_PUBLIC_URL = "wss://testnet-ws-evm.orderly.org/ws/stream"
+
+# Private WebSocket requires account_id in path
+PERPETUAL_WS_PRIVATE_URL = "wss://ws-private-evm.orderly.org/v2/ws/private/stream"
+TESTNET_WS_PRIVATE_URL = "wss://testnet-ws-private-evm.orderly.org/v2/ws/private/stream"
+
+# Funding rate update interval (seconds) - Orderly uses 8-hour funding
+FUNDING_RATE_UPDATE_INTERVAL_SECOND = 60
+
+# Collateral Currency
+CURRENCY = "USDC"
+
+# REST API Endpoints
+# Public Endpoints (no authentication required)
+EXCHANGE_INFO_URL = "/v1/public/futures" # Market info (prices, funding)
+TRADING_RULES_URL = "/v1/public/info" # All trading rules
+TRADING_RULE_URL = "/v1/public/info/{symbol}" # Single symbol trading rules
+SYMBOL_INFO_URL = "/v1/public/futures/{symbol}"
+TICKER_PRICE_URL = "/v1/public/futures"
+MARKET_TRADES_URL = "/v1/public/market_trades"
+FUNDING_RATES_URL = "/v1/public/funding_rates"
+FUNDING_RATE_URL = "/v1/public/funding_rate/{symbol}"
+FUNDING_RATE_HISTORY_URL = "/v1/public/funding_rate_history"
+SYSTEM_INFO_URL = "/v1/public/system_info" # System health check
+PING_URL = "/v1/public/system_info" # Using system info for health check
+
+# Private Endpoints (require authentication)
+# Market Data (authenticated)
+ORDERBOOK_SNAPSHOT_URL = "/v1/orderbook/{symbol}" # Requires auth per official SDK (_market.py:228)
+KLINE_URL = "/v1/kline" # Requires auth per official SDK (_market.py:251) - NOT YET IMPLEMENTED
+# Orders
+CREATE_ORDER_URL = "/v1/order"
+BATCH_CREATE_ORDER_URL = "/v1/batch-order"
+CANCEL_ORDER_URL = "/v1/order" # DELETE method
+CANCEL_ORDER_BY_CLIENT_ID_URL = "/v1/client/order" # DELETE method
+CANCEL_ALL_ORDERS_URL = "/v1/orders" # DELETE method
+BATCH_CANCEL_ORDER_URL = "/v1/batch-order" # DELETE method (by order_id)
+BATCH_CANCEL_ORDER_BY_CLIENT_ID_URL = "/v1/client/batch-order" # DELETE method (by client_order_id)
+EDIT_ORDER_URL = "/v1/order" # PUT method
+GET_ORDER_URL = "/v1/order/{order_id}"
+GET_ORDER_BY_CLIENT_ID_URL = "/v1/client/order/{client_order_id}"
+GET_ORDERS_URL = "/v1/orders"
+
+# Trades
+GET_TRADES_URL = "/v1/trades"
+GET_TRADE_URL = "/v1/trade/{trade_id}"
+GET_ORDER_TRADES_URL = "/v1/order/{order_id}/trades"
+
+# Account
+ACCOUNT_INFO_URL = "/v1/client/info"
+ACCOUNT_HOLDING_URL = "/v1/client/holding"
+ACCOUNT_STATISTICS_URL = "/v1/client/statistics"
+
+# Positions
+POSITIONS_URL = "/v1/positions"
+POSITION_URL = "/v1/position/{symbol}"
+POSITION_HISTORY_URL = "/v1/position_history"
+
+# Leverage
+SET_LEVERAGE_URL = "/v1/client/leverage"
+GET_LEVERAGE_URL = "/v1/client/leverage"
+
+# Funding
+FUNDING_FEE_HISTORY_URL = "/v1/funding_fee/history"
+
+# WebSocket Channel Names
+# Public Channels
+WS_ORDERBOOK_CHANNEL = "orderbook"
+WS_ORDERBOOK_UPDATE_CHANNEL = "orderbookupdate"
+WS_TRADES_CHANNEL = "trade"
+WS_TICKER_CHANNEL = "ticker"
+WS_BBO_CHANNEL = "bbo"
+WS_MARKPRICE_CHANNEL = "markprice"
+WS_KLINE_CHANNEL = "kline"
+
+# Private Channels
+WS_EXECUTION_REPORT_CHANNEL = "executionreport"
+WS_POSITION_CHANNEL = "position"
+WS_BALANCE_CHANNEL = "balance"
+WS_ACCOUNT_CHANNEL = "account"
+WS_WALLET_CHANNEL = "wallet"
+
+# WebSocket Configuration
+HEARTBEAT_TIME_INTERVAL = 30.0
+WS_PING_INTERVAL = 10 # Orderly sends ping every 10 seconds
+WS_PONG_TIMEOUT = 60 # Must respond within 60 seconds
+
+# Order States Mapping
+# Map Orderly order statuses to Hummingbot OrderState
+ORDER_STATE = {
+ "NEW": OrderState.OPEN,
+ "PARTIAL_FILLED": OrderState.PARTIALLY_FILLED,
+ "FILLED": OrderState.FILLED,
+ "CANCELLED": OrderState.CANCELED,
+ "REJECTED": OrderState.FAILED,
+ "INCOMPLETE": OrderState.OPEN,
+ "COMPLETED": OrderState.FILLED,
+}
+
+# Rate Limits
+# Based on Orderly Network rate limiting:
+# - Trading endpoints: 10 requests per second
+# - Other private endpoints: varies
+# - Public endpoints: more permissive
+
+# Conservative rate limits (requests per time_interval)
+MAX_REQUESTS_PER_SECOND = 10
+TRADING_ENDPOINTS_LIMIT = 10
+PRIVATE_ENDPOINTS_LIMIT = 20
+PUBLIC_ENDPOINTS_LIMIT = 50
+
+ALL_ENDPOINTS_LIMIT = "All"
+TRADING_LIMIT_ID = "Trading"
+PRIVATE_LIMIT_ID = "Private"
+PUBLIC_LIMIT_ID = "Public"
+
+RATE_LIMITS = [
+ # Global limits
+ RateLimit(ALL_ENDPOINTS_LIMIT, limit=100, time_interval=10),
+ RateLimit(TRADING_LIMIT_ID, limit=TRADING_ENDPOINTS_LIMIT, time_interval=1),
+ RateLimit(PRIVATE_LIMIT_ID, limit=PRIVATE_ENDPOINTS_LIMIT, time_interval=1),
+ RateLimit(PUBLIC_LIMIT_ID, limit=PUBLIC_ENDPOINTS_LIMIT, time_interval=1),
+
+ # Trading endpoints (10 req/sec limit)
+ RateLimit(
+ limit_id=CREATE_ORDER_URL,
+ limit=TRADING_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Batch Create Order: 1 req/sec per swagger docs
+ RateLimit(
+ limit_id=BATCH_CREATE_ORDER_URL,
+ limit=1,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=CANCEL_ORDER_URL,
+ limit=TRADING_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Batch Cancel Order: 10 req/sec per swagger docs (both variants)
+ RateLimit(
+ limit_id=BATCH_CANCEL_ORDER_URL,
+ limit=TRADING_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=BATCH_CANCEL_ORDER_BY_CLIENT_ID_URL,
+ limit=TRADING_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=EDIT_ORDER_URL,
+ limit=TRADING_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(TRADING_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+
+ # Private endpoints
+ RateLimit(
+ limit_id=GET_ORDERS_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=GET_TRADES_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Positions: 30 req per 10 seconds per swagger docs
+ RateLimit(
+ limit_id=POSITIONS_URL,
+ limit=30,
+ time_interval=10,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Account Info: 10 req per 60 seconds per swagger docs
+ RateLimit(
+ limit_id=ACCOUNT_INFO_URL,
+ limit=10,
+ time_interval=60,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=ACCOUNT_HOLDING_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Set Leverage: 5 req per 60 seconds per swagger docs
+ RateLimit(
+ limit_id=SET_LEVERAGE_URL,
+ limit=5,
+ time_interval=60,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Get Leverage: 1 req per 1 second per swagger docs (was missing)
+ RateLimit(
+ limit_id=GET_LEVERAGE_URL,
+ limit=1,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+
+ # Public endpoints
+ RateLimit(
+ limit_id=EXCHANGE_INFO_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=TRADING_RULES_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=TRADING_RULE_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ # Orderbook Snapshot: 10 req per 1 second per swagger docs
+ RateLimit(
+ limit_id=ORDERBOOK_SNAPSHOT_URL,
+ limit=10,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=MARKET_TRADES_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=FUNDING_RATES_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=FUNDING_RATE_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ ),
+ RateLimit(
+ limit_id=FUNDING_RATE_HISTORY_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ ),
+ RateLimit(
+ limit_id=SYMBOL_INFO_URL,
+ limit=PUBLIC_ENDPOINTS_LIMIT,
+ time_interval=1,
+ ),
+ # System Info / Ping: 1 req per 1 second per swagger docs
+ RateLimit(
+ limit_id=SYSTEM_INFO_URL,
+ limit=1,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=PING_URL,
+ limit=1,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PUBLIC_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=GET_ORDER_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=GET_ORDER_TRADES_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+ RateLimit(
+ limit_id=FUNDING_FEE_HISTORY_URL,
+ limit=PRIVATE_ENDPOINTS_LIMIT,
+ time_interval=1,
+ linked_limits=[
+ LinkedLimitWeightPair(PRIVATE_LIMIT_ID),
+ LinkedLimitWeightPair(ALL_ENDPOINTS_LIMIT)
+ ]
+ ),
+]
+
+# Error Messages
+ORDER_NOT_FOUND_ERROR_CODE = 1006 # Orderly error code for order not found
+ORDER_NOT_EXIST_MESSAGE = "Order not found"
+ORDER_ALREADY_CANCELLED_MESSAGE = "Order already cancelled"
+ORDER_ALREADY_FILLED_MESSAGE = "Order already filled"
+CANCELLING_COMPLETED_ORDER_MESSAGE = "The order is completed"
+
+# ALL_ERROR_CODES = [
+# {"error_code": -1000, "status_code": 500, "error_name": "UNKNOWN", "description": "An unknown error occurred while processing the request."},
+# {"error_code:" -1000, "status_code": 500, "error_name": "UNKNOWN", "description": "The data does not exist"},
+# {"error_code:" -1001, "status_code": 401, "error_name": "INVALID_SIGNATURE", "description": "The api key or secret is in wrong format."},
+# {"error_code:" -1002, "status_code": 401, "error_name": "UNAUTHORIZED", "description": "API key or secret is invalid, it may be because key have insufficient permission or the key is expired/revoked."},
+# {"error_code:" -1003, "status_code": 429, "error_name": "TOO_MANY_REQUEST", "description": "Rate limit exceed."},
+# {"error_code:" -1004, "status_code": 400, "error_name": "UNKNOWN_PARAM", "description": "An unknown parameter was sent."},
+# {"error_code:" -1005, "status_code": 400, "error_name": "INVALID_PARAM", "description": "Some parameters are in wrong format for api."},
+# {"error_code:" -1005, "status_code": 400, "error_name": "INVALID_PARAM", "description": "ratio_qty_request should be in range 0-1."},
+# {"error_code:" -1005, "status_code": 400, "error_name": "INVALID_PARAM", "description": "extra_liquidation_ratio should be in range 0-1."},
+# {"error_code:" -1005, "status_code": 400, "error_name": "INVALID_PARAM", "description": "if you set extra_liquidation_ratio > 0, ratio_qty_request must be 1."},
+# {"error_code:" -1006, "status_code": 400, "error_name": "RESOURCE_NOT_FOUND", "description": "The data is not found in server. For example, when client try canceling a CANCELLED order, will raise this error."},
+# {"error_code:" -1007, "status_code": 409, "error_name": "DUPLICATE_REQUEST", "description": "The data is already exists or your request is duplicated."},
+# {"error_code:" -1008, "status_code": 400, "error_name": "QUANTITY_TOO_HIGH", "description": "The quantity of settlement is too high than you can request."},
+# {"error_code:" -1009, "status_code": 400, "error_name": "CAN_NOT_WITHDRAWAL", "description": "Can not request withdrawal settlement, you need to deposit other arrears first."},
+# {"error_code:" -1011, "status_code": 400, "error_name": "RPC_NOT_CONNECT", "description": "Can not place/cancel orders, it may be because internal network error. Please try again in a few seconds."},
+# {"error_code:" -1012, "status_code": 400, "error_name": "RPC_REJECT", "description": "The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds."},
+# {"error_code:" -1012, "status_code": 400, "error_name": "RPC_REJECT", "description": "Another liquidation is in process"},
+# {"error_code:" -1101, "status_code": 400, "error_name": "RISK_TOO_HIGH", "description": "The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure."},
+# {"error_code:" -1101, "status_code": 400, "error_name": "RISK_TOO_HIGH", "description": "The margin will be insufficient after."},
+# {"error_code:" -1102, "status_code": 400, "error_name": "MIN_NOTIONAL", "description": "The order value (price * size) is too small."},
+# {"error_code:" -1103, "status_code": 400, "error_name": "PRICE_FILTER", "description": "The order price is not following the scope or range rules."},
+# {"error_code:" -1104, "status_code": 400, "error_name": "SIZE_FILTER", "description": "The order quantity is not following the step size rule for the symbol."},
+# {"error_code:" -1105, "status_code": 400, "error_name": "PERCENTAGE_FILTER", "description": "Price is X% too high or X% too low from the mid price."},
+# {"error_code:" -1201, "status_code": 400, "error_name": "LIQUIDATION_REQUEST_RATIO_TOO_SMALL", "description": "total notional < 10000, least req ratio should = 1"},
+# {"error_code:" -1201, "status_code": 400, "error_name": "LIQUIDATION_REQUEST_RATIO_TOO_SMALL", "description": "least req ratio should = xxxx"},
+# {"error_code:" -1202, "status_code": 400, "error_name": "LIQUIDATION_STATUS_ERROR", "description": "No need to liquidation because user margin is enough."},
+# {"error_code:" -1202, "status_code": 400, "error_name": "LIQUIDATION_STATUS_ERROR", "description": "Can not find given liquidationId."},
+# ]
\ No newline at end of file
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_derivative.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_derivative.py
new file mode 100644
index 00000000000..81f2916ef7d
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_derivative.py
@@ -0,0 +1,1864 @@
+"""
+Orderly Network Perpetual Derivative Connector
+
+This module implements the main connector class for Orderly Network perpetual futures trading.
+
+The connector provides:
+- Order placement and management
+- Position tracking
+- Balance management
+- Funding rate information
+- Real-time market data via WebSocket
+
+Reference: Orderly Network EVM API
+https://orderly.network/docs/build-on-omnichain/evm-api/introduction
+"""
+
+import asyncio
+import json
+from decimal import Decimal
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+
+from bidict import bidict
+
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_constants as CONSTANTS
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_web_utils as web_utils
+from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_api_order_book_data_source import (
+ OrderlyPerpetualAPIOrderBookDataSource,
+)
+from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_auth import OrderlyPerpetualAuth
+from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_user_stream_data_source import (
+ OrderlyPerpetualUserStreamDataSource,
+)
+from hummingbot.connector.derivative.position import Position
+from hummingbot.connector.perpetual_derivative_py_base import PerpetualDerivativePyBase
+from hummingbot.connector.trading_rule import TradingRule
+from hummingbot.connector.utils import combine_to_hb_trading_pair, get_new_client_order_id
+from hummingbot.core.api_throttler.data_types import RateLimit
+from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide, TradeType
+from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, OrderUpdate, TradeUpdate
+from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource
+from hummingbot.core.data_type.trade_fee import TokenAmount, TradeFeeBase
+from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource
+from hummingbot.core.utils.async_utils import safe_gather
+from hummingbot.core.utils.estimate_fee import build_trade_fee
+from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest
+from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
+
+
+class OrderlyPerpetualDerivative(PerpetualDerivativePyBase):
+ """
+ Orderly Network Perpetual Derivative Connector.
+
+ Main connector class that integrates with Hummingbot's trading framework.
+ """
+
+ web_utils = web_utils
+
+ def __init__(
+ self,
+ balance_asset_limit: Optional[Dict[str, Dict[str, Decimal]]] = None,
+ rate_limits_share_pct: Decimal = Decimal("100"),
+ orderly_perpetual_api_key: str = None,
+ orderly_perpetual_api_secret: str = None,
+ orderly_perpetual_account_id: str = None,
+ trading_pairs: Optional[List[str]] = None,
+ trading_required: bool = True,
+ domain: str = CONSTANTS.DOMAIN,
+ ):
+ """
+ Initialize Orderly Perpetual connector.
+
+ Args:
+ balance_asset_limit: Optional balance limits per asset
+ rate_limits_share_pct: Percentage of rate limits to use
+ orderly_perpetual_api_key: Orderly API key (public key)
+ orderly_perpetual_api_secret: Orderly API secret (private key)
+ orderly_perpetual_account_id: Orderly account ID
+ trading_pairs: List of trading pairs to trade
+ trading_required: Whether trading is required
+ domain: Domain (mainnet or testnet)
+ """
+ self._orderly_perpetual_api_key = orderly_perpetual_api_key
+ self._orderly_perpetual_api_secret = orderly_perpetual_api_secret
+ self._orderly_perpetual_account_id = orderly_perpetual_account_id
+ self._trading_required = trading_required
+ self._trading_pairs = trading_pairs or []
+ self._domain = domain
+ self._position_mode = None
+ super().__init__(balance_asset_limit, rate_limits_share_pct)
+
+ # ============================================================
+ # Properties
+ # ============================================================
+
+ @property
+ def name(self) -> str:
+ """Exchange name"""
+ return self._domain
+
+ @property
+ def authenticator(self) -> Optional[OrderlyPerpetualAuth]:
+ """Return authenticator if API keys are provided (needed for balance queries even if trading not required)"""
+ # Check if API keys are provided - balance queries require authentication
+ has_api_keys = (
+ self._orderly_perpetual_api_key and
+ self._orderly_perpetual_api_secret and
+ self._orderly_perpetual_account_id
+ )
+
+ if has_api_keys:
+ return OrderlyPerpetualAuth(
+ account_id=self._orderly_perpetual_account_id,
+ orderly_key=self._orderly_perpetual_api_key,
+ orderly_secret=self._orderly_perpetual_api_secret,
+ )
+ return None
+
+ @property
+ def rate_limits_rules(self) -> List[RateLimit]:
+ """Rate limits from constants"""
+ return CONSTANTS.RATE_LIMITS
+
+ @property
+ def domain(self) -> str:
+ """Domain identifier"""
+ return self._domain
+
+ @property
+ def client_order_id_max_length(self) -> int:
+ """Max length for client order IDs"""
+ return CONSTANTS.MAX_ORDER_ID_LEN
+
+ @property
+ def client_order_id_prefix(self) -> str:
+ """Prefix for client order IDs"""
+ return CONSTANTS.BROKER_ID
+
+ @property
+ def trading_rules_request_path(self) -> str:
+ """API path for trading rules"""
+ return CONSTANTS.TRADING_RULES_URL
+
+ @property
+ def trading_pairs_request_path(self) -> str:
+ """API path for trading pairs"""
+ return CONSTANTS.EXCHANGE_INFO_URL
+
+ @property
+ def check_network_request_path(self) -> str:
+ """API path for network check"""
+ return CONSTANTS.SYSTEM_INFO_URL
+
+ @property
+ def trading_pairs(self) -> List[str]:
+ """List of trading pairs"""
+ return self._trading_pairs
+
+ @property
+ def is_cancel_request_in_exchange_synchronous(self) -> bool:
+ """Whether cancel requests are synchronous"""
+ return True
+
+ @property
+ def is_trading_required(self) -> bool:
+ """Whether trading is enabled"""
+ return self._trading_required
+
+ @property
+ def funding_fee_poll_interval(self) -> int:
+ """Funding fee polling interval in seconds"""
+ return 120
+
+ # ============================================================
+ # Abstract Methods Implementation
+ # ============================================================
+
+ def supported_order_types(self) -> List[OrderType]:
+ """
+ :return a list of OrderType supported by this connector
+ """
+ return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET]
+
+ def supported_position_modes(self) -> List[PositionMode]:
+ """
+ Return list of supported position modes.
+
+ Orderly supports ONEWAY mode (single position per symbol).
+ """
+ return [PositionMode.ONEWAY]
+
+ def get_buy_collateral_token(self, trading_pair: str) -> str:
+ """Return collateral token for buy orders"""
+ return CONSTANTS.CURRENCY
+
+ def get_sell_collateral_token(self, trading_pair: str) -> str:
+ """Return collateral token for sell orders"""
+ return CONSTANTS.CURRENCY
+
+ def _create_web_assistants_factory(self) -> WebAssistantsFactory:
+ """Create web assistants factory"""
+ return web_utils.build_api_factory(
+ throttler=self._throttler,
+ auth=self._auth,
+ )
+
+ def _create_order_book_data_source(self) -> OrderBookTrackerDataSource:
+ """Create order book data source"""
+ return OrderlyPerpetualAPIOrderBookDataSource(
+ trading_pairs=self._trading_pairs,
+ connector=self,
+ api_factory=self._web_assistants_factory,
+ domain=self._domain,
+ )
+
+ def _create_user_stream_data_source(self) -> UserStreamTrackerDataSource:
+ """Create user stream data source"""
+ return OrderlyPerpetualUserStreamDataSource(
+ auth=self._auth,
+ trading_pairs=self._trading_pairs,
+ connector=self,
+ api_factory=self._web_assistants_factory,
+ domain=self._domain,
+ )
+
+ # ============================================================
+ # Symbol Mapping
+ # ============================================================
+
+ async def _initialize_trading_pair_symbol_map(self):
+ """
+ Initialize trading pair symbol map by fetching trading rules.
+
+ This method is called by the base class when exchange_symbol_associated_to_pair()
+ is called before trading rules have been fetched.
+ """
+ try:
+ exchange_info = await self._make_trading_rules_request()
+ self._initialize_trading_pair_symbols_from_exchange_info(exchange_info=exchange_info)
+ except Exception:
+ self.logger().exception("There was an error requesting exchange info for symbol map initialization.")
+
+ def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]):
+ """
+ Initialize bidirectional mapping between exchange symbols and Hummingbot trading pairs.
+
+ Orderly format: PERP_BTC_USDC
+ Hummingbot format: BTC-USDC
+
+ Args:
+ exchange_info: Exchange information dictionary
+ """
+ mapping = bidict()
+ symbols_processed = 0
+ symbols_skipped = 0
+
+ for symbol_data in exchange_info:
+ try:
+ exchange_symbol = symbol_data.get("symbol")
+ if not exchange_symbol or not exchange_symbol.startswith("PERP_"):
+ symbols_skipped += 1
+ continue
+
+ # Convert to Hummingbot format
+ trading_pair = web_utils.format_trading_pair(exchange_symbol)
+
+ # Orderly uses unique symbols (PERP_BTC_USDC), no duplicates expected
+ if trading_pair not in mapping.inverse:
+ mapping[exchange_symbol] = trading_pair
+ symbols_processed += 1
+ else:
+ self.logger().error(f"[SYMBOL CONVERSION] Duplicate symbol found: {exchange_symbol} -> {trading_pair}")
+
+ except Exception:
+ self.logger().exception(f"[SYMBOL CONVERSION] Error parsing symbol: {symbol_data}")
+
+ self._set_trading_pair_symbol_map(mapping)
+
+ async def exchange_symbol_associated_to_pair(self, trading_pair: str) -> str:
+ """
+ Override to add logging for symbol conversion.
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format (e.g., "ETH-USDC")
+
+ Returns:
+ Symbol in Orderly format (e.g., "PERP_ETH_USDC")
+ """
+ try:
+ symbol_map = await self.trading_pair_symbol_map()
+
+ if trading_pair not in symbol_map.inverse:
+ raise KeyError(f"Trading pair '{trading_pair}' not found in symbol map")
+
+ orderly_symbol = symbol_map.inverse[trading_pair]
+ return orderly_symbol
+ except KeyError:
+ # Re-raise KeyError with more context
+ raise
+ except Exception as e:
+ self.logger().error(
+ f"[SYMBOL CONVERSION] Error converting trading pair '{trading_pair}': {e}",
+ exc_info=True
+ )
+ raise
+
+ # ============================================================
+ # Trading Rules
+ # ============================================================
+
+ async def _make_trading_rules_request(self) -> Any:
+ """
+ Fetch trading rules from exchange.
+
+ According to Orderly API docs:
+ GET /v1/public/info - Returns all available symbols with trading rules
+
+ Response structure:
+ {
+ "success": true,
+ "data": {
+ "rows": [
+ {
+ "symbol": "PERP_BTC_USDC",
+ "base_min": 1.0E-5,
+ "base_max": 20,
+ "base_tick": 1.0E-5,
+ "quote_min": 0,
+ "quote_max": 100000,
+ "quote_tick": 0.1,
+ "min_notional": 1,
+ ...
+ }
+ ]
+ }
+ }
+
+ Returns:
+ List of trading rule dictionaries (rows from response)
+ """
+ url = web_utils.public_rest_url(
+ CONSTANTS.TRADING_RULES_URL,
+ domain=self._domain
+ )
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.TRADING_RULES_URL,
+ method=RESTMethod.GET,
+ )
+
+ if not response.get("success", False):
+ self.logger().error(f"[TRADING RULES] Failed to fetch trading rules: {response}")
+ raise IOError(f"Failed to fetch trading rules: {response}")
+
+ # Return the rows array which contains all trading rules
+ data = response.get("data", {})
+ rows = data.get("rows", [])
+
+ # Log fetched trading rules
+ self.logger().info(f"[TRADING RULES] Fetched {len(rows)} trading rules from exchange")
+ if rows:
+ self.logger().debug(f"[TRADING RULES] Sample symbols from exchange: {[r.get('symbol') for r in rows[:5]]}")
+
+ return rows
+
+ async def _make_trading_pairs_request(self) -> Any:
+ """
+ Fetch available trading pairs.
+
+ Returns:
+ Raw trading pairs response
+ """
+ url = web_utils.public_rest_url(
+ CONSTANTS.EXCHANGE_INFO_URL,
+ domain=self._domain
+ )
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.EXCHANGE_INFO_URL,
+ method=RESTMethod.GET,
+ )
+
+ if not response.get("success", False):
+ raise IOError(f"Failed to fetch trading pairs: {response}")
+
+ return response.get("data", {}).get("rows", [])
+
+ async def _format_trading_rules(self, exchange_info_list: List[Dict[str, Any]]) -> List[TradingRule]:
+ """
+ Parse raw trading rules into TradingRule objects.
+
+ Args:
+ exchange_info_list: List of raw trading rule dictionaries
+
+ Returns:
+ List of TradingRule objects
+ """
+ trading_rules = []
+
+ for rule_data in exchange_info_list:
+ try:
+ if not web_utils.is_exchange_information_valid(rule_data):
+ continue
+
+ orderly_symbol = rule_data["symbol"]
+ # Format trading pair directly from symbol (don't use mapping since it's not initialized yet)
+ trading_pair = web_utils.format_trading_pair(orderly_symbol)
+
+ trading_rule = TradingRule(
+ trading_pair=trading_pair,
+ min_order_size=Decimal(str(rule_data.get("base_min", "0"))),
+ max_order_size=Decimal(str(rule_data.get("base_max", "1000000"))),
+ min_price_increment=Decimal(str(rule_data.get("quote_tick", "0.01"))),
+ min_base_amount_increment=Decimal(str(rule_data.get("base_tick", "0.01"))),
+ min_notional_size=Decimal(str(rule_data.get("min_notional", "0"))),
+ buy_order_collateral_token=CONSTANTS.CURRENCY,
+ sell_order_collateral_token=CONSTANTS.CURRENCY,
+ )
+
+ trading_rules.append(trading_rule)
+
+ except Exception:
+ self.logger().exception(f"Error parsing trading rule: {rule_data}")
+
+ return trading_rules
+
+ # ============================================================
+ # Network & Connectivity
+ # ============================================================
+
+ async def _make_network_check_request(self):
+ """Make network check request"""
+ url = web_utils.public_rest_url(CONSTANTS.SYSTEM_INFO_URL, domain=self._domain)
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.SYSTEM_INFO_URL,
+ method=RESTMethod.GET,
+ )
+ return response.get("success", False) and response.get("data", {}).get("status") == 0
+
+ def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception):
+ """Check if error is time-related (Orderly doesn't require time sync)"""
+ return False
+
+ def _is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool:
+ """Check if error is due to order not found"""
+ return CONSTANTS.ORDER_NOT_EXIST_MESSAGE in str(status_update_exception)
+
+ def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool:
+ """Check if error is due to order not found during cancel"""
+ error_str = str(cancelation_exception)
+ return (
+ CONSTANTS.ORDER_NOT_EXIST_MESSAGE in error_str
+ or CONSTANTS.ORDER_ALREADY_CANCELLED_MESSAGE in error_str
+ or CONSTANTS.ORDER_ALREADY_FILLED_MESSAGE in error_str
+ or CONSTANTS.CANCELLING_COMPLETED_ORDER_MESSAGE in error_str
+ or f"'code': {CONSTANTS.ORDER_NOT_FOUND_ERROR_CODE}" in error_str # Check code -1006
+ or ("-1005" in error_str and "order" in error_str.lower() and "invalid" in error_str.lower()) # -1005 "The order ID is invalid"
+ )
+
+ # ============================================================
+ # Helper Methods
+ # ============================================================
+
+ async def _get_last_traded_price(self, trading_pair: str) -> float:
+ """
+ Get last traded price for a trading pair.
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format
+
+ Returns:
+ Last traded price
+ """
+ symbol = await self.exchange_symbol_associated_to_pair(trading_pair)
+ url = web_utils.public_rest_url(
+ CONSTANTS.SYMBOL_INFO_URL.format(symbol=symbol),
+ domain=self._domain
+ )
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.SYMBOL_INFO_URL,
+ method=RESTMethod.GET,
+ )
+
+ if response.get("success"):
+ data = response.get("data", {})
+ return float(data.get("mark_price", 0))
+
+ return 0.0
+
+ # ============================================================
+ # Order Placement & Management - Helper Methods
+ # ============================================================
+
+ async def _start_tracking_and_validate_order(
+ self,
+ trade_type: TradeType,
+ order_id: str,
+ trading_pair: str,
+ amount: Decimal,
+ order_type: OrderType,
+ price: Optional[Decimal] = None,
+ position_action: PositionAction = PositionAction.NIL,
+ **kwargs
+ ) -> Optional[InFlightOrder]:
+ """
+ Start tracking an order and validate it before placing.
+
+ This method:
+ 1. Calculates/quantizes price and amount
+ 2. Starts tracking the order
+ 3. Validates order parameters (type, min size, min notional)
+ 4. Returns the tracked order object or None on failure
+
+ Args:
+ trade_type: BUY or SELL
+ order_id: Client order ID
+ trading_pair: Trading pair
+ amount: Order amount
+ order_type: Order type (LIMIT, LIMIT_MAKER, MARKET)
+ price: Order price (optional for MARKET orders)
+ position_action: Position action (OPEN/CLOSE)
+ **kwargs: Additional parameters
+
+ Returns:
+ InFlightOrder object if valid, None if validation fails
+ """
+ try:
+ # Calculate price for market orders
+ if price is None or price.is_nan():
+ price = self.get_price_for_volume(
+ trading_pair,
+ True if trade_type == TradeType.BUY else False,
+ amount
+ ).result_price
+
+ # Quantize price and amount
+ price = self.quantize_order_price(trading_pair, price)
+ amount = self.quantize_order_amount(trading_pair, amount)
+
+ # Start tracking the order
+ self.start_tracking_order(
+ order_id=order_id,
+ exchange_order_id=None, # Will be set after API call
+ trading_pair=trading_pair,
+ trade_type=trade_type,
+ price=price,
+ amount=amount,
+ order_type=order_type,
+ position_action=position_action,
+ )
+
+ # Get the tracked order
+ tracked_order = self._order_tracker.all_updatable_orders.get(order_id)
+ if not tracked_order:
+ self.logger().error(f"Failed to start tracking order {order_id}")
+ return None
+
+ # Validate order type support
+ if order_type not in self.supported_order_types():
+ self._update_order_after_creation_failure(
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ trade_type=trade_type,
+ order_type=order_type,
+ price=price,
+ exception=ValueError(f"Order type {order_type} is not supported"),
+ )
+ return None
+
+ # Get trading rules
+ trading_rule = self._trading_rules.get(trading_pair)
+ if not trading_rule:
+ self._update_order_after_creation_failure(
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ trade_type=trade_type,
+ order_type=order_type,
+ price=price,
+ exception=ValueError(f"Trading rule not found for {trading_pair}"),
+ )
+ return None
+
+ # Validate min order size
+ if amount < trading_rule.min_order_size:
+ self._update_order_after_creation_failure(
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ trade_type=trade_type,
+ order_type=order_type,
+ price=price,
+ exception=ValueError(
+ f"Order amount {amount} is below minimum order size {trading_rule.min_order_size}"
+ ),
+ )
+ return None
+
+ # Validate min notional size
+ notional_size = amount * price
+ if notional_size < trading_rule.min_notional_size:
+ self._update_order_after_creation_failure(
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ trade_type=trade_type,
+ order_type=order_type,
+ price=price,
+ exception=ValueError(
+ f"Order notional size {notional_size} is below minimum {trading_rule.min_notional_size}"
+ ),
+ )
+ return None
+
+ return tracked_order
+
+ except Exception as e:
+ self.logger().error(
+ f"Error in _start_tracking_and_validate_order for {order_id}: {e}",
+ exc_info=True
+ )
+ self._update_order_after_creation_failure(
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ trade_type=trade_type,
+ order_type=order_type,
+ price=price if price else Decimal("0"),
+ exception=e,
+ )
+ return None
+
+ def _update_order_after_creation_success(
+ self,
+ exchange_order_id: Optional[str],
+ order: InFlightOrder,
+ update_timestamp: float,
+ misc_updates: Optional[Dict[str, Any]] = None
+ ):
+ """
+ Update order after successful creation on the exchange.
+
+ Creates an OrderUpdate with the exchange_order_id and processes it
+ through the order tracker. This triggers the appropriate order
+ creation events.
+
+ Args:
+ exchange_order_id: Exchange-assigned order ID
+ order: InFlightOrder object
+ update_timestamp: Timestamp of the update
+ misc_updates: Optional additional updates dictionary
+ """
+ order_update: OrderUpdate = OrderUpdate(
+ client_order_id=order.client_order_id,
+ exchange_order_id=exchange_order_id,
+ trading_pair=order.trading_pair,
+ update_timestamp=update_timestamp,
+ new_state=order.current_state, # Keep current state (typically PENDING_CREATE)
+ misc_updates=misc_updates,
+ )
+ self._order_tracker.process_order_update(order_update)
+
+ def _on_order_creation_failure(
+ self,
+ order_id: str,
+ trading_pair: str,
+ amount: Decimal,
+ trade_type: TradeType,
+ order_type: OrderType,
+ price: Decimal,
+ exception: Exception,
+ position_action: PositionAction = PositionAction.NIL,
+ ):
+ """
+ Handle order creation failure that occurred during API call.
+
+ Creates an OrderUpdate with FAILED state and processes it through
+ the order tracker. This triggers the OrderFailure event.
+
+ Args:
+ order_id: Client order ID
+ trading_pair: Trading pair
+ amount: Order amount
+ trade_type: BUY or SELL
+ order_type: Order type
+ price: Order price
+ exception: Exception that caused the failure
+ position_action: Position action (OPEN/CLOSE)
+ """
+ self.logger().error(
+ f"Order creation failed for {order_id}: {exception}",
+ exc_info=True
+ )
+
+ order_update: OrderUpdate = OrderUpdate(
+ client_order_id=order_id,
+ trading_pair=trading_pair,
+ update_timestamp=self.current_timestamp,
+ new_state=OrderState.FAILED,
+ )
+ self._order_tracker.process_order_update(order_update)
+
+ def _update_order_after_creation_failure(
+ self,
+ order_id: str,
+ trading_pair: str,
+ amount: Decimal,
+ trade_type: TradeType,
+ order_type: OrderType,
+ price: Decimal,
+ exception: Exception,
+ position_action: PositionAction = PositionAction.NIL,
+ ):
+ """
+ Handle order creation failure during validation (before API call).
+
+ Similar to _on_order_creation_failure but used for validation failures
+ that occur before the API call is made.
+
+ Args:
+ order_id: Client order ID
+ trading_pair: Trading pair
+ amount: Order amount
+ trade_type: BUY or SELL
+ order_type: Order type
+ price: Order price
+ exception: Exception that caused the failure
+ position_action: Position action (OPEN/CLOSE)
+ """
+ self.logger().warning(
+ f"Order validation failed for {order_id}: {exception}"
+ )
+
+ order_update: OrderUpdate = OrderUpdate(
+ client_order_id=order_id,
+ trading_pair=trading_pair,
+ update_timestamp=self.current_timestamp,
+ new_state=OrderState.FAILED,
+ )
+ self._order_tracker.process_order_update(order_update)
+
+ # ============================================================
+ # Order Placement & Management
+ # ============================================================
+
+ async def _place_order(
+ self,
+ order_id: str,
+ trading_pair: str,
+ amount: Decimal,
+ trade_type: TradeType,
+ order_type: OrderType,
+ price: Decimal,
+ position_action: PositionAction = PositionAction.NIL,
+ **kwargs,
+ ) -> Tuple[str, float]:
+ """
+ Place an order on the exchange.
+
+ Args:
+ order_id: Client order ID
+ trading_pair: Trading pair
+ amount: Order amount
+ trade_type: BUY or SELL
+ order_type: LIMIT or MARKET
+ price: Order price
+ position_action: OPEN or CLOSE
+
+ Returns:
+ Tuple of (exchange_order_id, timestamp)
+ """
+ symbol = await self.exchange_symbol_associated_to_pair(trading_pair)
+
+ # Build order parameters according to Orderly API spec
+ # Map Hummingbot order types to Orderly order types
+ orderly_order_type = "MARKET"
+ if order_type == OrderType.LIMIT:
+ orderly_order_type = "LIMIT"
+ elif order_type == OrderType.LIMIT_MAKER:
+ orderly_order_type = "POST_ONLY"
+
+ order_params = {
+ "symbol": symbol,
+ "client_order_id": order_id,
+ "side": "BUY" if trade_type == TradeType.BUY else "SELL",
+ "order_type": orderly_order_type,
+ "order_quantity": float(self.quantize_order_amount(trading_pair, amount)),
+ "reduce_only": position_action == PositionAction.CLOSE,
+ }
+
+ # Add price for non-MARKET orders
+ if order_type != OrderType.MARKET:
+ order_params["order_price"] = float(self.quantize_order_price(trading_pair, price))
+
+ # Make API call
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.CREATE_ORDER_URL,
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.CREATE_ORDER_URL,
+ method=RESTMethod.POST,
+ data=order_params,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ raise IOError(f"Order placement failed: {response}")
+
+ data = response.get("data", {})
+ exchange_order_id = str(data.get("order_id"))
+
+ return exchange_order_id, self.current_timestamp
+
+ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder):
+ """
+ Cancel an order.
+
+ Args:
+ order_id: Client order ID
+ tracked_order: Tracked order object
+ """
+ symbol = await self.exchange_symbol_associated_to_pair(tracked_order.trading_pair)
+
+ # Use exchange_order_id if available, otherwise use client_order_id
+ # Orderly has separate endpoints:
+ # - /v1/order: Cancel by exchange_order_id (requires order_id param)
+ # - /v1/client/order: Cancel by client_order_id (requires client_order_id param)
+ if tracked_order.exchange_order_id:
+ # Cancel by exchange_order_id
+ params = {
+ "order_id": str(tracked_order.exchange_order_id),
+ "symbol": symbol,
+ }
+ url = web_utils.public_rest_url(
+ CONSTANTS.CANCEL_ORDER_URL,
+ domain=self._domain
+ )
+ throttler_limit_id = CONSTANTS.CANCEL_ORDER_URL
+ else:
+ # Cancel by client_order_id
+ params = {
+ "client_order_id": str(order_id),
+ "symbol": symbol,
+ }
+ url = web_utils.public_rest_url(
+ CONSTANTS.CANCEL_ORDER_BY_CLIENT_ID_URL,
+ domain=self._domain
+ )
+ throttler_limit_id = CONSTANTS.CANCEL_ORDER_BY_CLIENT_ID_URL
+
+ # Make API call
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=throttler_limit_id,
+ method=RESTMethod.DELETE,
+ params=params,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ raise IOError(f"Order cancellation failed: {response}")
+
+ async def batch_order_create(
+ self,
+ orders_to_create: List[Dict[str, Any]]
+ ) -> List[Tuple[str, float]]:
+ """
+ Place multiple orders in a single batch request using modular pattern.
+
+ This method:
+ 1. Generates client_order_ids for each order
+ 2. Tracks and validates each order using _start_tracking_and_validate_order()
+ 3. Makes batch API call with valid orders
+ 4. Processes results through order tracker (_update_order_after_creation_success
+ or _on_order_creation_failure)
+
+ Args:
+ orders_to_create: List of order dictionaries with keys:
+ - order_id: Client order ID (string, optional - will be generated if not provided)
+ - trading_pair: Trading pair in Hummingbot format
+ - amount: Order amount (Decimal)
+ - trade_type: TradeType.BUY or TradeType.SELL
+ - order_type: OrderType (LIMIT, LIMIT_MAKER, or MARKET)
+ - price: Order price (Decimal)
+ - position_action: PositionAction (OPEN or CLOSE)
+
+ Returns:
+ List of (exchange_order_id, timestamp) tuples for each order.
+ If an order fails, exchange_order_id will be empty string.
+
+ Raises:
+ ValueError: If more than 10 orders provided (Orderly limitation)
+ IOError: If the API request itself fails
+ """
+ # Validation: Check batch size limit
+ if len(orders_to_create) > 10:
+ raise ValueError(
+ f"Batch order creation limited to 10 orders per request. "
+ f"Received {len(orders_to_create)} orders."
+ )
+
+ if not orders_to_create:
+ self.logger().warning("[BATCH ORDER] No orders to create")
+ return []
+
+ # Step 1: Generate client_order_ids and track/validate orders
+ inflight_orders_to_create = []
+ order_id_map = {} # Map index to order_id for result matching
+
+ for i, order_data in enumerate(orders_to_create):
+ try:
+ # Extract order parameters
+ order_id = order_data.get("order_id")
+
+ # Generate client_order_id if not provided
+ if not order_id:
+ order_id = get_new_client_order_id(
+ is_buy=order_data["trade_type"] == TradeType.BUY,
+ trading_pair=order_data["trading_pair"],
+ hbot_order_id_prefix=self.client_order_id_prefix,
+ max_id_len=self.client_order_id_max_length,
+ )
+ order_data["order_id"] = order_id
+
+ trading_pair = order_data["trading_pair"]
+ amount = order_data["amount"]
+ trade_type = order_data["trade_type"]
+ order_type = order_data["order_type"]
+ price = order_data.get("price")
+ position_action = order_data.get("position_action", PositionAction.NIL)
+
+ # Track and validate order
+ valid_order = await self._start_tracking_and_validate_order(
+ trade_type=trade_type,
+ order_id=order_id,
+ trading_pair=trading_pair,
+ amount=amount,
+ order_type=order_type,
+ price=price,
+ position_action=position_action,
+ )
+
+ if valid_order is not None:
+ inflight_orders_to_create.append(valid_order)
+ order_id_map[i] = order_id
+ else:
+ # Order failed validation, already handled by _start_tracking_and_validate_order
+ order_id_map[i] = None
+
+ except Exception as e:
+ self.logger().error(
+ f"[BATCH ORDER] Error preparing order {order_data.get('order_id', 'unknown')}: {e}",
+ exc_info=True
+ )
+ order_id_map[i] = None
+
+ # Step 2: Build batch API request for valid orders
+ if not inflight_orders_to_create:
+ self.logger().error("[BATCH ORDER] No valid orders to submit after validation")
+ return [("", self.current_timestamp) for _ in orders_to_create]
+
+ batch_orders = []
+ for in_flight_order in inflight_orders_to_create:
+ try:
+ # Get exchange symbol
+ symbol = await self.exchange_symbol_associated_to_pair(in_flight_order.trading_pair)
+
+ # Map Hummingbot order types to Orderly order types
+ orderly_order_type = "MARKET"
+ if in_flight_order.order_type == OrderType.LIMIT:
+ orderly_order_type = "LIMIT"
+ elif in_flight_order.order_type == OrderType.LIMIT_MAKER:
+ orderly_order_type = "POST_ONLY"
+
+ # Build order parameters (price and amount already quantized)
+ order_params = {
+ "symbol": symbol,
+ "client_order_id": in_flight_order.client_order_id,
+ "side": "BUY" if in_flight_order.trade_type == TradeType.BUY else "SELL",
+ "order_type": orderly_order_type,
+ "order_quantity": float(in_flight_order.amount),
+ "reduce_only": in_flight_order.position == PositionAction.CLOSE,
+ }
+
+ # Add price for non-MARKET orders
+ if in_flight_order.order_type != OrderType.MARKET:
+ order_params["order_price"] = float(in_flight_order.price)
+
+ batch_orders.append(order_params)
+
+ except Exception as e:
+ self.logger().error(
+ f"[BATCH ORDER] Error building order params for {in_flight_order.client_order_id}: {e}",
+ exc_info=True
+ )
+
+ if not batch_orders:
+ self.logger().error("[BATCH ORDER] Failed to build batch order params")
+ return [("", self.current_timestamp) for _ in orders_to_create]
+
+ # Step 3: Make batch API call
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.BATCH_CREATE_ORDER_URL,
+ domain=self._domain
+ )
+
+ request_data = {"orders": batch_orders}
+
+ self.logger().info(f"[BATCH ORDER] Submitting batch of {len(batch_orders)} orders")
+
+ try:
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.BATCH_CREATE_ORDER_URL,
+ method=RESTMethod.POST,
+ data=request_data,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ self.logger().error(f"[BATCH ORDER] Batch order creation failed: {response}")
+ # Mark all orders as failed
+ for in_flight_order in inflight_orders_to_create:
+ self._on_order_creation_failure(
+ order_id=in_flight_order.client_order_id,
+ trading_pair=in_flight_order.trading_pair,
+ amount=in_flight_order.amount,
+ trade_type=in_flight_order.trade_type,
+ order_type=in_flight_order.order_type,
+ price=in_flight_order.price,
+ exception=IOError(f"Batch order creation failed: {response}"),
+ position_action=in_flight_order.position,
+ )
+ raise IOError(f"Batch order creation failed: {response}")
+
+ # Step 4: Process results through order tracker
+ data = response.get("data", {})
+ rows = data.get("rows", [])
+ timestamp = self.current_timestamp
+
+ # Map results by client_order_id
+ result_map = {row.get("client_order_id"): row for row in rows}
+
+ # Process each in-flight order
+ for in_flight_order in inflight_orders_to_create:
+ order_result = result_map.get(in_flight_order.client_order_id)
+
+ if order_result:
+ error_message = order_result.get("error_message", "")
+
+ # Check if order succeeded
+ if not error_message or error_message.lower() in ["none", "", "null"]:
+ exchange_order_id = str(order_result.get("order_id", ""))
+ self._update_order_after_creation_success(
+ exchange_order_id=exchange_order_id,
+ order=in_flight_order,
+ update_timestamp=timestamp,
+ )
+ self.logger().info(
+ f"[BATCH ORDER] Order {in_flight_order.client_order_id} created successfully "
+ f"with exchange_order_id {exchange_order_id}"
+ )
+ else:
+ # Order failed on exchange
+ self._on_order_creation_failure(
+ order_id=in_flight_order.client_order_id,
+ trading_pair=in_flight_order.trading_pair,
+ amount=in_flight_order.amount,
+ trade_type=in_flight_order.trade_type,
+ order_type=in_flight_order.order_type,
+ price=in_flight_order.price,
+ exception=IOError(f"Exchange error: {error_message}"),
+ position_action=in_flight_order.position,
+ )
+ self.logger().error(
+ f"[BATCH ORDER] Order {in_flight_order.client_order_id} failed: {error_message}"
+ )
+ else:
+ # Order not found in response
+ self._on_order_creation_failure(
+ order_id=in_flight_order.client_order_id,
+ trading_pair=in_flight_order.trading_pair,
+ amount=in_flight_order.amount,
+ trade_type=in_flight_order.trade_type,
+ order_type=in_flight_order.order_type,
+ price=in_flight_order.price,
+ exception=IOError("Order not found in response"),
+ position_action=in_flight_order.position,
+ )
+ self.logger().error(
+ f"[BATCH ORDER] Order {in_flight_order.client_order_id} not found in response"
+ )
+
+ # Build return results (maintain order with original indices)
+ results = []
+ for i in range(len(orders_to_create)):
+ order_id = order_id_map.get(i)
+
+ if order_id is None:
+ # Order failed validation
+ results.append(("", timestamp))
+ else:
+ # Look up result
+ order_result = result_map.get(order_id)
+ if order_result:
+ error_message = order_result.get("error_message", "")
+ if not error_message or error_message.lower() in ["none", "", "null"]:
+ exchange_order_id = str(order_result.get("order_id", ""))
+ results.append((exchange_order_id, timestamp))
+ else:
+ results.append(("", timestamp))
+ else:
+ results.append(("", timestamp))
+
+ success_count = sum(1 for exchange_id, _ in results if exchange_id)
+ self.logger().info(
+ f"[BATCH ORDER] Batch completed: {success_count}/{len(orders_to_create)} orders successful"
+ )
+
+ return results
+
+ except IOError:
+ # Re-raise IOError (API request failed)
+ raise
+ except Exception as e:
+ self.logger().error(f"[BATCH ORDER] Unexpected error in batch order creation: {e}", exc_info=True)
+ # Mark all in-flight orders as failed
+ for in_flight_order in inflight_orders_to_create:
+ self._on_order_creation_failure(
+ order_id=in_flight_order.client_order_id,
+ trading_pair=in_flight_order.trading_pair,
+ amount=in_flight_order.amount,
+ trade_type=in_flight_order.trade_type,
+ order_type=in_flight_order.order_type,
+ price=in_flight_order.price,
+ exception=e,
+ position_action=in_flight_order.position,
+ )
+ raise IOError(f"Batch order creation failed: {e}")
+
+ async def batch_order_cancel(
+ self,
+ orders_to_cancel: List[InFlightOrder]
+ ) -> List[Dict[str, Any]]:
+ """
+ Cancel multiple orders in a single batch request using modular pattern.
+
+ This method:
+ 1. Makes batch API call to cancel orders
+ 2. Processes results through order tracker (creates OrderUpdate with CANCELED state)
+ 3. Triggers appropriate cancellation events
+
+ Args:
+ orders_to_cancel: List of InFlightOrder objects to cancel
+
+ Returns:
+ List of cancellation result dictionaries with keys:
+ - client_order_id: Client order ID
+ - success: Boolean indicating if cancellation succeeded
+ - error_message: Error message if failed
+
+ Raises:
+ ValueError: If more than 10 orders provided (Orderly limitation)
+ IOError: If the API request itself fails
+ """
+ # Validation: Check batch size limit
+ if len(orders_to_cancel) > 10:
+ raise ValueError(
+ f"Batch order cancellation limited to 10 orders per request. "
+ f"Received {len(orders_to_cancel)} orders."
+ )
+
+ if not orders_to_cancel:
+ self.logger().warning("[BATCH CANCEL] No orders to cancel")
+ return []
+
+ # Collect order IDs and symbols
+ # Prefer exchange_order_id when available, fall back to client_order_id
+ exchange_order_ids = []
+ client_order_ids = []
+ use_exchange_ids = True
+
+ # Get symbols (Orderly requires symbol parameter)
+ symbols = set()
+
+ for order in orders_to_cancel:
+ symbols.add(order.trading_pair)
+
+ if order.exchange_order_id:
+ exchange_order_ids.append(str(order.exchange_order_id))
+ else:
+ client_order_ids.append(order.client_order_id)
+ use_exchange_ids = False # Must use client_order_id endpoint if any order lacks exchange_order_id
+
+ # If mixed IDs or no exchange IDs, use client_order_id endpoint
+ if not use_exchange_ids or not exchange_order_ids:
+ use_exchange_ids = False
+ # Collect all client_order_ids
+ client_order_ids = [order.client_order_id for order in orders_to_cancel]
+
+ # Get exchange symbols for all trading pairs
+ exchange_symbols = []
+ for trading_pair in symbols:
+ try:
+ symbol = await self.exchange_symbol_associated_to_pair(trading_pair)
+ exchange_symbols.append(symbol)
+ except Exception as e:
+ self.logger().error(
+ f"[BATCH CANCEL] Error getting exchange symbol for {trading_pair}: {e}"
+ )
+
+ if not exchange_symbols:
+ self.logger().error("[BATCH CANCEL] No valid symbols found")
+ return [
+ {
+ "client_order_id": order.client_order_id,
+ "success": False,
+ "error_message": "No valid symbols found"
+ }
+ for order in orders_to_cancel
+ ]
+
+ # Build API request
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+
+ if use_exchange_ids:
+ # Use DELETE /v1/batch-order with exchange order_ids
+ url = web_utils.public_rest_url(
+ CONSTANTS.BATCH_CANCEL_ORDER_URL,
+ domain=self._domain
+ )
+ throttler_limit_id = CONSTANTS.BATCH_CANCEL_ORDER_URL
+
+ # Build comma-separated order_ids parameter
+ order_ids_str = ",".join(exchange_order_ids)
+ params = {
+ "order_ids": order_ids_str,
+ "symbol": exchange_symbols[0] # Use first symbol (typically all orders are same symbol)
+ }
+
+ self.logger().info(
+ f"[BATCH CANCEL] Cancelling {len(exchange_order_ids)} orders by exchange_order_id"
+ )
+ else:
+ # Use DELETE /v1/client/batch-order with client_order_ids
+ url = web_utils.public_rest_url(
+ CONSTANTS.BATCH_CANCEL_ORDER_BY_CLIENT_ID_URL,
+ domain=self._domain
+ )
+ throttler_limit_id = CONSTANTS.BATCH_CANCEL_ORDER_BY_CLIENT_ID_URL
+
+ # Build comma-separated client_order_ids parameter
+ client_order_ids_str = ",".join(client_order_ids)
+ params = {
+ "client_order_ids": client_order_ids_str,
+ "symbol": exchange_symbols[0] # Use first symbol
+ }
+
+ self.logger().info(
+ f"[BATCH CANCEL] Cancelling {len(client_order_ids)} orders by client_order_id"
+ )
+
+ try:
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=throttler_limit_id,
+ method=RESTMethod.DELETE,
+ params=params,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ # Check if this is an "order not found/invalid" error - orders don't exist on exchange
+ error_msg = str(response)
+ error_exception = IOError(f"Batch order cancellation failed: {response}")
+
+ if self._is_order_not_found_during_cancelation_error(error_exception):
+ # Orders don't exist on exchange - mark all as cancelled locally
+ self.logger().warning(
+ f"[BATCH CANCEL] Orders not found/invalid on exchange - marking all as cancelled locally: {response}"
+ )
+ timestamp = self.current_timestamp
+ results = []
+ for order in orders_to_cancel:
+ order_update = OrderUpdate(
+ client_order_id=order.client_order_id,
+ exchange_order_id=order.exchange_order_id,
+ trading_pair=order.trading_pair,
+ update_timestamp=timestamp,
+ new_state=OrderState.CANCELED,
+ )
+ self._order_tracker.process_order_update(order_update)
+ results.append({
+ "client_order_id": order.client_order_id,
+ "success": True,
+ "error_message": "Order not found/invalid on exchange (already cancelled/filled)"
+ })
+ return results
+
+ # Other errors - raise exception
+ self.logger().error(f"[BATCH CANCEL] Batch cancellation failed: {response}")
+ raise IOError(f"Batch order cancellation failed: {response}")
+
+ # Process response through order tracker
+ data = response.get("data", {})
+ rows = data.get("rows", [])
+ timestamp = self.current_timestamp
+
+ # Build results list
+ results = []
+
+ # Create a map of results by order ID
+ if use_exchange_ids:
+ result_map = {str(row.get("order_id")): row for row in rows if row.get("order_id")}
+ else:
+ result_map = {row.get("client_order_id"): row for row in rows if row.get("client_order_id")}
+
+ # Match results to original orders and process through order tracker
+ for order in orders_to_cancel:
+ lookup_id = str(order.exchange_order_id) if use_exchange_ids else order.client_order_id
+
+ if lookup_id in result_map:
+ order_result = result_map[lookup_id]
+ error_message = order_result.get("error_message", "")
+
+ if not error_message or error_message.lower() in ["none", "", "null"]:
+ # Cancellation succeeded - process through order tracker
+ order_update = OrderUpdate(
+ client_order_id=order.client_order_id,
+ exchange_order_id=order.exchange_order_id,
+ trading_pair=order.trading_pair,
+ update_timestamp=timestamp,
+ new_state=OrderState.CANCELED,
+ )
+ self._order_tracker.process_order_update(order_update)
+
+ results.append({
+ "client_order_id": order.client_order_id,
+ "success": True,
+ "error_message": ""
+ })
+ self.logger().info(
+ f"[BATCH CANCEL] Order {order.client_order_id} cancelled successfully"
+ )
+ else:
+ # Cancellation failed - log but don't update order state
+ results.append({
+ "client_order_id": order.client_order_id,
+ "success": False,
+ "error_message": error_message
+ })
+ self.logger().error(
+ f"[BATCH CANCEL] Order {order.client_order_id} cancellation failed: {error_message}"
+ )
+ else:
+ # Order not found in response - exchange confirms it doesn't exist
+ # Mark as cancelled locally to sync with exchange state
+ self.logger().warning(
+ f"[BATCH CANCEL] Order {order.client_order_id} not found in response - marking as cancelled locally"
+ )
+ order_update = OrderUpdate(
+ client_order_id=order.client_order_id,
+ exchange_order_id=order.exchange_order_id,
+ trading_pair=order.trading_pair,
+ update_timestamp=timestamp,
+ new_state=OrderState.CANCELED,
+ )
+ self._order_tracker.process_order_update(order_update)
+
+ results.append({
+ "client_order_id": order.client_order_id,
+ "success": True, # Consider as success (order doesn't exist on exchange)
+ "error_message": "Order not found in response (already cancelled/filled)"
+ })
+
+ success_count = sum(1 for result in results if result["success"])
+ self.logger().info(
+ f"[BATCH CANCEL] Batch completed: {success_count}/{len(orders_to_cancel)} orders cancelled"
+ )
+
+ return results
+
+ except IOError:
+ # Re-raise IOError (API request failed)
+ raise
+ except Exception as e:
+ self.logger().error(f"[BATCH CANCEL] Unexpected error in batch cancellation: {e}", exc_info=True)
+ raise IOError(f"Batch order cancellation failed: {e}")
+
+ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate:
+ """
+ Request order status from exchange.
+
+ Args:
+ tracked_order: Order to check status for
+
+ Returns:
+ OrderUpdate with current status
+ """
+ exchange_order_id = tracked_order.exchange_order_id
+
+ if not exchange_order_id:
+ return OrderUpdate(
+ trading_pair=tracked_order.trading_pair,
+ update_timestamp=self.current_timestamp,
+ new_state=OrderState.FAILED,
+ client_order_id=tracked_order.client_order_id,
+ )
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.GET_ORDER_URL.format(order_id=exchange_order_id),
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.GET_ORDER_URL,
+ method=RESTMethod.GET,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ raise IOError(f"Failed to fetch order status: {response}")
+
+ data = response.get("data", {})
+ order_state = CONSTANTS.ORDER_STATE.get(data.get("status"), OrderState.OPEN)
+
+ return OrderUpdate(
+ trading_pair=tracked_order.trading_pair,
+ update_timestamp=data.get("updated_time", self.current_timestamp) * 1e-3,
+ new_state=order_state,
+ client_order_id=tracked_order.client_order_id,
+ exchange_order_id=exchange_order_id,
+ )
+
+ async def _update_order_status(self):
+ """Update status of all active orders"""
+ await super()._update_order_status()
+
+ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]:
+ """
+ Fetches all trade updates for a specific order from Orderly.
+
+ Uses the GET /v1/order/{order_id}/trades endpoint to fetch all fills for an order.
+
+ Args:
+ order: The InFlightOrder to fetch trades for
+
+ Returns:
+ List of TradeUpdate objects representing all fills for this order
+ """
+ trade_updates = []
+
+ try:
+ exchange_order_id = await order.get_exchange_order_id()
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.GET_ORDER_TRADES_URL.format(order_id=str(exchange_order_id)),
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.GET_ORDER_TRADES_URL,
+ method=RESTMethod.GET,
+ is_auth_required=True
+ )
+
+ if not response.get("success", False):
+ self.logger().warning(f"Failed to fetch trades for order {order.client_order_id}: {response}")
+ return trade_updates
+
+ data = response.get("data", {})
+ rows = data.get("rows", [])
+
+ for trade in rows:
+ # Determine position action (OPEN or CLOSE)
+ # Orderly returns side as "BUY" or "SELL" for the trade
+ position_action = PositionAction.OPEN # Default
+
+ # Parse fee information
+ fee_asset = trade.get("fee_asset", order.quote_asset)
+ fee_amount = Decimal(str(trade.get("fee", "0")))
+
+ fee = TradeFeeBase.new_perpetual_fee(
+ fee_schema=self.trade_fee_schema(),
+ position_action=position_action,
+ percent_token=fee_asset,
+ flat_fees=[TokenAmount(amount=fee_amount, token=fee_asset)] if fee_amount > 0 else []
+ )
+
+ # Create TradeUpdate
+ trade_update = TradeUpdate(
+ trade_id=str(trade.get("id")),
+ client_order_id=order.client_order_id,
+ exchange_order_id=str(trade.get("order_id")),
+ trading_pair=order.trading_pair,
+ fill_timestamp=int(trade.get("executed_timestamp", 0) * 1e-3),
+ fill_price=Decimal(str(trade.get("executed_price", "0"))),
+ fill_base_amount=Decimal(str(trade.get("executed_quantity", "0"))),
+ fill_quote_amount=Decimal(str(trade.get("executed_price", "0"))) * Decimal(str(trade.get("executed_quantity", "0"))),
+ fee=fee,
+ )
+
+ trade_updates.append(trade_update)
+
+ except asyncio.TimeoutError:
+ raise IOError(f"Skipped order trade updates for {order.client_order_id} - waiting for exchange order id.")
+ except Exception as e:
+ self.logger().warning(f"Failed to fetch trade updates for order {order.client_order_id}: {e}")
+
+ return trade_updates
+
+ # ============================================================
+ # Position Management
+ # ============================================================
+ async def _update_positions(self):
+ """Fetch and update positions"""
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.POSITIONS_URL,
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.POSITIONS_URL,
+ method=RESTMethod.GET,
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ self.logger().error(f"Failed to fetch positions: {response}")
+ return
+
+ positions_data = response.get("data", {}).get("rows", [])
+
+ for position_data in positions_data:
+ try:
+ symbol = position_data["symbol"]
+ trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol)
+
+ position_qty = Decimal(str(position_data.get("position_qty", "0")))
+
+ # Determine position side before checking for zero (needed for pos_key)
+ position_side = PositionSide.LONG if position_qty > 0 else PositionSide.SHORT
+ pos_key = self._perpetual_trading.position_key(trading_pair, position_side)
+
+ if position_qty == 0:
+ # Remove position if it exists
+ self._perpetual_trading.remove_position(pos_key)
+ continue
+
+ unrealized_pnl = Decimal(str(position_data.get("unrealized_pnl", "0")))
+ entry_price = Decimal(str(position_data.get("average_open_price", "0")))
+ leverage = Decimal(str(position_data.get("leverage", "1")))
+
+ position = self._perpetual_trading.get_position(trading_pair, position_side)
+ if position is not None:
+ position.update_position(
+ position_side=position_side,
+ unrealized_pnl=unrealized_pnl,
+ entry_price=entry_price,
+ amount=abs(position_qty),
+ )
+ else:
+ _position = Position(
+ trading_pair=trading_pair,
+ position_side=position_side,
+ unrealized_pnl=unrealized_pnl,
+ entry_price=entry_price,
+ amount=abs(position_qty),
+ leverage=leverage,
+ )
+ self._perpetual_trading.set_leverage(trading_pair, int(leverage))
+ self._perpetual_trading.set_position(pos_key, _position)
+
+ except Exception:
+ self.logger().exception(f"Error updating position: {position_data}")
+
+ async def _set_trading_pair_leverage(self, trading_pair: str, leverage: int) -> Tuple[bool, str]:
+ """
+ Set leverage for a trading pair.
+
+ Args:
+ trading_pair: Trading pair
+ leverage: Leverage value
+
+ Returns:
+ Tuple of (success, message)
+ """
+ try:
+ symbol = await self.exchange_symbol_associated_to_pair(trading_pair)
+
+ data = {
+ "symbol": symbol,
+ "leverage": leverage,
+ }
+
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.SET_LEVERAGE_URL,
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.SET_LEVERAGE_URL,
+ method=RESTMethod.POST,
+ data=data,
+ is_auth_required=True
+ )
+
+ if response.get("success", False):
+ return True, f"Leverage set to {leverage}x for {trading_pair}"
+ else:
+ error_msg = response.get("message", "Unknown error")
+ return False, f"Failed to set leverage: {error_msg}"
+
+ except Exception as e:
+ return False, f"Error setting leverage: {str(e)}"
+
+ async def _get_position_mode(self) -> Optional[PositionMode]:
+ """
+ Get current position mode.
+
+ Orderly only supports ONEWAY position mode (single position per symbol).
+
+ Returns:
+ PositionMode.ONEWAY - Orderly only supports one-way mode
+ """
+ return PositionMode.ONEWAY
+
+ async def _trading_pair_position_mode_set(self, mode: PositionMode, trading_pair: str) -> Tuple[bool, str]:
+ """
+ Set position mode (Orderly only supports ONEWAY).
+
+ Args:
+ mode: Position mode
+ trading_pair: Trading pair
+
+ Returns:
+ Tuple of (success, message)
+ """
+ if mode != PositionMode.ONEWAY:
+ return False, "Orderly only supports ONEWAY position mode"
+ return True, "Position mode is ONEWAY"
+
+ # ============================================================
+ # Balance Management
+ # ============================================================
+
+ async def _update_balances(self):
+ """Fetch and update account balances"""
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.ACCOUNT_HOLDING_URL,
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.ACCOUNT_HOLDING_URL,
+ method=RESTMethod.GET,
+ is_auth_required=True,
+ )
+ data = response.get("data", {})
+ holdings = data.get("holding", [])
+
+ self._account_balances.clear()
+ self._account_available_balances.clear()
+
+ for holding in holdings:
+ token = holding.get("token")
+ total = Decimal(str(holding.get("holding", "0")))
+ frozen = Decimal(str(holding.get("frozen", "0")))
+ available = total - frozen
+
+ self._account_balances[token] = total
+ self._account_available_balances[token] = available
+
+ # ============================================================
+ # Funding
+ # ============================================================
+
+ async def _fetch_last_fee_payment(self, trading_pair: str) -> Tuple[int, Decimal, Decimal]:
+ """
+ Fetch last funding payment.
+
+ Args:
+ trading_pair: Trading pair
+
+ Returns:
+ Tuple of (timestamp, funding_rate, payment_amount)
+ """
+ try:
+ symbol = await self.exchange_symbol_associated_to_pair(trading_pair)
+ rest_assistant = await self._web_assistants_factory.get_rest_assistant()
+ url = web_utils.public_rest_url(
+ CONSTANTS.FUNDING_FEE_HISTORY_URL,
+ domain=self._domain
+ )
+ response = await rest_assistant.execute_request(
+ url=url,
+ throttler_limit_id=CONSTANTS.FUNDING_FEE_HISTORY_URL,
+ method=RESTMethod.GET,
+ params={"symbol": symbol, "size": "1"},
+ is_auth_required=True,
+ )
+
+ if not response.get("success", False):
+ return 0, Decimal("-1"), Decimal("-1")
+
+ data = response.get("data", {})
+ rows = data.get("rows", [])
+
+ if not rows:
+ return 0, Decimal("-1"), Decimal("-1")
+
+ last_payment = rows[0]
+ timestamp = int(last_payment.get("timestamp", 0) * 1e-3)
+ funding_rate = Decimal(str(last_payment.get("funding_rate", "0")))
+ payment = Decimal(str(last_payment.get("funding_fee", "0")))
+
+ return timestamp, funding_rate, payment
+
+ except Exception:
+ self.logger().exception(f"Error fetching funding payment for {trading_pair}")
+ return 0, Decimal("-1"), Decimal("-1")
+
+ # ============================================================
+ # Fees
+ # ============================================================
+
+ async def _update_trading_fees(self):
+ """
+ Update fees information from the exchange.
+
+ Note: Orderly provides fee information in the account info endpoint,
+ but fees are already handled per-trade. This method is stubbed as
+ fees are retrieved with each trade/order response.
+ """
+ pass
+
+ def _get_fee(
+ self,
+ base_currency: str,
+ quote_currency: str,
+ order_type: OrderType,
+ order_side: TradeType,
+ position_action: PositionAction,
+ amount: Decimal,
+ price: Decimal = Decimal("NaN"),
+ is_maker: Optional[bool] = None,
+ ) -> TradeFeeBase:
+ """
+ Calculate trading fees.
+
+ Args:
+ base_currency: Base currency
+ quote_currency: Quote currency
+ order_type: Order type
+ order_side: Trade side
+ amount: Order amount
+ price: Order price
+ is_maker: Whether order is maker
+
+ Returns:
+ TradeFeeBase object
+ """
+ is_maker = is_maker or False
+ return build_trade_fee(
+ exchange=self.name,
+ is_maker=is_maker,
+ base_currency=base_currency,
+ quote_currency=quote_currency,
+ order_type=order_type,
+ order_side=order_side,
+ amount=amount,
+ price=price,
+ )
+
+ # ============================================================
+ # User Stream Event Handling
+ # ============================================================
+
+ async def _user_stream_event_listener(self):
+ """
+ Listen to user stream messages and process them.
+
+ Handles order updates, trade updates, position updates, and balance updates.
+ """
+ async for event_message in self._iter_user_event_queue():
+ try:
+ topic = event_message.get("topic")
+
+ if topic == CONSTANTS.WS_EXECUTION_REPORT_CHANNEL:
+ await self._process_order_event(event_message)
+ elif topic == CONSTANTS.WS_POSITION_CHANNEL:
+ await self._process_position_event(event_message)
+ elif topic == CONSTANTS.WS_BALANCE_CHANNEL:
+ await self._process_balance_event(event_message)
+
+ except asyncio.CancelledError:
+ raise
+ except Exception:
+ self.logger().error("Unexpected error in user stream listener", exc_info=True)
+
+ async def _process_order_event(self, event: Dict[str, Any]):
+ """Process order update event from WebSocket"""
+ data = event.get("data", {})
+
+ client_order_id = data.get("client_order_id")
+ if not client_order_id:
+ return
+
+ tracked_order = self._order_tracker.all_updatable_orders.get(client_order_id)
+ if not tracked_order:
+ return
+
+ new_state = CONSTANTS.ORDER_STATE.get(data.get("status"), OrderState.OPEN)
+
+ order_update = OrderUpdate(
+ trading_pair=tracked_order.trading_pair,
+ update_timestamp=data.get("timestamp", self.current_timestamp) * 1e-3,
+ new_state=new_state,
+ client_order_id=client_order_id,
+ exchange_order_id=str(data.get("order_id", "")),
+ )
+
+ self._order_tracker.process_order_update(order_update)
+
+ async def _process_position_event(self, event: Dict[str, Any]):
+ """Process position update event from WebSocket"""
+ # Trigger position update with the event data itself instead of fetching via the rest client
+ await self._update_positions()
+
+ async def _process_balance_event(self, event: Dict[str, Any]):
+ """Process balance update event from WebSocket"""
+ # Trigger balance update
+ await self._update_balances()
+
+ # ============================================================
+ # Status Polling
+ # ============================================================
+
+ async def _status_polling_loop_fetch_updates(self):
+ """Fetch updates in status polling loop"""
+ await safe_gather(
+ self._update_order_status(),
+ self._update_balances(),
+ self._update_positions(),
+ )
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_user_stream_data_source.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_user_stream_data_source.py
new file mode 100644
index 00000000000..69fb6e7fff0
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_user_stream_data_source.py
@@ -0,0 +1,418 @@
+"""
+User Stream Data Source for Orderly Network Perpetual Connector
+
+This module handles:
+- Private WebSocket connections with authentication
+- Order execution updates (fills, cancellations, rejections)
+- Position updates
+- Balance updates
+- Account-level events
+
+Reference: Orderly Network EVM API
+https://orderly.network/docs/build-on-omnichain/evm-api/websocket-api/private
+"""
+
+import asyncio
+import json
+import logging
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
+
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_constants as CONSTANTS
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_web_utils as web_utils
+from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_auth import OrderlyPerpetualAuth
+from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource
+from hummingbot.core.web_assistant.connections.data_types import WSJSONRequest
+from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
+from hummingbot.core.web_assistant.ws_assistant import WSAssistant
+from hummingbot.logger import HummingbotLogger
+
+if TYPE_CHECKING:
+ from hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_derivative import (
+ OrderlyPerpetualDerivative,
+ )
+
+
+class OrderlyPerpetualUserStreamDataSource(UserStreamTrackerDataSource):
+ """
+ User stream data source for Orderly Network Perpetual.
+
+ Manages private WebSocket connection for:
+ - Order execution reports
+ - Position updates
+ - Balance changes
+ """
+
+ _logger: Optional[HummingbotLogger] = None
+
+ def __init__(
+ self,
+ auth: OrderlyPerpetualAuth,
+ trading_pairs: List[str],
+ connector: 'OrderlyPerpetualDerivative',
+ api_factory: WebAssistantsFactory,
+ domain: str = CONSTANTS.DOMAIN,
+ ):
+ """
+ Initialize the user stream data source.
+
+ Args:
+ auth: Authentication handler
+ trading_pairs: List of trading pairs to track
+ connector: Reference to main connector instance
+ api_factory: Factory for creating REST/WS assistants
+ domain: Domain identifier (mainnet or testnet)
+ """
+ super().__init__()
+ self._auth = auth
+ self._domain = domain
+ self._api_factory = api_factory
+ self._connector = connector
+ self._trading_pairs = trading_pairs
+ self._last_ws_message_timestamp = 0
+
+ @classmethod
+ def logger(cls) -> HummingbotLogger:
+ """Get logger instance"""
+ if cls._logger is None:
+ cls._logger = logging.getLogger(__name__)
+ return cls._logger
+
+ @property
+ def last_recv_time(self) -> float:
+ """
+ Return the time of the last received message.
+
+ Returns:
+ Timestamp of last received WebSocket message
+ """
+ return self._last_ws_message_timestamp
+
+ async def _connected_websocket_assistant(self) -> WSAssistant:
+ """
+ Create and connect a WebSocket assistant for private stream.
+
+ Orderly private WebSocket URL includes account_id:
+ wss://ws-private-evm.orderly.org/v2/ws/private/stream/{account_id}
+
+ Returns:
+ Connected WSAssistant instance
+ """
+ # Get account ID from auth
+ account_id = self._auth.account_id
+ self.logger().info(f"[WEBSOCKET] Preparing private WebSocket connection with account_id: {account_id}")
+
+ # Build private WebSocket URL with account_id
+ ws_url = web_utils.wss_url(
+ endpoint_type="private",
+ domain=self._domain,
+ account_id=account_id
+ )
+
+ self.logger().info(f"[WEBSOCKET] Attempting to connect to private WebSocket: {ws_url}")
+
+ try:
+ # Create WebSocket assistant
+ ws: WSAssistant = await self._api_factory.get_ws_assistant()
+ self.logger().debug(f"[WEBSOCKET] WSAssistant created, connecting to: {ws_url}")
+
+ # Connect to private WebSocket
+ await ws.connect(
+ ws_url=ws_url,
+ ping_timeout=CONSTANTS.HEARTBEAT_TIME_INTERVAL
+ )
+
+ self.logger().info(f"[WEBSOCKET] Successfully connected to private WebSocket: {ws_url}")
+
+ # Authenticate the connection
+ await self._authenticate(ws)
+
+ return ws
+
+ except Exception as e:
+ self.logger().error(
+ f"[WEBSOCKET] Failed to connect to private WebSocket: {ws_url}. "
+ f"Error type: {type(e).__name__}, Error: {str(e)}",
+ exc_info=True
+ )
+ raise
+
+ async def _authenticate(self, ws: WSAssistant):
+ """
+ Authenticate the WebSocket connection.
+
+ Orderly private WebSocket requires authentication after connection:
+ {
+ "id": "auth",
+ "event": "auth",
+ "params": {
+ "orderly_key": "ed25519:BASE58_PUBLIC_KEY",
+ "sign": "BASE58_SIGNATURE",
+ "timestamp": 1683270060000
+ }
+ }
+
+ The signature is: sign(timestamp) using ed25519 private key
+
+ Note: The server may send ping messages before/after auth response.
+ We handle these by responding with pong and continuing to wait for auth.
+
+ Args:
+ ws: WebSocket assistant to authenticate
+ """
+ try:
+ # Generate authentication payload
+ auth_payload = await self._auth.get_ws_auth_payload()
+
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Generated auth payload - id: {auth_payload.get('id')}, "
+ f"event: {auth_payload.get('event')}, timestamp: {auth_payload.get('params', {}).get('timestamp')}"
+ )
+ self.logger().debug(
+ f"[WEBSOCKET AUTH] Full auth payload: {auth_payload}"
+ )
+
+ # Send authentication message
+ auth_request = WSJSONRequest(payload=auth_payload)
+ await ws.send(auth_request)
+
+ self.logger().info("[WEBSOCKET AUTH] Authentication request sent, waiting for response...")
+
+ # Wait for authentication response
+ # Note: Server may send ping messages before auth response, so we need to loop
+ max_attempts = 10 # Prevent infinite loops
+ attempt = 0
+ auth_response = None
+
+ while attempt < max_attempts:
+ attempt += 1
+ self.logger().debug(f"[WEBSOCKET AUTH] Waiting for message (attempt {attempt}/{max_attempts})...")
+
+ auth_response_raw = await ws.receive()
+
+ if auth_response_raw is None:
+ self.logger().warning(
+ f"[WEBSOCKET AUTH] No response received (attempt {attempt}/{max_attempts}), continuing to wait..."
+ )
+ continue
+
+ # WSResponse has a 'data' attribute that contains the actual message
+ response_data = auth_response_raw.data
+
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Received message (attempt {attempt}/{max_attempts}) - "
+ f"type: {type(response_data)}, raw data: {response_data}"
+ )
+
+ # Parse JSON if it's a string
+ if isinstance(response_data, str):
+ try:
+ message = json.loads(response_data)
+ except json.JSONDecodeError as e:
+ self.logger().error(
+ f"[WEBSOCKET AUTH] Failed to parse JSON string: {response_data}, error: {e}"
+ )
+ continue
+ elif isinstance(response_data, dict):
+ message = response_data
+ else:
+ # Try to parse as JSON if it's bytes or other format
+ try:
+ message = json.loads(str(response_data))
+ except (json.JSONDecodeError, ValueError) as e:
+ self.logger().error(
+ f"[WEBSOCKET AUTH] Failed to parse response data: {response_data}, "
+ f"type: {type(response_data)}, error: {e}"
+ )
+ continue
+
+ # Extract event type for logging
+ event_type = message.get("event")
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Parsed message - event: {event_type}, "
+ f"full message: {message}"
+ )
+
+ # Handle ping messages - respond with pong and continue waiting
+ if event_type == "ping":
+ timestamp = message.get("ts")
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Received ping message (ts: {timestamp}), responding with pong..."
+ )
+ # Echo back the timestamp from ping, or use current time if not provided
+ pong_response = {
+ "event": "pong",
+ "ts": timestamp if timestamp is not None else int(asyncio.get_event_loop().time() * 1000)
+ }
+ pong_request = WSJSONRequest(payload=pong_response)
+ await ws.send(pong_request)
+ self.logger().info(f"[WEBSOCKET AUTH] Sent pong response with ts: {pong_response.get('ts')}")
+ continue # Continue waiting for auth response
+
+ # Handle pong messages (server acknowledgment) - just log and continue
+ if event_type == "pong":
+ self.logger().debug(f"[WEBSOCKET AUTH] Received pong message, continuing to wait for auth...")
+ continue
+
+ # Handle subscription confirmations - continue waiting for auth
+ if event_type in ["subscribe", "unsubscribe"]:
+ self.logger().debug(
+ f"[WEBSOCKET AUTH] Received {event_type} message, continuing to wait for auth..."
+ )
+ continue
+
+ # Check if this is the authentication response
+ if event_type == "auth":
+ auth_response = message
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Received authentication response on attempt {attempt}: {auth_response}"
+ )
+ break
+
+ # If we get here, it's an unexpected message type
+ self.logger().warning(
+ f"[WEBSOCKET AUTH] Received unexpected message type '{event_type}' during authentication. "
+ f"Full message: {message}. Continuing to wait for auth response..."
+ )
+
+ # Check if we got an auth response
+ if auth_response is None:
+ self.logger().error(
+ f"[WEBSOCKET AUTH] No authentication response received after {max_attempts} attempts"
+ )
+ raise IOError(f"No authentication response received after {max_attempts} message attempts")
+
+ # Verify authentication was successful
+ if auth_response.get("success", False):
+ self.logger().info(
+ f"[WEBSOCKET AUTH] Successfully authenticated private WebSocket connection. "
+ f"Response: {auth_response}"
+ )
+ else:
+ error_msg = auth_response.get("message", "Unknown authentication error")
+ error_code = auth_response.get("code", "N/A")
+ self.logger().error(
+ f"[WEBSOCKET AUTH] Authentication failed - code: {error_code}, message: {error_msg}, "
+ f"full response: {auth_response}"
+ )
+ raise IOError(f"WebSocket authentication failed: {error_msg}")
+
+ except Exception as e:
+ self.logger().error(
+ f"[WEBSOCKET AUTH] Error authenticating WebSocket: {e}. "
+ f"Error type: {type(e).__name__}",
+ exc_info=True
+ )
+ raise
+
+ async def _subscribe_channels(self, websocket_assistant: WSAssistant):
+ """
+ Subscribe to private channels after authentication.
+
+ Orderly private channels:
+ - executionreport: Order updates and fills
+ - position: Position changes
+ - balance: Balance updates
+
+ Subscription format:
+ {
+ "id": "unique-id",
+ "topic": "executionreport",
+ "event": "subscribe"
+ }
+
+ Args:
+ websocket_assistant: Authenticated WebSocket assistant
+ """
+ try:
+ # Subscribe to execution report (order updates and fills)
+ execution_payload = {
+ "id": "executionreport_subscribe",
+ "topic": CONSTANTS.WS_EXECUTION_REPORT_CHANNEL,
+ "event": "subscribe"
+ }
+ await websocket_assistant.send(WSJSONRequest(payload=execution_payload))
+
+ # Subscribe to position updates
+ position_payload = {
+ "id": "position_subscribe",
+ "topic": CONSTANTS.WS_POSITION_CHANNEL,
+ "event": "subscribe"
+ }
+ await websocket_assistant.send(WSJSONRequest(payload=position_payload))
+
+ # Subscribe to balance updates
+ balance_payload = {
+ "id": "balance_subscribe",
+ "topic": CONSTANTS.WS_BALANCE_CHANNEL,
+ "event": "subscribe"
+ }
+ await websocket_assistant.send(WSJSONRequest(payload=balance_payload))
+
+ self.logger().info("Subscribed to private channels: executionreport, position, balance")
+
+ except asyncio.CancelledError:
+ raise
+ except Exception as e:
+ self.logger().error(f"Error subscribing to private channels: {e}", exc_info=True)
+ raise
+
+ async def _process_event_message(self, event_message: Dict[str, Any], queue: asyncio.Queue):
+ """
+ Process and route private channel messages to the output queue.
+
+ Orderly private message format:
+ {
+ "topic": "executionreport",
+ "ts": 1683270060000,
+ "data": {...}
+ }
+
+ Args:
+ event_message: WebSocket message from private channel
+ queue: Output queue for processed messages
+ """
+ # Update last message timestamp
+ self._last_ws_message_timestamp = event_message.get("ts", 0) / 1000
+
+ # Check message type
+ event_type = event_message.get("event")
+
+ # Skip subscription confirmations and pings
+ if event_type in ["subscribe", "unsubscribe", "pong"]:
+ return
+
+ # Handle authentication responses
+ if event_type == "auth":
+ if not event_message.get("success", False):
+ error_msg = event_message.get("message", "Authentication failed")
+ self.logger().error(f"WebSocket authentication error: {error_msg}")
+ return
+
+ # Handle error messages
+ if event_type == "error":
+ error_msg = event_message.get("message", "Unknown error")
+ self.logger().error(f"WebSocket error: {error_msg}")
+ return
+
+ # Route data messages to the queue
+ topic = event_message.get("topic")
+
+ if topic in [
+ CONSTANTS.WS_EXECUTION_REPORT_CHANNEL,
+ CONSTANTS.WS_POSITION_CHANNEL,
+ CONSTANTS.WS_BALANCE_CHANNEL,
+ ]:
+ # Forward the message to the connector for processing
+ queue.put_nowait(event_message)
+
+ async def _process_websocket_messages(self, websocket_assistant: WSAssistant, queue: asyncio.Queue):
+ """
+ Process incoming WebSocket messages continuously.
+
+ Args:
+ websocket_assistant: Connected WebSocket assistant
+ queue: Output queue for messages
+ """
+ async for ws_response in websocket_assistant.iter_messages():
+ data = ws_response.data
+ await self._process_event_message(event_message=data, queue=queue)
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_utils.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_utils.py
new file mode 100644
index 00000000000..f11bff1ed47
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_utils.py
@@ -0,0 +1,137 @@
+"""
+Configuration and utilities for Orderly Network Perpetual Connector
+
+This module defines:
+- Connector configuration (API credentials)
+- Default fees
+- Trading pair examples
+- Domain configuration for testnet/mainnet
+"""
+
+from decimal import Decimal
+
+from pydantic import Field, SecretStr
+
+from hummingbot.client.config.config_data_types import BaseConnectorConfigMap
+from hummingbot.core.data_type.trade_fee import TradeFeeSchema
+
+# Orderly Network Fee Structure
+# Fees are builder-specific and may vary
+# Default conservative estimates:
+# - Maker: 0.02% (2 basis points)
+# - Taker: 0.05% (5 basis points)
+# Actual fees should be queried from /v1/public/fee/program endpoint
+DEFAULT_FEES = TradeFeeSchema(
+ maker_percent_fee_decimal=Decimal("0.0002"),
+ taker_percent_fee_decimal=Decimal("0.0005"),
+ buy_percent_fee_deducted_from_returns=True,
+)
+
+CENTRALIZED = True
+
+EXAMPLE_PAIR = "BTC-USDC"
+
+BROKER_ID = "hummingbot" # check and update whre is this used
+
+
+class OrderlyPerpetualConfigMap(BaseConnectorConfigMap):
+ """
+ Configuration map for Orderly Network Perpetual connector.
+
+ Users must provide pre-generated Orderly Network credentials:
+ - orderly_account_id: Account identifier (hex string)
+ - orderly_key: Public key (ed25519:BASE58_ENCODED)
+ - orderly_secret: Private key (ed25519:BASE58_ENCODED)
+
+ These credentials should be generated using:
+ 1. Orderly Network UI, or
+ 2. The provided setup script (scripts/orderly_setup.py)
+
+ See ORDERLY_AUTH_ANALYSIS.md for detailed setup instructions.
+ """
+
+ connector: str = "orderly_perpetual"
+
+ orderly_perpetual_account_id: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network account ID (e.g., 0x1234...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+ orderly_perpetual_api_key: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network API key (e.g., ed25519:ABC...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+ orderly_perpetual_api_secret: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network API secret (e.g., ed25519:123...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+
+KEYS = OrderlyPerpetualConfigMap.model_construct()
+
+# Testnet Domain Configuration
+OTHER_DOMAINS = ["orderly_perpetual_testnet"]
+OTHER_DOMAINS_PARAMETER = {"orderly_perpetual_testnet": "orderly_perpetual_testnet"}
+OTHER_DOMAINS_EXAMPLE_PAIR = {"orderly_perpetual_testnet": "BTC-USDC"}
+OTHER_DOMAINS_DEFAULT_FEES = {"orderly_perpetual_testnet": [0.0002, 0.0005]}
+
+
+class OrderlyPerpetualTestnetConfigMap(BaseConnectorConfigMap):
+ """
+ Configuration map for Orderly Network Testnet.
+
+ Same credential structure as mainnet, but uses testnet API endpoints.
+ """
+
+ connector: str = "orderly_perpetual_testnet"
+
+ orderly_perpetual_testnet_account_id: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network testnet account ID (e.g., 0x1234...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+ orderly_perpetual_testnet_api_key: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network testnet API key (e.g., ed25519:ABC...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+ orderly_perpetual_testnet_api_secret: SecretStr = Field(
+ default=...,
+ json_schema_extra={
+ "prompt": "Enter your Orderly Network testnet API secret (e.g., ed25519:123...)",
+ "is_secure": True,
+ "is_connect_key": True,
+ "prompt_on_new": True,
+ },
+ )
+
+
+OTHER_DOMAINS_KEYS = {
+ "orderly_perpetual_testnet": OrderlyPerpetualTestnetConfigMap.model_construct()
+}
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_web_utils.py b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_web_utils.py
new file mode 100644
index 00000000000..1188b7a240a
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_perpetual_web_utils.py
@@ -0,0 +1,309 @@
+"""
+Web utilities for Orderly Network Perpetual Connector
+
+This module provides utility functions for:
+- URL construction (REST and WebSocket)
+- Web assistants factory creation
+- Rate limiting (throttler)
+- Request preprocessing
+"""
+
+import time
+from typing import Any, Dict, Optional
+
+import hummingbot.connector.derivative.orderly_perpetual.orderly_perpetual_constants as CONSTANTS
+from hummingbot.core.api_throttler.async_throttler import AsyncThrottler
+from hummingbot.core.web_assistant.auth import AuthBase
+from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest
+from hummingbot.core.web_assistant.rest_pre_processors import RESTPreProcessorBase
+from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
+
+
+class OrderlyPerpetualRESTPreProcessor(RESTPreProcessorBase):
+ """
+ REST request preprocessor for Orderly Network.
+
+ Adds standard headers to all REST requests.
+ """
+
+ async def pre_process(self, request: RESTRequest) -> RESTRequest:
+ """
+ Add standard headers to REST request.
+
+ Args:
+ request: REST request to preprocess
+
+ Returns:
+ Preprocessed REST request
+ """
+ if request.headers is None:
+ request.headers = {}
+
+ # Handle Content-Type header based on HTTP method
+ # Match official SDK behavior: POST/PUT use application/json, DELETE/GET use application/x-www-form-urlencoded
+ # Official SDK sets Content-Type for all requests (see orderly_evm_connector/api.py:162-175)
+ if request.method in (RESTMethod.POST, RESTMethod.PUT):
+ request.headers["Content-Type"] = "application/json"
+ elif request.method in (RESTMethod.DELETE, RESTMethod.GET):
+ # Official SDK uses application/x-www-form-urlencoded for DELETE/GET requests
+ request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+ return request
+
+
+def rest_url(path_url: str, domain: str = CONSTANTS.DOMAIN) -> str:
+ """
+ Construct full REST API URL.
+
+ Args:
+ path_url: API endpoint path (e.g., "/v1/order")
+ domain: Domain identifier (mainnet or testnet)
+
+ Returns:
+ Full URL for the API endpoint
+
+ Example:
+ >>> rest_url("/v1/order", "orderly_perpetual")
+ 'https://api.orderly.org/v1/order'
+ """
+ base_url = CONSTANTS.PERPETUAL_BASE_URL if domain == CONSTANTS.DOMAIN else CONSTANTS.TESTNET_BASE_URL
+ return base_url + path_url
+
+
+def public_rest_url(path_url: str, domain: str = CONSTANTS.DOMAIN) -> str:
+ """
+ Construct full REST API URL for public endpoints.
+
+ Alias for rest_url() since Orderly uses the same base URL for public and private endpoints.
+
+ Args:
+ path_url: API endpoint path
+ domain: Domain identifier
+
+ Returns:
+ Full URL for the public endpoint
+ """
+ return rest_url(path_url, domain)
+
+
+def private_rest_url(path_url: str, domain: str = CONSTANTS.DOMAIN) -> str:
+ """
+ Construct full REST API URL for private endpoints.
+
+ Alias for rest_url() since Orderly uses the same base URL for public and private endpoints.
+
+ Args:
+ path_url: API endpoint path
+ domain: Domain identifier
+
+ Returns:
+ Full URL for the private endpoint
+ """
+ return rest_url(path_url, domain)
+
+
+def wss_url(endpoint_type: str = "public", domain: str = CONSTANTS.DOMAIN, account_id: Optional[str] = None) -> str:
+ """
+ Construct WebSocket URL.
+
+ Args:
+ endpoint_type: Type of WebSocket endpoint ("public" or "private")
+ domain: Domain identifier (mainnet or testnet)
+ account_id: Account ID (required for private WebSocket)
+
+ Returns:
+ Full WebSocket URL
+
+ Raises:
+ ValueError: If account_id is not provided for public or private endpoint
+
+ Example:
+ >>> wss_url("public", "orderly_perpetual")
+ 'wss://ws-evm.orderly.org/ws/stream/0x1234...'
+ >>> wss_url("private", "orderly_perpetual", "0x1234...")
+ 'wss://ws-private-evm.orderly.org/v2/ws/private/stream/0x1234...'
+ """
+ if endpoint_type == "public":
+ base_ws_url = (
+ CONSTANTS.PERPETUAL_WS_PUBLIC_URL
+ if domain == CONSTANTS.DOMAIN
+ else CONSTANTS.TESTNET_WS_PUBLIC_URL
+ )
+ if account_id is None:
+ raise ValueError("account_id is required for public WebSocket URL")
+ return f"{base_ws_url}/{account_id}"
+
+ elif endpoint_type == "private":
+ if account_id is None:
+ raise ValueError("account_id is required for private WebSocket URL")
+
+ base_ws_url = (
+ CONSTANTS.PERPETUAL_WS_PRIVATE_URL
+ if domain == CONSTANTS.DOMAIN
+ else CONSTANTS.TESTNET_WS_PRIVATE_URL
+ )
+ # Orderly private WebSocket URL includes account_id in the path
+ return f"{base_ws_url}/{account_id}"
+ else:
+ raise ValueError(f"Invalid endpoint_type: {endpoint_type}. Must be 'public' or 'private'")
+
+
+def build_api_factory(
+ throttler: Optional[AsyncThrottler] = None,
+ auth: Optional[AuthBase] = None,
+) -> WebAssistantsFactory:
+ """
+ Create WebAssistantsFactory with throttler and authentication.
+
+ Args:
+ throttler: Rate limiter (creates default if None)
+ auth: Authentication handler (optional)
+
+ Returns:
+ Configured WebAssistantsFactory instance
+ """
+ throttler = throttler or create_throttler()
+ api_factory = WebAssistantsFactory(
+ throttler=throttler,
+ rest_pre_processors=[OrderlyPerpetualRESTPreProcessor()],
+ auth=auth,
+ )
+ return api_factory
+
+
+def build_api_factory_without_time_synchronizer_pre_processor(
+ throttler: AsyncThrottler,
+) -> WebAssistantsFactory:
+ """
+ Create WebAssistantsFactory without time synchronizer.
+
+ Used for endpoints that don't require time synchronization.
+
+ Args:
+ throttler: Rate limiter
+
+ Returns:
+ Configured WebAssistantsFactory instance
+ """
+ api_factory = WebAssistantsFactory(
+ throttler=throttler,
+ rest_pre_processors=[OrderlyPerpetualRESTPreProcessor()],
+ )
+ return api_factory
+
+
+def create_throttler() -> AsyncThrottler:
+ """
+ Create rate limiter with Orderly Network rate limits.
+
+ Returns:
+ Configured AsyncThrottler instance
+ """
+ return AsyncThrottler(CONSTANTS.RATE_LIMITS)
+
+
+async def get_current_server_time(throttler: AsyncThrottler, domain: str) -> float:
+ """
+ Get current server time.
+
+ For Orderly, we use local time since they validate timestamps within a 300-second window.
+
+ Args:
+ throttler: Rate limiter (unused, kept for interface compatibility)
+ domain: Domain identifier (unused, kept for interface compatibility)
+
+ Returns:
+ Current timestamp as float
+ """
+ return time.time()
+
+
+def is_exchange_information_valid(exchange_info: Dict[str, Any]) -> bool:
+ """
+ Validate if trading pair information is valid.
+
+ Checks if a trading pair is enabled and has necessary information.
+
+ Args:
+ exchange_info: Trading pair information from exchange
+
+ Returns:
+ True if the trading pair is valid and enabled
+
+ Example trading pair info from Orderly:
+ {
+ "symbol": "PERP_BTC_USDC",
+ "base_tick": "0.01",
+ "base_min": "0.01",
+ "base_max": "1000",
+ "quote_tick": "0.1",
+ "quote_min": "10",
+ "quote_max": "1000000",
+ "min_notional": "10",
+ "price_range": "0.1",
+ "price_scope": "0.05"
+ }
+ """
+ # Check if required fields exist
+ required_fields = ["symbol", "base_tick", "quote_tick", "base_min", "quote_min"]
+
+ for field in required_fields:
+ if field not in exchange_info:
+ return False
+
+ # Check if symbol is active (Orderly may have a status field)
+ if "status" in exchange_info:
+ return exchange_info["status"].upper() in ["ACTIVE", "TRADING"]
+
+ # If no status field, assume active if required fields are present
+ return True
+
+
+def format_trading_pair(orderly_symbol: str) -> str:
+ """
+ Convert Orderly symbol format to Hummingbot trading pair format.
+
+ Orderly format: PERP_BTC_USDC
+ Hummingbot format: BTC-USDC
+
+ Args:
+ orderly_symbol: Symbol in Orderly format
+
+ Returns:
+ Trading pair in Hummingbot format
+
+ Example:
+ >>> format_trading_pair("PERP_BTC_USDC")
+ 'BTC-USDC'
+ """
+ if not orderly_symbol.startswith("PERP_"):
+ return orderly_symbol # Return as-is if not in expected format
+
+ # Remove "PERP_" prefix and split
+ parts = orderly_symbol[5:].split("_")
+
+ if len(parts) != 2:
+ return orderly_symbol # Return as-is if unexpected format
+
+ base, quote = parts
+ return f"{base}-{quote}"
+
+
+def orderly_symbol_from_trading_pair(trading_pair: str) -> str:
+ """
+ Convert Hummingbot trading pair format to Orderly symbol format.
+
+ Hummingbot format: BTC-USDC
+ Orderly format: PERP_BTC_USDC
+
+ Args:
+ trading_pair: Trading pair in Hummingbot format
+
+ Returns:
+ Symbol in Orderly format
+
+ Example:
+ >>> orderly_symbol_from_trading_pair("BTC-USDC")
+ 'PERP_BTC_USDC'
+ """
+ base, quote = trading_pair.split("-")
+ return f"PERP_{base}_{quote}"
diff --git a/hummingbot/connector/derivative/orderly_perpetual/orderly_swagger.yml b/hummingbot/connector/derivative/orderly_perpetual/orderly_swagger.yml
new file mode 100644
index 00000000000..a07cc5e80fb
--- /dev/null
+++ b/hummingbot/connector/derivative/orderly_perpetual/orderly_swagger.yml
@@ -0,0 +1,13769 @@
+openapi: 3.0.1
+info:
+ title: EVM
+ description: ""
+ version: 1.0.0
+tags:
+ - name: public
+ - name: private
+paths:
+ /v1/client/add_sub_account:
+ post:
+ summary: Add sub-account
+ description: |
+ **Limit: 10 requests per second**
+
+ `POST /v1/client/add_sub_account`
+
+ Only the main account is allowed to call this API.
+
+ Main account’s Orderly key can control sub-accounts.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ description:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AddSubAccountResponse"
+
+ /v1/client/sub_account:
+ get:
+ summary: Get sub-account list
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/client/sub_account`
+
+ Only the main account is allowed to call this API.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SubAccountResponse"
+
+ /v1/client/update_sub_account:
+ post:
+ summary: Update sub-account
+ description: |
+ **Limit: 10 requests per second**
+
+ `POST /v1/client/update_sub_account`
+
+ Only the main account is allowed to call this API.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ required:
+ - sub_account_id
+ type: object
+ properties:
+ sub_account_id:
+ type: string
+ description:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/client/aggregate/positions:
+ get:
+ summary: Get aggregate positions
+ description: |
+
+ **Limit: 1 request per 60 seconds**
+
+ `GET v1/client/aggregate/positions`
+
+ Only the main account is allowed to call this API.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AggregatePositionsResponse"
+
+ /v1/client/aggregate/holding:
+ get:
+ summary: Get aggregate holding
+ description: |
+
+ **Limit: 1 request per 60 seconds**
+
+ `GET /v1/client/aggregate/holding`
+
+ Only the main account is allowed to call this API
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AggregateHoldingResponse"
+
+ /v1/sub_account_settle_pnl:
+ post:
+ summary: Settle sub-account PnL
+ description: |
+ **Limit: 10 requests per second**
+
+ `POST /v1/sub_account_settle_pnl`
+
+ Main account’s Orderly key can control sub-accounts.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - name: orderly_account_id
+ in: header
+ description: "account_id in header can only be sub_account"
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ required:
+ - settle_nonce
+ type: object
+ properties:
+ settle_nonce:
+ type: number
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SettlePnlResponse"
+ /v1/public/futures:
+ get:
+ summary: Get Market Info for All Symbols
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/futures`
+
+ Get basic market information for all the symbols.
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FuturesInfoResponse"
+
+ /v1/ip_info:
+ get:
+ summary: Get IP Info
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/ip_info`
+
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetIpInfoResponse"
+
+ /v1/public/futures_market:
+ get:
+ summary: Get Market Volume by Builder
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/futures_market`
+
+ Get market volume filtered by broker.
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: string
+ - name: broker_id
+ in: query
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetMarketVolumeByBrokerResponse"
+
+ /v1/public/balance/stats:
+ get:
+ summary: Get TVL by Builder
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/balance/stats`
+
+ Get TVL(Total Balance + Unsettled PnL) by broker
+ tags:
+ - public
+ parameters:
+ - name: broker_id
+ in: query
+ description: "Return all TVL if not specified"
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetTVLByBrokerResponse"
+
+ /v1/public/market_trades:
+ get:
+ summary: Get Market Trades
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_trades`
+
+ Get the latest market trades.
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: PERP_NEAR_USDC
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Numbers of trades want to query.
+ required: false
+ example: "10"
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/MarketTradesResponse"
+
+ /v1/public/market_info/price_changes:
+ get:
+ summary: Get Price Info for All Symbols
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_info/price_changes`
+
+ Return the current price and the price 5m, 30m, 1h, 24h, 3d, 7d, 30d ago for all symbols
+
+ Examples:
+
+ - Now: 11:06:30 (2024-07-11)
+
+ - 5m: 11:01:00 (2024-07-11)
+
+ - 30m: 10:36:00 (2024-07-11)
+
+ - 1h: 10:06:00 (2024-07-11)
+
+ - 4h: 07:06:00 (2024-07-11)
+
+ - 24h: 11:06:00 (2024-07-10)
+
+ - 3d: 00:00:00 (2024-07-08)
+
+ - 7d: 00:00:00 (2024-07-04)
+
+ - 30d: 00:00:00 (2024-06-11)
+ tags:
+ - public
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/MarketPriceChangeInfoResponse"
+
+ /v1/public/market_info/traders_open_interests:
+ get:
+ summary: Get Open Interests for All Symbols
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_info/traders_open_interests`
+
+ Return the long/short Open Interests for all symbols (mm accounts are excluded).
+
+ tags:
+ - public
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/MarketOpenInterestsResponse"
+
+ /v1/public/market_info/history_charts:
+ get:
+ summary: Get Historical Price List for All Symbols
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_info/history_charts`
+
+ Return the close price for all symbols
+
+ tags:
+ - public
+ parameters:
+ - name: time_interval
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetPriceForSmallChartsResponse"
+
+ /v1/public/market_info/funding_history:
+ get:
+ summary: Get Funding Rate for All Markets
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_info/funding_history`
+
+ tags:
+ - public
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetMarketFundingRateHistoryResponse"
+
+ /v1/public/volume/stats:
+ get:
+ summary: Get Builder Volume
+ deprecated: false
+ description: >
+
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/volume/stats`
+
+
+ Get the latest volume statistics of Orderly and its associated builders.
+ Note that for builder volume, the volume is counted as the sum of the
+ maker and taker volume, while for the full Orderly platform, the volume
+ is the total amount matched on the platform (ie taker and maker are not
+ double-counted.)
+ tags:
+ - public
+ parameters:
+ - name: broker_id
+ in: query
+ description: Provide broker_id to obtain statistics of a specific builder
+ required: false
+ example: test_broker
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VolumeStatsResponse"
+
+ /v1/public/broker/stats:
+ get:
+ summary: Get Builder Stats
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/broker/stats?broker_id={broker_id}`
+
+ Get the stats of a specific builder.
+ tags:
+ - public
+ parameters:
+ - name: broker_id
+ in: query
+ description: If provided, it will only return details for the particular builder.
+ required: false
+ example: test_broker
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BrokerStatsResponse"
+
+ /v1/public/broker/name:
+ get:
+ summary: Get Builder List
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/broker/name?broker_id={broker_id}`
+
+ Get the list of builders currently onboarded on Orderly.
+ tags:
+ - public
+ parameters:
+ - name: broker_id
+ in: query
+ description: If provided, it will only return details for the particular builder.
+ required: false
+ example: test_broker
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BrokerListResponse"
+
+ /v1/public/funding_rates:
+ get:
+ summary: Get Predicted Funding Rates for All Markets
+ deprecated: false
+ description: >-
+
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/funding_rates`
+
+
+ Get predicted funding rates for all futures trading pairs.
+
+
+ Get the :
+
+ * `last_funding_rate` : latest hourly funding rate for all the markets
+ for the last funding period (dt = 8h)
+
+ * `est_funding_rate` : rolling average of all funding rates over the
+ last 8 hours
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FundingRatesResponse"
+
+ /v1/public/funding_rate/{symbol}:
+ get:
+ summary: Get Predicted Funding Rate for One Market
+ deprecated: false
+ description: |
+
+ **Limit: 30 requests per 1 second per IP address**
+
+ `GET /v1/public/funding_rate/{symbol}`
+
+ Get latest funding rate for one market.
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: path
+ description: ""
+ required: true
+ example: PERP_ETH_USDC
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FundingRateResponse"
+
+ /v1/public/funding_rate_history:
+ get:
+ summary: Get Funding Rate History for One Market
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/funding_rate_history`
+
+
+ Get funding rate history for one market.
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: PERP_NEAR_USDC
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that you wish to query, noted that the time stamp
+ is a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: >-
+ end time range that you wish to query, noted that the time stamp is
+ a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "Default: 60"
+ required: false
+ example: ""
+ schema:
+ type: number
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FundingRateHistoryResponse"
+ /v1/public/futures/{symbol}:
+ get:
+ summary: Get Market Info for One Symbol
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/futures/{symbol}`
+
+ Get basic market information for one trading pair.
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: path
+ description: ""
+ required: true
+ example: PERP_ETH_USDC
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FuturesInfoResponse"
+ /v1/public/liquidation:
+ get:
+ summary: Get Positions Under Liquidation
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/liquidation`
+ tags:
+ - public
+ parameters:
+ - name: start_t
+ in: query
+ description: >-
+ start time range that you wish to query, noted that the time stamp
+ is a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: >-
+ end time range that you wish to query, noted that the time stamp is
+ a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: "Default: 60"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LiquidationResponse"
+ /v1/public/liquidated_positions:
+ get:
+ summary: Get Liquidated Positions Info
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/liquidated_positions`
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: PERP_ETH_USDC
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LiquidatedPositionsResponse"
+ /v1/public/info/{symbol}:
+ get:
+ summary: Get Order Rules per Symbol
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/info/{symbol}`
+
+
+ This endpoint provides all the values for the rules that an order need
+ to fulfil in order for it to be placed successfully. The rules are
+ defined as follows:
+
+
+ Price filter
+
+
+ - `price` >= `quote_min`
+
+ - `price` =< `quote_max`
+
+ - `(price - quote_min) % quote_tick` should equal to zero
+
+ - `price` =< `mark_price * (1 + price_range)` when BUY
+
+ - `price` >= `mark_price * (1 - price_range)` when SELL
+
+
+ Size filter
+
+
+ - `base_min` =< `quantity` =< `base_max`
+
+ - `(quantity - base_min) % base_tick` should equal to zero
+
+
+ Min Notional filter
+
+
+ - `price * quantity` should greater than `min_notional`
+
+
+ Risk Exposure filer
+
+
+ - Order size should be within holding threshold. See
+ [account_info](/docs/build-on-omnichain/evm-api/restful-api/private/get-account-information)
+
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: path
+ description: ""
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ExchangeInformationResponse"
+ /v1/public/token:
+ get:
+ summary: Get Supported Collateral Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/token`
+
+ Retrieves the available tokens to be used as collateral within Orderly.
+ tags:
+ - public
+ parameters:
+ - name: chain_id
+ in: query
+ description: "Filter results by chain_id"
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TokenInfoResponse"
+ /v1/public/info:
+ get:
+ summary: Get Available Symbols
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/info`
+
+
+ Get available symbols that Orderly supports, and also send order
+ rules for each symbol. The definition of rules can be found at [Get Order Rules per Market](/docs/build-on-omnichain/evm-api/restful-api/public/get-order-rules-per-symbol)
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AvailableSymbolsResponse"
+ /v1/public/index_price_source:
+ get:
+ summary: Get Index Price Source
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/index_price_source`
+
+ Retrieves the available tokens to be used as collateral within Orderly.
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetIndexPriceSourceResponse"
+ /v1/public/insurancefund:
+ get:
+ summary: Get Insurance Fund Info
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/insurancefund`
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/InsuranceFundResponse"
+ /v1/tv/config:
+ get:
+ summary: Get TradingView Localized Config Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/tv/config`
+ tags:
+ - public
+ parameters:
+ - name: locale
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TvConfigResponse"
+ /v1/tv/history:
+ get:
+ summary: Get TradingView History Bars
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/tv/history?symbol={}&resolution={}&from={}&to={}`
+ tags:
+ - public
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: resolution
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: from
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: to
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TvHistoryResponse"
+ /v1/tv/symbol_info:
+ get:
+ summary: Get TradingView Symbol Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/tv/symbol_info`
+ tags:
+ - public
+ parameters:
+ - name: group
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TvSymbolResponse"
+ /v1/positions:
+ get:
+ summary: Get All Positions Info
+ deprecated: false
+ description: |
+ **Limit: 30 requests per 10 second per user**
+
+ `GET /v1/positions`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PositionsResponse"
+ /v1/position/{symbol}:
+ get:
+ summary: Get One Position Info
+ deprecated: false
+ description: |
+ **Limit: 30 requests per 10 second per user**
+
+ `GET /v1/position/{symbol}`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: path
+ description: ""
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PositionResponse"
+
+ /v1/position_history:
+ get:
+ summary: Get Position History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per user**
+
+ `GET /v1/position_history`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PositionHistoryResponse"
+
+ /v1/funding_fee/history:
+ get:
+ summary: Get Funding Fee History
+ deprecated: false
+ description: |
+ **Limit: 20 requests per 60 second per user**
+
+ `GET /v1/funding_fee/history`
+
+ Get funding fee history.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: "omit to get funding fee history for all symbols"
+ required: false
+ example: PERP_ETH_USDC
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that you wish to query, noted that the time stamp
+ is a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_t
+ in: query
+ description: >-
+ end time range that you wish to query, noted that the time stamp is
+ a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: size
+ in: query
+ description: "Default: 60"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/FundingFeeHistoryResponse"
+ /v1/client/liquidator_liquidations:
+ get:
+ summary: Get Liquidated Positions by Liquidator
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/client/liquidator_liquidations`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LiquidatorLiquidationsResponse"
+ /v1/client/leverage:
+ get:
+ summary: Get Leverage Setting
+ deprecated: false
+ description: |
+ **Limit: 1 requests per 1 second per IP address**
+
+ `GET /v1/client/leverage`
+
+ Get account's leverage setting for a specific symbol.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: "PERP_BTC_USDC"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LeverageResponse"
+ post:
+ summary: Update Leverage Setting
+ deprecated: false
+ description: |
+ **Limit: 5 requests per 60 second per user**
+
+ `POST /v1/client/leverage`
+
+ Allow users to customize leverage for each symbol or choose maximum leverage for account's futures mode.
+
+ Validation Logic:
+
+ 1. Check the leverage range is eligible
+
+ 2. Check if the position notional under the updated leverage is acceptable
+ - `max_notional = (1 / (symbol_leverage * imr_factor)) ^ (1/0.8)`
+ - `symbol_leverage_max = round down to int → min(1 / (imr_factor * notional ^ 0.8), 1/base_imr)`
+
+ 3. Check if the margin is enough
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ leverage:
+ type: integer
+ description: Integer between 1 to 50
+ required:
+ - leverage
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LeverageResponse"
+
+ /v1/public/leverage:
+ get:
+ summary: Get Max Leverage Setting
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/leverage`
+
+ Get max leverage setting
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetLeverageSettingResponse"
+
+ /v1/liquidations:
+ get:
+ summary: Get Liquidated Positions of Account
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/liquidations`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: sort_by
+ in: query
+ description: "`liquidation_id`/`time`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: liquidation_id
+ in: query
+ description: "return the designed liquidation_id only"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LiquidationsResponse"
+ /v1/liquidation:
+ post:
+ summary: Claim Liquidated Positions
+ deprecated: false
+ description: |
+ **Limit: 5 requests per 1 second per IP address**
+
+ `POST /v1/liquidation`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LiquidationBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PostLiquidationResponse"
+ /v1/order:
+ post:
+ summary: Create Order
+ deprecated: false
+ description: >-
+ **Limit: 10 requests per 1 second**
+
+
+ `POST /v1/order`
+
+
+ Place order maker/taker, the order executed information will be update
+ from websocket stream.
+
+ will response immediately with an order created message.
+
+
+ `MARKET` type order behavior: it matches until the full size is
+ executed. If the size is too large (larger than whole book) or the
+ matching price exceeds the price limit (refer to `price_range`), then
+ the remaining quantity will be cancelled.
+
+
+ `IOC` type order behavior: it matches as much as possible at the
+ order_price. If not fully executed, then remaining quantity will be
+ cancelled.
+
+
+ `FOK` type order behavior: if the order can be fully executed at the
+ order_price then the order gets fully executed otherwise would be
+ cancelled without any execution.
+
+
+ `POST_ONLY` type order behavior: if the order will be executed with any
+ maker trades at the time of placement, then it will be cancelled without
+ any execution.
+
+
+ `ASK` type order behavior: the order price is guranteed to be the best
+ ask price of the orderbook at the time it gets accepted.
+
+
+ `BID` type order behavior: the order price is guranteed to be the best
+ bid price of the orderbook at the time it gets accepted.
+
+
+ `visible_quantity` behavior: it sets the maximum quantity to be shown on
+ orderbook. By default, it is equal to order_quantity, negative number
+ and number larger than `order_quantity` is not allowed. If it sets to 0,
+ the order would be hidden from the orderbook. It doesn't work for
+ `MARKET`/`IOC`/`FOK` orders since orders with these types would be
+ executed and cancelled immediately and not be shown on orderbook. For
+ `LIMIT` order, as long as it's not complete,
+ `visible_quantity` is the maximum quantity that shown on orderbook.
+
+
+ `order_amount` behavior: for `MARKET`/`BID`/`ASK` order, order can be
+ placed by `order_amount` instead of `order_quantity`. It's the size of
+ the order in terms of the quote currency instead of the base currency.
+ The order would be rejected if both `order_amount` and `order_quantity`
+ are provided. The precision of the number should be within 8 digits.
+
+
+ `client_order_id` behavior: customized order_id, a unique id among open
+ orders. Orders with the same `client_order_id` can be accepted only when
+ the previous one if completed, otherwise the order will be rejected.
+
+
+ For `MARKET`/`BID`/`ASK` order, `order_amount` is not supported when
+ placing SELL order while `order_quantity` is not supported when placing
+ BUY order.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ - $ref: "#/components/parameters/recv_window_header"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PostOrderBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PostOrderResponse"
+ delete:
+ summary: Cancel Order
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `DELETE /v1/order?order_id={order_id}&symbol={symbol}`
+
+ Cancels a single order by `order_id`.
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: order_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteOrderResponse"
+ put:
+ summary: Edit Order
+ deprecated: false
+ description: >-
+ **Limit: 10 request per 1 second**
+
+
+ `PUT /v1/order`
+
+
+ Edit a pending order by `order_id`. Only the `order_price` or
+ `order_quantity` can be amended.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PutOrderResponse"
+ /v1/order/{order_id}:
+ get:
+ summary: Get Order by order_id
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/order/{order_id}`
+
+ Get details of a single order by `order_id`.
+ tags:
+ - private
+ parameters:
+ - name: order_id
+ in: path
+ description: id of the order
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderResponse"
+ /v1/trades:
+ get:
+ summary: Get Trades
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/trades`
+
+ Return client’s trades history within a time range.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that wish to query, noted the time stamp is
+ 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: >-
+ end time range that wish to query, noted the time stamp is 13-digits
+ timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query. (max: 500)"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TradesResponse"
+ /v1/client/holding:
+ get:
+ summary: Get Current Holding
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 seconds**
+
+ `GET /v1/client/holding`
+
+ Get the current summary of user token holdings.
+ tags:
+ - private
+ parameters:
+ - name: all
+ in: query
+ description: >-
+ true/false. If true then will return all token even if balance is
+ empty.
+ required: false
+ example: ""
+ schema:
+ type: boolean
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientHoldingResponse"
+ /v1/client/info:
+ get:
+ summary: Get Account Information
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `GET /v1/client/info`
+
+ Get basic account information including current user fee rates.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientInfoResponse"
+ /v1/orders:
+ get:
+ summary: Get Orders
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `GET /v1/orders`
+
+
+ Get orders by customized filters.
+
+
+ For filter by status, one can reference special bundled statuses below
+ for ease of access of Open (ie `INCOMPLETE`) orders or `COMPLETED`
+ orders.
+
+
+ - `INCOMPLETE` = `NEW` + `PARTIAL_FILLED`
+
+ - `COMPLETED` = `CANCELLED` + `FILLED`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: side
+ in: query
+ description: "`BUY`/`SELL`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: order_type
+ in: query
+ description: "`LIMIT`/`MARKET`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: >-
+ `NEW`/`CANCELLED`/`PARTIAL_FILLED`/`FILLED`/`REJECTED`/`INCOMPLETE`/`COMPLETED`
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: order_tag
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that wish to query, noted the time stamp is
+ 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: >-
+ end time range that wish to query, noted the time stamp is 13-digits
+ timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: sort_by
+ in: query
+ description: |
+ If empty:
+
+ Descending order by updated_time if user passed update_start_t or update_end_t; descending order by order_id if updated_time are the same;
+
+ Otherwise descending order by created_time; descending order by order_id if created_time are the same;
+
+
+ If not empty:
+
+ If sort_by == `CREATED_TIME_DESC`, descending order by created_time. Descending order by order_id if created_time are same;
+
+ If sort_by == `CREATED_TIME_ASC`, ascending order by created_time. Ascending order by order_id if created_time are same;
+
+ If sort_by == `UPDATED_TIME_DESC`, descending order by updated_time. Descending order by order_id if updated_time are same;
+
+ If sort_by == `UPDATED_TIME_ASC`, ascending order by updated_time. Ascending order by order_id if updated_time are same;
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrdersResponse"
+ delete:
+ summary: Cancel All Pending Orders
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `DELETE /v1/orders?symbol={symbol}`
+
+
+ Cancel a list of orders, filtered by `symbol` optionally. This request
+ will cancel all open orders within the filter criteria, and will cancel
+ all open orders if no filter is provided.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteAllPendingAlgoOrderResponse"
+ /v1/client/statistics:
+ get:
+ summary: Get User Statistics
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `GET /v1/client/statistics`
+
+ Get statistics of the user account
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientStatisticsResponse"
+ /v1/asset/history:
+ get:
+ summary: Get Asset History
+ deprecated: false
+ description: |
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/asset/history`
+
+ Get asset history, including token deposits/withdrawals.
+ tags:
+ - private
+ parameters:
+ - name: token
+ in: query
+ description: token name you want to search
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: side
+ in: query
+ description: "`DEPOSIT`/`WITHDRAW`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: "`NEW`/`CONFIRM`/`PROCESSING`/`COMPLETED`/`FAILED`/`PENDING_REBALANCE`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AssetHistoryResponse"
+ /v1/claim_insurance_fund:
+ post:
+ summary: Claim Insurance Fund
+ deprecated: false
+ description: |
+ **Limit: 5 requests per 1 second**
+
+ `POST /v1/claim_insurance_fund`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClaimInsuranceFundBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClaimInsuranceFundResponse"
+ /v1/public/system_info:
+ get:
+ summary: Get System Maintenance Status
+ deprecated: false
+ description: >-
+ **Limit: 1 requests per 1 second per IP address**
+
+
+ `GET /v1/public/system_info`
+
+
+ Retrieve the current system maintenance status of Orderly. A
+ return value of status = 0 means the system is functioning properly and
+ a return value of status = 2 means the system is under maintenance.
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SystemInfoResponse"
+ /v1/registration_nonce:
+ get:
+ summary: Get Registration Nonce
+ deprecated: false
+ description: >-
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/registration_nonce`
+
+
+ Retrieve a nonce used for registering an account on Orderly. The
+ validity of the nonce value is `2` minutes. Each nonce can only be used
+ once (ie for one account).
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RegistrationNonceResponse"
+ /v1/get_account:
+ get:
+ summary: Check if Wallet is Registered
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/get_account`
+
+ Check whether a wallet has a registered account with provided broker_id.
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: The address of the user wallet
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: broker_id
+ in: query
+ description: The builder that the account is registered on
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAccountResponse"
+
+ /v1/get_all_accounts:
+ get:
+ summary: Check Account Details of an Address
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/get_all_accounts`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: The address of the user wallet
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: broker_id
+ in: query
+ description: The builder that the account is registered on
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAllAccountsResponse"
+
+ /v1/get_broker:
+ get:
+ summary: Check if Address is Registered
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/get_broker`
+
+ Check whether an address is registered already.
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: The address of the user
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetBrokerResponse"
+ /v1/register_account:
+ post:
+ summary: Register Account
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `POST /v1/register_account`
+
+
+ Registers a new account to Orderly. Note an account is unique
+ for each wallet address + builder id (ie the same wallet address can have
+ multiple accounts with Orderly, 1 with each builder)
+ tags:
+ - public
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RegisterAccountBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RegisterAccountResponse"
+ /v1/get_orderly_key:
+ get:
+ summary: Get Orderly Key
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/get_orderly_key`
+
+ Check the validity of an Orderly access key attached to the account.
+ tags:
+ - public
+ parameters:
+ - name: account_id
+ in: query
+ description: The account id of the user.
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: orderly_key
+ in: query
+ description: The public key of the Orderly access key.
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetOrderlyKeyResponse"
+ /v1/orderly_key:
+ post:
+ summary: Add Orderly Key
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `POST /v1/orderly_key`
+
+ Adds an Orderly access key to an account.
+
+ Main account's Orderly key can control sub-account.
+ tags:
+ - public
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderlyKeyBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderlyKeyResponse"
+
+ /v1/client/remove_orderly_key:
+ post:
+ summary: Remove Orderly Key
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `POST /v1/client/remove_orderly_key`
+
+ Remove an Orderly key from an account, deactivating it from all usage
+ tags:
+ - private
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RemoveOrderlyKeyBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/public/config:
+ get:
+ summary: Get Leverage Configuration
+ deprecated: false
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET v1/public/config`
+ tags:
+ - public
+ parameters: []
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ConfigResponse"
+ /v1/public/chain_info:
+ get:
+ summary: Get Supported Chains per Builder
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/chain_info`
+
+ Get chains specified builder is available on.
+ tags:
+ - public
+ parameters:
+ - name: broker_id
+ in: query
+ description: return chains that supports this builder
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ChainInfoResponse"
+ # /v1/public/chain_info:
+ # get:
+ # summary: Get Vault Chain Config
+ # deprecated: false
+ # description: |
+ # **Limit: 10 requests per 1 second per IP address**
+ #
+ # `GET /v1/public/chain_info`
+ # tags:
+ # - public
+ # responses:
+ # "200":
+ # description: OK
+ # content:
+ # application/json:
+ # schema:
+ # $ref: '#/components/schemas/ChainInfoResponse'
+ /v1/order/cancel_all_after:
+ post:
+ summary: Cancel All After
+ deprecated: false
+ description: >-
+ **Limit: 1 request per 1 second**
+
+
+ `POST /v1/order/cancel_all_after`
+
+
+ Cancels all of the user's ordinary and algo orders after being called (dead man switch).
+
+
+ Note: Users must apply for access before using this endpoint.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CancelAllAfterBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CancelAllAfterResponse"
+
+ /v1/batch-order:
+ post:
+ summary: Batch Create Order
+ deprecated: false
+ description: >-
+ **Limit: 1 request per 1 second**
+
+
+ `POST /v1/batch-order`
+
+
+ Creates a batch of orders as a list according to the same rules as a
+ single Create Order.
+
+
+ Parameters for each order within the batch will be the same as the
+ create single order. The batch of orders should be sent as a JSON array
+ containing all the orders. The maximum number of orders that can be sent
+ in 1 batch order request is 10. Each order within the batch order
+ request is counted as 1 order towards the overall create order rate
+ limit.
+
+
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ - $ref: "#/components/parameters/recv_window_header"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ orders:
+ type: array
+ items:
+ $ref: "#/components/schemas/BatchCreateOrderBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BatchOrderResponse"
+ delete:
+ summary: Batch Cancel Orders
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `DELETE /v1/batch-order?order_ids={order_id1},{order_id2}`
+
+
+ Cancel a list of orders by `order_ids`. The maximum orders that can be
+ cancelled within 1 batch cancel request is 10.
+
+
+ This endpoint will return invalid order_id error if 0 order_ids are
+ given or all of the given order_ids are invalid.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - name: order_ids
+ in: query
+ description: >-
+ List of order_ids, comma-separated, with a maximum of 10 order ids
+ per request.
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteAllPendingAlgoOrderResponse"
+ /v1/client/order:
+ delete:
+ summary: Cancel Order By client_order_id
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `DELETE
+ /v1/client/order?client_order_id={client_order_id}&symbol={symbol}`
+
+
+ Cancel an order by `client_order_id`.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: client_order_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteOrderResponse"
+ /v1/client/batch-order:
+ delete:
+ summary: Batch Cancel Orders By client_order_id
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `DELETE
+ /v1/client/batch-order?client_order_ids={client_order_id1},{client_order_id2}`
+
+
+ Cancel a list of orders by `client_order_ids`. The maximum orders that
+ can be cancelled within 1 batch cancel request is 10.
+
+
+ This endpoint will return invalid client_order_id error if 0
+ client_order_ids are given or all of the given client_order_ids are
+ invalid.
+
+
+ Note: This endpoint requires `trading` scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - name: client_order_ids
+ in: query
+ description: >-
+ List of client_order_ids, comma-separated, with a maximum of 10
+ order ids per request.
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteAllPendingAlgoOrderResponse"
+ /v1/client/order/{client_order_id}:
+ get:
+ summary: Get Order by client_order_id
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/client/order/{client_order_id}`
+
+ Get details of a single order by `client_order_id`.
+ tags:
+ - private
+ parameters:
+ - name: client_order_id
+ in: path
+ description: client_order_id of the order
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientOrderResponse"
+ /v1/orderbook/{symbol}:
+ get:
+ summary: Orderbook Snapshot
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `GET /v1/orderbook/{symbol}`
+
+
+ Snapshot of the current orderbook. Price of asks/bids are in descending
+ order.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: path
+ description: ""
+ required: true
+ example: PERP_BTC_USDC
+ schema:
+ type: string
+ - name: max_level
+ in: query
+ description: the levels wish to show on both side.
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderbookResponse"
+ /v1/kline:
+ get:
+ summary: Get Kline
+ deprecated: false
+ description: >
+
+ **Limit: 10 requests per 1 second**
+
+
+ `GET /v1/kline`
+
+
+ Get the latest klines (OHLC) of the specified symbol.
+
+
+ Please note that for symbols with low trading activity, the kline might
+ be generated through the mid price of the orderbook in addition to any
+ executed trades on the platform.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: PERP_BTC_USDC
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: 1m/5m/15m/30m/1h/4h/12h/1d/1w/1mon/1y
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Numbers of klines. Maximum of 1000 klines.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/KlineResponse"
+ /v1/order/{order_id}/trades:
+ get:
+ summary: Get All Trades of Specific Order
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/order/{order_id}/trades`
+
+ Get specific trades of an order by `order_id`.
+ tags:
+ - private
+ parameters:
+ - name: order_id
+ in: path
+ description: ""
+ required: true
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderTradesResponse"
+ /v1/trade/{trade_id}:
+ get:
+ summary: Get Trade
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/trade/{trade_id}`
+
+ Get specific transaction details by `trade_id`.
+ tags:
+ - private
+ parameters:
+ - name: trade_id
+ in: path
+ description: id of the trade
+ required: true
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TradeResponse"
+ /v1/client/maintenance_config:
+ post:
+ summary: Set Maintenance Config
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 60 seconds**
+
+
+ `POST /v1/client/maintenance_config`
+
+
+ Set the user config for whether the system should automatically cancel
+ the user's pending orders during maintenance.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientMaintenanceConfigBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ClientMaintenanceConfigResponse"
+ /v1/volume/user/stats:
+ get:
+ summary: Get User Volume Statistics
+ deprecated: false
+ description: |-
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/volume/user/stats`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VolumeStatsResponse"
+ /v1/volume/user/daily:
+ get:
+ summary: Get User Daily Volume
+ deprecated: false
+ description: |
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/volume/user/daily`
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DailyVolumeResponse"
+ /v1/withdraw_nonce:
+ get:
+ summary: Get Withdrawal Nonce
+ deprecated: false
+ description: >
+ **Limit 10 requests per 1 seconds**
+
+
+ `GET /v1/withdraw_nonce`
+
+
+ Retrieve a nonce used for requesting a withdrawal on Orderly.
+ Each nonce can only be used once.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WithdrawNonceReponse"
+ /v1/settle_nonce:
+ get:
+ summary: Get Settle PnL Nonce
+ deprecated: false
+ description: >
+ **Limit: 10 requests per 1 second**
+
+
+ `GET /v1/settle_nonce`
+
+
+ Retrieve a nonce used for requesting a withdrawal on Orderly.
+ Each nonce can only be used once.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SettleNonceResponse"
+ /v1/withdraw_request:
+ post:
+ summary: Create Withdraw Request
+ deprecated: false
+ description: >-
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `POST /v1/withdraw_request`
+
+
+ `verifyingContract` should use:
+ `0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203`.
+
+
+ This API will throw an error with message `22 - Cross-chain withdrawal
+ required for this withdrawal request.` if `allow_cross_chain_withdrawal`
+ is `false` while the request is a cross-chain request.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WithdrawRequestBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WithdrawResponse"
+ /v1/settle_pnl:
+ post:
+ summary: Request PnL Settlement
+ deprecated: false
+ description: >
+ **Limit: 1 requests per 1 second**
+
+
+ `POST /v1/settle_pnl`
+
+
+ `verifyingContract` should use:
+ `0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203`.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SettlePnlBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SettlePnlResponse"
+ examples:
+ "1":
+ summary: Successful examples
+ value:
+ success: true
+ data:
+ settle_pnl_id: 889
+ timestamp: 1692246987774
+ /v1/pnl_settlement/history:
+ get:
+ summary: Get PnL Settlement History
+ deprecated: false
+ description: |
+ **Limit: 20 requests per 1 second**
+
+ `GET /v1/pnl_settlement/history`
+
+ Retrieve the historical PnL settlement history of the account.
+ tags:
+ - private
+ parameters:
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PnlSettlementHistoryResponse"
+
+ /v1/internal_transfer:
+ post:
+ summary: Create Internal Transfer
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `POST /v1/internal_transfer`
+
+ This endpoint will return internal transfer is not available for Delegate Signer account if the `from` is a Delegate Signer account.
+
+ Scenarios where internal transfers are allowed:
+
+ a. Main and sub-accounts
+
+ b. Accounts within the same wallet group
+
+ c. Same address across all brokers.
+
+ d. A whitelisted `account_id` can be used as `from address`
+
+ e. If 2 `account_id` are under the same broker_id and the broker_id is whitelisted.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateInternalTransferRequest"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/internal_transfer_history:
+ get:
+ summary: Get Internal Transfer History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/internal_transfer_history`
+
+ Orderly key must has `asset` scope to access this endpoint.
+ tags:
+ - private
+ parameters:
+ - name: status
+ in: query
+ description: "`CREATED`/`PENDING`/`COMPLETED`/`FAILED`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: side
+ in: query
+ description: "`IN`/`OUT`"
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: from_account_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: to_account_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: main_sub_only
+ in: query
+ description: |
+ If True, return only internal transfers between main account and sub-accounts.
+ If False, return only internal transfers between main account and other main accounts.
+ If empty, return all transfer history.
+ required: false
+ example: ""
+ schema:
+ type: boolean
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetInternalTransferHistoryResponse"
+
+ /v1/client/key_info:
+ get:
+ summary: Get Current Orderly Key Info
+ deprecated: false
+ description: |
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/client/key_info`
+
+ Retrieve all the registered Orderly key pairs under the account.
+ tags:
+ - private
+ parameters:
+ - name: key_status
+ in: query
+ description: Filter by the status of the key (ACTIVE / REMOVING / REMOVED)
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/KeyInfoResponse"
+ /v1/client/orderly_key_ip_restriction:
+ get:
+ summary: Get Orderly Key IP Restriction
+ deprecated: false
+ description: |
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/client/orderly_key_ip_restriction`
+
+ Retrieves the current IP restriction of a particular Orderly key.
+ tags:
+ - private
+ parameters:
+ - name: orderly_key
+ in: query
+ description: The Orderly Key to query the IP restriction list
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/IpRestrictionResponse"
+ /v1/client/set_orderly_key_ip_restriction:
+ post:
+ summary: Set Orderly Key IP Restriction
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `POST /v1/client/set_orderly_key_ip_restriction`
+
+ Set the IP restrictions of a particular Orderly key under the account.
+ tags:
+ - private
+ parameters:
+ - name: orderly_key
+ in: query
+ description: The Orderly Key to set the IP restriction list.
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: ip_restriction_list
+ in: query
+ description: >-
+ List of IP or IP ranges (comma separated), that will be allowed to
+ place orders with the orderly_key.
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SetIpRestrictionResponse"
+ /v1/client/reset_orderly_key_ip_restriction:
+ post:
+ summary: Reset Orderly Key IP Restriction
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `POST /v1/client/reset_orderly_key_ip_restriction`
+
+ Reset the IP restriction of a particular Orderly key under the account.
+ tags:
+ - private
+ parameters:
+ - name: orderly_key
+ in: query
+ description: The Orderly Key to set the IP restriction list
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: reset_mode
+ in: query
+ description: >-
+ The new mode of the IP restriction for this Orderly Key
+ (ALLOW_ALL_IPS, DISALLOW_ALL_IPS).
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/notification/inbox/notifications:
+ get:
+ summary: Get All Notifications
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `GET /v1/notification/inbox/notifications`
+
+ Get all notifications for the user.
+
+ Currently the only supported message type is the order filled message.
+ tags:
+ - private
+ parameters:
+ - name: type
+ in: query
+ description: Filter nofications by type (TRADE / SYSTEM).
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AllNotificationsResponse"
+ /v1/notification/inbox/unread:
+ get:
+ summary: Get Unread Notifications
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `GET /v1/notification/inbox/unread`
+
+ Get the information on unread messages for the user.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UnreadNotificationsResponse"
+ /v1/notification/inbox/mark_read:
+ post:
+ summary: Set Read Status of Notifications
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `POST /v1/notification/inbox/mark_read`
+
+ Set the read status on a list of notifications for a user.
+ tags:
+ - private
+ parameters:
+ - name: flag
+ in: query
+ description: The value of the read flag, where 1 = READ and 0 = UNREAD
+ required: true
+ example: ""
+ schema:
+ type: number
+ - name: ids
+ in: query
+ description: The list of notification ids to flag as read/unread.
+ required: true
+ example: ""
+ schema:
+ type: array
+ items:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/notification/inbox/mark_read_all:
+ post:
+ summary: Set Read Status of All Notifications
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 60 seconds**
+
+ `POST /v1/notification/inbox/mark_read_all`
+
+ Set the read status on all notifications for a user.
+ tags:
+ - private
+ parameters:
+ - name: flag
+ in: query
+ description: The value of the read flag, where 1 = READ and 0 = UNREAD
+ required: true
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/public/vault_balance:
+ get:
+ summary: Get Vault Balance
+ deprecated: false
+ description: ""
+ tags:
+ - public
+ parameters:
+ - name: chain_id
+ in: query
+ description: "id of the chain you wish to query. `900900900` for Solana."
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: token
+ in: query
+ description: the token you wish to query
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VaultBalanceResponse"
+ /v1/faucet/usdc:
+ post:
+ servers:
+ - url: https://testnet-operator-evm.orderly.org
+ - url: https://testnet-operator-sol.orderly.org
+ summary: Get Faucet USDC(Testnet Only)
+ deprecated: false
+ description: >-
+ For `EVM`:
+
+
+ `GET https://testnet-operator-evm.orderly.org/v1/faucet/usdc`
+
+
+ For `Solana`:
+
+
+ `GET https://testnet-operator-sol.orderly.org/v1/faucet/usdc`
+
+
+ Receive 1,000 USDC in the EVM Testnet environment or 100 USDC in the Solana Testnet environment. Each account may only use
+ the faucet a maximum of 5 times.
+
+
+ This API is only available on Testnet so make sure to use Testnet domain
+ as above instead of Mainnet domain.
+ tags:
+ - public
+ requestBody:
+ content:
+ application/json:
+ schema:
+ required:
+ - user_address
+ - broker_id
+ type: object
+ properties:
+ chain_id:
+ type: string
+ description: The chain ID that the test USDC should be deposited to. Not applicable to Solana faucet.
+ user_address:
+ type: string
+ description: The address of the user account. Use `Zions51qQNUgWNyp4JegUFoMUpgFx43jBUsYmHtDPdr` for Solana address.
+ broker_id:
+ type: string
+ description: Builder ID of an account
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/algo/order:
+ post:
+ summary: Create Algo Order
+ deprecated: false
+ description: >-
+ **Limit: 1 request per 1 second**
+
+
+ `POST /v1/algo/order`
+
+
+ Place maker/taker order that requires an additional trigger for order
+ execution such as stop orders.
+
+
+ `STOP` type order bahavior: an order to buy or sell at the market/limit price
+ once the asset has traded at or through a specified price (the "stop
+ price"). If the asset reaches the stop price, the order becomes a market
+ order and is filled at the next available market price.
+
+
+ To place `Positional TP/SL` order, the input fields is 2 layer and includes an array of the objects named childOrder. The take-profit or stop-loss order should be the objects in the array. For the sub-order in childOrder, please input `CLOSE_POSITION` as type, and `TAKE_PROFIT` or `STOP_LOSS` in algoType field.
+
+ To place `Trailing Stop` order, please use `TRAILING_STOP` as algoType and `MARKET` as type. Please also input your trailing rate setting in callbackRate field.
+
+
+ Sample request can be found [here](/docs/build-on-omnichain/algo-order-samples)
+
+
+ Note: This endpoint requires trading scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ - $ref: "#/components/parameters/recv_window_header"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ symbol:
+ type: string
+ algo_type:
+ type: string
+ description: "`STOP`/`TP_SL`/`POSITIONAL_TP_SL`/`BRACKET`/`BRACKET + TP_SL`/`TRAILING_STOP`"
+ client_order_id:
+ type: string
+ description: "36 length, accepts hyphen but cannot be the first character, default: null"
+ type:
+ type: string
+ description: "`LIMIT` / `MARKET`, required if `algo_type` = `STOP`"
+ price:
+ type: number
+ description: "Optional for `TP_SL` and `POSTIONAL_TP_SL`"
+ quantity:
+ type: number
+ description: >-
+ For `MARKET`/`ASK`/`BID` order, if order_amount is given, it is
+ not required.
+ Not required if type is `POSITIONAL_TP_SL`.
+ trigger_price_type:
+ type: string
+ description: Only `MARK_PRICE` is available for now.
+ trigger_price:
+ type: number
+ reduce_only:
+ type: boolean
+ visible_quantity:
+ type: boolean
+ description: Default false
+ side:
+ type: string
+ description: "`SELL`/`BUY`, required if `STOP` type"
+ order_tag:
+ type: string
+ child_orders:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ algo_type:
+ type: string
+ child_orders:
+ type: array
+ items:
+ $ref: "#/components/schemas/CreateAlgoOrderChildRequest"
+ description: "Array of `CreateAlgoOrderChildRequest`"
+ activatedPrice:
+ type: string
+ description: "Activated price for algoType=`TRAILING_STOP`"
+ callbackRate:
+ type: string
+ description: "Callback rate, only for algoType=`TRAILING_STOP`, i.e. the value = 0.1 represent to 10%."
+ callbackValue:
+ type: string
+ description: "Callback value, only for algoType=`TRAILING_STOP`, i.e. the value = 100"
+ example:
+ - symbol: "PERP_ETH_USDC"
+ algo_type: "BRACKET"
+ quantity: 0.0032
+ side: "BUY"
+ type: "LIMIT"
+ price: 3415.9
+ child_orders:
+ - symbol: "PERP_ETH_USDC"
+ algo_type: "POSITIONAL_TP_SL"
+ child_orders:
+ - symbol: "PERP_ETH_USDC"
+ algo_type: "TAKE_PROFIT"
+ side: "SELL"
+ type: "CLOSE_POSITION"
+ trigger_price: 3518.4
+ reduce_only: true
+ - symbol: "PERP_ETH_USDC"
+ algo_type: "STOP_LOSS"
+ side: "SELL"
+ type: "CLOSE_POSITION"
+ trigger_price: 3313.4
+ reduce_only: true
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "TP_SL"
+ quantity: 5.5
+ trigger_price_type: "MARK_PRICE"
+ child_orders:
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "TAKE_PROFIT"
+ side: "SELL"
+ type: "MARKET"
+ trigger_price: 3.365
+ reduce_only: true
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "STOP_LOSS"
+ side: "SELL"
+ type: "MARKET"
+ trigger_price: 3.36
+ reduce_only: true
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "POSITIONAL_TP_SL"
+ trigger_price_type: "MARK_PRICE"
+ child_orders:
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "TAKE_PROFIT"
+ side: "SELL"
+ type: "CLOSE_POSITION"
+ trigger_price_type: "MARK_PRICE"
+ trigger_price: 4.05
+ reduce_only: true
+ - symbol: "PERP_NEAR_USDC"
+ algo_type: "STOP_LOSS"
+ side: "SELL"
+ type: "CLOSE_POSITION"
+ trigger_price_type: "MARK_PRICE"
+ trigger_price: 3.95
+ reduce_only: true
+
+ required:
+ - symbol
+ - algo_type
+ - quantity
+ - side
+ - type
+ - child_orders
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PostAlgoOrderResponse"
+ put:
+ summary: Edit Algo Order
+ deprecated: false
+ description: >-
+ **Limit: 5 requests per 1 second**
+
+
+ `PUT /v1/algo/order`
+
+
+ Edit a pending algo order by `order_id`. Only the price or quantity can
+ be amended.
+
+
+ Note: This endpoint requires trading scope in Orderly Key.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ order_id:
+ type: string
+ price:
+ type: number
+ quantity:
+ type: number
+ trigger_price:
+ type: number
+ trigger_price_type:
+ type: string
+ child_orders:
+ type: array
+ items:
+ $ref: "#/components/schemas/EditAlgoOrderChildRequest"
+ description: "Array of `EditAlgoOrderChildRequest`"
+ # EditAlgoOrderChildReq:
+ # type: object
+ # properties:
+ # order_id:
+ # type: string
+ # trigger_price:
+ # type: number
+ # price:
+ # type: number
+ # quantity:
+ # type: number
+ # is_activated:
+ # type: string
+ # trigger_price_type:
+ # type: string
+ # child_orders:
+ # type: array
+ # items:
+ # object
+ # description: Array of `EditAlgoOrderChildRequest`
+ required:
+ - order_id
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PutOrderResponse"
+ delete:
+ summary: Cancel Algo Order
+ deprecated: false
+ description: |
+ **Limit: 5 requests per 1 second**
+
+ `DELETE /v1/algo/order?order_id={order_id}&symbol={symbol}`
+
+ Cancels a single algo order by `order_id`.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: order_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteOrderResponse"
+ /v1/algo/client/order:
+ delete:
+ summary: Cancel Algo Order By client_order_id
+ deprecated: false
+ description: >-
+ **Limit: 5 requests per 1 second**
+
+
+ `DELETE
+ /v1/algo/client/order?client_order_id={client_order_id}&symbol={symbol}`
+
+
+ Cancel an algo order by `client_order_id`.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: client_order_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteOrderResponse"
+ /v1/algo/orders:
+ delete:
+ summary: Cancel All Pending Algo Orders
+ deprecated: false
+ description: |-
+ **Limit: 5 requests per 1 second**
+
+ `DELETE /v1/algo/orders?symbol={symbol}`
+
+ Cancel all pending algo orders.
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: algo_type
+ in: query
+ description: "Note: `STOP` will cancel both `TAKE_PROFIT` and `STOP_LOSS`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteAllPendingAlgoOrderResponse"
+ get:
+ summary: Get Algo Orders
+ deprecated: false
+ description: >-
+ **Limit: 5 requests per 1 second**
+
+
+ `GET /v1/algo/orders`
+
+
+ Get algo order details by customized filters.
+
+
+ For filter by status, one can reference special bundled statuses below
+ for ease of access of Open (i.e `INCOMPLETE`) orders or `COMPLETED`
+ orders.
+
+
+ `INCOMPLETE` = `NEW` + `PARTIAL_FILLED`
+
+
+
+ `COMPLETED` = `CANCELLED` + `FILLED`
+ tags:
+ - private
+ parameters:
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: order_type
+ in: query
+ description: "`LIMIT`/`MARKET`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: >-
+ `NEW`/`CANCELLED`/`PARTIAL_FILLED`/`FILLED`/`REJECTED`/`INCOMPLETE`/`COMPLETED`
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: order_tag
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that wish to query, noted the time stamp is
+ 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: end_t
+ in: query
+ description: >-
+ end time range that wish to query, noted the time stamp is 13-digits
+ timestamp.
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: side
+ in: query
+ description: "`BUY`/`SELL`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: algo_type
+ in: query
+ description: "Supported types are `STOP`, `TPSL`, `positional_TPSL`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: is_triggered
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AlgoOrdersResponse"
+
+ /v1/algo/order/{order_id}:
+ get:
+ summary: Get Algo Order by order_id
+ deprecated: false
+ description: |-
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/algo/order/{order_id}`
+
+ Get details of a single algo order by order_id.
+ tags:
+ - private
+ parameters:
+ - name: order_id
+ in: path
+ description: id of the order
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AlgoOrderResponse"
+ /v1/algo/client/order/{client_order_id}:
+ get:
+ summary: Get Algo Order by client_order_id
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/algo/client/order/{client_order_id}`
+
+ Get details of a single algo order by `client_order_id`.
+ tags:
+ - private
+ parameters:
+ - name: client_order_id
+ in: path
+ description: client_order_id of the order
+ required: true
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AlgoClientOrderResponse"
+ /v1/algo/order/{order_id}/trades:
+ get:
+ summary: Get All Trades of Specific Algo Order
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `GET /v1/algo/order/{order_id}/trades`
+
+ Get specific trades of an algo order by `order_id`.
+ tags:
+ - private
+ parameters:
+ - name: order_id
+ in: path
+ description: ""
+ required: true
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AlgoOrderTradesResponse"
+ /v1/public/account:
+ get:
+ summary: Check if Account Exists
+ deprecated: false
+ description: |-
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/account`
+
+ Check if provided account account is registered with Orderly and it's respective builder.
+ tags:
+ - public
+ parameters:
+ - name: account_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAccountDetailsResponse"
+
+ /v1/public/announcement:
+ get:
+ summary: Get Announcements
+ deprecated: false
+ description: |-
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/announcement`
+
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAnnouncementsResponse"
+
+ /v1/volume/broker/daily:
+ get:
+ summary: Get Builder's Users' Volumes
+ deprecated: false
+ description: >
+ **Limit 10 requests per 60 seconds**
+
+
+ `GET /v1/volume/broker/daily`
+
+
+ Get the daily historical breakdown of the user trading volume on
+ specified builder.
+
+
+ The provided `start_date`/`end_date` has to be within a 90-day range.
+
+ Updated hourly
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: order_tag
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: aggregateBy
+ in: query
+ description: "Either by `DATE` or by `ACCOUNT`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: sort
+ in: query
+ description: |
+ Allow sort by
+
+ `ascending_broker_fee`
+ `descending_broker_fee`
+ `ascending_perp_maker_volume`
+ `descending_perp_maker_volume`
+ `ascending_perp_taker_volume`
+ `descending_perp_taker_volume`
+ `ascending_perp_volume`
+ `descending_perp_volume`
+ `ascending_total_fee`
+ `descending_total_fee`
+ required: false
+ schema:
+ type: string
+ - name: orderly-timestamp
+ in: header
+ description: ""
+ required: true
+ example: "1649920583000"
+ schema:
+ type: string
+ - name: orderly-account-id
+ in: header
+ description: ""
+ required: true
+ example: "0x0f29bfb4c1bc9fea3f3be46bab6d795e22a6272354b136fde05f6b80cfcad546"
+ schema:
+ type: string
+ - name: orderly-key
+ in: header
+ description: ""
+ required: true
+ example: ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX
+ schema:
+ type: string
+ - name: orderly-signature
+ in: header
+ description: ""
+ required: true
+ example: >-
+ dG4bkKiqG0dUYLzViRZkvbI6Sy239JxAdNMIBxFZ4w030Jofr0ORV06GHtvXZkaZaWUXE+XAU3fnzKN/5fDeBQ==
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetBrokerDailyResponse"
+
+ /v1/broker/leaderboard/daily:
+ get:
+ summary: Get Builder's Leaderboard
+ deprecated: false
+ description: >
+ **Limit 10 requests per 60 seconds**
+
+
+ `GET /v1/broker/leaderboard/daily`
+
+
+ The provided `start_date`/`end_date` has to be within a 90-day range.
+
+
+ Updated hourly
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: order_tag
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: broker_id
+ in: query
+ description: "if the broker_id is empty, return all users"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: sort
+ in: query
+ description: |
+ Allow sort by
+
+ `ascending_realized_pnl`
+ `descending_realized_pnl`
+ `ascending_perp_volume`
+ `descending_perp_volume`
+ required: false
+ schema:
+ type: string
+ - name: address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: aggregateBy
+ in: query
+ description: |
+ `address`: Sum the value across all builders.
+ `address_per_builder`: Sum the value separately for each builder.
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LeaderboardResponse"
+ /v1/client/statistics/daily:
+ get:
+ summary: Get User Daily Statistics
+ deprecated: false
+ description: |
+ **Limit 10 requests per 60 seconds**
+
+ `GET /v1/client/statistics/daily`
+
+ Get user daily statistics of assets/pnl/volume.
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: Format YYYY-MM-DD
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: number
+ - name: include_historical_data
+ in: query
+ description: "When include_historical_data is set to True, the rate limit for this API becomes 1 request per 60 seconds, the API response will include archived data."
+ required: false
+ example: "False"
+ schema:
+ type: boolean
+ - name: orderly-timestamp
+ in: header
+ description: ""
+ required: true
+ example: "1649920583000"
+ schema:
+ type: string
+ - name: orderly-account-id
+ in: header
+ description: ""
+ required: true
+ example: "0x0f29bfb4c1bc9fea3f3be46bab6d795e22a6272354b136fde05f6b80cfcad546"
+ schema:
+ type: string
+ - name: orderly-key
+ in: header
+ description: ""
+ required: true
+ example: ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX
+ schema:
+ type: string
+ - name: orderly-signature
+ in: header
+ description: ""
+ required: true
+ example: >-
+ dG4bkKiqG0dUYLzViRZkvbI6Sy239JxAdNMIBxFZ4w030Jofr0ORV06GHtvXZkaZaWUXE+XAU3fnzKN/5fDeBQ==
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetUserDailyStatsResponse"
+
+ /v1/broker/user_info:
+ get:
+ summary: Get User Fee Rates
+ deprecated: false
+ description: |
+ **Limit 60 requests per 60 seconds**
+
+ `GET /v1/broker/user_info`
+
+ Scope: Only each builder_id's admin wallet can call this endpoint.
+
+ If `account_id` or `address` is provided, will always return the fee rate and indicate whether the user is on the builder’s default fee rate or not.
+ tags:
+ - private
+ parameters:
+ - name: account_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UserFeeRatesResponse"
+
+ /v1/broker/fee_rate/set:
+ post:
+ summary: Update User Fee Rate
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/broker/fee_rate/set`
+
+ Send a list of accounts assigned to a new fee rate. Max 500 accounts per request.
+
+ Scope: Only each builder_id's admin wallet can call this endpoint.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ maker_fee_rate:
+ type: number
+ description: "Maker fee charged to users by default (0.0001 = 0.01%)"
+ taker_fee_rate:
+ type: number
+ description: "Taker fee charged to users by default (0.0001 = 0.01%)"
+ rwa_taker_fee_rate:
+ type: number
+ rwa_maker_fee_rate:
+ type: number
+ account_ids:
+ type: array
+ items:
+ type: string
+ description: "List of account_ids for which the fees need to be changed"
+ required:
+ - maker_fee_rate
+ - taker_fee_rate
+ - account_ids
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/broker/fee_rate/set_default:
+ post:
+ summary: Reset User Fee Rate
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/broker/fee_rate/set_default`
+
+ Assign users to a default fee rate. Only send accounts that need to be updated. Max 500 accounts per request.
+
+ Scope: Only each builder’s admin can create the referral code for accounts that are related to the builder_id
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ account_ids:
+ type: array
+ items:
+ type: string
+ description: "List of account_ids for which the fees need to be changed"
+ required:
+ - account_ids
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/broker/fee_rate/default:
+ post:
+ summary: Update Default Builder Fee
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/broker/fee_rate/default`
+
+ Update the default fee rate for users assigned to a default fee rate.
+
+ Scope: Only each builder_id’s admin wallet can call this endpoint
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ maker_fee_rate:
+ type: number
+ description: "Maker fee charged to users by default (0.0001 = 0.01%)"
+ taker_fee_rate:
+ type: number
+ description: "Taker fee charged to users by default (0.0001 = 0.01%)"
+ rwa_taker_fee_rate:
+ type: number
+ rwa_maker_fee_rate:
+ type: number
+ required:
+ - maker_fee_rate
+ - taker_fee_rate
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ get:
+ summary: Get Default Builder Fee
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `GET /v1/broker/fee_rate/default`
+
+ Get default fee rate.
+
+ Scope: Only each builder_id’s admin wallet can call this endpoint
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetDefaultBrokerFeesResponse"
+
+ /v1/referral/create:
+ post:
+ summary: Create Referral Code
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/referral/create`
+
+ Scope: Only each builder’s admin can create the referral code for accounts that are related to the builder_id
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ account_id:
+ type: string
+ description: the account id of the referrer
+ referral_code:
+ type: string
+ description: the referral code that admin wants to create
+ max_rebate_rate:
+ type: number
+ description: the referral code’s rebate rate (max 1)
+ referrer_rebate_rate:
+ type: number
+ description: rebate rate of referrer
+ referee_rebate_rate:
+ type: number
+ description: rebate rate of referee
+ required:
+ - account_id
+ - referral_code
+ - max_rebate_rate
+ - referrer_rebate_rate
+ - referee_rebate_rate
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/referral/admin_info:
+ get:
+ summary: Get Referral Code Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/referral/admin_info`
+
+ Scope: Only each builder’s admin wallet can call this endpoint.
+ tags:
+ - private
+ parameters:
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: size
+ in: query
+ description: "Only one of `user_address` and `account_id` can be provided"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: user_address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: account_id
+ in: query
+ description: "Only one of `user_address` and `account_id` can be provided"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: sort_by
+ in: query
+ required: false
+ description: "`total_invites`/`total_traded`/`referee_volume` descending"
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetReferralCodeResponse"
+
+ /v1/public/referral/check_ref_code:
+ get:
+ summary: Check Referral Code
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/referral/check_ref_code`
+ tags:
+ - public
+ parameters:
+ - name: account_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CheckReferralCodeResponse"
+
+ /v1/public/referral/verify_ref_code:
+ get:
+ summary: Verify Referral Code
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+
+ `GET /v1/public/referral/verify_ref_code`
+ tags:
+ - public
+ parameters:
+ - name: referral_code
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VerifyReferralCodeResponse"
+
+ /v1/referral/update:
+ post:
+ summary: Update Referral Code
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/referral/update`
+
+ Scope: Only each Builder’s admin wallet can call this endpoint.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ account_id:
+ type: string
+ description: the account id of the referrer
+ referral_code:
+ type: string
+ description: the referral code that admin wants to create
+ max_rebate_rate:
+ type: number
+ description: the referral code’s rebate rate (max 1)
+ referrer_rebate_rate:
+ type: number
+ description: rebate rate of referrer
+ referee_rebate_rate:
+ type: number
+ description: rebate rate of referee
+ required:
+ - account_id
+ - referral_code
+ - max_rebate_rate
+ - referrer_rebate_rate
+ - referee_rebate_rate
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/referral/bind:
+ post:
+ summary: Bind Referral Code
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/referral/bind`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ referral_code:
+ type: string
+ description: the referral code that admin wants to bind
+ required:
+ - referral_code
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/referral/info:
+ get:
+ summary: Get Referral Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/referral/info`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetReferralInfoResponse"
+
+ /v1/referral/referral_history:
+ get:
+ summary: Get Referral History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/referral/referral_history`
+
+ Provides a detailed history of referral rebates paid to the user.
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: "start date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: "end date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetReferralHistoryResponse"
+
+ /v1/referral/rebate_summary:
+ get:
+ summary: Get Referral Rebate Summary
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/referral/rebate_summary`
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ description: "max date selection range is 3 months"
+ - name: end_date
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ description: "max date selection range is 3 months"
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetRebateSummaryResponse"
+
+ /v1/referral/referee_history:
+ get:
+ summary: Get Referee History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/referral/referee_history`
+
+ Provides a summary of all referee activity for the requesting user.
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: "start date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: "end date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetRefereeHistoryResponse"
+
+ /v1/referral/referee_info:
+ get:
+ summary: Get Referee Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+
+ `GET /v1/referral/referee_info`
+ tags:
+ - private
+ parameters:
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: sort
+ in: query
+ description: |
+ `ascending_code_binding_time`
+ `descending_code_binding_time`
+ `ascending_referral_rebate`
+ `descending_referral_rebate`
+ `ascending_volume`
+ `descending_volume`
+ example: "ascending_volume"
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetRefereeInfoResponse"
+
+ /v1/referral/referee_rebate_summary:
+ get:
+ summary: Get Referee Rebate Summary
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+
+ `GET /v1/referral/referee_rebate_summary`
+
+
+ Provides daily statistics on referee rebates
+ tags:
+ - private
+ parameters:
+ - name: start_date
+ in: query
+ description: "start date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: end_date
+ in: query
+ description: "end date range that you wish to query, in the format ‘YYYY-MM-DD’"
+ required: true
+ example: ""
+ schema:
+ type: string
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetRefereeRebateSummaryResponse"
+
+ /v1/referral/edit_split:
+ post:
+ summary: Edit Referral Code Split
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/referral/edit_split`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ referral_code:
+ type: string
+ description: the referral code that admin wants to create
+ referrer_rebate_rate:
+ type: number
+ description: the rebate rate of referrer
+ referee_rebate_rate:
+ type: number
+ description: the rebate rate of referee
+ required:
+ - referral_code
+ - referrer_rebate_rate
+ - referee_rebate_rate
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/referral/auto_referral/update:
+ post:
+ summary: Builder admin update auto referral
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/referral/auto_referral/update`
+
+ Scope: Only each Builder’s admin wallet can call this endpoint.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ required_trading_volume:
+ type: number
+ max_rebate:
+ type: number
+ referrer_rebate:
+ type: number
+ referee_rebate:
+ type: number
+ enable:
+ type: boolean
+ description:
+ type: string
+ description: All users who receive a ref code will see this description displayed in the admin tool.
+ required:
+ - required_trading_volume
+ - max_rebate
+ - referrer_rebate
+ - referee_rebate
+ - enable
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/referral/auto_referral/info:
+ get:
+ summary: Builder admin get auto referral info
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `GET /v1/referral/auto_referral/info`
+
+ Scope: Only each Builder’s admin wallet can call this endpoint.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BuilderAutoReferralInfoResponse"
+
+ /v1/referral/auto_referral/progress:
+ get:
+ summary: Get auto referral progress
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `GET /v1/referral/auto_referral/progress`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AutoReferralProgressResponse"
+
+ /v1/referral/edit_referral_code:
+ post:
+ summary: Edit Referral Code
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `POST /v1/referral/edit_referral_code`
+
+ Only Auto generated code can be updated.
+
+ The referral code can only be changed if it hasn’t been bound by other user yet.
+
+ There is no limit to the number of times it can be changed.
+
+ The referral code must be unique platform-wise.
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ current_referral_code:
+ type: string
+ new_referral_code:
+ type: string
+ required:
+ - current_referral_code
+ - new_referral_code
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/public/points/leaderboard:
+ get:
+ summary: Get Merits Leaderboard
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/points/leaderboard`
+ tags:
+ - public
+ parameters:
+ - name: start_r
+ in: query
+ description: "Starting rank you wish to query"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: end_r
+ in: query
+ description: "Ending rank you wish to query"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: epoch_id
+ in: query
+ description: "the epoch for which you want to check the leaderboard"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetPointsLeaderboardResponse"
+
+ /v1/client/points:
+ get:
+ summary: Get User's Merits
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per user**
+
+ `GET /v1/client/points`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: "sample_address"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetPointsResponse"
+
+ /v1/public/points/epoch_dates:
+ get:
+ summary: Get Start and End Date of All Epochs
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per user**
+
+ `GET /v1/public/points/epoch_dates`
+
+ Retrieve the dates of all epochs
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetEpochDatesResponse"
+
+ /v1/public/points/epoch:
+ get:
+ summary: Get Number of Merits for Distribution
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per user per IP address**
+
+ `GET /v1/public/points/epoch`
+
+ Retrieve the number of merits for past epochs and current epoch
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetEpochPointsResponse"
+
+ /v1/delegate_signer:
+ post:
+ summary: Delegate Signer
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/delegate_signer`
+ tags:
+ - public
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DelegateSignerBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DelegateSignerResponse"
+
+ /v1/delegate_orderly_key:
+ post:
+ summary: Add Delegate Signer Orderly Key
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/delegate_orderly_key`
+ tags:
+ - public
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DelegateOrderlyKeyBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OrderlyKeyResponse"
+
+ /v1/delegate_withdraw_request:
+ post:
+ summary: Delegate Signer Withdraw Request
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/delegate_withdraw_request`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DelegateWithdrawRequestBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WithdrawResponse"
+
+ /v1/delegate_settle_pnl:
+ post:
+ summary: Delegate Signer Settle PnL
+ deprecated: false
+ description: |
+ **Limit: 1 requests per second**
+
+ `POST /v1/delegate_settle_pnl`
+ tags:
+ - private
+ parameters:
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DelegateSettlePnlBody"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SettlePnlResponse"
+
+ /v1/client/distribution_history:
+ get:
+ summary: Get Distribution History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per second**
+
+ `GET /v1/client/distribution_history`
+ tags:
+ - private
+ parameters:
+ - name: status
+ in: query
+ example: "true"
+ description: "One of `CREATED`/`SPLIT`/`COMPLETED`"
+ schema:
+ type: string
+ - name: type
+ in: query
+ example: "true"
+ description: "One of `BROKER_FEE`/`REFEREE_REBATE`/`REFERRER_REBATE`"
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetDistributionHistoryResponse"
+
+ /v1/client/campaign/sign_up:
+ post:
+ summary: Sign Up Campaign
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second**
+
+ `POST /v1/client/campaign/sign_up`
+ tags:
+ - private
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ campaign_id:
+ type: number
+ required:
+ - campaign_id
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/public/campaigns:
+ get:
+ summary: Get List of Campaigns
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/campaigns`
+ tags:
+ - public
+ parameters:
+ - name: start_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: end_t
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: only_show_alive
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: address
+ in: query
+ description: "Filter the list that this address has participated in."
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignsResponse"
+
+ /v1/public/campaign/stats:
+ get:
+ summary: Get Campaign Statistics
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/campaign/stats`
+ tags:
+ - public
+ parameters:
+ - name: campaign_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: integer
+ - name: broker_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignStatsResponse"
+
+ /v1/public/campaign/stats/details:
+ get:
+ summary: Get Detailed Campaign Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 minute per IP address**
+
+ `GET /v1/public/campaign/stats/details`
+ tags:
+ - public
+ parameters:
+ - name: campaign_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: integer
+ - name: broker_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: symbols
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: group_by
+ in: query
+ description: "One of `BROKER` \ `SYMBOL` \ `NONE`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignDetailedStatsResponse"
+
+ /v1/public/campaign/ranking:
+ get:
+ summary: Get Campaign Ranking
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/campaign/ranking`
+ tags:
+ - public
+ parameters:
+ - name: campaign_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: integer
+ - name: broker_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "start from 1"
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: size
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: integer
+ - name: sort_by
+ in: query
+ description: "`volume`/`pnl`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: min_pnl
+ in: query
+ description: "Return only records where PnL is greater than or equal to this value."
+ required: false
+ example: "1000"
+ schema:
+ type: integer
+ - name: min_volume
+ in: query
+ description: "Return only records where Volume is greater than or equal to this value."
+ required: false
+ example: "5000"
+ schema:
+ type: integer
+ - name: aggregate_by
+ in: query
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignRankingResponse"
+
+ /v1/public/campaign/user:
+ get:
+ summary: Get Campaign User Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/campaign/user`
+ tags:
+ - public
+ parameters:
+ - name: campaign_id
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: integer
+ - name: account_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: broker_id
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: []
+ schema:
+ type: array
+ items:
+ type: string
+ example: "PERP_WOO_USDC"
+ - name: order_tag
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignUserResponse"
+
+ /v1/public/campaign/check:
+ get:
+ summary: Get Campaign Verification
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/campaign/check`
+ tags:
+ - public
+ parameters:
+ - name: campaign_id
+ in: query
+ description: ""
+ required: true
+ example: 10
+ schema:
+ type: number
+ - name: type
+ in: query
+ description: "`volume`/`deposit`/`withdraw`/`order_count`"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: address
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: lower_boundary
+ in: query
+ description: ""
+ required: false
+ example: 100
+ schema:
+ type: number
+ - name: cmp
+ in: query
+ description: "`gt`/`ge`/`le`/`lt`, default `gt`"
+ required: false
+ example: "gt"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCampaignCheckResponse"
+
+ /v1/staking/balance:
+ get:
+ summary: Get Wallet's Current Staked Balance
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/staking/balance`
+ tags:
+ - private
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetWalletStakedBalanceResponse"
+
+ /v1/staking/unstake_details:
+ get:
+ summary: Get Unstaking ORDER Details
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/staking/unstake_details`
+ tags:
+ - private
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetUnstakeDetailsResponse"
+
+ /v1/staking/overview:
+ get:
+ summary: Get Staking Overview
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/staking/overview`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetStakingOverviewResponse"
+
+ /v1/staking/valor/batch_info:
+ get:
+ summary: Get Valor Batch Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/staking/valor/batch_info`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetValorBatchInfoResponse"
+
+ /v1/staking/valor/pool_info:
+ get:
+ summary: Get Valor Pool Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/staking/valor/pool_info`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetValorPoolInfoResponse"
+
+ /v1/staking/valor/redeem:
+ get:
+ summary: Get Valor Redeem Info
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/staking/valor/redeem`
+ tags:
+ - private
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetValorRedeemInfoResponse"
+
+ /v1/staking/esorder/vesting_list:
+ get:
+ summary: Get esORDER vesting list
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/staking/esorder/vesting_list`
+ tags:
+ - private
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: is_vesting
+ in: query
+ description: ""
+ required: true
+ example: "False"
+ schema:
+ type: boolean
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetesORDERVestingListResponse"
+
+ /v1/public/trading_rewards/epoch_info:
+ get:
+ summary: Get Parameters of Each Epoch for All Epochs
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/trading_rewards/epoch_info`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetParameterOfEachEpochForAllEpochsResponse"
+
+ /v1/public/trading_rewards/epoch_data:
+ get:
+ summary: Get Epochs Data
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Get data of each epoch from epoch 1 to current epoch
+
+
+ `GET /v1/public/trading_rewards/epoch_data`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetEpochsDataResponse"
+
+ /v1/public/trading_rewards/broker_allocation_history:
+ get:
+ summary: Get Broker Allocation History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Return each epoch from Epoch 1 to last completed epoch.
+
+
+ `GET /v1/public/trading_rewards/broker_allocation_history`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetBrokerAllocationHistoryResponse"
+
+ /v1/public/trading_rewards/wallet_rewards_history:
+ get:
+ summary: Get Wallet Trading Rewards History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Return each epoch from Epoch 1 to last completed epoch.
+
+
+ `GET /v1/public/trading_rewards/wallet_rewards_history`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetWalletRewardsHistoryResponse"
+
+ /v1/public/trading_rewards/account_rewards_history:
+ get:
+ summary: Get Account Trading Rewards History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Return each epoch from Epoch 1 to last completed epoch.
+
+
+ `GET /v1/public/trading_rewards/account_rewards_history`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAccountRewardsHistoryResponse"
+
+ /v1/public/trading_rewards/status:
+ get:
+ summary: Get the Status of Trading Rewards Programme
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/trading_rewards/status`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetTheStatusOfTradingRewardsProgrammeResponse"
+
+ /v1/public/trading_rewards/symbol_category:
+ get:
+ summary: Get Symbol Rewards Category
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/trading_rewards/symbol_category`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetSymbolRewardsCategoryResponse"
+
+ /v1/public/market_making_rewards/epoch_info:
+ get:
+ summary: Get Parameters of Each Epoch
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_making_rewards/epoch_info`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetParameterOfEachMMEpochForAllMMEpochsResponse"
+
+ /v1/public/market_making_rewards/status:
+ get:
+ summary: Get the Status of Market Making Rewards Programme
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_making_rewards/status`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetTheStatusOfMarketMakingRewardsProgrammeResponse"
+
+ /v1/public/market_making_rewards/symbol_params:
+ get:
+ summary: Get Symbol Rewards Parameters
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/market_making_rewards/symbol_params`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetSymbolRewardsParametersResponse"
+
+ /v1/public/market_making_rewards/leaderboard:
+ get:
+ summary: Leaderboard for market maker rewards
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET v1/public/market_making_rewards/leaderboard`
+ tags:
+ - public
+ parameters:
+ - name: epoch
+ in: query
+ description: ""
+ required: true
+ example:
+ schema:
+ type: number
+ - name: market
+ in: query
+ description: "return all markets if empty"
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LeaderboardForMarketMakerRewardsResponse"
+
+ /v1/public/market_making_rewards/current_epoch_estimate:
+ get:
+ summary: Get Market Maker Current Epoch Estimate
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ `GET /v1/public/market_making_rewards/current_epoch_estimate`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: chain_type
+ in: query
+ description: "`EVM` or `SOL`"
+ required: false
+ example: "EVM"
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetMMCurrentEpochEstimateResponse"
+
+ /v1/public/trading_rewards/current_epoch_broker_estimate:
+ get:
+ summary: Get Current Epoch Estimate by Broker
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Get all broker’s estimated rewards allocation during the current epoch
+
+
+ `GET /v1/public/trading_rewards/current_epoch_broker_estimate`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCurrentEpochBrokerEstimateResponse"
+
+ /v1/public/trading_rewards/current_epoch_estimate:
+ get:
+ summary: Get Current Epoch Estimate
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Get user’s estimated trading rewards during the current epoch
+
+
+ `GET /v1/public/trading_rewards/current_epoch_estimate`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetCurrentEpochEstimateResponse"
+
+ /v1/public/market_making_rewards/group_rewards_history:
+ get:
+ summary: Get Wallet Group Market Making Rewards History
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+
+ Return each epoch from Epoch 1 to last completed epoch.
+
+
+ `GET /v1/public/market_making_rewards/group_rewards_history`
+ tags:
+ - public
+ parameters:
+ - name: address
+ in: query
+ description: ""
+ required: true
+ example: ""
+ schema:
+ type: string
+ - name: symbol
+ in: query
+ description: ""
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetWalletGroupMarketMakingRewardsHistory"
+
+ /v1/public/sv_nonce:
+ get:
+ summary: Get Strategy Vault Nonce for Account Transaction
+ deprecated: false
+ description: |
+ **Limit: 10 requests per 1 second per IP address**
+
+ `GET /v1/public/sv_nonce`
+ tags:
+ - public
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SvNonceResponse"
+
+ /v1/account_sv_transaction_history:
+ get:
+ summary: Get Account’s Strategy Vault Transaction history
+ deprecated: false
+ description: |
+ `GET /v1/account_sv_transaction_history`
+
+ Get account transaction history with strategy vaults
+ tags:
+ - public
+ parameters:
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ - name: vault_id
+ in: query
+ description: >-
+ if vault_id is empty, it will return all vaults
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: start_t
+ in: query
+ description: >-
+ start time range that you wish to query, noted that the time stamp
+ is a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: end_t
+ in: query
+ description: >-
+ end time range that you wish to query, noted that the time stamp is
+ a 13-digits timestamp.
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: the page you wish to query. start from 1
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: size
+ in: query
+ description: "Default: 60"
+ required: false
+ example: ""
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: "deposit/withdraw, if empty = return all"
+ required: false
+ example: ""
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetAccountStrategyVaultTransactionHistory"
+
+ /v1/sv_operation_request:
+ post:
+ summary: Create Strategy Vault Deposit/Withdrawal Request with Account
+ description: |
+
+ **Limit: 10 requests per 1 second per IP address**
+
+ `POST /v1/sv_operation_request`
+
+ Deposit into a strategy vault with account balance / withdraw from a strategy vault into your account balance
+ tags:
+ - public
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: object
+ properties:
+ payloadType:
+ type: number
+ description: "LP_DEPOSIT=0/LP_WITHDRAW=1/SP_DEPOSIT=2/SP_WITHDRAW=3"
+ nonce:
+ type: string
+ description: ""
+ receiver:
+ type: string
+ description: ""
+ amount:
+ type: string
+ description: ""
+ vaultId:
+ type: string
+ description: ""
+ token:
+ type: string
+ description: ""
+ dexBrokerId:
+ type: string
+ description: ""
+ chainId:
+ type: number
+ description: ""
+ chainType:
+ type: string
+ description: ""
+ signature:
+ type: string
+ userAddress:
+ type: string
+ verifyingContract:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/sv/sp_orderly_key:
+ post:
+ summary: Add SP Orderly Key
+ description: |
+
+ **Limit: 10 requests per second per IP address**
+
+ `POST /v1/sv/sp_orderly_key`
+
+ Adds an Orderly access key to a SP account
+ tags:
+ - private
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: object
+ properties:
+ contract:
+ type: string
+ description: ""
+ brokerId:
+ type: string
+ description: ""
+ chainId:
+ type: number
+ description: ""
+ orderlyKey:
+ type: string
+ description: ""
+ scope:
+ type: string
+ description: ""
+ timestamp:
+ type: number
+ description: ""
+ expiration:
+ type: number
+ description: ""
+ subAccountId:
+ type: string
+ description: create the api key for the input subAccountId
+ signature:
+ type: string
+ userAddress:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+
+ /v1/sv/sp_settle_pnl:
+ post:
+ summary: Request SP PnL Settlement
+ description: |
+
+ **Limit: 1 request per second**
+
+ `POST /v1/sv/sp_settle_pnl`
+ tags:
+ - private
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: object
+ properties:
+ contract:
+ type: string
+ description: ""
+ brokerId:
+ type: string
+ description: ""
+ chainId:
+ type: number
+ description: ""
+ settleNonce:
+ type: string
+ description: ""
+ timestamp:
+ type: number
+ description: ""
+ signature:
+ type: string
+ userAddress:
+ type: string
+ verifyingContract:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/sv/manual_period_delivery:
+ post:
+ summary: Trigger Manual Period Delivery
+ description: |
+
+ **Limit: 1 request per second per account**
+
+ `POST /v1/sv/manual_period_delivery`
+ tags:
+ - private
+ parameters:
+ - name: account_id
+ in: query
+ description: ""
+ required: true
+ schema:
+ type: string
+ - name: orderly_key
+ in: query
+ description: ""
+ required: true
+ schema:
+ type: string
+ - name: orderly_signature
+ in: query
+ description: ""
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - plan_id
+ properties:
+ plan_id:
+ type: string
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BasicResponse"
+ /v1/sv/venue_transfer_history:
+ get:
+ summary: Get Fund Inflow Allocation
+ description: |
+
+ **Limit: 10 requests per second per IP address**
+
+ `GET /v1/sv/venue_transfer_history`
+ tags:
+ - public
+ parameters:
+ - name: period_number
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VenueTransferHistoryResponse"
+ /v1/sv/venue_withdrawal_history:
+ get:
+ summary: Get Fund Outflow Allocation
+ description: |
+
+ **Limit: 10 requests per second per account**
+
+ `GET /v1/sv/venue_withdrawal_history`
+ tags:
+ - public
+ parameters:
+ - name: period_number
+ in: query
+ description: ""
+ required: false
+ schema:
+ type: number
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/VenueWithdrawalHistoryResponse"
+ /v1/sv/protocol_revenue_share_history:
+ get:
+ summary: Get Orderly Protocol Revenue Sharing History
+ description: |
+
+ **Limit: 10 requests per second per account**
+
+ `GET /v1/sv/protocol_revenue_share_history`
+ tags:
+ - public
+ parameters:
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ShareHistoryResponse"
+ /v1/sv/liquidation_fees_share_history:
+ get:
+ summary: Get Liquidation Fees Sharing History
+ description: |
+
+ **Limit: 10 requests per second per account**
+
+ `GET /v1/sv/liquidation_fees_share_history`
+ tags:
+ - public
+ parameters:
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ShareHistoryResponse"
+ /v1/sv/internal_transfer_history:
+ get:
+ summary: Get Internal Transfer History
+ description: |
+
+ **Limit: 10 requests per second per account**
+
+ `GET /v1/sv/internal_transfer_history`
+
+ Non-Vault Account to Strategy Provider Account
+ tags:
+ - public
+ parameters:
+ - name: page
+ in: query
+ description: "the page you wish to query start from 1"
+ required: false
+ schema:
+ type: number
+ - name: size
+ in: query
+ description: "the page size you wish to query (max: 500)"
+ required: false
+ schema:
+ type: number
+ - $ref: "#/components/parameters/orderly_timestamp"
+ - $ref: "#/components/parameters/orderly_account_id"
+ - $ref: "#/components/parameters/orderly_key"
+ - $ref: "#/components/parameters/orderly_signature"
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/InternalTransferHistoryResponse"
+components:
+ parameters:
+ orderly_timestamp:
+ name: orderly-timestamp
+ in: header
+ description: ""
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ example: "1649920583000"
+ orderly_account_id:
+ name: orderly-account-id
+ in: header
+ description: ""
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ example: 0x0f29bfb4c1bc9fea3f3be46bab6d795e22a6272354b136fde05f6b80cfcad546
+ orderly_key:
+ name: orderly-key
+ in: header
+ description: ""
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ example: ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX
+ orderly_signature:
+ name: orderly-signature
+ in: header
+ description: ""
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ example: dG4bkKiqG0dUYLzViRZkvbI6Sy239JxAdNMIBxFZ4w030Jofr0ORV06GHtvXZkaZaWUXE+XAU3fnzKN/5fDeBQ==
+ recv_window_header:
+ name: x-recv-window
+ in: header
+ description: "Use this parameter to control the timeout threshold for placing order, unit in miliseconds"
+ required: false
+ schema:
+ type: number
+ example: 20
+ schemas:
+ BasicResponse:
+ required:
+ - success
+ type: object
+ properties:
+ success:
+ type: boolean
+ example: true
+ timestamp:
+ type: integer
+ example: 1702989203989
+ AddSubAccountResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - main_account_id
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - sub_account_id
+ - description
+ properties:
+ sub_account_id:
+ type: number
+ example: 123
+ description:
+ type: string
+ example: "abc"
+
+ SubAccountResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - main_account_id
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - main_account_id
+ - rows
+ properties:
+ main_account_id:
+ type: number
+ example: 123
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - sub_account_id
+ - description
+ properties:
+ sub_account_id:
+ type: number
+ example: 123
+ description:
+ type: string
+ example: "abc"
+
+ SvNonceResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ nonce:
+ type: number
+ example: 14001212121234
+
+ GetAccountStrategyVaultTransactionHistory:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - vault_id
+ - created_time
+ - type
+ - status
+ - amount_change
+ properties:
+ vault_id:
+ type: string
+ created_time:
+ type: integer
+ example: 1734652800000
+ type:
+ type: string
+ example: "withdrawal"
+ status:
+ type: string
+ example: "pending"
+ description: "`pending` / `completed` / `failed`"
+ asset:
+ type: string
+ example: "USDC"
+ amount_change:
+ type: integer
+ example: 100.000000
+ description: |
+ All values are positive. When type = `deposit`, `amount_change` is shown normally.
+ When type = `withdrawal`, only when status == `completed` return `amount_change` value, all other status return null.
+ shares_change:
+ type: integer
+ example: 88.009000
+ description: |
+ All values are positive. When type = `withdrawal`, `shares_change` is shown normally.
+ When type = `deposit`, only when status == `completed` return `shares_change` value, all other status return null.
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+
+ AggregatePositionsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ account_id:
+ type: string
+ IMR_withdraw_orders:
+ type: number
+ MMR_with_orders:
+ type: number
+ average_open_price:
+ type: number
+ cost_position:
+ type: number
+ est_liq_price:
+ type: number
+ fee_24_h:
+ type: number
+ imr:
+ type: number
+ last_sum_unitary_funding:
+ type: number
+ mark_price:
+ type: number
+ mmr:
+ type: number
+ pending_long_qty:
+ type: number
+ pending_short_qty:
+ type: number
+ pnl_24_h:
+ type: number
+ position_qty:
+ type: number
+ settle_price:
+ type: number
+ symbol:
+ type: string
+ seq:
+ type: number
+ timestamp:
+ type: number
+ updated_time:
+ type: number
+ unsettled_pnl:
+ type: number
+ leverage:
+ type: number
+ example:
+ - account_id: "0x11111"
+ IMR_withdraw_orders: 0.1
+ MMR_with_orders: 0.05
+ average_open_price: 27908.14386047
+ cost_position: -139329.358492
+ est_liq_price: 117335.92899428
+ fee_24_h: 0
+ imr: 0.1
+ last_sum_unitary_funding: 70.38
+ mark_price: 27794.9
+ mmr: 0.05
+ pending_long_qty: 0
+ pending_short_qty: 0
+ pnl_24_h: 0
+ position_qty: -5
+ settle_price: 27865.8716984
+ symbol: "PERP_BTC_USDC"
+ seq: 1730181536341943600
+ timestamp: 1685429350571
+ updated_time: 1685429350571
+ unsettled_pnl: 354.858492
+ leverage: 10
+ - account_id: "0x22222"
+ IMR_withdraw_orders: 0.12
+ MMR_with_orders: 0.06
+ average_open_price: 28000.123456
+ cost_position: -100000.56789
+ est_liq_price: 120000.654321
+ fee_24_h: 5.5
+ imr: 0.12
+ last_sum_unitary_funding: 50.12
+ mark_price: 27794.9
+ mmr: 0.06
+ pending_long_qty: 1
+ pending_short_qty: 0
+ pnl_24_h: 150.75
+ position_qty: -3
+ settle_price: 27950.6789
+ symbol: "PERP_BTC_USDC"
+ seq: 1730181536341943601
+ timestamp: 1685429350572
+ updated_time: 1685429350571
+ unsettled_pnl: 354.858492
+ leverage: 10
+
+ AggregateHoldingResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ account_id:
+ type: string
+ updated_time:
+ type: number
+ token:
+ type: string
+ holding:
+ type: number
+ frozen:
+ type: number
+ pending_short:
+ type: number
+ example:
+ - account_id: "0x1111"
+ updated_time: 1580794149000
+ token: "BTC"
+ holding: -28.000752
+ frozen: 0
+ pending_short: -2000
+ - account_id: "0x2222"
+ updated_time: 1580794149000
+ token: "ETH"
+ holding: -10.123456
+ frozen: 0
+ pending_short: -500
+
+ EmptyDataResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties: {}
+
+ LeverageResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ leverage:
+ type: integer
+ example: 10
+
+ PaginationMeta:
+ required:
+ - current_page
+ - records_per_page
+ - total
+ type: object
+ properties:
+ total:
+ type: integer
+ example: 9
+ records_per_page:
+ type: integer
+ example: 25
+ current_page:
+ type: integer
+ example: 1
+ description: "start from 1"
+
+ FuturesInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/FutureInfo"
+
+ GetIpInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ city:
+ type: string
+ example: "Toronto"
+ ip:
+ type: string
+ example: "192.192.192.0"
+ checked:
+ type: boolean
+ example: false
+ region:
+ type: string
+ example: "Canada"
+
+ GetMarketVolumeByBrokerResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/FutureInfo"
+
+ GetTVLByBrokerResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ total_holding:
+ type: number
+ example: 100
+ total_unsettled_balance:
+ type: number
+ example: 21
+ last_update_time:
+ type: number
+ example: 1702989203989
+
+ FutureInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/FutureInfo"
+
+ FutureInfo:
+ type: object
+ required:
+ - 24h_amount
+ - 24h_close
+ - 24h_high
+ - 24h_low
+ - 24h_open
+ - 24h_volume
+ - est_funding_rate
+ - index_price
+ - last_funding_rate
+ - mark_price
+ - next_funding_time
+ - open_interest
+ - sum_unitary_funding
+ - symbol
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ index_price:
+ type: number
+ example: 1901.6
+ mark_price:
+ type: integer
+ example: 1950
+ sum_unitary_funding:
+ type: number
+ example: 387.891
+ est_funding_rate:
+ type: number
+ example: 4.6875E-4
+ last_funding_rate:
+ type: number
+ example: 4.6875E-4
+ next_funding_time:
+ type: integer
+ example: 1683270060000
+ open_interest:
+ type: string
+ example: "None"
+ 24h_open:
+ type: integer
+ example: 2115
+ 24h_close:
+ type: integer
+ example: 2115
+ 24h_high:
+ type: integer
+ example: 2115
+ 24h_low:
+ type: integer
+ example: 2115
+ 24h_amount:
+ type: integer
+ example: 0
+ 24h_volume:
+ type: integer
+ example: 0
+
+ FuturesFeeProgramResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - maker_fee
+ - taker_fee
+ - tier
+ - volume_max
+ - volume_min
+ properties:
+ tier:
+ type: string
+ example: "1"
+ maker_fee:
+ type: string
+ example: "0.1%"
+ taker_fee:
+ type: string
+ example: "0.1%"
+ volume_min:
+ type: integer
+ description: minimum 30-day volume (in USDC) required for eligibility of this tier
+ example: 100
+ volume_max:
+ type: integer
+ example: 500000
+
+ FeeProgramResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - maker_fee
+ - taker_fee
+ - tier
+ - volume_min
+ properties:
+ tier:
+ type: string
+ example: "1"
+ maker_fee:
+ type: string
+ example: "0.1%"
+ taker_fee:
+ type: string
+ example: "0.1%"
+ volume_min:
+ type: integer
+ description: minimum 30-day volume (in USDC) required for eligibility of this tier
+ example: 100
+ volume_max:
+ type: integer
+ example: 500000
+
+ MarketTradesResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - symbol
+ - side
+ - executed_price
+ - executed_quantity
+ - executed_timestamp
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ side:
+ type: string
+ example: "BUY"
+ executed_price:
+ type: number
+ example: 2050
+ executed_quantity:
+ type: number
+ example: 1
+ executed_timestamp:
+ type: number
+ example: 1683878609166
+
+ MarketPriceChangeInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ last_price:
+ type: number
+ example: 31000
+ 5m:
+ type: number
+ example: 31000
+ 30m:
+ type: number
+ example: 31000
+ 1h:
+ type: number
+ example: 31000
+ 4h:
+ type: number
+ example: 31000
+ 24h:
+ type: number
+ example: 31000
+ 3d:
+ type: number
+ example: 31000
+ 7d:
+ type: number
+ example: 31000
+ 30d:
+ type: number
+ example: null
+
+ MarketOpenInterestsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ long_oi:
+ type: number
+ example: 31000
+ short_oi:
+ type: number
+ example: 31000
+
+ GetPriceForSmallChartsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ price_list:
+ type: array
+ items:
+ type: object
+ properties:
+ ts:
+ type: number
+ example: 1702989203989
+ price:
+ type: number
+ example: 31000
+
+ GetMarketFundingRateHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ data_start_time:
+ type: string
+ example: "2022-01-01 00:00:00"
+ funding:
+ type: object
+ properties:
+ last:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.00234
+ positive:
+ type: number
+ example: 1
+ negative:
+ type: number
+ example: 0
+ 1d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: -0.00828
+ positive:
+ type: number
+ example: 10
+ negative:
+ type: number
+ example: 10
+ 3d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: -0.00412
+ positive:
+ type: number
+ example: 9
+ negative:
+ type: number
+ example: 11
+ 7d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.00012
+ positive:
+ type: number
+ example: 8
+ negative:
+ type: number
+ example: 12
+ 14d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.00156
+ positive:
+ type: number
+ example: 7
+ negative:
+ type: number
+ example: 13
+ 30d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.00345
+ positive:
+ type: number
+ example: 6
+ negative:
+ type: number
+ example: 14
+ 90d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.01023
+ positive:
+ type: number
+ example: 5
+ negative:
+ type: number
+ example: 15
+ 180d:
+ type: object
+ properties:
+ rate:
+ type: number
+ example: 0.01567
+ positive:
+ type: number
+ example: 4
+ negative:
+ type: number
+ example: 16
+
+ VolumeStatsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ perp_volume_ytd:
+ type: number
+ example: 274148.0364
+ perp_volume_ltd:
+ type: number
+ example: 274148.0364
+ perp_volume_today:
+ type: number
+ example: 120272.7674
+ perp_volume_last_1_day:
+ type: number
+ example: 120272.7674
+ perp_volume_last_7_days:
+ type: number
+ example: 120272.7674
+ perp_volume_last_30_days:
+ type: number
+ example: 300730.5454
+
+ BrokerListResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - broker_id
+ - broker_name
+ properties:
+ broker_id:
+ type: string
+ example: "woofi_pro"
+ broker_name:
+ type: string
+ example: "WOOFi Pro"
+
+ BrokerStatsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - connected_user
+ properties:
+ connected_user:
+ type: integer
+ example: "274148"
+
+ FundingRatesResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/FundingRate"
+
+ FundingRateResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/FundingRate"
+
+ FundingRate:
+ required:
+ - est_funding_rate
+ - est_funding_rate_timestamp
+ - last_funding_rate
+ - last_funding_rate_timestamp
+ - next_funding_time
+ - sum_unitary_funding
+ - symbol
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ est_funding_rate:
+ type: integer
+ example: 0
+ est_funding_rate_timestamp:
+ type: integer
+ example: 1683880020000
+ last_funding_rate:
+ type: number
+ example: 1.0E-4
+ last_funding_rate_timestamp:
+ type: integer
+ example: 1683878400000
+ next_funding_time:
+ type: integer
+ example: 1683907200000
+ sum_unitary_funding:
+ type: number
+ example: 521.367
+
+ FundingRateHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - funding_rate
+ - funding_rate_timestamp
+ - next_funding_time
+ - symbol
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ funding_rate:
+ type: number
+ example: 1.0E-4
+ funding_rate_timestamp:
+ type: integer
+ example: 1684224000000
+ next_funding_time:
+ type: integer
+ example: 1684252800000
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+
+ LiquidationResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ timestamp:
+ type: integer
+ example: 1685298157704
+ type:
+ type: string
+ example: "liquidated"
+ liquidation_id:
+ type: integer
+ example: 1730
+ positions_by_perp:
+ type: array
+ items:
+ # TODO different from PerpPosition?
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ position_qty:
+ type: number
+ example: -0.22457
+ liquidator_fee:
+ type: number
+ example: 0.015
+
+ LiquidatedPositionsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ required:
+ - timestamp
+ - liquidation_id
+ - transfer_amount_to_insurance_fund
+ - positions_by_perp
+ type: object
+ properties:
+ timestamp:
+ type: integer
+ example: 1683802462092
+ liquidation_id:
+ type: integer
+ example: 29
+ transfer_amount_to_insurance_fund:
+ type: integer
+ example: 0
+ type:
+ type: string
+ example: "liquidated"
+ positions_by_perp:
+ type: array
+ items:
+ # TODO different from PerpPosition?
+ type: object
+ required:
+ - abs_insurance_fund_fee
+ - abs_liquidation_fee
+ - cost_position_transfer
+ - insurance_fund_fee
+ - liquidator_fee
+ - position_qty
+ - symbol
+ - transfer_price
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ seq:
+ type: number
+ example: 1730181536341943600
+ position_qty:
+ type: integer
+ example: 0
+ liquidator_fee:
+ type: number
+ example: 0.015
+ cost_position_transfer:
+ type: integer
+ example: 0
+ transfer_price:
+ type: integer
+ example: 1860
+ insurance_fund_fee:
+ type: number
+ example: 0.011999
+ abs_insurance_fund_fee:
+ type: integer
+ example: 0
+ abs_liquidator_fee:
+ type: integer
+ example: 0
+
+ ExchangeInformationResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/Symbol"
+
+ TokenInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ # TODO example
+ token:
+ type: string
+ description: name of token
+ example: ETH
+ base_weight:
+ type: number
+ description: 1 for usdc
+ example: 0.8
+ discount_factor:
+ type: number
+ description: null for usdc
+ example: 0.02
+ haircut:
+ type: number
+ description: 0 for usdc
+ example: 0.95
+ user_max_qty:
+ type: number
+ description: -1 = infinity > usdc
+ example: 111
+ is_collateral:
+ type: boolean
+ description: True, the asset can be collateral, users are allowed to manually swap, and we will do auto-swap
+ example: true
+ decimals:
+ type: number
+ description: precision of the token
+ example: 6
+ minimum_withdraw_amount:
+ type: number
+ description: ""
+ example: 0.000001
+ on_chain_swap:
+ type: boolean
+ description: True, the asset is eligible for on-chain swaps on at least one supported blockchain
+ example: true
+ token_hash:
+ type: string
+ example: "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa"
+ chain_details:
+ type: array
+ items:
+ type: object
+ properties:
+ chain_id:
+ type: integer
+ example: 43113
+ contract_address:
+ type: string
+ example: "0x5d64c9cfb0197775b4b3ad9be4d3c7976e0d8dc3"
+ cross_chain_withdrawal_fee:
+ type: integer
+ example: 0
+ decimals:
+ type: integer
+ example: 18
+ withdraw_fee:
+ type: number
+ example: 2
+ display_name:
+ type: string
+ description: "`USDC.e` for `Mantle` chain"
+ example: "ETH"
+
+ GetIndexPriceSourceResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ description: "SPOT symbol name"
+ example: "SPOT_BNB_USDC"
+ updated_time:
+ type: number
+ example: 1710382176255
+ sources:
+ type: array
+ items:
+ type: string
+ example:
+ - "gateio"
+ - "kucoin"
+ - "binance"
+ - "okx"
+ - "huobi"
+ - "bybit"
+ - "mexc"
+
+ AvailableSymbolsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Symbol"
+
+ VenueTransferHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ meta:
+ type: object
+ properties:
+ total:
+ type: number
+ example: 2
+ records_per_page:
+ type: number
+ example: 25
+ current_page:
+ type: number
+ example: 1
+ description: "start from 1"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ created_time:
+ type: number
+ example: 1739257560000
+ executed_time:
+ type: number
+ example: 1739257561000
+ period_number:
+ type: number
+ example: 2
+ plan_id:
+ type: number
+ example: 3
+ amount:
+ type: number
+ example: 13633.532389
+ status:
+ type: string
+ example: "completed"
+ VenueWithdrawalHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ meta:
+ type: object
+ properties:
+ total:
+ type: number
+ example: 2
+ records_per_page:
+ type: number
+ example: 25
+ current_page:
+ type: number
+ example: 1
+ description: "start from 1"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ created_time:
+ type: number
+ example: 1739257260000
+ period_number:
+ type: number
+ example: 2
+ plan_id:
+ type: number
+ example: 4
+ schedule_time:
+ type: number
+ example: 1739268000000
+ executed_time:
+ type: number
+ example: null
+ trigger_by:
+ type: number
+ example: null
+ amount:
+ type: number
+ example: 13633.532389
+ status:
+ type: string
+ example: "pending"
+ ShareHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ meta:
+ type: object
+ properties:
+ total:
+ type: number
+ example: 2
+ records_per_page:
+ type: number
+ example: 25
+ current_page:
+ type: number
+ example: 1
+ description: "start from 1"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ executed_time:
+ type: number
+ example: 1734652800000
+ amount:
+ type: number
+ example: 13633.532389
+ InternalTransferHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ properties:
+ data:
+ type: object
+ properties:
+ meta:
+ type: object
+ properties:
+ total:
+ type: number
+ example: 2
+ records_per_page:
+ type: number
+ example: 25
+ current_page:
+ type: number
+ example: 1
+ description: "start from 1"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ executed_time:
+ type: number
+ example: 1734652800000
+ from_account:
+ type: string
+ example: ""
+ amount:
+ type: number
+ example: 13633.532389
+ Symbol:
+ type: object
+ required:
+ - base_imr
+ - base_max
+ - base_min
+ - base_mmr
+ - base_tick
+ - cap_funding
+ - claim_insurance_fund_discount
+ - created_time
+ - floor_funding
+ - funding_period
+ - interest_rate
+ - liquidator_fee
+ - min_notional
+ - price_range
+ - price_scope
+ - quote_max
+ - quote_min
+ - quote_tick
+ - std_liquidation_fee
+ - symbol
+ - updated_time
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ quote_min:
+ type: integer
+ example: 0
+ quote_max:
+ type: integer
+ example: 100000
+ quote_tick:
+ type: number
+ example: 0.1
+ base_min:
+ type: number
+ example: 1.0E-5
+ base_max:
+ type: integer
+ example: 20
+ base_tick:
+ type: number
+ example: 1.0E-5
+ min_notional:
+ type: integer
+ example: 1
+ price_range:
+ type: number
+ example: 0.02
+ price_scope:
+ type: number
+ example: 0.4
+ std_liquidation_fee:
+ type: number
+ example: 0.03
+ liquidator_fee:
+ type: number
+ example: 0.015
+ claim_insurance_fund_discount:
+ type: number
+ example: 0.0075
+ funding_period:
+ type: integer
+ example: 8
+ cap_funding:
+ type: number
+ example: 3.75E-4
+ floor_funding:
+ type: number
+ example: -3.75E-4
+ cap_ir:
+ type: number
+ example: 0.0003
+ floor_ir:
+ type: number
+ example: -0.0003
+ interest_rate:
+ type: number
+ example: 1.0E-4
+ imr_factor:
+ type: number
+ example: 0.025
+ created_time:
+ type: integer
+ example: 1684140107326
+ updated_time:
+ type: integer
+ example: 1685345968053
+ base_mmr:
+ type: number
+ example: 0.05
+ base_imr:
+ type: number
+ example: 0.1
+ liquidation_tier:
+ type: number
+ example: 1
+ global_max_oi_cap:
+ type: number
+ example: 11111
+
+ InsuranceFundResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - balance
+ - free_collateral
+ - margin_ratio
+ - min_insurance_fund_initial_margin_ratio
+ - min_insurance_fund_margin_ratio
+ - rows
+ - total_account_value
+ - total_collateral_value
+ - total_pnl_24_h
+ properties:
+ min_insurance_fund_initial_margin_ratio:
+ type: number
+ example: 0.3
+ min_insurance_fund_margin_ratio:
+ type: number
+ example: 0.25
+ margin_ratio:
+ type: integer
+ example: 10
+ total_collateral_value:
+ type: number
+ example: 1.0008662996861E7
+ balance:
+ type: number
+ example: 1.0001908678983E7
+ free_collateral:
+ type: number
+ example: 1.0008662996861E7
+ total_account_value:
+ type: number
+ example: 1.0008662996861E7
+ total_pnl_24_h:
+ type: integer
+ example: 0
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - symbol
+ - timestamp
+ - position_qty
+ - cost_position
+ - last_sum_unitary_funding
+ - settle_price
+ - average_open_price
+ - pnl_24_h
+ - fee_24_h
+ - mark_price
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ timestamp:
+ type: integer
+ example: 1685262631606
+ position_qty:
+ type: integer
+ example: 0
+ cost_position:
+ type: number
+ example: -617.672835
+ last_sum_unitary_funding:
+ type: number
+ example: 100.56
+ settle_price:
+ type: integer
+ example: 0
+ average_open_price:
+ type: integer
+ example: 0
+ pnl_24_h:
+ type: integer
+ example: 0
+ fee_24_h:
+ type: integer
+ example: 0
+ mark_price:
+ type: number
+ example: 27821.8
+
+ TvConfigResponse:
+ type: object
+ required:
+ - d
+ - s
+ properties:
+ s:
+ type: string
+ d:
+ type: object
+ required:
+ - accountManager
+ - accountSummaryRow
+ - durations
+ - orderCustomFields
+ - orderHistoryCustomFields
+ - positionCustomFields
+ - pullingInterval
+ properties:
+ accountSummaryRow:
+ type: array
+ items:
+ $ref: "#/components/schemas/AccountSummary"
+ accountManager:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ columns:
+ type: array
+ items:
+ $ref: "#/components/schemas/AccountSummary"
+ durations:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ hasDatePicker:
+ type: boolean
+ hasTimePicker:
+ type: boolean
+ default:
+ type: boolean
+ supportedOrderTypes:
+ type: array
+ items:
+ type: string
+ orderCustomFields:
+ type: array
+ items:
+ $ref: "#/components/schemas/OrderCustomField"
+ orderHistoryCustomFields:
+ type: array
+ items:
+ $ref: "#/components/schemas/OrderCustomField"
+ positionCustomFields:
+ type: array
+ items:
+ type: object
+ required:
+ - alignment
+ - id
+ - title
+ - tooltip
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ tooltip:
+ type: string
+ alignment:
+ type: string
+ pullingInterval:
+ type: object
+ required:
+ - accountManager
+ - balances
+ - orders
+ - positions
+ - quotes
+ properties:
+ quotes:
+ type: integer
+ orders:
+ type: integer
+ positions:
+ type: integer
+ accountManager:
+ type: integer
+ balances:
+ type: integer
+
+ AccountSummary:
+ type: object
+ required:
+ - id
+ - title
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+
+ OrderCustomField:
+ type: object
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ tooltip:
+ type: string
+ alignment:
+ type: string
+
+ TvHistoryResponse:
+ type: object
+ required:
+ - c
+ - h
+ - l
+ - o
+ - s
+ - t
+ - v
+ properties:
+ s:
+ type: string
+ t:
+ type: array
+ items:
+ type: integer
+ o:
+ type: array
+ items:
+ oneOf:
+ - type: integer
+ - type: number
+ h:
+ type: array
+ items:
+ type: number
+ l:
+ type: array
+ items:
+ type: number
+ c:
+ type: array
+ items:
+ type: number
+ v:
+ type: array
+ items:
+ type: number
+
+ TvSymbolResponse:
+ type: object
+ required:
+ - bar-fillgaps
+ - bar-source
+ - bar-transform
+ - base-currency
+ - currency
+ - description
+ - exchange-listed
+ - exchange-traded
+ - expiration
+ - fractional
+ - has-daily
+ - has-intraday
+ - has-no-volume
+ - has-weekly-and-monthly
+ - intraday-multipliers
+ - is-cfd
+ - isin
+ - minmovement
+ - minmovement2
+ - pointvalue
+ - pricescale
+ - root
+ - root-description
+ - s
+ - session-extended
+ - session-postmarket
+ - session-premarket
+ - session-regular
+ - supported-resolutions
+ - symbol
+ - ticker
+ - timezone
+ - type
+ - wkn
+ properties:
+ s:
+ type: string
+ symbol:
+ type: array
+ items:
+ type: string
+ description:
+ type: array
+ items:
+ type: string
+ currency:
+ type: array
+ items:
+ type: string
+ base-currency:
+ type: array
+ items:
+ oneOf:
+ - type: string
+ exchange-listed:
+ type: array
+ items:
+ type: string
+ exchange-traded:
+ type: array
+ items:
+ type: string
+ minmovement:
+ type: array
+ items:
+ type: integer
+ minmovement2:
+ type: array
+ items:
+ type: integer
+ fractional:
+ type: array
+ items:
+ type: boolean
+ pricescale:
+ type: array
+ items:
+ type: integer
+ root:
+ type: array
+ items:
+ oneOf:
+ - type: string
+ root-description:
+ type: array
+ items:
+ oneOf:
+ - type: string
+ has-intraday:
+ type: array
+ items:
+ type: boolean
+ has-no-volume:
+ type: array
+ items:
+ type: boolean
+ type:
+ type: array
+ items:
+ type: string
+ is-cfd:
+ type: array
+ items:
+ type: boolean
+ ticker:
+ type: array
+ items:
+ type: string
+ timezone:
+ type: array
+ items:
+ type: string
+ session-regular:
+ type: array
+ items:
+ type: string
+ session-extended:
+ type: array
+ items:
+ type: string
+ session-premarket:
+ type: array
+ items:
+ oneOf:
+ - type: string
+ session-postmarket:
+ type: array
+ items:
+ oneOf:
+ - type: string
+ supported-resolutions:
+ type: array
+ items:
+ type: array
+ items:
+ type: string
+ has-daily:
+ type: array
+ items:
+ type: boolean
+ intraday-multipliers:
+ type: array
+ items:
+ type: array
+ items:
+ type: string
+ has-weekly-and-monthly:
+ type: array
+ items:
+ type: boolean
+ pointvalue:
+ type: array
+ items:
+ oneOf:
+ - type: integer
+ - type: number
+ expiration:
+ type: array
+ items:
+ oneOf:
+ - type: integer
+ bar-source:
+ type: array
+ items:
+ type: string
+ bar-transform:
+ type: array
+ items:
+ type: string
+ bar-fillgaps:
+ type: array
+ items:
+ type: string
+ isin:
+ type: array
+ items:
+ type: string
+ wkn:
+ type: array
+ items:
+ type: string
+
+ PositionsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - current_margin_ratio_with_orders
+ - free_collateral
+ - initial_margin_ratio
+ - initial_margin_ratio_with_orders
+ - maintenance_margin_ratio
+ - maintenance_margin_ratio_with_orders
+ - margin_ratio
+ - open_margin_ratio
+ - total_collateral_value
+ - total_pnl_24_h
+ - rows
+ properties:
+ current_margin_ratio_with_orders:
+ type: number
+ example: 1.2385
+ free_collateral:
+ type: number
+ example: 450315.09115
+ initial_margin_ratio:
+ type: number
+ example: 0.1
+ initial_margin_ratio_with_orders:
+ type: number
+ example: 0.1
+ maintenance_margin_ratio:
+ type: number
+ example: 0.05
+ maintenance_margin_ratio_with_orders:
+ type: number
+ example: 0.05
+ margin_ratio:
+ type: number
+ example: 1.2385
+ open_margin_ratio:
+ type: number
+ example: 1.2102
+ total_collateral_value:
+ type: number
+ example: 489865.71329
+ total_pnl_24_h:
+ type: number
+ example: 0
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Position"
+
+ PositionHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ position_id:
+ type: number
+ example: 1
+ status:
+ type: string
+ example: "closed"
+ type:
+ type: string
+ example: "liquidated"
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ avg_open_price:
+ type: number
+ example: 61016.1
+ avg_close_price:
+ type: number
+ example: 61016.1
+ max_position_qty:
+ type: number
+ example: 56.6
+ closed_position_qty:
+ type: number
+ example: 56.6
+ side:
+ type: string
+ example: "LONG"
+ trading_fee:
+ type: number
+ example: 0.015
+ accumulated_funding_fee:
+ type: number
+ example: 0.11
+ insurance_fund_fee:
+ type: number
+ example: 0
+ liquidator_fee:
+ type: number
+ example: 0
+ liquidation_id:
+ type: number
+ example: null
+ realized_pnl:
+ type: number
+ example: -9.0969192731490491
+ open_timestamp:
+ type: number
+ example: 1685429350571
+ close_timestamp:
+ type: number
+ example: 1685429350571
+ last_update_timestamp:
+ type: number
+ example: 1685429350571
+ leverage:
+ type: number
+ example: 10
+
+ PositionResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/Position"
+
+ Position:
+ type: object
+ properties:
+ IMR_withdraw_orders:
+ type: number
+ example: 0.1
+ MMR_with_orders:
+ type: number
+ example: 0.05
+ average_open_price:
+ type: number
+ example: 27908.14386047
+ cost_position:
+ type: number
+ example: -139329.358492
+ est_liq_price:
+ type: number
+ example: 117335.92899428
+ fee_24_h:
+ type: number
+ example: 0
+ imr:
+ type: number
+ example: 0.1
+ last_sum_unitary_funding:
+ type: number
+ example: 70.38
+ mark_price:
+ type: number
+ example: 27794.9
+ mmr:
+ type: number
+ example: 0.05
+ pending_long_qty:
+ type: number
+ example: 0
+ pending_short_qty:
+ type: number
+ example: 0
+ pnl_24_h:
+ type: number
+ example: 0
+ position_qty:
+ type: number
+ example: -5
+ settle_price:
+ type: number
+ example: 27865.8716984
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ seq:
+ type: number
+ example: 1730181536341943600
+ timestamp:
+ type: number
+ description: "The timestamp when the position is created for the entry"
+ example: 1685429350571
+ updated_time:
+ type: number
+ example: 1685429350571
+ unsettled_pnl:
+ type: number
+ example: 354.858492
+ leverage:
+ type: number
+ example: 10
+
+ FundingFeeHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ funding_rate:
+ type: number
+ example: 4.6875E-4
+ mark_price:
+ type: number
+ example: 2100
+ funding_fee:
+ type: number
+ example: 1.6E-5
+ payment_type:
+ type: string
+ example: "Pay"
+ description: "`Receive`/`Pay`"
+ status:
+ type: string
+ example: "Accrued"
+ description: "`Accrued`/`Settled`"
+ created_time:
+ type: number
+ example: 1682235722003
+ updated_time:
+ type: number
+ example: 1682235722003
+
+ GetInternalTransferHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ example: "230707030600002"
+ token:
+ type: string
+ example: "USDC"
+ amount:
+ type: number
+ example: 555
+ from_account_id:
+ type: string
+ example: "0x000000"
+ to_account_id:
+ type: string
+ example: "0x000000"
+ status:
+ type: string
+ example: "FAILED"
+ created_time:
+ type: number
+ example: 1688699193034
+ updated_time:
+ type: number
+ example: 1688699193034
+
+ LiquidatorLiquidationsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ liquidation_id:
+ type: integer
+ example: 1728
+ timestamp:
+ type: number
+ example: 1685154032762
+ type:
+ type: string
+ example: "liquidated"
+ assigned:
+ type: number
+ example: 1
+ description: 1 assigned to SPV or MM; 0 means claimed
+ positions_by_perp:
+ type: array
+ items:
+ $ref: "#/components/schemas/PerpPosition"
+
+ PerpPosition:
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ abs_liquidator_fee:
+ type: number
+ example: 1.152279
+ cost_position_transfer:
+ type: number
+ example: 21.002786
+ liquidator_fee:
+ type: number
+ example: 0.006
+ position_qty:
+ type: number
+ example: 0.00017
+ transfer_price:
+ type: number
+ example: 123545.8
+
+ ClientLeverageBody:
+ type: object
+ required:
+ - leverage
+ properties:
+ leverage:
+ type: integer
+ # TODO min/max?
+
+ LiquidationsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ liquidation_id:
+ type: number
+ example: 101
+ timestamp:
+ type: number
+ example: 1663313562090
+ transfer_amount_to_insurance_fund:
+ type: number
+ example: 0
+ margin_ratio:
+ type: number
+ example: 0.10436069
+ account_mmr:
+ type: number
+ example: 0.012
+ position_notional:
+ type: number
+ example: 28.415534
+ collateral_value:
+ type: number
+ example: 2.965465
+ positions_by_perp:
+ type: array
+ items:
+ type: object
+ required:
+ - total_position
+
+ - abs_insurance_fund_fee
+ - abs_liquidation_fee
+ - cost_position_transfer
+ - insurance_fund_fee
+ - liquidator_fee
+ - position_qty
+ - symbol
+ - transfer_price
+ properties:
+ total_position:
+ type: number
+ example: 41.6
+ mark_price:
+ type: number
+ example: 123400
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ position_qty:
+ type: number
+ example: 0.00017
+ liquidator_fee:
+ type: number
+ example: 0.006
+ cost_position_transfer:
+ type: number
+ example: 21.002786
+ transfer_price:
+ type: number
+ example: 123545.8
+ insurance_fund_fee:
+ type: number
+ example: 0.006
+ abs_insurance_fund_fee:
+ type: number
+ example: 0.126017
+ abs_liquidator_fee:
+ type: number
+ example: 0.252034
+
+ LiquidationBody:
+ required:
+ - limit_price
+ - liquidation_id
+ - ratio_qty_request
+ type: object
+ properties:
+ liquidation_id:
+ type: integer
+ ratio_qty_request:
+ type: integer
+ extra_liquidation_ratio:
+ type: integer
+ limit_price:
+ type: object
+ required:
+ - symbol
+ description: "Liquidator’s instruction to let the system reject the liquidation claim if the following condition applies: if position_qty > 0, reject if mark_price > limit_price; if position_qty < 0, reject if mark_price < limit_price"
+ properties:
+ symbol:
+ type: number
+ description: The limit price for each symbol in the liquidation claim
+ symbols:
+ type: object
+ required:
+ - ratio_qty_request
+ - symbol
+ description: "For high risk tiers only"
+ properties:
+ ratio_qty_request:
+ type: number
+ description: "Field dedicated to high risk symbols liquidations type only (symbols with liquidation tier = 2), where a liquidator can specific the ratio he wish to liquidates per symbol"
+ symbol:
+ type: string
+ description: "Only relevant if this is a liquidation of type high tier (symbols with liquidation tier = 2)"
+
+ # TODO different from LiquidationResponse?
+ PostLiquidationResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - liquidation_id
+ - positions_by_perp
+ - timestamp
+ - type
+ properties:
+ liquidation_id:
+ type: integer
+ timestamp:
+ type: number
+ type:
+ type: string
+ description: OR "claim"
+ positions_by_perp:
+ type: array
+ items:
+ $ref: "#/components/schemas/PerpPosition"
+
+ OrderBody:
+ required:
+ - order_id
+ - order_type
+ - side
+ - symbol
+ type: object
+ properties:
+ order_id:
+ type: string
+ symbol:
+ type: string
+ client_order_id:
+ type: string
+ order_type:
+ type: string
+ order_price:
+ type: number
+ order_quantity:
+ type: number
+ order_amount:
+ type: number
+ reduce_only:
+ type: boolean
+ visible_quantity:
+ type: number
+ side:
+ type: string
+ order_tag:
+ type: string
+
+ BatchCreateOrderBody:
+ required:
+ - order_type
+ - side
+ - symbol
+ type: object
+ properties:
+ symbol:
+ type: string
+ client_order_id:
+ type: string
+ order_type:
+ type: string
+ order_price:
+ type: number
+ order_quantity:
+ type: number
+ order_amount:
+ type: number
+ visible_quantity:
+ type: number
+ side:
+ type: string
+ order_tag:
+ type: string
+
+ CancelAllAfterBody:
+ required:
+ - trigger_after
+ type: object
+ properties:
+ trigger_after:
+ type: number
+ example: 5000
+ description: |
+ Timeout in milliseconds.
+ Min timeout is 5000.
+ Max timeout can be set to 900000.
+ To cancel this timer, set timeout to 0.
+
+ CancelAllAfterResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ expected_trigger_time:
+ type: number
+ example: "1711534302938"
+
+ PutOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ status:
+ type: string
+ example: "EDIT_SENT"
+
+ CreateInternalTransferRequest:
+ type: object
+ properties:
+ token:
+ type: string
+ example: "USDC"
+ receiver_list:
+ type: array
+ description: "Maximum 50 receivers per request, transferring to yourself will lead to request failure. "
+ items:
+ type: object
+ properties:
+ account_id:
+ type: string
+ example: "0x0000000"
+ amount:
+ type: number
+ example: 100
+
+ PostOrderBody:
+ required:
+ - order_type
+ - side
+ - symbol
+ type: object
+ properties:
+ symbol:
+ type: string
+ example: "PERP_ETH_USDC"
+ client_order_id:
+ type: string
+ description: "36 length, accepts hyphen but cannot be the first character, default: null"
+ order_type:
+ type: string
+ description: "`LIMIT`/`MARKET`/`IOC`/`FOK`/`POST_ONLY`/`ASK`/`BID`"
+ order_price:
+ type: number
+ description: "If order_type is MARKET/ASK/BID, then is not required, otherwise this parameter is required."
+ order_quantity:
+ type: number
+ description: "For MARKET/ASK/BID order, if order_amount is given, it is not required."
+ order_amount:
+ type: number
+ description: "For MARKET/ASK/BID order, the order size in terms of quote currency"
+ visible_quantity:
+ type: number
+ description: |
+ The order quantity shown on orderbook. (default: equal to order_quantity)
+ Visible quantity is not supported for post-only orders.
+ side:
+ type: string
+ description: "`SELL`/`BUY`"
+ reduce_only:
+ type: boolean
+ description: Default false
+ slippage:
+ type: number
+ description: "`MARKET` orders beyond this slippage will not be executed"
+ order_tag:
+ type: string
+ level:
+ type: number
+ description: "Integer value from `0` to `4`. This parameter controls wether to present the price of bid0 to bid4 or ask0 to ask4. Only allowed when `order_type` is `BID` or `ASK`."
+ post_only_adjust:
+ type: boolean
+ description: "If set to true, then price will be adjusted to 1 tick close to current best price. Only supported for `POST_ONLY` type orders"
+
+ PostOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/CreatedOrder"
+
+ PostAlgoOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/CreatedAlgoOrder"
+
+ CreatedOrder:
+ type: object
+ properties:
+ order_id:
+ type: number
+ example: 13
+ client_order_id:
+ type: string
+ example: "testclientid"
+ order_type:
+ type: string
+ example: "LIMIT"
+ order_price:
+ type: number
+ example: 100.12
+ order_quantity:
+ type: number
+ example: 0.987654
+ order_amount:
+ type: number
+ example: 0.8
+ error_message:
+ type: string
+ example: "none"
+
+ CreatedAlgoOrder:
+ type: object
+ required:
+ - order_id
+ - client_order_id
+ - algo_type
+ - quantity
+ properties:
+ order_id:
+ type: number
+ example: 13
+ client_order_id:
+ type: string
+ example: "testclientid"
+ algo_type:
+ type: string
+ example: "STOP"
+ quantity:
+ type: number
+ example: 100.12
+
+ DeleteOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - status
+ properties:
+ status:
+ $ref: "#/components/schemas/OrderStatus"
+
+ DeleteAllPendingAlgoOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - status
+ properties:
+ status:
+ type: string
+ example: "CANCEL_ALL_SENT"
+
+ OrderStatus:
+ # TODO enum
+ type: string
+ example: "CANCEL_SENT"
+
+ OrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/Order"
+
+ Order:
+ type: object
+ required:
+ - amount
+ - average_executed_price
+ - client_order_id
+ - created_time
+ - executed_quantity
+ - total_executed_quantity
+ - fee_asset
+ - order_id
+ - price
+ - quantity
+ - side
+ - status
+ - symbol
+ - total_fee
+ - type
+ - updated_time
+ - user_id
+ - visible_quantity
+ - realized_pnl
+ properties:
+ order_id:
+ type: number
+ example: 78151
+ user_id:
+ type: number
+ example: 12345
+ price:
+ type: number
+ example: 0.67772
+ type:
+ type: string
+ example: "LIMIT"
+ quantity:
+ type: number
+ example: 20
+ amount:
+ type: number
+ example: 10
+ # TODO nullable & example
+ nullable: true
+ executed_quantity:
+ type: number
+ example: 20
+ total_executed_quantity:
+ type: number
+ example: 20
+ visible_quantity:
+ type: number
+ example: 1
+ symbol:
+ type: string
+ example: "PERP_WOO_USDC"
+ side:
+ type: string
+ example: "BUY"
+ status:
+ type: string
+ example: "FILLED"
+ description: NEW / FILLED / PARTIAL_FILLED / CANCELLED
+ total_fee:
+ type: number
+ example: 0.5
+ fee_asset:
+ type: string
+ example: "WOO"
+ client_order_id:
+ type: number
+ example: 1
+ # TODO nullable & example
+ nullable: true
+ average_executed_price:
+ type: number
+ example: 0.67772
+ created_time:
+ type: number
+ example: 1653563963000
+ updated_time:
+ type: number
+ example: 1653564213000
+ realized_pnl:
+ type: number
+ example: 0.0
+
+ AlgoOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/AlgoOrder"
+ AlgoOrder:
+ type: object
+ required:
+ - algo_order_id
+ - algo_status
+ - algo_type
+ - created_time
+ - fee_asset
+ - is_triggered
+ - order_tag
+ - parent_algo_order_id
+ - quantity
+ - root_algo_order_id
+ - root_algo_order_status
+ - side
+ - symbol
+ - total_executed_quantity
+ - total_fee
+ - trigger_price
+ - trigger_price_type
+ - type
+ - updated_time
+ - visible_quantity
+ properties:
+ algo_order_id:
+ type: number
+ example: 168400010
+ algo_status:
+ type: string
+ example: "NEW"
+ algo_type:
+ type: string
+ example: "STOP"
+ fee_asset:
+ type: string
+ example: "USDC"
+ is_triggered:
+ type: string
+ example: "false"
+ order_tag:
+ type: string
+ example: "test_tag"
+ parent_algo_order_id:
+ type: number
+ example: 0
+ quantity:
+ type: number
+ example: 5.5
+ root_algo_order_id:
+ type: number
+ example: 168400010
+ root_algo_order_status:
+ type: string
+ example: "NEW"
+ side:
+ type: string
+ example: "BUY"
+ symbol:
+ type: string
+ example: "PERP_NEAR_USDC"
+ executed_quantity:
+ type: number
+ example: 0
+ total_executed_quantity:
+ type: number
+ example: 0
+ total_fee:
+ type: number
+ example: 0
+ trigger_price:
+ type: number
+ example: 2.5
+ trigger_price_type:
+ type: string
+ example: "MARK_PRICE"
+ type:
+ type: string
+ example: "MARKET"
+ created_time:
+ type: string
+ example: 1702871974317
+ updated_time:
+ type: string
+ example: 1702871974317
+ visible_quantity:
+ type: number
+ example: 5.5
+ realized_pnl:
+ type: number
+ example: 0.0
+ child_orders:
+ type: array
+ items:
+ type: object
+ description: "Array of `AlgoOrder`"
+
+ TradesResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Trade"
+
+ Trade:
+ type: object
+ properties:
+ id:
+ type: number
+ example: 2
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ fee:
+ type: number
+ example: 1.0E-4
+ fee_asset:
+ type: string
+ example: "USDC"
+ description: "fee. use Base as unit when BUY, use Quote as unit when SELL"
+ side:
+ type: string
+ example: "BUY"
+ order_id:
+ type: number
+ example: 1
+ executed_price:
+ type: number
+ example: 123
+ executed_quantity:
+ type: number
+ example: 0.05
+ executed_timestamp:
+ type: number
+ example: 1567382401000
+ is_maker:
+ type: number
+ example: 1
+ realized_pnl:
+ type: number
+ example: 0.0
+ match_id:
+ type: string
+ example: ""
+
+ AlgoTrade:
+ type: object
+ properties:
+ id:
+ type: number
+ example: 2
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ fee:
+ type: number
+ example: 1.0E-4
+ fee_asset:
+ type: string
+ example: "USDC"
+ description: "fee. use Base as unit when BUY, use Quote as unit when SELL"
+ side:
+ type: string
+ example: "BUY"
+ order_id:
+ type: number
+ example: 1
+ executed_price:
+ type: number
+ example: 123
+ executed_quantity:
+ type: number
+ example: 0.05
+ executed_timestamp:
+ type: number
+ example: 1567382401000
+ is_maker:
+ type: number
+ example: 1
+ match_id:
+ type: string
+ example: ""
+
+ ClientHoldingResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - holding
+ properties:
+ holding:
+ type: array
+ items:
+ type: object
+ required:
+ - frozen
+ - holding
+ - pending_short
+ - token
+ - updated_time
+ properties:
+ updated_time:
+ type: number
+ example: 1580794149000
+ description: Unix epoch time in milliseconds
+ token:
+ type: string
+ example: "BTC"
+ holding:
+ type: number
+ example: -28.000752
+ frozen:
+ type: number
+ example: 0
+ pending_short:
+ type: number
+ example: -2000
+
+ ClientInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - account_id
+ - account_mode
+ - email
+ - futures_maker_fee_rate
+ - futures_taker_fee_rate
+ - imr_factor
+ - maintenance_cancel_orders
+ - maker_fee_rate
+ - max_leverage
+ - taker_fee_rate
+ properties:
+ account_id:
+ type: string
+ email:
+ type: string
+ example: "test@test.com"
+ account_mode:
+ type: string
+ example: "FUTURES"
+ description: account mode
+ max_leverage:
+ type: number
+ example: 20
+ taker_fee_rate:
+ type: number
+ example: 0
+ maker_fee_rate:
+ type: number
+ example: 0
+ rwa_taker_fee_rate:
+ type: number
+ example: 0
+ rwa_maker_fee_rate:
+ type: number
+ example: 0
+ futures_taker_fee_rate:
+ type: number
+ example: 0
+ futures_maker_fee_rate:
+ type: number
+ example: 0
+ maintenance_cancel_orders:
+ type: boolean
+ example: true
+ imr_factor:
+ # TODO
+ type: object
+ required:
+ - PERP_BTC_USDC
+ - PERP_ETH_USDC
+ - PERP_NEAR_USDC
+ properties:
+ PERP_BTC_USDC:
+ type: number
+ PERP_ETH_USDC:
+ type: number
+ PERP_NEAR_USDC:
+ type: number
+ max_notional:
+ # TODO
+ type: object
+ required:
+ - PERP_BTC_USDC
+ - PERP_ETH_USDC
+ - PERP_NEAR_USDC
+ properties:
+ PERP_BTC_USDC:
+ type: number
+ PERP_ETH_USDC:
+ type: number
+ PERP_NEAR_USDC:
+ type: number
+
+ OrdersResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Order"
+
+ AlgoOrdersResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/AlgoOrder"
+
+ ClientStatisticsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - days_since_registration
+ - fees_paid_last_30_days
+ - perp_fees_paid_last_30_days
+ - perp_trading_volume_last_24_hours
+ - perp_trading_volume_last_30_days
+ - perp_trading_volume_ytd
+ - trading_volume_last_24_hours
+ - trading_volume_last_30_days
+ - trading_volume_ytd
+ properties:
+ days_since_registration:
+ type: integer
+ example: 109
+ fees_paid_last_30_days:
+ type: number
+ example: 0
+ perp_fees_paid_last_30_days:
+ type: number
+ example: 0
+ perp_trading_volume_last_24_hours:
+ type: number
+ example: 0
+ perp_trading_volume_last_7_days:
+ type: number
+ example: 0
+ perp_trading_volume_last_30_days:
+ type: number
+ example: 0
+ perp_trading_volume_ytd:
+ type: number
+ example: 0
+ perp_trading_volume_ltd:
+ type: number
+ example: 0
+ trading_volume_last_24_hours:
+ type: number
+ example: 0
+ trading_volume_last_30_days:
+ type: number
+ example: 0
+ trading_volume_ytd:
+ type: number
+ example: 459.7436
+
+ AssetHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - id
+ - tx_id
+ - side
+ - token
+ - amount
+ - fee
+ - trans_status
+ - created_time
+ - updated_time
+ - chain_id
+ properties:
+ id:
+ type: string
+ example: "230707030600002"
+ tx_id:
+ type: string
+ example: "0x4b0714c63cc7abae72bf68e84e25860b88ca651b7d27dad1e32bf4c027fa5326"
+ side:
+ type: string
+ example: "WITHDRAW"
+ token:
+ type: string
+ example: "USDC"
+ amount:
+ type: number
+ example: 555
+ fee:
+ type: number
+ example: 0
+ trans_status:
+ type: string
+ example: "FAILED"
+ created_time:
+ type: number
+ example: 1688699193034
+ updated_time:
+ type: number
+ example: 1688699193096
+ chain_id:
+ type: string
+ example: "986532"
+
+ ClaimInsuranceFundBody:
+ type: object
+ required:
+ - claim_id
+ - qty_request
+ - symbol
+ properties:
+ claim_id:
+ type: number
+ symbol:
+ type: string
+ qty_request:
+ type: number
+ description: Quantity to be claimed from insurance fund
+ limit_price:
+ type: object
+ description: "Liquidator’s instruction to let the system reject the liquidation claim if the following condition applies: if position_qty > 0, reject if mark_price > limit_price; if position_qty < 0, reject if mark_price < limit_price"
+ required:
+ - symbol
+ properties:
+ symbol:
+ type: number
+ description: "The limit price for each symbol in the liquidation claim"
+
+ ClaimInsuranceFundResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - liquidation_id
+ - positions_by_perp
+ - timestamp
+ properties:
+ liquidation_id:
+ type: integer
+ example: 101
+ timestamp:
+ type: integer
+ example: 1663313562090
+ positions_by_perp:
+ type: array
+ items:
+ $ref: "#/components/schemas/PerpPosition"
+
+ SystemInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - msg
+ - status
+ properties:
+ status:
+ type: number
+ example: 0
+ scheduled_maintenance:
+ type: object
+ properties:
+ startTime:
+ type: number
+ example: 1618822380000
+ endTime:
+ type: number
+ example: 1618822440000
+ msg:
+ type: string
+
+ RegistrationNonceResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - registration_nonce
+ properties:
+ registration_nonce:
+ type: string
+ example: "194528949540"
+
+ GetAccountResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - account_id
+ - user_id
+ properties:
+ user_id:
+ type: integer
+ example: 24
+ account_id:
+ type: string
+ example: "0xb129e074a17dfba8d652ae1eca21c7d6bb6904f1aa693f4886d50db170933a46"
+
+ GetAllAccountsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - user_id
+ - account_id
+ - broker_id
+ - chain_type
+ - user_type
+ properties:
+ user_id:
+ type: integer
+ example: 24
+ account_id:
+ type: string
+ example: "0xb129e074a17dfba8d652ae1eca21c7d6bb6904f1aa693f4886d50db170933a46"
+ broker_id:
+ type: string
+ example: "aden"
+ chain_type:
+ type: string
+ example: "EVM"
+ description: "`EVM` or `SOL`"
+ user_type:
+ type: string
+ example: "SUB"
+ description: "`SUB`, `MAIN`, `SP`, `PV`, `UV`"
+
+ GetBrokerResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ properties:
+ broker_id:
+ type: array
+ items:
+ type: string
+ example: "woofi_pro"
+
+ DelegateSignerBody:
+ type: object
+ required:
+ - message
+ - signature
+ - userAddress
+ properties:
+ message:
+ type: object
+ required:
+ - delegateContract
+ - brokerId
+ - chainId
+ - registrationNonce
+ - timestamp
+ properties:
+ delegateContract:
+ type: string
+ example: "0xaddress"
+ brokerId:
+ type: string
+ description: Builder ID
+ example: "woofi_pro"
+ chainId:
+ type: integer
+ example: 421613
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ timestamp:
+ type: integer
+ example: 1704879369551
+ description: timestamp in UNIX milliseconds
+ registrationNonce:
+ type: integer
+ example: 161111791392
+ description: Message object containing the message that is signed by the wallet owner
+ txHash:
+ type: string
+ description: ""
+ example: "0xtxhash"
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ example: "0x8668447f28534dc1b76566e41f5fcdeb6b131355b9cf428f3fae5c134e9a0fc55642a2771860484651e61043e8ac67a1a6e54031a4c66de44acd77c9d9b281e81b"
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+ example: "0xDd3287043493E0a08d2B348397554096728B459c"
+
+ RegisterAccountBody:
+ type: object
+ required:
+ - message
+ - signature
+ - userAddress
+ properties:
+ message:
+ type: object
+ required:
+ - brokerId
+ - chainId
+ - registrationNonce
+ - timestamp
+ properties:
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ chainType:
+ type: string
+ description: "`EVM` or `SOL`"
+ timestamp:
+ type: string
+ description: timestamp in UNIX milliseconds
+ registrationNonce:
+ type: string
+ description: Message object containing the message that is signed by the wallet owner
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+
+ RegisterAccountResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - account_id
+ properties:
+ account_id:
+ type: string
+
+ GetOrderlyKeyResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - expiration
+ - orderly_key
+ - scope
+ properties:
+ orderly_key:
+ type: string
+ example: "ed25519:7tEoJo5hMBKBrQsqmc8yw1xNfoQCFQBVwQT1eFafRNRf"
+ scope:
+ type: string
+ example: "read,trading"
+ expiration:
+ type: integer
+ example: 1689086651947
+ tag:
+ type: string
+
+ DelegateOrderlyKeyBody:
+ type: object
+ required:
+ - message
+ - signature
+ - userAddress
+ properties:
+ message:
+ type: object
+ required:
+ - delegateContract
+ - brokerId
+ - chainId
+ - expiration
+ - orderlyKey
+ - scope
+ - timestamp
+ properties:
+ delegateContract:
+ type: string
+ description: ""
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ orderlyKey:
+ type: string
+ description: Orderly Key to be added
+ scope:
+ type: string
+ description: Valid nonce from Get Registration Nonce
+ timestamp:
+ type: number
+ description: timestamp in UNIX milliseconds
+ expiration:
+ type: number
+ description: Expiration time of the key in UNIX milliseconds. Maximum allowed expiration is 365 days from add
+ description: Message object containing the message that is signed by the wallet owner
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+
+ OrderlyKeyBody:
+ type: object
+ required:
+ - message
+ - signature
+ - userAddress
+ properties:
+ message:
+ type: object
+ required:
+ - brokerId
+ - chainId
+ - expiration
+ - orderlyKey
+ - scope
+ - timestamp
+ properties:
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ chainType:
+ type: string
+ description: "`EVM` or `SOL`"
+ orderlyKey:
+ type: string
+ description: Orderly Key to be added
+ scope:
+ type: string
+ description: "Valid scopes are `read`, `trading` and `asset`; multiple scopes can be sent by separating scopes with comma such as `read,trading`"
+ timestamp:
+ type: number
+ description: timestamp in UNIX milliseconds
+ expiration:
+ type: number
+ description: Expiration time of the key in UNIX milliseconds. Maximum allowed expiration is 365 days from add
+ tag:
+ type: string
+ description: An optional tag of string values.
+ subAccountId:
+ type: string
+ description: create the api key for the input subAccountId
+ description: Message object containing the message that is signed by the wallet owner
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+
+ RemoveOrderlyKeyBody:
+ type: object
+ required:
+ - orderly_key
+ properties:
+ orderly_key:
+ type: string
+ description: The Orderly key to be removed from the account
+
+ OrderlyKeyResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - orderly_key
+ properties:
+ id:
+ type: integer
+ example: 123
+ orderly_key:
+ type: string
+ example: "ed25519:FRXntsPJBCy6dzKv9WPw4eYSw3rKU9Npz3T6UmvvJc9Z"
+
+ ConfigResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - available_futures_leverage
+ properties:
+ available_futures_leverage:
+ type: string
+ example: "1,2,3,4,5,10,15,20"
+
+ ChainInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ type: object
+ required:
+ - name
+ - public_rpc_url
+ - chain_id
+ - currency_symbol
+ - explorer_base_url
+ # TODO vault_address has been missing. Check
+ - vault_address
+ properties:
+ name:
+ type: string
+ example: "Arbitrum"
+ public_rpc_url:
+ type: string
+ example: "https://arb1.arbitrum.io/rpc"
+ chain_id:
+ type: string
+ example: "42161"
+ currency_symbol:
+ type: string
+ example: "ETH"
+ currency_decimal:
+ type: number
+ example: 8
+ explorer_base_url:
+ type: string
+ example: https://arbiscan.io
+ vault_address:
+ type: string
+ example: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9"
+ broker_ids:
+ type: array
+ items:
+ type: string
+ example: "woofi_dex"
+ # token_info:
+ # type: object
+ # properties:
+ # token_name:
+ # type: string
+ # example: "USDC"
+ # description: "`USDC.e` for `Mantle` chain only"
+ # contract_address:
+ # type: string
+ # example: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
+ # decimals:
+ # type: number
+ # example: 6
+ # withdrawal_fee:
+ # type: number
+ # example: 2
+ # cross_chain_withdrawal_fee:
+ # type: number
+ # example: 5
+
+ BatchOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/CreatedOrder"
+
+ OrderStatusResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/OrderStatus"
+
+ ClientOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/Order"
+
+ AlgoClientOrderResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/AlgoOrder"
+
+ OrderbookResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - asks
+ - bids
+ - timestamp
+ properties:
+ asks:
+ type: array
+ items:
+ $ref: "#/components/schemas/OrderbookEntry"
+ bids:
+ type: array
+ items:
+ $ref: "#/components/schemas/OrderbookEntry"
+ timestamp:
+ type: integer
+ description: Unix epoch time in milliseconds
+
+ OrderbookEntry:
+ type: object
+ required:
+ - price
+ - quantity
+ properties:
+ price:
+ type: number
+ example: 10669.4
+ quantity:
+ type: number
+ example: 1.56263218
+
+ KlineResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ required:
+ - open
+ - close
+ - low
+ - high
+ - volume
+ - amount
+ - symbol
+ - type
+ - start_timestamp
+ - end_timestamp
+ type: object
+ properties:
+ open:
+ type: number
+ example: 66166.23
+ close:
+ type: number
+ example: 66124.56
+ low:
+ type: number
+ example: 66038.06
+ high:
+ type: number
+ example: 66176.97
+ volume:
+ type: number
+ example: 23.45528526
+ amount:
+ type: number
+ example: 1550436.21725288
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ type:
+ type: string
+ example: "1m"
+ start_timestamp:
+ type: integer
+ example: 1636388220000
+ description: Unix epoch time in milliseconds
+ end_timestamp:
+ type: integer
+ example: 1636388280000
+ description: Unix epoch time in milliseconds
+
+ OrderTradesResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Trade"
+
+ AlgoOrderTradesResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - rows
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/AlgoTrade"
+
+ TradeResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/Trade"
+
+ ClientMaintenanceConfigBody:
+ type: object
+ required:
+ - maintenance_cancel_order_flag
+ properties:
+ maintenance_cancel_order_flag:
+ type: boolean
+ description: "if true, system will cancel all of user's pending orders during maintenance."
+
+ ClientMaintenanceConfigResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - success_alter
+ properties:
+ success_alter:
+ type: string
+ example: "Success! Your change has been saved!"
+
+ DailyVolumeResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ required:
+ - date
+ - perp_volume
+ properties:
+ date:
+ type: string
+ example: 2024-01-01
+ perp_volume:
+ type: number
+ example: 100.24
+
+ WithdrawNonceReponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - withdraw_nonce
+ properties:
+ withdraw_nonce:
+ type: integer
+ example: 1
+
+ SettleNonceResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - settle_nonce
+ properties:
+ settle_nonce:
+ type: integer
+ example: 1
+
+ WithdrawRequestBody:
+ required:
+ - signature
+ - userAddress
+ - verifyingContract
+ - message
+ type: object
+ properties:
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+ verifyingContract:
+ type: string
+ description: Address of the Orderly ledger contract
+ message:
+ type: object
+ description: Message object containing the message that is signed by the wallet owner
+ required:
+ - brokerId
+ - chainId
+ - receiver
+ - token
+ - amount
+ - withdrawNonce
+ - timestamp
+ properties:
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ chainType:
+ type: string
+ description: "`EVM` or `SOL`"
+ receiver:
+ type: string
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ token:
+ type: string
+ description: The string representation of the token that is being withdrawn (eg "USDC")
+ amount:
+ type: number
+ description: Amount of tokens to be withdrawn
+ withdrawNonce:
+ type: string
+ description: Valid withdrawal nonce from Get withdrawal nonce
+ timestamp:
+ type: string
+ description: current timestamp in UNIX milliseconds
+
+ DelegateWithdrawRequestBody:
+ required:
+ - signature
+ - userAddress
+ - verifyingContract
+ - message
+ type: object
+ properties:
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+ verifyingContract:
+ type: string
+ description: Address of the Orderly ledger contract
+ message:
+ type: object
+ description: Message object containing the message that is signed by the wallet owner
+ required:
+ - delegateContract
+ - brokerId
+ - chainId
+ - receiver
+ - token
+ - amount
+ - withdrawNonce
+ - timestamp
+ properties:
+ delegateContract:
+ type: string
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ receiver:
+ type: string
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ token:
+ type: string
+ description: The string representation of the token that is being withdrawn (eg "USDC")
+ amount:
+ type: number
+ description: Amount of tokens to be withdrawn
+ withdrawNonce:
+ type: string
+ description: Valid withdrawal nonce from Get withdrawal nonce
+ timestamp:
+ type: string
+ description: current timestamp in UNIX milliseconds
+
+ WithdrawResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - withdraw_id
+ properties:
+ withdraw_id:
+ type: integer
+ example: 123
+
+ SettlePnlBody:
+ required:
+ - signature
+ - userAddress
+ - verifyingContract
+ - message
+ type: object
+ properties:
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+ verifyingContract:
+ type: string
+ description: Address of the Orderly ledger contract
+ message:
+ type: object
+ description: Message object containing the message that is signed by the wallet owner
+ required:
+ - brokerId
+ - chainId
+ - settleNonce
+ - timestamp
+ properties:
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ chainType:
+ type: string
+ description: "`EVM` or `SOL`"
+ settleNonce:
+ type: integer
+ description: Nonce from Get settle PnL Nonce
+ timestamp:
+ type: integer
+ description: current timestamp in UNIX milliseconds
+
+ DelegateSettlePnlBody:
+ required:
+ - signature
+ - userAddress
+ - verifyingContract
+ - message
+ type: object
+ properties:
+ signature:
+ type: string
+ description: The signature generated by signing the message object via EIP-712
+ userAddress:
+ type: string
+ description: The address of the wallet signing the message object via EIP-712
+ verifyingContract:
+ type: string
+ description: Address of the Orderly ledger contract
+ message:
+ type: object
+ description: Message object containing the message that is signed by the wallet owner
+ required:
+ - delegateContract
+ - brokerId
+ - chainId
+ - settleNonce
+ - timestamp
+ properties:
+ delegateContract:
+ type: string
+ brokerId:
+ type: string
+ description: Builder ID
+ chainId:
+ type: integer
+ description: Chain ID of registering chain (within those that are supported by the Network)
+ settleNonce:
+ type: integer
+ description: Nonce from Get settle PnL Nonce
+ timestamp:
+ type: integer
+ description: current timestamp in UNIX milliseconds
+
+ SettlePnlResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - settle_pnl_id
+ properties:
+ settle_pnl_id:
+ type: integer
+ example: 1
+
+ PnlSettlementHistoryResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - meta
+ - rows
+ properties:
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+ rows:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: integer
+ example: 10001
+ old_balance:
+ type: number
+ example: 4050
+ new_balance:
+ type: number
+ example: 3050
+ settled_amount:
+ type: number
+ example: -500
+ requested_time:
+ type: integer
+ description: Unix epoch time in ms
+ example: 1575014255089
+ settled_time:
+ type: integer
+ description: Unix epoch time in ms
+ example: 1575014255910
+ symbols:
+ type: array
+ items:
+ type: object
+ required:
+ - settled_amount
+ - symbol
+ properties:
+ symbol:
+ type: string
+ example: "PERP_BTC_USDC"
+ settled_amount:
+ # TODO should this be number?
+ type: integer
+ example: -500
+
+ KeyInfoResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ type: object
+ # TODO required
+ properties:
+ orderly_key:
+ type: string
+ example: ed25519:B4yEc8xMjoqdUiKT5ZT1b2cUy5cggZrbMPvCrqyjkQhf
+ key_status:
+ type: string
+ description: ACTIVE / REMOVING / REMOVED
+ example: "ACTIVE"
+ ip_restriction_list:
+ type: array
+ items:
+ type: string
+ ip_restricion_status:
+ type: string
+ description: ALLOW_RESTRICTION_LIST / ALLOW_ALL_IPS / DISALLOW_ALL_IPS
+ example: "ALLOW_ALL_IPS"
+ expiration:
+ type: integer
+ example: 1689086651947
+ scope:
+ type: string
+ example: "read,trading"
+ tag:
+ type: string
+ description: "Optional, could be null if there wasn't a tag"
+
+ IpRestrictionResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ type: object
+ required:
+ - account_id
+ - ip_restriction_list
+ - orderly_key
+ - tag
+ properties:
+ account_id:
+ type: string
+ example: "0x0f29bfb4c1bc9fea3f3be46bab6d795e22a6272354b136fde05f6b80cfcad546"
+ orderly_key:
+ type: string
+ example: "ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX"
+ ip_restriction_list:
+ type: array
+ items:
+ type: string
+ example: "218.190.230.163"
+ tag:
+ type: string
+ # TODO optional, but required?
+ description: "Optional, could be null if there wasn't a tag"
+
+ SetIpRestrictionResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ type: object
+ required:
+ - ip_restriction_list
+ properties:
+ ip_restriction_list:
+ type: array
+ items:
+ type: string
+ example: "192.168.0.1-192.168.0.10"
+
+ AllNotificationsResponse:
+ allOf:
+ - $ref: "#/components/schemas/BasicResponse"
+ - required:
+ - data
+ type: object
+ properties:
+ data:
+ required:
+ - meta
+ - rows
+ type: object
+ properties:
+ rows:
+ type: array
+ items:
+ $ref: "#/components/schemas/Notification"
+ meta:
+ $ref: "#/components/schemas/PaginationMeta"
+
+ Notification:
+ type: object
+ # TODO required
+ properties:
+ id:
+ type: integer
+ example: 12345
+ message_type:
+ type: string
+ description: ORDER_FILLED
+ example: "ORDER_FILLED"
+ email:
+ type: string
+ description: email if bounded to the account
+ example: test@orderly.network
+ application_id:
+ type: string
+ description: wallet address of the account
+ example: "0x0f29bfb4c1bc9fea3f3be46bab6d795e22a6272354b136fde05f6b80cfcad546"
+ title:
+ type: string
+ example: " Your order has been filled"
+ type:
+ type: string
+ description: TRADE / SYSTEM
+ example: "TRADE"
+ level:
+ type: string
+ description: GENERAL / IMPORTANT
+ example: "GENERAL"
+ content_summary:
+ type: string
+ example: "Your order to buy 1 NEAR-PERP has been filled: 0.5/1 at 1.9876."
+ content:
+ type: string
+ example: "
Your order to buy 1 NEAR-PERP has been filled: 0.5/1 at 1.9876.
" + content_raw: + required: + - executed_price + - executed_quantity + - executed_timestamp + - order_id + - side + - symbol + type: object + properties: + symbol: + type: string + example: "PERP_NEAR_USDC" + side: + type: string + example: "BUY" + order_id: + type: integer + example: 1 + executed_price: + type: number + example: 1.9876 + executed_quantity: + type: number + example: 0.5 + executed_timestamp: + type: integer + example: 1567382401000 + mark_read: + type: integer + description: "1 = READ, 0 = UNREAD" + example: 1 + operator: + type: integer + example: 1 + created_time: + type: integer + example: 1670425970373 + announcement_id: + type: integer + + UnreadNotificationsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - count + - lastId + - meta + - rows + properties: + rows: + type: array + items: + $ref: "#/components/schemas/Notification" + meta: + $ref: "#/components/schemas/PaginationMeta" + count: + type: integer + description: number of unread messages + lastId: + type: integer + description: the id of the last unread message + + VaultBalanceResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + chain_id: + type: integer + example: 80001 + token: + type: string + example: "USDC" + balance: + type: number + example: 1000.12345 + + GetAccountDetailsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - address + - broker_id + properties: + address: + type: string + example: "0xfcc17f2b240380d56f0615e8f654e4ae3cee8957" + broker_id: + type: string + example: "woofi_pro" + + GetAnnouncementsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - address + - broker_id + properties: + announcement_id: + type: number + example: 1 + message: + type: string + example: "Position information is incorrect due to a 1-minute delay." + url: + type: string + example: "null" + + GetBrokerDailyResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + snapshot_time: + type: number + example: 1702989203989 + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x58220761ade872c94f85e62f1b24a74eec792aaa3677b6201071fd05c1698e89" + date: + type: string + example: "2023-09-14" + perp_volume: + type: number + example: 451580.9523 + perp_maker_volume: + type: number + example: 123.456 + perp_taker_volum: + type: number + example: 123.456 + total_fee: + type: number + example: 123.456 + broker_fee: + type: number + example: 123.456 + address: + type: string + example: "0x0000000000" + realized_pnl: + type: number + example: 111.00 + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + LeaderboardResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + snapshot_time: + type: number + example: 1702989203989 + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x58220761ade872c94f85e62f1b24a74eec792aaa3677b6201071fd05c1698e89" + perp_volume: + type: number + example: 451580.9523 + perp_maker_volume: + type: number + example: 123.456 + perp_taker_volum: + type: number + example: 123.456 + address: + type: string + example: "0x0000000000" + realized_pnl: + type: number + example: 111.00 + broker_id: + type: number + example: "demo" + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + GetUserDailyStatsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + account_value: + type: number + example: 456789.12 + date: + type: string + example: "2023-09-14" + perp_volume: + type: number + example: 451580.9523 + pnl: + type: number + example: 123.45 + snapshot_time: + type: number + example: 1722989203989 + + UserFeeRatesResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + properties: + meta: + type: object + properties: + total: + type: number + example: 50 + records_per_page: + type: number + example: 25 + current_page: + type: number + example: 1 + description: "start from 1" + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x114f583aea808c5812e16109b6c087660285a26f0a31dcc0f9b54f1a6a0fefbd" + address: + type: string + example: "0xa8ec6970fec8842dd229eef170f624c3d77dab45" + is_default_fee_rate: + type: string + example: "true" + maker_fee_rate: + type: number + example: 0.03 + taker_fee_rate: + type: number + example: 0.01 + rwa_taker_fee_rate: + type: number + example: 0.0001 + rwa_maker_fee_rate: + type: number + example: 0.0001 + + CreateAlgoOrderChildRequest: + type: object + properties: + symbol: + type: string + algo_type: + type: string + side: + type: string + type: + type: string + trigger_price: + type: number + price: + type: number + reduce_only: + type: boolean + trigger_price_type: + type: string + # child_orders: + # type: array + # items: + # type: object + # description: Array of `CreateAlgoOrderChildRequest` + + EditAlgoOrderChildRequest: + type: object + properties: + order_id: + type: string + trigger_price: + type: number + price: + type: number + quantity: + type: number + is_activated: + type: string + trigger_price_type: + type: string + child_orders: + type: array + items: + type: object + description: "Array of `EditAlgoOrderChildRequest`" + + GetReferralCodeResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x114f583aea808c5812e16109b6c087660285a26f0a31dcc0f9b54f1a6a0fefbd" + user_address: + type: string + example: "0xa8ec6970fec8842dd229eef170f624c3d77dab45" + number_of_codes: + type: integer + example: "1" + total_invites: + type: integer + example: 0.03 + total_traded: + type: number + example: 0.01 + referee_volume: + type: number + example: 0.01 + referee_fee: + type: number + example: 0.01 + orderly_fee: + type: number + example: 1111 + referral_codes: + type: array + items: + type: object + properties: + code: + type: string + example: "C4" + max_rebate_rate: + type: number + example: 0.4 + referrer_rebate_rate: + type: number + example: 0.3 + referee_rebate_rate: + type: number + example: 0.1 + total_referrer_rebate: + type: number + example: 123.456 + total_referee_rebate: + type: number + example: 123.456 + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + VerifyReferralCodeResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + properties: + exist: + type: string + example: true + description: "`true`/`false`" + + CheckReferralCodeResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + properties: + referral_code: + type: string + example: code + + GetReferralInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + properties: + referrer_info: + type: object + properties: + 1d_invites: + type: integer + example: 5 + 7d_invites: + type: integer + example: 5 + 30d_invites: + type: integer + example: 5 + total_invites: + type: integer + example: 30 + 1d_traded: + type: integer + example: 5 + 7d_traded: + type: integer + example: 10 + 30d_traded: + type: integer + example: 20 + total_traded: + type: integer + example: 25 + 1d_referee_volume: + type: number + example: 5 + 7d_referee_volume: + type: number + example: 10 + 30d_referee_volume: + type: number + example: 20 + total_referee_volume: + type: number + example: 30 + 1d_referrer_rebate: + type: number + example: 5 + 7d_referrer_rebate: + type: number + example: 10 + 30d_referrer_rebate: + type: number + example: 20 + total_referrer_rebate: + type: number + example: 30 + referral_codes: + type: array + items: + type: object + properties: + code: + type: string + example: "C4" + max_rebate_rate: + type: number + example: 0.4 + referrer_rebate_rate: + type: number + example: 0.3 + referee_rebate_rate: + type: number + example: 0.1 + total_invites: + type: number + example: 30 + total_traded: + type: number + example: 25 + referee_info: + type: object + properties: + referer_code: + type: string + example: "Kevin" + referee_rebate_rate: + type: number + example: 0.1 + 1d_referee_rebate: + type: number + example: 5 + 7d_referee_rebate: + type: number + example: 10 + 30d_referee_rebate: + type: number + example: 20 + total_referee_rebate: + type: number + example: 30 + + GetReferralHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x114f583aea808c5812e16109b6c087660285a26f0a31dcc0f9b54f1a6a0fefbd" + description: "account_id of the referee" + user_address: + type: string + example: "0xa8ec6970fec8842dd229eef170f624c3d77dab45" + description: "address of the referee" + referral_code: + type: string + example: "C4" + description: "the referral code used by the referee" + volume: + type: number + example: 1229.12 + description: "the total volume traded by the referee" + referral_rebate: + type: number + example: 2.22 + description: "the total referral rebate paid to the referer based on the referee's fees paid" + date: + type: string + example: "2023-11-14" + description: "the date of this entry" + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + GetRebateSummaryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + traded_referral: + type: number + example: 11 + description: "track referral trades during the specified date" + referral_rebate: + type: number + example: 2.22 + description: "the total referral rebate received by the user on this day" + volume: + type: number + example: 123.456 + description: "" + date: + type: string + example: "2023-11-14" + description: "" + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + GetRefereeRebateSummaryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + referee_rebate: + type: number + example: 2.22 + description: "the total referee rebate received by the user on this day" + date: + type: string + example: "2023-11-14" + description: "" + + GetRefereeHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + volume: + type: number + example: 1229.12 + referee_rebate: + type: number + example: 2.22 + date: + type: string + example: 2023-11-14 + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + GetRefereeInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + account_id: + type: string + example: "0x114f583aea808c5812e16109b6c087660285a26f0a31dcc0f9b54f1a6a0fefbd" + description: "account_id of the referee" + user_address: + type: string + example: "0xa8ec6970fec8842dd229eef170f624c3d77dab45" + description: "address of the referee" + register_time: + type: number + example: 1663936930000 + description: "register time of the referee" + code_binding_time: + type: number + example: 1663936930000 + description: "the time the referee bind the account to this referral code" + referral_code: + type: string + example: "C4" + description: "the referral code used by the referee" + volume: + type: number + example: 1229.12 + description: "the total volume traded by the referee" + referral_rebate: + type: number + example: 2.22 + description: "the total referral rebate paid to the referer based on the referee's fees paid" + trade_status: + type: string + example: "Registered" + description: "`Registered` = Not traded but bind the code / `Traded` = bind the code and volume > 0" + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + GetDistributionHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + required: + - data + properties: + data: + type: object + required: + - rows + properties: + rows: + type: array + items: + type: object + properties: + id: + type: integer + example: 12345 + description: "" + token: + type: string + example: "USDC" + description: "" + type: + type: string + example: "REFERRAL_REBATE" + description: "`REFERRAL_REBATE`/`REFEREE_REBATE`/`BROKER_FEE`" + amount: + type: number + example: 12.12 + description: "" + status: + type: string + example: "COMPLETED" + description: "" + created_time: + type: number + example: 1703511545867 + description: "" + updated_time: + type: number + example: 1703511545867 + description: "" + meta: + type: object + properties: + records_per_page: + type: integer + example: 25 + current_page: + type: integer + example: 1 + description: "start from 1" + total: + type: integer + example: 50 + + DelegateSignerResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + user_id: + type: integer + example: 100338 + account_id: + type: string + example: "0xd890c9b8b86fba7312b919e32bbabd619228b32d59f6d0f68fdf4451a684a654" + valid_signer: + type: string + example: "0xc6c4c5c41efa74ebc63c675dcf7034e8382f3a42" + + GetCampaignsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: "campaign_20230806" + start_time: + type: integer + example: 1575014255089 + end_time: + type: integer + example: 1585014255089 + signup_start_time: + type: integer + example: 1585014255089 + signup_end_time: + type: integer + example: 1585014255089 + register_time: + type: integer + example: 1585014255089 + description: "only response when address is input, can be null for no registration record" + + GetCampaignStatsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + volume: + type: number + example: 65615479.490133 + user_count: + type: integer + example: 1680 + sign_up_count: + type: integer + example: 1680 + updated_time: + type: integer + example: 1750766370033 + + GetCampaignDetailedStatsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + meta: + $ref: "#/components/schemas/PaginationMeta" + rows: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_dex" + symbol: + type: string + example: "PERP_NEAR_USDC" + volume: + type: number + example: 800.12 + user_count: + type: integer + example: 10 + + GetCampaignRankingResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + meta: + $ref: "#/components/schemas/PaginationMeta" + rows: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_dex" + account_id: + type: string + example: "account01.eth" + address: + type: string + example: "0xaccount01" + volume: + type: number + example: 800.12 + pnl: + type: number + example: 800.12 + total_deposit_amount: + type: number + example: 800.12 + total_withdrawal_amount: + type: number + example: 800.12 + start_account_value: + type: number + example: 800.12 + end_account_value: + type: number + example: 800.12 + + GetCampaignUserResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + volume: + type: number + example: 1000.123 + filled_order_count: + type: integer + example: 10 + updated_time: + type: integer + example: 1688371971550 + pnl: + type: number + example: 800.12 + total_deposit_amount: + type: number + example: 800.12 + total_withdrawal_amount: + type: number + example: 800.12 + start_account_value: + type: number + example: 800.12 + end_account_value: + type: number + example: 800.12 + new_invited_referee: + type: number + example: 1 + new_traded_referee: + type: number + example: 10 + total_staked_order: + type: number + example: 111 + total_staked_esorder: + type: number + example: 111 + + GetCampaignCheckResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + status: + type: string + example: "success" + + BuilderAutoReferralInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + required_trading_volume: + type: number + example: 10000 + max_rebate: + type: number + example: 1 + referrer_rebate: + type: number + example: 0.5 + referee_rebate: + type: number + example: 0.5 + description: + type: string + enable: + type: boolean + example: True + + AutoReferralProgressResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + required_volume: + type: number + example: 10000 + completed_volume: + type: number + example: 10000 + auto_referral_code: + type: string + + GetPointsLeaderboardResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + epoch_id: + type: number + example: 1 + meta: + $ref: "#/components/schemas/PaginationMeta" + rows: + type: array + items: + type: object + properties: + rank: + type: number + example: 1 + tier: + type: number + example: 1 + address: + type: string + example: "0xfcc17f2b240380d56f0615e8f654e4ae3cee8957" + points: + type: number + example: 15000 + previous_rank: + type: number + example: 4 + + GetEpochPointsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + current_total_epoch_points: + type: number + example: 547 + current_epoch_points: + type: number + example: 17 + current_epoch_id: + type: number + example: 3 + total_traders: + type: number + example: 200 + rows: + type: array + items: + type: object + properties: + rank: + type: number + example: 1 + epoch_id: + type: number + example: 1 + total_points: + type: number + example: 15000 + + GetEpochDatesResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + epoch_start_date: + type: number + example: 1708905600000 + epoch_id: + type: number + example: 1 + epoch_end_date: + type: number + example: 1708992000000 + + GetPointsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + total_points: + type: number + example: 547 + global_rank: + type: number + example: 17 + tier: + type: number + example: 3 + current_epoch_points: + type: number + example: 200 + current_epoch_rank: + type: number + example: 3 + rows: + type: array + items: + type: object + properties: + rank: + type: number + example: 1 + epoch_id: + type: number + example: 1 + points: + type: number + example: 15000 + + GetDefaultBrokerFeesResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + maker_fee_rate: + type: number + example: 0.0005 + taker_fee_rate: + type: number + example: 0.001 + rwa_taker_fee_rate: + type: number + example: 0.0001 + rwa_maker_fee_rate: + type: number + example: 0.0001 + + GetWalletStakedBalanceResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + total_staked_balance: + type: number + example: 10000 + total_staked_order: + type: number + example: 2000 + total_staked_esorder: + type: number + example: 8000 + + GetUnstakeDetailsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + total_unstake_order: + type: number + example: 500 + unstaked_end_time: + type: number + example: 1684671623456 + + GetStakingOverviewResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + total_stake: + type: number + example: 845599630.06 + rows: + type: array + items: + type: object + properties: + date: + type: string + example: "2023-05-20" + usd_reward: + type: number + example: 1268.34 + total_staked_usd: + type: number + example: 117654321.99 + + GetValorBatchInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + batch_id: + type: number + example: 1 + batch_start_time: + type: number + example: 1684502400000 + batch_end_time: + type: number + example: 1684588800000 + + GetValorPoolInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + valor_price_usdc: + type: number + example: 50 + treasury_pool_value: + type: number + example: 20000 + next_settlement_date: + type: string + example: "2024-06-15" + revenue_rate: + type: number + example: 0.6 + est_treasury_inflow: + type: number + example: 30000 + total_redeemed_amount: + type: number + example: 2000 + + GetValorRedeemInfoResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + redeeming_valor_amount: + type: number + example: 1000 + chain_details: + type: array + items: + type: object + properties: + chain_id: + type: number + example: 1 + pending_usdc: + type: number + example: 5000 + available_usdc: + type: number + example: 1500 + + GetesORDERVestingListResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + vest_request_id: + type: number + example: 1 + vest_start_time: + type: number + example: 1685600000000 + description: "`unlockTimestamp` - 15days" + vest_end_time: + type: number + example: 1691200000000 + description: "`unlockTimestamp` + 75days" + total_vest_amount: + type: number + example: 1500000.0 + status: + type: string + example: "claimed" + description: "`claimed` / `canceled` / `vesting`" + + GetParameterOfEachEpochForAllEpochsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + current_epoch: + type: number + example: 23 + description: The current on-going epoch for trading rewards + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + start_time: + type: number + example: 1711411200000 + description: epoch start timestamp + end_time: + type: number + example: 1711497600000 + description: epoch end timestamp + power_fees_paid_major: + type: number + example: 0.8 + description: The weight (power) of feesPaid for trader score calculation for major symbols + power_fees_paid_alts: + type: number + example: 0.8 + description: The weight (power) of feesPaid for trader score calculation for alts symbols + power_staked_major: + type: number + example: 0.2 + description: The weight (power) of stakedORDER for trader score calculation for major symbols + power_staked_alts: + type: number + example: 0.2 + description: The weight (power) of stakedORDER for trader score calculation for alts symbols + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + max_reward_amount: + type: number + example: 200000 + description: The maximum rewards distributable in this epoch + k_constant_major: + type: number + example: 10 + description: The number used in the function for weight of stakedORDER for major symbols + k_constant_alts: + type: number + example: 10 + description: The number used in the function for weight of stakedORDER for alts symbols + + GetEpochsDataResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + total_released_trading_rewards_order: + type: number + example: 3591403.07726818 + description: The sum of all confirmed $ORDER trading rewards order on Orderly level + total_released_trading_rewards_escrow: + type: number + example: 0 + description: The sum of all confirmed es$ORDER trading rewards order on Orderly level + current_epoch: + type: number + example: 66 + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + total_trader_score_major: + type: number + example: 8278.08333486 + description: | + The total amount of all trader scores for every broker in this epoch. + This value is 0 for epoch == current epoch + + Note: This value can be updated during the 7 days grace period after an epoch has ended + total_trader_score_alts: + type: number + example: 20901.92612209 + description: | + The total amount of all trader scores for every broker in this epoch. + This value is 0 for epoch == current epoch + + Note: This value can be updated during the 7 days grace period after an epoch has ended + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + reward_status: + type: string + example: "Confirmed" + description: | + - `Ongoing` = Current epoch in progress + - `Pending` = during the 7 days grace period where Orderly can stop/modify distribution. + - `Confirmed` = After the 7 days grace period, rewards are finalized and ready for users to claim. + - `0` = Future epoch + r_major: + type: number + example: 150000 + description: | + The total amount of rewards Orderly distributed for this epoch. + This value is 0 for epoch == current epoch + + Note: This value for might be updated during the 7 days grace period after the current epoch has ended + r_alts: + type: number + example: 50000 + description: | + The total amount of rewards Orderly distributed for this epoch. + This value is 0 for epoch == current epoch + + Note: This value for might be updated during the 7 days grace period after the current epoch has ended + + GetBrokerAllocationHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + rewards: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_pro" + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + pct_allocation: + type: number + example: 0.2 + description: The percentage rewards allocated to this broker in this epoch + rewards_allocation: + type: number + example: 100 + description: The amount of rewards allocated to this broker in this epoch + + GetWalletRewardsHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + wallet_lifetime_trading_rewards_order: + type: number + example: 3378176.44849794 + description: Sum of all confirmed $ORDER trading rewards on the wallet level + wallet_lifetime_trading_rewards_escrow: + type: number + example: 0 + description: Sum of all confirmed es$ORDER trading rewards on wallet level + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + wallet_epoch_avg_staked: + type: number + example: 0 + description: The epoch average staked balance used to calculate trading rewards + max_reward_amount: + type: number + example: 200000 + description: The maximum rewards distributable in this epoch + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + reward_status: + type: string + example: "Pending" + description: | + - `Pending` = During the waiting period after the epoch has ended + - `Confirmed` = When the epoch rewards have been finalized and ready for users to claim + r_wallet: + type: string + example: 199712.92309234 + description: | + The amount of rewards distributed to this wallet in this epoch + + Note: This value can be updated when reward_status == pending. + + GetAccountRewardsHistoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + broker: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_pro" + wallet_epoch_avg_staked: + type: number + example: 2 + description: The epoch average staked balance used to calculate trading rewards + trader_score_major: + type: number + example: 2 + description: | + The total trader scores from major symbols for this account under this broker in this epoch + + Note: This value can be updated when reward_status == pending + trader_score_alts: + type: number + example: 2 + description: | + The total trader scores from alts symbols for this account under this broker in this epoch + + Note: This value can be updated when reward_status == pending + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + reward_status: + type: string + example: "Confirmed" + description: | + - `Pending` = During the waiting period after the epoch has ended + - `Confirmed` = When the epoch rewards have been finalized and ready for users to claim + r_major: + type: number + example: 2 + description: | + The amount of rewards for major symbols distributed to this account under this broker in this epoch + + Note: This value can be updated when reward_status == pending + r_alts: + type: number + example: 2 + description: | + The amount of rewards for alts symbols distributed to this account under this broker in this epoch + + Note: This value can be updated when reward_status == pending + r_account: + type: number + example: 2 + description: | + The amount of rewards distributed to this account under this broker in this epoch + + Note: This value can be updated when reward_status == pending + + GetWalletGroupMarketMakingRewardsHistory: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + group_lifetime_mm_rewards_order: + type: number + example: 3378176.44849794 + description: Sum of all confirmed $ORDER market making rewards on the wallet group level + group_lifetime_mm_rewards_escrow: + type: number + example: 0 + description: Sum of all confirmed es$ORDER market making rewards on the wallet group level + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + epoch_avg_staked: + type: number + example: 0 + description: The epoch average staked balance used to calculate market making rewards for this wallet group + max_reward_amount: + type: number + example: 200000 + description: The maximum rewards distributable in this epoch + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + reward_status: + type: string + example: "PENDING" + description: | + - `Pending` = During the waiting period after the epoch has ended + - `Confirmed` = When the epoch rewards have been finalized and ready for users to claim + total_reward: + type: number + example: 10 + description: | + The amount of rewards distributed to this wallet group in this epoch + + Note: This value can be updated when reward_status == pending + + GetCurrentEpochBrokerEstimateResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + rows: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_pro" + est_pct_allocation: + type: number + example: 0.2 + description: The estimated percentage of trading rewards allocated to this broker for the on-going epoch + est_allocation: + type: number + example: 8278.08333486 + description: The estimated amount of trading rewards allocated to this broker for the on-going epoch + + GetCurrentEpochEstimateResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + est_trading_volume: + type: number + example: 120 + description: The estimated trading volume the user has traded for this ongoing epoch + est_avg_stake: + type: number + example: 55 + description: The estimated average amount of the user’s wallet stake balance used to compute estimated rewards + est_stake_boost: + type: number + example: 2.228807384 + description: The estimated stake boost used to compute estimated rewards + est_r_wallet: + type: number + example: 17.4236779074 + description: The estimated amount of trading rewards the user earn for the on-going epoch across all brokers + rows: + type: array + items: + type: object + properties: + broker_id: + type: string + example: "woofi_pro" + est_r_account: + type: number + example: 13.4236779074 + description: The estimated amount of trading rewards the user earns for the on-going epoch under this broker + + GetTheStatusOfMarketMakingRewardsProgrammeResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + epoch_status: + type: string + example: "Active" + current_epoch: + type: number + example: 23 + description: The current on-going epoch for market making rewards + last_completed_epoch: + type: number + example: 22 + + GetSymbolRewardsParametersResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + updated_time: + type: number + example: 123891923213 + rows: + type: array + items: + type: object + properties: + symbol: + type: string + example: "PERP_ETH_USDC" + multiplier: + type: number + example: 1.0 + min_depth: + type: number + example: 1.0 + max_spread: + type: number + example: 2.3 + description: "in bps" + + LeaderboardForMarketMakerRewardsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + market: + type: string + example: "PERP_ETH_USDC" + epoch: + type: number + example: 1 + epoch_status: + type: string + example: "Active" + rows: + type: array + items: + type: object + properties: + address: + type: string + example: "0xxx" + est_score: + description: "The estimated Qfinal" + type: number + example: 2.22 + est_r_wallet: + description: "The estimated market making rewards" + type: number + example: 2.228807384 + est_avg_stake: + type: number + example: 55 + est_stake_boost: + type: number + example: 1 + uptime: + type: number + example: 1 + maker_volume: + type: number + example: 2.3 + + GetTheStatusOfTradingRewardsProgrammeResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + epoch_status: + type: string + example: "Active" + current_epoch: + type: number + example: 23 + last_completed_epoch: + type: number + example: 22 + + GetSymbolRewardsCategoryResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + current: + type: object + properties: + epoch_id: + type: number + example: 23 + major_weightage: + type: number + example: 0.8 + alts_weightage: + type: number + example: 0.2 + major_symbols: + type: string + example: "BTC,ETH" + updated_time: + type: number + example: 123891923213 + next: + type: object + properties: + epoch_id: + type: number + example: 24 + major_weightage: + type: number + example: 0.7 + alts_weightage: + type: number + example: 0.3 + major_symbols: + type: string + example: "BTC,ETH" + updated_time: + type: number + example: 132891923213 + + GetMMCurrentEpochEstimateResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + est_maker_volume: + type: number + example: 120 + description: The estimated maker volume the wallet group has traded for this ongoing epoch + est_avg_stake: + type: number + example: 55 + description: The estimated average amount of the wallet group’s total stake balance used to compute estimated rewards + est_stake_boost: + type: number + example: 2.228807384 + description: The estimated stake boost used to compute estimated rewards + est_r_wallet: + type: number + example: 17.4236779074 + description: The estimated amount of market making rewards the wallet group earns for the on-going epoch + + GetParameterOfEachMMEpochForAllMMEpochsResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + current_epoch: + type: number + example: 23 + description: The current on-going epoch for market making rewards + rows: + type: array + items: + type: object + properties: + epoch_id: + type: number + example: 2 + start_time: + type: number + example: 1711411200000 + description: epoch start timestamp + end_time: + type: number + example: 1711497600000 + description: epoch end timestamp + epoch_token: + type: string + example: "ORDER" + description: Type of reward distributed for this epoch ($ORDER or es$ORDER) + max_reward_amount: + type: number + example: 200000 + description: The maximum rewards distributable in this epoch + + GetLeverageSettingResponse: + allOf: + - $ref: "#/components/schemas/BasicResponse" + - type: object + properties: + data: + type: object + properties: + max_futures_leverage: + type: integer + example: 50 + +servers: + - url: https://api.orderly.org + description: Mainnet + - url: https://testnet-api.orderly.org + description: Testnet diff --git a/hummingbot/connector/exchange_base.pyx b/hummingbot/connector/exchange_base.pyx index b3764914799..282a04d6eaf 100644 --- a/hummingbot/connector/exchange_base.pyx +++ b/hummingbot/connector/exchange_base.pyx @@ -337,7 +337,7 @@ cdef class ExchangeBase(ConnectorBase): def get_price_by_type(self, trading_pair: str, price_type: PriceType) -> Decimal: """ - Gets price by type (BestBid, BestAsk, MidPrice or LastTrade) + Gets price by type (BestBid, BestAsk, MidPrice, LastTrade, or MarkPrice) :param trading_pair: The market trading pair :param price_type: The price type :returns The price @@ -350,6 +350,10 @@ cdef class ExchangeBase(ConnectorBase): return (self.c_get_price(trading_pair, True) + self.c_get_price(trading_pair, False)) / Decimal("2") elif price_type is PriceType.LastTrade: return Decimal(self.c_get_order_book(trading_pair).last_trade_price) + elif price_type is PriceType.MarkPrice: + # MarkPrice is only available for perpetual derivatives + # PerpetualDerivativePyBase overrides this method to provide mark price + raise NotImplementedError(f"MarkPrice is not supported for {self.name}. Use a perpetual derivative connector.") async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal) -> Decimal: """ diff --git a/hummingbot/connector/perpetual_derivative_py_base.py b/hummingbot/connector/perpetual_derivative_py_base.py index 34afb94381f..d0ce8b6d955 100644 --- a/hummingbot/connector/perpetual_derivative_py_base.py +++ b/hummingbot/connector/perpetual_derivative_py_base.py @@ -8,7 +8,7 @@ from hummingbot.connector.derivative.position import Position from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.perpetual_trading import PerpetualTrading -from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, TradeType +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PriceType, TradeType from hummingbot.core.data_type.funding_info import FundingInfo from hummingbot.core.data_type.in_flight_order import PerpetualDerivativeInFlightOrder from hummingbot.core.data_type.perpetual_api_order_book_data_source import PerpetualAPIOrderBookDataSource @@ -33,6 +33,7 @@ def __init__(self, self._perpetual_trading = PerpetualTrading(self.trading_pairs) self._funding_info_listener_task: Optional[asyncio.Task] = None + self._mark_price_listener_task: Optional[asyncio.Task] = None self._funding_fee_polling_task: Optional[asyncio.Task] = None self._funding_fee_poll_notifier = asyncio.Event() self._orderbook_ds: PerpetualAPIOrderBookDataSource = self._orderbook_ds # for type-hinting @@ -95,6 +96,9 @@ async def start_network(self): await super().start_network() self._perpetual_trading.start() self._funding_info_listener_task = safe_ensure_future(self._listen_for_funding_info()) + # Start separate mark price listener if supported by data source + if hasattr(self._orderbook_ds, 'listen_for_mark_price'): + self._mark_price_listener_task = safe_ensure_future(self._listen_for_mark_price()) if self.is_trading_required: self._funding_fee_polling_task = safe_ensure_future(self._funding_payment_polling_loop()) @@ -207,6 +211,9 @@ async def stop_network(self): if self._funding_info_listener_task is not None: self._funding_info_listener_task.cancel() self._funding_info_listener_task = None + if self._mark_price_listener_task is not None: + self._mark_price_listener_task.cancel() + self._mark_price_listener_task = None self._last_funding_fee_payment_ts.clear() await super().stop_network() @@ -294,6 +301,18 @@ def _get_fee( ) -> TradeFeeBase: raise NotImplementedError + def get_price_by_type(self, trading_pair: str, price_type: PriceType) -> Decimal: + """ + Override to add MarkPrice support for perpetual derivatives. + For MarkPrice, returns the mark price from funding info. + For other price types, delegates to parent implementation. + """ + if price_type is PriceType.MarkPrice: + funding_info = self.get_funding_info(trading_pair) + return funding_info.mark_price + # Delegate to parent implementation for other price types + return super().get_price_by_type(trading_pair, price_type) + async def _status_polling_loop_fetch_updates(self): await safe_gather( self._update_positions(), @@ -365,6 +384,15 @@ async def _listen_for_funding_info(self): output=self._perpetual_trading.funding_info_stream ) + async def _listen_for_mark_price(self): + """ + Listen for mark price updates via WebSocket separately from funding rate. + Mark price updates are pushed to the same funding_info_stream but processed independently. + """ + await self._orderbook_ds.listen_for_mark_price( + output=self._perpetual_trading.funding_info_stream + ) + async def _init_funding_info(self): for trading_pair in self.trading_pairs: funding_info = await self._orderbook_ds.get_funding_info(trading_pair) diff --git a/hummingbot/connector/perpetual_trading.py b/hummingbot/connector/perpetual_trading.py index 8a0127b2035..260ad739253 100644 --- a/hummingbot/connector/perpetual_trading.py +++ b/hummingbot/connector/perpetual_trading.py @@ -172,6 +172,17 @@ async def _funding_info_updater(self): try: funding_info_message: FundingInfoUpdate = await self._funding_info_stream.get() trading_pair = funding_info_message.trading_pair + + # Check if funding info has been initialized for this trading pair + if trading_pair not in self._funding_info: + # Funding info not yet initialized - skip this update + # It will be initialized by _init_funding_info() and subsequent updates will be processed + self.logger().debug( + f"Funding info for {trading_pair} not yet initialized, skipping update. " + f"Will be initialized shortly." + ) + continue + funding_info = self._funding_info[trading_pair] funding_info.update(funding_info_message) except asyncio.CancelledError: diff --git a/hummingbot/core/data_type/common.py b/hummingbot/core/data_type/common.py index 21286b4ebe0..10fa173f3a1 100644 --- a/hummingbot/core/data_type/common.py +++ b/hummingbot/core/data_type/common.py @@ -55,6 +55,7 @@ class PriceType(Enum): LastOwnTrade = 5 InventoryCost = 6 Custom = 7 + MarkPrice = 8 class TradeType(Enum): diff --git a/hummingbot/core/data_type/perpetual_api_order_book_data_source.py b/hummingbot/core/data_type/perpetual_api_order_book_data_source.py index 1683bf9b4be..ed36b5f6d3a 100644 --- a/hummingbot/core/data_type/perpetual_api_order_book_data_source.py +++ b/hummingbot/core/data_type/perpetual_api_order_book_data_source.py @@ -37,9 +37,13 @@ async def _parse_funding_info_message(self, raw_message: Dict[str, Any], message raise NotImplementedError def _get_messages_queue_keys(self) -> List[str]: - return [ + keys = [ self._snapshot_messages_queue_key, self._diff_messages_queue_key, self._trade_messages_queue_key, self._funding_info_messages_queue_key, ] + # Add mark_price queue key if it exists (for connectors that support it) + if hasattr(self, '_mark_price_messages_queue_key'): + keys.append(self._mark_price_messages_queue_key) + return keys diff --git a/scripts/kodiak/pmm_avellaneda_algo.md b/scripts/kodiak/pmm_avellaneda_algo.md new file mode 100644 index 00000000000..3ee10b37765 --- /dev/null +++ b/scripts/kodiak/pmm_avellaneda_algo.md @@ -0,0 +1,107 @@ +Market Making Strategies for Crypto Perpetual Futures with 1-Second Updates +Introduction +A 1-second order update latency presents a fundamental challenge for market making in cryptocurrency perpetual futures. While high-frequency traders operate in microseconds, profitable market making remains viable at this speed through defensive positioning, wider spreads, and intelligent risk management. CNBC This guide provides practical, implementable strategies specifically designed for the 1-second constraint on single pairs like BTC-USD or ETH-USD. +The core insight: latency functions as "time to expiry" for an option. When you place quotes, the market can execute against you during the update window—creating risk proportional to the square root of the delay time. Headlandstech A 1-second latency means you're effectively writing an option that traders have one second to exercise, requiring spreads 3-5x wider than high-frequency competitors to remain profitable. +1. Simple foundational algorithm for 1-second updates +The foundational strategy combines the Avellaneda-Stoikov model for mathematical rigor with practical adaptations for crypto perpetual futures and latency constraints. Stack Exchange +Core algorithm components +Reservation price calculation establishes your fair value adjusted for inventory risk: +r = s - q·γ·σ²·τ + +Where: +r = reservation price (your fair value) +s = current mid-price +q = inventory position (+ for long, - for short) +γ = risk aversion parameter (typically 5-10 for crypto) +σ = annualized volatility (40-60% for BTC, 50-80% for ETH) +τ = effective risk horizon (use 1-4 hours for perpetuals) +Stack Exchange +The inventory term creates asymmetric pricing: if you accumulate long inventory (q > 0), your reservation price shifts below market mid-price, making your offers more attractive and bids less attractive to naturally revert toward neutral. Stack ExchangeResearchGate +Optimal spread calculation balances fill probability against profit per fill: +spread = γ·σ²·τ + (2/γ)·ln(1 + γ/κ) + latency_buffer + +Where: +κ = order arrival intensity (50-200 for liquid crypto pairs) +latency_buffer = σ·√(latency_seconds)·safety_factor + +For 1-second latency with BTC at 50% annualized volatility: +latency_buffer = 0.50·√(1)·1.5 ≈ 0.75% or 75 basis points +Stack Exchange +This latency buffer is critical—it compensates for the option-like risk that prices move against you during the update cycle. Headlandstech The safety factor of 1.5 provides cushion beyond the expected price movement. +Quote placement around your reservation price: +bid_price = r - (spread/2) +ask_price = r + (spread/2) + +But enforce minimum spread: +actual_spread = max(calculated_spread, min_spread) + +Where min_spread ensures profitability after fees: +min_spread = 2×max(maker_fee, taker_fee) + target_profit +For Binance (0.02% maker): min_spread ≥ 0.05% (5 basis points) +Stack Exchange +Order sizing with inventory awareness +Dynamic sizing encourages mean reversion: +pythondef calculate_order_sizes(base_size, inventory, target_inventory, max_inventory): + deviation = (inventory - target_inventory) / max_inventory + eta = -0.01 # Shape parameter + + if deviation > 0: # Long inventory + bid_size = base_size * exp(eta * deviation) # Reduce bids + ask_size = base_size # Full asks to sell + else: # Short inventory + bid_size = base_size # Full bids to buy + ask_size = base_size * exp(-eta * deviation) # Reduce asks + + return bid_size, ask_size +stanford +Update cycle for 1-second constraint +Implement a batch update every 1 second rather than continuous adjustments: +pythondef market_making_cycle(): + # 1. Gather current state (WebSocket data, <50ms) + mid_price = get_current_mid_price() + inventory = get_current_position() + volatility = calculate_rolling_volatility(window=200) + + # 2. Check if should quote (defensive filtering) + if not should_provide_liquidity(volatility, ROC_indicator, order_imbalance): + cancel_all_orders() # Takes ~500-1000ms + return + + # 3. Calculate new quotes + reservation_price = calculate_reservation_price(mid_price, inventory, volatility) + spread = calculate_optimal_spread(volatility, inventory, latency=1.0) + bid_size, ask_size = calculate_order_sizes(BASE_SIZE, inventory) + + # 4. Replace orders (single batched API call, ~500-1000ms) + new_bid = reservation_price - spread/2 + new_ask = reservation_price + spread/2 + + replace_orders_atomic( + cancel_orders=[existing_bid_id, existing_ask_id], + new_orders=[ + {'side': 'buy', 'price': new_bid, 'size': bid_size}, + {'side': 'sell', 'price': new_ask, 'size': ask_size} + ] + ) +Parameter starting points for BTC/ETH perpetuals +For Bitcoin (BTC-USD or BTC-USDT): + +γ (risk aversion): 5-8 +σ (volatility): 0.45 (45% annualized, adjust from recent data) +τ (horizon): 2 hours = 2/8760 years +κ (liquidity): 100-200 +Min spread: 0.05% (5 bps) +Base size: 0.01-0.05 BTC depending on capital +Max inventory: ±0.2 BTC (or ±2-5% of capital) + +For Ethereum (ETH-USD or ETH-USDT): + +γ: 5-10 (higher due to greater volatility) +σ: 0.60 (60% annualized) +τ: 2 hours +κ: 80-150 +Min spread: 0.06% (6 bps) +Base size: 0.1-0.5 ETH +Max inventory: ±2 ETH (or ±2-5% of capital) + +These parameters produce spreads of 8-15 basis points for BTC and 10-20 basis points for ETH, which are 3-5x wider than HFT market makers but remain competitive for retail and mid-sized institutional flow. \ No newline at end of file diff --git a/scripts/pmm_avellaneda.py b/scripts/pmm_avellaneda.py new file mode 100644 index 00000000000..70270d7b894 --- /dev/null +++ b/scripts/pmm_avellaneda.py @@ -0,0 +1,447 @@ +import logging +import os +from decimal import Decimal +from typing import Dict, List, Optional + +import pandas as pd +from pydantic import Field + +from hummingbot.client.config.config_data_types import BaseClientModel +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, PositionAction, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder +from hummingbot.core.data_type.order_candidate import PerpetualOrderCandidate +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketOrderFailureEvent, + OrderCancelledEvent, + OrderFilledEvent, + SellOrderCompletedEvent, + SellOrderCreatedEvent, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase +from hummingbot.strategy_v2.models.executors import TrackedOrder + + +class PMMAvellanedaConfig(BaseClientModel): + script_file_name: str = os.path.basename(__file__) + exchange: str = Field("orderly_perpetual") + trading_pair: str = Field("BTC-USDC") + order_amount_quote: Decimal = Field(20) + risk_aversion_gamma: Decimal = Field(6.0) + volatility_sigma: Decimal = Field(0.50) + risk_horizon_tau_hours: Decimal = Field(2.0) + bid_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + ask_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + order_refresh_time: int = Field(10) + max_inventory: Decimal = Field(0.01) # 1k usd + leverage: int = Field(10) + + # target_inventory: Decimal = Field(0.0) + + # for getting candles data for annualized volatility + # candles_exchange: str = Field(default="binance_perpetual") + # candles_pair: str = Field(default="BTC-USDT") + # candles_interval: str = Field(default="1d") + # candles_length: int = Field(default=365, gt=0) + +# Add Mark price as reference + +class PMMAvellaneda(ScriptStrategyBase): + """ + Avellaneda-Stoikov Pure Market Making Strategy (Phase 1) + + This strategy implements the reservation price calculation from the Avellaneda-Stoikov model + for perpetual futures market making. It places multiple order levels around the reservation + price with constant spreads. + + Phase 1 features: + - Reservation price calculation based on inventory + - Multiple constant spread levels + - Inventory management and tracking + - Improved order lifecycle management using TrackedOrder pattern + """ + + create_timestamp = 0 + account_config_set = False + + @classmethod + def init_markets(cls, config: PMMAvellanedaConfig): + cls.markets = {config.exchange: {config.trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase], config: PMMAvellanedaConfig): + super().__init__(connectors) + self.config = config + self._current_inventory: Decimal = Decimal("0") + self._last_update_timestamp: float = 0 + self._cached_mid_price: Decimal = Decimal("0") + self._cached_reservation_price: Decimal = Decimal("0") + + # Order tracking using TrackedOrder pattern + self._tracked_orders: Dict[str, TrackedOrder] = {} + # Track orders by spread level: {spread_index: {"buy": order_id, "sell": order_id}} + self._spread_level_orders: Dict[int, Dict[str, Optional[str]]] = {} + + # DataFrame to store last 6 filled orders + self._filled_orders_df: pd.DataFrame = pd.DataFrame(columns=[ + "Timestamp", "Order ID", "Side", "Amount", "Price", "Inventory" + ]) + + def _get_in_flight_order(self, order_id: str) -> Optional[InFlightOrder]: + """Get InFlightOrder from connector's order tracker""" + connector = self.connectors[self.config.exchange] + return connector._order_tracker.fetch_order(client_order_id=order_id) + + def _update_tracked_order(self, order_id: str): + """Update TrackedOrder with current InFlightOrder state""" + in_flight_order = self._get_in_flight_order(order_id) + if in_flight_order and order_id in self._tracked_orders: + self._tracked_orders[order_id].order = in_flight_order + + # Built-in event handler methods (called automatically by ScriptStrategyBase) + + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + """Called automatically by framework when buy order is created""" + if event.order_id not in self._tracked_orders: + self._tracked_orders[event.order_id] = TrackedOrder(order_id=event.order_id) + self._update_tracked_order(event.order_id) + self.logger().debug(f"Buy order created: {event.order_id}") + + def did_create_sell_order(self, event: SellOrderCreatedEvent): + """Called automatically by framework when sell order is created""" + if event.order_id not in self._tracked_orders: + self._tracked_orders[event.order_id] = TrackedOrder(order_id=event.order_id) + self._update_tracked_order(event.order_id) + self.logger().debug(f"Sell order created: {event.order_id}") + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp: + self.cancel_all_orders() + proposal: List[PerpetualOrderCandidate] = self.create_proposal() + proposal_adjusted: List[PerpetualOrderCandidate] = self.adjust_proposal_to_budget(proposal) + self.place_orders(proposal_adjusted) + self.create_timestamp = self.config.order_refresh_time + self.current_timestamp + + def apply_initial_setting(self): + if not self.account_config_set: + connector = self.connectors[self.config.exchange] + connector.set_leverage(self.config.trading_pair, self.config.leverage) + self.account_config_set = True + + def create_proposal(self) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + mid_price = connector.get_price_by_type(self.config.trading_pair, PriceType.MidPrice) + inventory = self._get_current_inventory() + + reservation_price = self._calculate_reservation_price(mid_price, inventory) + + # Cache values for status reporting + self._cached_mid_price = mid_price + self._cached_reservation_price = reservation_price + + orders = [] + for idx, bid_spread in enumerate(self.config.bid_spread_levels): + ask_spread = self.config.ask_spread_levels[idx] + bid_price = reservation_price * (Decimal("1") - bid_spread) + ask_price = reservation_price * (Decimal("1") + ask_spread) + + # Convert quote amount to base amount for both buy and sell orders + # For perpetual orders, amount must be in base currency (BTC), not quote (USDC) + bid_amount = Decimal(self.config.order_amount_quote) / bid_price + ask_amount = Decimal(self.config.order_amount_quote) / ask_price + + bid_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.BUY, + amount=bid_amount, + price=bid_price, + leverage=Decimal(self.config.leverage) + ) + + ask_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT, + order_side=TradeType.SELL, + amount=ask_amount, + price=ask_price, + leverage=Decimal(self.config.leverage) + ) + + if self._current_inventory >= self.config.max_inventory: + orders.extend([bid_order]) + elif self._current_inventory <= -self.config.max_inventory: + orders.extend([ask_order]) + else: + orders.extend([bid_order, ask_order]) + return orders + + def adjust_proposal_to_budget(self, proposal: List[PerpetualOrderCandidate]) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + budget_checker = connector.budget_checker + + # Debug: Check balance before adjustment + self.logger().info(f"Available balance (USDC): {connector.get_available_balance('USDC')}") + self.logger().info(f"Total balance (USDC): {connector.get_balance('USDC')}") + + # Debug: Populate collateral for first order to see what happens + if proposal: + test_order = proposal[0] + populated = budget_checker.populate_collateral_entries(test_order) + self.logger().info(f"After populate_collateral_entries:") + self.logger().info(f" order_collateral: {populated.order_collateral}") + self.logger().info(f" percent_fee_collateral: {populated.percent_fee_collateral}") + self.logger().info(f" amount: {populated.amount}") + self.logger().info(f" leverage: {populated.leverage}") + self.logger().info(f" position_close: {populated.position_close}") + + proposal_adjusted = budget_checker.adjust_candidates(proposal, all_or_none=True) + + # Debug: Check what happened after adjustment + if proposal_adjusted: + first_adjusted = proposal_adjusted[0] + self.logger().info(f"After adjust_candidates:") + self.logger().info(f" order_collateral: {first_adjusted.order_collateral}") + self.logger().info(f" amount: {first_adjusted.amount}") + self.logger().info(f" resized: {first_adjusted.resized}") + + return proposal_adjusted + + def place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + for order in proposal: + self.place_order(connector_name=self.config.exchange, order=order) + + def place_order(self, connector_name: str, order: PerpetualOrderCandidate): + """Place an order and immediately track it""" + client_order_id = None + if order.order_side == TradeType.SELL: + client_order_id = self.sell( + connector_name=connector_name, + trading_pair=order.trading_pair, + amount=order.amount, + order_type=order.order_type, + price=order.price, + position_action=PositionAction.OPEN + ) + elif order.order_side == TradeType.BUY: + client_order_id = self.buy( + connector_name=connector_name, + trading_pair=order.trading_pair, + amount=order.amount, + order_type=order.order_type, + price=order.price, + position_action=PositionAction.OPEN + ) + + # Immediately track the order + if client_order_id: + tracked_order = TrackedOrder(order_id=client_order_id) + self._tracked_orders[client_order_id] = tracked_order + # Update immediately if available + self._update_tracked_order(client_order_id) + self.logger().debug(f"Tracking order {client_order_id} for {order.order_side.name}") + + def cancel_all_orders(self): + """Cancel all active orders, checking if they're actually open first.""" + connector = self.connectors[self.config.exchange] + active_orders = self.get_active_orders(connector_name=self.config.exchange) + + orders_cancelled = 0 + orders_skipped = 0 + + for limit_order in active_orders: + # Get InFlightOrder to check actual state + in_flight_order = self._get_in_flight_order(limit_order.client_order_id) + # add filtering for current trading pair only + # Only cancel if order is actually still open + if in_flight_order and in_flight_order.is_open: + try: + self.cancel(self.config.exchange, limit_order.trading_pair, limit_order.client_order_id) + orders_cancelled += 1 + except Exception as e: + # Handle "order already completed" errors gracefully + error_str = str(e).lower() + if any(keyword in error_str for keyword in ["completed", "filled", "not found"]): + self.logger().debug( + f"Order {limit_order.client_order_id} already completed, skipping cancellation" + ) + orders_skipped += 1 + else: + # Re-raise unexpected errors + self.logger().error(f"Failed to cancel order {limit_order.client_order_id}: {e}") + raise + else: + # Order is already done (filled/cancelled/failed), skip it + if in_flight_order: + self.logger().debug( + f"Order {limit_order.client_order_id} is {in_flight_order.current_state.name}, " + f"skipping cancellation" + ) + orders_skipped += 1 + + if orders_cancelled > 0 or orders_skipped > 0: + self.logger().info(f"Cancellation: {orders_cancelled} cancelled, {orders_skipped} skipped (already done)") + + def did_fill_order(self, event: OrderFilledEvent): + """ + Called automatically by framework when order is filled. + Update tracked order state and inventory position based on order fills. + For perpetual futures: + - BUY fill increases inventory (long position) + - SELL fill decreases inventory (short position) + """ + # Update tracked order state + self._update_tracked_order(event.order_id) + + # Update inventory based on fill direction + if event.trade_type == TradeType.BUY: + # BUY fill = we bought, inventory increases (long position) + self._current_inventory += event.amount + elif event.trade_type == TradeType.SELL: + # SELL fill = we sold, inventory decreases (short position) + self._current_inventory -= event.amount + + # Add fill to DataFrame + try: + timestamp = pd.Timestamp.fromtimestamp(event.timestamp) if event.timestamp else pd.Timestamp.now() + except (ValueError, OSError, TypeError): + timestamp = pd.Timestamp.now() + + new_row = pd.DataFrame([{ + "Timestamp": timestamp, + "Order ID": event.order_id[:8] + "..." if len(event.order_id) > 8 else event.order_id, + "Side": event.trade_type.name, + "Amount": float(event.amount), + "Price": float(event.price), + "Inventory": float(self._current_inventory) + }]) + self._filled_orders_df = pd.concat([self._filled_orders_df, new_row], ignore_index=True) + + # Keep only last 6 fills + if len(self._filled_orders_df) > 6: + self._filled_orders_df = self._filled_orders_df.tail(6).reset_index(drop=True) + + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} " + f"{self.config.exchange} at {round(event.price, 2)} | Inventory: {self._current_inventory:.8f}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent): + """Called automatically by framework when buy order is completed""" + self._update_tracked_order(event.order_id) + if event.order_id in self._tracked_orders: + tracked = self._tracked_orders[event.order_id] + if tracked.is_done: + self.logger().info(f"Buy order {event.order_id} completed (filled)") + + def did_complete_sell_order(self, event: SellOrderCompletedEvent): + """Called automatically by framework when sell order is completed""" + self._update_tracked_order(event.order_id) + if event.order_id in self._tracked_orders: + tracked = self._tracked_orders[event.order_id] + if tracked.is_done: + self.logger().info(f"Sell order {event.order_id} completed (filled)") + + def did_cancel_order(self, event: OrderCancelledEvent): + """Called automatically by framework when order is cancelled""" + self._update_tracked_order(event.order_id) + self.logger().debug(f"Order cancelled: {event.order_id}") + + def did_fail_order(self, event: MarketOrderFailureEvent): + """Called automatically by framework when order fails""" + self._update_tracked_order(event.order_id) + self.logger().warning(f"Order {event.order_id} failed: {event}") + + def _get_current_inventory(self) -> Decimal: + """ + Get current inventory position tracked internally from order fills. + Returns signed position amount: positive for long, negative for short. + + Position is tracked by accumulating fills: + - BUY fills increase inventory (long position) + - SELL fills decrease inventory (short position) + """ + return self._current_inventory + + def _calculate_reservation_price(self, mid_price: Decimal, inventory: Decimal) -> Decimal: + """ + Calculate reservation price using Avellaneda-Stoikov formula: + r = s - q·γ·σ²·τ + + Where: + - s = mid_price + - q = inventory (signed, positive for long, negative for short) + - γ = risk_aversion_gamma + - σ = volatility_sigma (annualized) + - τ = risk_horizon_tau_hours / 8760 (convert hours to years) + """ + # Convert tau from hours to years + tau_years = self.config.risk_horizon_tau_hours / Decimal("8760") + + # Calculate inventory adjustment: q·γ·σ²·τ + inventory_adjustment = inventory * self.config.risk_aversion_gamma * \ + (self.config.volatility_sigma ** Decimal("2")) * tau_years + + # Reservation price: r = s - q·γ·σ²·τ + reservation_price = mid_price - inventory_adjustment + + return reservation_price + + def format_status(self) -> str: + """ + Return status string showing current strategy state. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + + # Use cached values instead of making connector calls + mid_price = self._cached_mid_price + reservation_price = self._cached_reservation_price + inventory = self._current_inventory + + lines = [] + lines.append("") + lines.append(" Strategy Status:") + lines.append(f" Trading Pair: {self.config.trading_pair}") + lines.append(f" Exchange: {self.config.exchange}") + lines.append(f" Mid Price: {mid_price:.8f}") + lines.append(f" Reservation Price: {reservation_price:.8f}") + lines.append(f" Price Adjustment: {reservation_price - mid_price:.8f}") + lines.append(f" Current Inventory: {inventory:.8f}") + lines.append(f" Max Inventory: {self.config.max_inventory:.8f}") + lines.append(f" Spread Levels: {[f'{s*100:.4f}%' for s in self.config.ask_spread_levels]}") + lines.append(f" Bid Spread Levels: {[f'{s*100:.4f}%' for s in self.config.bid_spread_levels]}") + lines.append(f" Tracked Orders: {len(self._tracked_orders)}") + + # Show order states + if self._tracked_orders: + open_count = sum(1 for tracked in self._tracked_orders.values() if tracked.is_open) + done_count = sum(1 for tracked in self._tracked_orders.values() if tracked.is_done) + lines.append(f" Order States: {open_count} open, {done_count} done") + + try: + df = self.active_orders_df() + lines.append("") + lines.append(" Active Orders:") + lines.extend([" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.append("") + lines.append(" No active orders.") + + # Display last 6 filled orders (most recent last) + if len(self._filled_orders_df) > 0: + lines.append("") + lines.append(" Last 6 Filled Orders:") + # Display in reverse order so most recent appears at bottom + filled_df_display = self._filled_orders_df.iloc[::-1].copy() + # Format timestamp for display + filled_df_display["Timestamp"] = filled_df_display["Timestamp"].dt.strftime("%H:%M:%S") + lines.extend([" " + line for line in filled_df_display.to_string(index=False).split("\n")]) + else: + lines.append("") + lines.append(" No filled orders yet.") + + return "\n".join(lines) \ No newline at end of file diff --git a/scripts/pmm_avellaneda_batch.py b/scripts/pmm_avellaneda_batch.py new file mode 100644 index 00000000000..d518c3dfd0b --- /dev/null +++ b/scripts/pmm_avellaneda_batch.py @@ -0,0 +1,349 @@ +import logging +import os +from decimal import Decimal +from typing import Dict, List, Optional + +import pandas as pd +from pydantic import Field + +from hummingbot.client.config.config_data_types import BaseClientModel +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, PositionAction, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder +from hummingbot.core.data_type.order_candidate import PerpetualOrderCandidate +from hummingbot.core.event.events import ( + OrderFilledEvent, +) +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class PMMAvellanedaMultiConfig(BaseClientModel): + script_file_name: str = os.path.basename(__file__) + exchange: str = Field("orderly_perpetual") + trading_pair: str = Field("BTC-USDC") + order_amount_quote: Decimal = Field(20) + risk_aversion_gamma: Decimal = Field(6.0) + volatility_sigma: Decimal = Field(0.50) + risk_horizon_tau_hours: Decimal = Field(2.0) + bid_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + ask_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + order_refresh_time: int = Field(10) + max_inventory: Decimal = Field(0.01) # 1k usd + leverage: int = Field(10) + + # target_inventory: Decimal = Field(0.0) + + # for getting candles data for annualized volatility + # candles_exchange: str = Field(default="binance_perpetual") + # candles_pair: str = Field(default="BTC-USDT") + # candles_interval: str = Field(default="1d") + # candles_length: int = Field(default=365, gt=0) + +# Add Mark price as reference + +class PMMAvellanedaMulti(ScriptStrategyBase): + """ + Avellaneda-Stoikov Pure Market Making Strategy with Batch Order Operations + + This strategy implements the reservation price calculation from the Avellaneda-Stoikov model + for perpetual futures market making. It places multiple order levels around the reservation + price with constant spreads. + + Key features: + - Reservation price calculation based on inventory + - Multiple constant spread levels + - Inventory management and tracking + - Order lifecycle management via connector's order tracker + - Batch order placement and cancellation for improved efficiency + """ + + create_timestamp = 0 + account_config_set = False + + @classmethod + def init_markets(cls, config: PMMAvellanedaMultiConfig): + cls.markets = {config.exchange: {config.trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase], config: PMMAvellanedaMultiConfig): + super().__init__(connectors) + self.config = config + self._current_inventory: Decimal = Decimal("0") + self._last_update_timestamp: float = 0 + self._cached_mid_price: Decimal = Decimal("0") + self._cached_reservation_price: Decimal = Decimal("0") + + # DataFrame to store last 6 filled orders + self._filled_orders_df: pd.DataFrame = pd.DataFrame(columns=[ + "Timestamp", "Order ID", "Side", "Amount", "Price", "Inventory" + ]) + + def _get_in_flight_order(self, order_id: str) -> Optional[InFlightOrder]: + """Get InFlightOrder from connector's order tracker""" + connector = self.connectors[self.config.exchange] + return connector._order_tracker.fetch_order(client_order_id=order_id) + + + # Built-in event handler methods (called automatically by ScriptStrategyBase) + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp: + proposals: List[PerpetualOrderCandidate] = self.create_proposal() + proposal_adjusted: List[PerpetualOrderCandidate] = self.adjust_proposal_to_budget(proposals) + # Execute cancel then place sequentially to avoid order accumulation + safe_ensure_future(self._cancel_and_place_orders(proposal_adjusted)) + self.create_timestamp = self.config.order_refresh_time + self.current_timestamp + + async def _cancel_and_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """ + Cancel all active orders and then place new orders sequentially. + This ensures old orders are cancelled before new ones are placed. + """ + # First, cancel all active orders and wait for completion + await self._async_cancel_all_orders() + # Then place new orders + await self._async_place_orders(proposal) + + + def apply_initial_setting(self): + if not self.account_config_set: + connector = self.connectors[self.config.exchange] + connector.set_leverage(self.config.trading_pair, self.config.leverage) + self.account_config_set = True + + def create_proposal(self) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + mid_price = connector.get_price_by_type(self.config.trading_pair, PriceType.MidPrice) + inventory = self._get_current_inventory() + + reservation_price = self._calculate_reservation_price(mid_price, inventory) + + # Cache values for status reporting + self._cached_mid_price = mid_price + self._cached_reservation_price = reservation_price + + orders = [] + for idx, bid_spread in enumerate(self.config.bid_spread_levels): + ask_spread = self.config.ask_spread_levels[idx] + bid_price = reservation_price * (Decimal("1") - bid_spread) + ask_price = reservation_price * (Decimal("1") + ask_spread) + + # Convert quote amount to base amount for both buy and sell orders + # For perpetual orders, amount must be in base currency (BTC), not quote (USDC) + bid_amount = Decimal(self.config.order_amount_quote) / bid_price + ask_amount = Decimal(self.config.order_amount_quote) / ask_price + + bid_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.BUY, + amount=bid_amount, + price=bid_price, + leverage=Decimal(self.config.leverage) + ) + + ask_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.SELL, + amount=ask_amount, + price=ask_price, + leverage=Decimal(self.config.leverage) + ) + + if self._current_inventory >= self.config.max_inventory: + orders.extend([bid_order]) + elif self._current_inventory <= -self.config.max_inventory: + orders.extend([ask_order]) + else: + orders.extend([bid_order, ask_order]) + return orders + + def adjust_proposal_to_budget(self, proposals: List[PerpetualOrderCandidate]) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + budget_checker = connector.budget_checker + + proposals_adjusted = budget_checker.adjust_candidates(proposals, all_or_none=True) + return proposals_adjusted + + async def _async_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """Place multiple orders using batch API and wait for completion""" + if not proposal: + return + + connector = self.connectors[self.config.exchange] + + # Convert PerpetualOrderCandidate objects to order dictionaries for batch_order_create + orders_to_create = [] + for order in proposal: + order_dict = { + "trading_pair": order.trading_pair, + "amount": order.amount, + "trade_type": order.order_side, + "order_type": order.order_type, + "price": order.price, + "position_action": PositionAction.OPEN + } + orders_to_create.append(order_dict) + + # Call batch_order_create and wait for completion + await connector.batch_order_create(orders_to_create) + + async def _async_cancel_all_orders(self): + """Cancel all active orders using batch API and wait for completion""" + connector = self.connectors[self.config.exchange] + + # Get orders directly from connector's order tracker instead of strategy's order tracker + # This ensures we get the most up-to-date list including orders just placed + all_in_flight_orders = connector._order_tracker.active_orders + + # Collect InFlightOrder objects for orders to cancel + orders_to_cancel = [] + orders_skipped = 0 + + for in_flight_order in all_in_flight_orders.values(): + # Filter by current trading pair only + if in_flight_order.trading_pair != self.config.trading_pair: + continue + + # Check if order is actually still open + if in_flight_order.is_open: + orders_to_cancel.append(in_flight_order) + else: + # Order is already done (filled/cancelled/failed), skip it + self.logger().debug( + f"Order {in_flight_order.client_order_id} is {in_flight_order.current_state.name}, " + f"skipping cancellation" + ) + orders_skipped += 1 + + # Use batch cancellation if we have orders to cancel and wait for completion + if orders_to_cancel: + await connector.batch_order_cancel(orders_to_cancel) + + def did_fill_order(self, event: OrderFilledEvent): + """ + Called automatically by framework when order is filled. + Update tracked order state and inventory position based on order fills. + For perpetual futures: + - BUY fill increases inventory (long position) + - SELL fill decreases inventory (short position) + """ + # Update inventory based on fill direction + if event.trade_type == TradeType.BUY: + # BUY fill = we bought, inventory increases (long position) + self._current_inventory += event.amount + elif event.trade_type == TradeType.SELL: + # SELL fill = we sold, inventory decreases (short position) + self._current_inventory -= event.amount + + # Add fill to DataFrame + try: + timestamp = pd.Timestamp.fromtimestamp(event.timestamp) if event.timestamp else pd.Timestamp.now() + except (ValueError, OSError, TypeError): + timestamp = pd.Timestamp.now() + + new_row = pd.DataFrame([{ + "Timestamp": timestamp, + "Order ID": event.order_id[:8] + "..." if len(event.order_id) > 8 else event.order_id, + "Side": event.trade_type.name, + "Amount": float(event.amount), + "Price": float(event.price), + "Inventory": float(self._current_inventory) + }]) + self._filled_orders_df = pd.concat([self._filled_orders_df, new_row], ignore_index=True) + + # Keep only last 6 fills + if len(self._filled_orders_df) > 6: + self._filled_orders_df = self._filled_orders_df.tail(6).reset_index(drop=True) + + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} " + f"{self.config.exchange} at {round(event.price, 2)} | Inventory: {self._current_inventory:.8f}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def _get_current_inventory(self) -> Decimal: + """ + Get current inventory position tracked internally from order fills. + Returns signed position amount: positive for long, negative for short. + + Position is tracked by accumulating fills: + - BUY fills increase inventory (long position) + - SELL fills decrease inventory (short position) + """ + return self._current_inventory + + def _calculate_reservation_price(self, mid_price: Decimal, inventory: Decimal) -> Decimal: + """ + Calculate reservation price using Avellaneda-Stoikov formula: + r = s - q·γ·σ²·τ + + Where: + - s = mid_price + - q = inventory (signed, positive for long, negative for short) + - γ = risk_aversion_gamma + - σ = volatility_sigma (annualized) + - τ = risk_horizon_tau_hours / 8760 (convert hours to years) + """ + # Convert tau from hours to years + tau_years = self.config.risk_horizon_tau_hours / Decimal("8760") + + # Calculate inventory adjustment: q·γ·σ²·τ + inventory_adjustment = inventory * self.config.risk_aversion_gamma * \ + (self.config.volatility_sigma ** Decimal("2")) * tau_years + + # Reservation price: r = s - q·γ·σ²·τ + reservation_price = mid_price - inventory_adjustment + + return reservation_price + + def format_status(self) -> str: + """ + Return status string showing current strategy state. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + + # Use cached values instead of making connector calls + mid_price = self._cached_mid_price + reservation_price = self._cached_reservation_price + inventory = self._current_inventory + + lines = [] + lines.append("") + lines.append(" Strategy Status:") + lines.append(f" Trading Pair: {self.config.trading_pair}") + lines.append(f" Exchange: {self.config.exchange}") + lines.append(f" Mid Price: {mid_price:.8f}") + lines.append(f" Reservation Price: {reservation_price:.8f}") + lines.append(f" Price Adjustment: {reservation_price - mid_price:.8f}") + lines.append(f" Current Inventory: {inventory:.8f}") + lines.append(f" Max Inventory: {self.config.max_inventory:.8f}") + lines.append(f" Spread Levels: {[f'{s*100:.4f}%' for s in self.config.ask_spread_levels]}") + lines.append(f" Bid Spread Levels: {[f'{s*100:.4f}%' for s in self.config.bid_spread_levels]}") + + try: + df = self.active_orders_df() + lines.append("") + lines.append(" Active Orders:") + lines.extend([" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.append("") + lines.append(" No active orders.") + + # Display last 6 filled orders (most recent last) + if len(self._filled_orders_df) > 0: + lines.append("") + lines.append(" Last 6 Filled Orders:") + # Display in reverse order so most recent appears at bottom + filled_df_display = self._filled_orders_df.iloc[::-1].copy() + # Format timestamp for display + filled_df_display["Timestamp"] = filled_df_display["Timestamp"].dt.strftime("%H:%M:%S") + lines.extend([" " + line for line in filled_df_display.to_string(index=False).split("\n")]) + else: + lines.append("") + lines.append(" No filled orders yet.") + + return "\n".join(lines) \ No newline at end of file diff --git a/scripts/pmm_avellaneda_batch_markprice.py b/scripts/pmm_avellaneda_batch_markprice.py new file mode 100644 index 00000000000..29ec8d2dac0 --- /dev/null +++ b/scripts/pmm_avellaneda_batch_markprice.py @@ -0,0 +1,365 @@ +import logging +import os +from decimal import Decimal +from typing import Dict, List, Optional + +import pandas as pd +from pydantic import Field + +from hummingbot.client.config.config_data_types import BaseClientModel +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, PositionAction, PositionSide, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder +from hummingbot.core.data_type.order_candidate import PerpetualOrderCandidate +from hummingbot.core.event.events import ( + OrderFilledEvent, +) +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class PMMAvellanedaMultiConfig(BaseClientModel): + script_file_name: str = os.path.basename(__file__) + exchange: str = Field("orderly_perpetual") + trading_pair: str = Field("BTC-USDC") + order_amount_quote: Decimal = Field(20) + risk_aversion_gamma: Decimal = Field(6.0) + volatility_sigma: Decimal = Field(0.50) + risk_horizon_tau_hours: Decimal = Field(2.0) + bid_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + ask_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + order_refresh_time: int = Field(10) + max_inventory: Decimal = Field(0.01) # 1k usd + leverage: int = Field(10) + + # target_inventory: Decimal = Field(0.0) + + # for getting candles data for annualized volatility + # candles_exchange: str = Field(default="binance_perpetual") + # candles_pair: str = Field(default="BTC-USDT") + # candles_interval: str = Field(default="1d") + # candles_length: int = Field(default=365, gt=0) + +# Add Mark price as reference + +class PMMAvellanedaMulti(ScriptStrategyBase): + """ + Avellaneda-Stoikov Pure Market Making Strategy with Batch Order Operations + + This strategy implements the reservation price calculation from the Avellaneda-Stoikov model + for perpetual futures market making. It places multiple order levels around the reservation + price with constant spreads. + + Key features: + - Reservation price calculation based on inventory + - Multiple constant spread levels + - Inventory management and tracking + - Order lifecycle management via connector's order tracker + - Batch order placement and cancellation for improved efficiency + """ + + create_timestamp = 0 + account_config_set = False + + @classmethod + def init_markets(cls, config: PMMAvellanedaMultiConfig): + cls.markets = {config.exchange: {config.trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase], config: PMMAvellanedaMultiConfig): + super().__init__(connectors) + self.config = config + self._last_update_timestamp: float = 0 + self._cached_mid_price: Decimal = Decimal("0") + self._cached_reservation_price: Decimal = Decimal("0") + + # DataFrame to store last 6 filled orders + self._filled_orders_df: pd.DataFrame = pd.DataFrame(columns=[ + "Timestamp", "Order ID", "Side", "Amount", "Price", "Inventory" + ]) + + def _get_in_flight_order(self, order_id: str) -> Optional[InFlightOrder]: + """Get InFlightOrder from connector's order tracker""" + connector = self.connectors[self.config.exchange] + return connector._order_tracker.fetch_order(client_order_id=order_id) + + + # Built-in event handler methods (called automatically by ScriptStrategyBase) + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp: + proposals: List[PerpetualOrderCandidate] = self.create_proposal() + proposal_adjusted: List[PerpetualOrderCandidate] = self.adjust_proposal_to_budget(proposals) + # Execute cancel then place sequentially to avoid order accumulation + safe_ensure_future(self._cancel_and_place_orders(proposal_adjusted)) + self.create_timestamp = self.config.order_refresh_time + self.current_timestamp + + async def _cancel_and_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """ + Cancel all active orders and then place new orders sequentially. + This ensures old orders are cancelled before new ones are placed. + """ + # First, cancel all active orders and wait for completion + await self._async_cancel_all_orders() + # Then place new orders + await self._async_place_orders(proposal) + + + def apply_initial_setting(self): + if not self.account_config_set: + connector = self.connectors[self.config.exchange] + connector.set_leverage(self.config.trading_pair, self.config.leverage) + self.account_config_set = True + + def create_proposal(self) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + # Use mark price from exchange instead of mid price + mark_price = connector.get_price_by_type(self.config.trading_pair, PriceType.MarkPrice) + inventory = self._get_current_inventory() + + reservation_price = self._calculate_reservation_price(mark_price, inventory) + + # Cache values for status reporting + self._cached_mid_price = mark_price + self._cached_reservation_price = reservation_price + + orders = [] + for idx, bid_spread in enumerate(self.config.bid_spread_levels): + ask_spread = self.config.ask_spread_levels[idx] + bid_price = reservation_price * (Decimal("1") - bid_spread) + ask_price = reservation_price * (Decimal("1") + ask_spread) + + # Convert quote amount to base amount for both buy and sell orders + # For perpetual orders, amount must be in base currency (BTC), not quote (USDC) + bid_amount = Decimal(self.config.order_amount_quote) / bid_price + ask_amount = Decimal(self.config.order_amount_quote) / ask_price + + bid_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.BUY, + amount=bid_amount, + price=bid_price, + leverage=Decimal(self.config.leverage) + ) + + ask_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.SELL, + amount=ask_amount, + price=ask_price, + leverage=Decimal(self.config.leverage) + ) + + current_inventory = self._get_current_inventory() + if current_inventory >= self.config.max_inventory: + orders.extend([ask_order]) + elif current_inventory <= -self.config.max_inventory: + orders.extend([bid_order]) + else: + orders.extend([bid_order, ask_order]) + return orders + + def adjust_proposal_to_budget(self, proposals: List[PerpetualOrderCandidate]) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + budget_checker = connector.budget_checker + + proposals_adjusted = budget_checker.adjust_candidates(proposals, all_or_none=True) + return proposals_adjusted + + async def _async_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """Place multiple orders using batch API and wait for completion""" + if not proposal: + return + + connector = self.connectors[self.config.exchange] + + # Convert PerpetualOrderCandidate objects to order dictionaries for batch_order_create + orders_to_create = [] + for order in proposal: + order_dict = { + "trading_pair": order.trading_pair, + "amount": order.amount, + "trade_type": order.order_side, + "order_type": order.order_type, + "price": order.price, + "position_action": PositionAction.OPEN + } + orders_to_create.append(order_dict) + + # Call batch_order_create and wait for completion + await connector.batch_order_create(orders_to_create) + + async def _async_cancel_all_orders(self): + """Cancel all active orders using batch API and wait for completion""" + connector = self.connectors[self.config.exchange] + + # Get orders directly from connector's order tracker instead of strategy's order tracker + # This ensures we get the most up-to-date list including orders just placed + all_in_flight_orders = connector._order_tracker.active_orders + + # Collect InFlightOrder objects for orders to cancel + orders_to_cancel = [] + orders_skipped = 0 + + for in_flight_order in all_in_flight_orders.values(): + # Filter by current trading pair only + if in_flight_order.trading_pair != self.config.trading_pair: + continue + + # Check if order is actually still open + if in_flight_order.is_open: + orders_to_cancel.append(in_flight_order) + else: + # Order is already done (filled/cancelled/failed), skip it + self.logger().debug( + f"Order {in_flight_order.client_order_id} is {in_flight_order.current_state.name}, " + f"skipping cancellation" + ) + orders_skipped += 1 + + # Use batch cancellation if we have orders to cancel and wait for completion + if orders_to_cancel: + try: + await connector.batch_order_cancel(orders_to_cancel) + except Exception as e: + self.logger().warning( + f"Error cancelling orders: {e}. " + f"This may be normal if orders were already cancelled." + ) + + def did_fill_order(self, event: OrderFilledEvent): + """ + Called automatically by framework when order is filled. + Logs the fill and updates the filled orders DataFrame. + Note: Inventory is now retrieved from the connector's actual position, + not tracked manually from fills. + """ + # Get current inventory from connector's actual position + current_inventory = self._get_current_inventory() + + # Add fill to DataFrame + try: + timestamp = pd.Timestamp.fromtimestamp(event.timestamp) if event.timestamp else pd.Timestamp.now() + except (ValueError, OSError, TypeError): + timestamp = pd.Timestamp.now() + + new_row = pd.DataFrame([{ + "Timestamp": timestamp, + "Order ID": event.order_id[:8] + "..." if len(event.order_id) > 8 else event.order_id, + "Side": event.trade_type.name, + "Amount": float(event.amount), + "Price": float(event.price), + "Inventory": float(current_inventory) + }]) + self._filled_orders_df = pd.concat([self._filled_orders_df, new_row], ignore_index=True) + + # Keep only last 6 fills + if len(self._filled_orders_df) > 6: + self._filled_orders_df = self._filled_orders_df.tail(6).reset_index(drop=True) + + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} " + f"{self.config.exchange} at {round(event.price, 2)} | Inventory: {current_inventory:.8f}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def _get_current_inventory(self) -> Decimal: + """ + Get current inventory position from the connector's actual position. + Returns signed position amount: positive for long, negative for short. + + For perpetual futures in ONEWAY mode: + - Gets the actual position from the exchange via connector + - Converts to signed value: positive for long, negative for short + """ + connector = self.connectors[self.config.exchange] + + # For ONEWAY mode, get position by trading pair (no side needed) + position = connector._perpetual_trading.get_position(self.config.trading_pair) + + if position is None: + return Decimal("0") + + # Convert to signed inventory: positive for long, negative for short + if position.position_side == PositionSide.LONG: + return position.amount + elif position.position_side == PositionSide.SHORT: + return -position.amount + else: + return Decimal("0") + + def _calculate_reservation_price(self, mid_price: Decimal, inventory: Decimal) -> Decimal: + """ + Calculate reservation price using Avellaneda-Stoikov formula: + r = s - q·γ·σ²·τ + + Where: + - s = mid_price + - q = inventory (signed, positive for long, negative for short) + - γ = risk_aversion_gamma + - σ = volatility_sigma (annualized) + - τ = risk_horizon_tau_hours / 8760 (convert hours to years) + """ + # Convert tau from hours to years + tau_years = self.config.risk_horizon_tau_hours / Decimal("8760") + + + # Calculate inventory adjustment: q·γ·σ²·τ + inventory_adjustment = inventory * self.config.risk_aversion_gamma * \ + (self.config.volatility_sigma ** Decimal("2")) * tau_years + + # Reservation price: r = s - q·γ·σ²·τ + reservation_price = mid_price - inventory_adjustment + + return reservation_price + + def format_status(self) -> str: + """ + Return status string showing current strategy state. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + + # Use cached values instead of making connector calls + mark_price = self._cached_mid_price # Note: variable name kept for compatibility, but contains mark price + reservation_price = self._cached_reservation_price + inventory = self._get_current_inventory() + + lines = [] + lines.append("") + lines.append(" Strategy Status:") + lines.append(f" Trading Pair: {self.config.trading_pair}") + lines.append(f" Exchange: {self.config.exchange}") + lines.append(f" Mark Price: {mark_price:.8f}") + lines.append(f" Reservation Price: {reservation_price:.8f}") + lines.append(f" Price Adjustment: {reservation_price - mark_price:.8f}") + lines.append(f" Current Inventory: {inventory:.8f}") + lines.append(f" Max Inventory: {self.config.max_inventory:.8f}") + lines.append(f" Spread Levels: {[f'{s*100:.4f}%' for s in self.config.ask_spread_levels]}") + lines.append(f" Bid Spread Levels: {[f'{s*100:.4f}%' for s in self.config.bid_spread_levels]}") + + try: + df = self.active_orders_df() + lines.append("") + lines.append(" Active Orders:") + lines.extend([" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.append("") + lines.append(" No active orders.") + + # Display last 6 filled orders (most recent last) + if len(self._filled_orders_df) > 0: + lines.append("") + lines.append(" Last 6 Filled Orders:") + # Display in reverse order so most recent appears at bottom + filled_df_display = self._filled_orders_df.iloc[::-1].copy() + # Format timestamp for display + filled_df_display["Timestamp"] = filled_df_display["Timestamp"].dt.strftime("%H:%M:%S") + lines.extend([" " + line for line in filled_df_display.to_string(index=False).split("\n")]) + else: + lines.append("") + lines.append(" No filled orders yet.") + + return "\n".join(lines) \ No newline at end of file diff --git a/scripts/pmm_avellaneda_batch_multi.py b/scripts/pmm_avellaneda_batch_multi.py new file mode 100644 index 00000000000..dbec160649d --- /dev/null +++ b/scripts/pmm_avellaneda_batch_multi.py @@ -0,0 +1,501 @@ +import logging +import os +from decimal import Decimal +from typing import Dict, List, Optional, Set + +import pandas as pd +from pydantic import Field, field_validator + +from hummingbot.client.config.config_data_types import BaseClientModel +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, PositionAction, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder +from hummingbot.core.data_type.order_candidate import PerpetualOrderCandidate +from hummingbot.core.event.events import ( + OrderFilledEvent, +) +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase +import asyncio + + +class PairConfig(BaseClientModel): + """Configuration for a single trading pair""" + trading_pair: str = Field("BTC-USDC") + order_amount_quote: Decimal = Field(20) + risk_aversion_gamma: Decimal = Field(6.0) + volatility_sigma: Decimal = Field(0.50) + risk_horizon_tau_hours: Decimal = Field(2.0) + bid_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) # 10 bps + ask_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) # 10 bps + max_inventory: Decimal = Field(0.01) # 1k usd + leverage: int = Field(10) + + @field_validator("order_amount_quote", "risk_aversion_gamma", "volatility_sigma", + "risk_horizon_tau_hours", "max_inventory", mode="before") + @classmethod + def convert_to_decimal(cls, v): + """Convert float/int values to Decimal when loading from YAML""" + if isinstance(v, (int, float)): + return Decimal(str(v)) + return v + + @field_validator("bid_spread_levels", "ask_spread_levels", mode="before") + @classmethod + def convert_list_to_decimal(cls, v): + """Convert list of float/int values to Decimal""" + if isinstance(v, list): + return [Decimal(str(x)) if isinstance(x, (int, float)) else x for x in v] + return v + + +class PMMAvellanedaMultiConfig(BaseClientModel): + script_file_name: str = os.path.basename(__file__) + exchange: str = Field("orderly_perpetual") + pairs: List[PairConfig] = Field( + default_factory=lambda: [PairConfig()], + json_schema_extra={ + "prompt": "Enter pair configurations (trading_pair,order_amount_quote,risk_aversion_gamma,volatility_sigma,risk_horizon_tau_hours,max_inventory,leverage:same_for_other_pairs)", + "prompt_on_new": True + } + ) + order_refresh_time: int = Field(10, ge=5) # Minimum 5 seconds to avoid rate limits + + @field_validator("pairs", mode="before") + @classmethod + def validate_pairs(cls, v): + if isinstance(v, str): + pairs = [] + for pair_str in v.split(":"): + parts = pair_str.split(",") + if len(parts) >= 1: + trading_pair = parts[0].strip() + order_amount_quote = Decimal(parts[1].strip()) if len(parts) > 1 else Decimal(20) + risk_aversion_gamma = Decimal(parts[2].strip()) if len(parts) > 2 else Decimal(6.0) + volatility_sigma = Decimal(parts[3].strip()) if len(parts) > 3 else Decimal(0.50) + risk_horizon_tau_hours = Decimal(parts[4].strip()) if len(parts) > 4 else Decimal(2.0) + max_inventory = Decimal(parts[5].strip()) if len(parts) > 5 else Decimal(0.01) + leverage = int(parts[6].strip()) if len(parts) > 6 else 10 + pairs.append(PairConfig( + trading_pair=trading_pair, + order_amount_quote=order_amount_quote, + risk_aversion_gamma=risk_aversion_gamma, + volatility_sigma=volatility_sigma, + risk_horizon_tau_hours=risk_horizon_tau_hours, + max_inventory=max_inventory, + leverage=leverage + )) + return pairs if pairs else [PairConfig()] + return v + +class PMMAvellanedaMulti(ScriptStrategyBase): + """ + Avellaneda-Stoikov Pure Market Making Strategy with Batch Order Operations + + This strategy implements the reservation price calculation from the Avellaneda-Stoikov model + for perpetual futures market making. It places multiple order levels around the reservation + price with constant spreads. Supports multiple trading pairs, each with its own configuration. + + Key features: + - Reservation price calculation based on inventory + - Multiple constant spread levels + - Inventory management and tracking per pair + - Order lifecycle management via connector's order tracker + - Batch order placement and cancellation for improved efficiency + - Multi-pair support with independent configurations + """ + + create_timestamp = 0 + account_config_set = False + + @classmethod + def init_markets(cls, config: PMMAvellanedaMultiConfig): + """Initialize markets from all pair configurations. All pairs use the same exchange.""" + if not config.pairs: + raise ValueError("At least one pair configuration is required") + + # All pairs use the same exchange from config + exchange = config.exchange + markets: Dict[str, Set[str]] = {exchange: set()} + for pair_config in config.pairs: + markets[exchange].add(pair_config.trading_pair) + cls.markets = markets + + def __init__(self, connectors: Dict[str, ConnectorBase], config: PMMAvellanedaMultiConfig): + super().__init__(connectors) + self.config = config + + # Store the exchange from config + self._exchange = config.exchange + if self._exchange not in connectors: + raise ValueError(f"Connector '{self._exchange}' not found in connectors") + + self._connector = connectors[self._exchange] + + # Initialize lock for preventing concurrent order operations + self._order_operation_lock = asyncio.Lock() + self._order_operation_in_progress = False + + # Create a mapping from trading_pair to pair config (exchange is same for all) + self._pair_configs: Dict[str, PairConfig] = {} + for pair_config in config.pairs: + self._pair_configs[pair_config.trading_pair] = pair_config + + # Per-pair state storage (keyed by trading_pair only since exchange is same) + self._current_inventory: Dict[str, Decimal] = {} + self._cached_mid_price: Dict[str, Decimal] = {} + self._cached_reservation_price: Dict[str, Decimal] = {} + + # Per-pair DataFrames to store last 6 filled orders + self._filled_orders_df: Dict[str, pd.DataFrame] = {} + + # Initialize per-pair state + for trading_pair in self._pair_configs.keys(): + self._current_inventory[trading_pair] = Decimal("0") + self._cached_mid_price[trading_pair] = Decimal("0") + self._cached_reservation_price[trading_pair] = Decimal("0") + self._filled_orders_df[trading_pair] = pd.DataFrame(columns=[ + "Timestamp", "Order ID", "Side", "Amount", "Price", "Inventory" + ]) + + def _get_pair_config(self, trading_pair: str) -> Optional[PairConfig]: + """Get pair configuration for a trading pair""" + return self._pair_configs.get(trading_pair) + + def _get_in_flight_order(self, order_id: str) -> Optional[InFlightOrder]: + """Get InFlightOrder from connector's order tracker""" + return self._connector._order_tracker.fetch_order(client_order_id=order_id) + + + # Built-in event handler methods (called automatically by ScriptStrategyBase) + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp: + # Prevent concurrent execution + if self._order_operation_in_progress: + self.logger().debug("Order operation already in progress, skipping tick") + return + + # Create all order proposals across all pairs first + all_proposals: List[PerpetualOrderCandidate] = [] + for trading_pair, pair_config in self._pair_configs.items(): + proposals = self.create_proposal(trading_pair) + all_proposals.extend(proposals) + + # Adjust all proposals to budget together + all_proposals_adjusted = self.adjust_proposal_to_budget(all_proposals) + + # Execute cancel all orders, then place all new orders together + safe_ensure_future(self._cancel_and_place_orders(all_proposals_adjusted)) + self.create_timestamp = self.config.order_refresh_time + self.current_timestamp + + async def _cancel_and_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """ + Cancel all active orders across all pairs, then place all new orders together. + Uses batch operations for efficiency. Uses a lock to prevent concurrent execution. + """ + # Check if operation is already in progress + if self._order_operation_in_progress: + self.logger().debug("Order operation already in progress, skipping") + return + + async with self._order_operation_lock: + try: + self._order_operation_in_progress = True + # First, cancel all active orders across all pairs in one batch + await self._async_cancel_all_orders() + # Then place all new orders together in one batch + await self._async_place_orders(proposal) + except Exception as e: + self.logger().error(f"Error in _cancel_and_place_orders: {e}", exc_info=True) + finally: + self._order_operation_in_progress = False + + + def apply_initial_setting(self): + if not self.account_config_set: + # Apply settings for all pairs + for trading_pair, pair_config in self._pair_configs.items(): + self._connector.set_leverage(trading_pair, pair_config.leverage) + self.account_config_set = True + + def create_proposal(self, trading_pair: str) -> List[PerpetualOrderCandidate]: + """Create order proposals for a specific trading pair""" + pair_config = self._get_pair_config(trading_pair) + if not pair_config: + return [] + + mid_price = self._connector.get_price_by_type(trading_pair, PriceType.MidPrice) + inventory = self._get_current_inventory(trading_pair) + + reservation_price = self._calculate_reservation_price( + mid_price, inventory, pair_config + ) + + # Cache values for status reporting + self._cached_mid_price[trading_pair] = mid_price + self._cached_reservation_price[trading_pair] = reservation_price + + orders = [] + for idx, bid_spread in enumerate(pair_config.bid_spread_levels): + ask_spread = pair_config.ask_spread_levels[idx] + # Ensure spreads are Decimal + bid_spread_decimal = Decimal(str(bid_spread)) + ask_spread_decimal = Decimal(str(ask_spread)) + bid_price = reservation_price * (Decimal("1") - bid_spread_decimal) + ask_price = reservation_price * (Decimal("1") + ask_spread_decimal) + + # Convert quote amount to base amount for both buy and sell orders + # For perpetual orders, amount must be in base currency (BTC), not quote (USDC) + order_amount = Decimal(str(pair_config.order_amount_quote)) + bid_amount = order_amount / bid_price + ask_amount = order_amount / ask_price + + bid_order = PerpetualOrderCandidate( + trading_pair=trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.BUY, + amount=bid_amount, + price=bid_price, + leverage=Decimal(pair_config.leverage) + ) + + ask_order = PerpetualOrderCandidate( + trading_pair=trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.SELL, + amount=ask_amount, + price=ask_price, + leverage=Decimal(pair_config.leverage) + ) + + max_inv = Decimal(str(pair_config.max_inventory)) + if inventory >= max_inv: + orders.extend([bid_order]) + elif inventory <= -max_inv: + orders.extend([ask_order]) + else: + orders.extend([bid_order, ask_order]) + return orders + + def adjust_proposal_to_budget(self, proposals: List[PerpetualOrderCandidate]) -> List[PerpetualOrderCandidate]: + """Adjust proposals to budget for the exchange""" + budget_checker = self._connector.budget_checker + proposals_adjusted = budget_checker.adjust_candidates(proposals, all_or_none=True) + return proposals_adjusted + + async def _async_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """Place all orders together using batch API for the single exchange""" + if not proposal: + return + + # Convert all order candidates to order dictionaries + orders_to_create: List[Dict] = [] + for order in proposal: + # Verify this trading pair is in our config + if order.trading_pair not in self._pair_configs: + self.logger().warning(f"Order for unknown trading pair: {order.trading_pair}") + continue + + order_dict = { + "trading_pair": order.trading_pair, + "amount": order.amount, + "trade_type": order.order_side, + "order_type": order.order_type, + "price": order.price, + "position_action": PositionAction.OPEN + } + orders_to_create.append(order_dict) + + # Place all orders together in one batch call + if orders_to_create: + await self._connector.batch_order_create(orders_to_create) + + async def _async_cancel_all_orders(self): + """Gather all active orders across all pairs and cancel them together using batch API""" + # Get all trading pairs we're tracking + tracked_trading_pairs = set(self._pair_configs.keys()) + + # Get all orders directly from connector's order tracker + all_in_flight_orders = self._connector._order_tracker.active_orders + + # Gather all open orders across all tracked trading pairs + orders_to_cancel: List[InFlightOrder] = [] + for in_flight_order in all_in_flight_orders.values(): + # Only include orders for our tracked trading pairs + if in_flight_order.trading_pair not in tracked_trading_pairs: + continue + + # Check if order is actually still open + if in_flight_order.is_open: + orders_to_cancel.append(in_flight_order) + else: + # Order is already done (filled/cancelled/failed), skip it + self.logger().debug( + f"Order {in_flight_order.client_order_id} is {in_flight_order.current_state.name}, " + f"skipping cancellation" + ) + + # Cancel all orders together in one batch call + if orders_to_cancel: + try: + await self._connector.batch_order_cancel(orders_to_cancel) + except Exception as e: + # Log error but don't fail - orders might already be cancelled + self.logger().warning( + f"Error cancelling orders: {e}. " + f"This may be normal if orders were already cancelled." + ) + + def did_fill_order(self, event: OrderFilledEvent): + """ + Called automatically by framework when order is filled. + Update tracked order state and inventory position based on order fills. + For perpetual futures: + - BUY fill increases inventory (long position) + - SELL fill decreases inventory (short position) + """ + # Verify this trading pair is in our config + trading_pair = event.trading_pair + if trading_pair not in self._pair_configs: + self.logger().warning(f"Fill event for unknown trading pair: {trading_pair} (order_id: {event.order_id})") + return + + # Verify this connector has this order + order = self._connector._order_tracker.fetch_order(client_order_id=event.order_id) + if order is None: + self.logger().warning(f"Fill event for unknown order: {event.order_id}") + return + + # Update inventory based on fill direction + if event.trade_type == TradeType.BUY: + # BUY fill = we bought, inventory increases (long position) + self._current_inventory[trading_pair] += event.amount + elif event.trade_type == TradeType.SELL: + # SELL fill = we sold, inventory decreases (short position) + self._current_inventory[trading_pair] -= event.amount + + # Add fill to DataFrame for this pair + try: + timestamp = pd.Timestamp.fromtimestamp(event.timestamp) if event.timestamp else pd.Timestamp.now() + except (ValueError, OSError, TypeError): + timestamp = pd.Timestamp.now() + + new_row = pd.DataFrame([{ + "Timestamp": timestamp, + "Order ID": event.order_id[:8] + "..." if len(event.order_id) > 8 else event.order_id, + "Side": event.trade_type.name, + "Amount": float(event.amount), + "Price": float(event.price), + "Inventory": float(self._current_inventory[trading_pair]) + }]) + self._filled_orders_df[trading_pair] = pd.concat([self._filled_orders_df[trading_pair], new_row], ignore_index=True) + + # Keep only last 6 fills per pair + if len(self._filled_orders_df[trading_pair]) > 6: + self._filled_orders_df[trading_pair] = self._filled_orders_df[trading_pair].tail(6).reset_index(drop=True) + + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {trading_pair} " + f"{self._exchange} at {round(event.price, 2)} | Inventory: {self._current_inventory[trading_pair]:.8f}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def _get_current_inventory(self, trading_pair: str) -> Decimal: + """ + Get current inventory position tracked internally from order fills for a specific pair. + Returns signed position amount: positive for long, negative for short. + + Position is tracked by accumulating fills: + - BUY fills increase inventory (long position) + - SELL fills decrease inventory (short position) + """ + return self._current_inventory.get(trading_pair, Decimal("0")) + + def _calculate_reservation_price(self, mid_price: Decimal, inventory: Decimal, pair_config: PairConfig) -> Decimal: + """ + Calculate reservation price using Avellaneda-Stoikov formula: + r = s - q·γ·σ²·τ + + Where: + - s = mid_price + - q = inventory (signed, positive for long, negative for short) + - γ = risk_aversion_gamma + - σ = volatility_sigma (annualized) + - τ = risk_horizon_tau_hours / 8760 (convert hours to years) + """ + # Ensure risk_horizon_tau_hours is Decimal (convert if needed) + tau_hours = Decimal(str(pair_config.risk_horizon_tau_hours)) + # Convert tau from hours to years + tau_years = tau_hours / Decimal("8760") + + # Ensure all values are Decimal (convert if needed) + gamma = Decimal(str(pair_config.risk_aversion_gamma)) + sigma = Decimal(str(pair_config.volatility_sigma)) + + # Calculate inventory adjustment: q·γ·σ²·τ + inventory_adjustment = inventory * gamma * (sigma ** Decimal("2")) * tau_years + + # Reservation price: r = s - q·γ·σ²·τ + reservation_price = mid_price - inventory_adjustment + + return reservation_price + + def format_status(self) -> str: + """ + Return status string showing current strategy state for all pairs. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + + lines = [] + lines.append("") + lines.append(" Strategy Status (Multi-Pair):") + lines.append(f" Order Refresh Time: {self.config.order_refresh_time}s") + lines.append(f" Number of Pairs: {len(self._pair_configs)}") + lines.append("") + + # Display status for each pair + lines.append(f" Exchange: {self._exchange}") + for trading_pair, pair_config in self._pair_configs.items(): + mid_price = self._cached_mid_price.get(trading_pair, Decimal("0")) + reservation_price = self._cached_reservation_price.get(trading_pair, Decimal("0")) + inventory = self._current_inventory.get(trading_pair, Decimal("0")) + + lines.append(f" Pair: {trading_pair}") + lines.append(f" Mid Price: {mid_price:.8f}") + lines.append(f" Reservation Price: {reservation_price:.8f}") + lines.append(f" Price Adjustment: {reservation_price - mid_price:.8f}") + lines.append(f" Current Inventory: {inventory:.8f}") + lines.append(f" Max Inventory: {pair_config.max_inventory:.8f}") + lines.append(f" Leverage: {pair_config.leverage}x") + lines.append(f" Spread Levels: {[f'{s*100:.4f}%' for s in pair_config.ask_spread_levels]}") + lines.append(f" Bid Spread Levels: {[f'{s*100:.4f}%' for s in pair_config.bid_spread_levels]}") + lines.append("") + + # Display active orders + try: + df = self.active_orders_df() + if len(df) > 0: + lines.append(" Active Orders (All Pairs):") + lines.extend([" " + line for line in df.to_string(index=False).split("\n")]) + else: + lines.append(" No active orders.") + except ValueError: + lines.append(" No active orders.") + + # Display last 6 filled orders for each pair + for trading_pair, pair_config in self._pair_configs.items(): + filled_df = self._filled_orders_df.get(trading_pair) + + if filled_df is not None and len(filled_df) > 0: + lines.append("") + lines.append(f" Last 6 Filled Orders ({self._exchange}:{trading_pair}):") + # Display in reverse order so most recent appears at bottom + filled_df_display = filled_df.iloc[::-1].copy() + # Format timestamp for display + filled_df_display["Timestamp"] = filled_df_display["Timestamp"].dt.strftime("%H:%M:%S") + lines.extend([" " + line for line in filled_df_display.to_string(index=False).split("\n")]) + + return "\n".join(lines) \ No newline at end of file diff --git a/scripts/pmm_kodiak.py b/scripts/pmm_kodiak.py new file mode 100644 index 00000000000..b424abef50d --- /dev/null +++ b/scripts/pmm_kodiak.py @@ -0,0 +1,461 @@ +import logging +import os +from decimal import Decimal +from typing import Any, Dict, List, Optional + +import pandas as pd +from pydantic import Field + +from hummingbot.client.config.config_data_types import BaseClientModel +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, PositionAction, PositionSide, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder +from hummingbot.core.data_type.order_candidate import PerpetualOrderCandidate +from hummingbot.core.event.events import ( + OrderFilledEvent, +) +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class PMMAvellanedaMultiConfig(BaseClientModel): + script_file_name: str = os.path.basename(__file__) + exchange: str = Field("orderly_perpetual") + trading_pair: str = Field("BTC-USDC") + order_amount_quote: List[Decimal] = Field(default=[Decimal("20")]) + bid_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + ask_spread_levels: List[Decimal] = Field(default=[Decimal("0.001")]) #10 bps + order_refresh_time: int = Field(10) + order_cooldown: int = Field(0) # Cooldown in seconds after an order fill + max_inventory: Decimal = Field(0.01) # 1k usd + max_price_adjustment: Decimal = Field(default=Decimal("0.001")) # 10 bps + leverage: int = Field(100) + + # target_inventory: Decimal = Field(0.0) + + # for getting candles data for annualized volatility + # candles_exchange: str = Field(default="binance_perpetual") + # candles_pair: str = Field(default="BTC-USDT") + # candles_interval: str = Field(default="1d") + # candles_length: int = Field(default=365, gt=0) + +# Add Mark price as reference + +class PMMAvellanedaMulti(ScriptStrategyBase): + """ + Avellaneda-Stoikov Pure Market Making Strategy with Batch Order Operations + + This strategy implements the reservation price calculation from the Avellaneda-Stoikov model + for perpetual futures market making. It places multiple order levels around the reservation + price with constant spreads. + + Key features: + - Reservation price calculation based on inventory + - Multiple constant spread levels + - Inventory management and tracking + - Order lifecycle management via connector's order tracker + - Batch order placement and cancellation for improved efficiency + """ + + create_timestamp = 0 + account_config_set = False + + total_sell_orders = 0 + total_buy_orders = 0 + total_sell_volume = 0 + total_buy_volume = 0 + + + @classmethod + def init_markets(cls, config: PMMAvellanedaMultiConfig): + cls.markets = {config.exchange: {config.trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase], config: PMMAvellanedaMultiConfig): + super().__init__(connectors) + self.config = config + self._last_update_timestamp: float = 0 + self._cached_mark_price: Decimal = Decimal("0") + self._cached_reservation_price: Decimal = Decimal("0") + self._cooldown_until_timestamp: float = 0 # Timestamp until which we're in cooldown + + # DataFrame to store last 6 filled orders + self._filled_orders_df: pd.DataFrame = pd.DataFrame(columns=[ + "Timestamp", "Order ID", "Side", "Amount", "Price", "Inventory" + ]) + + def _get_in_flight_order(self, order_id: str) -> Optional[InFlightOrder]: + """Get InFlightOrder from connector's order tracker""" + connector = self.connectors[self.config.exchange] + return connector._order_tracker.fetch_order(client_order_id=order_id) + + # Built-in event handler methods (called automatically by ScriptStrategyBase) + + def on_tick(self): + # Check if we're in cooldown period + if self.current_timestamp < self._cooldown_until_timestamp: + return + + if self.create_timestamp <= self.current_timestamp: + proposals: List[PerpetualOrderCandidate] = self.create_proposal() + proposal_adjusted: List[PerpetualOrderCandidate] = self.adjust_proposal_to_budget(proposals) + # Execute cancel then place sequentially to avoid order accumulation + safe_ensure_future(self._cancel_and_place_orders(proposal_adjusted)) + self.create_timestamp = self.config.order_refresh_time + self.current_timestamp + + async def _cancel_and_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """ + Cancel all active orders and then place new orders sequentially. + This ensures old orders are cancelled before new ones are placed. + """ + # First, cancel all active orders and wait for completion + cancel_results = await self._async_cancel_all_orders() + + # Verify cancellation succeeded before placing new orders + if cancel_results is not None: + # Check if any cancel failed + if any(not result.get("success", False) for result in cancel_results): + self.logger().warning( + "Some orders failed to cancel. Skipping new order placement this cycle." + ) + return + + # Then place new orders + await self._async_place_orders(proposal) + + + def apply_initial_setting(self): + if not self.account_config_set: + connector = self.connectors[self.config.exchange] + connector.set_leverage(self.config.trading_pair, self.config.leverage) + self.account_config_set = True + + def create_proposal(self) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + # Use mark price from exchange instead of mid price + mark_price = connector.get_price_by_type(self.config.trading_pair, PriceType.MarkPrice) + # best_bid_price = connector.get_price_by_type(self.config.trading_pair, PriceType.BestBid) + # best_ask_price = connector.get_price_by_type(self.config.trading_pair, PriceType.BestAsk) + + # Get order book + order_book = connector.get_order_book(self.config.trading_pair) + bids_df, asks_df = order_book.snapshot + + # Get best bid/ask prices and sizes + best_bid_price = Decimal(str(bids_df.iloc[0].price)) + best_bid_size = Decimal(str(bids_df.iloc[0].amount)) + best_ask_price = Decimal(str(asks_df.iloc[0].price)) + best_ask_size = Decimal(str(asks_df.iloc[0].amount)) + + mid_price = (best_bid_price + best_ask_price) / 2 + + if best_bid_size + best_ask_size > 0: + mid_price = ((best_bid_price * best_ask_size) + (best_bid_size * best_ask_price)) / (best_bid_size + best_ask_size) + + # self.logger().info(f"Mid price: {mid_price}, Mid price weighted: {mid_price_weighted}, bid_size: {best_bid_size}, ask_size: {best_ask_size}, Difference: {mid_price_weighted - mid_price}") + + inventory = self._get_current_inventory() + + reservation_price_mark = self._get_reservation_price(mark_price, inventory) + reservation_price_mid = self._get_reservation_price(mid_price, inventory) + + # Cache values for status reporting + self._cached_mark_price = mark_price + self._cached_mid_price = mid_price + self._cached_reservation_price_mark = reservation_price_mark + self._cached_reservation_price_mid = reservation_price_mid + + orders = [] + for idx, bid_spread in enumerate(self.config.bid_spread_levels): + ask_spread = self.config.ask_spread_levels[idx] + bid_price = min(reservation_price_mid, reservation_price_mark) * (Decimal("1") - bid_spread) + ask_price = max(reservation_price_mid, reservation_price_mark) * (Decimal("1") + ask_spread) + + # To make sure the limit maker orders are not immediately taken + # Only the offending side is adjusted, but still respecting the spread level + if bid_price >= best_ask_price: + bid_price = mid_price * (Decimal("1") - bid_spread) + + if ask_price <= best_bid_price: + ask_price = mid_price * (Decimal("1") + ask_spread) + + bid_amount = self.config.order_amount_quote[idx] / reservation_price_mid # reservation_price_mid used only to keep the order size same + ask_amount = self.config.order_amount_quote[idx] / reservation_price_mid # reservation_price_mid usedonly to keep the order size same + + bid_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.BUY, + amount=bid_amount, + price=bid_price, + leverage=Decimal(self.config.leverage) + ) + + ask_order = PerpetualOrderCandidate( + trading_pair=self.config.trading_pair, + is_maker=True, + order_type=OrderType.LIMIT_MAKER, + order_side=TradeType.SELL, + amount=ask_amount, + price=ask_price, + leverage=Decimal(self.config.leverage) + ) + + # Inventory-based order placement logic: + if inventory >= self.config.max_inventory: + # At max long position, only place ask orders to reduce position + orders.extend([ask_order]) + elif inventory <= -self.config.max_inventory: + # At max short position, only place bid orders to reduce position + orders.extend([bid_order]) + else: + # Normal market making: place both sides + orders.extend([bid_order, ask_order]) + return orders + + def adjust_proposal_to_budget(self, proposals: List[PerpetualOrderCandidate]) -> List[PerpetualOrderCandidate]: + connector = self.connectors[self.config.exchange] + budget_checker = connector.budget_checker + + proposals_adjusted = budget_checker.adjust_candidates(proposals, all_or_none=True) + return proposals_adjusted + + async def _async_place_orders(self, proposal: List[PerpetualOrderCandidate]) -> None: + """Place multiple orders using batch API and wait for completion""" + if not proposal: + return + + connector = self.connectors[self.config.exchange] + + # Convert PerpetualOrderCandidate objects to order dictionaries for batch_order_create + orders_to_create = [] + for order in proposal: + order_dict = { + "trading_pair": order.trading_pair, + "amount": order.amount, + "trade_type": order.order_side, + "order_type": order.order_type, + "price": order.price, + "position_action": PositionAction.OPEN + } + orders_to_create.append(order_dict) + + # Call batch_order_create and wait for completion + await connector.batch_order_create(orders_to_create) + + async def _async_cancel_all_orders(self) -> Optional[List[Dict[str, Any]]]: + """Cancel all active orders using batch API and wait for completion""" + connector = self.connectors[self.config.exchange] + + # Get orders directly from connector's order tracker instead of strategy's order tracker + # This ensures we get the most up-to-date list including orders just placed + all_in_flight_orders = connector._order_tracker.active_orders + + # Collect InFlightOrder objects for orders to cancel + orders_to_cancel = [] + orders_skipped = 0 + total_orders_checked = 0 + + for in_flight_order in all_in_flight_orders.values(): + # Filter by current trading pair only + if in_flight_order.trading_pair != self.config.trading_pair: + continue + + total_orders_checked += 1 + + # Check if order is actually still open + if in_flight_order.is_open: + orders_to_cancel.append(in_flight_order) + else: + # Order is already done (filled/cancelled/failed), skip it + self.logger().debug( + f"Order {in_flight_order.client_order_id} is {in_flight_order.current_state.name}, " + f"skipping cancellation" + ) + orders_skipped += 1 + + # Log when no orders are found to cancel (important for debugging) + if not orders_to_cancel: + if total_orders_checked == 0: + self.logger().debug( + f"No active orders found for {self.config.trading_pair} to cancel. " + f"This may be normal if all orders were already filled/cancelled." + ) + else: + self.logger().info( + f"Found {total_orders_checked} order(s) for {self.config.trading_pair}, " + f"but all are already {orders_skipped} filled/cancelled/failed. " + f"No cancellation needed." + ) + return [] + + # Use batch cancellation if we have orders to cancel and wait for completion + try: + self.logger().debug( + f"Cancelling {len(orders_to_cancel)} active order(s) for {self.config.trading_pair}" + ) + return await connector.batch_order_cancel(orders_to_cancel) + except Exception as e: + self.logger().warning( + f"Error cancelling orders: {e}. " + f"This may be normal if orders were already cancelled." + ) + return None + + def did_fill_order(self, event: OrderFilledEvent): + """ + Called automatically by framework when order is filled. + Logs the fill and updates the filled orders DataFrame. + Note: Inventory is now retrieved from the connector's actual position, + not tracked manually from fills. + """ + # Set cooldown period after order fill + if self.config.order_cooldown > 0: + self._cooldown_until_timestamp = self.current_timestamp + self.config.order_cooldown + + # Get current inventory from connector's actual position + current_inventory = self._get_current_inventory() + + if event.trade_type == TradeType.BUY: + self.total_buy_orders += 1 + self.total_buy_volume += event.amount * event.price + elif event.trade_type == TradeType.SELL: + self.total_sell_orders += 1 + self.total_sell_volume += event.amount * event.price + + # Add fill to DataFrame + try: + timestamp = pd.Timestamp.fromtimestamp(event.timestamp) if event.timestamp else pd.Timestamp.now() + except (ValueError, OSError, TypeError): + timestamp = pd.Timestamp.now() + + new_row = pd.DataFrame([{ + "Timestamp": timestamp, + "Order ID": event.order_id[:8] + "..." if len(event.order_id) > 8 else event.order_id, + "Side": event.trade_type.name, + "Amount": float(event.amount), + "Price": float(event.price), + "Inventory": float(current_inventory) + }]) + self._filled_orders_df = pd.concat([self._filled_orders_df, new_row], ignore_index=True) + + # Keep only last 6 fills + if len(self._filled_orders_df) > 6: + self._filled_orders_df = self._filled_orders_df.tail(6).reset_index(drop=True) + + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} " + f"{self.config.exchange} at {round(event.price, 2)} | Inventory: {current_inventory:.8f}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def _get_current_inventory(self) -> Decimal: + """ + Get current inventory position from the connector's actual position. + Returns signed position amount: positive for long, negative for short. + + For perpetual futures in ONEWAY mode: + - Gets the actual position from the exchange via connector + - Converts to signed value: positive for long, negative for short + """ + connector = self.connectors[self.config.exchange] + + # For ONEWAY mode, get position by trading pair (no side needed) + position = connector._perpetual_trading.get_position(self.config.trading_pair) + + if position is None: + return Decimal("0") + + # Convert to signed inventory: positive for long, negative for short + if position.position_side == PositionSide.LONG: + return position.amount + elif position.position_side == PositionSide.SHORT: + return -position.amount + else: + return Decimal("0") + + def _get_reservation_price(self, reference_price: Decimal, inventory: Decimal) -> Decimal: + """ + Calculate reservation price with inventory-based adjustment. + + Logic: + - If abs(position_size) <= 25% of max_position_size: no adjustment + - Else: linearly increasing adjustment from 0% at 25% to max_adjustment at 100% + + For long positions (inventory > 0): lower reservation price to encourage selling + For short positions (inventory < 0): raise reservation price to encourage buying + """ + # Calculate absolute inventory ratio + abs_inventory = abs(inventory) + inventory_ratio = abs_inventory / self.config.max_inventory + inventory_ratio = min(inventory_ratio, Decimal("1")) + + # If position size <= 25% max position size, no adjustment + if inventory_ratio <= Decimal("0.25"): + return reference_price + + # Linearly increasing adjustment from 0 at 25% to max_adjustment at 100% + # Formula: adjustment = (ratio - 0.25) / (1.0 - 0.25) * max_adjustment + adjustment_ratio = (inventory_ratio - Decimal("0.25")) / Decimal("0.75") + adjustment = adjustment_ratio * self.config.max_price_adjustment + + # Apply adjustment with correct sign: + # - Long position (inventory > 0): lower price (subtract adjustment) + # - Short position (inventory < 0): raise price (add adjustment) + if inventory > 0: + reservation_price = reference_price * (Decimal("1") - adjustment) + else: + reservation_price = reference_price * (Decimal("1") + adjustment) + + return reservation_price + + + + def format_status(self) -> str: + """ + Return status string showing current strategy state. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + + + mark_price = self._cached_mark_price + mid_price = self._cached_mid_price + reservation_price_mark = self._cached_reservation_price_mark + reservation_price_mid = self._cached_reservation_price_mid + inventory = self._get_current_inventory() + + lines = [] + lines.append("") + lines.append(" Strategy Status:") + lines.append(f" Trading Pair: {self.config.trading_pair}") + lines.append(f" Exchange: {self.config.exchange}") + lines.append(f" Mark Price: {mark_price:.8f}") + lines.append(f" Mid Price: {mid_price:.8f}") + lines.append(f" Reservation Price Mid: {reservation_price_mid:.8f}") + lines.append(f" Reservation Price Mark: {reservation_price_mark:.8f}") + lines.append(f" Price Adjustment Mid: {reservation_price_mid - mid_price:.8f}") + lines.append(f" Price Adjustment Mark: {reservation_price_mark - mark_price:.8f}") + lines.append(f" Current Inventory: {inventory:.8f}") + lines.append(f" Max Inventory: {self.config.max_inventory:.8f}") + lines.append(f" Spread Levels: {[f'{s*100:.4f}%' for s in self.config.ask_spread_levels]}") + lines.append(f" Bid Spread Levels: {[f'{s*100:.4f}%' for s in self.config.bid_spread_levels]}") + + lines.append(f" Total Filled Buy Orders: {self.total_buy_orders:.2f}") + lines.append(f" Total Filled Sell Orders: {self.total_sell_orders:.2f}") + lines.append(f" Total Buy Volume: {self.total_buy_volume:.2f}") + lines.append(f" Total Sell Volume: {self.total_sell_volume:.2f}") + + # Display last 6 filled orders (most recent last) + if len(self._filled_orders_df) > 0: + lines.append("") + lines.append(" Last 6 Filled Orders:") + # Display in reverse order so most recent appears at bottom + filled_df_display = self._filled_orders_df.iloc[::-1].copy() + # Format timestamp for display + filled_df_display["Timestamp"] = filled_df_display["Timestamp"].dt.strftime("%H:%M:%S") + lines.extend([" " + line for line in filled_df_display.to_string(index=False).split("\n")]) + else: + lines.append("") + lines.append(" No filled orders yet.") + + return "\n".join(lines) \ No newline at end of file