Files

336 lines
11 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
stocks.py - Unified stock management for holdings and watchlist.
Single source of truth for:
- Holdings (stocks you own)
- Watchlist (stocks you're watching to buy)
Usage:
from stocks import load_stocks, save_stocks, get_holdings, get_watchlist
from stocks import add_to_watchlist, add_to_holdings, move_to_holdings
CLI:
stocks.py list [--holdings|--watchlist]
stocks.py add-watchlist TICKER [--target 380] [--notes "Buy zone"]
stocks.py add-holding TICKER --name "Company" [--category "Tech"]
stocks.py move TICKER # watchlist → holdings (you bought it)
stocks.py remove TICKER [--from holdings|watchlist]
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
# Default path - can be overridden
STOCKS_FILE = Path(__file__).parent.parent / "config" / "stocks.json"
def load_stocks(path: Optional[Path] = None) -> dict:
"""Load the unified stocks file."""
path = path or STOCKS_FILE
if not path.exists():
return {
"version": "1.0",
"updated": datetime.now().strftime("%Y-%m-%d"),
"holdings": [],
"watchlist": [],
"alert_definitions": {}
}
with open(path, 'r') as f:
return json.load(f)
def save_stocks(data: dict, path: Optional[Path] = None):
"""Save the unified stocks file."""
path = path or STOCKS_FILE
data["updated"] = datetime.now().strftime("%Y-%m-%d")
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def get_holdings(data: Optional[dict] = None) -> list:
"""Get list of holdings."""
if data is None:
data = load_stocks()
return data.get("holdings", [])
def get_watchlist(data: Optional[dict] = None) -> list:
"""Get list of watchlist items."""
if data is None:
data = load_stocks()
return data.get("watchlist", [])
def get_holding_tickers(data: Optional[dict] = None) -> set:
"""Get set of holding tickers for quick lookup."""
holdings = get_holdings(data)
return {h.get("ticker") for h in holdings}
def get_watchlist_tickers(data: Optional[dict] = None) -> set:
"""Get set of watchlist tickers for quick lookup."""
watchlist = get_watchlist(data)
return {w.get("ticker") for w in watchlist}
def add_to_watchlist(
ticker: str,
target: Optional[float] = None,
stop: Optional[float] = None,
notes: str = "",
alerts: Optional[list] = None
) -> bool:
"""Add a stock to the watchlist."""
data = load_stocks()
# Check if already in watchlist
for w in data["watchlist"]:
if w.get("ticker") == ticker:
# Update existing
if target is not None:
w["target"] = target
if stop is not None:
w["stop"] = stop
if notes:
w["notes"] = notes
if alerts is not None:
w["alerts"] = alerts
save_stocks(data)
return True
# Add new
data["watchlist"].append({
"ticker": ticker,
"target": target,
"stop": stop,
"alerts": alerts or [],
"notes": notes
})
data["watchlist"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def add_to_holdings(
ticker: str,
name: str = "",
category: str = "",
notes: str = "",
target: Optional[float] = None,
stop: Optional[float] = None,
alerts: Optional[list] = None
) -> bool:
"""Add a stock to holdings. Target/stop for 'buy more' alerts."""
data = load_stocks()
# Check if already in holdings
for h in data["holdings"]:
if h.get("ticker") == ticker:
# Update existing
if name:
h["name"] = name
if category:
h["category"] = category
if notes:
h["notes"] = notes
if target is not None:
h["target"] = target
if stop is not None:
h["stop"] = stop
if alerts is not None:
h["alerts"] = alerts
save_stocks(data)
return True
# Add new
data["holdings"].append({
"ticker": ticker,
"name": name,
"category": category,
"notes": notes,
"target": target,
"stop": stop,
"alerts": alerts or []
})
data["holdings"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def move_to_holdings(
ticker: str,
name: str = "",
category: str = "",
notes: str = ""
) -> bool:
"""Move a stock from watchlist to holdings (you bought it)."""
data = load_stocks()
# Find in watchlist
watchlist_item = None
for i, w in enumerate(data["watchlist"]):
if w.get("ticker") == ticker:
watchlist_item = data["watchlist"].pop(i)
break
if not watchlist_item:
print(f"⚠️ {ticker} not found in watchlist", file=sys.stderr)
return False
# Add to holdings
data["holdings"].append({
"ticker": ticker,
"name": name or watchlist_item.get("notes", ""),
"category": category,
"notes": notes or f"Bought (was on watchlist with target ${watchlist_item.get('target', 'N/A')})"
})
data["holdings"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def remove_stock(ticker: str, from_list: str = "both") -> bool:
"""Remove a stock from holdings, watchlist, or both."""
data = load_stocks()
removed = False
if from_list in ("holdings", "both"):
original_len = len(data["holdings"])
data["holdings"] = [h for h in data["holdings"] if h.get("ticker") != ticker]
if len(data["holdings"]) < original_len:
removed = True
if from_list in ("watchlist", "both"):
original_len = len(data["watchlist"])
data["watchlist"] = [w for w in data["watchlist"] if w.get("ticker") != ticker]
if len(data["watchlist"]) < original_len:
removed = True
if removed:
save_stocks(data)
return removed
def list_stocks(show_holdings: bool = True, show_watchlist: bool = True):
"""Print stocks list."""
data = load_stocks()
if show_holdings:
print(f"\n📊 HOLDINGS ({len(data['holdings'])})")
print("-" * 50)
for h in data["holdings"][:20]:
print(f" {h['ticker']:10} {h.get('name', '')[:30]}")
if len(data["holdings"]) > 20:
print(f" ... and {len(data['holdings']) - 20} more")
if show_watchlist:
print(f"\n👀 WATCHLIST ({len(data['watchlist'])})")
print("-" * 50)
for w in data["watchlist"][:20]:
target = f"${w['target']}" if w.get('target') else "no target"
print(f" {w['ticker']:10} {target:>10} {w.get('notes', '')[:25]}")
if len(data["watchlist"]) > 20:
print(f" ... and {len(data['watchlist']) - 20} more")
def main():
parser = argparse.ArgumentParser(description="Unified stock management")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# list
list_parser = subparsers.add_parser("list", help="List stocks")
list_parser.add_argument("--holdings", action="store_true", help="Show only holdings")
list_parser.add_argument("--watchlist", action="store_true", help="Show only watchlist")
# add-watchlist
add_watch = subparsers.add_parser("add-watchlist", help="Add to watchlist")
add_watch.add_argument("ticker", help="Stock ticker")
add_watch.add_argument("--target", type=float, help="Target price")
add_watch.add_argument("--stop", type=float, help="Stop loss")
add_watch.add_argument("--notes", default="", help="Notes")
# add-holding
add_hold = subparsers.add_parser("add-holding", help="Add to holdings")
add_hold.add_argument("ticker", help="Stock ticker")
add_hold.add_argument("--name", default="", help="Company name")
add_hold.add_argument("--category", default="", help="Category")
add_hold.add_argument("--notes", default="", help="Notes")
add_hold.add_argument("--target", type=float, help="Buy-more target price")
add_hold.add_argument("--stop", type=float, help="Stop loss price")
# move (watchlist → holdings)
move = subparsers.add_parser("move", help="Move from watchlist to holdings")
move.add_argument("ticker", help="Stock ticker")
move.add_argument("--name", default="", help="Company name")
move.add_argument("--category", default="", help="Category")
# remove
remove = subparsers.add_parser("remove", help="Remove stock")
remove.add_argument("ticker", help="Stock ticker")
remove.add_argument("--from", dest="from_list", choices=["holdings", "watchlist", "both"],
default="both", help="Remove from which list")
# set-alert (for existing holdings)
set_alert = subparsers.add_parser("set-alert", help="Set buy-more/stop alert on holding")
set_alert.add_argument("ticker", help="Stock ticker")
set_alert.add_argument("--target", type=float, help="Buy-more target price")
set_alert.add_argument("--stop", type=float, help="Stop loss price")
args = parser.parse_args()
if args.command == "list":
show_h = not args.watchlist or args.holdings
show_w = not args.holdings or args.watchlist
if not args.holdings and not args.watchlist:
show_h = show_w = True
list_stocks(show_holdings=show_h, show_watchlist=show_w)
elif args.command == "add-watchlist":
add_to_watchlist(args.ticker.upper(), args.target, args.stop, args.notes)
print(f"✅ Added {args.ticker.upper()} to watchlist")
elif args.command == "add-holding":
add_to_holdings(args.ticker.upper(), args.name, args.category, args.notes,
args.target, args.stop)
print(f"✅ Added {args.ticker.upper()} to holdings")
elif args.command == "move":
if move_to_holdings(args.ticker.upper(), args.name, args.category):
print(f"✅ Moved {args.ticker.upper()} from watchlist to holdings")
elif args.command == "remove":
if remove_stock(args.ticker.upper(), args.from_list):
print(f"✅ Removed {args.ticker.upper()}")
else:
print(f"⚠️ {args.ticker.upper()} not found")
elif args.command == "set-alert":
data = load_stocks()
found = False
for h in data["holdings"]:
if h.get("ticker") == args.ticker.upper():
if args.target is not None:
h["target"] = args.target
if args.stop is not None:
h["stop"] = args.stop
save_stocks(data)
found = True
print(f"✅ Set alert on {args.ticker.upper()}: target=${args.target}, stop=${args.stop}")
break
if not found:
print(f"⚠️ {args.ticker.upper()} not found in holdings")
else:
parser.print_help()
if __name__ == "__main__":
main()