* feat: add one-shot index analysis via --stocks with structured AnalysisTarget pipeline
* fix: render index data sources in aggregate reports
* fix: refresh index registry before --stocks parsing and filter indices on CN holidays
---------
Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
Introduce the StrategyEngine facade and centralize deterministic opinion partitioning, aggregation, synthesis, and signal normalization.
Keep invalid opinions in diagnostics, protect synthesis ownership across fallback paths, and add localized strategy rendering for notification, history, Markdown, and WeChat outputs.
Harden legacy and malformed strategy_synthesis payload handling through shared renderer helpers, with public-entry regression coverage and an updated Phase 1 contract.
Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
* fix: stabilize related board rendering
* fix(review-feedback-1836): Recognize existing Chinese sector tables before appending
* fix(review-feedback-1836): Use unique segment keys for combined reviews
* fix(review-feedback-1836): return or otherwise propagate the rendered markdown for the merge path
* fix(review-feedback-1836): Insert sector fallback inside each market segment
* fix: preserve market review segment boundaries
* fix: compact aggregate report market status
* fix(review-feedback-1612): preserve input order when selecting market status
* chore: rerun review after clarifying PR scope
* feat: enrich email report with financial / dividend / related-boards blocks for CN/HK/US
Render three structured sections in single-stock and dashboard markdown reports
(used by email and other static channels) sourced from fundamental_context:
- 财务摘要: report date, revenue, parent net profit, operating cash flow, ROE,
YoY growth, gross margin
- 股东回报: TTM cash dividend per share (pre-tax), TTM event count, TTM yield,
latest ex-dividend date
- 关联板块: belong_boards with sector-leading/lagging signal & change_pct when
available; auto-collapses to a 2-col (name / type) layout when no sector
ranking data exists (typical for HK/US)
To unlock the offshore (HK/US) data path:
- Add YfinanceFundamentalAdapter producing the same bundle shape as the
AkShare adapter, with growth, financial_report (incl. currency),
TTM dividend events, and sector+industry as belong_boards
- Route get_fundamental_context for hk/us through a new
_build_offshore_fundamental_context helper instead of short-circuiting
to not_supported; keep capital_flow / dragon_tiger / boards as
not_supported (no equivalent feed)
- Stop overwriting belong_boards in the pipeline for non-CN markets so the
adapter-provided sector/industry data survives
Notification renderer:
- Attach fundamental_context onto AnalysisResult so the notification layer
can reach it without an extra DB round-trip
- Currency-aware formatting: USD -> 亿/万美元, HKD -> 亿/万港元,
CNY/other -> 亿/万元; per-share amounts get the same suffix
- Right-align numeric columns and center the report-date column in the
financial summary and shareholder return tables for cleaner rendering
Tests:
- Offline coverage for the new yfinance adapter (symbol conversion, bundle
shape, refusal to use QoQ as YoY, graceful degradation when yfinance is
unavailable)
- Offshore market path through DataFetcherManager.get_fundamental_context
- Adaptive related-boards rendering (2-col when no sector signal,
4-col when at least one row carries data)
- Currency-aware amount formatting for AAPL (USD) and HK09988 (CNY)
* fix: split financial vs dividend currency and recompute TTM yield for HK/US
HK ADRs typically report financialCurrency=CNY but pay dividends in HKD
(live yfinance confirms: 9988.HK / 0700.HK both have financialCurrency=CNY
and currency=HKD; BABA US ADR has financialCurrency=CNY and currency=USD).
Previously the adapter wrote a single currency field shared by both the
financial report and the dividend block, which rendered HK dividends as
yuan in the email report.
This change writes earnings.financial_report.currency from
info.financialCurrency and earnings.dividend.currency from info.currency
independently, and the notification renderer reads each block's own
currency (falling back to financial_report.currency when dividend.currency
is absent, which keeps AkShare A-share rendering unchanged).
TTM dividend yield was sourced from info.dividendYield, mixing the
numerator (TTM cash window) with a denominator-defined yield from a
different yfinance computation. The yield is now recomputed as
ttm_cash_dividend_per_share / latest_price * 100 with both sides in the
trading currency (latest_price falls through info.currentPrice ->
regularMarketPrice -> previousClose). info.dividendYield is now a
last-resort passthrough (no *100, since current yfinance already returns
percent), behind info.trailingAnnualDividendYield (decimal).
Docs (full-guide / full-guide_EN) updated: HK/US now returns
valuation/growth/earnings/belong_boards rather than not_supported, with
the dual-currency split and the TTM yield formula explicitly documented.
CHANGELOG entries rewritten to reflect the corrected behavior.
Verified live against 9988.HK / 0700.HK / AAPL / 600519: the rendered
email shows financial summary in 亿元 for HK ADRs (CNY statements) while
the shareholder return block shows 港元 (HKD dividends), and the
recomputed yields match ttm_cash / latest_price in the same currency.
---------
Co-authored-by: suan baby <suanai@tianle.me>
* feat: add Slack as a first-class notification channel
* fix: guard Slack image delivery with _should_use_image_for_channel()
* fix: require SLACK_CHANNEL_ID in workflow config check
* docs: add Slack channel entry to CHANGELOG and fix README_CHT nav link
* fix: use Slack new file upload API for image delivery
* fix: update Slack image test for new upload API, add slack to md2img example
* fix: send raw bytes to Slack upload URL and unify bot/webhook precedence
* docs: unify Slack Bot > Webhook precedence across all docs
* docs: add Slack config entries and setup guide to full-guide (zh/en)
Add SLACK_BOT_TOKEN, SLACK_CHANNEL_ID, SLACK_WEBHOOK_URL rows to both
config tables (GitHub Actions + local run) and a dedicated Slack setup
section with Bot-first precedence, mirroring the existing Discord
section structure. Applied to both full-guide.md and full-guide_EN.md.
* feat: register Slack config fields in config_registry for Web settings
Add SLACK_BOT_TOKEN, SLACK_CHANNEL_ID, SLACK_WEBHOOK_URL to
_FIELD_DEFINITIONS (display_order 36-38, between Discord and Pushover)
so /api/v1/system/config/schema and the Web settings page can expose
them. Add SLACK to _infer_category() notification prefix list.
New test file test_config_registry.py (6 cases) verifies field
registration, sensitivity flags, display ordering, and schema response
inclusion — prevents future channels from shipping without UI exposure.
* fix: use password control for SLACK_WEBHOOK_URL to prevent plaintext leak
SLACK_WEBHOOK_URL was marked is_sensitive=True but ui_control was text,
inconsistent with DISCORD_WEBHOOK_URL and other sensitive Webhook fields.
The Web settings page would display the URL in plaintext.
Also added a registry-wide regression test: any is_sensitive field must
use ui_control=password, preventing future channels from repeating this.
* feat: add configurable report language
* Fix review follow-ups for history and fallback localization
* fix: prefer .env report language at startup
* fix: address report language review feedback
* fix: address latest report language review feedback
* feat: mark model used in reports and notifications (#528)
* refactor: centralize model_used normalization and raw_result parsing (#528)
---------
Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
* fix: discord把超过0x10000的字符都计算为2个字
* refactor & perf: truncate_to_bytes and chunk_markdown_by_bytes are now slice_at_max_bytes and chunk_content_by_max_bytes, improved logic and performance for both, removed legacy truncate method in notification.py
* style: changed emoji_len to special_char_len for clarity
* refactor & perf: refactored formatters, combined and removed similar codes. Made chunk_content_by_xxx universal.
* fix: fixed a bug in wechatsender
* fix: bug in feishu_stream where wrong arguments for _send_interactive_card was used.
* fix: fixed a bug in formatters which can adds unexpected separators in chunked messages. fixed a bug where sleep timer was missed in wechat sender and feishu stream.
* test: added more tests
* perf: improved performance of _effective_len and _slice_at_effective_len
* fix: fixed a bug where recursion was set incorrectly
* refactor: refactored notification.py; moved different send methods to dedicated sender classes, and static formatting methods to formatters.py
* fix: fixed missing import WECHAT_IMAGE_MAX_BYTES
* docs: added docstring
* test: added unit tests for notication.py