aria-code 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/sources/alpha_vantage_source.py — Alpha Vantage 免费层数据
|
|
3
|
+
======================================================================
|
|
4
|
+
免费 API key 从 https://www.alphavantage.co/support/#api-key 申请(秒得)。
|
|
5
|
+
免费限制: 25 请求/天,5 请求/分钟。
|
|
6
|
+
|
|
7
|
+
功能: 美股/ETF 行情、技术指标、外汇汇率、大宗商品、基本面数据。
|
|
8
|
+
|
|
9
|
+
配置: ALPHA_VANTAGE_KEY 环境变量 或 ~/.aria/.env
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
import urllib.request
|
|
18
|
+
from datetime import date, timedelta
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, Optional
|
|
21
|
+
|
|
22
|
+
from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_BASE = "https://www.alphavantage.co/query"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_key() -> str:
|
|
30
|
+
key = os.getenv("ALPHA_VANTAGE_KEY", "") or os.getenv("ALPHAVANTAGE_KEY", "")
|
|
31
|
+
if not key:
|
|
32
|
+
for p in [Path.home() / ".aria" / ".env", Path.home() / ".arthera" / ".env"]:
|
|
33
|
+
if p.exists():
|
|
34
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
35
|
+
if line.startswith(("ALPHA_VANTAGE_KEY=", "ALPHAVANTAGE_KEY=")):
|
|
36
|
+
key = line.split("=", 1)[1].strip()
|
|
37
|
+
break
|
|
38
|
+
if key:
|
|
39
|
+
break
|
|
40
|
+
if not key:
|
|
41
|
+
# providers.json 中存储的键名是 "alphavantage"(无下划线)
|
|
42
|
+
try:
|
|
43
|
+
import json as _json
|
|
44
|
+
_p = Path.home() / ".arthera" / "providers.json"
|
|
45
|
+
if _p.exists():
|
|
46
|
+
_raw = _json.loads(_p.read_text(encoding="utf-8"))
|
|
47
|
+
key = (
|
|
48
|
+
_raw.get("data", {}).get("alphavantage", {}).get("api_key", "")
|
|
49
|
+
or _raw.get("data", {}).get("alpha_vantage", {}).get("api_key", "")
|
|
50
|
+
)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
return key
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _fetch(params: dict, timeout: int = 15) -> Optional[dict]:
|
|
57
|
+
import json
|
|
58
|
+
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
|
59
|
+
url = f"{_BASE}?{qs}"
|
|
60
|
+
try:
|
|
61
|
+
req = urllib.request.Request(url, headers={"User-Agent": "aria-code/1.0"})
|
|
62
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
63
|
+
return json.loads(resp.read())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.debug(f"[alpha_vantage] fetch 失败: {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AlphaVantageSource(BaseDataSource):
|
|
70
|
+
"""
|
|
71
|
+
Alpha Vantage 数据源。
|
|
72
|
+
免费 key 足够日常使用(25次/天);付费 key 无频率限制。
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name = "alpha_vantage"
|
|
76
|
+
markets = ["us", "hk", "forex", "commodity"]
|
|
77
|
+
requires_key = True
|
|
78
|
+
|
|
79
|
+
def __init__(self, config=None):
|
|
80
|
+
super().__init__(config)
|
|
81
|
+
self._key = _load_key()
|
|
82
|
+
|
|
83
|
+
def is_configured(self) -> bool:
|
|
84
|
+
return bool(self._key)
|
|
85
|
+
|
|
86
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
87
|
+
data = _fetch({"function": "GLOBAL_QUOTE", "symbol": symbol, "apikey": self._key})
|
|
88
|
+
if not data:
|
|
89
|
+
return None
|
|
90
|
+
q = data.get("Global Quote", {})
|
|
91
|
+
if not q or "05. price" not in q:
|
|
92
|
+
return None
|
|
93
|
+
price = float(q.get("05. price", 0))
|
|
94
|
+
change = float(q.get("09. change", 0))
|
|
95
|
+
pct = float(q.get("10. change percent", "0%").replace("%", ""))
|
|
96
|
+
vol = float(q.get("06. volume", 0))
|
|
97
|
+
return QuoteResult(
|
|
98
|
+
symbol = symbol,
|
|
99
|
+
price = price,
|
|
100
|
+
change = change,
|
|
101
|
+
change_pct = pct,
|
|
102
|
+
volume = vol,
|
|
103
|
+
market = "us",
|
|
104
|
+
source = self.name,
|
|
105
|
+
timestamp = q.get("07. latest trading day", ""),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def history(
|
|
109
|
+
self,
|
|
110
|
+
symbol: str,
|
|
111
|
+
days: int = 365,
|
|
112
|
+
interval: str = "1d",
|
|
113
|
+
) -> Optional[HistoryResult]:
|
|
114
|
+
try:
|
|
115
|
+
import pandas as pd
|
|
116
|
+
size = "compact" if days <= 100 else "full"
|
|
117
|
+
func = "TIME_SERIES_DAILY_ADJUSTED"
|
|
118
|
+
data = _fetch({"function": func, "symbol": symbol,
|
|
119
|
+
"outputsize": size, "apikey": self._key})
|
|
120
|
+
if not data:
|
|
121
|
+
return None
|
|
122
|
+
ts = data.get("Time Series (Daily)", {})
|
|
123
|
+
if not ts:
|
|
124
|
+
return None
|
|
125
|
+
cutoff = (date.today() - timedelta(days=days)).isoformat()
|
|
126
|
+
rows = []
|
|
127
|
+
for d, v in ts.items():
|
|
128
|
+
if d < cutoff:
|
|
129
|
+
continue
|
|
130
|
+
rows.append({
|
|
131
|
+
"date": d,
|
|
132
|
+
"open": float(v.get("1. open", 0)),
|
|
133
|
+
"high": float(v.get("2. high", 0)),
|
|
134
|
+
"low": float(v.get("3. low", 0)),
|
|
135
|
+
"close": float(v.get("5. adjusted close", v.get("4. close", 0))),
|
|
136
|
+
"volume": float(v.get("6. volume", 0)),
|
|
137
|
+
})
|
|
138
|
+
if not rows:
|
|
139
|
+
return None
|
|
140
|
+
df = pd.DataFrame(rows)
|
|
141
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
142
|
+
df = df.set_index("date").sort_index()
|
|
143
|
+
return HistoryResult(symbol=symbol, data=df, source=self.name, interval="1d")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug(f"[alpha_vantage] history {symbol} 失败: {e}")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
149
|
+
data = _fetch({"function": "OVERVIEW", "symbol": symbol, "apikey": self._key})
|
|
150
|
+
if not data or not data.get("Symbol"):
|
|
151
|
+
return None
|
|
152
|
+
def _f(k, mult: float = 1.0):
|
|
153
|
+
v = data.get(k, "None")
|
|
154
|
+
try:
|
|
155
|
+
fv = float(v) if v not in ("None", "-", "", "0") else None
|
|
156
|
+
return fv * mult if fv is not None else None
|
|
157
|
+
except ValueError:
|
|
158
|
+
return None
|
|
159
|
+
return FundamentalsResult(
|
|
160
|
+
symbol = symbol,
|
|
161
|
+
pe_ttm = _f("TrailingPE"),
|
|
162
|
+
pb = _f("PriceToBookRatio"),
|
|
163
|
+
roe = _f("ReturnOnEquityTTM", 100),
|
|
164
|
+
revenue_growth = _f("RevenueGrowthQtrlyYOY"),
|
|
165
|
+
dividend_yield = _f("DividendYield", 100),
|
|
166
|
+
source = self.name,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def get_forex(self, from_currency: str, to_currency: str) -> Optional[Dict]:
|
|
170
|
+
"""实时外汇汇率。"""
|
|
171
|
+
data = _fetch({"function": "CURRENCY_EXCHANGE_RATE",
|
|
172
|
+
"from_currency": from_currency,
|
|
173
|
+
"to_currency": to_currency,
|
|
174
|
+
"apikey": self._key})
|
|
175
|
+
if not data:
|
|
176
|
+
return None
|
|
177
|
+
r = data.get("Realtime Currency Exchange Rate", {})
|
|
178
|
+
return {
|
|
179
|
+
"from": from_currency, "to": to_currency,
|
|
180
|
+
"rate": float(r.get("5. Exchange Rate", 0)),
|
|
181
|
+
"time": r.get("6. Last Refreshed", ""),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def get_commodity(self, symbol: str = "WTI") -> Optional[HistoryResult]:
|
|
185
|
+
"""大宗商品历史价格(WTI/BRENT/GOLD/COPPER 等)。"""
|
|
186
|
+
_MAP = {"WTI": "WTI", "BRENT": "BRENT", "GOLD": "GOLD",
|
|
187
|
+
"COPPER": "COPPER", "ALUMINUM": "ALUMINUM", "WHEAT": "WHEAT"}
|
|
188
|
+
func = _MAP.get(symbol.upper(), symbol.upper())
|
|
189
|
+
data = _fetch({"function": func, "interval": "monthly", "apikey": self._key})
|
|
190
|
+
if not data:
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
import pandas as pd
|
|
194
|
+
rows = [{"date": r["date"], "close": float(r["value"])}
|
|
195
|
+
for r in data.get("data", []) if r.get("value") not in (".", None)]
|
|
196
|
+
df = pd.DataFrame(rows)
|
|
197
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
198
|
+
df = df.set_index("date").sort_index()
|
|
199
|
+
return HistoryResult(symbol=symbol, data=df, source=self.name, interval="1mo")
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.debug(f"[alpha_vantage] commodity {symbol} 失败: {e}")
|
|
202
|
+
return None
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/sources/edgar_source.py — SEC EDGAR 美国上市公司财务数据
|
|
3
|
+
====================================================================
|
|
4
|
+
完全免费,无需 API key。数据来源:https://data.sec.gov (SEC EDGAR API)
|
|
5
|
+
覆盖:10-K / 10-Q 财报、公司基本信息、财务报表、内幕交易披露。
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
src = EDGARSource()
|
|
9
|
+
facts = src.get_company_facts("AAPL") # → 财务指标历史
|
|
10
|
+
filings = src.get_recent_filings("MSFT") # → 最近10-K/10-Q
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
import urllib.request
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from ..base import BaseDataSource, FundamentalsResult, QuoteResult
|
|
21
|
+
|
|
22
|
+
import os as _os
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_EDGAR_API = "https://data.sec.gov"
|
|
27
|
+
_EDGAR_WWW = "https://www.sec.gov"
|
|
28
|
+
_HEADERS = {
|
|
29
|
+
"User-Agent": _os.environ.get("EDGAR_USER_AGENT", "aria-code contact@example.com"), # SEC requires self-identification
|
|
30
|
+
"Accept-Encoding": "gzip, deflate",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# 主要美股 ticker → CIK 缓存(常用的直接查,避免每次API调用)
|
|
34
|
+
_TICKER_CIK_CACHE: Dict[str, str] = {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fetch_json(url: str, timeout: int = 15) -> Optional[Dict]:
|
|
38
|
+
try:
|
|
39
|
+
import gzip, json
|
|
40
|
+
req = urllib.request.Request(url, headers=_HEADERS)
|
|
41
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
42
|
+
raw = resp.read()
|
|
43
|
+
if resp.headers.get("Content-Encoding") == "gzip":
|
|
44
|
+
raw = gzip.decompress(raw)
|
|
45
|
+
return json.loads(raw)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug(f"[edgar] fetch {url} 失败: {e}")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EDGARSource(BaseDataSource):
|
|
52
|
+
"""
|
|
53
|
+
SEC EDGAR 数据源 — 美国上市公司财报和披露文件。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name = "edgar"
|
|
57
|
+
markets = ["us"]
|
|
58
|
+
requires_key = False
|
|
59
|
+
|
|
60
|
+
def __init__(self, config=None):
|
|
61
|
+
super().__init__(config)
|
|
62
|
+
self._tickers: Optional[Dict] = None
|
|
63
|
+
self._cik_map: Dict[str, str] = {}
|
|
64
|
+
|
|
65
|
+
def is_configured(self) -> bool:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def _load_ticker_map(self) -> None:
|
|
69
|
+
"""加载 SEC 全量 ticker→CIK 映射(首次调用时下载一次)"""
|
|
70
|
+
if self._tickers is not None:
|
|
71
|
+
return
|
|
72
|
+
data = _fetch_json(f"{_EDGAR_WWW}/files/company_tickers.json")
|
|
73
|
+
if data:
|
|
74
|
+
for v in data.values():
|
|
75
|
+
ticker = v.get("ticker", "").upper()
|
|
76
|
+
cik = str(v.get("cik_str", "")).zfill(10)
|
|
77
|
+
if ticker:
|
|
78
|
+
self._cik_map[ticker] = cik
|
|
79
|
+
self._tickers = self._cik_map
|
|
80
|
+
|
|
81
|
+
def ticker_to_cik(self, symbol: str) -> Optional[str]:
|
|
82
|
+
self._load_ticker_map()
|
|
83
|
+
return self._cik_map.get(symbol.upper())
|
|
84
|
+
|
|
85
|
+
def get_company_facts(self, symbol: str) -> Optional[Dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
获取公司全部财务事实(XBRL 格式)。
|
|
88
|
+
返回包含 EPS/Revenue/NetIncome/Assets 等历史序列的字典。
|
|
89
|
+
"""
|
|
90
|
+
cik = self.ticker_to_cik(symbol)
|
|
91
|
+
if not cik:
|
|
92
|
+
return {"error": f"未找到 {symbol} 的 CIK"}
|
|
93
|
+
data = _fetch_json(f"{_EDGAR_API}/api/xbrl/companyfacts/CIK{cik}.json")
|
|
94
|
+
if not data:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
us_gaap = data.get("facts", {}).get("us-gaap", {})
|
|
98
|
+
result = {"symbol": symbol, "cik": cik, "metrics": {}}
|
|
99
|
+
wanted = {
|
|
100
|
+
"Revenues": "revenue",
|
|
101
|
+
"RevenueFromContractWithCustomerExcludingAssessedTax": "revenue",
|
|
102
|
+
"NetIncomeLoss": "net_income",
|
|
103
|
+
"EarningsPerShareBasic": "eps_basic",
|
|
104
|
+
"EarningsPerShareDiluted": "eps_diluted",
|
|
105
|
+
"Assets": "total_assets",
|
|
106
|
+
"LiabilitiesAndStockholdersEquity": "total_equity",
|
|
107
|
+
"OperatingIncomeLoss": "operating_income",
|
|
108
|
+
"CommonStockSharesOutstanding": "shares_outstanding",
|
|
109
|
+
}
|
|
110
|
+
for gaap_key, alias in wanted.items():
|
|
111
|
+
if gaap_key in us_gaap:
|
|
112
|
+
units = us_gaap[gaap_key].get("units", {})
|
|
113
|
+
unit_key = "USD" if "USD" in units else ("shares" if "shares" in units else next(iter(units), None))
|
|
114
|
+
if unit_key and unit_key in units:
|
|
115
|
+
entries = [
|
|
116
|
+
{"end": e["end"], "val": e["val"], "form": e.get("form", "")}
|
|
117
|
+
for e in units[unit_key]
|
|
118
|
+
if e.get("form") in ("10-K", "10-Q", "20-F")
|
|
119
|
+
]
|
|
120
|
+
if entries:
|
|
121
|
+
entries.sort(key=lambda x: x["end"], reverse=True)
|
|
122
|
+
result["metrics"][alias] = entries[:20]
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def get_recent_filings(self, symbol: str, form_types: List[str] = None) -> List[Dict]:
|
|
126
|
+
"""获取最近提交的财务报告(10-K、10-Q、8-K 等)。"""
|
|
127
|
+
cik = self.ticker_to_cik(symbol)
|
|
128
|
+
if not cik:
|
|
129
|
+
return [{"error": f"未找到 {symbol} 的 CIK"}]
|
|
130
|
+
form_types = form_types or ["10-K", "10-Q", "8-K"]
|
|
131
|
+
data = _fetch_json(f"{_EDGAR_API}/submissions/CIK{cik}.json")
|
|
132
|
+
if not data:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
recent = data.get("filings", {}).get("recent", {})
|
|
136
|
+
forms = recent.get("form", [])
|
|
137
|
+
dates = recent.get("filingDate", [])
|
|
138
|
+
accnos = recent.get("accessionNumber", [])
|
|
139
|
+
docs = recent.get("primaryDocument", [])
|
|
140
|
+
|
|
141
|
+
results = []
|
|
142
|
+
for form, filing_date, accno, doc in zip(forms, dates, accnos, docs):
|
|
143
|
+
if form in form_types:
|
|
144
|
+
accno_clean = accno.replace("-", "")
|
|
145
|
+
url = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{accno_clean}/{doc}"
|
|
146
|
+
results.append({
|
|
147
|
+
"form": form,
|
|
148
|
+
"date": filing_date,
|
|
149
|
+
"accession": accno,
|
|
150
|
+
"url": url,
|
|
151
|
+
"cik": cik,
|
|
152
|
+
})
|
|
153
|
+
if len(results) >= 20:
|
|
154
|
+
break
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
def get_insider_trades(self, symbol: str, days: int = 90) -> List[Dict]:
|
|
158
|
+
"""获取内幕交易披露(Form 4)。"""
|
|
159
|
+
cik = self.ticker_to_cik(symbol)
|
|
160
|
+
if not cik:
|
|
161
|
+
return []
|
|
162
|
+
data = _fetch_json(f"{_EDGAR_API}/submissions/CIK{cik}.json")
|
|
163
|
+
if not data:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
recent = data.get("filings", {}).get("recent", {})
|
|
167
|
+
forms = recent.get("form", [])
|
|
168
|
+
dates = recent.get("filingDate", [])
|
|
169
|
+
results = []
|
|
170
|
+
from datetime import date as _date, timedelta as _td
|
|
171
|
+
cutoff = (_date.today() - _td(days=days)).isoformat()
|
|
172
|
+
|
|
173
|
+
for form, filing_date in zip(forms, dates):
|
|
174
|
+
if form == "4" and filing_date >= cutoff:
|
|
175
|
+
results.append({"form": "4", "date": filing_date, "type": "insider"})
|
|
176
|
+
if len(results) >= 30:
|
|
177
|
+
break
|
|
178
|
+
return results
|
|
179
|
+
|
|
180
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
181
|
+
facts = self.get_company_facts(symbol)
|
|
182
|
+
if not facts or "metrics" in facts and not facts["metrics"]:
|
|
183
|
+
return None
|
|
184
|
+
m = facts.get("metrics", {})
|
|
185
|
+
rev = m.get("revenue", [{}])[0].get("val", 0) if m.get("revenue") else 0
|
|
186
|
+
return QuoteResult(
|
|
187
|
+
symbol = symbol,
|
|
188
|
+
name = f"EDGAR:{symbol}",
|
|
189
|
+
price = 0.0,
|
|
190
|
+
market = "us",
|
|
191
|
+
source = self.name,
|
|
192
|
+
extra = {"annual_revenue": rev},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
196
|
+
facts = self.get_company_facts(symbol)
|
|
197
|
+
if not facts:
|
|
198
|
+
return None
|
|
199
|
+
m = facts.get("metrics", {})
|
|
200
|
+
|
|
201
|
+
def _latest(key: str) -> float:
|
|
202
|
+
entries = m.get(key, [])
|
|
203
|
+
return float(entries[0]["val"]) if entries else 0.0
|
|
204
|
+
|
|
205
|
+
net_income = _latest("net_income")
|
|
206
|
+
revenue = _latest("revenue")
|
|
207
|
+
rev_yoy = 0.0
|
|
208
|
+
if m.get("revenue") and len(m["revenue"]) >= 2:
|
|
209
|
+
cur = float(m["revenue"][0]["val"])
|
|
210
|
+
prev = float(m["revenue"][1]["val"])
|
|
211
|
+
if prev:
|
|
212
|
+
rev_yoy = (cur - prev) / abs(prev) * 100
|
|
213
|
+
|
|
214
|
+
return FundamentalsResult(
|
|
215
|
+
symbol = symbol,
|
|
216
|
+
revenue_growth = rev_yoy,
|
|
217
|
+
source = self.name,
|
|
218
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/sources/finnhub_source.py — Finnhub 美股/港股数据源
|
|
3
|
+
================================================================
|
|
4
|
+
使用 ~/.arthera/providers.json 中配置的 Finnhub API key,
|
|
5
|
+
提供实时行情、历史 K 线、基本面数据。
|
|
6
|
+
免费套餐:每分钟 60 次请求,支持美股 / ETF / 指数 / 外汇 / 加密。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
import time
|
|
15
|
+
import urllib.request
|
|
16
|
+
from datetime import datetime, timedelta, date, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_PROVIDERS_FILE = Path.home() / ".arthera" / "providers.json"
|
|
27
|
+
_BASE = "https://finnhub.io/api/v1"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_finnhub_key() -> str:
|
|
31
|
+
try:
|
|
32
|
+
import os
|
|
33
|
+
env = os.getenv("FINNHUB_API_KEY", "") or os.getenv("FINNHUB_KEY", "")
|
|
34
|
+
if env:
|
|
35
|
+
return env
|
|
36
|
+
if _PROVIDERS_FILE.exists():
|
|
37
|
+
raw = json.loads(_PROVIDERS_FILE.read_text(encoding="utf-8"))
|
|
38
|
+
key = raw.get("data", {}).get("finnhub", {}).get("api_key", "")
|
|
39
|
+
if key:
|
|
40
|
+
return key
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fh_get(path: str, key: str, params: dict = None, timeout: int = 8) -> dict:
|
|
47
|
+
qs = "&".join(f"{k}={v}" for k, v in (params or {}).items())
|
|
48
|
+
url = f"{_BASE}{path}?token={key}" + (f"&{qs}" if qs else "")
|
|
49
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
50
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
51
|
+
return json.loads(r.read())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FinnhubSource(BaseDataSource):
|
|
55
|
+
|
|
56
|
+
name = "finnhub"
|
|
57
|
+
markets = ["us", "hk", "crypto"]
|
|
58
|
+
requires_key = True
|
|
59
|
+
|
|
60
|
+
def __init__(self, config=None):
|
|
61
|
+
super().__init__(config)
|
|
62
|
+
self._key = _read_finnhub_key()
|
|
63
|
+
|
|
64
|
+
def is_configured(self) -> bool:
|
|
65
|
+
return bool(self._key)
|
|
66
|
+
|
|
67
|
+
# ── Quote ─────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
70
|
+
if not self._key:
|
|
71
|
+
return None
|
|
72
|
+
sym = symbol.upper().replace(".HK", "")
|
|
73
|
+
try:
|
|
74
|
+
data = _fh_get("/quote", self._key, {"symbol": sym})
|
|
75
|
+
price = float(data.get("c") or 0)
|
|
76
|
+
if price <= 0:
|
|
77
|
+
return None
|
|
78
|
+
prev = float(data.get("pc") or price)
|
|
79
|
+
chg_pct = round((price - prev) / prev * 100, 2) if prev else 0
|
|
80
|
+
|
|
81
|
+
# Extra: company profile for name / market cap
|
|
82
|
+
name = sym
|
|
83
|
+
mkt_cap = 0.0
|
|
84
|
+
currency = "USD"
|
|
85
|
+
try:
|
|
86
|
+
prof = _fh_get("/stock/profile2", self._key, {"symbol": sym})
|
|
87
|
+
name = prof.get("name") or sym
|
|
88
|
+
mkt_cap = float(prof.get("marketCapitalization") or 0) * 1e6
|
|
89
|
+
currency = prof.get("currency") or "USD"
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return QuoteResult(
|
|
94
|
+
symbol = symbol,
|
|
95
|
+
name = name,
|
|
96
|
+
price = price,
|
|
97
|
+
change = round(price - prev, 4),
|
|
98
|
+
change_pct = chg_pct,
|
|
99
|
+
volume = float(data.get("v") or 0),
|
|
100
|
+
high_52w = float(data.get("h") or 0),
|
|
101
|
+
low_52w = float(data.get("l") or 0),
|
|
102
|
+
market_cap = mkt_cap,
|
|
103
|
+
currency = currency,
|
|
104
|
+
market = "us",
|
|
105
|
+
source = self.name,
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug(f"[finnhub] quote({symbol}) 失败: {e}")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# ── History ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def history(self, symbol: str, days: int = 90, interval: str = "1d") -> Optional[HistoryResult]:
|
|
114
|
+
if not self._key:
|
|
115
|
+
return None
|
|
116
|
+
sym = symbol.upper()
|
|
117
|
+
resolution = "D" if interval in ("1d", "day", "daily") else "60"
|
|
118
|
+
_end = int(time.time())
|
|
119
|
+
_start = int((datetime.now() - timedelta(days=days + 5)).timestamp())
|
|
120
|
+
try:
|
|
121
|
+
data = _fh_get("/stock/candle", self._key, {
|
|
122
|
+
"symbol": sym, "resolution": resolution,
|
|
123
|
+
"from": _start, "to": _end,
|
|
124
|
+
})
|
|
125
|
+
if data.get("s") != "ok":
|
|
126
|
+
return None
|
|
127
|
+
t = data.get("t", [])
|
|
128
|
+
o = data.get("o", [])
|
|
129
|
+
h = data.get("h", [])
|
|
130
|
+
l = data.get("l", [])
|
|
131
|
+
c = data.get("c", [])
|
|
132
|
+
v = data.get("v", [])
|
|
133
|
+
if not c:
|
|
134
|
+
return None
|
|
135
|
+
df = pd.DataFrame({
|
|
136
|
+
"日期": [datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d") for ts in t],
|
|
137
|
+
"开盘": o, "最高": h, "最低": l, "收盘": c, "成交量": v,
|
|
138
|
+
})
|
|
139
|
+
df.index = pd.to_datetime(df["日期"])
|
|
140
|
+
# Also add English column aliases for DataService compatibility
|
|
141
|
+
df["Open"] = df["开盘"]
|
|
142
|
+
df["High"] = df["最高"]
|
|
143
|
+
df["Low"] = df["最低"]
|
|
144
|
+
df["Close"] = df["收盘"]
|
|
145
|
+
df["Volume"] = df["成交量"]
|
|
146
|
+
return HistoryResult(symbol=symbol, data=df, source=self.name, interval=interval)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.debug(f"[finnhub] history({symbol}) 失败: {e}")
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# ── Fundamentals ──────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
154
|
+
if not self._key:
|
|
155
|
+
return None
|
|
156
|
+
sym = symbol.upper()
|
|
157
|
+
try:
|
|
158
|
+
# Basic financials (PE, market cap, etc.)
|
|
159
|
+
metrics_data = _fh_get("/stock/metric", self._key, {"symbol": sym, "metric": "all"})
|
|
160
|
+
m = metrics_data.get("metric") or {}
|
|
161
|
+
|
|
162
|
+
def _mf(key: str) -> Optional[float]:
|
|
163
|
+
v = m.get(key)
|
|
164
|
+
if v is None:
|
|
165
|
+
return None
|
|
166
|
+
try:
|
|
167
|
+
fv = float(v)
|
|
168
|
+
return None if (math.isnan(fv) or fv == 0) else fv
|
|
169
|
+
except (TypeError, ValueError):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
pe = _mf("peTTM") or _mf("peExclExtraItemsTTM")
|
|
173
|
+
pb = _mf("pbAnnual") or _mf("pbQuarterly")
|
|
174
|
+
roe = _mf("roeTTM") or _mf("roeAnnual")
|
|
175
|
+
div_yield = _mf("dividendYieldIndicatedAnnual")
|
|
176
|
+
rev_growth = _mf("revenueGrowthTTMYoy")
|
|
177
|
+
eps_growth = _mf("epsGrowthTTMYoy")
|
|
178
|
+
mktcap_raw = _mf("marketCapitalization")
|
|
179
|
+
total_mv = (mktcap_raw * 1e6) if mktcap_raw else None
|
|
180
|
+
|
|
181
|
+
if pe is None and pb is None and roe is None:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return FundamentalsResult(
|
|
185
|
+
symbol = symbol,
|
|
186
|
+
pe_ttm = pe,
|
|
187
|
+
pb = pb,
|
|
188
|
+
roe = roe,
|
|
189
|
+
revenue_growth = rev_growth,
|
|
190
|
+
net_profit_growth = eps_growth,
|
|
191
|
+
dividend_yield = div_yield,
|
|
192
|
+
total_mv = total_mv,
|
|
193
|
+
source = self.name,
|
|
194
|
+
)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.debug(f"[finnhub] fundamentals({symbol}) 失败: {e}")
|
|
197
|
+
return None
|