Files
big-qmt/py-client/config/__init__.py
2026-08-29 01:50:09 +08:00

148 lines
4.6 KiB
Python

from __future__ import annotations
import socket
from dataclasses import dataclass, field
from pathlib import Path
import yaml
@dataclass(slots=True)
class SignalConfig:
"""单个交易信号的数据源及开仓限制配置。"""
# 信号接口相对于 api_host 的路径。
url: str = ""
# 允许使用该信号的时间段;"*" 表示不限制时间。
timezone: str = "*"
# 当前价格高于信号昨收价时是否仍允许开仓。
gt_last_price_is_open: bool = False
@dataclass(slots=True)
class GlobalConfig:
"""所有主机共享的系统配置。"""
qmt_base_url: str = ""
qmt_token: str = ""
api_host: str = ""
qmt_data_dir: str = ""
# Windows 主机名到对应账户配置文件的映射。
hosts: dict[str, str] = field(default_factory=dict)
# 信号名称到信号配置的映射。
signals: dict[str, SignalConfig] = field(default_factory=dict)
@dataclass(slots=True)
class AccountConfig:
"""当前主机所使用的账户及交易策略参数。"""
account_id: str = ""
host_key: str = ""
buy_value: float = 0
min_cash_ratio: float = 0
loss_trigger_pct: float = 0
grid_step_pct: float = 1
min_profit_pct: float = 0
enable_loss_add_position: bool = False
enable_auto_ipo: bool = True
signal_allow: list[str] = field(default_factory=list)
excluded_codes: list[str] = field(default_factory=list)
# 当前账户启用的策略名称,例如 trend。
strategy: str = ""
# load() 成功后保存已加载的配置,供策略模块直接读取。
global_config: GlobalConfig | None = None
account_config: AccountConfig | None = None
# QMT 和外部 HTTP 接口的默认请求超时时间,单位为秒。
HTTP_TIMEOUT = 5.0
def load(
etc_dir: str | Path | None = None,
hostname: str | None = None,
) -> tuple[GlobalConfig, AccountConfig]:
"""加载公共配置以及当前主机对应的账户配置。
Args:
etc_dir: 配置文件目录,其中必须包含 ``_global.yaml``;为空时
默认使用 py-client 下的 ``etc`` 目录。
hostname: 指定要加载的主机名;为空时使用当前计算机名。
Returns:
由全局配置和账户配置组成的二元组。
Raises:
ValueError: 配置缺失、格式错误或策略参数不合法。
"""
global global_config, account_config
root = Path(etc_dir) if etc_dir is not None else Path(__file__).parent.parent / "etc"
raw = _yaml(root / "_global.yaml")
# 将原始字典转换为带类型的信号配置,方便业务代码使用属性访问。
signals = {
key: SignalConfig(**(value or {}))
for key, value in (raw.get("signals") or {}).items()
}
values = {
key: raw.get(key, "")
for key in ("qmt_base_url", "qmt_token", "api_host", "qmt_data_dir")
}
current = hostname or socket.gethostname()
hosts = raw.get("hosts") or {}
account_file = next(
(
value
for key, value in hosts.items()
if key.strip().lower() == current.strip().lower()
),
"",
)
# QMT 地址、外部 API 地址和数据目录是启动策略的必要参数。
if (
not values["qmt_base_url"]
or not values["api_host"]
or values["qmt_data_dir"] == "."
):
raise ValueError("Global 配置缺少必要参数")
if not account_file:
raise ValueError(f'_global.yaml 未配置计算机 "{current}"')
if not Path(account_file).suffix:
account_file += ".yaml"
global_config = GlobalConfig(**values, hosts=hosts, signals=signals)
# 策略状态文件写入该目录,启动时提前确保目录存在。
Path(global_config.qmt_data_dir).mkdir(parents=True, exist_ok=True)
account_config = AccountConfig(**_yaml(root / account_file))
if account_config.buy_value <= 0 or account_config.grid_step_pct <= 0:
raise ValueError("buy_value、grid_step_pct 必须大于 0")
if not account_config.strategy.strip():
raise ValueError("strategy 不能为空")
# host_key 统一为小写,避免不同模块比较时受大小写影响。
account_config.host_key = account_config.host_key.lower()
account_config.strategy = account_config.strategy.lower()
return global_config, account_config
def _yaml(path: Path) -> dict:
"""读取 YAML 文件,并将空文件转换为空字典。"""
try:
with path.open(encoding="utf-8") as handle:
return yaml.safe_load(handle) or {}
except (OSError, yaml.YAMLError) as exc:
raise ValueError(f"读取或解析配置 {path} 失败: {exc}") from exc