""" Executor process entry point. Starts the Unix-socket signal server, REST API control interface, and orchestrates clean shutdown on SIGTERM/SIGINT. """ import asyncio import signal from pathlib import Path from typing import Optional import structlog import uvicorn from common.log import configure_logging from executor.config import Settings from executor.executor import Executor from executor.kucoin_api import KuCoinAPI from executor.rest_api import create_app from executor.socket_server import SignalSocketServer from executor.ws_client import KuCoinWSClient async def main() -> None: config_path = Path("config.yaml") settings = await Settings.from_yaml(config_path) if config_path.exists() else Settings() configure_logging(settings.executor.log_level) # file I/O handled by Executor's _DualLogger log = structlog.get_logger().bind(component="executor") log.info("executor_starting", live_mode=settings.live_mode) # Always initialise KuCoinAPI even in paper mode — symbol metadata is # needed for size/precision validation regardless of execution mode. api = KuCoinAPI( api_key=settings.kucoin_api_key, api_secret=settings.kucoin_api_secret, api_passphrase=settings.kucoin_api_passphrase, ) await api.fetch_symbols() ws_client: Optional[KuCoinWSClient] = None if settings.live_mode: # Live mode requires the private WebSocket client to receive fill events. ws_client = KuCoinWSClient( kucoin_api=api, private_token_url=settings.executor.private_token_url, ) executor = Executor( kucoin_api=api, settings=settings.executor, ws_client=ws_client, log_file=settings.executor.log_file, live_mode=settings.live_mode, ) await executor.start() should_exit = asyncio.Event() def shutdown_callback() -> None: should_exit.set() rest_app = create_app(executor, shutdown_callback=shutdown_callback) rest_config = uvicorn.Config( rest_app, host="127.0.0.1", port=settings.executor.rest_port, log_level="warning", ) rest_server = uvicorn.Server(rest_config) socket_server = SignalSocketServer( socket_path=settings.executor.socket_path, on_signal=executor.handle_signal, ) socket_task: asyncio.Task | None = None rest_task: asyncio.Task | None = None exit_task: asyncio.Task | None = None ws_task: asyncio.Task | None = None async def shutdown(sig: signal.Signals) -> None: """Clean up on shutdown signal: pause executor, cancel tasks, close server.""" log.info("shutdown_signal_received", signal=sig.name) await executor.pause() await executor.close() if socket_task is not None and not socket_task.done(): socket_task.cancel() if rest_task is not None: rest_server.should_exit = True if ws_client is not None: await ws_client.stop() should_exit.set() loop = asyncio.get_running_loop() # Register signal handlers so shutdown runs in the asyncio event loop # rather than in a plain threading context. for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(shutdown(s))) socket_task = asyncio.create_task(socket_server.start()) rest_task = asyncio.create_task(rest_server.serve()) exit_task = asyncio.create_task(should_exit.wait()) if ws_client is not None: ws_task = asyncio.create_task(ws_client.start()) log.info( "executor_ready", rest_endpoint=f"http://127.0.0.1:{settings.executor.rest_port}", socket_path=str(settings.executor.socket_path), live_mode=settings.live_mode, ) tasks = {t for t in (socket_task, rest_task, exit_task, ws_task) if t is not None} try: done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.CancelledError: log.info("executor_cancelled") finally: rest_server.should_exit = True for t in tasks: if not t.done(): await asyncio.wait({t}, timeout=3.0) if not t.done(): t.cancel() log.info("executor_shutdown_complete") if __name__ == "__main__": asyncio.run(main())