#!/usr/bin/env python3 """ Live Trading Bot for Regime Reversion Strategy on OKX. This script runs the regime-based mean reversion strategy on ETH perpetual futures using OKX exchange. Usage: # Run with demo account (default) uv run python -m live_trading.main # Run with specific settings uv run python -m live_trading.main --max-position 500 --leverage 2 """ import argparse import logging import signal import sys import time from datetime import datetime, timezone from pathlib import Path # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from live_trading.config import get_config, OKXConfig, TradingConfig, PathConfig from live_trading.okx_client import OKXClient from live_trading.data_feed import DataFeed from live_trading.position_manager import PositionManager from live_trading.live_regime_strategy import LiveRegimeStrategy def setup_logging(log_dir: Path) -> logging.Logger: """Configure logging for the trading bot.""" log_file = log_dir / "live_trading.log" logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', handlers=[ logging.FileHandler(log_file), logging.StreamHandler(sys.stdout), ], force=True ) return logging.getLogger(__name__) class LiveTradingBot: """ Main trading bot orchestrator. Coordinates data fetching, signal generation, and order execution in a continuous loop. """ def __init__( self, okx_config: OKXConfig, trading_config: TradingConfig, path_config: PathConfig ): self.okx_config = okx_config self.trading_config = trading_config self.path_config = path_config self.logger = logging.getLogger(__name__) self.running = True # Initialize components self.logger.info("Initializing trading bot components...") self.okx_client = OKXClient(okx_config, trading_config) self.data_feed = DataFeed(self.okx_client, trading_config, path_config) self.position_manager = PositionManager( self.okx_client, trading_config, path_config ) self.strategy = LiveRegimeStrategy(trading_config, path_config) # Register signal handlers for graceful shutdown signal.signal(signal.SIGINT, self._handle_shutdown) signal.signal(signal.SIGTERM, self._handle_shutdown) self._print_startup_banner() def _print_startup_banner(self) -> None: """Print startup information.""" mode = "DEMO/SANDBOX" if self.okx_config.demo_mode else "LIVE" print("=" * 60) print(f" Regime Reversion Strategy - Live Trading Bot") print("=" * 60) print(f" Mode: {mode}") print(f" Trading Pair: {self.trading_config.eth_symbol}") print(f" Context Pair: {self.trading_config.btc_symbol}") print(f" Timeframe: {self.trading_config.timeframe}") print(f" Max Position: ${self.trading_config.max_position_usdt if self.trading_config.max_position_usdt > 0 else 'All available'}") print(f" Leverage: {self.trading_config.leverage}x") print(f" Stop Loss: {self.trading_config.stop_loss_pct * 100:.1f}%") print(f" Take Profit: {self.trading_config.take_profit_pct * 100:.1f}%") print(f" Cycle Interval: {self.trading_config.sleep_seconds // 60} minutes") print("=" * 60) if not self.okx_config.demo_mode: print("\n *** WARNING: LIVE TRADING MODE - REAL FUNDS AT RISK ***\n") def _handle_shutdown(self, signum, frame) -> None: """Handle shutdown signals gracefully.""" self.logger.info("Shutdown signal received, stopping...") self.running = False def run_trading_cycle(self) -> None: """ Execute one trading cycle. 1. Fetch latest market data 2. Update open positions 3. Generate trading signal 4. Execute trades if signal triggers """ cycle_start = datetime.now(timezone.utc) self.logger.info(f"--- Trading Cycle Start: {cycle_start.isoformat()} ---") try: # 1. Fetch market data features = self.data_feed.get_latest_data() if features is None or features.empty: self.logger.warning("No market data available, skipping cycle") return # Get current prices eth_price = features['eth_close'].iloc[-1] btc_price = features['btc_close'].iloc[-1] current_prices = { self.trading_config.eth_symbol: eth_price, self.trading_config.btc_symbol: btc_price, } # 2. Update existing positions (check SL/TP) closed_trades = self.position_manager.update_positions(current_prices) if closed_trades: for trade in closed_trades: self.logger.info( f"Trade closed: {trade['trade_id']} " f"PnL=${trade['pnl_usd']:.2f} ({trade['reason']})" ) # 3. Sync with exchange positions self.position_manager.sync_with_exchange() # 4. Get current funding rates funding = self.data_feed.get_current_funding_rates() # 5. Generate trading signal signal = self.strategy.generate_signal(features, funding) # 6. Execute trades based on signal if signal['action'] == 'entry': self._execute_entry(signal, eth_price) elif signal['action'] == 'check_exit': self._execute_exit(signal) # 7. Log portfolio summary summary = self.position_manager.get_portfolio_summary() self.logger.info( f"Portfolio: {summary['open_positions']} positions, " f"exposure=${summary['total_exposure_usdt']:.2f}, " f"unrealized_pnl=${summary['total_unrealized_pnl']:.2f}" ) except Exception as e: self.logger.error(f"Trading cycle error: {e}", exc_info=True) # Save positions on error self.position_manager.save_positions() cycle_duration = (datetime.now(timezone.utc) - cycle_start).total_seconds() self.logger.info(f"--- Cycle completed in {cycle_duration:.1f}s ---") def _execute_entry(self, signal: dict, current_price: float) -> None: """Execute entry trade.""" symbol = self.trading_config.eth_symbol side = signal['side'] # Check if we can open a position if not self.position_manager.can_open_position(): self.logger.info("Cannot open position: max positions reached") return # Get account balance balance = self.okx_client.get_balance() available_usdt = balance['free'] # Calculate position size size_usdt = self.strategy.calculate_position_size(signal, available_usdt) if size_usdt <= 0: self.logger.info("Position size too small, skipping entry") return size_eth = size_usdt / current_price # Calculate SL/TP stop_loss, take_profit = self.strategy.calculate_sl_tp(current_price, side) self.logger.info( f"Executing {side.upper()} entry: {size_eth:.4f} ETH @ {current_price:.2f} " f"(${size_usdt:.2f}), SL={stop_loss:.2f}, TP={take_profit:.2f}" ) try: # Place market order order_side = "buy" if side == "long" else "sell" order = self.okx_client.place_market_order(symbol, order_side, size_eth) # Get filled price (handle None values from OKX response) filled_price = order.get('average') or order.get('price') or current_price filled_amount = order.get('filled') or order.get('amount') or size_eth # Ensure we have valid numeric values if filled_price is None or filled_price == 0: self.logger.warning(f"No fill price in order response, using current price: {current_price}") filled_price = current_price if filled_amount is None or filled_amount == 0: self.logger.warning(f"No fill amount in order response, using requested: {size_eth}") filled_amount = size_eth # Recalculate SL/TP with filled price stop_loss, take_profit = self.strategy.calculate_sl_tp(filled_price, side) # Get order ID from response order_id = order.get('id', '') # Record position locally position = self.position_manager.open_position( symbol=symbol, side=side, entry_price=filled_price, size=filled_amount, stop_loss_price=stop_loss, take_profit_price=take_profit, order_id=order_id, ) if position: self.logger.info( f"Position opened: {position.trade_id}, " f"{filled_amount:.4f} ETH @ {filled_price:.2f}" ) # Try to set SL/TP on exchange try: self.okx_client.set_stop_loss_take_profit( symbol, side, filled_amount, stop_loss, take_profit ) except Exception as e: self.logger.warning(f"Could not set SL/TP on exchange: {e}") except Exception as e: self.logger.error(f"Order execution failed: {e}", exc_info=True) def _execute_exit(self, signal: dict) -> None: """Execute exit based on mean reversion signal.""" symbol = self.trading_config.eth_symbol # Get position for ETH position = self.position_manager.get_position_for_symbol(symbol) if not position: return current_price = signal.get('eth_price', position.current_price) self.logger.info( f"Mean reversion exit signal: closing {position.trade_id} " f"@ {current_price:.2f}" ) try: # Close position on exchange exit_order = self.okx_client.close_position(symbol) exit_order_id = exit_order.get('id', '') if exit_order else '' # Record closure locally self.position_manager.close_position( position.trade_id, current_price, reason="mean_reversion_complete", exit_order_id=exit_order_id, ) except Exception as e: self.logger.error(f"Exit execution failed: {e}", exc_info=True) def run(self) -> None: """Main trading loop.""" self.logger.info("Starting trading loop...") while self.running: try: self.run_trading_cycle() if self.running: sleep_seconds = self.trading_config.sleep_seconds minutes = sleep_seconds // 60 self.logger.info(f"Sleeping for {minutes} minutes...") # Sleep in smaller chunks to allow faster shutdown for _ in range(sleep_seconds): if not self.running: break time.sleep(1) except KeyboardInterrupt: self.logger.info("Keyboard interrupt received") break except Exception as e: self.logger.error(f"Unexpected error in main loop: {e}", exc_info=True) time.sleep(60) # Wait before retry # Cleanup self.logger.info("Shutting down...") self.position_manager.save_positions() self.logger.info("Shutdown complete") def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="Live Trading Bot for Regime Reversion Strategy" ) parser.add_argument( "--max-position", type=float, default=None, help="Maximum position size in USDT" ) parser.add_argument( "--leverage", type=int, default=None, help="Trading leverage (1-125)" ) parser.add_argument( "--interval", type=int, default=None, help="Trading cycle interval in seconds" ) parser.add_argument( "--live", action="store_true", help="Use live trading mode (requires OKX_DEMO_MODE=false)" ) return parser.parse_args() def main(): """Main entry point.""" args = parse_args() # Load configuration okx_config, trading_config, path_config = get_config() # Apply command line overrides if args.max_position is not None: trading_config.max_position_usdt = args.max_position if args.leverage is not None: trading_config.leverage = args.leverage if args.interval is not None: trading_config.sleep_seconds = args.interval if args.live: okx_config.demo_mode = False # Setup logging logger = setup_logging(path_config.logs_dir) try: # Create and run bot bot = LiveTradingBot(okx_config, trading_config, path_config) bot.run() except ValueError as e: logger.error(f"Configuration error: {e}") sys.exit(1) except Exception as e: logger.error(f"Fatal error: {e}", exc_info=True) sys.exit(1) if __name__ == "__main__": main()