// TradingView Lightweight Charts wrapper. // Owns chart lifecycle, data sync, toolbar, trade markers, and loading state. // Props (read from TerminalVariant): // - priceRows: array of {ts, open, high, low, close, volume} (strings or numbers accepted) // - indicatorsByPane: {price: [...], subpane_1: [...], subpane_2: [...]} // - trades: array of trade dicts (see spec §4.6) // - fetchStatus: 'idle' | 'loading' | 'slow-loading' // - fetchContext: {mode, symbol, timeframe, rangeLabel, strategy} — used in the slow-loading overlay // mode 'preview' shows "LOADING BARS · SYM/TF · RANGE" // mode 'run' shows "RUNNING BACKTEST · STRATEGY" // - dateRange: [startISO, endISO] // - onRangeChange: (newDateRange) => void // - theme: object of color tokens {fg, bg, bg2, line, dim, green, amber, red} // // TODO(tech-debt): decompose into: // - trading-chart/chart-core.jsx (main chart lifecycle + panes) // - trading-chart/chart-toolbar.jsx (RANGE presets + OPTIONS dropdown) // - trading-chart/chart-primitives.jsx (VerticalLinePrimitive + related) // - trading-chart/chart-loader.jsx (adaptive progress bar + overlay) // Current file is ~1000 lines; split when any pane type or primitive is added. // Accept OHLCV as either string (legacy run payload from bootstrap.py) or number // (new /api/bars payload). LWC needs numbers. const toNum = (v) => (v === null || v === undefined ? null : typeof v === 'number' ? v : parseFloat(v)); // LWC time format: UNIX seconds for intraday, business-day object for daily. // We use UNIX seconds uniformly — it's valid for all timeframes. const toUnixSec = (iso) => Math.floor(new Date(iso).getTime() / 1000); // Map [{ts, open, high, low, close}] -> LWC candlestick rows. const mapCandles = (rows) => (rows || []) .map((r) => ({ time: toUnixSec(r.ts), open: toNum(r.open), high: toNum(r.high), low: toNum(r.low), close: toNum(r.close), })) .filter( (r) => Number.isFinite(r.time) && Number.isFinite(r.open) && Number.isFinite(r.high) && Number.isFinite(r.low) && Number.isFinite(r.close) ); // Heikin-Ashi candle conversion. Each HA candle depends on the previous HA candle's open+close. const toHeikinAshi = (candles) => { const out = []; let prev = null; for (const c of candles) { const haClose = (c.open + c.high + c.low + c.close) / 4; const haOpen = prev ? (prev.open + prev.close) / 2 : (c.open + c.close) / 2; const haHigh = Math.max(c.high, haOpen, haClose); const haLow = Math.min(c.low, haOpen, haClose); const ha = { time: c.time, open: haOpen, high: haHigh, low: haLow, close: haClose }; out.push(ha); prev = ha; } return out; }; // Map ts-aligned series (indicator values array) -> LWC line series rows, skipping nulls. const mapIndicatorLine = (rows, values) => (rows || []) .map((r, i) => ({ time: toUnixSec(r.ts), value: toNum(values[i]) })) .filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value)); // Expand one trade into two events (entry + exit) with label and color. // Trade payload shape (from bootstrap.py): {id, side, entry_ts, exit_ts, entry, exit, pnl_pct, ...} // side is serialized as "buy" / "sell" (OrderSide enum values) — treat "buy" as long, "sell" as short. const expandTradeEvents = (trade) => { const isLong = window.ChartUtils.isLongSide(trade.side || 'buy'); const entryPx = toNum(trade.entry); const exitPx = toNum(trade.exit); const pnlPct = toNum(trade.pnl_pct); const entry = { type: 'entry', isLong, time: toUnixSec(trade.entry_ts), price: entryPx, label: `${isLong ? 'BUY' : 'SELL'} ${window.ChartUtils.fmt(entryPx, 2)}`, color: isLong ? 'green' : 'red', }; const exit = { type: 'exit', isLong, time: toUnixSec(trade.exit_ts), price: exitPx, // pnl_pct from trade.py is already in percent form (e.g. 34.38 = 34.38%), not a fraction. label: Number.isFinite(pnlPct) ? `${isLong ? 'SELL' : 'BUY'} ${pnlPct >= 0 ? '+' : ''}${window.ChartUtils.fmt(Math.abs(pnlPct), 2)}%` : isLong ? 'SELL' : 'BUY', color: Number.isFinite(pnlPct) && pnlPct < 0 ? 'red' : 'green', }; return [entry, exit]; }; // Full-height dashed vertical line primitive for LWC v5 ISeriesPrimitive API. // Attached to a series; draws on that series' pane at a given timestamp. class VerticalLinePrimitive { constructor(options) { this._options = options; // { time, color, lineWidth, dashed } this._chart = null; this._paneViews = [new VerticalLinePaneView(this)]; } attached({ chart }) { this._chart = chart; } detached() { this._chart = null; } updateAllViews() { for (const v of this._paneViews) v.update(); } paneViews() { return this._paneViews; } } class VerticalLinePaneView { constructor(source) { this._source = source; this._x = null; // Cache renderer to avoid allocating a new instance on every repaint (60fps × N primitives). this._renderer = new VerticalLineRenderer(source, null); } update() { const src = this._source; if (!src._chart) { this._x = null; this._renderer._x = null; return; } const ts = src._chart.timeScale(); this._x = ts.timeToCoordinate(src._options.time); this._renderer._x = this._x; } renderer() { return this._renderer; } } class VerticalLineRenderer { constructor(source, x) { this._source = source; this._x = x; } draw(target) { if (this._x === null) return; const opts = this._source._options; target.useMediaCoordinateSpace((scope) => { const { context: ctx, mediaSize } = scope; ctx.save(); ctx.strokeStyle = opts.color; ctx.lineWidth = opts.lineWidth || 1; if (opts.dashed) ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.moveTo(this._x, 0); ctx.lineTo(this._x, mediaSize.height); ctx.stroke(); ctx.restore(); }); } } // Mode constants — avoid stringly-typed comparisons throughout this file and variant-terminal.jsx. // Exposed via window.ChartConstants so variant-terminal.jsx can reference them without duplication. const CHART_TYPE = { CANDLE: 'candle', LINE: 'line', HEIKIN: 'heikin' }; const SCALE_MODE = { LINEAR: 0, LOG: 1 }; const FETCH_STATUS = { IDLE: 'idle', LOADING: 'loading', SLOW: 'slow-loading' }; const FETCH_MODE = { PREVIEW: 'preview', RUN: 'run' }; window.ChartConstants = { CHART_TYPE, SCALE_MODE, FETCH_STATUS, FETCH_MODE }; const RANGE_PRESETS = [ { id: '1D', days: 1 }, { id: '1W', days: 7 }, { id: '1M', days: 30 }, { id: '3M', days: 90 }, { id: '6M', days: 180 }, { id: '1Y', days: 365 }, ]; const TradingChartToolbar = ({ dateRange, onRangeChange, theme, openOptions, onOptionsToggle, onFit, onReset, }) => { const [startISO, endISO] = dateRange || [null, null]; const activePreset = React.useMemo(() => { if (!startISO || !endISO) return null; const ms = new Date(endISO).getTime() - new Date(startISO).getTime(); const days = Math.round(ms / (1000 * 60 * 60 * 24)); return RANGE_PRESETS.find((p) => p.days === days)?.id || null; }, [startISO, endISO]); const applyPreset = (days) => { if (!endISO) return; const end = new Date(endISO); const start = new Date(end); start.setUTCDate(start.getUTCDate() - days); onRangeChange([start.toISOString().slice(0, 10), endISO]); }; return (
RANGE: {RANGE_PRESETS.map((p) => ( ))}
); }; const TradingChart = (props) => { const { dateRange, onRangeChange, theme } = props; const [openOptions, setOpenOptions] = React.useState(false); const [chartType, setChartType] = React.useState(CHART_TYPE.CANDLE); // candle | line | heikin const [scaleMode, setScaleMode] = React.useState(SCALE_MODE.LINEAR); // 0 Normal/Linear, 1 Log const [showVolume, setShowVolume] = React.useState(false); const [seriesVisibility, setSeriesVisibility] = React.useState({ supertrend: true, crsi_fast: true, crsi_slow: true, }); const [priceSeriesVersion, setPriceSeriesVersion] = React.useState(0); const mainContainerRef = React.useRef(null); const mainChartRef = React.useRef(null); const priceSeriesRef = React.useRef({ kind: null, series: null }); const supertrendSeriesRef = React.useRef(null); const crsiFastSeriesRef = React.useRef(null); const crsiSlowSeriesRef = React.useRef(null); const crsiSlowLbSeriesRef = React.useRef(null); const crsiSlowUbSeriesRef = React.useRef(null); const crsiFastLbSeriesRef = React.useRef(null); const crsiFastUbSeriesRef = React.useRef(null); const crsiSignalsMarkersRef = React.useRef(null); const volumeSeriesRef = React.useRef(null); // v5 API: createSeriesMarkers returns a handle with setMarkers() and detach(); no series.setMarkers(). const priceMarkersHandleRef = React.useRef(null); // Tracks { targetSeries, primitive } pairs for cleanup on re-render / unmount. const verticalPrimitivesRef = React.useRef([]); // Install (or swap) the price series for the given chart type. const installPriceSeries = (kind) => { const chart = mainChartRef.current; if (!chart) return; if (priceSeriesRef.current.series) { try { chart.removeSeries(priceSeriesRef.current.series); } catch (e) { console.warn('installPriceSeries: removeSeries failed:', e); } priceSeriesRef.current = { kind: null, series: null }; } let s; if (kind === CHART_TYPE.CANDLE || kind === CHART_TYPE.HEIKIN) { s = chart.addSeries(window.LightweightCharts.CandlestickSeries, { upColor: theme.green, downColor: theme.red, borderUpColor: theme.green, borderDownColor: theme.red, wickUpColor: theme.green, wickDownColor: theme.red, }); } else { s = chart.addSeries(window.LightweightCharts.LineSeries, { color: theme.green, lineWidth: 1, priceLineVisible: false, lastValueVisible: true, }); } priceSeriesRef.current = { kind, series: s }; }; // Push price data for the currently installed series kind. const pushPriceData = () => { const { kind, series } = priceSeriesRef.current; if (!series) return; if (kind === CHART_TYPE.CANDLE) { series.setData(mapCandles(props.priceRows)); } else if (kind === CHART_TYPE.HEIKIN) { series.setData(toHeikinAshi(mapCandles(props.priceRows))); } else { series.setData( (props.priceRows || []) .map((r) => ({ time: toUnixSec(r.ts), value: toNum(r.close) })) .filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value)) ); } }; // Create main chart on mount. React.useEffect(() => { const el = mainContainerRef.current; if (!el || !window.LightweightCharts) return; const chart = window.LightweightCharts.createChart(el, { layout: { background: { color: theme.bg }, textColor: theme.fg, fontFamily: 'JetBrains Mono, monospace', fontSize: 11, }, grid: { vertLines: { color: theme.line, style: 1 }, horzLines: { color: theme.line, style: 1 }, }, rightPriceScale: { borderColor: theme.line }, timeScale: { borderColor: theme.line, timeVisible: true, secondsVisible: false }, crosshair: { mode: 1 }, // Magnet autoSize: true, }); mainChartRef.current = chart; installPriceSeries(CHART_TYPE.CANDLE); supertrendSeriesRef.current = chart.addSeries(window.LightweightCharts.LineSeries, { color: theme.amber, lineWidth: 1, priceLineVisible: false, lastValueVisible: false, }); // Pane 1: cRSI 0-200 pane. Both main lines in white (fg), red bands for cRSI1 // (0-100, slow series), orange bands for cRSI2 (100-200, fast series). // addPane() with no args (v5 API expects optional PaneOptions, not int). chart.addPane(); // Main lines — white so they stand out on the colored band background. crsiFastSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: theme.fg, lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false }, 1, ); crsiSlowSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: theme.fg, lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false }, 1, ); // Red bands around cRSI1 slow series (0-100 scale). crsiSlowLbSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: '#f32121', lineWidth: 1, priceLineVisible: false, lastValueVisible: false }, 1, ); crsiSlowUbSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: '#f32121', lineWidth: 1, priceLineVisible: false, lastValueVisible: false }, 1, ); // Orange bands around cRSI2 fast series (100-200 scale, offset +100). crsiFastLbSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: '#ffa500', lineWidth: 1, priceLineVisible: false, lastValueVisible: false }, 1, ); crsiFastUbSeriesRef.current = chart.addSeries( window.LightweightCharts.LineSeries, { color: '#ffa500', lineWidth: 1, priceLineVisible: false, lastValueVisible: false }, 1, ); // Approximate 65/35 split. Stretch factor is relative; sum is normalized by LWC. const panes = chart.panes(); if (panes.length >= 2) { panes[0].setStretchFactor(65); panes[1].setStretchFactor(35); } // Lock cRSI pane Y-axis to exactly 0-200 (same trick Pine uses: force the // range regardless of data that may spike outside during warmup/edge cases). // Every series in pane 1 returns the same price range so the union equals // that range — no series can push the scale wider. const force0to200 = () => ({ priceRange: { minValue: 0, maxValue: 200 } }); for (const ref of [ crsiFastSeriesRef, crsiSlowSeriesRef, crsiSlowLbSeriesRef, crsiSlowUbSeriesRef, crsiFastLbSeriesRef, crsiFastUbSeriesRef, ]) { ref.current?.applyOptions({ autoscaleInfoProvider: force0to200 }); } return () => { chart.remove(); mainChartRef.current = null; priceSeriesRef.current = { kind: null, series: null }; supertrendSeriesRef.current = null; crsiFastSeriesRef.current = null; crsiSlowSeriesRef.current = null; crsiSlowLbSeriesRef.current = null; crsiSlowUbSeriesRef.current = null; crsiFastLbSeriesRef.current = null; crsiFastUbSeriesRef.current = null; crsiSignalsMarkersRef.current = null; volumeSeriesRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // create once; theme changes handled via options update below // Swap price series when chart type changes. React.useEffect(() => { if (!mainChartRef.current) return; installPriceSeries(chartType); // Re-bind data for the new series kind. pushPriceData(); // Bump the version so markers effect re-runs and re-attaches markers to new series. setPriceSeriesVersion((v) => v + 1); // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartType]); // Apply theme to live chart when it changes. React.useEffect(() => { const chart = mainChartRef.current; if (!chart) return; chart.applyOptions({ layout: { background: { color: theme.bg }, textColor: theme.fg }, grid: { vertLines: { color: theme.line }, horzLines: { color: theme.line }, }, rightPriceScale: { borderColor: theme.line }, timeScale: { borderColor: theme.line }, }); const { kind, series: priceSeries } = priceSeriesRef.current; if (priceSeries) { if (kind === CHART_TYPE.CANDLE || kind === CHART_TYPE.HEIKIN) { priceSeries.applyOptions({ upColor: theme.green, downColor: theme.red, borderUpColor: theme.green, borderDownColor: theme.red, wickUpColor: theme.green, wickDownColor: theme.red, }); } else { priceSeries.applyOptions({ color: theme.green }); } } supertrendSeriesRef.current?.applyOptions({ color: theme.amber }); // cRSI main lines track theme foreground for visibility in both dark/bright modes. // Band series keep their fixed red/orange colors per the Pine Script spec. crsiFastSeriesRef.current?.applyOptions({ color: theme.fg }); crsiSlowSeriesRef.current?.applyOptions({ color: theme.fg }); }, [theme.bg, theme.fg, theme.line, theme.green, theme.red, theme.amber]); // Push price + Supertrend data whenever inputs change. React.useEffect(() => { const { series: priceSeries } = priceSeriesRef.current; const supertrendSeries = supertrendSeriesRef.current; if (!priceSeries || !supertrendSeries) return; pushPriceData(); const supertrend = (props.indicatorsByPane?.price || []).find((i) => i.name === 'supertrend'); // Bicolor Supertrend: green when line is below price (uptrend, line = lower band), // red when line is above price (downtrend, line = upper band). LWC v5 LineData // supports per-point `color` that overrides the series default for that segment. if (supertrend && supertrend.values && props.priceRows && props.priceRows.length) { const points = []; for (let i = 0; i < props.priceRows.length; i++) { const row = props.priceRows[i]; const stVal = toNum(supertrend.values[i]); if (!Number.isFinite(stVal)) continue; const close = toNum(row.close); if (!Number.isFinite(close)) continue; points.push({ time: toUnixSec(row.ts), value: stVal, color: stVal < close ? theme.green : theme.red, }); } supertrendSeries.setData(points); } else { supertrendSeries.setData([]); } const subpane1 = props.indicatorsByPane?.subpane_1 || []; const crsiFast = subpane1.find((i) => i.name === 'crsi_fast'); const crsiSlow = subpane1.find((i) => i.name === 'crsi_slow'); crsiFastSeriesRef.current?.setData( crsiFast && crsiFast.values ? mapIndicatorLine(props.priceRows, crsiFast.values) : [] ); crsiSlowSeriesRef.current?.setData( crsiSlow && crsiSlow.values ? mapIndicatorLine(props.priceRows, crsiSlow.values) : [] ); // cRSI band series — fixed red/orange per Pine Script (no theme recolor). const slowLb = subpane1.find((i) => i.name === 'crsi_slow_lb'); const slowUb = subpane1.find((i) => i.name === 'crsi_slow_ub'); const fastLb = subpane1.find((i) => i.name === 'crsi_fast_lb'); const fastUb = subpane1.find((i) => i.name === 'crsi_fast_ub'); crsiSlowLbSeriesRef.current?.setData( slowLb?.values ? mapIndicatorLine(props.priceRows, slowLb.values) : [] ); crsiSlowUbSeriesRef.current?.setData( slowUb?.values ? mapIndicatorLine(props.priceRows, slowUb.values) : [] ); crsiFastLbSeriesRef.current?.setData( fastLb?.values ? mapIndicatorLine(props.priceRows, fastLb.values) : [] ); crsiFastUbSeriesRef.current?.setData( fastUb?.values ? mapIndicatorLine(props.priceRows, fastUb.values) : [] ); if (props.priceRows && props.priceRows.length > 0) { mainChartRef.current?.timeScale().fitContent(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.priceRows, props.indicatorsByPane, theme.green, theme.red]); // Render BOTH/SLOW signal markers on the cRSI slow series (subpane_1). // White arrow = BOTH (both series fired), maroon arrow = SLOW (slow series only). React.useEffect(() => { const crsiSlowSeries = crsiSlowSeriesRef.current; if (!crsiSlowSeries) return; // Detach previous marker handle before re-rendering. if (crsiSignalsMarkersRef.current) { try { crsiSignalsMarkersRef.current.detach(); } catch (_) { // ignore — series may have been removed } crsiSignalsMarkersRef.current = null; } const signals = (props.indicatorsByPane?.subpane_1 || []).find( (i) => i.name === 'crsi_signals', ); const raw = signals?.markers || []; if (!raw.length) return; const lwc = raw.map((m) => { const isLong = m.kind.endsWith('_long'); const isBoth = m.kind.startsWith('both'); return { time: toUnixSec(m.ts), position: isLong ? 'belowBar' : 'aboveBar', shape: isLong ? 'arrowUp' : 'arrowDown', // White for BOTH (visible on both dark/bright), maroon for SLOW-only signals. color: isBoth ? '#ffffff' : '#8b0000', text: isBoth ? 'BOTH' : 'SLOW', }; }); crsiSignalsMarkersRef.current = window.LightweightCharts.createSeriesMarkers( crsiSlowSeries, lwc, ); }, [props.indicatorsByPane]); // Apply scale mode (linear/log) to pane 0 only (cRSI oscillator pane should stay linear). React.useEffect(() => { const chart = mainChartRef.current; if (!chart) return; const panes = chart.panes(); // LWC v5: prefer per-pane priceScale; fall back to chart-level if API not available. if (panes[0] && typeof panes[0].priceScale === 'function') { panes[0].priceScale('right').applyOptions({ mode: scaleMode }); } else { // Fallback: applies to all panes — acceptable until LWC v5 per-pane API is confirmed. // TODO(follow-up): confirm panes()[i].priceScale availability on runtime LWC version. chart.priceScale('right').applyOptions({ mode: scaleMode }); } }, [scaleMode]); // Toggle volume histogram pane and refresh data when priceRows change while volume is on. React.useEffect(() => { const chart = mainChartRef.current; if (!chart) return; if (showVolume) { if (!volumeSeriesRef.current) { // addPane() with no args (v5 API takes optional PaneOptions, not an integer index). if (chart.panes().length < 3) chart.addPane(); volumeSeriesRef.current = chart.addSeries( window.LightweightCharts.HistogramSeries, { color: theme.dim, priceFormat: { type: 'volume' }, priceLineVisible: false, lastValueVisible: false, }, 2, ); const panes = chart.panes(); if (panes[2]) panes[2].setStretchFactor(20); } // Push/refresh data whether just created or pre-existing (handles priceRows changes). volumeSeriesRef.current.setData( (props.priceRows || []).map((r) => ({ time: toUnixSec(r.ts), value: toNum(r.volume) || 0, color: toNum(r.close) >= toNum(r.open) ? `${theme.green}66` : `${theme.red}66`, })) ); } else if (volumeSeriesRef.current) { try { chart.removeSeries(volumeSeriesRef.current); } catch (e) { console.warn('volume: removeSeries failed:', e); } volumeSeriesRef.current = null; } }, [showVolume, props.priceRows, theme.dim, theme.green, theme.red]); // Toggle series visibility (supertrend, cRSI fast/slow). React.useEffect(() => { supertrendSeriesRef.current?.applyOptions({ visible: seriesVisibility.supertrend }); crsiFastSeriesRef.current?.applyOptions({ visible: seriesVisibility.crsi_fast }); crsiSlowSeriesRef.current?.applyOptions({ visible: seriesVisibility.crsi_slow }); }, [ seriesVisibility.supertrend, seriesVisibility.crsi_fast, seriesVisibility.crsi_slow, ]); // Bind trade markers: full-height dashed vertical lines via LWC v5 ISeriesPrimitive API, // plus arrow markers on the price series via v5 createSeriesMarkers. React.useEffect(() => { const priceSeries = priceSeriesRef.current?.series; if (!mainChartRef.current || !priceSeries) return; // --- Detach previous vertical primitives --- for (const { targetSeries, primitive } of verticalPrimitivesRef.current) { try { targetSeries.detachPrimitive(primitive); } catch (e) { console.warn('trade markers: detachPrimitive failed:', e); } } verticalPrimitivesRef.current = []; // --- Detach previous arrow markers --- if (priceMarkersHandleRef.current) { try { priceMarkersHandleRef.current.detach(); } catch (e) { console.warn('trade markers: detach failed:', e); } priceMarkersHandleRef.current = null; } const trades = props.trades || []; if (!trades.length) return; const events = trades.flatMap(expandTradeEvents); // --- Attach vertical primitives (one per trade event per pane) --- // One representative series per pane is sufficient — the primitive draws on that pane's canvas. const verticalTargets = [ { series: priceSeries, opacity: 1 }, { series: crsiFastSeriesRef.current, opacity: 0.6 }, ].filter((t) => t.series != null); for (const ev of events) { const baseColor = ev.color === 'green' ? theme.green : theme.red; for (const { series, opacity } of verticalTargets) { // Append hex alpha suffix for attenuated panes (60% opacity ≈ 0x99). const color = opacity < 1 ? `${baseColor}99` : baseColor; const primitive = new VerticalLinePrimitive({ time: ev.time, color, lineWidth: 1, dashed: true, }); try { series.attachPrimitive(primitive); verticalPrimitivesRef.current.push({ targetSeries: series, primitive }); } catch (e) { console.warn('trade markers: attachPrimitive failed:', e); } } } // --- Arrow markers on price series (unchanged) --- const arrows = events.map((ev) => ({ time: ev.time, position: ev.type === 'entry' ? ev.isLong ? 'belowBar' : 'aboveBar' : ev.isLong ? 'aboveBar' : 'belowBar', color: ev.color === 'green' ? theme.green : theme.red, shape: ev.type === 'entry' ? ev.isLong ? 'arrowUp' : 'arrowDown' : ev.isLong ? 'arrowDown' : 'arrowUp', text: ev.label, })); priceMarkersHandleRef.current = window.LightweightCharts.createSeriesMarkers(priceSeries, arrows); }, [props.trades, theme.green, theme.red, priceSeriesVersion]); return (
setOpenOptions((v) => !v)} onFit={() => mainChartRef.current?.timeScale().fitContent()} onReset={() => { mainChartRef.current?.timeScale().resetTimeScale(); mainChartRef.current?.timeScale().fitContent(); }} />
{openOptions && (
TYPE:{' '} {[CHART_TYPE.CANDLE, CHART_TYPE.LINE, CHART_TYPE.HEIKIN].map((t) => ( ))}
SCALE:{' '} {[{ id: SCALE_MODE.LINEAR, label: 'LIN' }, { id: SCALE_MODE.LOG, label: 'LOG' }].map((o) => ( ))}
VOLUME:{' '}
SHOW:{' '} {[ { id: 'supertrend', label: 'ST' }, { id: 'crsi_fast', label: 'cRSI F' }, { id: 'crsi_slow', label: 'cRSI S' }, ].map((o) => ( ))}
)}
{props.fetchStatus !== FETCH_STATUS.IDLE && (
)} {props.fetchStatus === FETCH_STATUS.SLOW && (
{props.fetchContext?.mode === FETCH_MODE.RUN ? `RUNNING BACKTEST · ${props.fetchContext?.strategy || ''}` : `LOADING BARS · ${props.fetchContext?.symbol || ''} / ${props.fetchContext?.timeframe || ''}${props.fetchContext?.rangeLabel ? ` · ${props.fetchContext.rangeLabel}` : ''}`}
)}
); }; window.TradingChart = TradingChart; if (!document.getElementById('tc-keyframes')) { const style = document.createElement('style'); style.id = 'tc-keyframes'; style.textContent = ` @keyframes tc-progress { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } } @keyframes tc-spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); }