Skip to content

Commit 0ad63b5

Browse files
authored
Merge pull request #765 from c9s/strategy/pivotshort
improve: backtest-report layout improvements, EMA indicators and fixed the clean up issue
2 parents fc5a753 + 30079fe commit 0ad63b5

File tree

2 files changed

+201
-49
lines changed

2 files changed

+201
-49
lines changed

apps/backtest-report/components/ReportDetails.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@ const ReportDetails = (props: ReportDetailsProps) => {
185185

186186
const volumeUnit = reportSummary.symbolReports.length == 1 ? reportSummary.symbolReports[0].market.baseCurrency : '';
187187

188-
return <div>
189-
<Container my="sx">
188+
189+
190+
// size xl and padding xs
191+
return <Container size={"xl"} px="xs">
190192
<div>
191193
<Badge key={strategyName} color="teal">Strategy: {strategyName}</Badge>
192194
{reportSummary.sessions.map((session) => <Badge key={session} color="teal">Exchange: {session}</Badge>)}
@@ -239,8 +241,8 @@ const ReportDetails = (props: ReportDetailsProps) => {
239241
}
240242
</div>
241243

242-
</Container>
243-
</div>;
244+
</Container>;
244245
};
245246

247+
246248
export default ReportDetails;

apps/backtest-report/components/TradingViewChart.tsx

+195-45
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, {useEffect, useRef, useState} from 'react';
22
import {tsvParse} from "d3-dsv";
3-
import {SegmentedControl} from '@mantine/core';
3+
import {Checkbox, Group, SegmentedControl, Table} from '@mantine/core';
44

55

66
// https://github.com/tradingview/lightweight-charts/issues/543
77
// const createChart = dynamic(() => import('lightweight-charts'));
8-
import {createChart, CrosshairMode} from 'lightweight-charts';
8+
import {createChart, CrosshairMode, MouseEventParams, TimeRange} from 'lightweight-charts';
99
import {ReportSummary} from "../types";
1010
import moment from "moment";
1111

@@ -128,8 +128,10 @@ const parseInterval = (s: string) => {
128128
};
129129

