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
data_service.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
data_service.py — unified market data facade
|
|
3
|
+
============================================
|
|
4
|
+
|
|
5
|
+
This module sits above MarketDataClient and datasources.DataRouter. It returns
|
|
6
|
+
one normalized shape for quotes, history, fundamentals, and technical signals
|
|
7
|
+
so report/backtest code does not need to know provider-specific schemas.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import asdict, dataclass, field, is_dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from packages.aria_services.provider_health import (
|
|
18
|
+
GLOBAL_PROVIDER_HEALTH,
|
|
19
|
+
ProviderHealthRegistry,
|
|
20
|
+
ProviderIssue,
|
|
21
|
+
classify_provider_error,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _dedupe(values: List[Any]) -> List[str]:
|
|
26
|
+
return list(dict.fromkeys(str(v) for v in values if v not in (None, "", [], {})))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_present(value: Any) -> bool:
|
|
30
|
+
return value not in (None, "", [], {})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _to_dict(value: Any) -> Dict[str, Any]:
|
|
34
|
+
if value is None:
|
|
35
|
+
return {}
|
|
36
|
+
if isinstance(value, dict):
|
|
37
|
+
return dict(value)
|
|
38
|
+
if is_dataclass(value):
|
|
39
|
+
return asdict(value)
|
|
40
|
+
if hasattr(value, "to_dict"):
|
|
41
|
+
try:
|
|
42
|
+
return dict(value.to_dict())
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
if hasattr(value, "__dict__"):
|
|
46
|
+
return dict(value.__dict__)
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _provider_chain(data: Dict[str, Any], fallback: str) -> List[str]:
|
|
51
|
+
chain = data.get("provider_chain")
|
|
52
|
+
if isinstance(chain, list):
|
|
53
|
+
return _dedupe(chain)
|
|
54
|
+
return _dedupe([data.get("provider"), data.get("source"), fallback])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _provider_from_call(method: str, data: Dict[str, Any] | None = None) -> str:
|
|
58
|
+
data = data or {}
|
|
59
|
+
provider = data.get("provider") or data.get("source")
|
|
60
|
+
if provider:
|
|
61
|
+
return str(provider)
|
|
62
|
+
return "market_data_client" if method in {"quote", "history", "fundamentals", "technical_indicators"} else method
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _data_with_success(data: Dict[str, Any], success: bool) -> Dict[str, Any]:
|
|
66
|
+
"""Return payload with its embedded success flag matching DataServiceResult."""
|
|
67
|
+
if not data:
|
|
68
|
+
return {}
|
|
69
|
+
out = dict(data)
|
|
70
|
+
out["success"] = bool(success)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _utc_now() -> datetime:
|
|
75
|
+
return datetime.now(timezone.utc)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _utc_timestamp() -> str:
|
|
79
|
+
return _utc_now().isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _parse_timestamp(value: Any) -> Optional[datetime]:
|
|
83
|
+
if not value:
|
|
84
|
+
return None
|
|
85
|
+
if isinstance(value, datetime):
|
|
86
|
+
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
87
|
+
text = str(value).strip()
|
|
88
|
+
if not text:
|
|
89
|
+
return None
|
|
90
|
+
if text.endswith("Z"):
|
|
91
|
+
text = text[:-1] + "+00:00"
|
|
92
|
+
try:
|
|
93
|
+
dt = datetime.fromisoformat(text)
|
|
94
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
95
|
+
except ValueError:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class DataServiceResult:
|
|
101
|
+
kind: str
|
|
102
|
+
symbol: str
|
|
103
|
+
success: bool
|
|
104
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
105
|
+
provider_chain: List[str] = field(default_factory=list)
|
|
106
|
+
warnings: List[str] = field(default_factory=list)
|
|
107
|
+
errors: List[str] = field(default_factory=list)
|
|
108
|
+
missing_fields: List[str] = field(default_factory=list)
|
|
109
|
+
source: str = ""
|
|
110
|
+
stale: bool = False
|
|
111
|
+
quality: Dict[str, Any] = field(default_factory=dict)
|
|
112
|
+
timestamp: str = field(default_factory=_utc_timestamp)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class DataBundle:
|
|
117
|
+
symbol: str
|
|
118
|
+
quote: Dict[str, Any] = field(default_factory=dict)
|
|
119
|
+
history: Dict[str, Any] = field(default_factory=dict)
|
|
120
|
+
fundamentals: Dict[str, Any] = field(default_factory=dict)
|
|
121
|
+
technical: Dict[str, Any] = field(default_factory=dict)
|
|
122
|
+
provider_chain: List[str] = field(default_factory=list)
|
|
123
|
+
warnings: List[str] = field(default_factory=list)
|
|
124
|
+
errors: List[str] = field(default_factory=list)
|
|
125
|
+
missing_fields: List[str] = field(default_factory=list)
|
|
126
|
+
quality: Dict[str, Any] = field(default_factory=dict)
|
|
127
|
+
status: str = "data_unavailable"
|
|
128
|
+
timestamp: str = field(default_factory=_utc_timestamp)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class DataService:
|
|
132
|
+
"""Unified data entrypoint with cache, fallback, provenance and validation."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
market_client: Any = None,
|
|
137
|
+
router: Any = None,
|
|
138
|
+
ttl_seconds: int = 60,
|
|
139
|
+
max_quote_age_seconds: int = 900,
|
|
140
|
+
provider_health: ProviderHealthRegistry | None = None,
|
|
141
|
+
):
|
|
142
|
+
if market_client is None:
|
|
143
|
+
from market_data_client import MarketDataClient
|
|
144
|
+
market_client = MarketDataClient()
|
|
145
|
+
self._router_disabled = router is False
|
|
146
|
+
if self._router_disabled:
|
|
147
|
+
router = None
|
|
148
|
+
if router is None:
|
|
149
|
+
if not self._router_disabled:
|
|
150
|
+
try:
|
|
151
|
+
from datasources.router import get_router
|
|
152
|
+
router = get_router()
|
|
153
|
+
except Exception:
|
|
154
|
+
router = None
|
|
155
|
+
self.market_client = market_client
|
|
156
|
+
self.router = router
|
|
157
|
+
self.ttl_seconds = ttl_seconds
|
|
158
|
+
self.max_quote_age_seconds = max_quote_age_seconds
|
|
159
|
+
self.provider_health = provider_health or GLOBAL_PROVIDER_HEALTH
|
|
160
|
+
self._cache: Dict[Tuple[Any, ...], Tuple[float, DataServiceResult]] = {}
|
|
161
|
+
|
|
162
|
+
def quote(self, symbol: str) -> DataServiceResult:
|
|
163
|
+
return self._cached(("quote", symbol), lambda: self._quote_uncached(symbol))
|
|
164
|
+
|
|
165
|
+
def history(self, symbol: str, days: int = 370, interval: str = "1d") -> DataServiceResult:
|
|
166
|
+
return self._cached(("history", symbol, days, interval), lambda: self._history_uncached(symbol, days, interval))
|
|
167
|
+
|
|
168
|
+
def fundamentals(self, symbol: str) -> DataServiceResult:
|
|
169
|
+
return self._cached(("fundamentals", symbol), lambda: self._fundamentals_uncached(symbol))
|
|
170
|
+
|
|
171
|
+
def technical_indicators(self, symbol: str, days: int = 120) -> DataServiceResult:
|
|
172
|
+
return self._cached(("technical", symbol, days), lambda: self._technical_uncached(symbol, days))
|
|
173
|
+
|
|
174
|
+
def bundle(self, symbol: str, history_days: int = 370, technical_days: int = 120) -> DataBundle:
|
|
175
|
+
quote = self.quote(symbol)
|
|
176
|
+
history = self.history(symbol, days=history_days)
|
|
177
|
+
fundamentals = self.fundamentals(symbol)
|
|
178
|
+
technical = self.technical_indicators(symbol, days=technical_days)
|
|
179
|
+
|
|
180
|
+
provider_chain = _dedupe(
|
|
181
|
+
quote.provider_chain + history.provider_chain + fundamentals.provider_chain + technical.provider_chain
|
|
182
|
+
)
|
|
183
|
+
warnings = quote.warnings + history.warnings + fundamentals.warnings + technical.warnings
|
|
184
|
+
errors = quote.errors + history.errors + fundamentals.errors + technical.errors
|
|
185
|
+
missing_fields = self._bundle_missing_fields(quote, history, fundamentals, technical)
|
|
186
|
+
stale = quote.stale or history.stale or technical.stale
|
|
187
|
+
|
|
188
|
+
core_success = quote.success and history.success and technical.success
|
|
189
|
+
any_success = quote.success or history.success or fundamentals.success or technical.success
|
|
190
|
+
status = "complete" if core_success and not missing_fields else "partial" if any_success else "data_unavailable"
|
|
191
|
+
if stale and status == "complete":
|
|
192
|
+
status = "stale"
|
|
193
|
+
return DataBundle(
|
|
194
|
+
symbol=symbol,
|
|
195
|
+
quote=quote.data,
|
|
196
|
+
history=history.data,
|
|
197
|
+
fundamentals=fundamentals.data,
|
|
198
|
+
technical=technical.data,
|
|
199
|
+
provider_chain=provider_chain,
|
|
200
|
+
warnings=warnings[:10],
|
|
201
|
+
errors=errors[:10],
|
|
202
|
+
missing_fields=missing_fields,
|
|
203
|
+
quality={
|
|
204
|
+
"status": status,
|
|
205
|
+
"stale": stale,
|
|
206
|
+
"providers": provider_chain,
|
|
207
|
+
"provider_health": self.provider_health.snapshot(),
|
|
208
|
+
"missing_fields": missing_fields,
|
|
209
|
+
"warnings": warnings[:10],
|
|
210
|
+
"errors": errors[:10],
|
|
211
|
+
},
|
|
212
|
+
status=status,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _cached(self, key: Tuple[Any, ...], factory: Any) -> DataServiceResult:
|
|
216
|
+
now = time.time()
|
|
217
|
+
cached = self._cache.get(key)
|
|
218
|
+
if cached and now - cached[0] <= self.ttl_seconds:
|
|
219
|
+
return cached[1]
|
|
220
|
+
result = factory()
|
|
221
|
+
self._cache[key] = (now, result)
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def _quote_uncached(self, symbol: str) -> DataServiceResult:
|
|
225
|
+
warnings: List[str] = []
|
|
226
|
+
data = self._call_market("quote", symbol, warnings)
|
|
227
|
+
if not self._valid_quote(data):
|
|
228
|
+
primary = data
|
|
229
|
+
fallback = self._call_router("quote", symbol, warnings)
|
|
230
|
+
data = fallback or primary
|
|
231
|
+
return self._result("quote", symbol, data, warnings, required=["price"], validator=self._valid_quote)
|
|
232
|
+
|
|
233
|
+
def _history_uncached(self, symbol: str, days: int, interval: str) -> DataServiceResult:
|
|
234
|
+
warnings: List[str] = []
|
|
235
|
+
data = self._call_market("history", symbol, warnings, days=days, interval=interval)
|
|
236
|
+
if not self._valid_history(data):
|
|
237
|
+
data = self._call_router("history", symbol, warnings, days=days, interval=interval)
|
|
238
|
+
return self._result("history", symbol, data, warnings, required=["data"], validator=self._valid_history)
|
|
239
|
+
|
|
240
|
+
def _fundamentals_uncached(self, symbol: str) -> DataServiceResult:
|
|
241
|
+
warnings: List[str] = []
|
|
242
|
+
data = self._call_market("fundamentals", symbol, warnings)
|
|
243
|
+
if not self._valid_payload(data):
|
|
244
|
+
data = self._call_router("fundamentals", symbol, warnings)
|
|
245
|
+
return self._result("fundamentals", symbol, data, warnings, required=[])
|
|
246
|
+
|
|
247
|
+
def _technical_uncached(self, symbol: str, days: int) -> DataServiceResult:
|
|
248
|
+
warnings: List[str] = []
|
|
249
|
+
data = self._call_market("technical_indicators", symbol, warnings, days=days)
|
|
250
|
+
|
|
251
|
+
# Yahoo Finance v8 fallback — for US symbols when primary TA source is rate-limited
|
|
252
|
+
# or returns insufficient data (newly-listed stocks, ETFs without MDC coverage)
|
|
253
|
+
_sym = symbol.upper().strip()
|
|
254
|
+
_is_us = not (_sym.endswith((".SZ", ".SS", ".SH", ".HK")) or _sym.isdigit())
|
|
255
|
+
_needs_fallback = _is_us and (
|
|
256
|
+
not data or
|
|
257
|
+
data.get("success") is False or
|
|
258
|
+
(data.get("success") and data.get("rsi") is None)
|
|
259
|
+
)
|
|
260
|
+
if _needs_fallback:
|
|
261
|
+
try:
|
|
262
|
+
import json as _jv8, urllib.request as _uv8, statistics as _sv8
|
|
263
|
+
_url = (
|
|
264
|
+
f"https://query1.finance.yahoo.com/v8/finance/chart/{_sym}"
|
|
265
|
+
"?interval=1d&range=6mo"
|
|
266
|
+
)
|
|
267
|
+
_req = _uv8.Request(_url, headers={
|
|
268
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
269
|
+
"Accept": "application/json",
|
|
270
|
+
})
|
|
271
|
+
with _uv8.urlopen(_req, timeout=10) as _r:
|
|
272
|
+
_raw = _jv8.loads(_r.read())
|
|
273
|
+
_res = _raw["chart"]["result"][0]
|
|
274
|
+
_q = _res["indicators"]["quote"][0]
|
|
275
|
+
_c = [x for x in _q.get("close", []) if x is not None]
|
|
276
|
+
_v = [x for x in _q.get("volume", []) if x is not None]
|
|
277
|
+
if len(_c) >= 14:
|
|
278
|
+
def _ema(p, n):
|
|
279
|
+
k, r = 2/(n+1), [p[0]]
|
|
280
|
+
for x in p[1:]: r.append(x*k + r[-1]*(1-k))
|
|
281
|
+
return r
|
|
282
|
+
_d = [_c[i]-_c[i-1] for i in range(1, len(_c))]
|
|
283
|
+
_g = [max(x,0) for x in _d]; _l = [max(-x,0) for x in _d]
|
|
284
|
+
_ag = sum(_g[:14])/14; _al = sum(_l[:14])/14
|
|
285
|
+
for i in range(14, len(_g)):
|
|
286
|
+
_ag = (_ag*13+_g[i])/14; _al = (_al*13+_l[i])/14
|
|
287
|
+
_rsi = (100 - 100/(1+_ag/_al)) if _al else 100.0
|
|
288
|
+
n = len(_c)
|
|
289
|
+
_ma20 = sum(_c[-20:])/min(20, n)
|
|
290
|
+
_ma60 = sum(_c[-60:])/min(60, n) if n >= 14 else _ma20
|
|
291
|
+
_std = _sv8.stdev(_c[-min(20,n):]) if n >= 2 else 0
|
|
292
|
+
_v8_data: Dict[str, Any] = {
|
|
293
|
+
"success": True,
|
|
294
|
+
"price": round(_c[-1], 2),
|
|
295
|
+
"rsi": round(_rsi, 2),
|
|
296
|
+
"ma20": round(_ma20, 2),
|
|
297
|
+
"ma60": round(_ma60, 2),
|
|
298
|
+
"bb_upper": round(_ma20 + 2*_std, 2),
|
|
299
|
+
"bb_mid": round(_ma20, 2),
|
|
300
|
+
"bb_lower": round(_ma20 - 2*_std, 2),
|
|
301
|
+
"provider": "yahoo_v8",
|
|
302
|
+
"history_bars": n,
|
|
303
|
+
}
|
|
304
|
+
if n >= 26:
|
|
305
|
+
_e12 = _ema(_c, 12); _e26 = _ema(_c, 26)
|
|
306
|
+
_md = [a-b for a,b in zip(_e12, _e26)]
|
|
307
|
+
_sg = _ema(_md, 9)
|
|
308
|
+
_v8_data["macd"] = round(_md[-1], 4)
|
|
309
|
+
_v8_data["macd_signal"] = round(_sg[-1], 4)
|
|
310
|
+
_v8_data["macd_hist"] = round(_md[-1]-_sg[-1], 4)
|
|
311
|
+
if _v:
|
|
312
|
+
_v8_data["volume"] = int(_v[-1])
|
|
313
|
+
data = _v8_data
|
|
314
|
+
warnings.append(f"yahoo_v8 fallback ({n} bars)")
|
|
315
|
+
elif len(_c) > 0:
|
|
316
|
+
# Too few bars for TA — still surface current price and bar count
|
|
317
|
+
data = {
|
|
318
|
+
"success": False,
|
|
319
|
+
"price": round(_c[-1], 2),
|
|
320
|
+
"history_bars": len(_c),
|
|
321
|
+
"provider": "yahoo_v8",
|
|
322
|
+
}
|
|
323
|
+
if _v:
|
|
324
|
+
data["volume"] = int(_v[-1])
|
|
325
|
+
_bar_str = f"{len(_c)} 个交易日"
|
|
326
|
+
warnings.append(f"数据不足(仅 {_bar_str})— 新上市标的,TA 指标需至少 14 日历史")
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
return self._result("technical", symbol, data, warnings, required=["rsi", "macd", "ma20"])
|
|
331
|
+
|
|
332
|
+
def _call_market(self, method: str, symbol: str, warnings: List[str], **kwargs: Any) -> Dict[str, Any]:
|
|
333
|
+
try:
|
|
334
|
+
fn = getattr(self.market_client, method)
|
|
335
|
+
data = _to_dict(fn(symbol, **kwargs))
|
|
336
|
+
provider = _provider_from_call(method, data)
|
|
337
|
+
if data.get("success") is False:
|
|
338
|
+
issue = classify_provider_error(provider, data.get("error") or "unavailable")
|
|
339
|
+
self.provider_health.mark_issue(issue)
|
|
340
|
+
warnings.append(self._format_issue(method, issue))
|
|
341
|
+
elif data:
|
|
342
|
+
self.provider_health.mark_success(provider)
|
|
343
|
+
return data
|
|
344
|
+
except Exception as exc:
|
|
345
|
+
issue = classify_provider_error("market_data_client", exc)
|
|
346
|
+
self.provider_health.mark_issue(issue)
|
|
347
|
+
warnings.append(self._format_issue(method, issue))
|
|
348
|
+
return {}
|
|
349
|
+
|
|
350
|
+
def _call_router(self, method: str, symbol: str, warnings: List[str], **kwargs: Any) -> Dict[str, Any]:
|
|
351
|
+
if self._router_disabled:
|
|
352
|
+
return {}
|
|
353
|
+
if self.router is None:
|
|
354
|
+
issue = classify_provider_error("data_router", "router unavailable")
|
|
355
|
+
self.provider_health.mark_issue(issue)
|
|
356
|
+
warnings.append(self._format_issue(method, issue))
|
|
357
|
+
return {}
|
|
358
|
+
try:
|
|
359
|
+
fn = getattr(self.router, method)
|
|
360
|
+
data = _to_dict(fn(symbol, **kwargs))
|
|
361
|
+
if data:
|
|
362
|
+
data.setdefault("success", True)
|
|
363
|
+
data.setdefault("provider", data.get("source") or f"data_router.{method}")
|
|
364
|
+
data.setdefault("provider_chain", _dedupe([data.get("provider"), data.get("source")]))
|
|
365
|
+
self.provider_health.mark_success(str(data.get("provider")))
|
|
366
|
+
return data
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
issue = classify_provider_error("data_router", exc)
|
|
369
|
+
self.provider_health.mark_issue(issue)
|
|
370
|
+
warnings.append(self._format_issue(method, issue))
|
|
371
|
+
return {}
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _format_issue(method: str, issue: ProviderIssue) -> str:
|
|
375
|
+
return f"{issue.provider}.{method}: {issue.category} — {issue.message}"
|
|
376
|
+
|
|
377
|
+
def _result(
|
|
378
|
+
self,
|
|
379
|
+
kind: str,
|
|
380
|
+
symbol: str,
|
|
381
|
+
data: Dict[str, Any],
|
|
382
|
+
warnings: List[str],
|
|
383
|
+
required: List[str],
|
|
384
|
+
validator: Any = None,
|
|
385
|
+
) -> DataServiceResult:
|
|
386
|
+
validator = validator or self._valid_payload
|
|
387
|
+
success = validator(data)
|
|
388
|
+
missing = [key for key in required if not _is_present(data.get(key))]
|
|
389
|
+
if kind == "quote" and "price" in required and not self._valid_quote(data):
|
|
390
|
+
if "price" not in missing:
|
|
391
|
+
missing.append("price")
|
|
392
|
+
if data.get("success") is False:
|
|
393
|
+
success = False
|
|
394
|
+
payload = _data_with_success(data, success)
|
|
395
|
+
source = str(data.get("provider") or data.get("source") or "")
|
|
396
|
+
payload_ts = data.get("timestamp") or data.get("asof") or data.get("as_of")
|
|
397
|
+
timestamp = str(payload_ts or _utc_timestamp())
|
|
398
|
+
stale = self._is_stale(kind, payload_ts)
|
|
399
|
+
errors = []
|
|
400
|
+
if data.get("error"):
|
|
401
|
+
errors.append(str(data.get("error")))
|
|
402
|
+
status = "ok" if success and not missing and not stale else (
|
|
403
|
+
"stale" if success and stale else "partial" if success else "unavailable"
|
|
404
|
+
)
|
|
405
|
+
return DataServiceResult(
|
|
406
|
+
kind=kind,
|
|
407
|
+
symbol=symbol,
|
|
408
|
+
success=success,
|
|
409
|
+
data=payload if payload else {},
|
|
410
|
+
provider_chain=_provider_chain(data, kind) if data else [],
|
|
411
|
+
warnings=warnings[:5],
|
|
412
|
+
errors=errors[:5],
|
|
413
|
+
missing_fields=missing,
|
|
414
|
+
source=source,
|
|
415
|
+
stale=stale,
|
|
416
|
+
quality={
|
|
417
|
+
"status": status,
|
|
418
|
+
"stale": stale,
|
|
419
|
+
"source": source,
|
|
420
|
+
"providers": _provider_chain(data, kind) if data else [],
|
|
421
|
+
"provider_health": self.provider_health.snapshot(),
|
|
422
|
+
"missing_fields": missing,
|
|
423
|
+
"warnings": warnings[:5],
|
|
424
|
+
"errors": errors[:5],
|
|
425
|
+
},
|
|
426
|
+
timestamp=timestamp,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _is_stale(self, kind: str, timestamp: Any) -> bool:
|
|
430
|
+
if kind != "quote":
|
|
431
|
+
return False
|
|
432
|
+
dt = _parse_timestamp(timestamp)
|
|
433
|
+
if dt is None:
|
|
434
|
+
return False
|
|
435
|
+
return (_utc_now() - dt).total_seconds() > self.max_quote_age_seconds
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _valid_payload(data: Dict[str, Any]) -> bool:
|
|
439
|
+
return bool(data) and data.get("success") is not False
|
|
440
|
+
|
|
441
|
+
@staticmethod
|
|
442
|
+
def _valid_quote(data: Dict[str, Any]) -> bool:
|
|
443
|
+
if not data or data.get("success") is False:
|
|
444
|
+
return False
|
|
445
|
+
price = data.get("price")
|
|
446
|
+
try:
|
|
447
|
+
return price is not None and float(price) > 0
|
|
448
|
+
except (TypeError, ValueError):
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
@staticmethod
|
|
452
|
+
def _valid_history(data: Dict[str, Any]) -> bool:
|
|
453
|
+
if not data or data.get("success") is False:
|
|
454
|
+
return False
|
|
455
|
+
rows = data.get("data")
|
|
456
|
+
try:
|
|
457
|
+
return rows is not None and len(rows) > 0
|
|
458
|
+
except TypeError:
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
@staticmethod
|
|
462
|
+
def _bundle_missing_fields(
|
|
463
|
+
quote: DataServiceResult,
|
|
464
|
+
history: DataServiceResult,
|
|
465
|
+
fundamentals: DataServiceResult,
|
|
466
|
+
technical: DataServiceResult,
|
|
467
|
+
) -> List[str]:
|
|
468
|
+
missing: List[str] = []
|
|
469
|
+
if not quote.success or not _is_present(quote.data.get("price")):
|
|
470
|
+
missing.append("price")
|
|
471
|
+
if not history.success:
|
|
472
|
+
missing.append("history")
|
|
473
|
+
for key in ("pe_ratio", "pe_ttm", "pb_ratio", "pb", "roe"):
|
|
474
|
+
if _is_present(fundamentals.data.get(key)):
|
|
475
|
+
break
|
|
476
|
+
else:
|
|
477
|
+
missing.append("fundamentals")
|
|
478
|
+
for key in ("rsi", "macd", "ma20"):
|
|
479
|
+
if not _is_present(technical.data.get(key)):
|
|
480
|
+
missing.append(key)
|
|
481
|
+
return _dedupe(missing)
|
datasources/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/ — Aria Code 统一市场数据层
|
|
3
|
+
========================================
|
|
4
|
+
用户在 ~/.aria/datasources.yaml 配置数据源优先级,
|
|
5
|
+
路由器按序尝试,首个成功的结果直接返回。
|
|
6
|
+
|
|
7
|
+
支持市场:
|
|
8
|
+
A股 → akshare / tushare / eastmoney
|
|
9
|
+
美股 → yfinance / polygon / finnhub / alphavantage
|
|
10
|
+
加密 → ccxt (binance/okx) / yfinance
|
|
11
|
+
基本面 → tushare / yfinance
|
|
12
|
+
|
|
13
|
+
快速使用:
|
|
14
|
+
from datasources.router import DataRouter
|
|
15
|
+
router = DataRouter()
|
|
16
|
+
print(router.quote("AAPL"))
|
|
17
|
+
print(router.quote("000001")) # A股
|
|
18
|
+
print(router.quote("BTC/USDT")) # 加密
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .router import DataRouter, get_router
|
|
22
|
+
|
|
23
|
+
__all__ = ["DataRouter", "get_router"]
|
datasources/base.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
datasources/base.py — 数据源统一接口
|
|
3
|
+
=====================================
|
|
4
|
+
所有数据源实现 BaseDataSource,输出统一 schema,
|
|
5
|
+
上层代码不关心具体是 akshare / yfinance / tushare。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── 统一输出 schema ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class QuoteResult:
|
|
19
|
+
symbol: str
|
|
20
|
+
name: str = ""
|
|
21
|
+
price: float = 0.0
|
|
22
|
+
change: float = 0.0
|
|
23
|
+
change_pct: float = 0.0
|
|
24
|
+
volume: float = 0.0
|
|
25
|
+
market_cap: float = 0.0
|
|
26
|
+
pe_ttm: float = 0.0
|
|
27
|
+
pb: float = 0.0
|
|
28
|
+
high_52w: float = 0.0
|
|
29
|
+
low_52w: float = 0.0
|
|
30
|
+
currency: str = "CNY"
|
|
31
|
+
market: str = "" # "a_share" | "us" | "hk" | "crypto"
|
|
32
|
+
source: str = "" # 实际使用的数据源名称
|
|
33
|
+
timestamp: str = ""
|
|
34
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict:
|
|
37
|
+
return {
|
|
38
|
+
"symbol": self.symbol, "name": self.name,
|
|
39
|
+
"price": self.price, "change": self.change,
|
|
40
|
+
"change_pct": self.change_pct, "volume": self.volume,
|
|
41
|
+
"market_cap": self.market_cap, "pe_ttm": self.pe_ttm,
|
|
42
|
+
"pb": self.pb, "high_52w": self.high_52w, "low_52w": self.low_52w,
|
|
43
|
+
"currency": self.currency, "market": self.market,
|
|
44
|
+
"source": self.source, "timestamp": self.timestamp,
|
|
45
|
+
**self.extra,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class HistoryResult:
|
|
51
|
+
symbol: str
|
|
52
|
+
data: Any = None # pandas DataFrame
|
|
53
|
+
source: str = ""
|
|
54
|
+
interval: str = "1d"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class FundamentalsResult:
|
|
59
|
+
symbol: str
|
|
60
|
+
pe_ttm: Optional[float] = None
|
|
61
|
+
pb: Optional[float] = None
|
|
62
|
+
roe: Optional[float] = None
|
|
63
|
+
revenue_growth: Optional[float] = None
|
|
64
|
+
net_profit_growth: Optional[float] = None
|
|
65
|
+
dividend_yield: Optional[float] = None
|
|
66
|
+
total_mv: Optional[float] = None
|
|
67
|
+
source: str = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── 基类 ──────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
class BaseDataSource(ABC):
|
|
73
|
+
"""
|
|
74
|
+
所有数据源的抽象基类。
|
|
75
|
+
|
|
76
|
+
子类实现 `supports()` 判断是否支持该 symbol,
|
|
77
|
+
再实现具体的 `quote()` / `history()` / `fundamentals()`。
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
name: str = "base" # 数据源唯一标识
|
|
81
|
+
markets: List[str] = [] # 支持的市场: "a_share", "us", "hk", "crypto"
|
|
82
|
+
requires_key: bool = False # 是否需要 API key
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: Dict[str, Any] = None):
|
|
85
|
+
self.config = config or {}
|
|
86
|
+
self._available: Optional[bool] = None
|
|
87
|
+
|
|
88
|
+
def supports(self, symbol: str) -> bool:
|
|
89
|
+
"""判断该数据源是否能处理这个 symbol(子类可覆盖)"""
|
|
90
|
+
market = _detect_market(symbol)
|
|
91
|
+
return market in self.markets
|
|
92
|
+
|
|
93
|
+
def is_configured(self) -> bool:
|
|
94
|
+
"""数据源是否已配置好(有 key 等)"""
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def quote(self, symbol: str) -> Optional[QuoteResult]:
|
|
99
|
+
"""获取实时行情(同步)"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def history(
|
|
103
|
+
self,
|
|
104
|
+
symbol: str,
|
|
105
|
+
days: int = 90,
|
|
106
|
+
interval: str = "1d",
|
|
107
|
+
) -> Optional[HistoryResult]:
|
|
108
|
+
"""获取历史 OHLCV(同步,子类按需实现)"""
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
|
|
112
|
+
"""获取基本面数据(子类按需实现)"""
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── 工具函数 ──────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def _detect_market(symbol: str) -> str:
|
|
119
|
+
"""简单推断 symbol 所属市场"""
|
|
120
|
+
s = symbol.upper().replace(" ", "")
|
|
121
|
+
|
|
122
|
+
# 加密货币
|
|
123
|
+
if "/" in s or s in ("BTC", "ETH", "SOL", "DOGE", "BNB", "ADA", "XRP"):
|
|
124
|
+
return "crypto"
|
|
125
|
+
if s.endswith(("-USD", "-USDT", "-BTC")) or s.endswith("USDT"):
|
|
126
|
+
return "crypto"
|
|
127
|
+
|
|
128
|
+
# 大宗商品
|
|
129
|
+
_COMMODITIES = {"WTI", "BRENT", "GOLD", "SILVER", "COPPER",
|
|
130
|
+
"ALUMINUM", "WHEAT", "CORN", "SOYBEAN", "NATGAS", "GAS"}
|
|
131
|
+
if s in _COMMODITIES:
|
|
132
|
+
return "commodity"
|
|
133
|
+
|
|
134
|
+
# 外汇(格式: USDCNY, USD/CNY, EURUSD 等)
|
|
135
|
+
_FX_CURRENCIES = {"USD", "EUR", "GBP", "JPY", "CNY", "HKD",
|
|
136
|
+
"AUD", "CAD", "CHF", "KRW", "SGD", "INR"}
|
|
137
|
+
if len(s) == 6 and s[:3] in _FX_CURRENCIES and s[3:] in _FX_CURRENCIES:
|
|
138
|
+
return "forex"
|
|
139
|
+
if "/" in s:
|
|
140
|
+
parts = s.split("/")
|
|
141
|
+
if len(parts) == 2 and parts[0] in _FX_CURRENCIES and parts[1] in _FX_CURRENCIES:
|
|
142
|
+
return "forex"
|
|
143
|
+
|
|
144
|
+
# A股:数字代码 or 带前缀
|
|
145
|
+
if s.startswith(("SH", "SZ", "BJ")):
|
|
146
|
+
return "a_share"
|
|
147
|
+
try:
|
|
148
|
+
int(s[:6])
|
|
149
|
+
if len(s) == 6 or (len(s) > 6 and not s[6:].isalpha()):
|
|
150
|
+
return "a_share"
|
|
151
|
+
except ValueError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# 港股
|
|
155
|
+
if s.endswith(".HK") or (s.isdigit() and len(s) == 5):
|
|
156
|
+
return "hk"
|
|
157
|
+
|
|
158
|
+
# 宏观指标
|
|
159
|
+
_MACRO = {"GDP", "CPI", "CPIYOY", "PCE", "UNRATE", "NFP", "FEDFUNDS",
|
|
160
|
+
"US10Y", "US2Y", "US3M", "VIX", "M2", "MORTGAGE", "HOUSING",
|
|
161
|
+
"SP500", "NASDAQ", "WILSHIRE", "USDCNY", "USDINR", "USDEUR"}
|
|
162
|
+
if s in _MACRO:
|
|
163
|
+
return "macro"
|
|
164
|
+
|
|
165
|
+
# 默认美股
|
|
166
|
+
return "us"
|