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.
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/PKG-INFO +34 -7
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/README.md +32 -5
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/pyproject.toml +3 -4
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/__init__.py +1 -1
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/_core.py +75 -9
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/artifacts.py +42 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/__init__.py +1 -2
- young_stock_cli-0.2.4/src/young_stock/chat.py +307 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/cli.py +86 -7
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/llm.py +36 -2
- young_stock_cli-0.2.4/src/young_stock/market_routes.py +60 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/pdf.py +75 -10
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/reports.py +7 -4
- young_stock_cli-0.2.4/src/young_stock/research_style.py +207 -0
- young_stock_cli-0.2.2/src/young_stock/chat.py +0 -115
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/.gitignore +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/LICENSE +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/calendar.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/base.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/channels/feishu.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/config.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/evidence.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/health.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/local_store.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/methodology.py +0 -0
- {young_stock_cli-0.2.2 → young_stock_cli-0.2.4}/src/young_stock/profile.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
260
|
-
|
|
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.
|
|
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",
|
|
@@ -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
|
|
1999
|
-
activity =
|
|
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
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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 =
|
|
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", {})
|