#!/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()