aria-code 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,377 @@
1
+ """Report command parsing and prompt builders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ import logging
9
+ from pathlib import Path
10
+ import re
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ReportArgs:
18
+ symbol: str = "AAPL"
19
+ fmt: str = "html"
20
+ report_type: str = "standard"
21
+ export_pdf: bool = False
22
+ output_dir: Path | None = None
23
+
24
+ @property
25
+ def is_markdown(self) -> bool:
26
+ return self.fmt in ("md", "markdown")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SavedMarkdownReport:
31
+ path: Path
32
+ metadata_path: Path | None = None
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class GeneratedHtmlReport:
37
+ path: Path
38
+ team_result: Any = None
39
+ agent_names: tuple[str, ...] = ()
40
+
41
+
42
+ def report_agent_names(report_type: str) -> list[str]:
43
+ if report_type == "deep":
44
+ return ["macro", "fundamental", "technical", "risk", "news", "catalyst", "sector"]
45
+ return ["macro", "fundamental", "technical", "risk"]
46
+
47
+
48
+ def report_file_size_kb(path: Path) -> int:
49
+ return max(1, path.stat().st_size // 1024)
50
+
51
+
52
+ def all_agents_failed(team_result: Any) -> bool:
53
+ results = getattr(team_result, "results", None)
54
+ if not results:
55
+ return False
56
+ non_synthesis = [result for result in results if getattr(result, "agent", None) != "synthesis"]
57
+ if not non_synthesis:
58
+ return False
59
+ return all(not getattr(result, "success", False) for result in non_synthesis)
60
+
61
+
62
+ async def export_report_pdf(report_path: Path) -> Path | None:
63
+ from report_generator import export_pdf
64
+
65
+ return await asyncio.get_event_loop().run_in_executor(
66
+ None,
67
+ lambda: export_pdf(report_path),
68
+ )
69
+
70
+
71
+ async def update_report_index(report_dir: Path) -> Path | None:
72
+ from report_generator import update_reports_index
73
+
74
+ return await asyncio.get_event_loop().run_in_executor(
75
+ None,
76
+ lambda: update_reports_index(report_dir),
77
+ )
78
+
79
+
80
+ def build_report_llm_provider(config: dict[str, Any]) -> Any:
81
+ try:
82
+ from providers.llm.base import ProviderConfig
83
+ from providers.llm.ollama import OllamaProvider
84
+
85
+ model = config.get("model", "qwen2.5:7b")
86
+ url = config.get("ollama_url", "http://localhost:11434")
87
+ return OllamaProvider(ProviderConfig(name="ollama", model=model, base_url=url))
88
+ except Exception as exc:
89
+ logger.debug("[report] llm provider unavailable: %s", exc)
90
+ return None
91
+
92
+
93
+ async def run_report_agents(
94
+ *,
95
+ symbol: str,
96
+ report_type: str,
97
+ config: dict[str, Any],
98
+ ) -> Any:
99
+ from agents.team import run_team
100
+ from datasources.router import get_router
101
+
102
+ agent_names = report_agent_names(report_type)
103
+ llm_provider = build_report_llm_provider(config)
104
+ noisy_loggers = ["agents.base", "datasources.router", "data_cleaner"]
105
+ saved_levels = {name: logging.getLogger(name).level for name in noisy_loggers}
106
+
107
+ def suppress_token_stdout(_token: str) -> None:
108
+ return None
109
+
110
+ for name in noisy_loggers:
111
+ logging.getLogger(name).setLevel(logging.ERROR)
112
+ try:
113
+ return await run_team(
114
+ symbol=symbol,
115
+ agents=agent_names,
116
+ llm_provider=llm_provider,
117
+ data_router=get_router(),
118
+ on_token=suppress_token_stdout,
119
+ )
120
+ finally:
121
+ for name, level in saved_levels.items():
122
+ logging.getLogger(name).setLevel(level or logging.NOTSET)
123
+
124
+
125
+ async def generate_html_report(
126
+ *,
127
+ symbol: str,
128
+ report_type: str,
129
+ output_dir: Path | None,
130
+ config: dict[str, Any],
131
+ ) -> GeneratedHtmlReport:
132
+ from report_generator import generate_report
133
+
134
+ agent_names = report_agent_names(report_type)
135
+ team_result = None
136
+ try:
137
+ team_result = await run_report_agents(
138
+ symbol=symbol,
139
+ report_type=report_type,
140
+ config=config,
141
+ )
142
+ except Exception as exc:
143
+ logger.debug("[report] team analysis failed: %s", exc)
144
+
145
+ path = await generate_report(
146
+ symbol=symbol,
147
+ team_result=team_result,
148
+ output_dir=output_dir,
149
+ )
150
+ return GeneratedHtmlReport(
151
+ path=path,
152
+ team_result=team_result,
153
+ agent_names=tuple(agent_names),
154
+ )
155
+
156
+
157
+ def parse_report_args(args: str) -> ReportArgs:
158
+ parts = args.split()
159
+ symbol = "AAPL"
160
+ fmt = "html"
161
+ report_type = "standard"
162
+ export_pdf = False
163
+ output_dir_arg = None
164
+ skip_next = False
165
+
166
+ for idx, part in enumerate(parts):
167
+ if skip_next:
168
+ skip_next = False
169
+ continue
170
+ if part.startswith("--format="):
171
+ fmt = part.split("=", 1)[1].lower()
172
+ elif part == "--format" and idx + 1 < len(parts):
173
+ fmt = parts[idx + 1].lower()
174
+ skip_next = True
175
+ elif part.startswith("--type="):
176
+ report_type = part.split("=", 1)[1].lower()
177
+ elif part == "--type" and idx + 1 < len(parts):
178
+ report_type = parts[idx + 1].lower()
179
+ skip_next = True
180
+ elif part == "--pdf":
181
+ export_pdf = True
182
+ elif part.startswith("--output="):
183
+ output_dir_arg = part.split("=", 1)[1]
184
+ elif part == "--output" and idx + 1 < len(parts):
185
+ output_dir_arg = parts[idx + 1]
186
+ skip_next = True
187
+ elif not part.startswith("-"):
188
+ symbol = part.upper()
189
+
190
+ output_dir = Path(output_dir_arg).expanduser() if output_dir_arg else None
191
+ return ReportArgs(
192
+ symbol=symbol,
193
+ fmt=fmt,
194
+ report_type=report_type,
195
+ export_pdf=export_pdf,
196
+ output_dir=output_dir,
197
+ )
198
+
199
+
200
+ def _display_value(value: Any, digits: int = 2, suffix: str = "") -> str:
201
+ try:
202
+ if value in (None, "", "N/A", "-", "nan"):
203
+ return "-"
204
+ if isinstance(value, (int, float)):
205
+ return f"{float(value):,.{digits}f}{suffix}"
206
+ return str(value)
207
+ except Exception:
208
+ return "-"
209
+
210
+
211
+ def metric_line(label: str, value: Any, digits: int = 2, suffix: str = "") -> str:
212
+ rendered = _display_value(value, digits=digits, suffix=suffix)
213
+ return f"- {label}: {rendered}" if rendered != "-" else ""
214
+
215
+
216
+ def markdown_data_block(market_data: dict[str, Any]) -> str:
217
+ data_lines = [
218
+ metric_line("当前价", market_data.get("price")),
219
+ metric_line("涨跌", market_data.get("change_pct"), suffix="%"),
220
+ metric_line("RSI(14)", market_data.get("rsi")),
221
+ metric_line("MACD", market_data.get("macd"), digits=4),
222
+ metric_line("MA20", market_data.get("ma20")),
223
+ metric_line("MA60", market_data.get("ma60")),
224
+ metric_line("布林上轨", market_data.get("bb_upper")),
225
+ metric_line("布林下轨", market_data.get("bb_lower")),
226
+ ]
227
+ data_block = "\n".join(line for line in data_lines if line)
228
+ if data_block:
229
+ return data_block
230
+ return "- 实时行情数据暂不可用;报告必须明确说明数据限制,不得编造价格或指标。"
231
+
232
+
233
+ def markdown_provenance_block(data_quality: dict[str, Any], data_bundle: Any = None) -> str:
234
+ provider_chain = (
235
+ data_quality.get("providers")
236
+ or getattr(data_bundle, "provider_chain", [])
237
+ or []
238
+ )
239
+ missing_fields = (
240
+ getattr(data_bundle, "missing_fields", [])
241
+ or data_quality.get("missing_fields")
242
+ or []
243
+ )
244
+ lines = [
245
+ f"- 数据状态: {data_quality.get('status', getattr(data_bundle, 'status', 'unknown') if data_bundle else 'unknown')}",
246
+ f"- 是否过期: {'yes' if data_quality.get('stale') else 'no'}",
247
+ f"- 数据源链: {', '.join(provider_chain) if provider_chain else 'unknown'}",
248
+ f"- 缺失字段: {', '.join(missing_fields) if missing_fields else 'none'}",
249
+ ]
250
+ return "\n".join(lines)
251
+
252
+
253
+ def report_depth_description(report_type: str) -> str:
254
+ if report_type == "deep":
255
+ return "深度(8页)版本:包含估值模型(DCF + 相对估值)、财务分析(3年P&L)、管理层分析、行业竞争格局"
256
+ if report_type == "brief":
257
+ return "简评版本:1页,核心观点 + 关键数据 + 1句话结论"
258
+ return "标准版本:封面、技术分析、基本面概览、风险因素"
259
+
260
+
261
+ def build_markdown_report_prompt(
262
+ *,
263
+ symbol: str,
264
+ report_type: str,
265
+ market_data: dict[str, Any],
266
+ data_quality: dict[str, Any],
267
+ data_bundle: Any = None,
268
+ now: datetime | None = None,
269
+ ) -> str:
270
+ report_date = (now or datetime.now()).strftime("%Y-%m-%d")
271
+ data_block = markdown_data_block(market_data)
272
+ provenance_block = markdown_provenance_block(data_quality, data_bundle)
273
+ depth = report_depth_description(report_type)
274
+ return (
275
+ f"为 {symbol} 生成一份专业 Markdown 投研报告({depth})。\n\n"
276
+ f"**实时数据(仅使用下列已返回字段;缺失字段不要补写)**:\n"
277
+ f"{data_block}\n\n"
278
+ f"**数据质量(必须在报告中如实说明)**:\n"
279
+ f"{provenance_block}\n\n"
280
+ f"报告结构(Markdown):\n"
281
+ f"# {symbol} 投资研究报告\n"
282
+ f"**评级**: 买入/中性/减持 **目标价**: X.XX **日期**: {report_date}\n\n"
283
+ f"## 核心观点\n"
284
+ f"## 技术面分析\n"
285
+ f"## 基本面概况\n"
286
+ f"## 风险因素\n"
287
+ f"## 投资建议\n\n"
288
+ f"请用真实数据,不要使用占位符;缺失数据请说明限制,用中文输出。"
289
+ )
290
+
291
+
292
+ def clean_markdown_report_response(text: str) -> str:
293
+ """Remove injected market-data blocks from model output before saving."""
294
+
295
+ return re.sub(r"\n*## 📊.*?(?=\n#|\Z)", "", text, flags=re.DOTALL).strip()
296
+
297
+
298
+ def _missing_market_fields(market_data: dict[str, Any], data_bundle: Any = None) -> list[str]:
299
+ bundle_missing = getattr(data_bundle, "missing_fields", None) if data_bundle else None
300
+ if bundle_missing:
301
+ return list(bundle_missing)
302
+ return [
303
+ key
304
+ for key in ("price", "change_pct", "rsi", "macd", "ma20", "ma60")
305
+ if market_data.get(key) in (None, "", 0)
306
+ ]
307
+
308
+
309
+ def _provider_chain(market_data: dict[str, Any], data_quality: dict[str, Any], data_bundle: Any = None) -> list[Any]:
310
+ chain = (
311
+ data_quality.get("providers")
312
+ or getattr(data_bundle, "provider_chain", [])
313
+ or market_data.get("provider_chain")
314
+ or market_data.get("data_provider")
315
+ or []
316
+ )
317
+ return chain if isinstance(chain, list) else [chain]
318
+
319
+
320
+ def save_markdown_report(
321
+ *,
322
+ symbol: str,
323
+ report_type: str,
324
+ markdown_text: str,
325
+ timestamp: str,
326
+ output_dir: Path | None,
327
+ market_data: dict[str, Any],
328
+ data_quality: dict[str, Any],
329
+ data_bundle: Any = None,
330
+ created_at: datetime | None = None,
331
+ ) -> SavedMarkdownReport:
332
+ """Persist a Markdown report and sidecar metadata when using artifact storage."""
333
+
334
+ created = created_at or datetime.now()
335
+ if output_dir:
336
+ output_dir.mkdir(parents=True, exist_ok=True)
337
+ out_file = output_dir / f"{symbol}_report_{timestamp}.md"
338
+ out_file.write_text(clean_markdown_report_response(markdown_text), encoding="utf-8")
339
+ return SavedMarkdownReport(path=out_file)
340
+
341
+ from artifacts import create_user_artifact, write_artifact_metadata, write_artifact_raw_data
342
+
343
+ artifact = create_user_artifact("reports/market", symbol, f"{symbol}_market_report", ".md")
344
+ out_file = artifact.path
345
+ out_file.write_text(clean_markdown_report_response(markdown_text), encoding="utf-8")
346
+
347
+ write_artifact_metadata(artifact, {
348
+ "kind": "market_report",
349
+ "format": "markdown",
350
+ "status": "partial" if market_data else "data_unavailable",
351
+ "symbol": symbol,
352
+ "created_at": created.isoformat(timespec="seconds"),
353
+ "data": {
354
+ "provider_chain": _provider_chain(market_data, data_quality, data_bundle),
355
+ "warnings": data_quality.get("warnings") or getattr(data_bundle, "warnings", []) or [],
356
+ "errors": data_quality.get("errors") or getattr(data_bundle, "errors", []) or [],
357
+ "stale": bool(data_quality.get("stale", False)),
358
+ "quality": data_quality,
359
+ "missing_fields": _missing_market_fields(market_data, data_bundle),
360
+ },
361
+ "report": {
362
+ "type": report_type,
363
+ "metadata_path": str(artifact.metadata_path),
364
+ },
365
+ })
366
+ write_artifact_raw_data(artifact, {
367
+ "symbol": symbol,
368
+ "market_data": market_data,
369
+ "data_bundle": {
370
+ "quote": getattr(data_bundle, "quote", {}) if data_bundle else {},
371
+ "history": getattr(data_bundle, "history", {}) if data_bundle else {},
372
+ "fundamentals": getattr(data_bundle, "fundamentals", {}) if data_bundle else {},
373
+ "technical": getattr(data_bundle, "technical", {}) if data_bundle else {},
374
+ "quality": data_quality,
375
+ },
376
+ })
377
+ return SavedMarkdownReport(path=out_file, metadata_path=artifact.metadata_path)