feat: wavelets
This commit is contained in:
@@ -180,6 +180,10 @@ def startup():
|
||||
# Start Saxo OAuth token refresh + options-chain snapshot poller
|
||||
from services.saxo_scheduler import start_saxo_scheduler
|
||||
start_saxo_scheduler()
|
||||
# Start Saxo-priced Watchlist + wavelet recompute refresh (own cadence, independent of
|
||||
# the once-a-day auto_cycle — see services/wavelet_scheduler.py)
|
||||
from services.wavelet_scheduler import start_wavelet_scheduler
|
||||
start_wavelet_scheduler()
|
||||
|
||||
# One-time cleanup: collapse snapshot rows stored before save-time dedup existed
|
||||
try:
|
||||
@@ -270,6 +274,8 @@ def shutdown():
|
||||
stop_institutional_scheduler()
|
||||
from services.saxo_scheduler import stop_saxo_scheduler
|
||||
stop_saxo_scheduler()
|
||||
from services.wavelet_scheduler import stop_wavelet_scheduler
|
||||
stop_wavelet_scheduler()
|
||||
|
||||
|
||||
app.include_router(market_data.router)
|
||||
|
||||
@@ -211,6 +211,35 @@ def wavelet_reliability_endpoint(
|
||||
return result
|
||||
|
||||
|
||||
# ── Watchlist refresh scheduler — Saxo-priced quotes + wavelet recompute, own cadence ──
|
||||
# ── (services/wavelet_scheduler.py), independent of the once-a-day auto_cycle ─────────
|
||||
|
||||
class RefreshSettingsRequest(BaseModel):
|
||||
enabled: bool
|
||||
refresh_minutes: float
|
||||
|
||||
|
||||
@router.get("/refresh-settings")
|
||||
def get_refresh_settings():
|
||||
from services.wavelet_scheduler import get_settings
|
||||
return get_settings()
|
||||
|
||||
|
||||
@router.put("/refresh-settings")
|
||||
def update_refresh_settings(req: RefreshSettingsRequest):
|
||||
from services.wavelet_scheduler import set_settings, get_settings
|
||||
set_settings(req.enabled, req.refresh_minutes)
|
||||
return get_settings()
|
||||
|
||||
|
||||
@router.post("/refresh-now")
|
||||
def refresh_now():
|
||||
"""Manual immediate refresh of the whole Watchlist (Saxo-priced quotes + wavelet
|
||||
recompute) — doesn't wait for the periodic poll."""
|
||||
from services.wavelet_scheduler import run_refresh_pass
|
||||
return {"signal_rows": run_refresh_pass()}
|
||||
|
||||
|
||||
# ── Saved simulation/optimization runs ────────────────────────────────────────
|
||||
|
||||
class SimulationCreate(BaseModel):
|
||||
|
||||
76
backend/services/wavelet_scheduler.py
Normal file
76
backend/services/wavelet_scheduler.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Periodic Saxo-priced Watchlist refresh + wavelet recompute — mirrors services/saxo_scheduler.py's
|
||||
pattern (own thread, own config-driven interval, `while not stop.wait(0)` so the first pass runs
|
||||
immediately on startup). Independent of services/auto_cycle.py's once-a-day cycle, which also runs
|
||||
this same computation as one of its steps but only at cycle cadence — this lets the Dashboard's
|
||||
Wavelets Signal card and Instrument Analysis's cached Wavelet tab (services.wavelet_signals.
|
||||
scan_watchlist_wavelet_signals writes both) stay current without waiting for, or manually
|
||||
triggering, a full cycle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_thread: threading.Thread | None = None
|
||||
_stop = threading.Event()
|
||||
|
||||
DEFAULT_REFRESH_MINUTES = 15
|
||||
|
||||
|
||||
def get_settings() -> dict:
|
||||
from .database import get_config
|
||||
enabled = (get_config("wavelet_refresh_enabled") or "true").lower() == "true"
|
||||
try:
|
||||
minutes = float(get_config("wavelet_refresh_minutes") or str(DEFAULT_REFRESH_MINUTES))
|
||||
except (TypeError, ValueError):
|
||||
minutes = DEFAULT_REFRESH_MINUTES
|
||||
return {"enabled": enabled, "refresh_minutes": minutes}
|
||||
|
||||
|
||||
def set_settings(enabled: bool, refresh_minutes: float) -> None:
|
||||
from .database import set_config
|
||||
set_config("wavelet_refresh_enabled", "true" if enabled else "false")
|
||||
set_config("wavelet_refresh_minutes", str(max(1.0, refresh_minutes)))
|
||||
|
||||
|
||||
def run_refresh_pass() -> int:
|
||||
"""One pass over the whole Watchlist: Saxo-first price fetch + wavelet recompute (see
|
||||
services.wavelet_signals.scan_watchlist_wavelet_signals) — the same work the daily cycle's
|
||||
own wavelet step does, just callable on its own cadence. Shared by the periodic loop and
|
||||
the manual 'refresh now' button. Returns the number of signal rows written."""
|
||||
from .wavelet_signals import compute_and_save_wavelet_signals
|
||||
run_id = f"refresh-{uuid.uuid4().hex[:10]}"
|
||||
try:
|
||||
results = compute_and_save_wavelet_signals(run_id)
|
||||
logger.info(f"[Wavelet Scheduler] Refresh pass complete: {len(results)} signal rows ({run_id})")
|
||||
return len(results)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Wavelet Scheduler] Refresh pass failed: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def _loop(stop: threading.Event):
|
||||
while not stop.wait(0):
|
||||
settings = get_settings()
|
||||
if not settings["enabled"]:
|
||||
stop.wait(timeout=300) # re-check periodically in case it gets enabled without a restart
|
||||
continue
|
||||
run_refresh_pass()
|
||||
stop.wait(timeout=settings["refresh_minutes"] * 60)
|
||||
|
||||
|
||||
def start_wavelet_scheduler():
|
||||
global _thread
|
||||
_stop.clear()
|
||||
if not (_thread and _thread.is_alive()):
|
||||
_thread = threading.Thread(target=_loop, args=(_stop,), name="wavelet-refresh", daemon=True)
|
||||
_thread.start()
|
||||
logger.info("[Wavelet Scheduler] Started")
|
||||
|
||||
|
||||
def stop_wavelet_scheduler():
|
||||
_stop.set()
|
||||
@@ -1852,6 +1852,28 @@ export const useUpdateSaxoSettings = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Wavelet Watchlist refresh — Saxo-priced quotes + wavelet recompute, own cadence
|
||||
// (services/wavelet_scheduler.py), independent of the once-a-day auto_cycle.
|
||||
export const useWaveletRefreshSettings = () =>
|
||||
useQuery<{ enabled: boolean; refresh_minutes: number }>({
|
||||
queryKey: ['wavelet-refresh-settings'],
|
||||
queryFn: () => api.get('/wavelet/refresh-settings').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useUpdateWaveletRefreshSettings = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (settings: { enabled: boolean; refresh_minutes: number }) =>
|
||||
api.put('/wavelet/refresh-settings', settings).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['wavelet-refresh-settings'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useWaveletRefreshNow = () =>
|
||||
useMutation({
|
||||
mutationFn: () => api.post('/wavelet/refresh-now').then(r => r.data as { signal_rows: number }),
|
||||
})
|
||||
|
||||
export type SaxoSnapshotRow = {
|
||||
id: string; symbol: string; snapshot_date: string; spot: number | null
|
||||
expiry_date: string; strike: number; option_type: 'call' | 'put'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, useRenameWatchlistInstrument, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi'
|
||||
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, useRenameWatchlistInstrument, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, useWaveletRefreshSettings, useUpdateWaveletRefreshSettings, useWaveletRefreshNow, type CycleStepDef } from '../hooks/useApi'
|
||||
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup, Radar, Link2, Unlink, Camera, ShieldCheck, ExternalLink } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import SaxoLinkPicker from '../components/SaxoLinkPicker'
|
||||
@@ -492,6 +492,9 @@ function SaxoConnectionCard() {
|
||||
const { data: settings } = useSaxoSettings()
|
||||
const updateSettings = useUpdateSaxoSettings()
|
||||
const snapshotAllNow = useSnapshotAllSaxoNow()
|
||||
const { data: waveletRefreshSettings } = useWaveletRefreshSettings()
|
||||
const updateWaveletRefreshSettings = useUpdateWaveletRefreshSettings()
|
||||
const waveletRefreshNow = useWaveletRefreshNow()
|
||||
const [input, setInput] = useState('')
|
||||
const [snapMsg, setSnapMsg] = useState('')
|
||||
const { data: catalogMatches } = useSaxoCatalog(undefined, input.length >= 2 ? input : undefined)
|
||||
@@ -649,6 +652,41 @@ function SaxoConnectionCard() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs text-slate-500 flex items-center gap-2">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={waveletRefreshSettings?.enabled ?? true}
|
||||
onChange={e => updateWaveletRefreshSettings.mutate({
|
||||
enabled: e.target.checked,
|
||||
refresh_minutes: waveletRefreshSettings?.refresh_minutes ?? 15,
|
||||
})}
|
||||
/>
|
||||
Prix Watchlist (Saxo) + ondelettes recalculés toutes les
|
||||
</label>
|
||||
<input
|
||||
type="number" min={1} max={720}
|
||||
value={waveletRefreshSettings?.refresh_minutes ?? ''}
|
||||
onChange={e => updateWaveletRefreshSettings.mutate({
|
||||
enabled: waveletRefreshSettings?.enabled ?? true,
|
||||
refresh_minutes: parseFloat(e.target.value) || 15,
|
||||
})}
|
||||
className="w-14 bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-xs text-white text-center"
|
||||
/>
|
||||
<span>min — indépendant du cycle complet, tourne dès le démarrage du backend</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => waveletRefreshNow.mutate()}
|
||||
disabled={waveletRefreshNow.isPending}
|
||||
title="Refresh immédiat de toute la watchlist (prix Saxo + ondelettes), sans attendre le prochain passage"
|
||||
className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200 border border-slate-700/50 px-2 py-1 rounded disabled:opacity-40 shrink-0"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', waveletRefreshNow.isPending && 'animate-spin')} /> Rafraîchir maintenant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-[10px] text-slate-600">
|
||||
Catalogue local (Futures/FX — tickers Saxo non-évidents, ex. Or = <code>OG:xcme</code>) :{' '}
|
||||
|
||||
Reference in New Issue
Block a user