from __future__ import annotations
|
|
import argparse
|
import json
|
import sys
|
from datetime import datetime
|
from pathlib import Path
|
from typing import Any, Sequence
|
from zoneinfo import ZoneInfo
|
|
from .core import (
|
LedgerError,
|
active_securities,
|
apply_daily_prices,
|
export_latest,
|
import_valuations,
|
list_rows,
|
load_price_fixture,
|
show_ticker,
|
)
|
from .database import DatabaseConfigError, load_settings
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
DEFAULT_RESULTS_ROOT = PROJECT_ROOT / "ana-data" / "result" / "股票估值"
|
DEFAULT_LEDGER_DIR = DEFAULT_RESULTS_ROOT / "估值台账"
|
|
|
def _path(value: str) -> Path:
|
return Path(value).expanduser().resolve()
|
|
|
def _json(value: Any) -> None:
|
print(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str))
|
|
|
def _connection_arguments(parser: argparse.ArgumentParser) -> None:
|
parser.add_argument("--database", default="stock_valuation")
|
parser.add_argument("--config", type=_path, help="不进入仓库的本机 MySQL JSON 配置")
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
parser = argparse.ArgumentParser(description="股票估值每日 MySQL 台账")
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
init = commands.add_parser("init", help="创建 schema 并幂等导入正式估值版本")
|
init.add_argument("--results-root", type=_path, default=DEFAULT_RESULTS_ROOT)
|
init.add_argument("--project-root", type=_path, default=PROJECT_ROOT)
|
init.add_argument("--gaps", type=_path, default=DEFAULT_LEDGER_DIR / "import_gaps.csv")
|
_connection_arguments(init)
|
|
daily = commands.add_parser("daily", help="刷新完整交易日收盘价、判定和最新导出")
|
daily.add_argument("--as-of", default="latest")
|
daily.add_argument("--prices", type=_path, help="离线完整交易日行情 fixture")
|
daily.add_argument("--output-dir", type=_path, default=DEFAULT_LEDGER_DIR)
|
_connection_arguments(daily)
|
|
listing = commands.add_parser("list", help="列出最新或指定日期判定")
|
listing.add_argument("--date", default="latest")
|
listing.add_argument("--label")
|
listing.add_argument("--ticker")
|
listing.add_argument("--company")
|
_connection_arguments(listing)
|
|
show = commands.add_parser("show", help="显示单股全部版本、价格和判定历史")
|
show.add_argument("--ticker", required=True)
|
_connection_arguments(show)
|
|
export = commands.add_parser("export", help="生成 latest.md 和 latest.csv")
|
export.add_argument("--date", default="latest")
|
export.add_argument("--output-dir", type=_path, default=DEFAULT_LEDGER_DIR)
|
_connection_arguments(export)
|
return parser
|
|
|
def _as_of(value: str) -> str:
|
if value == "latest":
|
return datetime.now(ZoneInfo("Asia/Shanghai")).date().isoformat()
|
from datetime import date
|
|
return date.fromisoformat(value).isoformat()
|
|
|
def run(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
|
settings = load_settings(database=args.database, config_path=args.config)
|
if args.command == "init":
|
result = import_valuations(
|
settings,
|
args.results_root,
|
args.project_root,
|
args.gaps,
|
)
|
result.update({"database": settings.database, "results_root": str(args.results_root)})
|
return 0, result
|
if args.command == "daily":
|
as_of = _as_of(args.as_of)
|
securities = active_securities(settings)
|
if args.prices:
|
prices = load_price_fixture(args.prices)
|
present = {row.ticker for row in prices}
|
failures = [
|
{"ticker": item["ticker"], "code": "E_PRICE_MISSING", "message": "fixture 未提供行情"}
|
for item in securities
|
if item["ticker"] not in present
|
]
|
market_metrics = {"provider": "offline_fixture", "requested": len(securities), "succeeded": len(prices), "failed": len(failures)}
|
else:
|
from .market import fetch_trading_closes
|
|
prices, failures, market_metrics = fetch_trading_closes(settings, securities, as_of)
|
result = apply_daily_prices(settings, as_of, prices, failures)
|
result["market"] = market_metrics
|
if result["inserted_judgements"] + result["unchanged"] > 0:
|
result["export"] = export_latest(settings, args.output_dir, "latest")
|
else:
|
result["export"] = {
|
"status": "PRESERVED_NO_TRUSTED_JUDGEMENT",
|
"output_dir": str(args.output_dir),
|
}
|
return (4 if result["failure_count"] else 0), result
|
if args.command == "list":
|
rows = list_rows(
|
settings,
|
args.date,
|
label=args.label,
|
ticker=args.ticker,
|
company=args.company,
|
)
|
return 0, {"status": "OK", "count": len(rows), "rows": rows}
|
if args.command == "show":
|
return 0, {"status": "OK", **show_ticker(settings, args.ticker)}
|
if args.command == "export":
|
return 0, export_latest(settings, args.output_dir, args.date)
|
raise LedgerError("E_COMMAND", "未知命令")
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
parser = build_parser()
|
try:
|
args = parser.parse_args(argv)
|
code, result = run(args)
|
_json(result)
|
return code
|
except (LedgerError, DatabaseConfigError) as exc:
|
code = exc.code if isinstance(exc, LedgerError) else "E_DATABASE_CONFIG"
|
_json({"status": "FAILED", "error_code": code, "error": str(exc)})
|
return 2
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
_json({"status": "FAILED", "error_code": "E_INPUT", "error": str(exc)})
|
return 2
|
|
|
if __name__ == "__main__":
|
raise SystemExit(main(sys.argv[1:]))
|