Skip to content

Commit 058ab26

Browse files
yjwyjw
authored andcommitted
Refine research report pipeline and release 0.2.3
1 parent ef72720 commit 058ab26

21 files changed

Lines changed: 832 additions & 32 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ 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.2.3] - 2026-06-18
9+
10+
### Added
11+
- Added a mandatory research-style boundary that converts internal evidence and stock-analysis methodology into publication-safe investment language before LLM synthesis.
12+
- Added a second report review pass that removes Markdown-wrapped internal fields, technical terms, local locations, and implementation filenames before persistence.
13+
- Added shared board and fund-flow routing for `young a` and LLM evidence, including optional Camofox and Playwright page retrieval after public data endpoints.
14+
- Added date/session/topic report identities such as `20260618-盘后-A股深度复盘.md`.
15+
16+
### Changed
17+
- Reports generated in the same trading date, session, and topic now overwrite the prior artifact; reports from other sessions remain available.
18+
- Markdown, HTML, PDF, Evidence, and metadata now share the date/session/topic identity.
19+
- Legacy generic report files no longer take precedence once a session-aware report exists.
20+
- LLM auth failures now surface the provider response message and a concrete Ark model-discovery hint without exposing secrets.
21+
822
## [0.2.2] - 2026-06-18
923

1024
### Fixed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,27 @@ checks. Markdown, metadata, and `evidence.json` are retained under:
234234
~/.young_stock/reports/YYYYMMDD/
235235
```
236236

237+
Before the model sees any data, young converts internal evidence into research-only Chinese terminology. The same
238+
conversion is applied to the downloaded stock-analysis methodology, so implementation guidance never enters the
239+
report-writing context. The returned Markdown is reviewed again before it can be saved or exported. Formal reports
240+
use only these source phrases:
241+
242+
- normal data: `据公开市场数据`, `据交易所及财经终端披露`
243+
- missing data: `该指标当日未披露`, `历史数据不可得`, `本模块证据暂缺`
244+
- historical lookback: `按惯例回溯至该日`, `历史口径回溯`
245+
246+
Report artifacts include the date, market session, and topic:
247+
248+
```text
249+
20260618-早盘-A股深度复盘.md
250+
20260618-盘中-A股深度复盘.md
251+
20260618-盘后-A股深度复盘.md
252+
20260618-盘后-600519深度分析.md
253+
```
254+
255+
Generating the same topic again in the same session replaces the previous artifact. A report from another session
256+
is retained.
257+
237258
### Professional PDF reports
238259

239260
Install the optional renderer:
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Research Report Pipeline Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Build a mandatory research-language conversion boundary, improve board/fund-flow routing, and produce session-aware report artifacts that overwrite within the same trading session.
6+
7+
**Architecture:** Internal evidence remains diagnostic and machine-readable. A new `research_style` module creates a separate research-only view for the LLM and validates the returned Markdown before persistence. A focused market routing module shares board and fund-flow acquisition between evidence generation and `young a`; report artifacts use a date/session/topic identity.
8+
9+
**Tech Stack:** Python 3.9+, requests, Click, Rich, pytest, optional Camofox/Playwright.
10+
11+
---
12+
13+
### Task 1: Mandatory research-language conversion
14+
15+
**Files:**
16+
- Create: `src/young_stock/research_style.py`
17+
- Modify: `src/young_stock/reports.py`
18+
- Test: `tests/test_research_style.py`
19+
- Test: `tests/test_llm_reports.py`
20+
21+
- [ ] Write failing tests for Markdown-wrapped internal fields, forbidden terms, paths, extensions, missing-data wording, and input immutability.
22+
- [ ] Run the focused tests and verify the current implementation fails.
23+
- [ ] Implement `to_research_evidence(evidence)` with explicit module/field mappings and no mutation.
24+
- [ ] Implement `review_research_report(markdown, evidence)` with sentence-level rejection and research-language replacement.
25+
- [ ] Make `generate_llm_daily_report()` use only the converted evidence and reject output that still fails the final scan.
26+
- [ ] Run focused tests and commit.
27+
28+
### Task 2: Unified board and fund-flow routing
29+
30+
**Files:**
31+
- Create: `src/young_stock/market_routes.py`
32+
- Modify: `src/young_stock/_core.py`
33+
- Modify: `src/young_stock/evidence.py`
34+
- Test: `tests/test_market_routes.py`
35+
- Test: `tests/test_core.py`
36+
- Test: `tests/test_evidence.py`
37+
38+
- [ ] Write failing tests for route ordering, no-proxy Eastmoney access, structured browser rows, historical-date discipline, and final browser attempt.
39+
- [ ] Run the focused tests and verify failures.
40+
- [ ] Implement the shared board route without changing stable quote/index functions.
41+
- [ ] Implement the shared fund-flow route while preserving source semantics.
42+
- [ ] Route `young a` and Evidence construction through the shared functions.
43+
- [ ] Run focused tests and commit.
44+
45+
### Task 3: Session-aware artifact identity
46+
47+
**Files:**
48+
- Modify: `src/young_stock/artifacts.py`
49+
- Modify: `src/young_stock/cli.py`
50+
- Modify: `src/young_stock/pdf.py`
51+
- Modify: `src/young_stock/channels/__init__.py`
52+
- Test: `tests/test_artifacts.py`
53+
- Test: `tests/test_pdf.py`
54+
- Test: `tests/test_cli.py`
55+
56+
- [ ] Write failing tests for early, intraday, midday, after-close names; topic slugs; same-session overwrite; and cross-session retention.
57+
- [ ] Run focused tests and verify failures.
58+
- [ ] Add a `ReportIdentity` value object with date, session, topic, and stable prefix.
59+
- [ ] Write Markdown, Evidence, metadata, HTML, and PDF using the same prefix.
60+
- [ ] Update latest-report lookup to prefer current session identities and ignore legacy generic files when a new identity exists.
61+
- [ ] Run focused tests and commit.
62+
63+
### Task 4: Documentation, migration, and verification
64+
65+
**Files:**
66+
- Modify: `README.md`
67+
- Modify: `CHANGELOG.md`
68+
- Modify: `pyproject.toml`
69+
- Modify: `src/young_stock/__init__.py`
70+
- Test: `tests/test_packaging_docs.py`
71+
72+
- [ ] Document research-language guarantees, data route behavior, report names, and overwrite semantics.
73+
- [ ] Bump the patch version.
74+
- [ ] Run full pytest.
75+
- [ ] Run ruff and `git diff --check`.
76+
- [ ] Build wheel and source archive.
77+
- [ ] Install the wheel in the Python 3.9 smoke environment and verify CLI help/version.
78+
- [ ] Commit, push `main`, tag, publish, and verify GitHub/PyPI.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Young Stock 研报管线与数据路由设计
2+
3+
## 目标
4+
5+
完善 `young replay``young daily --llm``young analyze``young a` 与 PDF 导出:
6+
7+
- 所有正式研报必须经过“工程数据 → 纯研究语义 → LLM → 研报审校”的强制管线。
8+
- 正文不得出现内部字段、程序结构、工具名称、本地位置、文件扩展名或技术切换过程。
9+
- 板块和资金流复用 stock-analysis 4.2.0 的数据路由与质量纪律,但不形成包级循环依赖。
10+
- 已稳定的数据源及传统命令行为保持不变。
11+
- 数据接口无法取得结果时,最终尝试配置好的浏览器能力。
12+
- 报告名称包含交易日期、市场时段和主题;同一日期、时段、主题覆盖旧产物。
13+
14+
## 研报风格转换层
15+
16+
新增独立的 `research_style` 模块,作为正式报告的强制边界:
17+
18+
1. `to_research_evidence()` 接收内部 Evidence Pack。
19+
2. 删除或转换 `available``quality_score``missing_modules``degrade_mode`
20+
`growth_board_count`、来源内部标记和错误详情。
21+
3. 模块和字段转换为中文研究语义,例如:
22+
- `M2``板块强弱与资金流`
23+
- `available=false``本模块证据暂缺`
24+
- `growth_board_count=14``科创板与创业板活跃样本数为 14 家`
25+
4. 正常、缺失和回溯数据只使用用户指定的统一话术。
26+
5. `review_research_report()` 对模型输出按行审校。出现内部字段、禁词、路径、
27+
扩展名或技术过程时,删除整句并依据同模块已知证据生成研究语义替代句。
28+
6. 审校后再次扫描;不合格内容不得写入 Markdown、HTML 或 PDF。
29+
30+
原始 Evidence JSON 仍保留完整诊断字段,便于排障,但不会进入正式研报正文。
31+
32+
## 数据路由
33+
34+
新增统一的板块与资金流路由,供 Evidence 构建和 `young a` 共用。
35+
36+
### 板块
37+
38+
1. 保留当前东财轻量板块榜。
39+
2. 使用 stock-analysis 的东财纪律:无代理 Session、串行请求、至少一秒间隔、
40+
随机抖动、指数退避、最多三次。
41+
3. 接口无有效结果时尝试 Camofox。
42+
4. Camofox 不可用时尝试可选 Playwright 浏览器。
43+
5. 各层输出统一结构化字段,避免浏览器结果进入下游后无法读取。
44+
45+
### 资金流
46+
47+
保留目前已验证的来源顺序与语义边界:
48+
49+
1. 同花顺概念资金流双边榜。
50+
2. 东财全市场资金流,执行统一限速请求纪律。
51+
3. 东财公开页面可核验指标。
52+
4. 新浪行业资金流。
53+
5. 浏览器读取公开页面。
54+
6. 最近一次可信本地记录。
55+
56+
指数活跃度只能作为市场活跃参考,不得冒充主力资金净流入。
57+
58+
## 报告时段与命名
59+
60+
按北京时间识别:
61+
62+
- 09:00–09:30:早盘
63+
- 09:30–11:30、13:00–15:00:盘中
64+
- 11:30–13:00:午间
65+
- 15:00 后:盘后
66+
67+
文件前缀为 `{YYYYMMDD}-{时段}-{主题}`,例如:
68+
69+
- `20260618-早盘-A股深度复盘.md`
70+
- `20260618-盘中-A股深度复盘.md`
71+
- `20260618-盘后-A股深度复盘.md`
72+
- `20260618-盘后-贵州茅台深度分析.md`
73+
74+
同一交易日期、时段、主题固定写入同一路径,因此新报告原子覆盖旧报告。
75+
Markdown、HTML、PDF、Evidence 和 metadata 使用同一前缀。选择“最新报告”时优先
76+
读取当前时段的主题产物,不再让旧 `replay.md` 抢占。
77+
78+
## 错误与边界
79+
80+
- 正式研报只展示研究结论和统一缺失话术。
81+
- 数据来源失败原因只进入 Evidence 和 diagnose。
82+
- 浏览器依赖未配置时不伪装成功。
83+
- 历史日期不混入实时板块榜;对应位置使用“历史数据不可得”。
84+
- stock-analysis 规范仍按严格版本比较下载文本规范与模板并校验,不执行远端代码。
85+
86+
## 测试
87+
88+
- 复现带 Markdown 加粗和反引号的工程字段泄漏。
89+
- 覆盖所有禁词、路径、扩展名及模块内部名称。
90+
- 验证转换前后原始 Evidence 不被修改。
91+
- 验证板块和资金流每一层的顺序、结构和日期纪律。
92+
- 验证浏览器末级路径及全部失败时的统一缺失语义。
93+
- 验证早盘、盘中、午间、盘后命名。
94+
- 验证同一时段覆盖,跨时段保留。
95+
- 全量 pytest、ruff、构建与 Python 3.9 安装验证。

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.2.2"
7+
version = "0.2.3"
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.9"

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.2.2"
3+
__version__ = "0.2.3"

src/young_stock/_core.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
from .calendar import nearest_trade_date as calendar_nearest_trade_date
3838
from .health import SourceHealthBook
39+
from .market_routes import route_board_data
3940

4041
# ------------------------------------------------------------------
4142
# 配置
@@ -716,6 +717,32 @@ def fetch_ths_concept_money_flow_snapshot(date_str: str) -> dict[str, str]:
716717
return result
717718

718719

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+
719746
def _market_activity_snapshot(rows: list[dict[str, Any]], source: str, date_str: str) -> dict[str, str]:
720747
usable = [r for r in rows if r.get("f12") in {"000001", "399001"}]
721748
if not usable:
@@ -1995,8 +2022,15 @@ def get_fund_flow(date_str: str, *, strict_date: bool = True) -> dict[str, str]:
19952022
latest_result["_date_note"] = "latest_available"
19962023
cache_save("fund_flow", date_str, "eastmoney", latest_result)
19972024
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)
20002034
if activity:
20012035
return activity
20022036
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)
21712205

21722206
def get_board_list(board_type: str, date_str: str, limit: int = 100) -> dict[str, Any]:
21732207
"""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+
)
21812217

21822218

21832219
@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]]:
27512787
return rows
27522788

27532789

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+
27542820
def camofox_board_list(board_type: str = "industry") -> dict[str, Any]:
27552821
base = os.environ.get("CAMOFOX_URL", "http://localhost:9377")
27562822
user_id = os.environ.get("CAMOFOX_USER_ID", "")

0 commit comments

Comments
 (0)