feat: builder strategy
This commit is contained in:
@@ -167,6 +167,21 @@ def _compute_indicators(df: pd.DataFrame, config: Dict) -> Dict[str, Any]:
|
|||||||
result["bb_lower"] = []
|
result["bb_lower"] = []
|
||||||
result["bb_mid"] = []
|
result["bb_mid"] = []
|
||||||
|
|
||||||
|
# Realized volatility (annualized %, rolling 20d stddev of log returns) — a "how
|
||||||
|
# turbulent has price action actually been" overlay, distinct from the market-implied
|
||||||
|
# vol computed elsewhere (vol_surface.py) for the options Strategy Builder.
|
||||||
|
vol_window = chart_cfg.get("volatility_window", 20)
|
||||||
|
if len(close) >= vol_window + 1:
|
||||||
|
log_ret = np.log(close / close.shift(1))
|
||||||
|
realized_vol = log_ret.rolling(vol_window).std() * np.sqrt(252) * 100
|
||||||
|
valid_vol = realized_vol.dropna()
|
||||||
|
result["volatility"] = [
|
||||||
|
{"time": idx.strftime("%Y-%m-%d"), "value": _round(val, 2)}
|
||||||
|
for idx, val in valid_vol.items()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
result["volatility"] = []
|
||||||
|
|
||||||
# RSI 14
|
# RSI 14
|
||||||
if len(close) >= 15:
|
if len(close) >= 15:
|
||||||
delta = close.diff()
|
delta = close.diff()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useEffect } from 'react'
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface PriceCandle {
|
interface PriceCandle {
|
||||||
time: string
|
time: string
|
||||||
@@ -51,6 +51,20 @@ const MA_COLORS: Record<string, string> = {
|
|||||||
ma200: '#a855f7',
|
ma200: '#a855f7',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every overlay a user can show/hide independently. 'bb' toggles the upper+lower band pair
|
||||||
|
// together (they're one visual concept); 'volatility' gets its own price scale since
|
||||||
|
// annualized % lives on a totally different axis than the price series.
|
||||||
|
type OverlayKey = 'ma20' | 'ma50' | 'ma100' | 'ma200' | 'bb' | 'volatility'
|
||||||
|
const OVERLAY_KEYS: OverlayKey[] = ['ma20', 'ma50', 'ma100', 'ma200', 'bb', 'volatility']
|
||||||
|
const OVERLAY_META: Record<OverlayKey, { label: string; color: string; dashed?: boolean; defaultVisible: boolean }> = {
|
||||||
|
ma20: { label: 'MA20', color: MA_COLORS.ma20, defaultVisible: true },
|
||||||
|
ma50: { label: 'MA50', color: MA_COLORS.ma50, defaultVisible: true },
|
||||||
|
ma100: { label: 'MA100', color: MA_COLORS.ma100, defaultVisible: true },
|
||||||
|
ma200: { label: 'MA200', color: MA_COLORS.ma200, defaultVisible: true },
|
||||||
|
bb: { label: 'BB(20,2)', color: '#94a3b8', dashed: true, defaultVisible: true },
|
||||||
|
volatility: { label: 'Volatilité', color: '#ec4899', defaultVisible: false },
|
||||||
|
}
|
||||||
|
|
||||||
const CAT_COLORS: Record<string, string> = {
|
const CAT_COLORS: Record<string, string> = {
|
||||||
event_calendar: '#f59e0b',
|
event_calendar: '#f59e0b',
|
||||||
geopolitical: '#ef4444',
|
geopolitical: '#ef4444',
|
||||||
@@ -74,6 +88,32 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
const cleanupRef = useRef<(() => void) | null>(null)
|
const cleanupRef = useRef<(() => void) | null>(null)
|
||||||
const onHoverRef = useRef(onDateHover)
|
const onHoverRef = useRef(onDateHover)
|
||||||
const onChartReadyRef = useRef(onChartReady)
|
const onChartReadyRef = useRef(onChartReady)
|
||||||
|
const chartRef = useRef<any>(null)
|
||||||
|
const seriesRefs = useRef<Partial<Record<OverlayKey, any[]>>>({})
|
||||||
|
|
||||||
|
// Which overlays have any data at all for THIS instrument — only these get a toggle pill.
|
||||||
|
const availableOverlays = OVERLAY_KEYS.filter(k =>
|
||||||
|
k === 'bb' ? (indicators.bb_upper?.length && indicators.bb_lower?.length) : indicators[k]?.length
|
||||||
|
)
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState<Record<OverlayKey, boolean>>(() =>
|
||||||
|
Object.fromEntries(OVERLAY_KEYS.map(k => [k, OVERLAY_META[k].defaultVisible])) as Record<OverlayKey, boolean>
|
||||||
|
)
|
||||||
|
// Read at chart-(re)build time without forcing a rebuild on every toggle — toggling
|
||||||
|
// itself is handled cheaply below via each series' own applyOptions({visible}).
|
||||||
|
const visibleRef = useRef(visible)
|
||||||
|
useEffect(() => { visibleRef.current = visible }, [visible])
|
||||||
|
|
||||||
|
const toggleOverlay = (key: OverlayKey) => {
|
||||||
|
setVisible(v => {
|
||||||
|
const next = { ...v, [key]: !v[key] }
|
||||||
|
seriesRefs.current[key]?.forEach(s => s.applyOptions({ visible: next[key] }))
|
||||||
|
if (key === 'volatility') {
|
||||||
|
chartRef.current?.priceScale('volatility').applyOptions({ visible: next[key] })
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Keep callback refs fresh without triggering chart rebuild
|
// Keep callback refs fresh without triggering chart rebuild
|
||||||
useEffect(() => { onHoverRef.current = onDateHover }, [onDateHover])
|
useEffect(() => { onHoverRef.current = onDateHover }, [onDateHover])
|
||||||
@@ -116,6 +156,8 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
handleScroll: true,
|
handleScroll: true,
|
||||||
handleScale: true,
|
handleScale: true,
|
||||||
})
|
})
|
||||||
|
chartRef.current = chart
|
||||||
|
seriesRefs.current = {}
|
||||||
|
|
||||||
// Volume (bottom 18%)
|
// Volume (bottom 18%)
|
||||||
const totalVol = priceData.reduce((s, d) => s + (d.volume || 0), 0)
|
const totalVol = priceData.reduce((s, d) => s + (d.volume || 0), 0)
|
||||||
@@ -158,21 +200,24 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
candleSeries.setData(priceData)
|
candleSeries.setData(priceData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MA lines
|
// MA lines — each individually toggleable
|
||||||
for (const [key, color] of Object.entries(MA_COLORS)) {
|
for (const [key, color] of Object.entries(MA_COLORS)) {
|
||||||
const data = indicators[key]
|
const data = indicators[key]
|
||||||
if (data?.length) {
|
if (data?.length) {
|
||||||
chart.addLineSeries({
|
const series = chart.addLineSeries({
|
||||||
color,
|
color,
|
||||||
lineWidth: key === 'ma200' ? 1.5 : 1,
|
lineWidth: key === 'ma200' ? 1.5 : 1,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: true,
|
lastValueVisible: true,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
}).setData(data)
|
visible: visibleRef.current[key as OverlayKey],
|
||||||
|
})
|
||||||
|
series.setData(data)
|
||||||
|
seriesRefs.current[key as OverlayKey] = [series]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bollinger Bands
|
// Bollinger Bands — upper+lower toggle together as one 'bb' overlay
|
||||||
if (indicators.bb_upper?.length && indicators.bb_lower?.length) {
|
if (indicators.bb_upper?.length && indicators.bb_lower?.length) {
|
||||||
const bbOpts = {
|
const bbOpts = {
|
||||||
color: 'rgba(148,163,184,0.32)',
|
color: 'rgba(148,163,184,0.32)',
|
||||||
@@ -181,9 +226,35 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
|
visible: visibleRef.current.bb,
|
||||||
}
|
}
|
||||||
chart.addLineSeries(bbOpts).setData(indicators.bb_upper)
|
const bbUpper = chart.addLineSeries(bbOpts)
|
||||||
chart.addLineSeries(bbOpts).setData(indicators.bb_lower)
|
bbUpper.setData(indicators.bb_upper)
|
||||||
|
const bbLower = chart.addLineSeries(bbOpts)
|
||||||
|
bbLower.setData(indicators.bb_lower)
|
||||||
|
seriesRefs.current.bb = [bbUpper, bbLower]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volatilité réalisée (annualisée, %) — échelle dédiée, cachée par défaut
|
||||||
|
if (indicators.volatility?.length) {
|
||||||
|
chart.priceScale('volatility').applyOptions({
|
||||||
|
visible: visibleRef.current.volatility,
|
||||||
|
borderColor: 'rgba(236,72,153,0.25)',
|
||||||
|
scaleMargins: { top: 0.78, bottom: 0.02 },
|
||||||
|
})
|
||||||
|
const volatilitySeries = chart.addLineSeries({
|
||||||
|
color: '#ec4899',
|
||||||
|
lineWidth: 1,
|
||||||
|
priceScaleId: 'volatility',
|
||||||
|
title: 'Vol. réalisée %',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: true,
|
||||||
|
crosshairMarkerVisible: true,
|
||||||
|
crosshairMarkerRadius: 3,
|
||||||
|
visible: visibleRef.current.volatility,
|
||||||
|
})
|
||||||
|
volatilitySeries.setData(indicators.volatility)
|
||||||
|
seriesRefs.current.volatility = [volatilitySeries]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Theoretical curve — left scale (pips) ─────────────────────────────
|
// ── Theoretical curve — left scale (pips) ─────────────────────────────
|
||||||
@@ -328,6 +399,8 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars)
|
chart.timeScale().unsubscribeVisibleLogicalRangeChange(renderStars)
|
||||||
if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay)
|
if (starsOverlay.parentNode) starsOverlay.parentNode.removeChild(starsOverlay)
|
||||||
chart.remove()
|
chart.remove()
|
||||||
|
chartRef.current = null
|
||||||
|
seriesRefs.current = {}
|
||||||
onChartReadyRef.current?.(null as any, 0)
|
onChartReadyRef.current?.(null as any, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -337,22 +410,35 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
|||||||
cleanupRef.current?.()
|
cleanupRef.current?.()
|
||||||
cleanupRef.current = null
|
cleanupRef.current = null
|
||||||
}
|
}
|
||||||
|
// `visible` intentionally excluded — toggling is handled directly via series refs
|
||||||
|
// above, without tearing down and rebuilding the whole chart.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [priceData, indicators, events, height, chartType, theoryCurve])
|
}, [priceData, indicators, events, height, chartType, theoryCurve])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">
|
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">
|
||||||
<div className="flex items-center gap-5 px-4 py-2 border-b border-slate-700/30 text-xs text-slate-500">
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-slate-700/30 text-xs flex-wrap">
|
||||||
{Object.entries(MA_COLORS).map(([k, c]) => (
|
{availableOverlays.map(key => {
|
||||||
<span key={k} className="flex items-center gap-1.5">
|
const meta = OVERLAY_META[key]
|
||||||
<span className="inline-block w-5 h-0.5 rounded" style={{ background: c }} />
|
const on = visible[key]
|
||||||
{k.toUpperCase()}
|
return (
|
||||||
</span>
|
<button
|
||||||
))}
|
key={key}
|
||||||
<span className="flex items-center gap-1.5 opacity-40">
|
onClick={() => toggleOverlay(key)}
|
||||||
<span className="inline-block w-5 border-t border-dashed border-slate-400" />BB(20,2)
|
title={on ? `Masquer ${meta.label}` : `Afficher ${meta.label}`}
|
||||||
</span>
|
className="flex items-center gap-1.5 px-1.5 py-0.5 rounded transition-opacity hover:bg-slate-700/30"
|
||||||
|
style={{ opacity: on ? 1 : 0.35 }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={meta.dashed ? 'inline-block w-5 border-t border-dashed' : 'inline-block w-5 h-0.5 rounded'}
|
||||||
|
style={meta.dashed ? { borderColor: meta.color } : { background: meta.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-slate-400">{meta.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{theoryCurve?.some(p => p.contributions.length > 0) && (
|
{theoryCurve?.some(p => p.contributions.length > 0) && (
|
||||||
<span className="flex items-center gap-1.5" style={{ color: '#a78bfa' }}>
|
<span className="flex items-center gap-1.5 px-1.5" style={{ color: '#a78bfa' }}>
|
||||||
<span className="inline-block w-5 h-0.5 rounded" style={{ background: '#a78bfa' }} />
|
<span className="inline-block w-5 h-0.5 rounded" style={{ background: '#a78bfa' }} />
|
||||||
Théorie (pips)
|
Théorie (pips)
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user