bbgo_origin/apps/bbgo-backtest-report/components/TradingViewChart.js

438 lines
10 KiB
JavaScript
Raw Normal View History

2022-05-16 18:07:05 +00:00
import React, {useEffect, useRef, useState} from 'react';
import {tsvParse} from "d3-dsv";
2022-05-16 14:24:25 +00:00
// https://github.com/tradingview/lightweight-charts/issues/543
// const createChart = dynamic(() => import('lightweight-charts'));
2022-05-17 17:53:48 +00:00
import {createChart, CrosshairMode} from 'lightweight-charts';
import {Button} from "@nextui-org/react";
2022-05-16 14:24:25 +00:00
2022-05-16 18:07:05 +00:00
// const parseDate = timeParse("%Y-%m-%d");
2022-05-16 14:24:25 +00:00
2022-05-16 18:07:05 +00:00
const parseKline = () => {
2022-05-16 14:24:25 +00:00
return (d) => {
2022-05-16 18:07:05 +00:00
d.startTime = new Date(Number(d.startTime) * 1000);
d.endTime = new Date(Number(d.endTime) * 1000);
d.time = d.startTime.getTime() / 1000;
2022-05-16 14:24:25 +00:00
for (const key in d) {
// convert number fields
2022-05-16 18:07:05 +00:00
if (Object.prototype.hasOwnProperty.call(d, key)) {
switch (key) {
case "open":
case "high":
case "low":
case "close":
case "volume":
d[key] = +d[key];
break
}
2022-05-16 14:24:25 +00:00
}
}
return d;
};
};
const parseOrder = () => {
return (d) => {
for (const key in d) {
// convert number fields
if (Object.prototype.hasOwnProperty.call(d, key)) {
switch (key) {
case "order_id":
case "price":
case "quantity":
2022-05-17 04:40:25 +00:00
d[key] = +d[key];
break;
case "time":
d[key] = new Date(d[key]);
break;
}
}
}
return d;
};
}
const parsePosition = () => {
return (d) => {
for (const key in d) {
// convert number fields
if (Object.prototype.hasOwnProperty.call(d, key)) {
switch (key) {
case "accumulated_profit":
case "average_cost":
case "quote":
case "base":
d[key] = +d[key];
break
case "time":
d[key] = new Date(d[key]);
2022-05-17 04:40:25 +00:00
break
}
}
}
return d;
};
}
2022-05-17 17:53:48 +00:00
const fetchPositionHistory = (basePath, runID, setter) => {
// TODO: load the filename from the manifest
2022-05-17 04:40:25 +00:00
return fetch(
2022-05-17 17:53:48 +00:00
`${basePath}/${runID}/bollmaker:ETHUSDT-position.tsv`,
2022-05-17 04:40:25 +00:00
)
.then((response) => response.text())
.then((data) => tsvParse(data, parsePosition()))
.catch((e) => {
console.error("failed to fetch orders", e)
});
};
2022-05-17 17:53:48 +00:00
const fetchOrders = (basePath, runID, setter) => {
return fetch(
2022-05-17 17:53:48 +00:00
`${basePath}/${runID}/orders.tsv`,
)
.then((response) => response.text())
.then((data) => tsvParse(data, parseOrder()))
.then((data) => {
setter(data);
})
.catch((e) => {
console.error("failed to fetch orders", e)
});
}
2022-05-17 07:52:24 +00:00
const parseInterval = (s) => {
switch (s) {
case "1m":
return 60;
case "5m":
return 60 * 5;
case "15m":
return 60 * 15;
case "30m":
return 60 * 30;
case "1h":
return 60 * 60;
case "4h":
return 60 * 60 * 4;
case "6h":
return 60 * 60 * 6;
case "12h":
return 60 * 60 * 12;
case "1d":
return 60 * 60 * 24;
2022-05-17 07:52:24 +00:00
}
return 60;
};
const orderAbbr = (order) => {
let s = '';
switch (order.side) {
case "BUY":
s += 'B';
break;
case "SELL":
s += 'S';
break
}
switch (order.order_type) {
case "STOP_LIMIT":
s += ' StopLoss';
}
return s
}
const ordersToMarkets = (interval, orders) => {
const markers = [];
2022-05-17 07:52:24 +00:00
const intervalSecs = parseInterval(interval);
// var markers = [{ time: data[data.length - 48].time, position: 'aboveBar', color: '#f68410', shape: 'circle', text: 'D' }];
for (let i = 0; i < orders.length; i++) {
let order = orders[i];
2022-05-17 07:52:24 +00:00
let t = order.time.getTime() / 1000.0;
let lastMarker = markers.length > 0 ? markers[markers.length - 1] : null;
if (lastMarker) {
let remainder = lastMarker.time % intervalSecs;
let startTime = lastMarker.time - remainder;
let endTime = (startTime + intervalSecs);
// skip the marker in the same interval of the last marker
if (t < endTime) {
continue
}
}
switch (order.side) {
case "BUY":
markers.push({
2022-05-17 07:52:24 +00:00
time: t,
position: 'belowBar',
color: '#239D10',
shape: 'arrowDown',
// text: 'Buy @ ' + order.price
text: 'B',
});
break;
case "SELL":
markers.push({
2022-05-17 07:52:24 +00:00
time: t,
position: 'aboveBar',
color: '#e91e63',
shape: 'arrowDown',
// text: 'Sell @ ' + order.price
text: 'S',
});
break;
}
}
return markers;
};
2022-05-17 09:35:11 +00:00
const removeDuplicatedKLines = (klines) => {
const newK = [];
2022-05-17 17:53:48 +00:00
for (let i = 0; i < klines.length; i++) {
2022-05-17 09:35:11 +00:00
const k = klines[i];
2022-05-17 17:53:48 +00:00
if (i > 0 && k.time === klines[i - 1].time) {
2022-05-17 09:35:11 +00:00
continue
}
newK.push(k);
}
return newK;
}
2022-05-17 17:53:48 +00:00
function fetchKLines(basePath, runID, symbol, interval, setter) {
return fetch(
2022-05-17 17:53:48 +00:00
`${basePath}/${runID}/klines/${symbol}-${interval}.tsv`,
)
.then((response) => response.text())
.then((data) => tsvParse(data, parseKline()))
// .then((data) => tsvParse(data))
.catch((e) => {
console.error("failed to fetch klines", e)
});
}
2022-05-17 06:56:41 +00:00
const klinesToVolumeData = (klines) => {
const volumes = [];
2022-05-17 17:53:48 +00:00
for (let i = 0; i < klines.length; i++) {
2022-05-17 06:56:41 +00:00
const kline = klines[i];
volumes.push({
time: (kline.startTime.getTime() / 1000),
value: kline.volume,
})
}
return volumes;
}
const positionBaseHistoryToLineData = (interval, hs) => {
const bases = [];
const intervalSeconds = parseInterval(interval);
for (let i = 0; i < hs.length; i++) {
2022-05-17 10:10:37 +00:00
const pos = hs[i];
let t = pos.time.getTime() / 1000;
t = (t - t % intervalSeconds)
2022-05-17 17:53:48 +00:00
if (i > 0 && (pos.base === hs[i - 1].base || t === hs[i - 1].time)) {
continue;
}
bases.push({
time: t,
value: pos.base,
});
}
return bases;
}
2022-05-17 07:52:24 +00:00
const positionAverageCostHistoryToLineData = (interval, hs) => {
2022-05-17 04:40:25 +00:00
const avgCosts = [];
2022-05-17 07:52:24 +00:00
const intervalSeconds = parseInterval(interval);
2022-05-17 04:40:25 +00:00
for (let i = 0; i < hs.length; i++) {
2022-05-17 10:10:37 +00:00
const pos = hs[i];
2022-05-17 07:52:24 +00:00
let t = pos.time.getTime() / 1000;
t = (t - t % intervalSeconds)
2022-05-17 04:40:25 +00:00
2022-05-17 17:53:48 +00:00
if (i > 0 && (pos.average_cost === hs[i - 1].average_cost || t === hs[i - 1].time)) {
2022-05-17 06:56:41 +00:00
continue;
}
2022-05-17 07:52:52 +00:00
if (pos.base === 0) {
2022-05-17 04:40:25 +00:00
avgCosts.push({
2022-05-17 07:52:24 +00:00
time: t,
2022-05-17 04:40:25 +00:00
value: 0,
});
} else {
avgCosts.push({
2022-05-17 07:52:24 +00:00
time: t,
2022-05-17 04:40:25 +00:00
value: pos.average_cost,
});
}
}
return avgCosts;
}
2022-05-16 14:24:25 +00:00
const TradingViewChart = (props) => {
const chartContainerRef = useRef();
const chart = useRef();
const resizeObserver = useRef();
2022-05-16 14:24:25 +00:00
const [data, setData] = useState(null);
const [orders, setOrders] = useState(null);
const [markers, setMarkers] = useState(null);
2022-05-17 04:40:25 +00:00
const [positionHistory, setPositionHistory] = useState(null);
const [currentInterval, setCurrentInterval] = useState('5m');
const intervals = props.intervals || [];
2022-05-16 14:24:25 +00:00
useEffect(() => {
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
2022-05-16 14:24:25 +00:00
return;
}
if (!data) {
const ordersFetcher = fetchOrders(props.basePath, props.runID, (orders) => {
const markers = ordersToMarkets(currentInterval, orders);
setOrders(orders);
setMarkers(markers);
});
const positionHistoryFetcher = fetchPositionHistory(props.basePath, props.runID).then((data) => {
setPositionHistory(data);
});
Promise.all([ordersFetcher, positionHistoryFetcher]).then(() => {
fetchKLines(props.basePath, props.runID, 'ETHUSDT', currentInterval).then((data) => {
setData(removeDuplicatedKLines(data));
})
});
2022-05-16 14:24:25 +00:00
return;
}
2022-05-16 14:24:25 +00:00
2022-05-17 17:53:48 +00:00
console.log("createChart")
chart.current = createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight,
timeScale: {
timeVisible: true,
borderColor: '#D1D4DC',
},
rightPriceScale: {
borderColor: '#D1D4DC',
},
leftPriceScale: {
visible: true,
borderColor: 'rgba(197, 203, 206, 1)',
},
layout: {
backgroundColor: '#ffffff',
textColor: '#000',
},
crosshair: {
mode: CrosshairMode.Normal,
},
grid: {
horzLines: {
color: '#F0F3FA',
},
vertLines: {
color: '#F0F3FA',
},
},
2022-05-16 14:24:25 +00:00
});
const series = chart.current.addCandlestickSeries({
upColor: 'rgb(38,166,154)',
downColor: 'rgb(255,82,82)',
wickUpColor: 'rgb(38,166,154)',
wickDownColor: 'rgb(255,82,82)',
borderVisible: false,
});
series.setData(data);
series.setMarkers(markers);
const lineSeries = chart.current.addLineSeries();
const costLine = positionAverageCostHistoryToLineData(currentInterval, positionHistory);
2022-05-17 06:56:41 +00:00
lineSeries.setData(costLine);
const baseLineSeries = chart.current.addLineSeries({
priceScaleId: 'left',
color: '#98338C',
});
const baseLine = positionBaseHistoryToLineData(currentInterval, positionHistory)
baseLineSeries.setData(baseLine);
2022-05-17 06:56:41 +00:00
const volumeData = klinesToVolumeData(data);
const volumeSeries = chart.current.addHistogramSeries({
2022-05-17 06:56:41 +00:00
color: '#182233',
lineWidth: 2,
priceFormat: {
type: 'volume',
},
overlay: true,
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
volumeSeries.setData(volumeData);
chart.current.timeScale().fitContent();
2022-05-17 17:53:48 +00:00
return () => {
chart.current.remove();
};
}, [props.runID, currentInterval, data])
// see:
// https://codesandbox.io/s/9inkb?file=/src/styles.css
useEffect(() => {
resizeObserver.current = new ResizeObserver(entries => {
if (!chart.current) {
return;
}
2022-05-17 17:53:48 +00:00
const {width, height} = entries[0].contentRect;
chart.current.applyOptions({width, height});
setTimeout(() => {
chart.current.timeScale().fitContent();
}, 0);
});
2022-05-16 14:24:25 +00:00
resizeObserver.current.observe(chartContainerRef.current);
return () => resizeObserver.current.disconnect();
}, []);
return (
<div>
2022-05-17 09:35:11 +00:00
<Button.Group>
2022-05-17 17:53:48 +00:00
{intervals.map((interval) => {
2022-05-17 09:35:11 +00:00
return <Button size="xs" key={interval} onPress={(e) => {
setCurrentInterval(interval)
2022-05-17 09:35:11 +00:00
setData(null);
setMarkers(null);
}}>
{interval}
</Button>
2022-05-17 17:53:48 +00:00
})}
2022-05-17 09:35:11 +00:00
</Button.Group>
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 300}}>
</div>
2022-05-16 14:24:25 +00:00
</div>
);
2022-05-16 14:24:25 +00:00
};
export default TradingViewChart;