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
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()} · MODE: {mode.upper()} · DATA: MARKET DATA SERVICE + LOCAL DB · 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}")
|