Skip to content

Commit ff195f4

Browse files
committed
feat: improve Asia fundamental data pipeline
1 parent 48c9417 commit ff195f4

9 files changed

Lines changed: 992 additions & 47 deletions

‎backend_api_python/app/data_sources/cn_hk_fundamentals.py‎

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import math
1616
import os
17+
import threading
1718
import time
1819
from contextlib import contextmanager
1920
from typing import Any, Dict, Generator, Optional
@@ -54,6 +55,9 @@ def _bypass_proxy() -> Generator[None, None, None]:
5455
_TD_TIMEOUT = 15
5556
_TD_MAX_ATTEMPTS = 2
5657
_TD_BACKOFF_SEC = 2.0
58+
_HK_INDICATOR_CACHE_TTL_SEC = 1_800
59+
_HK_INDICATOR_CACHE: Dict[str, tuple[float, Any]] = {}
60+
_HK_INDICATOR_CACHE_LOCK = threading.Lock()
5761

5862

5963
def _float_clean(x: Any) -> Optional[float]:
@@ -68,6 +72,30 @@ def _float_clean(x: Any) -> Optional[float]:
6872
return None
6973

7074

75+
def _hk_financial_indicator_frame(hk5: str):
76+
now = time.monotonic()
77+
with _HK_INDICATOR_CACHE_LOCK:
78+
cached = _HK_INDICATOR_CACHE.get(hk5)
79+
if cached and cached[0] > now:
80+
return cached[1].copy(deep=True)
81+
try:
82+
import akshare as ak # type: ignore
83+
84+
with _bypass_proxy():
85+
frame = ak.stock_hk_financial_indicator_em(symbol=hk5)
86+
except Exception as exc:
87+
logger.debug("stock_hk_financial_indicator_em failed %s: %s", hk5, exc)
88+
return None
89+
if frame is None or frame.empty:
90+
return frame
91+
with _HK_INDICATOR_CACHE_LOCK:
92+
_HK_INDICATOR_CACHE[hk5] = (
93+
now + _HK_INDICATOR_CACHE_TTL_SEC,
94+
frame.copy(deep=True),
95+
)
96+
return frame
97+
98+
7199
# ---------------------------------------------------------------------------
72100
# Twelve Data fundamentals (globally stable, paid)
73101
# ---------------------------------------------------------------------------
@@ -426,13 +454,7 @@ def fetch_hk_fundamental_akshare(tencent_code: str) -> Dict[str, Any]:
426454
if not hk5:
427455
return {}
428456
result: Dict[str, Any] = {"source": "akshare_em"}
429-
try:
430-
import akshare as ak # type: ignore
431-
with _bypass_proxy():
432-
df = ak.stock_hk_financial_indicator_em(symbol=hk5)
433-
except Exception as e:
434-
logger.debug("stock_hk_financial_indicator_em failed %s: %s", hk5, e)
435-
return result
457+
df = _hk_financial_indicator_frame(hk5)
436458
if df is None or df.empty:
437459
return result
438460
r = df.iloc[0]
@@ -445,6 +467,13 @@ def fetch_hk_fundamental_akshare(tencent_code: str) -> Dict[str, Any]:
445467
if mcap is not None:
446468
result["market_cap"] = mcap
447469
result["dividend_yield"] = _float_clean(r.get("股息率TTM(%)"))
470+
result["revenue"] = _float_clean(r.get("营业总收入(元)")) or _float_clean(r.get("营业总收入"))
471+
result["net_income"] = _float_clean(r.get("净利润(元)")) or _float_clean(r.get("净利润"))
472+
result["book_value"] = _float_clean(r.get("每股净资产(元)"))
473+
result["shares_outstanding"] = _float_clean(r.get("总股本(股)")) or _float_clean(r.get("已发行股本(股)"))
474+
if result.get("book_value") is not None and result.get("shares_outstanding") is not None:
475+
result["shareholder_equity"] = result["book_value"] * result["shares_outstanding"]
476+
result["revenue_growth"] = _float_clean(r.get("营业总收入滚动环比增长(%)"))
448477
return result
449478

450479

@@ -671,23 +700,40 @@ def fetch_hk_financial_indicators(tencent_code: str) -> Dict[str, Any]:
671700
result: Dict[str, Any] = {}
672701

673702
try:
674-
import akshare as ak # type: ignore
675-
with _bypass_proxy():
676-
df = ak.stock_hk_financial_indicator_em(symbol=hk5)
703+
df = _hk_financial_indicator_frame(hk5)
677704
if df is None or df.empty:
678705
return result
679706

680707
curr = df.iloc[0]
681708
prev = df.iloc[1] if len(df) > 1 else None
682709

