feat: instrument analysis

This commit is contained in:
OpenSquared
2026-06-28 16:57:16 +02:00
parent e4e17330b4
commit 6a683411ed
3 changed files with 85 additions and 20 deletions

View File

@@ -25,10 +25,11 @@ interface ChartEvent {
}
interface Props {
priceData: PriceCandle[]
indicators: Record<string, LinePoint[]>
events?: ChartEvent[]
height?: number
priceData: PriceCandle[]
indicators: Record<string, LinePoint[]>
events?: ChartEvent[]
height?: number
chartType?: 'candles' | 'line'
onDateHover?: (date: string | null) => void
}
@@ -57,7 +58,7 @@ const CAT_LABELS: Record<string, string> = {
technical: 'Tech.',
}
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, onDateHover }: Props) {
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover }: Props) {
const containerRef = useRef<HTMLDivElement>(null)
const cleanupRef = useRef<(() => void) | null>(null)
const onHoverRef = useRef(onDateHover)
@@ -119,16 +120,30 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
})))
}
// Candlesticks
const candleSeries = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#6ee7b7',
wickDownColor: '#fca5a5',
})
candleSeries.setData(priceData)
// Prix principal — chandeliers ou courbe lisse
if (chartType === 'line') {
const areaSeries = chart.addAreaSeries({
lineColor: '#3b82f6',
topColor: 'rgba(59,130,246,0.18)',
bottomColor: 'rgba(59,130,246,0.01)',
lineWidth: 2,
priceLineVisible: true,
lastValueVisible: true,
crosshairMarkerVisible: true,
crosshairMarkerRadius: 4,
})
areaSeries.setData(priceData.map(d => ({ time: d.time as any, value: d.close })))
} else {
const candleSeries = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#6ee7b7',
wickDownColor: '#fca5a5',
})
candleSeries.setData(priceData)
}
// MA lines
for (const [key, color] of Object.entries(MA_COLORS)) {
@@ -278,7 +293,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
cleanupRef.current?.()
cleanupRef.current = null
}
}, [priceData, indicators, events, height])
}, [priceData, indicators, events, height, chartType])
return (
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">

View File

@@ -1066,14 +1066,20 @@ function ExplanationScore({
// ── Main page ─────────────────────────────────────────────────────────────────
const PERIODS = [
{ key: '3mo', label: '3M' }, { key: '6mo', label: '6M' },
{ key: '1y', label: '1Y' }, { key: '2y', label: '2Y' }, { key: '5y', label: '5Y' },
{ key: '5d', label: '5D' },
{ key: '1mo', label: '1M' },
{ key: '3mo', label: '3M' },
{ key: '6mo', label: '6M' },
{ key: '1y', label: '1Y' },
{ key: '2y', label: '2Y' },
{ key: '5y', label: '5Y' },
]
export default function InstrumentDashboard() {
const { id = 'SPY' } = useParams<{ id: string }>()
const navigate = useNavigate()
const [period, setPeriod] = useState('1y')
const [chartStyle, setChartStyle] = useState<'candles' | 'line'>('candles')
const [instruments, setInstruments] = useState<InstrumentConfig[]>([])
const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
const [narrative, setNarrative] = useState('')
@@ -1265,6 +1271,20 @@ export default function InstrumentDashboard() {
)}
<div className="ml-auto flex items-center gap-2">
{/* Chart style toggle */}
<div className="flex items-center bg-dark-800 border border-slate-700/40 rounded-xl p-1">
<button
onClick={() => setChartStyle('candles')}
title="Chandeliers"
className={clsx('p-1.5 rounded-lg transition-colors', chartStyle === 'candles' ? 'bg-blue-700/50 text-blue-300' : 'text-slate-500 hover:text-white hover:bg-dark-700')}
><BarChart2 className="w-3.5 h-3.5" /></button>
<button
onClick={() => setChartStyle('line')}
title="Courbe lisse"
className={clsx('p-1.5 rounded-lg transition-colors', chartStyle === 'line' ? 'bg-blue-700/50 text-blue-300' : 'text-slate-500 hover:text-white hover:bg-dark-700')}
><TrendingUp className="w-3.5 h-3.5" /></button>
</div>
{/* Period selector */}
<div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
{PERIODS.map(p => (
<button key={p.key} onClick={() => setPeriod(p.key)}
@@ -1300,6 +1320,7 @@ export default function InstrumentDashboard() {
indicators={snapshot.indicators}
events={[]}
height={420}
chartType={chartStyle}
onDateHover={handleDateHover}
/>

View File

@@ -998,6 +998,7 @@ export default function MarketEvents() {
// ── Filter state ─────────────────────────────────────────────────────────
const [search, setSearch] = useState('')
const [category, setCategory] = useState('')
const [level, setLevel] = useState('')
const [minScore, setMinScore] = useState(0)
const [evaluated, setEvaluated] = useState<'' | 'true' | 'false'>('')
@@ -1049,6 +1050,7 @@ export default function MarketEvents() {
setLoading(true)
const params = new URLSearchParams()
if (search) params.set('search', search)
if (category) params.set('category', category)
if (level) params.set('level', level)
if (minScore) params.set('min_score', String(minScore))
if (dateFrom) params.set('date_from', dateFrom)
@@ -1075,7 +1077,7 @@ export default function MarketEvents() {
if (e.name === 'AbortError') return // superseded by a newer load
}
setLoading(false)
}, [search, level, minScore, dateFrom, dateTo, genFrom, genTo, evaluated,
}, [search, category, level, minScore, dateFrom, dateTo, genFrom, genTo, evaluated,
instTicker, instMinScore, instDir, sortBy, sortDir])
useEffect(() => {
@@ -1163,6 +1165,33 @@ export default function MarketEvents() {
</div>
{/* ── Quick category filter ── */}
<div className="px-3 py-1.5 border-b border-slate-800/40 flex items-center gap-1 flex-wrap">
{([
{ key: '', label: 'Tout', dot: 'bg-slate-500' },
{ key: 'event_calendar', label: '★ Cal.', dot: 'bg-amber-500' },
{ key: 'geopolitical', label: '★ Géo.', dot: 'bg-red-500' },
{ key: 'fundamental', label: '★ Fund.', dot: 'bg-emerald-500' },
{ key: 'report', label: '★ Rep.', dot: 'bg-blue-500' },
{ key: 'sentiment', label: '★ Sent.', dot: 'bg-violet-500' },
{ key: 'technical', label: '★ Tech.', dot: 'bg-cyan-500' },
] as const).map(({ key, label, dot }) => (
<button
key={key}
onClick={() => setCategory(key)}
className={clsx(
'flex items-center gap-1 px-2 py-0.5 rounded text-[10px] border transition-colors',
category === key
? 'bg-slate-700/80 border-slate-500/60 text-slate-200'
: 'border-slate-800/60 text-slate-600 hover:border-slate-600 hover:text-slate-400'
)}
>
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', dot)} />
{label}
</button>
))}
</div>
{/* ── Sort + more filters toggle ── */}
<div className="px-3 py-1.5 border-b border-slate-800/40 flex items-center gap-2">
<span className="text-xs text-slate-600">Tri:</span>
@@ -1183,7 +1212,7 @@ export default function MarketEvents() {
? 'bg-slate-700 border-slate-600 text-slate-300'
: 'border-slate-800 text-slate-600 hover:border-slate-600')}>
<SlidersHorizontal size={10} />
Filtres {(level || minScore > 0 || evaluated || instTicker) ? '●' : ''}
Filtres {(category || level || minScore > 0 || evaluated || instTicker) ? '●' : ''}
</button>
</div>