Files
gavindiazandClaude Code 1a7b0111ad fix(startup): build asyncio socketpair over AF_UNIX - unblocks .bat console freeze
The run_bot.bat console path froze for minutes after two log lines with 0 CPU:
asyncio.run() -> ProactorEventLoop.__init__ -> socket.socketpair() -> Python
3.12 _fallback_socketpair() blocks forever in accept() on the loopback TCP
handshake (security software filters 127.0.0.1 in this interactive console +
xau-trader.exe session). Git-bash redirect runs worked 4/4, proving it is
environment, not interpreter.

Windows 10 17063+ supports AF_UNIX sockets, which need no loopback TCP at all.
Monkey-patch socket.socketpair on win32 to self-pair over AF_UNIX via ctypes
(ws2_32; python.org builds don't expose the AF_UNIX constant), falling back to
the stock socketpair if AF_UNIX is unavailable. socket.socket(fileno=N) treats
N as a raw SOCKET handle on Windows, so the raw handles wrap cleanly.

Verified: smoke test raw pair family=1 + full asyncio.run Proactor loop; the
exe boot went from hang to 'Main loop started' in ~10s.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 20:58:08 +08:00

308 lines
10 KiB
Python

"""
XAUUSD AI Trading Bot — Entry Point
Usage:
python main.py # Start the trading bot
python main.py --train # Train AI models first, then start
python main.py --backtest # Run backtesting only
python main.py --dashboard # Start web dashboard only
"""
import argparse
import asyncio
import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
from loguru import logger
# ── Windows startup-freeze fix ─────────────────────────────────────────────
# asyncio.run() → ProactorEventLoop.__init__ calls socket.socketpair(). On
# Windows Python 3.12 that goes through _fallback_socketpair(), which does a
# loopback (127.0.0.1) TCP connect/accept dance. Under the run_bot.bat console
# path, security software filters that loopback handshake → accept() blocks
# FOREVER → the bot hangs before start() ever runs (2 init lines on console,
# log file stays empty, process idle at 0 CPU). Windows 10+ supports AF_UNIX
# sockets, which need no loopback TCP at all, so build the self-pipe pair over
# AF_UNIX and fall back to the stock socketpair if AF_UNIX is unavailable.
if sys.platform == "win32":
import ctypes as _ctypes
import os as _os
import socket as _socket_mod
import tempfile as _tempfile
# Python builds (incl. python.org 3.12) don't expose socket.AF_UNIX on
# Windows even though Winsock has supported it since Win10 17063, so we
# call ws2_32 directly. socket.socket(fileno=N) on Windows interprets N as
# a raw SOCKET handle (proven empirically), so the pair wraps cleanly.
_ws2 = _ctypes.WinDLL("ws2_32", use_last_error=True)
_ws2.socket.argtypes = [_ctypes.c_int, _ctypes.c_int, _ctypes.c_int]
_ws2.socket.restype = _ctypes.c_size_t
_ws2.bind.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.c_int]
_ws2.bind.restype = _ctypes.c_int
_ws2.listen.argtypes = [_ctypes.c_size_t, _ctypes.c_int]
_ws2.listen.restype = _ctypes.c_int
_ws2.connect.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.c_int]
_ws2.connect.restype = _ctypes.c_int
_ws2.accept.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.POINTER(_ctypes.c_int)]
_ws2.accept.restype = _ctypes.c_size_t
_ws2.closesocket.argtypes = [_ctypes.c_size_t]
_ws2.closesocket.restype = _ctypes.c_int
class _SockAddrUn(_ctypes.Structure):
_fields_ = [
("sun_family", _ctypes.c_ushort),
("sun_path", _ctypes.c_char * 260), # Windows uses MAX_PATH
]
_AF_UNIX = 1 # Winsock2.h: AF_UNIX == 1
_SOCK_STREAM = 1
_INVALID_SOCKET = _ctypes.c_size_t(-1).value
def _unix_socketpair():
path = _os.path.join(
_tempfile.gettempdir(),
f"xau_sp_{_os.getpid()}_{_os.urandom(4).hex()}.sock",
)
server = _ws2.socket(_AF_UNIX, _SOCK_STREAM, 0)
if server == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "socket(AF_UNIX)")
client = _INVALID_SOCKET
accepted = _INVALID_SOCKET
ok = False
try:
addr = _SockAddrUn()
addr.sun_family = _AF_UNIX
path_b = path.encode("utf-8")
if len(path_b) >= 260:
raise ValueError("AF_UNIX path too long")
addr.sun_path = path_b + b"\x00"
addr_len = _ctypes.sizeof(_ctypes.c_ushort) + len(path_b) + 1
if _ws2.bind(server, _ctypes.byref(addr), addr_len) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "bind AF_UNIX")
if _ws2.listen(server, 1) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "listen AF_UNIX")
client = _ws2.socket(_AF_UNIX, _SOCK_STREAM, 0)
if client == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "socket(AF_UNIX) client")
if _ws2.connect(client, _ctypes.byref(addr), addr_len) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "connect AF_UNIX")
accepted = _ws2.accept(server, None, None)
if accepted == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "accept AF_UNIX")
# Ownership of both raw handles transfers to the Python sockets.
result = (_socket_mod.socket(fileno=accepted), _socket_mod.socket(fileno=client))
ok = True
return result
finally:
if server != _INVALID_SOCKET:
_ws2.closesocket(server)
if not ok:
for h in (accepted, client):
if h != _INVALID_SOCKET:
_ws2.closesocket(h)
try:
_os.unlink(path)
except OSError:
pass # unique temp name → leftovers never collide
_orig_socketpair = _socket_mod.socketpair
def _safe_socketpair(*args, **kwargs):
try:
return _unix_socketpair()
except Exception:
# Rare fallback (pre-17063 Windows without AF_UNIX) — same
# behavior as before the patch.
return _orig_socketpair(*args, **kwargs)
_socket_mod.socketpair = _safe_socketpair
def parse_args():
parser = argparse.ArgumentParser(
description="XAUUSD AI Trading Bot",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--train",
action="store_true",
help="Train AI models before starting the bot",
)
parser.add_argument(
"--train-only",
action="store_true",
help="Train AI models and exit without starting the bot",
)
parser.add_argument(
"--backtest",
action="store_true",
help="Run backtesting only (no live trading)",
)
parser.add_argument(
"--dashboard",
action="store_true",
help="Start web dashboard only (no trading)",
)
parser.add_argument(
"--telegram",
action="store_true",
help="Start interactive Telegram Command Center only (in standby mode)",
)
parser.add_argument(
"--standby",
action="store_true",
help="Start bot in standby mode (Telegram active, trading paused until started via Telegram)",
)
parser.add_argument(
"--demo",
action="store_true",
default=True,
help="Run in demo mode (default: True)",
)
return parser.parse_args()
async def train_models():
"""Train AI models using historical data."""
from core.mt5_connector import MT5Connector
from ai.price_predictor import PricePredictor
from ai.signal_classifier import SignalClassifier
logger.info("=" * 60)
logger.info(" Training AI Models")
logger.info("=" * 60)
mt5 = MT5Connector()
if not mt5.connect():
logger.error("Cannot connect to MT5 for training data")
return
try:
from scripts.train_ai import run_training_pipeline
run_training_pipeline(timeframe="M15", count=3000)
# Optional LSTM if TensorFlow is installed
try:
import tensorflow
df = mt5.get_rates(timeframe="H1", count=2000)
if df is not None:
lstm = PricePredictor()
lstm.train(df, epochs=10)
except Exception:
pass
logger.success("✅ AI models trained and ready for live execution")
finally:
mt5.disconnect()
def start_dashboard():
"""Start the web dashboard server."""
import socket
import uvicorn
from config.settings import get_settings
settings = get_settings()
host = settings.dashboard.host
port = settings.dashboard.port
# Check if dashboard is already running
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(1.0)
if s.connect_ex((host, port)) == 0:
logger.warning(f"⚠️ Dashboard is already running on http://{host}:{port}! Opening browser or keeping existing instance.")
return
logger.info(f"Starting dashboard on http://{host}:{port}")
uvicorn.run(
"dashboard.app:app",
host=host,
port=port,
reload=False,
)
def run_telegram():
"""Start the full Trading Bot in Standby Mode with 2-way Telegram Command Center active."""
from core.bot import TradingBot
logger.info("=" * 60)
logger.info(" Starting XAUUSD Bot in Standby Mode (Telegram Controlled)")
logger.info("=" * 60)
bot = TradingBot(start_paused=True)
asyncio.run(bot.start())
def run_backtest():
"""Run historical backtesting simulation."""
from backtesting.backtester import Backtester
from core.mt5_connector import MT5Connector
logger.info("=" * 60)
logger.info(" Starting XAUUSD Multi-Strategy Backtester")
logger.info("=" * 60)
bt = Backtester(initial_balance=10_000.0, lot_size=0.02)
df = None
# Try fetching real data from MT5 first if available
try:
mt5 = MT5Connector()
if mt5.connect(max_retries=1, retry_delay=1):
logger.info("Fetching real historical H1 data from MT5...")
df = mt5.get_rates(timeframe="H1", count=2000)
mt5.disconnect()
except Exception as e:
logger.debug(f"MT5 historical data fetch skipped: {e}")
if df is None or len(df) < 500:
logger.info("Using high-fidelity synthetic Gold benchmark data (1,500 candles)...")
df = bt.generate_benchmark_data(bars=1500)
bt.run(df)
def main():
args = parse_args()
if args.dashboard:
start_dashboard()
return
if args.telegram:
run_telegram()
return
if args.backtest:
run_backtest()
return
if args.train_only:
asyncio.run(train_models())
return
if args.train:
asyncio.run(train_models())
# Start the live trading bot
from core.bot import TradingBot
start_paused = getattr(args, "standby", False)
bot = TradingBot(start_paused=start_paused)
asyncio.run(bot.start())
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("Bot terminated by user")
sys.exit(0)