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,617 @@
|
|
|
1
|
+
# Auto-extracted from aria_cli.py (SlashCommands._SCAFFOLD_TEMPLATES)
|
|
2
|
+
|
|
3
|
+
SCAFFOLD_TEMPLATES = {
|
|
4
|
+
"quant": {
|
|
5
|
+
"desc": "量化策略项目(数据层 / 信号层 / 回测引擎 / 报告)",
|
|
6
|
+
"dirs": ["data/raw", "data/processed", "strategy", "backtest", "report", "tests"],
|
|
7
|
+
"files": {
|
|
8
|
+
"requirements.txt": "akshare\nyfinance\npandas\nnumpy\nmatplotlib\nscipy\n",
|
|
9
|
+
"data/fetcher.py": '''\
|
|
10
|
+
"""数据获取层:akshare A股 / yfinance 美股,带本地 CSV 缓存。"""
|
|
11
|
+
import os, pathlib
|
|
12
|
+
import akshare as ak
|
|
13
|
+
import yfinance as yf
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
CACHE = pathlib.Path(__file__).parent / "processed"
|
|
17
|
+
CACHE.mkdir(exist_ok=True)
|
|
18
|
+
|
|
19
|
+
def fetch_ashare(symbol: str, start: str, end: str, adjust: str = "qfq") -> pd.DataFrame:
|
|
20
|
+
cache_f = CACHE / f"{symbol}_{adjust}.csv"
|
|
21
|
+
if cache_f.exists():
|
|
22
|
+
df = pd.read_csv(cache_f, index_col=0, parse_dates=True)
|
|
23
|
+
if str(df.index[-1].date()) >= end:
|
|
24
|
+
return df
|
|
25
|
+
df = ak.stock_zh_a_hist(symbol=symbol, period="daily",
|
|
26
|
+
start_date=start.replace("-",""),
|
|
27
|
+
end_date=end.replace("-",""), adjust=adjust)
|
|
28
|
+
df = df.rename(columns={"日期":"Date","开盘":"Open","最高":"High",
|
|
29
|
+
"最低":"Low","收盘":"Close","成交量":"Volume"})
|
|
30
|
+
df["Date"] = pd.to_datetime(df["Date"])
|
|
31
|
+
df = df.set_index("Date").sort_index()
|
|
32
|
+
df.to_csv(cache_f)
|
|
33
|
+
return df
|
|
34
|
+
|
|
35
|
+
def fetch_us(symbol: str, period: str = "2y") -> pd.DataFrame:
|
|
36
|
+
return yf.Ticker(symbol).history(period=period, auto_adjust=True)[
|
|
37
|
+
["Open","High","Low","Close","Volume"]]
|
|
38
|
+
''',
|
|
39
|
+
"strategy/base.py": '''\
|
|
40
|
+
"""抽象策略基类:子类只需实现 generate_signals()。"""
|
|
41
|
+
from abc import ABC, abstractmethod
|
|
42
|
+
import pandas as pd
|
|
43
|
+
|
|
44
|
+
class Strategy(ABC):
|
|
45
|
+
name: str = "base"
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def generate_signals(self, df: pd.DataFrame) -> pd.Series:
|
|
49
|
+
"""返回 +1(做多)/ -1(做空)/ 0(空仓)的 Series,与 df.index 对齐。"""
|
|
50
|
+
...
|
|
51
|
+
''',
|
|
52
|
+
"strategy/dual_ma.py": '''\
|
|
53
|
+
"""双均线策略:短期 MA 上穿长期 MA 买入,下穿卖出。"""
|
|
54
|
+
import pandas as pd
|
|
55
|
+
from .base import Strategy
|
|
56
|
+
|
|
57
|
+
class DualMA(Strategy):
|
|
58
|
+
name = "dual_ma"
|
|
59
|
+
|
|
60
|
+
def __init__(self, fast: int = 5, slow: int = 20):
|
|
61
|
+
self.fast = fast
|
|
62
|
+
self.slow = slow
|
|
63
|
+
|
|
64
|
+
def generate_signals(self, df: pd.DataFrame) -> pd.Series:
|
|
65
|
+
close = df["Close"]
|
|
66
|
+
fast_ma = close.rolling(self.fast).mean()
|
|
67
|
+
slow_ma = close.rolling(self.slow).mean()
|
|
68
|
+
sig = pd.Series(0, index=df.index)
|
|
69
|
+
sig[fast_ma > slow_ma] = 1
|
|
70
|
+
sig[fast_ma < slow_ma] = -1
|
|
71
|
+
# 只在交叉时换仓(减少换手)
|
|
72
|
+
return sig.diff().fillna(0).clip(-1, 1).cumsum().clip(-1, 1)
|
|
73
|
+
''',
|
|
74
|
+
"strategy/__init__.py": "from .dual_ma import DualMA\n",
|
|
75
|
+
"backtest/engine.py": '''\
|
|
76
|
+
"""向量化回测引擎,含 A股交易成本。"""
|
|
77
|
+
import pandas as pd
|
|
78
|
+
import numpy as np
|
|
79
|
+
|
|
80
|
+
# A股交易成本:双边手续费 0.025%×2 + 印花税 0.05%(卖出)+ 滑点 0.1%
|
|
81
|
+
A_SHARE_COST = 0.025/100 * 2 + 0.05/100 + 0.1/100
|
|
82
|
+
|
|
83
|
+
def backtest(df: pd.DataFrame, signals: pd.Series,
|
|
84
|
+
cost_rate: float = A_SHARE_COST,
|
|
85
|
+
initial_capital: float = 1_000_000) -> pd.DataFrame:
|
|
86
|
+
"""
|
|
87
|
+
df : OHLCV DataFrame(index=DatetimeIndex)
|
|
88
|
+
signals : +1 做多 / -1 做空 / 0 空仓
|
|
89
|
+
返回含 equity_curve 和各日 pnl 的 DataFrame
|
|
90
|
+
"""
|
|
91
|
+
ret = df["Close"].pct_change()
|
|
92
|
+
pos = signals.shift(1).fillna(0) # 信号次日执行
|
|
93
|
+
turnover = pos.diff().abs().fillna(0)
|
|
94
|
+
strat_ret = pos * ret - turnover * cost_rate
|
|
95
|
+
bnh_ret = ret.copy()
|
|
96
|
+
|
|
97
|
+
cum_strat = (1 + strat_ret).cumprod() * initial_capital
|
|
98
|
+
cum_bnh = (1 + bnh_ret).cumprod() * initial_capital
|
|
99
|
+
|
|
100
|
+
result = pd.DataFrame({
|
|
101
|
+
"strategy": cum_strat, "buy_hold": cum_bnh,
|
|
102
|
+
"daily_ret": strat_ret, "position": pos,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# 统计指标
|
|
106
|
+
ann = 252
|
|
107
|
+
sharpe = strat_ret.mean() / strat_ret.std() * np.sqrt(ann) if strat_ret.std() else 0
|
|
108
|
+
max_dd = (cum_strat / cum_strat.cummax() - 1).min()
|
|
109
|
+
total_r = cum_strat.iloc[-1] / initial_capital - 1
|
|
110
|
+
ann_r = (1 + total_r) ** (ann / len(result)) - 1
|
|
111
|
+
wins = (strat_ret[pos.shift(-1) != pos] > 0).mean()
|
|
112
|
+
|
|
113
|
+
result.attrs = dict(sharpe=round(sharpe,2), max_drawdown=round(max_dd,4),
|
|
114
|
+
total_return=round(total_r,4), annual_return=round(ann_r,4),
|
|
115
|
+
win_rate=round(wins,4))
|
|
116
|
+
return result
|
|
117
|
+
''',
|
|
118
|
+
"backtest/__init__.py": "from .engine import backtest\n",
|
|
119
|
+
"report/plot.py": '''\
|
|
120
|
+
"""生成净值曲线 + 回撤图,保存为 PNG。"""
|
|
121
|
+
import matplotlib
|
|
122
|
+
matplotlib.use("Agg")
|
|
123
|
+
import matplotlib.pyplot as plt
|
|
124
|
+
import matplotlib.gridspec as gridspec
|
|
125
|
+
import pandas as pd
|
|
126
|
+
import pathlib
|
|
127
|
+
|
|
128
|
+
OUT = pathlib.Path(__file__).parent / "output"
|
|
129
|
+
OUT.mkdir(exist_ok=True)
|
|
130
|
+
|
|
131
|
+
def plot_result(result: pd.DataFrame, title: str = "Backtest") -> pathlib.Path:
|
|
132
|
+
fig = plt.figure(figsize=(12, 7), facecolor="#0d1117")
|
|
133
|
+
gs = gridspec.GridSpec(2, 1, height_ratios=[3,1], hspace=0.08)
|
|
134
|
+
|
|
135
|
+
ax1 = fig.add_subplot(gs[0])
|
|
136
|
+
ax1.plot(result.index, result["strategy"], color="#3fb950", lw=1.5, label="Strategy")
|
|
137
|
+
ax1.plot(result.index, result["buy_hold"], color="#58a6ff", lw=1.0, label="Buy & Hold", alpha=0.7)
|
|
138
|
+
ax1.set_facecolor("#161b22"); ax1.tick_params(colors="#8b949e"); ax1.legend(facecolor="#21262d", edgecolor="#30363d", labelcolor="#e6edf3")
|
|
139
|
+
ax1.set_title(title, color="#e6edf3", fontsize=13)
|
|
140
|
+
for spine in ax1.spines.values(): spine.set_color("#30363d")
|
|
141
|
+
|
|
142
|
+
dd = result["strategy"] / result["strategy"].cummax() - 1
|
|
143
|
+
ax2 = fig.add_subplot(gs[1], sharex=ax1)
|
|
144
|
+
ax2.fill_between(result.index, dd, 0, color="#f85149", alpha=0.6)
|
|
145
|
+
ax2.set_facecolor("#161b22"); ax2.tick_params(colors="#8b949e")
|
|
146
|
+
ax2.set_ylabel("Drawdown", color="#8b949e", fontsize=9)
|
|
147
|
+
for spine in ax2.spines.values(): spine.set_color("#30363d")
|
|
148
|
+
|
|
149
|
+
a = result.attrs
|
|
150
|
+
fig.text(0.12, 0.02, f"Annual {a.get('annual_return',0):.1%} Sharpe {a.get('sharpe',0):.2f} MaxDD {a.get('max_drawdown',0):.1%} Win {a.get('win_rate',0):.1%}", color="#8b949e", fontsize=9)
|
|
151
|
+
|
|
152
|
+
out_f = OUT / f"{title.replace(' ','_')}.png"
|
|
153
|
+
plt.savefig(out_f, dpi=130, bbox_inches="tight", facecolor=fig.get_facecolor())
|
|
154
|
+
plt.close(fig)
|
|
155
|
+
print(f"图表已保存: {out_f}")
|
|
156
|
+
return out_f
|
|
157
|
+
''',
|
|
158
|
+
"report/__init__.py": "from .plot import plot_result\n",
|
|
159
|
+
"main.py": '''\
|
|
160
|
+
"""运行量化策略回测的主入口。"""
|
|
161
|
+
import sys, pathlib
|
|
162
|
+
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
|
163
|
+
|
|
164
|
+
from data.fetcher import fetch_ashare
|
|
165
|
+
from strategy import DualMA
|
|
166
|
+
from backtest import backtest
|
|
167
|
+
from report import plot_result
|
|
168
|
+
from datetime import datetime, timedelta
|
|
169
|
+
|
|
170
|
+
SYMBOL = "600519" # 贵州茅台(示例)
|
|
171
|
+
END = datetime.now().strftime("%Y-%m-%d")
|
|
172
|
+
START = (datetime.now() - timedelta(days=730)).strftime("%Y-%m-%d")
|
|
173
|
+
|
|
174
|
+
print(f"抓取 {SYMBOL} 数据 {START} → {END}...")
|
|
175
|
+
df = fetch_ashare(SYMBOL, START, END)
|
|
176
|
+
print(f"共 {len(df)} 条 K线")
|
|
177
|
+
|
|
178
|
+
strategy = DualMA(fast=5, slow=20)
|
|
179
|
+
signals = strategy.generate_signals(df)
|
|
180
|
+
result = backtest(df, signals)
|
|
181
|
+
|
|
182
|
+
a = result.attrs
|
|
183
|
+
print(f"\n===== 回测结果 ({SYMBOL} · 双均线5/20) =====")
|
|
184
|
+
print(f"总收益: {a['total_return']:.2%}")
|
|
185
|
+
print(f"年化收益: {a['annual_return']:.2%}")
|
|
186
|
+
print(f"夏普比率: {a['sharpe']:.2f}")
|
|
187
|
+
print(f"最大回撤: {a['max_drawdown']:.2%}")
|
|
188
|
+
print(f"胜率: {a['win_rate']:.2%}")
|
|
189
|
+
|
|
190
|
+
plot_result(result, f"{SYMBOL}_DualMA")
|
|
191
|
+
''',
|
|
192
|
+
"tests/test_strategy.py": '''\
|
|
193
|
+
"""基础单元测试:策略信号生成 + 回测引擎。"""
|
|
194
|
+
import sys, pathlib
|
|
195
|
+
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
|
|
196
|
+
import pandas as pd
|
|
197
|
+
import numpy as np
|
|
198
|
+
from strategy import DualMA
|
|
199
|
+
from backtest import backtest
|
|
200
|
+
|
|
201
|
+
def _make_df(n=100):
|
|
202
|
+
dates = pd.date_range("2024-01-01", periods=n)
|
|
203
|
+
close = pd.Series(100 + np.cumsum(np.random.randn(n)), index=dates)
|
|
204
|
+
return pd.DataFrame({"Open":close,"High":close*1.01,"Low":close*0.99,"Close":close,"Volume":1e6})
|
|
205
|
+
|
|
206
|
+
def test_signal_shape():
|
|
207
|
+
df = _make_df()
|
|
208
|
+
sig = DualMA(fast=5,slow=20).generate_signals(df)
|
|
209
|
+
assert len(sig) == len(df)
|
|
210
|
+
assert set(sig.unique()).issubset({-1,0,1})
|
|
211
|
+
|
|
212
|
+
def test_backtest_runs():
|
|
213
|
+
df = _make_df()
|
|
214
|
+
sig = DualMA().generate_signals(df)
|
|
215
|
+
res = backtest(df, sig)
|
|
216
|
+
assert "strategy" in res.columns
|
|
217
|
+
assert res.attrs.get("sharpe") is not None
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
test_signal_shape(); test_backtest_runs(); print("All tests passed.")
|
|
221
|
+
''',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
"analysis": {
|
|
225
|
+
"desc": "数据分析项目(数据加载 / 清洗 / 可视化 / 报告)",
|
|
226
|
+
"dirs": ["data/raw", "data/processed", "src", "output", "notebooks"],
|
|
227
|
+
"files": {
|
|
228
|
+
"requirements.txt": "pandas\nnumpy\nmatplotlib\nseaborn\nopenpyxl\nyfinance\n",
|
|
229
|
+
"src/loader.py": '''\
|
|
230
|
+
"""数据加载工具:支持 CSV / Excel / yfinance。"""
|
|
231
|
+
import pandas as pd, pathlib, yfinance as yf
|
|
232
|
+
|
|
233
|
+
DATA = pathlib.Path(__file__).parent.parent / "data"
|
|
234
|
+
|
|
235
|
+
def load_csv(filename: str, **kw) -> pd.DataFrame:
|
|
236
|
+
return pd.read_csv(DATA / "raw" / filename, **kw)
|
|
237
|
+
|
|
238
|
+
def load_excel(filename: str, sheet=0, **kw) -> pd.DataFrame:
|
|
239
|
+
return pd.read_excel(DATA / "raw" / filename, sheet_name=sheet, **kw)
|
|
240
|
+
|
|
241
|
+
def load_stock(symbol: str, period: str = "1y") -> pd.DataFrame:
|
|
242
|
+
df = yf.Ticker(symbol).history(period=period, auto_adjust=True, progress=False)
|
|
243
|
+
return df[["Open","High","Low","Close","Volume"]]
|
|
244
|
+
|
|
245
|
+
def save_processed(df: pd.DataFrame, name: str) -> pathlib.Path:
|
|
246
|
+
out = DATA / "processed" / name
|
|
247
|
+
df.to_csv(out); return out
|
|
248
|
+
''',
|
|
249
|
+
"src/analyzer.py": '''\
|
|
250
|
+
"""常用分析函数:描述统计 / 相关性 / 滚动指标。"""
|
|
251
|
+
import pandas as pd, numpy as np
|
|
252
|
+
|
|
253
|
+
def describe_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
254
|
+
return df.describe().round(4)
|
|
255
|
+
|
|
256
|
+
def correlation_matrix(df: pd.DataFrame) -> pd.DataFrame:
|
|
257
|
+
return df.corr().round(4)
|
|
258
|
+
|
|
259
|
+
def rolling_stats(series: pd.Series, window: int = 20) -> pd.DataFrame:
|
|
260
|
+
return pd.DataFrame({
|
|
261
|
+
"mean": series.rolling(window).mean(),
|
|
262
|
+
"std": series.rolling(window).std(),
|
|
263
|
+
"zscore": (series - series.rolling(window).mean()) / series.rolling(window).std(),
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
def annualized_return(series: pd.Series) -> float:
|
|
267
|
+
ret = series.pct_change().dropna()
|
|
268
|
+
return float((1 + ret.mean()) ** 252 - 1)
|
|
269
|
+
|
|
270
|
+
def max_drawdown(series: pd.Series) -> float:
|
|
271
|
+
return float((series / series.cummax() - 1).min())
|
|
272
|
+
''',
|
|
273
|
+
"src/visualizer.py": '''\
|
|
274
|
+
"""可视化工具:折线图 / 直方图 / 热力图。"""
|
|
275
|
+
import matplotlib
|
|
276
|
+
matplotlib.use("Agg")
|
|
277
|
+
import matplotlib.pyplot as plt
|
|
278
|
+
import seaborn as sns
|
|
279
|
+
import pandas as pd, pathlib
|
|
280
|
+
|
|
281
|
+
OUT = pathlib.Path(__file__).parent.parent / "output"
|
|
282
|
+
OUT.mkdir(exist_ok=True)
|
|
283
|
+
plt.style.use("dark_background")
|
|
284
|
+
|
|
285
|
+
def plot_series(series: pd.Series, title: str = "", filename: str = "plot.png") -> pathlib.Path:
|
|
286
|
+
fig, ax = plt.subplots(figsize=(11,4), facecolor="#0d1117")
|
|
287
|
+
ax.plot(series.index, series.values, color="#58a6ff", lw=1.2)
|
|
288
|
+
ax.set_title(title, color="#e6edf3"); ax.set_facecolor("#161b22")
|
|
289
|
+
for sp in ax.spines.values(): sp.set_color("#30363d")
|
|
290
|
+
ax.tick_params(colors="#8b949e")
|
|
291
|
+
out = OUT / filename
|
|
292
|
+
plt.savefig(out, dpi=130, bbox_inches="tight", facecolor=fig.get_facecolor())
|
|
293
|
+
plt.close(); return out
|
|
294
|
+
|
|
295
|
+
def plot_heatmap(df: pd.DataFrame, title: str = "Correlation", filename: str = "heatmap.png") -> pathlib.Path:
|
|
296
|
+
fig, ax = plt.subplots(figsize=(8,6), facecolor="#0d1117")
|
|
297
|
+
sns.heatmap(df, annot=True, fmt=".2f", cmap="RdYlGn", ax=ax,
|
|
298
|
+
linewidths=0.3, linecolor="#30363d", cbar_kws={"shrink":0.7})
|
|
299
|
+
ax.set_title(title, color="#e6edf3"); ax.tick_params(colors="#8b949e")
|
|
300
|
+
out = OUT / filename
|
|
301
|
+
plt.savefig(out, dpi=130, bbox_inches="tight", facecolor=fig.get_facecolor())
|
|
302
|
+
plt.close(); return out
|
|
303
|
+
''',
|
|
304
|
+
"main.py": '''\
|
|
305
|
+
"""数据分析项目入口示例。"""
|
|
306
|
+
import sys, pathlib
|
|
307
|
+
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
|
308
|
+
from src.loader import load_stock
|
|
309
|
+
from src.analyzer import describe_df, correlation_matrix, annualized_return, max_drawdown
|
|
310
|
+
from src.visualizer import plot_series
|
|
311
|
+
|
|
312
|
+
# 示例:分析多支股票
|
|
313
|
+
SYMBOLS = ["AAPL","MSFT","NVDA"]
|
|
314
|
+
closes = {}
|
|
315
|
+
for sym in SYMBOLS:
|
|
316
|
+
df = load_stock(sym, period="1y")
|
|
317
|
+
closes[sym] = df["Close"]
|
|
318
|
+
print(f"{sym}: 年化收益 {annualized_return(df['Close']):.2%} 最大回撤 {max_drawdown(df['Close']):.2%}")
|
|
319
|
+
|
|
320
|
+
import pandas as pd
|
|
321
|
+
closes_df = pd.DataFrame(closes)
|
|
322
|
+
print("\n相关性矩阵:")
|
|
323
|
+
print(correlation_matrix(closes_df))
|
|
324
|
+
|
|
325
|
+
plot_series(closes_df["AAPL"], "AAPL Close Price", "aapl_close.png")
|
|
326
|
+
print("图表已保存到 output/")
|
|
327
|
+
''',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
"fastapi": {
|
|
331
|
+
"desc": "FastAPI 金融数据 API 服务(行情 / 技术指标 / 基本面)",
|
|
332
|
+
"dirs": ["app/routers", "app/schemas", "app/services", "tests"],
|
|
333
|
+
"files": {
|
|
334
|
+
"requirements.txt": "fastapi\nuvicorn[standard]\nyfinance\nrequests\npandas\nnumpy\n",
|
|
335
|
+
"app/__init__.py": "",
|
|
336
|
+
"app/main.py": '''\
|
|
337
|
+
"""FastAPI 主应用入口。"""
|
|
338
|
+
from fastapi import FastAPI
|
|
339
|
+
from app.routers import market, health
|
|
340
|
+
|
|
341
|
+
app = FastAPI(title="Aria Finance API", version="1.0.0",
|
|
342
|
+
description="金融行情与分析 REST API")
|
|
343
|
+
|
|
344
|
+
app.include_router(health.router)
|
|
345
|
+
app.include_router(market.router, prefix="/market", tags=["market"])
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
import uvicorn
|
|
349
|
+
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
|
350
|
+
''',
|
|
351
|
+
"app/routers/__init__.py": "",
|
|
352
|
+
"app/routers/health.py": '''\
|
|
353
|
+
from fastapi import APIRouter
|
|
354
|
+
from datetime import datetime
|
|
355
|
+
|
|
356
|
+
router = APIRouter()
|
|
357
|
+
|
|
358
|
+
@router.get("/health")
|
|
359
|
+
def health_check():
|
|
360
|
+
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
|
361
|
+
''',
|
|
362
|
+
"app/routers/market.py": '''\
|
|
363
|
+
"""行情路由:报价 / 历史 K 线 / 技术指标。"""
|
|
364
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
365
|
+
from app.services.market_service import get_quote, get_history, get_technicals
|
|
366
|
+
|
|
367
|
+
router = APIRouter()
|
|
368
|
+
|
|
369
|
+
@router.get("/quote/{symbol}")
|
|
370
|
+
def quote(symbol: str):
|
|
371
|
+
data = get_quote(symbol.upper())
|
|
372
|
+
if not data:
|
|
373
|
+
raise HTTPException(status_code=404, detail=f"No data for {symbol}")
|
|
374
|
+
return data
|
|
375
|
+
|
|
376
|
+
@router.get("/history/{symbol}")
|
|
377
|
+
def history(symbol: str, period: str = Query("3mo", description="1mo/3mo/6mo/1y/2y")):
|
|
378
|
+
records = get_history(symbol.upper(), period)
|
|
379
|
+
return {"symbol": symbol.upper(), "period": period, "data": records}
|
|
380
|
+
|
|
381
|
+
@router.get("/technicals/{symbol}")
|
|
382
|
+
def technicals(symbol: str):
|
|
383
|
+
return get_technicals(symbol.upper())
|
|
384
|
+
''',
|
|
385
|
+
"app/services/__init__.py": "",
|
|
386
|
+
"app/services/market_service.py": '''\
|
|
387
|
+
"""市场数据服务层(基于 yfinance)。"""
|
|
388
|
+
import yfinance as yf
|
|
389
|
+
import pandas as pd, numpy as np
|
|
390
|
+
from typing import Optional
|
|
391
|
+
|
|
392
|
+
def get_quote(symbol: str) -> Optional[dict]:
|
|
393
|
+
try:
|
|
394
|
+
t = yf.Ticker(symbol)
|
|
395
|
+
info = t.info or {}
|
|
396
|
+
price = info.get("currentPrice") or info.get("regularMarketPrice")
|
|
397
|
+
if not price:
|
|
398
|
+
hist = t.history(period="1d", auto_adjust=True)
|
|
399
|
+
price = float(hist["Close"].iloc[-1]) if not hist.empty else None
|
|
400
|
+
return {
|
|
401
|
+
"symbol": symbol,
|
|
402
|
+
"price": price,
|
|
403
|
+
"prev_close": info.get("previousClose"),
|
|
404
|
+
"open": info.get("open"),
|
|
405
|
+
"day_high": info.get("dayHigh"),
|
|
406
|
+
"day_low": info.get("dayLow"),
|
|
407
|
+
"volume": info.get("volume"),
|
|
408
|
+
"market_cap": info.get("marketCap"),
|
|
409
|
+
"pe_ratio": info.get("trailingPE"),
|
|
410
|
+
"name": info.get("longName", symbol),
|
|
411
|
+
"currency": info.get("currency", "USD"),
|
|
412
|
+
}
|
|
413
|
+
except Exception:
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
def get_history(symbol: str, period: str = "3mo") -> list:
|
|
417
|
+
try:
|
|
418
|
+
df = yf.Ticker(symbol).history(period=period, auto_adjust=True)
|
|
419
|
+
df = df.reset_index()
|
|
420
|
+
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
|
421
|
+
return df[["Date","Open","High","Low","Close","Volume"]].round(4).to_dict(orient="records")
|
|
422
|
+
except Exception:
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
def get_technicals(symbol: str) -> dict:
|
|
426
|
+
try:
|
|
427
|
+
df = yf.Ticker(symbol).history(period="6mo", auto_adjust=True)
|
|
428
|
+
close = df["Close"]
|
|
429
|
+
rsi_period = 14
|
|
430
|
+
delta = close.diff()
|
|
431
|
+
gain = delta.clip(lower=0).rolling(rsi_period).mean()
|
|
432
|
+
loss = (-delta.clip(upper=0)).rolling(rsi_period).mean()
|
|
433
|
+
rs = gain / loss
|
|
434
|
+
rsi = float((100 - 100 / (1 + rs)).iloc[-1])
|
|
435
|
+
ema12 = close.ewm(span=12).mean()
|
|
436
|
+
ema26 = close.ewm(span=26).mean()
|
|
437
|
+
macd = ema12 - ema26
|
|
438
|
+
signal = macd.ewm(span=9).mean()
|
|
439
|
+
return {
|
|
440
|
+
"symbol": symbol,
|
|
441
|
+
"rsi": round(rsi, 2),
|
|
442
|
+
"macd": round(float(macd.iloc[-1]), 4),
|
|
443
|
+
"macd_signal": round(float(signal.iloc[-1]), 4),
|
|
444
|
+
"ma20": round(float(close.rolling(20).mean().iloc[-1]), 2),
|
|
445
|
+
"ma60": round(float(close.rolling(60).mean().iloc[-1]), 2),
|
|
446
|
+
"bb_upper": round(float(close.rolling(20).mean().iloc[-1] + 2*close.rolling(20).std().iloc[-1]), 2),
|
|
447
|
+
"bb_lower": round(float(close.rolling(20).mean().iloc[-1] - 2*close.rolling(20).std().iloc[-1]), 2),
|
|
448
|
+
}
|
|
449
|
+
except Exception as e:
|
|
450
|
+
return {"error": str(e)}
|
|
451
|
+
''',
|
|
452
|
+
"tests/test_api.py": '''\
|
|
453
|
+
"""API 单元测试(不启动服务器)。"""
|
|
454
|
+
import sys, pathlib
|
|
455
|
+
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
|
|
456
|
+
from fastapi.testclient import TestClient
|
|
457
|
+
from app.main import app
|
|
458
|
+
|
|
459
|
+
client = TestClient(app)
|
|
460
|
+
|
|
461
|
+
def test_health():
|
|
462
|
+
r = client.get("/health")
|
|
463
|
+
assert r.status_code == 200
|
|
464
|
+
assert r.json()["status"] == "ok"
|
|
465
|
+
|
|
466
|
+
def test_quote_valid():
|
|
467
|
+
r = client.get("/market/quote/AAPL")
|
|
468
|
+
assert r.status_code == 200
|
|
469
|
+
data = r.json()
|
|
470
|
+
assert "price" in data
|
|
471
|
+
assert data["symbol"] == "AAPL"
|
|
472
|
+
|
|
473
|
+
def test_history():
|
|
474
|
+
r = client.get("/market/history/AAPL?period=1mo")
|
|
475
|
+
assert r.status_code == 200
|
|
476
|
+
assert len(r.json()["data"]) > 0
|
|
477
|
+
|
|
478
|
+
if __name__ == "__main__":
|
|
479
|
+
test_health(); test_quote_valid(); test_history(); print("All tests passed.")
|
|
480
|
+
''',
|
|
481
|
+
"run.py": '''\
|
|
482
|
+
"""启动 FastAPI 开发服务器。"""
|
|
483
|
+
import uvicorn
|
|
484
|
+
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
|
485
|
+
''',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
"dashboard": {
|
|
489
|
+
"desc": "Plotly Dash 交互式金融看板",
|
|
490
|
+
"dirs": ["assets", "components", "data"],
|
|
491
|
+
"files": {
|
|
492
|
+
"requirements.txt": "dash\nplotly\nyfinance\npandas\nnumpy\n",
|
|
493
|
+
"components/__init__.py": "",
|
|
494
|
+
"components/chart.py": '''\
|
|
495
|
+
"""K线图 + 均线组件。"""
|
|
496
|
+
import yfinance as yf
|
|
497
|
+
import plotly.graph_objects as go
|
|
498
|
+
from plotly.subplots import make_subplots
|
|
499
|
+
import pandas as pd, numpy as np
|
|
500
|
+
|
|
501
|
+
def build_candlestick(symbol: str, period: str = "6mo") -> go.Figure:
|
|
502
|
+
df = yf.Ticker(symbol).history(period=period, auto_adjust=True)
|
|
503
|
+
df["MA20"] = df["Close"].rolling(20).mean()
|
|
504
|
+
df["MA60"] = df["Close"].rolling(60).mean()
|
|
505
|
+
|
|
506
|
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
|
507
|
+
row_heights=[0.75,0.25], vertical_spacing=0.03)
|
|
508
|
+
fig.add_trace(go.Candlestick(
|
|
509
|
+
x=df.index, open=df["Open"], high=df["High"],
|
|
510
|
+
low=df["Low"], close=df["Close"], name="K线",
|
|
511
|
+
increasing_line_color="#3fb950", decreasing_line_color="#f85149"
|
|
512
|
+
), row=1, col=1)
|
|
513
|
+
fig.add_trace(go.Scatter(x=df.index, y=df["MA20"], name="MA20",
|
|
514
|
+
line=dict(color="#58a6ff", width=1)), row=1, col=1)
|
|
515
|
+
fig.add_trace(go.Scatter(x=df.index, y=df["MA60"], name="MA60",
|
|
516
|
+
line=dict(color="#e3b341", width=1)), row=1, col=1)
|
|
517
|
+
fig.add_trace(go.Bar(x=df.index, y=df["Volume"], name="成交量",
|
|
518
|
+
marker_color="#30363d"), row=2, col=1)
|
|
519
|
+
fig.update_layout(
|
|
520
|
+
template="plotly_dark", plot_bgcolor="#0d1117", paper_bgcolor="#010409",
|
|
521
|
+
title=f"{symbol} | {period}", height=600,
|
|
522
|
+
xaxis_rangeslider_visible=False,
|
|
523
|
+
legend=dict(bgcolor="#161b22", bordercolor="#30363d"),
|
|
524
|
+
)
|
|
525
|
+
return fig
|
|
526
|
+
''',
|
|
527
|
+
"app.py": '''\
|
|
528
|
+
"""Dash 交互式金融看板主应用。"""
|
|
529
|
+
import dash
|
|
530
|
+
from dash import dcc, html, Input, Output, State
|
|
531
|
+
from components.chart import build_candlestick
|
|
532
|
+
import yfinance as yf
|
|
533
|
+
|
|
534
|
+
app = dash.Dash(__name__, title="Aria Dashboard")
|
|
535
|
+
|
|
536
|
+
PERIODS = ["1mo","3mo","6mo","1y","2y","5y"]
|
|
537
|
+
|
|
538
|
+
app.layout = html.Div([
|
|
539
|
+
html.Div([
|
|
540
|
+
html.H1("Aria 金融看板", style={"color":"#e6edf3","fontSize":"22px","fontWeight":"700","margin":"0"}),
|
|
541
|
+
html.Div([
|
|
542
|
+
dcc.Input(id="symbol-input", value="AAPL", type="text", debounce=True,
|
|
543
|
+
placeholder="输入股票代码",
|
|
544
|
+
style={"background":"#161b22","color":"#e6edf3","border":"1px solid #30363d",
|
|
545
|
+
"borderRadius":"6px","padding":"8px 12px","width":"160px","marginRight":"8px"}),
|
|
546
|
+
dcc.Dropdown(id="period-dd", options=[{"label":p,"value":p} for p in PERIODS],
|
|
547
|
+
value="6mo", clearable=False,
|
|
548
|
+
style={"width":"100px","background":"#161b22","color":"#0d1117"}),
|
|
549
|
+
], style={"display":"flex","alignItems":"center","gap":"8px"}),
|
|
550
|
+
], style={"display":"flex","justifyContent":"space-between","alignItems":"center",
|
|
551
|
+
"padding":"16px 24px","borderBottom":"1px solid #21262d","background":"#010409"}),
|
|
552
|
+
|
|
553
|
+
dcc.Loading(dcc.Graph(id="main-chart", style={"height":"600px"}),
|
|
554
|
+
color="#58a6ff"),
|
|
555
|
+
|
|
556
|
+
html.Div(id="stats-row", style={"display":"flex","gap":"10px",
|
|
557
|
+
"padding":"12px 24px","background":"#010409"}),
|
|
558
|
+
], style={"background":"#010409","minHeight":"100vh","fontFamily":"-apple-system,sans-serif"})
|
|
559
|
+
|
|
560
|
+
@app.callback(
|
|
561
|
+
[Output("main-chart","figure"), Output("stats-row","children")],
|
|
562
|
+
[Input("symbol-input","value"), Input("period-dd","value")],
|
|
563
|
+
prevent_initial_call=False,
|
|
564
|
+
)
|
|
565
|
+
def update_chart(symbol, period):
|
|
566
|
+
if not symbol:
|
|
567
|
+
return dash.no_update, dash.no_update
|
|
568
|
+
symbol = symbol.strip().upper()
|
|
569
|
+
fig = build_candlestick(symbol, period)
|
|
570
|
+
try:
|
|
571
|
+
info = yf.Ticker(symbol).info or {}
|
|
572
|
+
price = info.get("currentPrice") or info.get("regularMarketPrice","—")
|
|
573
|
+
mktcap = info.get("marketCap")
|
|
574
|
+
pe = info.get("trailingPE","—")
|
|
575
|
+
name = info.get("longName", symbol)
|
|
576
|
+
mktcap_s = f"${mktcap/1e9:.1f}B" if mktcap else "—"
|
|
577
|
+
except Exception:
|
|
578
|
+
name,price,mktcap_s,pe = symbol,"—","—","—"
|
|
579
|
+
|
|
580
|
+
def kpi(label, val):
|
|
581
|
+
return html.Div([
|
|
582
|
+
html.Div(label, style={"fontSize":"11px","color":"#8b949e","marginBottom":"4px"}),
|
|
583
|
+
html.Div(str(val), style={"fontSize":"18px","fontWeight":"700","color":"#e6edf3"}),
|
|
584
|
+
], style={"background":"#161b22","border":"1px solid #30363d","borderRadius":"8px",
|
|
585
|
+
"padding":"12px 16px","minWidth":"120px"})
|
|
586
|
+
|
|
587
|
+
stats = [kpi("公司", name[:20]), kpi("现价", price),
|
|
588
|
+
kpi("市值", mktcap_s), kpi("市盈率", pe)]
|
|
589
|
+
return fig, stats
|
|
590
|
+
|
|
591
|
+
if __name__ == "__main__":
|
|
592
|
+
app.run(debug=True, host="0.0.0.0", port=8050)
|
|
593
|
+
''',
|
|
594
|
+
"README.md": '''\
|
|
595
|
+
# Aria Dashboard
|
|
596
|
+
|
|
597
|
+
交互式金融看板(Plotly Dash)。
|
|
598
|
+
|
|
599
|
+
## 启动
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
pip install -r requirements.txt
|
|
603
|
+
python app.py
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
浏览器访问 http://localhost:8050
|
|
607
|
+
|
|
608
|
+
## 功能
|
|
609
|
+
|
|
610
|
+
- K线图 + 成交量(MA20/MA60)
|
|
611
|
+
- 多周期切换(1mo 到 5y)
|
|
612
|
+
- 基本面 KPI(市值、市盈率)
|
|
613
|
+
''',
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
|