feat: saxo
This commit is contained in:
@@ -39,6 +39,45 @@ def update_watchlist(req: WatchlistRequest):
|
|||||||
return {"symbols": get_watchlist()}
|
return {"symbols": get_watchlist()}
|
||||||
|
|
||||||
|
|
||||||
|
class ExpandWatchlistRequest(BaseModel):
|
||||||
|
keyword: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/watchlist/expand")
|
||||||
|
def expand_watchlist(req: ExpandWatchlistRequest):
|
||||||
|
"""Adds every catalog entry matching a keyword (ex. 'EURUSD' -> every weekly/monthly
|
||||||
|
FuturesOption root, not just one) — avoids hand-picking each maturity's exact ticker."""
|
||||||
|
matches = get_saxo_catalog(None, req.keyword, limit=200)
|
||||||
|
if not matches:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Aucune entrée du catalogue ne correspond à '{req.keyword}' — rafraîchissez le catalogue d'abord si besoin.")
|
||||||
|
current = get_watchlist()
|
||||||
|
added = [m["symbol"] for m in matches if m["symbol"] not in current]
|
||||||
|
set_watchlist(current + added)
|
||||||
|
return {"symbols": get_watchlist(), "added": added}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/debug-chain/{symbol}")
|
||||||
|
def debug_chain(symbol: str):
|
||||||
|
"""Raw, unparsed options-chain snapshot for one symbol — diagnostic endpoint only.
|
||||||
|
Also written to System Logs (details field) so it can be read from the Logs page
|
||||||
|
instead of requiring direct URL/API access."""
|
||||||
|
from services.saxo_client import debug_chain_raw, SaxoNotConnected, SaxoApiError
|
||||||
|
from services.database import log_system_event
|
||||||
|
try:
|
||||||
|
result = debug_chain_raw(symbol)
|
||||||
|
log_system_event(
|
||||||
|
level="INFO", source="saxo_debug",
|
||||||
|
message=f"Raw options chain snapshot for {symbol.upper()} (voir details)",
|
||||||
|
ticker=symbol, details=result,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except SaxoNotConnected as e:
|
||||||
|
raise HTTPException(status_code=401, detail=str(e))
|
||||||
|
except SaxoApiError as e:
|
||||||
|
log_system_event(level="ERROR", source="saxo_debug", message=f"debug-chain failed for {symbol.upper()}: {e}", ticker=symbol)
|
||||||
|
raise HTTPException(status_code=502, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
def _validate_symbol(symbol: str) -> dict:
|
def _validate_symbol(symbol: str) -> dict:
|
||||||
from services.saxo_client import resolve_instrument, SaxoNotConnected
|
from services.saxo_client import resolve_instrument, SaxoNotConnected
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -242,6 +242,15 @@ def _snapshot_via_subscription(uic: int, asset_type: str = "StockOption") -> Dic
|
|||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def debug_chain_raw(symbol: str) -> Dict[str, Any]:
|
||||||
|
"""Returns the raw (unparsed) options-chain subscription response for one symbol —
|
||||||
|
diagnostic only, used to pin down exactly where Mid/Greeks/Spot live in Saxo's real
|
||||||
|
payload instead of guessing field names blind."""
|
||||||
|
instrument = resolve_instrument(symbol)
|
||||||
|
subscription = _snapshot_via_subscription(instrument["uic"], asset_type=instrument["asset_type"] or "StockOption")
|
||||||
|
return {"instrument": instrument, "raw": subscription}
|
||||||
|
|
||||||
|
|
||||||
def snapshot_options_chain(symbol: str, target_days: int = 30) -> List[Dict[str, Any]]:
|
def snapshot_options_chain(symbol: str, target_days: int = 30) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns normalized rows ready for services/database.save_saxo_snapshot_rows:
|
Returns normalized rows ready for services/database.save_saxo_snapshot_rows:
|
||||||
|
|||||||
@@ -1701,6 +1701,14 @@ export const useUpdateSaxoWatchlist = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useExpandSaxoWatchlist = () => {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (keyword: string) => api.post('/saxo/watchlist/expand', { keyword }).then(r => r.data as { symbols: string[]; added: string[] }),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-watchlist'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useSnapshotSaxoNow = () =>
|
export const useSnapshotSaxoNow = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: (symbol: string) => api.post(`/saxo/snapshot-now/${encodeURIComponent(symbol)}`).then(r => r.data),
|
mutationFn: (symbol: string) => api.post(`/saxo/snapshot-now/${encodeURIComponent(symbol)}`).then(r => r.data),
|
||||||
|
|||||||
@@ -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, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, 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, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, 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'
|
||||||
|
|
||||||
@@ -439,6 +439,9 @@ function SaxoConnectionCard() {
|
|||||||
const testQuote = useTestSaxoQuote()
|
const testQuote = useTestSaxoQuote()
|
||||||
const [quoteSymbol, setQuoteSymbol] = useState('EURUSD')
|
const [quoteSymbol, setQuoteSymbol] = useState('EURUSD')
|
||||||
const [quoteAssetType, setQuoteAssetType] = useState('FxSpot')
|
const [quoteAssetType, setQuoteAssetType] = useState('FxSpot')
|
||||||
|
const expandWatchlist = useExpandSaxoWatchlist()
|
||||||
|
const [expandKeyword, setExpandKeyword] = useState('')
|
||||||
|
const [expandMsg, setExpandMsg] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
@@ -463,6 +466,21 @@ function SaxoConnectionCard() {
|
|||||||
|
|
||||||
const removeSymbol = (sym: string) => updateWatchlist.mutate(symbols.filter(s => s !== sym))
|
const removeSymbol = (sym: string) => updateWatchlist.mutate(symbols.filter(s => s !== sym))
|
||||||
|
|
||||||
|
const handleExpandWatchlist = async () => {
|
||||||
|
const kw = expandKeyword.trim()
|
||||||
|
if (!kw) return
|
||||||
|
setExpandMsg('')
|
||||||
|
try {
|
||||||
|
const r = await expandWatchlist.mutateAsync(kw)
|
||||||
|
setExpandMsg(r.added.length
|
||||||
|
? `✓ ${r.added.length} ticker(s) ajouté(s) : ${r.added.join(', ')}`
|
||||||
|
: `○ Aucun nouveau ticker pour "${kw}" (déjà présents, ou rafraîchissez le catalogue)`)
|
||||||
|
setExpandKeyword('')
|
||||||
|
} catch (e: any) {
|
||||||
|
setExpandMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSnapshotNow = async (sym: string) => {
|
const handleSnapshotNow = async (sym: string) => {
|
||||||
setSnapMsg('')
|
setSnapMsg('')
|
||||||
try {
|
try {
|
||||||
@@ -595,6 +613,24 @@ function SaxoConnectionCard() {
|
|||||||
<Plus className="w-3.5 h-3.5" /> Ajouter
|
<Plus className="w-3.5 h-3.5" /> Ajouter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
value={expandKeyword}
|
||||||
|
onChange={e => setExpandKeyword(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleExpandWatchlist()}
|
||||||
|
placeholder="Mot-clé sous-jacent (ex. EURUSD, Gold, Brent)"
|
||||||
|
className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-72 focus:outline-none focus:border-blue-500/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleExpandWatchlist}
|
||||||
|
disabled={!expandKeyword.trim() || expandWatchlist.isPending}
|
||||||
|
title="Ajoute toutes les maturités/racines du catalogue correspondant à ce mot-clé (pas besoin de connaître chaque ticker exact)"
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-dark-700 hover:bg-dark-600 border border-slate-700/50 text-slate-300 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" /> Ajouter toutes les maturités
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{expandMsg && <div className="text-[10px] text-slate-500 mb-2">{expandMsg}</div>}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{symbols.map(sym => {
|
{symbols.map(sym => {
|
||||||
const check = validation?.find(v => v.symbol === sym.toUpperCase())
|
const check = validation?.find(v => v.symbol === sym.toUpperCase())
|
||||||
|
|||||||
Reference in New Issue
Block a user