145 lines
5.9 KiB
Python
145 lines
5.9 KiB
Python
"""mootdx 数据源适配器。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Dict, List
|
||
|
||
from sidecar.models import KlineBar, Snapshot, parse_date, parse_float
|
||
from sidecar.symbols import mootdx_market, normalize_symbol, split_symbol
|
||
|
||
|
||
PERIOD_CATEGORY = {
|
||
"day": 4,
|
||
"week": 5,
|
||
"month": 6,
|
||
"1m": 7,
|
||
"5m": 8,
|
||
"15m": 9,
|
||
"30m": 10,
|
||
"60m": 11,
|
||
}
|
||
|
||
|
||
class MootdxSource:
|
||
"""mootdx 行情适配器。"""
|
||
|
||
def __init__(self) -> None:
|
||
"""
|
||
功能说明:创建 mootdx 数据源。
|
||
参数说明:无。
|
||
返回值说明:无返回值。
|
||
注意事项:mootdx 依赖在初始化时导入,未安装会抛出清晰异常。
|
||
"""
|
||
try:
|
||
from mootdx.quotes import Quotes
|
||
except ImportError as exc:
|
||
raise RuntimeError("请先安装 mootdx:pip install mootdx") from exc
|
||
self.client = Quotes.factory(market="std")
|
||
|
||
def fetch_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]:
|
||
"""
|
||
功能说明:通过 mootdx 获取 K 线数据。
|
||
参数说明:symbol 为股票代码,period 为周期,limit 为最大条数。
|
||
返回值说明:返回统一 KlineBar 列表。
|
||
注意事项:period 支持 day/week/month/1m/5m/15m/30m/60m。
|
||
"""
|
||
normalized = normalize_symbol(symbol)
|
||
category = PERIOD_CATEGORY.get(period)
|
||
if category is None:
|
||
raise ValueError("不支持的 K 线周期: %s" % period)
|
||
_, code = split_symbol(normalized)
|
||
raw = self._call_bars(code, mootdx_market(normalized), category, limit)
|
||
records = self._records(raw)
|
||
bars = []
|
||
for item in records:
|
||
trade_date = parse_date(item.get("datetime") or item.get("date") or item.get("time"))
|
||
if trade_date is None:
|
||
continue
|
||
bars.append(
|
||
KlineBar(
|
||
symbol=normalized,
|
||
period=period,
|
||
trade_date=trade_date,
|
||
open=parse_float(item.get("open")),
|
||
high=parse_float(item.get("high")),
|
||
low=parse_float(item.get("low")),
|
||
close=parse_float(item.get("close")),
|
||
volume=parse_float(item.get("vol") or item.get("volume")),
|
||
amount=parse_float(item.get("amount")),
|
||
source="mootdx",
|
||
)
|
||
)
|
||
return bars[-limit:]
|
||
|
||
def fetch_snapshot(self, symbol: str) -> Snapshot:
|
||
"""
|
||
功能说明:通过 mootdx 获取实时行情快照。
|
||
参数说明:symbol 为股票代码。
|
||
返回值说明:返回统一 Snapshot 模型。
|
||
注意事项:mootdx 快照字段因版本可能不同,缺失字段统一返回 None。
|
||
"""
|
||
normalized = normalize_symbol(symbol)
|
||
_, code = split_symbol(normalized)
|
||
raw = self.client.quotes(symbol=[code], market=mootdx_market(normalized))
|
||
records = self._records(raw)
|
||
if not records:
|
||
raise ValueError("mootdx 未返回快照: %s" % symbol)
|
||
item = records[0]
|
||
price = parse_float(item.get("price") or item.get("now") or item.get("last_close"))
|
||
previous_close = parse_float(item.get("last_close") or item.get("pre_close"))
|
||
return Snapshot(
|
||
symbol=normalized,
|
||
name=item.get("name"),
|
||
trade_time=None,
|
||
price=price,
|
||
previous_close=previous_close,
|
||
open=parse_float(item.get("open")),
|
||
high=parse_float(item.get("high")),
|
||
low=parse_float(item.get("low")),
|
||
volume=parse_float(item.get("vol") or item.get("volume")),
|
||
amount=parse_float(item.get("amount")),
|
||
change=None if price is None or previous_close is None else price - previous_close,
|
||
change_percent=None if price is None or previous_close in (None, 0) else (price - previous_close) / previous_close * 100,
|
||
turnover_rate=parse_float(item.get("turnover")),
|
||
pe_ttm=parse_float(item.get("pe_ttm") or item.get("pe")),
|
||
pe_static=None,
|
||
pb=parse_float(item.get("pb")),
|
||
market_cap=None,
|
||
float_market_cap=None,
|
||
limit_up=None,
|
||
limit_down=None,
|
||
source="mootdx",
|
||
)
|
||
|
||
def _call_bars(self, code: str, market: int, category: int, limit: int) -> Any:
|
||
"""
|
||
功能说明:兼容调用不同版本 mootdx 的 bars 接口。
|
||
参数说明:code 为六位股票代码,market 为市场编号,category 为周期编号,limit 为数量。
|
||
返回值说明:返回 mootdx 原始结果对象。
|
||
注意事项:不同 mootdx 版本参数名存在差异,因此保留一次兼容重试。
|
||
"""
|
||
try:
|
||
return self.client.bars(symbol=code, market=market, category=category, count=limit)
|
||
except TypeError:
|
||
return self.client.bars(symbol=code, market=market, frequency=category, offset=0)
|
||
|
||
def _records(self, raw: Any) -> List[Dict[str, Any]]:
|
||
"""
|
||
功能说明:把 mootdx 返回值转换为字典列表。
|
||
参数说明:raw 为 DataFrame、字典列表或其他可迭代对象。
|
||
返回值说明:返回普通 dict 列表。
|
||
注意事项:处理 datetime 同时存在于索引和列时的 reset_index 冲突。
|
||
"""
|
||
if hasattr(raw, "to_dict"):
|
||
data = raw
|
||
if hasattr(data, "columns") and "datetime" in data.columns:
|
||
data = data.copy()
|
||
if hasattr(data, "reset_index"):
|
||
try:
|
||
data = data.reset_index()
|
||
except ValueError:
|
||
data = data.drop(columns=["datetime"]).reset_index()
|
||
return data.to_dict("records")
|
||
if isinstance(raw, list):
|
||
return raw
|
||
return list(raw or []) |