130130
interface Order {
131+
order_id: number;
131132
order_type: string;
132133
side: string;
134+
symbol: string;
133135
price: number;
134136
quantity: number;
135137
executed_quantity: number;
@@ -147,7 +149,7 @@ interface Marker {
147149
text: string;
148150
}
149151

150-
const ordersToMarkets = (interval: string, orders: Array<Order> | void): Array<Marker> => {
152+
const ordersToMarkers = (interval: string, orders: Array<Order> | void): Array<Marker> => {
151153
const markers: Array<Marker> = [];
152154
const intervalSecs = parseInterval(interval);
153155

@@ -376,6 +378,10 @@ const TradingViewChart = (props: TradingViewChartProps) => {
376378
const resizeObserver = useRef<any>();
377379
const intervals = props.reportSummary.intervals || [];
378380
const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[0] : '1m');
381+
const [showPositionBase, setShowPositionBase] = useState(false);
382+
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
383+
const [showPositionAverageCost, setShowPositionAverageCost] = useState(false);
384+
const [orders, setOrders] = useState<Order[]>([]);
379385

380386
useEffect(() => {
381387
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
@@ -384,10 +390,13 @@ const TradingViewChart = (props: TradingViewChartProps) => {
384390

385391
const chartData: any = {};
386392
const fetchers = [];
387-
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders) => {
388-
const markers = ordersToMarkets(currentInterval, orders);
389-
chartData.orders = orders;
390-
chartData.markers = markers;
393+
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => {
394+
if (orders) {
395+
const markers = ordersToMarkers(currentInterval, orders);
396+
chartData.orders = orders;
397+
chartData.markers = markers;
398+
setOrders(orders);
399+
}
391400
return orders;
392401
});
393402
fetchers.push(ordersFetcher);
@@ -409,11 +418,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
409418

410419
Promise.all(fetchers).then(() => {
411420
console.log("createChart")
412-
413-
if (chart.current) {
414-
chart.current.remove();
415-
}
416-
417421
chart.current = createBaseChart(chartContainerRef);
418422

419423
const series = chart.current.addCandlestickSeries({
@@ -426,6 +430,38 @@ const TradingViewChart = (props: TradingViewChartProps) => {
426430
series.setData(chartData.klines);
427431
series.setMarkers(chartData.markers);
428432

433+
[9, 27, 99].forEach((w, i) => {
434+
const emaValues = calculateEMA(chartData.klines, w)
435+
const emaColor = 'rgba(' + w + ', ' + (111 - w) + ', 232, 0.9)'
436+
const emaLine = chart.current.addLineSeries({
437+
color: emaColor,
438+
lineWidth: 1,
439+
});
440+
emaLine.setData(emaValues);
441+
442+
const legend = document.createElement('div');
443+
legend.className = 'ema-legend';
444+
legend.style.display = 'block';
445+
legend.style.position = 'absolute';
446+
legend.style.left = 3 + 'px';
447+
legend.style.zIndex = '99';
448+
legend.style.top = 3 + (i * 22) + 'px';
449+
chartContainerRef.current.appendChild(legend);
450+
451+
const setLegendText = (priceValue: any) => {
452+
let val = '∅';
453+
if (priceValue !== undefined) {
454+
val = (Math.round(priceValue * 100) / 100).toFixed(2);
455+
}
456+
legend.innerHTML = 'EMA' + w + ' <span style="color:' + emaColor + '">' + val + '</span>';
457+
}
458+
459+
setLegendText(emaValues[emaValues.length - 1].value);
460+
chart.current.subscribeCrosshairMove((param: MouseEventParams) => {
461+
setLegendText(param.seriesPrices.get(emaLine));
462+
});
463+
})
464+
429465
const volumeData = klinesToVolumeData(chartData.klines);
430466
const volumeSeries = chart.current.addHistogramSeries({
431467
color: '#182233',
@@ -442,64 +478,178 @@ const TradingViewChart = (props: TradingViewChartProps) => {
442478
volumeSeries.setData(volumeData);
443479

444480
if (chartData.positionHistory) {
445-
const lineSeries = chart.current.addLineSeries();
446-
const costLine = positionAverageCostHistoryToLineData(currentInterval, chartData.positionHistory);
447-
lineSeries.setData(costLine);
481+
if (showPositionAverageCost) {
482+
const costLineSeries = chart.current.addLineSeries();
483+
const costLine = positionAverageCostHistoryToLineData(currentInterval, chartData.positionHistory);
484+
costLineSeries.setData(costLine);
485+
}
448486

449-
const baseLineSeries = chart.current.addLineSeries({
450-
priceScaleId: 'left',
451-
color: '#98338C',
452-
});
453-
const baseLine = positionBaseHistoryToLineData(currentInterval, chartData.positionHistory)
454-
baseLineSeries.setData(baseLine);
487+
if (showPositionBase) {
488+
const baseLineSeries = chart.current.addLineSeries({
489+
priceScaleId: 'left',
490+
color: '#98338C',
491+
});
492+
const baseLine = positionBaseHistoryToLineData(currentInterval, chartData.positionHistory)
493+
baseLineSeries.setData(baseLine);
494+
}
455495
}
456496

457497
chart.current.timeScale().fitContent();
498+
499+
/*
500+
chart.current.timeScale().setVisibleRange({
501+
from: (new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0))).getTime() / 1000,
502+
to: (new Date(Date.UTC(2018, 1, 1, 0, 0, 0, 0))).getTime() / 1000,
503+
});
504+
*/
505+
506+
// see:
507+
// https://codesandbox.io/s/9inkb?file=/src/styles.css
508+
resizeObserver.current = new ResizeObserver(entries => {
509+
if (!chart.current) {
510+
return;
511+
}
512+
513+
const {width, height} = entries[0].contentRect;
514+
chart.current.applyOptions({width, height});
515+
516+
setTimeout(() => {
517+
if (chart.current) {
518+
chart.current.timeScale().fitContent();
519+
}
520+
}, 0);
521+
});
522+
523+
resizeObserver.current.observe(chartContainerRef.current);
458524
});
459525

460526
return () => {
527+
console.log("removeChart")
528+
529+
resizeObserver.current.disconnect();
530+
461531
if (chart.current) {
462532
chart.current.remove();
463533
}
464-
};
465-
}, [props.runID, props.reportSummary, currentInterval])
466-
467-
// see:
468-
// https://codesandbox.io/s/9inkb?file=/src/styles.css
469-
useEffect(() => {
470-
resizeObserver.current = new ResizeObserver(entries => {
471-
if (!chart.current) {
472-
return;
534+
if (chartContainerRef.current) {
535+
// remove all the children because we created the legend elements
536+
chartContainerRef.current.replaceChildren();
473537
}
474538

475-
const {width, height} = entries[0].contentRect;
476-
chart.current.applyOptions({width, height});
477-
478-
setTimeout(() => {
479-
chart.current.timeScale().fitContent();
480-
}, 0);
481-
});
482-
483-
resizeObserver.current.observe(chartContainerRef.current);
484-
return () => resizeObserver.current.disconnect();
485-
}, []);
486-
539+
};
540+
}, [props.runID, props.reportSummary, currentInterval, showPositionBase, showPositionAverageCost])
487541

488542
return (
489543
<div>
490-
<div>
544+
<Group>
491545
<SegmentedControl
492546
data={intervals.map((interval) => {
493547
return {label: interval, value: interval}
494548
})}
495549
onChange={setCurrentInterval}
496550
/>
497-
</div>
551+
<Checkbox label="Position Base" checked={showPositionBase}
552+
onChange={(event) => setShowPositionBase(event.currentTarget.checked)}/>
553+
<Checkbox label="Position Average Cost" checked={showPositionAverageCost}
554+
onChange={(event) => setShowPositionAverageCost(event.currentTarget.checked)}/>
555+
</Group>
556+
557+
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 500, position: 'relative'}}>
498558

499-
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 300}}>
500559
</div>
560+
561+
<Group>
562+
<Checkbox label="Show Canceled" checked={showCanceledOrders}
563+
onChange={(event) => setShowCanceledOrders(event.currentTarget.checked)}/>
564+
565+
</Group>
566+
<OrderListTable orders={orders} showCanceled={showCanceledOrders} onClick={(order) => {
567+
console.log("selected order", order);
568+
const visibleRange = chart.current.timeScale().getVisibleRange()
569+
const seconds = parseInterval(currentInterval)
570+
const bars = 12
571+
const orderTime = order.creation_time.getTime() / 1000
572+
const from = orderTime - bars * seconds
573+
const to = orderTime + bars * seconds
574+
575+
console.log("orderTime", orderTime)
576+
console.log("visibleRange", visibleRange)
577+
console.log("setVisibleRange", from, to, to - from)
578+
chart.current.timeScale().setVisibleRange({ from, to } as TimeRange);
579+
// chart.current.timeScale().scrollToPosition(20, true);
580+
}}/>
501581
</div>
502582
);
503583
};
504584

585+
interface OrderListTableProps {
586+
orders: Order[];
587+
showCanceled: boolean;
588+
onClick?: (order: Order) => void;
589+
}
590+
591+
const OrderListTable = (props: OrderListTableProps) => {
592+
let orders = props.orders;
593+
594+
if (!props.showCanceled) {
595+
orders = orders.filter((order : Order) => {
596+
return order.status != "CANCELED"
597+
})
598+
}
599+
600+
const rows = orders.map((order: Order) => (
601+
<tr key={order.order_id} onClick={(e) => {
602+
props.onClick ? props.onClick(order) : null;
603+
const nodes = e.currentTarget?.parentNode?.querySelectorAll(".selected")
604+
nodes?.forEach((node, i) => {
605+
node.classList.remove("selected")
606+
})
607+
e.currentTarget.classList.add("selected")
608+
}}>
609+
<td>{order.order_id}</td>
610+
<td>{order.symbol}</td>
611+
<td>{order.side}</td>
612+
<td>{order.order_type}</td>
613+
<td>{order.price}</td>
614+
<td>{order.quantity}</td>
615+
<td>{order.status}</td>
616+
<td>{order.creation_time.toString()}</td>
617+
</tr>
618+
));
619+
return <Table highlightOnHover striped>
620+
<thead>
621+
<tr>
622+
<th>Order ID</th>
623+
<th>Symbol</th>
624+
<th>Side</th>
625+
<th>Order Type</th>
626+
<th>Price</th>
627+
<th>Quantity</th>
628+
<th>Status</th>
629+
<th>Creation Time</th>
630+
</tr>
631+
</thead>
632+
<tbody>{rows}</tbody>
633+
</Table>
634+
}
635+
636+
const calculateEMA = (a: KLine[], r: number) => {
637+
return a.map((k) => {
638+
return {time: k.time, value: k.close}
639+
}).reduce((p: any[], n: any, i: number) => {
640+
if (i) {
641+
const last = p[p.length - 1]
642+
const v = 2 * n.value / (r + 1) + last.value * (r - 1) / (r + 1)
643+
return p.concat({value: v, time: n.time})
644+
}
645+
646+
return p
647+
}, [{
648+
value: a[0].close,
649+
time: a[0].time
650+
}])
651+
}
652+
653+
505654
export default TradingViewChart;
655+

0 commit comments

Comments
 (0)