Multi-Broker Integration¶
The platform is designed from the ground up for multi-broker support. Every broker — traditional securities (Shioaji for Taiwan futures), cryptocurrency exchanges (Gate.io), or mock simulators — implements the same abstract interface and returns the same Pydantic response models. The factory pattern uses auto-discovery: adding a new broker means creating a single Python file in the right directory. Zero changes to the factory, API layer, or any other file. A new broker integration takes 2–3 days of implementation effort.
Abstract Interface¶
Every broker must implement the BrokerService abstract class:
from abc import ABC, abstractmethod
class BrokerService(ABC):
"""Abstract base for all broker integrations."""
def __init__(self, credentials, simulation=True, broker_type=""):
self._credentials = credentials
self._simulation = simulation
self._broker_type = broker_type
self._connected = False
@abstractmethod
def connect(self) -> ConnectionResult:
"""Authenticate and initialize broker session."""
...
@abstractmethod
def disconnect(self) -> DisconnectionResult:
"""Clean close, release resources."""
...
@abstractmethod
def get_symbols(self) -> NormalizedSymbolsResponse:
"""All tradeable symbols (flat list + grouped by category)."""
...
@abstractmethod
def get_positions(self) -> PositionsResponse:
"""Current open positions with P&L."""
...
@abstractmethod
def place_entry_order(self, symbol, quantity, action) -> OrderResult:
"""Open or add to a position."""
...
@abstractmethod
def place_exit_order(self, symbol, direction) -> OrderResult | None:
"""Close position by direction (long/short)."""
...
@abstractmethod
def close_position(self, symbol) -> OrderResult | None:
"""Auto-detect direction, reduce-only close."""
...
@abstractmethod
def check_order_status(self, order_result) -> OrderStatusResult:
"""Check fill status via cached broker reference (fast)."""
...
@abstractmethod
def check_order_status_by_id(self, order_id) -> OrderStatusResult | None:
"""Check fill status via order ID (slower, scans trade list)."""
...
@abstractmethod
def verify_credentials(self, verify_ca=False) -> CredentialVerificationResult:
"""Test login without creating a persistent worker session."""
...
Concrete Methods (Inherited)¶
The base class provides several concrete methods that all brokers inherit:
| Method | Purpose |
|---|---|
place_entry_order_with_reversal(symbol, qty, action) |
Close opposite position first, then open new one |
get_position(symbol) |
Find position by symbol or contract code |
get_position_quantity(symbol) |
Signed quantity (+ long, - short) |
has_position(symbol) |
Boolean check for open position |
get_position_direction(symbol) |
Returns "long", "short", or None |
get_code_to_symbol_mapping() |
Map broker contract codes to user-friendly symbols |
is_connection_error(error) |
Classify error for reconnection decisions |
is_order_error(error) |
Classify error as order-related (no reconnect needed) |
Standardized Response Models¶
Every method returns Pydantic models — never raw dicts, tuples, or broker-specific types. This is what makes the rest of the system broker-agnostic. The file app/services/brokers/models.py defines the complete contract.
Why Pydantic?
Pydantic models give us automatic JSON serialization, validation, and type safety. The API layer can return any broker's response directly as JSON without transformation. The frontend receives the same shape regardless of whether the order went to a Taiwan futures broker or a crypto exchange.
NormalizedSymbol¶
A universal symbol representation that works across all markets (futures, stocks, crypto) and all brokers. Core fields are required; market-specific fields are optional.
| Field | Type | Required | Description |
|---|---|---|---|
code |
str | Yes | Primary identifier: TXFR1, AAPL, BTC_USDT |
name |
str | Yes | Display name: 台指期近月, Apple Inc, Bitcoin/USDT |
market |
MarketType | Yes | Market type: futures, stock, crypto |
category |
str | Yes | Category code: TXF, tech, defi |
category_name |
str | Yes | Category display: 台指期貨, Technology, DeFi |
base_symbol |
str | No | Underlying: TXF, BTC, None for stocks |
base_currency |
str | No | Base currency (crypto/forex): BTC, EUR |
quote_currency |
str | No | Quote currency: USDT, USD |
expiry_date |
str | No | ISO date for derivatives: 2025-03-19 |
strike_price |
float | No | Options strike price |
exchange |
str | No | Exchange: TAIFEX, NYSE, Gate.io |
tick_size |
float | No | Minimum price increment |
lot_size |
float | No | Minimum quantity increment |
contract_size |
float | No | Contract multiplier (e.g., 0.001 BTC per contract) |
min_order_size |
float | No | Minimum order size |
max_order_size |
float | No | Maximum order size |
leverage_min |
float | No | Min leverage (crypto) |
leverage_max |
float | No | Max leverage (crypto) |
broker_symbol |
str | No | Original broker symbol if different from code |
Cross-market examples:
| Field | Taiwan Futures | Crypto Perpetual | US Stock |
|---|---|---|---|
code |
TXFR1 |
BTC_USDT |
AAPL |
name |
台指期近月 |
BTC/USDT Perp |
Apple Inc |
market |
futures |
crypto |
stock |
category |
TXF |
layer1 |
tech |
base_currency |
— | BTC |
— |
quote_currency |
— | USDT |
— |
expiry_date |
2025-03-19 |
— | — |
exchange |
TAIFEX |
Gate.io |
NYSE |
contract_size |
200 (TWD/pt) |
0.001 (BTC) |
1 |
leverage_max |
— | 100 |
— |
The NormalizedSymbolsResponse wraps the flat list with hierarchical grouping metadata (markets → categories) for UI organization, plus summary statistics. The from_symbols() class method auto-computes groupings.
Position¶
A trading position held in the account. Core fields work for all brokers; crypto-specific fields (margin, leverage, liquidation) are optional.
| Field | Type | Required | Description |
|---|---|---|---|
code |
str | Yes | Contract code: TXFB5, BTC_USDT |
name |
str | No | Human-readable: 臺指期貨, BTC/USDT Perp |
quantity |
int | Yes | Absolute size (always positive) |
direction |
PositionDirection | Yes | long or short |
entry_price |
float | Yes | Average entry price |
last_price |
float | Yes | Current market price |
unrealized_pnl |
float | Yes | Unrealized P&L |
realized_pnl |
float | No | Realized P&L |
market_value |
float | No | Current market value |
contract_multiplier |
float | No | Contract multiplier for P&L |
margin_mode |
str | No | Crypto: cross or isolated |
leverage |
int | No | Crypto: leverage multiplier |
liquidation_price |
float | No | Crypto: estimated liquidation |
mark_price |
float | No | Crypto: mark price |
Computed properties:
| Property | Return | Logic |
|---|---|---|
signed_quantity |
int | +quantity if long, -quantity if short |
is_long / is_short |
bool | Direction check |
price_change |
float | last_price - entry_price |
price_change_percent |
float | (price_change / entry_price) * 100 |
calculate_pnl(multiplier) |
float | price_change * quantity * multiplier (flipped for shorts) |
The PositionsResponse wrapper provides get_position_by_code(), get_long_positions(), get_short_positions(), and total_unrealized_pnl. The signed_quantity property is critical for the auto-reversal logic — it determines how many contracts to close before placing the new entry.
OrderResult¶
Returned immediately after order submission. Contains the order ID for tracking, auto-reversal metadata, and a broker-specific reference for status polling.
| Field | Type | Required | Description |
|---|---|---|---|
success |
bool | Yes | Whether order was accepted |
order_id |
str | Yes | Unique order identifier |
symbol |
str | Yes | Trading symbol |
code |
str | No | Contract code |
action |
BrokerAction | Yes | buy or sell |
quantity |
int | Yes | Order quantity |
status |
OrderStatus | Yes | pending, submitted, filled, cancelled, failed |
message |
str | No | Status message or error |
broker_order_ref |
Any | No | Broker-specific object for status checks (excluded from JSON) |
broker_refs |
dict | No | Broker-specific IDs (e.g., Shioaji: {"seqno": "...", "ordno": "..."}) |
auto_exit |
bool | No | Whether order auto-closed opposite position |
exit_quantity |
int | No | Quantity of opposite position closed |
position_before |
int | No | Position quantity before order |
exit_order_result |
OrderResult | No | Nested result for the close leg of a reversal |
fill_price |
float | No | Average fill price (populated after status check) |
reduce_only |
bool | No | Whether order can only close positions |
Auto-Reversal Metadata
When a long_entry signal triggers an auto-reversal of a short position, the OrderResult captures the full context: auto_exit=true, exit_quantity=2 (contracts closed), position_before=-2 (was short 2), and exit_order_result contains the separate close order's result. This metadata is stored in order_history for audit and P&L tracking.
OrderStatusResult¶
Detailed execution status including partial fills. Used by the background fill verification Lambda.
| Field | Type | Required | Description |
|---|---|---|---|
order_id |
str | Yes | Order identifier |
broker_refs |
dict | No | Broker-specific IDs for matching |
status |
OrderStatus | Yes | Current status |
order_quantity |
int | Yes | Original quantity |
filled_quantity |
int | Yes | Quantity filled so far |
cancelled_quantity |
int | No | Quantity cancelled |
remaining_quantity |
int | No | Quantity remaining |
average_fill_price |
float | Yes | Average fill price |
deals |
list[OrderDeal] | No | Individual fills (deal_id, price, quantity, timestamp) |
is_complete |
bool | Computed | True if filled, cancelled, rejected, or failed |
fill_rate |
float | Computed | filled_quantity / order_quantity * 100 |
Model Summary¶
| Model | Purpose | Consumer |
|---|---|---|
NormalizedSymbol |
Universal symbol across all markets | Symbol browser, order forms, strategy builder |
NormalizedSymbolsResponse |
Grouped symbols with market hierarchy | Frontend symbol picker |
Position |
Current position with signed quantity | Dashboard, auto-reversal logic |
PositionsResponse |
Position list with query helpers | Account view |
OrderResult |
Immediate order confirmation + reversal metadata | Order confirmation, webhook response, audit log |
OrderStatusResult |
Detailed fill status with individual deals | Background Lambda fill verification |
OrderDeal |
Single fill execution | OrderStatusResult child |
ConnectionResult |
Broker connection status | Worker lifecycle |
CredentialVerificationResult |
Credential validation status | Credential setup flow |
BrokerCredentials |
Base credential model (api_key, secret_key) | Worker credential loading |
Database Schema¶
The broker system is backed by a normalized schema that separates broker definitions, type definitions, and per-user trading accounts. This design allows adding new brokers and types purely through database inserts — no migrations needed.
erDiagram
User ||--o{ TradingAccount : "has many"
Broker ||--o{ BrokerSupportedType : "supports"
BrokerType ||--o{ BrokerSupportedType : "supported by"
Broker ||--o{ TradingAccount : "used by"
BrokerType ||--o{ TradingAccount : "used by"
TradingAccount ||--o{ OrderHistory : "has many"
Broker {
int id PK
string code UK "shioaji, gate"
string name "Sinopac Shioaji"
boolean is_active
}
BrokerType {
int id PK
string code UK "future, future_usdt"
string name "Futures and Options"
boolean is_active
}
BrokerSupportedType {
int broker_id FK
int broker_type_id FK
jsonb credential_schema "form config"
boolean testnet_supported
}
TradingAccount {
int id PK
int user_id FK
int broker_id FK
int broker_type_id FK
string name "Main Trading"
string webhook_token UK
string webhook_secret
binary user_encryption_key "per-account"
binary api_key_encrypted
binary secret_key_encrypted
boolean is_verified
boolean is_default
}
OrderHistory {
int id PK
int trading_account_id FK
string action "long_entry"
string symbol "TXFR1"
string status "filled"
datetime created_at
}
Key Design Decisions¶
| Decision | Rationale |
|---|---|
| Broker and BrokerType are lookup tables | New brokers and types are added via DB insert, not code changes. The admin panel can manage them. |
| BrokerSupportedType is an association model | Not just a join table — stores credential_schema (JSONB) that drives the frontend's dynamic credential form for each broker+type pair, and testnet_supported flag. |
| Per-account encryption keys | Each TradingAccount has its own AES-256 key (user_encryption_key), encrypted with the master key. Compromising one account's key reveals nothing about others. |
| Per-account webhook tokens | Each account gets a unique webhook_token in the URL path. One user with 3 accounts gets 3 independent webhook URLs. |
| UniqueConstraint on (user, broker, broker_type) | A user can have at most one account per broker+type combination (e.g., one Shioaji futures account, one Gate.io USDT account). |
| Credentials nullable | Account shell is created first (name, broker, type), credentials added separately. This supports a two-step onboarding flow. |
Example Data¶
| Table | code | name |
|---|---|---|
brokers |
shioaji |
Sinopac Shioaji |
brokers |
gate |
Gate.io |
broker_types |
future |
Futures & Options |
broker_types |
future_usdt |
USDT Perpetual Futures |
broker_types |
future_btc |
BTC Perpetual Futures |
broker_supported_types |
shioaji + future | credential_schema: {simulation_fields: [...], real_trading_fields: [...]} |
broker_supported_types |
gate + future_usdt | testnet_supported: true |
broker_supported_types |
gate + future_btc | testnet_supported: false |
Factory Auto-Discovery¶
flowchart TB
A["get_broker_service('shioaji', 'future')"] --> B{"In-memory<br/>cache?"}
B -->|Hit| RET["Return class"]
B -->|Miss| C{"Redis cache?<br/>7-day TTL"}
C -->|Hit| CACHE1["Cache in-memory"] --> RET
C -->|Miss| D["importlib.import_module"]
D --> E["inspect for BrokerService subclass"]
E --> CACHE2["Cache in Redis + memory"] --> RET
The factory follows a file convention to discover broker implementations:
Each file must contain exactly one concrete BrokerService subclass. The factory:
- Checks the in-memory dict (per-process, instant lookup)
- Checks Redis cache (shared across processes, 7-day TTL)
- Falls back to full discovery via
importlib+inspect(writes to both caches)
service = get_broker_service(
broker_name="shioaji", # → brokers/shioaji/
broker_type="future", # → brokers/shioaji/future.py
credentials=creds, # BrokerCredentials or dict
simulation=True, # Testnet/sandbox mode
)
No factory registration needed
When you create app/services/brokers/ib/stock.py with a class IBStockBrokerService(BrokerService), calling get_broker_service("ib", "stock", creds) will automatically discover and instantiate it. No imports, no registry, no configuration file.
Current Implementations¶
| Broker | File | Markets | API | Notes |
|---|---|---|---|---|
| Shioaji | shioaji/future.py |
Taiwan futures & options | Shioaji SDK | Session-based, CA certificate for production |
| Gate.io BTC | gate/future_btc.py |
BTC-margined perpetual futures | REST + WebSocket | One-way position mode only |
| Gate.io USDT | gate/future_usdt.py |
USDT-margined perpetual futures | REST + WebSocket | One-way position mode only |
| Mock | mock/mock.py |
Simulated | None | For testing and demo accounts |
Directory Structure¶
app/services/brokers/
├── base.py # Abstract BrokerService + reversal logic
├── models.py # Standardized Pydantic response models
├── crypto_mixin.py # Shared crypto exchange utilities
├── __init__.py # Factory + auto-discovery + caching
│
├── shioaji/ # Taiwan futures broker
│ ├── __init__.py
│ ├── base.py # Shared Shioaji login/session logic
│ ├── future.py # ShioajiFutureBrokerService
│ ├── models.py # ShioajiCredentials
│ ├── contracts.py # Contract loading/filtering helpers
│ ├── constants.py # Market hours, symbol codes
│ └── exceptions.py # ShioajiError, ShioajiConnectionError
│
├── gate/ # Gate.io crypto exchange
│ ├── base.py # Shared Gate.io connection logic
│ ├── futures_base.py # Shared futures order/position logic
│ ├── future_btc.py # GateBTCFutureBrokerService
│ ├── future_usdt.py # GateUSDTFutureBrokerService
│ ├── models.py # GateCredentials
│ ├── categories.py # Contract categorization
│ └── exceptions.py # GateError, GatePositionModeError
│
└── mock/ # Simulation broker
├── __init__.py
└── mock.py # MockBrokerService
Auto-Reversal Logic¶
Built into the base class, place_entry_order_with_reversal() handles the common case where a signal flips direction. This mirrors TradingView's strategy.entry() behavior where entering a long position automatically exits any existing short.
sequenceDiagram
participant Signal as Trading Signal
participant Base as BrokerService (base)
participant Broker as Concrete Broker
Signal->>Base: place_entry_order_with_reversal<br/>(symbol="TXFR1", qty=1, action="buy")
Base->>Base: Check current position
Note over Base: Has short position: -2 contracts
Base->>Broker: close_position("TXFR1")<br/>→ buy 2 (reduce_only, Cover)
Broker-->>Base: OrderResult (success)
loop Poll fill status (up to 10 × 0.5s)
Base->>Broker: check_order_status(order)
Broker-->>Base: status: filled
end
Base->>Broker: place_entry_order("TXFR1", 1, "buy")
Broker-->>Base: OrderResult (success)
Base-->>Signal: OrderResult<br/>auto_exit=true, exit_quantity=2,<br/>position_before=-2
Reversal Flow¶
Current position: -2 (short) Incoming signal: buy 1
Step 1: close_position() → buy 2 (reduce_only / Cover)
Step 2: Poll fill status (10 attempts × 0.5s intervals)
Step 3: place_entry_order() → buy 1 (new long)
Result: position changes from -2 to +1
Reversal timeout
If the close order doesn't fill within 10 status checks (5 seconds), the method aborts and returns a failure with "Manual intervention may be required". This prevents the system from opening a new position while the old one is still open — which would double the exposure.
Adding a New Broker¶
Step-by-Step¶
-
Create the directory:
-
Create the broker service:
# app/services/brokers/ib/stock.py from app.services.brokers.base import BrokerService from app.services.brokers.models import ( ConnectionResult, DisconnectionResult, NormalizedSymbolsResponse, PositionsResponse, OrderResult, OrderStatusResult, CredentialVerificationResult, ) class IBStockBrokerService(BrokerService): @property def broker_name(self) -> str: return "ib" def __init__(self, credentials, simulation=True, broker_type="stock"): super().__init__(credentials, simulation, broker_type) def connect(self) -> ConnectionResult: # Connect to IB TWS/Gateway ... def get_symbols(self) -> NormalizedSymbolsResponse: # Return NormalizedSymbol list ... def place_entry_order(self, symbol, qty, action) -> OrderResult: # Place order via IB API, return OrderResult ... # ... implement all abstract methods -
Optional — credential model:
-
Optional — broker-specific exceptions:
-
Done. Call
get_broker_service("ib", "stock", creds)— auto-discovery handles the rest.
Checklist¶
- [ ] Class inherits from
BrokerService - [ ] All abstract methods implemented
- [ ] Every method returns standardized Pydantic models
- [ ]
super().__init__()called in constructor - [ ] Exactly one concrete
BrokerServicesubclass per file - [ ]
is_connection_error()overridden for broker-specific error patterns - [ ] Simulation mode connects to testnet/sandbox endpoint
- [ ]
broker_order_refstored inOrderResultfor fast status checks
Error Classification¶
The worker uses error classification to decide whether to reconnect or return an error:
| Error Type | Action | Examples |
|---|---|---|
| Connection error | Invalidate connection, retry request | Timeout, connection refused, token expired, 401 |
| Order error | Return error to caller, no reconnect | Insufficient margin, market closed, invalid symbol |
| Unknown error | Log, return error, consider reconnect | Unexpected exceptions |
if broker_class.is_connection_error(e):
worker._invalidate_connection(conn_key)
# Request retries with fresh connection (up to 3 times)
elif broker_class.is_order_error(e):
return TradingResponse(success=False, error=str(e))
# No reconnect — the error is with the order, not the connection
Subclasses override is_connection_error() and is_order_error() to classify broker-specific error types. The base class provides sensible defaults (checking for patterns like "timeout", "connection refused", "token expired", "401").
File Map¶
| Concept | File |
|---|---|
| Abstract base class + reversal logic | app/services/brokers/base.py |
| Factory + auto-discovery + caching | app/services/brokers/__init__.py |
| Standardized response models | app/services/brokers/models.py |
| Shioaji futures implementation | app/services/brokers/shioaji/future.py |
| Shioaji shared login logic | app/services/brokers/shioaji/base.py |
| Gate.io shared futures logic | app/services/brokers/gate/futures_base.py |
| Gate.io BTC-margined futures | app/services/brokers/gate/future_btc.py |
| Gate.io USDT-margined futures | app/services/brokers/gate/future_usdt.py |
| Mock broker for testing | app/services/brokers/mock/mock.py |
| Crypto exchange mixin | app/services/brokers/crypto_mixin.py |