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
realty_data_tools.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""
|
|
2
|
+
realty_data_tools.py — 不动产市场数据层
|
|
3
|
+
==========================================
|
|
4
|
+
数据来源:
|
|
5
|
+
- AKShare — 中国房价指数/房地产投资/REITs
|
|
6
|
+
- FRED — 美国住房数据 (Case-Shiller/新屋开工/NAHB)
|
|
7
|
+
- 本地计算 — 租金收益率/物业估值/资产评级
|
|
8
|
+
|
|
9
|
+
全部函数返回 {"success": bool, ...} 统一格式。
|
|
10
|
+
|
|
11
|
+
安装依赖(可选):
|
|
12
|
+
pip install akshare # 中国数据
|
|
13
|
+
pip install openpyxl # Excel 导出
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import akshare as ak
|
|
28
|
+
_HAS_AK = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_HAS_AK = False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import pandas as pd
|
|
34
|
+
_HAS_PD = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
_HAS_PD = False
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import numpy as np
|
|
40
|
+
_HAS_NP = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
_HAS_NP = False
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import requests as _req
|
|
46
|
+
_HAS_REQ = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
_HAS_REQ = False
|
|
49
|
+
|
|
50
|
+
# ── 70城房价指数 ─────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
# akshare 支持的城市(70城新房价格指数覆盖范围)
|
|
53
|
+
CN_CITIES_TIER1 = ["北京", "上海", "广州", "深圳"]
|
|
54
|
+
CN_CITIES_TIER2 = [
|
|
55
|
+
"成都", "杭州", "武汉", "重庆", "南京", "西安", "天津", "苏州",
|
|
56
|
+
"长沙", "郑州", "青岛", "沈阳", "宁波", "合肥", "厦门", "济南",
|
|
57
|
+
"东莞", "佛山", "福州", "南宁", "昆明", "贵阳", "大连", "哈尔滨",
|
|
58
|
+
"长春", "石家庄", "太原", "南昌", "兰州", "银川", "西宁", "乌鲁木齐",
|
|
59
|
+
"海口", "三亚", "珠海", "温州", "泉州", "烟台", "洛阳", "唐山",
|
|
60
|
+
"南通", "常州", "徐州", "扬州", "嘉兴", "金华", "绍兴",
|
|
61
|
+
"湖州", "台州", "芜湖", "湘潭", "株洲", "常德", "桂林", "柳州",
|
|
62
|
+
"汕头", "惠州", "江门", "湛江", "中山", "保定", "廊坊",
|
|
63
|
+
]
|
|
64
|
+
CN_CITIES_ALL = CN_CITIES_TIER1 + CN_CITIES_TIER2
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_house_price_index(city: str = "北京", city2: str = "上海") -> dict:
|
|
68
|
+
"""
|
|
69
|
+
获取中国 70 城新房 / 二手房价格指数。
|
|
70
|
+
|
|
71
|
+
返回同比、环比数据(最近 12 个月)。
|
|
72
|
+
city: 主要城市(默认北京)
|
|
73
|
+
city2: 对比城市(默认上海)
|
|
74
|
+
"""
|
|
75
|
+
if not _HAS_AK:
|
|
76
|
+
return {"success": False, "error": "akshare 未安装"}
|
|
77
|
+
try:
|
|
78
|
+
df = ak.macro_china_new_house_price(city_first=city, city_second=city2)
|
|
79
|
+
if df is None or df.empty:
|
|
80
|
+
return {"success": False, "error": "无数据"}
|
|
81
|
+
|
|
82
|
+
# Keep last 24 months
|
|
83
|
+
df = df.tail(24).copy()
|
|
84
|
+
df.columns = [c.strip() for c in df.columns]
|
|
85
|
+
records = df.to_dict("records")
|
|
86
|
+
|
|
87
|
+
# Split by city
|
|
88
|
+
city1_records = [r for r in records if str(r.get("城市","")) == city][-12:]
|
|
89
|
+
city2_records = [r for r in records if str(r.get("城市","")) == city2][-12:]
|
|
90
|
+
|
|
91
|
+
def _latest(recs):
|
|
92
|
+
if not recs: return {}
|
|
93
|
+
r = recs[-1]
|
|
94
|
+
return {
|
|
95
|
+
"date": str(r.get("日期",""))[:7],
|
|
96
|
+
"new_yoy": r.get("新建商品住宅价格指数-同比"),
|
|
97
|
+
"new_mom": r.get("新建商品住宅价格指数-环比"),
|
|
98
|
+
"second_yoy": r.get("二手住宅价格指数-同比"),
|
|
99
|
+
"second_mom": r.get("二手住宅价格指数-环比"),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"city1": city,
|
|
105
|
+
"city2": city2,
|
|
106
|
+
"latest_city1": _latest(city1_records),
|
|
107
|
+
"latest_city2": _latest(city2_records),
|
|
108
|
+
"series_city1": city1_records,
|
|
109
|
+
"series_city2": city2_records,
|
|
110
|
+
"provider": "akshare_NBS",
|
|
111
|
+
}
|
|
112
|
+
except Exception as e:
|
|
113
|
+
return {"success": False, "error": str(e)}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_re_investment() -> dict:
|
|
117
|
+
"""
|
|
118
|
+
中国房地产开发投资额(月度累计同比)。
|
|
119
|
+
反映房地产行业景气度。
|
|
120
|
+
"""
|
|
121
|
+
if not _HAS_AK:
|
|
122
|
+
return {"success": False, "error": "akshare 未安装"}
|
|
123
|
+
try:
|
|
124
|
+
df = ak.macro_china_real_estate()
|
|
125
|
+
if df is None or df.empty:
|
|
126
|
+
return {"success": False, "error": "无数据"}
|
|
127
|
+
df = df.tail(12)
|
|
128
|
+
records = df.to_dict("records")
|
|
129
|
+
latest = records[-1] if records else {}
|
|
130
|
+
return {
|
|
131
|
+
"success": True,
|
|
132
|
+
"latest": latest,
|
|
133
|
+
"series": records,
|
|
134
|
+
"provider": "akshare_NBS",
|
|
135
|
+
}
|
|
136
|
+
except Exception as e:
|
|
137
|
+
return {"success": False, "error": str(e)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_multi_city_comparison(cities: Optional[List[str]] = None) -> dict:
|
|
141
|
+
"""
|
|
142
|
+
多城市房价指数对比(同比涨跌幅热力图数据)。
|
|
143
|
+
默认对比 8 个核心城市(两两调用拼合)。
|
|
144
|
+
"""
|
|
145
|
+
if not _HAS_AK:
|
|
146
|
+
return {"success": False, "error": "akshare 未安装"}
|
|
147
|
+
|
|
148
|
+
if cities is None:
|
|
149
|
+
cities = ["北京", "上海", "深圳", "广州", "成都", "杭州", "武汉", "南京",
|
|
150
|
+
"重庆", "西安", "长沙", "天津", "苏州", "郑州", "厦门", "青岛"]
|
|
151
|
+
|
|
152
|
+
results: List[Dict] = []
|
|
153
|
+
# akshare requires city_first + city_second; batch by pairs
|
|
154
|
+
city_pairs = list(zip(cities[::2], cities[1::2]))
|
|
155
|
+
if len(cities) % 2 == 1:
|
|
156
|
+
city_pairs.append((cities[-1], cities[0])) # pair last with first
|
|
157
|
+
|
|
158
|
+
seen: set = set()
|
|
159
|
+
for c1, c2 in city_pairs:
|
|
160
|
+
try:
|
|
161
|
+
df = ak.macro_china_new_house_price(city_first=c1, city_second=c2)
|
|
162
|
+
if df is None or df.empty:
|
|
163
|
+
continue
|
|
164
|
+
df = df.tail(4)
|
|
165
|
+
for city in (c1, c2):
|
|
166
|
+
if city in seen:
|
|
167
|
+
continue
|
|
168
|
+
sub = df[df["城市"] == city]
|
|
169
|
+
if sub.empty:
|
|
170
|
+
continue
|
|
171
|
+
row = sub.iloc[-1]
|
|
172
|
+
results.append({
|
|
173
|
+
"city": city,
|
|
174
|
+
"tier": "一线" if city in CN_CITIES_TIER1 else "二线",
|
|
175
|
+
"date": str(row.get("日期",""))[:7],
|
|
176
|
+
"new_yoy": _safe_float(row.get("新建商品住宅价格指数-同比")),
|
|
177
|
+
"new_mom": _safe_float(row.get("新建商品住宅价格指数-环比")),
|
|
178
|
+
"second_yoy": _safe_float(row.get("二手住宅价格指数-同比")),
|
|
179
|
+
"second_mom": _safe_float(row.get("二手住宅价格指数-环比")),
|
|
180
|
+
})
|
|
181
|
+
seen.add(city)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.debug("City pair (%s,%s) failed: %s", c1, c2, e)
|
|
184
|
+
|
|
185
|
+
if not results:
|
|
186
|
+
return {"success": False, "error": "无法获取城市数据"}
|
|
187
|
+
|
|
188
|
+
# Sort by new_yoy descending
|
|
189
|
+
results.sort(key=lambda x: -(x.get("new_yoy") or 0))
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"success": True,
|
|
193
|
+
"cities": results,
|
|
194
|
+
"top_riser": results[0]["city"] if results else "",
|
|
195
|
+
"top_faller":results[-1]["city"] if results else "",
|
|
196
|
+
"provider": "akshare_NBS",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ── REITs 分析 ───────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
def get_reits_list() -> dict:
|
|
203
|
+
"""获取中国 REITs 实时行情列表(东方财富)。"""
|
|
204
|
+
if not _HAS_AK:
|
|
205
|
+
return {"success": False, "error": "akshare 未安装"}
|
|
206
|
+
try:
|
|
207
|
+
df = ak.reits_realtime_em()
|
|
208
|
+
if df is None or df.empty:
|
|
209
|
+
return {"success": False, "error": "无 REIT 数据"}
|
|
210
|
+
cols = [c for c in ["代码","名称","最新价","涨跌额","涨跌幅","昨收","成交量","成交额"] if c in df.columns]
|
|
211
|
+
records = df[cols].to_dict("records")
|
|
212
|
+
return {
|
|
213
|
+
"success": True,
|
|
214
|
+
"count": len(records),
|
|
215
|
+
"reits": records,
|
|
216
|
+
"provider": "akshare_em",
|
|
217
|
+
}
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return {"success": False, "error": str(e)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_reit_analysis(code: str) -> dict:
|
|
223
|
+
"""
|
|
224
|
+
单只 REIT 深度分析:历史行情 + 估值指标。
|
|
225
|
+
|
|
226
|
+
code: REIT 代码,如 "508603" (唯品商业)
|
|
227
|
+
"""
|
|
228
|
+
if not _HAS_AK:
|
|
229
|
+
return {"success": False, "error": "akshare 未安装"}
|
|
230
|
+
try:
|
|
231
|
+
import yfinance as yf
|
|
232
|
+
_HAS_YF = True
|
|
233
|
+
except ImportError:
|
|
234
|
+
_HAS_YF = False
|
|
235
|
+
|
|
236
|
+
result: Dict[str, Any] = {"success": True, "code": code}
|
|
237
|
+
|
|
238
|
+
# Historical prices
|
|
239
|
+
try:
|
|
240
|
+
df_hist = ak.reits_hist_em(symbol=code, period="daily",
|
|
241
|
+
start_date="20230101",
|
|
242
|
+
end_date=datetime.now().strftime("%Y%m%d"),
|
|
243
|
+
adjust="qfq")
|
|
244
|
+
if df_hist is not None and not df_hist.empty:
|
|
245
|
+
df_hist = df_hist.tail(252) # 1 year
|
|
246
|
+
last = df_hist.iloc[-1]
|
|
247
|
+
cols = df_hist.columns.tolist()
|
|
248
|
+
close_col = next((c for c in cols if "收盘" in c or "close" in c.lower()), cols[-1])
|
|
249
|
+
prices = df_hist[close_col].astype(float)
|
|
250
|
+
cur_price = float(last[close_col])
|
|
251
|
+
result["price"] = round(cur_price, 3)
|
|
252
|
+
|
|
253
|
+
# Performance
|
|
254
|
+
if len(prices) >= 2:
|
|
255
|
+
result["return_1y"] = round((prices.iloc[-1]/prices.iloc[0] - 1)*100, 2)
|
|
256
|
+
if len(prices) >= 20:
|
|
257
|
+
result["return_1m"] = round((prices.iloc[-1]/prices.iloc[-20] - 1)*100, 2)
|
|
258
|
+
|
|
259
|
+
# Volatility
|
|
260
|
+
if _HAS_NP and len(prices) >= 20:
|
|
261
|
+
rets = prices.pct_change().dropna()
|
|
262
|
+
result["volatility_annual"] = round(float(rets.std() * np.sqrt(252) * 100), 2)
|
|
263
|
+
|
|
264
|
+
result["history_tail"] = df_hist.tail(5).to_dict("records")
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.debug("REIT hist failed %s: %s", code, e)
|
|
267
|
+
|
|
268
|
+
# Realtime info from list
|
|
269
|
+
try:
|
|
270
|
+
df_rt = ak.reits_realtime_em()
|
|
271
|
+
if df_rt is not None and not df_rt.empty:
|
|
272
|
+
row = df_rt[df_rt["代码"] == code]
|
|
273
|
+
if not row.empty:
|
|
274
|
+
r = row.iloc[0]
|
|
275
|
+
result["name"] = str(r.get("名称",""))
|
|
276
|
+
result["price"] = result.get("price") or _safe_float(r.get("最新价"))
|
|
277
|
+
result["chg_pct"] = _safe_float(r.get("涨跌幅"))
|
|
278
|
+
result["prev_close"] = _safe_float(r.get("昨收"))
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.debug("REIT realtime failed: %s", e)
|
|
281
|
+
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── 租金收益率计算器 ──────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
def calc_rental_yield(params: dict) -> dict:
|
|
288
|
+
"""
|
|
289
|
+
物业租金收益率计算(毛/净收益率)。
|
|
290
|
+
|
|
291
|
+
参数:
|
|
292
|
+
purchase_price: 购入价格(万元)
|
|
293
|
+
monthly_rent: 月租金(元)
|
|
294
|
+
annual_costs: 年维护成本(元,默认0)
|
|
295
|
+
tax_rate: 租金税率(小数,默认0.05)
|
|
296
|
+
loan_ratio: 贷款成数(0-1,默认0=全款)
|
|
297
|
+
loan_rate: 贷款年利率(小数,默认0.04)
|
|
298
|
+
loan_years: 贷款年数(默认30)
|
|
299
|
+
"""
|
|
300
|
+
price_wan = float(params.get("purchase_price", 0))
|
|
301
|
+
monthly_rent= float(params.get("monthly_rent", 0))
|
|
302
|
+
annual_costs= float(params.get("annual_costs", 0))
|
|
303
|
+
tax_rate = float(params.get("tax_rate", 0.05))
|
|
304
|
+
loan_ratio = float(params.get("loan_ratio", 0))
|
|
305
|
+
loan_rate = float(params.get("loan_rate", 0.04))
|
|
306
|
+
loan_years = int(params.get("loan_years", 30))
|
|
307
|
+
|
|
308
|
+
if price_wan <= 0 or monthly_rent <= 0:
|
|
309
|
+
return {"success": False, "error": "purchase_price 和 monthly_rent 均为必填项"}
|
|
310
|
+
|
|
311
|
+
price_yuan = price_wan * 10000
|
|
312
|
+
annual_rent = monthly_rent * 12
|
|
313
|
+
tax_deduction = annual_rent * tax_rate
|
|
314
|
+
net_rent = annual_rent - tax_deduction - annual_costs
|
|
315
|
+
|
|
316
|
+
# Gross yield
|
|
317
|
+
gross_yield = annual_rent / price_yuan * 100
|
|
318
|
+
|
|
319
|
+
# Net yield (without leverage)
|
|
320
|
+
net_yield = net_rent / price_yuan * 100
|
|
321
|
+
|
|
322
|
+
# Leveraged yield (with mortgage)
|
|
323
|
+
equity = price_yuan * (1 - loan_ratio)
|
|
324
|
+
monthly_payment = 0.0
|
|
325
|
+
annual_interest = 0.0
|
|
326
|
+
if loan_ratio > 0:
|
|
327
|
+
loan_amount = price_yuan * loan_ratio
|
|
328
|
+
monthly_rate = loan_rate / 12
|
|
329
|
+
n = loan_years * 12
|
|
330
|
+
if monthly_rate > 0:
|
|
331
|
+
monthly_payment = loan_amount * monthly_rate * (1 + monthly_rate)**n / ((1 + monthly_rate)**n - 1)
|
|
332
|
+
annual_interest = monthly_payment * 12 - loan_amount / loan_years # approximate
|
|
333
|
+
|
|
334
|
+
leveraged_net_rent = net_rent - (monthly_payment * 12 if loan_ratio > 0 else 0)
|
|
335
|
+
leveraged_yield = (leveraged_net_rent / equity * 100) if equity > 0 else 0
|
|
336
|
+
|
|
337
|
+
# Payback period
|
|
338
|
+
payback_years = price_yuan / net_rent if net_rent > 0 else 999
|
|
339
|
+
|
|
340
|
+
# Cap rate (NOI / price)
|
|
341
|
+
noi = annual_rent - annual_costs
|
|
342
|
+
cap_rate = noi / price_yuan * 100
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
"success": True,
|
|
346
|
+
"purchase_price_wan": price_wan,
|
|
347
|
+
"monthly_rent": monthly_rent,
|
|
348
|
+
"annual_rent": annual_rent,
|
|
349
|
+
"gross_yield_pct": round(gross_yield, 2),
|
|
350
|
+
"net_yield_pct": round(net_yield, 2),
|
|
351
|
+
"cap_rate_pct": round(cap_rate, 2),
|
|
352
|
+
"leveraged_yield_pct": round(leveraged_yield, 2) if loan_ratio > 0 else None,
|
|
353
|
+
"monthly_payment": round(monthly_payment, 2) if loan_ratio > 0 else None,
|
|
354
|
+
"payback_years": round(payback_years, 1),
|
|
355
|
+
"equity_invested": round(equity / 10000, 2),
|
|
356
|
+
"assessment": (
|
|
357
|
+
"优质标的" if gross_yield >= 5
|
|
358
|
+
else "合理收益" if gross_yield >= 3
|
|
359
|
+
else "收益偏低"
|
|
360
|
+
),
|
|
361
|
+
"benchmark": "一线城市租金收益率通常 1.5-3%,二线 2-4%,商业地产 4-7%",
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ── 物业估值模型 ──────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
def property_valuation(params: dict) -> dict:
|
|
368
|
+
"""
|
|
369
|
+
物业估值三合一模型:收益法 + DCF + 市场比较法。
|
|
370
|
+
|
|
371
|
+
必填:
|
|
372
|
+
area_sqm: 建筑面积(平米)
|
|
373
|
+
monthly_rent: 当前月租金(元)
|
|
374
|
+
location_tier: 区位层级("tier1"/"tier2"/"tier3")
|
|
375
|
+
|
|
376
|
+
可选:
|
|
377
|
+
vacancy_rate: 空置率(默认0.05)
|
|
378
|
+
cap_rate: 市场资本化率(默认由区位自动推算)
|
|
379
|
+
annual_growth: 租金年增长率(默认0.02)
|
|
380
|
+
discount_rate: 折现率(默认0.06)
|
|
381
|
+
hold_years: 持有年数(默认10)
|
|
382
|
+
price_per_sqm: 参考市场单价(元/m²,不填则用租金反推)
|
|
383
|
+
"""
|
|
384
|
+
area = float(params.get("area_sqm", 100))
|
|
385
|
+
monthly_rent = float(params.get("monthly_rent", 0))
|
|
386
|
+
tier = str(params.get("location_tier", "tier2")).lower()
|
|
387
|
+
vacancy = float(params.get("vacancy_rate", 0.05))
|
|
388
|
+
annual_growth= float(params.get("annual_growth", 0.02))
|
|
389
|
+
hold_years = int(params.get("hold_years", 10))
|
|
390
|
+
|
|
391
|
+
# Tier-based defaults
|
|
392
|
+
_TIER_DEFAULTS = {
|
|
393
|
+
"tier1": {"cap_rate": 0.025, "discount_rate": 0.055, "price_range": (80000, 200000)},
|
|
394
|
+
"tier2": {"cap_rate": 0.04, "discount_rate": 0.065, "price_range": (20000, 60000)},
|
|
395
|
+
"tier3": {"cap_rate": 0.06, "discount_rate": 0.08, "price_range": (5000, 20000)},
|
|
396
|
+
}
|
|
397
|
+
defaults = _TIER_DEFAULTS.get(tier, _TIER_DEFAULTS["tier2"])
|
|
398
|
+
cap_rate = float(params.get("cap_rate", defaults["cap_rate"]))
|
|
399
|
+
discount_rate = float(params.get("discount_rate", defaults["discount_rate"]))
|
|
400
|
+
price_range = defaults["price_range"]
|
|
401
|
+
|
|
402
|
+
annual_rent = monthly_rent * 12
|
|
403
|
+
noi = annual_rent * (1 - vacancy)
|
|
404
|
+
|
|
405
|
+
# ── 1. 收益法 (Income Approach) ────────────────────────────────────────
|
|
406
|
+
income_value = noi / cap_rate if cap_rate > 0 else 0
|
|
407
|
+
|
|
408
|
+
# ── 2. DCF 法 ──────────────────────────────────────────────────────────
|
|
409
|
+
dcf_value = 0.0
|
|
410
|
+
if _HAS_NP:
|
|
411
|
+
cf = noi
|
|
412
|
+
for yr in range(1, hold_years + 1):
|
|
413
|
+
dcf_value += cf / (1 + discount_rate) ** yr
|
|
414
|
+
cf *= (1 + annual_growth)
|
|
415
|
+
# Terminal value (Gordon growth)
|
|
416
|
+
terminal_growth = min(annual_growth, 0.015)
|
|
417
|
+
terminal_cf = noi * (1 + annual_growth) ** hold_years
|
|
418
|
+
terminal_val = terminal_cf / (discount_rate - terminal_growth)
|
|
419
|
+
dcf_value += terminal_val / (1 + discount_rate) ** hold_years
|
|
420
|
+
else:
|
|
421
|
+
dcf_value = income_value # fallback
|
|
422
|
+
|
|
423
|
+
# ── 3. 市场比较法 (Market Comparable) ─────────────────────────────────
|
|
424
|
+
ref_price_per_sqm = float(params.get("price_per_sqm", 0))
|
|
425
|
+
if ref_price_per_sqm <= 0:
|
|
426
|
+
# Derive from rent using typical rent-to-price ratio
|
|
427
|
+
rent_per_sqm_monthly = monthly_rent / area if area > 0 else 0
|
|
428
|
+
# Typical P/R ratio: 300-600 months for residential, 150-250 for commercial
|
|
429
|
+
ratio = 400 if tier == "tier1" else 300 if tier == "tier2" else 200
|
|
430
|
+
ref_price_per_sqm = rent_per_sqm_monthly * ratio
|
|
431
|
+
|
|
432
|
+
market_value = ref_price_per_sqm * area
|
|
433
|
+
|
|
434
|
+
# ── Weighted average ───────────────────────────────────────────────────
|
|
435
|
+
weights = {"income": 0.4, "dcf": 0.4, "market": 0.2}
|
|
436
|
+
blended = (income_value * weights["income"] +
|
|
437
|
+
dcf_value * weights["dcf"] +
|
|
438
|
+
market_value * weights["market"])
|
|
439
|
+
|
|
440
|
+
# Price range from market benchmarks
|
|
441
|
+
lo = price_range[0] * area
|
|
442
|
+
hi = price_range[1] * area
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
"success": True,
|
|
446
|
+
"area_sqm": area,
|
|
447
|
+
"monthly_rent": monthly_rent,
|
|
448
|
+
"noi_annual": round(noi, 0),
|
|
449
|
+
"income_approach": round(income_value / 10000, 2), # 万元
|
|
450
|
+
"dcf_approach": round(dcf_value / 10000, 2),
|
|
451
|
+
"market_approach": round(market_value / 10000, 2),
|
|
452
|
+
"blended_value_wan": round(blended / 10000, 2),
|
|
453
|
+
"market_range_wan": [round(lo/10000, 0), round(hi/10000, 0)],
|
|
454
|
+
"price_per_sqm": round(blended / area, 0) if area > 0 else 0,
|
|
455
|
+
"gross_yield_pct": round(annual_rent / blended * 100, 2) if blended > 0 else 0,
|
|
456
|
+
"cap_rate_used": round(cap_rate * 100, 2),
|
|
457
|
+
"discount_rate_used": round(discount_rate * 100, 2),
|
|
458
|
+
"hold_years": hold_years,
|
|
459
|
+
"verdict": (
|
|
460
|
+
"价值被低估" if market_value < blended * 0.85
|
|
461
|
+
else "价值被高估" if market_value > blended * 1.15
|
|
462
|
+
else "定价合理"
|
|
463
|
+
),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ── 资产综合评分 ─────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
def asset_location_score(params: dict) -> dict:
|
|
470
|
+
"""
|
|
471
|
+
资产区位 + 业态潜力综合评分(0-100)。
|
|
472
|
+
结合区位因素、空间条件、业态适配度生成量化评分。
|
|
473
|
+
|
|
474
|
+
参数:
|
|
475
|
+
city: 城市名称
|
|
476
|
+
district: 区域/商圈
|
|
477
|
+
area_sqm: 面积(平米)
|
|
478
|
+
floor: 楼层
|
|
479
|
+
foot_traffic: 客流量评估 ("high"/"medium"/"low")
|
|
480
|
+
competition: 周边竞争 ("high"/"medium"/"low")
|
|
481
|
+
renovation_allowed: 是否允许改造 (bool)
|
|
482
|
+
open_fire_allowed: 是否允许明火 (bool)
|
|
483
|
+
vacant_days: 空置天数
|
|
484
|
+
expected_rent: 期望租金(元/月)
|
|
485
|
+
"""
|
|
486
|
+
city = str(params.get("city",""))
|
|
487
|
+
area = float(params.get("area_sqm", 100))
|
|
488
|
+
floor = int(params.get("floor", 1))
|
|
489
|
+
traffic = str(params.get("foot_traffic", "medium")).lower()
|
|
490
|
+
competition = str(params.get("competition", "medium")).lower()
|
|
491
|
+
reno_ok = bool(params.get("renovation_allowed", True))
|
|
492
|
+
fire_ok = bool(params.get("open_fire_allowed", False))
|
|
493
|
+
vacant_days = int(params.get("vacant_days", 0))
|
|
494
|
+
exp_rent = float(params.get("expected_rent", 0))
|
|
495
|
+
|
|
496
|
+
score = 50 # base
|
|
497
|
+
breakdown = {}
|
|
498
|
+
|
|
499
|
+
# 区位得分 (0-30)
|
|
500
|
+
city_bonus = 30 if city in CN_CITIES_TIER1 else 20 if city in CN_CITIES_TIER2 else 10
|
|
501
|
+
score += city_bonus - 20 # normalize around 50
|
|
502
|
+
breakdown["区位城市"] = f"{city_bonus - 20:+d}"
|
|
503
|
+
|
|
504
|
+
# 客流量 (0-15)
|
|
505
|
+
traffic_score = {"high": 15, "medium": 8, "low": 2}.get(traffic, 8)
|
|
506
|
+
score += traffic_score - 8
|
|
507
|
+
breakdown["客流量"] = f"{traffic_score - 8:+d}"
|
|
508
|
+
|
|
509
|
+
# 楼层因素 (0-10)
|
|
510
|
+
floor_score = 10 if floor == 1 else 6 if floor <= 3 else 2
|
|
511
|
+
score += floor_score - 5
|
|
512
|
+
breakdown["楼层"] = f"{floor_score - 5:+d}"
|
|
513
|
+
|
|
514
|
+
# 面积适配 (0-10) — 50-300m² 最优
|
|
515
|
+
area_score = 10 if 50 <= area <= 300 else 5 if area < 50 else 7
|
|
516
|
+
score += area_score - 5
|
|
517
|
+
breakdown["面积适配"] = f"{area_score - 5:+d}"
|
|
518
|
+
|
|
519
|
+
# 竞争强度(负分)
|
|
520
|
+
comp_penalty = {"high": -8, "medium": -3, "low": 0}.get(competition, -3)
|
|
521
|
+
score += comp_penalty
|
|
522
|
+
breakdown["竞争环境"] = f"{comp_penalty:+d}"
|
|
523
|
+
|
|
524
|
+
# 改造灵活性
|
|
525
|
+
if reno_ok:
|
|
526
|
+
score += 5; breakdown["可改造"] = "+5"
|
|
527
|
+
if fire_ok:
|
|
528
|
+
score += 3; breakdown["允许明火"] = "+3"
|
|
529
|
+
|
|
530
|
+
# 空置惩罚
|
|
531
|
+
if vacant_days > 180:
|
|
532
|
+
score -= 8; breakdown["空置惩罚"] = "-8"
|
|
533
|
+
elif vacant_days > 90:
|
|
534
|
+
score -= 3; breakdown["空置惩罚"] = "-3"
|
|
535
|
+
|
|
536
|
+
# 租金合理性(与城市基准对比)
|
|
537
|
+
if exp_rent > 0 and area > 0:
|
|
538
|
+
per_sqm = exp_rent / area
|
|
539
|
+
benchmarks = {"tier1": 300, "tier2": 120, "tier3": 50}
|
|
540
|
+
tier = "tier1" if city in CN_CITIES_TIER1 else "tier2" if city in CN_CITIES_TIER2 else "tier3"
|
|
541
|
+
bench = benchmarks[tier]
|
|
542
|
+
if per_sqm < bench * 0.7:
|
|
543
|
+
score += 5; breakdown["低于市场租金"] = "+5"
|
|
544
|
+
elif per_sqm > bench * 1.3:
|
|
545
|
+
score -= 5; breakdown["高于市场租金"] = "-5"
|
|
546
|
+
|
|
547
|
+
score = max(0, min(100, score))
|
|
548
|
+
|
|
549
|
+
if score >= 75:
|
|
550
|
+
rating = "A级 — 优质资产"
|
|
551
|
+
elif score >= 60:
|
|
552
|
+
rating = "B级 — 良好资产"
|
|
553
|
+
elif score >= 45:
|
|
554
|
+
rating = "C级 — 一般资产"
|
|
555
|
+
else:
|
|
556
|
+
rating = "D级 — 需改善"
|
|
557
|
+
|
|
558
|
+
# Suitable business types based on conditions
|
|
559
|
+
suitable = []
|
|
560
|
+
if fire_ok and area >= 80:
|
|
561
|
+
suitable.extend(["餐饮/火锅/烤肉"])
|
|
562
|
+
if traffic == "high" and area <= 150:
|
|
563
|
+
suitable.extend(["连锁零售/便利店/奶茶"])
|
|
564
|
+
if reno_ok and area >= 200:
|
|
565
|
+
suitable.extend(["健身房/儿童乐园/培训机构"])
|
|
566
|
+
if floor == 1 and area >= 100:
|
|
567
|
+
suitable.extend(["美容美发/洗车/宠物店"])
|
|
568
|
+
if not suitable:
|
|
569
|
+
suitable = ["轻餐饮", "服务类商铺"]
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
"success": True,
|
|
573
|
+
"score": score,
|
|
574
|
+
"rating": rating,
|
|
575
|
+
"breakdown": breakdown,
|
|
576
|
+
"suitable_businesses": list(set(suitable))[:5],
|
|
577
|
+
"city": city,
|
|
578
|
+
"area_sqm": area,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# ── 美国住房数据 (FRED) ──────────────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
def get_us_housing_data() -> dict:
|
|
585
|
+
"""
|
|
586
|
+
美国住房市场数据:
|
|
587
|
+
- 新屋开工 (HOUST)
|
|
588
|
+
- NAHB 建筑商信心指数
|
|
589
|
+
- S&P/Case-Shiller 20城房价指数 (SPCS20RSA)
|
|
590
|
+
- 30年固定按揭利率 (MORTGAGE30US)
|
|
591
|
+
"""
|
|
592
|
+
from macro_tools import _fred_series # reuse existing FRED helper
|
|
593
|
+
|
|
594
|
+
indicators = {
|
|
595
|
+
"housing_starts": ("HOUST", "新屋开工(千套,季调年化)"),
|
|
596
|
+
"nahb_index": ("NAHB REALTORS", "NAHB建筑商信心指数"),
|
|
597
|
+
"case_shiller_20": ("SPCS20RSA", "Case-Shiller 20城房价指数"),
|
|
598
|
+
"mortgage_30y": ("MORTGAGE30US", "30年固定按揭利率(%)"),
|
|
599
|
+
"existing_home_sales":("EXHOSLUSM495S","成屋销售(季调年化)"),
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# Fallback NAHB via yfinance-equivalent approximation
|
|
603
|
+
_FRED_MAP = {
|
|
604
|
+
"housing_starts": "HOUST",
|
|
605
|
+
"case_shiller_20": "SPCS20RSA",
|
|
606
|
+
"mortgage_30y": "MORTGAGE30US",
|
|
607
|
+
"existing_home_sales":"EXHOSLUSM495S",
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
results = {}
|
|
611
|
+
for key, series_id in _FRED_MAP.items():
|
|
612
|
+
data = _fred_series(series_id, limit=12)
|
|
613
|
+
if data:
|
|
614
|
+
results[key] = {
|
|
615
|
+
"label": indicators[key][1],
|
|
616
|
+
"latest": data[-1],
|
|
617
|
+
"series": data[-6:],
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
# NAHB via hardcoded label (no clean FRED key)
|
|
621
|
+
nahb_data = _fred_series("NHSUSSPT", limit=12)
|
|
622
|
+
if nahb_data:
|
|
623
|
+
results["nahb_index"] = {
|
|
624
|
+
"label": "NAHB建筑商信心指数 (>50=乐观)",
|
|
625
|
+
"latest": nahb_data[-1],
|
|
626
|
+
"series": nahb_data[-6:],
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if not results:
|
|
630
|
+
return {"success": False, "error": "FRED 数据获取失败"}
|
|
631
|
+
|
|
632
|
+
# Market assessment
|
|
633
|
+
mortgage = (results.get("mortgage_30y", {}).get("latest") or {}).get("value") or 0
|
|
634
|
+
cs_yoy = None # Case-Shiller is level index, need to compute YoY separately
|
|
635
|
+
|
|
636
|
+
assessment = []
|
|
637
|
+
if mortgage > 7:
|
|
638
|
+
assessment.append("按揭利率偏高(>7%),购房负担较重")
|
|
639
|
+
elif mortgage < 4:
|
|
640
|
+
assessment.append("按揭利率较低(<4%),购房环境宽松")
|
|
641
|
+
else:
|
|
642
|
+
assessment.append(f"按揭利率 {mortgage:.2f}%,属正常区间")
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
"success": True,
|
|
646
|
+
"country": "US",
|
|
647
|
+
"data": results,
|
|
648
|
+
"assessment": assessment,
|
|
649
|
+
"provider": "FRED",
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# ── Helper ────────────────────────────────────────────────────────────────────
|
|
654
|
+
|
|
655
|
+
def _safe_float(val) -> Optional[float]:
|
|
656
|
+
try:
|
|
657
|
+
return float(val) if val is not None and str(val) not in ("nan","None","") else None
|
|
658
|
+
except (ValueError, TypeError):
|
|
659
|
+
return None
|