baba521 commited on
Commit
abf5292
·
1 Parent(s): d4d2656
app.py CHANGED
@@ -5,9 +5,13 @@ import re
5
  import pandas as pd
6
  import globals as g
7
  from service.mysql_service import get_companys, insert_company, get_company_by_name
8
- from service.chat_service import get_analysis_report, search_company, search_news, get_invest_suggest, chat_bot
9
  from service.company import check_company_exists
10
  from service.hf_upload import get_hf_files_with_links
 
 
 
 
11
  from service.tool_processor import get_stock_price
12
 
13
  custom_css = """
@@ -620,6 +624,104 @@ def create_report_section():
620
  report_display = gr.HTML(initial_content)
621
  return report_display
622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  def create_sidebar():
624
  """创建侧边栏组件"""
625
  # 初始化 companies_map
@@ -710,7 +812,83 @@ def create_sidebar():
710
 
711
  # 返回公司列表组件和报告部分组件
712
  return company_list, report_section_group, report_display
713
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  def create_metrics_dashboard():
715
  """创建指标仪表板组件"""
716
  with gr.Row(elem_classes=["metrics-dashboard"]):
@@ -737,23 +915,43 @@ def create_metrics_dashboard():
737
  "volume": "27.10M"
738
  }
739
 
 
 
740
  financial_metrics = [
741
- {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
742
- {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
743
- {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
744
- {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
745
- {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
746
- ]
747
-
748
- income_statement = [
749
- ["Category", "2024/FY", "2023/FY", "2022/FY"],
750
- ["Total", "130350M", "126491M", "134567M"],
751
- ["Net Income", "11081", "10598M", "9818.4M"],
752
- ["Earnings Per Share", "4.38", "4.03", "3.62"],
753
- ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
754
- ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
755
  ]
756
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  # 增长变化的 HTML 字符(箭头+百分比)
758
  def render_change(change: str, color: str):
759
  if change.startswith("+"):
@@ -798,62 +996,15 @@ def create_metrics_dashboard():
798
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
799
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
800
  </svg>
801
- <div style="font-size: 18px; font-weight: 600;">2025 Q3 Financial Metrics</div>
802
  </div>
803
  {metrics_html}
804
  </div>
805
  """
806
  return html
807
 
808
- # 构建右侧表格
809
- def build_income_table():
810
- table_rows = ""
811
- for i, row in enumerate(income_statement):
812
- if i == 0:
813
- row_style = "background-color: #f5f5f5; font-weight: 500;"
814
- else:
815
- row_style = "background-color: #f9f9f9;"
816
- cells = ""
817
- for j, cell in enumerate(row):
818
- if j == 0:
819
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: left; font-size: 14px;'>{cell}</td>"
820
- else:
821
- # 添加增长箭头(模拟数据)
822
- growth = None
823
- if i == 1 and j == 1: growth = "+3.05%"
824
- elif i == 1 and j == 2: growth = "-6.00%"
825
- elif i == 2 and j == 1: growth = "+3.05%"
826
- elif i == 2 and j == 2: growth = "-6.00%"
827
- elif i == 3 and j == 1: growth = "+3.05%"
828
- elif i == 3 and j == 2: growth = "-6.00%"
829
- elif i == 4 and j == 1: growth = "+29.17%"
830
- elif i == 4 and j == 2: growth = "+29.17%"
831
- elif i == 5 and j == 1: growth = "-13.05%"
832
- elif i == 5 and j == 2: growth = "+29.17%"
833
-
834
- if growth:
835
- arrow = "▲" if growth.startswith("+") else "▼"
836
- color = "green" if growth.startswith("+") else "red"
837
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px; position: relative;'><div>{cell}</div><div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div></td>"
838
- else:
839
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px;'>{cell}</td>"
840
- table_rows += f"<tr style='{row_style}'>{cells}</tr>"
841
-
842
- html = f"""
843
- <div style="min-width: 400px;max-width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
844
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
845
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
846
- <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
847
- </svg>
848
- <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
849
- </div>
850
- <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
851
- {table_rows}
852
- </table>
853
- </div>
854
- """
855
- return html
856
- # 主函数:返回所有 HTML 片段
857
  def get_dashboard():
858
  with gr.Row():
859
  with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
@@ -861,7 +1012,8 @@ def create_metrics_dashboard():
861
  with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
862
  financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
863
  with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
864
- income_table_html = gr.HTML(build_income_table(), elem_classes=["metric-card-right"])
 
865
  return stock_card_html, financial_metrics_html, income_table_html
866
 
867
  # 创建指标仪表板并保存引用
@@ -875,77 +1027,160 @@ def create_metrics_dashboard():
875
  def update_metrics_dashboard(company_name):
876
  """根据选择的公司更新指标仪表板"""
877
  # 模拟数据
878
- company_info = {
879
- "name": company_name,
880
- "symbol": "NYSE:BABA",
881
- "price": 157.65,
882
- "change": 0.64,
883
- "change_percent": 0.41,
884
- "open": 165.20,
885
- "high": 166.37,
886
- "low": 156.15,
887
- "prev_close": 157.01,
888
- "volume": "27.10M"
889
- }
890
-
891
  # 尝试获取股票价格数据,但不中断程序执行
892
  try:
893
  # 根据选择的公司获取股票代码
894
  stock_code = get_stock_code_by_company_name(company_name)
895
- company_info2 = get_stock_price(stock_code)
896
- print(f"股票价格数据: {company_info2}")
897
-
 
898
  # 如果成功获取数据,则用实际数据替换模拟数据
899
- if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
900
- import json
901
- # 解析返回的JSON数据
902
- data_text = company_info2["content"][0]["text"]
903
- stock_data = json.loads(data_text)
904
 
905
- # 提取数据
906
- quote = stock_data["Global Quote"]
907
 
908
- # 转换交易量单位
909
- volume = int(quote['06. volume'])
910
- if volume >= 1000000:
911
- volume_str = f"{volume / 1000000:.2f}M"
912
- elif volume >= 1000:
913
- volume_str = f"{volume / 1000:.2f}K"
914
- else:
915
- volume_str = str(volume)
916
 
917
- company_info = {
918
- "name": company_name,
919
- "symbol": f"NYSE:{quote['01. symbol']}",
920
- "price": float(quote['05. price']),
921
- "change": float(quote['09. change']),
922
- "change_percent": float(quote['10. change percent'].rstrip('%')),
923
- "open": float(quote['02. open']),
924
- "high": float(quote['03. high']),
925
- "low": float(quote['04. low']),
926
- "prev_close": float(quote['08. previous close']),
927
- "volume": volume_str
928
- }
929
  except Exception as e:
930
  print(f"获取股票价格数据失败: {e}")
931
  company_info2 = None
932
 
