feat: system logs page + dynamic IV watchlist with auto-add from cycle
Backend:
- DB: add system_logs table (level/source/cycle_id/ticker/message) and
iv_watchlist table (ticker/added_by/is_active); seed builtin 18 tickers
- DBLogHandler attached at startup — all WARNING+ logs auto-persist to DB
- log_system_event() helper for structured manual events
- New router /api/logs: GET with filters (level, source, cycle_id, ticker,
date range), GET /sources, GET /cycles for dropdowns, DELETE /clear
- iv_watchlist now read from DB instead of hardcoded constant; options_vol
watchlist/refresh/bootstrap endpoints all use get_watchlist_tickers()
- New endpoints: POST/DELETE /options-vol/watchlist-tickers/{ticker} to
add/remove tickers; adding triggers background 1-year bootstrap
- auto_cycle: after log_trade_entries(), auto-detect new underlying proxies
not yet in watchlist, add them and bootstrap their IV history
Frontend:
- New page SystemLogs (/logs): log table with level/source/cycle/ticker/date
filters, color-coded rows, expandable JSON details, auto-refresh 30s
- Options Lab: WatchlistManager section — add ticker input, chip list with
builtin/auto/manual color coding, remove button for non-builtins
- Sidebar: Logs Système nav link (ScrollText icon)
- useApi: useSystemLogs, useLogSources, useLogCycles, useClearLogs,
useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -342,10 +342,31 @@ def init_db():
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)""")
|
||||
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS iv_watchlist (
|
||||
ticker TEXT PRIMARY KEY,
|
||||
added_date TEXT NOT NULL DEFAULT (date('now')),
|
||||
added_by TEXT DEFAULT 'builtin',
|
||||
is_active INTEGER DEFAULT 1
|
||||
)""")
|
||||
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS system_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
level TEXT NOT NULL,
|
||||
source TEXT,
|
||||
cycle_id TEXT,
|
||||
ticker TEXT,
|
||||
message TEXT NOT NULL,
|
||||
details TEXT
|
||||
)""")
|
||||
|
||||
try:
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
|
||||
c.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_iv_history_ticker_date ON iv_history(ticker, recorded_date)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_syslog_ts ON system_logs(ts DESC)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_syslog_level ON system_logs(level, ts DESC)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_syslog_cycle ON system_logs(cycle_id, ts DESC)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -361,6 +382,18 @@ def init_db():
|
||||
(_key, _val)
|
||||
)
|
||||
|
||||
# Seed built-in watchlist tickers (idempotent)
|
||||
_builtin_tickers = [
|
||||
"SPY", "QQQ", "GLD", "SLV", "USO", "BNO", "UNG",
|
||||
"XLE", "UUP", "TLT", "GDX", "EWJ", "FEZ",
|
||||
"XOM", "CVX", "LMT", "RTX", "BA",
|
||||
]
|
||||
for _t in _builtin_tickers:
|
||||
c.execute(
|
||||
"INSERT OR IGNORE INTO iv_watchlist (ticker, added_by) VALUES (?, 'builtin')",
|
||||
(_t,)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -1637,6 +1670,134 @@ def get_iv_history(ticker: str, days: int = 90) -> List[Dict]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ── IV Watchlist ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_watchlist_tickers() -> List[str]:
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
"SELECT ticker FROM iv_watchlist WHERE is_active=1 ORDER BY added_by='builtin' DESC, added_date ASC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [r["ticker"] for r in rows]
|
||||
|
||||
|
||||
def get_watchlist_entries() -> List[Dict]:
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
"SELECT ticker, added_date, added_by, is_active FROM iv_watchlist ORDER BY added_by, ticker"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def add_watchlist_ticker(ticker: str, added_by: str = "manual") -> bool:
|
||||
"""Add ticker to watchlist. Returns True if newly inserted, False if already existed."""
|
||||
conn = get_conn()
|
||||
existing = conn.execute("SELECT ticker, is_active FROM iv_watchlist WHERE ticker=?", (ticker.upper(),)).fetchone()
|
||||
if existing:
|
||||
if not existing["is_active"]:
|
||||
conn.execute("UPDATE iv_watchlist SET is_active=1, added_by=? WHERE ticker=?", (added_by, ticker.upper()))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
conn.close()
|
||||
return False
|
||||
conn.execute(
|
||||
"INSERT INTO iv_watchlist (ticker, added_date, added_by) VALUES (?, date('now'), ?)",
|
||||
(ticker.upper(), added_by)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
def remove_watchlist_ticker(ticker: str) -> bool:
|
||||
conn = get_conn()
|
||||
conn.execute("UPDATE iv_watchlist SET is_active=0 WHERE ticker=?", (ticker.upper(),))
|
||||
changed = conn.total_changes > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return changed
|
||||
|
||||
|
||||
# ── System Logs ───────────────────────────────────────────────────────────────
|
||||
|
||||
def log_system_event(
|
||||
level: str,
|
||||
source: str,
|
||||
message: str,
|
||||
cycle_id: Optional[str] = None,
|
||||
ticker: Optional[str] = None,
|
||||
details: Optional[Dict] = None,
|
||||
) -> None:
|
||||
try:
|
||||
conn = get_conn()
|
||||
conn.execute(
|
||||
"""INSERT INTO system_logs (level, source, cycle_id, ticker, message, details)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
level.upper(),
|
||||
source,
|
||||
cycle_id,
|
||||
ticker.upper() if ticker else None,
|
||||
message,
|
||||
json.dumps(details, default=str) if details else None,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass # Never raise from logging
|
||||
|
||||
|
||||
def get_system_logs(
|
||||
level: Optional[str] = None,
|
||||
source: Optional[str] = None,
|
||||
cycle_id: Optional[str] = None,
|
||||
ticker: Optional[str] = None,
|
||||
date_from: Optional[str] = None,
|
||||
date_to: Optional[str] = None,
|
||||
limit: int = 300,
|
||||
) -> List[Dict]:
|
||||
conn = get_conn()
|
||||
clauses = []
|
||||
params: List[Any] = []
|
||||
if level:
|
||||
clauses.append("level = ?")
|
||||
params.append(level.upper())
|
||||
if source:
|
||||
clauses.append("source LIKE ?")
|
||||
params.append(f"%{source}%")
|
||||
if cycle_id:
|
||||
clauses.append("cycle_id = ?")
|
||||
params.append(cycle_id)
|
||||
if ticker:
|
||||
clauses.append("ticker = ?")
|
||||
params.append(ticker.upper())
|
||||
if date_from:
|
||||
clauses.append("ts >= ?")
|
||||
params.append(date_from)
|
||||
if date_to:
|
||||
clauses.append("ts <= ?")
|
||||
params.append(date_to + "T23:59:59")
|
||||
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
rows = conn.execute(
|
||||
f"SELECT id, ts, level, source, cycle_id, ticker, message, details FROM system_logs {where} ORDER BY ts DESC LIMIT ?",
|
||||
params + [limit],
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def clear_system_logs(older_than_days: int = 30) -> int:
|
||||
conn = get_conn()
|
||||
conn.execute(f"DELETE FROM system_logs WHERE ts < datetime('now', '-{older_than_days} days')")
|
||||
deleted = conn.total_changes
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return deleted
|
||||
|
||||
|
||||
# ── Knowledge Base Decay ──────────────────────────────────────────────────────
|
||||
|
||||
def decay_kb_confidence() -> int:
|
||||
|
||||
Reference in New Issue
Block a user