Files
big-qmt/py-client/main.py
2026-08-30 00:34:27 +08:00

137 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import sys
from apscheduler.schedulers.background import BackgroundScheduler
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)
logging.basicConfig(
level=logging.INFO,
format='[%(levelname)s] %(asctime)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
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:
logging.error(f"无法创建互斥锁,错误代码:{ctypes.get_last_error()}")
return False
if ctypes.get_last_error() == error_already_exists:
logging.error("程序已在运行中,无法启动多个实例")
kernel32.CloseHandle(handle)
return False
logging.info(f"成功获取互斥锁:{mutex_name}")
return True
except Exception as exc:
logging.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()
logging.info(f"API 服务已连通:{config.global_config.qmt_base_url}")
client.close()
return
except (APIError, httpx.RequestError) as exc:
logging.warning(
"API 服务未就绪:%s%g 秒后重试:%s",
config.global_config.qmt_base_url,
retry_interval,
exc,
)
retry_event.wait(retry_interval)
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():
logging.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()")
wait_for_qmt_api()
# 后台调度不受趋势策略永久循环阻塞;同一时刻最多执行一个实例。
scheduler = BackgroundScheduler(
timezone="Asia/Shanghai",
job_defaults={"coalesce": True, "max_instances": 1},
)
scheduler.add_job(
AutoBuyIpo,
trigger="cron",
hour="10,14",
minute=0,
id="auto_buy_ipo",
replace_existing=True,
)
scheduler.start()
logging.info("IPO 自动打新定时任务已启动:每日 10:00、14:00")
STRATEGIES[config.account_config.strategy].start_strategy()
logging.info("%s 策略启动成功",config.account_config.strateg)
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())