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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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)
|