"""ETF 策略入口:每 30 秒运行,日线指标当天缓存,失败标的单独重试。""" from datetime import datetime, timedelta import hashlib import logging as log from pathlib import Path import time import httpx import config from libs.calc import trading_time from libs.snapshot import cache_portfolio from sdk import Client from .config import load from .data import daily_bars from .engine import Engine from .indicators import calculate from .state import Store def StartETF() -> None: cfg = load(config.account_config.etf_config_path or None) account = str(config.account_config.account_id).strip() if not account: raise ValueError('ETF 策略缺少账户编号') key = hashlib.sha256(account.encode('utf-8')).hexdigest() store = Store(Path(config.global_config.qmt_data_dir) / 'etf' / key / 'state.json', account) # 独立 HTTP 连接池读取外部日线,不向外部接口发送 QMT 认证信息。 with Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT) as client, \ httpx.Client(timeout=config.HTTP_TIMEOUT) as history_client: engine = Engine(client, cfg, store, config.account_config.min_cash_ratio, config.account_config.excluded_codes) log.info('[ETF启动] 标的=%s 每次=%d手 每只上限=%d手 状态=%s', cfg.codes, cfg.buy_hands, cfg.max_hands, store.path) log.info('[ETF启动] 管理配置白名单内已有持仓,卖出以券商可用份额为限') indicators, retry_at = {}, {} cached_day = None while True: now = datetime.now() if now.hour >= 15: log.info('[ETF结束] 已到 15:00') return if trading_time(now): try: if cached_day != now.date(): indicators, retry_at, cached_day = {}, {}, now.date() for code in cfg.codes: if code in indicators or now < retry_at.get(code, datetime.min): continue try: rows = daily_bars(history_client, code, now.date()) indicators[code] = calculate(rows, now.date(), cfg) log.info('[ETF指标] %s %s', code, indicators[code]) except Exception: retry_at[code] = now + timedelta(minutes=5) log.exception('[ETF日线] %s 获取或计算失败,5分钟后重试', code) portfolio = client.portfolio() ticks = client.full_tick(list(cfg.codes)) engine.run(portfolio, ticks, indicators, datetime.now()) try: cache_portfolio(account, portfolio.assets, list(portfolio.positions.values()), client.deals()) except Exception: log.exception('[ETF采集] 成交快照读取失败') except Exception: log.exception('[ETF异常] 本轮失败,下一轮继续') time.sleep(30 - datetime.now().second % 30)