Files
NexQuant/rdagent/log/daily_log.py
T
TPTBusiness 2cec08bc91 feat: add daily log rotation, llama health wait, factor auto-fixer, and README updates
- Add rdagent/log/daily_log.py: daily-rotating structured logs per command
  (fin_quant, strategies, evaluate, parallel) with loguru; all.log combined sink
- predix.py: route TeeWriter output to logs/YYYY-MM-DD/ instead of root dir;
  wrap quant() and evaluate() in daily_log.session() for start/stop/duration tracking
- rdagent/app/cli.py: fin_quant_cli waits for llama.cpp /health endpoint before
  starting pipeline (up to 300 s); daily_log integration for fin_quant,
  generate_strategies, eval_all, parallel commands
- scripts/predix_gen_strategies_real_bt.py: daily_log integration with
  per-strategy ACCEPTED/REJECTED entries and summary on completion
- rdagent/components/coder/factor_coder/auto_fixer.py: new module that patches
  common LLM-generated factor issues (min_periods, inf/NaN, groupby.transform,
  MultiIndex corrections)
- rdagent/components/coder/factor_coder/prompts.yaml: add critical rules for
  EURUSD 1-min intraday factors (min_periods, inf handling, groupby, date range)
- README.md: document --reasoning off and --n-gpu-layers 28 for llama-server;
  explain VRAM constraints when Ollama is running alongside llama.cpp
- .bandit.yml: suppress B615 (HuggingFace unsafe download) for RL benchmark files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 07:20:08 +02:00

163 lines
5.5 KiB
Python

"""
Daily-rotating log for all Predix commands.
Automatically organizes logs by date:
logs/
YYYY-MM-DD/
fin_quant.log ← R&D loop (structured)
strategies.log ← strategy generation
strategies_bt.log ← parallel strategy generator script
evaluate.log ← factor evaluation
parallel.log ← parallel runs
all.log ← every command combined
Usage:
from rdagent.log.daily_log import setup, session
# One-shot setup (returns a bound logger):
log = setup("fin_quant", model="local", loops=10)
log.info("Loop started")
# Context manager (logs start + elapsed + stop automatically):
with session("strategies", style="swing", count=10) as log:
log.info("Generating…")
run_generation()
"""
from __future__ import annotations
import sys
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any
from loguru import logger as _root
# ── paths ─────────────────────────────────────────────────────────────────────
LOGS_ROOT: Path = Path(__file__).parent.parent.parent / "logs"
# ── format ────────────────────────────────────────────────────────────────────
_FILE_FMT = (
"{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {extra[cmd]: <18} | {message}"
)
# ── internal state ─────────────────────────────────────────────────────────────
_registered: set[str] = set() # command keys that already have a file sink
_all_added: bool = False # whether the combined all.log sink is active
# ── helpers ───────────────────────────────────────────────────────────────────
def _today_dir() -> Path:
d = LOGS_ROOT / datetime.now().strftime("%Y-%m-%d")
d.mkdir(parents=True, exist_ok=True)
return d
def _fmt_td(td) -> str:
s = int(td.total_seconds())
h, r = divmod(s, 3600)
m, sec = divmod(r, 60)
if h:
return f"{h}h {m:02d}m {sec:02d}s"
if m:
return f"{m}m {sec:02d}s"
return f"{sec}s"
def _banner(log, title: str, meta: dict[str, Any]) -> None:
sep = "─" * 76
log.info(sep)
log.info(f" {title}")
if meta:
pairs = " ".join(f"{k}={v}" for k, v in meta.items())
log.info(f" {pairs}")
log.info(sep)
# ── public API ────────────────────────────────────────────────────────────────
def setup(command: str, **context: Any):
"""
Initialise daily log sinks for *command* and return a bound logger.
Idempotent — safe to call multiple times within the same process.
Args:
command: Short slug, e.g. "fin_quant", "strategies", "evaluate".
**context: Key/value pairs printed in the startup banner.
Returns:
loguru.Logger bound with extra["cmd"] = command.upper().
"""
global _all_added
log_dir = _today_dir()
key = command.lower()
if key not in _registered:
# Per-command rotating file
_root.add(
str(log_dir / f"{key}.log"),
format=_FILE_FMT,
filter=lambda r, k=key: r["extra"].get("cmd", "").lower() == k,
rotation="00:00", # new file at midnight
retention="30 days",
encoding="utf-8",
enqueue=True,
backtrace=False,
diagnose=False,
)
_registered.add(key)
if not _all_added:
# Combined log — all commands
_root.add(
str(log_dir / "all.log"),
format=_FILE_FMT,
filter=lambda r: "cmd" in r["extra"],
rotation="00:00",
retention="60 days",
encoding="utf-8",
enqueue=True,
backtrace=False,
diagnose=False,
)
_all_added = True
bound = _root.bind(cmd=command.upper())
_banner(bound, f"▶ START {command.upper()}", context)
return bound
@contextmanager
def session(command: str, **context: Any):
"""
Context manager: logs start, stop, and elapsed duration automatically.
Usage::
with daily_log.session("fin_quant", model="local", loops=10) as log:
log.info("Step 1 complete")
run_loop()
On success logs: ``◼ DONE FIN_QUANT (12m 34s)``
On interrupt: ``⚠ INTERRUPTED FIN_QUANT (2m 01s)``
On error: ``✖ FAILED FIN_QUANT (0s) — <exception>``
"""
log = setup(command, **context)
t0 = datetime.now()
try:
yield log
elapsed = datetime.now() - t0
_banner(log, f"◼ DONE {command.upper()} ({_fmt_td(elapsed)})", {})
except KeyboardInterrupt:
elapsed = datetime.now() - t0
_banner(log, f"⚠ INTERRUPTED {command.upper()} ({_fmt_td(elapsed)})", {})
raise
except Exception as exc:
elapsed = datetime.now() - t0
log.error(f"✖ FAILED {command.upper()} ({_fmt_td(elapsed)}) — {exc}")
raise