#!/usr/bin/env python # -*- coding: utf-8 -*- import logging as log from logging.handlers import TimedRotatingFileHandler import os import sys import time import config from dataclasses import dataclass import yaml import httpx PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) GLOBAL_CONFIG_PATH = os.path.join(PROJECT_ROOT, "etc", "_global.yaml") if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) from sdk import APIError, Client from strategy.trend.boot import StartTrend from strategy.ipo import AutoBuyIpo @dataclass(frozen=True, slots=True) class StrategyDefinition: mutex_scope: str start_strategy: object STRATEGIES = { "trend": StrategyDefinition("Trend", StartTrend), } def require_windows() -> bool: return os.name == "nt" def check_single_instance(project_root: str) -> bool: """使用 Windows 命名互斥锁保证单实例。""" try: import ctypes error_already_exists = 183 invalid_handle_value = -1 safe_path = project_root.replace(":", "_").replace("\\", "_") mutex_name = f"Global\\QMT_System_{safe_path}" kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) handle = kernel32.CreateMutexW(None, True, mutex_name) if not handle or handle == invalid_handle_value: log.error(f"无法创建互斥锁,错误代码:{ctypes.get_last_error()}") return False if ctypes.get_last_error() == error_already_exists: log.error("程序已在运行中,无法启动多个实例") kernel32.CloseHandle(handle) return False log.info(f"成功获取互斥锁:{mutex_name}") return True except Exception as exc: log.error(f"单实例检测失败:{exc}", exc_info=True) return False def wait_for_qmt_api(retry_interval: float = 5.0) -> None: """循环检查 API 地址,连通后才返回。""" client = Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT) retry_event = __import__("threading").Event() while not retry_event.is_set(): try: client.assets() log.info(f"API 服务已连通:{config.global_config.qmt_base_url}") client.close() return except (APIError, httpx.RequestError) as exc: log.warning( "API 服务未就绪:%s,%g 秒后重试:%s", config.global_config.qmt_base_url, retry_interval, exc, ) retry_event.wait(retry_interval) def configure_logging(data_dir: str) -> None: """同时输出控制台日志和按天轮转的文本日志。""" log_dir = os.path.join(data_dir, "logs") os.makedirs(log_dir, exist_ok=True) root = log.getLogger() root.setLevel(log.INFO) formatter = log.Formatter("%(asctime)s [%(levelname)s] %(message)s") if not root.handlers: console = log.StreamHandler() console.setFormatter(formatter) root.addHandler(console) file_handler = TimedRotatingFileHandler( os.path.join(log_dir, "py-client.log"), when="midnight", interval=1, backupCount=30, encoding="utf-8", ) file_handler.setFormatter(formatter) root.addHandler(file_handler) def wait_for_any_key() -> None: print("按任意键退出...", flush=True) if os.name == "nt": import msvcrt msvcrt.getch() elif sys.stdin.isatty(): sys.stdin.read(1) def main() -> int: try: if not require_windows(): log.error("本程序仅支持 Windows 环境运行") return 1 if not check_single_instance(PROJECT_ROOT): return 1 config.load() if config.global_config is None or config.account_config is None: raise RuntimeError("配置尚未加载,请先调用 config.load()") configure_logging(config.global_config.qmt_data_dir) wait_for_qmt_api() # 自动打新 AutoBuyIpo() STRATEGIES[config.account_config.strategy].start_strategy() return 0 except (OSError, yaml.YAMLError, ValueError, RuntimeError, KeyError) as exc: print(f"启动失败: {exc}", file=sys.stderr, flush=True) wait_for_any_key() return 1 if __name__ == "__main__": sys.exit(main())