|
36 | 36 |
|
37 | 37 | from .calendar import nearest_trade_date as calendar_nearest_trade_date |
38 | 38 | from .health import SourceHealthBook |
| 39 | +from .market_routes import route_board_data |
39 | 40 |
|
40 | 41 | # ------------------------------------------------------------------ |
41 | 42 | # 配置 |
@@ -716,6 +717,32 @@ def fetch_ths_concept_money_flow_snapshot(date_str: str) -> dict[str, str]: |
716 | 717 | return result |
717 | 718 |
|
718 | 719 |
|
| 720 | +def fetch_browser_fund_flow_snapshot(date_str: str) -> dict[str, str]: |
| 721 | + raw = _playwright_html(THS_CONCEPT_MONEY_FLOW_URL) |
| 722 | + if not raw: |
| 723 | + return {} |
| 724 | + rows = _parse_ths_money_flow_table(raw) |
| 725 | + top_in_rows = sorted( |
| 726 | + [row for row in rows if float(row.get("net") or 0) > 0], |
| 727 | + key=lambda item: float(item.get("net") or 0), |
| 728 | + reverse=True, |
| 729 | + )[:5] |
| 730 | + top_out_rows = sorted( |
| 731 | + [row for row in rows if float(row.get("net") or 0) < 0], |
| 732 | + key=lambda item: float(item.get("net") or 0), |
| 733 | + )[:5] |
| 734 | + if not top_in_rows or not top_out_rows: |
| 735 | + return {} |
| 736 | + return { |
| 737 | + "date": _display_date(nearest_trade_date()), |
| 738 | + "_source": "公开财经页面概念资金流", |
| 739 | + "_scope": "A股", |
| 740 | + "_fallback_indicator": "concept_money_flow", |
| 741 | + "_concept_in": json.dumps(top_in_rows, ensure_ascii=False), |
| 742 | + "_concept_out": json.dumps(top_out_rows, ensure_ascii=False), |
| 743 | + } |
| 744 | + |
| 745 | + |
719 | 746 | def _market_activity_snapshot(rows: list[dict[str, Any]], source: str, date_str: str) -> dict[str, str]: |
720 | 747 | usable = [r for r in rows if r.get("f12") in {"000001", "399001"}] |
721 | 748 | if not usable: |
@@ -1995,8 +2022,15 @@ def get_fund_flow(date_str: str, *, strict_date: bool = True) -> dict[str, str]: |
1995 | 2022 | latest_result["_date_note"] = "latest_available" |
1996 | 2023 | cache_save("fund_flow", date_str, "eastmoney", latest_result) |
1997 | 2024 | return latest_result |
1998 | | - for fallback in (fetch_sina_sector_money_flow_snapshot, fetch_sina_market_activity_snapshot, fetch_tencent_market_activity_snapshot): |
1999 | | - activity = fallback(date_str) |
| 2025 | + for online_reference in (fetch_sina_sector_money_flow_snapshot,): |
| 2026 | + activity = online_reference(date_str) |
| 2027 | + if activity: |
| 2028 | + return activity |
| 2029 | + browser_flow = fetch_browser_fund_flow_snapshot(date_str) |
| 2030 | + if browser_flow: |
| 2031 | + return browser_flow |
| 2032 | + for market_reference in (fetch_sina_market_activity_snapshot, fetch_tencent_market_activity_snapshot): |
| 2033 | + activity = market_reference(date_str) |
2000 | 2034 | if activity: |
2001 | 2035 | return activity |
2002 | 2036 | cached_latest = load_latest_fund_flow_cache(date_str) |
@@ -2171,13 +2205,15 @@ def fetch_eastmoney_board_list(board_type: str, date_str: str, limit: int = 100) |
2171 | 2205 |
|
2172 | 2206 | def get_board_list(board_type: str, date_str: str, limit: int = 100) -> dict[str, Any]: |
2173 | 2207 | """Return board rankings through the stock-analysis source order.""" |
2174 | | - result = fetch_eastmoney_board_list(board_type, date_str, limit=limit) |
2175 | | - if result.get("rows"): |
2176 | | - return result |
2177 | | - browser_result = camofox_board_list(board_type) |
2178 | | - if browser_result.get("rows"): |
2179 | | - return browser_result |
2180 | | - return result |
| 2208 | + return route_board_data( |
| 2209 | + board_type, |
| 2210 | + date_str, |
| 2211 | + direct=fetch_eastmoney_board_list, |
| 2212 | + camofox=camofox_board_list, |
| 2213 | + playwright=playwright_board_list, |
| 2214 | + limit=limit, |
| 2215 | + current_trade_date=nearest_trade_date(), |
| 2216 | + ) |
2181 | 2217 |
|
2182 | 2218 |
|
2183 | 2219 | @retry_on_recoverable(max_retries=MAX_RETRIES, initial_delay=INITIAL_BACKOFF) |
@@ -2751,6 +2787,36 @@ def _parse_camofox_board_snapshot(markdown: str) -> list[dict[str, Any]]: |
2751 | 2787 | return rows |
2752 | 2788 |
|
2753 | 2789 |
|
| 2790 | +def _playwright_html(url: str) -> str: |
| 2791 | + try: |
| 2792 | + from playwright.sync_api import sync_playwright |
| 2793 | + except ImportError: |
| 2794 | + return "" |
| 2795 | + try: |
| 2796 | + with sync_playwright() as runtime: |
| 2797 | + browser = runtime.chromium.launch(headless=True) |
| 2798 | + page = browser.new_page() |
| 2799 | + page.goto(url, wait_until="networkidle", timeout=20_000) |
| 2800 | + content = page.content() |
| 2801 | + browser.close() |
| 2802 | + return content |
| 2803 | + except Exception as exc: |
| 2804 | + diag(f"Browser page unavailable: {exc}") |
| 2805 | + return "" |
| 2806 | + |
| 2807 | + |
| 2808 | +def playwright_board_list(board_type: str = "industry") -> dict[str, Any]: |
| 2809 | + anchor = "industry_board" if board_type == "industry" else "concept_board" |
| 2810 | + html_text = _playwright_html(f"https://quote.eastmoney.com/center/gridlist.html#{anchor}") |
| 2811 | + if not html_text: |
| 2812 | + return {"board_type": board_type, "rows": [], "_unavailable": "browser unavailable"} |
| 2813 | + text = re.sub(r"<[^>]+>", " ", html.unescape(html_text)) |
| 2814 | + rows = _parse_camofox_board_snapshot( |
| 2815 | + "\n".join(f'row "{line.strip()}"' for line in text.splitlines() if re.match(r"^\s*\d+\s+", line)) |
| 2816 | + ) |
| 2817 | + return {"board_type": board_type, "rows": rows, "count": len(rows), "_source": "公开财经页面"} |
| 2818 | + |
| 2819 | + |
2754 | 2820 | def camofox_board_list(board_type: str = "industry") -> dict[str, Any]: |
2755 | 2821 | base = os.environ.get("CAMOFOX_URL", "http://localhost:9377") |
2756 | 2822 | user_id = os.environ.get("CAMOFOX_USER_ID", "") |
|
0 commit comments