Introduces a dashboard-managed agent registry (`state/agents.json`) used by nightly research and market-open execution, including support for custom/disabled agents and per-agent time budgets. Expands personas to four agents (adds Maverick), shifts pick targets to include mandatory options, and updates prompts/finalization nudges to enforce option research without hard-failing runs. Strengthens execution safety by adding option-specific guardrails (conviction floor, premium cap, reserved option slot through trade/day/buying-power caps), validates OCC option symbols in schemas, and normalizes invalid option direction cases. The dashboard was refactored into modular views, adds an Agents admin tab, and improves live portfolio visibility with Alpaca-first data and snapshot fallback.
141 lines
5.6 KiB
Python
141 lines
5.6 KiB
Python
"""ENTRYPOINT #1 — the nightly research run.
|
|
|
|
Brings up oMLX, then runs the 3 personas strictly sequentially (one model, time-sliced),
|
|
each under a rollover wall-clock budget bounded by the hard stop. Every agent's output is
|
|
checkpointed to state/output/<trade_date>/. Run with --compressed for a fast end-to-end
|
|
test (short budgets).
|
|
|
|
uv run python -m orchestration.run_nightly [--compressed]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from loguru import logger # noqa: E402
|
|
|
|
from agents.graph import run_agent # noqa: E402
|
|
from agents.schemas import RunManifest # noqa: E402
|
|
from config.agent_registry import load_personas # noqa: E402
|
|
from config.settings import get_settings # noqa: E402
|
|
from llm import server # noqa: E402
|
|
from llm.client import resolve_model_id # noqa: E402
|
|
from orchestration.budget import ( # noqa: E402
|
|
local_hard_stop_ts,
|
|
next_deadline,
|
|
next_market_trade_date,
|
|
)
|
|
from orchestration import committee as committee_mod # noqa: E402
|
|
from orchestration.checkpoint import Checkpointer # noqa: E402
|
|
from orchestration.regime import market_regime # noqa: E402
|
|
from reporting import report_writer # noqa: E402
|
|
from reporting.journal import lessons_for # noqa: E402
|
|
|
|
|
|
def main(compressed: bool = False) -> int:
|
|
s = get_settings()
|
|
s.ensure_dirs()
|
|
trade_date = next_market_trade_date(s.market_tz)
|
|
logger.add(s.log_dir / f"nightly_{trade_date}.log", rotation="10 MB", retention="14 days")
|
|
logger.info("=== nightly run trade_date={} profile={} compressed={} ===",
|
|
trade_date, s.model_profile, compressed)
|
|
|
|
if not server.ensure_ready():
|
|
logger.error("oMLX not ready; aborting.")
|
|
return 1
|
|
|
|
# preflight: confirm internet before committing to a long run (warn but continue)
|
|
try:
|
|
from tools.web import web_search
|
|
|
|
probe = web_search.invoke({"query": "stock market news today"})
|
|
logger.info("internet preflight: {}", "OK" if "http" in probe else "no results (degraded)")
|
|
except Exception as e:
|
|
logger.warning("internet preflight failed (continuing): {}", e)
|
|
|
|
model_name = resolve_model_id()
|
|
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
cp = Checkpointer(s.output_dir, trade_date)
|
|
manifest = RunManifest(
|
|
run_id=run_id,
|
|
trade_date=trade_date,
|
|
model=model_name,
|
|
started_at=datetime.now(timezone.utc),
|
|
status="running",
|
|
)
|
|
cp.write_manifest(manifest)
|
|
|
|
hard_stop = local_hard_stop_ts(s.hard_stop, s.local_tz)
|
|
step_budget = 12 if compressed else s.per_agent_step_budget
|
|
min_steps = 4 if compressed else s.per_agent_min_steps
|
|
|
|
# dashboard-managed registry: built-ins + overrides + custom agents, disabled excluded
|
|
personas = load_personas()
|
|
logger.info("agents tonight: {}", ", ".join(p.id for p in personas))
|
|
|
|
# shared ground truth for all agents tonight (computed, deterministic)
|
|
regime = market_regime()
|
|
if regime:
|
|
logger.info("regime: {}", regime[:140])
|
|
|
|
outputs = []
|
|
records: dict[str, str] = {}
|
|
for i, persona in enumerate(personas):
|
|
# per-agent budgets (dashboard-editable), falling back to the global settings
|
|
if compressed:
|
|
per_agent_cap_s, min_seconds = 5 * 60, 60
|
|
else:
|
|
per_agent_cap_s = (persona.time_budget_min or s.per_agent_wallclock_min) * 60
|
|
min_seconds = (persona.min_research_min or s.per_agent_min_minutes) * 60
|
|
now = time.time()
|
|
if now >= hard_stop:
|
|
logger.warning("hard stop reached; skipping {} remaining agents", len(personas) - i)
|
|
break
|
|
deadline = next_deadline(now, hard_stop, len(personas) - i, per_agent_cap_s)
|
|
logger.info("--- {} until {} ---", persona.id,
|
|
datetime.fromtimestamp(deadline).strftime("%H:%M:%S"))
|
|
if not server.server_up(): # recover if a prior agent left oMLX unresponsive
|
|
logger.warning("oMLX unreachable before {}; recovering...", persona.id)
|
|
server.ensure_ready(warm=False)
|
|
lessons = lessons_for(persona.id) # self-learning: past picks marked to market
|
|
records[persona.id] = lessons
|
|
blurb = "\n\n".join(x for x in (regime, lessons) if x)
|
|
out = run_agent(persona, deadline_ts=deadline, step_budget=step_budget,
|
|
min_steps=min_steps, min_seconds=min_seconds,
|
|
context_blurb=blurb, checkpointer=cp, model_name=model_name)
|
|
cp.write(out)
|
|
outputs.append(out)
|
|
manifest.agents.append(persona.id)
|
|
try:
|
|
report_writer.write_agent_report(out, trade_date) # analyst research note
|
|
except Exception as e:
|
|
logger.warning("agent report failed for {}: {}", persona.id, e)
|
|
|
|
# Investment Committee: risk-officer pass over the whole book (verdicts enforce at open)
|
|
try:
|
|
committee_mod.run_committee(outputs, trade_date, regime, records)
|
|
except Exception as e:
|
|
logger.warning("committee failed (fail-open): {}", e)
|
|
|
|
# combined daily report (execution results appended in the morning by run_market_open)
|
|
try:
|
|
daily = report_writer.write_daily_report(trade_date, outputs)
|
|
report_writer.to_pdf(daily)
|
|
except Exception as e:
|
|
logger.warning("daily report failed: {}", e)
|
|
|
|
manifest.status = "complete"
|
|
manifest.ended_at = datetime.now(timezone.utc)
|
|
cp.write_manifest(manifest)
|
|
logger.info("=== nightly run complete; output in {} ===", cp.dir)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(compressed="--compressed" in sys.argv))
|