Tortoise/sidecar/sources/mootdx_source.py
2026-06-24 16:50:59 +08:00

146 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 is_index_symbol, 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("请先安装 mootdxpip 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(normalized, 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, symbol: str, code: str, market: int, category: int, limit: int) -> Any:
"""
功能说明:兼容调用不同版本 mootdx 的 K 线接口。
参数说明symbol 为标准代码code 为六位代码market 为市场编号category 为周期编号limit 为数量。
返回值说明:返回 mootdx 原始结果对象。
注意事项:沪深指数必须使用 index_bars普通 bars 会返回错误价格或异常日期。
"""
method = self.client.index_bars if is_index_symbol(symbol) else self.client.bars
try:
return method(symbol=code, market=market, category=category, count=limit)
except TypeError:
return method(symbol=code, market=market, frequency=category, offset=limit)
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 [])