feat: overlay entry/exit signal markers on candlestick chart

- Increase position sample limit from 500 to 5000
- Pass positions to CandlestickChart; render entry (green arrowUp) and
  exit (green/red arrowDown by PnL) markers via createSeriesMarkers(),
  pinned to exact entry/exit prices on the chart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:09:22 +02:00
parent e2cbd53354
commit d0706a5d8f
2 changed files with 54 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import {
createChart,
createSeriesMarkers,
CandlestickSeries,
LineSeries,
ColorType,
@@ -8,19 +9,15 @@ import {
type IChartApi,
type CandlestickData,
type LineData,
type SeriesMarker,
type UTCTimestamp,
} from 'lightweight-charts';
import type { CandleEntry } from '../types/api';
import type { CandleEntry, PaperRunPosition } from '../types/api';
import type { SmaSpec } from '../utils/extractSmaSpecs';
// Distinct colours for SMA lines, assigned by index
const SMA_COLORS = ['#2962ff', '#ff6d00', '#6a1b9a', '#e53935', '#00897b', '#795548'];
function computeSma(
candles: CandleEntry[],
field: string,
period: number,
): LineData[] {
function computeSma(candles: CandleEntry[], field: string, period: number): LineData[] {
const result: LineData[] = [];
const key = field as keyof CandleEntry;
for (let i = period - 1; i < candles.length; i++) {
@@ -36,13 +33,50 @@ function computeSma(
return result;
}
function buildMarkers(positions: PaperRunPosition[]): SeriesMarker<UTCTimestamp>[] {
const markers: SeriesMarker<UTCTimestamp>[] = [];
for (const pos of positions) {
const entryTime = (new Date(pos.time_enter).getTime() / 1000) as UTCTimestamp;
const exitTime = (new Date(pos.time_exit).getTime() / 1000) as UTCTimestamp;
const entryPrice = parseFloat(pos.price_entry);
const exitPrice = parseFloat(pos.price_exit);
const profitable = parseFloat(pos.pnl) >= 0;
markers.push({
time: entryTime,
position: 'atPriceBottom',
shape: 'arrowUp',
color: '#198754',
price: entryPrice,
size: 1,
});
markers.push({
time: exitTime,
position: 'atPriceTop',
shape: 'arrowDown',
color: profitable ? '#198754' : '#dc3545',
price: exitPrice,
size: 1,
});
}
// lightweight-charts requires markers sorted by time
markers.sort((a, b) => (a.time as number) - (b.time as number));
return markers;
}
interface Props {
candles: CandleEntry[];
interval: string;
smaSpecs?: SmaSpec[];
positions?: PaperRunPosition[];
}
export default function CandlestickChart({ candles, interval, smaSpecs = [] }: Props) {
export default function CandlestickChart({
candles,
interval,
smaSpecs = [],
positions = [],
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
@@ -85,7 +119,10 @@ export default function CandlestickChart({ candles, interval, smaSpecs = [] }: P
}));
candleSeries.setData(data);
// Overlay SMA lines for specs matching this chart's timeframe
if (positions.length > 0) {
createSeriesMarkers(candleSeries, buildMarkers(positions));
}
smaSpecs.forEach((spec, i) => {
const smaData = computeSma(candles, spec.field, spec.period);
if (smaData.length === 0) return;
@@ -108,7 +145,7 @@ export default function CandlestickChart({ candles, interval, smaSpecs = [] }: P
chart.remove();
chartRef.current = null;
};
}, [candles, interval, smaSpecs]);
}, [candles, interval, smaSpecs, positions]);
return <div ref={containerRef} style={{ height: 400 }} />;
}

View File

@@ -182,7 +182,7 @@ export default function PaperRunDetailPage() {
const { data: run, isLoading, isError, error } = usePaperRun(id!);
const cancelMutation = useCancelPaperRun();
const isComplete = run?.status === 'complete';
const { data: positionsData } = usePaperRunPositions(isComplete ? id : undefined, 500);
const { data: positionsData } = usePaperRunPositions(isComplete ? id : undefined, 5000);
const { data: candlesData } = usePaperRunCandles(isComplete ? id : undefined, run?.candle_interval ?? null);
// Extract SMA specs from the strategy config, keeping only those whose timeframe
@@ -447,7 +447,12 @@ export default function PaperRunDetailPage() {
</small>
</Card.Header>
<Card.Body className="p-0">
<CandlestickChart candles={candlesData.candles} interval={candlesData.interval} smaSpecs={smaSpecs} />
<CandlestickChart
candles={candlesData.candles}
interval={candlesData.interval}
smaSpecs={smaSpecs}
positions={positionsData?.positions}
/>
</Card.Body>
</Card>
)}