feat: wavelets
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user