feat: cycle
This commit is contained in:
@@ -107,6 +107,7 @@ export const useEcoCalendar = (params: { period?: string; limit?: number; impact
|
||||
queryKey: ['eco-calendar', params],
|
||||
queryFn: () => api.get('/eco/calendar', { params: { period: 'recent', limit: 50, impacts: 'high,medium', ...params } }).then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
refetchInterval: 5 * 60_000,
|
||||
})
|
||||
|
||||
// ── Instruments Watchlist (Dashboard "radar" card) ────────────────────────────
|
||||
@@ -484,6 +485,16 @@ export const useCycleStatus = () =>
|
||||
refetchInterval: (query) => ((query.state.data as any)?.running ? 5_000 : 30_000),
|
||||
})
|
||||
|
||||
export interface CycleStepParam { type: 'bool' | 'int' | 'float'; label?: string; default: unknown; min?: number; max?: number }
|
||||
export interface CycleStepDef { id: string; label: string; description: string; group: string; params: Record<string, CycleStepParam> }
|
||||
|
||||
export const useCycleStepCatalog = () =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-step-catalog'],
|
||||
queryFn: () => api.get('/cycle/step-catalog').then(r => r.data.steps as CycleStepDef[]),
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
export const useCycleHistory = (limit = 20) =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-history', limit],
|
||||
@@ -494,7 +505,7 @@ export const useCycleHistory = (limit = 20) =>
|
||||
export const useUpdateCycleConfig = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number; trade_budget_eur?: number; preferred_horizon_min?: number; preferred_horizon_max?: number; journal_retention_days?: number; maturity_threshold_pct?: number; weekend_cycle_enabled?: boolean; weekend_cycle_times?: string }) =>
|
||||
mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number; trade_budget_eur?: number; preferred_horizon_min?: number; preferred_horizon_max?: number; journal_retention_days?: number; maturity_threshold_pct?: number; weekend_cycle_enabled?: boolean; weekend_cycle_times?: string; cycle_step_config?: Record<string, Record<string, unknown>> }) =>
|
||||
api.post('/cycle/config', cfg).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['cycle-status'] }),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, validateTicker } 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, 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 } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
@@ -409,6 +409,65 @@ function WatchlistCard() {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Cycle step config — generic renderer driven by GET /api/cycle/step-catalog ─
|
||||
// Same declarative pattern as AI Desks' SIGNAL_CATALOG/SignalToggle: adding a new
|
||||
// knob to CYCLE_STEP_CATALOG (backend) needs zero new frontend code here.
|
||||
function CycleStepRow({
|
||||
step, value, onChange,
|
||||
}: {
|
||||
step: CycleStepDef
|
||||
value: Record<string, any>
|
||||
onChange: (v: Record<string, any>) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const hasToggle = 'enabled' in step.params
|
||||
const enabled = hasToggle ? (value.enabled ?? step.params.enabled.default) : true
|
||||
const otherParams = Object.entries(step.params).filter(([k]) => k !== 'enabled')
|
||||
const update = (key: string, val: any) => onChange({ ...value, [key]: val })
|
||||
|
||||
return (
|
||||
<div className={clsx('rounded-lg border transition-colors', enabled ? 'border-cyan-700/40 bg-cyan-900/10' : 'border-slate-700/30 bg-dark-800/60')}>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||
{hasToggle ? (
|
||||
<button onClick={() => update('enabled', !enabled)} className="shrink-0 text-xs px-2 py-1 rounded border font-semibold"
|
||||
style={{ borderColor: enabled ? '#22d3ee88' : '#47556955', color: enabled ? '#22d3ee' : '#64748b' }}>
|
||||
{enabled ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-2 h-2 rounded-full bg-slate-600 shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={clsx('text-sm font-medium', enabled ? 'text-white' : 'text-slate-500')}>{step.label}</div>
|
||||
<div className="text-xs text-slate-600 truncate">{step.description}</div>
|
||||
</div>
|
||||
{enabled && otherParams.length > 0 && (
|
||||
<button onClick={() => setOpen(o => !o)} className="text-slate-500 hover:text-slate-300 text-xs shrink-0">
|
||||
{open ? '▲' : '▼'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{open && enabled && otherParams.length > 0 && (
|
||||
<div className="px-4 pb-3 space-y-2 border-t border-slate-700/20 pt-2">
|
||||
{otherParams.map(([key, p]) => (
|
||||
<div key={key} className="flex items-center gap-3">
|
||||
<label className="text-xs text-slate-400 w-40 shrink-0">{p.label ?? key}</label>
|
||||
<input
|
||||
type="number"
|
||||
min={p.min}
|
||||
max={p.max}
|
||||
step={p.type === 'float' ? 0.01 : 1}
|
||||
value={value[key] ?? p.default}
|
||||
onChange={e => update(key, p.type === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value))}
|
||||
className="w-28 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Config() {
|
||||
const { data: sources, isLoading } = useSources()
|
||||
const { data: config } = useConfig()
|
||||
@@ -517,6 +576,8 @@ export default function Config() {
|
||||
const [maturityThreshold, setMaturityThreshold] = useState(35)
|
||||
const [weekendEnabled, setWeekendEnabled] = useState(true)
|
||||
const [weekendTimes, setWeekendTimes] = useState<string[]>(['08:00', '22:00'])
|
||||
const [stepConfig, setStepConfig] = useState<Record<string, Record<string, any>>>({})
|
||||
const { data: stepCatalog } = useCycleStepCatalog()
|
||||
useEffect(() => {
|
||||
if (cs) {
|
||||
setCycleEnabled(cs.enabled ?? false)
|
||||
@@ -532,6 +593,7 @@ export default function Config() {
|
||||
setWeekendEnabled(cs.weekend_cycle_enabled ?? true)
|
||||
const times = (cs.weekend_cycle_times || '08:00,22:00').split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
setWeekendTimes(times)
|
||||
if (cs.cycle_step_config) setStepConfig(cs.cycle_step_config)
|
||||
}
|
||||
}, [cs])
|
||||
|
||||
@@ -1060,6 +1122,7 @@ export default function Config() {
|
||||
maturity_threshold_pct: maturityThreshold,
|
||||
weekend_cycle_enabled: weekendEnabled,
|
||||
weekend_cycle_times: weekendTimes.length > 0 ? weekendTimes.join(',') : '08:00,22:00',
|
||||
cycle_step_config: stepConfig,
|
||||
},
|
||||
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configured'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
@@ -1078,6 +1141,41 @@ export default function Config() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Étapes du cycle ── */}
|
||||
<div className="card">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
|
||||
<SlidersHorizontal className="w-4 h-4 text-cyan-400" /> Étapes du cycle
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Chaque étape du cycle automatique (y compris en mode planifié) lit ces réglages —
|
||||
activer/désactiver ou ajuster une fenêtre ici change réellement ce qui se passe au prochain cycle.
|
||||
</p>
|
||||
{['portfolio', 'ai', 'data'].map(group => {
|
||||
const groupSteps = (stepCatalog ?? []).filter(s => s.group === group)
|
||||
if (groupSteps.length === 0) return null
|
||||
return (
|
||||
<div key={group} className="mb-4 last:mb-0">
|
||||
<div className="text-[10px] uppercase tracking-wide text-slate-600 mb-2">
|
||||
{group === 'portfolio' ? 'Portefeuille' : group === 'ai' ? 'Analyse IA' : 'Données & fenêtres'}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{groupSteps.map(step => (
|
||||
<CycleStepRow
|
||||
key={step.id}
|
||||
step={step}
|
||||
value={stepConfig[step.id] ?? {}}
|
||||
onChange={v => setStepConfig(prev => ({ ...prev, [step.id]: v }))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="mt-2 text-[10px] text-slate-600">
|
||||
Ces réglages sont appliqués via le bouton "Apply" ci-dessus (Auto-Cycle Intelligence).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── VaR & PnL Schedulers ── */}
|
||||
<div className="card">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
|
||||
|
||||
@@ -61,6 +61,11 @@ const formatDateShort = (dateStr: string) => {
|
||||
return d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })
|
||||
}
|
||||
|
||||
const formatTimeShort = (isoStr: string) => {
|
||||
const d = new Date(isoStr.includes('Z') || isoStr.includes('+') ? isoStr : isoStr.replace(' ', 'T') + 'Z')
|
||||
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
|
||||
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
|
||||
e.preventDefault(); e.stopPropagation(); onChange(v)
|
||||
@@ -330,17 +335,26 @@ export default function Dashboard() {
|
||||
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5 mt-1 shrink-0">
|
||||
{riskScore.top_risks?.map(([cat, val]) => (
|
||||
<div key={cat} className="flex items-center justify-end gap-1.5 text-[9px]">
|
||||
<span className="text-slate-500 capitalize truncate max-w-[86px]">{(cat as string).replace('_', ' ')}</span>
|
||||
<span className="text-slate-400 font-mono w-7 text-right">{Math.round((val as number) * 100)}%</span>
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(riskScore.breakdown ?? {})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 3)
|
||||
.map(([cat, val]) => (
|
||||
<div key={cat} className="flex items-center justify-end gap-1.5 text-[9px]">
|
||||
<span className="text-slate-500 capitalize truncate max-w-[86px]">{cat.replace('_', ' ')}</span>
|
||||
<span className="text-slate-400 font-mono w-7 text-right">{Math.round(val)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 bg-dark-700 rounded-full h-2 shrink-0">
|
||||
<div className={clsx('h-2 rounded-full', gauge.bg)} style={{ width: `${riskScore.score}%` }} />
|
||||
</div>
|
||||
{riskScore.computed_at && (
|
||||
<div className="mt-1.5 text-[9px] text-slate-600 shrink-0">
|
||||
Score IA — figé au dernier cycle ({formatTimeShort(riskScore.computed_at)})
|
||||
{riskScore.rationale && <span className="text-slate-500"> · {riskScore.rationale}</span>}
|
||||
</div>
|
||||
)}
|
||||
{topNews.length > 0 && (
|
||||
<div className="mt-2.5 pt-2 border-t border-slate-700/30 flex flex-col flex-1 min-h-0">
|
||||
<div className="flex items-center gap-1 text-[9px] text-slate-600 mb-1 shrink-0">
|
||||
|
||||
@@ -35,7 +35,10 @@ export interface GeoRiskScore {
|
||||
score: number
|
||||
level: RiskLevel
|
||||
breakdown: Record<string, number>
|
||||
top_risks: [string, number][]
|
||||
top_risks: string[]
|
||||
computed_at?: string
|
||||
rationale?: string
|
||||
run_id?: string
|
||||
}
|
||||
|
||||
export interface GeoPattern {
|
||||
|
||||
Reference in New Issue
Block a user