feat: wavelets
This commit is contained in:
@@ -180,6 +180,10 @@ def startup():
|
|||||||
# Start Saxo OAuth token refresh + options-chain snapshot poller
|
# Start Saxo OAuth token refresh + options-chain snapshot poller
|
||||||
from services.saxo_scheduler import start_saxo_scheduler
|
from services.saxo_scheduler import start_saxo_scheduler
|
||||||
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
|
# One-time cleanup: collapse snapshot rows stored before save-time dedup existed
|
||||||
try:
|
try:
|
||||||
@@ -270,6 +274,8 @@ def shutdown():
|
|||||||
stop_institutional_scheduler()
|
stop_institutional_scheduler()
|
||||||
from services.saxo_scheduler import stop_saxo_scheduler
|
from services.saxo_scheduler import stop_saxo_scheduler
|
||||||
stop_saxo_scheduler()
|
stop_saxo_scheduler()
|
||||||
|
from services.wavelet_scheduler import stop_wavelet_scheduler
|
||||||
|
stop_wavelet_scheduler()
|
||||||
|
|
||||||
|
|
||||||
app.include_router(market_data.router)
|
app.include_router(market_data.router)
|
||||||
|
|||||||
@@ -211,6 +211,35 @@ def wavelet_reliability_endpoint(
|
|||||||
return result
|
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 ────────────────────────────────────────
|
# ── Saved simulation/optimization runs ────────────────────────────────────────
|
||||||
|
|
||||||
class SimulationCreate(BaseModel):
|
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 = {
|
export type SaxoSnapshotRow = {
|
||||||
id: string; symbol: string; snapshot_date: string; spot: number | null
|
id: string; symbol: string; snapshot_date: string; spot: number | null
|
||||||
expiry_date: string; strike: number; option_type: 'call' | 'put'
|
expiry_date: string; strike: number; option_type: 'call' | 'put'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
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 { 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 clsx from 'clsx'
|
||||||
import SaxoLinkPicker from '../components/SaxoLinkPicker'
|
import SaxoLinkPicker from '../components/SaxoLinkPicker'
|
||||||
@@ -492,6 +492,9 @@ function SaxoConnectionCard() {
|
|||||||
const { data: settings } = useSaxoSettings()
|
const { data: settings } = useSaxoSettings()
|
||||||
const updateSettings = useUpdateSaxoSettings()
|
const updateSettings = useUpdateSaxoSettings()
|
||||||
const snapshotAllNow = useSnapshotAllSaxoNow()
|
const snapshotAllNow = useSnapshotAllSaxoNow()
|
||||||
|
const { data: waveletRefreshSettings } = useWaveletRefreshSettings()
|
||||||
|
const updateWaveletRefreshSettings = useUpdateWaveletRefreshSettings()
|
||||||
|
const waveletRefreshNow = useWaveletRefreshNow()
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [snapMsg, setSnapMsg] = useState('')
|
const [snapMsg, setSnapMsg] = useState('')
|
||||||
const { data: catalogMatches } = useSaxoCatalog(undefined, input.length >= 2 ? input : undefined)
|
const { data: catalogMatches } = useSaxoCatalog(undefined, input.length >= 2 ? input : undefined)
|
||||||
@@ -649,6 +652,41 @@ function SaxoConnectionCard() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</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="flex items-center justify-between mb-2">
|
||||||
<div className="text-[10px] text-slate-600">
|
<div className="text-[10px] text-slate-600">
|
||||||
Catalogue local (Futures/FX — tickers Saxo non-évidents, ex. Or = <code>OG:xcme</code>) :{' '}
|
Catalogue local (Futures/FX — tickers Saxo non-évidents, ex. Or = <code>OG:xcme</code>) :{' '}
|
||||||
|
|||||||
Reference in New Issue
Block a user