|
1 | 1 | """young-stock-cli command line interface.""" |
2 | 2 | from __future__ import annotations |
3 | 3 |
|
| 4 | +import json |
| 5 | +import os |
4 | 6 | import subprocess |
5 | 7 | import sys |
| 8 | +from pathlib import Path |
6 | 9 |
|
7 | 10 | import click |
8 | 11 |
|
@@ -40,6 +43,54 @@ def _run(market: str, date: str | None, refresh: bool, include_news: bool = True |
40 | 43 | _refresh_opt = click.option("--refresh", is_flag=True, help="Skip cache and force re-fetch.") |
41 | 44 |
|
42 | 45 |
|
| 46 | +def _profile_path() -> Path: |
| 47 | + override = os.environ.get("YOUNG_STOCK_PROFILE") |
| 48 | + if override: |
| 49 | + return Path(override).expanduser() |
| 50 | + return Path.home() / ".young_stock" / "profile.json" |
| 51 | + |
| 52 | + |
| 53 | +def _load_profile() -> dict[str, list[str]]: |
| 54 | + path = _profile_path() |
| 55 | + if not path.exists(): |
| 56 | + return {"stocks": [], "funds": []} |
| 57 | + try: |
| 58 | + data = json.loads(path.read_text(encoding="utf-8")) |
| 59 | + except (OSError, json.JSONDecodeError): |
| 60 | + return {"stocks": [], "funds": []} |
| 61 | + return { |
| 62 | + "stocks": [str(v) for v in data.get("stocks", []) if str(v).strip()], |
| 63 | + "funds": [str(v) for v in data.get("funds", []) if str(v).strip()], |
| 64 | + } |
| 65 | + |
| 66 | + |
| 67 | +def _save_profile(profile: dict[str, list[str]]) -> None: |
| 68 | + path = _profile_path() |
| 69 | + path.parent.mkdir(parents=True, exist_ok=True) |
| 70 | + path.write_text(json.dumps(profile, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") |
| 71 | + |
| 72 | + |
| 73 | +def _add_profile_item(kind: str, value: str) -> dict[str, list[str]]: |
| 74 | + profile = _load_profile() |
| 75 | + items = profile.setdefault(kind, []) |
| 76 | + normalized = value.strip() |
| 77 | + if normalized and normalized not in items: |
| 78 | + items.append(normalized) |
| 79 | + _save_profile(profile) |
| 80 | + return profile |
| 81 | + |
| 82 | + |
| 83 | +def _print_first_use_guide() -> None: |
| 84 | + click.echo("# 每日行情日报") |
| 85 | + click.echo() |
| 86 | + click.echo("尚未设置投资记忆。首次使用请先添加你关注的股票、ETF 或基金:") |
| 87 | + click.echo(" young profile add-stock 600519") |
| 88 | + click.echo(" young profile add-stock 0700.HK") |
| 89 | + click.echo(" young profile add-fund 161725") |
| 90 | + click.echo() |
| 91 | + click.echo(f"配置会保存到: {_profile_path()}") |
| 92 | + |
| 93 | + |
43 | 94 | @cli.command(help="A-share dashboard: indices, ZT/DT pool, verified A-share fund flow, boards.") |
44 | 95 | @_date_opt |
45 | 96 | @_refresh_opt |
@@ -164,6 +215,50 @@ def news(parts: tuple[str, ...], date: str | None, refresh: bool, limit: int) -> |
164 | 215 | _core.run_stock_news(symbol, date_str, size=limit) |
165 | 216 |
|
166 | 217 |
|
| 218 | +@cli.command(help="Personal daily report from your saved watchlist.") |
| 219 | +@_date_opt |
| 220 | +@_refresh_opt |
| 221 | +@click.option("--no-news", is_flag=True, help="Only show market data, skip news lookup.") |
| 222 | +def daily(date: str | None, refresh: bool, no_news: bool) -> None: |
| 223 | + if refresh: |
| 224 | + _core.NO_CACHE = True |
| 225 | + _core.cache_clear_old(days=7) |
| 226 | + date_str = date or _core.nearest_trade_date() |
| 227 | + profile = _load_profile() |
| 228 | + if not profile.get("stocks") and not profile.get("funds"): |
| 229 | + _print_first_use_guide() |
| 230 | + return |
| 231 | + _core.run_daily_report(date_str, profile, include_news=not no_news) |
| 232 | + |
| 233 | + |
| 234 | +@cli.group(help="Manage local investment memory for daily reports.") |
| 235 | +def profile() -> None: |
| 236 | + pass |
| 237 | + |
| 238 | + |
| 239 | +@profile.command("add-stock", help="Add a stock/ETF symbol to your daily watchlist.") |
| 240 | +@click.argument("symbol") |
| 241 | +def profile_add_stock(symbol: str) -> None: |
| 242 | + data = _add_profile_item("stocks", symbol) |
| 243 | + click.echo(f"Added stock: {symbol.strip()}") |
| 244 | + click.echo(f"Stocks: {', '.join(data.get('stocks', [])) or '-'}") |
| 245 | + |
| 246 | + |
| 247 | +@profile.command("add-fund", help="Add a fund code to your daily watchlist.") |
| 248 | +@click.argument("code") |
| 249 | +def profile_add_fund(code: str) -> None: |
| 250 | + data = _add_profile_item("funds", code) |
| 251 | + click.echo(f"Added fund: {code.strip()}") |
| 252 | + click.echo(f"Funds: {', '.join(data.get('funds', [])) or '-'}") |
| 253 | + |
| 254 | + |
| 255 | +@profile.command("show", help="Show saved daily-report investment memory.") |
| 256 | +def profile_show() -> None: |
| 257 | + data = _load_profile() |
| 258 | + click.echo(f"Stocks: {', '.join(data.get('stocks', [])) or '-'}") |
| 259 | + click.echo(f"Funds: {', '.join(data.get('funds', [])) or '-'}") |
| 260 | + |
| 261 | + |
167 | 262 | @cli.command(help="Clear cached responses older than N days.") |
168 | 263 | @click.option("--days", default=7, show_default=True, help="Delete cache files older than this many days.") |
169 | 264 | def cache_clear(days: int) -> None: |
|
0 commit comments