Centralized Exchange Adapters
Route orders to centralized exchanges with unified validation and agent permission controls.
Overview
The CEX adapter system provides:
- Unified API - Same interface for all exchanges
- Order Validation - Validate against agent limits before submission
- Credential Passthrough - Credentials never stored by the router
- Event Tracking - Subscribe to order lifecycle events
- Dry-Run Mode - Validate without submitting
Quick Start
from zeroquant import (
ExchangeRouter,
RouterConfig,
ValidationRules,
RouteOrderRequest,
SupportedExchange,
ExchangeCredentials,
)
from zeroquant.orders.mvo import MinimumViableOrder
# Create router with agent limits
router = ExchangeRouter(RouterConfig(
default_validation_rules=ValidationRules(
daily_limit_usd=50000,
per_tx_limit_usd=5000,
max_order_value_usd=10000,
),
enable_logging=True,
))
# Create order
order = MinimumViableOrder(
symbol="ETH/USDC",
side="buy",
original_quantity_base=1.5,
original_price=2000.0,
)
# Route to Binance
result = await router.route_order(RouteOrderRequest(
order=order,
exchange=SupportedExchange.BINANCE,
credentials=ExchangeCredentials(
api_key="your_api_key",
api_secret="your_api_secret",
),
))
if result.success:
print(f"Order placed: {result.exchange_response.exchange_order_id}")
else:
print(f"Failed: {result.error}")
Architecture
┌─────────────────────────────────────────────────────────────┐
│ AI Agent / Application │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ExchangeRouter │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Validation │ │ Event Tracking │ │
│ │ - Agent limits │ │ - Order events │ │
│ │ - Exchange rules│ │ - Fill events │ │
│ └─────────────────┘ └─────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Binance │ │ Coinbase │ │ Kraken │
│ Adapter │ │ Adapter │ │ Adapter │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Binance API │ │Coinbase API │ │ Kraken API │
└─────────────┘ └─────────────┘ └─────────────┘
Supported Exchanges
| Exchange | Status | Features |
|---|---|---|
| Binance | Supported | Spot, Futures |
| Coinbase | Supported | Spot |
| Kraken | Supported | Spot, Futures |
| OKX | Planned | - |
| Bybit | Planned | - |
| KuCoin | Planned | - |
ExchangeRouter
Central routing layer for all exchange operations.
Configuration
from zeroquant import ExchangeRouter, RouterConfig, ValidationRules
router = ExchangeRouter(RouterConfig(
# Default validation for all orders
default_validation_rules=ValidationRules(
daily_limit_usd=50000,
per_tx_limit_usd=5000,
max_order_value_usd=10000,
allowed_pairs=["ETH/USDC", "BTC/USDC", "SOL/USDC"],
blocked_pairs=["SHIB/USDC"], # Block specific pairs
require_reduce_only=False,
max_leverage=5.0,
),
enable_logging=True,
))
Routing Orders
result = await router.route_order(RouteOrderRequest(
order=MinimumViableOrder(
symbol="ETH/USDC",
side="buy",
original_quantity_base=1.5,
original_price=2000.0,
),
exchange=SupportedExchange.BINANCE,
credentials=ExchangeCredentials(
api_key="...",
api_secret="...",
),
# Override default rules for this order
validation_rules=ValidationRules(
per_tx_limit_usd=10000, # Higher limit for this order
),
skip_validation=False, # Set True to bypass validation
dry_run=False, # Set True to validate without submitting
))
Response Structure
class RouteOrderResponse:
success: bool # Whether routing succeeded
validation: ValidationResult # Validation result
exchange_response: Optional[ExchangeOrderResponse] # Exchange response
raw_response: Optional[Dict] # Raw exchange response
error: Optional[str] # Error message
error_code: Optional[str] # Error code
timestamp: int # Timestamp
request_id: str # Request tracking ID
Batch Routing
# Route multiple orders in parallel
requests = [
RouteOrderRequest(order=order1, exchange=SupportedExchange.BINANCE, ...),
RouteOrderRequest(order=order2, exchange=SupportedExchange.COINBASE, ...),
RouteOrderRequest(order=order3, exchange=SupportedExchange.KRAKEN, ...),
]
results = await router.route_orders_batch(requests)
for result in results:
if result.success:
print(f"Order: {result.exchange_response.exchange_order_id}")
Validation
Validation Rules
class ValidationRules:
max_order_value_usd: Optional[float] # Maximum single order value
max_position_size: Optional[float] # Maximum position size
allowed_pairs: Optional[List[str]] # Whitelist of trading pairs
blocked_pairs: Optional[List[str]] # Blacklist of trading pairs
allowed_order_types: Optional[List[str]]# Allowed order types
daily_limit_usd: Optional[float] # Daily spending limit
daily_spent_usd: Optional[float] # Current daily spent
per_tx_limit_usd: Optional[float] # Per-transaction limit
require_reduce_only: bool # Require reduce-only orders
max_leverage: Optional[float] # Maximum leverage
Validation Errors
result = await router.route_order(request)
if not result.validation.valid:
for error in result.validation.errors:
print(f"Error: {error.code} - {error.message}")
print(f" Field: {error.field}")
print(f" Value: {error.value}")
print(f" Limit: {error.limit}")
Error Codes:
| Code | Description |
|---|---|
MISSING_SYMBOL | Order missing symbol |
MISSING_SIDE | Order missing side |
MISSING_QUANTITY | Order missing quantity |
INVALID_QUANTITY | Quantity not positive |
PAIR_NOT_ALLOWED | Pair not in whitelist |
PAIR_BLOCKED | Pair in blacklist |
EXCEEDS_TX_LIMIT | Exceeds per-tx limit |
EXCEEDS_DAILY_LIMIT | Exceeds daily limit |
EXCEEDS_MAX_VALUE | Exceeds max order value |
BELOW_MIN_SIZE | Below exchange minimum |
ABOVE_MAX_SIZE | Above exchange maximum |
REDUCE_ONLY_REQUIRED | Reduce-only required |
Validate Without Submitting
# Dry run - validates but doesn't submit
result = await router.route_order(RouteOrderRequest(
order=order,
exchange=SupportedExchange.BINANCE,
credentials=credentials,
dry_run=True, # Validate only
))
if result.validation.valid:
print("Order would be accepted")
else:
print("Order would be rejected")
Credentials
Security
Credentials are passed through to exchanges and are never stored by the router. Always use environment variables or secrets management.
Binance
credentials = ExchangeCredentials(
api_key=os.environ["BINANCE_API_KEY"],
api_secret=os.environ["BINANCE_API_SECRET"],
)
Coinbase
credentials = ExchangeCredentials(
api_key=os.environ["COINBASE_API_KEY"],
api_secret=os.environ["COINBASE_API_SECRET"],
passphrase=os.environ["COINBASE_PASSPHRASE"], # Required for Coinbase
)
Kraken
credentials = ExchangeCredentials(
api_key=os.environ["KRAKEN_API_KEY"],
api_secret=os.environ["KRAKEN_API_SECRET"],
)
MinimumViableOrder
The standard order format across all exchanges.
class MinimumViableOrder:
symbol: Optional[str] # Trading pair (e.g., "ETH/USDC")
base_asset: Optional[str] # Base asset (e.g., "ETH")
quote_asset: Optional[str] # Quote asset (e.g., "USDC")
side: Optional[str] # "buy" or "sell"
original_quantity_base: Optional[float] # Quantity in base asset
original_price: Optional[float] # Price (None for market orders)
time_in_force: Optional[str] # "gtc", "ioc", "fok"
reduce_only: Optional[bool] # Reduce-only flag
client_order_id: Optional[str] # Client-provided ID
Order Types
# Market order (no price)
market_order = MinimumViableOrder(
symbol="ETH/USDC",
side="buy",
original_quantity_base=1.5,
)
# Limit order (with price)
limit_order = MinimumViableOrder(
symbol="ETH/USDC",
side="buy",
original_quantity_base=1.5,
original_price=2000.0,
time_in_force="gtc",
)
Order Management
Cancel Orders
from zeroquant import CancelOrderRequest
result = await router.cancel_order(CancelOrderRequest(
order_id="internal_order_id",
exchange_order_id="exchange_order_id", # Optional
symbol="ETH/USDC",
exchange=SupportedExchange.BINANCE,
credentials=credentials,
))
if result.success:
print(f"Cancelled: {result.order_id}")
Query Orders
from zeroquant import QueryOrdersRequest
# Query open orders
open_orders = await router.query_orders(QueryOrdersRequest(
exchange=SupportedExchange.BINANCE,
credentials=credentials,
symbol="ETH/USDC", # Optional filter
open_only=True,
))
# Query all orders
all_orders = await router.query_orders(QueryOrdersRequest(
exchange=SupportedExchange.BINANCE,
credentials=credentials,
limit=100,
))
for order in open_orders:
print(f"{order.exchange_order_id}: {order.status}")
Event Subscriptions
Subscribe to Events
def on_event(event):
event_type = event.get("type")
if event_type == "order_validated":
print(f"Validated: {event['result']['valid']}")
elif event_type == "order_submitted":
print(f"Submitted to: {event['exchange']}")
elif event_type == "order_response":
print(f"Response: {event['response']}")
elif event_type == "order_rejected":
print(f"Rejected: {event['errors']}")
elif event_type == "order_error":
print(f"Error: {event['error']}")
# Subscribe
unsubscribe = router.subscribe(on_event)
# Later: unsubscribe()
Event Types
| Event | Description |
|---|---|
order_validated | Order passed/failed validation |
order_submitted | Order submitted to exchange |
order_response | Exchange response received |
order_rejected | Order rejected by validation |
order_error | Error during submission |
Statistics
Get Router Stats
stats = router.get_stats()
print(f"Total Orders: {stats.total_orders}")
print(f"Successful: {stats.successful_orders}")
print(f"Failed: {stats.failed_orders}")
print(f"Rejected: {stats.rejected_orders}")
print(f"Avg Routing Time: {stats.average_routing_time:.3f}s")
print(f"Avg Validation Time: {stats.average_validation_time:.3f}s")
print("By Exchange:")
for exchange, count in stats.orders_by_exchange.items():
print(f" {exchange}: {count}")
Order Log
# Get order log
log = router.get_order_log(limit=100)
for entry in log:
print(f"{entry.timestamp}: {entry.action} on {entry.exchange}")
print(f" Success: {entry.success}, Duration: {entry.duration:.3f}s")
if entry.error:
print(f" Error: {entry.error}")
# Clear log
router.clear_order_log()
# Reset stats
router.reset_stats()
Helper Functions
Create Router
from zeroquant import create_router, create_agent_router
# Basic router
router = create_router()
# Router with agent limits
router = create_agent_router(
daily_limit_usd=50000,
per_tx_limit_usd=5000,
allowed_pairs=["ETH/USDC", "BTC/USDC"],
)
Custom Adapters
Implementing a Custom Adapter
from zeroquant.exchange_adapters import BaseExchangeAdapter, ExchangeConfig
from zeroquant import SupportedExchange
class CustomExchangeAdapter(BaseExchangeAdapter):
@property
def exchange(self) -> SupportedExchange:
return SupportedExchange.CUSTOM # Add to enum
@property
def display_name(self) -> str:
return "Custom Exchange"
def to_exchange_format(self, order):
# Convert MVO to exchange format
return {...}
def from_exchange_format(self, response):
# Convert exchange response to standard format
return ExchangeOrderResponse(...)
def to_exchange_symbol(self, symbol: str) -> str:
return symbol.replace("/", "")
def from_exchange_symbol(self, symbol: str) -> str:
return symbol
async def _submit_to_exchange(self, order, credentials):
# Submit to exchange API
return {...}
async def _cancel_on_exchange(self, order_id, symbol, credentials):
# Cancel on exchange API
return {...}
async def _query_from_exchange(self, request):
# Query from exchange API
return [...]
async def get_market_data(self, symbol, credentials):
# Get market data for validation
return MarketData(...)
def _create_signature(self, method, path, body, credentials):
# Create signed headers
return {...}
# Register custom adapter
router = ExchangeRouter(custom_adapters=[CustomExchangeAdapter()])
Error Handling
try:
result = await router.route_order(request)
if not result.success:
if result.error_code == "VALIDATION_FAILED":
# Handle validation failure
for error in result.validation.errors:
print(f"Validation error: {error.message}")
elif result.error_code == "SUBMISSION_ERROR":
# Handle exchange submission error
print(f"Exchange error: {result.error}")
elif result.error_code == "UNSUPPORTED_EXCHANGE":
print("Exchange not supported")
except Exception as e:
print(f"Unexpected error: {e}")
finally:
await router.close()
Best Practices
- Always use environment variables for credentials
- Set appropriate validation limits for agents
- Use dry_run mode to test before submitting
- Subscribe to events for monitoring
- Handle all error cases gracefully
- Close the router when done
Next Steps
- Order Management - Full order management
- Position Tracking - Track positions
- TEE Integration - Secure agent execution
- Python SDK Reference - Full API reference