aria-code 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
datasources/router.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/router.py — 数据源路由器
|
|
3
|
+
=====================================
|
|
4
|
+
读取 ~/.aria/datasources.yaml 配置,按优先级依次尝试每个数据源,
|
|
5
|
+
首个成功的结果直接返回;所有失败则返回 None。
|
|
6
|
+
|
|
7
|
+
配置示例 (~/.aria/datasources.yaml):
|
|
8
|
+
a_shares:
|
|
9
|
+
- akshare
|
|
10
|
+
- tushare
|
|
11
|
+
us_stocks:
|
|
12
|
+
- yfinance
|
|
13
|
+
- finnhub
|
|
14
|
+
crypto:
|
|
15
|
+
- ccxt/binance
|
|
16
|
+
- yfinance
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import threading
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional, Type
|
|
25
|
+
|
|
26
|
+
from .base import (
|
|
27
|
+
BaseDataSource, QuoteResult, HistoryResult, FundamentalsResult, _detect_market
|
|
28
|
+
)
|
|
29
|
+
from .sources import (
|
|
30
|
+
AkshareSource, YFinanceSource, TushareSource,
|
|
31
|
+
FREDSource, EDGARSource, AlphaVantageSource, WorldBankSource,
|
|
32
|
+
FinnhubSource, WebScraperSource,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# ── 数据源目录 ────────────────────────────────────────────────────────────────
|
|
38
|
+
_SOURCE_REGISTRY: Dict[str, Type[BaseDataSource]] = {
|
|
39
|
+
"akshare": AkshareSource,
|
|
40
|
+
"yfinance": YFinanceSource,
|
|
41
|
+
"finnhub": FinnhubSource,
|
|
42
|
+
"tushare": TushareSource,
|
|
43
|
+
"fred": FREDSource,
|
|
44
|
+
"edgar": EDGARSource,
|
|
45
|
+
"alpha_vantage": AlphaVantageSource,
|
|
46
|
+
"world_bank": WorldBankSource,
|
|
47
|
+
"web_scraper": WebScraperSource,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_datasource(name: str, cls: Type[BaseDataSource]) -> None:
|
|
52
|
+
"""注册自定义数据源(供插件/用户扩展)"""
|
|
53
|
+
_SOURCE_REGISTRY[name.lower()] = cls
|
|
54
|
+
logger.info(f"✓ 注册自定义数据源: {name}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── 默认优先级链 ──────────────────────────────────────────────────────────────
|
|
58
|
+
_DEFAULT_CHAINS: Dict[str, List[str]] = {
|
|
59
|
+
"a_share": ["tushare", "akshare"],
|
|
60
|
+
"us": ["yfinance", "finnhub", "alpha_vantage", "edgar"],
|
|
61
|
+
"hk": ["yfinance", "finnhub", "akshare"],
|
|
62
|
+
"crypto": ["yfinance"],
|
|
63
|
+
"macro": ["fred", "world_bank"],
|
|
64
|
+
"forex": ["alpha_vantage", "yfinance"],
|
|
65
|
+
"commodity": ["alpha_vantage"],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# ── 配置文件路径 ──────────────────────────────────────────────────────────────
|
|
69
|
+
_CONFIG_PATHS = [
|
|
70
|
+
Path.home() / ".aria" / "datasources.yaml",
|
|
71
|
+
Path.home() / ".aria" / "datasources.json",
|
|
72
|
+
Path(".aria.json"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_user_chains() -> Dict[str, List[str]]:
|
|
77
|
+
for p in _CONFIG_PATHS:
|
|
78
|
+
if not p.exists():
|
|
79
|
+
continue
|
|
80
|
+
try:
|
|
81
|
+
if p.suffix in (".yaml", ".yml"):
|
|
82
|
+
import yaml
|
|
83
|
+
with open(p, encoding="utf-8") as f:
|
|
84
|
+
data = yaml.safe_load(f) or {}
|
|
85
|
+
else:
|
|
86
|
+
import json
|
|
87
|
+
with open(p, encoding="utf-8") as f:
|
|
88
|
+
data = json.load(f)
|
|
89
|
+
ds = data.get("datasources", data)
|
|
90
|
+
if isinstance(ds, dict):
|
|
91
|
+
chains = {}
|
|
92
|
+
key_map = {
|
|
93
|
+
"a_shares": "a_share", "a_share": "a_share",
|
|
94
|
+
"us_stocks": "us", "us": "us",
|
|
95
|
+
"hk_stocks": "hk", "hk": "hk",
|
|
96
|
+
"crypto": "crypto",
|
|
97
|
+
}
|
|
98
|
+
for k, v in ds.items():
|
|
99
|
+
market = key_map.get(k.lower(), k.lower())
|
|
100
|
+
if isinstance(v, list):
|
|
101
|
+
chains[market] = v
|
|
102
|
+
return chains
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.debug(f"加载 datasources 配置 {p} 失败: {e}")
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DataRouter:
|
|
109
|
+
"""
|
|
110
|
+
统一数据路由器。线程安全(内部缓存用锁保护)。
|
|
111
|
+
|
|
112
|
+
用法:
|
|
113
|
+
router = DataRouter()
|
|
114
|
+
q = router.quote("600519")
|
|
115
|
+
h = router.history("AAPL", days=90)
|
|
116
|
+
f = router.fundamentals("000858")
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self):
|
|
120
|
+
self._user_chains = _load_user_chains()
|
|
121
|
+
self._source_cache: Dict[str, BaseDataSource] = {}
|
|
122
|
+
self._lock = threading.Lock()
|
|
123
|
+
|
|
124
|
+
def _get_chain(self, market: str) -> List[str]:
|
|
125
|
+
user = self._user_chains.get(market) or self._user_chains.get(
|
|
126
|
+
{"a_share": "a_shares"}.get(market, market)
|
|
127
|
+
)
|
|
128
|
+
if user:
|
|
129
|
+
return user
|
|
130
|
+
return _DEFAULT_CHAINS.get(market, ["yfinance"])
|
|
131
|
+
|
|
132
|
+
def _get_source(self, name: str) -> Optional[BaseDataSource]:
|
|
133
|
+
with self._lock:
|
|
134
|
+
if name not in self._source_cache:
|
|
135
|
+
cls = _SOURCE_REGISTRY.get(name.lower())
|
|
136
|
+
if not cls:
|
|
137
|
+
logger.debug(f"未知数据源: {name}")
|
|
138
|
+
return None
|
|
139
|
+
src = cls()
|
|
140
|
+
if not src.is_configured():
|
|
141
|
+
logger.debug(f"数据源 {name} 未配置(缺少 API key)")
|
|
142
|
+
return None
|
|
143
|
+
self._source_cache[name] = src
|
|
144
|
+
return self._source_cache[name]
|
|
145
|
+
|
|
146
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
147
|
+
market = _detect_market(symbol)
|
|
148
|
+
for src_name in self._get_chain(market):
|
|
149
|
+
if src_name == "edgar":
|
|
150
|
+
continue
|
|
151
|
+
src = self._get_source(src_name)
|
|
152
|
+
if not src or not src.supports(symbol):
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
result = src.quote(symbol)
|
|
156
|
+
try:
|
|
157
|
+
valid_price = result is not None and float(getattr(result, "price", 0) or 0) > 0
|
|
158
|
+
except Exception:
|
|
159
|
+
valid_price = False
|
|
160
|
+
if result and valid_price:
|
|
161
|
+
logger.debug(f"quote({symbol}) ← {src_name}")
|
|
162
|
+
return result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug(f"[{src_name}] quote {symbol} 异常: {e}")
|
|
165
|
+
logger.debug(f"所有数据源均无法获取 {symbol} 行情")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def history(
|
|
169
|
+
self, symbol: str, days: int = 90, interval: str = "1d"
|
|
170
|
+
) -> Optional[HistoryResult]:
|
|
171
|
+
market = _detect_market(symbol)
|
|
172
|
+
for src_name in self._get_chain(market):
|
|
173
|
+
src = self._get_source(src_name)
|
|
174
|
+
if not src or not src.supports(symbol):
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
result = src.history(symbol, days=days, interval=interval)
|
|
178
|
+
if result is not None:
|
|
179
|
+
logger.debug(f"history({symbol}) ← {src_name}")
|
|
180
|
+
return result
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.debug(f"[{src_name}] history {symbol} 异常: {e}")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
186
|
+
market = _detect_market(symbol)
|
|
187
|
+
for src_name in self._get_chain(market):
|
|
188
|
+
src = self._get_source(src_name)
|
|
189
|
+
if not src or not src.supports(symbol):
|
|
190
|
+
continue
|
|
191
|
+
try:
|
|
192
|
+
result = src.fundamentals(symbol)
|
|
193
|
+
if result:
|
|
194
|
+
logger.debug(f"fundamentals({symbol}) ← {src_name}")
|
|
195
|
+
return result
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.debug(f"[{src_name}] fundamentals {symbol} 异常: {e}")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def list_sources(self) -> List[Dict[str, Any]]:
|
|
201
|
+
"""列出所有数据源及其状态(用于 /config 展示)"""
|
|
202
|
+
result = []
|
|
203
|
+
for name, cls in _SOURCE_REGISTRY.items():
|
|
204
|
+
src = cls()
|
|
205
|
+
result.append({
|
|
206
|
+
"name": name,
|
|
207
|
+
"markets": cls.markets,
|
|
208
|
+
"needs_key": cls.requires_key,
|
|
209
|
+
"configured": src.is_configured(),
|
|
210
|
+
})
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ── 单例 ──────────────────────────────────────────────────────────────────────
|
|
215
|
+
_router: Optional[DataRouter] = None
|
|
216
|
+
|
|
217
|
+
def get_router() -> DataRouter:
|
|
218
|
+
global _router
|
|
219
|
+
if _router is None:
|
|
220
|
+
_router = DataRouter()
|
|
221
|
+
return _router
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .akshare_source import AkshareSource
|
|
2
|
+
from .yfinance_source import YFinanceSource
|
|
3
|
+
from .tushare_source import TushareSource
|
|
4
|
+
from .fred_source import FREDSource
|
|
5
|
+
from .edgar_source import EDGARSource
|
|
6
|
+
from .alpha_vantage_source import AlphaVantageSource
|
|
7
|
+
from .world_bank_source import WorldBankSource
|
|
8
|
+
from .web_scraper_source import WebScraperSource
|
|
9
|
+
from .finnhub_source import FinnhubSource
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AkshareSource", "YFinanceSource", "TushareSource",
|
|
13
|
+
"FREDSource", "EDGARSource", "AlphaVantageSource",
|
|
14
|
+
"WorldBankSource", "WebScraperSource", "FinnhubSource",
|
|
15
|
+
]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/sources/akshare_source.py — Akshare A股数据源
|
|
3
|
+
==========================================================
|
|
4
|
+
免费,无需 API key,覆盖 A股实时行情、历史数据、北向资金等。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import date, timedelta
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AkshareSource(BaseDataSource):
|
|
19
|
+
|
|
20
|
+
name = "akshare"
|
|
21
|
+
markets = ["a_share"]
|
|
22
|
+
requires_key = False
|
|
23
|
+
|
|
24
|
+
def _normalize_code(self, symbol: str) -> str:
|
|
25
|
+
"""'sh600519' / '600519' / '600519.SH' → '600519'"""
|
|
26
|
+
s = symbol.upper().replace("SH", "").replace("SZ", "").replace("BJ", "")
|
|
27
|
+
s = s.replace(".", "").strip()
|
|
28
|
+
if s.startswith("6") or s.startswith("0") or s.startswith("3") or s.startswith("8"):
|
|
29
|
+
return s[:6]
|
|
30
|
+
return s
|
|
31
|
+
|
|
32
|
+
def _yf_symbol(self, code: str) -> str:
|
|
33
|
+
"""600519 → 600519.SS,000858 → 000858.SZ,300xxx → 300xxx.SZ"""
|
|
34
|
+
if code.startswith("6") or code.startswith("9"):
|
|
35
|
+
return f"{code}.SS"
|
|
36
|
+
return f"{code}.SZ"
|
|
37
|
+
|
|
38
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
39
|
+
"""
|
|
40
|
+
三级降级:
|
|
41
|
+
1. akshare stock_zh_a_spot_em (全市场快照)
|
|
42
|
+
2. akshare stock_individual_info_em (单股)
|
|
43
|
+
3. yfinance .SS/.SZ (国际用户兜底)
|
|
44
|
+
4. 最近交易日收盘价 from history
|
|
45
|
+
"""
|
|
46
|
+
import warnings
|
|
47
|
+
warnings.filterwarnings("ignore")
|
|
48
|
+
code = self._normalize_code(symbol)
|
|
49
|
+
|
|
50
|
+
# ── 方法1: 全市场快照 ────────────────────────────────────────────────
|
|
51
|
+
try:
|
|
52
|
+
import akshare as ak
|
|
53
|
+
df = ak.stock_zh_a_spot_em()
|
|
54
|
+
row = df[df["代码"] == code]
|
|
55
|
+
if not row.empty:
|
|
56
|
+
r = row.iloc[0]
|
|
57
|
+
price = float(r.get("最新价", 0) or 0)
|
|
58
|
+
if price > 0:
|
|
59
|
+
return QuoteResult(
|
|
60
|
+
symbol = symbol,
|
|
61
|
+
name = str(r.get("名称", "")),
|
|
62
|
+
price = price,
|
|
63
|
+
change = float(r.get("涨跌额", 0) or 0),
|
|
64
|
+
change_pct = float(r.get("涨跌幅", 0) or 0),
|
|
65
|
+
volume = float(r.get("成交量", 0) or 0),
|
|
66
|
+
market_cap = float(r.get("总市值", 0) or 0),
|
|
67
|
+
pe_ttm = float(r.get("市盈率-动态", 0) or 0),
|
|
68
|
+
pb = float(r.get("市净率", 0) or 0),
|
|
69
|
+
high_52w = float(r.get("52周最高", 0) or 0),
|
|
70
|
+
low_52w = float(r.get("52周最低", 0) or 0),
|
|
71
|
+
currency = "CNY",
|
|
72
|
+
market = "a_share",
|
|
73
|
+
source = self.name,
|
|
74
|
+
)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# ── 方法2: 单股信息(EastMoney 单接口) ────────────────────────────
|
|
79
|
+
try:
|
|
80
|
+
import akshare as ak
|
|
81
|
+
df2 = ak.stock_individual_info_em(symbol=code)
|
|
82
|
+
if df2 is not None and not df2.empty:
|
|
83
|
+
info = dict(zip(df2["item"], df2["value"]))
|
|
84
|
+
price = float(str(info.get("最新", info.get("收盘", 0))).replace(",", "") or 0)
|
|
85
|
+
prev = float(str(info.get("昨收", price)).replace(",", "") or price)
|
|
86
|
+
if price > 0:
|
|
87
|
+
return QuoteResult(
|
|
88
|
+
symbol = symbol,
|
|
89
|
+
name = str(info.get("股票简称", "")),
|
|
90
|
+
price = price,
|
|
91
|
+
change = round(price - prev, 4),
|
|
92
|
+
change_pct = round((price - prev) / prev * 100, 2) if prev else 0,
|
|
93
|
+
currency = "CNY",
|
|
94
|
+
market = "a_share",
|
|
95
|
+
source = self.name,
|
|
96
|
+
)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# ── 方法3: yfinance .SS/.SZ(海外访问兜底) ─────────────────────────
|
|
101
|
+
try:
|
|
102
|
+
import yfinance as yf
|
|
103
|
+
yf_sym = self._yf_symbol(code)
|
|
104
|
+
ticker = yf.Ticker(yf_sym)
|
|
105
|
+
|
|
106
|
+
price = None
|
|
107
|
+
prev = None
|
|
108
|
+
info = {}
|
|
109
|
+
try:
|
|
110
|
+
fi = ticker.fast_info
|
|
111
|
+
price = getattr(fi, "last_price", None) or getattr(fi, "previous_close", None)
|
|
112
|
+
prev = getattr(fi, "previous_close", None) or price
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
if not price:
|
|
116
|
+
try:
|
|
117
|
+
info = ticker.info or {}
|
|
118
|
+
price = info.get("currentPrice") or info.get("regularMarketPrice") or info.get("previousClose")
|
|
119
|
+
prev = info.get("previousClose") or price
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
if price and float(price) > 0:
|
|
123
|
+
if not info:
|
|
124
|
+
try:
|
|
125
|
+
info = ticker.info or {}
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
chg = float(price) - float(prev) if prev else 0
|
|
129
|
+
chg_p = chg / float(prev) * 100 if prev else 0
|
|
130
|
+
return QuoteResult(
|
|
131
|
+
symbol = symbol,
|
|
132
|
+
name = info.get("shortName") or info.get("longName") or symbol,
|
|
133
|
+
price = float(price),
|
|
134
|
+
change = chg,
|
|
135
|
+
change_pct = chg_p,
|
|
136
|
+
volume = float(info.get("volume") or 0),
|
|
137
|
+
market_cap = float(info.get("marketCap") or 0),
|
|
138
|
+
pe_ttm = float(info.get("trailingPE") or 0),
|
|
139
|
+
pb = float(info.get("priceToBook") or 0),
|
|
140
|
+
currency = "CNY",
|
|
141
|
+
market = "a_share",
|
|
142
|
+
source = f"{self.name}+yfinance",
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# ── 方法4: 最近历史收盘价 ────────────────────────────────────────────
|
|
148
|
+
try:
|
|
149
|
+
h = self.history(symbol, days=5)
|
|
150
|
+
if h and h.data is not None and not h.data.empty:
|
|
151
|
+
df = h.data
|
|
152
|
+
# Flexible column detection (akshare returns Chinese column names)
|
|
153
|
+
close_col = next(
|
|
154
|
+
(c for c in df.columns if "收盘" in str(c) or "close" in str(c).lower()),
|
|
155
|
+
df.columns[-1]
|
|
156
|
+
)
|
|
157
|
+
price = float(df[close_col].iloc[-1])
|
|
158
|
+
prev = float(df[close_col].iloc[-2]) if len(df) > 1 else price
|
|
159
|
+
if price > 0:
|
|
160
|
+
return QuoteResult(
|
|
161
|
+
symbol = symbol,
|
|
162
|
+
price = price,
|
|
163
|
+
change = round(price - prev, 4),
|
|
164
|
+
change_pct = round((price - prev) / prev * 100, 2) if prev else 0,
|
|
165
|
+
currency = "CNY",
|
|
166
|
+
market = "a_share",
|
|
167
|
+
source = f"{self.name}(last_close)",
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.debug(f"[akshare] quote history fallback {symbol} 失败: {e}")
|
|
171
|
+
|
|
172
|
+
logger.debug(f"[akshare] quote {symbol}: 所有方法均失败")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def history(self, symbol: str, days: int = 90, interval: str = "1d") -> Optional[HistoryResult]:
|
|
176
|
+
# ── 方法1: akshare stock_zh_a_hist ──────────────────────────────────
|
|
177
|
+
try:
|
|
178
|
+
import akshare as ak
|
|
179
|
+
code = self._normalize_code(symbol)
|
|
180
|
+
end = date.today().strftime("%Y%m%d")
|
|
181
|
+
start = (date.today() - timedelta(days=days + 10)).strftime("%Y%m%d")
|
|
182
|
+
df = ak.stock_zh_a_hist(
|
|
183
|
+
symbol=code, period="daily",
|
|
184
|
+
start_date=start, end_date=end, adjust="qfq"
|
|
185
|
+
)
|
|
186
|
+
if df is not None and not df.empty:
|
|
187
|
+
return HistoryResult(symbol=symbol, data=df, source=self.name, interval=interval)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.debug(f"[akshare] history(akshare) {symbol} 失败: {e}")
|
|
190
|
+
|
|
191
|
+
# ── 方法2: yfinance .SS/.SZ (海外兜底) ──────────────────────────────
|
|
192
|
+
try:
|
|
193
|
+
import yfinance as yf
|
|
194
|
+
import pandas as pd
|
|
195
|
+
code = self._normalize_code(symbol)
|
|
196
|
+
yf_sym = self._yf_symbol(code)
|
|
197
|
+
ticker = yf.Ticker(yf_sym)
|
|
198
|
+
period = f"{days}d" if days <= 730 else "2y"
|
|
199
|
+
df = ticker.history(period=period, auto_adjust=True)
|
|
200
|
+
if df is not None and not df.empty:
|
|
201
|
+
df.index = pd.to_datetime(df.index)
|
|
202
|
+
return HistoryResult(symbol=symbol, data=df, source=f"{self.name}+yfinance", interval=interval)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.debug(f"[akshare] history(yfinance) {symbol} 失败: {e}")
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
209
|
+
"""A股基本面:优先 akshare,降级 yfinance .SS/.SZ"""
|
|
210
|
+
# ── 方法1: akshare 市场快照字段 ──────────────────────────────────────
|
|
211
|
+
try:
|
|
212
|
+
import akshare as ak
|
|
213
|
+
import math
|
|
214
|
+
code = self._normalize_code(symbol)
|
|
215
|
+
df = ak.stock_zh_a_spot_em()
|
|
216
|
+
row = df[df["代码"] == code]
|
|
217
|
+
if not row.empty:
|
|
218
|
+
r = row.iloc[0]
|
|
219
|
+
def _ak(key: str) -> Optional[float]:
|
|
220
|
+
v = r.get(key)
|
|
221
|
+
if v is None:
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
fv = float(v)
|
|
225
|
+
return None if (math.isnan(fv) or fv == 0) else fv
|
|
226
|
+
except (TypeError, ValueError):
|
|
227
|
+
return None
|
|
228
|
+
pe = _ak("市盈率-动态")
|
|
229
|
+
pb = _ak("市净率")
|
|
230
|
+
mv = _ak("总市值")
|
|
231
|
+
if pe or pb:
|
|
232
|
+
return FundamentalsResult(
|
|
233
|
+
symbol=symbol, pe_ttm=pe, pb=pb,
|
|
234
|
+
total_mv=mv, source=self.name,
|
|
235
|
+
)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
# ── 方法2: yfinance .SS/.SZ ───────────────────────────────────────────
|
|
240
|
+
try:
|
|
241
|
+
from .yfinance_source import YFinanceSource
|
|
242
|
+
code = self._normalize_code(symbol)
|
|
243
|
+
yf_sym = self._yf_symbol(code)
|
|
244
|
+
fund = YFinanceSource().fundamentals(yf_sym)
|
|
245
|
+
if fund:
|
|
246
|
+
fund.symbol = symbol
|
|
247
|
+
fund.source = f"{self.name}+yfinance"
|
|
248
|
+
return fund
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.debug(f"[akshare] fundamentals yfinance fallback {symbol} 失败: {e}")
|
|
251
|
+
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def northbound_flow(self) -> Optional[Dict]:
|
|
255
|
+
"""北向资金净流入(akshare 特有)"""
|
|
256
|
+
try:
|
|
257
|
+
import akshare as ak
|
|
258
|
+
df = ak.stock_hsgt_north_net_flow_in_em(symbol="北向资金")
|
|
259
|
+
if df is None or df.empty:
|
|
260
|
+
return None
|
|
261
|
+
latest = df.iloc[-1]
|
|
262
|
+
return {
|
|
263
|
+
"date": str(latest.get("日期", "")),
|
|
264
|
+
"net_flow": float(latest.get("当日净流入", 0) or 0),
|
|
265
|
+
"source": self.name,
|
|
266
|
+
}
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.debug(f"[akshare] northbound_flow 失败: {e}")
|
|
269
|
+
return None
|