Files
big-qmt/py-client/main.py

155 lines
5.0 KiB
Python
Raw Normal View History

2026-08-28 18:52:27 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
2026-08-29 12:00:51 +08:00
import logging
2026-08-28 18:52:27 +08:00
import os
import sys
2026-08-31 15:33:39 +08:00
from datetime import datetime
2026-09-01 14:28:49 +08:00
import traceback
2026-08-30 00:34:27 +08:00
from apscheduler.schedulers.background import BackgroundScheduler
2026-08-28 18:52:27 +08:00
import config
from dataclasses import dataclass
import yaml
2026-08-28 22:46:04 +08:00
import httpx
2026-08-28 18:52:27 +08:00
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)
2026-08-29 12:00:51 +08:00
logging.basicConfig(
level=logging.INFO,
format='[%(levelname)s] %(asctime)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
2026-09-01 14:28:49 +08:00
logging.getLogger("apscheduler").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
2026-08-29 12:00:51 +08:00
2026-08-28 22:46:04 +08:00
from sdk import APIError, Client
2026-08-31 15:33:39 +08:00
from libs.market import refresh_market
2026-08-28 18:52:27 +08:00
from strategy.trend.boot import StartTrend
2026-08-31 13:00:22 +08:00
from strategy.zt.boot import StartZT
2026-08-29 00:44:41 +08:00
from strategy.ipo import AutoBuyIpo
2026-08-28 18:52:27 +08:00
2026-08-28 22:46:04 +08:00
@dataclass(frozen=True, slots=True)
2026-08-28 18:52:27 +08:00
class StrategyDefinition:
mutex_scope: str
start_strategy: object
STRATEGIES = {
"trend": StrategyDefinition("Trend", StartTrend),
2026-08-31 13:00:22 +08:00
"zt": StrategyDefinition("ZT", StartZT),
2026-08-28 18:52:27 +08:00
}
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:
2026-08-29 12:00:51 +08:00
logging.error(f"无法创建互斥锁,错误代码:{ctypes.get_last_error()}")
2026-08-28 18:52:27 +08:00
return False
if ctypes.get_last_error() == error_already_exists:
2026-08-29 12:00:51 +08:00
logging.error("程序已在运行中,无法启动多个实例")
2026-08-28 18:52:27 +08:00
kernel32.CloseHandle(handle)
return False
2026-08-29 12:00:51 +08:00
logging.info(f"成功获取互斥锁:{mutex_name}")
2026-08-28 18:52:27 +08:00
return True
except Exception as exc:
2026-08-29 12:00:51 +08:00
logging.error(f"单实例检测失败:{exc}", exc_info=True)
2026-08-28 18:52:27 +08:00
return False
2026-08-28 22:46:04 +08:00
def wait_for_qmt_api(retry_interval: float = 5.0) -> None:
2026-08-28 18:52:27 +08:00
"""循环检查 API 地址,连通后才返回。"""
client = Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT)
2026-08-28 22:46:04 +08:00
retry_event = __import__("threading").Event()
while not retry_event.is_set():
2026-08-28 18:52:27 +08:00
try:
2026-08-28 22:46:04 +08:00
client.assets()
2026-08-29 12:00:51 +08:00
logging.info(f"API 服务已连通:{config.global_config.qmt_base_url}")
2026-08-28 22:46:04 +08:00
client.close()
2026-08-28 18:52:27 +08:00
return
2026-08-28 22:46:04 +08:00
except (APIError, httpx.RequestError) as exc:
2026-08-29 12:00:51 +08:00
logging.warning(
2026-08-28 22:46:04 +08:00
"API 服务未就绪:%s%g 秒后重试:%s",
config.global_config.qmt_base_url,
retry_interval,
exc,
2026-08-28 18:52:27 +08:00
)
2026-08-28 22:46:04 +08:00
retry_event.wait(retry_interval)
2026-08-28 18:52:27 +08:00
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():
2026-08-30 00:34:27 +08:00
logging.error("本程序仅支持 Windows 环境运行")
2026-08-28 18:52:27 +08:00
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()")
2026-08-28 22:46:04 +08:00
wait_for_qmt_api()
2026-08-28 18:52:27 +08:00
2026-08-30 00:34:27 +08:00
# 后台调度不受趋势策略永久循环阻塞;同一时刻最多执行一个实例。
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,
)
2026-08-31 15:33:39 +08:00
scheduler.add_job(
refresh_market,
trigger="interval",
minutes=1,
args=[config.global_config.api_host],
id="market_refresh",
replace_existing=True,
next_run_time=datetime.now(),
)
2026-08-30 00:34:27 +08:00
scheduler.start()
logging.info("IPO 自动打新定时任务已启动:每日 10:00、14:00")
2026-08-31 15:33:39 +08:00
logging.info("大盘信号后台刷新已启动:每分钟一次")
2026-08-29 00:44:41 +08:00
2026-08-28 18:52:27 +08:00
STRATEGIES[config.account_config.strategy].start_strategy()
2026-08-29 12:00:51 +08:00
logging.info("%s 策略启动成功",config.account_config.strateg)
2026-08-28 18:52:27 +08:00
return 0
2026-09-01 14:28:49 +08:00
except (OSError, yaml.YAMLError, ValueError, RuntimeError, KeyError) as e:
print(f"启动失败: {e}", file=sys.stderr, flush=True)
traceback.print_exception(type(e), e, e.__traceback__)
2026-08-28 18:52:27 +08:00
wait_for_any_key()
return 1
if __name__ == "__main__":
sys.exit(main())