替换数据源:Yahoo Finance → 腾讯财经

- 解决 Yahoo Finance 限频问题
- 使用腾讯财经实时行情 API (qt.gtimg.cn)
- 使用腾讯财经 K 线 API (web.ifzq.gtimg.cn)
- 支持港股 PE/PB/市值/52周高低点数据
- 移除 yfinance 依赖,仅保留 numpy/pandas
This commit is contained in:
黑猴
2026-03-30 21:56:07 +08:00
parent 64316f59a6
commit 46f06fc967

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
港股分析脚本 - 获取港股数据进行技术面+基本面分析,给出操作建议。 港股分析脚本 - 使用腾讯财经数据进行技术面+基本面分析
用法: 用法:
python3 analyze_stock.py <股票代码> [--period <周期>] [--output <输出文件>] python3 analyze_stock.py <股票代码> [--period <周期>] [--output <输出文件>]
@@ -9,9 +9,6 @@
python3 analyze_stock.py 0700.HK python3 analyze_stock.py 0700.HK
python3 analyze_stock.py 0700.HK --period 6mo --output report.json python3 analyze_stock.py 0700.HK --period 6mo --output report.json
python3 analyze_stock.py 9988.HK --period 1y python3 analyze_stock.py 9988.HK --period 1y
股票代码格式: 数字.HK (如 0700.HK 腾讯控股, 9988.HK 阿里巴巴)
周期选项: 1mo, 3mo, 6mo, 1y, 2y, 5y (默认 6mo)
""" """
import sys import sys
@@ -19,15 +16,11 @@ import json
import argparse import argparse
import time import time
import hashlib import hashlib
import urllib.request
import urllib.error
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
try:
import yfinance as yf
except ImportError:
print("ERROR: yfinance 未安装。请运行: pip3 install yfinance", file=sys.stderr)
sys.exit(1)
try: try:
import numpy as np import numpy as np
except ImportError: except ImportError:
@@ -42,13 +35,13 @@ except ImportError:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# 缓存与重试机制(解决 Yahoo Finance 限频问题) # 缓存与重试机制
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
CACHE_DIR = Path.home() / ".stock_buddy_cache" CACHE_DIR = Path.home() / ".stock_buddy_cache"
CACHE_TTL_SECONDS = 600 # 缓存有效期 10 分钟(同一股票短时间内不重复请求) CACHE_TTL_SECONDS = 600 # 缓存有效期 10 分钟
MAX_RETRIES = 4 # 最大重试次数 MAX_RETRIES = 3
RETRY_BASE_DELAY = 5 # 重试基础延迟(秒),指数退避: 5s, 10s, 20s, 40s RETRY_BASE_DELAY = 2
def _cache_key(code: str, period: str) -> str: def _cache_key(code: str, period: str) -> str:
@@ -58,7 +51,7 @@ def _cache_key(code: str, period: str) -> str:
def _read_cache(code: str, period: str) -> dict | None: def _read_cache(code: str, period: str) -> dict | None:
"""读取缓存,若未过期则返回缓存数据""" """读取缓存"""
cache_file = CACHE_DIR / _cache_key(code, period) cache_file = CACHE_DIR / _cache_key(code, period)
if not cache_file.exists(): if not cache_file.exists():
return None return None
@@ -82,37 +75,159 @@ def _write_cache(code: str, period: str, data: dict):
with open(cache_file, "w", encoding="utf-8") as f: with open(cache_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2, default=str) json.dump(data, f, ensure_ascii=False, indent=2, default=str)
except OSError: except OSError:
pass # 缓存写入失败不影响主流程 pass
def _retry_request(func, *args, max_retries=MAX_RETRIES, **kwargs):
"""
带指数退避的重试包装器。
捕获 Yahoo Finance 限频错误并自动重试。
"""
last_error = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
error_msg = str(e).lower()
# 仅对限频/网络类错误重试
is_rate_limit = any(kw in error_msg for kw in [
"rate limit", "too many requests", "429", "throttl",
"connection", "timeout", "timed out",
])
if not is_rate_limit:
raise # 非限频错误直接抛出
if attempt < max_retries - 1:
delay = RETRY_BASE_DELAY * (2 ** attempt) # 3s, 6s, 12s
print(f"⏳ 请求被限频,{delay}秒后第{attempt+2}次重试...", file=sys.stderr)
time.sleep(delay)
raise last_error
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# 技术指标计算 # 腾讯财经数据获取
# ─────────────────────────────────────────────
def normalize_hk_code(code: str) -> tuple[str, str]:
"""标准化港股代码,返回 (原始数字代码, 带.HK后缀代码)"""
code = code.strip().upper().replace(".HK", "")
digits = code.lstrip("0")
if digits.isdigit():
numeric_code = code.zfill(4)
return numeric_code, numeric_code + ".HK"
return code, code + ".HK"
def fetch_tencent_quote(code: str) -> dict:
"""获取腾讯财经实时行情"""
numeric_code, full_code = normalize_hk_code(code)
url = f"http://qt.gtimg.cn/q=hk{numeric_code}"
for attempt in range(MAX_RETRIES):
try:
req = urllib.request.Request(url, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
with urllib.request.urlopen(req, timeout=10) as response:
data = response.read().decode("gb2312", errors="ignore")
return _parse_tencent_quote(data, numeric_code)
except urllib.error.URLError as e:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_BASE_DELAY * (attempt + 1))
else:
raise Exception(f"获取实时行情失败: {e}")
return {}
def _parse_tencent_quote(data: str, code: str) -> dict:
"""解析腾讯财经实时行情响应"""
var_name = f"v_hk{code}"
for line in data.strip().split(";"):
line = line.strip()
if not line or var_name not in line:
continue
# 提取引号内的内容
parts = line.split('"')
if len(parts) < 2:
continue
values = parts[1].split("~")
if len(values) < 35: # 至少需要35个字段
continue
def safe_float(idx: int, default: float = 0.0) -> float:
try:
return float(values[idx]) if values[idx] else default
except (ValueError, IndexError):
return default
def safe_str(idx: int, default: str = "") -> str:
return values[idx] if idx < len(values) else default
# 字段映射 (根据腾讯财经API实际数据)
# 0:市场 1:名称 2:代码 3:现价 4:昨收 5:今开 6:成交量
# 30:时间戳 31:涨跌额 32:涨跌幅 33:最高 34:最低
# 39:市盈率 47:市净率 37:总市值 48:52周高 49:52周低
return {
"name": values[1],
"code": values[2],
"price": safe_float(3),
"prev_close": safe_float(4),
"open": safe_float(5),
"volume": safe_float(6),
"high": safe_float(33),
"low": safe_float(34),
"change_amount": safe_float(31),
"change_pct": safe_float(32),
"timestamp": safe_str(30),
"pe": safe_float(39) if len(values) > 39 else None,
"pb": safe_float(47) if len(values) > 47 else None,
"market_cap": safe_str(37),
"52w_high": safe_float(48) if len(values) > 48 else None,
"52w_low": safe_float(49) if len(values) > 49 else None,
}
return {}
def fetch_tencent_kline(code: str, days: int = 120) -> pd.DataFrame:
"""获取腾讯财经K线数据"""
numeric_code, full_code = normalize_hk_code(code)
url = f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=hk{numeric_code},day,,,{days},qfq"
for attempt in range(MAX_RETRIES):
try:
req = urllib.request.Request(url, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
with urllib.request.urlopen(req, timeout=15) as response:
data = json.loads(response.read().decode("utf-8"))
return _parse_tencent_kline(data, numeric_code)
except (urllib.error.URLError, json.JSONDecodeError) as e:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_BASE_DELAY * (attempt + 1))
else:
raise Exception(f"获取K线数据失败: {e}")
return pd.DataFrame()
def _parse_tencent_kline(data: dict, code: str) -> pd.DataFrame:
"""解析腾讯财经K线数据"""
key = f"hk{code}"
if data.get("code") != 0 or not data.get("data") or key not in data["data"]:
return pd.DataFrame()
day_data = data["data"][key].get("day", [])
if not day_data:
return pd.DataFrame()
# 格式: [日期, 开盘价, 收盘价, 最低价, 最高价, 成交量]
records = []
for item in day_data:
if len(item) >= 6:
records.append({
"Date": item[0],
"Open": float(item[1]),
"Close": float(item[2]),
"Low": float(item[3]),
"High": float(item[4]),
"Volume": float(item[5]),
})
df = pd.DataFrame(records)
if not df.empty:
df["Date"] = pd.to_datetime(df["Date"])
df.set_index("Date", inplace=True)
return df
def period_to_days(period: str) -> int:
"""将周期字符串转换为天数"""
mapping = {
"1mo": 30,
"3mo": 90,
"6mo": 180,
"1y": 250,
"2y": 500,
"5y": 1250,
}
return mapping.get(period, 180)
# ─────────────────────────────────────────────
# 技术指标计算 (保持不变)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
def calc_ma(close: pd.Series, windows: list[int] = None) -> dict: def calc_ma(close: pd.Series, windows: list[int] = None) -> dict:
@@ -152,18 +267,14 @@ def _macd_signal(dif: pd.Series, dea: pd.Series, macd_hist: pd.Series) -> str:
"""MACD信号判断""" """MACD信号判断"""
if len(dif) < 3: if len(dif) < 3:
return "中性" return "中性"
# 金叉DIF上穿DEA
if dif.iloc[-1] > dea.iloc[-1] and dif.iloc[-2] <= dea.iloc[-2]: if dif.iloc[-1] > dea.iloc[-1] and dif.iloc[-2] <= dea.iloc[-2]:
return "金叉-买入信号" return "金叉-买入信号"
# 死叉DIF下穿DEA
if dif.iloc[-1] < dea.iloc[-1] and dif.iloc[-2] >= dea.iloc[-2]: if dif.iloc[-1] < dea.iloc[-1] and dif.iloc[-2] >= dea.iloc[-2]:
return "死叉-卖出信号" return "死叉-卖出信号"
# 零轴上方
if dif.iloc[-1] > 0 and dea.iloc[-1] > 0: if dif.iloc[-1] > 0 and dea.iloc[-1] > 0:
if macd_hist.iloc[-1] > macd_hist.iloc[-2]: if macd_hist.iloc[-1] > macd_hist.iloc[-2]:
return "多头增强" return "多头增强"
return "多头区域" return "多头区域"
# 零轴下方
if dif.iloc[-1] < 0 and dea.iloc[-1] < 0: if dif.iloc[-1] < 0 and dea.iloc[-1] < 0:
if macd_hist.iloc[-1] < macd_hist.iloc[-2]: if macd_hist.iloc[-1] < macd_hist.iloc[-2]:
return "空头增强" return "空头增强"
@@ -186,7 +297,6 @@ def calc_rsi(close: pd.Series, periods: list[int] = None) -> dict:
rsi = 100 - (100 / (1 + rs)) rsi = 100 - (100 / (1 + rs))
val = round(rsi.iloc[-1], 2) val = round(rsi.iloc[-1], 2)
result[f"RSI{p}"] = val result[f"RSI{p}"] = val
# 综合信号
rsi_main = result.get("RSI12", result.get("RSI6", 50)) rsi_main = result.get("RSI12", result.get("RSI6", 50))
if rsi_main > 80: if rsi_main > 80:
result["signal"] = "严重超买-卖出信号" result["signal"] = "严重超买-卖出信号"
@@ -321,135 +431,64 @@ def calc_ma_trend(close: pd.Series) -> dict:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# 基本面分析 # 基本面分析 (基于腾讯数据)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
def get_fundamentals(ticker: yf.Ticker) -> dict: def get_fundamentals(quote: dict) -> dict:
"""获取基本面数据""" """基于实时行情数据的基本面分析"""
info = ticker.info
result = {} result = {}
# 估值指标 # 估值指标 (腾讯提供的)
pe = info.get("trailingPE") or info.get("forwardPE") pe = quote.get("pe")
pb = info.get("priceToBook") pb = quote.get("pb")
ps = info.get("priceToSalesTrailing12Months")
result["PE"] = round(pe, 2) if pe else None result["PE"] = round(pe, 2) if pe else None
result["PB"] = round(pb, 2) if pb else None result["PB"] = round(pb, 2) if pb else None
result["PS"] = round(ps, 2) if ps else None result["PS"] = None # 腾讯不提供
# 股息 (yfinance 有时返回异常值,限制在合理范围)
div_yield = info.get("dividendYield")
if div_yield is not None and 0 < div_yield < 1:
result["dividend_yield_pct"] = round(div_yield * 100, 2)
elif div_yield is not None and div_yield >= 1:
# 可能已经是百分比形式
result["dividend_yield_pct"] = round(div_yield, 2) if div_yield < 30 else None
else:
result["dividend_yield_pct"] = None
# 市值 # 市值
market_cap = info.get("marketCap") result["market_cap"] = quote.get("market_cap", "")
if market_cap:
if market_cap >= 1e12:
result["market_cap"] = f"{market_cap/1e12:.2f} 万亿"
elif market_cap >= 1e8:
result["market_cap"] = f"{market_cap/1e8:.2f} 亿"
else:
result["market_cap"] = f"{market_cap:,.0f}"
else:
result["market_cap"] = None
# 盈利能力
result["profit_margin_pct"] = round(info.get("profitMargins", 0) * 100, 2) if info.get("profitMargins") else None
result["roe_pct"] = round(info.get("returnOnEquity", 0) * 100, 2) if info.get("returnOnEquity") else None
result["roa_pct"] = round(info.get("returnOnAssets", 0) * 100, 2) if info.get("returnOnAssets") else None
# 增长指标
result["revenue_growth_pct"] = round(info.get("revenueGrowth", 0) * 100, 2) if info.get("revenueGrowth") else None
result["earnings_growth_pct"] = round(info.get("earningsGrowth", 0) * 100, 2) if info.get("earningsGrowth") else None
# 负债
result["debt_to_equity"] = round(info.get("debtToEquity", 0), 2) if info.get("debtToEquity") else None
# 52周价格区间 # 52周价格区间
result["52w_high"] = info.get("fiftyTwoWeekHigh") result["52w_high"] = quote.get("52w_high")
result["52w_low"] = info.get("fiftyTwoWeekLow") result["52w_low"] = quote.get("52w_low")
result["50d_avg"] = info.get("fiftyDayAverage")
result["200d_avg"] = info.get("twoHundredDayAverage")
# 公司信息 # 公司信息
result["company_name"] = info.get("longName") or info.get("shortName", "未知") result["company_name"] = quote.get("name", "未知")
result["sector"] = info.get("sector", "未知") result["sector"] = "港股"
result["industry"] = info.get("industry", "未知") result["industry"] = "港股"
result["currency"] = info.get("currency", "HKD") result["currency"] = "HKD"
# 基本面评分 # 基本面信号
result["fundamental_signal"] = _fundamental_signal(result) result["fundamental_signal"] = _fundamental_signal(result)
return result return result
def _fundamental_signal(data: dict) -> str: def _fundamental_signal(data: dict) -> str:
"""基本面信号判断""" """基本面信号判断 (简化版)"""
score = 0 score = 0
reasons = [] reasons = []
# PE 评估
pe = data.get("PE") pe = data.get("PE")
if pe is not None: if pe is not None and pe > 0:
if pe < 0: if pe < 15:
score -= 1
reasons.append("PE为负(亏损)")
elif pe < 15:
score += 2 score += 2
reasons.append("PE低估值") reasons.append(f"PE低估值({pe})")
elif pe < 25: elif pe < 25:
score += 1 score += 1
reasons.append("PE合理") reasons.append(f"PE合理({pe})")
elif pe > 40: elif pe > 40:
score -= 1 score -= 1
reasons.append("PE高估") reasons.append(f"PE偏高({pe})")
# PB 评估
pb = data.get("PB") pb = data.get("PB")
if pb is not None: if pb is not None:
if pb < 1: if pb < 1:
score += 1 score += 1
reasons.append("PB破净") reasons.append(f"PB破净({pb})")
elif pb > 5: elif pb > 5:
score -= 1 score -= 1
reasons.append(f"PB偏高({pb})")
# 股息率
div = data.get("dividend_yield_pct")
if div is not None and div > 3:
score += 1
reasons.append(f"高股息{div}%")
# ROE
roe = data.get("roe_pct")
if roe is not None:
if roe > 15:
score += 1
reasons.append("ROE优秀")
elif roe < 5:
score -= 1
# 增长
rev_growth = data.get("revenue_growth_pct")
if rev_growth is not None and rev_growth > 10:
score += 1
reasons.append("收入增长良好")
earnings_growth = data.get("earnings_growth_pct")
if earnings_growth is not None and earnings_growth > 15:
score += 1
reasons.append("利润增长强劲")
# 负债
de = data.get("debt_to_equity")
if de is not None and de > 200:
score -= 1
reasons.append("负债率偏高")
if score >= 3: if score >= 3:
signal = "基本面优秀" signal = "基本面优秀"
@@ -460,7 +499,7 @@ def _fundamental_signal(data: dict) -> str:
else: else:
signal = "基本面较差" signal = "基本面较差"
return f"{signal} ({'; '.join(reasons[:4])})" if reasons else signal return f"{signal} ({'; '.join(reasons[:3])})" if reasons else signal
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
@@ -469,10 +508,10 @@ def _fundamental_signal(data: dict) -> str:
def generate_recommendation(technical: dict, fundamental: dict, current_price: float) -> dict: def generate_recommendation(technical: dict, fundamental: dict, current_price: float) -> dict:
"""综合技术面和基本面给出操作建议""" """综合技术面和基本面给出操作建议"""
score = 0 # 范围大约 -10 到 +10 score = 0
signals = [] signals = []
# ── 技术面评分 ── # 技术面评分
macd_sig = technical.get("macd", {}).get("signal", "") macd_sig = technical.get("macd", {}).get("signal", "")
if "买入" in macd_sig or "金叉" in macd_sig: if "买入" in macd_sig or "金叉" in macd_sig:
score += 2 score += 2
@@ -531,7 +570,7 @@ def generate_recommendation(technical: dict, fundamental: dict, current_price: f
score -= 1 score -= 1
signals.append(f"成交量: {vol_sig}") signals.append(f"成交量: {vol_sig}")
# ── 基本面评分 ── # 基本面评分
fund_sig = fundamental.get("fundamental_signal", "") fund_sig = fundamental.get("fundamental_signal", "")
if "优秀" in fund_sig: if "优秀" in fund_sig:
score += 2 score += 2
@@ -557,7 +596,7 @@ def generate_recommendation(technical: dict, fundamental: dict, current_price: f
else: else:
signals.append(f"52周位置: {position:.0%}") signals.append(f"52周位置: {position:.0%}")
# ── 映射到操作建议 ── # 映射到操作建议
if score >= 5: if score >= 5:
action = "强烈买入" action = "强烈买入"
action_en = "STRONG_BUY" action_en = "STRONG_BUY"
@@ -593,82 +632,47 @@ def generate_recommendation(technical: dict, fundamental: dict, current_price: f
# 主流程 # 主流程
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
def normalize_hk_code(code: str) -> str:
"""标准化港股代码"""
code = code.strip().upper()
if not code.endswith(".HK"):
# 尝试补全
digits = code.lstrip("0")
if digits.isdigit():
code = code.zfill(4) + ".HK"
return code
def analyze_stock(code: str, period: str = "6mo", use_cache: bool = True) -> dict: def analyze_stock(code: str, period: str = "6mo", use_cache: bool = True) -> dict:
"""对单只港股进行完整分析(内置缓存 + 自动重试)""" """对单只港股进行完整分析"""
code = normalize_hk_code(code) numeric_code, full_code = normalize_hk_code(code)
# 1. 尝试读取缓存
if use_cache: if use_cache:
cached = _read_cache(code, period) cached = _read_cache(full_code, period)
if cached: if cached:
print(f"📦 使用缓存数据 ({code}),缓存有效期 {CACHE_TTL_SECONDS}s", file=sys.stderr) print(f"📦 使用缓存数据 ({full_code}),缓存有效期 {CACHE_TTL_SECONDS}s", file=sys.stderr)
return cached return cached
result = {"code": code, "analysis_time": datetime.now().isoformat(), "error": None} result = {"code": full_code, "analysis_time": datetime.now().isoformat(), "error": None}
try: try:
ticker = yf.Ticker(code) # 1. 获取实时行情
quote = fetch_tencent_quote(numeric_code)
# 2. 带重试的数据获取(限频时可能返回空数据或抛异常) if not quote or not quote.get("price"):
hist = None result["error"] = f"无法获取 {full_code} 的实时行情"
for attempt in range(MAX_RETRIES):
try:
hist = ticker.history(period=period)
if hist is not None and not hist.empty:
break # 成功获取数据
# 空数据可能是限频导致,重试
if attempt < MAX_RETRIES - 1:
delay = RETRY_BASE_DELAY * (2 ** attempt)
print(f"⏳ 数据为空(可能限频),{delay}秒后第{attempt+2}次重试...", file=sys.stderr)
time.sleep(delay)
ticker = yf.Ticker(code) # 重新创建 ticker 对象
except Exception as e:
error_msg = str(e).lower()
is_retriable = any(kw in error_msg for kw in [
"rate limit", "too many requests", "429", "throttl",
"connection", "timeout", "timed out",
])
if not is_retriable or attempt >= MAX_RETRIES - 1:
raise
delay = RETRY_BASE_DELAY * (2 ** attempt)
print(f"⏳ 请求被限频,{delay}秒后第{attempt+2}次重试...", file=sys.stderr)
time.sleep(delay)
ticker = yf.Ticker(code)
if hist is None or hist.empty:
result["error"] = f"无法获取 {code} 的历史数据,请检查股票代码是否正确"
return result return result
current_price = quote["price"]
result["current_price"] = current_price
result["price_date"] = quote.get("timestamp", "")
result["price_change"] = quote.get("change_amount")
result["price_change_pct"] = quote.get("change_pct")
# 2. 获取K线数据
days = period_to_days(period)
hist = fetch_tencent_kline(numeric_code, days)
if hist.empty or len(hist) < 30:
result["error"] = f"无法获取 {full_code} 的历史K线数据 (仅获得 {len(hist)} 条)"
return result
result["data_points"] = len(hist)
close = hist["Close"] close = hist["Close"]
high = hist["High"] high = hist["High"]
low = hist["Low"] low = hist["Low"]
volume = hist["Volume"] volume = hist["Volume"]
current_price = round(close.iloc[-1], 3)
result["current_price"] = current_price # 3. 技术分析
result["price_date"] = str(hist.index[-1].date())
result["data_points"] = len(hist)
# 价格变动
if len(close) > 1:
prev_close = close.iloc[-2]
change = current_price - prev_close
change_pct = change / prev_close * 100
result["price_change"] = round(change, 3)
result["price_change_pct"] = round(change_pct, 2)
# 技术分析
technical = {} technical = {}
technical["ma_trend"] = calc_ma_trend(close) technical["ma_trend"] = calc_ma_trend(close)
technical["macd"] = calc_macd(close) technical["macd"] = calc_macd(close)
@@ -678,20 +682,16 @@ def analyze_stock(code: str, period: str = "6mo", use_cache: bool = True) -> dic
technical["volume"] = calc_volume_analysis(volume, close) technical["volume"] = calc_volume_analysis(volume, close)
result["technical"] = technical result["technical"] = technical
# 3. 带重试的基本面数据获取 # 4. 基本面分析
try: fundamental = get_fundamentals(quote)
fundamental = _retry_request(get_fundamentals, ticker) result["fundamental"] = fundamental
result["fundamental"] = fundamental
except Exception as e:
result["fundamental"] = {"error": str(e), "fundamental_signal": "数据获取失败"}
fundamental = result["fundamental"]
# 综合建议 # 5. 综合建议
result["recommendation"] = generate_recommendation(technical, fundamental, current_price) result["recommendation"] = generate_recommendation(technical, fundamental, current_price)
# 4. 写入缓存(仅成功分析时) # 6. 写入缓存
if result.get("error") is None: if result.get("error") is None:
_write_cache(code, period, result) _write_cache(full_code, period, result)
except Exception as e: except Exception as e:
result["error"] = f"分析过程出错: {str(e)}" result["error"] = f"分析过程出错: {str(e)}"
@@ -700,15 +700,14 @@ def analyze_stock(code: str, period: str = "6mo", use_cache: bool = True) -> dic
def main(): def main():
parser = argparse.ArgumentParser(description="港股分析工具") parser = argparse.ArgumentParser(description="港股分析工具 (腾讯财经数据源)")
parser.add_argument("code", help="港股代码 (如 0700.HK)") parser.add_argument("code", help="港股代码 (如 0700.HK, 00700, 腾讯)")
parser.add_argument("--period", default="6mo", help="数据周期 (1mo/3mo/6mo/1y/2y/5y)") parser.add_argument("--period", default="6mo", help="数据周期 (1mo/3mo/6mo/1y/2y/5y)")
parser.add_argument("--output", help="输出JSON文件路径") parser.add_argument("--output", help="输出JSON文件路径")
parser.add_argument("--no-cache", action="store_true", help="跳过缓存,强制重新请求数据") parser.add_argument("--no-cache", action="store_true", help="跳过缓存,强制重新请求数据")
parser.add_argument("--clear-cache", action="store_true", help="清除所有缓存后退出") parser.add_argument("--clear-cache", action="store_true", help="清除所有缓存后退出")
args = parser.parse_args() args = parser.parse_args()
# 清除缓存
if args.clear_cache: if args.clear_cache:
import shutil import shutil
if CACHE_DIR.exists(): if CACHE_DIR.exists():