每日备份 2026-03-27
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "daily-stock-analysis",
|
||||
"installedVersion": "1.0.2",
|
||||
"installedAt": 1774625590876
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: daily-stock-analysis
|
||||
description: Deterministic daily stock analysis skill for global equities. Use when users need daily analysis, next-trading-day close prediction, prior forecast review, rolling accuracy, and reliable markdown report output.
|
||||
---
|
||||
|
||||
# Daily Stock Analysis
|
||||
|
||||
Perform market-aware, evidence-based daily stock analysis with prediction, next-run review, rolling accuracy tracking, and a structured self-evolution mechanism that updates future assumptions from observed forecast errors.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
1. Read and write files only under `working_directory`.
|
||||
2. Save new reports only to:
|
||||
|
||||
- `<working_directory>/daily-stock-analysis/reports/`
|
||||
|
||||
3. Use filename:
|
||||
|
||||
- `YYYY-MM-DD-<TICKER>-analysis.md`
|
||||
|
||||
4. If same ticker/day file exists, ask user:
|
||||
|
||||
- `overwrite` or `new_version` (`-v2`, `-v3`, ...)
|
||||
- For unattended runs, default to `new_version`
|
||||
|
||||
5. Always review history before new prediction.
|
||||
6. Limit history read count to control token usage:
|
||||
|
||||
- Script mode: max 5 files (default)
|
||||
- Compatibility mode: max 3 files
|
||||
|
||||
## Required Scripts (Use First)
|
||||
|
||||
1. Plan output path + collect history:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/report_manager.py plan \
|
||||
--workdir <working_directory> \
|
||||
--ticker <TICKER> \
|
||||
--run-date <YYYY-MM-DD> \
|
||||
--versioning auto \
|
||||
--history-limit 5
|
||||
```
|
||||
|
||||
2. Compute rolling accuracy from existing reports:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/calc_accuracy.py \
|
||||
--workdir <working_directory> \
|
||||
--ticker <TICKER> \
|
||||
--windows 1,3,7,30 \
|
||||
--history-limit 60
|
||||
```
|
||||
|
||||
3. Optional: migrate legacy files after explicit user confirmation:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/report_manager.py migrate \
|
||||
--workdir <working_directory> \
|
||||
--file <ABS_PATH_1> --file <ABS_PATH_2>
|
||||
```
|
||||
|
||||
## Compatibility Mode (No Python / Small Model)
|
||||
|
||||
If Python scripts are unavailable or model capability is limited, switch to minimal mode:
|
||||
|
||||
1. Read at most 3 recent reports for the same ticker.
|
||||
2. Use only a minimal source set:
|
||||
|
||||
- one official disclosure source
|
||||
- one reliable market data source (Yahoo Finance acceptable)
|
||||
|
||||
3. Output concise result only:
|
||||
|
||||
- recommendation
|
||||
- `pred_close_t1`
|
||||
- prior review (`prev_pred_close_t1`, `prev_actual_close_t1`, `AE`, `APE`) if available
|
||||
- one `improvement_action`
|
||||
|
||||
4. Save report with same filename rules in canonical reports directory.
|
||||
|
||||
See `references/minimal_mode.md`.
|
||||
|
||||
## Minimal Run Protocol
|
||||
|
||||
1. Resolve ticker/exchange/market (ask if ambiguous).
|
||||
2. Run `report_manager.py plan`.
|
||||
3. Read `history_files` returned by script.
|
||||
4. If `legacy_files` exist, list all absolute paths and ask whether to migrate.
|
||||
5. Gather data using `references/sources.md` + `references/search_queries.md`.
|
||||
6. Run `calc_accuracy.py` for consistent metrics.
|
||||
7. Render report using `references/report_template.md`.
|
||||
8. Save to `selected_output_file` returned by `report_manager.py`.
|
||||
|
||||
## Required Output Fields
|
||||
|
||||
Must include:
|
||||
|
||||
- `recommendation`
|
||||
- `pred_close_t1`
|
||||
- `prev_pred_close_t1`
|
||||
- `prev_actual_close_t1`
|
||||
- `AE`, `APE`
|
||||
- rolling strict/loose accuracy fields
|
||||
- `improvement_actions`
|
||||
|
||||
## Self-Improvement (Required)
|
||||
|
||||
Each run must include 1-3 concrete `improvement_actions` from recent misses and use them in the next run.
|
||||
Do not skip this step.
|
||||
|
||||
## Scheduling Recommendation
|
||||
|
||||
Recommend users set this as a weekday recurring task (for example 10:00 local time) to keep prediction-review windows continuous.
|
||||
|
||||
## References
|
||||
|
||||
Default:
|
||||
|
||||
- `references/workflow.md`
|
||||
- `references/report_template.md`
|
||||
- `references/metrics.md`
|
||||
- `references/search_queries.md`
|
||||
- `references/sources.md`
|
||||
- `references/minimal_mode.md`
|
||||
- `references/security.md`
|
||||
|
||||
Deep-dive only (`full_report` mode):
|
||||
|
||||
- `references/fundamental-analysis.md`
|
||||
- `references/technical-analysis.md`
|
||||
- `references/financial-metrics.md`
|
||||
|
||||
## Compliance
|
||||
|
||||
Always append:
|
||||
|
||||
"This content is for research and informational purposes only and does not constitute investment advice or a return guarantee. Markets are risky; invest with caution."
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7arpc65p9wdnhbw70435rrf181jvtk",
|
||||
"slug": "daily-stock-analysis",
|
||||
"version": "1.0.2",
|
||||
"publishedAt": 1772265206033
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Financial Metrics Reference
|
||||
|
||||
Use these formulas and interpretations consistently. Metric availability may differ across US/CN/HK data providers.
|
||||
|
||||
## 1. Profitability
|
||||
|
||||
1. Gross Margin
|
||||
- Formula: `(Revenue - COGS) / Revenue * 100%`
|
||||
- Use: Pricing power and production efficiency.
|
||||
|
||||
2. Operating Margin
|
||||
- Formula: `Operating Income / Revenue * 100%`
|
||||
- Use: Core operating efficiency.
|
||||
|
||||
3. Net Margin
|
||||
- Formula: `Net Income / Revenue * 100%`
|
||||
- Use: End-to-end profitability quality.
|
||||
|
||||
4. ROE
|
||||
- Formula: `Net Income / Average Equity * 100%`
|
||||
- Use: Equity capital efficiency.
|
||||
|
||||
5. ROIC
|
||||
- Formula: `NOPAT / Invested Capital * 100%`
|
||||
- Use: Capital allocation quality across debt and equity.
|
||||
|
||||
## 2. Growth
|
||||
|
||||
1. Revenue Growth (YoY / QoQ)
|
||||
- Formula: `(Current Revenue - Prior Revenue) / Prior Revenue * 100%`
|
||||
|
||||
2. EPS Growth
|
||||
- Formula: `(Current EPS - Prior EPS) / Prior EPS * 100%`
|
||||
|
||||
3. Multi-year CAGR
|
||||
- Formula: `(Ending / Beginning)^(1/Years) - 1`
|
||||
|
||||
## 3. Valuation
|
||||
|
||||
1. P/E (Trailing / Forward)
|
||||
- Formula: `Price / EPS`
|
||||
|
||||
2. PEG
|
||||
- Formula: `P/E / Earnings Growth Rate`
|
||||
|
||||
3. P/B
|
||||
- Formula: `Price / Book Value Per Share`
|
||||
|
||||
4. P/S
|
||||
- Formula: `Market Cap / Revenue`
|
||||
|
||||
5. EV
|
||||
- Formula: `Market Cap + Total Debt - Cash`
|
||||
|
||||
6. EV/EBITDA
|
||||
- Formula: `EV / EBITDA`
|
||||
|
||||
7. EV/Sales
|
||||
- Formula: `EV / Revenue`
|
||||
|
||||
## 4. Leverage and Liquidity
|
||||
|
||||
1. Debt-to-Equity
|
||||
- Formula: `Total Debt / Total Equity`
|
||||
|
||||
2. Interest Coverage
|
||||
- Formula: `EBIT / Interest Expense`
|
||||
|
||||
3. Current Ratio
|
||||
- Formula: `Current Assets / Current Liabilities`
|
||||
|
||||
4. Quick Ratio
|
||||
- Formula: `(Current Assets - Inventory) / Current Liabilities`
|
||||
|
||||
## 5. Cash Flow Quality
|
||||
|
||||
1. Free Cash Flow (FCF)
|
||||
- Formula: `Operating Cash Flow - Capital Expenditures`
|
||||
|
||||
2. FCF Yield
|
||||
- Formula: `FCF Per Share / Price * 100%`
|
||||
|
||||
3. Cash Conversion
|
||||
- Formula: `FCF / Net Income`
|
||||
|
||||
## 6. Interpretation Guidance
|
||||
|
||||
1. Always compare metrics against:
|
||||
- Company historical range
|
||||
- Sector and direct peers
|
||||
- Current macro regime
|
||||
|
||||
2. Avoid single-metric conclusions.
|
||||
3. Flag where accounting standards or reporting cadence reduce cross-market comparability.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Fundamental Analysis Reference
|
||||
|
||||
Use this framework in both `daily` and `full_report` modes. Keep outputs concise unless full report is requested.
|
||||
|
||||
## 1. Business Quality
|
||||
|
||||
Assess:
|
||||
|
||||
1. Moat quality:
|
||||
- Brand, network effects, switching costs, cost advantage, IP/regulatory barriers.
|
||||
|
||||
2. Management quality:
|
||||
- Capital allocation discipline, communication quality, execution consistency.
|
||||
|
||||
3. Business model durability:
|
||||
- Revenue concentration, customer concentration, geographic risk, pricing power.
|
||||
|
||||
## 2. Financial Health
|
||||
|
||||
Focus areas:
|
||||
|
||||
1. Profitability trend:
|
||||
- Gross, operating, net margin direction.
|
||||
|
||||
2. Growth quality:
|
||||
- Revenue and earnings growth consistency, segment contribution quality.
|
||||
|
||||
3. Balance sheet:
|
||||
- Debt burden, liquidity, refinancing risk, cash buffer.
|
||||
|
||||
4. Cash flow quality:
|
||||
- OCF consistency, FCF conversion, capex intensity.
|
||||
|
||||
## 3. Valuation Lens
|
||||
|
||||
Use multiple perspectives:
|
||||
|
||||
1. Relative multiples:
|
||||
- P/E, PEG, P/B, P/S, EV/EBITDA, EV/Sales.
|
||||
|
||||
2. Historical range context:
|
||||
- Current valuation vs own history.
|
||||
|
||||
3. Peer context:
|
||||
- Premium/discount vs direct peers and rationale.
|
||||
|
||||
## 4. Risk Framework
|
||||
|
||||
Map risks by category:
|
||||
|
||||
1. Company-specific:
|
||||
- Product concentration, customer concentration, execution risk, governance issues.
|
||||
|
||||
2. Market/macro:
|
||||
- Rate sensitivity, FX exposure, commodity sensitivity, policy risk.
|
||||
|
||||
3. Event risk:
|
||||
- Earnings, regulatory approvals, legal actions, financing events.
|
||||
|
||||
## 5. Market-Specific Notes (US/CN/HK)
|
||||
|
||||
1. Data depth may vary by market and language.
|
||||
2. Prefer exchange filings and official disclosure portals in each market.
|
||||
3. Align accounting period labels and fiscal calendars before comparison.
|
||||
4. Flag where metric comparability is limited.
|
||||
|
||||
## 6. Output Guidance
|
||||
|
||||
For daily mode, include:
|
||||
|
||||
- 2-3 key fundamental drivers
|
||||
- 1-2 valuation signals
|
||||
- top downside risks
|
||||
|
||||
For full report mode, include full multi-year trend tables and peer comparison.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Metrics Definition
|
||||
|
||||
Use these definitions consistently across all reports.
|
||||
|
||||
## 1. Core Error Metrics
|
||||
|
||||
Let:
|
||||
|
||||
- `pred` = predicted close for target session
|
||||
- `actual` = official actual close for that session
|
||||
|
||||
Compute:
|
||||
|
||||
- Absolute Error (AE): `|pred - actual|`
|
||||
- Absolute Percentage Error (APE): `|pred - actual| / actual * 100%`
|
||||
|
||||
## 2. Hit Criteria
|
||||
|
||||
Report two hit criteria in parallel:
|
||||
|
||||
- Strict hit: `APE <= 1%`
|
||||
- Loose hit: `APE <= 2%`
|
||||
|
||||
These thresholds are the default correctness criteria for predicted close price.
|
||||
|
||||
## 3. Rolling Accuracy Windows
|
||||
|
||||
For each window `W` (1d, 3d, 7d, 30d, custom):
|
||||
|
||||
- `strict_accuracy_W = strict_hits_W / n_W`
|
||||
- `loose_accuracy_W = loose_hits_W / n_W`
|
||||
|
||||
Where `n_W` is number of valid forecast/actual pairs in that window.
|
||||
|
||||
## 4. Optional Direction Accuracy
|
||||
|
||||
Let direction be sign of close-to-close return.
|
||||
|
||||
- Direction hit if predicted direction equals realized direction.
|
||||
- `direction_accuracy_W = direction_hits_W / n_W`
|
||||
|
||||
Use only when direction labels are explicitly available.
|
||||
|
||||
## 5. Forecast Correctness Score (Optional)
|
||||
|
||||
For a single forecast, you may map APE to a score:
|
||||
|
||||
- `correctness_score = max(0, 100 - 50 * APE_percent)`
|
||||
|
||||
Examples:
|
||||
|
||||
- `APE = 0.8%` -> score `60`
|
||||
- `APE = 1.5%` -> score `25`
|
||||
- `APE >= 2.0%` -> score `0` (or near 0)
|
||||
|
||||
## 6. Sample Size and Insufficient Data Rules
|
||||
|
||||
1. Never pad missing samples.
|
||||
2. If `n_W = 0`, output `N/A` for the window.
|
||||
3. If `0 < n_W < target_window_size`, output partial result and annotate as partial.
|
||||
4. Always display `n_W` beside each window metric.
|
||||
|
||||
## 7. Adjustment and Comparability Rules
|
||||
|
||||
1. Prefer adjusted price series when corporate actions materially affect comparability.
|
||||
2. If non-adjusted close is used, state it explicitly.
|
||||
3. Keep forecast and actual on the same price basis.
|
||||
|
||||
## 8. Improvement Trend Metrics
|
||||
|
||||
Track whether forecast quality is improving over time:
|
||||
|
||||
1. `delta_APE_7d_vs_prev7d`
|
||||
- Difference between current 7-day average APE and previous 7-day average APE.
|
||||
|
||||
2. `delta_strict_hit_rate_7d`
|
||||
- Change in strict hit rate versus previous 7-day block.
|
||||
|
||||
3. `trend_label`
|
||||
- `improving`, `stable`, or `degrading` based on combined delta signals.
|
||||
|
||||
## 9. Reporting Format (Minimum)
|
||||
|
||||
Every report should include:
|
||||
|
||||
1. Prior-session review row:
|
||||
- `prev_pred_close_t1`, `prev_actual_close_t1`, `AE`, `APE`, strict/loose hit status
|
||||
|
||||
2. Rolling table with at least:
|
||||
- 1d, 3d, 7d, 30d, optional custom
|
||||
- strict accuracy, loose accuracy, optional direction accuracy
|
||||
- sample size `n`
|
||||
|
||||
3. One-line interpretation:
|
||||
- whether model performance is improving, stable, or degrading
|
||||
|
||||
4. Improvement block:
|
||||
- what changed from review
|
||||
- what will be adjusted in next run
|
||||
@@ -0,0 +1,33 @@
|
||||
# Minimal Compatibility Mode
|
||||
|
||||
Use this mode when Python scripts are unavailable or model capability is limited.
|
||||
|
||||
## Goal
|
||||
|
||||
Maximize success rate and correctness with minimal token and logic complexity.
|
||||
|
||||
## Rules
|
||||
|
||||
1. Read at most 3 recent reports for the same ticker.
|
||||
2. Use minimal sources:
|
||||
- one official disclosure source
|
||||
- one reliable market data source (Yahoo Finance acceptable)
|
||||
3. Keep output short and deterministic.
|
||||
4. Still include one self-improvement action from prior misses.
|
||||
|
||||
## Minimal Output Schema
|
||||
|
||||
- `recommendation`: Buy/Hold/Sell/Watch
|
||||
- `pred_close_t1`: point estimate
|
||||
- `prev_pred_close_t1`: if available, else `N/A`
|
||||
- `prev_actual_close_t1`: if available, else `N/A/pending`
|
||||
- `AE`, `APE`: if available, else `N/A`
|
||||
- `improvement_actions`: exactly 1 item
|
||||
- `status`: `ok|pending_data|blocked`
|
||||
|
||||
## Minimal Source Checklist
|
||||
|
||||
1. Official disclosure page (exchange/regulator/IR)
|
||||
2. Market quote page (for example Yahoo Finance quote)
|
||||
|
||||
If the two sources conflict on critical values, set confidence to `Low`.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Report Template (Strict)
|
||||
|
||||
Use this template exactly. Keep key names unchanged for downstream parsing.
|
||||
|
||||
```markdown
|
||||
---
|
||||
version: 1
|
||||
run_date: <YYYY-MM-DD>
|
||||
run_time_local: <YYYY-MM-DD HH:mm TZ>
|
||||
mode: <daily|daily_minimal|full_report>
|
||||
ticker: <TICKER>
|
||||
exchange: <EXCHANGE>
|
||||
market: <TEXT>
|
||||
report_dir: <working_directory>/daily-stock-analysis/reports/
|
||||
output_file: <YYYY-MM-DD-TICKER-analysis.md or -vN.md>
|
||||
report_versioning_mode: <overwrite|new_version>
|
||||
history_window_days: <N>
|
||||
|
||||
recommendation: <Buy|Hold|Sell|Watch>
|
||||
recommendation_triggers: <ENTRY/EXIT/INVALIDATION SUMMARY>
|
||||
|
||||
pred_close_t1: <NUMBER>
|
||||
pred_range_t1: <LOW-HIGH or N/A>
|
||||
pred_confidence: <High|Medium|Low>
|
||||
pred_assumptions: <SHORT TEXT>
|
||||
|
||||
prev_pred_close_t1: <NUMBER or N/A>
|
||||
prev_actual_close_t1: <NUMBER or pending or N/A>
|
||||
AE: <NUMBER or N/A>
|
||||
APE: <PERCENT or N/A>
|
||||
strict_hit: <true|false|N/A>
|
||||
loose_hit: <true|false|N/A>
|
||||
|
||||
acc_1d_strict: <PERCENT or N/A>
|
||||
acc_1d_loose: <PERCENT or N/A>
|
||||
acc_3d_strict: <PERCENT or N/A>
|
||||
acc_3d_loose: <PERCENT or N/A>
|
||||
acc_7d_strict: <PERCENT or N/A>
|
||||
acc_7d_loose: <PERCENT or N/A>
|
||||
acc_30d_strict: <PERCENT or N/A>
|
||||
acc_30d_loose: <PERCENT or N/A>
|
||||
acc_custom_strict: <PERCENT or N/A>
|
||||
acc_custom_loose: <PERCENT or N/A>
|
||||
|
||||
improvement_actions:
|
||||
- <ACTION_1>
|
||||
- <ACTION_2 or N/A>
|
||||
- <ACTION_3 or N/A>
|
||||
status: <ok|pending_data|blocked>
|
||||
status_note: <SHORT TEXT>
|
||||
---
|
||||
|
||||
# Daily Stock Analysis - <TICKER> (<EXCHANGE>)
|
||||
|
||||
## 1) Market Snapshot
|
||||
- Last/Close: <VALUE>
|
||||
- Session Range: <LOW-HIGH>
|
||||
- Volume/Volatility: <SUMMARY>
|
||||
- Thesis: <BULLISH/NEUTRAL/BEARISH + concise rationale>
|
||||
|
||||
## 2) Recommendation
|
||||
- Recommendation: <Buy/Hold/Sell/Watch>
|
||||
- Trigger Conditions: <ENTRY/EXIT/INVALIDATION>
|
||||
- Risk Controls: <SHORT TEXT>
|
||||
|
||||
## 3) Next Trading Day Prediction
|
||||
- Point Forecast: <pred_close_t1>
|
||||
- Range: <pred_range_t1>
|
||||
- Confidence: <pred_confidence>
|
||||
- Assumptions: <pred_assumptions>
|
||||
|
||||
## 4) Prior Forecast Review
|
||||
- Previous Forecast: <prev_pred_close_t1>
|
||||
- Actual Close: <prev_actual_close_t1>
|
||||
- AE / APE: <AE> / <APE>
|
||||
- Attribution: <WHY HIT OR MISS>
|
||||
|
||||
## 5) Rolling Accuracy
|
||||
| Window | Strict | Loose |
|
||||
|---|---:|---:|
|
||||
| 1d | <acc_1d_strict> | <acc_1d_loose> |
|
||||
| 3d | <acc_3d_strict> | <acc_3d_loose> |
|
||||
| 7d | <acc_7d_strict> | <acc_7d_loose> |
|
||||
| 30d | <acc_30d_strict> | <acc_30d_loose> |
|
||||
| Custom | <acc_custom_strict> | <acc_custom_loose> |
|
||||
|
||||
## 6) Self-Improvement Actions for Next Run
|
||||
1. <ACTION_1>
|
||||
2. <ACTION_2 or N/A>
|
||||
3. <ACTION_3 or N/A>
|
||||
|
||||
## 7) Sources (with timestamp)
|
||||
- <SOURCE_1>
|
||||
- <SOURCE_2>
|
||||
- <SOURCE_3>
|
||||
|
||||
## 8) Disclaimer
|
||||
This content is for research and informational purposes only and does not constitute investment advice or a return guarantee. Markets are risky; invest with caution.
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Search Query Templates (Concise)
|
||||
|
||||
Use these templates with search engines and `site:` filters.
|
||||
|
||||
Detailed source list is in `references/sources.md`.
|
||||
|
||||
## 1) Identity and Listing
|
||||
|
||||
- `<COMPANY> ticker symbol exchange`
|
||||
- `<TICKER> exchange listing market`
|
||||
|
||||
## 2) Official Filings and Disclosures
|
||||
|
||||
- `<TICKER> official filings latest`
|
||||
- `<COMPANY> investor relations latest release`
|
||||
- `site:sec.gov <TICKER> 10-Q OR 10-K OR 8-K`
|
||||
- `site:hkexnews.hk <TICKER> announcement`
|
||||
- `site:sse.com.cn <TICKER> 公告`
|
||||
- `site:szse.cn <TICKER> 公告`
|
||||
|
||||
## 3) Market Data and Price Context
|
||||
|
||||
- `site:finance.yahoo.com <TICKER> quote`
|
||||
- `<TICKER> latest close open high low volume`
|
||||
- `<TICKER> 52 week range market cap`
|
||||
|
||||
## 4) News and Analyst Context
|
||||
|
||||
- `site:reuters.com <TICKER> earnings guidance`
|
||||
- `site:bloomberg.com <TICKER> stock news`
|
||||
- `<TICKER> analyst rating target price`
|
||||
|
||||
## 5) Technical Context
|
||||
|
||||
- `<TICKER> RSI MACD moving average`
|
||||
- `<TICKER> support resistance trend`
|
||||
|
||||
## 6) Macro Context (if used in thesis)
|
||||
|
||||
- `<MARKET> benchmark index today`
|
||||
- `<MARKET> central bank policy rate outlook`
|
||||
- `US 10Y yield today`
|
||||
|
||||
## Data Quality Rules
|
||||
|
||||
1. Prefer Tier-1 official sources first.
|
||||
2. Cross-check critical values with two independent sources.
|
||||
3. Record source URL and timestamp in report.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Security and Privacy Rules
|
||||
|
||||
These rules are mandatory for both script mode and compatibility mode.
|
||||
|
||||
## Scope Control
|
||||
|
||||
1. Operate only under `working_directory`.
|
||||
2. Do not read, move, or write files outside `working_directory`.
|
||||
3. Do not follow symlinks when scanning report files.
|
||||
|
||||
## Data Minimization
|
||||
|
||||
1. Read only report files matching:
|
||||
- `YYYY-MM-DD-<TICKER>-analysis.md`
|
||||
- `YYYY-MM-DD-<TICKER>-analysis-vN.md`
|
||||
2. Parse only required metadata fields.
|
||||
3. Cap historical reads:
|
||||
- script mode default: 5 files
|
||||
- compatibility mode: 3 files
|
||||
|
||||
## Script Safety
|
||||
|
||||
1. Scripts are local-file utilities only; no network calls.
|
||||
2. Migration is explicit and non-destructive:
|
||||
- move only user-confirmed files
|
||||
- skip when target already exists
|
||||
3. If a safety check fails, return `blocked` with reason.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Authoritative Information Sources
|
||||
|
||||
Use search engines with `site:` filters to prioritize authoritative sources.
|
||||
|
||||
## Source Priority
|
||||
|
||||
1. Tier 1 (Primary / official)
|
||||
- Exchange and regulator disclosures
|
||||
- Company investor-relations pages
|
||||
- Official macro data publishers
|
||||
|
||||
2. Tier 2 (High-quality financial media/data)
|
||||
- Yahoo Finance, Reuters, Bloomberg, WSJ, CNBC, MarketWatch
|
||||
|
||||
3. Tier 3 (Supporting context)
|
||||
- TradingView, StockCharts, sector/ETF summaries
|
||||
|
||||
For critical values (close price, earnings, guidance, major filings), cross-check with at least two independent sources.
|
||||
|
||||
## Tier 1: Exchange and Regulatory Sources
|
||||
|
||||
### Global baseline
|
||||
- Company Investor Relations pages
|
||||
- Official exchange announcements for the ticker listing venue
|
||||
|
||||
### United States
|
||||
- SEC EDGAR: [https://www.sec.gov/edgar/searchedgar/companysearch](https://www.sec.gov/edgar/searchedgar/companysearch)
|
||||
- Nasdaq company pages: [https://www.nasdaq.com](https://www.nasdaq.com)
|
||||
- NYSE company pages: [https://www.nyse.com](https://www.nyse.com)
|
||||
|
||||
### Hong Kong
|
||||
- HKEXnews: [https://www.hkexnews.hk](https://www.hkexnews.hk)
|
||||
|
||||
### Mainland China
|
||||
- SSE disclosures: [https://www.sse.com.cn/disclosure/](https://www.sse.com.cn/disclosure/)
|
||||
- SZSE disclosures: [https://www.szse.cn/disclosure/](https://www.szse.cn/disclosure/)
|
||||
|
||||
### Japan
|
||||
- JPX: [https://www.jpx.co.jp/english/](https://www.jpx.co.jp/english/)
|
||||
- TDnet (Timely Disclosure): [https://www.release.tdnet.info/](https://www.release.tdnet.info/)
|
||||
|
||||
### United Kingdom
|
||||
- London Stock Exchange RNS: [https://www.londonstockexchange.com/news](https://www.londonstockexchange.com/news)
|
||||
|
||||
### Europe (multi-country)
|
||||
- Euronext news/disclosures: [https://live.euronext.com/en/markets](https://live.euronext.com/en/markets)
|
||||
|
||||
## Tier 2: High-Quality Financial Data and News
|
||||
|
||||
- Yahoo Finance: [https://finance.yahoo.com](https://finance.yahoo.com)
|
||||
- Reuters Markets: [https://www.reuters.com/markets/](https://www.reuters.com/markets/)
|
||||
- Bloomberg Markets: [https://www.bloomberg.com/markets](https://www.bloomberg.com/markets)
|
||||
- WSJ Markets: [https://www.wsj.com/market-data](https://www.wsj.com/market-data)
|
||||
- CNBC Markets: [https://www.cnbc.com/markets/](https://www.cnbc.com/markets/)
|
||||
- MarketWatch: [https://www.marketwatch.com](https://www.marketwatch.com)
|
||||
- Morningstar (supporting valuation context): [https://www.morningstar.com](https://www.morningstar.com)
|
||||
|
||||
## Tier 1 Macro Data (for market regime context)
|
||||
|
||||
### United States
|
||||
- U.S. Treasury rates: [https://home.treasury.gov](https://home.treasury.gov)
|
||||
- Federal Reserve (FRED): [https://fred.stlouisfed.org](https://fred.stlouisfed.org)
|
||||
- BLS: [https://www.bls.gov](https://www.bls.gov)
|
||||
- BEA: [https://www.bea.gov](https://www.bea.gov)
|
||||
|
||||
### Global
|
||||
- IMF Data: [https://www.imf.org/en/Data](https://www.imf.org/en/Data)
|
||||
- World Bank Data: [https://data.worldbank.org](https://data.worldbank.org)
|
||||
- ECB: [https://www.ecb.europa.eu](https://www.ecb.europa.eu)
|
||||
- BoE: [https://www.bankofengland.co.uk](https://www.bankofengland.co.uk)
|
||||
- BoJ: [https://www.boj.or.jp/en/](https://www.boj.or.jp/en/)
|
||||
|
||||
## Technical/Charting Support (Tier 3)
|
||||
|
||||
- TradingView: [https://www.tradingview.com](https://www.tradingview.com)
|
||||
- StockCharts: [https://stockcharts.com](https://stockcharts.com)
|
||||
|
||||
## Search Engine Patterns
|
||||
|
||||
Use search engines with domain filters:
|
||||
|
||||
- `site:finance.yahoo.com <TICKER> quote`
|
||||
- `site:reuters.com <TICKER> earnings`
|
||||
- `site:sec.gov <TICKER> 10-Q`
|
||||
- `site:hkexnews.hk <TICKER> announcement`
|
||||
- `site:sse.com.cn <TICKER> 公告`
|
||||
- `site:szse.cn <TICKER> 公告`
|
||||
- `site:investor.<company-domain> earnings release`
|
||||
|
||||
## Minimum Source Set Per Run
|
||||
|
||||
At least include:
|
||||
|
||||
1. One Tier-1 disclosure source
|
||||
2. One Tier-2 market data source (Yahoo Finance is acceptable baseline)
|
||||
3. One Tier-2/Tier-1 news source
|
||||
4. One macro source if macro is cited in thesis
|
||||
|
||||
## Compatibility Mode Minimum Source Set
|
||||
|
||||
When running in minimal compatibility mode, use:
|
||||
|
||||
1. One Tier-1 disclosure source
|
||||
2. One Tier-2 market data source (Yahoo Finance acceptable)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Technical Analysis Reference
|
||||
|
||||
Use technical analysis as a decision support layer, not as a standalone certainty signal.
|
||||
|
||||
## 1. Trend Framework
|
||||
|
||||
1. Moving averages:
|
||||
- 20/50/200-period moving averages as baseline trend map.
|
||||
- Bullish regime: price above key moving averages with supportive slope.
|
||||
- Bearish regime: price below key moving averages with negative slope.
|
||||
|
||||
2. Trend strength:
|
||||
- Confirm with higher highs/higher lows (uptrend) or lower highs/lower lows (downtrend).
|
||||
|
||||
## 2. Momentum Framework
|
||||
|
||||
1. RSI:
|
||||
- Overbought > 70, oversold < 30.
|
||||
- Use divergence vs price as an early warning, not a standalone trigger.
|
||||
|
||||
2. MACD:
|
||||
- Track line/signal crossovers and histogram trend.
|
||||
- Prefer signals aligned with broader trend.
|
||||
|
||||
## 3. Volatility and Structure
|
||||
|
||||
1. ATR context:
|
||||
- Use ATR expansion/contraction to assess regime change risk.
|
||||
|
||||
2. Bollinger context:
|
||||
- Squeeze can precede expansion.
|
||||
- Band walk can persist in strong trends.
|
||||
|
||||
## 4. Support and Resistance
|
||||
|
||||
Map levels from:
|
||||
|
||||
- Recent swing highs/lows
|
||||
- Volume clusters
|
||||
- Moving-average confluence
|
||||
- Psychological round numbers
|
||||
|
||||
Use level breaks with volume confirmation where possible.
|
||||
|
||||
## 5. Volume Confirmation
|
||||
|
||||
1. Breakout quality improves with volume expansion.
|
||||
2. Weak volume breakouts carry higher failure risk.
|
||||
3. Divergence between price trend and volume trend can signal exhaustion.
|
||||
|
||||
## 6. Multi-Timeframe Alignment
|
||||
|
||||
1. Use higher timeframe (weekly/daily) for primary bias.
|
||||
2. Use lower timeframe (daily/intraday where available) for timing.
|
||||
3. Do not let lower timeframe noise override higher timeframe structure without strong evidence.
|
||||
|
||||
## 7. Signal Quality Rules
|
||||
|
||||
1. Require at least two independent confirmations before strong directional calls.
|
||||
2. Mark low-confidence calls when indicators conflict.
|
||||
3. Define invalidation level for every directional view.
|
||||
|
||||
## 8. Market-Specific Notes (US/CN/HK)
|
||||
|
||||
1. Liquidity and session structure differ by market.
|
||||
2. Gap behavior and close auction effects may vary.
|
||||
3. Technical indicator reliability can degrade during event-driven sessions.
|
||||
|
||||
## 9. Daily Output Guidance
|
||||
|
||||
At minimum provide:
|
||||
|
||||
- Trend state (bullish/neutral/bearish)
|
||||
- 2-3 key levels
|
||||
- RSI/MACD summary
|
||||
- volume confirmation status
|
||||
- invalidation condition
|
||||
@@ -0,0 +1,104 @@
|
||||
# Workflow (Command-First)
|
||||
|
||||
Use this sequence exactly.
|
||||
|
||||
## 0) Choose Execution Mode
|
||||
|
||||
Use `script` mode by default.
|
||||
|
||||
Switch to `compatibility` mode when:
|
||||
|
||||
- `python3` is unavailable, or
|
||||
- model capability is low and deterministic minimal output is preferred.
|
||||
|
||||
## 1) Resolve Instrument
|
||||
|
||||
- Resolve `ticker`, `exchange`, `market`, and next valid trading day.
|
||||
- If ticker is ambiguous, stop and ask user.
|
||||
|
||||
## 2) Plan Files and History
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/report_manager.py plan \
|
||||
--workdir <working_directory> \
|
||||
--ticker <TICKER> \
|
||||
--run-date <YYYY-MM-DD> \
|
||||
--versioning auto \
|
||||
--history-limit 5
|
||||
```
|
||||
|
||||
Use returned JSON fields:
|
||||
|
||||
- `selected_output_file`
|
||||
- `requires_user_choice`
|
||||
- `history_files`
|
||||
- `legacy_files`
|
||||
|
||||
If `requires_user_choice=true`, ask user `overwrite` vs `new_version`.
|
||||
Read only `history_files` returned by script (default max 5).
|
||||
|
||||
## 3) Legacy Compatibility (Optional Migration)
|
||||
|
||||
- Read legacy files from `legacy_files` for review history.
|
||||
- If user agrees to migrate, run:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/report_manager.py migrate \
|
||||
--workdir <working_directory> \
|
||||
--file <ABS_PATH_1> --file <ABS_PATH_2>
|
||||
```
|
||||
|
||||
Security: only process files under `working_directory`.
|
||||
|
||||
## 4) Collect New Data
|
||||
|
||||
- Use `references/sources.md` tier priority.
|
||||
- Use `references/search_queries.md` templates.
|
||||
- For critical values, cross-check with at least 2 sources.
|
||||
|
||||
## 5) Compute Accuracy via Script
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/calc_accuracy.py \
|
||||
--workdir <working_directory> \
|
||||
--ticker <TICKER> \
|
||||
--windows 1,3,7,30 \
|
||||
--history-limit 60
|
||||
```
|
||||
|
||||
Use script output to fill rolling accuracy fields.
|
||||
|
||||
## 6) Generate Report
|
||||
|
||||
- Render with `references/report_template.md`.
|
||||
- Keep all required frontmatter keys.
|
||||
- Include `improvement_actions`.
|
||||
|
||||
## 7) Persist and Return
|
||||
|
||||
- Save to `selected_output_file` from step 2.
|
||||
- Return summary + absolute file path + pending/blocked status.
|
||||
|
||||
## 8) Recommended Operation
|
||||
|
||||
Use recurring weekday schedule to stabilize review windows and success rate.
|
||||
|
||||
## Compatibility Mode (Fallback)
|
||||
|
||||
When scripts cannot run:
|
||||
|
||||
1. Manually locate report files only under `working_directory`.
|
||||
2. Read at most 3 recent same-ticker reports.
|
||||
3. Collect minimal sources:
|
||||
- one official disclosure source
|
||||
- one reliable market data source
|
||||
4. Produce minimal output:
|
||||
- recommendation
|
||||
- `pred_close_t1`
|
||||
- prior review metrics (if available)
|
||||
- one `improvement_action`
|
||||
5. Save report in canonical reports directory using standard filename rules.
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared helpers for daily-stock-analysis scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
FILENAME_RE = re.compile(
|
||||
r"^(?P<run_date>\d{4}-\d{2}-\d{2})-(?P<ticker>[A-Za-z0-9._-]+)-analysis(?:-v(?P<version>\d+))?\.md$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReportFile:
|
||||
path: str
|
||||
run_date: str
|
||||
ticker: str
|
||||
version: int
|
||||
in_canonical_dir: bool
|
||||
|
||||
|
||||
def canonical_reports_dir(workdir: str) -> str:
|
||||
return os.path.join(os.path.abspath(workdir), "daily-stock-analysis", "reports")
|
||||
|
||||
|
||||
def compatible_dirs(workdir: str) -> List[str]:
|
||||
root = os.path.abspath(workdir)
|
||||
return [
|
||||
canonical_reports_dir(root),
|
||||
os.path.join(root, "daily-stock-analysis"),
|
||||
root,
|
||||
]
|
||||
|
||||
|
||||
def is_within_workdir(path: str, workdir: str) -> bool:
|
||||
root = os.path.realpath(os.path.abspath(workdir))
|
||||
target = os.path.realpath(os.path.abspath(path))
|
||||
return target == root or target.startswith(root + os.sep)
|
||||
|
||||
|
||||
def parse_filename(name: str) -> Optional[Dict[str, str]]:
|
||||
match = FILENAME_RE.match(name)
|
||||
if not match:
|
||||
return None
|
||||
return {
|
||||
"run_date": match.group("run_date"),
|
||||
"ticker": match.group("ticker").upper(),
|
||||
"version": str(int(match.group("version") or "1")),
|
||||
}
|
||||
|
||||
|
||||
def discover_reports(workdir: str, ticker: str) -> List[ReportFile]:
|
||||
root = os.path.abspath(workdir)
|
||||
ticker_upper = ticker.upper()
|
||||
canonical_dir = canonical_reports_dir(root)
|
||||
seen = set()
|
||||
records: List[ReportFile] = []
|
||||
|
||||
for directory in compatible_dirs(root):
|
||||
if not is_within_workdir(directory, root):
|
||||
continue
|
||||
if not os.path.isdir(directory):
|
||||
continue
|
||||
for entry in os.scandir(directory):
|
||||
# Never follow symlinks for safety/privacy.
|
||||
if not entry.is_file(follow_symlinks=False):
|
||||
continue
|
||||
parsed = parse_filename(entry.name)
|
||||
if not parsed:
|
||||
continue
|
||||
if parsed["ticker"] != ticker_upper:
|
||||
continue
|
||||
abs_path = os.path.abspath(entry.path)
|
||||
real_path = os.path.realpath(abs_path)
|
||||
if real_path in seen:
|
||||
continue
|
||||
seen.add(real_path)
|
||||
records.append(
|
||||
ReportFile(
|
||||
path=abs_path,
|
||||
run_date=parsed["run_date"],
|
||||
ticker=parsed["ticker"],
|
||||
version=int(parsed["version"]),
|
||||
in_canonical_dir=os.path.dirname(abs_path) == canonical_dir,
|
||||
)
|
||||
)
|
||||
|
||||
def sort_key(record: ReportFile):
|
||||
try:
|
||||
d = date.fromisoformat(record.run_date)
|
||||
except ValueError:
|
||||
d = date.min
|
||||
return (d, record.version, 1 if record.in_canonical_dir else 0)
|
||||
|
||||
return sorted(records, key=sort_key, reverse=True)
|
||||
|
||||
|
||||
def read_frontmatter(path: str) -> Dict[str, str]:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
first_line = f.readline()
|
||||
if first_line.strip() != "---":
|
||||
return {}
|
||||
|
||||
# Read only a bounded header section to avoid loading large files.
|
||||
frontmatter: Dict[str, str] = {}
|
||||
total_chars = len(first_line)
|
||||
for _ in range(200):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
total_chars += len(line)
|
||||
if total_chars > 64 * 1024:
|
||||
break
|
||||
raw = line.rstrip("\n")
|
||||
if raw.strip() == "---":
|
||||
break
|
||||
if not raw.strip():
|
||||
continue
|
||||
if raw.startswith(" - "):
|
||||
continue
|
||||
if ":" not in raw:
|
||||
continue
|
||||
key, value = raw.split(":", 1)
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
return frontmatter
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def parse_float(value: Optional[str]) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return None
|
||||
if text.upper() in {"N/A", "NA", "NONE", "NULL", "PENDING"}:
|
||||
return None
|
||||
text = text.replace(",", "")
|
||||
if text.endswith("%"):
|
||||
text = text[:-1]
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_bool(value: Optional[str]) -> Optional[bool]:
|
||||
if value is None:
|
||||
return None
|
||||
text = value.strip().lower()
|
||||
if text in {"true", "yes", "1"}:
|
||||
return True
|
||||
if text in {"false", "no", "0"}:
|
||||
return False
|
||||
return None
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compute rolling forecast accuracy from existing report files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from statistics import mean
|
||||
from typing import Dict, List
|
||||
|
||||
from _report_utils import discover_reports, parse_bool, parse_float, read_frontmatter
|
||||
|
||||
|
||||
def _window_list(text: str) -> List[int]:
|
||||
windows = []
|
||||
for item in text.split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
value = int(item)
|
||||
if value <= 0:
|
||||
continue
|
||||
if value not in windows:
|
||||
windows.append(value)
|
||||
return windows or [1, 3, 7, 30]
|
||||
|
||||
|
||||
def _build_review_rows(workdir: str, ticker: str, history_limit: int) -> List[Dict[str, object]]:
|
||||
reports = discover_reports(workdir, ticker)[:history_limit]
|
||||
rows: List[Dict[str, object]] = []
|
||||
seen_run_date = set()
|
||||
|
||||
for report in reports:
|
||||
# Keep the newest report for each run_date to avoid same-day duplicate counting.
|
||||
if report.run_date in seen_run_date:
|
||||
continue
|
||||
frontmatter = read_frontmatter(report.path)
|
||||
ape = parse_float(frontmatter.get("APE"))
|
||||
strict = parse_bool(frontmatter.get("strict_hit"))
|
||||
loose = parse_bool(frontmatter.get("loose_hit"))
|
||||
|
||||
if strict is None and ape is not None:
|
||||
strict = ape <= 1.0
|
||||
if loose is None and ape is not None:
|
||||
loose = ape <= 2.0
|
||||
|
||||
if ape is None and strict is None and loose is None:
|
||||
continue
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"run_date": report.run_date,
|
||||
"path": report.path,
|
||||
"ape": ape,
|
||||
"strict_hit": strict,
|
||||
"loose_hit": loose,
|
||||
}
|
||||
)
|
||||
seen_run_date.add(report.run_date)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def _rate(hit_count: int, total: int):
|
||||
if total == 0:
|
||||
return None
|
||||
return round(hit_count * 100.0 / total, 2)
|
||||
|
||||
|
||||
def compute_accuracy(workdir: str, ticker: str, windows: List[int], history_limit: int) -> Dict[str, object]:
|
||||
rows = _build_review_rows(workdir, ticker, history_limit)
|
||||
metrics = {}
|
||||
|
||||
for window in windows:
|
||||
sample = rows[:window]
|
||||
n = len(sample)
|
||||
strict_hits = sum(1 for r in sample if r["strict_hit"] is True)
|
||||
loose_hits = sum(1 for r in sample if r["loose_hit"] is True)
|
||||
ape_values = [r["ape"] for r in sample if isinstance(r["ape"], float)]
|
||||
metrics[str(window)] = {
|
||||
"n": n,
|
||||
"strict_rate_percent": _rate(strict_hits, n),
|
||||
"loose_rate_percent": _rate(loose_hits, n),
|
||||
"avg_ape_percent": round(mean(ape_values), 4) if ape_values else None,
|
||||
}
|
||||
|
||||
latest = rows[0] if rows else None
|
||||
return {
|
||||
"ticker": ticker.upper(),
|
||||
"workdir": os.path.abspath(workdir),
|
||||
"windows": metrics,
|
||||
"review_samples": len(rows),
|
||||
"latest_review": latest,
|
||||
"status": "ok" if rows else "insufficient_history",
|
||||
"security_scope": "working_directory_only",
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Calculate rolling forecast accuracy.")
|
||||
parser.add_argument("--workdir", default=os.getcwd())
|
||||
parser.add_argument("--ticker", required=True)
|
||||
parser.add_argument("--windows", default="1,3,7,30")
|
||||
parser.add_argument("--history-limit", type=int, default=60)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
result = compute_accuracy(
|
||||
workdir=args.workdir,
|
||||
ticker=args.ticker,
|
||||
windows=_window_list(args.windows),
|
||||
history_limit=max(args.history_limit, 1),
|
||||
)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic report path and migration manager for daily-stock-analysis."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from datetime import date
|
||||
from typing import Dict, List
|
||||
|
||||
from _report_utils import (
|
||||
FILENAME_RE,
|
||||
canonical_reports_dir,
|
||||
discover_reports,
|
||||
is_within_workdir,
|
||||
)
|
||||
|
||||
|
||||
def _same_day_versions_in_canonical(
|
||||
reports: List, reports_dir: str, run_date: str, ticker_upper: str
|
||||
) -> List[int]:
|
||||
versions = []
|
||||
for report in reports:
|
||||
if report.run_date != run_date:
|
||||
continue
|
||||
if report.ticker != ticker_upper:
|
||||
continue
|
||||
if os.path.dirname(report.path) != reports_dir:
|
||||
continue
|
||||
versions.append(report.version)
|
||||
return versions
|
||||
|
||||
|
||||
def plan_output(
|
||||
workdir: str,
|
||||
ticker: str,
|
||||
run_date: str,
|
||||
versioning: str,
|
||||
unattended: bool,
|
||||
history_limit: int,
|
||||
) -> Dict[str, object]:
|
||||
root = os.path.abspath(workdir)
|
||||
ticker_upper = ticker.upper()
|
||||
reports_dir = canonical_reports_dir(root)
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
reports = discover_reports(root, ticker_upper)
|
||||
history_files = [r.path for r in reports[:history_limit]]
|
||||
legacy_files = [r.path for r in reports if not r.in_canonical_dir]
|
||||
|
||||
base_name = f"{run_date}-{ticker_upper}-analysis.md"
|
||||
base_path = os.path.join(reports_dir, base_name)
|
||||
base_exists = os.path.exists(base_path)
|
||||
|
||||
requires_user_choice = False
|
||||
selected_mode = "new_file"
|
||||
selected_path = base_path
|
||||
|
||||
if base_exists:
|
||||
if versioning == "overwrite":
|
||||
selected_mode = "overwrite"
|
||||
elif versioning == "new_version":
|
||||
selected_mode = "new_version"
|
||||
else:
|
||||
if unattended:
|
||||
selected_mode = "new_version"
|
||||
else:
|
||||
selected_mode = "new_version"
|
||||
requires_user_choice = True
|
||||
|
||||
if selected_mode == "new_version":
|
||||
versions = _same_day_versions_in_canonical(
|
||||
reports, reports_dir, run_date, ticker_upper
|
||||
)
|
||||
next_version = max(versions or [1]) + 1
|
||||
selected_path = os.path.join(
|
||||
reports_dir, f"{run_date}-{ticker_upper}-analysis-v{next_version}.md"
|
||||
)
|
||||
|
||||
return {
|
||||
"ticker": ticker_upper,
|
||||
"workdir": root,
|
||||
"reports_dir": reports_dir,
|
||||
"base_output_file": base_path,
|
||||
"selected_output_file": selected_path,
|
||||
"selected_versioning_mode": selected_mode,
|
||||
"requires_user_choice": requires_user_choice,
|
||||
"history_files": history_files,
|
||||
"legacy_files": legacy_files,
|
||||
"history_limit": history_limit,
|
||||
"security_scope": "working_directory_only",
|
||||
}
|
||||
|
||||
|
||||
def migrate_files(workdir: str, files: List[str]) -> Dict[str, object]:
|
||||
root = os.path.abspath(workdir)
|
||||
reports_dir = canonical_reports_dir(root)
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
moved = []
|
||||
skipped = []
|
||||
|
||||
for raw_path in files:
|
||||
src = os.path.abspath(raw_path)
|
||||
if not is_within_workdir(src, root):
|
||||
skipped.append({"file": src, "reason": "outside_workdir"})
|
||||
continue
|
||||
if not os.path.isfile(src):
|
||||
skipped.append({"file": src, "reason": "not_file"})
|
||||
continue
|
||||
if os.path.islink(src):
|
||||
skipped.append({"file": src, "reason": "symlink_not_allowed"})
|
||||
continue
|
||||
if not FILENAME_RE.match(os.path.basename(src)):
|
||||
skipped.append({"file": src, "reason": "filename_not_supported"})
|
||||
continue
|
||||
|
||||
dst = os.path.join(reports_dir, os.path.basename(src))
|
||||
if os.path.abspath(src) == os.path.abspath(dst):
|
||||
skipped.append({"file": src, "reason": "already_in_reports_dir"})
|
||||
continue
|
||||
|
||||
if os.path.exists(dst):
|
||||
# Keep migration deterministic and non-destructive.
|
||||
skipped.append({"file": src, "reason": "target_exists"})
|
||||
continue
|
||||
|
||||
try:
|
||||
shutil.move(src, dst)
|
||||
except OSError as exc:
|
||||
skipped.append({"file": src, "reason": f"move_failed:{exc}"})
|
||||
continue
|
||||
|
||||
moved.append({"from": src, "to": dst})
|
||||
|
||||
return {
|
||||
"reports_dir": reports_dir,
|
||||
"moved": moved,
|
||||
"skipped": skipped,
|
||||
"security_scope": "working_directory_only",
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Manage report paths and migrations.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
plan_parser = subparsers.add_parser("plan", help="Plan output path and history usage.")
|
||||
plan_parser.add_argument("--workdir", default=os.getcwd())
|
||||
plan_parser.add_argument("--ticker", required=True)
|
||||
plan_parser.add_argument("--run-date", default=date.today().isoformat())
|
||||
plan_parser.add_argument(
|
||||
"--versioning",
|
||||
choices=["auto", "overwrite", "new_version"],
|
||||
default="auto",
|
||||
)
|
||||
plan_parser.add_argument("--unattended", action="store_true")
|
||||
plan_parser.add_argument("--history-limit", type=int, default=5)
|
||||
|
||||
migrate_parser = subparsers.add_parser(
|
||||
"migrate", help="Move legacy report files into canonical reports directory."
|
||||
)
|
||||
migrate_parser.add_argument("--workdir", default=os.getcwd())
|
||||
migrate_parser.add_argument("--file", action="append", required=True)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
if args.command == "plan":
|
||||
result = plan_output(
|
||||
workdir=args.workdir,
|
||||
ticker=args.ticker,
|
||||
run_date=args.run_date,
|
||||
versioning=args.versioning,
|
||||
unattended=args.unattended,
|
||||
history_limit=max(args.history_limit, 1),
|
||||
)
|
||||
else:
|
||||
result = migrate_files(workdir=args.workdir, files=args.file)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user