mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
add report navigation
This commit is contained in:
parent
7dffccb3bf
commit
76949ed4f2
40
apps/bbgo-backtest-report/components/ReportDetails.tsx
Normal file
40
apps/bbgo-backtest-report/components/ReportDetails.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import TradingViewChart from './TradingViewChart';
|
||||
|
||||
import {Container} from '@nextui-org/react';
|
||||
import {ReportSummary} from "../types";
|
||||
|
||||
interface ReportDetailsProps {
|
||||
basePath: string;
|
||||
runID: string;
|
||||
}
|
||||
|
||||
const fetchReportSummary = (basePath: string, runID: string) => {
|
||||
return fetch(
|
||||
`${basePath}/${runID}/summary.json`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error("failed to fetch index", e)
|
||||
});
|
||||
}
|
||||
|
||||
const ReportDetails = (props: ReportDetailsProps) => {
|
||||
const [reportSummary, setReportSummary] = useState<ReportSummary>()
|
||||
useEffect(() => {
|
||||
fetchReportSummary(props.basePath, props.runID).then((summary: ReportSummary) => {
|
||||
console.log("summary", props.runID, summary);
|
||||
setReportSummary(summary)
|
||||
})
|
||||
}, [props.runID])
|
||||
|
||||
return <Container>
|
||||
<h2>Back-test Run ${props.runID}</h2>
|
||||
<div>
|
||||
<TradingViewChart basePath={props.basePath} runID={props.runID} intervals={["1m", "5m", "1h"]}/>
|
||||
</div>
|
||||
</Container>;
|
||||
};
|
||||
|
||||
export default ReportDetails;
|
57
apps/bbgo-backtest-report/components/ReportNavigator.tsx
Normal file
57
apps/bbgo-backtest-report/components/ReportNavigator.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from '@nextui-org/react';
|
||||
import {ReportEntry, ReportIndex} from '../types';
|
||||
|
||||
function fetchIndex(basePath: string, setter: (data: any) => void) {
|
||||
return fetch(
|
||||
`${basePath}/index.json`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("reportIndex", data);
|
||||
setter(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("failed to fetch index", e)
|
||||
});
|
||||
}
|
||||
|
||||
interface ReportNavigatorProps {
|
||||
onSelect: (reportEntry: ReportEntry) => void;
|
||||
}
|
||||
|
||||
const ReportNavigator = (props: ReportNavigatorProps) => {
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [reportIndex, setReportIndex] = useState<ReportIndex>({runs: []});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchIndex('/output', setReportIndex).then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (reportIndex.runs.length == 0) {
|
||||
return <div>No back-test report data</div>
|
||||
}
|
||||
|
||||
return <div>
|
||||
{
|
||||
reportIndex.runs.map((entry) => {
|
||||
return <Link key={entry.id} onClick={() => {
|
||||
if (props.onSelect) {
|
||||
props.onSelect(entry);
|
||||
}
|
||||
}}>{entry.id}</Link>
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default ReportNavigator;
|
|
@ -3,7 +3,7 @@ import {tsvParse} from "d3-dsv";
|
|||
|
||||
// https://github.com/tradingview/lightweight-charts/issues/543
|
||||
// const createChart = dynamic(() => import('lightweight-charts'));
|
||||
import {createChart, CrosshairMode } from 'lightweight-charts';
|
||||
import {createChart, CrosshairMode} from 'lightweight-charts';
|
||||
import {Button} from "@nextui-org/react";
|
||||
|
||||
// const parseDate = timeParse("%Y-%m-%d");
|
||||
|
@ -77,10 +77,10 @@ const parsePosition = () => {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
const fetchPositionHistory = (setter) => {
|
||||
const fetchPositionHistory = (basePath, runID, setter) => {
|
||||
// TODO: load the filename from the manifest
|
||||
return fetch(
|
||||
`/data/bollmaker:ETHUSDT-position.tsv`,
|
||||
`${basePath}/${runID}/bollmaker:ETHUSDT-position.tsv`,
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((data) => tsvParse(data, parsePosition()))
|
||||
|
@ -93,13 +93,12 @@ const fetchPositionHistory = (setter) => {
|
|||
});
|
||||
};
|
||||
|
||||
const fetchOrders = (setter) => {
|
||||
const fetchOrders = (basePath, runID, setter) => {
|
||||
return fetch(
|
||||
`/data/orders.tsv`,
|
||||
`${basePath}/${runID}/orders.tsv`,
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((data) => tsvParse(data, parseOrder()))
|
||||
// .then((data) => tsvParse(data))
|
||||
.then((data) => {
|
||||
setter(data);
|
||||
})
|
||||
|
@ -198,10 +197,10 @@ const ordersToMarkets = (interval, orders) => {
|
|||
|
||||
const removeDuplicatedKLines = (klines) => {
|
||||
const newK = [];
|
||||
for (let i = 0 ; i < klines.length ; i++) {
|
||||
for (let i = 0; i < klines.length; i++) {
|
||||
const k = klines[i];
|
||||
|
||||
if (i > 0 && k.time === klines[i-1].time) {
|
||||
if (i > 0 && k.time === klines[i - 1].time) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -210,9 +209,9 @@ const removeDuplicatedKLines = (klines) => {
|
|||
return newK;
|
||||
}
|
||||
|
||||
function fetchKLines(symbol, interval, setter) {
|
||||
function fetchKLines(basePath, runID, symbol, interval, setter) {
|
||||
return fetch(
|
||||
`/data/klines/${symbol}-${interval}.tsv`,
|
||||
`${basePath}/${runID}/klines/${symbol}-${interval}.tsv`,
|
||||
)
|
||||
.then((response) => response.text())
|
||||
.then((data) => tsvParse(data, parseKline()))
|
||||
|
@ -228,7 +227,7 @@ function fetchKLines(symbol, interval, setter) {
|
|||
const klinesToVolumeData = (klines) => {
|
||||
const volumes = [];
|
||||
|
||||
for (let i = 0 ; i < klines.length ; i++) {
|
||||
for (let i = 0; i < klines.length; i++) {
|
||||
const kline = klines[i];
|
||||
volumes.push({
|
||||
time: (kline.startTime.getTime() / 1000),
|
||||
|
@ -248,7 +247,7 @@ const positionBaseHistoryToLineData = (interval, hs) => {
|
|||
let t = pos.time.getTime() / 1000;
|
||||
t = (t - t % intervalSeconds)
|
||||
|
||||
if (i > 0 && (pos.base === hs[i-1].base || t === hs[i-1].time)) {
|
||||
if (i > 0 && (pos.base === hs[i - 1].base || t === hs[i - 1].time)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -269,7 +268,7 @@ const positionAverageCostHistoryToLineData = (interval, hs) => {
|
|||
let t = pos.time.getTime() / 1000;
|
||||
t = (t - t % intervalSeconds)
|
||||
|
||||
if (i > 0 && (pos.average_cost === hs[i-1].average_cost || t === hs[i-1].time)) {
|
||||
if (i > 0 && (pos.average_cost === hs[i - 1].average_cost || t === hs[i - 1].time)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -308,22 +307,20 @@ const TradingViewChart = (props) => {
|
|||
}
|
||||
|
||||
if (!data || !orders || !markers || !positionHistory) {
|
||||
fetchKLines('ETHUSDT', currentInterval, setData).then(() => {
|
||||
fetchOrders((orders) => {
|
||||
fetchKLines(props.basePath, props.runID, 'ETHUSDT', currentInterval, setData).then(() => {
|
||||
fetchOrders(props.basePath, props.runID, (orders) => {
|
||||
setOrders(orders);
|
||||
|
||||
const markers = ordersToMarkets(currentInterval, orders);
|
||||
setMarkers(markers);
|
||||
});
|
||||
fetchPositionHistory(setPositionHistory)
|
||||
fetchPositionHistory(props.basePath, props.runID, setPositionHistory)
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("createChart", {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: chartContainerRef.current.clientHeight,
|
||||
})
|
||||
console.log("createChart")
|
||||
|
||||
chart.current = createChart(chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: chartContainerRef.current.clientHeight,
|
||||
|
@ -393,7 +390,10 @@ const TradingViewChart = (props) => {
|
|||
volumeSeries.setData(volumeData);
|
||||
|
||||
chart.current.timeScale().fitContent();
|
||||
}, [chart.current, data, currentInterval])
|
||||
return () => {
|
||||
chart.current.remove();
|
||||
};
|
||||
}, [chart.current, props.runID, data, currentInterval])
|
||||
|
||||
// see:
|
||||
// https://codesandbox.io/s/9inkb?file=/src/styles.css
|
||||
|
@ -403,8 +403,8 @@ const TradingViewChart = (props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { width, height } = entries[0].contentRect;
|
||||
chart.current.applyOptions({ width, height });
|
||||
const {width, height} = entries[0].contentRect;
|
||||
chart.current.applyOptions({width, height});
|
||||
|
||||
setTimeout(() => {
|
||||
chart.current.timeScale().fitContent();
|
||||
|
@ -418,16 +418,15 @@ const TradingViewChart = (props) => {
|
|||
return (
|
||||
<div>
|
||||
<Button.Group>
|
||||
{ intervals.map((interval) => {
|
||||
{intervals.map((interval) => {
|
||||
return <Button size="xs" key={interval} onPress={(e) => {
|
||||
setCurrentInterval(interval)
|
||||
setData(null);
|
||||
setMarkers(null);
|
||||
chart.current.remove();
|
||||
}}>
|
||||
{interval}
|
||||
</Button>
|
||||
}) }
|
||||
})}
|
||||
</Button.Group>
|
||||
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 300}}>
|
||||
</div>
|
|
@ -2,9 +2,12 @@ import type {NextPage} from 'next'
|
|||
import Head from 'next/head'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
import Report from '../src/components/Report';
|
||||
import ReportDetails from '../components/ReportDetails';
|
||||
import ReportNavigator from '../components/ReportNavigator';
|
||||
import {useState} from "react";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [currentReport, setCurrentReport] = useState<any>();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
|
@ -13,9 +16,12 @@ const Home: NextPage = () => {
|
|||
<link rel="icon" href="/favicon.ico"/>
|
||||
</Head>
|
||||
<main className={styles.main}>
|
||||
<Report>
|
||||
|
||||
</Report>
|
||||
<ReportNavigator onSelect={(reportEntry) => {
|
||||
setCurrentReport(reportEntry)
|
||||
}}/>
|
||||
{
|
||||
currentReport ? <ReportDetails basePath={'/output'} runID={currentReport.id}/> : null
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import TradingViewChart from './TradingViewChart';
|
||||
|
||||
import {Container} from '@nextui-org/react';
|
||||
|
||||
const Report = (props) => {
|
||||
/*
|
||||
<Button>Click me</Button>
|
||||
*/
|
||||
return <Container>
|
||||
<h2>Back-test Report</h2>
|
||||
<div>
|
||||
<TradingViewChart intervals={["1m", "5m", "1h"]}/>
|
||||
</div>
|
||||
</Container>;
|
||||
};
|
||||
|
||||
export default Report;
|
1
apps/bbgo-backtest-report/types/index.ts
Normal file
1
apps/bbgo-backtest-report/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './report';
|
16
apps/bbgo-backtest-report/types/market.ts
Normal file
16
apps/bbgo-backtest-report/types/market.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export interface Market {
|
||||
symbol: string;
|
||||
localSymbol: string;
|
||||
pricePrecision: number;
|
||||
volumePrecision: number;
|
||||
quoteCurrency: string;
|
||||
baseCurrency: string;
|
||||
minNotional: number;
|
||||
minAmount: number;
|
||||
minQuantity: number;
|
||||
maxQuantity: number;
|
||||
stepSize: number;
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
tickSize: number;
|
||||
}
|
75
apps/bbgo-backtest-report/types/report.ts
Normal file
75
apps/bbgo-backtest-report/types/report.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {Market} from './market';
|
||||
|
||||
export interface ReportEntry {
|
||||
id: string;
|
||||
|
||||
config: object;
|
||||
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface ReportIndex {
|
||||
runs: Array<ReportEntry>;
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
currency: string;
|
||||
available: number;
|
||||
locked: number;
|
||||
borrowed: number;
|
||||
}
|
||||
|
||||
export interface BalanceMap {
|
||||
[currency: string]: Balance;
|
||||
}
|
||||
|
||||
export interface ReportSummary {
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
sessions: string[];
|
||||
symbols: null;
|
||||
initialTotalBalances: BalanceMap;
|
||||
finalTotalBalances: BalanceMap;
|
||||
symbolReports: SymbolReport[];
|
||||
manifests: Manifest[];
|
||||
}
|
||||
|
||||
export interface SymbolReport {
|
||||
exchange: string;
|
||||
symbol: string;
|
||||
market: Market;
|
||||
lastPrice: number;
|
||||
startPrice: number;
|
||||
pnl: PnL;
|
||||
initialBalances: BalanceMap;
|
||||
finalBalances: BalanceMap;
|
||||
}
|
||||
|
||||
|
||||
export interface Manifest {
|
||||
type: string;
|
||||
filename: string;
|
||||
strategyID: string;
|
||||
strategyInstance: string;
|
||||
strategyProperty: string;
|
||||
}
|
||||
|
||||
export interface CurrencyFeeMap {
|
||||
[currency: string]: number;
|
||||
}
|
||||
|
||||
export interface PnL {
|
||||
lastPrice: number;
|
||||
startTime: Date;
|
||||
symbol: string;
|
||||
market: Market;
|
||||
numTrades: number;
|
||||
profit: number;
|
||||
netProfit: number;
|
||||
unrealizedProfit: number;
|
||||
averageCost: number;
|
||||
buyVolume: number;
|
||||
sellVolume: number;
|
||||
feeInUSD: number;
|
||||
currencyFees: CurrencyFeeMap;
|
||||
}
|
Loading…
Reference in New Issue
Block a user