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>
308 lines
10 KiB
Python
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)
|