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
@@ -0,0 +1,434 @@
1
+ """TradingView symbol mapping helpers.
2
+
3
+ TradingView is an optional chart/alert surface. These helpers only translate
4
+ Aria's canonical market symbols into TradingView URLs; they do not fetch or
5
+ trust TradingView data for analysis.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sqlite3
11
+ import time
12
+ import uuid
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.parse import quote
16
+
17
+ from artifacts import slugify_topic, user_generated_dir
18
+
19
+
20
+ _INDEX_SYMBOLS = {
21
+ "^GSPC": "SP:SPX",
22
+ "^IXIC": "NASDAQ:IXIC",
23
+ "^DJI": "DJ:DJI",
24
+ "^RUT": "RUSSELL:RUT",
25
+ "^VIX": "CBOE:VIX",
26
+ "^HSI": "HKEX:HSI",
27
+ "^HSTECH": "HKEX:HSTECH",
28
+ "^N225": "TVC:NI225",
29
+ "^FTSE": "TVC:UKX",
30
+ "^GDAXI": "XETR:DAX",
31
+ "^FCHI": "EURONEXT:PX1",
32
+ }
33
+
34
+ _FUTURES_SYMBOLS = {
35
+ "GC=F": "COMEX:GC1!",
36
+ "SI=F": "COMEX:SI1!",
37
+ "CL=F": "NYMEX:CL1!",
38
+ "BZ=F": "NYMEX:BRN1!",
39
+ "HG=F": "COMEX:HG1!",
40
+ "NG=F": "NYMEX:NG1!",
41
+ "ZC=F": "CBOT:ZC1!",
42
+ "ZS=F": "CBOT:ZS1!",
43
+ }
44
+
45
+ _FX_SYMBOLS = {
46
+ "CNY=X": "FX_IDC:USDCNY",
47
+ "EURUSD=X": "FX:EURUSD",
48
+ "GBPUSD=X": "FX:GBPUSD",
49
+ "JPY=X": "FX:USDJPY",
50
+ "DX-Y.NYB": "TVC:DXY",
51
+ }
52
+
53
+
54
+ def tradingview_symbol(symbol: str) -> str:
55
+ """Map an Aria canonical symbol to a TradingView symbol."""
56
+ s = (symbol or "").strip().upper()
57
+ if not s:
58
+ return ""
59
+ if s in _INDEX_SYMBOLS:
60
+ return _INDEX_SYMBOLS[s]
61
+ if s in _FUTURES_SYMBOLS:
62
+ return _FUTURES_SYMBOLS[s]
63
+ if s in _FX_SYMBOLS:
64
+ return _FX_SYMBOLS[s]
65
+ if s.endswith("-USD"):
66
+ return f"BINANCE:{s[:-4]}USDT"
67
+ if s.endswith(".HK"):
68
+ digits = "".join(ch for ch in s[:-3] if ch.isdigit()).lstrip("0") or s[:-3]
69
+ return f"HKEX:{digits}"
70
+ if s.endswith(".SS") or (s.isdigit() and len(s) == 6 and s.startswith(("6", "9"))):
71
+ return f"SSE:{s[:6]}"
72
+ if s.endswith(".SZ") or (s.isdigit() and len(s) == 6):
73
+ return f"SZSE:{s[:6]}"
74
+ if "." in s:
75
+ base, suffix = s.rsplit(".", 1)
76
+ exchange = {
77
+ "DE": "XETR",
78
+ "PA": "EURONEXT",
79
+ "AS": "EURONEXT",
80
+ "MI": "MIL",
81
+ "MC": "BME",
82
+ "L": "LSE",
83
+ "TO": "TSX",
84
+ }.get(suffix, suffix)
85
+ return f"{exchange}:{base}"
86
+ return f"NASDAQ:{s}"
87
+
88
+
89
+ def tradingview_url(symbol: str, *, interval: str | None = None) -> str:
90
+ tv_symbol = tradingview_symbol(symbol)
91
+ if not tv_symbol:
92
+ return ""
93
+ url = f"https://www.tradingview.com/chart/?symbol={quote(tv_symbol, safe='')}"
94
+ if interval:
95
+ url += f"&interval={quote(str(interval), safe='')}"
96
+ return url
97
+
98
+
99
+ def parse_tradingview_alert(payload: dict[str, Any] | str) -> dict[str, Any]:
100
+ """Normalize a TradingView webhook payload.
101
+
102
+ TradingView alert bodies are user-defined, so accept common JSON fields and
103
+ a compact text fallback such as "NVDA buy".
104
+ """
105
+ if isinstance(payload, str):
106
+ raw = payload.strip()
107
+ try:
108
+ payload = json.loads(raw)
109
+ except Exception:
110
+ parts = raw.replace(",", " ").split()
111
+ payload = {
112
+ "symbol": parts[0] if parts else "",
113
+ "action": parts[1] if len(parts) > 1 else "",
114
+ "message": raw,
115
+ }
116
+ data = dict(payload or {})
117
+ raw_symbol = (
118
+ data.get("symbol")
119
+ or data.get("ticker")
120
+ or data.get("tv_symbol")
121
+ or data.get("syminfo.ticker")
122
+ or data.get("syminfo.tickerid")
123
+ or data.get("s")
124
+ or ""
125
+ )
126
+ strategy = data.get("strategy") if isinstance(data.get("strategy"), dict) else {}
127
+ action = (
128
+ data.get("action")
129
+ or data.get("side")
130
+ or data.get("signal")
131
+ or data.get("order_action")
132
+ or data.get("strategy.order.action")
133
+ or strategy.get("order_action")
134
+ or ""
135
+ )
136
+ symbol = normalize_tradingview_alert_symbol(str(raw_symbol))
137
+ action_norm = str(action or "").strip().upper()
138
+ if action_norm in {"LONG", "BUY", "B"}:
139
+ action_norm = "BUY"
140
+ elif action_norm in {"SHORT", "SELL", "S"}:
141
+ action_norm = "SELL"
142
+ elif action_norm in {"EXIT", "CLOSE", "FLAT"}:
143
+ action_norm = "EXIT"
144
+ elif not action_norm:
145
+ action_norm = "ALERT"
146
+ return {
147
+ "symbol": symbol,
148
+ "action": action_norm,
149
+ "price": data.get("price") or data.get("close") or data.get("last") or data.get("strategy.order.price"),
150
+ "time": data.get("time") or data.get("timestamp") or data.get("t"),
151
+ "message": data.get("message") or data.get("alert_message") or "",
152
+ "channels": data.get("channels"),
153
+ "raw": data,
154
+ }
155
+
156
+
157
+ def _as_float_or_none(value: Any) -> float | None:
158
+ try:
159
+ if value in (None, ""):
160
+ return None
161
+ out = float(value)
162
+ return out if out == out else None
163
+ except Exception:
164
+ return None
165
+
166
+
167
+ def _first_present(raw: dict[str, Any], names: tuple[str, ...]) -> Any:
168
+ for name in names:
169
+ if name in raw and raw.get(name) not in (None, ""):
170
+ return raw.get(name)
171
+ return None
172
+
173
+
174
+ def _strategy_field(raw: dict[str, Any], names: tuple[str, ...]) -> Any:
175
+ strategy = raw.get("strategy")
176
+ if not isinstance(strategy, dict):
177
+ return None
178
+ return _first_present(strategy, names)
179
+
180
+
181
+ def _current_position_quantity(broker: Any, symbol: str) -> float:
182
+ sym = str(symbol or "").upper()
183
+ try:
184
+ for pos in broker.positions() or []:
185
+ if str(getattr(pos, "symbol", "") or "").upper() == sym:
186
+ return float(getattr(pos, "quantity", 0.0) or 0.0)
187
+ except Exception:
188
+ pass
189
+ return 0.0
190
+
191
+
192
+ def _ensure_tradingview_broker(broker_id: str | None = None) -> Any:
193
+ """Connect a broker for TradingView alert previews.
194
+
195
+ If no broker is configured, create a local paper account. This keeps
196
+ webhooks useful while never defaulting to live execution.
197
+ """
198
+ from brokers.config import add_broker_config, get_broker_config, get_default_broker_config, set_default_broker
199
+ from brokers.registry import BrokerRegistry
200
+
201
+ selected_id = str(broker_id or "").strip()
202
+ if not selected_id:
203
+ selected_id = str((get_default_broker_config() or {}).get("id") or "")
204
+
205
+ if not selected_id:
206
+ selected_id = "paper_main"
207
+ if not get_broker_config(selected_id):
208
+ add_broker_config({
209
+ "id": selected_id,
210
+ "type": "paper",
211
+ "label": "Aria TradingView 仿盘",
212
+ "mode": "paper",
213
+ "starting_cash": 100000,
214
+ "currency": "USD",
215
+ "default": True,
216
+ })
217
+ set_default_broker(selected_id)
218
+
219
+ registry = BrokerRegistry()
220
+ return registry.connect(selected_id)
221
+
222
+
223
+ def build_tradingview_order_preview(
224
+ payload: dict[str, Any] | str,
225
+ *,
226
+ broker: Any | None = None,
227
+ broker_id: str | None = None,
228
+ ) -> dict[str, Any]:
229
+ """Turn a TradingView alert into an Aria trade preview.
230
+
231
+ The function never executes an order. It only creates a `preview_id` through
232
+ the broker trading service, so live trading still requires manual
233
+ confirmation through `/trade confirm <preview_id>` or `broker_order`.
234
+ """
235
+ alert = parse_tradingview_alert(payload)
236
+ symbol = str(alert.get("symbol") or "").upper()
237
+ action = str(alert.get("action") or "ALERT").upper()
238
+ raw = dict(alert.get("raw") or {})
239
+ if not symbol:
240
+ return {"success": False, "error": "symbol is required", "alert": alert}
241
+ if action not in {"BUY", "SELL", "EXIT"}:
242
+ return {
243
+ "success": True,
244
+ "trade_preview_created": False,
245
+ "reason": "non_trade_alert",
246
+ "alert": alert,
247
+ }
248
+
249
+ selected_broker_id = broker_id or raw.get("broker_id") or raw.get("account_id")
250
+ if broker is None:
251
+ try:
252
+ broker = _ensure_tradingview_broker(str(selected_broker_id or "") or None)
253
+ except Exception as exc:
254
+ return {"success": False, "error": f"broker connect failed: {exc}", "alert": alert}
255
+
256
+ qty = _as_float_or_none(
257
+ _first_present(raw, (
258
+ "quantity",
259
+ "qty",
260
+ "shares",
261
+ "contracts",
262
+ "order_size",
263
+ "strategy.order.contracts",
264
+ "strategy.position_size",
265
+ ))
266
+ or _strategy_field(raw, ("order_contracts", "position_size"))
267
+ )
268
+ target_weight = _as_float_or_none(_first_present(raw, ("target_weight", "weight", "target")))
269
+ price = _as_float_or_none(alert.get("price"))
270
+ order_type = str(raw.get("order_type") or raw.get("type") or "limit").lower()
271
+ if order_type not in {"limit", "market"}:
272
+ order_type = "limit"
273
+
274
+ side = "buy" if action == "BUY" else "sell"
275
+ if side == "sell" and qty is None:
276
+ qty = _current_position_quantity(broker, symbol)
277
+ if qty <= 0:
278
+ return {
279
+ "success": True,
280
+ "trade_preview_created": False,
281
+ "reason": "no_position_to_exit" if action == "EXIT" else "missing_quantity",
282
+ "alert": alert,
283
+ "broker_id": getattr(broker, "broker_id", ""),
284
+ "broker_label": getattr(broker, "label", ""),
285
+ }
286
+ if side == "buy" and qty is None and target_weight is None:
287
+ return {
288
+ "success": True,
289
+ "trade_preview_created": False,
290
+ "reason": "missing_quantity",
291
+ "alert": alert,
292
+ "hint": "TradingView BUY alerts need quantity/qty or target_weight to create an order preview.",
293
+ "broker_id": getattr(broker, "broker_id", ""),
294
+ "broker_label": getattr(broker, "label", ""),
295
+ }
296
+
297
+ from brokers import OrderIntent, build_order_preview
298
+
299
+ preview = build_order_preview(
300
+ broker,
301
+ OrderIntent(
302
+ symbol=symbol,
303
+ side=side,
304
+ quantity=qty,
305
+ price=price,
306
+ order_type=order_type,
307
+ target_weight=target_weight,
308
+ source="tradingview_alert",
309
+ user_message=str(alert.get("message") or ""),
310
+ metadata={
311
+ "tradingview_action": action,
312
+ "tradingview_time": alert.get("time"),
313
+ },
314
+ ),
315
+ )
316
+ return {
317
+ "success": True,
318
+ "trade_preview_created": True,
319
+ "alert": alert,
320
+ "preview_id": preview.get("preview_id"),
321
+ "trade_preview": preview,
322
+ "can_execute": preview.get("can_execute"),
323
+ "mode": preview.get("mode"),
324
+ "broker_id": preview.get("broker_id"),
325
+ "broker_label": preview.get("broker_label"),
326
+ "execution_blockers": preview.get("execution_blockers") or [],
327
+ "confirm_command": f"/trade confirm {preview.get('preview_id')}",
328
+ }
329
+
330
+
331
+ def normalize_tradingview_alert_symbol(symbol: str) -> str:
332
+ """Convert common TradingView symbols back to Aria/yfinance-style symbols."""
333
+ raw = str(symbol or "").strip().upper()
334
+ if not raw:
335
+ return ""
336
+ if ":" in raw:
337
+ exchange, ticker = raw.split(":", 1)
338
+ ticker = ticker.strip()
339
+ if exchange in {"NASDAQ", "NYSE", "AMEX"}:
340
+ return ticker
341
+ if exchange == "HKEX":
342
+ return ticker.zfill(4) + ".HK"
343
+ if exchange == "SSE":
344
+ return ticker.zfill(6)
345
+ if exchange == "SZSE":
346
+ return ticker.zfill(6)
347
+ if exchange in {"BINANCE", "BYBIT", "OKX"} and ticker.endswith("USDT"):
348
+ return ticker[:-4] + "-USD"
349
+ if exchange in {"COMEX", "NYMEX", "CBOT"} and ticker.endswith("1!"):
350
+ reverse = {value: key for key, value in _FUTURES_SYMBOLS.items()}
351
+ return reverse.get(raw, ticker)
352
+ if exchange == "FX":
353
+ reverse = {value: key for key, value in _FX_SYMBOLS.items()}
354
+ return reverse.get(raw, ticker + "=X")
355
+ return ticker
356
+ if raw.endswith("USDT"):
357
+ return raw[:-4] + "-USD"
358
+ return raw
359
+
360
+
361
+ def enqueue_tradingview_alert(payload: dict[str, Any] | str, *, db_path: str | Path | None = None) -> dict[str, Any]:
362
+ """Queue a TradingView alert for the daemon webhook executor."""
363
+ alert = parse_tradingview_alert(payload)
364
+ if not alert["symbol"]:
365
+ return {"success": False, "error": "symbol is required", "alert": alert}
366
+ path = Path(db_path).expanduser() if db_path else Path.home() / ".aria" / "daemon.db"
367
+ path.parent.mkdir(parents=True, exist_ok=True)
368
+ job_id = "tv_" + uuid.uuid4().hex[:12]
369
+ with sqlite3.connect(path) as conn:
370
+ conn.execute(
371
+ """
372
+ CREATE TABLE IF NOT EXISTS webhook_jobs (
373
+ id TEXT PRIMARY KEY,
374
+ command TEXT NOT NULL,
375
+ payload TEXT DEFAULT '{}',
376
+ source TEXT DEFAULT 'external',
377
+ status TEXT DEFAULT 'pending',
378
+ result TEXT,
379
+ created_at TEXT DEFAULT (datetime('now')),
380
+ started_at TEXT,
381
+ done_at TEXT
382
+ )
383
+ """
384
+ )
385
+ conn.execute(
386
+ "INSERT INTO webhook_jobs(id, command, payload, source, status) VALUES (?, ?, ?, ?, 'pending')",
387
+ (job_id, "tradingview_alert", json.dumps(alert, ensure_ascii=False), "tradingview"),
388
+ )
389
+ conn.commit()
390
+ return {"success": True, "job_id": job_id, "alert": alert}
391
+
392
+
393
+ def generate_pine_strategy(symbol: str, *, name: str | None = None) -> str:
394
+ """Generate a TradingView Pine Script strategy template."""
395
+ sym = str(symbol or "SYMBOL").strip().upper()
396
+ title = name or f"Aria {sym} EMA RSI Strategy"
397
+ return f"""//@version=5
398
+ strategy("{title}", overlay=true, initial_capital=100000, commission_type=strategy.commission.percent, commission_value=0.05)
399
+
400
+ fastLen = input.int(20, "Fast EMA", minval=1)
401
+ slowLen = input.int(60, "Slow EMA", minval=1)
402
+ rsiLen = input.int(14, "RSI Length", minval=1)
403
+ rsiBuy = input.float(55, "RSI buy threshold")
404
+ rsiSell = input.float(45, "RSI sell threshold")
405
+
406
+ fast = ta.ema(close, fastLen)
407
+ slow = ta.ema(close, slowLen)
408
+ rsi = ta.rsi(close, rsiLen)
409
+
410
+ longCondition = ta.crossover(fast, slow) and rsi > rsiBuy
411
+ exitCondition = ta.crossunder(fast, slow) or rsi < rsiSell
412
+
413
+ if longCondition
414
+ strategy.entry("Aria Long", strategy.long)
415
+
416
+ if exitCondition
417
+ strategy.close("Aria Long")
418
+
419
+ plot(fast, "Fast EMA", color=color.teal)
420
+ plot(slow, "Slow EMA", color=color.orange)
421
+ alertcondition(longCondition, "Aria BUY {sym}", "{{\\"symbol\\":\\"{sym}\\",\\"action\\":\\"BUY\\",\\"quantity\\":1,\\"price\\":{{{{close}}}}}}")
422
+ alertcondition(exitCondition, "Aria EXIT {sym}", "{{\\"symbol\\":\\"{sym}\\",\\"action\\":\\"EXIT\\",\\"price\\":{{{{close}}}}}}")
423
+ """
424
+
425
+
426
+ def export_pine_strategy(symbol: str, *, name: str | None = None, output_dir: str | Path | None = None) -> Path:
427
+ """Write a Pine Script strategy file and return its path."""
428
+ sym = str(symbol or "SYMBOL").strip().upper()
429
+ directory = Path(output_dir).expanduser() if output_dir else user_generated_dir()
430
+ directory.mkdir(parents=True, exist_ok=True)
431
+ fname = f"{int(time.time())}_{slugify_topic(sym, 'symbol')}_strategy.pine"
432
+ path = directory / fname
433
+ path.write_text(generate_pine_strategy(sym, name=name), encoding="utf-8")
434
+ return path
@@ -0,0 +1,152 @@
1
+ """Background npm-registry version checker for Aria Code.
2
+
3
+ Checks registry.npmjs.org once per 24 hours in a daemon thread so startup is
4
+ never blocked. The result is cached to ~/.arthera/update_check.json and read
5
+ at banner render time.
6
+
7
+ Public API
8
+ ----------
9
+ start_update_check(current_version: str) -> None
10
+ Call once, early in startup. Spawns daemon thread; returns immediately.
11
+
12
+ get_update_notice() -> str | None
13
+ Call at banner render time. Returns a Rich-markup string if a newer
14
+ version is available, otherwise None. Thread-safe — safe to call
15
+ before the background thread finishes (returns cached result then).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import threading
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ _NPM_URL = "https://registry.npmjs.org/aria-code/latest"
26
+ _CACHE_FILE = Path.home() / ".arthera" / "update_check.json"
27
+ _CACHE_TTL_S = 86_400 # 24 hours
28
+ _FETCH_TIMEOUT = 4 # seconds — fail cleanly on slow networks
29
+
30
+ _notice: Optional[str] = None
31
+ _lock = threading.Lock()
32
+
33
+
34
+ # ── Version comparison ────────────────────────────────────────────────────────
35
+
36
+ def _parse(v: str) -> tuple[int, ...]:
37
+ """'4.1.2' → (4, 1, 2). Tolerates 'v' prefix and non-numeric suffixes."""
38
+ parts: list[int] = []
39
+ for seg in v.lstrip("v").split("."):
40
+ try:
41
+ parts.append(int(seg))
42
+ except ValueError:
43
+ break
44
+ return tuple(parts) or (0,)
45
+
46
+
47
+ def _newer(latest: str, current: str) -> bool:
48
+ return _parse(latest) > _parse(current)
49
+
50
+
51
+ # ── Cache helpers ─────────────────────────────────────────────────────────────
52
+
53
+ def _read_cache() -> dict:
54
+ try:
55
+ return json.loads(_CACHE_FILE.read_text())
56
+ except Exception:
57
+ return {}
58
+
59
+
60
+ def _write_cache(data: dict) -> None:
61
+ try:
62
+ _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
63
+ _CACHE_FILE.write_text(json.dumps(data))
64
+ except Exception:
65
+ pass
66
+
67
+
68
+ # ── Notice builder ────────────────────────────────────────────────────────────
69
+
70
+ def _build_notice(latest: str, current: str, lang: str) -> str:
71
+ cmd = "npm update -g @artheras/aria-code"
72
+ if lang == "zh":
73
+ return (
74
+ f"[yellow]⬆ 新版本可用[/yellow] "
75
+ f"[dim]v{current}[/dim] [dim]→[/dim] [bold]v{latest}[/bold]"
76
+ f" [dim]{cmd}[/dim]"
77
+ )
78
+ return (
79
+ f"[yellow]⬆ Update available[/yellow] "
80
+ f"[dim]v{current}[/dim] [dim]→[/dim] [bold]v{latest}[/bold]"
81
+ f" [dim]{cmd}[/dim]"
82
+ )
83
+
84
+
85
+ # ── Background worker ─────────────────────────────────────────────────────────
86
+
87
+ def _worker(current: str, lang: str) -> None:
88
+ global _notice
89
+
90
+ # 1. Serve from cache if still fresh
91
+ cache = _read_cache()
92
+ now = time.time()
93
+ if cache.get("checked_at", 0) + _CACHE_TTL_S > now:
94
+ latest = cache.get("latest", "")
95
+ if latest and _newer(latest, current):
96
+ with _lock:
97
+ _notice = _build_notice(latest, current, lang)
98
+ return
99
+
100
+ # 2. Fetch npm registry
101
+ try:
102
+ import urllib.request
103
+ req = urllib.request.Request(
104
+ _NPM_URL,
105
+ headers={"Accept": "application/json"},
106
+ )
107
+ with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT) as resp:
108
+ data = json.loads(resp.read())
109
+ latest = data["version"]
110
+ except Exception:
111
+ return # network error → silently skip, try again next day
112
+
113
+ # 3. Persist to cache
114
+ _write_cache({"checked_at": now, "latest": latest})
115
+
116
+ # 4. Set notice
117
+ if _newer(latest, current):
118
+ with _lock:
119
+ _notice = _build_notice(latest, current, lang)
120
+
121
+
122
+ # ── Public API ────────────────────────────────────────────────────────────────
123
+
124
+ def start_update_check(current_version: str, lang: str = "en") -> None:
125
+ """Start background version check. Call once, early in startup."""
126
+ t = threading.Thread(
127
+ target=_worker,
128
+ args=(current_version, lang),
129
+ daemon=True,
130
+ name="aria-update-check",
131
+ )
132
+ t.start()
133
+
134
+
135
+ def get_update_notice(wait_ms: int = 1200) -> Optional[str]:
136
+ """Return Rich-markup update notice, or None if up to date / not yet known.
137
+
138
+ Waits up to *wait_ms* ms for the background thread so the notice can appear
139
+ on the same run (not just next run). Startup already takes >1s so this
140
+ almost never adds real delay.
141
+ """
142
+ deadline = time.monotonic() + wait_ms / 1000
143
+ while time.monotonic() < deadline:
144
+ with _lock:
145
+ if _notice is not None:
146
+ return _notice
147
+ alive = any(t.name == "aria-update-check" for t in threading.enumerate())
148
+ if not alive:
149
+ break
150
+ time.sleep(0.05)
151
+ with _lock:
152
+ return _notice
File without changes