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,977 @@
|
|
|
1
|
+
"""pdf_report.py — 通用"结构化报告 → 设计过的 PDF"渲染引擎。
|
|
2
|
+
|
|
3
|
+
背景:`report_generator.py` 已经有 `export_pdf()`(weasyprint/wkhtmltopdf 通用
|
|
4
|
+
HTML→PDF 转换,直接复用,不重写)和 `_md_to_html()`,但它的 `_build_html()`
|
|
5
|
+
是写死给单股票分析报告用的;`ui.py` 的 `get_ui_css_base()` 是给 LLM 现场生成
|
|
6
|
+
HTML 用的 Bloomberg 黑橙配色 token;`backtest_report.py` 又是第三套配色。三者
|
|
7
|
+
互不复用,也都没有"任意报告 → 好看的 PDF"的通用能力。
|
|
8
|
+
|
|
9
|
+
这个模块提供的是中间层:一个与具体报告内容无关的 `Document`(title + 若干
|
|
10
|
+
`Section`),每个 Section 有一个 `kind`(今天先实现短线报告实际用到的几种:
|
|
11
|
+
stats/badge_table/change_columns/card_grid/row_list/data_table/detail_cards/
|
|
12
|
+
news_cards/text),配合一个 `Theme`(配色/字体/密度)渲染成 HTML,再交给
|
|
13
|
+
report_generator.export_pdf() 转 PDF。新增一种报告类型,只需要写一个"解析成
|
|
14
|
+
Document"的函数(参考 parse_shortterm_report),不需要重新设计 CSS。
|
|
15
|
+
|
|
16
|
+
用法(命令行侧见 apps/cli/commands/pdf_export_cmds.py 的 /export-pdf):
|
|
17
|
+
doc = parse_shortterm_report(md_text)
|
|
18
|
+
html = render_document(doc, THEMES["institutional"])
|
|
19
|
+
Path("out.html").write_text(html)
|
|
20
|
+
from report_generator import export_pdf
|
|
21
|
+
export_pdf(Path("out.html"))
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import html as _html
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any, Callable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def esc(s: str) -> str:
|
|
32
|
+
return _html.escape(str(s or ""), quote=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def strip_md_bold(s: str) -> str:
|
|
36
|
+
return re.sub(r"\*\*(.*?)\*\*", r"\1", s or "")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# weasyprint (Pango/cairo) has no color-emoji glyph support and no emoji font
|
|
40
|
+
# in this theme's font stack — when an emoji leaks into plain rendered text,
|
|
41
|
+
# cairo substitutes a glyph from whatever monochrome fallback font is on the
|
|
42
|
+
# system, with different font metrics than the surrounding text, so it
|
|
43
|
+
# renders visibly offset from the baseline ("floating" above/beside the
|
|
44
|
+
# line). The fix everywhere in this file is either (a) convert the emoji to
|
|
45
|
+
# its structured {tone, label} form and render it as a `.badge`/`.tag` pill
|
|
46
|
+
# (see _signal_tone/_badge_html — the correct path, used by sections that
|
|
47
|
+
# render cleanly), or (b) strip it outright from any freeform text that has
|
|
48
|
+
# no such structured equivalent. This regex is the fallback for (b): a broad
|
|
49
|
+
# Unicode emoji-block range rather than an enumerated character class, so it
|
|
50
|
+
# doesn't silently miss new emoji the report starts using later.
|
|
51
|
+
_EMOJI_RE = re.compile(
|
|
52
|
+
"["
|
|
53
|
+
"\U0001F300-\U0001FAFF" # misc symbols/pictographs, emoticons, transport, supplemental symbols
|
|
54
|
+
"\U00002600-\U000027BF" # misc symbols, dingbats (⚠️☀✅ etc.)
|
|
55
|
+
"\U00002B00-\U00002BFF" # misc symbols and arrows — NOT the same block as the
|
|
56
|
+
# single-arrow ↑/↓/→ (U+2190-21FF) this report's
|
|
57
|
+
# "均线" trend column relies on; this block is stars/
|
|
58
|
+
# squares (⭐U+2B50, ⬜U+2B1C) only, safe to strip
|
|
59
|
+
"\U0001F100-\U0001F1FF" # enclosed alphanumeric supplement (🆕🆓🆙🆒🆗 etc.)
|
|
60
|
+
# + regional indicators (flag pairs) — same block,
|
|
61
|
+
# found missing 2026-07-21 when 🆕 (U+1F195) leaked
|
|
62
|
+
# through into a section-column label and dragged
|
|
63
|
+
# in an Apple-Color-Emoji-Bold font embed even
|
|
64
|
+
# though every other emoji in the doc was clean
|
|
65
|
+
"\U0000FE0F" # variation selector-16 (emoji presentation)
|
|
66
|
+
"\U0000200D" # zero-width joiner
|
|
67
|
+
"]+"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def strip_emoji(s: str) -> str:
|
|
72
|
+
return _EMOJI_RE.sub("", str(s or "")).strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── 数据模型 ──────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Section:
|
|
79
|
+
kind: str
|
|
80
|
+
title: str
|
|
81
|
+
icon: str = "•"
|
|
82
|
+
note: str = ""
|
|
83
|
+
content: Any = None
|
|
84
|
+
new_page: bool = True # 大部分 section 独立起一页;小的(如 stats)可设 False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class Document:
|
|
89
|
+
title: str
|
|
90
|
+
meta: str = ""
|
|
91
|
+
warning: str = ""
|
|
92
|
+
sections: list[Section] = field(default_factory=list)
|
|
93
|
+
footer: str = ""
|
|
94
|
+
|
|
95
|
+
def select(self, include: list[str] | None = None, exclude: list[str] | None = None) -> "Document":
|
|
96
|
+
"""按 section 标题子串过滤(内容取舍)。include 为空表示全选。"""
|
|
97
|
+
secs = self.sections
|
|
98
|
+
if include:
|
|
99
|
+
secs = [s for s in secs if any(k.lower() in s.title.lower() for k in include)]
|
|
100
|
+
if exclude:
|
|
101
|
+
secs = [s for s in secs if not any(k.lower() in s.title.lower() for k in exclude)]
|
|
102
|
+
return Document(title=self.title, meta=self.meta, warning=self.warning,
|
|
103
|
+
sections=secs, footer=self.footer)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class Theme:
|
|
108
|
+
name: str
|
|
109
|
+
bg: str
|
|
110
|
+
surface: str # 卡片/表头以外的浅底色
|
|
111
|
+
text: str
|
|
112
|
+
text_muted: str
|
|
113
|
+
border: str
|
|
114
|
+
accent: str # section 图标底色 / 表头底色
|
|
115
|
+
buy: str; sell: str; hold: str; reduce: str
|
|
116
|
+
buy_bg: str; sell_bg: str; hold_bg: str; reduce_bg: str
|
|
117
|
+
font: str
|
|
118
|
+
mono: str
|
|
119
|
+
radius: str = "3px"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
THEMES: dict[str, Theme] = {
|
|
123
|
+
# 浅色研报风格 —— 面向对外交付(尽调材料/合规文档同调性)
|
|
124
|
+
"institutional": Theme(
|
|
125
|
+
name="institutional",
|
|
126
|
+
bg="#ffffff", surface="#f8f9fb", text="#1c2230", text_muted="#6b7280", border="#e7e9ee",
|
|
127
|
+
accent="#1f3a5f",
|
|
128
|
+
buy="#16803c", sell="#c81e1e", hold="#5b6472", reduce="#b4650a",
|
|
129
|
+
buy_bg="#e8f8ee", sell_bg="#fde8e8", hold_bg="#eef0f3", reduce_bg="#fef3e0",
|
|
130
|
+
# STHeiti first, not PingFang SC: PingFang ships as a multi-face .ttc
|
|
131
|
+
# (TrueType Collection) — see fc-list — and weasyprint has to extract/
|
|
132
|
+
# resubset a single face out of that container into a standalone
|
|
133
|
+
# CID-keyed CFF font for PDF embedding. That conversion isn't something
|
|
134
|
+
# Apple's own font ever shipped as; poppler-based readers parse the
|
|
135
|
+
# result fine, but some other PDF viewers mis-render it as substituted/
|
|
136
|
+
# wrong glyphs (coherent-looking nonsense, not blank characters — the
|
|
137
|
+
# signature of a glyph-index mismatch, not missing content). STHeiti
|
|
138
|
+
# is a single-file font with no .ttc-extraction step, avoiding the
|
|
139
|
+
# whole failure class. Less refined-looking than PingFang, but correct
|
|
140
|
+
# everywhere beats good-looking in one viewer and broken in another.
|
|
141
|
+
font='"STHeiti", "PingFang SC", "Helvetica Neue", Arial, sans-serif',
|
|
142
|
+
mono='"SF Mono", Menlo, Consolas, monospace',
|
|
143
|
+
),
|
|
144
|
+
# 深色 Bloomberg 终端风格 —— 沿用 apps/cli/prompts/ui.py 的黑橙配色 token,
|
|
145
|
+
# 跟 /ui 生成的仪表盘、dashboard_generator.py 保持视觉一致
|
|
146
|
+
"bloomberg": Theme(
|
|
147
|
+
name="bloomberg",
|
|
148
|
+
bg="#000000", surface="#111111", text="#E8E9EA", text_muted="#808080", border="#2A2A2A",
|
|
149
|
+
accent="#F5A623",
|
|
150
|
+
buy="#00CC66", sell="#FF3B3B", hold="#4A9EFF", reduce="#FFB800",
|
|
151
|
+
buy_bg="#00331a", sell_bg="#330d0d", hold_bg="#0d1a26", reduce_bg="#332400",
|
|
152
|
+
font='"IBM Plex Sans", "STHeiti", "PingFang SC", "Helvetica Neue", sans-serif',
|
|
153
|
+
mono='"IBM Plex Mono", "SF Mono", Menlo, monospace',
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ── CSS(按 Theme 参数化,两套主题共用同一份布局规则) ──────────────────────────
|
|
159
|
+
|
|
160
|
+
def build_css(t: Theme) -> str:
|
|
161
|
+
return f"""
|
|
162
|
+
/* @page 的 margin 区域(页眉页脚跑马灯文字所在的地方)默认不继承 body 的
|
|
163
|
+
背景色 —— 深色主题下会在页面上下各留一道刺眼的白边,这里显式把整张纸
|
|
164
|
+
(html 根元素)的背景设成主题色,margin 区域才会跟内容区一致。 */
|
|
165
|
+
html {{ background: {t.bg}; }}
|
|
166
|
+
@page {{
|
|
167
|
+
size: A4;
|
|
168
|
+
margin: 20mm 16mm 18mm 16mm;
|
|
169
|
+
background: {t.bg};
|
|
170
|
+
@top-center {{ content: "{{TITLE}}"; font-family: {t.font}; font-size: 8.5pt; color: {t.text_muted}; }}
|
|
171
|
+
@bottom-right {{ content: counter(page) " / " counter(pages); font-family: {t.font}; font-size: 8.5pt; color: {t.text_muted}; }}
|
|
172
|
+
}}
|
|
173
|
+
* {{ box-sizing: border-box; }}
|
|
174
|
+
body {{ font-family: {t.font}; color: {t.text}; background: {t.bg}; font-size: 9.5pt; line-height: 1.55; }}
|
|
175
|
+
.mono {{ font-family: {t.mono}; }}
|
|
176
|
+
|
|
177
|
+
.cover-title {{ font-size: 24pt; font-weight: 700; color: {t.text}; margin: 0 0 3mm 0; letter-spacing: -0.3px; }}
|
|
178
|
+
.cover-meta {{ font-size: 9pt; color: {t.text_muted}; margin-bottom: 3mm; }}
|
|
179
|
+
.warning-banner {{ background: {t.reduce_bg}; border-left: 3px solid {t.reduce}; color: {t.reduce};
|
|
180
|
+
padding: 2.5mm 4mm; border-radius: 2px; font-size: 8.8pt; margin-bottom: 6mm; }}
|
|
181
|
+
|
|
182
|
+
.section {{ margin-top: 8mm; }}
|
|
183
|
+
.section.new-page {{ break-before: page; }}
|
|
184
|
+
.section-head {{ display: flex; align-items: center; gap: 2.5mm; border-bottom: 1.5px solid {t.accent};
|
|
185
|
+
padding-bottom: 2mm; margin-bottom: 4mm; }}
|
|
186
|
+
.section-icon {{ width: 5.5mm; height: 5.5mm; border-radius: 50%; background: {t.accent}; color: {t.bg};
|
|
187
|
+
font-size: 7.5pt; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }}
|
|
188
|
+
.section-title {{ font-size: 13pt; font-weight: 700; color: {t.text}; }}
|
|
189
|
+
.section-note {{ font-size: 8.3pt; color: {t.text_muted}; background: {t.surface}; border-radius: 2px;
|
|
190
|
+
padding: 2mm 3mm; margin-bottom: 4mm; border-left: 2px solid {t.border}; }}
|
|
191
|
+
|
|
192
|
+
.stat-grid {{ display: flex; flex-wrap: wrap; gap: 3mm; margin-bottom: 4mm; }}
|
|
193
|
+
.stat-card {{ flex: 1 1 27mm; min-width: 27mm; background: {t.surface}; border: 1px solid {t.border};
|
|
194
|
+
border-radius: {t.radius}; padding: 3mm 3.5mm; break-inside: avoid; }}
|
|
195
|
+
.stat-card .label {{ font-size: 7.8pt; color: {t.text_muted}; margin-bottom: 1mm; }}
|
|
196
|
+
.stat-card .value {{ font-size: 13pt; font-weight: 700; color: {t.text}; }}
|
|
197
|
+
.stat-card.buy .value {{ color: {t.buy}; }}
|
|
198
|
+
.stat-card.sell .value {{ color: {t.sell}; }}
|
|
199
|
+
.meta-table {{ width: 100%; border-collapse: collapse; font-size: 8.6pt; margin-bottom: 2mm; }}
|
|
200
|
+
.meta-table td {{ padding: 1.6mm 2mm; border-bottom: 1px solid {t.border}; vertical-align: top; }}
|
|
201
|
+
.meta-table td:first-child {{ color: {t.text_muted}; width: 32mm; white-space: nowrap; }}
|
|
202
|
+
|
|
203
|
+
.badge {{ display: inline-block; padding: 0.6mm 2.2mm; border-radius: 8px; font-size: 8pt; font-weight: 600; white-space: nowrap; }}
|
|
204
|
+
.badge.buy {{ background: {t.buy_bg}; color: {t.buy}; }}
|
|
205
|
+
.badge.buy2 {{ background: {t.buy}; color: {t.bg}; }}
|
|
206
|
+
.badge.hold {{ background: {t.hold_bg}; color: {t.hold}; }}
|
|
207
|
+
.badge.reduce {{ background: {t.reduce_bg}; color: {t.reduce}; }}
|
|
208
|
+
.badge.sell {{ background: {t.sell_bg}; color: {t.sell}; }}
|
|
209
|
+
.tag {{ display: inline-block; padding: 0.4mm 1.8mm; border-radius: 6px; font-size: 7.6pt; font-weight: 600; }}
|
|
210
|
+
.tag.up {{ background: {t.sell_bg}; color: {t.sell}; }}
|
|
211
|
+
.tag.down {{ background: {t.sell_bg}; color: {t.sell}; }}
|
|
212
|
+
.tag.pos {{ background: {t.buy_bg}; color: {t.buy}; }}
|
|
213
|
+
.tag.neutral {{ background: {t.hold_bg}; color: {t.hold}; }}
|
|
214
|
+
.tag.confirm {{ background: {t.hold_bg}; color: {t.hold}; }}
|
|
215
|
+
|
|
216
|
+
table.gtable {{ width: 100%; border-collapse: collapse; font-size: 8.5pt; }}
|
|
217
|
+
table.gtable th {{ background: {t.accent}; color: {t.bg}; text-align: left; font-weight: 600; padding: 2mm 2.2mm; font-size: 8pt; }}
|
|
218
|
+
table.gtable td {{ padding: 1.9mm 2.2mm; border-bottom: 1px solid {t.border}; vertical-align: middle; }}
|
|
219
|
+
table.gtable tr:nth-child(even) td {{ background: {t.surface}; }}
|
|
220
|
+
table.gtable tr {{ break-inside: avoid; }}
|
|
221
|
+
table.gtable .name-cell {{ font-weight: 600; color: {t.text}; }}
|
|
222
|
+
table.gtable .code {{ color: {t.text_muted}; font-size: 7.6pt; }}
|
|
223
|
+
table.gtable.compact {{ font-size: 7.4pt; }}
|
|
224
|
+
table.gtable.compact th {{ padding: 1.6mm 1.3mm; font-size: 7.1pt; text-align: center; }}
|
|
225
|
+
table.gtable.compact td {{ padding: 1.4mm 1.3mm; text-align: center; }}
|
|
226
|
+
table.gtable .up-txt {{ color: {t.sell}; }}
|
|
227
|
+
table.gtable .down-txt {{ color: {t.buy}; }}
|
|
228
|
+
|
|
229
|
+
.change-cols {{ display: flex; gap: 4mm; }}
|
|
230
|
+
.change-col {{ flex: 1; }}
|
|
231
|
+
.change-col h4 {{ font-size: 9.3pt; margin: 0 0 2mm 0; color: {t.text}; }}
|
|
232
|
+
.change-col ul {{ margin: 0; padding-left: 4mm; font-size: 8.4pt; }}
|
|
233
|
+
.change-col li {{ margin-bottom: 1.3mm; break-inside: avoid; }}
|
|
234
|
+
|
|
235
|
+
/* 卡片网格用 CSS columns 而不是 flex-wrap —— weasyprint 的 flexbox 分页/
|
|
236
|
+
分片实现有个已验证过的真实 bug:同样内容前面只要有一段稍微复杂点的封面区块
|
|
237
|
+
(标题+meta+警示条),紧跟着的 flex-wrap 卡片网格就会被整体错误地推到下一页,
|
|
238
|
+
哪怕当前页还有大片空白。CSS columns 是印刷排版的标准工具,weasyprint 对它的
|
|
239
|
+
分页支持成熟得多,同样的内容不会触发这个问题(用 /tmp 下 15 轮最小复现实验
|
|
240
|
+
二分定位到的,不是瞎猜的)。*/
|
|
241
|
+
.card-grid {{ columns: 2; column-gap: 2.5mm; }}
|
|
242
|
+
.card {{ border: 1px solid {t.border}; border-left: 3px solid {t.border};
|
|
243
|
+
border-radius: 2px; padding: 2.2mm 3mm; break-inside: avoid; font-size: 8.2pt; background: {t.bg};
|
|
244
|
+
margin-bottom: 2.5mm; }}
|
|
245
|
+
.card.buy {{ border-left-color: {t.buy}; }}
|
|
246
|
+
.card.sell {{ border-left-color: {t.sell}; }}
|
|
247
|
+
.card.reduce {{ border-left-color: {t.reduce}; }}
|
|
248
|
+
.card .head {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.3mm; }}
|
|
249
|
+
.card .head .name {{ font-weight: 700; color: {t.text}; }}
|
|
250
|
+
.card .head .score {{ color: {t.text_muted}; font-size: 7.8pt; }}
|
|
251
|
+
.card .line {{ color: {t.text_muted}; margin-top: 0.8mm; }}
|
|
252
|
+
|
|
253
|
+
/* 同样避开 flex:当 .detail 文本长到要换行时,flex 行内的换行会在分页边界
|
|
254
|
+
被从中间切开(子元素各自独立分页,不是整行一起移动)。改用普通块级 +
|
|
255
|
+
inline-block 定宽标签,避免这个问题。 */
|
|
256
|
+
.row-list {{ font-size: 8.2pt; }}
|
|
257
|
+
.row-list .row {{ padding: 1.6mm 0; border-bottom: 1px solid {t.border}; break-inside: avoid; }}
|
|
258
|
+
.row-list .row .name {{ font-weight: 600; display: inline-block; width: 30mm; vertical-align: top; }}
|
|
259
|
+
.row-list .row .meta {{ color: {t.text_muted}; display: inline-block; width: 14mm; vertical-align: top; }}
|
|
260
|
+
.row-list .row .detail {{ color: {t.text_muted}; display: inline; }}
|
|
261
|
+
|
|
262
|
+
.detail-grid {{ columns: 2; column-gap: 3.5mm; }}
|
|
263
|
+
.detail-card {{ border: 1px solid {t.border}; border-radius: 3px;
|
|
264
|
+
overflow: hidden; break-inside: avoid; margin-bottom: 3.5mm; }}
|
|
265
|
+
.detail-card .dc-head {{ background: {t.buy_bg}; padding: 2.2mm 3.5mm; display: flex; justify-content: space-between;
|
|
266
|
+
align-items: center; border-bottom: 1px solid {t.border}; }}
|
|
267
|
+
.detail-card .dc-head .name {{ font-size: 10.5pt; font-weight: 700; color: {t.text}; }}
|
|
268
|
+
.detail-card .dc-head .code {{ font-size: 8pt; color: {t.text_muted}; margin-left: 1.5mm; }}
|
|
269
|
+
.detail-card .dc-body {{ padding: 2.5mm 3.5mm; }}
|
|
270
|
+
.kv-grid {{ display: flex; flex-wrap: wrap; }}
|
|
271
|
+
.kv-grid .item {{ width: 50%; padding: 1mm 0; font-size: 8.3pt; }}
|
|
272
|
+
.kv-grid .item .k {{ color: {t.text_muted}; font-size: 7.6pt; }}
|
|
273
|
+
.kv-grid .item .v {{ color: {t.text}; font-weight: 600; }}
|
|
274
|
+
.callout {{ margin-top: 1.5mm; background: {t.reduce_bg}; color: {t.reduce}; font-size: 7.9pt;
|
|
275
|
+
padding: 1.8mm 2.5mm; border-radius: 2px; }}
|
|
276
|
+
|
|
277
|
+
.news-item {{ border: 1px solid {t.border}; border-radius: 2px; padding: 2.5mm 3.5mm; margin-bottom: 2.5mm; break-inside: avoid; }}
|
|
278
|
+
.news-item .n-head {{ display: flex; align-items: center; gap: 2mm; margin-bottom: 1.3mm; }}
|
|
279
|
+
.news-item .n-head .name {{ font-weight: 700; color: {t.text}; font-size: 9.3pt; }}
|
|
280
|
+
.news-item .kw {{ font-size: 7.8pt; color: {t.text_muted}; margin-bottom: 1.3mm; }}
|
|
281
|
+
.news-item .article {{ font-size: 7.9pt; margin: 0.8mm 0; color: {t.text}; }}
|
|
282
|
+
.news-item .article .src {{ color: {t.text_muted}; }}
|
|
283
|
+
.news-item .article a {{ color: {t.text}; }}
|
|
284
|
+
|
|
285
|
+
.footer-note {{ text-align: center; color: {t.text_muted}; font-size: 8pt; margin-top: 10mm; font-style: italic; }}
|
|
286
|
+
|
|
287
|
+
.text-block {{ color: {t.text}; font-size: 9pt; line-height: 1.7; }}
|
|
288
|
+
.text-block strong {{ color: {t.text}; font-weight: 700; }}
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── 各 section kind 的渲染器 ──────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
def _r_stats(content: dict, t: Theme) -> str:
|
|
295
|
+
cards = "".join(
|
|
296
|
+
f'<div class="stat-card {c.get("tone","")}"><div class="label">{esc(c["label"])}</div>'
|
|
297
|
+
f'<div class="value">{esc(c["value"])}</div></div>'
|
|
298
|
+
for c in content.get("cards", [])
|
|
299
|
+
)
|
|
300
|
+
meta = "".join(f"<tr><td>{esc(m['label'])}</td><td>{esc(m['value'])}</td></tr>" for m in content.get("meta", []))
|
|
301
|
+
return f'<div class="stat-grid">{cards}</div><table class="meta-table">{meta}</table>'
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _badge_html(tone: str, label: str) -> str:
|
|
305
|
+
cls = "buy2" if tone == "buy2" else tone
|
|
306
|
+
return f'<span class="badge {cls}">{esc(label)}</span>'
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _tone_cell_html(v: dict, as_badge: bool) -> str:
|
|
310
|
+
if as_badge:
|
|
311
|
+
return _badge_html(v["tone"], v["label"])
|
|
312
|
+
return f'<span class="tag {v["tone"]}">{esc(v["label"])}</span>'
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _r_badge_table(content: dict, t: Theme) -> str:
|
|
316
|
+
cols = content["columns"]
|
|
317
|
+
ths = "".join(f'<th style="{c.get("style","")}">{esc(c["label"])}</th>' for c in cols)
|
|
318
|
+
trs = []
|
|
319
|
+
for row in content["rows"]:
|
|
320
|
+
tds = [f'<td class="name-cell">{esc(row["name"])} <span class="code">{esc(row.get("code",""))}</span></td>']
|
|
321
|
+
for c in cols[1:]:
|
|
322
|
+
v = row.get(c["key"])
|
|
323
|
+
if isinstance(v, dict) and "tone" in v:
|
|
324
|
+
tds.append(f'<td>{_tone_cell_html(v, bool(c.get("badge")))}</td>')
|
|
325
|
+
else:
|
|
326
|
+
tds.append(f'<td>{esc(v)}</td>')
|
|
327
|
+
trs.append(f"<tr>{''.join(tds)}</tr>")
|
|
328
|
+
return f'<table class="gtable"><tr>{ths}</tr>{"".join(trs)}</table>'
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _r_change_columns(content: dict, t: Theme) -> str:
|
|
332
|
+
cols = []
|
|
333
|
+
for col in content["columns"]:
|
|
334
|
+
items = col.get("items", [])
|
|
335
|
+
if not items:
|
|
336
|
+
continue
|
|
337
|
+
lis = "".join(f"<li>{esc(it)}</li>" for it in items)
|
|
338
|
+
cols.append(f'<div class="change-col"><h4>{esc(col["label"])} ({len(items)})</h4><ul>{lis}</ul></div>')
|
|
339
|
+
if not cols:
|
|
340
|
+
return '<div class="section-note">无显著变化</div>'
|
|
341
|
+
return f'<div class="change-cols">{"".join(cols)}</div>'
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _r_card_grid(content: dict, t: Theme) -> str:
|
|
345
|
+
cards = []
|
|
346
|
+
for it in content["cards"]:
|
|
347
|
+
lines = "".join(f'<div class="line">{esc(ln)}</div>' for ln in it.get("lines", []))
|
|
348
|
+
cards.append(f"""<div class="card {it.get('tone','')}">
|
|
349
|
+
<div class="head"><span class="name">{esc(it['title'])} <span class="code">{esc(it.get('code',''))}</span></span>
|
|
350
|
+
<span class="score">{esc(it.get('subtitle',''))}</span></div>{lines}</div>""")
|
|
351
|
+
return f'<div class="card-grid">{"".join(cards)}</div>'
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _r_row_list(content: dict, t: Theme) -> str:
|
|
355
|
+
rows = []
|
|
356
|
+
for it in content["rows"]:
|
|
357
|
+
badge = _badge_html(it["tone"], it["badge_label"]) if it.get("badge_label") else ""
|
|
358
|
+
rows.append(f"""<div class="row">
|
|
359
|
+
<span class="name">{esc(it['name'])} {badge}</span>
|
|
360
|
+
<span class="meta">{esc(it.get('meta',''))}</span>
|
|
361
|
+
<span class="detail">{esc(it.get('detail',''))}</span></div>""")
|
|
362
|
+
return f'<div class="row-list">{"".join(rows)}</div>'
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _r_data_table(content: dict, t: Theme) -> str:
|
|
366
|
+
cols = content["columns"]
|
|
367
|
+
compact = " compact" if content.get("compact") else ""
|
|
368
|
+
ths = "".join(f"<th>{esc(c['label'])}</th>" for c in cols)
|
|
369
|
+
trs = []
|
|
370
|
+
for row in content["rows"]:
|
|
371
|
+
tds = []
|
|
372
|
+
for c in cols:
|
|
373
|
+
v = row.get(c["key"], "")
|
|
374
|
+
cls = []
|
|
375
|
+
if c.get("numeric"):
|
|
376
|
+
cls.append("mono")
|
|
377
|
+
if c.get("align") == "left":
|
|
378
|
+
cls.append("name-cell")
|
|
379
|
+
if c.get("color_rule") == "cn_updown":
|
|
380
|
+
sv = str(v).strip()
|
|
381
|
+
if sv.startswith("+"):
|
|
382
|
+
cls.append("up-txt")
|
|
383
|
+
elif sv.startswith("-"):
|
|
384
|
+
cls.append("down-txt")
|
|
385
|
+
if c.get("badge") and _has_signal_emoji(str(v)):
|
|
386
|
+
tone, label = _signal_tone(str(v))
|
|
387
|
+
cell = _badge_html(tone, label)
|
|
388
|
+
elif c.get("badge"):
|
|
389
|
+
# badge column but this particular row has no recognized signal
|
|
390
|
+
# emoji (e.g. a subtotal/footer row mixed into a signal table) —
|
|
391
|
+
# forcing it through _signal_tone would default to a fake "持有
|
|
392
|
+
# 观望" badge that misrepresents a row that was never a signal.
|
|
393
|
+
cell = esc(strip_emoji(str(v)))
|
|
394
|
+
else:
|
|
395
|
+
cell = esc(v)
|
|
396
|
+
tds.append(f'<td class="{" ".join(cls)}">{cell}</td>')
|
|
397
|
+
trs.append(f"<tr>{''.join(tds)}</tr>")
|
|
398
|
+
return f'<table class="gtable{compact}"><tr>{ths}</tr>{"".join(trs)}</table>'
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _r_detail_cards(content: dict, t: Theme) -> str:
|
|
402
|
+
cards = []
|
|
403
|
+
for it in content["cards"]:
|
|
404
|
+
items = "".join(
|
|
405
|
+
f'<div class="item"><div class="k">{esc(kv["k"])}</div><div class="v">{esc(kv["v"])}</div></div>'
|
|
406
|
+
for kv in it.get("kv", [])
|
|
407
|
+
)
|
|
408
|
+
callout = f'<div class="callout">{esc(it["callout"])}</div>' if it.get("callout") else ""
|
|
409
|
+
badge = _badge_html(it["tone"], it["badge_label"])
|
|
410
|
+
cards.append(f"""<div class="detail-card">
|
|
411
|
+
<div class="dc-head"><span><span class="name">{esc(it['title'])}</span><span class="code">{esc(it.get('code',''))}</span></span>{badge}</div>
|
|
412
|
+
<div class="dc-body"><div class="kv-grid">{items}</div>{callout}</div></div>""")
|
|
413
|
+
return f'<div class="detail-grid">{"".join(cards)}</div>'
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _r_news_cards(content: dict, t: Theme) -> str:
|
|
417
|
+
cards = []
|
|
418
|
+
for it in content["cards"]:
|
|
419
|
+
arts = "".join(
|
|
420
|
+
f'<div class="article"><a href="{esc(a["url"])}">{esc(a["title"])}</a> <span class="src">{esc(a.get("meta",""))}</span></div>'
|
|
421
|
+
for a in it.get("articles", [])
|
|
422
|
+
)
|
|
423
|
+
kw = f'<div class="kw">{esc(it["kw"])}</div>' if it.get("kw") else ""
|
|
424
|
+
cards.append(f"""<div class="news-item">
|
|
425
|
+
<div class="n-head"><span class="name">{esc(it['title'])}</span><span class="code">{esc(it.get('code',''))}</span>
|
|
426
|
+
<span class="tag {it['tone']}">{esc(it['tag_label'])}</span></div>{kw}{arts}</div>""")
|
|
427
|
+
return "".join(cards)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _r_text(content: dict, t: Theme) -> str:
|
|
431
|
+
# Split on **bold** runs first so each piece can be escaped independently,
|
|
432
|
+
# then wrap the bold pieces in <strong> — escaping after adding the tags
|
|
433
|
+
# would turn the tags themselves into visible text.
|
|
434
|
+
text = content.get("text", "")
|
|
435
|
+
parts = re.split(r"(\*\*.+?\*\*)", text)
|
|
436
|
+
html_parts = [
|
|
437
|
+
f"<strong>{esc(p[2:-2])}</strong>" if p.startswith("**") and p.endswith("**")
|
|
438
|
+
else esc(p)
|
|
439
|
+
for p in parts
|
|
440
|
+
]
|
|
441
|
+
body = "".join(html_parts).replace("\n\n", "<br><br>").replace("\n", "<br>")
|
|
442
|
+
return f'<div class="text-block">{body}</div>'
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
RENDERERS: dict[str, Callable[[Any, Theme], str]] = {
|
|
446
|
+
"stats": _r_stats,
|
|
447
|
+
"badge_table": _r_badge_table,
|
|
448
|
+
"change_columns": _r_change_columns,
|
|
449
|
+
"card_grid": _r_card_grid,
|
|
450
|
+
"row_list": _r_row_list,
|
|
451
|
+
"data_table": _r_data_table,
|
|
452
|
+
"detail_cards": _r_detail_cards,
|
|
453
|
+
"news_cards": _r_news_cards,
|
|
454
|
+
"text": _r_text,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def render_document(doc: Document, theme: Theme) -> str:
|
|
459
|
+
css = build_css(theme).replace("{TITLE}", esc(doc.title))
|
|
460
|
+
parts = [f"""<div class="cover">
|
|
461
|
+
<div class="cover-title">{esc(doc.title)}</div>
|
|
462
|
+
<div class="cover-meta">{esc(doc.meta)}</div>
|
|
463
|
+
{f'<div class="warning-banner">{esc(doc.warning)}</div>' if doc.warning else ""}
|
|
464
|
+
</div>"""]
|
|
465
|
+
for i, sec in enumerate(doc.sections):
|
|
466
|
+
body = RENDERERS.get(sec.kind, _r_text)(sec.content, theme)
|
|
467
|
+
note_html = f'<div class="section-note">{esc(sec.note)}</div>' if sec.note else ""
|
|
468
|
+
page_cls = " new-page" if (sec.new_page and i > 0) else ""
|
|
469
|
+
parts.append(f"""<div class="section{page_cls}">
|
|
470
|
+
<div class="section-head"><span class="section-icon">{esc(sec.icon)}</span><span class="section-title">{esc(sec.title)}</span></div>
|
|
471
|
+
{note_html}{body}</div>""")
|
|
472
|
+
if doc.footer:
|
|
473
|
+
parts.append(f'<div class="footer-note">{esc(doc.footer)}</div>')
|
|
474
|
+
return f'<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body>{"".join(parts)}</body></html>'
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ── 报告类型解析器:短线分析报告(阿萨阿股短线报告的 .md 结构) ────────────────
|
|
478
|
+
# 新增报告类型时,照这个函数的样子写一个新的 parse_xxx_report(text) -> Document,
|
|
479
|
+
# 不用碰上面的渲染逻辑。
|
|
480
|
+
|
|
481
|
+
def _parse_md_table(block: str) -> list[dict]:
|
|
482
|
+
lines = [ln for ln in block.strip().splitlines() if ln.strip().startswith("|")]
|
|
483
|
+
if len(lines) < 2:
|
|
484
|
+
return []
|
|
485
|
+
headers = [c.strip() for c in lines[0].strip("|").split("|")]
|
|
486
|
+
rows = []
|
|
487
|
+
for ln in lines[2:]:
|
|
488
|
+
cells = [c.strip() for c in ln.strip("|").split("|")]
|
|
489
|
+
if len(cells) == len(headers):
|
|
490
|
+
rows.append(dict(zip(headers, cells)))
|
|
491
|
+
return rows
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _parse_name_code(cell: str) -> tuple[str, str]:
|
|
495
|
+
m = re.match(r"\*\*(.+?)\*\*\s*\((\d+)\)", cell.strip())
|
|
496
|
+
return (m.group(1), m.group(2)) if m else (cell.strip(), "")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _signal_tone(cell: str) -> tuple[str, str]:
|
|
500
|
+
if "🟢🟢" in cell:
|
|
501
|
+
return "buy2", "强力买入"
|
|
502
|
+
if "🟢" in cell:
|
|
503
|
+
return "buy", "买入"
|
|
504
|
+
if "🟡" in cell:
|
|
505
|
+
return "reduce", "减仓"
|
|
506
|
+
if "🔴" in cell:
|
|
507
|
+
return "sell", "卖出"
|
|
508
|
+
return "hold", "持有观望"
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _has_signal_emoji(cell: str) -> bool:
|
|
512
|
+
return any(m in cell for m in ("🟢", "🟡", "🔴", "⬜"))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def parse_shortterm_report(md_text: str) -> Document:
|
|
516
|
+
title_m = re.search(r"^# (.+)$", md_text, re.M)
|
|
517
|
+
meta_m = re.search(r"^> \*\*生成时间\*\*:(.+)$", md_text, re.M)
|
|
518
|
+
warn_m = re.search(r"^> ⚠️ (.+)$", md_text, re.M)
|
|
519
|
+
|
|
520
|
+
sections_raw = re.split(r"^## ", md_text, flags=re.M)[1:]
|
|
521
|
+
sec_map = {}
|
|
522
|
+
for s in sections_raw:
|
|
523
|
+
heading, _, body = s.partition("\n")
|
|
524
|
+
sec_map[heading.strip()] = body
|
|
525
|
+
|
|
526
|
+
doc = Document(
|
|
527
|
+
title=title_m.group(1).strip() if title_m else "分析报告",
|
|
528
|
+
meta=strip_md_bold(meta_m.group(1)).strip() if meta_m else "",
|
|
529
|
+
warning=warn_m.group(1).strip() if warn_m else "",
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# ① 今日概览
|
|
533
|
+
ov_key = next((k for k in sec_map if "今日概览" in k), None)
|
|
534
|
+
if ov_key:
|
|
535
|
+
rows = _parse_md_table(sec_map[ov_key])
|
|
536
|
+
stat_keys = {"分析标的": "hold", "🟢 买入信号": "buy", "⬜ 持有观望": "hold", "🔴 减仓/离场": "sell",
|
|
537
|
+
"平均综合评分": "hold", "深度数据覆盖": "hold"}
|
|
538
|
+
cards, meta = [], []
|
|
539
|
+
for r in rows:
|
|
540
|
+
k, v = r.get("项目", ""), r.get("数值", "")
|
|
541
|
+
if k in stat_keys:
|
|
542
|
+
cards.append({"label": strip_emoji(k), "value": v, "tone": stat_keys[k]})
|
|
543
|
+
else:
|
|
544
|
+
meta.append({"label": strip_emoji(k), "value": strip_emoji(v)})
|
|
545
|
+
doc.sections.append(Section("stats", "今日概览", "1", content={"cards": cards, "meta": meta}, new_page=False))
|
|
546
|
+
|
|
547
|
+
# ② 综合研判
|
|
548
|
+
vd_key = next((k for k in sec_map if "综合研判" in k), None)
|
|
549
|
+
if vd_key:
|
|
550
|
+
raw = _parse_md_table(sec_map[vd_key])
|
|
551
|
+
rows = []
|
|
552
|
+
for r in raw:
|
|
553
|
+
name, code = _parse_name_code(r.get("标的", ""))
|
|
554
|
+
tone, label = _signal_tone(r.get("🚦 综合研判", r.get("综合研判", "")))
|
|
555
|
+
news_cell = r.get("新闻面", "").strip()
|
|
556
|
+
score_m = re.search(r"[+-][\d.]+", news_cell)
|
|
557
|
+
score_txt = score_m.group(0) if score_m else ""
|
|
558
|
+
if "利好" in news_cell:
|
|
559
|
+
news = {"tone": "pos", "label": f"利好 {score_txt}".strip()}
|
|
560
|
+
elif "利空" in news_cell:
|
|
561
|
+
news = {"tone": "up", "label": f"利空 {score_txt}".strip()}
|
|
562
|
+
elif "中性" in news_cell:
|
|
563
|
+
news = {"tone": "neutral", "label": "中性 +0.00"}
|
|
564
|
+
else:
|
|
565
|
+
news = {"tone": "neutral", "label": "无"}
|
|
566
|
+
confirm_cell = r.get("印证", "").strip()
|
|
567
|
+
confirm = {"tone": "confirm" if "共振" in confirm_cell else "neutral",
|
|
568
|
+
"label": "共振" if "共振" in confirm_cell else ("中性" if "中性" in confirm_cell else "—")}
|
|
569
|
+
rows.append({"name": name, "code": code, "signal": {"tone": tone, "label": label}, "news": news,
|
|
570
|
+
"confirm": confirm, "action": strip_md_bold(r.get("一句话操作", "")).strip()})
|
|
571
|
+
doc.sections.append(Section("badge_table", "综合研判 · 一眼看清买卖", "2", content={
|
|
572
|
+
"columns": [
|
|
573
|
+
{"key": "name", "label": "标的", "style": "width:26%"},
|
|
574
|
+
{"key": "signal", "label": "综合研判", "badge": True, "style": "width:12%"},
|
|
575
|
+
{"key": "news", "label": "新闻面", "style": "width:14%"},
|
|
576
|
+
{"key": "confirm", "label": "印证", "style": "width:9%"},
|
|
577
|
+
{"key": "action", "label": "一句话操作"},
|
|
578
|
+
],
|
|
579
|
+
"rows": rows,
|
|
580
|
+
}, note="综合研判以已回测的量化技术/基本面信号为主导,新闻面情绪仅作共振/分歧提示,分歧不等于反向。"))
|
|
581
|
+
|
|
582
|
+
# ③ 信号变化
|
|
583
|
+
ch_key = next((k for k in sec_map if "信号变化" in k), None)
|
|
584
|
+
if ch_key:
|
|
585
|
+
body = sec_map[ch_key]
|
|
586
|
+
cols = []
|
|
587
|
+
for pat, label in [(r"\*\*🆕 新增买入\*\*.*?(?=\n\*\*|\Z)", "🆕 新增买入"),
|
|
588
|
+
(r"\*\*🚪 退出买入\*\*.*?(?=\n\*\*|\Z)", "🚪 退出买入"),
|
|
589
|
+
(r"\*\*⬇️ 信号下调\*\*.*?(?=\n\*\*|\Z)", "⬇️ 信号下调"),
|
|
590
|
+
(r"\*\*⬆️ 信号上调\*\*.*?(?=\n\*\*|\Z)", "⬆️ 信号上调")]:
|
|
591
|
+
m = re.search(pat, body, re.S)
|
|
592
|
+
if not m:
|
|
593
|
+
continue
|
|
594
|
+
items = [strip_emoji(strip_md_bold(ln.strip()[2:])) for ln in m.group(0).splitlines() if ln.strip().startswith("- ")]
|
|
595
|
+
if items:
|
|
596
|
+
cols.append({"label": strip_emoji(label), "items": items})
|
|
597
|
+
if cols:
|
|
598
|
+
date_m = re.search(r"vs 上一交易日 ([\d\-]+)", ch_key)
|
|
599
|
+
doc.sections.append(Section("change_columns", f"信号变化(vs 上一交易日 {date_m.group(1) if date_m else ''})",
|
|
600
|
+
"3", content={"columns": cols}, new_page=False))
|
|
601
|
+
|
|
602
|
+
# ④ 今日特殊行情
|
|
603
|
+
sp_key = next((k for k in sec_map if "今日特殊行情" in k), None)
|
|
604
|
+
if sp_key:
|
|
605
|
+
blocks = re.split(r"\n(?=- \*\*)", sec_map[sp_key].strip())
|
|
606
|
+
cards = []
|
|
607
|
+
for b in blocks:
|
|
608
|
+
hm = re.match(r"- \*\*(.+?)\*\* \((\d+)\) (\S+)\s*评分(\d+)", b.strip())
|
|
609
|
+
if not hm:
|
|
610
|
+
continue
|
|
611
|
+
name, code, badge_emoji, score = hm.groups()
|
|
612
|
+
tone, _ = _signal_tone(badge_emoji)
|
|
613
|
+
# 第一行是标题行本身(- **name** (code) ...),之后每一行缩进的
|
|
614
|
+
# " - 详情" 才是风险提示,用 splitlines 按行处理,不猜整块的缩进规律
|
|
615
|
+
alerts = [strip_emoji(ln.strip()[2:]) for ln in b.splitlines()[1:] if ln.strip().startswith("- ")]
|
|
616
|
+
cards.append({"title": name, "code": code, "subtitle": f"评分 {score}", "tone": tone,
|
|
617
|
+
"lines": [a.strip() for a in alerts]})
|
|
618
|
+
if cards:
|
|
619
|
+
doc.sections.append(Section("card_grid", "今日特殊行情 & 风险提示", "4", content={"cards": cards}))
|
|
620
|
+
|
|
621
|
+
# ⑤ 今日优选短线标的
|
|
622
|
+
pk_key = next((k for k in sec_map if "今日优选短线标的" in k), None)
|
|
623
|
+
if pk_key:
|
|
624
|
+
rows = _parse_md_table(sec_map[pk_key])
|
|
625
|
+
cols_spec = [
|
|
626
|
+
{"key": "代码", "label": "代码"}, {"key": "名称", "label": "名称", "align": "left"},
|
|
627
|
+
{"key": "分", "label": "分", "numeric": True}, {"key": "信号", "label": "信号", "badge": True},
|
|
628
|
+
{"key": "现价", "label": "现价", "numeric": True}, {"key": "今涨", "label": "今涨", "numeric": True, "color_rule": "cn_updown"},
|
|
629
|
+
{"key": "5日", "label": "5日", "numeric": True, "color_rule": "cn_updown"}, {"key": "量比", "label": "量比", "numeric": True},
|
|
630
|
+
{"key": "目标/参考", "label": "目标/参考", "numeric": True}, {"key": "止损/参考", "label": "止损/参考", "numeric": True},
|
|
631
|
+
{"key": "R/R", "label": "R/R", "numeric": True}, {"key": "RSI", "label": "RSI", "numeric": True},
|
|
632
|
+
{"key": "MACD", "label": "MACD"}, {"key": "均线", "label": "均线"}, {"key": "形态", "label": "形态"},
|
|
633
|
+
]
|
|
634
|
+
# "信号" carries its raw 🟢/⬜/🔴 emoji through untouched — _r_data_table's
|
|
635
|
+
# badge column path converts it via _signal_tone()/_badge_html() the same
|
|
636
|
+
# way the ②⑥⑦ sections already do. Every other column gets emoji-stripped:
|
|
637
|
+
# source data appends decorative suffix emoji straight onto plain values
|
|
638
|
+
# (e.g. "建设银行⭐", RSI "77🔴") that have no badge equivalent and were
|
|
639
|
+
# rendering as floating glyphs (see strip_emoji's docstring above).
|
|
640
|
+
clean_rows = [
|
|
641
|
+
{
|
|
642
|
+
c["key"]: strip_md_bold(r.get(c["key"], "")) if c["key"] == "信号"
|
|
643
|
+
else strip_emoji(strip_md_bold(r.get(c["key"], "")))
|
|
644
|
+
for c in cols_spec
|
|
645
|
+
}
|
|
646
|
+
for r in rows
|
|
647
|
+
]
|
|
648
|
+
doc.sections.append(Section("data_table", "今日优选短线标的", "5", content={
|
|
649
|
+
"columns": cols_spec, "rows": clean_rows, "compact": True,
|
|
650
|
+
}, note="~¥ 前缀 = 持有标的参考区间,非买入入场价。"))
|
|
651
|
+
|
|
652
|
+
# ⑥ 买入/加仓候选
|
|
653
|
+
buy_key = next((k for k in sec_map if k.startswith("🟢 买入")), None)
|
|
654
|
+
if buy_key:
|
|
655
|
+
blocks = re.split(r"\n(?=### )", sec_map[buy_key].strip())
|
|
656
|
+
cards = []
|
|
657
|
+
kv_order = ["现价", "建议入场", "目标价", "止损价", "盈亏比", "今日涨幅", "RSI(14)", "MACD状态", "信号类型"]
|
|
658
|
+
for b in blocks:
|
|
659
|
+
hm = re.match(r"### (.+?) \((\d+)\)\s*(\S+)", b)
|
|
660
|
+
if not hm:
|
|
661
|
+
continue
|
|
662
|
+
name, code, badge = hm.groups()
|
|
663
|
+
tone, label = _signal_tone(badge)
|
|
664
|
+
rows = _parse_md_table(b)
|
|
665
|
+
kv = {r["项目"]: r["数值"] for r in rows if "项目" in r}
|
|
666
|
+
risk = strip_emoji(kv.get("⚠️ 追高风险", ""))
|
|
667
|
+
cards.append({"title": name, "code": code, "tone": tone, "badge_label": label,
|
|
668
|
+
"kv": [{"k": strip_emoji(k), "v": strip_emoji(kv[k])} for k in kv_order if k in kv],
|
|
669
|
+
"callout": f"注意:{risk}" if risk else None})
|
|
670
|
+
if cards:
|
|
671
|
+
doc.sections.append(Section("detail_cards", "买入/加仓候选", "6", content={"cards": cards}))
|
|
672
|
+
|
|
673
|
+
# ⑦ 减仓/观察
|
|
674
|
+
rd_key = next((k for k in sec_map if k.startswith("🔴 减仓")), None)
|
|
675
|
+
if rd_key:
|
|
676
|
+
rows = []
|
|
677
|
+
for line in sec_map[rd_key].splitlines():
|
|
678
|
+
m = re.match(r"- \*\*(.+?)\*\* \((\d+)\) — (\S+)\s*评分\s*(\d+)\s*\|\s*(.+)", line.strip())
|
|
679
|
+
if not m:
|
|
680
|
+
continue
|
|
681
|
+
name, code, badge, score, detail = m.groups()
|
|
682
|
+
tone, label = _signal_tone(badge)
|
|
683
|
+
rows.append({"name": name, "tone": tone, "badge_label": label, "meta": f"评分{score}", "detail": strip_emoji(detail)})
|
|
684
|
+
if rows:
|
|
685
|
+
doc.sections.append(Section("row_list", "减仓/观察", "7", content={"rows": rows}))
|
|
686
|
+
|
|
687
|
+
# ⑧ 新闻面预测
|
|
688
|
+
nw_key = next((k for k in sec_map if "新闻面预测" in k), None)
|
|
689
|
+
if nw_key:
|
|
690
|
+
blocks = re.split(r"\n(?=### )", sec_map[nw_key].strip())
|
|
691
|
+
cards = []
|
|
692
|
+
for b in blocks:
|
|
693
|
+
hm = re.match(r"### (\S+) (.+?) \((\d+)\) · (\S+) ([+\-][\d.]+)((\d+) 条相关)", b)
|
|
694
|
+
if not hm:
|
|
695
|
+
continue
|
|
696
|
+
_e, name, code, label, score, count = hm.groups()
|
|
697
|
+
tone = {"利好": "pos", "利空": "up", "中性": "neutral"}.get(label, "neutral")
|
|
698
|
+
kw_m = re.search(r"- 情绪关键词: (.+)$", b, re.M)
|
|
699
|
+
articles = [{"title": t, "url": u, "meta": f"{s} · {d}"}
|
|
700
|
+
for t, u, s, d in re.findall(r"- \[(.+?)\]\((.+?)\)\s*<sub>(.+?) · (.+?)</sub>", b)]
|
|
701
|
+
cards.append({"title": name, "code": code, "tone": tone, "tag_label": f"{label} {score}({count}条)",
|
|
702
|
+
"kw": f"情绪关键词: {kw_m.group(1).strip()}" if kw_m else "", "articles": articles})
|
|
703
|
+
if cards:
|
|
704
|
+
doc.sections.append(Section("news_cards", "新闻面预测(近两周资讯情绪)", "8", content={"cards": cards},
|
|
705
|
+
note="情绪分 ∈ [−1, +1]:> +0.15 偏多、< −0.15 偏空,其间为中性。"))
|
|
706
|
+
|
|
707
|
+
# ⑨ 短线交易纪律
|
|
708
|
+
rl_key = next((k for k in sec_map if "短线交易纪律" in k), None)
|
|
709
|
+
if rl_key:
|
|
710
|
+
raw = _parse_md_table(sec_map[rl_key])
|
|
711
|
+
rows = [{"规则": strip_emoji(strip_md_bold(r.get("规则", ""))),
|
|
712
|
+
"要求": strip_emoji(strip_md_bold(r.get("要求", "")))} for r in raw]
|
|
713
|
+
doc.sections.append(Section("data_table", "短线交易纪律", "9", content={
|
|
714
|
+
"columns": [{"key": "规则", "label": "规则", "align": "left"}, {"key": "要求", "label": "要求", "align": "left"}],
|
|
715
|
+
"rows": rows,
|
|
716
|
+
}))
|
|
717
|
+
|
|
718
|
+
doc.footer = "Arthera Quant — 短线分析,严格止损,风险自担 · 每日盘后自动更新"
|
|
719
|
+
return doc
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def parse_longterm_report(md_text: str) -> Document:
|
|
723
|
+
title_m = re.search(r"^# (.+)$", md_text, re.M)
|
|
724
|
+
meta_m = re.search(r"^> \*\*生成日期\*\*:(.+)$", md_text, re.M)
|
|
725
|
+
warn_m = re.search(r"^> ⚠️ (.+)$", md_text, re.M)
|
|
726
|
+
|
|
727
|
+
sections_raw = re.split(r"^## ", md_text, flags=re.M)[1:]
|
|
728
|
+
sec_map = {}
|
|
729
|
+
for s in sections_raw:
|
|
730
|
+
heading, _, body = s.partition("\n")
|
|
731
|
+
sec_map[heading.strip()] = body
|
|
732
|
+
|
|
733
|
+
doc = Document(
|
|
734
|
+
title=title_m.group(1).strip() if title_m else "长线分析报告",
|
|
735
|
+
meta=strip_md_bold(meta_m.group(1)).strip() if meta_m else "",
|
|
736
|
+
warning=warn_m.group(1).strip() if warn_m else "",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# ① 本期概览
|
|
740
|
+
ov_key = next((k for k in sec_map if "本期概览" in k), None)
|
|
741
|
+
if ov_key:
|
|
742
|
+
rows = _parse_md_table(sec_map[ov_key])
|
|
743
|
+
stat_keys = {"分析标的": "hold", "🟢 买入信号": "buy", "⬜ 持有/观望": "hold", "🔴 减仓/卖出": "sell"}
|
|
744
|
+
cards, meta = [], []
|
|
745
|
+
for r in rows:
|
|
746
|
+
k, v = r.get("项目", ""), r.get("数值", "")
|
|
747
|
+
if k in stat_keys:
|
|
748
|
+
cards.append({"label": strip_emoji(k), "value": v, "tone": stat_keys[k]})
|
|
749
|
+
else:
|
|
750
|
+
meta.append({"label": strip_emoji(k), "value": strip_emoji(v)})
|
|
751
|
+
doc.sections.append(Section("stats", "本期概览", "1", content={"cards": cards, "meta": meta}, new_page=False))
|
|
752
|
+
|
|
753
|
+
# ② 市场环境 — free-text paragraph, not a table. The section body runs up
|
|
754
|
+
# to the next "^## " heading, which includes the "---" hr separator line
|
|
755
|
+
# before it — that's markdown page-break decoration, not content, strip it.
|
|
756
|
+
mk_key = next((k for k in sec_map if "市场环境" in k), None)
|
|
757
|
+
if mk_key:
|
|
758
|
+
body = re.sub(r"\n-{3,}\s*$", "", sec_map[mk_key].strip())
|
|
759
|
+
text = strip_emoji(body.strip())
|
|
760
|
+
if text:
|
|
761
|
+
doc.sections.append(Section("text", "市场环境", "2", content={"text": text}, new_page=False))
|
|
762
|
+
|
|
763
|
+
# ③ 本期信号变化(买卖候选进出)— richer table form of the same data the
|
|
764
|
+
# bullet-list "🆕 新增买入" section shows; skip the bullet-list version to
|
|
765
|
+
# avoid rendering the same 14 stocks twice.
|
|
766
|
+
ch_key = next((k for k in sec_map if "本期信号变化" in k), None)
|
|
767
|
+
if ch_key:
|
|
768
|
+
raw = _parse_md_table(sec_map[ch_key])
|
|
769
|
+
cols_spec = [
|
|
770
|
+
{"key": "代码", "label": "代码"},
|
|
771
|
+
{"key": "名称", "label": "名称", "align": "left"},
|
|
772
|
+
{"key": "当前信号", "label": "当前信号", "badge": True},
|
|
773
|
+
{"key": "上期信号", "label": "上期信号"},
|
|
774
|
+
{"key": "当前评分", "label": "当前评分", "numeric": True},
|
|
775
|
+
]
|
|
776
|
+
clean_rows = [
|
|
777
|
+
{
|
|
778
|
+
c["key"]: strip_md_bold(r.get(c["key"], "")) if c["key"] == "当前信号"
|
|
779
|
+
else strip_emoji(strip_md_bold(r.get(c["key"], "")))
|
|
780
|
+
for c in cols_spec
|
|
781
|
+
}
|
|
782
|
+
for r in raw
|
|
783
|
+
]
|
|
784
|
+
if clean_rows:
|
|
785
|
+
doc.sections.append(Section("data_table", "本期信号变化(买卖候选进出)", "3",
|
|
786
|
+
content={"columns": cols_spec, "rows": clean_rows}))
|
|
787
|
+
|
|
788
|
+
# ④ 行业景气度
|
|
789
|
+
ig_key = next((k for k in sec_map if "行业景气度" in k), None)
|
|
790
|
+
if ig_key:
|
|
791
|
+
raw = _parse_md_table(sec_map[ig_key])
|
|
792
|
+
cols_spec = [
|
|
793
|
+
{"key": "行业", "label": "行业", "align": "left"},
|
|
794
|
+
{"key": "标的数", "label": "标的数", "numeric": True},
|
|
795
|
+
{"key": "平均分", "label": "平均分", "numeric": True},
|
|
796
|
+
{"key": "景气度", "label": "景气度"},
|
|
797
|
+
{"key": "3月均涨跌", "label": "3月均涨跌", "numeric": True, "color_rule": "cn_updown"},
|
|
798
|
+
{"key": "看多/看空", "label": "看多/看空"},
|
|
799
|
+
]
|
|
800
|
+
# "平均分" 单元格里带一个反引号包住的进度条(`███████░░░`)——那是 markdown
|
|
801
|
+
# 行内代码语法,不是数据,得先去掉反引号本身,保留方块字符(区块元素
|
|
802
|
+
# U+2580-259F,跟 emoji 完全不同的 Unicode 区块,正常渲染)。
|
|
803
|
+
clean_rows = [
|
|
804
|
+
{c["key"]: strip_emoji(r.get(c["key"], "").replace("`", "")) for c in cols_spec}
|
|
805
|
+
for r in raw
|
|
806
|
+
]
|
|
807
|
+
if clean_rows:
|
|
808
|
+
doc.sections.append(Section("data_table", "行业景气度", "4",
|
|
809
|
+
content={"columns": cols_spec, "rows": clean_rows}))
|
|
810
|
+
|
|
811
|
+
# ⑤ 优选长线标的(评分前列)— the flagship table, same treatment as
|
|
812
|
+
# shortterm's "今日优选短线标的": 信号 column badge, everything else stripped.
|
|
813
|
+
pk_key = next((k for k in sec_map if "优选长线标的" in k), None)
|
|
814
|
+
if pk_key:
|
|
815
|
+
rows = _parse_md_table(sec_map[pk_key])
|
|
816
|
+
cols_spec = [
|
|
817
|
+
{"key": "代码", "label": "代码"}, {"key": "名称", "label": "名称", "align": "left"},
|
|
818
|
+
{"key": "行业", "label": "行业"}, {"key": "综合分", "label": "分", "numeric": True},
|
|
819
|
+
{"key": "信号", "label": "信号", "badge": True},
|
|
820
|
+
{"key": "现价", "label": "现价", "numeric": True},
|
|
821
|
+
{"key": "技术参考价", "label": "参考价", "numeric": True},
|
|
822
|
+
{"key": "参考空间", "label": "空间", "numeric": True},
|
|
823
|
+
{"key": "止损", "label": "止损", "numeric": True},
|
|
824
|
+
{"key": "趋势", "label": "趋势"},
|
|
825
|
+
{"key": "PE", "label": "PE", "numeric": True}, {"key": "ROE", "label": "ROE", "numeric": True},
|
|
826
|
+
{"key": "3月涨跌", "label": "3月涨跌", "numeric": True, "color_rule": "cn_updown"},
|
|
827
|
+
]
|
|
828
|
+
clean_rows = [
|
|
829
|
+
{
|
|
830
|
+
c["key"]: strip_md_bold(r.get(c["key"], "")) if c["key"] == "信号"
|
|
831
|
+
else strip_emoji(strip_md_bold(r.get(c["key"], "")))
|
|
832
|
+
for c in cols_spec
|
|
833
|
+
}
|
|
834
|
+
for r in rows
|
|
835
|
+
]
|
|
836
|
+
if clean_rows:
|
|
837
|
+
doc.sections.append(Section("data_table", "优选长线标的(评分前列)", "5",
|
|
838
|
+
content={"columns": cols_spec, "rows": clean_rows, "compact": True}))
|
|
839
|
+
|
|
840
|
+
# 完整分析明细(按行业拆的附录级子表)不纳入 PDF——⑤ 的评分前列表已经把
|
|
841
|
+
# 各行业最值得看的标的挑出来了,附录里的完整清单价值有限,不值得为一个
|
|
842
|
+
# h3 子标题嵌套结构再加一层解析复杂度。
|
|
843
|
+
|
|
844
|
+
# ⑥/⑦ 操作建议摘要 → 买入/加仓候选 + 减仓/观察候选(拆成两个 row_list)
|
|
845
|
+
op_key = next((k for k in sec_map if "操作建议摘要" in k), None)
|
|
846
|
+
if op_key:
|
|
847
|
+
body = sec_map[op_key]
|
|
848
|
+
buy_m = re.search(r"### 🟢 买入/加仓候选\s*\n(.*?)(?=\n### |\Z)", body, re.S)
|
|
849
|
+
sell_m = re.search(r"### 🔴 减仓/观察候选\s*\n(.*?)(?=\n### |\Z)", body, re.S)
|
|
850
|
+
|
|
851
|
+
def _rows_from_bullets(block: str) -> list[dict]:
|
|
852
|
+
out = []
|
|
853
|
+
for line in (block or "").splitlines():
|
|
854
|
+
m = re.match(r"-\s+\*\*(.+?)\*\*\s*\((\d+)\)\s*—\s*(\S+)\s*(.+)", line.strip())
|
|
855
|
+
if not m:
|
|
856
|
+
continue
|
|
857
|
+
name, code, badge, rest = m.groups()
|
|
858
|
+
tone, label = _signal_tone(badge)
|
|
859
|
+
out.append({"name": f"{name} ({code})", "tone": tone, "badge_label": label,
|
|
860
|
+
"detail": strip_emoji(rest.strip())})
|
|
861
|
+
return out
|
|
862
|
+
|
|
863
|
+
buy_rows = _rows_from_bullets(buy_m.group(1) if buy_m else "")
|
|
864
|
+
sell_rows = _rows_from_bullets(sell_m.group(1) if sell_m else "")
|
|
865
|
+
if buy_rows:
|
|
866
|
+
doc.sections.append(Section("row_list", "买入/加仓候选", "6", content={"rows": buy_rows}))
|
|
867
|
+
if sell_rows:
|
|
868
|
+
doc.sections.append(Section("row_list", "减仓/观察候选", "7", content={"rows": sell_rows}))
|
|
869
|
+
|
|
870
|
+
# ⑧ 分批建仓参考价
|
|
871
|
+
bd_key = next((k for k in sec_map if "分批建仓参考价" in k), None)
|
|
872
|
+
if bd_key:
|
|
873
|
+
rows = _parse_md_table(sec_map[bd_key])
|
|
874
|
+
cols_spec = [
|
|
875
|
+
{"key": "代码", "label": "代码"}, {"key": "名称", "label": "名称", "align": "left"},
|
|
876
|
+
{"key": "信号", "label": "信号", "badge": True},
|
|
877
|
+
{"key": "当前价", "label": "当前价", "numeric": True},
|
|
878
|
+
{"key": "首仓(立即)", "label": "首仓", "numeric": True},
|
|
879
|
+
{"key": "二仓(-5%回撤)", "label": "二仓(-5%)", "numeric": True},
|
|
880
|
+
{"key": "三仓(-10%回撤)", "label": "三仓(-10%)", "numeric": True},
|
|
881
|
+
{"key": "建议仓位", "label": "建议仓位"},
|
|
882
|
+
]
|
|
883
|
+
clean_rows = [
|
|
884
|
+
{
|
|
885
|
+
c["key"]: strip_md_bold(r.get(c["key"], "")) if c["key"] == "信号"
|
|
886
|
+
else strip_emoji(strip_md_bold(r.get(c["key"], "")))
|
|
887
|
+
for c in cols_spec
|
|
888
|
+
}
|
|
889
|
+
for r in rows
|
|
890
|
+
]
|
|
891
|
+
if clean_rows:
|
|
892
|
+
doc.sections.append(Section("data_table", "分批建仓参考价", "8",
|
|
893
|
+
content={"columns": cols_spec, "rows": clean_rows, "compact": True},
|
|
894
|
+
note="长线建仓策略:分 2–3 批次入场,止损统一为 -15%。"))
|
|
895
|
+
|
|
896
|
+
# ⑨ 止盈追踪(高涨幅持仓参考)— "建议操作"故意不走 badge:里面的具体指令
|
|
897
|
+
# ("建议减仓 50%")比单纯的买/卖徽章信息量更大,转成徽章会丢掉这个细节,
|
|
898
|
+
# 这里只清 emoji 前缀,保留完整文字。
|
|
899
|
+
tp_key = next((k for k in sec_map if "止盈追踪" in k), None)
|
|
900
|
+
if tp_key:
|
|
901
|
+
rows = _parse_md_table(sec_map[tp_key])
|
|
902
|
+
cols_spec = [
|
|
903
|
+
{"key": "代码", "label": "代码"}, {"key": "名称", "label": "名称", "align": "left"},
|
|
904
|
+
{"key": "当前价", "label": "当前价", "numeric": True},
|
|
905
|
+
{"key": "1月涨", "label": "1月涨", "numeric": True, "color_rule": "cn_updown"},
|
|
906
|
+
{"key": "3月涨", "label": "3月涨", "numeric": True, "color_rule": "cn_updown"},
|
|
907
|
+
{"key": "年内涨", "label": "年内涨", "numeric": True, "color_rule": "cn_updown"},
|
|
908
|
+
{"key": "技术参考价", "label": "参考价", "numeric": True},
|
|
909
|
+
{"key": "建议操作", "label": "建议操作"},
|
|
910
|
+
]
|
|
911
|
+
clean_rows = [{c["key"]: strip_emoji(strip_md_bold(r.get(c["key"], ""))) for c in cols_spec} for r in rows]
|
|
912
|
+
if clean_rows:
|
|
913
|
+
doc.sections.append(Section("data_table", "止盈追踪(高涨幅持仓参考)", "9",
|
|
914
|
+
content={"columns": cols_spec, "rows": clean_rows, "compact": True},
|
|
915
|
+
note="以下持仓已有显著盈利,建议逐步减仓 30–50% 锁定利润。"))
|
|
916
|
+
|
|
917
|
+
# ⑩ 下周核心关注(接近买点)
|
|
918
|
+
wk_key = next((k for k in sec_map if "下周核心关注" in k), None)
|
|
919
|
+
if wk_key:
|
|
920
|
+
rows = _parse_md_table(sec_map[wk_key])
|
|
921
|
+
cols_spec = [
|
|
922
|
+
{"key": "代码", "label": "代码"}, {"key": "名称", "label": "名称", "align": "left"},
|
|
923
|
+
{"key": "评分", "label": "评分", "numeric": True}, {"key": "现价", "label": "现价", "numeric": True},
|
|
924
|
+
{"key": "月RSI", "label": "月RSI", "numeric": True}, {"key": "趋势", "label": "趋势"},
|
|
925
|
+
{"key": "3月涨跌", "label": "3月涨跌", "numeric": True, "color_rule": "cn_updown"},
|
|
926
|
+
{"key": "上行空间", "label": "上行空间", "numeric": True},
|
|
927
|
+
{"key": "观察要点", "label": "观察要点", "align": "left"},
|
|
928
|
+
]
|
|
929
|
+
clean_rows = [{c["key"]: strip_emoji(strip_md_bold(r.get(c["key"], ""))) for c in cols_spec} for r in rows]
|
|
930
|
+
if clean_rows:
|
|
931
|
+
doc.sections.append(Section("data_table", "下周核心关注(接近买点)", "10",
|
|
932
|
+
content={"columns": cols_spec, "rows": clean_rows, "compact": True}))
|
|
933
|
+
|
|
934
|
+
# ⑪ 组合配置建议 — 末行是合计小计,"信号层级"列没有 emoji,_r_data_table
|
|
935
|
+
# 的 badge 分支会自动识别并原样显示文字,不会被硬套一个假的"持有观望"徽章。
|
|
936
|
+
pf_key = next((k for k in sec_map if "组合配置建议" in k), None)
|
|
937
|
+
if pf_key:
|
|
938
|
+
raw = _parse_md_table(sec_map[pf_key])
|
|
939
|
+
cols_spec = [
|
|
940
|
+
{"key": "信号层级", "label": "信号层级", "badge": True},
|
|
941
|
+
{"key": "数量", "label": "数量"},
|
|
942
|
+
{"key": "单只建议仓位", "label": "单只建议仓位"},
|
|
943
|
+
{"key": "小计(中值)", "label": "小计(中值)"},
|
|
944
|
+
]
|
|
945
|
+
clean_rows = [
|
|
946
|
+
{
|
|
947
|
+
c["key"]: strip_md_bold(r.get(c["key"], "")) if c["key"] == "信号层级"
|
|
948
|
+
else strip_emoji(strip_md_bold(r.get(c["key"], "")))
|
|
949
|
+
for c in cols_spec
|
|
950
|
+
}
|
|
951
|
+
for r in raw
|
|
952
|
+
]
|
|
953
|
+
if clean_rows:
|
|
954
|
+
doc.sections.append(Section("data_table", "组合配置建议", "11",
|
|
955
|
+
content={"columns": cols_spec, "rows": clean_rows}))
|
|
956
|
+
|
|
957
|
+
# ⑫ 风险与注意事项
|
|
958
|
+
rk_key = next((k for k in sec_map if "风险与注意事项" in k), None)
|
|
959
|
+
if rk_key:
|
|
960
|
+
# _r_card_grid's "line" divs are plain esc()'d text with no bold support
|
|
961
|
+
# (unlike _r_text above) — strip ** markers here rather than teach that
|
|
962
|
+
# renderer markdown, consistent with how every table cell in this file
|
|
963
|
+
# already handles bold (drop the markers, don't try to preserve emphasis).
|
|
964
|
+
lines = [strip_emoji(strip_md_bold(ln.strip()[2:])) for ln in sec_map[rk_key].splitlines()
|
|
965
|
+
if ln.strip().startswith("- ")]
|
|
966
|
+
if lines:
|
|
967
|
+
doc.sections.append(Section("card_grid", "风险与注意事项", "12",
|
|
968
|
+
content={"cards": [{"title": "重要提示", "lines": lines}]}))
|
|
969
|
+
|
|
970
|
+
doc.footer = "Arthera Quant — 长线分析,辅助决策,风险自担 · 下次更新: 下周同期"
|
|
971
|
+
return doc
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
PARSERS: dict[str, Callable[[str], Document]] = {
|
|
975
|
+
"shortterm": parse_shortterm_report,
|
|
976
|
+
"longterm": parse_longterm_report,
|
|
977
|
+
}
|