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
portfolio_ledger.py ADDED
@@ -0,0 +1,262 @@
1
+ """
2
+ portfolio_ledger.py — 本地持仓账本(SQLite)
3
+ ==============================================
4
+ 记录买卖交易 → 自动计算持仓成本、未实现盈亏、已实现盈亏。
5
+ 存储路径:~/.arthera/portfolio.db
6
+
7
+ 公开 API:
8
+ PortfolioLedger.add_trade(symbol, side, qty, price, date, reason, fee)
9
+ PortfolioLedger.get_positions() → List[Dict]
10
+ PortfolioLedger.get_trades(symbol, limit) → List[Dict]
11
+ PortfolioLedger.get_pnl_with_prices(prices_dict) → List[Dict]
12
+ PortfolioLedger.get_realized_pnl() → List[Dict]
13
+ PortfolioLedger.export_csv(path) → Path
14
+ PortfolioLedger.delete_trade(id) → bool
15
+ PortfolioLedger.trade_count() → int
16
+ PortfolioLedger.position_count() → int
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import csv
22
+ import logging
23
+ import sqlite3
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Dict, List, Optional, Tuple
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _DB_PATH = Path.home() / ".arthera" / "portfolio.db"
31
+
32
+ _SCHEMA = """
33
+ CREATE TABLE IF NOT EXISTS trades (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ symbol TEXT NOT NULL,
36
+ side TEXT NOT NULL CHECK(side IN ('BUY','SELL')),
37
+ qty REAL NOT NULL CHECK(qty > 0),
38
+ price REAL NOT NULL CHECK(price > 0),
39
+ amount REAL NOT NULL,
40
+ fee REAL NOT NULL DEFAULT 0,
41
+ date TEXT NOT NULL,
42
+ reason TEXT NOT NULL DEFAULT '',
43
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_trades_symbol ON trades(symbol);
46
+ CREATE INDEX IF NOT EXISTS idx_trades_date ON trades(date);
47
+ """
48
+
49
+
50
+ class PortfolioLedger:
51
+
52
+ def __init__(self, db_path: Optional[Path] = None):
53
+ self.db_path = db_path or _DB_PATH
54
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
55
+ self._ensure_schema()
56
+
57
+ def _conn(self) -> sqlite3.Connection:
58
+ conn = sqlite3.connect(str(self.db_path))
59
+ conn.row_factory = sqlite3.Row
60
+ return conn
61
+
62
+ def _ensure_schema(self) -> None:
63
+ with self._conn() as conn:
64
+ conn.executescript(_SCHEMA)
65
+
66
+ # ── Write ─────────────────────────────────────────────────────────────────
67
+
68
+ def add_trade(
69
+ self,
70
+ symbol: str,
71
+ side: str,
72
+ qty: float,
73
+ price: float,
74
+ date: Optional[str] = None,
75
+ reason: str = "",
76
+ fee: float = 0.0,
77
+ ) -> int:
78
+ """
79
+ Record a trade. Returns the new row id.
80
+ side: "BUY" or "SELL"
81
+ date: "YYYY-MM-DD" (defaults to today)
82
+ """
83
+ symbol = symbol.upper().strip()
84
+ side = side.upper().strip()
85
+ if side not in ("BUY", "SELL"):
86
+ raise ValueError(f"side 必须是 BUY 或 SELL,收到: {side!r}")
87
+ qty = float(qty)
88
+ price = float(price)
89
+ if qty <= 0:
90
+ raise ValueError("qty 必须大于 0")
91
+ if price <= 0:
92
+ raise ValueError("price 必须大于 0")
93
+
94
+ date = (date or datetime.now().strftime("%Y-%m-%d")).strip()
95
+ amount = round(qty * price, 6)
96
+
97
+ with self._conn() as conn:
98
+ cur = conn.execute(
99
+ "INSERT INTO trades (symbol, side, qty, price, amount, fee, date, reason) "
100
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
101
+ (symbol, side, qty, price, amount, fee, date, reason),
102
+ )
103
+ row_id = cur.lastrowid
104
+ logger.debug("Ledger: added trade #%s %s %s %.4f @ %.4f", row_id, side, symbol, qty, price)
105
+ return row_id
106
+
107
+ def delete_trade(self, trade_id: int) -> bool:
108
+ with self._conn() as conn:
109
+ cur = conn.execute("DELETE FROM trades WHERE id = ?", (trade_id,))
110
+ return cur.rowcount > 0
111
+
112
+ # ── Read: Trades ──────────────────────────────────────────────────────────
113
+
114
+ def get_trades(
115
+ self,
116
+ symbol: Optional[str] = None,
117
+ limit: int = 50,
118
+ ) -> List[Dict]:
119
+ sql = "SELECT * FROM trades"
120
+ params: list = []
121
+ if symbol:
122
+ sql += " WHERE symbol = ?"
123
+ params.append(symbol.upper())
124
+ sql += " ORDER BY date DESC, id DESC LIMIT ?"
125
+ params.append(limit)
126
+
127
+ with self._conn() as conn:
128
+ rows = conn.execute(sql, params).fetchall()
129
+ return [dict(r) for r in rows]
130
+
131
+ # ── Read: Positions ───────────────────────────────────────────────────────
132
+
133
+ def get_positions(self) -> List[Dict]:
134
+ """
135
+ Current open positions. Average cost = total BUY amount / total BUY qty.
136
+ Returns only positions with net_qty > 0.
137
+ """
138
+ with self._conn() as conn:
139
+ rows = conn.execute("""
140
+ SELECT
141
+ symbol,
142
+ SUM(CASE WHEN side='BUY' THEN qty ELSE -qty END) AS net_qty,
143
+ SUM(CASE WHEN side='BUY' THEN amount ELSE 0 END) AS total_buy_amt,
144
+ SUM(CASE WHEN side='BUY' THEN qty ELSE 0 END) AS total_buy_qty,
145
+ SUM(CASE WHEN side='SELL' THEN amount ELSE 0 END) AS total_sell_amt,
146
+ MIN(date) AS first_trade_date,
147
+ MAX(date) AS last_trade_date
148
+ FROM trades
149
+ GROUP BY symbol
150
+ HAVING net_qty > 0.0001
151
+ ORDER BY symbol
152
+ """).fetchall()
153
+
154
+ positions = []
155
+ for row in rows:
156
+ r = dict(row)
157
+ buy_qty = r["total_buy_qty"] or 0
158
+ buy_amt = r["total_buy_amt"] or 0
159
+ net_qty = r["net_qty"]
160
+ avg_cost = buy_amt / buy_qty if buy_qty > 0 else 0
161
+ positions.append({
162
+ "symbol": r["symbol"],
163
+ "net_qty": round(net_qty, 4),
164
+ "avg_cost": round(avg_cost, 4),
165
+ "cost_basis": round(net_qty * avg_cost, 2),
166
+ "first_trade": r["first_trade_date"],
167
+ "last_trade": r["last_trade_date"],
168
+ })
169
+ return positions
170
+
171
+ def get_pnl_with_prices(self, current_prices: Dict[str, float]) -> List[Dict]:
172
+ """Attach live prices to positions and compute unrealized P&L."""
173
+ positions = self.get_positions()
174
+ result = []
175
+ for pos in positions:
176
+ sym = pos["symbol"]
177
+ price = current_prices.get(sym) or current_prices.get(sym.lower())
178
+ if price:
179
+ diff = price - pos["avg_cost"]
180
+ unreal = round(diff * pos["net_qty"], 2)
181
+ pct = round(diff / pos["avg_cost"] * 100, 2) if pos["avg_cost"] else 0
182
+ pos.update({
183
+ "current_price": price,
184
+ "market_value": round(price * pos["net_qty"], 2),
185
+ "unrealized_pnl": unreal,
186
+ "unrealized_pct": pct,
187
+ })
188
+ result.append(pos)
189
+ return result
190
+
191
+ def get_realized_pnl(self) -> List[Dict]:
192
+ """FIFO realized P&L per symbol (all closed lots)."""
193
+ with self._conn() as conn:
194
+ symbols = [r[0] for r in conn.execute(
195
+ "SELECT DISTINCT symbol FROM trades ORDER BY symbol"
196
+ ).fetchall()]
197
+
198
+ realized = []
199
+ for sym in symbols:
200
+ with self._conn() as conn:
201
+ trades = [dict(r) for r in conn.execute(
202
+ "SELECT * FROM trades WHERE symbol=? ORDER BY date, id", (sym,)
203
+ ).fetchall()]
204
+
205
+ buy_queue: List[Tuple[float, float]] = [] # (qty, price)
206
+ total_pnl = 0.0
207
+ total_sold = 0.0
208
+
209
+ for t in trades:
210
+ if t["side"] == "BUY":
211
+ buy_queue.append((t["qty"], t["price"]))
212
+ else:
213
+ remaining = t["qty"]
214
+ while remaining > 0.0001 and buy_queue:
215
+ bq, bp = buy_queue[0]
216
+ matched = min(bq, remaining)
217
+ total_pnl += matched * (t["price"] - bp)
218
+ total_sold += matched * t["price"]
219
+ remaining -= matched
220
+ if matched >= bq - 0.0001:
221
+ buy_queue.pop(0)
222
+ else:
223
+ buy_queue[0] = (bq - matched, bp)
224
+
225
+ open_lots = sum(q for q, _ in buy_queue)
226
+ realized.append({
227
+ "symbol": sym,
228
+ "realized_pnl": round(total_pnl, 2),
229
+ "open_lots": round(open_lots, 4),
230
+ "has_open": open_lots > 0.0001,
231
+ })
232
+ return realized
233
+
234
+ # ── Export ────────────────────────────────────────────────────────────────
235
+
236
+ def export_csv(self, path: Optional[Path] = None) -> Path:
237
+ out = path or (Path.home() / "Desktop" / f"trades_{datetime.now():%Y%m%d_%H%M}.csv")
238
+ trades = self.get_trades(limit=100_000)
239
+ if not trades:
240
+ out.write_text("no trades\n", encoding="utf-8")
241
+ return out
242
+ with open(out, "w", newline="", encoding="utf-8") as f:
243
+ writer = csv.DictWriter(f, fieldnames=trades[0].keys())
244
+ writer.writeheader()
245
+ writer.writerows(trades)
246
+ return out
247
+
248
+ # ── Stats ─────────────────────────────────────────────────────────────────
249
+
250
+ def trade_count(self) -> int:
251
+ with self._conn() as conn:
252
+ return conn.execute("SELECT COUNT(*) FROM trades").fetchone()[0]
253
+
254
+ def position_count(self) -> int:
255
+ return len(self.get_positions())
256
+
257
+ def summary(self) -> Dict:
258
+ return {
259
+ "trade_count": self.trade_count(),
260
+ "position_count": self.position_count(),
261
+ "db_path": str(self.db_path),
262
+ }
privacy/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Privacy and feedback helpers for Aria Code."""
2
+
3
+ from .feedback import FeedbackRecord, FeedbackStore, PrivacySettings
4
+
5
+ __all__ = ["FeedbackRecord", "FeedbackStore", "PrivacySettings"]
privacy/feedback.py ADDED
@@ -0,0 +1,123 @@
1
+ """Local-first feedback storage and privacy settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Iterable, List
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PrivacySettings:
15
+ """User-controlled data sharing settings."""
16
+
17
+ data_sharing: bool = False
18
+ feedback_upload: bool = False
19
+
20
+ @classmethod
21
+ def from_config(cls, config: Dict[str, Any]) -> "PrivacySettings":
22
+ return cls(
23
+ data_sharing=bool(config.get("data_sharing", False)),
24
+ feedback_upload=bool(config.get("feedback_upload", False)),
25
+ )
26
+
27
+ def apply_to_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
28
+ config["data_sharing"] = self.data_sharing
29
+ config["feedback_upload"] = self.feedback_upload
30
+ return config
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class FeedbackRecord:
35
+ """One local feedback event for a model response."""
36
+
37
+ rating: str
38
+ message: str
39
+ comment: str = ""
40
+ model: str = ""
41
+ session_id: str = ""
42
+ timestamp: str = ""
43
+ message_index: int | None = None
44
+ shared: bool = False
45
+
46
+ @classmethod
47
+ def create(
48
+ cls,
49
+ *,
50
+ rating: str,
51
+ message: str,
52
+ comment: str = "",
53
+ model: str = "",
54
+ session_id: str = "",
55
+ message_index: int | None = None,
56
+ shared: bool = False,
57
+ ) -> "FeedbackRecord":
58
+ return cls(
59
+ rating=rating,
60
+ message=message,
61
+ comment=comment,
62
+ model=model,
63
+ session_id=session_id,
64
+ timestamp=datetime.now().isoformat(),
65
+ message_index=message_index,
66
+ shared=shared,
67
+ )
68
+
69
+ def to_json(self) -> str:
70
+ return json.dumps(asdict(self), ensure_ascii=False)
71
+
72
+
73
+ class FeedbackStore:
74
+ """Append-only local feedback store with explicit export/delete actions."""
75
+
76
+ def __init__(self, config_dir: str | Path) -> None:
77
+ self.config_dir = Path(config_dir).expanduser()
78
+ self.feedback_dir = self.config_dir / "feedback"
79
+ self.feedback_file = self.feedback_dir / "feedback.jsonl"
80
+ self.export_dir = self.feedback_dir / "exports"
81
+
82
+ def append(self, record: FeedbackRecord) -> Path:
83
+ self.feedback_dir.mkdir(parents=True, exist_ok=True)
84
+ with self.feedback_file.open("a", encoding="utf-8") as handle:
85
+ handle.write(record.to_json() + "\n")
86
+ return self.feedback_file
87
+
88
+ def iter_records(self) -> Iterable[Dict[str, Any]]:
89
+ if not self.feedback_file.exists():
90
+ return []
91
+ rows: List[Dict[str, Any]] = []
92
+ with self.feedback_file.open("r", encoding="utf-8") as handle:
93
+ for line in handle:
94
+ line = line.strip()
95
+ if not line:
96
+ continue
97
+ try:
98
+ rows.append(json.loads(line))
99
+ except json.JSONDecodeError:
100
+ continue
101
+ return rows
102
+
103
+ def count(self) -> int:
104
+ return sum(1 for _ in self.iter_records())
105
+
106
+ def export_jsonl(self, destination: str | Path | None = None) -> Path:
107
+ self.export_dir.mkdir(parents=True, exist_ok=True)
108
+ if destination is None:
109
+ stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
110
+ destination = self.export_dir / f"feedback_export_{stamp}.jsonl"
111
+ dest = Path(destination).expanduser()
112
+ dest.parent.mkdir(parents=True, exist_ok=True)
113
+ if self.feedback_file.exists():
114
+ shutil.copyfile(self.feedback_file, dest)
115
+ else:
116
+ dest.write_text("", encoding="utf-8")
117
+ return dest
118
+
119
+ def delete_all(self) -> int:
120
+ count = self.count()
121
+ if self.feedback_file.exists():
122
+ self.feedback_file.unlink()
123
+ return count