933
- financial_metrics = [
934
- {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
935
- {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
936
- {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
937
- {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
938
- {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
939
- ]
940
-
941
- income_statement = [
942
- ["Category", "2024/FY", "2023/FY", "2022/FY"],
943
- ["Total", "130350M", "126491M", "134567M"],
944
- ["Net Income", "11081", "10598M", "9818.4M"],
945
- ["Earnings Per Share", "4.38", "4.03", "3.62"],
946
- ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
947
- ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
948
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949
 
950
  # 增长变化的 HTML 字符(箭头+百分比)
951
  def render_change(change: str, color: str):
@@ -955,45 +1190,128 @@ def update_metrics_dashboard(company_name):
955
  return f'<span style="color:{color};">▼{change}</span>'
956
 
957
  # 构建左侧卡片
958
- def build_stock_card():
959
- # 检查是否获取到了股票数据,如果没有则显示N/A
960
- if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
961
- price = company_info["price"]
962
- change = company_info["change"]
963
- change_percent = company_info["change_percent"]
964
-
965
- # 格式化价格变动
966
- change_color = "green" if change > 0 else "red"
967
- change_html = f'<span style="color:{change_color};">+{change:.2f}({change_percent:+.2f}%)</span>' if change > 0 else \
968
- f'<span style="color:{change_color};">{change:.2f}({change_percent:+.2f}%)</span>'
969
- else:
970
- # 如果没有获取到数据,所有数值显示N/A
971
- price = "N/A"
972
- change = 0
973
- change_percent = 0
974
- change_html = "<span style=\"color:#888;\">N/A</span>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
 
976
- html = f"""
977
- <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
978
- <div style="font-size: 16px; color: #555;font-weight: 500;">{company_info['name']}</div>
979
- <div style="font-size: 12px; color: #888;">{company_info['symbol'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'NYSE:N/A'}</div>
980
- <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
981
- <div style="font-size: 32px; font-weight: bold;">{price}</div>
982
- <div style="font-size: 14px;">{change_html}</div>
983
- </div>
984
- <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
985
- <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['open'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
986
- <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['high'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
987
- <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['low'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
988
- <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['prev_close'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
989
- <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['volume'] if (company_info2 and 'content' in company_info2 and len(company_info2['content']) > 0) else 'N/A'}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  </div>
991
- </div>
992
- """
993
- return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
994
 
995
  # 构建中间卡片
996
- def build_financial_metrics():
997
  metrics_html = ""
998
  for item in financial_metrics:
999
  change_html = render_change(item["change"], item["color"])
@@ -1010,7 +1328,7 @@ def update_metrics_dashboard(company_name):
1010
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1011
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1012
  </svg>
1013
- <div style="font-size: 18px; font-weight: 600;">2025 Q3 Financial Metrics</div>
1014
  </div>
1015
  {metrics_html}
1016
  </div>
@@ -1018,56 +1336,59 @@ def update_metrics_dashboard(company_name):
1018
  return html
1019
 
1020
  # 构建右侧表格
1021
- def build_income_table():
1022
- table_rows = ""
1023
- for i, row in enumerate(income_statement):
1024
- if i == 0:
1025
- row_style = "background-color: #f5f5f5; font-weight: 500;"
1026
- else:
1027
- row_style = "background-color: #f9f9f9;"
1028
- cells = ""
1029
- for j, cell in enumerate(row):
1030
- if j == 0:
1031
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: left; font-size: 14px;'>{cell}</td>"
1032
- else:
1033
- # 添加增长箭头(模拟数据)
1034
- growth = None
1035
- if i == 1 and j == 1: growth = "+3.05%"
1036
- elif i == 1 and j == 2: growth = "-6.00%"
1037
- elif i == 2 and j == 1: growth = "+3.05%"
1038
- elif i == 2 and j == 2: growth = "-6.00%"
1039
- elif i == 3 and j == 1: growth = "+3.05%"
1040
- elif i == 3 and j == 2: growth = "-6.00%"
1041
- elif i == 4 and j == 1: growth = "+29.17%"
1042
- elif i == 4 and j == 2: growth = "+29.17%"
1043
- elif i == 5 and j == 1: growth = "-13.05%"
1044
- elif i == 5 and j == 2: growth = "+29.17%"
1045
-
1046
- if growth:
1047
- arrow = "▲" if growth.startswith("+") else "▼"
1048
- color = "green" if growth.startswith("+") else "red"
1049
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px; position: relative;'><div>{cell}</div><div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div></td>"
1050
- else:
1051
- cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: right; font-size: 14px;'>{cell}</td>"
1052
- table_rows += f"<tr style='{row_style}'>{cells}</tr>"
 
 
 
1053
 
1054
- html = f"""
1055
- <div style="width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1056
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1057
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1058
- <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1059
- </svg>
1060
- <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1061
- </div>
1062
- <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1063
- {table_rows}
1064
- </table>
1065
- </div>
1066
- """
1067
- return html
1068
 
1069
  # 返回三个HTML组件的内容
1070
- return build_stock_card(), build_financial_metrics(), build_income_table()
1071
  # gr.Column(scale=1, min_width=250)
1072
  # gr.HTML(f'''
1073
  # <div class="metric-card-item" style="{card_custom_style}width:300px;">
@@ -1521,16 +1842,16 @@ def main():
1521
  def update_metrics_dashboard_wrapper(company_name):
1522
  if company_name:
1523
  # 显示loading状态
1524
- loading_html = '''
1525
  <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1526
  <div style="text-align: center;">
1527
  <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1528
  <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1529
  <style>
1530
- @keyframes spin {
1531
- 0% { transform: rotate(0deg); }
1532
- 100% { transform: rotate(360deg); }
1533
- }
1534
  </style>
1535
  </div>
1536
  </div>
 
5
  import pandas as pd
6
  import globals as g
7
  from service.mysql_service import get_companys, insert_company, get_company_by_name
8
+ from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
9
  from service.company import check_company_exists
10
  from service.hf_upload import get_hf_files_with_links
11
+ from service.report_mcp import query_financial_data
12
+ from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
13
+ from service.three_year_table_tool import build_table_format
14
+ from service.three_year_tool import process_financial_data_with_metadata
15
  from service.tool_processor import get_stock_price
16
 
17
  custom_css = """
 
624
  report_display = gr.HTML(initial_content)
625
  return report_display
626
 
627
+ def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
628
+ """
629
+ 将原始财务数据转换为 financial_metrics 格式。
630
+
631
+ Args:
632
+ data (dict): 当前财年数据(必须包含 total_revenue, net_income 等字段)
633
+ prev_data (dict, optional): 上一财年数据,用于计算 change。若未提供,change 设为 "--"
634
+
635
+ Returns:
636
+ list[dict]: 符合 financial_metrics 格式的列表
637
+ """
638
+
639
+ def format_currency(value: float) -> str:
640
+ """将数字格式化为 $XB / $XM / $XK"""
641
+ if value >= 1e9:
642
+ return f"${value / 1e9:.2f}B"
643
+ elif value >= 1e6:
644
+ return f"${value / 1e6:.2f}M"
645
+ elif value >= 1e3:
646
+ return f"${value / 1e3:.2f}K"
647
+ else:
648
+ return f"${value:.2f}"
649
+
650
+ def calculate_change(current: float, previous: float) -> tuple:
651
+ """计算变化百分比和颜色"""
652
+ if previous == 0:
653
+ return "--", "gray"
654
+ change_pct = (current - previous) / abs(previous) * 100
655
+ sign = "+" if change_pct >= 0 else ""
656
+ color = "green" if change_pct >= 0 else "red"
657
+ return f"{sign}{change_pct:.1f}%", color
658
+
659
+ # 定义指标映射
660
+ metrics_config = [
661
+ {
662
+ "key": "total_revenue",
663
+ "label": "Total Revenue",
664
+ "is_currency": True,
665
+ "eps_like": False
666
+ },
667
+ {
668
+ "key": "net_income",
669
+ "label": "Net Income",
670
+ "is_currency": True,
671
+ "eps_like": False
672
+ },
673
+ {
674
+ "key": "earnings_per_share",
675
+ "label": "Earnings Per Share",
676
+ "is_currency": False, # EPS 不用 B/M 单位
677
+ "eps_like": True
678
+ },
679
+ {
680
+ "key": "operating_expenses",
681
+ "label": "Operating Expenses",
682
+ "is_currency": True,
683
+ "eps_like": False
684
+ },
685
+ {
686
+ "key": "operating_cash_flow",
687
+ "label": "Cash Flow",
688
+ "is_currency": True,
689
+ "eps_like": False
690
+ }
691
+ ]
692
+
693
+ result = []
694
+ for item in metrics_config:
695
+ key = item["key"]
696
+ current_val = data.get(key)
697
+ if current_val is None:
698
+ continue
699
+
700
+ # 格式化 value
701
+ if item["eps_like"]:
702
+ value_str = f"${current_val:.2f}"
703
+ elif item["is_currency"]:
704
+ value_str = format_currency(current_val)
705
+ else:
706
+ value_str = str(current_val)
707
+
708
+ # 计算 change(如果有上期数据)
709
+ if prev_data and key in prev_data:
710
+ prev_val = prev_data[key]
711
+ change_str, color = calculate_change(current_val, prev_val)
712
+ else:
713
+ change_str = "--"
714
+ color = "gray"
715
+
716
+ result.append({
717
+ "label": item["label"],
718
+ "value": value_str,
719
+ "change": change_str,
720
+ "color": color
721
+ })
722
+
723
+ return result
724
+
725
  def create_sidebar():
726
  """创建侧边栏组件"""
727
  # 初始化 companies_map
 
812
 
813
  # 返回公司列表组件和报告部分组件
814
  return company_list, report_section_group, report_display
815
+
816
+ def build_income_table(table_data):
817
+ # 兼容两种数据结构:
818
+ # 1. 新结构:包含 list_data 和 yoy_rates 的字典
819
+ # 2. 旧结构:直接是二维数组
820
+ if isinstance(table_data, dict) and "list_data" in table_data:
821
+ # 新结构
822
+ income_statement = table_data["list_data"]
823
+ yoy_rates = table_data["yoy_rates"] or []
824
+ else:
825
+ # 旧结构,直接使用传入的数据
826
+ income_statement = table_data
827
+ yoy_rates = []
828
+
829
+ # 创建一个映射,将年份列索引映射到增长率
830
+ yoy_map = {}
831
+ if len(yoy_rates) > 1 and len(yoy_rates[0]) > 1:
832
+ # 获取增长率表头(跳过第一列"Category")
833
+ yoy_headers = yoy_rates[0][1:]
834
+
835
+ # 为每个指标行创建增长率映射
836
+ for i, yoy_row in enumerate(yoy_rates[1:], 1): # 跳过标题行
837
+ category = yoy_row[0]
838
+ yoy_map[category] = {}
839
+ for j, rate in enumerate(yoy_row[1:]):
840
+ if j < len(yoy_headers):
841
+ yoy_map[category][yoy_headers[j]] = rate
842
+
843
+ table_rows = ""
844
+ header_row = income_statement[0]
845
+
846
+ for i, row in enumerate(income_statement):
847
+ if i == 0:
848
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
849
+ else:
850
+ row_style = "background-color: #f9f9f9;"
851
+ cells = ""
852
+
853
+ for j, cell in enumerate(row):
854
+ if j == 0:
855
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
856
+ else:
857
+ # 添加增长率箭头(如果有的话)
858
+ growth = None
859
+ category = row[0]
860
+ # j是当前单元格索引,0是类别列,1,2,3...是数据列
861
+ # yoy_map的键是年份,例如"2024/FY"
862
+ if i > 0 and category in yoy_map and j > 0 and j < len(header_row):
863
+ year_header = header_row[j]
864
+ if year_header in yoy_map[category]:
865
+ growth = yoy_map[category][year_header]
866
+
867
+ if growth and growth != "N/A":
868
+ arrow = "▲" if growth.startswith("+") else "▼"
869
+ color = "green" if growth.startswith("+") else "red"
870
+ cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
871
+ <div>{cell}</div>
872
+ <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
873
+ </td>"""
874
+ else:
875
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
876
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
877
+
878
+ html = f"""
879
+ <div style="min-width: 400px;max-width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
880
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
881
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
882
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
883
+ </svg>
884
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
885
+ </div>
886
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
887
+ {table_rows}
888
+ </table>
889
+ </div>
890
+ """
891
+ return html
892
  def create_metrics_dashboard():
893
  """创建指标仪表板组件"""
894
  with gr.Row(elem_classes=["metrics-dashboard"]):
 
915
  "volume": "27.10M"
916
  }
917
 
918
+ # financial_metrics = query_financial_data("NVDA", "最新财务数据")
919
+ # print(f"最新财务数据: {financial_metrics}")
920
  financial_metrics = [
921
+ {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
922
+ {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
923
+ {"label": "Earnings Per Share", "value": "N/A", "change": "N/A", "color": "grey"},
924
+ {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
925
+ {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
 
 
 
 
 
 
 
 
 
926
  ]
927
+ # income_statement = [
928
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
929
+ # ["Total", "130350M", "126491M", "134567M"],
930
+ # ["Net Income", "11081", "10598M", "9818.4M"],
931
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
932
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
933
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
934
+ # ]
935
+ income_statement = {
936
+ "list_data": [
937
+ ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
938
+ ["Total", "N/A", "N/A", "N/A"],
939
+ ["Net Income", "N/A", "N/A", "N/A.4M"],
940
+ ["Earnings Per Share", "N/A", "N/A", "N/A"],
941
+ ["Operating Expenses", "N/A", "N/A", "N/A"],
942
+ ["Cash Flow", "N/A", "N/A", "N/A"]
943
+ ],
944
+ "yoy_rates": []
945
+ # "yoy_rates": [
946
+ # ["Category", "N/A/FY", "N/A/FY"],
947
+ # ["Total", "N/A", "N/A"],
948
+ # ["Net Income", "+3.05%", "-6.00%"],
949
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
950
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
951
+ # ["Cash Flow", "-13.05%", "-6.00%"]
952
+ # ]
953
+ }
954
+ yearly_data = 'N/A'
955
  # 增长变化的 HTML 字符(箭头+百分比)
956
  def render_change(change: str, color: str):
957
  if change.startswith("+"):
 
996
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
997
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
998
  </svg>
999
+ <div style="font-size: 18px; font-weight: 600;">{yearly_data} Financial Metrics</div>
1000
  </div>
1001
  {metrics_html}
1002
  </div>
1003
  """
1004
  return html
1005
 
1006
+
1007
+ # 主函数:返回所有 HTML 片段
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  def get_dashboard():
1009
  with gr.Row():
1010
  with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
 
1012
  with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1013
  financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1014
  with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1015
+ # 传递income_statement参数
1016
+ income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"])
1017
  return stock_card_html, financial_metrics_html, income_table_html
1018
 
1019
  # 创建指标仪表板并保存引用
 
1027
  def update_metrics_dashboard(company_name):
1028
  """根据选择的公司更新指标仪表板"""
1029
  # 模拟数据
1030
+ # company_info = {
1031
+ # "name": company_name,
1032
+ # "symbol": "NYSE:BABA",
1033
+ # "price": 157.65,
1034
+ # "change": 0.64,
1035
+ # "change_percent": 0.41,
1036
+ # "open": 165.20,
1037
+ # "high": 166.37,
1038
+ # "low": 156.15,
1039
+ # "prev_close": 157.01,
1040
+ # "volume": "27.10M"
1041
+ # }
1042
+ company_info = {}
1043
  # 尝试获取股票价格数据,但不中断程序执行
1044
  try:
1045
  # 根据选择的公司获取股票代码
1046
  stock_code = get_stock_code_by_company_name(company_name)
1047
+ # company_info2 = get_stock_price(stock_code)
1048
+ company_info2 = get_stock_price_from_bailian(stock_code)
1049
+ # print(f"股票价格数据: {company_info2}")
1050
+ company_info = company_info2
1051
  # 如果成功获取数据,则用实际数据替换模拟数据
1052
+ # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1053
+ # import json
1054
+ # # 解析返回的JSON数据
1055
+ # data_text = company_info2["content"][0]["text"]
1056
+ # stock_data = json.loads(data_text)
1057
 
1058
+ # # 提取数据
1059
+ # quote = stock_data["Global Quote"]
1060
 
1061
+ # # 转换交易量单位
1062
+ # volume = int(quote['06. volume'])
1063
+ # if volume >= 1000000:
1064
+ # volume_str = f"{volume / 1000000:.2f}M"
1065
+ # elif volume >= 1000:
1066
+ # volume_str = f"{volume / 1000:.2f}K"
1067
+ # else:
1068
+ # volume_str = str(volume)
1069
 
1070
+ # company_info = {
1071
+ # "name": company_name,
1072
+ # "symbol": f"NYSE:{quote['01. symbol']}",
1073
+ # "price": float(quote['05. price']),
1074
+ # "change": float(quote['09. change']),
1075
+ # "change_percent": float(quote['10. change percent'].rstrip('%')),
1076
+ # "open": float(quote['02. open']),
1077
+ # "high": float(quote['03. high']),
1078
+ # "low": float(quote['04. low']),
1079
+ # "prev_close": float(quote['08. previous close']),
1080
+ # "volume": volume_str
1081
+ # }
1082
  except Exception as e:
1083
  print(f"获取股票价格数据失败: {e}")
1084
  company_info2 = None
1085
 
1086
+ # financial_metrics = [
1087
+ # {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
1088
+ # {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
1089
+ # {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
1090
+ # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1091
+ # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1092
+ # ]
1093
+ financial_metrics_pre = query_financial_data(company_name, "5-Year")
1094
+ # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1095
+ # print(f"最新财务数据: {financial_metrics_pre}")
1096
+ # financial_metrics = format_financial_metrics(financial_metrics_pre)
1097
+
1098
+
1099
+ # financial_metrics_pre_2 = extract_last_three_with_fallback(financial_metrics_pre)
1100
+ # print(f"提取的3年数据: {financial_metrics_pre_2}")
1101
+ # financial_metrics_pre = {
1102
+ # "metrics": financial_metrics_pre_2
1103
+ # }
1104
+ financial_metrics = []
1105
+ # try:
1106
+ # # financial_metrics = calculate_yoy_comparison(financial_metrics_pre)
1107
+ # financial_metrics = build_financial_metrics_three_year_data(financial_metrics_pre)
1108
+ # print(f"格式化后的财务数据: {financial_metrics}")
1109
+ # except Exception as e:
1110
+ # print(f"Error calculating YOY comparison: {e}")
1111
+ year_data = None
1112
+ three_year_data = None
1113
+ try:
1114
+ # financial_metrics = process_financial_data_with_metadata(financial_metrics_pre)
1115
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1116
+
1117
+ # 按需提取字段
1118
+ financial_metrics = result["financial_metrics"]
1119
+ year_data = result["year_data"]
1120
+ three_year_data = result["three_year_data"]
1121
+ print(f"格式化后的财务数据: {financial_metrics}")
1122
+ except Exception as e:
1123
+ print(f"Error process_financial_data: {e}")
1124
+
1125
+
1126
+ # income_statement = [
1127
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1128
+ # ["Total", "130350M", "126491M", "134567M"],
1129
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1130
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1131
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1132
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1133
+ # ]
1134
+
1135
+ # table_data = None
1136
+ # try:
1137
+ # table_data = extract_financial_table(financial_metrics_pre)
1138
+ # print(table_data)
1139
+ # except Exception as e:
1140
+ # print(f"Error extract_financial_table: {e}")
1141
+ # yearly_data = None
1142
+ # try:
1143
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1144
+ # except Exception as e:
1145
+ # print(f"Error get_yearly_data: {e}")
1146
+
1147
+ # ======
1148
+ # table_data = [
1149
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1150
+ # ["Total", "130350M", "126491M", "134567M"],
1151
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1152
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1153
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1154
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1155
+ # ]
1156
+ yearly_data = year_data
1157
+ table_data = build_table_format(three_year_data)
1158
+ print(f"table_data: {table_data}")
1159
+ # yearly_data = None
1160
+ # try:
1161
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1162
+ # except Exception as e:
1163
+ # print(f"Error get_yearly_data: {e}")
1164
+ #=======
1165
+
1166
+ # exp = {
1167
+ # "list_data": [
1168
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1169
+ # ["Total", "130350M", "126491M", "134567M"],
1170
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1171
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1172
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1173
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1174
+ # ],
1175
+ # "yoy_rates": [
1176
+ # ["Category", "2024/FY", "2023/FY"],
1177
+ # ["Total", "+3.05%", "-6.00%"],
1178
+ # ["Net Income", "+3.05%", "-6.00%"],
1179
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1180
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1181
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1182
+ # ]
1183
+ # }
1184
 
1185
  # 增长变化的 HTML 字符(箭头+百分比)
1186
  def render_change(change: str, color: str):
 
1190
  return f'<span style="color:{color};">▼{change}</span>'
1191
 
1192
  # 构建左侧卡片
1193
+ def format_volume(vol_str):
1194
+ """将成交量字符串转为带 M/B 的简洁格式"""
1195
+ if vol_str == "N/A" or not vol_str:
1196
+ return "N/A"
1197
+ try:
1198
+ vol = int(float(vol_str)) # 兼容 "21453064" 或 "2.145e7"
1199
+ if vol >= 1_000_000_000:
1200
+ val = vol / 1_000_000_000
1201
+ return f"{val:.2f}B".rstrip('0').rstrip('.')
1202
+ elif vol >= 1_000_000:
1203
+ val = vol / 1_000_000
1204
+ return f"{val:.2f}M".rstrip('0').rstrip('.')
1205
+ elif vol >= 1_000:
1206
+ return f"{vol // 1_000}K"
1207
+ else:
1208
+ return str(vol)
1209
+ except (ValueError, TypeError):
1210
+ return "N/A"
1211
+ def build_stock_card(company_info):
1212
+ try:
1213
+ if not company_info or not isinstance(company_info, dict):
1214
+ company_name = "N/A"
1215
+ symbol = "N/A"
1216
+ price = "N/A"
1217
+ change_html = '<span style="color:#888;">N/A</span>'
1218
+ open_val = high_val = low_val = prev_close_val = volume_display = "N/A"
1219
+ else:
1220
+ company_name = company_info.get("company", "N/A")
1221
+ symbol = company_info.get("symbol", "N/A")
1222
+ price = company_info.get("price", "N/A")
1223
+
1224
+ # 解析 change
1225
+ change_str = company_info.get("change", "0")
1226
+ try:
1227
+ change = float(change_str)
1228
+ except (ValueError, TypeError):
1229
+ change = 0.0
1230
 
1231
+ # 解析 change_percent
1232
+ change_percent_str = company_info.get("change_percent", "0%")
1233
+ try:
1234
+ change_percent = float(change_percent_str.rstrip('%'))
1235
+ except (ValueError, TypeError):
1236
+ change_percent = 0.0
1237
+
1238
+ change_color = "green" if change >= 0 else "red"
1239
+ sign = "+" if change >= 0 else ""
1240
+ change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>'
1241
+
1242
+ # 其他价格字段(可选:也可格式化为 2 位小数)
1243
+ open_val = company_info.get("open", "N/A")
1244
+ high_val = company_info.get("high", "N/A")
1245
+ low_val = company_info.get("low", "N/A")
1246
+ prev_close_val = company_info.get("previous close", "N/A")
1247
+ raw_volume = company_info.get("volume", "N/A")
1248
+ volume_display = format_volume(raw_volume)
1249
+
1250
+ html = f"""
1251
+ <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1252
+ <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1253
+ <div style="font-size: 12px; color: #888;">{symbol}</div>
1254
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1255
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
1256
+ <div style="font-size: 14px;">{change_html}</div>
1257
+ </div>
1258
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1259
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div>
1260
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1261
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1262
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1263
+ <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1264
+ </div>
1265
  </div>
1266
+ """
1267
+ return html
1268
+
1269
+ except Exception as e:
1270
+ print(f"Error building stock card: {e}")
1271
+ return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1272
+ # def build_stock_card():
1273
+ # try:
1274
+ # # 检查是否获取到了股票数据,如果没有则显示N/A
1275
+ # if company_info:
1276
+ # price = company_info["price"]
1277
+ # change = int(company_info["change"])
1278
+ # change_percent = company_info["change_percent"]
1279
+
1280
+ # # 格式化价格变动
1281
+ # change_color = "green" if change > 0 else "red"
1282
+ # change_html = f'<span style="color:{change_color};">+{change:.2f}({change_percent:+.2f}%)</span>' if change > 0 else \
1283
+ # f'<span style="color:{change_color};">{change:.2f}({change_percent:+.2f}%)</span>'
1284
+ # else:
1285
+ # # 如果没有获取到数据,所有数值显示N/A
1286
+ # price = "N/A"
1287
+ # change = 0
1288
+ # change_percent = 0
1289
+ # change_html = "<span style=\"color:#888;\">N/A</span>"
1290
+ # html = f"""
1291
+ # <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1292
+ # <div style="font-size: 16px; color: #555;font-weight: 500;">{company_info['name']}</div>
1293
+ # <div style="font-size: 12px; color: #888;">{company_info['symbol'] if (company_info and 'symbol' in company_info) else 'NYSE:N/A'}</div>
1294
+ # <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1295
+ # <div style="font-size: 32px; font-weight: bold;">{price}</div>
1296
+ # <div style="font-size: 14px;">{change_html}</div>
1297
+ # </div>
1298
+ # <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1299
+ # <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['open'] if (company_info and 'open' in company_info) else 'N/A'}</div>
1300
+ # <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['high'] if (company_info and 'high' in company_info) else 'N/A'}</div>
1301
+ # <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['low'] if (company_info and 'low' in company_info) else 'N/A'}</div>
1302
+ # <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['prev_close'] if (company_info and 'prev_close' in company_info) else 'N/A'}</div>
1303
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['volume'] if (company_info and 'volume' in company_info) else 'N/A'}</div>
1304
+ # </div>
1305
+ # </div>
1306
+ # """
1307
+ # return html
1308
+ # except Exception as e:
1309
+ # print(f"Error building stock card: {e}")
1310
+
1311
+
1312
 
1313
  # 构建中间卡片
1314
+ def build_financial_metrics(yearly_data):
1315
  metrics_html = ""
1316
  for item in financial_metrics:
1317
  change_html = render_change(item["change"], item["color"])
 
1328
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1329
  <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1330
  </svg>
1331
+ <div style="font-size: 18px; font-weight: 600;">{yearly_data} Financial Metrics</div>
1332
  </div>
1333
  {metrics_html}
1334
  </div>
 
1336
  return html
1337
 
1338
  # 构建右侧表格
1339
+ # def build_income_table(income_statement):
1340
+ # table_rows = ""
1341
+ # for i, row in enumerate(income_statement):
1342
+ # if i == 0:
1343
+ # row_style = "background-color: #f5f5f5; font-weight: 500;"
1344
+ # else:
1345
+ # row_style = "background-color: #f9f9f9;"
1346
+ # cells = ""
1347
+ # for j, cell in enumerate(row):
1348
+ # if j == 0:
1349
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1350
+ # else:
1351
+ # # 添加增长箭头(模拟数据)
1352
+ # growth = None
1353
+ # if i == 1 and j == 1: growth = "+3.05%"
1354
+ # elif i == 1 and j == 2: growth = "-6.00%"
1355
+ # elif i == 2 and j == 1: growth = "+3.05%"
1356
+ # elif i == 2 and j == 2: growth = "-6.00%"
1357
+ # elif i == 3 and j == 1: growth = "+3.05%"
1358
+ # elif i == 3 and j == 2: growth = "-6.00%"
1359
+ # elif i == 4 and j == 1: growth = "+29.17%"
1360
+ # elif i == 4 and j == 2: growth = "+29.17%"
1361
+ # elif i == 5 and j == 1: growth = "-13.05%"
1362
+ # elif i == 5 and j == 2: growth = "+29.17%"
1363
+
1364
+ # if growth:
1365
+ # arrow = "▲" if growth.startswith("+") else "▼"
1366
+ # color = "green" if growth.startswith("+") else "red"
1367
+ # cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
1368
+ # <div>{cell}</div>
1369
+ # <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
1370
+ # </td>"""
1371
+ # else:
1372
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1373
+ # table_rows += f"<tr style='{row_style}'>{cells}</tr>"
1374
 
1375
+ # html = f"""
1376
+ # <div style="width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1377
+ # <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1378
+ # <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1379
+ # <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1380
+ # </svg>
1381
+ # <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1382
+ # </div>
1383
+ # <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1384
+ # {table_rows}
1385
+ # </table>
1386
+ # </div>
1387
+ # """
1388
+ # return html
1389
 
1390
  # 返回三个HTML组件的内容
1391
+ return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1392
  # gr.Column(scale=1, min_width=250)
1393
  # gr.HTML(f'''
1394
  # <div class="metric-card-item" style="{card_custom_style}width:300px;">
 
1842
  def update_metrics_dashboard_wrapper(company_name):
1843
  if company_name:
1844
  # 显示loading状态
1845
+ loading_html = f'''
1846
  <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1847
  <div style="text-align: center;">
1848
  <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1849
  <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1850
  <style>
1851
+ @keyframes spin {{
1852
+ 0% {{ transform: rotate(0deg); }}
1853
+ 100% {{ transform: rotate(360deg); }}
1854
+ }}
1855
  </style>
1856
  </div>
1857
  </div>
requirements.txt CHANGED
@@ -2,5 +2,12 @@ gradio>=5.49.1
2
  huggingface_hub>=0.20.0
3
  python-dotenv>=1.0.0
4
  SQLAlchemy>=2.0.44
 
5
  pandas>=2.2.2
6
- numpy>=1.26.4
 
 
 
 
 
 
 
2
  huggingface_hub>=0.20.0
3
  python-dotenv>=1.0.0
4
  SQLAlchemy>=2.0.44
5
+ plotly>=5.24.1
6
  pandas>=2.2.2
7
+ numpy>=1.26.4
8
+ aiohttp>=3.8.1
9
+ pdfplumber>=0.7.0
10
+ beautifulsoup4>=4.11.0
11
+ requests>=2.32.0
12
+ urllib3>=2.5.0
13
+ httpx>=0.23.0
service/chat_service.py CHANGED
@@ -126,6 +126,18 @@ def search_news(user_input: str):
126
  return choices
127
  except json.JSONDecodeError:
128
  return []
 
 
 
 
 
 
 
 
 
 
 
 
129
  # def search_company(user_input: str):
130
  # if not user_input.strip():
131
  # return [] # 返回空列表,而不是 yield
 
126
  return choices
127
  except json.JSONDecodeError:
128
  return []
129
+
130
+ def get_stock_price_from_bailian(user_input: str):
131
+ if not user_input.strip():
132
+ return {}
133
+ # 获取非流式响应
134
+ response = chat_with_bailian_non_streaming(user_input, "d2b919f9b32e4fa28a75234cbb78a787")
135
+ print(f"查询结果:{response}{user_input}")
136
+ try:
137
+ parsed_data = json.loads(response)
138
+ return parsed_data if isinstance(parsed_data, dict) else {}
139
+ except json.JSONDecodeError:
140
+ return {}
141
  # def search_company(user_input: str):
142
  # if not user_input.strip():
143
  # return [] # 返回空列表,而不是 yield
service/report_mcp.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+
6
+ MCP_SPACE = "JC321/EasyReportsMCPServer"
7
+ MCP_URL = "https://jc321-easyreportsmcpserver.hf.space"
8
+
9
+ # 设置请求头
10
+ HEADERS = {
11
+ "Content-Type": "application/json",
12
+ "User-Agent": "SEC-Query-Assistant/1.0 ([email protected])"
13
+ }
14
+
15
+ # 格式化数值显示
16
+ def format_value(value, value_type="money"):
17
+ """
18
+ 格式化数值:0显示为N/A,其他显示为带单位的格式
19
+ value_type: "money" (金额), "eps" (每股收益), "number" (普通数字)
20
+ """
21
+ if value is None or value == 0:
22
+ return "N/A"
23
+
24
+ if value_type == "money":
25
+ return f"${value:.2f}B"
26
+ elif value_type == "eps":
27
+ return f"${value:.2f}"
28
+ else: # number
29
+ return f"{value:.2f}"
30
+
31
+ def normalize_cik(cik):
32
+ """
33
+ 格式化 CIK 为标准的 10 位格式
34
+ """
35
+ if not cik:
36
+ return None
37
+ # 转换为字符串并移除非数字字符
38
+ cik_str = str(cik).replace('-', '').replace(' ', '')
39
+ # 仅保留数字
40
+ cik_str = ''.join(c for c in cik_str if c.isdigit())
41
+ # 填充前导 0 至 10 位
42
+ return cik_str.zfill(10) if cik_str else None
43
+
44
+ def parse_mcp_response(response_data):
45
+ """
46
+ 解析 MCP 协议响应数据
47
+ 支持格式:
48
+ 1. {"result": {"content": [{"type": "text", "text": "{...}"}]}}
49
+ 2. {"content": [{"type": "text", "text": "{...}"}]}
50
+ 3. 直接的 JSON 数据
51
+ """
52
+ if not isinstance(response_data, dict):
53
+ return response_data
54
+
55
+ # 格式 1: {"result": {"content": [...]}}
56
+ if "result" in response_data and "content" in response_data["result"]:
57
+ content = response_data["result"]["content"]
58
+ if content and len(content) > 0:
59
+ text_content = content[0].get("text", "{}")
60
+ # 直接解析 JSON(MCP Server 已移除 emoji 前缀)
61
+ try:
62
+ return json.loads(text_content)
63
+ except json.JSONDecodeError:
64
+ return text_content
65
+ return {}
66
+
67
+ # 格式 2: {"content": [...]}
68
+ elif "content" in response_data:
69
+ content = response_data.get("content", [])
70
+ if content and len(content) > 0:
71
+ text_content = content[0].get("text", "{}")
72
+ # 直接解析 JSON
73
+ try:
74
+ return json.loads(text_content)
75
+ except json.JSONDecodeError:
76
+ return text_content
77
+ return {}
78
+
79
+ # 格式 3: 直接返回
80
+ return response_data
81
+
82
+ # MCP 工具定义
83
+ def create_mcp_tools():
84
+ """创建 MCP 工具列表"""
85
+ return [
86
+ {
87
+ "name": "query_financial_data",
88
+ "description": "Query SEC financial data for US listed companies",
89
+ "parameters": {
90
+ "type": "object",
91
+ "properties": {
92
+ "company_name": {
93
+ "type": "string",
94
+ "description": "Company name or stock symbol (e.g., Apple, NVIDIA, AAPL)"
95
+ },
96
+ "query_type": {
97
+ "type": "string",
98
+ "enum": ["Latest Financial Data", "3-Year Trends", "5-Year Trends"],
99
+ "description": "Type of financial query"
100
+ }
101
+ },
102
+ "required": ["company_name", "query_type"]
103
+ }
104
+ }
105
+ ]
106
+
107
+ # 工具执行函数
108
+ def execute_tool(tool_name, **kwargs):
109
+ """执行 MCP 工具"""
110
+ if tool_name == "query_financial_data":
111
+ return query_financial_data(kwargs.get("company_name"), kwargs.get("query_type"))
112
+ return f"Unknown tool: {tool_name}"
113
+ # 创建超链接
114
+ def create_source_link(source_form, source_url=None):
115
+ """为Source Form创建超链接,使用MCP后端返回的URL"""
116
+ if not source_form or source_form == 'N/A':
117
+ return source_form
118
+
119
+ # 如果后端提供了URL,使用后端的URL
120
+ if source_url and source_url != 'N/A':
121
+ return f"[{source_form}]({source_url})"
122
+
123
+ # 如果没有URL,只显示文本
124
+ return source_form
125
+
126
+ def query_financial_data(company_name, query_type):
127
+ """查询财务数据的主函数"""
128
+
129
+ if not company_name:
130
+ return "Please enter a company name or stock symbol"
131
+
132
+ # 翻译英文查询类型为中文(用于后端处理)
133
+ query_type_mapping = {
134
+ "Latest": "最新财务数据",
135
+ "3-Year": "3年趋势",
136
+ "5-Year": "5年趋势",
137
+ "Filings": "公司报表列表"
138
+ }
139
+ internal_query_type = query_type_mapping.get(query_type, query_type)
140
+
141
+ try:
142
+ # 使用 MCP 协议调用工具
143
+ # 先搜索公司(使用 advanced_search_company)
144
+ search_resp = requests.post(
145
+ f"{MCP_URL}/message",
146
+ json={
147
+ "method": "tools/call",
148
+ "params": {
149
+ "name": "advanced_search_company",
150
+ "arguments": {"company_input": company_name}
151
+ }
152
+ },
153
+ headers=HEADERS,
154
+ timeout=30
155
+ )
156
+
157
+ print(f"搜索公司:{company_name},search_resp.status_code: {search_resp.status_code}\nSearch Response: {search_resp.text}")
158
+
159
+ if search_resp.status_code != 200:
160
+ print(f"❌ Server Error: HTTP {search_resp.status_code}\n\nResponse: {search_resp.text[:500]}")
161
+ return []
162
+
163
+ try:
164
+ result = search_resp.json()
165
+ # 使用统一的 MCP 响应解析函数
166
+ company = parse_mcp_response(result)
167
+ except (ValueError, KeyError, json.JSONDecodeError) as e:
168
+ return f"❌ JSON Parse Error: {str(e)}\n\nResponse: {search_resp.text[:500]}"
169
+
170
+ if isinstance(company, dict) and company.get("error"):
171
+ return f"❌ Error: {company['error']}"
172
+
173
+ # advanced_search 返回的字段: cik, name, ticker
174
+ # 注意: 不是 tickers 和 sic_description
175
+ company_name = company.get('name', 'Unknown')
176
+ ticker = company.get('ticker', 'N/A')
177
+
178
+ result = f"# {company_name}\n\n"
179
+ result += f"**Stock Symbol**: {ticker}\n"
180
+ # sic_description 需要后续通过 get_company_info 获取,这里暂时不显示
181
+ result += "\n---\n\n"
182
+
183
+ # 获取并格式化 CIK 为 10 位标准格式
184
+ cik = normalize_cik(company.get('cik'))
185
+ if not cik:
186
+ return result + f"❌ Error: Invalid CIK from company search\n\nDebug: company data = {json.dumps(company, indent=2)}"
187
+
188
+ # 根据查询类型获取数据
189
+ if internal_query_type == "最新财务数据":
190
+ data_resp = requests.post(
191
+ f"{MCP_URL}/message",
192
+ json={
193
+ "method": "tools/call",
194
+ "params": {
195
+ "name": "get_latest_financial_data",
196
+ "arguments": {"cik": cik}
197
+ }
198
+ },
199
+ headers=HEADERS,
200
+ timeout=30
201
+ )
202
+
203
+ if data_resp.status_code != 200:
204
+ return result + f"❌ Server Error: HTTP {data_resp.status_code}\n\n{data_resp.text[:500]}"
205
+
206
+ try:
207
+ data_result = data_resp.json()
208
+ # 使用统一的 MCP 响应解析函数
209
+ data = parse_mcp_response(data_result)
210
+ except (ValueError, KeyError, json.JSONDecodeError) as e:
211
+ return result + f"❌ JSON Parse Error: {str(e)}\n\n{data_resp.text[:500]}"
212
+
213
+ if isinstance(data, dict) and data.get("error"):
214
+ return result + f"❌ {data['error']}"
215
+
216
+ cik = data.get('cik')
217
+ result += f"## Fiscal Year {data.get('period', 'N/A')}\n\n"
218
+
219
+ total_revenue = data.get('total_revenue', 0) / 1e9 if data.get('total_revenue') else 0
220
+ net_income = data.get('net_income', 0) / 1e9 if data.get('net_income') else 0
221
+ eps = data.get('earnings_per_share', 0) if data.get('earnings_per_share') else 0
222
+ opex = data.get('operating_expenses', 0) / 1e9 if data.get('operating_expenses') else 0
223
+ ocf = data.get('operating_cash_flow', 0) / 1e9 if data.get('operating_cash_flow') else 0
224
+
225
+ result += f"- **Total Revenue**: {format_value(total_revenue)}\n"
226
+ result += f"- **Net Income**: {format_value(net_income)}\n"
227
+ result += f"- **Earnings Per Share**: {format_value(eps, 'eps')}\n"
228
+ result += f"- **Operating Expenses**: {format_value(opex)}\n"
229
+ result += f"- **Operating Cash Flow**: {format_value(ocf)}\n"
230
+ # 使用后端返回的 source_url
231
+ source_form = data.get('source_form', 'N/A')
232
+ source_url = data.get('source_url', None) # 从后端获取URL
233
+ result += f"- **Source Form**: {create_source_link(source_form, source_url)}\n"
234
+
235
+ elif internal_query_type == "3年趋势":
236
+ metrics_resp = requests.post(
237
+ f"{MCP_URL}/message",
238
+ json={
239
+ "method": "tools/call",
240
+ "params": {
241
+ "name": "extract_financial_metrics",
242
+ "arguments": {"cik": cik, "years": 3}
243
+ }
244
+ },
245
+ headers=HEADERS,
246
+ timeout=60
247
+ )
248
+
249
+ # 调试:显示 HTTP 响应状态
250
+ result += f"\n**Debug Info (3-Year)**:\n- HTTP Status: {metrics_resp.status_code}\n"
251
+
252
+ if metrics_resp.status_code != 200:
253
+ return result + f"❌ Server Error: HTTP {metrics_resp.status_code}\n\n{metrics_resp.text[:500]}"
254
+
255
+ try:
256
+ metrics_result = metrics_resp.json()
257
+ # 调试:显示原始 JSON 响应
258
+ result += f"- Raw Response Length: {len(metrics_resp.text)} chars\n"
259
+ result += f"- Response Preview: {metrics_resp.text[:200]}...\n\n"
260
+
261
+ # 使用统一的 MCP 响应解析函数
262
+ metrics = parse_mcp_response(metrics_result)
263
+
264
+ # 调试:显示解析后的数据类型和内容
265
+ result += f"- Parsed Type: {type(metrics).__name__}\n"
266
+ if isinstance(metrics, dict):
267
+ result += f"- Parsed Keys: {list(metrics.keys())}\n"
268
+ result += f"- Periods: {metrics.get('periods', 'N/A')}\n"
269
+ result += f"- Data Length: {len(metrics.get('data', []))}\n\n"
270
+ except (ValueError, KeyError, json.JSONDecodeError) as e:
271
+ return result + f"❌ JSON Parse Error: {str(e)}\n\nResponse: {metrics_resp.text[:500]}"
272
+
273
+ if isinstance(metrics, dict) and metrics.get("error"):
274
+ return result + f"❌ {metrics['error']}"
275
+
276
+ # 调试:显示原始响应
277
+ if not isinstance(metrics, dict):
278
+ return result + f"❌ Invalid response format\n\nDebug: {str(metrics)[:500]}"
279
+
280
+ result += f"## 3-Year Financial Trends ({metrics.get('periods', 0)} periods)\n\n"
281
+
282
+ # 显示所有数据(包括年度和季度)
283
+ all_data = metrics.get('data', []) # MCP Server 返回的字段是 'data'
284
+
285
+ # 调试:检查是否有数据
286
+ if not all_data:
287
+ return result + f"❌ No data returned from MCP Server\n\nDebug: metrics keys = {list(metrics.keys())}\n\nFull response: {json.dumps(metrics, indent=2, ensure_ascii=False)[:1000]}"
288
+
289
+ # 去重:根据period和source_form去重
290
+ seen = set()
291
+ unique_data = []
292
+ for m in all_data:
293
+ key = (m.get('period', 'N/A'), m.get('source_form', 'N/A'))
294
+ if key not in seen:
295
+ seen.add(key)
296
+ unique_data.append(m)
297
+
298
+ # 按期间降序排序,确保显示最近的3年数据
299
+ # 使用更智能的排序:先按年份,再按是否是季度
300
+ # 正确顺序:FY2024 → 2024Q3 → 2024Q2 → 2024Q1 → FY2023
301
+ def sort_key(x):
302
+ period = x.get('period', '0000')
303
+ # 提取年份(前4位)
304
+ year = period[:4] if len(period) >= 4 else '0000'
305
+ # 如果有Q,提取季度号
306
+ if 'Q' in period:
307
+ quarter = period[period.index('Q')+1] if period.index('Q')+1 < len(period) else '0'
308
+ return (year, 1, 4 - int(quarter)) # Q在FY后面:Q3, Q2, Q1 (4-3=1, 4-2=2, 4-1=3)
309
+ else:
310
+ return (year, 0, 0) # FY 排在同年的所有Q之前
311
+
312
+ unique_data = sorted(unique_data, key=sort_key, reverse=True)
313
+
314
+ result += "| Period | Revenue (B) | Net Income (B) | EPS | Operating Expenses (B) | Operating Cash Flow (B) | Source Form |\n"
315
+ result += "|--------|-------------|----------------|-----|------------------------|-------------------------|-------------|\n"
316
+
317
+ for m in unique_data:
318
+ period = m.get('period', 'N/A')
319
+ rev = (m.get('total_revenue') or 0) / 1e9
320
+ inc = (m.get('net_income') or 0) / 1e9
321
+ eps_val = m.get('earnings_per_share') or 0
322
+ opex = (m.get('operating_expenses') or 0) / 1e9
323
+ ocf = (m.get('operating_cash_flow') or 0) / 1e9
324
+ source_form = m.get('source_form', 'N/A')
325
+ source_url = m.get('source_url', None) # 从后端获取URL
326
+
327
+ # 区分年度和季度,修复双重FY前缀问题
328
+ if 'Q' in period:
329
+ # 季度数据,不添加前缀
330
+ display_period = period
331
+ else:
332
+ # 年度数据,只在没有FY的情况下添加
333
+ display_period = period if period.startswith('FY') else f"FY{period}"
334
+
335
+ source_link = create_source_link(source_form, source_url)
336
+
337
+ result += f"| {display_period} | {format_value(rev)} | {format_value(inc)} | {format_value(eps_val, 'eps')} | {format_value(opex)} | {format_value(ocf)} | {source_link} |\n"
338
+
339
+ elif internal_query_type == "5年趋势":
340
+ metrics_resp = requests.post(
341
+ f"{MCP_URL}/message",
342
+ json={
343
+ "method": "tools/call",
344
+ "params": {
345
+ "name": "extract_financial_metrics",
346
+ "arguments": {"cik": cik, "years": 5}
347
+ }
348
+ },
349
+ headers=HEADERS,
350
+ timeout=60
351
+ )
352
+
353
+ # 调试:显示 HTTP 响应状态
354
+ result += f"\n**Debug Info (5-Year)**:\n- HTTP Status: {metrics_resp.status_code}\n"
355
+
356
+ if metrics_resp.status_code != 200:
357
+ return result + f"❌ Server Error: HTTP {metrics_resp.status_code}\n\n{metrics_resp.text[:500]}"
358
+
359
+ try:
360
+ metrics_result = metrics_resp.json()
361
+ # 调试:显示原始 JSON 响应
362
+ result += f"- Raw Response Length: {len(metrics_resp.text)} chars\n"
363
+ result += f"- Response Preview: {metrics_resp.text[:200]}...\n\n"
364
+
365
+ # 使用统一的 MCP 响应解析函数
366
+ metrics = parse_mcp_response(metrics_result)
367
+
368
+ # 调试:显示解析后的数据类型和内容
369
+ result += f"- Parsed Type: {type(metrics).__name__}\n"
370
+ if isinstance(metrics, dict):
371
+ result += f"- Parsed Keys: {list(metrics.keys())}\n"
372
+ result += f"- Periods: {metrics.get('periods', 'N/A')}\n"
373
+ result += f"- Data Length: {len(metrics.get('data', []))}\n\n"
374
+ except (ValueError, KeyError, json.JSONDecodeError) as e:
375
+ return result + f"❌ JSON Parse Error: {str(e)}\n\nResponse: {metrics_resp.text[:500]}"
376
+
377
+ if isinstance(metrics, dict) and metrics.get("error"):
378
+ return result + f"❌ {metrics['error']}"
379
+
380
+ # 调试:显示原始响应
381
+ if not isinstance(metrics, dict):
382
+ return result + f"❌ Invalid response format\n\nDebug: {str(metrics)[:500]}"
383
+
384
+ # 显示所有数据(包括年度和季度)
385
+ all_data = metrics.get('data', []) # MCP Server 返回的字段是 'data'
386
+
387
+ # 调试:检查是否有数据
388
+ if not all_data:
389
+ return result + f"❌ No data returned from MCP Server\n\nDebug: metrics keys = {list(metrics.keys())}\n\nFull response: {json.dumps(metrics, indent=2, ensure_ascii=False)[:1000]}"
390
+
391
+ # 去重:根据period和source_form去重
392
+ seen = set()
393
+ unique_data = []
394
+ for m in all_data:
395
+ key = (m.get('period', 'N/A'), m.get('source_form', 'N/A'))
396
+ if key not in seen:
397
+ seen.add(key)
398
+ unique_data.append(m)
399
+
400
+ # 按期间降序排序,确保显示最近的5年数据
401
+ # 使用更智能的排序:先按年份,再按是否是季度
402
+ # 正确顺序:FY2024 → 2024Q3 → 2024Q2 → 2024Q1 → FY2023
403
+ def sort_key(x):
404
+ period = x.get('period', '0000')
405
+ # 提取年份(前4位)
406
+ year = period[:4] if len(period) >= 4 else '0000'
407
+ # 如果有Q,提取季度号
408
+ if 'Q' in period:
409
+ quarter = period[period.index('Q')+1] if period.index('Q')+1 < len(period) else '0'
410
+ return (year, 1, 4 - int(quarter)) # Q在FY后面:Q3, Q2, Q1 (4-3=1, 4-2=2, 4-1=3)
411
+ else:
412
+ return (year, 0, 0) # FY 排在同年的所有Q之前
413
+
414
+ unique_data = sorted(unique_data, key=sort_key, reverse=True)
415
+ print(f'5年数据::{unique_data}')
416
+ result = unique_data
417
+
418
+ elif internal_query_type == "公司报表列表":
419
+ # 查询公司所有报表
420
+ filings_resp = requests.post(
421
+ f"{MCP_URL}/message",
422
+ json={
423
+ "method": "tools/call",
424
+ "params": {
425
+ "name": "get_company_filings",
426
+ "arguments": {"cik": cik, "limit": 50}
427
+ }
428
+ },
429
+ headers=HEADERS,
430
+ timeout=60
431
+ )
432
+
433
+ if filings_resp.status_code != 200:
434
+ return result + f"❌ Server Error: HTTP {filings_resp.status_code}\n\n{filings_resp.text[:500]}"
435
+
436
+ try:
437
+ filings_result = filings_resp.json()
438
+ # 使用统一的 MCP 响应解析函数
439
+ filings_data = parse_mcp_response(filings_result)
440
+ except (ValueError, KeyError, json.JSONDecodeError) as e:
441
+ return result + f"❌ JSON Parse Error: {str(e)}\n\n{filings_resp.text[:500]}"
442
+
443
+ if isinstance(filings_data, dict) and filings_data.get("error"):
444
+ return result + f"❌ {filings_data['error']}"
445
+
446
+ filings = filings_data.get('filings', []) if isinstance(filings_data, dict) else filings_data
447
+
448
+ result += f"## Company Filings ({len(filings)} records)\n\n"
449
+ result += "| Form Type | Filing Date | Accession Number | Primary Document |\n"
450
+ result += "|-----------|-------------|------------------|------------------|\n"
451
+
452
+ for filing in filings:
453
+ form_type = filing.get('form_type', 'N/A')
454
+ filing_date = filing.get('filing_date', 'N/A')
455
+ accession_num = filing.get('accession_number', 'N/A')
456
+ primary_doc = filing.get('primary_document', 'N/A')
457
+ filing_url = filing.get('filing_url', None) # 从后端获取URL
458
+
459
+ # 使用后端返回的URL创建链接
460
+ if filing_url and filing_url != 'N/A':
461
+ form_link = f"[{form_type}]({filing_url})"
462
+ primary_doc_link = f"[{primary_doc}]({filing_url})"
463
+ else:
464
+ form_link = form_type
465
+ primary_doc_link = primary_doc
466
+
467
+ result += f"| {form_link} | {filing_date} | {accession_num} | {primary_doc_link} |\n"
468
+
469
+ return result
470
+
471
+ except requests.exceptions.RequestException as e:
472
+ return f"❌ Network Error: {str(e)}\n\nMCP Server: {MCP_URL}"
473
+ except Exception as e:
474
+ import traceback
475
+ return f"❌ Unexpected Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
service/report_tools.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ def extract_last_three_with_fallback(data_list):
4
+ # 定义年份范围(当前最新是 FY2025,所以前三年是 2025, 2024, 2023)
5
+ years = [2025, 2024, 2023]
6
+
7
+ # 构建 period 映射:按优先级
8
+ priority_levels = [
9
+ ("FY", [f"FY{y}" for y in years]),
10
+ ("Q4", [f"{y}Q4" for y in years]),
11
+ ("Q3", [f"{y}Q3" for y in years]),
12
+ ("Q2", [f"{y}Q2" for y in years]),
13
+ ("Q1", [f"{y}Q1" for y in years]),
14
+ ]
15
+
16
+ # 转为字典便于查找
17
+ data_map = {item["period"]: item for item in data_list if "period" in item}
18
+
19
+ # 按优先级尝试
20
+ for level_name, periods in priority_levels:
21
+ records = []
22
+ valid = True
23
+
24
+ for period in periods:
25
+ item = data_map.get(period)
26
+ if item is None or item.get("total_revenue") is None:
27
+ valid = False
28
+ break
29
+ # 提取关键字段
30
+ clean_item = {
31
+ "period": period,
32
+ "fiscal_year": int(period[:4]) if level_name != "FY" else int(period[2:]),
33
+ "level": level_name,
34
+ "total_revenue": item["total_revenue"],
35
+ "net_income": item.get("net_income"),
36
+ "earnings_per_share": item.get("earnings_per_share"),
37
+ "operating_expenses": item.get("operating_expenses"),
38
+ "operating_cash_flow": item.get("operating_cash_flow"),
39
+ "source_url": item.get("source_url")
40
+ }
41
+ records.append(clean_item)
42
+
43
+ if valid:
44
+ # 找到完整三年数据,返回
45
+ return records
46
+
47
+ # 如果所有层级都不完整,可选择返回最高优先级中有效的部分(或抛异常)
48
+ # 这里我们返回最高优先级中非空的记录(保守策略)
49
+ for level_name, periods in priority_levels:
50
+ records = []
51
+ for period in periods:
52
+ item = data_map.get(period)
53
+ if item and item.get("total_revenue") is not None:
54
+ clean_item = {
55
+ "period": period,
56
+ "fiscal_year": int(period[:4]) if level_name != "FY" else int(period[2:]),
57
+ "level": level_name,
58
+ "total_revenue": item["total_revenue"],
59
+ "net_income": item.get("net_income"),
60
+ "earnings_per_share": item.get("earnings_per_share"),
61
+ "operating_expenses": item.get("operating_expenses"),
62
+ "operating_cash_flow": item.get("operating_cash_flow"),
63
+ "source_url": item.get("source_url")
64
+ }
65
+ records.append(clean_item)
66
+ if records:
67
+ return records # 返回第一个有数据的层级(即使不全)
68
+
69
+ return [] # 完全无数据
70
+
71
+
72
+ def format_number(value):
73
+ """将大数字格式化为 $XM 或 $XB"""
74
+ if value >= 1_000_000_000:
75
+ return f"${value / 1_000_000_000:.2f}B".replace(".00B", "B").replace(".0B", "B")
76
+ elif value >= 1_000_000:
77
+ return f"${value / 1_000_000:.1f}M".replace(".0M", "M")
78
+ else:
79
+ return f"${value:,.0f}"
80
+
81
+ def format_eps(value):
82
+ """EPS 保留两位小数"""
83
+ return f"${value:.2f}"
84
+
85
+ def safe_int(val):
86
+ """安全转换为 int,支持字符串或 None"""
87
+ if val is None:
88
+ return 0
89
+ try:
90
+ return int(float(val)) # 兼容字符串或 float
91
+ except (ValueError, TypeError):
92
+ return 0
93
+
94
+ def calculate_change(current, previous):
95
+ """计算同比变化百分比,返回如 '+12.4%' 或 '-3.2%'"""
96
+ if previous == 0:
97
+ return "+0.0%" if current >= 0 else "-0.0%"
98
+ change = (current - previous) / abs(previous) * 100
99
+ sign = "+" if change >= 0 else "-"
100
+ return f"{sign}{abs(change):.1f}%"
101
+
102
+ def build_financial_metrics_three_year_data(three_year_data):
103
+ # 确保按 fiscal_year 降序排列(最新在前)
104
+ sorted_data = sorted(three_year_data, key=lambda x: x["fiscal_year"], reverse=True)
105
+ if len(sorted_data) < 2:
106
+ raise ValueError("至少需要两年数据来计算同比变化")
107
+
108
+ latest = sorted_data[0]
109
+ previous = sorted_data[1]
110
+
111
+ # 提取并转为 int
112
+ rev_curr = safe_int(latest.get("total_revenue"))
113
+ rev_prev = safe_int(previous.get("total_revenue"))
114
+
115
+ net_curr = safe_int(latest.get("net_income"))
116
+ net_prev = safe_int(previous.get("net_income"))
117
+
118
+ eps_curr = float(latest.get("earnings_per_share", 0) or 0)
119
+ eps_prev = float(previous.get("earnings_per_share", 0) or 0)
120
+
121
+ opex_curr = safe_int(latest.get("operating_expenses"))
122
+ opex_prev = safe_int(previous.get("operating_expenses"))
123
+
124
+ cash_curr = safe_int(latest.get("operating_cash_flow"))
125
+ cash_prev = safe_int(previous.get("operating_cash_flow"))
126
+
127
+ metrics = [
128
+ {
129
+ "label": "Total Revenue",
130
+ "value": format_number(rev_curr),
131
+ "change": calculate_change(rev_curr, rev_prev),
132
+ "color": "green" if rev_curr >= rev_prev else "red"
133
+ },
134
+ {
135
+ "label": "Net Income",
136
+ "value": format_number(net_curr),
137
+ "change": calculate_change(net_curr, net_prev),
138
+ "color": "green" if net_curr >= net_prev else "red"
139
+ },
140
+ {
141
+ "label": "Earnings Per Share",
142
+ "value": format_eps(eps_curr),
143
+ "change": calculate_change(eps_curr, eps_prev),
144
+ "color": "green" if eps_curr >= eps_prev else "red"
145
+ },
146
+ {
147
+ "label": "Operating Expenses",
148
+ "value": format_number(opex_curr),
149
+ "change": calculate_change(opex_curr, opex_prev),
150
+ "color": "green" if opex_curr >= opex_prev else "red"
151
+ },
152
+ {
153
+ "label": "Cash Flow",
154
+ "value": format_number(cash_curr),
155
+ "change": calculate_change(cash_curr, cash_prev),
156
+ "color": "green" if cash_curr >= cash_prev else "red"
157
+ }
158
+ ]
159
+
160
+ return metrics
161
+ # 假设你的原始数据变量名为 raw_data(即你提供的大列表)
162
+ # raw_data = [ {...}, ... ]
163
+
164
+ # 执行
165
+ # result = extract_last_three_with_fallback(raw_data)
166
+
167
+ # # 输出 JSON
168
+ # json_output = json.dumps(result, indent=2)
169
+ # print(json_output)
170
+
171
+ # ==========
172
+
173
+ from collections import defaultdict
174
+ import re
175
+
176
+ def parse_period(period):
177
+ """解析 period 字符串,返回 (year, type, quarter)"""
178
+ if period.startswith('FY'):
179
+ year = int(period[2:])
180
+ return year, 'FY', None
181
+ elif re.match(r'Q[1-4]-\d{4}', period):
182
+ q, year = period.split('-')
183
+ return int(year), 'Q', int(q[1])
184
+ else:
185
+ raise ValueError(f"Unknown period format: {period}")
186
+
187
+ def get_best_value_for_year(year_data, key):
188
+ """
189
+ year_data: dict like {'FY': value, 'Q1': val, 'Q2': val, ...}
190
+ 返回该财年该指标的最佳可用值(优先 FY,其次 Q4->Q3->Q2->Q1)
191
+ """
192
+ if year_data.get('FY') is not None:
193
+ return year_data['FY']
194
+ # 否则从 Q4 到 Q1 找第一个非 None
195
+ for q in ['Q4', 'Q3', 'Q2', 'Q1']:
196
+ if year_data.get(q) is not None:
197
+ return year_data[q]
198
+ return None
199
+ # def get_yearly_data(data_json):
200
+ # metrics_list = data_json['metrics']
201
+
202
+ # # 按年份组织数据:year -> { 'FY': {...}, 'Q1': {...}, ... }
203
+ # yearly_data = "N/A"
204
+
205
+ # for metric in metrics_list:
206
+ # period = metric['period']
207
+ # year, ptype, quarter = parse_period(period)
208
+ # if ptype == 'FY':
209
+ # yearly_data = f"{year} {ptype}"
210
+ # else:
211
+ # yearly_data = f"{year} {ptype} Q{quarter}"
212
+ # return yearly_data
213
+ import re
214
+
215
+ def parse_period_year_data(period):
216
+ """
217
+ 支持以下格式:
218
+ - FY2025
219
+ - Q1-2025
220
+ - 2025Q1 (新增支持)
221
+ """
222
+ if not isinstance(period, str):
223
+ return None, None, None
224
+
225
+ # 格式 1: FY2025
226
+ if period.startswith('FY'):
227
+ try:
228
+ year = int(period[2:])
229
+ return year, 'FY', None
230
+ except ValueError:
231
+ pass
232
+
233
+ # 格式 2: Q1-2025
234
+ match = re.match(r'Q([1-4])-(\d{4})', period)
235
+ if match:
236
+ quarter = int(match.group(1))
237
+ year = int(match.group(2))
238
+ return year, 'Q', quarter
239
+
240
+ # 格式 3: 2025Q1 (新增)
241
+ match = re.match(r'(\d{4})Q([1-4])', period)
242
+ if match:
243
+ year = int(match.group(1))
244
+ quarter = int(match.group(2))
245
+ return year, 'Q', quarter
246
+
247
+ # 无法解析
248
+ return None, None, None
249
+ def get_yearly_data(data_json):
250
+ metrics_list = data_json.get('metrics', [])
251
+ latest_desc = "N/A"
252
+
253
+ for metric in metrics_list:
254
+ period = metric.get('period')
255
+ if not period:
256
+ continue
257
+ year, ptype, quarter = parse_period_year_data(period)
258
+ if year is None:
259
+ continue # 跳过无法解析的
260
+
261
+ if ptype == 'FY':
262
+ desc = f"{year} FY"
263
+ else:
264
+ desc = f"{year} Q{quarter}"
265
+
266
+ # 简单认为列表顺序是时间顺序,最后一条最新
267
+ latest_desc = desc
268
+
269
+ return latest_desc
270
+ def parse_period_yoy(period):
271
+ """解析 period 为 (year, type, quarter)"""
272
+ if period.startswith('FY'):
273
+ year = int(period[2:])
274
+ return year, 'FY', None
275
+ elif re.match(r'Q[1-4]-\d{4}', period):
276
+ q_part, year_str = period.split('-')
277
+ return int(year_str), 'Q', int(q_part[1])
278
+ else:
279
+ # 忽略无法解析的 period
280
+ return None, None, None
281
+
282
+ def get_best_value_for_year_yoy(values_dict, key):
283
+ """
284
+ 从年度数据中获取指定指标的最佳值(优先 FY,其次 Q4 → Q1)
285
+ values_dict: {'FY': {...}, 'Q1': {...}, ...}
286
+ """
287
+ order = ['FY', 'Q4', 'Q3', 'Q2', 'Q1']
288
+ for q in order:
289
+ metric = values_dict.get(q)
290
+ if metric is not None and isinstance(metric, dict):
291
+ val = metric.get(key)
292
+ if val is not None:
293
+ return val
294
+ return None
295
+ import json
296
+ def calculate_yoy_comparison(data_json):
297
+ metrics_list = data_json.get('metrics', [])
298
+ if not metrics_list:
299
+ return []
300
+ if not isinstance(metrics_list, list):
301
+ return []
302
+ if not isinstance(metrics_list[0], dict):
303
+ return []
304
+ # 安全处理:确保每个 metric 是字典(防止双重 JSON 编码)
305
+ cleaned_metrics = []
306
+ for i, metric in enumerate(metrics_list):
307
+ if isinstance(metric, str):
308
+ try:
309
+ metric = json.loads(metric)
310
+ # metric = metric
311
+ except Exception as e:
312
+ raise ValueError(f"Failed to parse metrics[{i}] as JSON string: {metric}") from e
313
+ if not isinstance(metric, dict):
314
+ raise TypeError(f"metrics[{i}] is not a dictionary or valid JSON string. Type: {type(metric)}")
315
+ cleaned_metrics.append(metric)
316
+
317
+ # 按年份组织数据:year -> { 'FY': {...}, 'Q1': {...}, ... }
318
+ yearly_data = defaultdict(lambda: defaultdict(dict))
319
+
320
+ for metric in cleaned_metrics:
321
+ period = metric.get('period')
322
+ if not period:
323
+ continue # 跳过没有 period 的条目
324
+
325
+ year, ptype, quarter = parse_period_yoy(period)
326
+ if year is None:
327
+ continue # 跳过无法解析的 period
328
+
329
+ if ptype == 'FY':
330
+ yearly_data[year]['FY'] = metric
331
+ elif ptype == 'Q':
332
+ yearly_data[year][f'Q{quarter}'] = metric
333
+ # 否则忽略
334
+
335
+ # 获取所有年份并排序(最新在前)
336
+ years = sorted(yearly_data.keys(), reverse=True)
337
+ if len(years) < 2:
338
+ raise ValueError("至少需要两个财年的数据")
339
+
340
+ latest_year = years[0]
341
+ prev_year = years[1]
342
+
343
+ result = []
344
+ indicators = [
345
+ ("Total Revenue", "total_revenue"),
346
+ ("Net Income", "net_income"),
347
+ ("Earnings Per Share", "earnings_per_share"),
348
+ ("Operating Expenses", "operating_expenses"),
349
+ ("Cash Flow", "operating_cash_flow")
350
+ ]
351
+
352
+ def format_value(val):
353
+ if val is None:
354
+ return "N/A"
355
+ try:
356
+ val = float(val)
357
+ except (TypeError, ValueError):
358
+ return "N/A"
359
+ abs_val = abs(val)
360
+ if abs_val >= 1e9:
361
+ return f"${val / 1e9:.2f}B"
362
+ elif abs_val >= 1e6:
363
+ return f"${val / 1e6:.1f}M"
364
+ elif abs_val >= 1e3:
365
+ return f"${val / 1e3:.1f}K"
366
+ else:
367
+ return f"${val:.2f}"
368
+
369
+ for label, key in indicators:
370
+ # 获取本财年最佳值
371
+ current_val = get_best_value_for_year_yoy(yearly_data[latest_year], key)
372
+ # 获取去年财年最佳值
373
+ prev_val = get_best_value_for_year_yoy(yearly_data[prev_year], key)
374
+
375
+ if current_val is None or prev_val is None or prev_val == 0:
376
+ change_str = "N/A"
377
+ color = "N/A"
378
+ else:
379
+ try:
380
+ current_val = float(current_val)
381
+ prev_val = float(prev_val)
382
+ except (TypeError, ValueError):
383
+ change_str = "N/A"
384
+ color = "N/A"
385
+ else:
386
+ change_pct = (current_val - prev_val) / abs(prev_val) * 100
387
+ if change_pct > 0:
388
+ change_str = f"+{change_pct:.1f}%"
389
+ color = "green"
390
+ elif change_pct < 0:
391
+ change_str = f"{change_pct:.1f}%"
392
+ color = "red"
393
+ else:
394
+ change_str = "0.0%"
395
+ color = "N/A"
396
+
397
+ formatted_value = format_value(current_val)
398
+
399
+ result.append({
400
+ "label": label,
401
+ "value": formatted_value,
402
+ "change": change_str,
403
+ "color": color
404
+ })
405
+
406
+ return result
407
+ # def parse_period_yoy(period):
408
+ # """解析 period 为 (year, type, quarter)"""
409
+ # if period.startswith('FY'):
410
+ # year = int(period[2:])
411
+ # return year, 'FY', None
412
+ # elif re.match(r'Q[1-4]-\d{4}', period):
413
+ # q_part, year_str = period.split('-')
414
+ # return int(year_str), 'Q', int(q_part[1])
415
+ # else:
416
+ # # 忽略无法解析的 period
417
+ # return None, None, None
418
+ # def calculate_yoy_comparison(data_json):
419
+ # metrics_list = data_json['metrics']
420
+
421
+ # # 按年份组织数据:year -> { 'FY': {...}, 'Q1': {...}, ... }
422
+ # yearly_data = defaultdict(lambda: defaultdict(dict))
423
+
424
+ # for metric in metrics_list:
425
+ # period = metric['period']
426
+ # year, ptype, quarter = parse_period_yoy(period)
427
+ # if ptype == 'FY':
428
+ # yearly_data[year]['FY'] = metric
429
+ # else:
430
+ # yearly_data[year][f'Q{quarter}'] = metric
431
+
432
+ # # 获取所有年份并排序(最新在前)
433
+ # years = sorted(yearly_data.keys(), reverse=True)
434
+ # if len(years) < 2:
435
+ # raise ValueError("至少需要两个财年的数据")
436
+
437
+ # latest_year = years[0]
438
+ # prev_year = years[1]
439
+
440
+ # result = []
441
+ # indicators = [
442
+ # ("Total Revenue", "total_revenue"),
443
+ # ("Net Income", "net_income"),
444
+ # ("Earnings Per Share", "earnings_per_share"),
445
+ # ("Operating Expenses", "operating_expenses"),
446
+ # ("Cash Flow", "operating_cash_flow")
447
+ # ]
448
+
449
+ # def format_value(val):
450
+ # if val is None:
451
+ # return "N/A"
452
+ # abs_val = abs(val)
453
+ # if abs_val >= 1e9:
454
+ # return f"${val / 1e9:.2f}B"
455
+ # elif abs_val >= 1e6:
456
+ # return f"${val / 1e6:.1f}M"
457
+ # elif abs_val >= 1e3:
458
+ # return f"${val / 1e3:.1f}K"
459
+ # else:
460
+ # return f"${val:.2f}"
461
+
462
+ # for label, key in indicators:
463
+ # # 获取本财年最佳值
464
+ # current_val = get_best_value_for_year(
465
+ # {k: v.get(key) for k, v in yearly_data[latest_year].items()},
466
+ # key
467
+ # )
468
+ # # 获取去年财年最佳值
469
+ # prev_val = get_best_value_for_year(
470
+ # {k: v.get(key) for k, v in yearly_data[prev_year].items()},
471
+ # key
472
+ # )
473
+
474
+ # if current_val is None or prev_val is None or prev_val == 0:
475
+ # change_str = "N/A"
476
+ # color = "N/A"
477
+ # else:
478
+ # change_pct = (current_val - prev_val) / abs(prev_val) * 100
479
+ # if change_pct > 0:
480
+ # change_str = f"+{change_pct:.1f}%"
481
+ # color = "green"
482
+ # elif change_pct < 0:
483
+ # change_str = f"{change_pct:.1f}%"
484
+ # color = "red"
485
+ # else:
486
+ # change_str = "0.0%"
487
+ # color = "N/A"
488
+
489
+ # formatted_value = format_value(current_val)
490
+
491
+ # result.append({
492
+ # "label": label,
493
+ # "value": formatted_value,
494
+ # "change": change_str,
495
+ # "color": color
496
+ # })
497
+
498
+ # return result
499
+
500
+
501
+
502
+
503
+ import re
504
+ import json
505
+ from collections import defaultdict
506
+
507
+ def parse_period_three_year(period):
508
+ """解析 period 为 (year, type, quarter)"""
509
+ if period.startswith('FY'):
510
+ year = int(period[2:])
511
+ return year, 'FY', None
512
+ elif re.match(r'Q[1-4]-\d{4}', period):
513
+ q_part, year_str = period.split('-')
514
+ return int(year_str), 'Q', int(q_part[1])
515
+ else:
516
+ # 忽略无法解析的 period
517
+ return None, None, None
518
+
519
+ def extract_financial_table(data_json):
520
+ metrics_list = data_json.get('metrics', [])
521
+ if not metrics_list:
522
+ return []
523
+ if not isinstance(metrics_list, list):
524
+ return []
525
+ if not isinstance(metrics_list[0], dict):
526
+ return []
527
+ # === 安全清洗:确保每个 metric 是字典 ===
528
+ cleaned_metrics = []
529
+ for i, metric in enumerate(metrics_list):
530
+ if isinstance(metric, str):
531
+ try:
532
+ metric = json.loads(metric)
533
+ except Exception as e:
534
+ raise ValueError(f"Failed to parse metrics[{i}] as JSON string: {metric}") from e
535
+ if not isinstance(metric, dict):
536
+ raise TypeError(f"metrics[{i}] is not a dictionary or valid JSON string. Type: {type(metric)}")
537
+ cleaned_metrics.append(metric)
538
+
539
+ # 按年份组织所有报告:year -> { 'FY': metric_dict, 'Q1': ..., 'Q2': ... }
540
+ yearly_reports = defaultdict(dict)
541
+ all_years = set()
542
+
543
+ for metric in cleaned_metrics:
544
+ period = metric.get('period')
545
+ if not period:
546
+ continue # 跳过无 period 的条目
547
+
548
+ year, ptype, quarter = parse_period_three_year(period)
549
+ if year is None:
550
+ continue
551
+ all_years.add(year)
552
+ if ptype == 'FY':
553
+ yearly_reports[year]['FY'] = metric
554
+ elif ptype == 'Q':
555
+ yearly_reports[year][f'Q{quarter}'] = metric
556
+
557
+ if not all_years:
558
+ raise ValueError("未找到任何有效报告期")
559
+
560
+ # 取最近三个财年(倒序)
561
+ sorted_years = sorted(all_years, reverse=True)[:3]
562
+ # 补齐到3年(如果不足)
563
+ while len(sorted_years) < 3:
564
+ sorted_years.append(None)
565
+
566
+ # 为每个年份获取最佳值(优先 FY,其次 Q4→Q1)
567
+ def get_best_value(year, key):
568
+ if year is None:
569
+ return None
570
+ reports = yearly_reports.get(year, {})
571
+ # 确保 reports[q] 是 dict
572
+ fy_report = reports.get('FY')
573
+ if fy_report and isinstance(fy_report, dict):
574
+ fy_val = fy_report.get(key)
575
+ if fy_val is not None:
576
+ return fy_val
577
+ # 否则 Q4 → Q1
578
+ for q in ['Q4', 'Q3', 'Q2', 'Q1']:
579
+ q_report = reports.get(q)
580
+ if q_report and isinstance(q_report, dict):
581
+ q_val = q_report.get(key)
582
+ if q_val is not None:
583
+ return q_val
584
+ return None
585
+
586
+ # 指标定义
587
+ indicators = [
588
+ ("Total", "total_revenue"),
589
+ ("Net Income", "net_income"),
590
+ ("Earnings Per Share", "earnings_per_share"),
591
+ ("Operating Expenses", "operating_expenses"),
592
+ ("Cash Flow", "operating_cash_flow")
593
+ ]
594
+
595
+ # 格式化函数
596
+ def format_to_m(value):
597
+ if value is None:
598
+ return "N/A"
599
+ try:
600
+ val = float(value)
601
+ except (TypeError, ValueError):
602
+ return "N/A"
603
+ val_in_m = val / 1e6
604
+ if abs(val_in_m - round(val_in_m)) < 1e-6:
605
+ return f"{int(round(val_in_m))}M"
606
+ else:
607
+ return f"{val_in_m:.1f}M"
608
+
609
+ def format_eps(value):
610
+ if value is None:
611
+ return "N/A"
612
+ try:
613
+ val = float(value)
614
+ except (TypeError, ValueError):
615
+ return "N/A"
616
+ return f"{val:.2f}"
617
+
618
+ # 构建 list_data
619
+ header = ["Category"] + [f"{year}/FY" for year in sorted_years if year is not None]
620
+ list_data = [header]
621
+
622
+ for label, key in indicators:
623
+ row = [label]
624
+ for year in sorted_years:
625
+ if year is None:
626
+ row.append("N/A")
627
+ else:
628
+ val = get_best_value(year, key)
629
+ if label == "Earnings Per Share":
630
+ row.append(format_eps(val))
631
+ else:
632
+ row.append(format_to_m(val))
633
+ list_data.append(row)
634
+
635
+ # 构建 yoy_rates
636
+ valid_years = [y for y in sorted_years if y is not None]
637
+ yoy_header = ["Category"]
638
+ yoy_pairs = []
639
+
640
+ if len(valid_years) >= 2:
641
+ yoy_header.append(f"{valid_years[0]}/FY")
642
+ yoy_pairs.append((valid_years[0], valid_years[1]))
643
+ if len(valid_years) >= 3:
644
+ yoy_header.append(f"{valid_years[1]}/FY")
645
+ yoy_pairs.append((valid_years[1], valid_years[2]))
646
+
647
+ yoy_rates = [yoy_header]
648
+
649
+ for label, key in indicators:
650
+ row = [label]
651
+ for curr_y, prev_y in yoy_pairs:
652
+ curr_val = get_best_value(curr_y, key)
653
+ prev_val = get_best_value(prev_y, key)
654
+
655
+ if curr_val is None or prev_val is None or prev_val == 0:
656
+ row.append("N/A")
657
+ else:
658
+ try:
659
+ curr_val = float(curr_val)
660
+ prev_val = float(prev_val)
661
+ except (TypeError, ValueError):
662
+ row.append("N/A")
663
+ else:
664
+ pct = (curr_val - prev_val) / abs(prev_val) * 100
665
+ if pct >= 0:
666
+ row.append(f"+{pct:.2f}%")
667
+ else:
668
+ row.append(f"{pct:.2f}%")
669
+ yoy_rates.append(row)
670
+
671
+ return {
672
+ "list_data": list_data,
673
+ "yoy_rates": yoy_rates
674
+ }
service/three_year_table_tool.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def format_number_for_table(value):
2
+ """用于表格的数字格式化:自动 B/M,保留适当小数"""
3
+ if value is None or value == 0:
4
+ return "0"
5
+ if abs(value) >= 1_000_000_000:
6
+ num = value / 1_000_000_000
7
+ if num.is_integer():
8
+ return f"{int(num)}B"
9
+ else:
10
+ return f"{num:.2f}B".rstrip('0').rstrip('.')
11
+ elif abs(value) >= 1_000_000:
12
+ num = value / 1_000_000
13
+ if num.is_integer():
14
+ return f"{int(num)}M"
15
+ else:
16
+ return f"{num:.1f}M".rstrip('0').rstrip('.')
17
+ else:
18
+ return f"{value:,.1f}".rstrip('0').rstrip('.')
19
+
20
+ def safe_float_or_zero(val):
21
+ if val is None:
22
+ return 0.0
23
+ try:
24
+ return float(val)
25
+ except (ValueError, TypeError):
26
+ return 0.0
27
+
28
+ def calculate_yoy_rate(current, previous):
29
+ if previous == 0:
30
+ return "+0.0%" if current >= 0 else "-0.0%"
31
+ rate = (current - previous) / abs(previous) * 100
32
+ sign = "+" if rate >= 0 else "-"
33
+ return f"{sign}{abs(rate):.1f}%"
34
+
35
+ def build_table_format(three_year_data):
36
+ # 按 fiscal_year 降序排列(最新在前)
37
+ sorted_data = sorted(three_year_data, key=lambda x: x["fiscal_year"], reverse=True)
38
+
39
+ # 生成年份标签:如 "2025 FY" 或 "2025 Q3"
40
+ year_labels = [f"{item['fiscal_year']} {item['level']}" for item in sorted_data]
41
+
42
+ # 提取数值(确保至少三年,不足用 0 补齐)
43
+ while len(sorted_data) < 3:
44
+ sorted_data.append({
45
+ "fiscal_year": 0,
46
+ "level": "N/A",
47
+ "total_revenue": 0,
48
+ "net_income": 0,
49
+ "earnings_per_share": 0.0,
50
+ "operating_expenses": 0,
51
+ "operating_cash_flow": 0
52
+ })
53
+ year_labels.append("N/A")
54
+
55
+ # 取前三
56
+ y0, y1, y2 = sorted_data[0], sorted_data[1], sorted_data[2]
57
+
58
+ # 构建 list_data
59
+ list_data = [
60
+ ["Category"] + year_labels[:3],
61
+ ["Total Revenue",
62
+ format_number_for_table(safe_float_or_zero(y0["total_revenue"])),
63
+ format_number_for_table(safe_float_or_zero(y1["total_revenue"])),
64
+ format_number_for_table(safe_float_or_zero(y2["total_revenue"]))],
65
+ ["Net Income",
66
+ format_number_for_table(safe_float_or_zero(y0["net_income"])),
67
+ format_number_for_table(safe_float_or_zero(y1["net_income"])),
68
+ format_number_for_table(safe_float_or_zero(y2["net_income"]))],
69
+ ["Earnings Per Share",
70
+ f"{safe_float_or_zero(y0['earnings_per_share']):.2f}",
71
+ f"{safe_float_or_zero(y1['earnings_per_share']):.2f}",
72
+ f"{safe_float_or_zero(y2['earnings_per_share']):.2f}"],
73
+ ["Operating Expenses",
74
+ format_number_for_table(safe_float_or_zero(y0["operating_expenses"])),
75
+ format_number_for_table(safe_float_or_zero(y1["operating_expenses"])),
76
+ format_number_for_table(safe_float_or_zero(y2["operating_expenses"]))],
77
+ ["Operating Cash Flow",
78
+ format_number_for_table(safe_float_or_zero(y0["operating_cash_flow"])),
79
+ format_number_for_table(safe_float_or_zero(y1["operating_cash_flow"])),
80
+ format_number_for_table(safe_float_or_zero(y2["operating_cash_flow"]))]
81
+ ]
82
+
83
+ # 构建 yoy_rates(只有两列:2025 vs 2024,2024 vs 2023)
84
+ yoy_rates = [
85
+ ["Category"] + year_labels[:2], # 只取前两年标签
86
+ ["Total Revenue",
87
+ calculate_yoy_rate(safe_float_or_zero(y0["total_revenue"]), safe_float_or_zero(y1["total_revenue"])),
88
+ calculate_yoy_rate(safe_float_or_zero(y1["total_revenue"]), safe_float_or_zero(y2["total_revenue"]))],
89
+ ["Net Income",
90
+ calculate_yoy_rate(safe_float_or_zero(y0["net_income"]), safe_float_or_zero(y1["net_income"])),
91
+ calculate_yoy_rate(safe_float_or_zero(y1["net_income"]), safe_float_or_zero(y2["net_income"]))],
92
+ ["Earnings Per Share",
93
+ calculate_yoy_rate(safe_float_or_zero(y0["earnings_per_share"]), safe_float_or_zero(y1["earnings_per_share"])),
94
+ calculate_yoy_rate(safe_float_or_zero(y1["earnings_per_share"]), safe_float_or_zero(y2["earnings_per_share"]))],
95
+ ["Operating Expenses",
96
+ calculate_yoy_rate(safe_float_or_zero(y0["operating_expenses"]), safe_float_or_zero(y1["operating_expenses"])),
97
+ calculate_yoy_rate(safe_float_or_zero(y1["operating_expenses"]), safe_float_or_zero(y2["operating_expenses"]))],
98
+ ["Operating Cash Flow",
99
+ calculate_yoy_rate(safe_float_or_zero(y0["operating_cash_flow"]), safe_float_or_zero(y1["operating_cash_flow"])),
100
+ calculate_yoy_rate(safe_float_or_zero(y1["operating_cash_flow"]), safe_float_or_zero(y2["operating_cash_flow"]))]
101
+ ]
102
+
103
+ return {
104
+ "list_data": list_data,
105
+ "yoy_rates": yoy_rates
106
+ }
service/three_year_tool.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ # -----------------------------
4
+ # 辅助函数:安全转换数值
5
+ # -----------------------------
6
+ def safe_int(val, default=0):
7
+ if val is None:
8
+ return default
9
+ try:
10
+ return int(float(val))
11
+ except (ValueError, TypeError):
12
+ return default
13
+
14
+ def safe_float(val, default=0.0):
15
+ if val is None:
16
+ return default
17
+ try:
18
+ return float(val)
19
+ except (ValueError, TypeError):
20
+ return default
21
+
22
+ # -----------------------------
23
+ # 步骤1:提取带 fallback 的三年数据
24
+ # -----------------------------
25
+ def extract_last_three_with_fallback(data_list):
26
+ years = [2025, 2024, 2023]
27
+ priority_levels = [
28
+ ("FY", [f"FY{y}" for y in years]),
29
+ ("Q4", [f"{y}Q4" for y in years]),
30
+ ("Q3", [f"{y}Q3" for y in years]),
31
+ ("Q2", [f"{y}Q2" for y in years]),
32
+ ("Q1", [f"{y}Q1" for y in years]),
33
+ ]
34
+
35
+ data_map = {item["period"]: item for item in data_list if isinstance(item, dict) and "period" in item}
36
+
37
+ for level_name, periods in priority_levels:
38
+ records = []
39
+ valid = True
40
+ for period in periods:
41
+ item = data_map.get(period)
42
+ if not isinstance(item, dict) or item.get("total_revenue") is None:
43
+ valid = False
44
+ break
45
+ fiscal_year = int(period[:4]) if level_name != "FY" else int(period[2:])
46
+ records.append({
47
+ "period": period,
48
+ "fiscal_year": fiscal_year,
49
+ "level": level_name,
50
+ "total_revenue": item.get("total_revenue"),
51
+ "net_income": item.get("net_income"),
52
+ "earnings_per_share": item.get("earnings_per_share"),
53
+ "operating_expenses": item.get("operating_expenses"),
54
+ "operating_cash_flow": item.get("operating_cash_flow")
55
+ })
56
+ if valid:
57
+ return records
58
+
59
+ # Fallback: 返回第一个有数据的层级(即使不全)
60
+ for level_name, periods in priority_levels:
61
+ records = []
62
+ for period in periods:
63
+ item = data_map.get(period)
64
+ if isinstance(item, dict) and item.get("total_revenue") is not None:
65
+ fiscal_year = int(period[:4]) if level_name != "FY" else int(period[2:])
66
+ records.append({
67
+ "period": period,
68
+ "fiscal_year": fiscal_year,
69
+ "level": level_name,
70
+ "total_revenue": item.get("total_revenue"),
71
+ "net_income": item.get("net_income"),
72
+ "earnings_per_share": item.get("earnings_per_share"),
73
+ "operating_expenses": item.get("operating_expenses"),
74
+ "operating_cash_flow": item.get("operating_cash_flow")
75
+ })
76
+ if records:
77
+ return records
78
+ return []
79
+
80
+ # -----------------------------
81
+ # 步骤2:格式化数字(B / M)
82
+ # -----------------------------
83
+ def format_number(value):
84
+ if value >= 1_000_000_000:
85
+ num = value / 1_000_000_000
86
+ if num == int(num):
87
+ return f"${int(num)}B"
88
+ else:
89
+ return f"${num:.2f}B".rstrip('0').rstrip('.')
90
+ elif value >= 1_000_000:
91
+ num = value / 1_000_000
92
+ if num == int(num):
93
+ return f"${int(num)}M"
94
+ else:
95
+ return f"${num:.1f}M".rstrip('0').rstrip('.')
96
+ elif value >= 1_000:
97
+ return f"${value:,.0f}"
98
+ else:
99
+ return f"${value}"
100
+
101
+ def format_eps(value):
102
+ return f"${value:.2f}"
103
+
104
+ def calculate_change(current, previous):
105
+ if previous == 0:
106
+ return "+0.0%" if current >= 0 else "-0.0%"
107
+ change = (current - previous) / abs(previous) * 100
108
+ sign = "+" if change >= 0 else "-"
109
+ return f"{sign}{abs(change):.1f}%"
110
+
111
+ # -----------------------------
112
+ # 步骤3:构建最终 metrics
113
+ # -----------------------------
114
+ def build_financial_metrics(three_year_data):
115
+ if len(three_year_data) < 2:
116
+ raise ValueError("至少需要两年数据来计算同比变化")
117
+
118
+ sorted_data = sorted(three_year_data, key=lambda x: x["fiscal_year"], reverse=True)
119
+ latest = sorted_data[0]
120
+ previous = sorted_data[1]
121
+
122
+ rev_curr = safe_int(latest["total_revenue"])
123
+ rev_prev = safe_int(previous["total_revenue"])
124
+
125
+ net_curr = safe_int(latest["net_income"])
126
+ net_prev = safe_int(previous["net_income"])
127
+
128
+ eps_curr = safe_float(latest["earnings_per_share"])
129
+ eps_prev = safe_float(previous["earnings_per_share"])
130
+
131
+ opex_curr = safe_int(latest["operating_expenses"])
132
+ opex_prev = safe_int(previous["operating_expenses"])
133
+
134
+ cash_curr = safe_int(latest["operating_cash_flow"])
135
+ cash_prev = safe_int(previous["operating_cash_flow"])
136
+
137
+ return [
138
+ {
139
+ "label": "Total Revenue",
140
+ "value": format_number(rev_curr),
141
+ "change": calculate_change(rev_curr, rev_prev),
142
+ "color": "green" if rev_curr >= rev_prev else "red"
143
+ },
144
+ {
145
+ "label": "Net Income",
146
+ "value": format_number(net_curr),
147
+ "change": calculate_change(net_curr, net_prev),
148
+ "color": "green" if net_curr >= net_prev else "red"
149
+ },
150
+ {
151
+ "label": "Earnings Per Share",
152
+ "value": format_eps(eps_curr),
153
+ "change": calculate_change(eps_curr, eps_prev),
154
+ "color": "green" if eps_curr >= eps_prev else "red"
155
+ },
156
+ {
157
+ "label": "Operating Expenses",
158
+ "value": format_number(opex_curr),
159
+ "change": calculate_change(opex_curr, opex_prev),
160
+ "color": "green" if opex_curr >= opex_prev else "red"
161
+ },
162
+ {
163
+ "label": "Operating Cash Flow",
164
+ "value": format_number(cash_curr),
165
+ "change": calculate_change(cash_curr, cash_prev),
166
+ "color": "green" if cash_curr >= cash_prev else "red"
167
+ }
168
+ ]
169
+
170
+ # -----------------------------
171
+ # 主流程:输入 raw_data,输出 financial_metrics
172
+ # -----------------------------
173
+ # def process_financial_data(raw_data):
174
+ # # 如果是字符串,先解析 JSON
175
+ # if isinstance(raw_data, str):
176
+ # raw_data = json.loads(raw_data)
177
+
178
+ # # 确保是列表
179
+ # if not isinstance(raw_data, list):
180
+ # raise TypeError("raw_data 必须是列表或 JSON 字符串表示的列表")
181
+
182
+ # # 提取三年数据
183
+ # three_years = extract_last_three_with_fallback(raw_data)
184
+
185
+ # if not three_years:
186
+ # raise ValueError("无法提取有效的三年财务数据")
187
+
188
+ # # 构建指标
189
+ # return build_financial_metrics(three_years)
190
+
191
+ def process_financial_data_with_metadata(raw_data):
192
+ """
193
+ 返回包含 financial_metrics + year_data + three_year_data 的完整结果
194
+ """
195
+ return_value = {"financial_metrics": [], "year_data": "N/A", "three_year_data": []}
196
+ if not raw_data:
197
+ return return_value
198
+ if not isinstance(raw_data, list):
199
+ return return_value
200
+ if not isinstance(raw_data[0], dict):
201
+ return {"financial_metrics": [], "year_data": "N/A", "three_year_data": []}
202
+ # 1. 解析输入
203
+ if isinstance(raw_data, str):
204
+ raw_data = json.loads(raw_data)
205
+ if not isinstance(raw_data, list):
206
+ raise TypeError("raw_data 必须是列表或 JSON 字符串")
207
+
208
+ # 2. 提取三年数据(带 fallback)
209
+ three_years = extract_last_three_with_fallback(raw_data)
210
+ if not three_years:
211
+ print("无法提取有效的三年财务数据")
212
+ # raise ValueError("无法提取有效的三年财务数据")
213
+ return return_value
214
+
215
+ # 3. 获取最新年份和报告类型(用于 year_data)
216
+ latest = max(three_years, key=lambda x: x["fiscal_year"])
217
+ year = latest["fiscal_year"]
218
+ level = latest["level"]
219
+
220
+ if level == "FY":
221
+ year_data = f"{year} FY"
222
+ else: # Q1, Q2, Q3, Q4
223
+ year_data = f"{year} {level}"
224
+
225
+ # 4. 构建指标
226
+ financial_metrics = build_financial_metrics(three_years)
227
+
228
+ # 5. 返回完整结构
229
+ return {
230
+ "financial_metrics": financial_metrics,
231
+ "year_data": year_data,
232
+ "three_year_data": three_years # 已包含 period, fiscal_year, level, 各项财务字段
233
+ }
234
+
235
+ # -----------------------------
236
+ # 示例使用(替换为你的真实数据)
237
+ # -----------------------------
238
+ # if __name__ == "__main__":
239
+ # # 👇 在这里粘贴你的原始数据(可以是字符串或变量)
240
+ # # 示例:从文件读取或直接赋值
241
+ # with open("financial_data.json", "r", encoding="utf-8") as f:
242
+ # raw_input = f.read() # 或者直接赋值为你的数据变量
243
+
244
+ # # 处理
245
+ # try:
246
+ # financial_metrics = process_financial_data(raw_input)
247
+ # print(json.dumps(financial_metrics, indent=2))
248
+ # except Exception as e:
249
+ # print("处理失败:", e)