683-
rev_curr = _float_clean(curr.get("营业总收入(元)")) or _float_clean(curr.get("营业收入(元)"))
710+
rev_curr = (
711+
_float_clean(curr.get("营业总收入(元)"))
712+
or _float_clean(curr.get("营业收入(元)"))
713+
or _float_clean(curr.get("营业总收入"))
714+
or _float_clean(curr.get("营业收入"))
715+
)
716+
direct_growth = _float_clean(curr.get("营业总收入滚动环比增长(%)"))
717+
if direct_growth is not None:
718+
result["revenue_growth"] = direct_growth
719+
direct_margin = _float_clean(curr.get("销售净利率(%)"))
720+
if direct_margin is not None:
721+
result["profit_margin"] = direct_margin
684722
if prev is not None:
685-
rev_prev = _float_clean(prev.get("营业总收入(元)")) or _float_clean(prev.get("营业收入(元)"))
686-
result["revenue_growth"] = _pct_change(rev_curr, rev_prev)
687-
688-
net_curr = _float_clean(curr.get("净利润(元)"))
689-
net_prev = _float_clean(prev.get("净利润(元)"))
723+
rev_prev = (
724+
_float_clean(prev.get("营业总收入(元)"))
725+
or _float_clean(prev.get("营业收入(元)"))
726+
or _float_clean(prev.get("营业总收入"))
727+
or _float_clean(prev.get("营业收入"))
728+
)
729+
if result.get("revenue_growth") is None:
730+
result["revenue_growth"] = _pct_change(rev_curr, rev_prev)
731+
732+
net_curr = _float_clean(curr.get("净利润(元)")) or _float_clean(curr.get("净利润"))
733+
net_prev = _float_clean(prev.get("净利润(元)")) or _float_clean(prev.get("净利润"))
690734
result["earnings_growth"] = _pct_change(net_curr, net_prev)
735+
else:
736+
result["earnings_growth"] = _float_clean(curr.get("净利润滚动环比增长(%)"))
691737

692738
de = _float_clean(curr.get("资产负债率(%)"))
693739
if de is not None:
@@ -697,7 +743,7 @@ def fetch_hk_financial_indicators(tencent_code: str) -> Dict[str, Any]:
697743
result["quick_ratio"] = _float_clean(curr.get("速动比率"))
698744

699745
op_cf = _float_clean(curr.get("每股经营现金流(元)"))
700-
shares = _float_clean(curr.get("总股本(股)"))
746+
shares = _float_clean(curr.get("总股本(股)")) or _float_clean(curr.get("已发行股本(股)"))
701747
if op_cf is not None and shares and shares > 0:
702748
result["operating_cash_flow"] = round(op_cf * shares, 2)
703749

@@ -717,9 +763,7 @@ def fetch_hk_financial_statements(tencent_code: str) -> Dict[str, Any]:
717763
statements: Dict[str, Any] = {}
718764

719765
try:
720-
import akshare as ak # type: ignore
721-
with _bypass_proxy():
722-
df = ak.stock_hk_financial_indicator_em(symbol=hk5)
766+
df = _hk_financial_indicator_frame(hk5)
723767
if df is None or df.empty:
724768
return {}
725769

@@ -730,8 +774,13 @@ def fetch_hk_financial_statements(tencent_code: str) -> Dict[str, Any]:
730774
)
731775
report_date = str(curr[date_col])[:10] if date_col and curr.get(date_col) is not None else None
732776

733-
rev = _float_clean(curr.get("营业总收入(元)")) or _float_clean(curr.get("营业收入(元)"))
734-
net_income = _float_clean(curr.get("净利润(元)"))
777+
rev = (
778+
_float_clean(curr.get("营业总收入(元)"))
779+
or _float_clean(curr.get("营业收入(元)"))
780+
or _float_clean(curr.get("营业总收入"))
781+
or _float_clean(curr.get("营业收入"))
782+
)
783+
net_income = _float_clean(curr.get("净利润(元)")) or _float_clean(curr.get("净利润"))
735784
total_assets = _float_clean(curr.get("总资产(元)"))
736785
de_pct = _float_clean(curr.get("资产负债率(%)"))
737786

@@ -760,7 +809,7 @@ def fetch_hk_financial_statements(tencent_code: str) -> Dict[str, Any]:
760809
}
761810

762811
op_cf_per_share = _float_clean(curr.get("每股经营现金流(元)"))
763-
shares = _float_clean(curr.get("总股本(股)"))
812+
shares = _float_clean(curr.get("总股本(股)")) or _float_clean(curr.get("已发行股本(股)"))
764813
op_cf = round(op_cf_per_share * shares, 2) if op_cf_per_share is not None and shares and shares > 0 else None
765814
statements["cash_flow"] = {
766815
"latest_date": report_date,

‎backend_api_python/app/services/fundamental_coverage.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def member_coverage(members, fields, as_of, source=None):
1515
as_of = date.fromisoformat(str(as_of)[:10])
1616
rows = query('''SELECT DISTINCT ON (market,symbol) * FROM qd_fundamental_snapshots
1717
WHERE symbol=ANY(%s) AND available_at<=%s AND (%s IS NULL OR source=%s)
18-
ORDER BY market,symbol,available_at DESC,period_end DESC,ingested_at DESC''',
18+
ORDER BY market,symbol,available_at DESC,ingested_at DESC,period_end DESC''',
1919
([item['symbol'] for item in members], as_of, source, source), True)
2020
found = {(r['market'], r['symbol']): r for r in rows}
2121
result = []

0 commit comments

Comments
 (0)