young-stock-cli 0.2.2__tar.gz → 0.2.4__tar.gz

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.
Files changed (27) hide show
  1. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/PKG-INFO +34 -7
  2. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/README.md +32 -5
  3. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/pyproject.toml +3 -4
  4. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/__init__.py +1 -1
  5. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/_core.py +75 -9
  6. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/artifacts.py +42 -0
  7. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/__init__.py +1 -2
  8. young_stock_cli-0.2.4/src/young_stock/chat.py +307 -0
  9. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/cli.py +86 -7
  10. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/llm.py +36 -2
  11. young_stock_cli-0.2.4/src/young_stock/market_routes.py +60 -0
  12. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/pdf.py +75 -10
  13. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/reports.py +7 -4
  14. young_stock_cli-0.2.4/src/young_stock/research_style.py +207 -0
  15. young_stock_cli-0.2.2/src/young_stock/chat.py +0 -115
  16. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/.gitignore +0 -0
  17. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/LICENSE +0 -0
  18. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/calendar.py +0 -0
  19. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/base.py +0 -0
  20. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/feishu.py +0 -0
  21. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/config.py +0 -0
  22. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/evidence.py +0 -0
  23. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/health.py +0 -0
  24. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/local_store.py +0 -0
  25. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/methodology.py +0 -0
  26. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/profile.py +0 -0
  27. {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/templates/equity-report.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: young-stock-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data.
5
5
  Project-URL: Homepage, https://github.com/AdvancingTitans/young-stock-cli
6
6
  Project-URL: Repository, https://github.com/AdvancingTitans/young-stock-cli
@@ -25,12 +25,12 @@ Requires-Python: >=3.9
25
25
  Requires-Dist: click>=8.1
26
26
  Requires-Dist: requests>=2.31
27
27
  Requires-Dist: rich>=13.0
28
+ Requires-Dist: weasyprint>=62
28
29
  Provides-Extra: dev
29
30
  Requires-Dist: pytest-cov>=4; extra == 'dev'
30
31
  Requires-Dist: pytest>=7; extra == 'dev'
31
32
  Requires-Dist: ruff>=0.4; extra == 'dev'
32
33
  Provides-Extra: pdf
33
- Requires-Dist: weasyprint>=62; extra == 'pdf'
34
34
  Description-Content-Type: text/markdown
35
35
 
36
36
  # young-stock-cli
@@ -56,6 +56,7 @@ Recommended for CLI isolation:
56
56
 
57
57
  ```bash
58
58
  uv tool install young-stock-cli
59
+ young init
59
60
  ```
60
61
 
61
62
  Or install into the active Python environment:
@@ -80,6 +81,10 @@ If you installed `young` with `uv tool`, upgrade that tool-managed environment i
80
81
  uv tool install --upgrade young-stock-cli
81
82
  ```
82
83
 
84
+ `young init` creates the local home/profile files, verifies whether PDF rendering is available in the current
85
+ environment, and prints the recommended next steps. You only need to install the tool once per environment; you do
86
+ not need to reinstall it before every report.
87
+
83
88
  If `python3 -m pip install --upgrade young-stock-cli` succeeds but `young --version` still shows an older release,
84
89
  you are probably running a different executable entrypoint than the interpreter you just upgraded. A quick check:
85
90
 
@@ -114,6 +119,7 @@ young daily --format summary # concise personalized daily report
114
119
  young daily --format key-points # short report with trend/risk points
115
120
  young daily --format full # full personalized daily report
116
121
  young daily --llm # evidence-driven deep replay with your configured LLM
122
+ young init # initialize local state and verify report/LLM readiness
117
123
  young replay # deep M1-M6 market replay
118
124
  young analyze 600519 # deep single-stock analysis
119
125
  young chat # Rich chat mode with slash commands
@@ -269,18 +275,39 @@ checks. Markdown, metadata, and `evidence.json` are retained under:
269
275
  ~/.young_stock/reports/YYYYMMDD/
270
276
  ```
271
277
 
278
+ Before the model sees any data, young converts internal evidence into research-only Chinese terminology. The same
279
+ conversion is applied to the downloaded stock-analysis methodology, so implementation guidance never enters the
280
+ report-writing context. The returned Markdown is reviewed again before it can be saved or exported. Formal reports
281
+ use only these source phrases:
282
+
283
+ - normal data: `据公开市场数据`, `据交易所及财经终端披露`
284
+ - missing data: `该指标当日未披露`, `历史数据不可得`, `本模块证据暂缺`
285
+ - historical lookback: `按惯例回溯至该日`, `历史口径回溯`
286
+
287
+ Report artifacts include the date, market session, and topic:
288
+
289
+ ```text
290
+ 20260618-早盘-A股深度复盘.md
291
+ 20260618-盘中-A股深度复盘.md
292
+ 20260618-盘后-A股深度复盘.md
293
+ 20260618-盘后-600519深度分析.md
294
+ ```
295
+
296
+ Generating the same topic again in the same session replaces the previous artifact. A report from another session
297
+ is retained.
298
+
272
299
  ### Professional PDF reports
273
300
 
274
- Install the optional renderer:
301
+ The standard install already includes PDF support. If you upgraded from an older environment, refresh the tool once:
275
302
 
276
303
  ```bash
277
- uv tool install --force 'young-stock-cli[pdf]'
304
+ uv tool install --force 'young-stock-cli'
278
305
  ```
279
306
 
280
307
  If `young` was installed into the active Python environment instead:
281
308
 
282
309
  ```bash
283
- python3 -m pip install "young-stock-cli[pdf]"
310
+ python3 -m pip install --upgrade young-stock-cli
284
311
  ```
285
312
 
286
313
  Then export the latest report:
@@ -291,8 +318,8 @@ young report --date 20260618
291
318
  ```
292
319
 
293
320
  If no Markdown report exists for the selected date, `young report` first reuses a saved diary entry when available,
294
- otherwise it automatically generates a deterministic full daily report. It keeps `daily.md`/`replay.md`,
295
- `report.html`, and `report.pdf` together.
321
+ otherwise it automatically generates a deterministic full daily report. Run `young init` first if you want a quick
322
+ readiness check for PDF rendering, config, and local storage paths.
296
323
 
297
324
  The bundled Equity Report layout follows the
298
325
  [`tw93/Kami`](https://github.com/tw93/Kami) editorial language: parchment `#f5f4ed`, ink blue `#1B365D`,
@@ -21,6 +21,7 @@ Recommended for CLI isolation:
21
21
 
22
22
  ```bash
23
23
  uv tool install young-stock-cli
24
+ young init
24
25
  ```
25
26
 
26
27
  Or install into the active Python environment:
@@ -45,6 +46,10 @@ If you installed `young` with `uv tool`, upgrade that tool-managed environment i
45
46
  uv tool install --upgrade young-stock-cli
46
47
  ```
47
48
 
49
+ `young init` creates the local home/profile files, verifies whether PDF rendering is available in the current
50
+ environment, and prints the recommended next steps. You only need to install the tool once per environment; you do
51
+ not need to reinstall it before every report.
52
+
48
53
  If `python3 -m pip install --upgrade young-stock-cli` succeeds but `young --version` still shows an older release,
49
54
  you are probably running a different executable entrypoint than the interpreter you just upgraded. A quick check:
50
55
 
@@ -79,6 +84,7 @@ young daily --format summary # concise personalized daily report
79
84
  young daily --format key-points # short report with trend/risk points
80
85
  young daily --format full # full personalized daily report
81
86
  young daily --llm # evidence-driven deep replay with your configured LLM
87
+ young init # initialize local state and verify report/LLM readiness
82
88
  young replay # deep M1-M6 market replay
83
89
  young analyze 600519 # deep single-stock analysis
84
90
  young chat # Rich chat mode with slash commands
@@ -234,18 +240,39 @@ checks. Markdown, metadata, and `evidence.json` are retained under:
234
240
  ~/.young_stock/reports/YYYYMMDD/
235
241
  ```
236
242
 
243
+ Before the model sees any data, young converts internal evidence into research-only Chinese terminology. The same
244
+ conversion is applied to the downloaded stock-analysis methodology, so implementation guidance never enters the
245
+ report-writing context. The returned Markdown is reviewed again before it can be saved or exported. Formal reports
246
+ use only these source phrases:
247
+
248
+ - normal data: `据公开市场数据`, `据交易所及财经终端披露`
249
+ - missing data: `该指标当日未披露`, `历史数据不可得`, `本模块证据暂缺`
250
+ - historical lookback: `按惯例回溯至该日`, `历史口径回溯`
251
+
252
+ Report artifacts include the date, market session, and topic:
253
+
254
+ ```text
255
+ 20260618-早盘-A股深度复盘.md
256
+ 20260618-盘中-A股深度复盘.md
257
+ 20260618-盘后-A股深度复盘.md
258
+ 20260618-盘后-600519深度分析.md
259
+ ```
260
+
261
+ Generating the same topic again in the same session replaces the previous artifact. A report from another session
262
+ is retained.
263
+
237
264
  ### Professional PDF reports
238
265
 
239
- Install the optional renderer:
266
+ The standard install already includes PDF support. If you upgraded from an older environment, refresh the tool once:
240
267
 
241
268
  ```bash
242
- uv tool install --force 'young-stock-cli[pdf]'
269
+ uv tool install --force 'young-stock-cli'
243
270
  ```
244
271
 
245
272
  If `young` was installed into the active Python environment instead:
246
273
 
247
274
  ```bash
248
- python3 -m pip install "young-stock-cli[pdf]"
275
+ python3 -m pip install --upgrade young-stock-cli
249
276
  ```
250
277
 
251
278
  Then export the latest report:
@@ -256,8 +283,8 @@ young report --date 20260618
256
283
  ```
257
284
 
258
285
  If no Markdown report exists for the selected date, `young report` first reuses a saved diary entry when available,
259
- otherwise it automatically generates a deterministic full daily report. It keeps `daily.md`/`replay.md`,
260
- `report.html`, and `report.pdf` together.
286
+ otherwise it automatically generates a deterministic full daily report. Run `young init` first if you want a quick
287
+ readiness check for PDF rendering, config, and local storage paths.
261
288
 
262
289
  The bundled Equity Report layout follows the
263
290
  [`tw93/Kami`](https://github.com/tw93/Kami) editorial language: parchment `#f5f4ed`, ink blue `#1B365D`,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "young-stock-cli"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "requests>=2.31",
30
30
  "rich>=13.0",
31
31
  "click>=8.1",
32
+ "weasyprint>=62",
32
33
  ]
