add report navigation

This commit is contained in:
c9s 2022-05-18 01:53:48 +08:00
parent 7dffccb3bf
commit 76949ed4f2
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
8 changed files with 225 additions and 50 deletions

View 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;

View 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;

View File

@ -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>

View File

@ -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>
)

View File

@ -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;

View File

@ -0,0 +1 @@
export * from './report';

View 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;
}

View 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;
}