chstockdata 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.
- chstockdata/__init__.py +158 -0
- chstockdata/_easy_tdx_bridge.py +293 -0
- chstockdata/a_stock.py +7484 -0
- chstockdata/adjusted_bars.py +726 -0
- chstockdata/block_trades.py +527 -0
- chstockdata/config.py +217 -0
- chstockdata/corporate_actions.py +479 -0
- chstockdata/market_breadth.py +944 -0
- chstockdata/mcp_server.py +110 -0
- chstockdata/northbound_data.py +585 -0
- chstockdata/northbound_store.py +219 -0
- chstockdata/point_in_time.py +140 -0
- chstockdata/policy_news.py +311 -0
- chstockdata/policy_news_models.py +222 -0
- chstockdata/policy_news_registry.py +434 -0
- chstockdata/policy_news_sources.py +1100 -0
- chstockdata/provenance.py +392 -0
- chstockdata/py.typed +0 -0
- chstockdata/refresh_vipdoc.py +386 -0
- chstockdata/source_context.py +67 -0
- chstockdata/tdx_bridge.py +199 -0
- chstockdata/trading_calendar.py +397 -0
- chstockdata/utils.py +80 -0
- chstockdata/vendor_errors.py +123 -0
- chstockdata/vipdoc_history.py +277 -0
- chstockdata-0.1.0.dist-info/METADATA +363 -0
- chstockdata-0.1.0.dist-info/RECORD +32 -0
- chstockdata-0.1.0.dist-info/WHEEL +5 -0
- chstockdata-0.1.0.dist-info/entry_points.txt +3 -0
- chstockdata-0.1.0.dist-info/licenses/LICENSE +201 -0
- chstockdata-0.1.0.dist-info/licenses/NOTICE +14 -0
- chstockdata-0.1.0.dist-info/top_level.txt +1 -0
chstockdata/__init__.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""chstockdata — free China A-share market data toolkit.
|
|
2
|
+
|
|
3
|
+
Zero third-party data SDKs, no API keys. Direct HTTP/TCP access to public
|
|
4
|
+
quote vendors (Tencent, mootdx/TDX, Eastmoney, Sina, THS, CLS, SSE/SZSE),
|
|
5
|
+
extracted and battle-tested from TradingAgents-astock.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from chstockdata import configure, get_stock_data, resolve_ticker
|
|
10
|
+
|
|
11
|
+
configure(cache_dir="./cache") # optional
|
|
12
|
+
resolve_ticker("贵州茅台") # -> "600519"
|
|
13
|
+
df = get_stock_data("600519", 365) # daily OHLCV
|
|
14
|
+
|
|
15
|
+
Eastmoney requests are throttled module-wide (``EM_MIN_INTERVAL``, default
|
|
16
|
+
1.0s) — keep it that way; do not fan out concurrent full-market scans.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .config import (
|
|
22
|
+
configure,
|
|
23
|
+
get_config,
|
|
24
|
+
get_setting,
|
|
25
|
+
reset_config,
|
|
26
|
+
validate_vipdoc_history_config,
|
|
27
|
+
)
|
|
28
|
+
from .vendor_errors import (
|
|
29
|
+
DeadlineExceeded,
|
|
30
|
+
SourceContextDeadlineExceeded,
|
|
31
|
+
VendorError,
|
|
32
|
+
VendorNetworkError,
|
|
33
|
+
VendorNoDataError,
|
|
34
|
+
VendorNotConfiguredError,
|
|
35
|
+
VendorRateLimitError,
|
|
36
|
+
)
|
|
37
|
+
from .utils import safe_ticker_component
|
|
38
|
+
|
|
39
|
+
# ── Core vendor functions (a_stock) ─────────────────────────────────────────
|
|
40
|
+
from .a_stock import (
|
|
41
|
+
ensure_name_code_map_warmup,
|
|
42
|
+
name_code_map_ready,
|
|
43
|
+
resolve_ticker,
|
|
44
|
+
reset_mootdx_client,
|
|
45
|
+
get_realtime_snapshot,
|
|
46
|
+
get_hot_concept_examples,
|
|
47
|
+
get_ohlcv_frame_cached,
|
|
48
|
+
get_stock_data,
|
|
49
|
+
get_fundamentals,
|
|
50
|
+
get_balance_sheet,
|
|
51
|
+
get_cashflow,
|
|
52
|
+
get_income_statement,
|
|
53
|
+
get_free_financial_indicators,
|
|
54
|
+
get_news,
|
|
55
|
+
get_global_news,
|
|
56
|
+
get_insider_transactions,
|
|
57
|
+
get_research_reports,
|
|
58
|
+
get_earnings_forecast,
|
|
59
|
+
get_shareholder_pledge,
|
|
60
|
+
get_corporate_buyback,
|
|
61
|
+
get_margin_trading,
|
|
62
|
+
get_valuation_history,
|
|
63
|
+
get_macro_indicators,
|
|
64
|
+
get_disclosure_schedule,
|
|
65
|
+
get_suspension_info,
|
|
66
|
+
get_delisting_info,
|
|
67
|
+
get_hot_stocks,
|
|
68
|
+
get_stock_monitor,
|
|
69
|
+
get_market_breadth,
|
|
70
|
+
get_corporate_actions,
|
|
71
|
+
get_announcement_index,
|
|
72
|
+
get_block_trades,
|
|
73
|
+
get_northbound_flow,
|
|
74
|
+
get_policy_news,
|
|
75
|
+
get_concept_blocks,
|
|
76
|
+
get_fund_flow,
|
|
77
|
+
get_dragon_tiger_board,
|
|
78
|
+
get_lockup_expiry,
|
|
79
|
+
get_industry_comparison,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# ── Adjusted bars / calendar / vipdoc history ───────────────────────────────
|
|
83
|
+
from .adjusted_bars import get_adjusted_bars
|
|
84
|
+
from .trading_calendar import local_is_trading_day, load_trading_calendar
|
|
85
|
+
from .vipdoc_history import (
|
|
86
|
+
load_vipdoc_daily,
|
|
87
|
+
vipdoc_history_dir,
|
|
88
|
+
vipdoc_history_status,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ── Policy news ─────────────────────────────────────────────────────────────
|
|
92
|
+
from .policy_news import get_policy_news_for_context
|
|
93
|
+
|
|
94
|
+
# ── Provenance (evidence envelopes; capability resolver is injectable) ─────
|
|
95
|
+
from .provenance import (
|
|
96
|
+
ATTEMPT_FAILED_AUTH,
|
|
97
|
+
ATTEMPT_FAILED_NETWORK,
|
|
98
|
+
ATTEMPT_FAILED_RATE_LIMIT,
|
|
99
|
+
ATTEMPT_FAILED_STRUCTURE,
|
|
100
|
+
ATTEMPT_NORMAL_EMPTY,
|
|
101
|
+
ATTEMPT_SKIPPED,
|
|
102
|
+
ATTEMPT_SKIPPED_DUE_TO_RUN_CIRCUIT,
|
|
103
|
+
ATTEMPT_SUCCESS,
|
|
104
|
+
COMPLETENESS_FULL,
|
|
105
|
+
COMPLETENESS_MINIMAL,
|
|
106
|
+
COMPLETENESS_PARTIAL,
|
|
107
|
+
EvidenceEnvelope,
|
|
108
|
+
ProviderAttempt,
|
|
109
|
+
make_attempt,
|
|
110
|
+
make_envelope,
|
|
111
|
+
set_capability_resolver,
|
|
112
|
+
validate_envelope,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
__version__ = "0.1.0"
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
# config
|
|
119
|
+
"configure", "get_config", "get_setting", "reset_config",
|
|
120
|
+
"validate_vipdoc_history_config",
|
|
121
|
+
# errors
|
|
122
|
+
"DeadlineExceeded", "SourceContextDeadlineExceeded", "VendorError",
|
|
123
|
+
"VendorNetworkError", "VendorNoDataError", "VendorNotConfiguredError",
|
|
124
|
+
"VendorRateLimitError",
|
|
125
|
+
# ticker safety / name resolution
|
|
126
|
+
"safe_ticker_component", "resolve_ticker", "name_code_map_ready",
|
|
127
|
+
"ensure_name_code_map_warmup", "reset_mootdx_client",
|
|
128
|
+
# quotes / OHLCV
|
|
129
|
+
"get_realtime_snapshot", "get_ohlcv_frame_cached", "get_stock_data",
|
|
130
|
+
"get_adjusted_bars", "get_stock_monitor", "get_hot_concept_examples",
|
|
131
|
+
# fundamentals / financials
|
|
132
|
+
"get_fundamentals", "get_balance_sheet", "get_cashflow",
|
|
133
|
+
"get_income_statement", "get_free_financial_indicators",
|
|
134
|
+
"get_earnings_forecast", "get_research_reports",
|
|
135
|
+
# corporate events / governance
|
|
136
|
+
"get_corporate_actions", "get_announcement_index",
|
|
137
|
+
"get_disclosure_schedule", "get_suspension_info", "get_delisting_info",
|
|
138
|
+
"get_insider_transactions", "get_shareholder_pledge",
|
|
139
|
+
"get_corporate_buyback",
|
|
140
|
+
# money flow / microstructure
|
|
141
|
+
"get_fund_flow", "get_dragon_tiger_board", "get_block_trades",
|
|
142
|
+
"get_northbound_flow", "get_market_breadth", "get_margin_trading",
|
|
143
|
+
"get_valuation_history", "get_industry_comparison", "get_concept_blocks",
|
|
144
|
+
# news / policy
|
|
145
|
+
"get_news", "get_global_news", "get_policy_news", "get_policy_news_for_context",
|
|
146
|
+
"get_hot_stocks", "get_macro_indicators",
|
|
147
|
+
# calendar / vipdoc history
|
|
148
|
+
"local_is_trading_day", "load_trading_calendar",
|
|
149
|
+
"load_vipdoc_daily", "vipdoc_history_dir", "vipdoc_history_status",
|
|
150
|
+
# provenance
|
|
151
|
+
"ATTEMPT_SUCCESS", "ATTEMPT_NORMAL_EMPTY", "ATTEMPT_FAILED_NETWORK",
|
|
152
|
+
"ATTEMPT_FAILED_AUTH", "ATTEMPT_FAILED_RATE_LIMIT",
|
|
153
|
+
"ATTEMPT_FAILED_STRUCTURE", "ATTEMPT_SKIPPED",
|
|
154
|
+
"ATTEMPT_SKIPPED_DUE_TO_RUN_CIRCUIT",
|
|
155
|
+
"COMPLETENESS_FULL", "COMPLETENESS_PARTIAL", "COMPLETENESS_MINIMAL",
|
|
156
|
+
"EvidenceEnvelope", "ProviderAttempt", "make_attempt", "make_envelope",
|
|
157
|
+
"set_capability_resolver", "validate_envelope",
|
|
158
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A small JSON bridge for an isolated easy-tdx runtime.
|
|
3
|
+
|
|
4
|
+
The main application intentionally keeps pandas 3.x. easy-tdx 1.20.6
|
|
5
|
+
declares pandas<3, so this program is executed by ``EASY_TDX_PYTHON`` from a
|
|
6
|
+
separate virtual environment rather than imported by the application process.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from datetime import date, datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 全池选优(from_best_host 会并发 ping 全部 ~52 台候选)的限频窗口。issues/023
|
|
22
|
+
# 止血:此前每次桥调用都全池扫描(资金流一次最多 2 轮 ≈ 104 个并发 TCP 连接),
|
|
23
|
+
# 从机房风控视角等同端口扫描。缓存 best_host 连不上时才允许一次选优,且全机
|
|
24
|
+
# (所有桥子进程共享该 stamp 文件)每窗口最多一次。
|
|
25
|
+
_BEST_HOST_REFRESH_INTERVAL_S = float(
|
|
26
|
+
os.environ.get("TDX_BEST_HOST_REFRESH_INTERVAL_S", str(6 * 3600))
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _refresh_stamp_path() -> Path:
|
|
31
|
+
base = Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
|
|
32
|
+
return base / "besthost-refresh.stamp"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _best_host_refresh_allowed() -> bool:
|
|
36
|
+
try:
|
|
37
|
+
return time.time() - _refresh_stamp_path().stat().st_mtime >= (
|
|
38
|
+
_BEST_HOST_REFRESH_INTERVAL_S
|
|
39
|
+
)
|
|
40
|
+
except OSError:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mark_best_host_refresh() -> None:
|
|
45
|
+
try:
|
|
46
|
+
stamp = _refresh_stamp_path()
|
|
47
|
+
stamp.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
stamp.touch()
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_clients():
|
|
54
|
+
"""Return delayed easy-tdx client factories without importing at module load.
|
|
55
|
+
|
|
56
|
+
Each factory accepts ``refresh=False``: 默认返回绑定 config.json 缓存
|
|
57
|
+
best_host 的未连接 client;``refresh=True`` 才做全池选优(from_best_host)。
|
|
58
|
+
"""
|
|
59
|
+
from easy_tdx import MacClient, Market, TdxClient
|
|
60
|
+
|
|
61
|
+
def _factory(cls):
|
|
62
|
+
def make(refresh: bool = False):
|
|
63
|
+
return cls.from_best_host() if refresh else cls()
|
|
64
|
+
|
|
65
|
+
return make
|
|
66
|
+
|
|
67
|
+
return _factory(MacClient), _factory(TdxClient), Market
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _market(market_name: str, market_enum):
|
|
71
|
+
name = market_name.upper()
|
|
72
|
+
if name not in {"SH", "SZ", "BJ"}:
|
|
73
|
+
raise ValueError(f"unsupported market: {market_name}")
|
|
74
|
+
return getattr(market_enum, name)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _records(frame) -> list[dict[str, Any]]:
|
|
78
|
+
"""Normalize a pandas DataFrame or record-like payload to JSON scalars."""
|
|
79
|
+
if frame is None:
|
|
80
|
+
return []
|
|
81
|
+
to_dict = getattr(frame, "to_dict", None)
|
|
82
|
+
raw = to_dict("records") if callable(to_dict) else list(frame)
|
|
83
|
+
|
|
84
|
+
def clean(value: Any):
|
|
85
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
86
|
+
return value
|
|
87
|
+
if isinstance(value, float):
|
|
88
|
+
return value if math.isfinite(value) else None
|
|
89
|
+
if isinstance(value, (datetime, date)):
|
|
90
|
+
return value.isoformat()
|
|
91
|
+
item = getattr(value, "item", None)
|
|
92
|
+
if callable(item):
|
|
93
|
+
return clean(item())
|
|
94
|
+
return str(value)
|
|
95
|
+
|
|
96
|
+
return [{str(key): clean(value) for key, value in row.items()} for row in raw]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _change_pct(close: Any, pre_close: Any) -> float | None:
|
|
100
|
+
"""板块涨跌幅 = (close - pre_close) / pre_close * 100;缺值/零基准返回 None。"""
|
|
101
|
+
try:
|
|
102
|
+
close_value = float(close)
|
|
103
|
+
pre_close_value = float(pre_close)
|
|
104
|
+
except (TypeError, ValueError):
|
|
105
|
+
return None
|
|
106
|
+
if (
|
|
107
|
+
not math.isfinite(close_value)
|
|
108
|
+
or not math.isfinite(pre_close_value)
|
|
109
|
+
or pre_close_value == 0
|
|
110
|
+
):
|
|
111
|
+
return None
|
|
112
|
+
return round((close_value - pre_close_value) / pre_close_value * 100, 2)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _board_type_enum():
|
|
116
|
+
"""延迟导入 BoardType:本模块只在隔离 easy-tdx 环境中执行。"""
|
|
117
|
+
from easy_tdx import BoardType
|
|
118
|
+
|
|
119
|
+
return BoardType
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _run_on(factory, operation, *, refresh: bool = False):
|
|
123
|
+
client = factory(refresh=refresh)
|
|
124
|
+
client.connect()
|
|
125
|
+
try:
|
|
126
|
+
return operation(client)
|
|
127
|
+
finally:
|
|
128
|
+
try:
|
|
129
|
+
client.close()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _with_client(factory, operation):
|
|
135
|
+
"""Run ``operation`` on one connected client, cached best_host first.
|
|
136
|
+
|
|
137
|
+
缓存 host 上连接或取数失败(含"TCP 通、协议拒"的 issues/023 形态)且选优
|
|
138
|
+
窗口允许时,才做一次全池选优重试;窗口内失败直接上抛,由主进程熔断器接管。
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
return _run_on(factory, operation)
|
|
142
|
+
except Exception:
|
|
143
|
+
if not _best_host_refresh_allowed():
|
|
144
|
+
raise
|
|
145
|
+
_mark_best_host_refresh()
|
|
146
|
+
return _run_on(factory, operation, refresh=True)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def execute(argv: list[str]) -> dict[str, Any]:
|
|
150
|
+
"""Run a bridge command and return the normalized audit payload."""
|
|
151
|
+
if not argv:
|
|
152
|
+
raise ValueError("missing command")
|
|
153
|
+
command = argv[0]
|
|
154
|
+
if command not in {"fund-flow", "industry-ranking", "belong-board", "concept-ranking"}:
|
|
155
|
+
raise ValueError(f"unsupported command: {command}")
|
|
156
|
+
mac_factory, standard_factory, market_enum = _load_clients()
|
|
157
|
+
|
|
158
|
+
if command == "fund-flow":
|
|
159
|
+
if len(argv) != 4:
|
|
160
|
+
raise ValueError("fund-flow requires market, ticker, and history count")
|
|
161
|
+
market_name, code, count_text = argv[1:]
|
|
162
|
+
if not code.isdigit() or len(code) != 6:
|
|
163
|
+
raise ValueError("ticker must be a 6-digit A-share code")
|
|
164
|
+
try:
|
|
165
|
+
history_count = min(max(int(count_text), 0), 20)
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
raise ValueError("history count must be an integer") from exc
|
|
168
|
+
market = _market(market_name, market_enum)
|
|
169
|
+
current = _with_client(
|
|
170
|
+
mac_factory, lambda client: client.get_capital_flow(market, code)
|
|
171
|
+
)
|
|
172
|
+
history = []
|
|
173
|
+
if history_count:
|
|
174
|
+
history = _with_client(
|
|
175
|
+
standard_factory,
|
|
176
|
+
lambda client: client.get_history_fund_flow(
|
|
177
|
+
market, code, 0, history_count
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
return {
|
|
181
|
+
"source": "tdx",
|
|
182
|
+
"methodology": "tdx_l1_reconstructed",
|
|
183
|
+
"current": _records(current),
|
|
184
|
+
"history": _records(history),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if command == "industry-ranking":
|
|
188
|
+
if len(argv) != 3:
|
|
189
|
+
raise ValueError("industry-ranking requires top and bottom counts")
|
|
190
|
+
try:
|
|
191
|
+
top_n = min(max(int(argv[1]), 1), 50)
|
|
192
|
+
bottom_n = min(max(int(argv[2]), 0), 50)
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
raise ValueError("ranking counts must be integers") from exc
|
|
195
|
+
|
|
196
|
+
# issues/023 止血:top/bottom 共用同一个 mac 连接(原来各连一次)。
|
|
197
|
+
def _both_rankings(client):
|
|
198
|
+
top = client.get_board_ranking(top_n=top_n, ascending=False)
|
|
199
|
+
bottom = (
|
|
200
|
+
client.get_board_ranking(top_n=bottom_n, ascending=True)
|
|
201
|
+
if bottom_n
|
|
202
|
+
else []
|
|
203
|
+
)
|
|
204
|
+
return top, bottom
|
|
205
|
+
|
|
206
|
+
top, bottom = _with_client(mac_factory, _both_rankings)
|
|
207
|
+
return {
|
|
208
|
+
"source": "tdx",
|
|
209
|
+
"methodology": "tdx_board_aggregate",
|
|
210
|
+
"taxonomy": "tdx_industry",
|
|
211
|
+
"top": _records(top),
|
|
212
|
+
"bottom": _records(bottom),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if command == "belong-board":
|
|
216
|
+
if len(argv) != 3:
|
|
217
|
+
raise ValueError("belong-board requires market and ticker")
|
|
218
|
+
market_name, code = argv[1:]
|
|
219
|
+
if not code.isdigit() or len(code) != 6:
|
|
220
|
+
raise ValueError("ticker must be a 6-digit A-share code")
|
|
221
|
+
market = _market(market_name, market_enum)
|
|
222
|
+
frame = _with_client(
|
|
223
|
+
mac_factory, lambda client: client.get_belong_board(market, code)
|
|
224
|
+
)
|
|
225
|
+
boards = _records(frame)
|
|
226
|
+
for board in boards:
|
|
227
|
+
board["change_pct"] = _change_pct(
|
|
228
|
+
board.get("close"), board.get("pre_close")
|
|
229
|
+
)
|
|
230
|
+
return {
|
|
231
|
+
"source": "tdx",
|
|
232
|
+
"methodology": "tdx_board_snapshot",
|
|
233
|
+
"taxonomy": "tdx_board",
|
|
234
|
+
"boards": boards,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if command == "concept-ranking":
|
|
238
|
+
if len(argv) != 3:
|
|
239
|
+
raise ValueError("concept-ranking requires top and bottom counts")
|
|
240
|
+
try:
|
|
241
|
+
top_n = min(max(int(argv[1]), 1), 50)
|
|
242
|
+
bottom_n = min(max(int(argv[2]), 0), 50)
|
|
243
|
+
except ValueError as exc:
|
|
244
|
+
raise ValueError("ranking counts must be integers") from exc
|
|
245
|
+
|
|
246
|
+
board_type_enum = _board_type_enum()
|
|
247
|
+
|
|
248
|
+
def _concept_changes(client):
|
|
249
|
+
frame = client.get_board_list(board_type_enum.GN)
|
|
250
|
+
rows: list[dict[str, Any]] = []
|
|
251
|
+
for record in _records(frame):
|
|
252
|
+
change = _change_pct(record.get("price"), record.get("pre_close"))
|
|
253
|
+
name = str(record.get("name") or "").strip()
|
|
254
|
+
if change is None or not name:
|
|
255
|
+
continue
|
|
256
|
+
rows.append(
|
|
257
|
+
{
|
|
258
|
+
"code": record.get("code", ""),
|
|
259
|
+
"name": name,
|
|
260
|
+
"change_pct": change,
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
rows.sort(key=lambda item: item["change_pct"], reverse=True)
|
|
264
|
+
return rows
|
|
265
|
+
|
|
266
|
+
# 全市场概念板块一次 board_list 全量返回 price/pre_close,本地算涨跌幅;
|
|
267
|
+
# 不做逐板块成分聚合(概念板块 500+,聚合开销远超行业排行)。
|
|
268
|
+
rows = _with_client(mac_factory, _concept_changes)
|
|
269
|
+
return {
|
|
270
|
+
"source": "tdx",
|
|
271
|
+
"methodology": "tdx_board_snapshot",
|
|
272
|
+
"taxonomy": "tdx_concept",
|
|
273
|
+
"top": rows[:top_n],
|
|
274
|
+
"bottom": rows[-bottom_n:][::-1] if bottom_n else [],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
raise AssertionError("validated command was not handled")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def main() -> int:
|
|
281
|
+
try:
|
|
282
|
+
print(json.dumps(execute(sys.argv[1:]), ensure_ascii=False, allow_nan=False))
|
|
283
|
+
return 0
|
|
284
|
+
except Exception as exc: # noqa: BLE001 - bridge process boundary
|
|
285
|
+
print(
|
|
286
|
+
json.dumps({"error_type": type(exc).__name__}, ensure_ascii=False),
|
|
287
|
+
file=sys.stderr,
|
|
288
|
+
)
|
|
289
|
+
return 2
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
raise SystemExit(main())
|