feat: isolated cycle action — Check New Market Events
Décompose le cycle en 8 actions appelables individuellement. Action 1 implémentée : scan de 4 sources (news RSS, surprises FRED, MA crossovers yfinance, rapports institutionnels) → création de market_events avec déduplication. UI CycleActions page + sidebar link. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import Timeline from './pages/Timeline'
|
||||
import ExternalSnapshot from './pages/ExternalSnapshot'
|
||||
import InstrumentDashboard from './pages/InstrumentDashboard'
|
||||
import ImpactMonitor from './pages/ImpactMonitor'
|
||||
import CycleActions from './pages/CycleActions'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useCycleWatcher } from './hooks/useApi'
|
||||
|
||||
@@ -71,6 +72,7 @@ export default function App() {
|
||||
<Route path="/instruments" element={<Navigate to="/instruments/SPY" replace />} />
|
||||
<Route path="/instruments/:id" element={<InstrumentDashboard />} />
|
||||
<Route path="/impact" element={<ImpactMonitor />} />
|
||||
<Route path="/cycle-actions" element={<CycleActions />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard, Globe, BarChart2, FlaskConical,
|
||||
History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText, Gauge, GitCompare, Building2, Users, Layers, ScanEye, CandlestickChart, Crosshair
|
||||
History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText, Gauge, GitCompare, Building2, Users, Layers, ScanEye, CandlestickChart, Crosshair, PlayCircle
|
||||
} from 'lucide-react'
|
||||
import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
@@ -29,6 +29,7 @@ const nav = [
|
||||
{ to: '/specialist-desks', icon: Users, label: 'Specialist Desks' },
|
||||
{ to: '/instruments', icon: CandlestickChart, label: 'Instrument Snap.' },
|
||||
{ to: '/impact', icon: Crosshair, label: 'Impact Monitor' },
|
||||
{ to: '/cycle-actions', icon: PlayCircle, label: 'Cycle Actions' },
|
||||
{ to: '/snapshot', icon: ScanEye, label: 'Snapshot Externe' },
|
||||
{ to: '/timeline', icon: Layers, label: 'Timeline' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'System Logs' },
|
||||
|
||||
323
frontend/src/pages/CycleActions.tsx
Normal file
323
frontend/src/pages/CycleActions.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Play, CheckSquare, Clock, AlertCircle, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'
|
||||
|
||||
const API = 'http://localhost:8000'
|
||||
|
||||
interface ActionDef {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
status: 'idle' | 'running'
|
||||
implemented: boolean
|
||||
}
|
||||
|
||||
interface CreatedEvent {
|
||||
name: string
|
||||
category: string
|
||||
date: string
|
||||
source: string
|
||||
}
|
||||
|
||||
interface ActionResult {
|
||||
action: string
|
||||
status: string
|
||||
started_at: string
|
||||
finished_at: string
|
||||
duration_s: number
|
||||
total_created: number
|
||||
detail: {
|
||||
news: CreatedEvent[]
|
||||
eco: CreatedEvent[]
|
||||
technical: CreatedEvent[]
|
||||
reports: CreatedEvent[]
|
||||
}
|
||||
}
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
news: 'News',
|
||||
eco: 'Éco. FRED',
|
||||
technical: 'Technique',
|
||||
reports: 'Reports',
|
||||
}
|
||||
|
||||
const CAT_COLORS: Record<string, string> = {
|
||||
event_calendar: 'text-amber-400',
|
||||
geopolitical: 'text-red-400',
|
||||
fundamental: 'text-emerald-400',
|
||||
report: 'text-blue-400',
|
||||
sentiment: 'text-purple-400',
|
||||
technical: 'text-cyan-400',
|
||||
}
|
||||
|
||||
// ── Check Market Events config panel ────────────────────────────────────────
|
||||
|
||||
interface CheckEventsConfig {
|
||||
sources: string[]
|
||||
news_impact_min: number
|
||||
news_lookback_hours: number
|
||||
eco_z_threshold: number
|
||||
eco_days: number
|
||||
technical_lookback_days: number
|
||||
report_days: number
|
||||
report_min_importance: number
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: CheckEventsConfig = {
|
||||
sources: ['news', 'eco', 'technical', 'reports'],
|
||||
news_impact_min: 0.55,
|
||||
news_lookback_hours: 48,
|
||||
eco_z_threshold: 1.5,
|
||||
eco_days: 7,
|
||||
technical_lookback_days: 7,
|
||||
report_days: 7,
|
||||
report_min_importance: 3,
|
||||
}
|
||||
|
||||
function CheckEventsPanel() {
|
||||
const [config, setConfig] = useState<CheckEventsConfig>(DEFAULT_CONFIG)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [result, setResult] = useState<ActionResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [showConfig, setShowConfig] = useState(false)
|
||||
|
||||
const toggleSource = (src: string) => {
|
||||
setConfig(c => ({
|
||||
...c,
|
||||
sources: c.sources.includes(src)
|
||||
? c.sources.filter(s => s !== src)
|
||||
: [...c.sources, src],
|
||||
}))
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
setRunning(true)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API}/api/actions/check-market-events`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
||||
throw new Error(err.detail || res.statusText)
|
||||
}
|
||||
const data: ActionResult = await res.json()
|
||||
setResult(data)
|
||||
setExpanded(true)
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const allEvents = result
|
||||
? [...(result.detail.news || []), ...(result.detail.eco || []),
|
||||
...(result.detail.technical || []), ...(result.detail.reports || [])]
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800/60 border border-slate-700/40 rounded-xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 px-5 py-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
||||
<span className="text-slate-100 font-semibold text-sm">Check New Market Events</span>
|
||||
<span className="text-xs text-slate-500 ml-1">
|
||||
Scans news · FRED · MA crossovers · rapports institutionnels
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowConfig(x => !x)}
|
||||
className="text-xs text-slate-400 hover:text-slate-200 border border-slate-700 rounded px-2 py-1"
|
||||
>
|
||||
{showConfig ? 'Masquer config' : 'Config'}
|
||||
</button>
|
||||
<button
|
||||
disabled={running || config.sources.length === 0}
|
||||
onClick={run}
|
||||
className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-700
|
||||
disabled:text-slate-500 text-white text-sm font-medium rounded-lg px-4 py-2 transition-colors"
|
||||
>
|
||||
{running ? <Loader2 size={14} className="animate-spin" /> : <Play size={14} />}
|
||||
{running ? 'En cours...' : 'Lancer'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Config panel */}
|
||||
{showConfig && (
|
||||
<div className="px-5 pb-4 border-t border-slate-700/30 pt-4 grid grid-cols-2 gap-4 text-sm">
|
||||
{/* Sources */}
|
||||
<div>
|
||||
<div className="text-slate-400 mb-2 text-xs uppercase tracking-wide">Sources actives</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['news', 'eco', 'technical', 'reports'].map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => toggleSource(s)}
|
||||
className={`px-3 py-1 rounded-full text-xs border transition-colors ${
|
||||
config.sources.includes(s)
|
||||
? 'bg-emerald-600/30 border-emerald-500/50 text-emerald-300'
|
||||
: 'border-slate-700 text-slate-500 hover:border-slate-500'
|
||||
}`}
|
||||
>
|
||||
{SOURCE_LABELS[s]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thresholds */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">News impact min</span>
|
||||
<input type="number" step="0.05" min="0" max="1"
|
||||
value={config.news_impact_min}
|
||||
onChange={e => setConfig(c => ({ ...c, news_impact_min: +e.target.value }))}
|
||||
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">News lookback (h)</span>
|
||||
<input type="number" step="12" min="6" max="168"
|
||||
value={config.news_lookback_hours}
|
||||
onChange={e => setConfig(c => ({ ...c, news_lookback_hours: +e.target.value }))}
|
||||
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">Éco z-score min</span>
|
||||
<input type="number" step="0.25" min="0.5" max="5"
|
||||
value={config.eco_z_threshold}
|
||||
onChange={e => setConfig(c => ({ ...c, eco_z_threshold: +e.target.value }))}
|
||||
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">Report importance min</span>
|
||||
<input type="number" step="1" min="1" max="5"
|
||||
value={config.report_min_importance}
|
||||
onChange={e => setConfig(c => ({ ...c, report_min_importance: +e.target.value }))}
|
||||
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mx-5 mb-4 flex items-center gap-2 text-red-400 text-sm bg-red-900/20 rounded-lg px-3 py-2 border border-red-800/30">
|
||||
<AlertCircle size={14} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result summary */}
|
||||
{result && (
|
||||
<div className="border-t border-slate-700/30">
|
||||
{/* Summary bar */}
|
||||
<div
|
||||
className="flex items-center gap-4 px-5 py-3 cursor-pointer hover:bg-slate-800/30"
|
||||
onClick={() => setExpanded(x => !x)}
|
||||
>
|
||||
<CheckSquare size={14} className="text-emerald-400" />
|
||||
<span className="text-emerald-400 font-semibold text-sm">
|
||||
{result.total_created} événement{result.total_created !== 1 ? 's' : ''} créé{result.total_created !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<div className="flex gap-3 text-xs text-slate-400">
|
||||
{(['news', 'eco', 'technical', 'reports'] as const).map(src => {
|
||||
const count = result.detail[src]?.length ?? 0
|
||||
return count > 0 ? (
|
||||
<span key={src}>{SOURCE_LABELS[src]}: <span className="text-slate-200">{count}</span></span>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
<span className="ml-auto text-xs text-slate-500">{result.duration_s}s</span>
|
||||
{expanded ? <ChevronUp size={14} className="text-slate-400" /> : <ChevronDown size={14} className="text-slate-400" />}
|
||||
</div>
|
||||
|
||||
{/* Event list */}
|
||||
{expanded && allEvents.length > 0 && (
|
||||
<div className="px-5 pb-4 space-y-1">
|
||||
{allEvents.map((ev, i) => (
|
||||
<div key={i} className="flex items-start gap-3 py-1.5 border-b border-slate-800/60 last:border-0">
|
||||
<span className="text-xs text-slate-500 w-20 shrink-0">{ev.date}</span>
|
||||
<span className={`text-xs font-mono w-20 shrink-0 ${CAT_COLORS[ev.category] ?? 'text-slate-400'}`}>
|
||||
{ev.category?.replace('_', ' ')}
|
||||
</span>
|
||||
<span className="text-sm text-slate-200 flex-1">{ev.name}</span>
|
||||
<span className="text-xs text-slate-600 shrink-0">{SOURCE_LABELS[ev.source] ?? ev.source}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expanded && allEvents.length === 0 && (
|
||||
<div className="px-5 pb-4 text-sm text-slate-500 italic">
|
||||
Aucun nouvel événement — tout était déjà enregistré ou sous le seuil.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Planned action card ───────────────────────────────────────────────────────
|
||||
|
||||
function PlannedActionCard({ action }: { action: ActionDef }) {
|
||||
return (
|
||||
<div className="bg-dark-800/40 border border-slate-800/50 rounded-xl px-5 py-4 flex items-center gap-3 opacity-50">
|
||||
<Clock size={14} className="text-slate-600 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-slate-400 text-sm font-medium">{action.label}</div>
|
||||
<div className="text-slate-600 text-xs mt-0.5 truncate">{action.description}</div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-700 border border-slate-800 rounded px-2 py-0.5 shrink-0">Planifié</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function CycleActions() {
|
||||
const [actions, setActions] = useState<ActionDef[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API}/api/actions`)
|
||||
.then(r => r.json())
|
||||
.then(d => setActions(d.actions || []))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const planned = actions.filter(a => !a.implemented)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-100">Actions Cycle</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">
|
||||
Décomposition du cycle en 8 actions isolées — lancez-les indépendamment pour tester ou rejouer une étape.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Implemented actions */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold">Actions disponibles</div>
|
||||
<CheckEventsPanel />
|
||||
</div>
|
||||
|
||||
{/* Planned */}
|
||||
{planned.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs uppercase tracking-widest text-slate-500 font-semibold">Prochaines actions</div>
|
||||
{planned.map(a => <PlannedActionCard key={a.id} action={a} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user