Add dynamic agent registry and options mandate
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.
This commit is contained in:
1 parent
b3821abd69
commit
e1ac514f30
36 files changed
+1476
-294
No files matched your search
@@ -0,0 +1,178 @@
|
||||
"""Agents tab — the management console for the agent roster.
|
||||
|
||||
Everything here writes through ``config/agent_registry`` to ``state/agents.json``:
|
||||
enable/disable agents, per-agent research time budgets, risk knobs, system prompts, and
|
||||
adding brand-new agents (cloned from a template persona). The nightly run reads the same
|
||||
registry, so changes take effect on the next run — no code edits needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from config import agent_registry
|
||||
from config.personas import PERSONAS_BY_ID
|
||||
from config.settings import _dotenv_extras, get_settings
|
||||
|
||||
_HORIZONS = ["intraday", "swing", "weeks", "months"]
|
||||
|
||||
|
||||
def _has_dedicated_creds(pid: str) -> bool:
|
||||
s = get_settings()
|
||||
key_name = f"ALPACA_{pid.upper()}_KEY"
|
||||
sec_name = f"ALPACA_{pid.upper()}_SECRET"
|
||||
extras = _dotenv_extras()
|
||||
key = getattr(s, f"alpaca_{pid}_key", "") or os.environ.get(key_name, "") or extras.get(key_name, "")
|
||||
sec = getattr(s, f"alpaca_{pid}_secret", "") or os.environ.get(sec_name, "") or extras.get(sec_name, "")
|
||||
return bool(key and sec)
|
||||
|
||||
|
||||
def _lessons_snippet(pid: str) -> str:
|
||||
try:
|
||||
from reporting.journal import lessons_for
|
||||
|
||||
return lessons_for(pid)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _agent_card(persona, s) -> None:
|
||||
is_builtin = persona.id in PERSONAS_BY_ID
|
||||
creds_ok = _has_dedicated_creds(persona.id)
|
||||
status = "🟢" if persona.enabled else "⚪️ disabled"
|
||||
creds = "🔑" if creds_ok else "❗️no keys"
|
||||
with st.expander(f"{status} **{persona.display_name}** (`{persona.id}`) · {creds}"):
|
||||
if not creds_ok:
|
||||
st.warning(
|
||||
f"No dedicated Alpaca paper account keys found. Add to `.env`:\n\n"
|
||||
f"`ALPACA_{persona.id.upper()}_KEY` and `ALPACA_{persona.id.upper()}_SECRET`\n\n"
|
||||
"(Until then this agent falls back to the shared/single-account keys.)"
|
||||
)
|
||||
with st.form(f"edit_{persona.id}"):
|
||||
c1, c2, c3 = st.columns([2, 1, 1])
|
||||
display_name = c1.text_input("Display name", persona.display_name)
|
||||
enabled = c2.checkbox("Enabled", persona.enabled)
|
||||
options_quota = c3.number_input("Options quota / night", 0, 4, persona.options_quota)
|
||||
|
||||
st.markdown("**Research time** (0 = use global default: "
|
||||
f"{s.per_agent_wallclock_min} min cap / {s.per_agent_min_minutes} min floor)")
|
||||
t1, t2 = st.columns(2)
|
||||
time_budget = t1.number_input(
|
||||
"Wall-clock cap (min)", 0, 360, persona.time_budget_min or 0, 5,
|
||||
key=f"tb_{persona.id}",
|
||||
)
|
||||
min_research = t2.number_input(
|
||||
"Research floor (min)", 0, 360, persona.min_research_min or 0, 5,
|
||||
key=f"mr_{persona.id}",
|
||||
)
|
||||
|
||||
st.markdown("**Risk knobs**")
|
||||
r1, r2, r3, r4 = st.columns(4)
|
||||
max_pos = r1.slider("Max position %", 0.01, 0.25, float(persona.max_position_pct), 0.01,
|
||||
key=f"mp_{persona.id}")
|
||||
picks = r2.number_input("Picks / night", 1, 8, persona.target_pick_count,
|
||||
key=f"pk_{persona.id}")
|
||||
stop = r3.slider("Default stop %", 0.02, 0.25, float(persona.default_stop_pct), 0.01,
|
||||
key=f"st_{persona.id}")
|
||||
take = r4.slider("Default take %", 0.05, 0.60, float(persona.default_take_pct), 0.01,
|
||||
key=f"tk_{persona.id}")
|
||||
horizon = st.selectbox(
|
||||
"Preferred horizon", _HORIZONS,
|
||||
index=_HORIZONS.index(persona.preferred_horizon)
|
||||
if persona.preferred_horizon in _HORIZONS else 1,
|
||||
key=f"hz_{persona.id}",
|
||||
)
|
||||
risk_style = st.text_input(
|
||||
"Risk style (shown to the Investment Committee)", persona.risk_style,
|
||||
key=f"rs_{persona.id}",
|
||||
)
|
||||
prompt = st.text_area("System prompt", persona.system_prompt, height=260,
|
||||
key=f"sp_{persona.id}")
|
||||
|
||||
b1, b2 = st.columns([1, 1])
|
||||
saved = b1.form_submit_button("💾 Save", type="primary")
|
||||
reset = b2.form_submit_button(
|
||||
"↩️ Reset to code defaults" if is_builtin else "🗑 Delete agent"
|
||||
)
|
||||
|
||||
if saved:
|
||||
agent_registry.upsert_agent(
|
||||
persona.id,
|
||||
{
|
||||
"display_name": display_name,
|
||||
"enabled": enabled,
|
||||
"system_prompt": prompt,
|
||||
"max_position_pct": float(max_pos),
|
||||
"target_pick_count": int(picks),
|
||||
"options_quota": int(options_quota),
|
||||
"default_stop_pct": float(stop),
|
||||
"default_take_pct": float(take),
|
||||
"preferred_horizon": horizon,
|
||||
"risk_style": risk_style,
|
||||
"time_budget_min": int(time_budget) or None,
|
||||
"min_research_min": int(min_research) or None,
|
||||
},
|
||||
)
|
||||
st.success("Saved — applies from the next nightly run.")
|
||||
st.rerun()
|
||||
if reset:
|
||||
agent_registry.delete_agent(persona.id)
|
||||
st.success("Reset to code defaults." if is_builtin else "Agent deleted.")
|
||||
st.rerun()
|
||||
|
||||
lessons = _lessons_snippet(persona.id)
|
||||
if lessons:
|
||||
st.caption("Track record")
|
||||
st.code(lessons, language="text")
|
||||
|
||||
|
||||
def _add_agent_form() -> None:
|
||||
st.subheader("➕ Add a new agent")
|
||||
templates = list(PERSONAS_BY_ID)
|
||||
with st.form("add_agent"):
|
||||
c1, c2 = st.columns([1, 2])
|
||||
new_id = c1.text_input("Agent id (lowercase, e.g. `scalper`)")
|
||||
new_name = c2.text_input("Display name")
|
||||
template = st.selectbox("Clone style & knobs from", templates, index=0)
|
||||
prompt = st.text_area(
|
||||
"System prompt (leave blank to copy the template's prompt)", "", height=160,
|
||||
)
|
||||
create = st.form_submit_button("Create agent", type="primary")
|
||||
if create:
|
||||
if not new_id.strip() or not new_name.strip():
|
||||
st.error("Both id and display name are required.")
|
||||
return
|
||||
base = PERSONAS_BY_ID[template].model_dump()
|
||||
base.pop("id")
|
||||
base["display_name"] = new_name.strip()
|
||||
if prompt.strip():
|
||||
base["system_prompt"] = prompt.strip()
|
||||
try:
|
||||
agent_registry.upsert_agent(new_id, base)
|
||||
except Exception as e:
|
||||
st.error(f"Could not create agent: {e}")
|
||||
return
|
||||
pid = new_id.strip().lower().replace(" ", "_").replace("-", "_")
|
||||
st.success(
|
||||
f"Agent `{pid}` created (enabled). Give it its own Alpaca paper account by adding "
|
||||
f"`ALPACA_{pid.upper()}_KEY` and `ALPACA_{pid.upper()}_SECRET` to `.env` — "
|
||||
"until then it uses the shared fallback keys."
|
||||
)
|
||||
st.rerun()
|
||||
|
||||
|
||||
def render() -> None:
|
||||
s = get_settings()
|
||||
st.caption(
|
||||
f"Roster and per-agent settings live in `state/agents.json` — the nightly run reads it "
|
||||
f"on start. Registry file: `{agent_registry.registry_path()}`"
|
||||
)
|
||||
personas = agent_registry.load_personas(include_disabled=True)
|
||||
enabled_n = sum(1 for p in personas if p.enabled)
|
||||
st.markdown(f"**{len(personas)} agents** ({enabled_n} enabled)")
|
||||
for persona in personas:
|
||||
_agent_card(persona, s)
|
||||
st.divider()
|
||||
_add_agent_form()
|
||||
Reference in new issue
Block a user