33
34
 
34
35
  [project.urls]
@@ -41,9 +42,7 @@ Changelog = "https://github.com/AdvancingTitans/young-stock-cli/blob/main/CHANGE
41
42
  young = "young_stock.cli:cli"
42
43
 
43
44
  [project.optional-dependencies]
44
- pdf = [
45
- "weasyprint>=62",
46
- ]
45
+ pdf = []
47
46
  dev = [
48
47
  "pytest>=7",
49
48
  "pytest-cov>=4",
@@ -1,3 +1,3 @@
1
1
  """young-stock-cli: A-share after-hours CLI."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -36,6 +36,7 @@ from typing import Any
36
36
 
37
37
  from .calendar import nearest_trade_date as calendar_nearest_trade_date
38
38
  from .health import SourceHealthBook
39
+ from .market_routes import route_board_data
39
40
 
40
41
  # ------------------------------------------------------------------
41
42
  # 配置
@@ -716,6 +717,32 @@ def fetch_ths_concept_money_flow_snapshot(date_str: str) -> dict[str, str]:
716
717
  return result
717
718
 
718
719
 
720
+ def fetch_browser_fund_flow_snapshot(date_str: str) -> dict[str, str]:
721
+ raw = _playwright_html(THS_CONCEPT_MONEY_FLOW_URL)
722
+ if not raw:
723
+ return {}
724
+ rows = _parse_ths_money_flow_table(raw)
725
+ top_in_rows = sorted(
726
+ [row for row in rows if float(row.get("net") or 0) > 0],
727
+ key=lambda item: float(item.get("net") or 0),
728
+ reverse=True,
729
+ )[:5]
730
+ top_out_rows = sorted(
731
+ [row for row in rows if float(row.get("net") or 0) < 0],
732
+ key=lambda item: float(item.get("net") or 0),
733
+ )[:5]
734
+ if not top_in_rows or not top_out_rows:
735
+ return {}
736
+ return {
737
+ "date": _display_date(nearest_trade_date()),
738
+ "_source": "公开财经页面概念资金流",
739
+ "_scope": "A股",
740
+ "_fallback_indicator": "concept_money_flow",
741
+ "_concept_in": json.dumps(top_in_rows, ensure_ascii=False),
742
+ "_concept_out": json.dumps(top_out_rows, ensure_ascii=False),
743
+ }
744
+
745
+
719
746
  def _market_activity_snapshot(rows: list[dict[str, Any]], source: str, date_str: str) -> dict[str, str]:
720
747
  usable = [r for r in rows if r.get("f12") in {"000001", "399001"}]
721
748
  if not usable:
@@ -1995,8 +2022,15 @@ def get_fund_flow(date_str: str, *, strict_date: bool = True) -> dict[str, str]:
1995
2022
  latest_result["_date_note"] = "latest_available"
1996
2023
  cache_save("fund_flow", date_str, "eastmoney", latest_result)
1997
2024
  return latest_result
1998
- for fallback in (fetch_sina_sector_money_flow_snapshot, fetch_sina_market_activity_snapshot, fetch_tencent_market_activity_snapshot):
1999
- activity = fallback(date_str)
2025
+ for online_reference in (fetch_sina_sector_money_flow_snapshot,):
2026
+ activity = online_reference(date_str)
2027
+ if activity:
2028
+ return activity
2029
+ browser_flow = fetch_browser_fund_flow_snapshot(date_str)
2030
+ if browser_flow:
2031
+ return browser_flow
2032
+ for market_reference in (fetch_sina_market_activity_snapshot, fetch_tencent_market_activity_snapshot):
2033
+ activity = market_reference(date_str)
2000
2034
  if activity:
2001
2035
  return activity
2002
2036
  cached_latest = load_latest_fund_flow_cache(date_str)
@@ -2171,13 +2205,15 @@ def fetch_eastmoney_board_list(board_type: str, date_str: str, limit: int = 100)
2171
2205
 
2172
2206
  def get_board_list(board_type: str, date_str: str, limit: int = 100) -> dict[str, Any]:
2173
2207
  """Return board rankings through the stock-analysis source order."""
2174
- result = fetch_eastmoney_board_list(board_type, date_str, limit=limit)
2175
- if result.get("rows"):
2176
- return result
2177
- browser_result = camofox_board_list(board_type)
2178
- if browser_result.get("rows"):
2179
- return browser_result
2180
- return result
2208
+ return route_board_data(
2209
+ board_type,
2210
+ date_str,
2211
+ direct=fetch_eastmoney_board_list,
2212
+ camofox=camofox_board_list,
2213
+ playwright=playwright_board_list,
2214
+ limit=limit,
2215
+ current_trade_date=nearest_trade_date(),
2216
+ )
2181
2217
 
2182
2218
 
2183
2219
  @retry_on_recoverable(max_retries=MAX_RETRIES, initial_delay=INITIAL_BACKOFF)
@@ -2751,6 +2787,36 @@ def _parse_camofox_board_snapshot(markdown: str) -> list[dict[str, Any]]:
2751
2787
  return rows
2752
2788
 
2753
2789
 
2790
+ def _playwright_html(url: str) -> str:
2791
+ try:
2792
+ from playwright.sync_api import sync_playwright
2793
+ except ImportError:
2794
+ return ""
2795
+ try:
2796
+ with sync_playwright() as runtime:
2797
+ browser = runtime.chromium.launch(headless=True)
2798
+ page = browser.new_page()
2799
+ page.goto(url, wait_until="networkidle", timeout=20_000)
2800
+ content = page.content()
2801
+ browser.close()
2802
+ return content
2803
+ except Exception as exc:
2804
+ diag(f"Browser page unavailable: {exc}")
2805
+ return ""
2806
+
2807
+
2808
+ def playwright_board_list(board_type: str = "industry") -> dict[str, Any]:
2809
+ anchor = "industry_board" if board_type == "industry" else "concept_board"
2810
+ html_text = _playwright_html(f"https://quote.eastmoney.com/center/gridlist.html#{anchor}")
2811
+ if not html_text:
2812
+ return {"board_type": board_type, "rows": [], "_unavailable": "browser unavailable"}
2813
+ text = re.sub(r"<[^>]+>", " ", html.unescape(html_text))
2814
+ rows = _parse_camofox_board_snapshot(
2815
+ "\n".join(f'row "{line.strip()}"' for line in text.splitlines() if re.match(r"^\s*\d+\s+", line))
2816
+ )
2817
+ return {"board_type": board_type, "rows": rows, "count": len(rows), "_source": "公开财经页面"}
2818
+
2819
+
2754
2820
  def camofox_board_list(board_type: str = "industry") -> dict[str, Any]:
2755
2821
  base = os.environ.get("CAMOFOX_URL", "http://localhost:9377")
2756
2822
  user_id = os.environ.get("CAMOFOX_USER_ID", "")
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import re
6
7
  from dataclasses import dataclass
7
8
  from datetime import datetime
8
9
  from pathlib import Path
@@ -11,6 +12,37 @@ from typing import Any
11
12
  from .local_store import young_home
12
13
 
13
14
 
15
+ def market_session(now: datetime | None = None) -> str:
16
+ now = now or datetime.now()
17
+ minute = now.hour * 60 + now.minute
18
+ if 9 * 60 <= minute < 11 * 60 + 30:
19
+ return "早盘"
20
+ if 11 * 60 + 30 <= minute < 13 * 60:
21
+ return "午间"
22
+ if 13 * 60 <= minute < 15 * 60:
23
+ return "盘中"
24
+ return "盘后"
25
+
26
+
27
+ def report_session(trade_date: str, now: datetime | None = None) -> str:
28
+ now = now or datetime.now()
29
+ if trade_date != now.strftime("%Y%m%d"):
30
+ return "盘后"
31
+ return market_session(now)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ReportIdentity:
36
+ trade_date: str
37
+ session: str
38
+ topic: str
39
+
40
+ @property
41
+ def prefix(self) -> str:
42
+ safe_topic = "".join(character for character in self.topic if character.isalnum() or character in "-_")
43
+ return f"{self.trade_date}-{self.session}-{safe_topic}"
44
+
45
+
14
46
  @dataclass
15
47
  class ReportArtifacts:
16
48
  trade_date: str
@@ -36,6 +68,9 @@ class ReportArtifacts:
36
68
  path.write_text(content.rstrip() + "\n", encoding="utf-8")
37
69
  return path
38
70
 
71
+ def write_report_markdown(self, identity: ReportIdentity, content: str) -> Path:
72
+ return self.write_markdown(identity.prefix, content)
73
+
39
74
  def write_json(self, name: str, data: dict[str, Any]) -> Path:
40
75
  path = self.path(name, "json")
41
76
  path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
@@ -48,6 +83,13 @@ class ReportArtifacts:
48
83
  candidates = list((reports / trade_date).glob("*.md"))
49
84
  else:
50
85
  candidates = list(reports.glob("*/*.md"))
86
+ identified = [
87
+ path
88
+ for path in candidates
89
+ if re.match(r"^\d{8}-(?:早盘|盘中|午间|盘后)-.+\.md$", path.name)
90
+ ]
91
+ if identified:
92
+ candidates = identified
51
93
  return max(candidates, key=lambda path: path.stat().st_mtime) if candidates else None
52
94
 
53
95
  @classmethod
@@ -13,9 +13,8 @@ from .feishu import FeishuChannel
13
13
  def send_report(trade_date: str | None, *, channel_name: str | None = None) -> list[DeliveryResult]:
14
14
  if not trade_date:
15
15
  raise ValueError("没有报告日期;请先运行 `young report`。")
16
- artifacts = ReportArtifacts(trade_date)
17
16
  markdown = ReportArtifacts.latest_markdown(trade_date)
18
- pdf = artifacts.path("report", "pdf")
17
+ pdf = markdown.with_suffix(".pdf") if markdown else None
19
18
  if markdown is None or not pdf.exists():
20
19
  raise ValueError(f"{trade_date} 缺少 Markdown/PDF;请先运行 `young report --date {trade_date}`。")
21
20
  configs = load_config(strict=False).get("channels", {}).get("feishu", {})