133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""
|
|
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())
|