feat: instrument analysis
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user