Skip to content

Commit d0f569f

Browse files
yjwyjw
authored andcommitted
Add Tencent quote enrichment
1 parent 6146834 commit d0f569f

7 files changed

Lines changed: 280 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.19] - 2026-06-15
9+
10+
### Added
11+
- Added Tencent Finance stock quotes as a verified enrichment source for A-share, Hong Kong, and US single-stock snapshots, market focus-stock sections, and fund holding quotes.
12+
- Single-stock output now shows turnover, turnover rate when available, market cap, PE/PB, and 52-week range when a validated no-login source provides those fields.
13+
14+
### Changed
15+
- Sina Finance remains the primary quote path for HK/US focus stocks, while Tencent now supplements missing valuation/liquidity fields before Eastmoney `stock/get`/`clist` fallback is used.
16+
817
## [0.1.18] - 2026-06-14
918

1019
### Fixed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ The internals are being split into focused modules: `young_stock.calendar` handl
9797
## What's in the box
9898

9999
- **Multiple public quote sources** — Tencent Finance, Sina Finance, and Eastmoney are tried in sequence so temporary source failures can be filled by another no-login endpoint.
100-
- **Single-stock lookup**`young stock 600519`, `young stock 0700.HK`, or `young stock AAPL` prints a compact quote snapshot with source, trade date, price, change, volume, turnover when available, and optional news.
100+
- **Single-stock lookup**`young stock 600519`, `young stock 0700.HK`, or `young stock AAPL` prints a compact quote snapshot with source, trade date, price, change, volume, turnover, market cap, PE/PB, 52-week range when available, and optional news.
101101
- **Fund holding lookup**`young fund 161725` prints the fund's same-day estimated change, latest NAV date, top holdings, holding-stock quotes, rough contribution estimate, and same-day holding-stock news. Official fund NAVs usually update at night, so intraday/close values are clearly labeled as estimates.
102102
- **Personal daily report**`young daily` reads your local investment memory from `~/.young_stock/profile.json`, then prints saved stock/ETF trends, fund estimates, only the markets relevant to your stocks and fund top holdings, and portfolio-style suggestions grounded in your funds, stocks, holding dates, quantities, and available news. First use requires a verified symbol plus `--buy-date` and `--quantity`, for example `young profile add-stock 600519 --buy-date 2026-01-15 --quantity 100` or `young profile add-fund 161725 --buy-date 2026-01-10 --quantity 1000`.
103103
- **Short report modes**`young daily --format summary` keeps terminal output compact; `--format key-points` adds a few trend/risk bullets; `--only`, `--order`, and `--quick` trim slower or irrelevant sections.
@@ -172,6 +172,6 @@ young a -d 20260530 --refresh # 指定日期 + 强制刷新
172172
young update # 在当前 Python 环境中更新 CLI
173173
```
174174

175-
数据来源:同花顺概念资金流页面(仅在同时拿到净流入/净流出榜时采用)、腾讯财经、新浪财经、东方财富公开行情与快讯接口(`data.10jqka.com.cn` / `qt.gtimg.cn` / `hq.sinajs.cn` / `push2.eastmoney.com` / `push2ex.eastmoney.com` / `np-listapi.eastmoney.com`),多源自动切换。本地缓存 7 天,目录 `~/.young_stock/cache/`,可用 `young cache-clear` 清理。命令会标注当前阶段:上午盘、午间、下午盘或盘后;若展示的是非请求日的最新可用数据,会标注为该交易日盘后数据。普通输出不展示完整度和数据源诊断,排查问题时可设置 `YOUNG_STOCK_DEBUG=1` 查看详细切换记录。
175+
数据来源:同花顺概念资金流页面(仅在同时拿到净流入/净流出榜时采用)、腾讯财经、新浪财经、东方财富公开行情与快讯接口(`data.10jqka.com.cn` / `qt.gtimg.cn` / `hq.sinajs.cn` / `push2.eastmoney.com` / `push2ex.eastmoney.com` / `np-listapi.eastmoney.com`),多源自动切换。单只股票和基金持仓股会用腾讯财经补充成交额、换手率、市值、PE/PB、52 周高低等字段;价格主口径仍优先使用当前验证更稳的新浪/腾讯/东财链路。本地缓存 7 天,目录 `~/.young_stock/cache/`,可用 `young cache-clear` 清理。命令会标注当前阶段:上午盘、午间、下午盘或盘后;若展示的是非请求日的最新可用数据,会标注为该交易日盘后数据。普通输出不展示完整度和数据源诊断,排查问题时可设置 `YOUNG_STOCK_DEBUG=1` 查看详细切换记录。
176176

177177
适用人群:每天盘后想用一条命令看完五张图的开发者 / 量化研究者 / 自动化爱好者。欢迎 issue / PR。

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "young-stock-cli"
7-
version = "0.1.18"
7+
version = "0.1.19"
88
description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/young_stock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""young-stock-cli: A-share after-hours CLI."""
22

3-
__version__ = "0.1.18"
3+
__version__ = "0.1.19"

src/young_stock/_core.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ class QuoteData:
223223
low: float | None = None
224224
volume: int | None = None
225225
turnover: float | None = None
226+
turnover_rate: float | None = None
227+
market_cap: float | None = None
228+
float_market_cap: float | None = None
229+
pe: float | None = None
230+
pb: float | None = None
231+
high_52w: float | None = None
232+
low_52w: float | None = None
233+
amplitude_pct: float | None = None
226234
currency: str = "USD"
227235
source: str = ""
228236
quality_flags: list[str] = field(default_factory=list)
@@ -800,6 +808,19 @@ def _source_date(value: str | None) -> str:
800808
return m.group(1) if m else value
801809

802810

811+
def _tencent_source_date(value: str | None, date_str: str) -> str:
812+
value = str(value or "").strip()
813+
if not value:
814+
return _display_date(date_str)
815+
if re.match(r"\d{4}/\d{2}/\d{2}", value):
816+
return _source_date(value)
817+
if re.match(r"\d{4}-\d{2}-\d{2}", value):
818+
return _source_date(value)
819+
if re.match(r"\d{14}$", value):
820+
return f"{value[:4]}-{value[4:6]}-{value[6:8]}"
821+
return _display_date(date_str)
822+
823+
803824
def session_stage_label(dt: datetime | None = None, *, data_date: str | None = None, requested_date: str | None = None) -> str:
804825
"""返回股民更容易理解的行情阶段。历史/非请求日数据按盘后展示。"""
805826
if data_date and requested_date and data_date.replace("-", "") != requested_date:
@@ -1471,6 +1492,109 @@ def fetch_us_indices_tencent(symbols_map: dict[str, str], date_str: str) -> list
14711492
return results
14721493

14731494

1495+
def _tencent_cn_code(symbol: str) -> str:
1496+
raw = symbol.upper().replace(".SH", "").replace(".SZ", "").replace(".BJ", "")
1497+
if raw.startswith(("5", "6", "9")):
1498+
return f"sh{raw}"
1499+
if raw.startswith(("4", "8")):
1500+
return f"bj{raw}"
1501+
return f"sz{raw}"
1502+
1503+
1504+
def _tencent_hk_code(symbol: str) -> str:
1505+
raw = symbol.upper().replace(".HK", "").lstrip("0") or "0"
1506+
return f"r_hk{raw.zfill(5)}"
1507+
1508+
1509+
def _tencent_us_code(symbol: str) -> str:
1510+
return f"us{symbol.upper().lstrip('^')}"
1511+
1512+
1513+
def _tencent_stock_to_quote(fields: list[str], symbol: str, market: str, date_str: str) -> QuoteData | None:
1514+
if len(fields) < 45:
1515+
return None
1516+
currency = {"cn_market": "CNY", "hk_market": "HKD", "us_market": "USD"}.get(market, "USD")
1517+
turnover = _safe_float(fields[37] if len(fields) > 37 else None)
1518+
if market == "cn_market" and turnover is not None:
1519+
turnover *= 1e4 # A股腾讯字段为万元
1520+
qd = QuoteData(
1521+
symbol=symbol,
1522+
name=str(fields[1] or symbol),
1523+
market=market,
1524+
date=_tencent_source_date(fields[30] if len(fields) > 30 else "", date_str),
1525+
price=_safe_float(fields[3]),
1526+
prev_close=_safe_float(fields[4]),
1527+
change=_safe_float(fields[31] if len(fields) > 31 else None),
1528+
change_pct=_safe_float(fields[32] if len(fields) > 32 else None),
1529+
open_price=_safe_float(fields[5]),
1530+
high=_safe_float(fields[33] if len(fields) > 33 else None),
1531+
low=_safe_float(fields[34] if len(fields) > 34 else None),
1532+
volume=_safe_int(fields[6]),
1533+
turnover=turnover,
1534+
turnover_rate=_safe_float(fields[38] if len(fields) > 38 else None),
1535+
market_cap=_safe_float(fields[44] if len(fields) > 44 else None),
1536+
float_market_cap=_safe_float(fields[45] if len(fields) > 45 else None),
1537+
pe=_safe_float(fields[39] if len(fields) > 39 else None),
1538+
high_52w=_safe_float(fields[48] if len(fields) > 48 else None),
1539+
low_52w=_safe_float(fields[49] if len(fields) > 49 else None),
1540+
amplitude_pct=_safe_float(fields[43] if len(fields) > 43 else None),
1541+
currency=currency,
1542+
source="tencent",
1543+
)
1544+
if market == "cn_market":
1545+
qd.pb = _safe_float(fields[46] if len(fields) > 46 else None)
1546+
qd.high_52w = _safe_float(fields[47] if len(fields) > 47 else None)
1547+
qd.low_52w = _safe_float(fields[48] if len(fields) > 48 else None)
1548+
elif market == "hk_market":
1549+
# 港股腾讯字段 58 为 PB;56/57 可能随标的含义变化,不作为 PB。
1550+
qd.pb = _safe_float(fields[58] if len(fields) > 58 else None)
1551+
if qd.turnover_rate == 0:
1552+
qd.turnover_rate = None
1553+
return validate_quote(qd)
1554+
1555+
1556+
def fetch_cn_stocks_tencent(symbols: list[str], date_str: str) -> list[QuoteData]:
1557+
raw_map = _parse_tencent_batch(fetch_tencent_batch([_tencent_cn_code(s) for s in symbols]))
1558+
results: list[QuoteData] = []
1559+
for sym in symbols:
1560+
fields = raw_map.get(_tencent_cn_code(sym))
1561+
if not fields:
1562+
diag(f"Tencent missing A-share: {sym}")
1563+
continue
1564+
qd = _tencent_stock_to_quote(fields, sym, "cn_market", date_str)
1565+
if qd:
1566+
results.append(qd)
1567+
return results
1568+
1569+
1570+
def fetch_hk_stocks_tencent(symbols: list[str], date_str: str) -> list[QuoteData]:
1571+
raw_map = _parse_tencent_batch(fetch_tencent_batch([_tencent_hk_code(s) for s in symbols]))
1572+
results: list[QuoteData] = []
1573+
for sym in symbols:
1574+
fields = raw_map.get(_tencent_hk_code(sym))
1575+
if not fields:
1576+
diag(f"Tencent missing HK: {sym}")
1577+
continue
1578+
qd = _tencent_stock_to_quote(fields, sym, "hk_market", date_str)
1579+
if qd:
1580+
results.append(qd)
1581+
return results
1582+
1583+
1584+
def fetch_us_stocks_tencent(symbols: list[str], date_str: str) -> list[QuoteData]:
1585+
raw_map = _parse_tencent_batch(fetch_tencent_batch([_tencent_us_code(s) for s in symbols]))
1586+
results: list[QuoteData] = []
1587+
for sym in symbols:
1588+
fields = raw_map.get(_tencent_us_code(sym))
1589+
if not fields:
1590+
diag(f"Tencent missing US: {sym}")
1591+
continue
1592+
qd = _tencent_stock_to_quote(fields, sym, "us_market", date_str)
1593+
if qd:
1594+
results.append(qd)
1595+
return results
1596+
1597+
14741598
def _fetch_a_indices_tencent() -> list[dict[str, Any]]:
14751599
raw_map = _parse_tencent_batch(fetch_tencent_batch(list(TENCENT_A_INDEX_MAP.keys())))
14761600
result = []
@@ -1498,6 +1622,41 @@ def merge_quotes_by_symbol(primary: list[QuoteData], fallback: list[QuoteData],
14981622
return [lookup[s.upper()] for s in order if s.upper() in lookup]
14991623

15001624

1625+
def enrich_quotes_by_symbol(primary: list[QuoteData], supplemental: list[QuoteData], order: list[str]) -> list[QuoteData]:
1626+
"""保留主源价格口径,用补充源填充成交额、估值、市值等扩展字段。"""
1627+
fallback = {q.symbol.upper(): q for q in supplemental}
1628+
primary_lookup = {q.symbol.upper(): q for q in primary}
1629+
enriched: list[QuoteData] = []
1630+
enrich_fields = (
1631+
"turnover",
1632+
"turnover_rate",
1633+
"market_cap",
1634+
"float_market_cap",
1635+
"pe",
1636+
"pb",
1637+
"high_52w",
1638+
"low_52w",
1639+
"amplitude_pct",
1640+
)
1641+
for sym in order:
1642+
key = sym.upper()
1643+
qd = primary_lookup.get(key) or fallback.get(key)
1644+
if not qd:
1645+
continue
1646+
extra = fallback.get(key)
1647+
if extra and qd is not extra:
1648+
changed = False
1649+
for field_name in enrich_fields:
1650+
if getattr(qd, field_name, None) is None and getattr(extra, field_name, None) is not None:
1651+
setattr(qd, field_name, getattr(extra, field_name))
1652+
changed = True
1653+
if changed:
1654+
qd.notes.append("腾讯财经补充成交额/估值字段")
1655+
validate_quote(qd)
1656+
enriched.append(qd)
1657+
return enriched
1658+
1659+
15011660
def _has_all_quotes(quotes: list[QuoteData], order: list[str]) -> bool:
15021661
found = {q.symbol.upper() for q in quotes}
15031662
return all(symbol.upper() in found for symbol in order)
@@ -2586,6 +2745,21 @@ def print_global_stock(qd: QuoteData) -> None:
25862745
if any(f in qd.quality_flags for f in ["volume_zero", "volume_anomaly"]):
25872746
vol_str += " *"
25882747
print(f" 成交量: {vol_str}")
2748+
if qd.turnover is not None:
2749+
print(f" 成交额: {fmt_amount(qd.turnover)}")
2750+
if qd.turnover_rate is not None and qd.turnover_rate > 0:
2751+
print(f" 换手率: {fmt_pct(qd.turnover_rate)}")
2752+
if qd.market_cap is not None:
2753+
print(f" 总市值: {fmt_amount(qd.market_cap * 1e8)}")
2754+
valuation = []
2755+
if qd.pe is not None and qd.pe > 0:
2756+
valuation.append(f"PE {qd.pe:.2f}")
2757+
if qd.pb is not None and qd.pb > 0:
2758+
valuation.append(f"PB {qd.pb:.2f}")
2759+
if valuation:
2760+
print(f" 估值: {' | '.join(valuation)}")
2761+
if qd.high_52w is not None or qd.low_52w is not None:
2762+
print(f" 52周: {fmt_price(qd.low_52w)} - {fmt_price(qd.high_52w)}")
25892763
print(f" 货币: {qd.currency}")
25902764
if qd.change is not None and qd.change_pct is not None:
25912765
print(f" 涨跌: {qd.change:+.2f} ({qd.change_pct:+.2f}%)")
@@ -2819,6 +2993,10 @@ def fetch_fund_holding_quotes(holdings: list[dict[str, Any]], date_str: str) ->
28192993
if not codes:
28202994
return {}
28212995
quotes = fetch_cn_stocks_sina(codes, date_str)
2996+
tencent_quotes = fetch_cn_stocks_tencent(codes, date_str)
2997+
if not _has_all_quotes(quotes, codes):
2998+
quotes = merge_quotes_by_symbol(quotes, tencent_quotes, codes)
2999+
quotes = enrich_quotes_by_symbol(quotes, tencent_quotes, codes)
28223000
if not _has_all_quotes(quotes, codes):
28233001
quotes = merge_quotes_by_symbol(quotes, fetch_cn_stocks_direct(codes, date_str), codes)
28243002
return {q.symbol: q for q in quotes if _is_usable_quote(q)}
@@ -2833,16 +3011,28 @@ def get_single_stock_quote(symbol: str, date_str: str) -> QuoteData | None:
28333011
quotes: list[QuoteData] = []
28343012
if market == "cn_market":
28353013
quotes = fetch_cn_stocks_sina([normalized], date_str)
3014+
tencent_quotes = fetch_cn_stocks_tencent([normalized], date_str)
3015+
if not _has_all_quotes(quotes, [normalized]):
3016+
quotes = merge_quotes_by_symbol(quotes, tencent_quotes, [normalized])
3017+
quotes = enrich_quotes_by_symbol(quotes, tencent_quotes, [normalized])
28363018
if not _has_all_quotes(quotes, [normalized]):
28373019
quotes = merge_quotes_by_symbol(quotes, fetch_cn_stocks_direct([normalized], date_str), [normalized])
28383020
elif market == "hk_market":
28393021
quotes = fetch_hk_stocks_sina([normalized], date_str)
3022+
tencent_quotes = fetch_hk_stocks_tencent([normalized], date_str)
3023+
if not _has_all_quotes(quotes, [normalized]):
3024+
quotes = merge_quotes_by_symbol(quotes, tencent_quotes, [normalized])
3025+
quotes = enrich_quotes_by_symbol(quotes, tencent_quotes, [normalized])
28403026
if not _has_all_quotes(quotes, [normalized]):
28413027
quotes = merge_quotes_by_symbol(quotes, fetch_hk_stocks_direct([normalized], date_str), [normalized])
28423028
if not _has_all_quotes(quotes, [normalized]):
28433029
quotes = merge_quotes_by_symbol(quotes, fetch_em_stocks([normalized], date_str, EM_FS["hk_stock"]), [normalized])
28443030
elif market == "us_market":
28453031
quotes = fetch_us_stocks_sina([normalized], date_str)
3032+
tencent_quotes = fetch_us_stocks_tencent([normalized], date_str)
3033+
if not _has_all_quotes(quotes, [normalized]):
3034+
quotes = merge_quotes_by_symbol(quotes, tencent_quotes, [normalized])
3035+
quotes = enrich_quotes_by_symbol(quotes, tencent_quotes, [normalized])
28463036
if not _has_all_quotes(quotes, [normalized]):
28473037
quotes = merge_quotes_by_symbol(quotes, fetch_us_stocks_direct([normalized], date_str), [normalized])
28483038
if not _has_all_quotes(quotes, [normalized]):
@@ -2881,6 +3071,19 @@ def print_single_stock_report(qd: QuoteData, requested_date: str) -> None:
28813071
if qd.turnover is not None:
28823072
print(f" 成交额: {fmt_amount(qd.turnover)}")
28833073
print(f" 成交量: {fmt_volume(qd.volume)}")
3074+
if qd.turnover_rate is not None and qd.turnover_rate > 0:
3075+
print(f" 换手率: {fmt_pct(qd.turnover_rate)}")
3076+
if qd.market_cap is not None:
3077+
print(f" 总市值: {fmt_amount(qd.market_cap * 1e8)}")
3078+
valuation = []
3079+
if qd.pe is not None and qd.pe > 0:
3080+
valuation.append(f"PE {qd.pe:.2f}")
3081+
if qd.pb is not None and qd.pb > 0:
3082+
valuation.append(f"PB {qd.pb:.2f}")
3083+
if valuation:
3084+
print(f" 估值: {' | '.join(valuation)}")
3085+
if qd.high_52w is not None or qd.low_52w is not None:
3086+
print(f" 52周: {fmt_price(qd.low_52w)} - {fmt_price(qd.high_52w)}")
28843087
if qd.notes:
28853088
print(f" 提醒: {';'.join(qd.notes)}")
28863089
print()
@@ -3372,6 +3575,10 @@ def run_us_market(date_str: str, include_news: bool = True) -> None:
33723575
hot_stocks = list(dict.fromkeys([*ranked_news, *default_hot_stocks]))[:5]
33733576
print("## 重点个股行情\n")
33743577
stocks = fetch_us_stocks_sina(hot_stocks, date_str)
3578+
tencent_stocks = fetch_us_stocks_tencent(hot_stocks, date_str)
3579+
if not _has_all_quotes(stocks, hot_stocks):
3580+
stocks = merge_quotes_by_symbol(stocks, tencent_stocks, hot_stocks)
3581+
stocks = enrich_quotes_by_symbol(stocks, tencent_stocks, hot_stocks)
33753582
if not _has_all_quotes(stocks, hot_stocks):
33763583
stocks = merge_quotes_by_symbol(stocks, fetch_us_stocks_direct(hot_stocks, date_str), hot_stocks)
33773584
if not _has_all_quotes(stocks, hot_stocks):
@@ -3448,6 +3655,10 @@ def run_hk_market(date_str: str, include_news: bool = True) -> None:
34483655
hot_stocks = list(dict.fromkeys([*ranked_news, *default_hot_stocks]))[:5]
34493656
print("## 重点个股行情\n")
34503657
stocks = fetch_hk_stocks_sina(hot_stocks, date_str)
3658+
tencent_stocks = fetch_hk_stocks_tencent(hot_stocks, date_str)
3659+
if not _has_all_quotes(stocks, hot_stocks):
3660+
stocks = merge_quotes_by_symbol(stocks, tencent_stocks, hot_stocks)
3661+
stocks = enrich_quotes_by_symbol(stocks, tencent_stocks, hot_stocks)
34513662
if not _has_all_quotes(stocks, hot_stocks):
34523663
stocks = merge_quotes_by_symbol(stocks, fetch_hk_stocks_direct(hot_stocks, date_str), hot_stocks)
34533664
if not _has_all_quotes(stocks, hot_stocks):

0 commit comments

Comments
 (0)