|
69 | 69 | from src.analyzer import AnalysisResult |
70 | 70 |
|
71 | 71 |
|
| 72 | +def _format_large_number(value: Any) -> str: |
| 73 | + """ |
| 74 | + 格式化大数字为亿/千万/万/千单位 |
| 75 | +
|
| 76 | + Args: |
| 77 | + value: 数值或数字字符串 |
| 78 | +
|
| 79 | + Returns: |
| 80 | + 格式化后的字符串,如 "547.29亿" 或 "106.39元" |
| 81 | + """ |
| 82 | + if value is None or value == '': |
| 83 | + return 'N/A' |
| 84 | + |
| 85 | + try: |
| 86 | + # 尝试转换为浮点数 |
| 87 | + num = float(value) |
| 88 | + except (ValueError, TypeError): |
| 89 | + return str(value) if value else 'N/A' |
| 90 | + |
| 91 | + # 处理负数 |
| 92 | + prefix = "-" if num < 0 else "" |
| 93 | + abs_num = abs(num) |
| 94 | + |
| 95 | + if abs_num >= 1_0000_0000: # >= 1亿 |
| 96 | + return f"{prefix}{abs_num / 1_0000_0000:.2f}亿" |
| 97 | + elif abs_num >= 1_0000_000: # >= 1000万 |
| 98 | + return f"{prefix}{abs_num / 1_0000_000:.2f}千万" |
| 99 | + elif abs_num >= 1_0000: # >= 1万 |
| 100 | + return f"{prefix}{abs_num / 1_0000:.2f}万" |
| 101 | + elif abs_num >= 1000: # >= 1000 |
| 102 | + return f"{prefix}{abs_num / 1000:.2f}千" |
| 103 | + else: |
| 104 | + return f"{prefix}{abs_num:.2f}" |
| 105 | + |
| 106 | + |
| 107 | +def _format_percentage(value: Any) -> str: |
| 108 | + """ |
| 109 | + 格式化百分比值 |
| 110 | +
|
| 111 | + Args: |
| 112 | + value: 百分比数值或字符串 |
| 113 | +
|
| 114 | + Returns: |
| 115 | + 格式化后的百分比字符串,如 "10.57%" |
| 116 | + """ |
| 117 | + if value is None or value == '': |
| 118 | + return 'N/A' |
| 119 | + |
| 120 | + try: |
| 121 | + num = float(value) |
| 122 | + return f"{num:.2f}%" |
| 123 | + except (ValueError, TypeError): |
| 124 | + return str(value) if value else 'N/A' |
| 125 | + |
| 126 | + |
72 | 127 | class NotificationChannel(Enum): |
73 | 128 | """通知渠道类型""" |
74 | 129 | WECHAT = "wechat" # 企业微信 |
@@ -1023,28 +1078,51 @@ def generate_dashboard_report( |
1023 | 1078 | f"### 💰 {labels.get('financial_report_heading', '财务与分红')}", |
1024 | 1079 | "", |
1025 | 1080 | ]) |
1026 | | - # 财务报告 |
| 1081 | + # 财务报告 - 使用表格展示 |
1027 | 1082 | if financial_report and isinstance(financial_report, dict): |
1028 | 1083 | report_lines.extend([ |
1029 | 1084 | f"**📊 {labels.get('financial_report_label', '财务报告')}**", |
1030 | 1085 | "", |
1031 | | - f"| {labels.get('report_period_label', '报告期')} | {financial_report.get('report_date', 'N/A')} |", |
1032 | | - f"| {labels.get('revenue_label', '营业收入')} | {financial_report.get('revenue', 'N/A')} |", |
1033 | | - f"| {labels.get('net_profit_label', '归母净利润')} | {financial_report.get('net_profit_parent', 'N/A')} |", |
1034 | | - f"| {labels.get('roe_label', 'ROE')} | {financial_report.get('roe', 'N/A')} |", |
| 1086 | + f"| 指标 | 数值 |", |
| 1087 | + f"| --- | --- |", |
| 1088 | + f"| 报告期 | {financial_report.get('report_date', 'N/A')} |", |
| 1089 | + f"| 营业收入 | {_format_large_number(financial_report.get('revenue'))} |", |
| 1090 | + f"| 归母净利润 | {_format_large_number(financial_report.get('net_profit_parent'))} |", |
| 1091 | + f"| ROE | {_format_percentage(financial_report.get('roe'))} |", |
1035 | 1092 | "", |
1036 | 1093 | ]) |
1037 | | - # 分红指标 |
| 1094 | + # 分红指标 - 使用表格展示 |
1038 | 1095 | if dividend_metrics and isinstance(dividend_metrics, dict): |
1039 | | - ttm_yield = dividend_metrics.get('ttm_dividend_yield_pct', 'N/A') |
1040 | | - ttm_cash = dividend_metrics.get('ttm_cash_dividend_per_share', 'N/A') |
| 1096 | + ttm_yield_raw = dividend_metrics.get('ttm_dividend_yield_pct') |
| 1097 | + ttm_cash_raw = dividend_metrics.get('ttm_cash_dividend_per_share') |
1041 | 1098 | ttm_count = dividend_metrics.get('ttm_event_count', 'N/A') |
| 1099 | + |
| 1100 | + # 格式化股息率 |
| 1101 | + if ttm_yield_raw is not None: |
| 1102 | + try: |
| 1103 | + ttm_yield = f"{float(ttm_yield_raw):.2f}%" |
| 1104 | + except (ValueError, TypeError): |
| 1105 | + ttm_yield = str(ttm_yield_raw) |
| 1106 | + else: |
| 1107 | + ttm_yield = 'N/A' |
| 1108 | + |
| 1109 | + # 格式化每股分红(添加"元"单位) |
| 1110 | + if ttm_cash_raw is not None: |
| 1111 | + try: |
| 1112 | + ttm_cash = f"{float(ttm_cash_raw):.2f}元" |
| 1113 | + except (ValueError, TypeError): |
| 1114 | + ttm_cash = str(ttm_cash_raw) |
| 1115 | + else: |
| 1116 | + ttm_cash = 'N/A' |
| 1117 | + |
1042 | 1118 | report_lines.extend([ |
1043 | 1119 | f"**💵 {labels.get('dividend_label', '分红指标')}**", |
1044 | 1120 | "", |
1045 | | - f"| {labels.get('ttm_yield_label', 'TTM股息率')} | {ttm_yield} |", |
1046 | | - f"| {labels.get('ttm_cash_label', 'TTM每股现金分红')} | {ttm_cash} |", |
1047 | | - f"| {labels.get('ttm_count_label', 'TTM分红次数')} | {ttm_count} |", |
| 1121 | + f"| 指标 | 数值 |", |
| 1122 | + f"| --- | --- |", |
| 1123 | + f"| TTM股息率 | {ttm_yield} |", |
| 1124 | + f"| TTM每股现金分红 | {ttm_cash} |", |
| 1125 | + f"| TTM分红次数 | {ttm_count} 次 |", |
1048 | 1126 | "", |
1049 | 1127 | ]) |
1050 | 1128 |
|
@@ -1456,28 +1534,54 @@ def generate_wechat_dashboard(self, results: List[AnalysisResult]) -> str: |
1456 | 1534 | dividend_metrics = intel.get('dividend_metrics', {}) if intel else {} |
1457 | 1535 | if financial_report or dividend_metrics: |
1458 | 1536 | fin_lines = [] |
| 1537 | + # 财务报告 - 使用表格展示 |
1459 | 1538 | if financial_report and isinstance(financial_report, dict): |
1460 | | - revenue = financial_report.get('revenue', '') |
1461 | | - net_profit = financial_report.get('net_profit_parent', '') |
1462 | | - roe = financial_report.get('roe', '') |
| 1539 | + revenue = financial_report.get('revenue') |
| 1540 | + net_profit = financial_report.get('net_profit_parent') |
| 1541 | + roe = financial_report.get('roe') |
1463 | 1542 | if any([revenue, net_profit, roe]): |
1464 | 1543 | fin_lines.append("### 💰 财务数据") |
1465 | | - if revenue: |
1466 | | - fin_lines.append(f"营收: {revenue}") |
1467 | | - if net_profit: |
1468 | | - fin_lines.append(f"净利润: {net_profit}") |
1469 | | - if roe: |
1470 | | - fin_lines.append(f"ROE: {roe}") |
1471 | | - |
| 1544 | + fin_lines.append("") |
| 1545 | + fin_lines.append("| 指标 | 数值 |") |
| 1546 | + fin_lines.append("| --- | --- |") |
| 1547 | + fin_lines.append(f"| 报告期 | {financial_report.get('report_date', 'N/A')} |") |
| 1548 | + fin_lines.append(f"| 营业收入 | {_format_large_number(revenue)} |") |
| 1549 | + fin_lines.append(f"| 归母净利润 | {_format_large_number(net_profit)} |") |
| 1550 | + fin_lines.append(f"| ROE | {_format_percentage(roe)} |") |
| 1551 | + |
| 1552 | + # 分红指标 - 使用表格展示 |
1472 | 1553 | if dividend_metrics and isinstance(dividend_metrics, dict): |
1473 | | - ttm_yield = dividend_metrics.get('ttm_dividend_yield_pct') |
1474 | | - ttm_cash = dividend_metrics.get('ttm_cash_dividend_per_share') |
1475 | | - if ttm_yield or ttm_cash: |
| 1554 | + ttm_yield_raw = dividend_metrics.get('ttm_dividend_yield_pct') |
| 1555 | + ttm_cash_raw = dividend_metrics.get('ttm_cash_dividend_per_share') |
| 1556 | + ttm_count = dividend_metrics.get('ttm_event_count') |
| 1557 | + if ttm_yield_raw or ttm_cash_raw or ttm_count: |
| 1558 | + fin_lines.append("") |
1476 | 1559 | fin_lines.append("### 💵 分红指标") |
1477 | | - if ttm_yield: |
1478 | | - fin_lines.append(f"股息率: {ttm_yield}%") |
1479 | | - if ttm_cash: |
1480 | | - fin_lines.append(f"每股分红: {ttm_cash}元") |
| 1560 | + fin_lines.append("") |
| 1561 | + fin_lines.append("| 指标 | 数值 |") |
| 1562 | + fin_lines.append("| --- | --- |") |
| 1563 | + |
| 1564 | + # 格式化股息率 |
| 1565 | + if ttm_yield_raw is not None: |
| 1566 | + try: |
| 1567 | + ttm_yield = f"{float(ttm_yield_raw):.2f}%" |
| 1568 | + except (ValueError, TypeError): |
| 1569 | + ttm_yield = str(ttm_yield_raw) |
| 1570 | + else: |
| 1571 | + ttm_yield = 'N/A' |
| 1572 | + |
| 1573 | + # 格式化每股分红 |
| 1574 | + if ttm_cash_raw is not None: |
| 1575 | + try: |
| 1576 | + ttm_cash = f"{float(ttm_cash_raw):.2f}元" |
| 1577 | + except (ValueError, TypeError): |
| 1578 | + ttm_cash = str(ttm_cash_raw) |
| 1579 | + else: |
| 1580 | + ttm_cash = 'N/A' |
| 1581 | + |
| 1582 | + fin_lines.append(f"| TTM股息率 | {ttm_yield} |") |
| 1583 | + fin_lines.append(f"| TTM每股分红 | {ttm_cash} |") |
| 1584 | + fin_lines.append(f"| TTM分红次数 | {ttm_count if ttm_count else 'N/A'} 次 |") |
1481 | 1585 |
|
1482 | 1586 | if fin_lines: |
1483 | 1587 | lines.extend(fin_lines) |
|
0 commit comments