feat: cockpit

This commit is contained in:
OpenSquared
2026-07-24 17:42:14 +02:00
parent de0675705b
commit 32f2d27782
3 changed files with 35 additions and 5 deletions

View File

@@ -302,5 +302,5 @@ def remove_simulation(sim_id: str):
@router.get("/watchlist-signals") @router.get("/watchlist-signals")
def watchlist_signals(): def watchlist_signals():
from services.database import get_latest_wavelet_signals from services.database import get_latest_wavelet_state_by_instrument
return {"signals": get_latest_wavelet_signals()} return {"signals": get_latest_wavelet_state_by_instrument()}

View File

@@ -3622,6 +3622,36 @@ def get_latest_wavelet_signals() -> List[Dict]:
return [dict(r) for r in rows] return [dict(r) for r in rows]
def get_latest_wavelet_state_by_instrument() -> List[Dict]:
"""One row per Watchlist instrument from the most recent scan — a fired signal if this
run had one for it (any band), otherwise the fastest band's current slope/direction, so
every scanned instrument shows *something* here instead of only the ones with an active
trigger this run. get_latest_wavelet_signals() (fired-only) stays as-is for any other
caller; this is specifically for the Dashboard's Wavelets Signal card, which should
reflect the latest state per instrument, not just a list of firings."""
run_id = _latest_wavelet_run_id()
if not run_id:
return []
conn = get_conn()
rows = conn.execute(
"SELECT * FROM wavelet_watchlist_signals WHERE run_id=? AND band_label != 'ridge' "
"ORDER BY ticker, (signal_kind IS NULL), period_low_days ASC",
(run_id,),
).fetchall()
conn.close()
by_ticker: Dict[str, Dict] = {}
for r in rows:
d = dict(r)
by_ticker.setdefault(d["ticker"], d)
out = list(by_ticker.values())
# No fired signal this run for this band -> direction is null; fall back to the band's
# own slope sign so the card can still show an up/down arrow instead of a blank one.
for d in out:
if d.get("direction") is None and d.get("slope") is not None:
d["direction"] = "up" if d["slope"] > 0 else "down"
return out
def get_latest_wavelet_state() -> List[Dict]: def get_latest_wavelet_state() -> List[Dict]:
"""Every (ticker, band) row from the most recent cycle scan — signal or """Every (ticker, band) row from the most recent cycle scan — signal or
not. Used for the rich AI chat context block (slope/energy/ridge state).""" not. Used for the rich AI chat context block (slope/energy/ridge state)."""

View File

@@ -1056,8 +1056,8 @@ export default function Dashboard() {
<ArrowUpRight className="w-3 h-3 text-slate-600" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</Link> </Link>
{signals.length > 0 ? ( {signals.length > 0 ? (
<div className="space-y-1.5 mt-1"> <div className="space-y-1.5 mt-1 max-h-[135px] overflow-y-auto pr-0.5">
{signals.slice(0, 5).map((s: any, i: number) => ( {signals.map((s: any, i: number) => (
<div <div
key={i} key={i}
onDoubleClick={() => quickAddInstrument.mutate(s.ticker, { onDoubleClick={() => quickAddInstrument.mutate(s.ticker, {
@@ -1073,7 +1073,7 @@ export default function Dashboard() {
<span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={s.ticker}> <span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={s.ticker}>
{nameByTicker[s.ticker] || s.ticker} {nameByTicker[s.ticker] || s.ticker}
</span> </span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{s.band_label} · {s.signal_kind}</span> <span className="text-slate-500 truncate flex-1 text-[9px]">{s.band_label} · {s.signal_kind ?? 'veille'}</span>
{s.computed_at && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{s.computed_at.slice(0, 10)}</span>} {s.computed_at && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{s.computed_at.slice(0, 10)}</span>}
</div> </div>
))} ))}