Skip to main content

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

ExchangeStatusFeatures
BinanceSupportedSpot, Futures
CoinbaseSupportedSpot
KrakenSupportedSpot, Futures
OKXPlanned-
BybitPlanned-
KuCoinPlanned-

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:

CodeDescription
MISSING_SYMBOLOrder missing symbol
MISSING_SIDEOrder missing side
MISSING_QUANTITYOrder missing quantity
INVALID_QUANTITYQuantity not positive
PAIR_NOT_ALLOWEDPair not in whitelist
PAIR_BLOCKEDPair in blacklist
EXCEEDS_TX_LIMITExceeds per-tx limit
EXCEEDS_DAILY_LIMITExceeds daily limit
EXCEEDS_MAX_VALUEExceeds max order value
BELOW_MIN_SIZEBelow exchange minimum
ABOVE_MAX_SIZEAbove exchange maximum
REDUCE_ONLY_REQUIREDReduce-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

EventDescription
order_validatedOrder passed/failed validation
order_submittedOrder submitted to exchange
order_responseExchange response received
order_rejectedOrder rejected by validation
order_errorError 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

  1. Always use environment variables for credentials
  2. Set appropriate validation limits for agents
  3. Use dry_run mode to test before submitting
  4. Subscribe to events for monitoring
  5. Handle all error cases gracefully
  6. Close the router when done

Next Steps