aria-code 0.167.0__py3-none-win_amd64.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.
- aria_bots/__init__.py +7 -0
- aria_bots/channels/README.md +97 -0
- aria_bots/channels/__init__.py +1 -0
- aria_bots/channels/approvals.py +263 -0
- aria_bots/channels/conversation.py +280 -0
- aria_bots/channels/digest.py +186 -0
- aria_bots/channels/registry.py +95 -0
- aria_bots/engine.py +144 -0
- aria_bots/feishu_bot.py +1385 -0
- aria_bots/logistics/__init__.py +1 -0
- aria_bots/logistics/carriers.py +253 -0
- aria_bots/logistics/freight.py +154 -0
- aria_bots/logistics/inventory.py +242 -0
- aria_bots/logistics/mcp_server.py +156 -0
- aria_bots/logistics/records.py +119 -0
- aria_bots/logistics/tenancy.py +152 -0
- aria_bots/market.py +91 -0
- aria_bots/relay_client.py +326 -0
- aria_bots/telegram_bot.py +299 -0
- aria_code/__init__.py +111 -0
- aria_code/_bin/aria-native.exe +0 -0
- aria_code/_bin/aria-runtime.exe +0 -0
- aria_code/_version.py +20 -0
- aria_code/account_memory.py +296 -0
- aria_code/account_session.py +166 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +49 -0
- aria_code/agents/base.py +329 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +315 -0
- aria_code/agents/financial/fundamental.py +178 -0
- aria_code/agents/financial/macro.py +111 -0
- aria_code/agents/financial/news.py +231 -0
- aria_code/agents/financial/risk.py +146 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +277 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +392 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +168 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +500 -0
- aria_code/agents/video/__init__.py +16 -0
- aria_code/agents/video/director.py +376 -0
- aria_code/agents/video/qa_agent.py +143 -0
- aria_code/agents/video/script_doctor.py +112 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/navigation.schema.json +110 -0
- aria_code/agents/warehouse/task_agent.py +172 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +9 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/approvals.py +11 -0
- aria_code/apps/channels/conversation.py +11 -0
- aria_code/apps/channels/digest.py +11 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +11 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/app_server.py +656 -0
- aria_code/apps/cli/auto_update.py +125 -0
- aria_code/apps/cli/bootstrap.py +206 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/chat_turn.py +1327 -0
- aria_code/apps/cli/cloud_models.py +231 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +34 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +378 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +215 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
- aria_code/apps/cli/commands/broker_cmds.py +1236 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +189 -0
- aria_code/apps/cli/commands/core_cmds.py +2748 -0
- aria_code/apps/cli/commands/data_cmds.py +521 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/engineering_cmds.py +399 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +544 -0
- aria_code/apps/cli/commands/market_cmds.py +1419 -0
- aria_code/apps/cli/commands/market_context.py +432 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2374 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
- aria_code/apps/cli/commands/report.py +476 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +723 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
- aria_code/apps/cli/commands/workflow_cmds.py +629 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +158 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/conversation_policy.py +53 -0
- aria_code/apps/cli/deterministic.py +200 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/earnings_view.py +172 -0
- aria_code/apps/cli/endpoint_migration.py +64 -0
- aria_code/apps/cli/exec_events.py +255 -0
- aria_code/apps/cli/file_references.py +155 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +239 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
- aria_code/apps/cli/handlers/market_handlers.py +2570 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/headless.py +352 -0
- aria_code/apps/cli/helpers.py +326 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +310 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +305 -0
- aria_code/apps/cli/intent_shadow.py +72 -0
- aria_code/apps/cli/intent_signals.py +347 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +550 -0
- aria_code/apps/cli/main.py +39 -0
- aria_code/apps/cli/market_hours.py +100 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/mixin_binding.py +32 -0
- aria_code/apps/cli/model_catalog.py +652 -0
- aria_code/apps/cli/model_probe.py +169 -0
- aria_code/apps/cli/model_retirement.py +65 -0
- aria_code/apps/cli/native_branding.py +12 -0
- aria_code/apps/cli/native_bridge.py +118 -0
- aria_code/apps/cli/native_entry.py +58 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +78 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +71 -0
- aria_code/apps/cli/prompts/select.py +193 -0
- aria_code/apps/cli/prompts/system_prompts.py +562 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +775 -0
- aria_code/apps/cli/providers/chat_routing.py +139 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
- aria_code/apps/cli/providers/runtime_bridge.py +586 -0
- aria_code/apps/cli/providers/vertexai_stream.py +592 -0
- aria_code/apps/cli/release_download.py +159 -0
- aria_code/apps/cli/review_runner.py +111 -0
- aria_code/apps/cli/runtime_consumer.py +769 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_refresh.py +123 -0
- aria_code/apps/cli/session_store.py +125 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/task_isolation.py +211 -0
- aria_code/apps/cli/terminal_chrome.py +412 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +932 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +113 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +181 -0
- aria_code/apps/cli/tools/market_tools.py +626 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +656 -0
- aria_code/apps/cli/tools/write_tools.py +961 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/transactions.py +269 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +257 -0
- aria_code/apps/cli/updater.py +373 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1663 -0
- aria_code/apps/cli/web_login.py +136 -0
- aria_code/apps/cli/workflow_runner.py +127 -0
- aria_code/apps/cli/workspace_route.py +121 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +5244 -0
- aria_code/aria_daemon.py +1372 -0
- aria_code/aria_feishu_bot.py +16 -0
- aria_code/aria_mcp_server.py +61 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_telegram_bot.py +11 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +16 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1985 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +190 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_server.py +276 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sec_statements.py +217 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/doctor.py +860 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/_finance_core/__init__.py +5 -0
- aria_code/domain/_finance_core/backtest_engine.py +325 -0
- aria_code/domain/_finance_core/performance_stats.py +101 -0
- aria_code/domain/backtest_engine.py +75 -0
- aria_code/domain/backtest_report.py +488 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/performance_stats.py +6 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/behavior.py +113 -0
- aria_code/evals/gap.py +93 -0
- aria_code/evals/harness.py +949 -0
- aria_code/evals/review_probe.py +132 -0
- aria_code/evals/runner.py +425 -0
- aria_code/evals/scorecard.py +322 -0
- aria_code/evals/summary.py +125 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +459 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +744 -0
- aria_code/memory_manager.py +273 -0
- aria_code/model_capability.py +512 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +26 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +58 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +232 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +162 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1705 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +393 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +75 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +304 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +137 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +160 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/_paths.py +92 -0
- aria_code/packages/aria_tools/registry.py +129 -0
- aria_code/packages/quant_engine/__init__.py +33 -0
- aria_code/packages/quant_engine/_namespace.py +24 -0
- aria_code/packages/quant_engine/backtest/__init__.py +10 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +102 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +76 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/healthcare/__init__.py +40 -0
- aria_code/packs/logistics/__init__.py +171 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/google_video_provider.py +256 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/arthera_gateway.py +90 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +137 -0
- aria_code/providers/llm/openai_compat.py +350 -0
- aria_code/providers/llm/registry.py +457 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +549 -0
- aria_code/providers/ollama_models.py +79 -0
- aria_code/providers/openai_params.py +32 -0
- aria_code/providers/tool_messages.py +100 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/review_service.py +497 -0
- aria_code/runtime/__init__.py +176 -0
- aria_code/runtime/acceptance.py +632 -0
- aria_code/runtime/agent_loop.py +1851 -0
- aria_code/runtime/agent_profiles.py +75 -0
- aria_code/runtime/approval.py +71 -0
- aria_code/runtime/baseline.py +219 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +481 -0
- aria_code/runtime/consult.py +65 -0
- aria_code/runtime/contract.py +282 -0
- aria_code/runtime/delivery.py +432 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +167 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/native_index.py +153 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/processes.py +343 -0
- aria_code/runtime/project_graph.py +614 -0
- aria_code/runtime/repo_map.py +712 -0
- aria_code/runtime/research_bundle.py +105 -0
- aria_code/runtime/review.py +269 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +522 -0
- aria_code/runtime/scoped_runs.py +113 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/semantic_diff.py +171 -0
- aria_code/runtime/subagent.py +721 -0
- aria_code/runtime/symbol_edit.py +117 -0
- aria_code/runtime/task_brief.py +46 -0
- aria_code/runtime/task_context.py +145 -0
- aria_code/runtime/task_ledger.py +108 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/task_outcome.py +33 -0
- aria_code/runtime/task_verification.py +266 -0
- aria_code/runtime/task_worktree.py +448 -0
- aria_code/runtime/tool_executor.py +329 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/transactions.py +216 -0
- aria_code/runtime/windows_job.py +115 -0
- aria_code/runtime/workflows.py +222 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +27 -0
- aria_code/safety/client_isolation.py +178 -0
- aria_code/safety/permissions.py +339 -0
- aria_code/safety/risk.py +699 -0
- aria_code/safety/sandbox.py +148 -0
- aria_code/safety/service.py +102 -0
- aria_code/service_endpoints.py +14 -0
- aria_code/setup_wizard.py +684 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +509 -0
- aria_code/tools/data_analysis_tools.py +831 -0
- aria_code/tools/enterprise_finance_tools.py +202 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +880 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3488 -0
- aria_code/tools/logistics_carriers.py +11 -0
- aria_code/tools/logistics_inventory.py +11 -0
- aria_code/tools/logistics_io.py +11 -0
- aria_code/tools/logistics_tenancy.py +11 -0
- aria_code/tools/logistics_tools.py +11 -0
- aria_code/tools/macro_tools.py +412 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +662 -0
- aria_code/tools/spreadsheet_tools.py +399 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/tools/video_tools.py +500 -0
- aria_code/training/__init__.py +1 -0
- aria_code/training/compare.py +117 -0
- aria_code/training/prelude_commands.py +111 -0
- aria_code/training/prelude_eval.py +390 -0
- aria_code/training/prelude_real.py +183 -0
- aria_code/training/prelude_train.py +248 -0
- aria_code/training/sft_dataset.py +230 -0
- aria_code/training/trajectory_dataset.py +263 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +404 -0
- aria_code/ui/completer.py +571 -0
- aria_code/ui/console.py +285 -0
- aria_code/ui/input_box.py +505 -0
- aria_code/ui/mentions.py +206 -0
- aria_code/ui/picker.py +248 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/actions.py +297 -0
- aria_code/ui/render/finance.py +1732 -0
- aria_code/ui/render/market.py +253 -0
- aria_code/ui/render/output.py +954 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +367 -0
- aria_code/ui/robot.py +239 -0
- aria_code/ui/startup_dashboard.py +110 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +518 -0
- aria_code/video_workflow/__init__.py +8 -0
- aria_code/video_workflow/__main__.py +61 -0
- aria_code/video_workflow/engines/__init__.py +18 -0
- aria_code/video_workflow/engines/base.py +69 -0
- aria_code/video_workflow/engines/custom_model.py +204 -0
- aria_code/video_workflow/engines/google_veo.py +94 -0
- aria_code/video_workflow/engines/local_engine.py +71 -0
- aria_code/video_workflow/engines/registry.py +98 -0
- aria_code/video_workflow/google_models.py +278 -0
- aria_code/video_workflow/music/__init__.py +5 -0
- aria_code/video_workflow/music/foley_service.py +132 -0
- aria_code/video_workflow/music/service.py +212 -0
- aria_code/video_workflow/pipeline.py +620 -0
- aria_code/video_workflow/plan.py +202 -0
- aria_code/video_workflow/session.py +297 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +219 -0
- aria_code/workspace/verify.py +167 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.167.0.dist-info/METADATA +330 -0
- aria_code-0.167.0.dist-info/RECORD +601 -0
- aria_code-0.167.0.dist-info/WHEEL +5 -0
- aria_code-0.167.0.dist-info/entry_points.txt +5 -0
- aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.167.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PortfolioCommandsMixin — Portfolio commands: journal, report, portfolio, apply_plan, team.
|
|
3
|
+
|
|
4
|
+
Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
|
|
5
|
+
by _rebind_mixin_globals() called at module load time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from aria_code.ui.render.output import file_uri as _file_uri
|
|
11
|
+
|
|
12
|
+
from aria_code.apps.cli.i18n import ui_text
|
|
13
|
+
|
|
14
|
+
from ._ui import Panel, print_error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _detect_lang_for_team(text: str) -> str:
|
|
18
|
+
if not text:
|
|
19
|
+
return "zh"
|
|
20
|
+
zh_chars = sum(1 for c in text if '一' <= c <= '鿿')
|
|
21
|
+
return "zh" if zh_chars / max(len(text), 1) > 0.15 else "en"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import json
|
|
26
|
+
import asyncio
|
|
27
|
+
import datetime
|
|
28
|
+
import time
|
|
29
|
+
import shlex
|
|
30
|
+
from typing import Dict, Any, Optional
|
|
31
|
+
|
|
32
|
+
def parse_team_args(*args, **kwargs):
|
|
33
|
+
from aria_cli import parse_team_args as fn
|
|
34
|
+
return fn(*args, **kwargs)
|
|
35
|
+
def resolve_team_symbols(*args, **kwargs):
|
|
36
|
+
from aria_cli import resolve_team_symbols as fn
|
|
37
|
+
return fn(*args, **kwargs)
|
|
38
|
+
def team_agent_names(*args, **kwargs):
|
|
39
|
+
from aria_cli import team_agent_names as fn
|
|
40
|
+
return fn(*args, **kwargs)
|
|
41
|
+
# execute_aria_tool lives in apps/cli/tool_executor.py and needs nothing from
|
|
42
|
+
# aria_cli — the cache it reads is owned there now, so the hop was pointless.
|
|
43
|
+
from aria_code.apps.cli.tool_executor import execute_aria_tool
|
|
44
|
+
def export_report_pdf(*args, **kwargs):
|
|
45
|
+
from aria_cli import export_report_pdf as fn
|
|
46
|
+
return fn(*args, **kwargs)
|
|
47
|
+
def run_team_analysis(*args, **kwargs):
|
|
48
|
+
from aria_cli import run_team_analysis as fn
|
|
49
|
+
return fn(*args, **kwargs)
|
|
50
|
+
def save_markdown_report(*args, **kwargs):
|
|
51
|
+
from aria_cli import save_markdown_report as fn
|
|
52
|
+
return fn(*args, **kwargs)
|
|
53
|
+
def all_agents_failed(*args, **kwargs):
|
|
54
|
+
from aria_cli import all_agents_failed as fn
|
|
55
|
+
return fn(*args, **kwargs)
|
|
56
|
+
def _get_Panel():
|
|
57
|
+
# Resolved locally now — see ._ui; no aria_cli round-trip.
|
|
58
|
+
return Panel
|
|
59
|
+
def save_team_report(*args, **kwargs):
|
|
60
|
+
from aria_cli import save_team_report as fn
|
|
61
|
+
return fn(*args, **kwargs)
|
|
62
|
+
def _print_verdict_banner(*args, **kwargs):
|
|
63
|
+
from aria_cli import _print_verdict_banner as fn
|
|
64
|
+
return fn(*args, **kwargs)
|
|
65
|
+
def _get_mdc(*args, **kwargs):
|
|
66
|
+
from .._optional import get_mdc as fn
|
|
67
|
+
return fn(*args, **kwargs)
|
|
68
|
+
def report_agent_names(*args, **kwargs):
|
|
69
|
+
from aria_cli import report_agent_names as fn
|
|
70
|
+
return fn(*args, **kwargs)
|
|
71
|
+
def _tool_run_command(*args, **kwargs):
|
|
72
|
+
from aria_cli import _tool_run_command as fn
|
|
73
|
+
return fn(*args, **kwargs)
|
|
74
|
+
def run_deep_cli(*args, **kwargs):
|
|
75
|
+
from aria_cli import run_deep_cli as fn
|
|
76
|
+
return fn(*args, **kwargs)
|
|
77
|
+
def report_agent_health(*args, **kwargs):
|
|
78
|
+
from aria_cli import report_agent_health as fn
|
|
79
|
+
return fn(*args, **kwargs)
|
|
80
|
+
def report_file_size_kb(*args, **kwargs):
|
|
81
|
+
from aria_cli import report_file_size_kb as fn
|
|
82
|
+
return fn(*args, **kwargs)
|
|
83
|
+
def generate_html_report(*args, **kwargs):
|
|
84
|
+
from aria_cli import generate_html_report as fn
|
|
85
|
+
return fn(*args, **kwargs)
|
|
86
|
+
def _get__HAS_MDC():
|
|
87
|
+
from .._optional import HAS_MDC as val
|
|
88
|
+
return val
|
|
89
|
+
def _sanitize_team_result_with_market_data(*args, **kwargs):
|
|
90
|
+
from aria_cli import _sanitize_team_result_with_market_data as fn
|
|
91
|
+
return fn(*args, **kwargs)
|
|
92
|
+
def update_report_index(*args, **kwargs):
|
|
93
|
+
from aria_cli import update_report_index as fn
|
|
94
|
+
return fn(*args, **kwargs)
|
|
95
|
+
def build_markdown_report_prompt(*args, **kwargs):
|
|
96
|
+
from aria_cli import build_markdown_report_prompt as fn
|
|
97
|
+
return fn(*args, **kwargs)
|
|
98
|
+
def parse_report_args(*args, **kwargs):
|
|
99
|
+
from aria_cli import parse_report_args as fn
|
|
100
|
+
return fn(*args, **kwargs)
|
|
101
|
+
|
|
102
|
+
import json
|
|
103
|
+
import asyncio
|
|
104
|
+
import datetime
|
|
105
|
+
import time
|
|
106
|
+
import shlex
|
|
107
|
+
import sys
|
|
108
|
+
import os
|
|
109
|
+
from typing import Dict, Any, Optional
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
import json
|
|
113
|
+
import asyncio
|
|
114
|
+
import datetime
|
|
115
|
+
import time
|
|
116
|
+
import shlex
|
|
117
|
+
import sys
|
|
118
|
+
import os
|
|
119
|
+
from typing import Dict, Any, Optional
|
|
120
|
+
|
|
121
|
+
# A real logger. This was a `def logger(*args, **kwargs)` forwarding to
|
|
122
|
+
# aria_cli, so every `logger.debug(...)` raised AttributeError — and all but
|
|
123
|
+
# one of them sit in an except block, where the failure replaced whatever
|
|
124
|
+
# error was being reported.
|
|
125
|
+
logger = logging.getLogger(__name__)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PortfolioCommandsMixin:
|
|
129
|
+
"""Mixin: Portfolio commands: journal, report, portfolio, apply_plan, team."""
|
|
130
|
+
|
|
131
|
+
async def cmd_journal(self, args: str):
|
|
132
|
+
"""
|
|
133
|
+
本地持仓账本(SQLite,~/.arthera/portfolio.db)
|
|
134
|
+
Usage:
|
|
135
|
+
/journal → 当前持仓
|
|
136
|
+
/journal add buy AAPL 100 185.50 [理由]
|
|
137
|
+
/journal add sell AAPL 50 200.00 [理由]
|
|
138
|
+
/journal trades [SYMBOL] → 交易记录
|
|
139
|
+
/journal pnl → 含实时报价的未实现盈亏
|
|
140
|
+
/journal realized → 已实现盈亏(FIFO)
|
|
141
|
+
/journal export → 导出 CSV 到桌面
|
|
142
|
+
/journal delete <id> → 删除指定记录
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
from portfolio_ledger import PortfolioLedger as _PL
|
|
146
|
+
except ImportError:
|
|
147
|
+
msg = "portfolio_ledger 模块未找到"
|
|
148
|
+
self.context.console.print(f"[red]{msg}[/red]") if self.context.has_rich else print(msg)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
ledger = _PL()
|
|
152
|
+
parts = args.strip().split() if args.strip() else []
|
|
153
|
+
sub = parts[0].lower() if parts else "positions"
|
|
154
|
+
|
|
155
|
+
# ── add buy/sell ─────────────────────────────────────────────────────
|
|
156
|
+
if sub == "add":
|
|
157
|
+
# /journal add buy AAPL 100 185.50 [reason...]
|
|
158
|
+
if len(parts) < 5:
|
|
159
|
+
usage = "用法: /journal add <buy|sell> <symbol> <qty> <price> [理由]"
|
|
160
|
+
self.context.console.print(f"[yellow]{usage}[/yellow]") if self.context.has_rich else print(usage)
|
|
161
|
+
return
|
|
162
|
+
try:
|
|
163
|
+
side = parts[1].upper()
|
|
164
|
+
symbol = parts[2].upper()
|
|
165
|
+
qty = float(parts[3])
|
|
166
|
+
price = float(parts[4])
|
|
167
|
+
reason = " ".join(parts[5:]) if len(parts) > 5 else ""
|
|
168
|
+
tid = ledger.add_trade(symbol, side, qty, price, reason=reason)
|
|
169
|
+
amount = round(qty * price, 2)
|
|
170
|
+
msg = (f"✓ 已记录: #{tid} {side} {symbol} × {qty} @ {price}"
|
|
171
|
+
f" 总额 {amount:,.2f} {reason}")
|
|
172
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self.context.console.print(f"[red]记录失败: {e}[/red]") if self.context.has_rich else print(f"记录失败: {e}")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# ── delete ───────────────────────────────────────────────────────────
|
|
178
|
+
if sub == "delete" and len(parts) >= 2:
|
|
179
|
+
try:
|
|
180
|
+
tid = int(parts[1])
|
|
181
|
+
ok = ledger.delete_trade(tid)
|
|
182
|
+
msg = f"✓ 已删除记录 #{tid}" if ok else f"未找到记录 #{tid}"
|
|
183
|
+
self.context.console.print(f"[{'green' if ok else 'yellow'}]{msg}[/{'green' if ok else 'yellow'}]") if self.context.has_rich else print(msg)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.context.console.print(f"[red]删除失败: {e}[/red]") if self.context.has_rich else print(f"删除失败: {e}")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# ── trades history ───────────────────────────────────────────────────
|
|
189
|
+
if sub == "trades":
|
|
190
|
+
sym = parts[1].upper() if len(parts) > 1 else None
|
|
191
|
+
trades = ledger.get_trades(symbol=sym, limit=30)
|
|
192
|
+
title = f"交易记录{f' — {sym}' if sym else ''} (最近 {len(trades)} 条)"
|
|
193
|
+
if self.context.has_rich:
|
|
194
|
+
from rich.table import Table
|
|
195
|
+
tbl = Table(title=title, box=None, show_header=True, header_style="bold")
|
|
196
|
+
tbl.add_column("#", style="dim", width=4)
|
|
197
|
+
tbl.add_column("日期", width=10)
|
|
198
|
+
tbl.add_column("方向", width=5)
|
|
199
|
+
tbl.add_column("标的", width=8)
|
|
200
|
+
tbl.add_column("数量", justify="right", width=10)
|
|
201
|
+
tbl.add_column("价格", justify="right", width=10)
|
|
202
|
+
tbl.add_column("总额", justify="right", width=12)
|
|
203
|
+
tbl.add_column("理由", width=20)
|
|
204
|
+
for t in trades:
|
|
205
|
+
side_color = "green" if t["side"] == "BUY" else "red"
|
|
206
|
+
tbl.add_row(
|
|
207
|
+
str(t["id"]),
|
|
208
|
+
t["date"],
|
|
209
|
+
f"[{side_color}]{t['side']}[/{side_color}]",
|
|
210
|
+
t["symbol"],
|
|
211
|
+
f"{t['qty']:,.4g}",
|
|
212
|
+
f"{t['price']:,.4f}",
|
|
213
|
+
f"{t['amount']:,.2f}",
|
|
214
|
+
(t["reason"] or "")[:18],
|
|
215
|
+
)
|
|
216
|
+
self.context.console.print(tbl)
|
|
217
|
+
if not trades:
|
|
218
|
+
self.context.console.print("[dim]无交易记录[/dim]")
|
|
219
|
+
else:
|
|
220
|
+
print(title)
|
|
221
|
+
for t in trades:
|
|
222
|
+
print(f" #{t['id']} {t['date']} {t['side']} {t['symbol']} "
|
|
223
|
+
f"× {t['qty']} @ {t['price']} {t['reason']}")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# ── export ───────────────────────────────────────────────────────────
|
|
227
|
+
if sub == "export":
|
|
228
|
+
try:
|
|
229
|
+
out = ledger.export_csv()
|
|
230
|
+
msg = f"✓ 已导出 {ledger.trade_count()} 条记录 → {out}"
|
|
231
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.context.console.print(f"[red]导出失败: {e}[/red]") if self.context.has_rich else print(f"导出失败: {e}")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# ── realized P&L ─────────────────────────────────────────────────────
|
|
237
|
+
if sub == "realized":
|
|
238
|
+
rows = ledger.get_realized_pnl()
|
|
239
|
+
if self.context.has_rich:
|
|
240
|
+
from rich.table import Table
|
|
241
|
+
tbl = Table(title="已实现盈亏(FIFO)", box=None, header_style="bold")
|
|
242
|
+
tbl.add_column("标的", width=8)
|
|
243
|
+
tbl.add_column("已实现盈亏", justify="right", width=14)
|
|
244
|
+
tbl.add_column("剩余持仓", justify="right", width=10)
|
|
245
|
+
for r in rows:
|
|
246
|
+
pnl = r["realized_pnl"]
|
|
247
|
+
color = "green" if pnl >= 0 else "red"
|
|
248
|
+
tbl.add_row(
|
|
249
|
+
r["symbol"],
|
|
250
|
+
f"[{color}]{pnl:+,.2f}[/{color}]",
|
|
251
|
+
f"{r['open_lots']:,.4g}" if r["has_open"] else "已平仓",
|
|
252
|
+
)
|
|
253
|
+
self.context.console.print(tbl)
|
|
254
|
+
total = sum(r["realized_pnl"] for r in rows)
|
|
255
|
+
tc = "green" if total >= 0 else "red"
|
|
256
|
+
self.context.console.print(f" [bold]合计已实现盈亏: [{tc}]{total:+,.2f}[/{tc}][/bold]")
|
|
257
|
+
else:
|
|
258
|
+
for r in rows:
|
|
259
|
+
print(f" {r['symbol']}: {r['realized_pnl']:+,.2f}")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# ── pnl with live prices ──────────────────────────────────────────────
|
|
263
|
+
if sub == "pnl":
|
|
264
|
+
positions = ledger.get_positions()
|
|
265
|
+
if not positions:
|
|
266
|
+
self.context.console.print("[dim]暂无持仓记录。用 /journal add buy … 添加。[/dim]") if self.context.has_rich else print("暂无持仓")
|
|
267
|
+
return
|
|
268
|
+
# fetch live prices via yfinance
|
|
269
|
+
live_prices: dict = {}
|
|
270
|
+
syms = [p["symbol"] for p in positions]
|
|
271
|
+
if self.context.has_rich:
|
|
272
|
+
self.context.console.print(f" [dim]获取 {len(syms)} 只股票实时报价…[/dim]")
|
|
273
|
+
try:
|
|
274
|
+
import yfinance as yf
|
|
275
|
+
for sym in syms:
|
|
276
|
+
try:
|
|
277
|
+
h = yf.Ticker(sym).history(period="1d")
|
|
278
|
+
if not h.empty:
|
|
279
|
+
live_prices[sym] = float(h["Close"].iloc[-1])
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
except ImportError:
|
|
283
|
+
pass
|
|
284
|
+
rows = ledger.get_pnl_with_prices(live_prices)
|
|
285
|
+
if self.context.has_rich:
|
|
286
|
+
from rich.table import Table
|
|
287
|
+
tbl = Table(title="持仓盈亏", box=None, header_style="bold")
|
|
288
|
+
tbl.add_column("标的", width=8)
|
|
289
|
+
tbl.add_column("数量", justify="right", width=10)
|
|
290
|
+
tbl.add_column("均价", justify="right", width=10)
|
|
291
|
+
tbl.add_column("现价", justify="right", width=10)
|
|
292
|
+
tbl.add_column("市值", justify="right", width=12)
|
|
293
|
+
tbl.add_column("未实现盈亏", justify="right", width=14)
|
|
294
|
+
tbl.add_column("涨跌%", justify="right", width=8)
|
|
295
|
+
for r in rows:
|
|
296
|
+
has_price = "current_price" in r
|
|
297
|
+
pnl = r.get("unrealized_pnl", "")
|
|
298
|
+
pct = r.get("unrealized_pct", "")
|
|
299
|
+
color = ("green" if isinstance(pnl, (int, float)) and pnl >= 0 else "red") if has_price else "dim"
|
|
300
|
+
tbl.add_row(
|
|
301
|
+
r["symbol"],
|
|
302
|
+
f"{r['net_qty']:,.4g}",
|
|
303
|
+
f"{r['avg_cost']:,.4f}",
|
|
304
|
+
f"{r.get('current_price', 'N/A'):,.4f}" if has_price else "N/A",
|
|
305
|
+
f"{r.get('market_value', ''):,.2f}" if has_price else "N/A",
|
|
306
|
+
f"[{color}]{pnl:+,.2f}[/{color}]" if has_price else "—",
|
|
307
|
+
f"[{color}]{pct:+.2f}%[/{color}]" if has_price else "—",
|
|
308
|
+
)
|
|
309
|
+
self.context.console.print(tbl)
|
|
310
|
+
total_pnl = sum(r.get("unrealized_pnl", 0) for r in rows if "unrealized_pnl" in r)
|
|
311
|
+
total_mv = sum(r.get("market_value", 0) for r in rows if "market_value" in r)
|
|
312
|
+
total_cost = sum(r["cost_basis"] for r in rows)
|
|
313
|
+
tc = "green" if total_pnl >= 0 else "red"
|
|
314
|
+
self.context.console.print(
|
|
315
|
+
f" [bold]总持仓成本 {total_cost:,.2f} "
|
|
316
|
+
f"总市值 {total_mv:,.2f} "
|
|
317
|
+
f"未实现盈亏 [{tc}]{total_pnl:+,.2f}[/{tc}][/bold]"
|
|
318
|
+
)
|
|
319
|
+
# Portfolio status banner
|
|
320
|
+
_pnl_pct = (total_pnl / total_cost * 100) if total_cost else 0
|
|
321
|
+
_pnl_verdict = "HEALTHY" if _pnl_pct >= 0 else ("NEEDS_ATTENTION" if _pnl_pct >= -10 else "HIGH_RISK")
|
|
322
|
+
_pnl_sub = f"总盈亏 {total_pnl:+,.2f} ({_pnl_pct:+.1f}%)"
|
|
323
|
+
_print_verdict_banner(_pnl_verdict, subtitle=_pnl_sub)
|
|
324
|
+
else:
|
|
325
|
+
for r in rows:
|
|
326
|
+
pnl = r.get("unrealized_pnl", "N/A")
|
|
327
|
+
print(f" {r['symbol']}: {r['net_qty']} × avg {r['avg_cost']} pnl {pnl}")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# ── default: positions ────────────────────────────────────────────────
|
|
331
|
+
positions = ledger.get_positions()
|
|
332
|
+
if not positions:
|
|
333
|
+
hint = "暂无持仓记录。\n 添加示例: /journal add buy AAPL 100 185.50 首次建仓"
|
|
334
|
+
self.context.console.print(f"[dim]{hint}[/dim]") if self.context.has_rich else print(hint)
|
|
335
|
+
return
|
|
336
|
+
if self.context.has_rich:
|
|
337
|
+
from rich.table import Table
|
|
338
|
+
tbl = Table(
|
|
339
|
+
title=f"当前持仓({len(positions)} 只,共 {ledger.trade_count()} 条交易)",
|
|
340
|
+
box=None, header_style="bold",
|
|
341
|
+
)
|
|
342
|
+
tbl.add_column("标的", width=8)
|
|
343
|
+
tbl.add_column("持仓量", justify="right", width=12)
|
|
344
|
+
tbl.add_column("均价成本", justify="right", width=12)
|
|
345
|
+
tbl.add_column("持仓成本", justify="right", width=14)
|
|
346
|
+
tbl.add_column("首次建仓", width=12)
|
|
347
|
+
for pos in positions:
|
|
348
|
+
tbl.add_row(
|
|
349
|
+
pos["symbol"],
|
|
350
|
+
f"{pos['net_qty']:,.4g}",
|
|
351
|
+
f"{pos['avg_cost']:,.4f}",
|
|
352
|
+
f"{pos['cost_basis']:,.2f}",
|
|
353
|
+
pos.get("first_trade", ""),
|
|
354
|
+
)
|
|
355
|
+
self.context.console.print(tbl)
|
|
356
|
+
self.context.console.print(
|
|
357
|
+
" [dim]更多命令: /journal pnl | /journal trades | "
|
|
358
|
+
"/journal realized | /journal export[/dim]"
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
print(f"当前持仓 ({len(positions)} 只):")
|
|
362
|
+
for pos in positions:
|
|
363
|
+
print(f" {pos['symbol']}: {pos['net_qty']} 股 均价 {pos['avg_cost']}")
|
|
364
|
+
|
|
365
|
+
async def cmd_report(self, args: str):
|
|
366
|
+
"""生成综合投资报告(图表 + 多 Agent 分析 → HTML / Markdown 文件)。
|
|
367
|
+
|
|
368
|
+
Usage:
|
|
369
|
+
/report AAPL
|
|
370
|
+
/report 000333
|
|
371
|
+
/report AAPL --format md # Markdown 投研报告(离线可用)
|
|
372
|
+
/report AAPL --type deep # 深度研报(8页)
|
|
373
|
+
/report AAPL --type brief # 简评(1页)
|
|
374
|
+
/report AAPL --pdf # 同时导出 PDF(自动检测 Chrome/Edge,macOS 零安装)
|
|
375
|
+
"""
|
|
376
|
+
from datetime import datetime as _dt
|
|
377
|
+
|
|
378
|
+
report_args = parse_report_args(args)
|
|
379
|
+
symbol = report_args.symbol
|
|
380
|
+
fmt = report_args.fmt
|
|
381
|
+
report_type = report_args.report_type
|
|
382
|
+
export_pdf_flag = report_args.export_pdf
|
|
383
|
+
out_dir = report_args.output_dir
|
|
384
|
+
if out_dir:
|
|
385
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
ts = _dt.now().strftime("%Y%m%d_%H%M")
|
|
387
|
+
|
|
388
|
+
# ── Markdown report mode (works fully offline) ────────────────────────
|
|
389
|
+
if fmt in ("md", "markdown"):
|
|
390
|
+
_gen = ui_text(self, f"生成 {symbol} Markdown 投研报告 ({report_type})", f"Writing the {symbol} Markdown report ({report_type})")
|
|
391
|
+
self.context.console.print(f"\n [dim]{_gen}…[/dim]") if self.context.has_rich else print(f"\n {_gen}…")
|
|
392
|
+
|
|
393
|
+
# Fetch real data through the service boundary so provenance and
|
|
394
|
+
# quality metadata travel with the report prompt and artifact.
|
|
395
|
+
mdc_data = {}
|
|
396
|
+
data_bundle = None
|
|
397
|
+
data_quality = {}
|
|
398
|
+
try:
|
|
399
|
+
from packages.aria_services.data import DataService as _ReportDataService
|
|
400
|
+
data_bundle = await asyncio.get_event_loop().run_in_executor(
|
|
401
|
+
None,
|
|
402
|
+
lambda: _ReportDataService().bundle(symbol, history_days=370, technical_days=120),
|
|
403
|
+
)
|
|
404
|
+
quote = data_bundle.quote or {}
|
|
405
|
+
technical = data_bundle.technical or {}
|
|
406
|
+
mdc_data = {**quote, **technical}
|
|
407
|
+
data_quality = data_bundle.quality or {}
|
|
408
|
+
except Exception as _ds_exc:
|
|
409
|
+
logger.debug("report markdown data service failed: %s", _ds_exc)
|
|
410
|
+
if _get__HAS_MDC():
|
|
411
|
+
try:
|
|
412
|
+
mdc = _get_mdc()
|
|
413
|
+
q = mdc.quote(symbol)
|
|
414
|
+
ti = mdc.technical_indicators(symbol, days=120)
|
|
415
|
+
mdc_data = {**q, **ti}
|
|
416
|
+
data_quality = {
|
|
417
|
+
"status": "partial",
|
|
418
|
+
"stale": False,
|
|
419
|
+
"providers": mdc_data.get("provider_chain") or list(dict.fromkeys(
|
|
420
|
+
str(v) for v in [mdc_data.get("provider"), mdc_data.get("source")] if v
|
|
421
|
+
)),
|
|
422
|
+
"warnings": [f"data service unavailable: {_ds_exc}"],
|
|
423
|
+
}
|
|
424
|
+
except Exception:
|
|
425
|
+
data_quality = {"status": "data_unavailable", "warnings": [str(_ds_exc)]}
|
|
426
|
+
|
|
427
|
+
ai_prompt = build_markdown_report_prompt(
|
|
428
|
+
symbol=symbol,
|
|
429
|
+
report_type=report_type,
|
|
430
|
+
market_data=mdc_data,
|
|
431
|
+
data_quality=data_quality,
|
|
432
|
+
data_bundle=data_bundle,
|
|
433
|
+
now=_dt.now(),
|
|
434
|
+
lang=ui_text(self, "zh", "en"),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
await self.terminal.send_message(ai_prompt)
|
|
438
|
+
|
|
439
|
+
# Extract last AI response and save as markdown
|
|
440
|
+
last_ai = next(
|
|
441
|
+
(m["content"] for m in reversed(self.terminal.conversation)
|
|
442
|
+
if m.get("role") == "assistant"), ""
|
|
443
|
+
)
|
|
444
|
+
if last_ai:
|
|
445
|
+
saved = save_markdown_report(
|
|
446
|
+
symbol=symbol,
|
|
447
|
+
report_type=report_type,
|
|
448
|
+
markdown_text=last_ai,
|
|
449
|
+
timestamp=ts,
|
|
450
|
+
output_dir=out_dir,
|
|
451
|
+
market_data=mdc_data,
|
|
452
|
+
data_quality=data_quality,
|
|
453
|
+
data_bundle=data_bundle,
|
|
454
|
+
created_at=_dt.now(),
|
|
455
|
+
)
|
|
456
|
+
out_f = saved.path
|
|
457
|
+
# A short, clickable path: the full temp-dir path wrapped over
|
|
458
|
+
# three lines, and the "预览: open …" line repeated it.
|
|
459
|
+
from aria_code.ui.render.output import display_path as _short_path
|
|
460
|
+
_saved = ui_text(self, "报告已保存", "Report saved")
|
|
461
|
+
if self.context.has_rich:
|
|
462
|
+
self.context.console.print(f"\n [green]✓[/green] {_saved}: "
|
|
463
|
+
f"[link={_file_uri(str(out_f))}]{_short_path(str(out_f))}[/link]\n")
|
|
464
|
+
else:
|
|
465
|
+
print(f"\n {_saved}: {out_f}")
|
|
466
|
+
|
|
467
|
+
# ── Markdown 报告 → 排版 PDF(--pdf;中英文模板自动检测)──
|
|
468
|
+
if export_pdf_flag:
|
|
469
|
+
from pathlib import Path as _Path # mixin 重绑定后无模块级 Path
|
|
470
|
+
try:
|
|
471
|
+
from markdown_pdf import markdown_to_pdf as _md_to_pdf
|
|
472
|
+
_pdf_out = await asyncio.get_event_loop().run_in_executor(
|
|
473
|
+
None,
|
|
474
|
+
lambda: _md_to_pdf(_Path(out_f),
|
|
475
|
+
_Path(out_f).with_suffix(".pdf")),
|
|
476
|
+
)
|
|
477
|
+
if _pdf_out:
|
|
478
|
+
if self.context.has_rich:
|
|
479
|
+
self.context.console.print(
|
|
480
|
+
f" [green]PDF 导出成功[/green] "
|
|
481
|
+
f"[link={_file_uri(_pdf_out)}]{_pdf_out.name}[/link]"
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
print(f" PDF: {_pdf_out}")
|
|
485
|
+
import subprocess as _subp_md
|
|
486
|
+
try:
|
|
487
|
+
_subp_md.Popen(["open", str(_pdf_out)])
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
else:
|
|
491
|
+
_msg = "PDF 导出失败:安装 Chrome/Edge,或 pip install pyobjc-framework-WebKit"
|
|
492
|
+
self.context.console.print(f" [yellow]{_msg}[/yellow]") if self.context.has_rich else print(f" {_msg}")
|
|
493
|
+
except Exception as _pdf_exc:
|
|
494
|
+
logger.debug("[report] md pdf export error: %s", _pdf_exc)
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# ── HTML 研报(Bloomberg 暗色主题)────────────────────────────────
|
|
498
|
+
if self.context.has_rich:
|
|
499
|
+
self.context.console.print(f"\n [dim]正在生成 [bold]{symbol}[/bold] 专业研报(数据清洗 + 图表 + Agent 分析)…[/dim]")
|
|
500
|
+
else:
|
|
501
|
+
print(f"\n 正在生成 {symbol} 研报…")
|
|
502
|
+
|
|
503
|
+
_agent_names_for_report = report_agent_names(report_type)
|
|
504
|
+
def _report_agent_done(name, result):
|
|
505
|
+
success = bool(getattr(result, "success", False))
|
|
506
|
+
degraded = bool(getattr(result, "degraded", False))
|
|
507
|
+
if success:
|
|
508
|
+
icon = "≈" if degraded else "✓"
|
|
509
|
+
detail = "降级结果" if degraded else "完成"
|
|
510
|
+
else:
|
|
511
|
+
icon = "✗"
|
|
512
|
+
error = str(getattr(result, "error", "") or "失败")
|
|
513
|
+
detail = "超时" if error == "timeout" else error[:60]
|
|
514
|
+
if self.context.has_rich:
|
|
515
|
+
color = "yellow" if degraded else "green" if success else "red"
|
|
516
|
+
self.context.console.print(f" [{color}]{icon}[/{color}] {name} [dim]{detail}[/dim]")
|
|
517
|
+
else:
|
|
518
|
+
print(f" {icon} {name} {detail}")
|
|
519
|
+
|
|
520
|
+
def _report_synthesis_start(results):
|
|
521
|
+
succeeded = sum(1 for result in results if getattr(result, "success", False))
|
|
522
|
+
total = len(results)
|
|
523
|
+
message = f"Agent 阶段完成 {succeeded}/{total},正在整理报告…"
|
|
524
|
+
self.context.console.print(f" [dim]{message}[/dim]") if self.context.has_rich else print(f" {message}")
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
if self.context.has_rich:
|
|
528
|
+
self.context.console.print(f" [dim]{len(_agent_names_for_report)} agents 并行分析…[/dim]")
|
|
529
|
+
_html_report = await generate_html_report(
|
|
530
|
+
symbol=symbol,
|
|
531
|
+
report_type=report_type,
|
|
532
|
+
output_dir=out_dir,
|
|
533
|
+
config=self.terminal.config,
|
|
534
|
+
on_agent_done=_report_agent_done,
|
|
535
|
+
on_synthesis_start=_report_synthesis_start,
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
_html_report = await generate_html_report(
|
|
539
|
+
symbol=symbol,
|
|
540
|
+
report_type=report_type,
|
|
541
|
+
output_dir=out_dir,
|
|
542
|
+
config=self.terminal.config,
|
|
543
|
+
on_agent_done=_report_agent_done,
|
|
544
|
+
on_synthesis_start=_report_synthesis_start,
|
|
545
|
+
)
|
|
546
|
+
out_f = _html_report.path
|
|
547
|
+
_team_result = _html_report.team_result
|
|
548
|
+
except Exception as e:
|
|
549
|
+
if self.context.has_rich:
|
|
550
|
+
self.context.console.print(f" [red]研报生成失败: {e}[/red]")
|
|
551
|
+
else:
|
|
552
|
+
print(f" 研报生成失败: {e}")
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
if not out_f:
|
|
556
|
+
self.context.console.print(" [red]研报生成失败(无输出文件)[/red]") if self.context.has_rich else print(" 研报生成失败")
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
path = str(out_f)
|
|
560
|
+
from ui.render.output import display_path as _display_path
|
|
561
|
+
path_label = _display_path(out_f, fallback="report")
|
|
562
|
+
_file_kb = report_file_size_kb(out_f)
|
|
563
|
+
# Check if all agents failed — show warning instead of false success
|
|
564
|
+
_agent_health = _html_report.agent_health or report_agent_health(
|
|
565
|
+
_team_result, _agent_names_for_report
|
|
566
|
+
)
|
|
567
|
+
_all_agents_failed = all_agents_failed(_team_result) or bool(
|
|
568
|
+
_agent_health.get("expected") and not _agent_health.get("succeeded")
|
|
569
|
+
)
|
|
570
|
+
_agents_partial = bool(
|
|
571
|
+
_agent_health.get("failed") and _agent_health.get("succeeded")
|
|
572
|
+
)
|
|
573
|
+
if self.context.has_rich:
|
|
574
|
+
if _all_agents_failed:
|
|
575
|
+
self.context.console.print(
|
|
576
|
+
f"\n [yellow]⚠ 研报已保存(所有 Agent 分析失败,内容仅含基础数据)[/yellow]"
|
|
577
|
+
f" [dim]{out_f.name} ({_file_kb}KB)[/dim]"
|
|
578
|
+
)
|
|
579
|
+
elif _agents_partial:
|
|
580
|
+
self.context.console.print(
|
|
581
|
+
f"\n [yellow]⚠ 研报已保存(Agent 部分完成 "
|
|
582
|
+
f"{_agent_health['succeeded']}/{_agent_health['expected']},结论置信度已降级)[/yellow]"
|
|
583
|
+
f" [dim]{out_f.name} ({_file_kb}KB)[/dim]"
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
self.context.console.print(
|
|
587
|
+
f"\n [green]✅ 研报已保存[/green]"
|
|
588
|
+
f" [link={_file_uri(path)}]{path_label}[/link]"
|
|
589
|
+
f" [dim]({_file_kb}KB)[/dim]"
|
|
590
|
+
)
|
|
591
|
+
self.context.console.print(f" [dim]文件: {path_label}[/dim]")
|
|
592
|
+
if _team_result:
|
|
593
|
+
_health_suffix = (
|
|
594
|
+
f" · agents {_agent_health['succeeded']}/{_agent_health['expected']}"
|
|
595
|
+
if _agent_health.get("failed") else ""
|
|
596
|
+
)
|
|
597
|
+
_print_verdict_banner(
|
|
598
|
+
_team_result.final_signal,
|
|
599
|
+
subtitle=f"耗时 {_team_result.elapsed_sec:.1f}s{_health_suffix}",
|
|
600
|
+
confidence=_team_result.confidence,
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
_pfx = "⚠ 研报已保存(Agent 全部失败)" if _all_agents_failed else "✅ 研报已保存"
|
|
604
|
+
print(f"\n {_pfx}: {path_label} ({_file_kb}KB)")
|
|
605
|
+
|
|
606
|
+
# ── PDF 导出 ──────────────────────────────────────────────────────────
|
|
607
|
+
if export_pdf_flag:
|
|
608
|
+
try:
|
|
609
|
+
if self.context.has_rich:
|
|
610
|
+
with self.context.console.status(f"[dim]{ui_text(self, '导出 PDF', 'Exporting PDF')}…[/dim]", spinner="dots"):
|
|
611
|
+
_pdf_path = await export_report_pdf(out_f)
|
|
612
|
+
else:
|
|
613
|
+
_pdf_path = await export_report_pdf(out_f)
|
|
614
|
+
if _pdf_path:
|
|
615
|
+
_pdf_kb = report_file_size_kb(_pdf_path)
|
|
616
|
+
if self.context.has_rich:
|
|
617
|
+
self.context.console.print(
|
|
618
|
+
f" [green]PDF 导出成功[/green]"
|
|
619
|
+
f" [link={_file_uri(_pdf_path)}]{_pdf_path.name}[/link]"
|
|
620
|
+
f" [dim]({_pdf_kb}KB)[/dim]"
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
print(f" PDF: {_pdf_path} ({_pdf_kb}KB)")
|
|
624
|
+
import subprocess as _subp2
|
|
625
|
+
try:
|
|
626
|
+
_subp2.Popen(["open", str(_pdf_path)])
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
629
|
+
else:
|
|
630
|
+
_hint = "安装 Chrome/Edge 即可(自动检测),或 pip install weasyprint / brew install wkhtmltopdf"
|
|
631
|
+
if self.context.has_rich:
|
|
632
|
+
self.context.console.print(
|
|
633
|
+
f" [yellow]PDF 导出失败[/yellow] "
|
|
634
|
+
f"[dim]请安装: {_hint} 或在浏览器按 Cmd+P → 存储为 PDF[/dim]"
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
print(f" PDF 导出失败,请安装: {_hint}")
|
|
638
|
+
except Exception as _e:
|
|
639
|
+
logger.debug("[report] pdf export error: %s", _e)
|
|
640
|
+
|
|
641
|
+
# ── 更新研报索引 ──────────────────────────────────────────────────────
|
|
642
|
+
try:
|
|
643
|
+
_idx = await update_report_index(out_f.parent)
|
|
644
|
+
if _idx and self.context.has_rich:
|
|
645
|
+
self.context.console.print(
|
|
646
|
+
f" [dim]索引已更新: [link={_file_uri(_idx)}]{_idx.name}[/link][/dim]"
|
|
647
|
+
)
|
|
648
|
+
except Exception as _e:
|
|
649
|
+
logger.debug("[report] index update error: %s", _e)
|
|
650
|
+
|
|
651
|
+
import subprocess as _subp
|
|
652
|
+
try:
|
|
653
|
+
_subp.Popen(["open", path])
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
async def cmd_portfolio(self, args: str):
|
|
658
|
+
"""
|
|
659
|
+
组合级跨标的分析(相关性/分散度/风险)
|
|
660
|
+
Usage:
|
|
661
|
+
/portfolio → 分析 watchlist(最多 10 只)
|
|
662
|
+
/portfolio analyze → 同上
|
|
663
|
+
/portfolio analyze AAPL TSLA MSFT
|
|
664
|
+
/portfolio rebalance → 生成再平衡建议(同 analyze,着重操作)
|
|
665
|
+
"""
|
|
666
|
+
import sys as _sys
|
|
667
|
+
parts = args.strip().split()
|
|
668
|
+
sub = parts[0].lower() if parts else "analyze"
|
|
669
|
+
sym_parts = parts[1:] if parts else []
|
|
670
|
+
rebalance = (sub == "rebalance")
|
|
671
|
+
|
|
672
|
+
# ── holdings: 实盘持仓看板,按来源策略分组 ──────────────────────────
|
|
673
|
+
if sub in ("holdings", "book", "positions"):
|
|
674
|
+
await self._portfolio_holdings_by_strategy()
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
# 解析标的:命令行 > 真实持仓账本 > watchlist
|
|
678
|
+
# 之前这里没有命令行参数时永远用 watchlist(等权虚构组合),哪怕
|
|
679
|
+
# portfolio_ledger 里有真实持仓也不会用——risk verdict 算的是一个
|
|
680
|
+
# 假想的等权组合,不是用户真实的仓位暴露。现在优先读真实持仓
|
|
681
|
+
# (按成本加权),ledger 为空才退回 watchlist。
|
|
682
|
+
ledger_weights: Optional[Dict[str, float]] = None
|
|
683
|
+
if sym_parts:
|
|
684
|
+
symbols = [s.strip(",").upper() for s in sym_parts if s.strip(",")]
|
|
685
|
+
else:
|
|
686
|
+
try:
|
|
687
|
+
from portfolio_ledger import PortfolioLedger as _PL_default
|
|
688
|
+
positions = _PL_default().get_positions()
|
|
689
|
+
except ImportError:
|
|
690
|
+
positions = []
|
|
691
|
+
if positions:
|
|
692
|
+
symbols = [p["symbol"].upper() for p in positions]
|
|
693
|
+
ledger_weights = {p["symbol"].upper(): p["cost_basis"] for p in positions}
|
|
694
|
+
else:
|
|
695
|
+
symbols = self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL", "NVDA", "TSLA"])[:10]
|
|
696
|
+
|
|
697
|
+
_lang = "zh" if str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh") else "en"
|
|
698
|
+
_T = (lambda a, b: a) if _lang == "zh" else (lambda a, b: b)
|
|
699
|
+
|
|
700
|
+
if not symbols:
|
|
701
|
+
msg = _T("请先设置 watchlist、记录持仓(/journal add buy ...)或指定标的:/portfolio analyze AAPL TSLA MSFT",
|
|
702
|
+
"Set a watchlist, record positions (/journal add buy ...) or name the symbols: "
|
|
703
|
+
"/portfolio analyze AAPL TSLA MSFT")
|
|
704
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
# 尝试使用新 PortfolioAgent
|
|
708
|
+
_use_new = False
|
|
709
|
+
try:
|
|
710
|
+
from agents.portfolio_agent import PortfolioAgent as _PA
|
|
711
|
+
_use_new = True
|
|
712
|
+
except ImportError:
|
|
713
|
+
pass
|
|
714
|
+
|
|
715
|
+
if _use_new:
|
|
716
|
+
if sym_parts:
|
|
717
|
+
hdr = _T(f"分析组合:{' '.join(symbols)}", f"portfolio of {' '.join(symbols)}")
|
|
718
|
+
elif ledger_weights:
|
|
719
|
+
hdr = _T("分析真实持仓组合(按成本加权)", "your positions, weighted by cost")
|
|
720
|
+
else:
|
|
721
|
+
hdr = _T("分析 watchlist 组合(等权,无真实持仓数据)",
|
|
722
|
+
"watchlist, equal weight (no positions recorded)")
|
|
723
|
+
if rebalance:
|
|
724
|
+
hdr = _T("再平衡方案:", "Rebalance · ") + hdr
|
|
725
|
+
_sym_label = _T("标的", "Symbols")
|
|
726
|
+
if self.context.has_rich:
|
|
727
|
+
self.context.console.print()
|
|
728
|
+
self.context.console.print(f" [bold cyan]━━━ /portfolio {hdr} ━━━[/bold cyan]")
|
|
729
|
+
self.context.console.print(f" [dim]{_sym_label} ({len(symbols)}): {', '.join(symbols)}[/dim]")
|
|
730
|
+
self.context.console.print()
|
|
731
|
+
else:
|
|
732
|
+
print(f"\n ━━━ /portfolio ━━━\n {_sym_label}: {', '.join(symbols)}\n")
|
|
733
|
+
|
|
734
|
+
# The model the session is configured for. This took the first
|
|
735
|
+
# local provider, so a Gemini session asked Ollama, which was not
|
|
736
|
+
# running, and printed the connection error as a log line.
|
|
737
|
+
from aria_code.apps.cli.commands.team import build_team_llm_provider
|
|
738
|
+
_llm = build_team_llm_provider(self.terminal.config)
|
|
739
|
+
|
|
740
|
+
tokens: list = []
|
|
741
|
+
def _on_tok(t):
|
|
742
|
+
tokens.append(t)
|
|
743
|
+
_sys.stdout.write(t); _sys.stdout.flush()
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
agent = _PA(llm_provider=_llm, on_token=_on_tok, lang=_lang)
|
|
747
|
+
result = await agent.run_portfolio(symbols, weights=ledger_weights)
|
|
748
|
+
if tokens:
|
|
749
|
+
print() # 换行(流式输出后)
|
|
750
|
+
|
|
751
|
+
if not result:
|
|
752
|
+
if self.context.has_rich:
|
|
753
|
+
self.context.console.print(f"[yellow] {_T('组合分析返回空结果', 'The portfolio analysis came back empty')}[/yellow]")
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
if not tokens and getattr(agent, "llm_error", None):
|
|
757
|
+
_note = _T("模型不可用,以下仅为统计结果", "Model unavailable, so this is the statistics only")
|
|
758
|
+
if self.context.has_rich:
|
|
759
|
+
self.context.console.print(f" [dim]{_note}[/dim]")
|
|
760
|
+
else:
|
|
761
|
+
print(f" {_note}")
|
|
762
|
+
|
|
763
|
+
# The verdict the analysis reached. Rebuilt from the signal,
|
|
764
|
+
# NEEDS_ATTENTION (signal SELL) came out as HIGH_RISK.
|
|
765
|
+
_port_verdict = str((result.data_used or {}).get("verdict") or "NEEDS_ATTENTION")
|
|
766
|
+
if self.context.has_rich:
|
|
767
|
+
# The header already ends in a blank line, and the banner's
|
|
768
|
+
# panel opens with one: two blank lines each side before.
|
|
769
|
+
if tokens:
|
|
770
|
+
self.context.console.print()
|
|
771
|
+
for pt in (result.key_points or []):
|
|
772
|
+
self.context.console.print(f" [dim]• {pt}[/dim]")
|
|
773
|
+
_subtitle = " · ".join(result.key_points[:2]) if result.key_points else ""
|
|
774
|
+
_print_verdict_banner(_port_verdict, subtitle=_subtitle,
|
|
775
|
+
confidence=result.confidence, lang=_lang)
|
|
776
|
+
else:
|
|
777
|
+
for pt in result.key_points:
|
|
778
|
+
print(f" • {pt}")
|
|
779
|
+
print(f"\n {_T('置信度', 'Confidence')}: {result.confidence:.0%} {_port_verdict}")
|
|
780
|
+
|
|
781
|
+
if rebalance and self.context.has_rich:
|
|
782
|
+
self.context.console.print("\n [dim]" + _T(
|
|
783
|
+
"提示: 再平衡建议已包含在上方分析中。如需详细方案,可追问 Aria 具体操作步骤。",
|
|
784
|
+
"The rebalancing suggestions are in the analysis above; ask Aria for the steps.") + "[/dim]")
|
|
785
|
+
|
|
786
|
+
except Exception as e:
|
|
787
|
+
msg = _T(f"组合分析失败: {e}", f"Portfolio analysis failed: {e}")
|
|
788
|
+
self.context.console.print(f" [red]{msg}[/red]") if self.context.has_rich else print(f" {msg}")
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
# 旧路径回退(无新 agents 包时)
|
|
792
|
+
if self.context.has_rich:
|
|
793
|
+
self.context.console.print("[dim]Assessing portfolio risk...[/dim]")
|
|
794
|
+
else:
|
|
795
|
+
print("Assessing portfolio risk...")
|
|
796
|
+
result = await execute_aria_tool(self.terminal.api_url, "assess_portfolio_risk", {
|
|
797
|
+
"symbols": symbols[:10],
|
|
798
|
+
})
|
|
799
|
+
if result.get("success") and result.get("data"):
|
|
800
|
+
if self.context.has_rich:
|
|
801
|
+
self.context.console.print("\n [bold]Portfolio Risk[/bold]\n")
|
|
802
|
+
self.context.console.print(f"[dim]{json.dumps(result['data'], indent=2, ensure_ascii=False)[:1000]}[/dim]")
|
|
803
|
+
else:
|
|
804
|
+
print(json.dumps(result.get("data", {}), indent=2, ensure_ascii=False))
|
|
805
|
+
else:
|
|
806
|
+
self.context.console.print(f"[dim]No data: {result.get('error', '')}[/dim]" if self.context.has_rich
|
|
807
|
+
else f"No data: {result.get('error', '')}")
|
|
808
|
+
|
|
809
|
+
async def _portfolio_holdings_by_strategy(self):
|
|
810
|
+
"""实盘持仓看板:按来源策略分组(策略名匹配交易 reason),含实时浮盈。"""
|
|
811
|
+
try:
|
|
812
|
+
from portfolio_ledger import PortfolioLedger
|
|
813
|
+
except ImportError:
|
|
814
|
+
self.context.console.print("[red]portfolio_ledger 模块未找到[/red]") if self.context.has_rich else print("portfolio_ledger 未找到")
|
|
815
|
+
return
|
|
816
|
+
ledger = PortfolioLedger()
|
|
817
|
+
names = []
|
|
818
|
+
try:
|
|
819
|
+
from strategy_vault import get_vault
|
|
820
|
+
names = get_vault().list_all_names()
|
|
821
|
+
except Exception:
|
|
822
|
+
pass
|
|
823
|
+
groups = ledger.positions_by_strategy(names)
|
|
824
|
+
if not groups:
|
|
825
|
+
self.context.console.print("[dim]暂无持仓。用 /deploy 或 /journal add 建仓后在此按策略查看。[/dim]"
|
|
826
|
+
if self.context.has_rich else "暂无持仓。")
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
syms = sorted({p["symbol"] for poss in groups.values() for p in poss})
|
|
830
|
+
if self.context.has_rich:
|
|
831
|
+
self.context.console.print(f" [dim]获取 {len(syms)} 只实时报价…[/dim]")
|
|
832
|
+
live = {}
|
|
833
|
+
try:
|
|
834
|
+
import yfinance as yf
|
|
835
|
+
for s in syms:
|
|
836
|
+
try:
|
|
837
|
+
h = yf.Ticker(s).history(period="1d")
|
|
838
|
+
if not h.empty:
|
|
839
|
+
live[s] = float(h["Close"].iloc[-1])
|
|
840
|
+
except Exception:
|
|
841
|
+
pass
|
|
842
|
+
except ImportError:
|
|
843
|
+
pass
|
|
844
|
+
|
|
845
|
+
g_cost = g_mv = 0.0
|
|
846
|
+
if self.context.has_rich:
|
|
847
|
+
from rich.table import Table
|
|
848
|
+
self.context.console.print()
|
|
849
|
+
self.context.console.print(" [bold cyan]持仓看板 · 按来源策略[/bold cyan]")
|
|
850
|
+
for grp, poss in groups.items():
|
|
851
|
+
tbl = Table(box=None, header_style="bold", pad_edge=False)
|
|
852
|
+
tbl.add_column("标的", width=8)
|
|
853
|
+
tbl.add_column("净持仓", justify="right", width=10)
|
|
854
|
+
tbl.add_column("均价", justify="right", width=10)
|
|
855
|
+
tbl.add_column("现价", justify="right", width=10)
|
|
856
|
+
tbl.add_column("市值", justify="right", width=12)
|
|
857
|
+
tbl.add_column("浮动盈亏", justify="right", width=13)
|
|
858
|
+
tbl.add_column("%", justify="right", width=8)
|
|
859
|
+
sub_cost = sub_mv = 0.0; have = False
|
|
860
|
+
for p in poss:
|
|
861
|
+
cost = p["cost_basis"]; sub_cost += cost
|
|
862
|
+
px = live.get(p["symbol"])
|
|
863
|
+
if px is not None:
|
|
864
|
+
have = True
|
|
865
|
+
mv = px * p["net_qty"]; sub_mv += mv
|
|
866
|
+
pnl = mv - cost; pct = (pnl / cost * 100) if cost else 0.0
|
|
867
|
+
c = "green" if pnl >= 0 else "red"
|
|
868
|
+
tbl.add_row(p["symbol"], f"{p['net_qty']:,.4g}", f"{p['avg_cost']:,.2f}",
|
|
869
|
+
f"{px:,.2f}", f"{mv:,.0f}", f"[{c}]{pnl:+,.0f}[/{c}]",
|
|
870
|
+
f"[{c}]{pct:+.1f}%[/{c}]")
|
|
871
|
+
else:
|
|
872
|
+
sub_mv += cost
|
|
873
|
+
tbl.add_row(p["symbol"], f"{p['net_qty']:,.4g}", f"{p['avg_cost']:,.2f}",
|
|
874
|
+
"N/A", f"{cost:,.0f}", "—", "—")
|
|
875
|
+
g_cost += sub_cost; g_mv += sub_mv
|
|
876
|
+
self.context.console.print(f"\n [bold]{grp}[/bold] [dim]{len(poss)} 持仓 · 成本 {sub_cost:,.0f}[/dim]")
|
|
877
|
+
self.context.console.print(tbl)
|
|
878
|
+
if have:
|
|
879
|
+
spnl = sub_mv - sub_cost; sc = "green" if spnl >= 0 else "red"
|
|
880
|
+
spct = spnl / sub_cost * 100 if sub_cost else 0.0
|
|
881
|
+
self.context.console.print(f" [dim]小计 市值 {sub_mv:,.0f} · 浮盈 [{sc}]{spnl:+,.0f} ({spct:+.1f}%)[/{sc}][/dim]")
|
|
882
|
+
gpnl = g_mv - g_cost; gc = "green" if gpnl >= 0 else "red"
|
|
883
|
+
gpct = gpnl / g_cost * 100 if g_cost else 0.0
|
|
884
|
+
self.context.console.print(f"\n [bold]合计[/bold] 成本 {g_cost:,.0f} · 市值 {g_mv:,.0f} · "
|
|
885
|
+
f"浮盈 [{gc}]{gpnl:+,.0f} ({gpct:+.1f}%)[/{gc}]")
|
|
886
|
+
self.context.console.print()
|
|
887
|
+
else:
|
|
888
|
+
for grp, poss in groups.items():
|
|
889
|
+
print(f"[{grp}]")
|
|
890
|
+
for p in poss:
|
|
891
|
+
px = live.get(p["symbol"])
|
|
892
|
+
pnl = (px * p["net_qty"] - p["cost_basis"]) if px else None
|
|
893
|
+
print(f" {p['symbol']} net={p['net_qty']:g} avg={p['avg_cost']:.2f}"
|
|
894
|
+
+ (f" pnl={pnl:+.0f}" if pnl is not None else ""))
|
|
895
|
+
|
|
896
|
+
def cmd_apply_plan(self, args: str):
|
|
897
|
+
"""Execute the pending command plan sequentially."""
|
|
898
|
+
plan = list(getattr(self.terminal, "pending_plan", []) or [])
|
|
899
|
+
arg_tokens = args.split()
|
|
900
|
+
start_idx = 0
|
|
901
|
+
if "--from" in arg_tokens:
|
|
902
|
+
idx = arg_tokens.index("--from")
|
|
903
|
+
if idx + 1 >= len(arg_tokens):
|
|
904
|
+
msg = "Usage: /apply-plan --from <step_number>"
|
|
905
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
906
|
+
return
|
|
907
|
+
try:
|
|
908
|
+
start_idx = max(0, int(arg_tokens[idx + 1]) - 1)
|
|
909
|
+
except ValueError:
|
|
910
|
+
msg = "Invalid step number for --from"
|
|
911
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
if not plan:
|
|
915
|
+
self.context.console.print("[dim]No pending plan. Use /plan first.[/dim]" if self.context.has_rich
|
|
916
|
+
else "No pending plan. Use /plan first.")
|
|
917
|
+
return
|
|
918
|
+
if start_idx > 0:
|
|
919
|
+
if start_idx >= len(plan):
|
|
920
|
+
msg = f"--from {start_idx + 1} exceeds available steps ({len(plan)})"
|
|
921
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
922
|
+
return
|
|
923
|
+
plan = plan[start_idx:]
|
|
924
|
+
if "--resume" in arg_tokens and self.context.has_rich:
|
|
925
|
+
self.context.console.print(f"[dim]Resuming execution from step 1 of remaining {len(plan)} step(s).[/dim]")
|
|
926
|
+
|
|
927
|
+
policy = self.terminal.config.get("command_policy", "safe")
|
|
928
|
+
results = []
|
|
929
|
+
failed = None
|
|
930
|
+
for i, step in enumerate(plan, 1):
|
|
931
|
+
started_at = time.time()
|
|
932
|
+
if self.context.has_rich:
|
|
933
|
+
self.context.console.print(f"[dim]Step {i}/{len(plan)}:[/dim] [bold]{step}[/bold]")
|
|
934
|
+
else:
|
|
935
|
+
print(f"Step {i}/{len(plan)}: {step}")
|
|
936
|
+
|
|
937
|
+
# Was evaluate_command_policy(step, policy) — which silently
|
|
938
|
+
# evaluated every step as workspace-write with networking on,
|
|
939
|
+
# ignoring the session's permission_mode and network_enabled.
|
|
940
|
+
# Only .risk was read (and risk is mode-independent), so nothing
|
|
941
|
+
# executed that shouldn't have; the service closes the gap before
|
|
942
|
+
# anyone reads .allowed here and gets a wrong answer.
|
|
943
|
+
from aria_code.safety import SafetyService
|
|
944
|
+
step_decision = SafetyService(self.terminal.config).evaluate_command(step, policy)
|
|
945
|
+
if step_decision.risk == "high":
|
|
946
|
+
if not self._confirm_high_risk_command(step_decision.normalized_command, step_decision.risk, policy):
|
|
947
|
+
failed = (i, step, "Cancelled by user at high-risk step confirmation")
|
|
948
|
+
results.append({
|
|
949
|
+
"step": step,
|
|
950
|
+
"status": "blocked",
|
|
951
|
+
"duration": round(time.time() - started_at, 3),
|
|
952
|
+
"exit_code": None,
|
|
953
|
+
"error": failed[2],
|
|
954
|
+
})
|
|
955
|
+
break
|
|
956
|
+
|
|
957
|
+
res = _tool_run_command({"command": step, "policy": policy})
|
|
958
|
+
duration = time.time() - started_at
|
|
959
|
+
exit_code = res.get("data", {}).get("exit_code", None) if res.get("success") else None
|
|
960
|
+
status = "completed" if res.get("success") and exit_code == 0 else "failed"
|
|
961
|
+
results.append({
|
|
962
|
+
"step": step,
|
|
963
|
+
"status": status,
|
|
964
|
+
"duration": round(duration, 3),
|
|
965
|
+
"exit_code": exit_code,
|
|
966
|
+
"error": None if status == "completed" else (res.get("error") or f"Command exited {exit_code}"),
|
|
967
|
+
})
|
|
968
|
+
if not res.get("success"):
|
|
969
|
+
failed = (i, step, res.get("error", "Unknown error"))
|
|
970
|
+
break
|
|
971
|
+
exit_code = res.get("data", {}).get("exit_code", 0)
|
|
972
|
+
if exit_code != 0:
|
|
973
|
+
failed = (i, step, f"Command exited {exit_code}")
|
|
974
|
+
break
|
|
975
|
+
|
|
976
|
+
self.terminal.last_plan_results = results
|
|
977
|
+
|
|
978
|
+
if failed:
|
|
979
|
+
idx, step, err = failed
|
|
980
|
+
self.terminal.pending_plan = plan[idx - 1:]
|
|
981
|
+
if self.context.has_rich:
|
|
982
|
+
self.context.console.print(f"[red]Plan failed at step {idx}[/red]: [bold]{step}[/bold]")
|
|
983
|
+
self.context.console.print(f"[red]{err}[/red]")
|
|
984
|
+
self.context.console.print("[dim]Recovery hints:[/dim]")
|
|
985
|
+
if "blocked by policy" in (err or "").lower():
|
|
986
|
+
self.context.console.print(" [dim]> /run --dry-run <command> to inspect risk[/dim]")
|
|
987
|
+
self.context.console.print(" [dim]> /config set command_policy=balanced (or full) if needed[/dim]")
|
|
988
|
+
else:
|
|
989
|
+
self.context.console.print(" [dim]> Fix code/config, then rerun /apply-plan[/dim]")
|
|
990
|
+
self.context.console.print(" [dim]> Use /git diff to inspect changes[/dim]")
|
|
991
|
+
else:
|
|
992
|
+
print(f"Plan failed at step {idx}: {step}\n{err}")
|
|
993
|
+
if "blocked by policy" in (err or "").lower():
|
|
994
|
+
print("Recovery: /run --dry-run <command> and /config set command_policy=balanced")
|
|
995
|
+
else:
|
|
996
|
+
print("Recovery: fix issue, then rerun /apply-plan")
|
|
997
|
+
else:
|
|
998
|
+
if self.context.has_rich:
|
|
999
|
+
self.context.console.print(f"[green]Plan completed ({len(plan)} steps)[/green]")
|
|
1000
|
+
for i, row in enumerate(results, 1):
|
|
1001
|
+
self.context.console.print(f" [dim]{i}. {row['step']} ({row['duration']}s)[/dim]")
|
|
1002
|
+
else:
|
|
1003
|
+
print(f"Plan completed ({len(plan)} steps)")
|
|
1004
|
+
self.terminal.pending_plan = []
|
|
1005
|
+
|
|
1006
|
+
async def cmd_deep(self, args: str):
|
|
1007
|
+
"""
|
|
1008
|
+
深度多层研究(Claude-Code 架构 P0–P3):
|
|
1009
|
+
团队并行 → 主题分组 → 工具深挖 → 量化融合+置信度校准 → Critic 自检 → 分级报告
|
|
1010
|
+
Usage: /deep NVDA ← 标准档
|
|
1011
|
+
/deep AAPL --deep ← 深度档(含量化地面真值/自检/数据血缘)
|
|
1012
|
+
/deep 000333 --brief ← 简报档
|
|
1013
|
+
/deep TSLA --agents technical,risk,macro
|
|
1014
|
+
/deep calibrate ← 用真实价回评历史预测,更新置信度校准
|
|
1015
|
+
"""
|
|
1016
|
+
def _latest_close(symbol: str):
|
|
1017
|
+
try:
|
|
1018
|
+
import data_cleaner
|
|
1019
|
+
df, _ = data_cleaner.get_clean_prices(symbol, period="5d")
|
|
1020
|
+
if df is not None and len(df):
|
|
1021
|
+
for col in ("close", "Close", "adj_close", "收盘"):
|
|
1022
|
+
if col in df.columns:
|
|
1023
|
+
return float(df[col].iloc[-1])
|
|
1024
|
+
except Exception:
|
|
1025
|
+
pass
|
|
1026
|
+
return None
|
|
1027
|
+
|
|
1028
|
+
# /deep calibrate — score logged predictions against realised price (P2 loop)
|
|
1029
|
+
if args.strip().lower().startswith(("calibrate", "校准")):
|
|
1030
|
+
from agents.deep.calibration_loop import (
|
|
1031
|
+
PredictionLog, evaluate_due, evaluate_from_ledger)
|
|
1032
|
+
from agents.deep.quant_fusion import CalibrationStore
|
|
1033
|
+
store, log = CalibrationStore(), PredictionLog()
|
|
1034
|
+
led_res = {"evaluated": 0, "hits": 0}
|
|
1035
|
+
try: # actual realised P&L first — the strongest ground truth
|
|
1036
|
+
from portfolio_ledger import PortfolioLedger
|
|
1037
|
+
led_res = evaluate_from_ledger(store, log, PortfolioLedger())
|
|
1038
|
+
except Exception:
|
|
1039
|
+
pass
|
|
1040
|
+
px_res = evaluate_due(store, log, _latest_close) # market price for the rest
|
|
1041
|
+
total = led_res["evaluated"] + px_res["evaluated"]
|
|
1042
|
+
hits = led_res["hits"] + px_res["hits"]
|
|
1043
|
+
if total:
|
|
1044
|
+
msg = (f"校准完成:评估 {total} 条(实盘 {led_res['evaluated']} + "
|
|
1045
|
+
f"市价 {px_res['evaluated']}),命中 {hits},"
|
|
1046
|
+
f"命中率 {hits / total:.0%}(置信度校准已更新)")
|
|
1047
|
+
else:
|
|
1048
|
+
msg = "暂无到期预测可校准(先用 /deep 跑几次分析积累预测)。"
|
|
1049
|
+
self.context.console.print(f"[green]✓[/green] {msg}") if self.context.has_rich else print(msg)
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
team_args = parse_team_args(args)
|
|
1053
|
+
symbols = resolve_team_symbols(team_args, self.terminal.config)
|
|
1054
|
+
agent_names = team_agent_names(team_args)
|
|
1055
|
+
_low = args.lower()
|
|
1056
|
+
tier = ("deep" if ("--deep" in _low or "--full" in _low)
|
|
1057
|
+
else "brief" if "--brief" in _low else "standard")
|
|
1058
|
+
_zh = sum(1 for c in args if '一' <= c <= '鿿')
|
|
1059
|
+
_lang = "zh" if _zh / max(len(args), 1) > 0.15 else "en"
|
|
1060
|
+
|
|
1061
|
+
from agents.deep.tiers import render_tier
|
|
1062
|
+
from ui.render.team import render_agent_tree_root, render_agent_node
|
|
1063
|
+
|
|
1064
|
+
for sym in symbols:
|
|
1065
|
+
def _on_agent_done(name, result):
|
|
1066
|
+
_kps = getattr(result, "key_points", None)
|
|
1067
|
+
_kp = (_kps[0] if isinstance(_kps, (list, tuple)) and _kps else "")
|
|
1068
|
+
if self.context.has_rich:
|
|
1069
|
+
render_agent_node(
|
|
1070
|
+
self.context.console, name, getattr(result, "signal", None), _kp,
|
|
1071
|
+
success=bool(getattr(result, "success", True)),
|
|
1072
|
+
error=getattr(result, "error", None),
|
|
1073
|
+
degraded=bool(getattr(result, "degraded", False)),
|
|
1074
|
+
lang=_lang,
|
|
1075
|
+
)
|
|
1076
|
+
else:
|
|
1077
|
+
print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
|
|
1078
|
+
|
|
1079
|
+
if self.context.has_rich:
|
|
1080
|
+
render_agent_tree_root(self.context.console, sym, len(agent_names), lang=_lang)
|
|
1081
|
+
else:
|
|
1082
|
+
print(f"\n ⏺ 深度分析 {sym} {len(agent_names)} 个分析师")
|
|
1083
|
+
|
|
1084
|
+
try:
|
|
1085
|
+
result = await run_deep_cli(
|
|
1086
|
+
symbol=sym, args=team_args, config=self.terminal.config,
|
|
1087
|
+
lang=_lang, on_agent_done=_on_agent_done,
|
|
1088
|
+
)
|
|
1089
|
+
except Exception as e:
|
|
1090
|
+
print_error(self.context, str(e), "deep")
|
|
1091
|
+
continue
|
|
1092
|
+
|
|
1093
|
+
md = render_tier(result, tier)
|
|
1094
|
+
if self.context.has_rich:
|
|
1095
|
+
from rich import box as _box
|
|
1096
|
+
from rich.markdown import Markdown
|
|
1097
|
+
from rich.panel import Panel
|
|
1098
|
+
self.context.console.print(Panel(
|
|
1099
|
+
Markdown(md), border_style="dim", box=_box.ROUNDED,
|
|
1100
|
+
title=f"[bold]深度研究 · {sym}[/bold] [dim]({tier})[/dim]",
|
|
1101
|
+
title_align="left", padding=(1, 2),
|
|
1102
|
+
))
|
|
1103
|
+
else:
|
|
1104
|
+
print("\n" + md)
|
|
1105
|
+
|
|
1106
|
+
# P2 closed loop: log the verdict so /deep calibrate can score it later
|
|
1107
|
+
try:
|
|
1108
|
+
from agents.deep.calibration_loop import PredictionLog
|
|
1109
|
+
_p = _latest_close(sym)
|
|
1110
|
+
if _p and result.final_signal:
|
|
1111
|
+
PredictionLog().log(sym, result.final_signal,
|
|
1112
|
+
result.calibrated_confidence, _p)
|
|
1113
|
+
except Exception:
|
|
1114
|
+
pass
|
|
1115
|
+
|
|
1116
|
+
async def cmd_team(self, args: str):
|
|
1117
|
+
"""
|
|
1118
|
+
多 Agent 金融研究团队:宏观 + 基本面 + 技术 + 风控 → 综合报告
|
|
1119
|
+
Usage: /team NVDA
|
|
1120
|
+
/team 000333 --agents technical,risk
|
|
1121
|
+
/team watchlist
|
|
1122
|
+
/team AAPL --full ← 7-agent 完整模式(+新闻/催化剂/行业)
|
|
1123
|
+
/team AAPL --pipeline ← 开启 DAG 串行协作模式(Context Sharing)
|
|
1124
|
+
"""
|
|
1125
|
+
|
|
1126
|
+
import sys as _sys
|
|
1127
|
+
team_args = parse_team_args(args)
|
|
1128
|
+
symbols = resolve_team_symbols(team_args, self.terminal.config)
|
|
1129
|
+
agent_names = team_agent_names(team_args)
|
|
1130
|
+
# The UI language, unless the request itself is Chinese. Guessing from
|
|
1131
|
+
# the arguments alone made "/team AAPL" English in a Chinese session.
|
|
1132
|
+
_zh = sum(1 for c in args if '一' <= c <= '鿿')
|
|
1133
|
+
_ui_zh = str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh")
|
|
1134
|
+
_lang = "zh" if (_zh / max(len(args), 1) > 0.15 or _ui_zh) else "en"
|
|
1135
|
+
_T = (lambda a, b: a) if _lang == "zh" else (lambda a, b: b)
|
|
1136
|
+
|
|
1137
|
+
for sym in symbols:
|
|
1138
|
+
_agent_count = len(agent_names)
|
|
1139
|
+
|
|
1140
|
+
# ── Streaming nested agent tree (Claude Code-style) ──────────────
|
|
1141
|
+
from ui.render.team import (
|
|
1142
|
+
render_agent_tree_root, render_agent_node,
|
|
1143
|
+
render_agent_synthesis_leaf,
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
def _on_agent_done(name, result):
|
|
1147
|
+
# Fires as each analyst finishes — render its leaf live.
|
|
1148
|
+
_kp = ""
|
|
1149
|
+
_kps = getattr(result, "key_points", None)
|
|
1150
|
+
if _kps:
|
|
1151
|
+
_kp = _kps[0] if isinstance(_kps, (list, tuple)) else str(_kps)
|
|
1152
|
+
if self.context.has_rich:
|
|
1153
|
+
render_agent_node(
|
|
1154
|
+
self.context.console, name,
|
|
1155
|
+
getattr(result, "signal", None), _kp,
|
|
1156
|
+
success=bool(getattr(result, "success", True)),
|
|
1157
|
+
error=getattr(result, "error", None),
|
|
1158
|
+
degraded=bool(getattr(result, "degraded", False)),
|
|
1159
|
+
lang=_lang,
|
|
1160
|
+
)
|
|
1161
|
+
else:
|
|
1162
|
+
print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
|
|
1163
|
+
|
|
1164
|
+
if self.context.has_rich:
|
|
1165
|
+
render_agent_tree_root(self.context.console, sym, _agent_count, lang=_lang)
|
|
1166
|
+
else:
|
|
1167
|
+
print(f"\n ⏺ 多代理分析 {sym} {_agent_count} 个分析师并行")
|
|
1168
|
+
|
|
1169
|
+
try:
|
|
1170
|
+
# Create a stream consumer for tool rendering and thinking animations
|
|
1171
|
+
from apps.cli.runtime_consumer import TerminalRuntimeEventConsumer
|
|
1172
|
+
consumer = TerminalRuntimeEventConsumer(
|
|
1173
|
+
terminal=self.terminal,
|
|
1174
|
+
console=self.context.console,
|
|
1175
|
+
has_rich=self.context.has_rich,
|
|
1176
|
+
markdown_cls=None,
|
|
1177
|
+
live_cls=None,
|
|
1178
|
+
strip_latex=lambda x: x,
|
|
1179
|
+
fallback_from="local",
|
|
1180
|
+
ui_lang=_lang,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# ── 新 Agent 系统(无 Ollama 依赖)────────────────────────
|
|
1184
|
+
_analysis = await run_team_analysis(
|
|
1185
|
+
symbol=sym,
|
|
1186
|
+
args=team_args,
|
|
1187
|
+
config=self.terminal.config,
|
|
1188
|
+
sanitize_result=_sanitize_team_result_with_market_data,
|
|
1189
|
+
lang=_lang,
|
|
1190
|
+
on_agent_done=_on_agent_done,
|
|
1191
|
+
on_token=consumer.on_token,
|
|
1192
|
+
on_thought=consumer.on_thinking,
|
|
1193
|
+
on_tool_start=consumer.on_tool_call,
|
|
1194
|
+
on_tool_end=consumer.on_tool_result,
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# A tool whose result never came back leaves its spinner up,
|
|
1198
|
+
# and the prompt does not return while it runs.
|
|
1199
|
+
consumer.close_tool_spinner()
|
|
1200
|
+
team_result = _analysis.team_result
|
|
1201
|
+
_data_bundle = _analysis.data_bundle
|
|
1202
|
+
_quality_notes = _analysis.quality_notes or []
|
|
1203
|
+
|
|
1204
|
+
if self.context.has_rich:
|
|
1205
|
+
# Synthesis leaf closes the tree, then the detailed _get_Panel()
|
|
1206
|
+
render_agent_synthesis_leaf(
|
|
1207
|
+
self.context.console,
|
|
1208
|
+
team_result.final_signal,
|
|
1209
|
+
team_result.confidence,
|
|
1210
|
+
team_result.elapsed_sec,
|
|
1211
|
+
lang=_lang,
|
|
1212
|
+
)
|
|
1213
|
+
if _quality_notes:
|
|
1214
|
+
from rich.markup import escape as _esc_q
|
|
1215
|
+
self.context.console.print(
|
|
1216
|
+
f" [yellow]{_T('数据质量警告', 'Data quality')}:[/yellow] "
|
|
1217
|
+
+ _esc_q("; ".join(_quality_notes[:3]))
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
# Signal divergence notice — only when DebateAgent ran
|
|
1221
|
+
_has_debate = any(
|
|
1222
|
+
getattr(r, "agent", "") == "debate"
|
|
1223
|
+
for r in (team_result.results or [])
|
|
1224
|
+
)
|
|
1225
|
+
if _has_debate:
|
|
1226
|
+
self.context.console.print(
|
|
1227
|
+
f" [#C08050]🔥 {_T('信号分歧已触发 DebateAgent 调解', 'Signals disagreed; the debate agent weighed in')}[/#C08050]"
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
# Synthesis in a _get_Panel() for visual separation
|
|
1231
|
+
from rich import box as _rbox_team
|
|
1232
|
+
from ui.render.team import SIGNAL_COLORS as _SC, VERDICT_STYLE as _VS
|
|
1233
|
+
from apps.cli.commands.team import (
|
|
1234
|
+
build_team_terminal_summary as _team_terminal_summary,
|
|
1235
|
+
clean_team_synthesis_text as _clean_team_synthesis,
|
|
1236
|
+
)
|
|
1237
|
+
from rich.markup import escape as _esc_syn
|
|
1238
|
+
_syn = _esc_syn(_clean_team_synthesis(
|
|
1239
|
+
team_result.synthesis or _T("(无综合结论)", "(no synthesis)")))
|
|
1240
|
+
_market_summary = _team_terminal_summary(_data_bundle, lang=_lang)
|
|
1241
|
+
_elapsed = f" [dim]{_T('耗时', 'took')} {team_result.elapsed_sec:.1f}s[/dim]"
|
|
1242
|
+
_sig_str = team_result.final_signal or ""
|
|
1243
|
+
_conf_str = (f" [dim]{_T('置信度', 'confidence')} {team_result.confidence:.0%}[/dim]"
|
|
1244
|
+
if team_result.confidence else "")
|
|
1245
|
+
_sig_color = _SC.get(_sig_str.upper(), "dim")
|
|
1246
|
+
_sig_icon = _VS.get(_sig_str.upper(), ("dim", "●"))[1]
|
|
1247
|
+
_footer = (f"[{_sig_color}]{_sig_icon} {_sig_str}[/{_sig_color}]"
|
|
1248
|
+
f"{_conf_str}{_elapsed}")
|
|
1249
|
+
self.context.console.print(Panel(
|
|
1250
|
+
f"{_market_summary}\n\n{_syn}\n\n{_footer}",
|
|
1251
|
+
title=f"[bold]{_T('综合结论', 'Conclusion')}[/bold]",
|
|
1252
|
+
box=_rbox_team.ROUNDED,
|
|
1253
|
+
border_style="#C08050",
|
|
1254
|
+
padding=(0, 1),
|
|
1255
|
+
))
|
|
1256
|
+
else:
|
|
1257
|
+
# agents already streamed via _on_agent_done (plain print)
|
|
1258
|
+
if _quality_notes:
|
|
1259
|
+
print(f" {_T('数据质量警告', 'Data quality')}: " + "; ".join(_quality_notes[:3]))
|
|
1260
|
+
print(f"\n ── {_T('综合结论', 'Conclusion')} ──")
|
|
1261
|
+
from apps.cli.commands.team import (
|
|
1262
|
+
build_team_terminal_summary as _team_terminal_summary,
|
|
1263
|
+
clean_team_synthesis_text as _clean_team_synthesis,
|
|
1264
|
+
)
|
|
1265
|
+
print(_team_terminal_summary(_data_bundle, lang=_lang))
|
|
1266
|
+
print()
|
|
1267
|
+
print(_clean_team_synthesis(team_result.synthesis or _T("(无综合结论)", "(no synthesis)")))
|
|
1268
|
+
print(f"\n {_T('耗时', 'took')} {team_result.elapsed_sec:.1f}s "
|
|
1269
|
+
f"Signal: {team_result.final_signal} "
|
|
1270
|
+
f"{_T('置信度', 'confidence')}: {team_result.confidence:.0%}")
|
|
1271
|
+
|
|
1272
|
+
# 保存报告
|
|
1273
|
+
await self._save_team_report(sym, team_result, _data_bundle, _quality_notes, lang=_lang)
|
|
1274
|
+
|
|
1275
|
+
# Record the directional call for outcome verification (DPO loop).
|
|
1276
|
+
# synthesis + final_signal → detect_direction; entry price fetched
|
|
1277
|
+
# by _record_prediction. Best-effort, never blocks.
|
|
1278
|
+
try:
|
|
1279
|
+
_call_text = f"{team_result.synthesis or ''} {team_result.final_signal or ''}"
|
|
1280
|
+
self.terminal._record_prediction(sym, _call_text)
|
|
1281
|
+
except Exception:
|
|
1282
|
+
pass
|
|
1283
|
+
|
|
1284
|
+
except ImportError as _imp_err:
|
|
1285
|
+
# agents 包不可用 — 不再回退到已废弃的 financial_agents
|
|
1286
|
+
_m = (f"多代理分析模块加载失败:{_imp_err}。"
|
|
1287
|
+
"请确认 agents 包完整(/install 或 pip install -e .)。")
|
|
1288
|
+
self.context.console.print(f"\n [red]{_m}[/red]") if self.context.has_rich else print(f"\n {_m}")
|
|
1289
|
+
continue
|
|
1290
|
+
except Exception as e:
|
|
1291
|
+
if "consumer" in locals():
|
|
1292
|
+
consumer.close_tool_spinner()
|
|
1293
|
+
msg = f"团队分析失败: {e}" if _lang == "zh" else f"Team analysis failed: {e}"
|
|
1294
|
+
self.context.console.print(f"\n [red]{msg}[/red]") if self.context.has_rich else print(f"\n {msg}")
|
|
1295
|
+
continue
|
|
1296
|
+
|
|
1297
|
+
async def _save_team_report(self, symbol: str, team_result, data_bundle=None, quality_notes: Optional[list] = None,
|
|
1298
|
+
lang: str = "zh") -> None:
|
|
1299
|
+
"""将 /team 分析结果保存为 Markdown 报告"""
|
|
1300
|
+
saved = save_team_report(
|
|
1301
|
+
symbol=symbol,
|
|
1302
|
+
team_result=team_result,
|
|
1303
|
+
data_bundle=data_bundle,
|
|
1304
|
+
quality_notes=quality_notes,
|
|
1305
|
+
)
|
|
1306
|
+
try:
|
|
1307
|
+
parts = saved.path.parts
|
|
1308
|
+
short_path = "/".join(parts[-5:]) if len(parts) > 5 else str(saved.path)
|
|
1309
|
+
except Exception:
|
|
1310
|
+
short_path = str(saved.path)
|
|
1311
|
+
label = "报告已保存" if str(lang).startswith("zh") else "Report saved"
|
|
1312
|
+
if self.context.has_rich:
|
|
1313
|
+
self.context.console.print(
|
|
1314
|
+
f" [dim]{label}:[/dim] [link={_file_uri(saved.path)}]…/{short_path}[/link]")
|
|
1315
|
+
else:
|
|
1316
|
+
print(f" {label}: .../{short_path}")
|