"""ETF 专用历史日线适配,不依赖或修改 QMT SDK。""" from datetime import date, datetime import math import re import httpx DAILY_URL = 'http://go.apinb.com/a/get_daily' def daily_bars(client: httpx.Client, code: str, today: date, count: int = 120) -> list[dict]: """读取指定证券日线;只使用 code 参数,截取历史窗口在本地完成。""" response = client.get(DAILY_URL, params={'code': code}) response.raise_for_status() return parse_daily(response.json(), code, today, count) def parse_daily(payload: dict, code: str, today: date, count: int = 120) -> list[dict]: """校验业务状态、证券归属和 OHLC,将 trade_date 转为指标需要的 date。""" if type(count) is not int or count <= 0: raise ValueError('日线数量必须为正整数') if not isinstance(payload, dict) or type(payload.get('code')) is not int or payload['code'] != 0: raise ValueError(f'日线接口业务失败:{payload.get("message", "状态无效") if isinstance(payload, dict) else "响应非对象"}') details = payload.get('details') if not isinstance(details, list) or not details: raise ValueError(f'{code} 日线接口未返回有效 details 列表') bars = {} for row in details: if not isinstance(row, dict) or row.get('ts_code') != code: raise ValueError(f'{code} 日线证券代码不一致') stamp = str(row.get('trade_date', '')) if not re.fullmatch(r'[0-9]{8}', stamp): raise ValueError(f'{code} 日线日期无效:{stamp}') day = datetime.strptime(stamp, '%Y%m%d').date() # 当前日及未来日线均不可用于盘中指标,先过滤再截取最近 count 根。 if day >= today: continue if stamp in bars: raise ValueError(f'{code} 日线日期重复:{stamp}') values = {} for key in ('open', 'high', 'low', 'close'): value = row.get(key) if isinstance(value, bool) or not isinstance(value, (int, float, str)): raise ValueError(f'{code} 日线 {key} 无效') value = float(value) if not math.isfinite(value) or value <= 0: raise ValueError(f'{code} 日线 {key} 非有限正数') values[key] = value if not (values['low'] <= values['open'] <= values['high'] and values['low'] <= values['close'] <= values['high']): raise ValueError(f'{code} 日线 OHLC 关系异常') bars[stamp] = dict(date=stamp, **values) return [bars[stamp] for stamp in sorted(bars)[-count:]]