Skip to content

Commit dcbe07f

Browse files
author
AdvancingTitans
committed
feat: improve daily UX and local workflows
1 parent ee4c9a2 commit dcbe07f

12 files changed

Lines changed: 634 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.13] - 2026-06-03
9+
10+
### Added
11+
- Added `young daily --format summary|key-points|full`, plus `--only`, `--order`, and `--quick` for shorter daily reports and configurable report sections.
12+
- Added profile management commands: `young profile list`, `remove-stock`, `remove-fund`, `clear`, and `profile group create/add`.
13+
- Added local productivity commands for staged workflows: `young portfolio`, `young alert`, `young note`, and `young diary`.
14+
- Added `young diagnose`, `young guide`, and `young example` for friendlier troubleshooting and onboarding.
15+
16+
### Changed
17+
- Fund holding reports now show the holding as-of date age and warn when stale quarterly holdings may have changed.
18+
- Daily report summary/key-points modes avoid long news and full market sections by default.
19+
820
## [0.1.12] - 2026-06-03
921

1022
### Added

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@ young stock 600519 # one stock snapshot (A-share / HK / US)
3535
young fund 161725 # fund estimate + top holdings quote/news
3636
young profile add-stock 600519
3737
young profile add-fund 161725
38-
young daily # personalized daily report from saved investment memory
38+
young profile list
39+
young daily --format summary # concise personalized daily report
40+
young daily --format key-points # short report with trend/risk points
41+
young daily --format full # full personalized daily report
42+
young daily --only 基金,A股 --quick
3943
young news 3690.HK # multi-source news only
44+
young diagnose # network/source diagnostic
45+
young note add "today I reduced chasing"
46+
young alert create 600519 "涨跌幅>5%"
4047
young stock AAPL --no-news
4148
young fund 161725 --no-news
4249
young us --no-news # market data only, skip news links
@@ -79,6 +86,10 @@ The internals are being split into focused modules: `young_stock.calendar` handl
7986
- **Single-stock lookup**`young stock 600519`, `young stock 0700.HK`, or `young stock AAPL` prints a compact quote snapshot with source, trade date, price, change, volume, turnover when available, and optional news.
8087
- **Fund holding lookup**`young fund 161725` prints the fund's same-day estimated change, latest NAV date, top holdings, holding-stock quotes, rough contribution estimate, and same-day holding-stock news. Official fund NAVs usually update at night, so intraday/close values are clearly labeled as estimates.
8188
- **Personal daily report**`young daily` reads your local investment memory from `~/.young_stock/profile.json`, then prints saved stock/ETF trends, fund estimates, global indices, A-share sentiment, and risk-oriented suggestions. First use: add symbols with `young profile add-stock 600519` and `young profile add-fund 161725`.
89+
- **Short report modes**`young daily --format summary` keeps terminal output compact; `--format key-points` adds a few trend/risk bullets; `--only`, `--order`, and `--quick` trim slower or irrelevant sections.
90+
- **Investment memory management** — list, remove, clear, and group saved stocks/funds with `young profile list`, `remove-stock`, `remove-fund`, `clear`, and `profile group create/add`.
91+
- **Local workflow helpers** — lightweight `portfolio`, `alert`, `note`, and `diary` commands store local records for portfolio experiments, reminder rules, investment notes, and saved daily-report text.
92+
- **Diagnostics**`young diagnose` summarizes recent source health and suggests cache/quick-mode fallbacks when public APIs are unstable.
8293
- **Single-stock news**`young news 3690.HK` prints only the news/momentum view, with each item showing source and link status.
8394
- **Smart caching**`~/.young_stock/cache/`, 7-day TTL, auto-pruned. Pass `--refresh` to skip.
8495
- **Trade-day awareness** — nearest-trade-day resolution including weekends and (best-effort) holidays.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "young-stock-cli"
7-
version = "0.1.12"
7+
version = "0.1.13"
88
description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/young_stock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""young-stock-cli: A-share after-hours CLI."""
22

3-
__version__ = "0.1.12"
3+
__version__ = "0.1.13"

src/young_stock/_core.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2819,6 +2819,10 @@ def print_fund_report(
28192819

28202820
asof = holdings_data.get("asof") or "-"
28212821
print(f"## 持仓股行情(前{min(len(holdings), 10)},持仓截止 {asof}\n")
2822+
stale_note = _fund_holding_staleness_note(asof, requested_date)
2823+
if stale_note:
2824+
print(stale_note)
2825+
print()
28222826
print(f"{'股票':<14} {'占净值':>8} {'最新价':>10} {'涨跌幅':>10} {'估算贡献':>10} {'来源':>8}")
28232827
print("-" * 72)
28242828
contribution = 0.0
@@ -2850,6 +2854,18 @@ def print_fund_report(
28502854
print()
28512855

28522856

2857+
def _fund_holding_staleness_note(asof: str, requested_date: str) -> str:
2858+
try:
2859+
asof_dt = datetime.strptime(str(asof), "%Y-%m-%d")
2860+
req_dt = datetime.strptime(requested_date, "%Y%m%d")
2861+
except (TypeError, ValueError):
2862+
return ""
2863+
days = max(0, (req_dt - asof_dt).days)
2864+
if days < 30:
2865+
return f"持仓时效: {asof}(距请求日 {days} 天)。"
2866+
return f"持仓时效: {asof}(距请求日 {days} 天,季报持仓可能已调仓;建议结合基金公告和实时估值变化。)"
2867+
2868+
28532869
def print_fund_holding_news(
28542870
holdings: list[dict[str, Any]],
28552871
heat: dict[str, dict[str, Any]],
@@ -2985,10 +3001,23 @@ def run_daily_report(
29853001
date_str: str,
29863002
watchlist: dict[str, list[str]] | None = None,
29873003
include_news: bool = True,
3004+
report_format: str = "full",
3005+
only: str | None = None,
3006+
order: str | None = None,
3007+
quick: bool = False,
29883008
) -> None:
29893009
from .reports import run_daily_report as _run_daily_report
29903010

2991-
_run_daily_report(sys.modules[__name__], date_str, watchlist, include_news=include_news)
3011+
_run_daily_report(
3012+
sys.modules[__name__],
3013+
date_str,
3014+
watchlist,
3015+
include_news=include_news,
3016+
report_format=report_format,
3017+
only=only,
3018+
order=order,
3019+
quick=quick,
3020+
)
29923021

29933022

29943023
def print_futu_news(news_data: dict, keyword: str, limit: int = 5) -> None:

src/young_stock/cli.py

Lines changed: 220 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
import click
88

99
from . import __version__, _core
10-
from .profile import add_profile_item, load_profile, profile_path
10+
from .local_store import load_store, now_label, save_store
11+
from .profile import (
12+
add_group,
13+
add_group_item,
14+
add_profile_item,
15+
clear_profile,
16+
load_profile,
17+
profile_path,
18+
remove_profile_item,
19+
)
1120

1221

1322
@click.group(
@@ -180,7 +189,11 @@ def news(parts: tuple[str, ...], date: str | None, refresh: bool, limit: int) ->
180189
@_date_opt
181190
@_refresh_opt
182191
@click.option("--no-news", is_flag=True, help="Only show market data, skip news lookup.")
183-
def daily(date: str | None, refresh: bool, no_news: bool) -> None:
192+
@click.option("--format", "report_format", type=click.Choice(["full", "summary", "key-points"]), default="full", show_default=True, help="Output style.")
193+
@click.option("--only", default=None, help="Only show selected parts, e.g. funds,stocks,a or 基金,A股.")
194+
@click.option("--order", default=None, help="Custom full-report order, e.g. 基金,A股,港股,美股.")
195+
@click.option("--quick", is_flag=True, help="Fast mode: skip slower global/news sections.")
196+
def daily(date: str | None, refresh: bool, no_news: bool, report_format: str, only: str | None, order: str | None, quick: bool) -> None:
184197
if refresh:
185198
_core.NO_CACHE = True
186199
_core.cache_clear_old(days=7)
@@ -189,7 +202,7 @@ def daily(date: str | None, refresh: bool, no_news: bool) -> None:
189202
if not profile.get("stocks") and not profile.get("funds"):
190203
_print_first_use_guide()
191204
return
192-
_core.run_daily_report(date_str, profile, include_news=not no_news)
205+
_core.run_daily_report(date_str, profile, include_news=not no_news, report_format=report_format, only=only, order=order, quick=quick)
193206

194207

195208
@cli.group(help="Manage local investment memory for daily reports.")
@@ -213,11 +226,214 @@ def profile_add_fund(code: str) -> None:
213226
click.echo(f"Funds: {', '.join(data.get('funds', [])) or '-'}")
214227

215228

216-
@profile.command("show", help="Show saved daily-report investment memory.")
229+
@profile.command("list", help="Show saved daily-report investment memory.")
217230
def profile_show() -> None:
218231
data = load_profile()
219232
click.echo(f"Stocks: {', '.join(data.get('stocks', [])) or '-'}")
220233
click.echo(f"Funds: {', '.join(data.get('funds', [])) or '-'}")
234+
groups = data.get("groups", {})
235+
if groups:
236+
click.echo("Groups:")
237+
for name, group in groups.items():
238+
stocks = ", ".join(group.get("stocks", [])) or "-"
239+
funds = ", ".join(group.get("funds", [])) or "-"
240+
click.echo(f" {name}: stocks={stocks}; funds={funds}")
241+
242+
243+
profile.add_command(profile_show, name="show")
244+
245+
246+
@profile.command("remove-stock", help="Remove a stock/ETF symbol from your watchlist.")
247+
@click.argument("symbol")
248+
def profile_remove_stock(symbol: str) -> None:
249+
data = remove_profile_item("stocks", symbol)
250+
click.echo(f"Removed stock: {symbol.strip()}")
251+
click.echo(f"Stocks: {', '.join(data.get('stocks', [])) or '-'}")
252+
253+
254+
@profile.command("remove-fund", help="Remove a fund code from your watchlist.")
255+
@click.argument("code")
256+
def profile_remove_fund(code: str) -> None:
257+
data = remove_profile_item("funds", code)
258+
click.echo(f"Removed fund: {code.strip()}")
259+
click.echo(f"Funds: {', '.join(data.get('funds', [])) or '-'}")
260+
261+
262+
@profile.command("clear", help="Clear stocks, funds, and groups from investment memory.")
263+
def profile_clear() -> None:
264+
clear_profile()
265+
click.echo("Cleared investment memory.")
266+
267+
268+
@profile.group("group", help="Manage investment-memory groups.")
269+
def profile_group() -> None:
270+
pass
271+
272+
273+
@profile_group.command("create", help="Create a named watchlist group.")
274+
@click.argument("name")
275+
def profile_group_create(name: str) -> None:
276+
add_group(name)
277+
click.echo(f"Created group: {name}")
278+
279+
280+
@profile_group.command("add", help="Add a symbol/code to a group.")
281+
@click.argument("name")
282+
@click.argument("code")
283+
def profile_group_add(name: str, code: str) -> None:
284+
add_group_item(name, code)
285+
click.echo(f"Added {code} to group: {name}")
286+
287+
288+
@cli.command(help="Run a lightweight network/source diagnostic.")
289+
def diagnose() -> None:
290+
click.echo("# 网络诊断")
291+
for name in ["eastmoney", "sina", "tencent", "ths", "futu"]:
292+
snap = _core.SOURCE_HEALTH.snapshot(name)
293+
state = "建议暂缓使用" if snap.should_skip else "可用/未发现近期异常"
294+
click.echo(f"{name}: 成功率 {snap.success_rate:.0%}, 平均延迟 {snap.average_latency_ms:.0f}ms, {state}")
295+
click.echo("建议: 若接口失败,可先使用缓存、加 --quick/--format summary,或稍后运行 --refresh 重试。")
296+
297+
298+
@cli.command(help="New-user guide.")
299+
def guide() -> None:
300+
click.echo("1. young profile add-stock 600519")
301+
click.echo("2. young profile add-fund 161725")
302+
click.echo("3. young daily --format summary")
303+
click.echo("4. young profile list / young diagnose")
304+
305+
306+
@cli.command(help="Show common examples.")
307+
def example() -> None:
308+
click.echo("young daily --format summary --quick")
309+
click.echo("young daily --format key-points --only 基金,A股")
310+
click.echo("young profile group create 稳健型")
311+
click.echo("young alert create 600519 '涨跌幅>5%'")
312+
313+
314+
@cli.group(help="Manage local portfolios.")
315+
def portfolio() -> None:
316+
pass
317+
318+
319+
@portfolio.command("create")
320+
@click.argument("name")
321+
def portfolio_create(name: str) -> None:
322+
data = load_store("portfolios", {})
323+
data.setdefault(name, [])
324+
save_store("portfolios", data)
325+
click.echo(f"Created portfolio: {name}")
326+
327+
328+
@portfolio.command("add")
329+
@click.argument("name")
330+
@click.argument("code")
331+
@click.argument("shares", type=float)
332+
def portfolio_add(name: str, code: str, shares: float) -> None:
333+
data = load_store("portfolios", {})
334+
items = data.setdefault(name, [])
335+
items.append({"code": code, "shares": shares})
336+
save_store("portfolios", data)
337+
click.echo(f"Added {code} x {shares:g} to {name}")
338+
339+
340+
@portfolio.command("show")
341+
@click.argument("name")
342+
def portfolio_show(name: str) -> None:
343+
data = load_store("portfolios", {})
344+
items = data.get(name, [])
345+
click.echo(f"# Portfolio: {name}")
346+
if not items:
347+
click.echo(" empty")
348+
for item in items:
349+
click.echo(f" {item.get('code')} x {item.get('shares')}")
350+
351+
352+
@portfolio.command("compare")
353+
@click.argument("code1")
354+
@click.argument("code2")
355+
def portfolio_compare(code1: str, code2: str) -> None:
356+
click.echo(f"{code1} vs {code2}: use young stock <code> for detail; historical comparison is on the roadmap.")
357+
358+
359+
@cli.group(help="Manage local price/change alerts.")
360+
def alert() -> None:
361+
pass
362+
363+
364+
@alert.command("create")
365+
@click.argument("code")
366+
@click.argument("condition")
367+
def alert_create(code: str, condition: str) -> None:
368+
data = load_store("alerts", [])
369+
data.append({"code": code, "condition": condition, "created_at": now_label()})
370+
save_store("alerts", data)
371+
click.echo(f"Created alert: {code} {condition}")
372+
373+
374+
@alert.command("list")
375+
def alert_list() -> None:
376+
data = load_store("alerts", [])
377+
if not data:
378+
click.echo("No alerts.")
379+
for item in data:
380+
click.echo(f"{item.get('code')}: {item.get('condition')} ({item.get('created_at')})")
381+
382+
383+
@alert.command("check")
384+
def alert_check() -> None:
385+
data = load_store("alerts", [])
386+
click.echo(f"Checked {len(data)} alerts. Realtime trigger evaluation is best-effort and will expand in a later release.")
387+
388+
389+
@cli.group(help="Manage investment notes.")
390+
def note() -> None:
391+
pass
392+
393+
394+
@note.command("add")
395+
@click.argument("content", nargs=-1, required=True)
396+
def note_add(content: tuple[str, ...]) -> None:
397+
data = load_store("notes", [])
398+
text = " ".join(content)
399+
data.append({"content": text, "created_at": now_label()})
400+
save_store("notes", data)
401+
click.echo("Added note.")
402+
403+
404+
@note.command("list")
405+
def note_list() -> None:
406+
data = load_store("notes", [])
407+
if not data:
408+
click.echo("No notes.")
409+
for item in data:
410+
click.echo(f"{item.get('created_at')}: {item.get('content')}")
411+
412+
413+
@cli.group(help="Save and read local daily-report snapshots.")
414+
def diary() -> None:
415+
pass
416+
417+
418+
@diary.command("save")
419+
@click.argument("date")
420+
@click.option("--text", default="", help="Diary text to save.")
421+
def diary_save(date: str, text: str) -> None:
422+
data = load_store("diaries", {})
423+
data[date] = {"text": text, "saved_at": now_label()}
424+
save_store("diaries", data)
425+
click.echo(f"Saved diary: {date}")
426+
427+
428+
@diary.command("show")
429+
@click.argument("date")
430+
def diary_show(date: str) -> None:
431+
data = load_store("diaries", {})
432+
item = data.get(date)
433+
if not item:
434+
click.echo("Diary not found.")
435+
return
436+
click.echo(item.get("text") or "")
221437

222438

223439
@cli.command(help="Clear cached responses older than N days.")

0 commit comments

Comments
 (0)