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-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-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-08-28 22:46:04 +08:00
|
|
|
|
except (OSError, yaml.YAMLError, ValueError, RuntimeError, KeyError) as exc:
|
2026-08-28 18:52:27 +08:00
|
|
|
|
print(f"启动失败: {exc}", file=sys.stderr, flush=True)
|
|
|
|
|
|
wait_for_any_key()
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(main())
|