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:
@@ -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 }} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user