@heylemon/lemonade 0.2.1 → 0.2.3
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.
- package/dist/agents/system-prompt.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/skills/brave-search/SKILL.md +57 -0
- package/skills/brave-search/content.js +86 -0
- package/skills/brave-search/package.json +14 -0
- package/skills/brave-search/search.js +179 -0
- package/skills/caldav-calendar/SKILL.md +104 -0
- package/skills/frontend-design/SKILL.md +39 -0
- package/skills/image-generation/SKILL.md +23 -84
- package/skills/openai-image-gen/SKILL.md +1 -1
- package/skills/openai-image-gen/scripts/gen.py +18 -3
- package/skills/self-improving-agent/SKILL.md +128 -0
- package/skills/stock-analysis/SKILL.md +131 -0
- package/skills/stock-analysis/scripts/analyze_stock.py +2532 -0
- package/skills/stock-analysis/scripts/dividends.py +365 -0
- package/skills/stock-analysis/scripts/hot_scanner.py +565 -0
- package/skills/stock-analysis/scripts/portfolio.py +528 -0
- package/skills/stock-analysis/scripts/rumor_scanner.py +330 -0
- package/skills/stock-analysis/scripts/watchlist.py +318 -0
- package/skills/tavily-search/SKILL.md +38 -0
- package/skills/tavily-search/scripts/extract.mjs +59 -0
- package/skills/tavily-search/scripts/search.mjs +101 -0
- package/skills/youtube-watcher/SKILL.md +46 -0
- package/skills/youtube-watcher/scripts/get_transcript.py +81 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["yfinance>=0.2.40"]
|
|
5
|
+
# ///
|
|
6
|
+
"""
|
|
7
|
+
Portfolio management for stock-analysis skill.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
uv run portfolio.py create "Portfolio Name"
|
|
11
|
+
uv run portfolio.py list
|
|
12
|
+
uv run portfolio.py show [--portfolio NAME]
|
|
13
|
+
uv run portfolio.py delete "Portfolio Name"
|
|
14
|
+
uv run portfolio.py rename "Old Name" "New Name"
|
|
15
|
+
|
|
16
|
+
uv run portfolio.py add TICKER --quantity 100 --cost 150.00 [--portfolio NAME]
|
|
17
|
+
uv run portfolio.py update TICKER --quantity 150 [--portfolio NAME]
|
|
18
|
+
uv run portfolio.py remove TICKER [--portfolio NAME]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass, asdict
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Literal
|
|
29
|
+
|
|
30
|
+
import yfinance as yf
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Top 20 supported cryptocurrencies
|
|
34
|
+
SUPPORTED_CRYPTOS = {
|
|
35
|
+
"BTC-USD", "ETH-USD", "BNB-USD", "SOL-USD", "XRP-USD",
|
|
36
|
+
"ADA-USD", "DOGE-USD", "AVAX-USD", "DOT-USD", "MATIC-USD",
|
|
37
|
+
"LINK-USD", "ATOM-USD", "UNI-USD", "LTC-USD", "BCH-USD",
|
|
38
|
+
"XLM-USD", "ALGO-USD", "VET-USD", "FIL-USD", "NEAR-USD",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_storage_path() -> Path:
|
|
43
|
+
"""Get the portfolio storage path."""
|
|
44
|
+
state_dir = os.environ.get("CLAWDBOT_STATE_DIR", os.path.expanduser("~/.clawdbot"))
|
|
45
|
+
portfolio_dir = Path(state_dir) / "skills" / "stock-analysis"
|
|
46
|
+
portfolio_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
return portfolio_dir / "portfolios.json"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def detect_asset_type(ticker: str) -> Literal["stock", "crypto"]:
|
|
51
|
+
"""Detect asset type from ticker format."""
|
|
52
|
+
ticker_upper = ticker.upper()
|
|
53
|
+
if ticker_upper.endswith("-USD"):
|
|
54
|
+
base = ticker_upper[:-4]
|
|
55
|
+
if base.isalpha() and f"{base}-USD" in SUPPORTED_CRYPTOS:
|
|
56
|
+
return "crypto"
|
|
57
|
+
if base.isalpha():
|
|
58
|
+
return "crypto"
|
|
59
|
+
return "stock"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Asset:
|
|
64
|
+
ticker: str
|
|
65
|
+
type: Literal["stock", "crypto"]
|
|
66
|
+
quantity: float
|
|
67
|
+
cost_basis: float
|
|
68
|
+
added_at: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Portfolio:
|
|
73
|
+
name: str
|
|
74
|
+
created_at: str
|
|
75
|
+
updated_at: str
|
|
76
|
+
assets: list[Asset]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PortfolioStore:
|
|
80
|
+
"""Manages portfolio storage with atomic writes."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, path: Path | None = None):
|
|
83
|
+
self.path = path or get_storage_path()
|
|
84
|
+
self._data: dict | None = None
|
|
85
|
+
|
|
86
|
+
def _load(self) -> dict:
|
|
87
|
+
"""Load portfolios from disk."""
|
|
88
|
+
if self._data is not None:
|
|
89
|
+
return self._data
|
|
90
|
+
|
|
91
|
+
if not self.path.exists():
|
|
92
|
+
self._data = {"version": 1, "portfolios": {}}
|
|
93
|
+
return self._data
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(self.path, "r", encoding="utf-8") as f:
|
|
97
|
+
self._data = json.load(f)
|
|
98
|
+
return self._data
|
|
99
|
+
except (json.JSONDecodeError, IOError):
|
|
100
|
+
self._data = {"version": 1, "portfolios": {}}
|
|
101
|
+
return self._data
|
|
102
|
+
|
|
103
|
+
def _save(self) -> None:
|
|
104
|
+
"""Save portfolios to disk with atomic write."""
|
|
105
|
+
if self._data is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
tmp_path = self.path.with_suffix(".tmp")
|
|
111
|
+
try:
|
|
112
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
113
|
+
json.dump(self._data, f, indent=2)
|
|
114
|
+
tmp_path.replace(self.path)
|
|
115
|
+
except Exception:
|
|
116
|
+
if tmp_path.exists():
|
|
117
|
+
tmp_path.unlink()
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def _get_portfolio_key(self, name: str) -> str:
|
|
121
|
+
"""Convert portfolio name to storage key."""
|
|
122
|
+
return name.lower().replace(" ", "-")
|
|
123
|
+
|
|
124
|
+
def list_portfolios(self) -> list[str]:
|
|
125
|
+
"""List all portfolio names."""
|
|
126
|
+
data = self._load()
|
|
127
|
+
return [p["name"] for p in data["portfolios"].values()]
|
|
128
|
+
|
|
129
|
+
def get_portfolio(self, name: str) -> Portfolio | None:
|
|
130
|
+
"""Get a portfolio by name."""
|
|
131
|
+
data = self._load()
|
|
132
|
+
key = self._get_portfolio_key(name)
|
|
133
|
+
|
|
134
|
+
if key not in data["portfolios"]:
|
|
135
|
+
for k, v in data["portfolios"].items():
|
|
136
|
+
if v["name"].lower() == name.lower():
|
|
137
|
+
key = k
|
|
138
|
+
break
|
|
139
|
+
else:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
p = data["portfolios"][key]
|
|
143
|
+
assets = [
|
|
144
|
+
Asset(
|
|
145
|
+
ticker=a["ticker"],
|
|
146
|
+
type=a["type"],
|
|
147
|
+
quantity=a["quantity"],
|
|
148
|
+
cost_basis=a["cost_basis"],
|
|
149
|
+
added_at=a["added_at"],
|
|
150
|
+
)
|
|
151
|
+
for a in p.get("assets", [])
|
|
152
|
+
]
|
|
153
|
+
return Portfolio(
|
|
154
|
+
name=p["name"],
|
|
155
|
+
created_at=p["created_at"],
|
|
156
|
+
updated_at=p["updated_at"],
|
|
157
|
+
assets=assets,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def create_portfolio(self, name: str) -> Portfolio:
|
|
161
|
+
"""Create a new portfolio."""
|
|
162
|
+
data = self._load()
|
|
163
|
+
key = self._get_portfolio_key(name)
|
|
164
|
+
|
|
165
|
+
if key in data["portfolios"]:
|
|
166
|
+
raise ValueError(f"Portfolio '{name}' already exists")
|
|
167
|
+
|
|
168
|
+
now = datetime.now().isoformat()
|
|
169
|
+
portfolio = {
|
|
170
|
+
"name": name,
|
|
171
|
+
"created_at": now,
|
|
172
|
+
"updated_at": now,
|
|
173
|
+
"assets": [],
|
|
174
|
+
}
|
|
175
|
+
data["portfolios"][key] = portfolio
|
|
176
|
+
self._save()
|
|
177
|
+
|
|
178
|
+
return Portfolio(name=name, created_at=now, updated_at=now, assets=[])
|
|
179
|
+
|
|
180
|
+
def delete_portfolio(self, name: str) -> bool:
|
|
181
|
+
"""Delete a portfolio."""
|
|
182
|
+
data = self._load()
|
|
183
|
+
key = self._get_portfolio_key(name)
|
|
184
|
+
|
|
185
|
+
if key not in data["portfolios"]:
|
|
186
|
+
for k, v in data["portfolios"].items():
|
|
187
|
+
if v["name"].lower() == name.lower():
|
|
188
|
+
key = k
|
|
189
|
+
break
|
|
190
|
+
else:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
del data["portfolios"][key]
|
|
194
|
+
self._save()
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
def rename_portfolio(self, old_name: str, new_name: str) -> bool:
|
|
198
|
+
"""Rename a portfolio."""
|
|
199
|
+
data = self._load()
|
|
200
|
+
old_key = self._get_portfolio_key(old_name)
|
|
201
|
+
new_key = self._get_portfolio_key(new_name)
|
|
202
|
+
|
|
203
|
+
if old_key not in data["portfolios"]:
|
|
204
|
+
for k, v in data["portfolios"].items():
|
|
205
|
+
if v["name"].lower() == old_name.lower():
|
|
206
|
+
old_key = k
|
|
207
|
+
break
|
|
208
|
+
else:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
if new_key in data["portfolios"] and new_key != old_key:
|
|
212
|
+
raise ValueError(f"Portfolio '{new_name}' already exists")
|
|
213
|
+
|
|
214
|
+
portfolio = data["portfolios"].pop(old_key)
|
|
215
|
+
portfolio["name"] = new_name
|
|
216
|
+
portfolio["updated_at"] = datetime.now().isoformat()
|
|
217
|
+
data["portfolios"][new_key] = portfolio
|
|
218
|
+
self._save()
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
def add_asset(
|
|
222
|
+
self,
|
|
223
|
+
portfolio_name: str,
|
|
224
|
+
ticker: str,
|
|
225
|
+
quantity: float,
|
|
226
|
+
cost_basis: float,
|
|
227
|
+
) -> Asset:
|
|
228
|
+
"""Add an asset to a portfolio."""
|
|
229
|
+
data = self._load()
|
|
230
|
+
key = self._get_portfolio_key(portfolio_name)
|
|
231
|
+
|
|
232
|
+
if key not in data["portfolios"]:
|
|
233
|
+
for k, v in data["portfolios"].items():
|
|
234
|
+
if v["name"].lower() == portfolio_name.lower():
|
|
235
|
+
key = k
|
|
236
|
+
break
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(f"Portfolio '{portfolio_name}' not found")
|
|
239
|
+
|
|
240
|
+
portfolio = data["portfolios"][key]
|
|
241
|
+
ticker = ticker.upper()
|
|
242
|
+
|
|
243
|
+
for asset in portfolio["assets"]:
|
|
244
|
+
if asset["ticker"] == ticker:
|
|
245
|
+
raise ValueError(f"Asset '{ticker}' already in portfolio. Use 'update' to modify.")
|
|
246
|
+
|
|
247
|
+
asset_type = detect_asset_type(ticker)
|
|
248
|
+
try:
|
|
249
|
+
stock = yf.Ticker(ticker)
|
|
250
|
+
info = stock.info
|
|
251
|
+
if "regularMarketPrice" not in info:
|
|
252
|
+
raise ValueError(f"Invalid ticker: {ticker}")
|
|
253
|
+
except Exception as e:
|
|
254
|
+
raise ValueError(f"Could not validate ticker '{ticker}': {e}")
|
|
255
|
+
|
|
256
|
+
now = datetime.now().isoformat()
|
|
257
|
+
asset = {
|
|
258
|
+
"ticker": ticker,
|
|
259
|
+
"type": asset_type,
|
|
260
|
+
"quantity": quantity,
|
|
261
|
+
"cost_basis": cost_basis,
|
|
262
|
+
"added_at": now,
|
|
263
|
+
}
|
|
264
|
+
portfolio["assets"].append(asset)
|
|
265
|
+
portfolio["updated_at"] = now
|
|
266
|
+
self._save()
|
|
267
|
+
|
|
268
|
+
return Asset(**asset)
|
|
269
|
+
|
|
270
|
+
def update_asset(
|
|
271
|
+
self,
|
|
272
|
+
portfolio_name: str,
|
|
273
|
+
ticker: str,
|
|
274
|
+
quantity: float | None = None,
|
|
275
|
+
cost_basis: float | None = None,
|
|
276
|
+
) -> Asset | None:
|
|
277
|
+
"""Update an asset in a portfolio."""
|
|
278
|
+
data = self._load()
|
|
279
|
+
key = self._get_portfolio_key(portfolio_name)
|
|
280
|
+
|
|
281
|
+
if key not in data["portfolios"]:
|
|
282
|
+
for k, v in data["portfolios"].items():
|
|
283
|
+
if v["name"].lower() == portfolio_name.lower():
|
|
284
|
+
key = k
|
|
285
|
+
break
|
|
286
|
+
else:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
portfolio = data["portfolios"][key]
|
|
290
|
+
ticker = ticker.upper()
|
|
291
|
+
|
|
292
|
+
for asset in portfolio["assets"]:
|
|
293
|
+
if asset["ticker"] == ticker:
|
|
294
|
+
if quantity is not None:
|
|
295
|
+
asset["quantity"] = quantity
|
|
296
|
+
if cost_basis is not None:
|
|
297
|
+
asset["cost_basis"] = cost_basis
|
|
298
|
+
portfolio["updated_at"] = datetime.now().isoformat()
|
|
299
|
+
self._save()
|
|
300
|
+
return Asset(**asset)
|
|
301
|
+
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def remove_asset(self, portfolio_name: str, ticker: str) -> bool:
|
|
305
|
+
"""Remove an asset from a portfolio."""
|
|
306
|
+
data = self._load()
|
|
307
|
+
key = self._get_portfolio_key(portfolio_name)
|
|
308
|
+
|
|
309
|
+
if key not in data["portfolios"]:
|
|
310
|
+
for k, v in data["portfolios"].items():
|
|
311
|
+
if v["name"].lower() == portfolio_name.lower():
|
|
312
|
+
key = k
|
|
313
|
+
break
|
|
314
|
+
else:
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
portfolio = data["portfolios"][key]
|
|
318
|
+
ticker = ticker.upper()
|
|
319
|
+
|
|
320
|
+
original_len = len(portfolio["assets"])
|
|
321
|
+
portfolio["assets"] = [a for a in portfolio["assets"] if a["ticker"] != ticker]
|
|
322
|
+
|
|
323
|
+
if len(portfolio["assets"]) < original_len:
|
|
324
|
+
portfolio["updated_at"] = datetime.now().isoformat()
|
|
325
|
+
self._save()
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
def get_default_portfolio_name(self) -> str | None:
|
|
331
|
+
"""Get the default (first) portfolio name, or None if empty."""
|
|
332
|
+
portfolios = self.list_portfolios()
|
|
333
|
+
return portfolios[0] if portfolios else None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def format_currency(value: float) -> str:
|
|
337
|
+
"""Format a value as currency."""
|
|
338
|
+
if abs(value) >= 1_000_000:
|
|
339
|
+
return f"${value/1_000_000:.2f}M"
|
|
340
|
+
elif abs(value) >= 1_000:
|
|
341
|
+
return f"${value/1_000:.2f}K"
|
|
342
|
+
else:
|
|
343
|
+
return f"${value:.2f}"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def show_portfolio(portfolio: Portfolio, verbose: bool = False) -> None:
|
|
347
|
+
"""Display portfolio details with current prices."""
|
|
348
|
+
print(f"\n{'='*60}")
|
|
349
|
+
print(f"PORTFOLIO: {portfolio.name}")
|
|
350
|
+
print(f"Created: {portfolio.created_at[:10]} | Updated: {portfolio.updated_at[:10]}")
|
|
351
|
+
print(f"{'='*60}\n")
|
|
352
|
+
|
|
353
|
+
if not portfolio.assets:
|
|
354
|
+
print(" No assets in portfolio. Use 'add' to add assets.\n")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
total_cost = 0.0
|
|
358
|
+
total_value = 0.0
|
|
359
|
+
|
|
360
|
+
print(f"{'Ticker':<12} {'Type':<8} {'Qty':>10} {'Cost':>12} {'Current':>12} {'Value':>14} {'P&L':>12}")
|
|
361
|
+
print("-" * 82)
|
|
362
|
+
|
|
363
|
+
for asset in portfolio.assets:
|
|
364
|
+
try:
|
|
365
|
+
stock = yf.Ticker(asset.ticker)
|
|
366
|
+
current_price = stock.info.get("regularMarketPrice", 0) or 0
|
|
367
|
+
except Exception:
|
|
368
|
+
current_price = 0
|
|
369
|
+
|
|
370
|
+
cost_total = asset.quantity * asset.cost_basis
|
|
371
|
+
current_value = asset.quantity * current_price
|
|
372
|
+
pnl = current_value - cost_total
|
|
373
|
+
pnl_pct = (pnl / cost_total * 100) if cost_total > 0 else 0
|
|
374
|
+
|
|
375
|
+
total_cost += cost_total
|
|
376
|
+
total_value += current_value
|
|
377
|
+
|
|
378
|
+
pnl_str = f"{'+' if pnl >= 0 else ''}{format_currency(pnl)} ({pnl_pct:+.1f}%)"
|
|
379
|
+
|
|
380
|
+
print(f"{asset.ticker:<12} {asset.type:<8} {asset.quantity:>10.4f} "
|
|
381
|
+
f"{format_currency(asset.cost_basis):>12} {format_currency(current_price):>12} "
|
|
382
|
+
f"{format_currency(current_value):>14} {pnl_str:>12}")
|
|
383
|
+
|
|
384
|
+
print("-" * 82)
|
|
385
|
+
total_pnl = total_value - total_cost
|
|
386
|
+
total_pnl_pct = (total_pnl / total_cost * 100) if total_cost > 0 else 0
|
|
387
|
+
print(f"{'TOTAL':<12} {'':<8} {'':<10} {format_currency(total_cost):>12} {'':<12} "
|
|
388
|
+
f"{format_currency(total_value):>14} {'+' if total_pnl >= 0 else ''}{format_currency(total_pnl)} ({total_pnl_pct:+.1f}%)")
|
|
389
|
+
print()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def main():
|
|
393
|
+
parser = argparse.ArgumentParser(description="Portfolio management for stock-analysis")
|
|
394
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
395
|
+
|
|
396
|
+
create_parser = subparsers.add_parser("create", help="Create a new portfolio")
|
|
397
|
+
create_parser.add_argument("name", help="Portfolio name")
|
|
398
|
+
|
|
399
|
+
subparsers.add_parser("list", help="List all portfolios")
|
|
400
|
+
|
|
401
|
+
show_parser = subparsers.add_parser("show", help="Show portfolio details")
|
|
402
|
+
show_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
|
|
403
|
+
|
|
404
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a portfolio")
|
|
405
|
+
delete_parser.add_argument("name", help="Portfolio name")
|
|
406
|
+
|
|
407
|
+
rename_parser = subparsers.add_parser("rename", help="Rename a portfolio")
|
|
408
|
+
rename_parser.add_argument("old_name", help="Current portfolio name")
|
|
409
|
+
rename_parser.add_argument("new_name", help="New portfolio name")
|
|
410
|
+
|
|
411
|
+
add_parser = subparsers.add_parser("add", help="Add an asset to portfolio")
|
|
412
|
+
add_parser.add_argument("ticker", help="Stock/crypto ticker (e.g., AAPL, BTC-USD)")
|
|
413
|
+
add_parser.add_argument("--quantity", "-q", type=float, required=True, help="Quantity")
|
|
414
|
+
add_parser.add_argument("--cost", "-c", type=float, required=True, help="Cost basis per unit")
|
|
415
|
+
add_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
|
|
416
|
+
|
|
417
|
+
update_parser = subparsers.add_parser("update", help="Update an asset in portfolio")
|
|
418
|
+
update_parser.add_argument("ticker", help="Stock/crypto ticker")
|
|
419
|
+
update_parser.add_argument("--quantity", "-q", type=float, help="New quantity")
|
|
420
|
+
update_parser.add_argument("--cost", "-c", type=float, help="New cost basis per unit")
|
|
421
|
+
update_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
|
|
422
|
+
|
|
423
|
+
remove_parser = subparsers.add_parser("remove", help="Remove an asset from portfolio")
|
|
424
|
+
remove_parser.add_argument("ticker", help="Stock/crypto ticker")
|
|
425
|
+
remove_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
|
|
426
|
+
|
|
427
|
+
args = parser.parse_args()
|
|
428
|
+
|
|
429
|
+
if not args.command:
|
|
430
|
+
parser.print_help()
|
|
431
|
+
sys.exit(1)
|
|
432
|
+
|
|
433
|
+
store = PortfolioStore()
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
if args.command == "create":
|
|
437
|
+
portfolio = store.create_portfolio(args.name)
|
|
438
|
+
print(f"Created portfolio: {portfolio.name}")
|
|
439
|
+
|
|
440
|
+
elif args.command == "list":
|
|
441
|
+
portfolios = store.list_portfolios()
|
|
442
|
+
if not portfolios:
|
|
443
|
+
print("No portfolios found. Use 'create' to create one.")
|
|
444
|
+
else:
|
|
445
|
+
print("\nPortfolios:")
|
|
446
|
+
for name in portfolios:
|
|
447
|
+
p = store.get_portfolio(name)
|
|
448
|
+
asset_count = len(p.assets) if p else 0
|
|
449
|
+
print(f" - {name} ({asset_count} assets)")
|
|
450
|
+
print()
|
|
451
|
+
|
|
452
|
+
elif args.command == "show":
|
|
453
|
+
portfolio_name = args.portfolio or store.get_default_portfolio_name()
|
|
454
|
+
if not portfolio_name:
|
|
455
|
+
print("No portfolios found. Use 'create' to create one.")
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
|
|
458
|
+
portfolio = store.get_portfolio(portfolio_name)
|
|
459
|
+
if not portfolio:
|
|
460
|
+
print(f"Portfolio '{portfolio_name}' not found.")
|
|
461
|
+
sys.exit(1)
|
|
462
|
+
|
|
463
|
+
show_portfolio(portfolio)
|
|
464
|
+
|
|
465
|
+
elif args.command == "delete":
|
|
466
|
+
if store.delete_portfolio(args.name):
|
|
467
|
+
print(f"Deleted portfolio: {args.name}")
|
|
468
|
+
else:
|
|
469
|
+
print(f"Portfolio '{args.name}' not found.")
|
|
470
|
+
sys.exit(1)
|
|
471
|
+
|
|
472
|
+
elif args.command == "rename":
|
|
473
|
+
if store.rename_portfolio(args.old_name, args.new_name):
|
|
474
|
+
print(f"Renamed portfolio: {args.old_name} -> {args.new_name}")
|
|
475
|
+
else:
|
|
476
|
+
print(f"Portfolio '{args.old_name}' not found.")
|
|
477
|
+
sys.exit(1)
|
|
478
|
+
|
|
479
|
+
elif args.command == "add":
|
|
480
|
+
portfolio_name = args.portfolio or store.get_default_portfolio_name()
|
|
481
|
+
if not portfolio_name:
|
|
482
|
+
print("No portfolios found. Use 'create' to create one first.")
|
|
483
|
+
sys.exit(1)
|
|
484
|
+
|
|
485
|
+
asset = store.add_asset(portfolio_name, args.ticker, args.quantity, args.cost)
|
|
486
|
+
print(f"Added {asset.ticker} ({asset.type}) to {portfolio_name}: "
|
|
487
|
+
f"{asset.quantity} units @ {format_currency(asset.cost_basis)}")
|
|
488
|
+
|
|
489
|
+
elif args.command == "update":
|
|
490
|
+
portfolio_name = args.portfolio or store.get_default_portfolio_name()
|
|
491
|
+
if not portfolio_name:
|
|
492
|
+
print("No portfolios found.")
|
|
493
|
+
sys.exit(1)
|
|
494
|
+
|
|
495
|
+
if args.quantity is None and args.cost is None:
|
|
496
|
+
print("Must specify --quantity and/or --cost to update.")
|
|
497
|
+
sys.exit(1)
|
|
498
|
+
|
|
499
|
+
asset = store.update_asset(portfolio_name, args.ticker, args.quantity, args.cost)
|
|
500
|
+
if asset:
|
|
501
|
+
print(f"Updated {asset.ticker} in {portfolio_name}: "
|
|
502
|
+
f"{asset.quantity} units @ {format_currency(asset.cost_basis)}")
|
|
503
|
+
else:
|
|
504
|
+
print(f"Asset '{args.ticker}' not found in portfolio '{portfolio_name}'.")
|
|
505
|
+
sys.exit(1)
|
|
506
|
+
|
|
507
|
+
elif args.command == "remove":
|
|
508
|
+
portfolio_name = args.portfolio or store.get_default_portfolio_name()
|
|
509
|
+
if not portfolio_name:
|
|
510
|
+
print("No portfolios found.")
|
|
511
|
+
sys.exit(1)
|
|
512
|
+
|
|
513
|
+
if store.remove_asset(portfolio_name, args.ticker):
|
|
514
|
+
print(f"Removed {args.ticker.upper()} from {portfolio_name}")
|
|
515
|
+
else:
|
|
516
|
+
print(f"Asset '{args.ticker}' not found in portfolio '{portfolio_name}'.")
|
|
517
|
+
sys.exit(1)
|
|
518
|
+
|
|
519
|
+
except ValueError as e:
|
|
520
|
+
print(f"Error: {e}")
|
|
521
|
+
sys.exit(1)
|
|
522
|
+
except Exception as e:
|
|
523
|
+
print(f"Unexpected error: {e}")
|
|
524
|
+
sys.exit(1)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == "__main__":
|
|
528
|
+
main()
|