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
aria_code/aria_daemon.py
ADDED
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
aria_daemon.py — Aria always-on background daemon.
|
|
4
|
+
|
|
5
|
+
Responsibilities:
|
|
6
|
+
1. Price alert watchdog — checks SQLite alerts every 30 s
|
|
7
|
+
2. APScheduler cron jobs — morning brief, market scan, custom schedules
|
|
8
|
+
3. Telegram bot — bidirectional command channel
|
|
9
|
+
4. Webhook job executor — runs pending jobs from the local webhook_jobs table
|
|
10
|
+
TradingView endpoint — POST /api/v1/webhook/tradingview (alias
|
|
11
|
+
/api/v1/tradingview/webhook) queues alerts
|
|
12
|
+
5. APNs push delivery — fires on alert trigger or scheduled job completion
|
|
13
|
+
|
|
14
|
+
Start manually: python3 aria_daemon.py [--debug]
|
|
15
|
+
Install daemon: python3 aria_daemon.py --install (macOS LaunchAgent)
|
|
16
|
+
Uninstall: python3 aria_daemon.py --uninstall
|
|
17
|
+
|
|
18
|
+
Config via env vars (or ~/.aria/.env):
|
|
19
|
+
TELEGRAM_BOT_TOKEN — Telegram bot token from @BotFather
|
|
20
|
+
TELEGRAM_ALLOWED_IDS — comma-separated chat IDs (e.g. "123456,789012")
|
|
21
|
+
APNS_KEY_ID — Apple Developer key ID (10-char string)
|
|
22
|
+
APNS_TEAM_ID — Apple Developer team ID
|
|
23
|
+
APNS_BUNDLE_ID — App bundle ID (default: com.arthera.app)
|
|
24
|
+
APNS_SANDBOX — "true" for sandbox / TestFlight (default: true)
|
|
25
|
+
APNS_AUTH_KEY_P8 — .p8 key content, or place file at ~/.aria/apns.p8
|
|
26
|
+
WEBHOOK_TOKEN — Token for /api/v1/webhook/tradingview, sent in the
|
|
27
|
+
X-Aria-Token, X-TradingView-Secret or
|
|
28
|
+
Authorization: Bearer header (never the query
|
|
29
|
+
string). TRADINGVIEW_WEBHOOK_SECRET takes precedence.
|
|
30
|
+
ARIA_WEBHOOK_SECRET — Passphrase TradingView puts in the alert body
|
|
31
|
+
(TradingView cannot send headers)
|
|
32
|
+
ARIA_WEBHOOK_HOST — Daemon webhook host (default: 127.0.0.1)
|
|
33
|
+
ARIA_WEBHOOK_PORT — Daemon webhook port (default: 8765 — the same
|
|
34
|
+
default as the relay server; set this when both
|
|
35
|
+
run on one machine)
|
|
36
|
+
(the FastAPI backend URL is the `api_url` config key, not an env var —
|
|
37
|
+
set it with `/config set api_url=...`)
|
|
38
|
+
ARIA_CODE_DIR — Path to aria-code directory
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import asyncio
|
|
43
|
+
import hmac
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import signal
|
|
47
|
+
import sqlite3
|
|
48
|
+
import sys
|
|
49
|
+
import time
|
|
50
|
+
from datetime import datetime
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import Optional
|
|
53
|
+
|
|
54
|
+
# ── Logging ───────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
# Importing this module creates the log file and daemon.db and loads .env into
|
|
57
|
+
# os.environ (aria_feishu_bot relies on that), so the directory can be moved:
|
|
58
|
+
# the test suite points it at a scratch directory instead of the user's own.
|
|
59
|
+
_ARIA_DIR = Path(os.environ.get("ARIA_DAEMON_DIR") or (Path.home() / ".aria")).expanduser()
|
|
60
|
+
_LOG_DIR = _ARIA_DIR / "logs"
|
|
61
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
logging.basicConfig(
|
|
64
|
+
level=logging.DEBUG if "--debug" in sys.argv else logging.INFO,
|
|
65
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
66
|
+
handlers=[
|
|
67
|
+
logging.StreamHandler(sys.stdout),
|
|
68
|
+
logging.FileHandler(_LOG_DIR / "daemon.log", encoding="utf-8"),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
logger = logging.getLogger("aria.daemon")
|
|
72
|
+
|
|
73
|
+
# ── Paths & config ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
_DB_PATH = _ARIA_DIR / "daemon.db"
|
|
76
|
+
_PID_FILE = _ARIA_DIR / "daemon.pid"
|
|
77
|
+
_ENV_FILE = _ARIA_DIR / ".env"
|
|
78
|
+
|
|
79
|
+
_ARIA_CODE_DIR = Path(os.environ.get("ARIA_CODE_DIR", Path(__file__).parent))
|
|
80
|
+
if str(_ARIA_CODE_DIR) not in sys.path:
|
|
81
|
+
sys.path.insert(0, str(_ARIA_CODE_DIR))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_env() -> None:
|
|
85
|
+
"""Load ~/.aria/.env into os.environ if it exists."""
|
|
86
|
+
if _ENV_FILE.exists():
|
|
87
|
+
for line in _ENV_FILE.read_text().splitlines():
|
|
88
|
+
line = line.strip()
|
|
89
|
+
if line and not line.startswith("#") and "=" in line:
|
|
90
|
+
k, _, v = line.partition("=")
|
|
91
|
+
os.environ.setdefault(k.strip(), v.strip())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
_load_env()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── DB bootstrap ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def _init_db() -> None:
|
|
100
|
+
_ARIA_DIR.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
102
|
+
conn.executescript("""
|
|
103
|
+
CREATE TABLE IF NOT EXISTS device_tokens (
|
|
104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
105
|
+
token TEXT UNIQUE NOT NULL,
|
|
106
|
+
platform TEXT DEFAULT 'ios',
|
|
107
|
+
user_id TEXT,
|
|
108
|
+
bundle_id TEXT,
|
|
109
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
110
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
111
|
+
);
|
|
112
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
113
|
+
id TEXT PRIMARY KEY,
|
|
114
|
+
symbol TEXT NOT NULL,
|
|
115
|
+
condition TEXT NOT NULL,
|
|
116
|
+
value REAL NOT NULL,
|
|
117
|
+
message TEXT,
|
|
118
|
+
notify_push INTEGER DEFAULT 1,
|
|
119
|
+
notify_telegram INTEGER DEFAULT 1,
|
|
120
|
+
notify_email TEXT,
|
|
121
|
+
once INTEGER DEFAULT 1,
|
|
122
|
+
active INTEGER DEFAULT 1,
|
|
123
|
+
trigger_count INTEGER DEFAULT 0,
|
|
124
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
125
|
+
triggered_at TEXT
|
|
126
|
+
);
|
|
127
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
name TEXT,
|
|
130
|
+
cron_expr TEXT NOT NULL,
|
|
131
|
+
command TEXT NOT NULL,
|
|
132
|
+
symbols TEXT DEFAULT '[]',
|
|
133
|
+
channels TEXT DEFAULT '["ios","telegram"]',
|
|
134
|
+
language TEXT DEFAULT 'zh',
|
|
135
|
+
user_id TEXT,
|
|
136
|
+
enabled INTEGER DEFAULT 1,
|
|
137
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
138
|
+
last_run TEXT,
|
|
139
|
+
next_run TEXT
|
|
140
|
+
);
|
|
141
|
+
CREATE TABLE IF NOT EXISTS webhook_jobs (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
command TEXT NOT NULL,
|
|
144
|
+
payload TEXT DEFAULT '{}',
|
|
145
|
+
source TEXT DEFAULT 'external',
|
|
146
|
+
status TEXT DEFAULT 'pending',
|
|
147
|
+
result TEXT,
|
|
148
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
149
|
+
started_at TEXT,
|
|
150
|
+
done_at TEXT
|
|
151
|
+
);
|
|
152
|
+
CREATE TABLE IF NOT EXISTS push_log (
|
|
153
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
154
|
+
alert_id TEXT,
|
|
155
|
+
device_token TEXT,
|
|
156
|
+
title TEXT,
|
|
157
|
+
body TEXT,
|
|
158
|
+
status TEXT DEFAULT 'pending',
|
|
159
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
160
|
+
);
|
|
161
|
+
""")
|
|
162
|
+
conn.commit()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_init_db()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ── Price fetch (lightweight, no VPN proxy) ────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async def _fetch_price(symbol: str) -> tuple[Optional[float], Optional[float]]:
|
|
171
|
+
"""Fast price lookup. Returns (current_price, prev_close) for alert evaluation."""
|
|
172
|
+
import math
|
|
173
|
+
try:
|
|
174
|
+
import yfinance as yf
|
|
175
|
+
if symbol.isdigit() and len(symbol) == 6:
|
|
176
|
+
yfn = symbol + (".SS" if symbol.startswith(("6", "5")) else ".SZ")
|
|
177
|
+
else:
|
|
178
|
+
yfn = symbol
|
|
179
|
+
ticker = yf.Ticker(yfn)
|
|
180
|
+
info = ticker.fast_info
|
|
181
|
+
raw_price = getattr(info, "last_price", None) or getattr(info, "previous_close", None)
|
|
182
|
+
raw_prev = getattr(info, "previous_close", None)
|
|
183
|
+
# yfinance can return float('nan') — treat as None
|
|
184
|
+
price = float(raw_price) if raw_price is not None and not math.isnan(float(raw_price)) else None
|
|
185
|
+
prev_close = float(raw_prev) if raw_prev is not None and not math.isnan(float(raw_prev)) else None
|
|
186
|
+
return (price, prev_close)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.debug("_fetch_price %s: %s", symbol, exc)
|
|
189
|
+
return (None, None)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Push notifications ────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async def _push_alert(title: str, body: str, extra: Optional[dict] = None, alert_id: Optional[str] = None) -> None:
|
|
195
|
+
"""Try to push via APNs; log any failure but never raise."""
|
|
196
|
+
try:
|
|
197
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "Arthera" / "apps" / "api" / "src"))
|
|
198
|
+
from services.apns_service import push_to_all
|
|
199
|
+
sent = await push_to_all(title, body, extra, alert_id)
|
|
200
|
+
logger.info("APNs push: %d device(s) notified for alert_id=%s", sent, alert_id)
|
|
201
|
+
except ImportError:
|
|
202
|
+
logger.debug("apns_service not reachable — skipping push")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ── Telegram push (one-shot, no polling) ─────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async def _telegram_push(text: str) -> None:
|
|
208
|
+
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
209
|
+
allowed_raw = os.environ.get("TELEGRAM_ALLOWED_IDS", "")
|
|
210
|
+
if not token or not allowed_raw:
|
|
211
|
+
return
|
|
212
|
+
chat_ids = [int(x.strip()) for x in allowed_raw.split(",") if x.strip().isdigit()]
|
|
213
|
+
try:
|
|
214
|
+
import httpx
|
|
215
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
216
|
+
for cid in chat_ids:
|
|
217
|
+
await client.post(
|
|
218
|
+
f"https://api.telegram.org/bot{token}/sendMessage",
|
|
219
|
+
json={"chat_id": cid, "text": text, "parse_mode": "Markdown"},
|
|
220
|
+
)
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.warning("_telegram_push failed: %s", exc)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ── Feishu push (webhook card) ────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
async def _feishu_push(title: str, body: str) -> None:
|
|
228
|
+
"""POST an interactive card to a Feishu group webhook (FEISHU_WEBHOOK_URL)."""
|
|
229
|
+
url = os.environ.get("FEISHU_WEBHOOK_URL", "")
|
|
230
|
+
if not url:
|
|
231
|
+
return
|
|
232
|
+
color = "red" if any(w in title for w in ("预警", "Alert", "熔断", "ERROR")) else \
|
|
233
|
+
"green" if any(w in title for w in ("晨报", "完成", "Brief")) else "blue"
|
|
234
|
+
card = {
|
|
235
|
+
"msg_type": "interactive",
|
|
236
|
+
"card": {
|
|
237
|
+
"header": {
|
|
238
|
+
"title": {"tag": "plain_text", "content": title},
|
|
239
|
+
"template": color,
|
|
240
|
+
},
|
|
241
|
+
"elements": [
|
|
242
|
+
{"tag": "div", "text": {"tag": "lark_md", "content": body[:2000]}},
|
|
243
|
+
{"tag": "hr"},
|
|
244
|
+
{"tag": "note", "elements": [
|
|
245
|
+
{"tag": "plain_text",
|
|
246
|
+
"content": "Aria Daemon · " + __import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M")},
|
|
247
|
+
]},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
try:
|
|
252
|
+
import httpx
|
|
253
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
254
|
+
resp = await client.post(url, json=card)
|
|
255
|
+
if resp.status_code >= 400:
|
|
256
|
+
logger.warning("_feishu_push HTTP %s: %s", resp.status_code, resp.text[:120])
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.warning("_feishu_push failed: %s", exc)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ── Alert watchdog ────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
def _go_daemon_running() -> bool:
|
|
264
|
+
"""Whether `aria-runtime daemon` runs on this folder. It owns price alerts
|
|
265
|
+
and schedules then; this daemon keeps its Telegram bot, webhook queue and
|
|
266
|
+
portfolio watchdog, and skips the rest so nothing fires twice."""
|
|
267
|
+
try:
|
|
268
|
+
pid = int((_ARIA_DIR / "daemon-go.pid").read_text().strip())
|
|
269
|
+
if pid <= 0 or pid == os.getpid():
|
|
270
|
+
return False
|
|
271
|
+
os.kill(pid, 0)
|
|
272
|
+
return True
|
|
273
|
+
except (OSError, ValueError):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def _alert_watchdog() -> None:
|
|
278
|
+
"""Check all active alerts against live prices every 30 seconds."""
|
|
279
|
+
logger.info("Alert watchdog started")
|
|
280
|
+
while True:
|
|
281
|
+
if _go_daemon_running():
|
|
282
|
+
await asyncio.sleep(30)
|
|
283
|
+
continue
|
|
284
|
+
try:
|
|
285
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
286
|
+
conn.row_factory = sqlite3.Row
|
|
287
|
+
rows = conn.execute(
|
|
288
|
+
"SELECT * FROM alerts WHERE active=1"
|
|
289
|
+
).fetchall()
|
|
290
|
+
alerts = [dict(r) for r in rows]
|
|
291
|
+
|
|
292
|
+
# Group by symbol to minimise API calls
|
|
293
|
+
by_symbol: dict[str, list[dict]] = {}
|
|
294
|
+
for a in alerts:
|
|
295
|
+
by_symbol.setdefault(a["symbol"], []).append(a)
|
|
296
|
+
|
|
297
|
+
for symbol, sym_alerts in by_symbol.items():
|
|
298
|
+
price, prev_close = await _fetch_price(symbol)
|
|
299
|
+
if price is None:
|
|
300
|
+
continue
|
|
301
|
+
for alert in sym_alerts:
|
|
302
|
+
await _check_alert(alert, price, prev_close)
|
|
303
|
+
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
logger.error("Alert watchdog error: %s", exc)
|
|
306
|
+
|
|
307
|
+
await asyncio.sleep(30)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def _check_alert(alert: dict, price: float, prev_close: Optional[float] = None) -> None:
|
|
311
|
+
cond = alert["condition"]
|
|
312
|
+
val = float(alert["value"])
|
|
313
|
+
|
|
314
|
+
# pct_change 条件: prev_close=None 时跳过而非永远不触发
|
|
315
|
+
if cond in ("pct_change_above", "pct_change_below") and (not prev_close or prev_close <= 0):
|
|
316
|
+
logger.debug("Alert %s: prev_close unavailable, skipping pct_change check", alert["id"])
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
pct_chg = ((price - prev_close) / prev_close * 100) if prev_close and prev_close > 0 else None
|
|
320
|
+
fired = (
|
|
321
|
+
(cond == "price_above" and price > val) or
|
|
322
|
+
(cond == "price_below" and price < val) or
|
|
323
|
+
(cond == "pct_change_above" and pct_chg is not None and pct_chg > val) or
|
|
324
|
+
(cond == "pct_change_below" and pct_chg is not None and pct_chg < val)
|
|
325
|
+
)
|
|
326
|
+
if not fired:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
symbol = alert["symbol"]
|
|
330
|
+
pct_str = f" ({pct_chg:+.2f}%)" if pct_chg is not None else ""
|
|
331
|
+
message = alert.get("message") or f"{symbol} {cond.replace('_', ' ')} {val}"
|
|
332
|
+
title = "🔔 ARIA 价格预警"
|
|
333
|
+
body = f"{symbol} ¥{price:.2f}{pct_str} — {message}"
|
|
334
|
+
|
|
335
|
+
logger.info("Alert triggered: %s | %s @ %.4f", alert["id"], symbol, price)
|
|
336
|
+
|
|
337
|
+
# Update DB
|
|
338
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
339
|
+
conn.execute(
|
|
340
|
+
"UPDATE alerts SET trigger_count=trigger_count+1, triggered_at=datetime('now')"
|
|
341
|
+
+ (", active=0" if alert.get("once", 1) else "")
|
|
342
|
+
+ " WHERE id=?",
|
|
343
|
+
(alert["id"],),
|
|
344
|
+
)
|
|
345
|
+
conn.commit()
|
|
346
|
+
|
|
347
|
+
extra = {"symbol": symbol, "price": price, "signal": "ALERT"}
|
|
348
|
+
|
|
349
|
+
if alert.get("notify_push", 1):
|
|
350
|
+
await _push_alert(title, body, extra, alert["id"])
|
|
351
|
+
|
|
352
|
+
if alert.get("notify_telegram", 1):
|
|
353
|
+
await _telegram_push(f"*{title}*\n{body}")
|
|
354
|
+
await _feishu_push(title, body)
|
|
355
|
+
_system_notify(title, body)
|
|
356
|
+
|
|
357
|
+
# 触发后自动运行轻量分析(technical + risk),结果推送给用户
|
|
358
|
+
asyncio.create_task(_auto_analyze_alert(symbol, price, pct_str))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def _auto_analyze_alert(symbol: str, price: float, pct_str: str) -> None:
|
|
362
|
+
"""Run a lightweight technical+risk analysis after an alert fires and push the result."""
|
|
363
|
+
try:
|
|
364
|
+
sys.path.insert(0, str(_ARIA_CODE_DIR))
|
|
365
|
+
from agents.team import AgentTeam
|
|
366
|
+
team = AgentTeam(agent_names=["technical", "risk"])
|
|
367
|
+
result = await asyncio.wait_for(team.run(symbol), timeout=45.0)
|
|
368
|
+
signal = result.signal or "N/A"
|
|
369
|
+
conf = f"{result.confidence:.0%}" if result.confidence else "?"
|
|
370
|
+
points = "\n".join(f" • {p}" for p in (result.key_points or [])[:3])
|
|
371
|
+
body = (
|
|
372
|
+
f"🤖 *{symbol}* 预警后快速分析\n"
|
|
373
|
+
f"现价 {price:.4f}{pct_str}\n\n"
|
|
374
|
+
f"Signal: *{signal}* 置信度: {conf}\n"
|
|
375
|
+
f"{points}"
|
|
376
|
+
)
|
|
377
|
+
await _telegram_push(body)
|
|
378
|
+
await _feishu_push(f"🤖 {symbol} 预警分析", body.replace("*", "**"))
|
|
379
|
+
except asyncio.TimeoutError:
|
|
380
|
+
logger.warning("_auto_analyze_alert %s: timeout", symbol)
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
logger.debug("_auto_analyze_alert %s: %s", symbol, exc)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ── Telegram command handler ─────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
async def _run_report(symbol: str) -> str:
|
|
388
|
+
"""Generate a quick text summary report."""
|
|
389
|
+
try:
|
|
390
|
+
price, _pc = await _fetch_price(symbol)
|
|
391
|
+
price_str = f"¥{price:.2f}" if price and len(symbol) == 6 and symbol.isdigit() else (f"${price:.2f}" if price else "N/A")
|
|
392
|
+
|
|
393
|
+
# Try to import aria-code market data for indicators
|
|
394
|
+
try:
|
|
395
|
+
import importlib.util
|
|
396
|
+
spec = importlib.util.spec_from_file_location(
|
|
397
|
+
"market_data_client",
|
|
398
|
+
_ARIA_CODE_DIR / "market_data_client.py",
|
|
399
|
+
)
|
|
400
|
+
mdc = importlib.util.module_from_spec(spec)
|
|
401
|
+
spec.loader.exec_module(mdc)
|
|
402
|
+
client = mdc.MarketDataClient()
|
|
403
|
+
quote = await asyncio.wait_for(client.quote(symbol), timeout=8.0)
|
|
404
|
+
name = quote.get("name", symbol)
|
|
405
|
+
chg = quote.get("change_pct", 0)
|
|
406
|
+
vol = quote.get("volume", 0)
|
|
407
|
+
chg_str = f"{chg:+.2f}%" if chg else ""
|
|
408
|
+
except Exception:
|
|
409
|
+
name, chg_str, vol = symbol, "", 0
|
|
410
|
+
|
|
411
|
+
lines = [
|
|
412
|
+
f"*{name}* `{symbol}` 快速分析",
|
|
413
|
+
"",
|
|
414
|
+
f"💰 价格: `{price_str}` {chg_str}",
|
|
415
|
+
f"📊 成交量: `{vol:,}`" if vol else "",
|
|
416
|
+
"",
|
|
417
|
+
f"⚠️ 深度研报请在 Terminal 使用 `/report {symbol}` 命令。",
|
|
418
|
+
"此处为轻量版摘要。",
|
|
419
|
+
]
|
|
420
|
+
return "\n".join(l for l in lines if l is not None)
|
|
421
|
+
|
|
422
|
+
except Exception as exc:
|
|
423
|
+
logger.error("_run_report %s: %s", symbol, exc)
|
|
424
|
+
return f"⚠️ 分析 {symbol} 时出错: {exc}"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _maybe_auto_execute(preview_result: dict) -> list[str]:
|
|
428
|
+
"""Opt-in controlled auto-execution. Default OFF → draft-only.
|
|
429
|
+
|
|
430
|
+
Only executes when the broker config turns on ``automation.enabled`` AND every
|
|
431
|
+
gate in brokers.automation passes (dry-run off, risk passed, value/rate limits).
|
|
432
|
+
"""
|
|
433
|
+
broker_id = preview_result.get("broker_id") or ""
|
|
434
|
+
preview_id = preview_result.get("preview_id") or ""
|
|
435
|
+
if not (broker_id and preview_id):
|
|
436
|
+
return ["Webhook 只生成预览,不会自动执行实盘/仿盘订单。"]
|
|
437
|
+
try:
|
|
438
|
+
from brokers.config import get_broker_config
|
|
439
|
+
from brokers.automation import AutoExecutePolicy, run_auto_execute
|
|
440
|
+
from brokers.registry import get_registry
|
|
441
|
+
|
|
442
|
+
policy = AutoExecutePolicy.from_config(get_broker_config(broker_id) or {})
|
|
443
|
+
if not policy.enabled:
|
|
444
|
+
return ["Webhook 只生成预览,不会自动执行(automation.enabled=false)。"]
|
|
445
|
+
reg = get_registry()
|
|
446
|
+
broker = reg.get(broker_id) or reg.ensure(broker_id)
|
|
447
|
+
result = run_auto_execute(broker, preview_id, policy)
|
|
448
|
+
if result.get("auto_executed"):
|
|
449
|
+
return [f"✅ 已按自动交易策略执行 (order_id: `{result.get('order_id', '')}`)"]
|
|
450
|
+
return ["⏸ 自动执行已跳过: " + "; ".join(str(r) for r in (result.get("reasons") or [])[:3])]
|
|
451
|
+
except Exception as exc:
|
|
452
|
+
logger.warning("auto-execute check failed: %s", exc)
|
|
453
|
+
return [f"自动执行检查失败,保留为预览: `{exc}`"]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def gateway_config() -> dict:
|
|
458
|
+
"""The config the daemon hands to the runtime gateway.
|
|
459
|
+
|
|
460
|
+
This used to be a dict literal pinned to Ollama:
|
|
461
|
+
|
|
462
|
+
{"model": os.environ.get("ARIA_DAEMON_MODEL", "qwen2.5:7b"),
|
|
463
|
+
"ollama_url": ..., "local_provider": "ollama"}
|
|
464
|
+
|
|
465
|
+
so the daemon never saw the user's configuration. Someone running Aria on
|
|
466
|
+
a Vertex/Gemini model had every alert analysed by a local qwen2.5:7b, and
|
|
467
|
+
someone with no Ollama at all had the gateway raise on every alert, fall
|
|
468
|
+
back to the quick summary, and say so only at log level info.
|
|
469
|
+
|
|
470
|
+
The gateway itself is fully config-driven — analyze_alert_via_gateway
|
|
471
|
+
passes model/api_url/ollama_url straight through — so it was only ever the
|
|
472
|
+
daemon fabricating this.
|
|
473
|
+
|
|
474
|
+
Loaded per call rather than cached: the daemon is long-lived, and a config
|
|
475
|
+
change should reach the next alert without a restart. The two env vars stay
|
|
476
|
+
as operator overrides (ARIA_DAEMON_MODEL is in this module's env table).
|
|
477
|
+
"""
|
|
478
|
+
from apps.cli.bootstrap import default_config
|
|
479
|
+
from apps.cli.config_paths import resolve_paths
|
|
480
|
+
from packages.aria_services.settings import SettingsService
|
|
481
|
+
|
|
482
|
+
paths = resolve_paths()
|
|
483
|
+
cfg = SettingsService(
|
|
484
|
+
config_dir=paths.config_dir,
|
|
485
|
+
config_file=paths.config_file,
|
|
486
|
+
sessions_dir=paths.sessions_dir,
|
|
487
|
+
defaults=default_config(),
|
|
488
|
+
).load()
|
|
489
|
+
|
|
490
|
+
if os.environ.get("ARIA_DAEMON_MODEL"):
|
|
491
|
+
cfg["model"] = os.environ["ARIA_DAEMON_MODEL"]
|
|
492
|
+
if os.environ.get("OLLAMA_URL"):
|
|
493
|
+
cfg["ollama_url"] = os.environ["OLLAMA_URL"]
|
|
494
|
+
return cfg
|
|
495
|
+
|
|
496
|
+
async def _run_tradingview_alert(payload: dict) -> str:
|
|
497
|
+
"""Handle a TradingView alert webhook job."""
|
|
498
|
+
try:
|
|
499
|
+
from apps.cli.tradingview_bridge import build_tradingview_order_preview, parse_tradingview_alert
|
|
500
|
+
alert = parse_tradingview_alert(payload)
|
|
501
|
+
except Exception as exc:
|
|
502
|
+
raise ValueError(f"TradingView alert 解析失败: {exc}") from exc
|
|
503
|
+
|
|
504
|
+
symbol = str(alert.get("symbol") or "").upper()
|
|
505
|
+
if not symbol:
|
|
506
|
+
raise ValueError("TradingView alert 缺少 symbol/ticker")
|
|
507
|
+
|
|
508
|
+
action = str(alert.get("action") or "ALERT").upper()
|
|
509
|
+
price = alert.get("price")
|
|
510
|
+
price_line = f"Price: `{price}`" if price not in (None, "") else "Price: `N/A`"
|
|
511
|
+
msg = str(alert.get("message") or "").strip()
|
|
512
|
+
header = [
|
|
513
|
+
f"*TradingView Alert* `{symbol}`",
|
|
514
|
+
f"Action: *{action}*",
|
|
515
|
+
price_line,
|
|
516
|
+
]
|
|
517
|
+
if msg:
|
|
518
|
+
header.append(f"Message: {msg[:300]}")
|
|
519
|
+
|
|
520
|
+
preview_lines: list[str] = []
|
|
521
|
+
try:
|
|
522
|
+
preview_result = build_tradingview_order_preview(payload)
|
|
523
|
+
if preview_result.get("trade_preview_created"):
|
|
524
|
+
blockers = preview_result.get("execution_blockers") or []
|
|
525
|
+
preview_lines.extend([
|
|
526
|
+
"",
|
|
527
|
+
"*Trade Preview*",
|
|
528
|
+
f"preview_id: `{preview_result.get('preview_id')}`",
|
|
529
|
+
f"Mode: `{preview_result.get('mode')}` Broker: `{preview_result.get('broker_label')}`",
|
|
530
|
+
f"Can execute after confirmation: `{bool(preview_result.get('can_execute'))}`",
|
|
531
|
+
f"Confirm: `{preview_result.get('confirm_command')}`",
|
|
532
|
+
])
|
|
533
|
+
if blockers:
|
|
534
|
+
preview_lines.append("Blockers: " + "; ".join(str(item) for item in blockers[:4]))
|
|
535
|
+
preview_lines.extend(_maybe_auto_execute(preview_result))
|
|
536
|
+
elif preview_result.get("success"):
|
|
537
|
+
reason = preview_result.get("reason", "no_trade_preview")
|
|
538
|
+
preview_lines.extend(["", "*Trade Preview*", f"未生成订单预览: `{reason}`"])
|
|
539
|
+
if preview_result.get("hint"):
|
|
540
|
+
preview_lines.append(str(preview_result["hint"]))
|
|
541
|
+
else:
|
|
542
|
+
preview_lines.extend(["", "*Trade Preview*", f"生成失败: `{preview_result.get('error')}`"])
|
|
543
|
+
except Exception as exc:
|
|
544
|
+
logger.warning("TradingView trade preview failed: %s", exc)
|
|
545
|
+
preview_lines.extend(["", "*Trade Preview*", f"生成失败: `{exc}`"])
|
|
546
|
+
|
|
547
|
+
# Channels contract: analysis goes through the shared runtime gateway
|
|
548
|
+
# (tool-less turn) instead of poking CLI internals; the legacy quick
|
|
549
|
+
# summary stays as the fallback when no model backend is reachable.
|
|
550
|
+
analysis = ""
|
|
551
|
+
try:
|
|
552
|
+
from apps.channels.intake import analyze_alert_via_gateway
|
|
553
|
+
from apps.channels.tradingview import task_prompt
|
|
554
|
+
analysis = await asyncio.wait_for(
|
|
555
|
+
analyze_alert_via_gateway(task_prompt(alert), gateway_config()), timeout=90.0
|
|
556
|
+
)
|
|
557
|
+
except Exception as exc:
|
|
558
|
+
# warning, not info: this is a silent capability downgrade — the alert
|
|
559
|
+
# still gets answered, but by the legacy summary rather than the model.
|
|
560
|
+
logger.warning("gateway analysis unavailable (%s) — using quick summary", exc)
|
|
561
|
+
if not analysis:
|
|
562
|
+
analysis = await _run_report(symbol)
|
|
563
|
+
return "\n".join(header + preview_lines) + "\n\n" + analysis
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
async def _run_morning_brief() -> str:
|
|
567
|
+
"""Enriched morning market brief — A-share + US + crypto + FX."""
|
|
568
|
+
try:
|
|
569
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
570
|
+
lines = [f"*🌅 Aria 晨报 {now_str}*\n"]
|
|
571
|
+
|
|
572
|
+
# ── A-share indices ────────────────────────────────────────────────
|
|
573
|
+
cn_indices = [
|
|
574
|
+
("上证指数", "000001.SS"), ("深证成指", "399001.SZ"),
|
|
575
|
+
("创业板", "399006.SZ"), ("沪深300", "000300.SS"),
|
|
576
|
+
]
|
|
577
|
+
lines.append("*🇨🇳 A股指数*")
|
|
578
|
+
for name, sym in cn_indices:
|
|
579
|
+
p, pc = await _fetch_price(sym)
|
|
580
|
+
if p:
|
|
581
|
+
arrow = "📈" if (pc or 0) >= 0 else "📉"
|
|
582
|
+
pct_str = f" {pc:+.2f}%" if pc is not None else ""
|
|
583
|
+
lines.append(f" {arrow} {name}: `¥{p:.2f}`{pct_str}")
|
|
584
|
+
else:
|
|
585
|
+
lines.append(f" — {name}: N/A")
|
|
586
|
+
|
|
587
|
+
# ── US markets ────────────────────────────────────────────────────
|
|
588
|
+
us_indices = [
|
|
589
|
+
("S&P 500", "^GSPC"), ("纳斯达克", "^IXIC"),
|
|
590
|
+
("道琼斯", "^DJI"), ("VIX恐慌", "^VIX"),
|
|
591
|
+
]
|
|
592
|
+
lines.append("\n*🇺🇸 美股指数*")
|
|
593
|
+
for name, sym in us_indices:
|
|
594
|
+
p, pc = await _fetch_price(sym)
|
|
595
|
+
if p:
|
|
596
|
+
arrow = "📈" if (pc or 0) >= 0 else "📉"
|
|
597
|
+
pct_str = f" {pc:+.2f}%" if pc is not None else ""
|
|
598
|
+
lines.append(f" {arrow} {name}: `{p:.2f}`{pct_str}")
|
|
599
|
+
else:
|
|
600
|
+
lines.append(f" — {name}: N/A")
|
|
601
|
+
|
|
602
|
+
# ── Crypto ────────────────────────────────────────────────────────
|
|
603
|
+
crypto_pairs = [("BTC", "BTC-USD"), ("ETH", "ETH-USD"), ("黄金", "GC=F")]
|
|
604
|
+
lines.append("\n*₿ 加密 & 大宗*")
|
|
605
|
+
for name, sym in crypto_pairs:
|
|
606
|
+
p, pc = await _fetch_price(sym)
|
|
607
|
+
if p:
|
|
608
|
+
pct_str = f" {pc:+.2f}%" if pc is not None else ""
|
|
609
|
+
lines.append(f" {name}: `${p:,.2f}`{pct_str}")
|
|
610
|
+
else:
|
|
611
|
+
lines.append(f" {name}: N/A")
|
|
612
|
+
|
|
613
|
+
# ── FX ───────────────────────────────────────────────────────────
|
|
614
|
+
fx_pairs = [("USD/CNY", "CNY=X"), ("USD/JPY", "JPY=X")]
|
|
615
|
+
lines.append("\n*💱 汇率*")
|
|
616
|
+
for name, sym in fx_pairs:
|
|
617
|
+
p, _ = await _fetch_price(sym)
|
|
618
|
+
lines.append(f" {name}: `{p:.4f}`" if p else f" {name}: N/A")
|
|
619
|
+
|
|
620
|
+
# ── Portfolio P&L summary ─────────────────────────────────────────
|
|
621
|
+
pnl = await _get_portfolio_daily_pnl()
|
|
622
|
+
if pnl:
|
|
623
|
+
lines.append("\n*💼 持仓日内 P&L*")
|
|
624
|
+
lines.append(pnl)
|
|
625
|
+
|
|
626
|
+
lines.append("\n_数据来自 yfinance,仅供参考_")
|
|
627
|
+
return "\n".join(lines)
|
|
628
|
+
except Exception as exc:
|
|
629
|
+
logger.warning("morning brief error: %s", exc)
|
|
630
|
+
return f"⚠️ 晨报生成失败: {exc}"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
async def _run_evening_brief() -> str:
|
|
634
|
+
"""Post-market A-share evening summary."""
|
|
635
|
+
try:
|
|
636
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
637
|
+
lines = [f"*🌆 Aria 收盘简报 {now_str}*\n"]
|
|
638
|
+
|
|
639
|
+
cn_indices = [
|
|
640
|
+
("上证指数", "000001.SS"), ("深证成指", "399001.SZ"),
|
|
641
|
+
("创业板", "399006.SZ"), ("沪深300", "000300.SS"),
|
|
642
|
+
]
|
|
643
|
+
lines.append("*今日收盘*")
|
|
644
|
+
for name, sym in cn_indices:
|
|
645
|
+
p, pc = await _fetch_price(sym)
|
|
646
|
+
if p:
|
|
647
|
+
arrow = "📈" if (pc or 0) >= 0 else "📉"
|
|
648
|
+
pct_str = f" {pc:+.2f}%" if pc is not None else ""
|
|
649
|
+
lines.append(f" {arrow} {name}: `¥{p:.2f}`{pct_str}")
|
|
650
|
+
|
|
651
|
+
pnl = await _get_portfolio_daily_pnl()
|
|
652
|
+
if pnl:
|
|
653
|
+
lines.append(f"\n*💼 今日持仓损益*\n{pnl}")
|
|
654
|
+
|
|
655
|
+
lines.append("\n_数据来自 yfinance · 请以实盘数据为准_")
|
|
656
|
+
return "\n".join(lines)
|
|
657
|
+
except Exception as exc:
|
|
658
|
+
return f"⚠️ 收盘简报失败: {exc}"
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
async def _run_weekly_recap() -> str:
|
|
662
|
+
"""Friday end-of-week performance summary."""
|
|
663
|
+
try:
|
|
664
|
+
now_str = datetime.now().strftime("%Y 第%W周")
|
|
665
|
+
lines = [f"*📅 Aria 周报 {now_str}*\n"]
|
|
666
|
+
|
|
667
|
+
us_etfs = [("S&P500", "SPY"), ("纳指", "QQQ"), ("罗素", "IWM"), ("黄金", "GLD")]
|
|
668
|
+
lines.append("*本周美股 ETF 表现*")
|
|
669
|
+
for name, sym in us_etfs:
|
|
670
|
+
try:
|
|
671
|
+
import yfinance as _yf
|
|
672
|
+
df = _yf.download(sym, period="5d", progress=False, auto_adjust=True)
|
|
673
|
+
if not df.empty and len(df) >= 2:
|
|
674
|
+
if hasattr(df.columns, 'levels'):
|
|
675
|
+
df.columns = df.columns.droplevel(1)
|
|
676
|
+
w_ret = (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100
|
|
677
|
+
arrow = "📈" if w_ret >= 0 else "📉"
|
|
678
|
+
lines.append(f" {arrow} {name}: `{w_ret:+.2f}%`")
|
|
679
|
+
except Exception:
|
|
680
|
+
lines.append(f" — {name}: N/A")
|
|
681
|
+
|
|
682
|
+
cn_indices = [("上证", "000001.SS"), ("沪深300", "000300.SS"), ("创业板", "399006.SZ")]
|
|
683
|
+
lines.append("\n*本周A股指数*")
|
|
684
|
+
for name, sym in cn_indices:
|
|
685
|
+
try:
|
|
686
|
+
import yfinance as _yf
|
|
687
|
+
df = _yf.download(sym, period="5d", progress=False, auto_adjust=True)
|
|
688
|
+
if not df.empty and len(df) >= 2:
|
|
689
|
+
if hasattr(df.columns, 'levels'):
|
|
690
|
+
df.columns = df.columns.droplevel(1)
|
|
691
|
+
w_ret = (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100
|
|
692
|
+
arrow = "📈" if w_ret >= 0 else "📉"
|
|
693
|
+
lines.append(f" {arrow} {name}: `{w_ret:+.2f}%`")
|
|
694
|
+
except Exception:
|
|
695
|
+
lines.append(f" — {name}: N/A")
|
|
696
|
+
|
|
697
|
+
lines.append("\n_数据来自 yfinance,仅供参考_")
|
|
698
|
+
return "\n".join(lines)
|
|
699
|
+
except Exception as exc:
|
|
700
|
+
return f"⚠️ 周报生成失败: {exc}"
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
async def _get_portfolio_daily_pnl() -> str:
|
|
704
|
+
"""Read portfolio positions from DB and compute daily P&L."""
|
|
705
|
+
try:
|
|
706
|
+
portfolio_db = _ARIA_DIR / "portfolio.db"
|
|
707
|
+
if not portfolio_db.exists():
|
|
708
|
+
return ""
|
|
709
|
+
with sqlite3.connect(portfolio_db) as conn:
|
|
710
|
+
conn.row_factory = sqlite3.Row
|
|
711
|
+
rows = conn.execute(
|
|
712
|
+
"SELECT symbol, quantity, avg_cost FROM positions WHERE quantity > 0 LIMIT 10"
|
|
713
|
+
).fetchall()
|
|
714
|
+
if not rows:
|
|
715
|
+
return ""
|
|
716
|
+
total_pnl = 0.0
|
|
717
|
+
position_lines = []
|
|
718
|
+
for r in rows:
|
|
719
|
+
sym = r["symbol"]
|
|
720
|
+
qty = r["quantity"]
|
|
721
|
+
cost = r["avg_cost"]
|
|
722
|
+
price, pct = await _fetch_price(sym)
|
|
723
|
+
if price and cost and qty:
|
|
724
|
+
day_pnl = (price - cost) * qty
|
|
725
|
+
total_pnl += day_pnl
|
|
726
|
+
arrow = "📈" if day_pnl >= 0 else "📉"
|
|
727
|
+
position_lines.append(
|
|
728
|
+
f" {arrow} {sym}: ¥{price:.2f} 日盈亏 `{day_pnl:+.2f}`"
|
|
729
|
+
)
|
|
730
|
+
if not position_lines:
|
|
731
|
+
return ""
|
|
732
|
+
result = "\n".join(position_lines)
|
|
733
|
+
result += f"\n 合计: `{total_pnl:+.2f}`"
|
|
734
|
+
return result
|
|
735
|
+
except Exception as exc:
|
|
736
|
+
logger.debug("portfolio pnl error: %s", exc)
|
|
737
|
+
return ""
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
async def _portfolio_loss_watchdog() -> None:
|
|
741
|
+
"""Check portfolio positions every 5 min; push alert if any position drops > threshold."""
|
|
742
|
+
LOSS_THRESHOLD_PCT = float(os.environ.get("ARIA_LOSS_ALERT_PCT", "5.0"))
|
|
743
|
+
logger.info("Portfolio loss watchdog started (threshold: %.1f%%)", LOSS_THRESHOLD_PCT)
|
|
744
|
+
while True:
|
|
745
|
+
try:
|
|
746
|
+
portfolio_db = _ARIA_DIR / "portfolio.db"
|
|
747
|
+
if portfolio_db.exists():
|
|
748
|
+
with sqlite3.connect(portfolio_db) as conn:
|
|
749
|
+
conn.row_factory = sqlite3.Row
|
|
750
|
+
rows = conn.execute(
|
|
751
|
+
"SELECT symbol, quantity, avg_cost FROM positions WHERE quantity > 0"
|
|
752
|
+
).fetchall()
|
|
753
|
+
for r in rows:
|
|
754
|
+
sym = r["symbol"]
|
|
755
|
+
qty = float(r["quantity"])
|
|
756
|
+
cost = float(r["avg_cost"] or 0)
|
|
757
|
+
if qty <= 0 or cost <= 0:
|
|
758
|
+
continue
|
|
759
|
+
price, pct = await _fetch_price(sym)
|
|
760
|
+
if price and pct is not None and pct <= -LOSS_THRESHOLD_PCT:
|
|
761
|
+
day_loss = (price - cost) * qty
|
|
762
|
+
title = f"⚠️ {sym} 跌幅预警"
|
|
763
|
+
body = (
|
|
764
|
+
f"{sym} 当前价 ¥{price:.2f},"
|
|
765
|
+
f"日跌幅 {pct:.2f}%,"
|
|
766
|
+
f"持仓损益 {day_loss:+.2f} 元"
|
|
767
|
+
)
|
|
768
|
+
logger.info("Portfolio loss alert: %s %.2f%%", sym, pct)
|
|
769
|
+
await _push_alert(title, body)
|
|
770
|
+
await _telegram_push(f"*{title}*\n{body}")
|
|
771
|
+
await _feishu_push(title, body)
|
|
772
|
+
except Exception as exc:
|
|
773
|
+
logger.debug("portfolio watchdog error: %s", exc)
|
|
774
|
+
await asyncio.sleep(300)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _system_notify(title: str, body: str) -> None:
|
|
778
|
+
"""Fire a native OS desktop notification (non-blocking, best-effort)."""
|
|
779
|
+
try:
|
|
780
|
+
import platform as _pl
|
|
781
|
+
if _pl.system() == "Darwin":
|
|
782
|
+
import subprocess as _sp
|
|
783
|
+
safe_title = title.replace('"', '\\"').replace("'", "\\'")
|
|
784
|
+
safe_body = body.replace('"', '\\"').replace("'", "\\'")
|
|
785
|
+
_sp.Popen(
|
|
786
|
+
["osascript", "-e",
|
|
787
|
+
f'display notification "{safe_body}" with title "{safe_title}" subtitle "Aria Code"'],
|
|
788
|
+
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
789
|
+
)
|
|
790
|
+
elif _pl.system() == "Windows":
|
|
791
|
+
try:
|
|
792
|
+
from win10toast import ToastNotifier
|
|
793
|
+
ToastNotifier().show_toast(title, body, duration=6, threaded=True)
|
|
794
|
+
except ImportError:
|
|
795
|
+
pass
|
|
796
|
+
except Exception:
|
|
797
|
+
pass
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
async def _run_screener() -> str:
|
|
801
|
+
"""Quick hot-stock screener using market_data_client if available."""
|
|
802
|
+
try:
|
|
803
|
+
spec = __import__("importlib.util").util.spec_from_file_location(
|
|
804
|
+
"market_data_client", _ARIA_CODE_DIR / "market_data_client.py"
|
|
805
|
+
)
|
|
806
|
+
mdc = __import__("importlib.util").util.module_from_spec(spec)
|
|
807
|
+
spec.loader.exec_module(mdc)
|
|
808
|
+
client = mdc.MarketDataClient()
|
|
809
|
+
result = await asyncio.wait_for(client.hot_ashare(limit=8), timeout=10.0)
|
|
810
|
+
stocks = result if isinstance(result, list) else []
|
|
811
|
+
if not stocks:
|
|
812
|
+
return "⚠️ 暂时无法获取行情数据(市场未开市或数据源异常)"
|
|
813
|
+
lines = ["*🔥 A股热门*\n"]
|
|
814
|
+
for s in stocks[:8]:
|
|
815
|
+
sym = s.get("symbol", "")
|
|
816
|
+
name = s.get("name", sym)
|
|
817
|
+
p = s.get("price", 0)
|
|
818
|
+
chg = s.get("change_pct", 0)
|
|
819
|
+
lines.append(f" `{sym}` {name} ¥{p:.2f} {chg:+.2f}%")
|
|
820
|
+
return "\n".join(lines)
|
|
821
|
+
except Exception as exc:
|
|
822
|
+
return f"⚠️ 筛选器出错: {exc}"
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
async def _run_chat(text: str) -> str:
|
|
826
|
+
"""Simple chat fallback using aria-code LLM if configured."""
|
|
827
|
+
return f"💬 _收到你的消息: \"{text[:80]}\"_\n\n请在 Terminal 启动 `/aria` 获得完整对话体验。"
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ── Webhook job executor ─────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
async def _webhook_executor() -> None:
|
|
833
|
+
"""Poll webhook_jobs table for pending jobs and execute them."""
|
|
834
|
+
logger.info("Webhook executor started")
|
|
835
|
+
while True:
|
|
836
|
+
try:
|
|
837
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
838
|
+
conn.row_factory = sqlite3.Row
|
|
839
|
+
rows = conn.execute(
|
|
840
|
+
"SELECT * FROM webhook_jobs WHERE status='pending' ORDER BY created_at LIMIT 5"
|
|
841
|
+
).fetchall()
|
|
842
|
+
for row in rows:
|
|
843
|
+
asyncio.create_task(_execute_job(dict(row)))
|
|
844
|
+
except Exception as exc:
|
|
845
|
+
logger.error("Webhook executor error: %s", exc)
|
|
846
|
+
await asyncio.sleep(2)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
async def _webhook_http_server() -> None:
|
|
850
|
+
"""Small local HTTP endpoint for TradingView webhooks."""
|
|
851
|
+
try:
|
|
852
|
+
from aiohttp import web
|
|
853
|
+
from apps.cli.tradingview_bridge import enqueue_tradingview_alert
|
|
854
|
+
except ImportError as exc:
|
|
855
|
+
logger.warning("Webhook HTTP server disabled: %s", exc)
|
|
856
|
+
return
|
|
857
|
+
|
|
858
|
+
token = os.environ.get("TRADINGVIEW_WEBHOOK_SECRET") or os.environ.get("WEBHOOK_TOKEN", "")
|
|
859
|
+
host = os.environ.get("ARIA_WEBHOOK_HOST", "127.0.0.1")
|
|
860
|
+
port = int(os.environ.get("ARIA_WEBHOOK_PORT", "8765"))
|
|
861
|
+
|
|
862
|
+
def _authorized(request) -> bool:
|
|
863
|
+
if not token:
|
|
864
|
+
return True
|
|
865
|
+
# Headers only: a token in the query string ends up in proxy and
|
|
866
|
+
# access logs and in browser history. TradingView, which cannot set
|
|
867
|
+
# headers, authenticates with the body passphrase (ARIA_WEBHOOK_SECRET).
|
|
868
|
+
supplied = (
|
|
869
|
+
request.headers.get("X-Aria-Token")
|
|
870
|
+
or request.headers.get("X-TradingView-Secret")
|
|
871
|
+
or request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
|
|
872
|
+
or ""
|
|
873
|
+
)
|
|
874
|
+
return hmac.compare_digest(supplied.encode("utf-8"), token.encode("utf-8"))
|
|
875
|
+
|
|
876
|
+
async def _handle_tradingview(request):
|
|
877
|
+
# Open-mode guard (channels contract): with neither an endpoint token
|
|
878
|
+
# nor ARIA_WEBHOOK_SECRET configured, only loopback clients may submit —
|
|
879
|
+
# an open endpoint reachable off-host would accept injected alerts.
|
|
880
|
+
try:
|
|
881
|
+
from apps.channels.intake import should_refuse_open_intake
|
|
882
|
+
except ImportError:
|
|
883
|
+
# Without the guard we cannot tell whether this client may use an
|
|
884
|
+
# open endpoint, so it may not: fail closed rather than skip it.
|
|
885
|
+
logger.error("open-intake guard unavailable; refusing TradingView webhook")
|
|
886
|
+
return web.json_response(
|
|
887
|
+
{"success": False, "error": "webhook intake guard unavailable"}, status=503,
|
|
888
|
+
)
|
|
889
|
+
secret_configured = bool(os.environ.get("ARIA_WEBHOOK_SECRET", "").strip())
|
|
890
|
+
if should_refuse_open_intake(
|
|
891
|
+
getattr(request, "remote", None),
|
|
892
|
+
token_configured=bool(token),
|
|
893
|
+
secret_configured=secret_configured,
|
|
894
|
+
):
|
|
895
|
+
return web.json_response(
|
|
896
|
+
{"success": False,
|
|
897
|
+
"error": "open-mode intake refused for non-local client; set ARIA_WEBHOOK_SECRET or WEBHOOK_TOKEN"},
|
|
898
|
+
status=403,
|
|
899
|
+
)
|
|
900
|
+
if not _authorized(request):
|
|
901
|
+
return web.json_response({"success": False, "error": "unauthorized"}, status=401)
|
|
902
|
+
text = await request.text()
|
|
903
|
+
try:
|
|
904
|
+
import json as _json
|
|
905
|
+
payload = _json.loads(text) if text.strip() else {}
|
|
906
|
+
except Exception:
|
|
907
|
+
payload = {"message": text}
|
|
908
|
+
if request.query.get("channels") and isinstance(payload, dict):
|
|
909
|
+
import json as _json
|
|
910
|
+
payload["channels"] = _json.dumps(
|
|
911
|
+
[item.strip() for item in request.query["channels"].split(",") if item.strip()]
|
|
912
|
+
)
|
|
913
|
+
if isinstance(payload, dict):
|
|
914
|
+
for key in ("broker_id", "quantity", "qty", "target_weight", "order_type"):
|
|
915
|
+
if request.query.get(key):
|
|
916
|
+
payload[key] = request.query[key]
|
|
917
|
+
result = enqueue_tradingview_alert(payload, db_path=_DB_PATH)
|
|
918
|
+
status = 202 if result.get("success") else 400
|
|
919
|
+
return web.json_response(result, status=status)
|
|
920
|
+
|
|
921
|
+
app = web.Application()
|
|
922
|
+
app.router.add_post("/api/v1/webhook/tradingview", _handle_tradingview)
|
|
923
|
+
app.router.add_post("/api/v1/tradingview/webhook", _handle_tradingview)
|
|
924
|
+
|
|
925
|
+
runner = web.AppRunner(app)
|
|
926
|
+
await runner.setup()
|
|
927
|
+
site = web.TCPSite(runner, host, port)
|
|
928
|
+
try:
|
|
929
|
+
await site.start()
|
|
930
|
+
except OSError as exc:
|
|
931
|
+
logger.error(
|
|
932
|
+
"TradingView webhook could not listen on %s:%d (%s). The relay server "
|
|
933
|
+
"also defaults to 8765; set ARIA_WEBHOOK_PORT to another port.", host, port, exc,
|
|
934
|
+
)
|
|
935
|
+
await runner.cleanup()
|
|
936
|
+
return
|
|
937
|
+
logger.info("TradingView webhook listening on http://%s:%d/api/v1/webhook/tradingview", host, port)
|
|
938
|
+
try:
|
|
939
|
+
while True:
|
|
940
|
+
await asyncio.sleep(3600)
|
|
941
|
+
finally:
|
|
942
|
+
await runner.cleanup()
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
async def _execute_job(job: dict) -> None:
|
|
946
|
+
job_id = job["id"]
|
|
947
|
+
command = job["command"]
|
|
948
|
+
try:
|
|
949
|
+
import json as _json
|
|
950
|
+
payload = _json.loads(job.get("payload") or "{}")
|
|
951
|
+
except Exception:
|
|
952
|
+
payload = {}
|
|
953
|
+
|
|
954
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
955
|
+
conn.execute(
|
|
956
|
+
"UPDATE webhook_jobs SET status='running', started_at=datetime('now') WHERE id=?",
|
|
957
|
+
(job_id,),
|
|
958
|
+
)
|
|
959
|
+
conn.commit()
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
if command.startswith("chat:"):
|
|
963
|
+
result = await _run_chat(command[5:])
|
|
964
|
+
elif command.startswith("report ") or command.startswith("/report "):
|
|
965
|
+
sym = command.split()[-1].upper()
|
|
966
|
+
result = await _run_report(sym)
|
|
967
|
+
elif command == "tradingview_alert":
|
|
968
|
+
result = await _run_tradingview_alert(payload)
|
|
969
|
+
elif command in ("morning-brief", "/morning-brief", "brief"):
|
|
970
|
+
result = await _run_morning_brief()
|
|
971
|
+
elif command in ("screen", "/screen"):
|
|
972
|
+
result = await _run_screener()
|
|
973
|
+
else:
|
|
974
|
+
raise ValueError(f"未知 Webhook 命令: {command!r}(支持: chat: / report / morning-brief / screen)")
|
|
975
|
+
|
|
976
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
977
|
+
conn.execute(
|
|
978
|
+
"UPDATE webhook_jobs SET status='done', result=?, done_at=datetime('now') WHERE id=?",
|
|
979
|
+
(result[:2000], job_id),
|
|
980
|
+
)
|
|
981
|
+
conn.commit()
|
|
982
|
+
|
|
983
|
+
# If channels include telegram, send result
|
|
984
|
+
channels = []
|
|
985
|
+
try:
|
|
986
|
+
import json as _j
|
|
987
|
+
raw_channels = payload.get("channels", "[]")
|
|
988
|
+
if isinstance(raw_channels, list):
|
|
989
|
+
channels = raw_channels
|
|
990
|
+
else:
|
|
991
|
+
channels = _j.loads(raw_channels or "[]")
|
|
992
|
+
except Exception:
|
|
993
|
+
pass
|
|
994
|
+
if command == "tradingview_alert" and not channels:
|
|
995
|
+
channels = ["telegram", "feishu"]
|
|
996
|
+
if "telegram" in channels:
|
|
997
|
+
await _telegram_push(result)
|
|
998
|
+
if "feishu" in channels:
|
|
999
|
+
await _feishu_push("⏰ Webhook 任务完成", result[:2000])
|
|
1000
|
+
|
|
1001
|
+
except Exception as exc:
|
|
1002
|
+
logger.error("Job %s failed: %s", job_id, exc)
|
|
1003
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
1004
|
+
conn.execute(
|
|
1005
|
+
"UPDATE webhook_jobs SET status='error', result=?, done_at=datetime('now') WHERE id=?",
|
|
1006
|
+
(str(exc)[:500], job_id),
|
|
1007
|
+
)
|
|
1008
|
+
conn.commit()
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
# ── Shipper digest ───────────────────────────────────────────────────────────
|
|
1012
|
+
|
|
1013
|
+
async def _run_shipper_digests() -> None:
|
|
1014
|
+
"""Push each bound shipper group its digest (apps/channels/digest.py)."""
|
|
1015
|
+
try:
|
|
1016
|
+
from apps.channels.conversation import ConversationStore
|
|
1017
|
+
from apps.channels.digest import run_digests
|
|
1018
|
+
|
|
1019
|
+
senders = {}
|
|
1020
|
+
import aria_feishu_bot
|
|
1021
|
+
# With the app's credentials (self-hosted), or through the relay client
|
|
1022
|
+
# this daemon runs in relay mode — the bot knows which it has.
|
|
1023
|
+
if aria_feishu_bot.can_send():
|
|
1024
|
+
senders["feishu"] = aria_feishu_bot.send_digest_card
|
|
1025
|
+
report = await run_digests(ConversationStore(), senders)
|
|
1026
|
+
for line in report:
|
|
1027
|
+
logger.info("shipper digest %s: %s", line["conversation"], line["status"])
|
|
1028
|
+
except Exception as exc:
|
|
1029
|
+
logger.error("shipper digest job failed: %s", exc)
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
# ── APScheduler cron ─────────────────────────────────────────────────────────
|
|
1033
|
+
|
|
1034
|
+
def _start_scheduler() -> None:
|
|
1035
|
+
"""Load schedules from DB and register with APScheduler."""
|
|
1036
|
+
try:
|
|
1037
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
1038
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
1039
|
+
|
|
1040
|
+
scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
|
|
1041
|
+
|
|
1042
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
1043
|
+
conn.row_factory = sqlite3.Row
|
|
1044
|
+
rows = conn.execute(
|
|
1045
|
+
"SELECT * FROM schedules WHERE enabled=1"
|
|
1046
|
+
).fetchall()
|
|
1047
|
+
|
|
1048
|
+
for row in rows:
|
|
1049
|
+
s = dict(row)
|
|
1050
|
+
try:
|
|
1051
|
+
trigger = CronTrigger.from_crontab(s["cron_expr"], timezone="Asia/Shanghai")
|
|
1052
|
+
scheduler.add_job(
|
|
1053
|
+
_run_scheduled_job,
|
|
1054
|
+
trigger=trigger,
|
|
1055
|
+
args=[s],
|
|
1056
|
+
id=s["id"],
|
|
1057
|
+
name=s.get("name") or s["command"],
|
|
1058
|
+
replace_existing=True,
|
|
1059
|
+
misfire_grace_time=300,
|
|
1060
|
+
)
|
|
1061
|
+
logger.info("Scheduled: %s [%s]", s.get("name", s["id"]), s["cron_expr"])
|
|
1062
|
+
except Exception as exc:
|
|
1063
|
+
logger.error("Failed to schedule %s: %s", s["id"], exc)
|
|
1064
|
+
|
|
1065
|
+
# Default scheduled jobs (always register regardless of user schedules)
|
|
1066
|
+
# A-share pre-market brief: 08:55 weekdays (before 09:30 open)
|
|
1067
|
+
scheduler.add_job(
|
|
1068
|
+
_run_morning_brief_and_push,
|
|
1069
|
+
CronTrigger.from_crontab("55 8 * * 1-5", timezone="Asia/Shanghai"),
|
|
1070
|
+
id="default_morning_brief",
|
|
1071
|
+
name="A股开盘前晨报",
|
|
1072
|
+
replace_existing=True,
|
|
1073
|
+
misfire_grace_time=600,
|
|
1074
|
+
)
|
|
1075
|
+
# US pre-market brief: 21:00 weekdays CN time (US market opens 21:30 CN)
|
|
1076
|
+
scheduler.add_job(
|
|
1077
|
+
_run_morning_brief_and_push,
|
|
1078
|
+
CronTrigger.from_crontab("0 21 * * 1-5", timezone="Asia/Shanghai"),
|
|
1079
|
+
id="default_us_brief",
|
|
1080
|
+
name="美股开盘前简报",
|
|
1081
|
+
replace_existing=True,
|
|
1082
|
+
misfire_grace_time=600,
|
|
1083
|
+
)
|
|
1084
|
+
# A-share post-market evening brief: 15:35 weekdays
|
|
1085
|
+
scheduler.add_job(
|
|
1086
|
+
_run_evening_brief_and_push,
|
|
1087
|
+
CronTrigger.from_crontab("35 15 * * 1-5", timezone="Asia/Shanghai"),
|
|
1088
|
+
id="default_evening_brief",
|
|
1089
|
+
name="A股收盘晚报",
|
|
1090
|
+
replace_existing=True,
|
|
1091
|
+
misfire_grace_time=600,
|
|
1092
|
+
)
|
|
1093
|
+
# Weekly recap: Friday 16:30 CN time
|
|
1094
|
+
scheduler.add_job(
|
|
1095
|
+
_run_weekly_recap_and_push,
|
|
1096
|
+
CronTrigger.from_crontab("30 16 * * 5", timezone="Asia/Shanghai"),
|
|
1097
|
+
id="default_weekly_recap",
|
|
1098
|
+
name="周报",
|
|
1099
|
+
replace_existing=True,
|
|
1100
|
+
misfire_grace_time=1800,
|
|
1101
|
+
)
|
|
1102
|
+
logger.info("Registered 4 default scheduled jobs (morning / US / evening / weekly)")
|
|
1103
|
+
|
|
1104
|
+
# Shipper digest: each group bound to a shipper gets what needs
|
|
1105
|
+
# attention. Only when the operator has said where the data is.
|
|
1106
|
+
if os.environ.get("ARIA_SHIPPER_FEEDS", "").strip():
|
|
1107
|
+
cron = os.environ.get("ARIA_DIGEST_CRON", "30 8 * * 1-6").strip()
|
|
1108
|
+
scheduler.add_job(
|
|
1109
|
+
_run_shipper_digests,
|
|
1110
|
+
CronTrigger.from_crontab(cron, timezone="Asia/Shanghai"),
|
|
1111
|
+
id="default_shipper_digest",
|
|
1112
|
+
name="货主库存与运费提醒",
|
|
1113
|
+
replace_existing=True,
|
|
1114
|
+
misfire_grace_time=1800,
|
|
1115
|
+
)
|
|
1116
|
+
logger.info("Registered shipper digest [%s]", cron)
|
|
1117
|
+
|
|
1118
|
+
scheduler.start()
|
|
1119
|
+
logger.info("APScheduler started with %d job(s)", len(scheduler.get_jobs()))
|
|
1120
|
+
return scheduler
|
|
1121
|
+
except ImportError:
|
|
1122
|
+
logger.warning("APScheduler not installed — cron disabled. pip install apscheduler")
|
|
1123
|
+
return None
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
async def _run_scheduled_job(schedule: dict) -> None:
|
|
1127
|
+
if _go_daemon_running():
|
|
1128
|
+
logger.info("Scheduled job %s left to aria-runtime daemon", schedule.get("name", schedule["id"]))
|
|
1129
|
+
return
|
|
1130
|
+
logger.info("Running scheduled job: %s", schedule.get("name", schedule["id"]))
|
|
1131
|
+
import json as _j
|
|
1132
|
+
channels = _j.loads(schedule.get("channels") or '["ios","telegram"]')
|
|
1133
|
+
cmd = schedule["command"]
|
|
1134
|
+
result = ""
|
|
1135
|
+
try:
|
|
1136
|
+
if cmd in ("morning-brief", "morning"):
|
|
1137
|
+
result = await _run_morning_brief()
|
|
1138
|
+
elif cmd in ("evening-brief", "evening"):
|
|
1139
|
+
result = await _run_evening_brief()
|
|
1140
|
+
elif cmd in ("weekly-recap", "weekly"):
|
|
1141
|
+
result = await _run_weekly_recap()
|
|
1142
|
+
elif cmd == "screen":
|
|
1143
|
+
result = await _run_screener()
|
|
1144
|
+
elif cmd.startswith("report "):
|
|
1145
|
+
result = await _run_report(cmd.split()[-1].upper())
|
|
1146
|
+
elif cmd.startswith("custom:"):
|
|
1147
|
+
result = f"Custom job: {cmd[7:]}"
|
|
1148
|
+
else:
|
|
1149
|
+
raise ValueError(
|
|
1150
|
+
f"未知定时命令: {cmd!r}(支持: morning-brief / evening-brief / weekly-recap / screen / report <symbol> / custom:<payload>)"
|
|
1151
|
+
)
|
|
1152
|
+
except Exception as exc:
|
|
1153
|
+
result = f"⚠️ 定时任务 [{cmd}] 失败: {exc}"
|
|
1154
|
+
|
|
1155
|
+
if "telegram" in channels:
|
|
1156
|
+
await _telegram_push(f"⏰ *定时任务完成*\n{result}")
|
|
1157
|
+
if "feishu" in channels:
|
|
1158
|
+
await _feishu_push(f"⏰ 定时任务:{cmd}", result[:2000])
|
|
1159
|
+
if "ios" in channels:
|
|
1160
|
+
await _push_alert(
|
|
1161
|
+
"Aria 定时任务",
|
|
1162
|
+
result[:150],
|
|
1163
|
+
{"command": cmd},
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
with sqlite3.connect(_DB_PATH) as conn:
|
|
1167
|
+
conn.execute(
|
|
1168
|
+
"UPDATE schedules SET last_run=datetime('now') WHERE id=?",
|
|
1169
|
+
(schedule["id"],),
|
|
1170
|
+
)
|
|
1171
|
+
conn.commit()
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
async def _run_morning_brief_and_push() -> None:
|
|
1175
|
+
if _go_daemon_running():
|
|
1176
|
+
return
|
|
1177
|
+
brief = await _run_morning_brief()
|
|
1178
|
+
await _telegram_push(brief)
|
|
1179
|
+
await _feishu_push("📊 Aria 晨报", brief[:2000])
|
|
1180
|
+
await _push_alert("Aria 晨报", brief[:150])
|
|
1181
|
+
_system_notify("Aria 晨报", brief[:120])
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
async def _run_evening_brief_and_push() -> None:
|
|
1185
|
+
if _go_daemon_running():
|
|
1186
|
+
return
|
|
1187
|
+
brief = await _run_evening_brief()
|
|
1188
|
+
await _telegram_push(brief)
|
|
1189
|
+
await _feishu_push("🌆 Aria 收盘简报", brief[:2000])
|
|
1190
|
+
await _push_alert("Aria 收盘", brief[:150])
|
|
1191
|
+
_system_notify("Aria 收盘简报", brief[:120])
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
async def _run_weekly_recap_and_push() -> None:
|
|
1195
|
+
if _go_daemon_running():
|
|
1196
|
+
return
|
|
1197
|
+
recap = await _run_weekly_recap()
|
|
1198
|
+
await _telegram_push(recap)
|
|
1199
|
+
await _feishu_push("📅 Aria 周报", recap[:2000])
|
|
1200
|
+
await _push_alert("Aria 周报", recap[:150])
|
|
1201
|
+
_system_notify("Aria 周报", recap[:120])
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
# ── PID management ────────────────────────────────────────────────────────────
|
|
1205
|
+
|
|
1206
|
+
def _write_pid() -> None:
|
|
1207
|
+
_ARIA_DIR.mkdir(parents=True, exist_ok=True)
|
|
1208
|
+
_PID_FILE.write_text(str(os.getpid()))
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _remove_pid() -> None:
|
|
1212
|
+
try:
|
|
1213
|
+
_PID_FILE.unlink(missing_ok=True)
|
|
1214
|
+
except Exception:
|
|
1215
|
+
pass
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
# ── Install / uninstall as macOS LaunchAgent ──────────────────────────────────
|
|
1219
|
+
|
|
1220
|
+
def _install_launchagent() -> None:
|
|
1221
|
+
python = sys.executable
|
|
1222
|
+
script = str(Path(__file__).resolve())
|
|
1223
|
+
log_out = str(_LOG_DIR / "daemon.log")
|
|
1224
|
+
log_err = str(_LOG_DIR / "daemon.err")
|
|
1225
|
+
plist_dir = Path.home() / "Library" / "LaunchAgents"
|
|
1226
|
+
plist_dir.mkdir(parents=True, exist_ok=True)
|
|
1227
|
+
plist_path = plist_dir / "com.aria.daemon.plist"
|
|
1228
|
+
|
|
1229
|
+
plist = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
1230
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
1231
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1232
|
+
<plist version="1.0">
|
|
1233
|
+
<dict>
|
|
1234
|
+
<key>Label</key>
|
|
1235
|
+
<string>com.aria.daemon</string>
|
|
1236
|
+
<key>ProgramArguments</key>
|
|
1237
|
+
<array>
|
|
1238
|
+
<string>{python}</string>
|
|
1239
|
+
<string>{script}</string>
|
|
1240
|
+
</array>
|
|
1241
|
+
<key>RunAtLoad</key>
|
|
1242
|
+
<true/>
|
|
1243
|
+
<key>KeepAlive</key>
|
|
1244
|
+
<true/>
|
|
1245
|
+
<key>StandardOutPath</key>
|
|
1246
|
+
<string>{log_out}</string>
|
|
1247
|
+
<key>StandardErrorPath</key>
|
|
1248
|
+
<string>{log_err}</string>
|
|
1249
|
+
<key>EnvironmentVariables</key>
|
|
1250
|
+
<dict>
|
|
1251
|
+
<key>PATH</key>
|
|
1252
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:{str(Path(python).parent)}</string>
|
|
1253
|
+
<key>ARIA_CODE_DIR</key>
|
|
1254
|
+
<string>{str(_ARIA_CODE_DIR)}</string>
|
|
1255
|
+
</dict>
|
|
1256
|
+
<key>ThrottleInterval</key>
|
|
1257
|
+
<integer>30</integer>
|
|
1258
|
+
</dict>
|
|
1259
|
+
</plist>
|
|
1260
|
+
"""
|
|
1261
|
+
plist_path.write_text(plist)
|
|
1262
|
+
os.system(f"launchctl unload '{plist_path}' 2>/dev/null; launchctl load -w '{plist_path}'")
|
|
1263
|
+
print(f"✅ Aria Daemon installed as LaunchAgent: {plist_path}")
|
|
1264
|
+
print(f" Logs: {log_out}")
|
|
1265
|
+
print(f" To uninstall: python3 {script} --uninstall")
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _uninstall_launchagent() -> None:
|
|
1269
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / "com.aria.daemon.plist"
|
|
1270
|
+
if plist_path.exists():
|
|
1271
|
+
os.system(f"launchctl unload '{plist_path}' 2>/dev/null")
|
|
1272
|
+
plist_path.unlink()
|
|
1273
|
+
print("✅ Aria Daemon LaunchAgent removed")
|
|
1274
|
+
else:
|
|
1275
|
+
print("ℹ️ LaunchAgent not installed")
|
|
1276
|
+
_remove_pid()
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
1280
|
+
|
|
1281
|
+
async def main() -> None:
|
|
1282
|
+
logger.info("═" * 55)
|
|
1283
|
+
logger.info(" Aria Daemon starting PID=%d", os.getpid())
|
|
1284
|
+
logger.info("═" * 55)
|
|
1285
|
+
_write_pid()
|
|
1286
|
+
|
|
1287
|
+
loop = asyncio.get_event_loop()
|
|
1288
|
+
# Graceful shutdown on SIGINT / SIGTERM
|
|
1289
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
1290
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(_shutdown()))
|
|
1291
|
+
|
|
1292
|
+
tasks = [
|
|
1293
|
+
asyncio.create_task(_alert_watchdog(), name="alert_watchdog"),
|
|
1294
|
+
asyncio.create_task(_webhook_executor(), name="webhook_executor"),
|
|
1295
|
+
asyncio.create_task(_webhook_http_server(), name="webhook_http_server"),
|
|
1296
|
+
asyncio.create_task(_portfolio_loss_watchdog(), name="portfolio_watchdog"),
|
|
1297
|
+
]
|
|
1298
|
+
|
|
1299
|
+
scheduler = _start_scheduler()
|
|
1300
|
+
|
|
1301
|
+
# Start Telegram bot if token configured
|
|
1302
|
+
tg_token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
1303
|
+
if tg_token:
|
|
1304
|
+
# Commands are answered by services/aria-bots (aria-runtime daemon for
|
|
1305
|
+
# market data, the Go engine for everything else).
|
|
1306
|
+
from aria_telegram_bot import TelegramBot, handle_command as _telegram_command
|
|
1307
|
+
allowed_raw = os.environ.get("TELEGRAM_ALLOWED_IDS", "")
|
|
1308
|
+
allowed_ids = set(
|
|
1309
|
+
int(x.strip()) for x in allowed_raw.split(",") if x.strip().isdigit()
|
|
1310
|
+
)
|
|
1311
|
+
bot = TelegramBot(token=tg_token, allowed_chat_ids=allowed_ids)
|
|
1312
|
+
me = await bot.get_me()
|
|
1313
|
+
if me:
|
|
1314
|
+
logger.info("Telegram bot: @%s", me.get("username", "?"))
|
|
1315
|
+
await _telegram_push("🤖 *Aria Daemon 已启动*\n发送 `/help` 查看命令。")
|
|
1316
|
+
tasks.append(asyncio.create_task(bot.start(_telegram_command), name="telegram_bot"))
|
|
1317
|
+
else:
|
|
1318
|
+
logger.info("TELEGRAM_BOT_TOKEN not set — Telegram bot disabled")
|
|
1319
|
+
|
|
1320
|
+
# Start Feishu relay client if relay mode configured
|
|
1321
|
+
relay_url = os.environ.get("ARIA_RELAY_URL", "")
|
|
1322
|
+
relay_client_id = os.environ.get("ARIA_RELAY_CLIENT_ID", "")
|
|
1323
|
+
relay_mode = os.environ.get("ARIA_RELAY_MODE", "")
|
|
1324
|
+
if relay_url and relay_client_id and relay_mode == "relay":
|
|
1325
|
+
try:
|
|
1326
|
+
from aria_relay_client import _connect_and_serve as _relay_serve
|
|
1327
|
+
tasks.append(asyncio.create_task(_relay_serve(), name="feishu_relay"))
|
|
1328
|
+
logger.info("Feishu relay client started → %s (client=%s)", relay_url, relay_client_id)
|
|
1329
|
+
except ImportError:
|
|
1330
|
+
logger.warning("aria_relay_client.py not found — Feishu relay disabled")
|
|
1331
|
+
|
|
1332
|
+
logger.info("All workers started. Daemon running.")
|
|
1333
|
+
|
|
1334
|
+
try:
|
|
1335
|
+
await asyncio.gather(*tasks)
|
|
1336
|
+
except asyncio.CancelledError:
|
|
1337
|
+
pass
|
|
1338
|
+
finally:
|
|
1339
|
+
_remove_pid()
|
|
1340
|
+
if scheduler:
|
|
1341
|
+
scheduler.shutdown(wait=False)
|
|
1342
|
+
logger.info("Aria Daemon stopped")
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
async def _shutdown() -> None:
|
|
1346
|
+
logger.info("Shutdown signal received")
|
|
1347
|
+
for task in asyncio.all_tasks():
|
|
1348
|
+
if task.get_name() not in ("main_task",):
|
|
1349
|
+
task.cancel()
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
if __name__ == "__main__":
|
|
1353
|
+
if "--install" in sys.argv:
|
|
1354
|
+
_install_launchagent()
|
|
1355
|
+
elif "--uninstall" in sys.argv:
|
|
1356
|
+
_uninstall_launchagent()
|
|
1357
|
+
elif "--status" in sys.argv:
|
|
1358
|
+
pid_file = _ARIA_DIR / "daemon.pid"
|
|
1359
|
+
if pid_file.exists():
|
|
1360
|
+
try:
|
|
1361
|
+
pid = int(pid_file.read_text().strip())
|
|
1362
|
+
os.kill(pid, 0)
|
|
1363
|
+
print(f"✅ Aria Daemon is running (PID {pid})")
|
|
1364
|
+
except ProcessLookupError:
|
|
1365
|
+
print("⚠️ PID file exists but process not running")
|
|
1366
|
+
else:
|
|
1367
|
+
print("⚫ Aria Daemon is not running")
|
|
1368
|
+
else:
|
|
1369
|
+
try:
|
|
1370
|
+
asyncio.run(main())
|
|
1371
|
+
except KeyboardInterrupt:
|
|
1372
|
+
pass
|