FiveTwenty SDK Best Practices¶
Essential patterns and practices for building robust applications with the FiveTwenty SDK.
Client Architecture Patterns¶
Context Manager Usage¶
Always use context managers for proper resource cleanup:
import os
from fivetwenty import AsyncClient, Client, Environment
# Step 1: Configure authentication and account parameters
# Environment variables provide secure token management
token = os.getenv("OANDA_TOKEN") # API token from environment for security
account_id = "101-001-0000000-001" # Replace with your OANDA account ID
# Step 2: AsyncClient usage with proper resource management
async def example_async():
"""Demonstrate AsyncClient usage with automatic resource cleanup."""
# Context manager ensures proper connection cleanup and resource management
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
# Step 3: HTTP connections and session cleanup handled automatically
# AsyncClient manages connection pooling and graceful shutdown
print(f"Config AsyncClient initialized with practice environment")
print(f"Global HTTP session created with connection pooling")
# Step 4: Execute API call with automatic error handling
account = await client.accounts.get_account(account_id)
print(f"Success Account retrieved: {account.id}")
return account
# Note: Client automatically cleaned up on exit from context manager
# Step 5: Sync Client usage for non-async applications
def example_sync():
"""Demonstrate sync Client usage with background thread management."""
# Context manager handles background asyncio thread and bounded queues
with Client(token=token, environment=Environment.PRACTICE) as client:
# Step 6: Background thread and queue management handled automatically
# Sync client runs AsyncClient in background thread with queue-based communication
print(f"Config Sync Client initialized with background thread")
print(f"Satellite Bounded queues prevent memory overflow in sync wrapper")
# Step 7: Synchronous API call - internally queued to async thread
account = client.accounts.get_account(account_id)
print(f"Success Account retrieved synchronously: {account.id}")
return account
# Note: Background thread and queues automatically cleaned up on exit
Connection Reuse¶
Reuse client instances across multiple operations:
import os
from fivetwenty import AsyncClient, Environment
# Step 1: Configure authentication parameters
token = os.getenv("OANDA_TOKEN") # API token from environment
account_id = "101-001-0000000-001" # Replace with your OANDA account ID
# Step 2: Success Good: Reuse client for multiple operations (RECOMMENDED)
async def good_example():
"""Optimal pattern: Single client instance for multiple operations."""
# Single context manager for all related operations
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
print(f"Config Single client instance created for multiple operations")
print(f"Global HTTP connection pool shared across all API calls")
# Step 3: Execute multiple operations using same client and connections
# Connection pooling provides significant performance benefits
account = await client.accounts.get_account(account_id) # Reuses connection
positions = await client.positions.get_positions(account_id) # Reuses connection
orders = await client.orders.get_orders(account_id) # Reuses connection
print(f"Success Retrieved account, positions, and orders using shared connections")
print(f"Lightning Connection reuse eliminates handshake overhead")
return account, positions, orders
# Single cleanup for all operations
# Step 4: Error Bad: Create new client for each operation (AVOID)
async def get_account():
"""Anti-pattern: New client instance per operation wastes resources."""
# New context manager creates unnecessary overhead
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
print(f"⚠️ New client created for single operation")
print(f"💸 Connection overhead: handshake, auth, session setup")
return await client.accounts.get_account(account_id)
# Immediate cleanup - connections destroyed and recreated for each call
# NOTE: This pattern multiplies network overhead and reduces performance
Concurrent Operations¶
Use asyncio.gather() for concurrent API calls:
import asyncio
import os
from fivetwenty import AsyncClient, Environment
# Step 1: Configure authentication parameters
token = os.getenv("OANDA_TOKEN") # API token from environment
account_id = "101-001-0000000-001" # Replace with your OANDA account ID
async def concurrent_example() -> None:
"""Demonstrate concurrent vs sequential API operations for optimal performance."""
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
print(f"Starting Starting concurrent operations demonstration...")
# Step 2: Success Efficient: Concurrent requests using asyncio.gather
# Multiple API calls execute simultaneously, reducing total latency
start_time = asyncio.get_event_loop().time()
account, positions, orders = await asyncio.gather(
client.accounts.get_account(account_id), # Executes concurrently
client.positions.get_positions(account_id), # Executes concurrently
client.orders.get_orders(account_id) # Executes concurrently
)
concurrent_time = asyncio.get_event_loop().time() - start_time
print(f"Lightning Concurrent execution: {concurrent_time:.3f} seconds")
print(f"Global Three API calls executed simultaneously")
# Step 3: Error Inefficient: Sequential requests (for comparison)
# Each API call waits for previous to complete - multiplies latency
start_time = asyncio.get_event_loop().time()
account = await client.accounts.get_account(account_id) # Waits for completion
positions = await client.positions.get_positions(account_id) # Waits for completion
orders = await client.orders.get_orders(account_id) # Waits for completion
sequential_time = asyncio.get_event_loop().time() - start_time
print(f"🐌 Sequential execution: {sequential_time:.3f} seconds")
print(f"Wait Each API call blocks the next")
# Step 4: Display performance comparison
performance_gain = (sequential_time - concurrent_time) / sequential_time * 100
print(f"\nData Performance Analysis:")
print(f" Concurrent: {concurrent_time:.3f}s")
print(f" Sequential: {sequential_time:.3f}s")
print(f" Improvement: {performance_gain:.1f}% faster with concurrency")
print(f"Note Use asyncio.gather() for independent API calls")
return account, positions, orders
Financial Precision¶
Decimal Usage¶
Always use Decimal for financial calculations:
from decimal import Decimal
# Example account balance (typically from OANDA API)
account_balance = Decimal("10000.00")
# Good: Exact precision
position_size = Decimal(10000)
risk_amount = account_balance * Decimal("0.02") # 2% risk
stop_distance = Decimal("0.0050") # 50 pips
# Bad: Floating point errors (commented out)
# position_size = 10000.0
# risk_amount = account_balance * Decimal("0.02")
# stop_distance = 0.0050
Price Calculation Precision¶
Handle OANDA's price precision requirements:
from decimal import Decimal
# OANDA price precision (5 decimal places for majors)
price = Decimal("1.08456").quantize(Decimal("0.00001"))
# Position sizing with exact arithmetic
def calculate_position_size(
account_balance: str, # AccountUnits from OANDA
risk_percentage: Decimal,
stop_loss_pips: int,
pip_value: Decimal,
) -> Decimal:
"""Calculate position size with exact precision."""
balance = Decimal(account_balance)
risk_amount = balance * (risk_percentage / 100)
risk_per_unit = stop_loss_pips * pip_value
position_size = risk_amount / risk_per_unit
return position_size.quantize(Decimal(1)) # Round to whole units
Field Type Handling¶
FiveTwenty automatically converts between Decimal and string fields:
import os
from decimal import Decimal
from fivetwenty import AsyncClient, Environment
# Setup
token = os.getenv("OANDA_TOKEN")
account_id = "101-001-0000000-001"
async def example_order() -> tuple[object, object]:
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
# SDK handles conversion automatically
order = await client.orders.post_limit_order(
account_id=account_id,
instrument="EUR_USD",
units=Decimal(10000), # Converted to string for API
price=Decimal("1.0850") # Converted to string for API
)
# Response fields are properly typed
filled_price = Decimal(order.order_fill_transaction.price) # string -> Decimal
return order, filled_price
Error Handling Patterns¶
Exception Hierarchy¶
Use specific exception types for targeted handling:
import asyncio
import os
from fivetwenty import AsyncClient, Environment
from fivetwenty.exceptions import FiveTwentyError
# Step 1: Configure authentication parameters
token = os.getenv("OANDA_TOKEN") # API token from environment
account_id = "101-001-0000000-001" # Replace with your OANDA account ID
async def example_error_handling() -> None:
"""Demonstrate comprehensive error handling for robust trading applications."""
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
try:
# Step 2: Attempt order placement with potential for various errors
print(f"Analysis Attempting market order placement...")
order = await client.orders.post_market_order(
account_id=account_id,
instrument="EUR_USD",
units=1000
)
print(f"Success Order placed successfully: {order.id}")
# Step 3: Handle specific OANDA API errors with targeted responses
except FiveTwentyError as e:
error_message = str(e)
print(f"⚠️ OANDA API Error: {error_message}")
# Step 4: Handle insufficient margin errors
if "INSUFFICIENT_MARGIN" in error_message:
print(f"Balance Insufficient margin detected")
print(f"Config Reducing position size and retrying...")
await handle_margin_error(e)
# Step 5: Handle invalid instrument errors
elif "INVALID_INSTRUMENT" in error_message:
print(f"⚠️ Invalid instrument: EUR_USD not available")
print(f"⏭️ Skipping this instrument and continuing...")
return None
# Step 6: Handle rate limiting errors
elif "RATE_LIMIT" in error_message or "TOO_MANY_REQUESTS" in error_message:
print(f"Time Rate limit exceeded - respecting API limits")
retry_after = getattr(e, 'retry_after', 60) # Default 60 seconds
print(f"Wait Waiting {retry_after} seconds before retry...")
await asyncio.sleep(retry_after)
# Step 7: Handle server errors with exponential backoff
elif "INTERNAL_SERVER_ERROR" in error_message or "503" in error_message:
print(f"Tools OANDA server error detected")
print(f"Processing Implementing exponential backoff retry...")
await retry_with_backoff()
else:
print(f"Error Unhandled OANDA error: {error_message}")
raise # Re-raise unhandled errors
# Step 8: Handle network and connection errors
except ConnectionError as e:
print(f"Global Network connection error: {e}")
print(f"Config Check internet connectivity and retry...")
await asyncio.sleep(5) # Brief pause before potential retry
# Step 9: Handle authentication errors
except PermissionError as e:
print(f"Secure Authentication error: {e}")
print(f"Note Check API token validity and permissions")
raise # Don't retry auth errors
return order
async def handle_margin_error(error) -> None:
"""Handle insufficient margin errors with position size reduction."""
print(f"Tools Implementing margin error recovery strategy...")
print(f"📉 Strategy: Reduce position size by 50%")
print(f"Data Strategy: Check account balance and available margin")
# Implementation would include account balance checks and position adjustment
async def retry_with_backoff() -> None:
"""Implement exponential backoff retry for server errors."""
print(f"Processing Implementing exponential backoff retry strategy...")
print(f"Time Delay pattern: 1s, 2s, 4s, 8s, 16s...")
print(f"🎲 Adding jitter to prevent thundering herd")
# Implementation would include actual retry logic with increasing delays
Retry Patterns¶
Implement exponential backoff for retryable errors:
import asyncio
import secrets
from collections.abc import Callable
from typing import Any
from fivetwenty.exceptions import FiveTwentyError
async def retry_with_backoff(
operation: Callable[[], Any],
max_retries: int = 3,
base_delay: float = 1.0
) -> Any:
"""Retry operation with exponential backoff."""
for attempt in range(max_retries):
try:
return await operation()
except FiveTwentyError as e:
if not (e.is_server_error or e.is_rate_limited):
raise
if attempt == max_retries - 1:
raise
# Exponential backoff with jitter
delay = base_delay * (2 ** attempt)
# Use secrets module for cryptographically secure random
jitter = secrets.randbelow(int(0.3 * delay * 1000)) / 1000
await asyncio.sleep(delay + jitter)
return None # Explicit return for all code paths
Streaming Best Practices¶
Stream Resource Management¶
Properly handle streaming connections:
import asyncio
from collections.abc import AsyncIterator
from fivetwenty import AsyncClient
from fivetwenty.exceptions import StreamStall
async def robust_price_stream(
client: AsyncClient,
account_id: str,
instruments: list[str]
) -> AsyncIterator:
"""Streaming with proper error handling."""
max_retries = 5
retry_count = 0
while retry_count < max_retries:
try:
async for price in client.pricing.get_pricing_stream(
account_id=account_id,
instruments=instruments
):
# Reset retry count on successful data
retry_count = 0
yield price
except StreamStall:
retry_count += 1
if retry_count >= max_retries:
raise
await asyncio.sleep(2 ** retry_count) # Exponential backoff
Backpressure Management¶
Handle fast-moving data appropriately:
import os
from fivetwenty import AsyncClient, Client, Environment
# Setup
token = os.getenv("OANDA_TOKEN")
account_id = "101-001-0000000-001"
async def async_stream_example() -> None:
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
# AsyncClient: Direct processing (no buffering)
async for price in client.pricing.get_pricing_stream(
account_id=account_id,
instruments=["EUR_USD"]
):
# Process immediately - don't block the stream
await process_price_async(price)
def sync_stream_example() -> None:
with Client(token=token, environment=Environment.PRACTICE) as client:
# Sync Client: Bounded queues prevent memory issues
for price in client.pricing.get_pricing_stream(
account_id=account_id,
instruments=["EUR_USD"]
):
# Queue automatically manages backpressure
process_price_sync(price)
async def process_price_async(price: object) -> None:
"""Process price data asynchronously."""
pass
def process_price_sync(price: object) -> None:
"""Process price data synchronously."""
pass
Environment Management¶
Environment-Specific Configuration¶
Use different settings for practice vs live:
import os
from fivetwenty import AsyncClient, Environment
def get_token(is_live: bool) -> str:
"""Get appropriate token for environment."""
if is_live:
return os.environ["OANDA_LIVE_TOKEN"]
return os.environ["OANDA_PRACTICE_TOKEN"]
def create_client(is_live: bool = False) -> AsyncClient:
"""Create client with environment-appropriate settings."""
env = Environment.LIVE if is_live else Environment.PRACTICE
timeout = 30.0 if is_live else 10.0 # Longer timeout for live
return AsyncClient(
token=get_token(is_live),
environment=env,
timeout=timeout
)
Token Security¶
Never hardcode tokens or log sensitive data:
import os
import logging
logger = logging.getLogger(__name__)
# Good: Environment variables
token = os.environ["OANDA_TOKEN"]
# Good: Masked logging
logger.info(f"Using token: {token[:8]}...")
# Bad: Hardcoded token
token = "your-actual-token-here"
# Bad: Token in logs
logger.info(f"Token: {token}")
Performance Optimization¶
Instrument Selection¶
Only stream instruments you actively use:
# Good: Specific instruments only
instruments = ["EUR_USD", "GBP_USD"] # Only what you need
# Bad: Too many instruments - undefined variables example
# This would cause undefined variable errors:
# bases = ["EUR", "GBP", "USD", "JPY", "CHF", "CAD", "AUD", "NZD"]
# quotes = ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD"]
# instruments = [f"{base}_{quote}" for base in bases for quote in quotes]
Request Batching¶
Batch related operations when possible:
import os
from fivetwenty import AsyncClient, Environment
# Setup
token = os.getenv("OANDA_TOKEN")
account_id = "101-001-0000000-001"
async def batch_pricing_example() -> tuple[object, object, object, object]:
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
# Good: Single request for multiple instruments
prices = await client.pricing.get_pricing(
account_id=account_id,
instruments=["EUR_USD", "GBP_USD", "USD_JPY"]
)
# Less efficient: Multiple requests
eur_usd = await client.pricing.get_pricing(account_id, ["EUR_USD"])
gbp_usd = await client.pricing.get_pricing(account_id, ["GBP_USD"])
usd_jpy = await client.pricing.get_pricing(account_id, ["USD_JPY"])
return prices, eur_usd, gbp_usd, usd_jpy
Common Anti-Patterns¶
Avoid These Patterns¶
import os
import time
from fivetwenty import AsyncClient, Environment
# Setup
token = os.getenv("OANDA_TOKEN")
account_id = "101-001-0000000-001"
# Error Creating clients in loops
async def bad_pattern_example():
symbols = ["EUR_USD", "GBP_USD"]
for symbol in symbols:
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client: # Expensive!
_price = await client.pricing.get_pricing(account_id, [symbol])
# Error Blocking operations in async context
async def bad_async():
time.sleep(1) # Blocks entire event loop
# Error Float arithmetic for money
profit_loss = 1234.56 + 0.1 # Precision errors!
# Error Ignoring error details
async def bad_error_handling():
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
try:
_order = await client.orders.post_market_order(
account_id=account_id,
instrument="EUR_USD",
units=1000
)
except Exception:
pass # Lost important error information
# Error Not handling rate limits
async def bad_rate_limit_handling():
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
while True:
await client.accounts.get_account(account_id) # Will hit rate limits
Correct Alternatives¶
import asyncio
import logging
import os
from decimal import Decimal
from fivetwenty import AsyncClient, Environment
from fivetwenty.exceptions import FiveTwentyError
# Step 1: Configure structured logging for production applications
logger = logging.getLogger(__name__)
# Step 2: Configure authentication parameters securely
token = os.getenv("OANDA_TOKEN") # Secure token from environment
account_id = "101-001-0000000-001" # Replace with your account ID
# Step 3: Success Reuse client across operations for optimal performance
async def good_pattern_example():
"""Demonstrate optimal client reuse pattern for multiple operations."""
symbols = ["EUR_USD", "GBP_USD"]
print(f"Success Best Practice: Single client for multiple operations")
# Single client instance handles all operations efficiently
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
print(f"Config Client created once for {len(symbols)} operations")
for symbol in symbols:
# Each operation reuses the same HTTP connections
_price = await client.pricing.get_pricing(account_id, [symbol])
print(f"Balance Retrieved pricing for {symbol} using shared connection")
print(f"Lightning Connection reuse provides significant performance benefit")
# Step 4: Success Async operations in async context for non-blocking execution
async def good_async():
"""Demonstrate proper async sleep vs blocking sleep."""
print(f"Success Best Practice: Non-blocking async operations")
print(f"Wait Starting 1-second non-blocking delay...")
await asyncio.sleep(1) # Non-blocking - allows other coroutines to run
print(f"Success Non-blocking delay completed - event loop remained responsive")
# Step 5: Success Decimal arithmetic for financial precision
print(f"Success Best Practice: Decimal arithmetic for financial calculations")
profit_loss = Decimal("1234.56") + Decimal("0.1") # Exact precision: 1234.66
print(f"Balance Precise calculation: 1234.56 + 0.1 = {profit_loss}")
print(f"✨ No floating-point precision errors with Decimal")
# Step 6: Success Specific error handling for robust applications
async def good_error_handling():
"""Demonstrate specific error handling with proper logging."""
print(f"Success Best Practice: Specific error handling with logging")
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
try:
print(f"Analysis Attempting order placement with error handling...")
_order = await client.orders.post_market_order(
account_id=account_id,
instrument="EUR_USD",
units=1000
)
print(f"Success Order placed successfully")
except FiveTwentyError as e:
# Structured logging with specific error details
logger.error(f"Order placement failed: {e}")
print(f"Notes Error logged with specific details for debugging")
print(f"Config Error handling allows graceful recovery")
# Step 7: Success Respect rate limits for API compliance
async def good_rate_limit_handling():
"""Demonstrate proper rate limit handling and compliance."""
print(f"Success Best Practice: Respectful rate limit handling")
async with AsyncClient(token=token, environment=Environment.PRACTICE) as client:
try:
print(f"Global Making API request with rate limit awareness...")
await client.accounts.get_account(account_id)
print(f"Success API request completed successfully")
except FiveTwentyError as e:
if not e.is_rate_limited:
raise
# Honor API rate limits to maintain good standing
retry_after = e.retry_after or 60 # Use server-specified delay or default
print(f"Time Rate limit hit - waiting {retry_after} seconds")
print(f"🤝 Respecting API limits maintains good standing with OANDA")
await asyncio.sleep(retry_after)
print(f"Success Rate limit delay completed - ready for next request")
Order Validation Framework¶
Pre-Order Validation System¶
Implement comprehensive validation to prevent costly trading errors:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Any
from fivetwenty import AsyncClient
class ValidationSeverity(Enum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
@dataclass
class ValidationResult:
is_valid: bool
severity: ValidationSeverity
rule_name: str
message: str
details: dict[str, Any] | None = None
class OrderValidator(ABC):
"""Base class for order validation rules."""
def __init__(self, name: str, severity: ValidationSeverity = ValidationSeverity.ERROR) -> None:
self.name = name
self.severity = severity
self.enabled = True
@abstractmethod
async def validate(self, order_params: dict[str, Any], context: dict[str, Any]) -> ValidationResult:
"""Validate order parameters and return result."""
pass
class OrderValidationFramework:
def __init__(self, client: AsyncClient, account_id: str) -> None:
self.client = client
self.account_id = account_id
self.validators: list[OrderValidator] = []
self.validation_history: list[object] = []
def add_validator(self, validator: OrderValidator) -> None:
"""Add a validator to the framework."""
self.validators.append(validator)
async def validate_order(
self,
order_params: dict[str, Any],
strict_mode: bool = True,
) -> dict[str, Any]:
"""Validate order against all registered validators."""
validation_session = {
"timestamp": datetime.utcnow(),
"order_params": order_params,
"results": [],
"passed": True,
"errors": [],
"warnings": [],
}
# Build context for validators
context = await self._build_validation_context()
# Run all validators
for validator in self.validators:
if not validator.enabled:
continue
try:
result = await validator.validate(order_params, context)
validation_session["results"].append(result)
if not result.is_valid:
if result.severity in [ValidationSeverity.ERROR, ValidationSeverity.CRITICAL]:
validation_session["errors"].append(result)
validation_session["passed"] = False
elif result.severity == ValidationSeverity.WARNING:
validation_session["warnings"].append(result)
if strict_mode:
validation_session["passed"] = False
except Exception as e:
error_result = ValidationResult(
is_valid=False,
severity=ValidationSeverity.CRITICAL,
rule_name=validator.name,
message=f"Validator failed: {e}",
details={"exception": str(e)},
)
validation_session["results"].append(error_result)
validation_session["errors"].append(error_result)
validation_session["passed"] = False
# Store validation history
self.validation_history.append(validation_session)
return validation_session
async def _build_validation_context(self) -> dict[str, Any]:
"""Build context information for validators."""
try:
# Get account information
account = await self.client.accounts.get_account(account_id=self.account_id)
positions = await self.client.positions.get_positions(account_id=self.account_id)
orders = await self.client.orders.get_orders(account_id=self.account_id)
return {
"account": account,
"positions": positions.positions,
"pending_orders": orders.orders,
"current_time": datetime.utcnow(),
"account_balance": Decimal(account.balance),
"margin_available": Decimal(account.margin_available),
"margin_used": Decimal(account.margin_used),
}
except Exception as e:
return {"error": str(e)}
Risk-Based Validators¶
class MaxPositionSizeValidator(OrderValidator):
"""Validate order doesn't exceed maximum position size limits."""
def __init__(self, max_units_per_instrument: int, max_total_exposure: Decimal) -> None:
super().__init__("MaxPositionSize", ValidationSeverity.ERROR)
self.max_units_per_instrument = max_units_per_instrument
self.max_total_exposure = max_total_exposure
async def validate(self, order_params: dict[str, Any], context: dict[str, Any]) -> ValidationResult:
"""Validate position size limits."""
instrument = order_params.get("instrument")
units = int(order_params.get("units", 0))
# Check individual instrument limit
current_position_size = 0
for position in context.get("positions", []):
if position.instrument == instrument:
if position.long.units != "0":
current_position_size += int(position.long.units)
if position.short.units != "0":
current_position_size += abs(int(position.short.units))
new_position_size = current_position_size + abs(units)
if new_position_size > self.max_units_per_instrument:
return ValidationResult(
is_valid=False,
severity=self.severity,
rule_name=self.name,
message=f"Position size {new_position_size} exceeds limit {self.max_units_per_instrument}",
details={
"current_size": current_position_size,
"order_size": abs(units),
"new_size": new_position_size,
"limit": self.max_units_per_instrument
}
)
return ValidationResult(
is_valid=True,
severity=ValidationSeverity.INFO,
rule_name=self.name,
message="Position size validation passed"
)
class RiskPerTradeValidator(OrderValidator):
"""Validate risk per trade doesn't exceed limits."""
def __init__(self, max_risk_per_trade: Decimal) -> None:
super().__init__("RiskPerTrade", ValidationSeverity.WARNING)
self.max_risk_per_trade = max_risk_per_trade
async def validate(self, order_params: dict[str, Any], context: dict[str, Any]) -> ValidationResult:
"""Validate risk per trade."""
units = order_params.get("units", 0)
entry_price = order_params.get("price")
stop_price = order_params.get("stop_loss_price")
if not entry_price or not stop_price:
return ValidationResult(
is_valid=True,
severity=ValidationSeverity.INFO,
rule_name=self.name,
message="No stop loss specified - cannot validate risk"
)
# Calculate risk amount
stop_distance = abs(Decimal(str(entry_price)) - Decimal(str(stop_price)))
risk_amount = abs(units) * stop_distance
account_balance = context.get("account_balance", Decimal("0"))
risk_percentage = risk_amount / account_balance if account_balance > 0 else Decimal("1")
if risk_percentage > self.max_risk_per_trade:
return ValidationResult(
is_valid=False,
severity=self.severity,
rule_name=self.name,
message=f"Risk {risk_percentage:.2%} exceeds limit {self.max_risk_per_trade:.2%}",
details={
"risk_amount": risk_amount,
"risk_percentage": risk_percentage,
"limit": self.max_risk_per_trade,
"account_balance": account_balance
}
)
return ValidationResult(
is_valid=True,
severity=ValidationSeverity.INFO,
rule_name=self.name,
message=f"Risk validation passed: {risk_percentage:.2%}"
)
Production Error Recovery¶
Implement comprehensive error recovery for production systems:
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Optional
import traceback
import asyncio
from fivetwenty.exceptions import FiveTwentyError
class ErrorCategory(Enum):
NETWORK = "network"
AUTHENTICATION = "authentication"
VALIDATION = "validation"
MARKET_DATA = "market_data"
ORDER_EXECUTION = "order_execution"
INSUFFICIENT_FUNDS = "insufficient_funds"
RATE_LIMITING = "rate_limiting"
SYSTEM = "system"
class TradingErrorHandler:
def __init__(self, client: AsyncClient, account_id: str) -> None:
self.client = client
self.account_id = account_id
self.error_handlers = {}
self.error_history = []
self.circuit_breaker_states = {}
def register_error_handler(
self,
error_category: ErrorCategory,
handler: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> None:
"""Register error handler for specific error category."""
self.error_handlers[error_category] = {
"handler": handler,
"max_retries": max_retries,
"retry_delay": retry_delay
}
async def handle_error(
self,
error: Exception,
operation_context: dict[str, Any],
error_category: Optional[ErrorCategory] = None
) -> dict[str, Any]:
"""Handle error with appropriate recovery strategy."""
if not error_category:
error_category = self._categorize_error(error)
# Record error
error_record = {
"timestamp": datetime.utcnow(),
"error_type": type(error).__name__,
"error_message": str(error),
"error_category": error_category,
"operation_context": operation_context,
"stack_trace": traceback.format_exc(),
"recovery_attempted": False,
"recovery_successful": False
}
self.error_history.append(error_record)
# Check circuit breaker
if self._should_circuit_break(error_category):
error_record["circuit_breaker_triggered"] = True
return error_record
# Attempt recovery
if error_category in self.error_handlers:
handler_config = self.error_handlers[error_category]
for attempt in range(handler_config["max_retries"]):
try:
error_record["recovery_attempted"] = True
recovery_result = await handler_config["handler"](
error, operation_context, attempt
)
if recovery_result.get("success", False):
error_record["recovery_successful"] = True
error_record["recovery_result"] = recovery_result
break
await asyncio.sleep(handler_config["retry_delay"] * (2 ** attempt))
except Exception as recovery_error:
error_record["recovery_error"] = str(recovery_error)
return error_record
def _categorize_error(self, error: Exception) -> ErrorCategory:
"""Categorize error based on type and message."""
error_message = str(error).lower()
if "network" in error_message or "connection" in error_message:
return ErrorCategory.NETWORK
if "authentication" in error_message or "unauthorized" in error_message:
return ErrorCategory.AUTHENTICATION
if "insufficient" in error_message and "margin" in error_message:
return ErrorCategory.INSUFFICIENT_FUNDS
if "rate limit" in error_message or "too many requests" in error_message:
return ErrorCategory.RATE_LIMITING
if "validation" in error_message or "invalid" in error_message:
return ErrorCategory.VALIDATION
if isinstance(error, FiveTwentyError):
return ErrorCategory.ORDER_EXECUTION
return ErrorCategory.SYSTEM
def _should_circuit_break(self, error_category: ErrorCategory) -> bool:
"""Determine if circuit breaker should trigger."""
recent_errors = [
err for err in self.error_history[-10:] # Last 10 errors
if err["error_category"] == error_category
and (datetime.utcnow() - err["timestamp"]).seconds < 300 # Last 5 minutes
]
return len(recent_errors) >= 5
Testing Considerations¶
Mock Testing¶
Use proper mocking for unit tests:
from unittest.mock import AsyncMock
import pytest
# Mock response object
mock_response = AsyncMock()
# Example trading function to test
async def trading_function(client, account_id: str, instrument: str, units: int):
return await client.orders.post_market_order(
account_id=account_id,
instrument=instrument,
units=units
)
@pytest.mark.asyncio
async def test_trading_logic():
# Mock the client
mock_client = AsyncMock()
mock_client.orders.post_market_order.return_value = mock_response
# Test your logic
result = await trading_function(mock_client, "101-001-0000000-001", "EUR_USD", 1000)
# Verify calls
mock_client.orders.post_market_order.assert_called_once_with(
account_id="101-001-0000000-001",
instrument="EUR_USD",
units=1000
)
Integration Testing¶
Test against practice environment:
import os
import pytest
from fivetwenty import AsyncClient, Environment
@pytest.mark.integration
async def test_live_api():
"""Test against OANDA practice environment."""
async with AsyncClient(
token=os.environ["OANDA_PRACTICE_TOKEN"],
environment=Environment.PRACTICE
) as client:
# Test real API calls
accounts = await client.accounts.get_accounts()
if len(accounts) == 0:
raise ValueError("Expected at least one account")
Validation Testing¶
Test your validation rules thoroughly:
@pytest.mark.asyncio
async def test_position_size_validator():
# Create validator
validator = MaxPositionSizeValidator(
max_units_per_instrument=100000,
max_total_exposure=Decimal("500000")
)
# Mock context with existing position
context = {
"positions": [
MockPosition(instrument="EUR_USD", long_units="50000", short_units="0")
]
}
# Test order that would exceed limit
order_params = {"instrument": "EUR_USD", "units": 60000}
result = await validator.validate(order_params, context)
if result.is_valid:
raise ValueError("Expected validation to fail")
if result.severity != ValidationSeverity.ERROR:
raise ValueError(f"Expected severity ERROR, got '{result.severity}'")
Summary¶
Key principles for FiveTwenty SDK usage:
- Use context managers for automatic resource cleanup
- Always use Decimal for financial calculations
- Handle specific exceptions rather than generic Exception
- Reuse client instances for better performance
- Implement proper retry logic with exponential backoff
- Secure token management - never hardcode or log tokens
- Test thoroughly in practice environment before live trading
- Implement comprehensive validation - prevent costly errors before they occur
- Build robust error recovery - handle failures gracefully with circuit breakers
- Monitor and categorize errors - track patterns for system improvement
Following these patterns ensures robust, maintainable, and secure trading applications with comprehensive risk management and validation frameworks.