feat: cockpit

This commit is contained in:
OpenSquared
2026-07-24 15:52:58 +02:00
parent 3ec0c024ad
commit 7cb81cbd65
6 changed files with 211 additions and 19 deletions

View File

@@ -231,7 +231,33 @@ def _technical_desk_wavelet_config() -> Dict:
return (desk.get("config") or {}).get("signals") or {}
def scan_watchlist_wavelet_signals() -> List[Dict]:
def _fetch_close_series(ticker: str, saxo_symbol: Optional[str]):
"""Saxo-first when this watchlist instrument has a saxo_quote_symbol link, yfinance
otherwise or as a silent fallback on any Saxo failure — same pattern as
routers/wavelet.py's _fetch_history, duplicated locally rather than importing across a
router boundary. Needed because several Watchlist instruments (BRENT, COPPER...) have no
real yfinance ticker at all — get_historical(ticker, ...) always failed for them, which
silently dropped them out of the per-cycle scan entirely (one bad ticker just gets
skipped, see the try/except around the caller) — that's why the Wavelets Signal card
only ever showed the yfinance-recognized subset of the Watchlist."""
if saxo_symbol:
try:
from services.database import get_saxo_catalog_by_symbol
from services.saxo_client import get_price_history
entry = get_saxo_catalog_by_symbol(saxo_symbol)
asset_type = entry["asset_type"] if entry else "FxSpot"
bars = get_price_history(saxo_symbol, asset_type, days=400)
return [b["close"] for b in bars], [b["date"] for b in bars]
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"[wavelet_signals] Saxo fetch failed for '{saxo_symbol}', falling back to yfinance: {e}")
from services.data_fetcher import get_historical
hist = get_historical(ticker, period="1y", interval="1d")
return [h["close"] for h in hist], [h["date"] for h in hist]
def scan_watchlist_wavelet_signals(run_id: Optional[str] = None) -> List[Dict]:
"""Compute a causal (no-look-ahead) band decomposition for each watchlist
instrument. Every (ticker, band) gets a row every cycle — current slope/
value/energy state always, plus signal_kind/direction/params_json when one
@@ -239,9 +265,13 @@ def scan_watchlist_wavelet_signals() -> List[Dict]:
wins, evaluated extremum -> level_threshold -> trend_flatten ->
acceleration -> band_cross -> energy_threshold). ridge_shift is evaluated
once per ticker (not per band — the ridge is a single track for the whole
decomposition) and stored as an extra band_label="ridge" row."""
from services.database import get_instruments_watchlist
from services.data_fetcher import get_historical
decomposition) and stored as an extra band_label="ridge" row.
Also caches the full (untruncated) decomposition per ticker in
wavelet_decomposition_cache — Instrument Analysis's Wavelet tab reads that instead of
running its own live decomposition on open, so it always agrees with the Watchlist
Signal card and never needs a click just to show the current state."""
from services.database import get_instruments_watchlist, save_wavelet_decomposition_cache
from services.wavelet_engine import rolling_causal_bands, rolling_causal_bands_ssq
sig_cfg = _technical_desk_wavelet_config()
@@ -267,11 +297,9 @@ def scan_watchlist_wavelet_signals() -> List[Dict]:
for item in get_instruments_watchlist():
ticker = item["ticker"]
try:
hist = get_historical(ticker, period="1y", interval="1d")
if len(hist) < lookback + 32:
values, dates = _fetch_close_series(ticker, item.get("saxo_quote_symbol"))
if len(values) < lookback + 32:
continue
values = [h["close"] for h in hist]
dates = [h["date"] for h in hist]
start_idx = max(lookback, len(values) - 60)
decomposed = decomposer(
values, dates,
@@ -280,6 +308,7 @@ def scan_watchlist_wavelet_signals() -> List[Dict]:
)
if not decomposed["dates"]:
continue
save_wavelet_decomposition_cache(ticker, run_id, method, wavelet, num_levels, lookback, decomposed)
price_at_signal = decomposed["original"][-1]
bands = decomposed["bands"]
@@ -378,6 +407,6 @@ def scan_watchlist_wavelet_signals() -> List[Dict]:
def compute_and_save_wavelet_signals(run_id: str) -> List[Dict]:
from services.database import save_wavelet_signals
results = scan_watchlist_wavelet_signals()
results = scan_watchlist_wavelet_signals(run_id)
save_wavelet_signals(run_id, results)
return results