diff --git a/README.md b/README.md index 9e46f30..fbe71bc 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,18 @@ python backend/scripts/init_db.py python backend/scripts/run_api.py ``` +4. 收盘后更新并发送邮件: + +```powershell +python backend/scripts/after_market_update.py --trade-date 2026-04-17 --send-email +``` + +默认约定: + +- 按 `Asia/Shanghai` 取当天日期 +- 预期定时为 A 股交易日 `17:00` +- 只有当日成功拉到新的龙虎榜数据,才会继续生成预警、PDF 并发邮件;否则自动跳过 + 默认地址: - `http://127.0.0.1:8000` diff --git a/backend/scripts/after_market_update.py b/backend/scripts/after_market_update.py new file mode 100644 index 0000000..981a4b6 --- /dev/null +++ b/backend/scripts/after_market_update.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import argparse +from datetime import datetime +from zoneinfo import ZoneInfo + +from _bootstrap import add_src_to_path + +add_src_to_path() + +from lhbfx.config import load_config +from lhbfx.mailer import send_email +from lhbfx.pdf_export import generate_daily_report_pdf +from lhbfx.pipeline import generate_warnings, import_daily, rematch_traders +from lhbfx.reporting import build_daily_report, build_email_body, default_report_output_path + + +SHANGHAI_TZ = ZoneInfo("Asia/Shanghai") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run lhbfx after-market update workflow") + parser.add_argument("--trade-date", help="Trade date in YYYY-MM-DD format, defaults to Asia/Shanghai today") + parser.add_argument("--send-email", action="store_true", help="Send completion email after report generation") + parser.add_argument("--force", action="store_true", help="Run even if the target date is not a weekday") + return parser.parse_args() + + +def default_trade_date() -> str: + return datetime.now(SHANGHAI_TZ).date().isoformat() + + +def is_weekday(trade_date: str) -> bool: + return datetime.fromisoformat(trade_date).weekday() < 5 + + +def build_update_email_body( + *, + trade_date: str, + overview_count: int, + detail_count: int, + rematch_updated: int, + warning_total: int, + report_body: str, +) -> str: + lines = [ + f"lhbfx 收盘后更新完成 - {trade_date}", + "", + f"龙虎榜概览更新数: {overview_count}", + f"席位明细更新数: {detail_count}", + f"游资重新匹配数: {rematch_updated}", + f"预警总数: {warning_total}", + "", + ] + lines.append(report_body) + return "\n".join(lines) + + +def main() -> None: + args = parse_args() + config = load_config() + trade_date = args.trade_date or default_trade_date() + + if not args.force and not is_weekday(trade_date): + print(f"Skip {trade_date}: not a weekday, assumed non-trading day.") + return + + import_result = import_daily(trade_date, config=config) + if import_result.overview_count <= 0: + print(f"Skip {trade_date}: no overview rows imported, source data may not be ready or market may be closed.") + return + + rematch_result = rematch_traders(config=config) + warning_result = generate_warnings(config=config) + + report = build_daily_report(config=config, trade_date=trade_date) + pdf_path = default_report_output_path(trade_date) + generate_daily_report_pdf(report, pdf_path) + + print( + "After-market update finished:", + { + "trade_date": trade_date, + "overview_count": import_result.overview_count, + "detail_count": import_result.detail_count, + "rematch_updated": rematch_result["updated"], + "warning_total": warning_result["total"], + "pdf_path": str(pdf_path), + }, + ) + + if args.send_email: + if config.mail is None: + raise RuntimeError("Mail config is missing") + + report_body = build_email_body(report) + body_text = build_update_email_body( + trade_date=trade_date, + overview_count=import_result.overview_count, + detail_count=import_result.detail_count, + rematch_updated=rematch_result["updated"], + warning_total=warning_result["total"], + report_body=report_body, + ) + subject = f"lhbfx 收盘后更新完成 - {trade_date}" + send_email( + mail_config=config.mail, + subject=subject, + body_text=body_text, + attachments=[pdf_path], + ) + print(f"Email sent to: {', '.join(config.mail.recipients)}") + + +if __name__ == "__main__": + main()