From bef0092ca86ae75989f693a409470349c53aedc4 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 30 Jul 2026 18:02:30 +0200 Subject: [PATCH] feat: saxo price --- backend/routers/instruments.py | 23 ---- backend/services/instrument_service.py | 28 ----- frontend/src/hooks/useApi.ts | 9 -- frontend/src/pages/Config.tsx | 142 ++++++++++++------------- 4 files changed, 71 insertions(+), 131 deletions(-) diff --git a/backend/routers/instruments.py b/backend/routers/instruments.py index 7947da4..3f20ad7 100644 --- a/backend/routers/instruments.py +++ b/backend/routers/instruments.py @@ -17,7 +17,6 @@ from services.instrument_service import ( update_instrument_drivers, update_instrument_saxo_link, quick_add_instrument_from_watchlist, - add_manual_instrument, ) class DriverUpdate(BaseModel): @@ -31,14 +30,6 @@ class SaxoLinkBody(BaseModel): class QuickAddBody(BaseModel): ticker: str - -class AddManualBody(BaseModel): - id: str - name: str - category: str - yf_ticker: Optional[str] = None - saxo_quote_symbol: Optional[str] = None - router = APIRouter(prefix="/api/instruments", tags=["instruments"]) @@ -60,20 +51,6 @@ def quick_add(body: QuickAddBody) -> Dict[str, Any]: raise HTTPException(status_code=404, detail=str(e)) -@router.post("/add-manual") -def add_manual(body: AddManualBody) -> Dict[str, Any]: - """Create a standalone Instrument Analysis entry from scratch — Config page's "Ajouter - un ticker" zone, for instruments that aren't in the Cockpit Watchlist (so quick-add - doesn't apply) and don't warrant hand-authoring an instruments.json entry.""" - try: - return add_manual_instrument( - body.id, name=body.name, category=body.category, - yf_ticker=body.yf_ticker, saxo_quote_symbol=body.saxo_quote_symbol, - ) - except ValueError as e: - raise HTTPException(status_code=409, detail=str(e)) - - @router.get("/{instrument_id}/snapshot") async def instrument_snapshot( instrument_id: str, diff --git a/backend/services/instrument_service.py b/backend/services/instrument_service.py index 5e05a22..2130b24 100644 --- a/backend/services/instrument_service.py +++ b/backend/services/instrument_service.py @@ -227,34 +227,6 @@ def quick_add_instrument_from_watchlist(ticker: str) -> Dict[str, Any]: return {"id": uid, "created": True} -def add_manual_instrument( - instrument_id: str, name: str, category: str, - yf_ticker: Optional[str] = None, saxo_quote_symbol: Optional[str] = None, -) -> Dict[str, Any]: - """Create a standalone Instrument Analysis entry directly (Config page's "Ajouter un - ticker" zone) — same instrument_overrides row shape as quick_add_instrument_from_watchlist - above, but without requiring a pre-existing Cockpit Watchlist row, since the whole point - here is to add a ticker that isn't tracked anywhere yet (e.g. EURCHF, resolved live - against Saxo just before this call). Rejects an id that already resolves — use the - existing drivers/saxo-link editors to modify it instead of silently overwriting.""" - global _configs - if _configs is None: - _load_configs() - - uid = instrument_id.strip().upper() - if uid in _configs: - raise ValueError(f"'{uid}' existe déjà dans Instrument Analysis") - - from services.database import set_instrument_override_quick_add - set_instrument_override_quick_add( - uid, name=name.strip() or uid, yf_ticker=(yf_ticker or uid).strip().upper(), - category=category, saxo_quote_symbol=saxo_quote_symbol, - ) - _load_configs() - logger.info(f"[instrument_service] Manually added {uid} (category={category}, saxo={saxo_quote_symbol})") - return {"id": uid, "created": True} - - # ── DataFrame helpers ────────────────────────────────────────────────────────── def _ohlcv_to_df(records: List[Dict]) -> pd.DataFrame: diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index df6e2f7..30d837c 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -211,15 +211,6 @@ export const useQuickAddInstrument = () => api.post('/instruments/quick-add', { ticker }).then(r => r.data as { id: string; created: boolean }), }) -// Config page's "Ajouter un ticker" zone — creates a standalone Instrument Analysis entry -// (instrument_overrides row) for a ticker that isn't in the Cockpit Watchlist, so -// useQuickAddInstrument above doesn't apply. Same "no instruments.json edit needed" model. -export const useAddManualInstrument = () => - useMutation({ - mutationFn: (body: { id: string; name: string; category: string; yf_ticker?: string; saxo_quote_symbol?: string | null }) => - api.post('/instruments/add-manual', body).then(r => r.data as { id: string; created: boolean }), - }) - // Free-form display name — no longer tied to yfinance's long_name lookup, since an // instrument can now be entirely Saxo-sourced. export const useRenameWatchlistInstrument = () => { diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index c8bbd3a..9dec030 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -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, useWaveletRefreshSettings, useUpdateWaveletRefreshSettings, useWaveletRefreshNow, useAddManualInstrument, 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, useQuickAddInstrument, 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' @@ -33,21 +33,6 @@ const SOURCE_DOCS: Record(null) const expandWatchlist = useExpandSaxoWatchlist() const [expandKeyword, setExpandKeyword] = useState('') const [expandMsg, setExpandMsg] = useState('') @@ -582,29 +574,26 @@ function SaxoConnectionCard() { } } - // Prefill the "add to Instrument Analysis" fields once a test resolves — the user can - // still override name/category/yf_ticker before actually adding it. - useEffect(() => { - if (!testQuote.data) return - setAddName(testQuote.data.description ?? testQuote.data.symbol) - setAddYfTicker(quoteAssetType === 'FxSpot' ? `${testQuote.data.symbol}=X` : testQuote.data.symbol) - setAddCategory(quoteAssetType === 'FxSpot' ? 'fx' : quoteAssetType === 'StockIndex' ? 'equity_index' : 'stock') - setAddMsg('') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [testQuote.data]) - - const handleAddManual = async () => { + // Adds the tested symbol to the Cockpit Watchlist (yfinance's "=X" form for FX, so + // yfinance actually recognizes it as forex and the fallback ticker is correct), links its + // Saxo quote symbol, then quick-adds it into the Instrument Analysis catalog — same three + // steps a user would otherwise do by hand across two different tabs. + const handleAddToInstrumentAnalysis = async () => { if (!testQuote.data) return setAddMsg('') + setAddedInstrumentId(null) + const resolvedSymbol = testQuote.data.symbol + const watchlistTicker = quoteAssetType === 'FxSpot' ? `${resolvedSymbol}=X` : resolvedSymbol try { - const r = await addManual.mutateAsync({ - id: testQuote.data.symbol, - name: addName.trim() || testQuote.data.description || testQuote.data.symbol, - category: addCategory, - yf_ticker: addYfTicker.trim() || undefined, - saxo_quote_symbol: testQuote.data.symbol, - }) - setAddMsg(`✓ ${r.id} ajouté à Instrument Analysis`) + try { + await addWatchlistTicker.mutateAsync(watchlistTicker) + } catch (e: any) { + if (e?.response?.status !== 409) throw e // already in the Watchlist — fine, just (re)link it below + } + await watchlistQuoteLink.mutateAsync({ ticker: watchlistTicker, saxoSymbol: resolvedSymbol }) + const r = await quickAddInstrument.mutateAsync(watchlistTicker) + setAddMsg(`✓ ${r.id} ${r.created ? 'ajouté' : 'déjà présent'} dans Instrument Analysis, lié à Saxo (${resolvedSymbol})`) + setAddedInstrumentId(r.id) } catch (e: any) { setAddMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`) } @@ -854,36 +843,14 @@ function SaxoConnectionCard() { ✓ {testQuote.data.symbol} ({testQuote.data.description}) — Bid {testQuote.data.bid} / Ask {testQuote.data.ask} · {testQuote.data.price_source} -
- setAddName(e.target.value)} - placeholder="Nom affiché" - className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-40 focus:outline-none focus:border-blue-500/50" - /> - - setAddYfTicker(e.target.value.toUpperCase())} - placeholder="Ticker yfinance (fallback)" - title="Utilisé si Saxo devient indisponible pour ce ticker — format yfinance, ex. EURCHF=X" - className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-44 focus:outline-none focus:border-blue-500/50" - /> - -
+ )} {testQuote.isError && ( @@ -891,7 +858,40 @@ function SaxoConnectionCard() { ✗ {(testQuote.error as any)?.response?.data?.detail ?? 'échec'} )} - {addMsg &&
{addMsg}
} + {addMsg && ( +
+ {addMsg} + {addedInstrumentId && ( + <> + {' — '} + + Ouvrir dans Instrument Analysis + + + )} +
+ )} + + {(instrumentWatchlistItems ?? []).length > 0 && ( +
+
+ Instruments Watchlist (disponibles dans Instrument Analysis via son sélecteur) : +
+
+ {(instrumentWatchlistItems as any[]).map(w => ( + + {w.ticker} + {w.saxo_quote_symbol && } + + + ))} +
+
+ )} )