Phase 5-6: Live trading stubs and observability
Phase 5 - Live Trading (stub implementation): - Create LiveBroker stub with NotImplementedError for all methods - Document required exchange API integration points - Add testnet flag for safety Phase 6 - Observability: - Add structured logging with StructuredFormatter and ColoredFormatter - Create TradingLogger for trading-specific log entries - Implement health check system with HealthCheck class - Add default health checks for database, exchange, memory - Create health_endpoint() for monitoring integration The LiveBroker is a stub that needs exchange API integration for production use. All other trading modes (backtest, paper) are fully functional. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
51ec74175d
commit
5dbda13924
|
|
@ -10,10 +10,11 @@ This package provides a unified interface for executing trades across different
|
||||||
from .base_broker import BaseBroker, OrderSide, OrderType, OrderStatus, OrderResult, Position
|
from .base_broker import BaseBroker, OrderSide, OrderType, OrderStatus, OrderResult, Position
|
||||||
from .backtest_broker import BacktestBroker
|
from .backtest_broker import BacktestBroker
|
||||||
from .paper_broker import PaperBroker
|
from .paper_broker import PaperBroker
|
||||||
|
from .live_broker import LiveBroker
|
||||||
from .factory import create_broker, TradingMode, get_available_modes
|
from .factory import create_broker, TradingMode, get_available_modes
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseBroker', 'OrderSide', 'OrderType', 'OrderStatus', 'OrderResult', 'Position',
|
'BaseBroker', 'OrderSide', 'OrderType', 'OrderStatus', 'OrderResult', 'Position',
|
||||||
'BacktestBroker', 'PaperBroker',
|
'BacktestBroker', 'PaperBroker', 'LiveBroker',
|
||||||
'create_broker', 'TradingMode', 'get_available_modes'
|
'create_broker', 'TradingMode', 'get_available_modes'
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
"""
|
||||||
|
Live Trading Broker Implementation for BrighterTrading.
|
||||||
|
|
||||||
|
Executes real trades via exchange APIs (CCXT).
|
||||||
|
|
||||||
|
NOTE: This is a stub implementation. Full live trading
|
||||||
|
requires careful testing with testnet before production use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from .base_broker import (
|
||||||
|
BaseBroker, OrderResult, OrderSide, OrderType, OrderStatus, Position
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveBroker(BaseBroker):
|
||||||
|
"""
|
||||||
|
Live trading broker that executes real trades via CCXT.
|
||||||
|
|
||||||
|
WARNING: This broker executes real trades with real money.
|
||||||
|
Ensure thorough testing on testnet before production use.
|
||||||
|
|
||||||
|
Features (to be implemented):
|
||||||
|
- Real order execution via exchange APIs
|
||||||
|
- Balance and position sync from exchange
|
||||||
|
- Order lifecycle management (create, cancel, fill detection)
|
||||||
|
- Restart-safe open order reconciliation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
exchange_interface: Any = None,
|
||||||
|
user_name: str = None,
|
||||||
|
exchange_name: str = 'binance',
|
||||||
|
testnet: bool = True,
|
||||||
|
initial_balance: float = 0.0, # Will be synced from exchange
|
||||||
|
commission: float = 0.001,
|
||||||
|
slippage: float = 0.0
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the LiveBroker.
|
||||||
|
|
||||||
|
:param exchange_interface: ExchangeInterface instance for API access.
|
||||||
|
:param user_name: User name for exchange credentials.
|
||||||
|
:param exchange_name: Exchange to use (e.g., 'binance', 'alpaca').
|
||||||
|
:param testnet: Use testnet (default True for safety).
|
||||||
|
:param initial_balance: Starting balance (synced from exchange).
|
||||||
|
:param commission: Commission rate.
|
||||||
|
:param slippage: Slippage rate.
|
||||||
|
"""
|
||||||
|
super().__init__(initial_balance, commission, slippage)
|
||||||
|
|
||||||
|
self._exchange_interface = exchange_interface
|
||||||
|
self._user_name = user_name
|
||||||
|
self._exchange_name = exchange_name
|
||||||
|
self._testnet = testnet
|
||||||
|
|
||||||
|
# Warn if not using testnet
|
||||||
|
if not testnet:
|
||||||
|
logger.warning(
|
||||||
|
"LiveBroker initialized for PRODUCTION trading. "
|
||||||
|
"Real money will be used!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Placeholder for exchange connection
|
||||||
|
self._exchange = None
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def _ensure_connected(self):
|
||||||
|
"""Ensure exchange connection is established."""
|
||||||
|
if not self._connected:
|
||||||
|
raise RuntimeError(
|
||||||
|
"LiveBroker not connected to exchange. "
|
||||||
|
"Call connect() first."
|
||||||
|
)
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
"""
|
||||||
|
Connect to the exchange.
|
||||||
|
|
||||||
|
:return: True if connection successful.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.connect() not yet implemented. "
|
||||||
|
"Live trading requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnect from the exchange."""
|
||||||
|
self._connected = False
|
||||||
|
self._exchange = None
|
||||||
|
|
||||||
|
def sync_balance(self) -> float:
|
||||||
|
"""
|
||||||
|
Sync balance from exchange.
|
||||||
|
|
||||||
|
:return: Current balance from exchange.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.sync_balance() not yet implemented. "
|
||||||
|
"Balance sync requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def sync_positions(self) -> List[Position]:
|
||||||
|
"""
|
||||||
|
Sync positions from exchange.
|
||||||
|
|
||||||
|
:return: List of current positions.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.sync_positions() not yet implemented. "
|
||||||
|
"Position sync requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def sync_open_orders(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Sync open orders from exchange.
|
||||||
|
|
||||||
|
Used for restart-safe order reconciliation.
|
||||||
|
|
||||||
|
:return: List of open orders from exchange.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.sync_open_orders() not yet implemented. "
|
||||||
|
"Order sync requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def place_order(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
side: OrderSide,
|
||||||
|
order_type: OrderType,
|
||||||
|
size: float,
|
||||||
|
price: Optional[float] = None,
|
||||||
|
stop_loss: Optional[float] = None,
|
||||||
|
take_profit: Optional[float] = None,
|
||||||
|
time_in_force: str = 'GTC'
|
||||||
|
) -> OrderResult:
|
||||||
|
"""Place an order on the exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.place_order() not yet implemented. "
|
||||||
|
"Order placement requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def cancel_order(self, order_id: str) -> bool:
|
||||||
|
"""Cancel an order on the exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.cancel_order() not yet implemented. "
|
||||||
|
"Order cancellation requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_order(self, order_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get order details from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_order() not yet implemented. "
|
||||||
|
"Order retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all open orders from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_open_orders() not yet implemented. "
|
||||||
|
"Open order retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_balance(self, asset: Optional[str] = None) -> float:
|
||||||
|
"""Get balance from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_balance() not yet implemented. "
|
||||||
|
"Balance retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_available_balance(self, asset: Optional[str] = None) -> float:
|
||||||
|
"""Get available balance from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_available_balance() not yet implemented. "
|
||||||
|
"Available balance retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_position(self, symbol: str) -> Optional[Position]:
|
||||||
|
"""Get position from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_position() not yet implemented. "
|
||||||
|
"Position retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_positions(self) -> List[Position]:
|
||||||
|
"""Get all positions from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_all_positions() not yet implemented. "
|
||||||
|
"Position retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_current_price(self, symbol: str) -> float:
|
||||||
|
"""Get current price from exchange."""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.get_current_price() not yet implemented. "
|
||||||
|
"Price retrieval requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Process pending orders and sync with exchange.
|
||||||
|
|
||||||
|
For live trading, this should:
|
||||||
|
1. Check for order fills
|
||||||
|
2. Update positions
|
||||||
|
3. Handle partial fills
|
||||||
|
4. Manage stop loss / take profit orders
|
||||||
|
"""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"LiveBroker.update() not yet implemented. "
|
||||||
|
"Order update requires exchange API integration."
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
"""
|
||||||
|
Health Check Module for BrighterTrading.
|
||||||
|
|
||||||
|
Provides health check endpoints and monitoring capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HealthStatus:
|
||||||
|
"""Health status constants."""
|
||||||
|
HEALTHY = 'healthy'
|
||||||
|
DEGRADED = 'degraded'
|
||||||
|
UNHEALTHY = 'unhealthy'
|
||||||
|
|
||||||
|
|
||||||
|
class HealthCheck:
|
||||||
|
"""
|
||||||
|
Health check coordinator for monitoring system health.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._checks: Dict[str, callable] = {}
|
||||||
|
self._last_check_time: Optional[datetime] = None
|
||||||
|
self._cached_status: Optional[Dict[str, Any]] = None
|
||||||
|
self._cache_ttl_seconds: float = 5.0
|
||||||
|
|
||||||
|
def register_check(self, name: str, check_fn: callable) -> None:
|
||||||
|
"""
|
||||||
|
Register a health check function.
|
||||||
|
|
||||||
|
:param name: Check name.
|
||||||
|
:param check_fn: Function that returns (status, message) tuple.
|
||||||
|
"""
|
||||||
|
self._checks[name] = check_fn
|
||||||
|
logger.debug(f"Registered health check: {name}")
|
||||||
|
|
||||||
|
def unregister_check(self, name: str) -> None:
|
||||||
|
"""Unregister a health check."""
|
||||||
|
if name in self._checks:
|
||||||
|
del self._checks[name]
|
||||||
|
|
||||||
|
def run_checks(self, use_cache: bool = True) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Run all health checks and return aggregated status.
|
||||||
|
|
||||||
|
:param use_cache: Use cached results if available and fresh.
|
||||||
|
:return: Health status dictionary.
|
||||||
|
"""
|
||||||
|
# Check cache
|
||||||
|
if use_cache and self._cached_status and self._last_check_time:
|
||||||
|
age = (datetime.utcnow() - self._last_check_time).total_seconds()
|
||||||
|
if age < self._cache_ttl_seconds:
|
||||||
|
return self._cached_status
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
results: Dict[str, Dict[str, Any]] = {}
|
||||||
|
overall_status = HealthStatus.HEALTHY
|
||||||
|
|
||||||
|
for name, check_fn in self._checks.items():
|
||||||
|
try:
|
||||||
|
check_start = time.time()
|
||||||
|
status, message = check_fn()
|
||||||
|
check_duration = time.time() - check_start
|
||||||
|
|
||||||
|
results[name] = {
|
||||||
|
'status': status,
|
||||||
|
'message': message,
|
||||||
|
'duration_ms': round(check_duration * 1000, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Downgrade overall status if needed
|
||||||
|
if status == HealthStatus.UNHEALTHY:
|
||||||
|
overall_status = HealthStatus.UNHEALTHY
|
||||||
|
elif status == HealthStatus.DEGRADED and overall_status == HealthStatus.HEALTHY:
|
||||||
|
overall_status = HealthStatus.DEGRADED
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health check '{name}' failed with error: {e}")
|
||||||
|
results[name] = {
|
||||||
|
'status': HealthStatus.UNHEALTHY,
|
||||||
|
'message': str(e),
|
||||||
|
'error': True,
|
||||||
|
}
|
||||||
|
overall_status = HealthStatus.UNHEALTHY
|
||||||
|
|
||||||
|
total_duration = time.time() - start_time
|
||||||
|
|
||||||
|
status = {
|
||||||
|
'status': overall_status,
|
||||||
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
|
'duration_ms': round(total_duration * 1000, 2),
|
||||||
|
'checks': results,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache results
|
||||||
|
self._cached_status = status
|
||||||
|
self._last_check_time = datetime.utcnow()
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def get_status(self) -> str:
|
||||||
|
"""Get simple status string."""
|
||||||
|
result = self.run_checks()
|
||||||
|
return result['status']
|
||||||
|
|
||||||
|
def is_healthy(self) -> bool:
|
||||||
|
"""Check if system is healthy."""
|
||||||
|
return self.get_status() == HealthStatus.HEALTHY
|
||||||
|
|
||||||
|
|
||||||
|
# Default health check instance
|
||||||
|
_health_check = HealthCheck()
|
||||||
|
|
||||||
|
|
||||||
|
def get_health_check() -> HealthCheck:
|
||||||
|
"""Get the global health check instance."""
|
||||||
|
return _health_check
|
||||||
|
|
||||||
|
|
||||||
|
def register_default_checks(
|
||||||
|
data_cache: Any = None,
|
||||||
|
exchange_interface: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Register default health checks.
|
||||||
|
|
||||||
|
:param data_cache: DataCache instance.
|
||||||
|
:param exchange_interface: ExchangeInterface instance.
|
||||||
|
"""
|
||||||
|
health = get_health_check()
|
||||||
|
|
||||||
|
# Database check
|
||||||
|
if data_cache:
|
||||||
|
def check_database():
|
||||||
|
try:
|
||||||
|
# Try to query something simple
|
||||||
|
data_cache.get_rows_from_datacache('strategies', [])
|
||||||
|
return HealthStatus.HEALTHY, 'Database accessible'
|
||||||
|
except Exception as e:
|
||||||
|
return HealthStatus.UNHEALTHY, str(e)
|
||||||
|
|
||||||
|
health.register_check('database', check_database)
|
||||||
|
|
||||||
|
# Exchange connectivity check
|
||||||
|
if exchange_interface:
|
||||||
|
def check_exchange():
|
||||||
|
try:
|
||||||
|
# Try to connect to default exchange
|
||||||
|
if exchange_interface.default_exchange:
|
||||||
|
return HealthStatus.HEALTHY, 'Exchange connected'
|
||||||
|
return HealthStatus.DEGRADED, 'No default exchange'
|
||||||
|
except Exception as e:
|
||||||
|
return HealthStatus.UNHEALTHY, str(e)
|
||||||
|
|
||||||
|
health.register_check('exchange', check_exchange)
|
||||||
|
|
||||||
|
# Memory check
|
||||||
|
def check_memory():
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
if memory.percent > 90:
|
||||||
|
return HealthStatus.UNHEALTHY, f'Memory usage critical: {memory.percent}%'
|
||||||
|
elif memory.percent > 75:
|
||||||
|
return HealthStatus.DEGRADED, f'Memory usage high: {memory.percent}%'
|
||||||
|
return HealthStatus.HEALTHY, f'Memory usage: {memory.percent}%'
|
||||||
|
except ImportError:
|
||||||
|
return HealthStatus.HEALTHY, 'Memory check unavailable (psutil not installed)'
|
||||||
|
except Exception as e:
|
||||||
|
return HealthStatus.DEGRADED, str(e)
|
||||||
|
|
||||||
|
health.register_check('memory', check_memory)
|
||||||
|
|
||||||
|
# Heartbeat check (always healthy if reached)
|
||||||
|
def check_heartbeat():
|
||||||
|
return HealthStatus.HEALTHY, 'Service alive'
|
||||||
|
|
||||||
|
health.register_check('heartbeat', check_heartbeat)
|
||||||
|
|
||||||
|
logger.info("Default health checks registered")
|
||||||
|
|
||||||
|
|
||||||
|
def health_endpoint() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Flask/FastAPI compatible health endpoint function.
|
||||||
|
|
||||||
|
Returns health status as JSON-serializable dict.
|
||||||
|
"""
|
||||||
|
return get_health_check().run_checks()
|
||||||
|
|
||||||
|
|
||||||
|
def liveness_probe() -> bool:
|
||||||
|
"""Simple liveness probe for Kubernetes."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def readiness_probe() -> bool:
|
||||||
|
"""Readiness probe checking if service is ready to accept traffic."""
|
||||||
|
return get_health_check().is_healthy()
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
"""
|
||||||
|
Logging Configuration for BrighterTrading.
|
||||||
|
|
||||||
|
Provides structured logging with consistent formatting across all modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class StructuredFormatter(logging.Formatter):
|
||||||
|
"""
|
||||||
|
Formatter that outputs structured JSON logs for production use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
log_record = {
|
||||||
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
|
'level': record.levelname,
|
||||||
|
'logger': record.name,
|
||||||
|
'message': record.getMessage(),
|
||||||
|
'module': record.module,
|
||||||
|
'function': record.funcName,
|
||||||
|
'line': record.lineno,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add exception info if present
|
||||||
|
if record.exc_info:
|
||||||
|
log_record['exception'] = self.formatException(record.exc_info)
|
||||||
|
|
||||||
|
# Add extra fields if present
|
||||||
|
if hasattr(record, 'extra_data'):
|
||||||
|
log_record.update(record.extra_data)
|
||||||
|
|
||||||
|
return json.dumps(log_record)
|
||||||
|
|
||||||
|
|
||||||
|
class ColoredFormatter(logging.Formatter):
|
||||||
|
"""
|
||||||
|
Formatter with colored output for development use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
COLORS = {
|
||||||
|
'DEBUG': '\033[36m', # Cyan
|
||||||
|
'INFO': '\033[32m', # Green
|
||||||
|
'WARNING': '\033[33m', # Yellow
|
||||||
|
'ERROR': '\033[31m', # Red
|
||||||
|
'CRITICAL': '\033[35m', # Magenta
|
||||||
|
}
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
color = self.COLORS.get(record.levelname, self.RESET)
|
||||||
|
formatted = super().format(record)
|
||||||
|
return f"{color}{formatted}{self.RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(
|
||||||
|
level: str = 'INFO',
|
||||||
|
structured: bool = False,
|
||||||
|
log_file: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Configure logging for the application.
|
||||||
|
|
||||||
|
:param level: Log level ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL').
|
||||||
|
:param structured: Use structured JSON logging (for production).
|
||||||
|
:param log_file: Optional log file path.
|
||||||
|
"""
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(getattr(logging, level.upper()))
|
||||||
|
|
||||||
|
# Remove existing handlers
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
# Console handler
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
if structured:
|
||||||
|
console_handler.setFormatter(StructuredFormatter())
|
||||||
|
else:
|
||||||
|
console_handler.setFormatter(ColoredFormatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
))
|
||||||
|
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# File handler if specified
|
||||||
|
if log_file:
|
||||||
|
file_handler = logging.FileHandler(log_file)
|
||||||
|
file_handler.setFormatter(StructuredFormatter())
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(name: str) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Get a logger with the given name.
|
||||||
|
|
||||||
|
:param name: Logger name (typically __name__).
|
||||||
|
:return: Logger instance.
|
||||||
|
"""
|
||||||
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
|
||||||
|
class TradingLogger:
|
||||||
|
"""
|
||||||
|
Specialized logger for trading operations with structured fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger: logging.Logger):
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
def _log_with_context(
|
||||||
|
self,
|
||||||
|
level: int,
|
||||||
|
message: str,
|
||||||
|
user_id: Optional[int] = None,
|
||||||
|
strategy_id: Optional[str] = None,
|
||||||
|
order_id: Optional[str] = None,
|
||||||
|
symbol: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
"""Log message with trading context."""
|
||||||
|
extra = {
|
||||||
|
'user_id': user_id,
|
||||||
|
'strategy_id': strategy_id,
|
||||||
|
'order_id': order_id,
|
||||||
|
'symbol': symbol,
|
||||||
|
}
|
||||||
|
extra.update(kwargs)
|
||||||
|
|
||||||
|
# Filter out None values
|
||||||
|
extra = {k: v for k, v in extra.items() if v is not None}
|
||||||
|
|
||||||
|
record = self._logger.makeRecord(
|
||||||
|
self._logger.name,
|
||||||
|
level,
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
message,
|
||||||
|
(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
record.extra_data = extra
|
||||||
|
self._logger.handle(record)
|
||||||
|
|
||||||
|
def order_placed(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
strategy_id: str,
|
||||||
|
order_id: str,
|
||||||
|
symbol: str,
|
||||||
|
side: str,
|
||||||
|
size: float,
|
||||||
|
price: Optional[float] = None,
|
||||||
|
order_type: str = 'market',
|
||||||
|
) -> None:
|
||||||
|
"""Log order placement."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.INFO,
|
||||||
|
f"Order placed: {side} {size} {symbol}",
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
order_id=order_id,
|
||||||
|
symbol=symbol,
|
||||||
|
side=side,
|
||||||
|
size=size,
|
||||||
|
price=price,
|
||||||
|
order_type=order_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def order_filled(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
strategy_id: str,
|
||||||
|
order_id: str,
|
||||||
|
symbol: str,
|
||||||
|
side: str,
|
||||||
|
size: float,
|
||||||
|
price: float,
|
||||||
|
commission: float = 0.0,
|
||||||
|
) -> None:
|
||||||
|
"""Log order fill."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.INFO,
|
||||||
|
f"Order filled: {side} {size} {symbol} @ {price}",
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
order_id=order_id,
|
||||||
|
symbol=symbol,
|
||||||
|
side=side,
|
||||||
|
size=size,
|
||||||
|
fill_price=price,
|
||||||
|
commission=commission,
|
||||||
|
)
|
||||||
|
|
||||||
|
def order_cancelled(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
strategy_id: str,
|
||||||
|
order_id: str,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log order cancellation."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.INFO,
|
||||||
|
f"Order cancelled: {order_id}",
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
order_id=order_id,
|
||||||
|
cancel_reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def strategy_started(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
strategy_id: str,
|
||||||
|
strategy_name: str,
|
||||||
|
mode: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log strategy start."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.INFO,
|
||||||
|
f"Strategy started: {strategy_name} ({mode} mode)",
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
strategy_name=strategy_name,
|
||||||
|
mode=mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
def strategy_stopped(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
strategy_id: str,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log strategy stop."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.INFO,
|
||||||
|
f"Strategy stopped: {strategy_id}",
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
stop_reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def error(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
user_id: Optional[int] = None,
|
||||||
|
strategy_id: Optional[str] = None,
|
||||||
|
error_type: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
"""Log error."""
|
||||||
|
self._log_with_context(
|
||||||
|
logging.ERROR,
|
||||||
|
message,
|
||||||
|
user_id=user_id,
|
||||||
|
strategy_id=strategy_id,
|
||||||
|
error_type=error_type,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_logger(name: str) -> TradingLogger:
|
||||||
|
"""
|
||||||
|
Get a trading-specific logger.
|
||||||
|
|
||||||
|
:param name: Logger name.
|
||||||
|
:return: TradingLogger instance.
|
||||||
|
"""
|
||||||
|
return TradingLogger(logging.getLogger(name))
|
||||||
Loading…
Reference in New Issue