This commit is contained in:
2026-09-17 00:56:05 +08:00
parent 33693adb66
commit c7d39938a2
14 changed files with 1005 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
"""独立读取 ETF 参数,不修改现有账户策略的默认行为。"""
from dataclasses import dataclass, fields
from pathlib import Path
import math
import re
import yaml
@dataclass(frozen=True)
class ETFConfig:
codes: tuple[str, ...] = ()
buy_hands: int = 1
max_hands: int = 10
atr_period: int = 14
atr_multiplier: float = 1.0
boll_period: int = 20
boll_std: float = 2.0
min_grid_pct: float = 0.5
rebound_pct: float = 0.61
watch_seconds: int = 600
min_profit_pct: float = 0.5
commission_rate: float = 0.0003
min_commission: float = 5.0
max_tick_age_seconds: int = 90
def __post_init__(self):
# 限定沪深场内 ETF 代码形态;具体跟踪 A 股的标的由配置白名单决定。
if not isinstance(self.codes, (list, tuple)) or not self.codes:
raise ValueError('etf.yaml 的 codes 必须是非空 ETF 代码列表')
if any(not isinstance(c, str) or not re.fullmatch(r'(?:5[0-9]{5}\.SH|1[58][0-9]{4}\.SZ)', c)
for c in self.codes) or len(set(self.codes)) != len(self.codes):
raise ValueError('ETF 代码必须唯一,使用完整沪深场内代码,如 510300.SH、159915.SZ')
object.__setattr__(self, 'codes', tuple(self.codes))
for name in ('buy_hands', 'max_hands', 'atr_period', 'boll_period', 'watch_seconds', 'max_tick_age_seconds'):
if type(getattr(self, name)) is not int or getattr(self, name) <= 0:
raise ValueError(f'{name} 必须为正整数')
if not self.buy_hands <= self.max_hands <= 10:
raise ValueError('必须满足 buy_hands <= max_hands <= 10每手 100 份')
if not 2 <= self.atr_period <= 60 or not 2 <= self.boll_period <= 60:
raise ValueError('ATR、BOLL 周期必须在 2 到 60 日之间')
for name in ('atr_multiplier', 'boll_std', 'min_grid_pct', 'rebound_pct', 'min_profit_pct',
'commission_rate', 'min_commission'):
value = getattr(self, name)
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
raise ValueError(f'{name} 必须是有限非负数')
if name not in ('commission_rate', 'min_commission') and value == 0:
raise ValueError(f'{name} 必须大于零')
def load(path: str | Path | None = None) -> ETFConfig:
path = Path(path) if path else Path(__file__).resolve().parents[2] / 'etc' / 'etf.yaml'
try:
raw = yaml.safe_load(path.read_text(encoding='utf-8'))
except (OSError, yaml.YAMLError) as exc:
raise ValueError(f'ETF 配置读取失败:{path}') from exc
if not isinstance(raw, dict) or set(raw) - {f.name for f in fields(ETFConfig)}:
raise ValueError('ETF 配置必须为对象,且不能包含未知参数')
return ETFConfig(**raw)