datapush-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
datapush/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ """datapush SDK —— A 股行情数据(REST + WebSocket)与本地按需缓存。
2
+
3
+ 两种用法,前缀都是 `datapush`::
4
+
5
+ # 1) Python
6
+ import datapush
7
+ c = datapush.Client() # Key 自动读 DATAPUSH_API_KEY / ~/.datapush/config.json
8
+ bars = c.kline("000001", days=250) # 一行拿到日K(自动缓存 + 增量刷新)
9
+ for m in c.watch("000001", types=["quote"]): ...
10
+
11
+ # 2) 命令行
12
+ $ datapush kline 000001 --days 250
13
+ $ datapush quote 000001
14
+ $ datapush cache info
15
+
16
+ 设计要点:
17
+ - **零第三方运行时依赖**(只用标准库),`pip install datapush-sdk` 即刻可用;
18
+ - **按需本地缓存**:只存不复权原始价 + 复权因子表,复权值读取时现算,除权后无需重下历史;
19
+ - 实时接口(股票列表 / 基础信息 / 交易日 / 板块)**直连不缓存**。
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from ._endpoints import PERIODS
25
+ from ._version import __version__
26
+ from .adjust import adjust_bars, pick_segment, turnover, verify_segments
27
+ from .cache import Cache
28
+ from .client import Client
29
+ from .config import DEFAULT_BASE_URL, config_file, default_cache_file, home_dir
30
+ from .errors import (
31
+ AuthError,
32
+ BadRequestError,
33
+ ConfigError,
34
+ DatapushError,
35
+ NetworkError,
36
+ NotFoundError,
37
+ OfflineError,
38
+ PermissionError,
39
+ ProtocolError,
40
+ QuotaError,
41
+ RateLimitError,
42
+ ServerError,
43
+ WSError,
44
+ )
45
+ from .models import (
46
+ AdjSegment,
47
+ AdjTable,
48
+ Bar,
49
+ Depth,
50
+ FactorTable,
51
+ KlinePage,
52
+ Quote,
53
+ StockDetail,
54
+ StockInfo,
55
+ WSEvent,
56
+ )
57
+ from .rest import ApiResponse, RestClient
58
+ from .ws import WsClient
59
+
60
+ __all__ = [
61
+ "__version__",
62
+ # 客户端
63
+ "Client",
64
+ "RestClient",
65
+ "WsClient",
66
+ "Cache",
67
+ "ApiResponse",
68
+ # 模型
69
+ "Bar",
70
+ "KlinePage",
71
+ "Quote",
72
+ "Depth",
73
+ "WSEvent",
74
+ "StockInfo",
75
+ "StockDetail",
76
+ "AdjTable",
77
+ "AdjSegment",
78
+ "FactorTable",
79
+ # 本地复权
80
+ "adjust_bars",
81
+ "pick_segment",
82
+ "turnover",
83
+ "verify_segments",
84
+ # 配置与常量
85
+ "PERIODS",
86
+ "DEFAULT_BASE_URL",
87
+ "home_dir",
88
+ "config_file",
89
+ "default_cache_file",
90
+ # 异常
91
+ "DatapushError",
92
+ "ConfigError",
93
+ "OfflineError",
94
+ "NetworkError",
95
+ "AuthError",
96
+ "QuotaError",
97
+ "PermissionError",
98
+ "NotFoundError",
99
+ "RateLimitError",
100
+ "BadRequestError",
101
+ "ServerError",
102
+ "ProtocolError",
103
+ "WSError",
104
+ ]
datapush/_endpoints.py ADDED
@@ -0,0 +1,68 @@
1
+ """datapush 后端接口路径的**唯一来源**。
2
+
3
+ CLI、SDK 与测试都从这里取路径,好处有三:
4
+
5
+ 1. 拼错路径在开发期就暴露(而不是线上 404);
6
+ 2. `tests/test_endpoints_match_router.py` 可用本文件的路径集合与后端
7
+ `server_go/internal/router/router.go` 做对照,防止 SDK 与后端契约漂移;
8
+ 3. 文档里出现的路径可被 `grep` 一次核清。
9
+
10
+ 后端统一响应:`{"code": 0, "message": "ok", "data": ...}`(`code` 与 HTTP 状态码同值)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ # ---- 公开(无需 API Key)----
16
+ HEALTH = "/health"
17
+ STATS = "/api/v1/stats"
18
+ MARKET_BOARD = "/api/v1/market/board"
19
+ MARKET_FEATURED = "/api/v1/market/featured"
20
+
21
+ # ---- 需 API Key ----
22
+ STOCKS = "/api/v1/stocks" # 列表:page / page_size(≤1000)
23
+ STOCK = "/api/v1/stocks/{code}" # 单只基础信息(12 字段)
24
+ KLINE = "/api/v1/stocks/{code}/kline" # 日K(offset/size/since/adj)
25
+ MINUTE = "/api/v1/stocks/{code}/minute" # 1 分钟 K 线(offset/size/since/adj)
26
+ FIVEMINUTE = "/api/v1/stocks/{code}/fiveminute" # 5 分钟 K 线
27
+ XDXR = "/api/v1/stocks/{code}/xdxr" # 复权因子表(segments + ltgb + latest_event_date)
28
+ FACTOR_CATALOG = "/api/v1/factors" # 量化因子目录(高级用户)
29
+ FACTORS = "/api/v1/stocks/{code}/factors" # 量化因子数组(日K + 因子列)
30
+ TRADING_TODAY = "/api/v1/trading/today" # 今日是否交易 / 交易日信息
31
+ MARKET_BOARDS = "/api/v1/market/boards" # 指数 / 板块清单
32
+
33
+ # ---- WebSocket ----
34
+ WS = "/ws" # 需 API Key 的实时推送
35
+ WS_BOARD = "/ws/board" # 公开:首页行情面板广播
36
+
37
+ #: 支持缓存的 K 线周期(与后端 `period` 取值一致)
38
+ PERIODS = ("day", "1m", "5m")
39
+
40
+ #: 「必须联网、不缓存」的接口(列表 / 基础信息 / 交易日 / 板块 / 公开接口)
41
+ NO_CACHE_PATHS = frozenset(
42
+ {HEALTH, STATS, STOCKS, STOCK, TRADING_TODAY, MARKET_BOARDS, MARKET_BOARD, MARKET_FEATURED}
43
+ )
44
+
45
+ #: 全部后端路径模板(含 `{code}` 占位)——供防漂移测试对照 router.go
46
+ PATH_TEMPLATES = (
47
+ HEALTH,
48
+ STATS,
49
+ MARKET_BOARD,
50
+ MARKET_FEATURED,
51
+ STOCKS,
52
+ STOCK,
53
+ KLINE,
54
+ MINUTE,
55
+ FIVEMINUTE,
56
+ XDXR,
57
+ FACTOR_CATALOG,
58
+ FACTORS,
59
+ TRADING_TODAY,
60
+ MARKET_BOARDS,
61
+ WS,
62
+ WS_BOARD,
63
+ )
64
+
65
+
66
+ def format_path(template: str, code: str = "") -> str:
67
+ """把 `{code}` 占位替换成标的代码(其余模板原样返回)。"""
68
+ return template.replace("{code}", code) if code else template
datapush/_version.py ADDED
@@ -0,0 +1,7 @@
1
+ """版本号唯一来源。
2
+
3
+ `pyproject.toml` 通过 `[tool.setuptools.dynamic] version = { attr = "datapush._version.__version__" }`
4
+ 引用本文件,因此只要改这里,包元数据、`datapush.__version__` 与 CLI `datapush --version` 同步变化。
5
+ """
6
+
7
+ __version__ = "0.1.0"
datapush/adjust.py ADDED
@@ -0,0 +1,114 @@
1
+ """本地复权计算:与 `pages/docs.html#xdxr` 的公式**逐字一致**。
2
+
3
+ 三步(客户端只需一次乘法):
4
+
5
+ 1. **选段**:`segments` 按 `from` 降序,取第一个 `from <= bar.date` 的段;都不满足(或 `from` 为空)⇒ 兜底段;
6
+ 2. **换算**:`前复权价 = raw价 × seg.qfq`、`后复权价 = raw价 × seg.hfq`、`成交额`同理、`换手率 = cjl / ltgb × share_ratio × 100`;
7
+ 3. **自检**:`seg.hfq × P == seg.qfq`,其中 `P = 兜底段.qfq`。
8
+
9
+ 两条注意(都在文档里强调过):
10
+
11
+ - **按段因子一次乘完**(不要逐事件累乘、不要反复乘除),这样与服务端复权视图的数值口径一致;
12
+ - **成交量 `cjl` 不缩放**(只有价格与成交额缩放),换手率与价格口径无关(分母是「当日流通股本」)。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Iterable, List, Optional, Sequence, Union
18
+
19
+ from .errors import ProtocolError
20
+ from .models import AdjSegment, AdjTable, Bar
21
+
22
+ __all__ = ["pick_segment", "scale_bar", "adjust_bars", "turnover", "verify_segments"]
23
+
24
+ Segments = Union[AdjTable, Sequence[AdjSegment]]
25
+ KIND_RAW, KIND_QFQ, KIND_HFQ = "raw", "qfq", "hfq"
26
+
27
+
28
+ def _segments_of(table: Segments) -> List[AdjSegment]:
29
+ """把 `AdjTable` 或 `list[AdjSegment]` 统一成段列表(保持**降序**)。"""
30
+ if isinstance(table, AdjTable):
31
+ return list(table.segments)
32
+ return list(table)
33
+
34
+
35
+ def pick_segment(table: Segments, date: str) -> AdjSegment:
36
+ """按日期选适用段;`date` 支持 `YYYY-MM-DD` 或完整时间戳(只取前 10 位)。"""
37
+ segs = _segments_of(table)
38
+ if not segs:
39
+ raise ProtocolError("复权因子表为空(至少应有一段兜底段)")
40
+ day = date[:10]
41
+ for seg in segs: # 已按 from 降序:第一个满足 from <= day 的即是适用段
42
+ if seg.from_date and seg.from_date <= day:
43
+ return seg
44
+ return segs[-1] # 兜底段(from 为空)
45
+
46
+
47
+ def turnover(cjl: float, ltgb: float, share_ratio: float = 1.0) -> float:
48
+ """换手率 %:`cjl / ltgb × share_ratio × 100`(乘除顺序与服务端 `turnover()` 一致)。"""
49
+ if not ltgb or ltgb <= 0:
50
+ return 0.0
51
+ ratio = share_ratio if share_ratio and share_ratio > 0 else 1.0
52
+ return cjl / ltgb * ratio * 100.0
53
+
54
+
55
+ def scale_bar(bar: Bar, seg: AdjSegment, kind: str = KIND_QFQ) -> Bar:
56
+ """按段因子换算一根 K 线(`kind=raw` 原样返回;`cjl` 恒不缩放)。"""
57
+ if kind == KIND_RAW:
58
+ return bar
59
+ factor = seg.qfq if kind == KIND_QFQ else seg.hfq
60
+ return Bar(
61
+ open=bar.open * factor,
62
+ close=bar.close * factor,
63
+ high=bar.high * factor,
64
+ low=bar.low * factor,
65
+ cjl=bar.cjl, # 成交量不缩放
66
+ cje=bar.cje * factor,
67
+ hsl=bar.hsl, # 换手率与价格口径无关:保持缓存里原始价自带的值(需要时用 recompute_hsl)
68
+ time=bar.time,
69
+ )
70
+
71
+
72
+ def adjust_bars(
73
+ bars: Iterable[Bar],
74
+ table: Segments,
75
+ kind: str = KIND_QFQ,
76
+ *,
77
+ ltgb: Optional[float] = None,
78
+ recompute_hsl: bool = False,
79
+ ) -> List[Bar]:
80
+ """批量复权。
81
+
82
+ - `kind`:`raw` / `qfq` / `hfq`;
83
+ - `ltgb` + `recompute_hsl=True` 时,按 `cjl / ltgb × share_ratio × 100` **重算**换手率
84
+ (原始价缓存里已带 hsl,通常不必重算;需要与服务端因口径变化对齐时才用)。
85
+ """
86
+ segs = _segments_of(table)
87
+ if not segs:
88
+ raise ProtocolError("复权因子表为空(至少应有一段兜底段)")
89
+ out: List[Bar] = []
90
+ for bar in bars:
91
+ seg = pick_segment(segs, bar.time)
92
+ scaled = scale_bar(bar, seg, kind)
93
+ if recompute_hsl and ltgb:
94
+ scaled = Bar(
95
+ open=scaled.open,
96
+ close=scaled.close,
97
+ high=scaled.high,
98
+ low=scaled.low,
99
+ cjl=scaled.cjl,
100
+ cje=scaled.cje,
101
+ hsl=turnover(bar.cjl, ltgb, seg.share_ratio),
102
+ time=scaled.time,
103
+ )
104
+ out.append(scaled)
105
+ return out
106
+
107
+
108
+ def verify_segments(table: Segments, tol: float = 1e-12) -> bool:
109
+ """自检 `hfq × P == qfq`(逐段;`P` = 兜底段 `qfq`)。"""
110
+ segs = _segments_of(table)
111
+ if not segs:
112
+ return False
113
+ p = segs[-1].qfq
114
+ return all(abs(seg.hfq * p - seg.qfq) <= tol for seg in segs)