mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 13:14:15 +08:00
+5
-2
@@ -95,6 +95,9 @@
|
||||
|
||||
新增跨边界映射时必须增加单位测试。禁止用“数值小于 1 就乘 100”一类启发式转换,这会掩盖真实数据错误。
|
||||
|
||||
五档盘口的 `bid_volumes` / `ask_volumes` 沿用现有封单量口径,单位为“手”;前端计算
|
||||
封单额时再乘 `100` 换算为股。provider 必须在数据边界完成单位转换。
|
||||
|
||||
### 3.2 价格与复权
|
||||
|
||||
- enriched 的 `open/high/low/close` 为前复权价格。
|
||||
@@ -122,7 +125,7 @@
|
||||
数据源已经插件化。任何通用功能都必须通过 provider 能力和标准化数据集访问数据,不能把 TickFlow SDK 调用硬编码到策略、监控、回测、API 或前端流程中。
|
||||
|
||||
- 使用现有的 `get_provider()`、`provider_has_dataset()` 和 preferences 路由能力。
|
||||
- 支持的数据集包括但不限于 `daily`、`adj_factor`、`minute`、`full_minute`(盘中全市场分钟落盘)、`realtime`、`financial`;新增数据集应先定义清晰的输入输出契约。
|
||||
- 支持的数据集包括但不限于 `daily`、`adj_factor`、`minute`、`full_minute`(盘中全市场分钟落盘)、`realtime`、`depth5`、`financial`;新增数据集应先定义清晰的输入输出契约。
|
||||
- provider 负责把供应商字段、单位、日期和代码格式转换为内部标准格式。
|
||||
- 上层服务依赖标准字段和能力声明,不依赖供应商响应结构。
|
||||
- 只有明确标注为 TickFlow 专属的功能才可以直接依赖 TickFlow,并且不得影响其他 provider。
|
||||
@@ -138,7 +141,7 @@
|
||||
- 各页面能力门控统一以矩阵的 `usable` 为准(生效源当前能否真正提供该能力),不是 TickFlow 套餐视角;缺能力提示统一引导到数据源配置。
|
||||
- 能力层中立:通用界面(侧栏徽章、能力路由卡、各页门控提示)不得出现 TickFlow 档位/订阅词汇;档位信息只在 TickFlow 专属详情卡展示。provider 名称作为路由事实可以出现。
|
||||
- 每个能力独立路由,禁止跟随/派生特殊值(`same_as_daily` 已下线);存量非法偏好值由 preferences getter 回退默认自愈,不做迁移。
|
||||
- 边界注记:分时监控由分钟能力兜底(`intraday_monitor_support`),不单设分时能力;`full_minute`(全量分钟)数据集已开放插件/自定义源声明;`depth5` 已进矩阵但插件数据集白名单暂未开放,当前仅 TickFlow 提供。
|
||||
- 边界注记:分时监控由分钟能力兜底(`intraday_monitor_support`),不单设分时能力;`full_minute`(全量分钟)和 `depth5` 数据集均已开放插件声明。`depth5` 独立路由且失败时不跨源回退。
|
||||
- 实时指数为产品级固定契约,不走路由矩阵:展示层(侧栏指数条、市场总览)固定核心四只(`backend/app/services/index_const.py` 单一权威:上证/深成/创业板/科创综指),后端各消费方与前端 Layout 引用同一份定义不建副本;指数页保留但标的固定为核心四只(无全指数搜索/浏览,`/api/index/list`、`/api/index/search` 已下线);侧栏指数多选配置已下线,相关偏好(`realtime_index_symbols`/`sidebar_index_symbols`/`indices_nav_pinned`/`realtime_pull_index`/`realtime_index_mode`)已删除。监控规则的指数标的不受限——quote_service 把核心四只 + 启用规则的指数并入显式拉取。
|
||||
- 自定义源指数补充协议:A 股快照普遍不含指数(fuyao 实测无指数,指数在其独立端点)。provider 可实现可选方法 `get_realtime_indices(symbols) -> list[dict] | None`(record 结构与 realtime 一致),quote_service 在自定义源分支鸭子类型调用补拉;`None` 表示请求失败,保留上轮有效指数缓存,`[]` 表示成功但无数据;未实现的源指数缓存为空,由本地日K兜底接管。fuyao 指数快照有连坐语义——请求混入未知代码整批失败,插件侧必须先行过滤不支持的后缀(如 `.BJ`)。
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class ProviderCapabilities:
|
||||
adj_factor: bool = False
|
||||
minute: bool = False
|
||||
realtime: bool = False
|
||||
depth5: bool = False
|
||||
financial: bool = False
|
||||
|
||||
|
||||
@@ -73,3 +74,6 @@ class MarketDataProvider(Protocol):
|
||||
symbols: list[str] | None = None,
|
||||
) -> pl.DataFrame:
|
||||
"""Return normalized realtime quotes. Implementations may return empty."""
|
||||
|
||||
def get_depth_batch(self, symbols: list[str]) -> dict[str, dict]:
|
||||
"""Return five-level order books keyed by symbol."""
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
能力 (capability) = 一个标准化数据集 (CONTRIBUTING「数据源插件化要求」):
|
||||
daily / adj_factor / realtime / minute / depth5 / financial (注册表顺序即设置页卡片顺序)。注册表集中声明每个
|
||||
能力的展示元数据、路由偏好字段与 TickFlow 档位要求, 前端设置页不再各自硬编码。
|
||||
depth5 目前仅 TickFlow 供 (插件数据集白名单未开放, 见 loader), 仍进矩阵是为了
|
||||
可用性门控诚实: 五档不可用时连板梯队封单/看板封单缺数据应有提示。
|
||||
depth5 与其他数据集一样可由插件声明并独立路由; 五档不可用时连板梯队封单/
|
||||
看板封单通过 usable 给出缺数据提示。
|
||||
|
||||
build_capability_matrix 把注册表、插件/自定义源的能力声明 (datasets) 和当前
|
||||
路由偏好合并为一个矩阵, 供设置页一次拉全。当前偏好由 API 层注入
|
||||
@@ -67,7 +67,6 @@ CAPABILITY_REGISTRY: list[dict] = [
|
||||
"field": "depth5_data_provider",
|
||||
"default": "tickflow",
|
||||
"tf_tier": "pro",
|
||||
# 插件契约暂未开放 depth5 数据集 (loader 白名单), 当前仅 TickFlow 供
|
||||
},
|
||||
{
|
||||
"id": "financial",
|
||||
|
||||
@@ -24,6 +24,7 @@ class TickFlowProvider:
|
||||
adj_factor=True,
|
||||
minute=True,
|
||||
realtime=True,
|
||||
depth5=True,
|
||||
financial=True,
|
||||
)
|
||||
|
||||
@@ -119,3 +120,9 @@ class TickFlowProvider:
|
||||
else:
|
||||
return pl.DataFrame()
|
||||
return pl.DataFrame(resp or [])
|
||||
|
||||
def get_depth_batch(self, symbols: list[str]) -> dict[str, dict]:
|
||||
if not symbols:
|
||||
return {}
|
||||
data = get_client().depth.batch(symbols)
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
+5
-5
@@ -135,11 +135,6 @@ async def _application_lifespan(app: FastAPI):
|
||||
# instruments/index/ETF 仍同步 (毫秒级)。应用立即 ready, 指标算完后自动替换。
|
||||
repo.refresh_cache(background=True)
|
||||
|
||||
# 能力探测
|
||||
capset = detect_capabilities()
|
||||
app.state.capabilities = capset
|
||||
logger.info("ready; %d capabilities active", len(capset.all()))
|
||||
|
||||
# 自定义数据源配置(可选): 失败只记录错误, 不影响 TickFlow 基准路径。
|
||||
try:
|
||||
from app.data_providers import custom as custom_sources
|
||||
@@ -148,6 +143,11 @@ async def _application_lifespan(app: FastAPI):
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("custom data sources init failed: %s", e)
|
||||
|
||||
# 自定义源必须先注册,能力探测才能补充其数据集能力。
|
||||
capset = detect_capabilities()
|
||||
app.state.capabilities = capset
|
||||
logger.info("ready; %d capabilities active", len(capset.all()))
|
||||
|
||||
# 全局行情服务
|
||||
qs = QuoteService()
|
||||
app.state.quote_service = qs
|
||||
|
||||
@@ -292,9 +292,30 @@ class DepthService:
|
||||
self._persist(enriched_date)
|
||||
|
||||
def _call_depth_batch(self, symbols: list[str]) -> dict:
|
||||
"""调 tf.depth.batch, 按 capset 的 batch 切片 + 节流。返回 {symbol: MarketDepth}。"""
|
||||
from app.tickflow.client import get_client
|
||||
tf = get_client()
|
||||
"""按独立五档路由取数; 所有 provider 共用分片限速且失败不跨源回退。"""
|
||||
from app.services import preferences
|
||||
|
||||
provider_name = preferences.get_depth5_data_provider()
|
||||
if provider_name == "tickflow":
|
||||
from app.data_providers.registry import get_provider
|
||||
|
||||
provider = get_provider("tickflow")
|
||||
else:
|
||||
from app.data_providers import custom as custom_sources
|
||||
|
||||
try:
|
||||
if not custom_sources.provider_has_dataset(provider_name, "depth5"):
|
||||
logger.warning("depth provider %s 未声明 depth5, 跳过本轮", provider_name)
|
||||
return {}
|
||||
provider = custom_sources.get_provider(provider_name)
|
||||
except Exception as e:
|
||||
logger.warning("depth provider %s 解析失败, 跳过本轮: %s", provider_name, e)
|
||||
return {}
|
||||
|
||||
fetch_depth = getattr(provider, "get_depth_batch", None)
|
||||
if not callable(fetch_depth):
|
||||
logger.warning("depth provider %s 未实现 get_depth_batch, 跳过本轮", provider_name)
|
||||
return {}
|
||||
|
||||
capset = self._get_capset()
|
||||
limit = resolve_limit(capset, Cap.DEPTH5_BATCH, default_batch=100, default_rpm=30)
|
||||
@@ -304,12 +325,23 @@ class DepthService:
|
||||
for i, chunk in enumerate(chunks):
|
||||
sleep_between_batches(i, limit.rpm, default_interval=2.0)
|
||||
try:
|
||||
# SDK 的 batch 内部已按 batch_size 切, 这里再切一层防单请求过大
|
||||
data = tf.depth.batch(chunk)
|
||||
data = fetch_depth(chunk)
|
||||
if isinstance(data, dict):
|
||||
result.update(data)
|
||||
else:
|
||||
logger.warning(
|
||||
"depth provider %s 第 %d 批返回非 dict, 已跳过",
|
||||
provider_name,
|
||||
i + 1,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("depth.batch 第 %d 批失败(%d 只): %s", i + 1, len(chunk), e)
|
||||
logger.warning(
|
||||
"depth provider %s 第 %d 批失败(%d 只): %s",
|
||||
provider_name,
|
||||
i + 1,
|
||||
len(chunk),
|
||||
e,
|
||||
)
|
||||
# 单批失败不影响其他批
|
||||
return result
|
||||
|
||||
|
||||
@@ -305,11 +305,12 @@ def detect_capabilities(force: bool = False) -> CapabilitySet:
|
||||
|
||||
# 数据集 → 能力映射: 第三方源声明某数据集且被选为当前 provider 时补授的能力。
|
||||
# 实时行情无对应能力键 (权限由 QuoteService.is_realtime_allowed 判定);
|
||||
# 五档盘口/WebSocket 暂无第三方数据集契约, 不增广。
|
||||
# WebSocket 暂无第三方数据集契约, 不增广。
|
||||
_DATASET_CAP_MAP: tuple[tuple[str, Cap], ...] = (
|
||||
("daily", Cap.KLINE_DAILY_BATCH),
|
||||
("adj_factor", Cap.ADJ_FACTOR),
|
||||
("minute", Cap.KLINE_MINUTE_BATCH),
|
||||
("depth5", Cap.DEPTH5_BATCH),
|
||||
("financial", Cap.FINANCIAL),
|
||||
("full_minute", Cap.INTRADAY_UNIVERSE),
|
||||
)
|
||||
@@ -327,6 +328,7 @@ def _augment_custom_sources(capset: CapabilitySet) -> None:
|
||||
"daily": daily_provider,
|
||||
"adj_factor": adj_provider,
|
||||
"minute": preferences.get_minute_data_provider(),
|
||||
"depth5": preferences.get_depth5_data_provider(),
|
||||
"financial": preferences.get_financial_provider(),
|
||||
"full_minute": preferences.get_full_minute_data_provider(),
|
||||
}
|
||||
@@ -548,8 +550,6 @@ def _compute_label_and_missing(
|
||||
|
||||
base_caps = _tier_caps_set(tiers, base)
|
||||
missing = sorted(c.value for c in (base_caps - held))
|
||||
extras = base_caps and (held - base_caps) or set() # extras 是超出该档的部分
|
||||
|
||||
# 实际超出 = held 中"既不属于本档、也不属于本档下方任何档"的 cap
|
||||
# 简化:extras = held - base_caps
|
||||
extras_set = held - base_caps
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""能力标准统一: 自定义/插件数据源能力增广回归测试。
|
||||
|
||||
对应 _augment_custom_sources 的数据集→能力映射 (daily/adj_factor/minute/financial/full_minute):
|
||||
对应 _augment_custom_sources 的数据集→能力映射
|
||||
(daily/adj_factor/minute/depth5/financial/full_minute):
|
||||
某数据集的当前 provider 非 tickflow 且声明了该数据集 → grant 对应能力;
|
||||
取数路由仍按 preferences 分流, 不会误调 TickFlow。
|
||||
"""
|
||||
@@ -13,12 +14,14 @@ from app.tickflow.policy import _augment_custom_sources
|
||||
|
||||
|
||||
def _set_providers(monkeypatch, *, daily="tickflow", adj="tickflow",
|
||||
minute="tickflow", financial="tickflow", full_minute="tickflow") -> None:
|
||||
minute="tickflow", depth5="tickflow", financial="tickflow",
|
||||
full_minute="tickflow") -> None:
|
||||
"""mock preferences 各数据集 provider getter。"""
|
||||
from app.services import preferences
|
||||
monkeypatch.setattr(preferences, "get_daily_data_provider", lambda: daily)
|
||||
monkeypatch.setattr(preferences, "get_adj_factor_provider", lambda: adj)
|
||||
monkeypatch.setattr(preferences, "get_minute_data_provider", lambda: minute)
|
||||
monkeypatch.setattr(preferences, "get_depth5_data_provider", lambda: depth5)
|
||||
monkeypatch.setattr(preferences, "get_financial_provider", lambda: financial)
|
||||
monkeypatch.setattr(preferences, "get_full_minute_data_provider", lambda: full_minute)
|
||||
|
||||
@@ -82,6 +85,15 @@ def test_minute_custom_source_grants_minute_batch(monkeypatch):
|
||||
assert capset.has(Cap.KLINE_MINUTE_BATCH)
|
||||
|
||||
|
||||
def test_depth5_custom_source_grants_depth_batch(monkeypatch):
|
||||
"""五档独立路由到声明 depth5 的自定义源时补授批量五档能力。"""
|
||||
_set_providers(monkeypatch, depth5="mock_src")
|
||||
_set_datasets(monkeypatch, {"depth5"})
|
||||
capset = CapabilitySet()
|
||||
_augment_custom_sources(capset)
|
||||
assert capset.has(Cap.DEPTH5_BATCH)
|
||||
|
||||
|
||||
def test_financial_custom_source_grants_financial(monkeypatch):
|
||||
_set_providers(monkeypatch, financial="mock_src")
|
||||
_set_datasets(monkeypatch, {"financial"})
|
||||
|
||||
@@ -203,26 +203,24 @@ def test_adj_factor_routes_independently(monkeypatch):
|
||||
|
||||
|
||||
def test_depth5_capability_semantics(monkeypatch):
|
||||
"""五档: pro+ 档 TickFlow 可供 (usable); 档位不足时不可用且无候选。
|
||||
|
||||
插件数据集白名单未开放 depth5, 假插件即使声明其他数据集也不进五档候选;
|
||||
未来契约开放后声明 depth5 的源会自然成为候选 (candidates 按 datasets 过滤)。
|
||||
"""
|
||||
"""五档可独立路由到声明 depth5 的插件, 不受 TickFlow 档位限制。"""
|
||||
_fake_sources(
|
||||
monkeypatch,
|
||||
[{"name": "fuyao", "display_name": "fuyao", "datasets": ["realtime"],
|
||||
[{"name": "depth_src", "display_name": "Depth", "datasets": ["depth5"],
|
||||
"available": True, "status": "ok"}],
|
||||
)
|
||||
# pro 档: TickFlow 进候选, 默认路由 tickflow → usable
|
||||
cap = _by_id(build_capability_matrix(dict(DEFAULT_CURRENT), tickflow_tier="pro"))["depth5"]
|
||||
assert cap["tf_available"] is True
|
||||
assert [c["name"] for c in cap["candidates"]] == ["tickflow"]
|
||||
assert [c["name"] for c in cap["candidates"]] == ["tickflow", "depth_src"]
|
||||
assert cap["usable"] is True
|
||||
# starter 档: 档位不足 → 无候选, usable False (连板梯队封单缺数据)
|
||||
cap = _by_id(build_capability_matrix(dict(DEFAULT_CURRENT), tickflow_tier="starter"))["depth5"]
|
||||
# starter 档: TickFlow 不可供, 但显式路由到插件后仍可用
|
||||
current = dict(DEFAULT_CURRENT, depth5_data_provider="depth_src")
|
||||
cap = _by_id(build_capability_matrix(current, tickflow_tier="starter"))["depth5"]
|
||||
assert cap["tf_available"] is False
|
||||
assert cap["candidates"] == []
|
||||
assert cap["usable"] is False
|
||||
assert [c["name"] for c in cap["candidates"]] == ["depth_src"]
|
||||
assert cap["effective"] == "depth_src"
|
||||
assert cap["usable"] is True
|
||||
|
||||
|
||||
def test_unknown_current_display_falls_back_to_name(monkeypatch):
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Custom depth provider routing and failure-isolation tests."""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from app.services import depth_service as depth_module
|
||||
from app.services.depth_service import DepthService
|
||||
from app.tickflow.capabilities import Cap, CapabilityLimits, CapabilitySet
|
||||
|
||||
|
||||
def _service(*, batch: int = 2, rpm: int = 30) -> DepthService:
|
||||
service = DepthService()
|
||||
service._app_state = SimpleNamespace(
|
||||
capabilities=CapabilitySet({
|
||||
Cap.DEPTH5_BATCH: CapabilityLimits(batch=batch, rpm=rpm),
|
||||
}),
|
||||
)
|
||||
return service
|
||||
|
||||
|
||||
def test_custom_depth_uses_shared_batching_and_rate_limit(monkeypatch):
|
||||
provider = SimpleNamespace(
|
||||
get_depth_batch=MagicMock(
|
||||
side_effect=lambda symbols: {symbol: {"ask_volumes": [0]} for symbol in symbols}
|
||||
)
|
||||
)
|
||||
sleep = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_depth5_data_provider",
|
||||
lambda: "custom_depth",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.data_providers.custom.provider_has_dataset",
|
||||
lambda name, dataset: name == "custom_depth" and dataset == "depth5",
|
||||
)
|
||||
monkeypatch.setattr("app.data_providers.custom.get_provider", lambda name: provider)
|
||||
monkeypatch.setattr(depth_module, "sleep_between_batches", sleep)
|
||||
|
||||
result = _service()._call_depth_batch(["A", "B", "C", "D", "E"])
|
||||
|
||||
assert set(result) == {"A", "B", "C", "D", "E"}
|
||||
assert provider.get_depth_batch.call_args_list == [
|
||||
call(["A", "B"]),
|
||||
call(["C", "D"]),
|
||||
call(["E"]),
|
||||
]
|
||||
assert sleep.call_args_list == [
|
||||
call(0, 24, default_interval=2.0),
|
||||
call(1, 24, default_interval=2.0),
|
||||
call(2, 24, default_interval=2.0),
|
||||
]
|
||||
|
||||
|
||||
def test_custom_depth_failure_does_not_fall_back_to_tickflow(monkeypatch):
|
||||
provider = SimpleNamespace(
|
||||
get_depth_batch=MagicMock(side_effect=RuntimeError("custom source down"))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_depth5_data_provider",
|
||||
lambda: "custom_depth",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.data_providers.custom.provider_has_dataset",
|
||||
lambda name, dataset: True,
|
||||
)
|
||||
monkeypatch.setattr("app.data_providers.custom.get_provider", lambda name: provider)
|
||||
monkeypatch.setattr(
|
||||
"app.tickflow.client.get_client",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("must not fall back to TickFlow")),
|
||||
)
|
||||
|
||||
assert _service()._call_depth_batch(["A"]) == {}
|
||||
provider.get_depth_batch.assert_called_once_with(["A"])
|
||||
|
||||
|
||||
def test_custom_depth_failure_isolated_per_batch(monkeypatch):
|
||||
provider = SimpleNamespace(
|
||||
get_depth_batch=MagicMock(
|
||||
side_effect=[
|
||||
RuntimeError("first batch down"),
|
||||
{"C": {"ask_volumes": [0]}},
|
||||
]
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_depth5_data_provider",
|
||||
lambda: "custom_depth",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.data_providers.custom.provider_has_dataset",
|
||||
lambda name, dataset: True,
|
||||
)
|
||||
monkeypatch.setattr("app.data_providers.custom.get_provider", lambda name: provider)
|
||||
monkeypatch.setattr(depth_module, "sleep_between_batches", MagicMock())
|
||||
|
||||
assert _service()._call_depth_batch(["A", "B", "C"]) == {
|
||||
"C": {"ask_volumes": [0]},
|
||||
}
|
||||
assert provider.get_depth_batch.call_count == 2
|
||||
|
||||
|
||||
def test_tickflow_depth_uses_provider_contract_and_shared_batching(monkeypatch):
|
||||
batch = MagicMock(
|
||||
side_effect=lambda symbols: {symbol: {"ask_volumes": [0]} for symbol in symbols}
|
||||
)
|
||||
tickflow = SimpleNamespace(depth=SimpleNamespace(batch=batch))
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_depth5_data_provider",
|
||||
lambda: "tickflow",
|
||||
)
|
||||
monkeypatch.setattr("app.data_providers.tickflow_provider.get_client", lambda: tickflow)
|
||||
monkeypatch.setattr(depth_module, "sleep_between_batches", MagicMock())
|
||||
|
||||
result = _service()._call_depth_batch(["A", "B", "C"])
|
||||
|
||||
assert set(result) == {"A", "B", "C"}
|
||||
assert batch.call_args_list == [call(["A", "B"]), call(["C"])]
|
||||
|
||||
|
||||
def test_invalid_custom_depth_contract_fails_closed(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.preferences.get_depth5_data_provider",
|
||||
lambda: "broken_depth",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.data_providers.custom.provider_has_dataset",
|
||||
lambda name, dataset: False,
|
||||
)
|
||||
get_provider = MagicMock()
|
||||
monkeypatch.setattr("app.data_providers.custom.get_provider", get_provider)
|
||||
|
||||
assert _service()._call_depth_batch(["A"]) == {}
|
||||
get_provider.assert_not_called()
|
||||
@@ -28,7 +28,7 @@ display_name: "我的数据源" # 设置页显示名
|
||||
runtime: none # 运行时类型: node | python | none
|
||||
entry: app.plugins.my_source.provider:MyProvider # provider 类的导入路径
|
||||
check: app.plugins.my_source.bridge:availability # 可用性检测函数(可选)
|
||||
datasets: [realtime] # 支持的数据集: daily/adj_factor/minute/realtime/financial
|
||||
datasets: [realtime] # 支持: daily/adj_factor/minute/realtime/depth5/financial
|
||||
api_key_env: MY_SOURCE_API_KEY # (可选)声明后设置页提供 Key 输入框
|
||||
hidden: false # (可选)true = 已加载但对设置页隐藏,不注册不展示
|
||||
description: "数据源描述"
|
||||
@@ -166,6 +166,9 @@ class MyProvider:
|
||||
强烈建议实现本方法, 否则指数行情冻结在本地日K兜底。失败返回 None,
|
||||
成功但无数据返回 []。"""
|
||||
|
||||
def get_depth_batch(self, symbols: list[str]) -> dict[str, dict]:
|
||||
"""(声明 depth5 时实现)五档盘口, 返回以 symbol 为键的标准盘口字典。"""
|
||||
|
||||
def get_financials(self, table, symbols, latest_only=False) -> pl.DataFrame:
|
||||
"""财务数据(声明 financial 数据集时实现, table 见 financial_sync 调用)。"""
|
||||
|
||||
@@ -178,6 +181,22 @@ class MyProvider:
|
||||
返回 error 字段说明会回退 TickFlow。"""
|
||||
```
|
||||
|
||||
`get_depth_batch` 返回结构如下。价格和数量数组均按一档到五档排列;数量单位为“手”,
|
||||
`timestamp` 为毫秒 Unix 时间戳。服务层按 capability 的 `batch` / `rpm` 统一分片限速,
|
||||
provider 不应自行切换或回退到其他数据源。
|
||||
|
||||
```python
|
||||
{
|
||||
"600519.SH": {
|
||||
"bid_prices": [1500.0, 1499.9, 1499.8, 1499.7, 1499.6],
|
||||
"bid_volumes": [10, 20, 30, 40, 50],
|
||||
"ask_prices": [1500.1, 1500.2, 1500.3, 1500.4, 1500.5],
|
||||
"ask_volumes": [12, 22, 32, 42, 52],
|
||||
"timestamp": 1788505200000,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### get_minute 的 datetime 时区契约
|
||||
|
||||
`datetime` 必须是**北京时间墙钟**(naive,如 `2026-08-28 09:35:00`),与日K的
|
||||
@@ -219,6 +238,7 @@ class MyProvider:
|
||||
| --- | --- |
|
||||
| `get_realtime` | **软失败**: 返回 `[]` + warning 日志, 保证轮询线程不中断 |
|
||||
| `get_realtime_indices` | **软失败**: 返回 `None` + warning 日志, 保留上轮有效缓存; 成功无数据返回 `[]` |
|
||||
| `get_depth_batch` | 单批异常由服务隔离并保留其他批次; 不跨数据源回退 |
|
||||
| `get_minute` | 抛异常时调用方自动回退 TickFlow 重试 |
|
||||
| `get_daily` / `get_adj_factors` / `get_financials` | 异常由上层同步流程捕获记录; 无数据返回空 DataFrame |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user