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.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
dashboard_generator.py ADDED
@@ -0,0 +1,578 @@
1
+ """
2
+ dashboard_generator.py — Bloomberg-style per-request dashboard HTML generator
3
+
4
+ Usage:
5
+ python3 dashboard_generator.py [--open]
6
+
7
+ Integration: triggered via /dashboard command in aria_cli.py.
8
+
9
+ Data sources (all local, embedded at generation time — no runtime API calls):
10
+ - ~/.arthera/portfolio.db -> positions, trades, realized P&L
11
+ - ~/.aria/daemon.db -> active price alerts
12
+ - aria_cli config -> watchlist
13
+ - MarketDataClient -> market prices with provider fallback
14
+ - artifacts.py -> recently generated files
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import platform
22
+ import sqlite3
23
+ import subprocess
24
+ import sys
25
+ from datetime import datetime, timedelta
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Optional, Tuple
28
+
29
+ _PORTFOLIO_DB = Path.home() / ".arthera" / "portfolio.db"
30
+ _DAEMON_DB = Path.home() / ".aria" / "daemon.db"
31
+
32
+
33
+ # ── Data collection ────────────────────────────────────────────────────────────
34
+
35
+ def _fetch_prices(symbols: List[str]) -> Dict[str, Dict]:
36
+ if not symbols:
37
+ return {}
38
+ result: Dict[str, Dict] = {}
39
+ try:
40
+ from market_data_client import MarketDataClient
41
+
42
+ quotes = MarketDataClient().multi_quote(symbols).get("quotes") or {}
43
+ for sym, quote in quotes.items():
44
+ if not quote or not quote.get("success"):
45
+ continue
46
+ price = quote.get("price")
47
+ prev = quote.get("prev_close") or quote.get("previous_close")
48
+ pct = quote.get("change_percent")
49
+ if pct is None and price is not None and prev:
50
+ try:
51
+ pct = round((float(price) / float(prev) - 1) * 100, 2)
52
+ except Exception:
53
+ pct = None
54
+ result[sym] = {
55
+ "price": round(float(price), 4) if price is not None else None,
56
+ "prev_close": round(float(prev), 4) if prev is not None else None,
57
+ "pct_change": pct,
58
+ "name": quote.get("name") or sym,
59
+ "provider": quote.get("provider") or quote.get("source") or "",
60
+ }
61
+ except Exception:
62
+ pass
63
+ return result
64
+
65
+
66
+ def _load_portfolio() -> Tuple[List[Dict], List[Dict]]:
67
+ if not _PORTFOLIO_DB.exists():
68
+ return [], []
69
+ try:
70
+ sys.path.insert(0, str(Path(__file__).parent))
71
+ from portfolio_ledger import PortfolioLedger
72
+ ledger = PortfolioLedger()
73
+ positions = ledger.get_positions()
74
+ realized = ledger.get_realized_pnl()
75
+ return positions, realized
76
+ except Exception:
77
+ return [], []
78
+
79
+
80
+ def _load_alerts() -> List[Dict]:
81
+ if not _DAEMON_DB.exists():
82
+ return []
83
+ try:
84
+ with sqlite3.connect(_DAEMON_DB) as conn:
85
+ conn.row_factory = sqlite3.Row
86
+ rows = conn.execute(
87
+ "SELECT id, symbol, condition, value, trigger_count, active, created_at "
88
+ "FROM alerts ORDER BY active DESC, created_at DESC LIMIT 50"
89
+ ).fetchall()
90
+ return [dict(r) for r in rows]
91
+ except Exception:
92
+ return []
93
+
94
+
95
+ def _load_recent_artifacts(limit: int = 10) -> List[Dict]:
96
+ items: List[Dict] = []
97
+ try:
98
+ sys.path.insert(0, str(Path(__file__).parent))
99
+ from artifacts import recent_artifacts_all
100
+ for art in recent_artifacts_all(limit=limit):
101
+ p = Path(str(art.get("path") or art.get("metadata_path") or "")).expanduser()
102
+ if p.exists():
103
+ items.append({
104
+ "name": p.name,
105
+ "path": str(p),
106
+ "category": str(art.get("kind") or art.get("category") or "artifact"),
107
+ "size_kb": round(p.stat().st_size / 1024, 1),
108
+ "mtime": datetime.fromtimestamp(p.stat().st_mtime).strftime("%Y-%m-%d %H:%M"),
109
+ })
110
+ except Exception:
111
+ return []
112
+ items.sort(key=lambda x: x["mtime"], reverse=True)
113
+ return items[:limit]
114
+
115
+
116
+ def _market_overview_symbols() -> List[str]:
117
+ return [
118
+ "000001.SS", "399001.SZ", "399006.SZ", "000300.SS",
119
+ "^GSPC", "^IXIC", "^DJI", "^VIX",
120
+ "BTC-USD", "ETH-USD", "GC=F", "CNY=X",
121
+ ]
122
+
123
+
124
+ _SYM_LABELS: Dict[str, str] = {
125
+ "000001.SS": "上证指数", "399001.SZ": "深证成指",
126
+ "399006.SZ": "创业板指", "000300.SS": "沪深300",
127
+ "^GSPC": "S&P 500", "^IXIC": "NASDAQ",
128
+ "^DJI": "DOW JONES", "^VIX": "VIX",
129
+ "BTC-USD": "BTC/USD", "ETH-USD": "ETH/USD",
130
+ "GC=F": "GOLD $/oz", "CNY=X": "USD/CNY",
131
+ }
132
+
133
+
134
+ # ── HTML helpers ───────────────────────────────────────────────────────────────
135
+
136
+ def _pct_cls(pct: Optional[float]) -> str:
137
+ if pct is None:
138
+ return "flat"
139
+ return "up" if pct >= 0 else "down"
140
+
141
+
142
+ def _pct_str(pct: Optional[float]) -> str:
143
+ if pct is None:
144
+ return "--"
145
+ sign = "+" if pct > 0 else ""
146
+ return f"{sign}{pct:.2f}%"
147
+
148
+
149
+ def _price_str(price: Optional[float], sym: str = "") -> str:
150
+ if not price:
151
+ return "--"
152
+ if price >= 10_000:
153
+ return f"{price:,.0f}"
154
+ if price >= 1_000:
155
+ return f"{price:,.2f}"
156
+ if price >= 100:
157
+ return f"{price:,.2f}"
158
+ if price >= 1:
159
+ return f"{price:,.4f}".rstrip("0").rstrip(".")
160
+ return f"{price:.6f}".rstrip("0").rstrip(".")
161
+
162
+
163
+ def _quote_tiles(items: List[Dict]) -> str:
164
+ parts = []
165
+ for d in items:
166
+ sym = d["symbol"]
167
+ label = d.get("label", sym)
168
+ pct = d.get("pct")
169
+ price = d.get("price")
170
+ cls = _pct_cls(pct)
171
+ arrow = "▲" if cls == "up" else "▼" if cls == "down" else ""
172
+ parts.append(
173
+ f'<div class="qt">'
174
+ f'<div class="qt-sym">{sym}</div>'
175
+ f'<div class="qt-name">{label}</div>'
176
+ f'<div class="qt-price">{_price_str(price, sym)}</div>'
177
+ f'<div class="qt-chg {cls}">{arrow} {_pct_str(pct)}</div>'
178
+ f'</div>'
179
+ )
180
+ return "\n".join(parts)
181
+
182
+
183
+ def _positions_table(positions: List[Dict]) -> str:
184
+ rows = []
185
+ for p in positions:
186
+ upnl = p.get("unrealized_pnl")
187
+ upct = p.get("unrealized_pct")
188
+ dpct = p.get("day_pct")
189
+ cu = _pct_cls(upnl)
190
+ cd = _pct_cls(dpct)
191
+ price_str = str(p.get("current_price") or "--")
192
+ mktv_str = f"{p.get('market_value'):,.0f}" if p.get("market_value") else "--"
193
+ upnl_str = ("+" if (upnl or 0) > 0 else "") + f"{upnl:,.0f}" if upnl is not None else "--"
194
+ upct_str = _pct_str(upct)
195
+ dpct_str = _pct_str(dpct)
196
+ rows.append(
197
+ f"<tr>"
198
+ f'<td class="sym">{p["symbol"]}</td>'
199
+ f'<td class="num">{p["net_qty"]:,}</td>'
200
+ f'<td class="num">{p["avg_cost"]:.4f}</td>'
201
+ f'<td class="num">{price_str}</td>'
202
+ f'<td class="num {cd}">{dpct_str}</td>'
203
+ f'<td class="num">{mktv_str}</td>'
204
+ f'<td class="num {cu}">{upnl_str}</td>'
205
+ f'<td class="num {cu}">{upct_str}</td>'
206
+ f"</tr>"
207
+ )
208
+ return (
209
+ '<table class="data-table">'
210
+ "<thead><tr>"
211
+ "<th>SYMBOL</th>"
212
+ '<th class="r">QTY</th>'
213
+ '<th class="r">AVG COST</th>'
214
+ '<th class="r">PRICE</th>'
215
+ '<th class="r">DAY CHG</th>'
216
+ '<th class="r">MKT VALUE</th>'
217
+ '<th class="r">UNREALIZED</th>'
218
+ '<th class="r">RETURN %</th>'
219
+ "</tr></thead>"
220
+ f"<tbody>{''.join(rows)}</tbody>"
221
+ "</table>"
222
+ )
223
+
224
+
225
+ def _alerts_table(alerts: List[Dict]) -> str:
226
+ rows = []
227
+ for a in alerts[:15]:
228
+ cond = (a.get("condition") or "").upper().replace("_", " ")
229
+ astat = "ACTIVE" if a.get("active") else "OFF"
230
+ bcls = "badge-on" if a.get("active") else "badge-off"
231
+ rows.append(
232
+ f"<tr>"
233
+ f'<td class="sym">{a.get("symbol", "")}</td>'
234
+ f"<td>{cond}</td>"
235
+ f'<td class="num">{a.get("value", "")}</td>'
236
+ f'<td class="num dim">{a.get("trigger_count", 0)}x</td>'
237
+ f'<td><span class="badge {bcls}">{astat}</span></td>'
238
+ f"</tr>"
239
+ )
240
+ return (
241
+ '<table class="data-table">'
242
+ "<thead><tr>"
243
+ "<th>SYMBOL</th><th>CONDITION</th>"
244
+ '<th class="r">LEVEL</th>'
245
+ '<th class="r">TRIGGERED</th>'
246
+ "<th>STATUS</th>"
247
+ "</tr></thead>"
248
+ f"<tbody>{''.join(rows)}</tbody>"
249
+ "</table>"
250
+ )
251
+
252
+
253
+ def _movers_table(items: List[Dict], limit: int = 8) -> str:
254
+ ranked = sorted(
255
+ [d for d in items if d.get("pct") is not None],
256
+ key=lambda d: d.get("pct") or 0,
257
+ reverse=True,
258
+ )[:limit]
259
+ rows = []
260
+ for d in ranked:
261
+ pct = d.get("pct")
262
+ cls = _pct_cls(pct)
263
+ rows.append(
264
+ f"<tr>"
265
+ f'<td class="sym">{d.get("symbol", "")}</td>'
266
+ f'<td>{d.get("label", d.get("symbol", ""))}</td>'
267
+ f'<td class="num">{_price_str(d.get("price"), d.get("symbol", ""))}</td>'
268
+ f'<td class="num {cls}">{_pct_str(pct)}</td>'
269
+ f"</tr>"
270
+ )
271
+ if not rows:
272
+ return '<div style="color:var(--text-muted);font-size:12px;padding:14px 0">NO MOVERS</div>'
273
+ return (
274
+ '<table class="data-table">'
275
+ "<thead><tr>"
276
+ "<th>SYMBOL</th><th>NAME</th>"
277
+ '<th class="r">PRICE</th>'
278
+ '<th class="r">CHG%</th>'
279
+ "</tr></thead>"
280
+ f"<tbody>{''.join(rows)}</tbody>"
281
+ "</table>"
282
+ )
283
+
284
+
285
+ def _artifacts_list(artifacts: List[Dict]) -> str:
286
+ rows = []
287
+ for a in artifacts:
288
+ cat = (a.get("category") or "").upper()
289
+ rows.append(
290
+ f"<tr>"
291
+ f'<td class="sym" style="font-size:10px">{cat}</td>'
292
+ f"<td><span style=\"color:var(--text-primary);font-size:12px\">{a['name']}</span></td>"
293
+ f'<td class="num dim">{a.get("size_kb", 0)} KB</td>'
294
+ f'<td class="dim">{a.get("mtime", "")}</td>'
295
+ f'<td><a href="file://{a["path"]}" target="_blank" class="badge badge-off" style="text-decoration:none">OPEN</a></td>'
296
+ f"</tr>"
297
+ )
298
+ return (
299
+ '<table class="data-table">'
300
+ "<thead><tr>"
301
+ "<th>TYPE</th><th>FILE</th>"
302
+ '<th class="r">SIZE</th>'
303
+ "<th>MODIFIED</th><th></th>"
304
+ "</tr></thead>"
305
+ f"<tbody>{''.join(rows)}</tbody>"
306
+ "</table>"
307
+ )
308
+
309
+
310
+ def _metric_card(label: str, value: str, sub: str = "", cls: str = "") -> str:
311
+ val_cls = f' class="{cls}"' if cls else ""
312
+ return (
313
+ '<div class="metric">'
314
+ f'<div class="metric-label">{label}</div>'
315
+ f'<div class="metric-val{val_cls}">{value}</div>'
316
+ + (f'<div class="metric-sub">{sub}</div>' if sub else "")
317
+ + "</div>"
318
+ )
319
+
320
+
321
+ # ── Main generator ─────────────────────────────────────────────────────────────
322
+
323
+ def generate(
324
+ watchlist: Optional[List[str]] = None,
325
+ config: Optional[Dict] = None,
326
+ mode: str = "full",
327
+ output_path: Optional[Path] = None,
328
+ ) -> Path:
329
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
330
+ stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
331
+ watchlist = watchlist or (config or {}).get("watchlist") or ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"]
332
+
333
+ positions, realized = _load_portfolio()
334
+ alerts = _load_alerts()
335
+ artifacts = _load_recent_artifacts()
336
+
337
+ port_syms = [p["symbol"] for p in positions]
338
+ all_syms = list(dict.fromkeys(_market_overview_symbols() + watchlist + port_syms))
339
+ prices = _fetch_prices(all_syms)
340
+
341
+ for pos in positions:
342
+ q = prices.get(pos["symbol"]) or {}
343
+ price = q.get("price", 0)
344
+ cost = pos.get("avg_cost") or 0
345
+ qty = pos.get("net_qty", 0)
346
+ pos["current_price"] = price or None
347
+ pos["market_value"] = round(price * qty, 2) if price else None
348
+ pos["unrealized_pnl"] = round((price - cost) * qty, 2) if price and cost else None
349
+ pos["unrealized_pct"] = round((price / cost - 1) * 100, 2) if price and cost else None
350
+ pos["day_pct"] = q.get("pct_change")
351
+
352
+ total_mktv = sum(p.get("market_value") or 0 for p in positions)
353
+ total_cost = sum(p.get("cost_basis") or 0 for p in positions)
354
+ total_unreal = sum(p.get("unrealized_pnl") or 0 for p in positions)
355
+ total_realized = sum(r.get("total_pnl", 0) for r in realized)
356
+
357
+ market_data = [
358
+ {"symbol": s, "label": _SYM_LABELS.get(s, s), "price": (prices.get(s) or {}).get("price"), "pct": (prices.get(s) or {}).get("pct_change")}
359
+ for s in _market_overview_symbols()
360
+ ]
361
+ watchlist_data = [
362
+ {"symbol": s, "label": s, "price": (prices.get(s) or {}).get("price"), "pct": (prices.get(s) or {}).get("pct_change")}
363
+ for s in watchlist
364
+ ]
365
+ _seen_symbols = set()
366
+ movers_data = []
367
+ for item in market_data + watchlist_data:
368
+ sym = item.get("symbol")
369
+ if sym and sym in _seen_symbols:
370
+ continue
371
+ if sym:
372
+ _seen_symbols.add(sym)
373
+ movers_data.append(item)
374
+
375
+ # ── Portfolio metrics ──────────────────────────────────────────────────────
376
+ mktv_str = f"{total_mktv:,.0f}" if total_mktv else "--"
377
+ cost_str = f"{total_cost:,.0f}" if total_cost else "--"
378
+ unreal_cls = "up" if total_unreal > 0 else "down" if total_unreal < 0 else ""
379
+ unreal_str = ("+" if total_unreal > 0 else "") + f"{total_unreal:,.0f}" if total_unreal else "--"
380
+ real_cls = "up" if total_realized > 0 else "down" if total_realized < 0 else ""
381
+ real_str = ("+" if total_realized > 0 else "") + f"{total_realized:,.0f}" if total_realized else "--"
382
+ active_alerts = len([a for a in alerts if a.get("active")])
383
+
384
+ positions_html = _positions_table(positions) if positions else (
385
+ '<div style="color:var(--text-muted);font-size:12px;padding:14px 0">'
386
+ 'NO POSITIONS — add via /journal add buy SYMBOL QTY PRICE'
387
+ '</div>'
388
+ )
389
+ alerts_html = _alerts_table(alerts) if alerts else (
390
+ '<div style="color:var(--text-muted);font-size:12px;padding:14px 0">'
391
+ 'NO ALERTS — add via /alert add SYMBOL gt 200'
392
+ '</div>'
393
+ )
394
+ artifacts_html = _artifacts_list(artifacts) if artifacts else (
395
+ '<div style="color:var(--text-muted);font-size:12px;padding:14px 0">'
396
+ 'NO RECENT FILES — run /backtest or /report to generate'
397
+ '</div>'
398
+ )
399
+
400
+ mode = (mode or "full").lower().strip()
401
+ if mode not in {"full", "brief", "market", "portfolio"}:
402
+ mode = "full"
403
+
404
+ include_portfolio = mode in {"full", "portfolio"}
405
+ include_market = mode in {"full", "market", "brief", "portfolio"}
406
+ include_watchlist = mode in {"full", "market", "brief"}
407
+ include_alerts = mode in {"full", "portfolio"}
408
+ include_artifacts = mode in {"full", "portfolio"}
409
+ include_movers = mode in {"brief", "market", "full"}
410
+ mode_blurb = {
411
+ "brief": "MORNING BRIEF — INDEXES, MOVERS, WATCHLIST",
412
+ "market": "MARKET DASHBOARD — OVERVIEW, MOVERS, WATCHLIST",
413
+ "portfolio": "PORTFOLIO DASHBOARD — POSITIONS, ALERTS, FILES",
414
+ "full": "FULL TERMINAL — PORTFOLIO + MARKET + ALERTS + FILES",
415
+ }.get(mode, "FULL TERMINAL")
416
+
417
+ from apps.cli.prompts.ui import get_ui_css_base
418
+ css = get_ui_css_base()
419
+
420
+ html = f"""<!DOCTYPE html>
421
+ <html lang="zh"><head>
422
+ <meta charset="UTF-8">
423
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
424
+ <title>ARIA TERMINAL — {now_str}</title>
425
+ <style>
426
+ {css}
427
+ /* ── Dashboard-specific layout ── */
428
+ .two-col {{ display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); }}
429
+ .two-col > * {{ background: var(--bg-primary); }}
430
+ .col-inner {{ padding: 14px; }}
431
+ .no-pos {{ color: var(--text-muted); font-size: 12px; padding: 14px 0; font-family: var(--font-mono); }}
432
+ .data-source {{ font-size: 10px; color: var(--text-muted); font-family: var(--font-mono);
433
+ margin-top: 8px; letter-spacing: 0.04em; }}
434
+ </style>
435
+ </head>
436
+ <body>
437
+
438
+ <!-- ── Header ── -->
439
+ <div class="topbar">
440
+ <div class="topbar-brand">ARIA <span>TERMINAL</span></div>
441
+ <div class="topbar-meta">GENERATED {now_str.upper()} &nbsp;·&nbsp; MODE: {mode.upper()} &nbsp;·&nbsp; DATA: MARKET DATA SERVICE + LOCAL DB &nbsp;·&nbsp; DELAYED/PROVIDER DEPENDENT</div>
442
+ </div>
443
+
444
+ <div class="section">
445
+ <div class="sh">{mode_blurb}</div>
446
+ <div class="metric-sub">Mode-specific layout keeps morning brief, market view, and portfolio view distinct.</div>
447
+ </div>
448
+
449
+ <!-- ── Portfolio Summary ── -->
450
+ {f'''<div class="section">
451
+ <div class="sh">PORTFOLIO SUMMARY</div>
452
+ <div class="grid g4" style="margin-bottom:1px">
453
+ {_metric_card("MARKET VALUE", mktv_str)}
454
+ {_metric_card("COST BASIS", cost_str)}
455
+ {_metric_card("UNREALIZED P&L", unreal_str, sub="mark-to-market", cls=unreal_cls)}
456
+ {_metric_card("REALIZED P&L", real_str, sub="all closed trades", cls=real_cls)}
457
+ </div>
458
+ </div>''' if include_portfolio else ''}
459
+
460
+ <!-- ── Positions Table ── -->
461
+ {f'''<div class="section">
462
+ <div class="sh">OPEN POSITIONS ({len(positions)})</div>
463
+ {positions_html}
464
+ </div>''' if include_portfolio else ''}
465
+
466
+ <!-- ── Market Overview ── -->
467
+ {f'''<div class="section">
468
+ <div class="sh">TOP MOVERS</div>
469
+ {_movers_table(movers_data, limit=8)}
470
+ </div>''' if include_movers else ''}
471
+
472
+ {f'''<div class="section">
473
+ <div class="sh">MARKET OVERVIEW — A-SHARE</div>
474
+ <div class="grid g4" style="margin-bottom:1px">
475
+ {_quote_tiles(market_data[:4])}
476
+ </div>
477
+ </div>''' if include_market else ''}
478
+
479
+ {f'''<div class="section">
480
+ <div class="sh">MARKET OVERVIEW — US EQUITY</div>
481
+ <div class="grid g4" style="margin-bottom:1px">
482
+ {_quote_tiles(market_data[4:8])}
483
+ </div>
484
+ </div>''' if include_market else ''}
485
+
486
+ {f'''<div class="section">
487
+ <div class="sh">CRYPTO / COMMODITY / FX</div>
488
+ <div class="grid g4" style="margin-bottom:1px">
489
+ {_quote_tiles(market_data[8:])}
490
+ </div>
491
+ <div class="data-source">PRICES VIA ARIA MARKET DATA ROUTER — PROVIDER ATTRIBUTED — NOT FOR TRADING</div>
492
+ </div>''' if include_market else ''}
493
+
494
+ <!-- ── Watchlist ── -->
495
+ {f'''<div class="section">
496
+ <div class="sh">WATCHLIST ({len(watchlist)} SYMBOLS)</div>
497
+ <div class="grid g{'6' if len(watchlist_data) > 4 else '4'}" style="margin-bottom:1px">
498
+ {_quote_tiles(watchlist_data)}
499
+ </div>
500
+ </div>''' if include_watchlist else ''}
501
+
502
+ <!-- ── Alerts + Artifacts ── -->
503
+ {f'''<div class="two-col">
504
+ <div class="col-inner">
505
+ <div class="sh">PRICE ALERTS ({active_alerts} ACTIVE)</div>
506
+ {alerts_html}
507
+ </div>
508
+ <div class="col-inner">
509
+ <div class="sh">RECENT GENERATED FILES</div>
510
+ {artifacts_html}
511
+ </div>
512
+ </div>''' if (include_alerts or include_artifacts) else ''}
513
+
514
+ </body></html>"""
515
+
516
+ artifact = None
517
+ if output_path is None:
518
+ from artifacts import create_user_artifact
519
+
520
+ artifact = create_user_artifact("dashboards", mode, f"aria_dashboard_{mode}", ".html")
521
+ out = artifact.path
522
+ else:
523
+ out = output_path
524
+ out.parent.mkdir(parents=True, exist_ok=True)
525
+ out.write_text(html, encoding="utf-8")
526
+ if artifact is not None:
527
+ try:
528
+ from artifacts import write_artifact_metadata, write_artifact_raw_data
529
+
530
+ write_artifact_metadata(artifact, {
531
+ "kind": "dashboard",
532
+ "status": "complete",
533
+ "mode": mode,
534
+ "created_at": datetime.now().isoformat(timespec="seconds"),
535
+ "data": {
536
+ "watchlist": watchlist,
537
+ "market_symbols": _market_overview_symbols(),
538
+ "position_count": len(positions),
539
+ "alert_count": len(alerts),
540
+ },
541
+ })
542
+ write_artifact_raw_data(artifact, {
543
+ "market": market_data,
544
+ "watchlist": watchlist_data,
545
+ "positions": positions,
546
+ "alerts": alerts,
547
+ })
548
+ except Exception:
549
+ pass
550
+ return out
551
+
552
+
553
+ def _open_in_browser(path: Path) -> None:
554
+ try:
555
+ sys_name = platform.system()
556
+ if sys_name == "Darwin":
557
+ subprocess.Popen(["open", str(path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
558
+ elif sys_name == "Windows":
559
+ os.startfile(str(path))
560
+ else:
561
+ subprocess.Popen(["xdg-open", str(path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
562
+ except Exception:
563
+ pass
564
+
565
+
566
+ def generate_and_open(
567
+ watchlist: Optional[List[str]] = None,
568
+ config: Optional[Dict] = None,
569
+ mode: str = "full",
570
+ ) -> Path:
571
+ out = generate(watchlist=watchlist, config=config, mode=mode)
572
+ _open_in_browser(out)
573
+ return out
574
+
575
+
576
+ if __name__ == "__main__":
577
+ p = generate_and_open()
578
+ print(f"Dashboard saved: {p}")