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
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
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
|