129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
"""Unified report-path helpers.
|
|
|
|
Every CLI command and internal save routine should use these helpers so that
|
|
all generated artifacts land under a single ``reports/`` tree.
|
|
|
|
When a ``run_id`` is supplied the layout becomes::
|
|
|
|
reports/
|
|
└── daily/{YYYY-MM-DD}/
|
|
├── runs/{run_id}/
|
|
│ ├── market/ # scan results
|
|
│ ├── {TICKER}/ # per-ticker analysis
|
|
│ └── portfolio/ # PM artefacts
|
|
├── latest.json # pointer → most recent run_id
|
|
└── daily_digest.md # append-only (shared across runs)
|
|
|
|
Without a ``run_id`` the legacy flat layout is preserved for backward
|
|
compatibility::
|
|
|
|
reports/
|
|
└── daily/{YYYY-MM-DD}/
|
|
├── market/
|
|
├── {TICKER}/
|
|
└── summary.md
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# Configurable via TRADINGAGENTS_REPORTS_DIR env var.
|
|
# Falls back to "reports" (relative to CWD) when unset.
|
|
REPORTS_ROOT = Path(os.getenv("TRADINGAGENTS_REPORTS_DIR") or "reports")
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# Run-ID helpers
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
def generate_run_id() -> str:
|
|
"""Return a short, human-readable run identifier (8-char hex)."""
|
|
return uuid.uuid4().hex[:8]
|
|
|
|
|
|
def write_latest_pointer(date: str, run_id: str, base_dir: Path | None = None) -> Path:
|
|
"""Write ``{base}/daily/{date}/latest.json`` pointing to *run_id*.
|
|
|
|
Args:
|
|
date: ISO date string.
|
|
run_id: Short identifier for the run.
|
|
base_dir: Reports root directory. Falls back to ``REPORTS_ROOT``
|
|
when ``None``.
|
|
|
|
Returns the path of the written file.
|
|
"""
|
|
root = base_dir or REPORTS_ROOT
|
|
daily = root / "daily" / date
|
|
daily.mkdir(parents=True, exist_ok=True)
|
|
pointer = daily / "latest.json"
|
|
payload = {
|
|
"run_id": run_id,
|
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
pointer.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
return pointer
|
|
|
|
|
|
def read_latest_pointer(date: str, base_dir: Path | None = None) -> str | None:
|
|
"""Read the latest run_id for *date*, or ``None`` if no pointer exists.
|
|
|
|
Args:
|
|
date: ISO date string.
|
|
base_dir: Reports root directory. Falls back to ``REPORTS_ROOT``
|
|
when ``None``.
|
|
"""
|
|
root = base_dir or REPORTS_ROOT
|
|
pointer = root / "daily" / date / "latest.json"
|
|
if not pointer.exists():
|
|
return None
|
|
try:
|
|
data = json.loads(pointer.read_text(encoding="utf-8"))
|
|
return data.get("run_id")
|
|
except (json.JSONDecodeError, OSError):
|
|
return None
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# Path helpers
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
def _run_prefix(date: str, run_id: str | None) -> Path:
|
|
"""Base directory for a date, optionally scoped by run_id."""
|
|
daily = REPORTS_ROOT / "daily" / date
|
|
if run_id:
|
|
return daily / "runs" / run_id
|
|
return daily
|
|
|
|
|
|
def get_daily_dir(date: str, run_id: str | None = None) -> Path:
|
|
"""``reports/daily/{date}/`` or ``reports/daily/{date}/runs/{run_id}/``"""
|
|
return _run_prefix(date, run_id)
|
|
|
|
|
|
def get_market_dir(date: str, run_id: str | None = None) -> Path:
|
|
"""``…/{date}[/runs/{run_id}]/market/``"""
|
|
return get_daily_dir(date, run_id) / "market"
|
|
|
|
|
|
def get_ticker_dir(date: str, ticker: str, run_id: str | None = None) -> Path:
|
|
"""``…/{date}[/runs/{run_id}]/{TICKER}/``"""
|
|
return get_daily_dir(date, run_id) / ticker.upper()
|
|
|
|
|
|
def get_eval_dir(date: str, ticker: str, run_id: str | None = None) -> Path:
|
|
"""``…/{date}[/runs/{run_id}]/{TICKER}/eval/``"""
|
|
return get_ticker_dir(date, ticker, run_id) / "eval"
|
|
|
|
|
|
def get_digest_path(date: str) -> Path:
|
|
"""``reports/daily/{date}/daily_digest.md``
|
|
|
|
The digest is always at the date level (shared across runs).
|
|
"""
|
|
return REPORTS_ROOT / "daily" / date / "daily_digest.md"
|