Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Chinese A-share stock analysis via natural language using AKShare — real-time quotes, technicals, fundamentals, sectors, and derivatives.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
formatter.py
1#!/usr/bin/env python32# -*- coding: utf-8 -*-34from datetime import date, datetime5from typing import Any6import json789MAX_LEN = 1000101112INTENT_EMOJI = {13"INDEX_REALTIME": "📈",14"KLINE_ANALYSIS": "🕯️",15"KLINE_CHART": "📊",16"INTRADAY_ANALYSIS": "⏱️",17"VOLUME_ANALYSIS": "📊",18"LIMIT_STATS": "🚦",19"MONEY_FLOW": "💰",20"FUNDAMENTAL": "📊",21"STOCK_OVERVIEW": "📌",22"MARGIN_LHB": "🏦",23"SECTOR_ANALYSIS": "🧩",24"DERIVATIVES": "📉",25"FUND_BOND": "🏛️",26"HK_US_MARKET": "🌍",27"NEWS": "📰",28"RESEARCH_REPORT": "📰",29"STOCK_PICK": "🏆",30}313233def _to_text(data: Any) -> str:34if data is None:35return "无数据"3637if isinstance(data, str):38return data3940if isinstance(data, (dict, list, tuple)):41import datetime as dt4243def convert(obj):44if isinstance(obj, dt.date):45return obj.isoformat()46if isinstance(obj, (dict, list, tuple)):47if isinstance(obj, dict):48return {k: convert(v) for k, v in obj.items()}49return [convert(i) for i in obj]50return obj5152data = convert(data)53return json.dumps(data, ensure_ascii=False, indent=2)5455if hasattr(data, "to_dict"):56try:57as_dict = data.to_dict(orient="records")58return json.dumps(as_dict, ensure_ascii=False, indent=2)59except Exception:60pass6162return str(data)636465def _truncate(text: str, limit: int = MAX_LEN) -> str:66if len(text) <= limit:67return text68suffix = "\n...\n(内容过长,已截断)"69keep = max(0, limit - len(suffix))70return text[:keep] + suffix717273def _safe_float(value: Any) -> Any:74if value is None:75return None76if isinstance(value, str):77value = value.replace(",", "").replace("%", "").strip()78try:79return float(value)80except Exception:81return None828384def _fmt_price(value: Any) -> str:85num = _safe_float(value)86if num is None:87return str(value) if value is not None else "?"88return f"{num:.2f}"899091def _fmt_pct(value: Any) -> str:92num = _safe_float(value)93if num is None:94return "?"95return f"{num:+.2f}%"969798def _fmt_amount(value: Any) -> str:99num = _safe_float(value)100if num is None:101return str(value) if value is not None else "?"102abs_num = abs(num)103if abs_num >= 1e8:104return f"{num / 1e8:.2f}亿"105if abs_num >= 1e4:106return f"{num / 1e4:.2f}万"107return f"{num:.0f}"108109110def _fmt_ratio(value: Any) -> str:111num = _safe_float(value)112if num is None:113return "?"114return f"{num:.2f}%"115116117def _fmt_date(value: Any) -> str:118if value is None:119return "未知"120if isinstance(value, datetime):121return value.strftime("%Y-%m-%d %H:%M")122if isinstance(value, date):123return value.strftime("%Y-%m-%d")124if hasattr(value, "strftime"):125try:126return value.strftime("%Y-%m-%d %H:%M")127except Exception:128pass129text = str(value)130if len(text) == 8 and text.isdigit():131return f"{text[:4]}-{text[4:6]}-{text[6:]}"132return text133134135def _pick(item: dict, keys: list[str], default: Any = None) -> Any:136for key in keys:137if key in item and item.get(key) not in (None, ""):138return item.get(key)139return default140141142def _fmt_clock(value: Any) -> str:143text = _fmt_date(value)144if len(text) >= 16 and text[10] == " ":145return text[11:16]146if ":" in text and len(text) >= 5:147return text[-5:]148return text149150151def _market_sentiment(changes: list[float]) -> str:152if not changes:153return "市场情绪:数据不足,偏中性。"154155pos = sum(1 for c in changes if c > 0)156neg = sum(1 for c in changes if c < 0)157avg_change = sum(changes) / len(changes)158spread = max(changes) - min(changes)159160if avg_change >= 0.8 and pos >= 4:161return "市场情绪:整体偏强,风险偏好回升。"162if avg_change <= -0.8 and neg >= 4:163return "市场情绪:整体偏弱,防御情绪升温。"164if spread >= 1.0 and 2 <= pos <= 3:165return "市场情绪:板块分化明显,结构性机会为主。"166return "市场情绪:震荡整理,资金观望为主。"167168169def render_output(intent_obj, result, platform: str = "qq") -> str:170_ = platform171ts = datetime.now().strftime("%Y-%m-%d %H:%M")172emoji = INTENT_EMOJI.get(getattr(intent_obj, "intent", ""), "📌")173intent = getattr(intent_obj, "intent", "")174175# 使用说明176if intent == "HELP" and result.get("ok") and result.get("source") == "help":177return result.get("text", "")178179# 持仓管理180if intent == "PORTFOLIO" and result.get("source") == "portfolio":181return result.get("text", "")182183if intent == "INDEX_REALTIME" and result.get("ok"):184items = result.get("data", {}).get("items", [])185index_targets = [186("上证指数", ["上证指数", "上证综指", "沪指"]),187("深证成指", ["深证成指", "深证指数"]),188("创业板指", ["创业板指"]),189("沪深300", ["沪深300"]),190("上证50", ["上证50"]),191]192193selected = []194for label, aliases in index_targets:195matched = None196for item in items:197name = str(item.get("名称", ""))198if any(alias in name for alias in aliases):199matched = item200break201if matched:202selected.append((label, matched))203204if not selected:205selected = [(str(item.get("名称", "?")), item) for item in items[:5]]206207lines = [f"📊 A股实时大盘 · {ts}", ""]208changes = []209for label, item in selected:210price = _pick(item, ["最新价", "最新点位", "收盘"])211change = _pick(item, ["涨跌幅", "涨跌幅%", "涨跌"])212amount = _pick(item, ["成交额", "成交金额", "成交额(元)", "总成交额"])213214change_num = _safe_float(change)215if change_num is not None:216changes.append(change_num)217direction = "📈" if (change_num or 0) >= 0 else "📉"218lines.append(219f"{direction} {label}: {_fmt_price(price)} ({_fmt_pct(change)}) | 成交额 {_fmt_amount(amount)}"220)221222lines.extend(["", f"💡 {_market_sentiment(changes)}", "", "数据源: akshare"])223return _truncate("\n".join(lines), MAX_LEN)224225if intent == "KLINE_ANALYSIS":226if not result.get("ok"):227return "\n".join([f"{emoji} A股分析 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])228229data = result.get("data", {})230items = data.get("items", [])231symbol = data.get("symbol") or getattr(intent_obj, "symbol", None) or ""232stock_name = data.get("name") or data.get("名称")233if not stock_name:234query = getattr(intent_obj, "query", "")235if query:236try:237from router import STOCK_NAME_MAP238239for name in sorted(STOCK_NAME_MAP, key=len, reverse=True):240if name in query:241stock_name = name242break243except Exception:244stock_name = None245if not stock_name:246stock_name = symbol or "未知"247248display_name = f"{stock_name}({symbol})" if symbol else stock_name249ts_date = datetime.now().strftime("%Y-%m-%d")250count = getattr(intent_obj, "top_n", None) or len(items) or 0251sections = [252f"{emoji} {display_name} 近{count}日K线 · {ts_date}",253"",254]255256show_items = items[:5]257for item in show_items:258if not isinstance(item, dict):259sections.append(str(item))260continue261date_text = _fmt_date(_pick(item, ["日期", "date", "时间"]))262open_price = _fmt_price(_pick(item, ["开盘", "open"]))263close_price = _fmt_price(_pick(item, ["收盘", "close"]))264change = _pick(item, ["涨跌幅", "pct_change", "涨跌幅%"])265change_value = _safe_float(change)266direction = "📈" if (change_value or 0) >= 0 else "📉"267change_text = f" {direction} ({_fmt_pct(change)})" if change_value is not None else ""268sections.append(f"📅 {date_text}: 开盘 {open_price} 收盘 {close_price}{change_text}")269270if len(items) > len(show_items):271sections.append("...")272273sections.append("\n数据源: akshare")274return _truncate("\n".join(sections), MAX_LEN)275276if intent == "KLINE_CHART":277if not result.get("ok"):278return "\n".join([f"{emoji} K线图 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])279280data = result.get("data", {})281symbol = data.get("symbol", "")282name = data.get("name", symbol)283filepath = data.get("filepath", "")284285if filepath:286# 直接返回图片标签,让 QQ 自动发送(不换行,避免路径前多了空格)287return f"📊 {name}({symbol}) 近期股价走势图<qqimg>{filepath}</qqimg>"288return f"📊 {name}({symbol}) 走势图生成失败"289290if intent == "INTRADAY_ANALYSIS":291if not result.get("ok"):292return "\n".join([f"{emoji} 分时分析 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])293294data = result.get("data", {})295items = data.get("items", [])296symbol = data.get("symbol") or getattr(intent_obj, "symbol", "?") or "?"297period = data.get("period") or getattr(intent_obj, "period", None) or "1"298299lines = [f"⏱️ {symbol} 分时({period}m) · {ts}", ""]300if not items:301lines.extend(["暂无分时数据", "", "数据源: akshare"])302return "\n".join(lines)303304latest = items[0] if isinstance(items[0], dict) else {}305latest_price = _pick(latest, ["收盘", "close", "最新价", "成交价", "价格"])306high_price = _pick(latest, ["最高", "high"])307low_price = _pick(latest, ["最低", "low"])308volume = _pick(latest, ["成交量", "volume", "手数"])309latest_time = _pick(latest, ["时间", "day", "datetime"])310311lines.append(312f"最新 {_fmt_date(latest_time)} | 价 {_fmt_price(latest_price)} | 高 {_fmt_price(high_price)} | 低 {_fmt_price(low_price)} | 量 {_fmt_amount(volume)}"313)314lines.append("")315lines.append("最近成交:")316317for item in items[:8]:318if not isinstance(item, dict):319lines.append(str(item))320continue321t = _fmt_date(_pick(item, ["时间", "day", "datetime"]))322p = _fmt_price(_pick(item, ["收盘", "close", "成交价", "价格"]))323v = _fmt_amount(_pick(item, ["成交量", "volume", "手数"]))324direction = _pick(item, ["买卖盘性质", "性质"], "")325tag = f" {direction}" if direction else ""326lines.append(f"- {t}: {p} | 量 {v}{tag}")327328lines.extend(["", "数据源: akshare"])329return _truncate("\n".join(lines), MAX_LEN)330331if intent == "VOLUME_ANALYSIS":332# 分时量能分析结果直接返回333if not result.get("ok"):334return "\n".join([f"{emoji} 分时量能分析 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])335336# 直接返回脚本输出337text = result.get("text", "")338return _truncate(f"📊 分时量能分析\n{text}", MAX_LEN)339340if intent == "LIMIT_STATS":341if not result.get("ok"):342return "\n".join([f"{emoji} 涨跌停统计 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])343344data = result.get("data", {})345date = _fmt_date(data.get("date") or getattr(intent_obj, "date", ""))346up_items = data.get("up_items") or data.get("items") or []347down_items = data.get("down_items") or []348up_count = data.get("up_count")349down_count = data.get("down_count")350351if up_count is None:352up_count = len(up_items)353if down_count is None:354down_count = len(down_items)355356lines = [f"🚦 涨跌停统计 · {date}", "", f"涨停: {up_count} 家 | 跌停: {down_count} 家", "", "涨停前10:"]357358for idx, item in enumerate(up_items[:10], start=1):359if not isinstance(item, dict):360lines.append(f"{idx}. {item}")361continue362name = _pick(item, ["名称", "股票简称", "简称"], "?")363code = _pick(item, ["代码", "股票代码", "symbol"], "?")364pct = _pick(item, ["涨跌幅", "涨跌幅%"], None)365board = _pick(item, ["连板数", "连板", "几天几板"], None)366board_text = f" | 连板 {board}" if board not in (None, "") else ""367pct_text = f" | {_fmt_pct(pct)}" if pct is not None else ""368lines.append(f"{idx}. {name}({code}){pct_text}{board_text}")369370lines.extend(["", "数据源: akshare"])371return _truncate("\n".join(lines), MAX_LEN)372373if intent == "STOCK_PICK":374if not result.get("ok"):375return "\n".join([376f"🏆 今日股票推荐 · {datetime.now().strftime('%Y-%m-%d')}",377f"\n⚠️ 错误: {result.get('error', '未知')}",378])379380data = result.get("data", {})381items = data.get("items", [])382today = datetime.now().strftime("%Y-%m-%d")383384lines = [f"🏆 今日股票推荐 · {today}", ""]385if not items:386lines.extend(["暂无满足条件的推荐标的", "", "数据源: akshare"])387return _truncate("\n".join(lines), MAX_LEN)388389for idx, item in enumerate(items[:5], start=1):390if not isinstance(item, dict):391continue392name = item.get("name") or "未知"393code = item.get("code") or "?"394pct = item.get("pct", 0)395stars = "⭐⭐⭐"396397lines.append(f"{idx}. {name}({code}) {stars}")398lines.append(f" 📈 近期涨幅: {_fmt_pct(pct)}")399400if item.get("report_rating"):401lines.append(f" 📰 研报: [{item.get('report_org', '机构')}] {item.get('report_rating')}")402lines.append("")403404lines.append("数据源: akshare")405return _truncate("\n".join(lines), MAX_LEN)406407if intent == "STOCK_OVERVIEW":408if not result.get("ok"):409return "\n".join([f"{emoji} 个股综合信息 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])410411data = result.get("data", {})412symbol = data.get("symbol") or getattr(intent_obj, "symbol", "?") or "?"413414stock_name = symbol415query = getattr(intent_obj, "query", "")416if query:417try:418from router import STOCK_NAME_MAP419420for name in sorted(STOCK_NAME_MAP, key=len, reverse=True):421if name in query:422stock_name = name423break424except Exception:425pass426427realtime = data.get("realtime") if isinstance(data.get("realtime"), dict) else {}428money_flow = data.get("money_flow") if isinstance(data.get("money_flow"), dict) else {}429fundamental = data.get("fundamental") if isinstance(data.get("fundamental"), dict) else {}430limit_stats = data.get("limit_stats") if isinstance(data.get("limit_stats"), dict) else {}431432rt_latest = realtime.get("latest") if isinstance(realtime.get("latest"), dict) else {}433flow_latest = money_flow.get("latest") if isinstance(money_flow.get("latest"), dict) else {}434fund_latest = fundamental.get("latest") if isinstance(fundamental.get("latest"), dict) else {}435436price = _pick(rt_latest, ["收盘", "close", "最新价", "成交价", "价格"])437if price is None:438price = _pick(flow_latest, ["收盘价", "收盘", "close", "最新价"])439pct = _pick(rt_latest, ["涨跌幅", "涨跌幅%", "pct_change"])440if pct is None:441pct = _pick(flow_latest, ["涨跌幅", "涨跌幅%"])442443main_inflow = _pick(flow_latest, ["主力净流入-净额", "主力净流入", "主力净额", "主力净流入额"])444main_ratio = _pick(flow_latest, ["主力净流入-净占比", "主力净占比", "主力净流入占比"])445446period = _pick(fund_latest, ["报告期", "日期", "报告日期", "公告日期"], "最新")447roe = _pick(fund_latest, ["净资产收益率", "净资产收益率-摊薄", "ROE", "净资产收益率(%)"])448gross_margin = _pick(fund_latest, ["销售毛利率", "毛利率", "毛利率(%)"])449net_margin = _pick(fund_latest, ["销售净利率", "净利率", "净利率(%)", "净利润率"])450debt_ratio = _pick(fund_latest, ["资产负债率", "资产负债率(%)"])451452up_count = limit_stats.get("up_count")453down_count = limit_stats.get("down_count")454days = limit_stats.get("days") or 10455456title_name = f"{stock_name}({symbol})" if stock_name != symbol else symbol457lines = [f"📌 个股综合信息 | {title_name}", ""]458459if realtime.get("ok") or price is not None or pct is not None:460lines.append(f"💹 实时: {_fmt_price(price)} ({_fmt_pct(pct)})")461else:462lines.append("💹 实时: 暂无")463464if money_flow.get("ok"):465lines.append(f"💰 主力净流入: {_fmt_amount(main_inflow)} (净占比 {_fmt_pct(main_ratio)})")466else:467lines.append("💰 主力净流入: 暂无")468469if fundamental.get("ok"):470lines.append("")471lines.append(f"📊 基本面({_fmt_date(period)}):")472lines.append(f"ROE {_fmt_ratio(roe)} | 毛利率 {_fmt_ratio(gross_margin)}")473lines.append(f"净利率 {_fmt_ratio(net_margin)} | 资产负债率 {_fmt_ratio(debt_ratio)}")474else:475lines.append("")476lines.append("📊 基本面: 暂无")477478if isinstance(up_count, int) and isinstance(down_count, int):479lines.append("")480lines.append(f"🚦 近{days}日涨跌停: 涨停{up_count}次 / 跌停{down_count}次")481else:482lines.append("")483lines.append("🚦 近10日涨跌停: 暂无")484485# 研报486research_report = data.get("research_report") if isinstance(data.get("research_report"), dict) else {}487report_items = research_report.get("items", [])488if report_items:489lines.append("")490lines.append("📰 研报:")491for item in report_items[:2]:492if not isinstance(item, dict):493continue494org = _pick(item, ["机构", "东财评级"], "?")495rating = _pick(item, ["东财评级", "评级"], "?")496pe = _pick(item, ["2025-盈利预测-市盈率", "2026-盈利预测-市盈率"], None)497date = _pick(item, ["日期", "报告日期"])498title = _pick(item, ["报告名称", "标题", "研报名称"], "(无标题)")499# 截取标题前25字500title = title[:25] + "..." if len(title) > 25 else title501pe_text = f" | PE {pe}x" if pe else ""502lines.append(f"• [{org}] {title}")503lines.append(f" 评级: {rating}{pe_text}")504elif research_report.get("ok") is False:505lines.append("")506lines.append("📰 研报: 暂无")507508return _truncate("\n".join(lines), MAX_LEN)509510if intent == "NEWS":511if not result.get("ok"):512return "\n".join([f"📰 财经要闻 · {datetime.now().strftime('%Y-%m-%d')}", f"\n⚠️ 错误: {result.get('error', '未知')}"])513514data = result.get("data", {})515items = data.get("items", [])516today = datetime.now().strftime("%Y-%m-%d")517lines = [f"📰 财经要闻 · {today}", ""]518519if not items:520lines.extend(["暂无新闻数据", "", "数据源: akshare"])521return _truncate("\n".join(lines), MAX_LEN)522523for idx, item in enumerate(items[:10], start=1):524if not isinstance(item, dict):525lines.append(f"{idx}. {item}")526continue527528source = _pick(item, ["文章来源", "新闻来源", "来源", "source"], "未知来源")529title = _pick(item, ["新闻标题", "标题", "title", "内容"], "(无标题)")530publish_time = _pick(item, ["发布时间", "时间", "date", "发布日期"])531url = _pick(item, ["新闻链接", "链接", "url", "link"], "")532533# 使用 markdown 格式(QQ支持可点击链接)534if url:535lines.append(f"{idx}. [{title}]({url})")536537lines.extend(["", "数据源: eastmoney(agent-browser)"])538# 财经新闻需要更多字符显示链接539return _truncate("\n".join(lines), 3000)540541if intent == "RESEARCH_REPORT":542if not result.get("ok"):543return "\n".join([f"📰 个股研报 · {datetime.now().strftime('%Y-%m-%d')}", f"\n⚠️ 错误: {result.get('error', '未知')}"])544545data = result.get("data", {})546items = data.get("items", [])547symbol = data.get("symbol") or getattr(intent_obj, "symbol", "") or ""548549stock_name = symbol550query = getattr(intent_obj, "query", "")551if query:552try:553from router import STOCK_NAME_MAP554555for name in sorted(STOCK_NAME_MAP, key=len, reverse=True):556if name in query:557stock_name = name558break559except Exception:560pass561562title_name = stock_name if stock_name else (symbol or "个股")563today = datetime.now().strftime("%Y-%m-%d")564lines = [f"📰 {title_name}研报 · {today}", ""]565566if not items:567lines.extend(["暂无研报数据", "", "数据源: akshare"])568return _truncate("\n".join(lines), MAX_LEN)569570for idx, item in enumerate(items[:10], start=1):571if not isinstance(item, dict):572lines.append(f"{idx}. {item}")573continue574575org = _pick(item, ["研究机构", "机构", "机构名称", "评级机构"], "未知机构")576stock_short = _pick(item, ["股票简称", "简称", "股票名称", "名称"], title_name)577report_name = _pick(item, ["报告名称", "研报标题", "标题", "报告标题"], "(无标题)")578rating = _pick(item, ["东财评级", "最新评级", "评级", "投资评级"], "未知")579date = _pick(item, ["日期", "报告日期", "发布时间", "发布日期"])580pe_2025 = _pick(item, ["2025-盈利预测-市盈率", "2025预测市盈率", "2025年PE"])581pe_2026 = _pick(item, ["2026-盈利预测-市盈率"])582eps_2025 = _pick(item, ["2025-盈利预测-收益", "2025每股收益", "预测EPS"])583584if pe_2025 is not None:585profit = f"2025年PE {pe_2025}"586elif pe_2026 is not None:587profit = f"2026年PE {pe_2026}"588elif eps_2025 is not None:589profit = f"2025年EPS {eps_2025}"590else:591profit = _pick(item, ["预测市盈率", "盈利预测"], None)592593lines.append(f"{idx}. [{org}] {stock_short} - {report_name}")594if profit is not None:595profit_text = str(profit)596if "x" not in profit_text.lower() and "倍" not in profit_text:597profit_text = f"{profit_text}x"598lines.append(f" 评级: {rating} | 盈利预测: {profit_text}")599else:600lines.append(f" 评级: {rating}")601if date is not None:602lines.append(f" 日期: {_fmt_date(date)}")603604lines.extend(["", "数据源: akshare"])605return _truncate("\n".join(lines), MAX_LEN)606607if intent == "MONEY_FLOW":608if not result.get("ok"):609return "\n".join([f"{emoji} 资金流向 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])610611data = result.get("data", {})612scope = data.get("scope") or "individual"613items = data.get("items", [])614615if scope == "market":616lines = [f"💰 市场资金流向 · {ts}", ""]617if not items:618lines.extend(["暂无市场资金流数据", "", "数据源: akshare"])619return "\n".join(lines)620621latest = items[0] if isinstance(items[0], dict) else {}622d = _fmt_date(_pick(latest, ["日期", "交易日期", "date", "时间"]))623624# 尝试获取主力净流入等字段625main_flow = _pick(latest, ["主力净流入-净额", "主力净流入", "净额"])626super_flow = _pick(latest, ["超大单净流入-净额", "超大单净流入"])627628lines.append(f"最新({d})")629if main_flow is not None:630lines.append(f"- 主力净流入: {_fmt_amount(main_flow)}")631if super_flow is not None:632lines.append(f"- 超大单净流入: {_fmt_amount(super_flow)}")633634lines.append("")635lines.append("近5日主力资金:")636for item in items[:5]:637if not isinstance(item, dict):638lines.append(f"- {item}")639continue640day = _fmt_date(_pick(item, ["日期", "交易日期", "date", "时间"]))641val = _pick(item, ["主力净流入-净额", "主力净流入", "净额", "净流入"])642if val is not None:643lines.append(f"- {day}: {_fmt_amount(val)}")644645lines.extend(["", "数据源: akshare"])646return _truncate("\n".join(lines), MAX_LEN)647648if scope == "sector":649lines = [f"💰 行业资金流向 · {ts}", ""]650if not items:651lines.extend(["暂无行业资金流数据", "", "数据源: akshare"])652return "\n".join(lines)653654lines.append("净流入前10行业:")655for idx, item in enumerate(items[:10], start=1):656if not isinstance(item, dict):657lines.append(f"{idx}. {item}")658continue659name = _pick(item, ["名称", "行业", "板块名称", "行业名称"], "?")660inflow = _pick(item, ["今日主力净流入-净额", "主力净流入", "今日净流入", "净流入", "主力净额", "今日主力净流入"])661pct = _pick(item, ["今日涨跌幅", "涨跌幅", "涨跌幅%"])662pct_text = f" | {_fmt_pct(pct)}" if pct is not None else ""663if inflow is not None:664lines.append(f"{idx}. {name}: {_fmt_amount(inflow)}{pct_text}")665else:666lines.append(f"{idx}. {name}{pct_text}")667668lines.extend(["", "数据源: akshare"])669return _truncate("\n".join(lines), MAX_LEN)670671symbol = data.get("symbol") or getattr(intent_obj, "symbol", "?") or "?"672lines = [f"💰 {symbol} 资金流向 · {ts}", ""]673if not items:674lines.extend(["暂无资金流数据", "", "数据源: akshare"])675return "\n".join(lines)676677latest = items[0] if isinstance(items[0], dict) else {}678d = _fmt_date(_pick(latest, ["日期", "交易日期", "date"]))679main_inflow = _pick(latest, ["主力净流入-净额", "主力净流入", "主力净额", "主力净流入额"])680main_ratio = _pick(latest, ["主力净流入-净占比", "主力净占比", "主力净流入占比"])681close_price = _pick(latest, ["收盘价", "收盘", "close"])682pct = _pick(latest, ["涨跌幅", "涨跌幅%"])683684lines.append(685f"最新({d}): 收盘 {_fmt_price(close_price)} ({_fmt_pct(pct)}) | 主力净流入 {_fmt_amount(main_inflow)} ({_fmt_pct(main_ratio)})"686)687lines.append("")688lines.append("近5日主力净流入:")689690for item in items[:5]:691if not isinstance(item, dict):692lines.append(str(item))693continue694day = _fmt_date(_pick(item, ["日期", "交易日期", "date"]))695inflow = _pick(item, ["主力净流入-净额", "主力净流入", "主力净额", "主力净流入额"])696ratio = _pick(item, ["主力净流入-净占比", "主力净占比", "主力净流入占比"])697lines.append(f"- {day}: {_fmt_amount(inflow)} ({_fmt_pct(ratio)})")698699lines.extend(["", "数据源: akshare"])700return _truncate("\n".join(lines), MAX_LEN)701702if intent == "FUNDAMENTAL":703if not result.get("ok"):704return "\n".join([f"{emoji} 基本面分析 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])705706data = result.get("data", {})707symbol = data.get("symbol") or getattr(intent_obj, "symbol", "?") or "?"708latest = data.get("latest") if isinstance(data.get("latest"), dict) else {}709items = data.get("items", [])710711if not latest and isinstance(items, list):712first_item = items[0] if items else None713if isinstance(first_item, dict):714latest = first_item715716lines = [f"📊 {symbol} 基本面摘要 · {ts}", ""]717if not latest:718lines.extend(["暂无基本面数据", "", "数据源: akshare"])719return _truncate("\n".join(lines), MAX_LEN)720721period = _pick(latest, ["报告期", "日期", "报告日期", "公告日期"], "最新")722roe = _pick(latest, ["净资产收益率", "净资产收益率-摊薄", "ROE", "净资产收益率(%)"])723gross_margin = _pick(latest, ["销售毛利率", "毛利率", "毛利率(%)"])724net_margin = _pick(latest, ["销售净利率", "净利率", "净利率(%)", "净利润率"])725debt_ratio = _pick(latest, ["资产负债率", "资产负债率(%)"])726rev_yoy = _pick(latest, ["营业总收入同比增长率", "营业收入同比增长率", "营收同比"])727np_yoy = _pick(latest, ["净利润同比增长率", "归母净利润同比增长率", "净利润同比"])728729# 更多指标730eps = _pick(latest, ["基本每股收益", "每股收益"])731bvps = _pick(latest, ["每股净资产", "每股净资产(元)"])732op_cashflow = _pick(latest, ["每股经营现金流", "每股经营现金流量"])733inv_turnover = _pick(latest, ["存货周转率", "存货周转次数"])734ar_turnover = _pick(latest, ["应收账款周转天数", "应收账款周转率"])735736lines.append(f"报告期: {_fmt_date(period)}")737738if eps is not None and str(eps) not in ('False', ''):739lines.append(f"- 每股收益: {eps}")740if bvps is not None and str(bvps) not in ('False', ''):741lines.append(f"- 每股净资产: {bvps}")742if roe is not None:743lines.append(f"- ROE: {_fmt_ratio(roe)}")744if gross_margin is not None:745lines.append(f"- 毛利率: {_fmt_ratio(gross_margin)}")746if net_margin is not None:747lines.append(f"- 净利率: {_fmt_ratio(net_margin)}")748if debt_ratio is not None:749lines.append(f"- 资产负债率: {_fmt_ratio(debt_ratio)}")750if rev_yoy is not None:751lines.append(f"- 营收同比: {_fmt_pct(rev_yoy)}")752if np_yoy is not None:753lines.append(f"- 净利润同比: {_fmt_pct(np_yoy)}")754755# 第二行:更多指标756if op_cashflow is not None and str(op_cashflow) not in ('False', ''):757lines.append(f"- 每股经营现金流: {op_cashflow}")758if inv_turnover is not None and str(inv_turnover) not in ('False', ''):759lines.append(f"- 存货周转率: {inv_turnover}")760if ar_turnover is not None and str(ar_turnover) not in ('False', ''):761lines.append(f"- 应收账款周转天数: {ar_turnover}")762763lines.extend(["", "数据源: akshare"])764return _truncate("\n".join(lines), MAX_LEN)765766if intent == "MARGIN_LHB":767if not result.get("ok"):768return "\n".join([f"{emoji} 两融/龙虎榜 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])769770data = result.get("data", {})771symbol = data.get("symbol") or getattr(intent_obj, "symbol", "") or ""772title = f"🏦 {symbol} 两融/龙虎榜 · {ts}" if symbol else f"🏦 两融/龙虎榜 · {ts}"773774margin_items = data.get("margin_items", [])775lhb_items = data.get("lhb_items", [])776777lines = [title, ""]778779if margin_items:780latest_margin = margin_items[0] if isinstance(margin_items[0], dict) else {}781m_date = _fmt_date(_pick(latest_margin, ["日期", "交易日期", "截止日期", "date"]))782rzye = _pick(latest_margin, ["融资余额", "融资余额(元)", "融资余额(万元)"])783rzmr = _pick(latest_margin, ["融资买入额", "融资买入", "融资买入额(元)"])784rzjme = _pick(latest_margin, ["融资净买入", "融资净买入额", "融资净偿还"])785rqye = _pick(latest_margin, ["融券余额", "融券余额(元)", "融券余额(万元)"])786lines.append(f"融资融券({m_date}):")787if rzye is not None:788lines.append(f"- 融资余额: {_fmt_amount(rzye)}")789if rzmr is not None:790lines.append(f"- 融资买入额: {_fmt_amount(rzmr)}")791if rzjme is not None:792lines.append(f"- 融资净买入: {_fmt_amount(rzjme)}")793if rqye is not None:794lines.append(f"- 融券余额: {_fmt_amount(rqye)}")795lines.append("")796else:797lines.append("融资融券: 暂无数据")798lines.append("")799800lines.append("龙虎榜前5:")801if lhb_items:802for idx, item in enumerate(lhb_items[:5], start=1):803if not isinstance(item, dict):804lines.append(f"{idx}. {item}")805continue806name = _pick(item, ["名称", "股票简称", "证券简称"], "?")807code = _pick(item, ["代码", "股票代码", "证券代码"], "?")808reason = _pick(item, ["上榜原因", "解读", "原因"], "")809net_buy = _pick(item, ["龙虎榜净买额", "净买额", "买卖净额"])810net_text = f" | 净买 {_fmt_amount(net_buy)}" if net_buy is not None else ""811reason_text = f" | {reason}" if reason else ""812lines.append(f"{idx}. {name}({code}){net_text}{reason_text}")813else:814lines.append("暂无龙虎榜数据")815816lines.extend(["", "数据源: akshare"])817return _truncate("\n".join(lines), MAX_LEN)818819if intent == "SECTOR_ANALYSIS":820if not result.get("ok"):821return "\n".join([f"{emoji} 板块分析 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])822823data = result.get("data", {})824sector_type = data.get("sector_type", "industry")825top_gain = data.get("top_gain") or data.get("items") or []826top_drop = data.get("top_drop") or []827label = "概念板块" if sector_type == "concept" else "行业板块"828829lines = [f"🧩 {label}涨跌排行 · {ts}", "", "涨幅前5:"]830for idx, item in enumerate(top_gain[:5], start=1):831if not isinstance(item, dict):832lines.append(f"{idx}. {item}")833continue834name = _pick(item, ["板块", "板块名称", "名称", "行业", "概念名称", "symbol"], "?")835pct = _pick(item, ["涨跌幅", "今日涨跌幅", "涨跌幅%", "涨跌"])836lines.append(f"{idx}. {name}: {_fmt_pct(pct)}")837838lines.append("")839lines.append("跌幅前5:")840for idx, item in enumerate(top_drop[:5], start=1):841if not isinstance(item, dict):842lines.append(f"{idx}. {item}")843continue844name = _pick(item, ["板块", "板块名称", "名称", "行业", "概念名称", "symbol"], "?")845pct = _pick(item, ["涨跌幅", "今日涨跌幅", "涨跌幅%", "涨跌"])846lines.append(f"{idx}. {name}: {_fmt_pct(pct)}")847848lines.extend(["", "数据源: akshare"])849return _truncate("\n".join(lines), MAX_LEN)850851if intent == "FUND_BOND":852if not result.get("ok"):853return "\n".join([f"{emoji} 基金/可转债 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])854855data = result.get("data", {})856scope = data.get("scope", "fund")857items = data.get("items", [])858859if scope == "bond":860lines = [f"🏛️ 可转债行情 · {ts}", ""]861if not items:862lines.extend(["暂无可转债数据", "", "数据源: akshare"])863return _truncate("\n".join(lines), MAX_LEN)864865for idx, item in enumerate(items[:8], start=1):866if not isinstance(item, dict):867lines.append(f"{idx}. {item}")868continue869name = _pick(item, ["name", "债券简称", "名称", "转债名称"], "?")870code = _pick(item, ["symbol", "code", "代码", "债券代码", "转债代码"], "?")871price = _pick(item, ["trade", "最新价", "现价", "收盘", "price"])872pct = _pick(item, ["changepercent", "涨跌幅", "涨跌幅%", "涨跌"])873lines.append(f"{idx}. {name}({code}): {_fmt_price(price)} {_fmt_pct(pct)}")874875lines.extend(["", "数据源: akshare"])876return _truncate("\n".join(lines), MAX_LEN)877878lines = [f"🏛️ 基金净值/行情 · {ts}", ""]879if not items:880lines.extend(["暂无基金数据", "", "数据源: akshare"])881return _truncate("\n".join(lines), MAX_LEN)882883for idx, item in enumerate(items[:8], start=1):884if not isinstance(item, dict):885lines.append(f"{idx}. {item}")886continue887name = _pick(item, ["基金简称", "名称", "基金名称", "symbol"], "?")888code = _pick(item, ["基金代码", "代码", "证券代码"], "?")889nav = _pick(item, ["单位净值", "净值", "最新价", "收盘", "close"])890pct = _pick(item, ["日增长率", "涨跌幅", "涨跌幅%", "涨跌"])891date = _pick(item, ["日期", "净值日期", "date"])892label = name if name != "?" else (code if code != "?" else "基金")893if date:894lines.append(f"{idx}. {_fmt_date(date)} {label}: {_fmt_price(nav)} {_fmt_pct(pct)}")895elif pct is not None:896lines.append(f"{idx}. {label}: {_fmt_price(nav)} {_fmt_pct(pct)}")897else:898lines.append(f"{idx}. {label}: {_fmt_price(nav)}")899900lines.extend(["", "数据源: akshare"])901return _truncate("\n".join(lines), MAX_LEN)902903if intent == "HK_US_MARKET":904if not result.get("ok"):905return "\n".join([f"{emoji} 港美股行情 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])906907data = result.get("data", {})908market = data.get("market", "hk")909items = data.get("items", [])910title = "🌍 美股行情" if market == "us" else "🌍 港股行情"911912lines = [f"{title} · {ts}", ""]913if not items:914lines.extend(["暂无跨市场数据", "", "数据源: akshare"])915return _truncate("\n".join(lines), MAX_LEN)916917for idx, item in enumerate(items[:8], start=1):918if not isinstance(item, dict):919lines.append(f"{idx}. {item}")920continue921name = _pick(item, ["名称", "股票名称", "英文名称", "name", "代码", "symbol"], "?")922code = _pick(item, ["代码", "股票代码", "证券代码", "symbol"], "?")923price = _pick(item, ["最新价", "现价", "收盘", "close", "price", "最新价(美元)", "最新"])924pct = _pick(item, ["涨跌幅", "涨跌幅%", "涨跌", "changepercent"])925lines.append(f"{idx}. {name}({code}): {_fmt_price(price)} {_fmt_pct(pct)}")926927lines.extend(["", "数据源: akshare"])928return _truncate("\n".join(lines), MAX_LEN)929930if intent == "DERIVATIVES":931if not result.get("ok"):932return "\n".join([f"{emoji} 期货/期权 · {ts}", f"\n⚠️ 错误: {result.get('error', '未知')}"])933934data = result.get("data", {})935scope = data.get("scope", "futures")936items = data.get("items", [])937title = "📉 期权数据" if scope == "options" else "📉 期货主力合约"938939lines = [f"{title} · {ts}", ""]940if not items:941lines.extend(["暂无衍生品数据", "", "数据源: akshare"])942return _truncate("\n".join(lines), MAX_LEN)943944for idx, item in enumerate(items[:8], start=1):945if not isinstance(item, dict):946lines.append(f"{idx}. {item}")947continue948name = _pick(item, ["名称", "合约", "品种", "主力合约", "symbol", "代码"], "?")949code = _pick(item, ["代码", "合约", "symbol", "合约代码"], "?")950price = _pick(item, ["最新价", "现价", "收盘", "close", "price", "结算价", "最新"])951pct = _pick(item, ["涨跌幅", "涨跌幅%", "涨跌", "changepercent"])952lines.append(f"{idx}. {name}({code}): {_fmt_price(price)} {_fmt_pct(pct)}")953954lines.extend(["", "数据源: akshare"])955return _truncate("\n".join(lines), MAX_LEN)956957sections = [958f"{emoji} A股分析 · {ts}",959]960961params = []962for key in ["symbol", "date", "period", "top_n"]:963value = getattr(intent_obj, key, None)964if value is not None:965params.append(f"{key}={value}")966967if params:968sections.append(f"参数: {' | '.join(params)}")969970if not result.get("ok"):971sections.append(f"\n⚠️ 错误: {result.get('error', '未知')}")972return "\n".join(sections)973974data = result.get("data", {})975items = data.get("items", [])976if items:977for item in items[:5]:978if isinstance(item, dict):979name = item.get("名称") or item.get("股票代码") or "未知"980price = item.get("最新价") or item.get("收盘")981change = item.get("涨跌幅")982if price is not None:983direction = "📈" if (_safe_float(change) or 0) >= 0 else "📉"984change_str = f" ({_fmt_pct(change)})" if change is not None else ""985sections.append(f"{direction} {name}: {price}{change_str}")986987if len(items) > 5:988sections.append(f"... 还有 {len(items)-5} 条")989990sections.append("\n数据源: akshare")991final = "\n".join(sections)992return _truncate(final, MAX_LEN)993