feat: wavelets

This commit is contained in:
OpenSquared
2026-07-14 16:23:18 +02:00
parent ce948f6b65
commit b693aca2dc
17 changed files with 2144 additions and 10 deletions

View File

@@ -110,12 +110,42 @@ def init_db():
sort_order INTEGER DEFAULT 0,
added_at TEXT DEFAULT (datetime('now'))
)""",
# Wavelets — saved optimization/simulation runs (ported from InstrumentSimulator's
# WaveletOptimizationRun: form/results are free-form JSON blobs, not modeled relationally)
"""CREATE TABLE IF NOT EXISTS wavelet_simulations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
form_json TEXT DEFAULT '{}',
results_json TEXT DEFAULT '[]',
excluded_instruments_json TEXT DEFAULT '[]'
)""",
# Wavelets — latest per-ticker signal detected during the auto-cycle watchlist scan
"""CREATE TABLE IF NOT EXISTS wavelet_watchlist_signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT,
ticker TEXT NOT NULL,
computed_at TEXT DEFAULT (datetime('now')),
band_label TEXT,
period_low_days REAL,
period_high_days REAL,
signal_kind TEXT,
direction TEXT,
price_at_signal REAL
)""",
"ALTER TABLE cycle_runs ADD COLUMN wavelet_signals_count INTEGER DEFAULT 0",
]:
try:
c.execute(_sql)
except Exception:
pass
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_wws_ticker_date ON wavelet_watchlist_signals(ticker, computed_at DESC)")
except Exception:
pass
# Specialist Reports — surprise index + text sentiment columns
try:
c.execute("ALTER TABLE specialist_reports ADD COLUMN consensus_estimate REAL")
@@ -2209,7 +2239,7 @@ def update_cycle_run(run_id: str, **fields) -> None:
if not fields:
return
allowed = {"completed_at", "patterns_suggested", "patterns_added", "patterns_scored",
"geo_score", "dominant_regime", "commentary", "status"}
"geo_score", "dominant_regime", "commentary", "status", "wavelet_signals_count"}
sets = ", ".join(f"{k}=?" for k in fields if k in allowed)
vals = [v for k, v in fields.items() if k in allowed]
if not sets:
@@ -3056,6 +3086,133 @@ def reorder_instruments_watchlist(tickers: List[str]) -> None:
conn.close()
# ── Wavelets — saved simulation/optimization runs ─────────────────────────────
def save_wavelet_simulation(name: str, form: Dict, results: Optional[List[Dict]] = None,
excluded_instruments: Optional[List[str]] = None) -> Dict:
import uuid
sim_id = uuid.uuid4().hex
now = datetime.utcnow().isoformat()
conn = get_conn()
conn.execute(
"INSERT INTO wavelet_simulations (id, name, created_at, updated_at, form_json, results_json, excluded_instruments_json) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(sim_id, name, now, now, json.dumps(form or {}), json.dumps(results or []), json.dumps(excluded_instruments or [])),
)
conn.commit()
conn.close()
return get_wavelet_simulation(sim_id)
def get_wavelet_simulations() -> List[Dict]:
conn = get_conn()
rows = conn.execute(
"SELECT id, name, created_at, updated_at, results_json FROM wavelet_simulations ORDER BY updated_at DESC"
).fetchall()
conn.close()
out = []
for r in rows:
d = dict(r)
try:
result_count = len(json.loads(d.pop("results_json")) or [])
except Exception:
result_count = 0
d["result_count"] = result_count
out.append(d)
return out
def get_wavelet_simulation(sim_id: str) -> Optional[Dict]:
conn = get_conn()
row = conn.execute("SELECT * FROM wavelet_simulations WHERE id=?", (sim_id,)).fetchone()
conn.close()
if not row:
return None
d = dict(row)
d["form"] = json.loads(d.pop("form_json") or "{}")
d["results"] = json.loads(d.pop("results_json") or "[]")
d["excluded_instruments"] = json.loads(d.pop("excluded_instruments_json") or "[]")
return d
def update_wavelet_simulation(sim_id: str, name: Optional[str] = None, form: Optional[Dict] = None,
results: Optional[List[Dict]] = None, append_results: Optional[List[Dict]] = None,
excluded_instruments: Optional[List[str]] = None) -> Optional[Dict]:
current = get_wavelet_simulation(sim_id)
if not current:
return None
new_name = name if name is not None else current["name"]
new_form = form if form is not None else current["form"]
if results is not None:
new_results = results
elif append_results:
new_results = [*current["results"], *append_results]
else:
new_results = current["results"]
new_excluded = excluded_instruments if excluded_instruments is not None else current["excluded_instruments"]
conn = get_conn()
conn.execute(
"UPDATE wavelet_simulations SET name=?, form_json=?, results_json=?, excluded_instruments_json=?, updated_at=? WHERE id=?",
(new_name, json.dumps(new_form), json.dumps(new_results), json.dumps(new_excluded), datetime.utcnow().isoformat(), sim_id),
)
conn.commit()
conn.close()
return get_wavelet_simulation(sim_id)
def delete_wavelet_simulation(sim_id: str) -> bool:
conn = get_conn()
conn.execute("DELETE FROM wavelet_simulations WHERE id=?", (sim_id,))
changed = conn.total_changes > 0
conn.commit()
conn.close()
return changed
# ── Wavelets — automated watchlist signal scan (per cycle) ────────────────────
def save_wavelet_signals(run_id: str, signals: List[Dict]) -> None:
if not signals:
return
conn = get_conn()
for s in signals:
conn.execute(
"INSERT INTO wavelet_watchlist_signals "
"(run_id, ticker, band_label, period_low_days, period_high_days, signal_kind, direction, price_at_signal) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(run_id, s.get("ticker"), s.get("band_label"), s.get("period_low_days"), s.get("period_high_days"),
s.get("signal_kind"), s.get("direction"), s.get("price_at_signal")),
)
conn.commit()
conn.close()
def get_latest_wavelet_signals() -> List[Dict]:
"""Most recent signal per ticker (one row per ticker, its latest computed_at)."""
conn = get_conn()
rows = conn.execute(
"""SELECT w.* FROM wavelet_watchlist_signals w
INNER JOIN (
SELECT ticker, MAX(computed_at) AS max_computed_at
FROM wavelet_watchlist_signals GROUP BY ticker
) latest ON w.ticker = latest.ticker AND w.computed_at = latest.max_computed_at
ORDER BY w.computed_at DESC"""
).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_wavelet_signals_history(ticker: str, days: int = 30) -> List[Dict]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM wavelet_watchlist_signals WHERE ticker=? AND computed_at >= datetime('now', ?) ORDER BY computed_at DESC",
(ticker.upper(), f"-{days} days"),
).fetchall()
conn.close()
return [dict(r) for r in rows]
# ── System Logs ───────────────────────────────────────────────────────────────
def log_system_event(