Merge pull request #605 from c9s/feature/backtest-report

feature: add web-based back-test report
This commit is contained in:
Yo-An Lin 2022-05-18 02:21:55 +08:00 committed by GitHub
commit e57c39e665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2042 additions and 360 deletions

View File

@ -2,7 +2,27 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
First, run the development server:
Install the dependencies:
```
yarn install
```
Create a symlink to your back-test report output directory:
```
(cd public && ln -s ../../../output output)
```
Generate some back-test reports:
```
(cd ../.. && go run ./cmd/bbgo backtest --config bollmaker_ethusdt.yaml --debug --session binance --output output --subdir)
```
Start the development server:
```bash
npm run dev

View File

@ -0,0 +1,51 @@
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])
if (!reportSummary) {
return <Container>
<h2>Loading {props.runID}</h2>
</Container>;
}
return <Container>
<h2>Back-test Run {props.runID}</h2>
<div>
{
reportSummary.symbols.map((symbol: string) => {
return <TradingViewChart basePath={props.basePath} runID={props.runID} symbol={symbol} 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

@ -0,0 +1,436 @@
import React, {useEffect, useRef, useState} from 'react';
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 {Button} from "@nextui-org/react";
// const parseDate = timeParse("%Y-%m-%d");
const parseKline = () => {
return (d) => {
d.startTime = new Date(Number(d.startTime) * 1000);
d.endTime = new Date(Number(d.endTime) * 1000);
d.time = d.startTime.getTime() / 1000;
for (const key in d) {
// convert number fields
if (Object.prototype.hasOwnProperty.call(d, key)) {
switch (key) {
case "open":
case "high":
case "low":
case "close":
case "volume":
d[key] = +d[key];
break
}
}
}
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":
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]);
break
}
}
}
return d;
};
}
const fetchPositionHistory = (basePath, runID) => {
// TODO: load the filename from the manifest
return fetch(
`${basePath}/${runID}/bollmaker:ETHUSDT-position.tsv`,
)
.then((response) => response.text())
.then((data) => tsvParse(data, parsePosition()))
.catch((e) => {
console.error("failed to fetch orders", e)
});
};
const fetchOrders = (basePath, runID, setter) => {
return fetch(
`${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)
});
}
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;
}
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 = [];
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];
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({
time: t,
position: 'belowBar',
color: '#239D10',
shape: 'arrowDown',
// text: 'Buy @ ' + order.price
text: 'B',
});
break;
case "SELL":
markers.push({
time: t,
position: 'aboveBar',
color: '#e91e63',
shape: 'arrowDown',
// text: 'Sell @ ' + order.price
text: 'S',
});
break;
}
}
return markers;
};
const removeDuplicatedKLines = (klines) => {
const newK = [];
for (let i = 0; i < klines.length; i++) {
const k = klines[i];
if (i > 0 && k.time === klines[i - 1].time) {
continue
}
newK.push(k);
}
return newK;
}
function fetchKLines(basePath, runID, symbol, interval, setter) {
return fetch(
`${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)
});
}
const klinesToVolumeData = (klines) => {
const volumes = [];
for (let i = 0; i < klines.length; i++) {
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++) {
const pos = hs[i];
let t = pos.time.getTime() / 1000;
t = (t - t % intervalSeconds)
if (i > 0 && (pos.base === hs[i - 1].base || t === hs[i - 1].time)) {
continue;
}
bases.push({
time: t,
value: pos.base,
});
}
return bases;
}
const positionAverageCostHistoryToLineData = (interval, hs) => {
const avgCosts = [];
const intervalSeconds = parseInterval(interval);
for (let i = 0; i < hs.length; i++) {
const pos = hs[i];
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)) {
continue;
}
if (pos.base === 0) {
avgCosts.push({
time: t,
value: 0,
});
} else {
avgCosts.push({
time: t,
value: pos.average_cost,
});
}
}
return avgCosts;
}
const TradingViewChart = (props) => {
const chartContainerRef = useRef();
const chart = useRef();
const resizeObserver = useRef();
const [data, setData] = useState(null);
const [orders, setOrders] = useState(null);
const [markers, setMarkers] = useState(null);
const [positionHistory, setPositionHistory] = useState(null);
const [currentInterval, setCurrentInterval] = useState('5m');
const intervals = props.intervals || [];
useEffect(() => {
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
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, props.symbol, currentInterval).then((data) => {
setData(removeDuplicatedKLines(data));
})
});
return;
}
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',
},
},
});
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);
lineSeries.setData(costLine);
const baseLineSeries = chart.current.addLineSeries({
priceScaleId: 'left',
color: '#98338C',
});
const baseLine = positionBaseHistoryToLineData(currentInterval, positionHistory)
baseLineSeries.setData(baseLine);
const volumeData = klinesToVolumeData(data);
const volumeSeries = chart.current.addHistogramSeries({
color: '#182233',
lineWidth: 2,
priceFormat: {
type: 'volume',
},
overlay: true,
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
volumeSeries.setData(volumeData);
chart.current.timeScale().fitContent();
return () => {
chart.current.remove();
setData(null);
};
}, [props.runID, currentInterval, data])
// see:
// https://codesandbox.io/s/9inkb?file=/src/styles.css
useEffect(() => {
resizeObserver.current = new ResizeObserver(entries => {
if (!chart.current) {
return;
}
const {width, height} = entries[0].contentRect;
chart.current.applyOptions({width, height});
setTimeout(() => {
chart.current.timeScale().fitContent();
}, 0);
});
resizeObserver.current.observe(chartContainerRef.current);
return () => resizeObserver.current.disconnect();
}, []);
return (
<div>
<Button.Group>
{intervals.map((interval) => {
return <Button size="xs" key={interval} onPress={(e) => {
setCurrentInterval(interval)
}}>
{interval}
</Button>
})}
</Button.Group>
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 300}}>
</div>
</div>
);
};
export default TradingViewChart;

View File

@ -1,6 +1,33 @@
// workaround for react financial charts
// https://github.com/react-financial/react-financial-charts/issues/606
// workaround for lightweight chart
// https://stackoverflow.com/questions/65936222/next-js-syntaxerror-unexpected-token-export
// https://stackoverflow.com/questions/66244968/cannot-use-import-statement-outside-a-module-error-when-importing-react-hook-m
const withTM = require('next-transpile-modules')([
'lightweight-charts',
'fancy-canvas',
// 'd3-array',
// 'd3-format',
// 'd3-time',
// 'd3-time-format',
// 'react-financial-charts',
// '@react-financial-charts/annotations',
// '@react-financial-charts/axes',
// '@react-financial-charts/coordinates',
// '@react-financial-charts/core',
// '@react-financial-charts/indicators',
// '@react-financial-charts/interactive',
// '@react-financial-charts/scales',
// '@react-financial-charts/series',
// '@react-financial-charts/tooltip',
// '@react-financial-charts/utils',
]);
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
module.exports = withTM(nextConfig);

View File

@ -9,20 +9,25 @@
"lint": "next lint"
},
"dependencies": {
"@nextui-org/react": "^1.0.0-beta.7",
"d3-dsv": "^3.0.1",
"d3-format": "^3.1.0",
"d3-time-format": "^4.1.0",
"klinecharts": "^8.3.6",
"lightweight-charts": "^3.8.0",
"next": "12.1.6",
"react": "18.1.0",
"react-dom": "18.1.0"
},
"devDependencies": {
"@types/d3-dsv": "^3.0.0",
"@types/d3-format": "^3.0.1",
"@types/d3-time-format": "^4.0.0",
"@types/node": "17.0.31",
"@types/react": "18.0.8",
"@types/react-dom": "18.0.3",
"eslint": "8.14.0",
"eslint-config-next": "12.1.6",
"next-transpile-modules": "^9.0.0",
"typescript": "4.6.4"
}
}

View File

@ -1,8 +1,11 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import type {AppProps} from 'next/app'
import {NextUIProvider} from '@nextui-org/react'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
function MyApp({Component, pageProps}: AppProps) {
return <NextUIProvider>
<Component {...pageProps} />
</NextUIProvider>
}
export default MyApp

View File

@ -0,0 +1,28 @@
import Document, {DocumentContext, Head, Html, Main, NextScript} from 'next/document';
import {CssBaseline} from '@nextui-org/react';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
// @ts-ignore
initialProps.styles = <>{initialProps.styles}</>;
return initialProps;
}
render() {
return (
<Html lang="en">
<Head>
{CssBaseline.flush()}
</Head>
<body>
<Main/>
<NextScript/>
</body>
</Html>
);
}
}
export default MyDocument;

View File

@ -1,70 +1,28 @@
import type { NextPage } from 'next'
import type {NextPage} from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
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>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
<title>Back-Test Report</title>
<meta name="description" content="Generated by create next app"/>
<link rel="icon" href="/favicon.ico"/>
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.tsx</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
<ReportNavigator onSelect={(reportEntry) => {
setCurrentReport(reportEntry)
}}/>
{
currentReport ? <ReportDetails basePath={'/output'} runID={currentReport.id}/> : null
}
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
)
}

View File

@ -0,0 +1,287 @@
import { format } from "d3-format";
import { tsvParse } from "d3-dsv";
import { timeFormat } from "d3-time-format";
import { timeParse } from "d3-time-format";
import * as React from "react";
import {
elderRay,
ema,
discontinuousTimeScaleProviderBuilder,
Chart,
ChartCanvas,
CurrentCoordinate,
BarSeries,
CandlestickSeries,
ElderRaySeries,
LineSeries,
MovingAverageTooltip,
OHLCTooltip,
SingleValueTooltip,
lastVisibleItemBasedZoomAnchor,
XAxis,
YAxis,
CrossHairCursor,
EdgeIndicator,
MouseCoordinateX,
MouseCoordinateY,
ZoomButtons,
withDeviceRatio,
withSize,
} from "react-financial-charts";
interface IOHLCData {
readonly close: number;
readonly date: Date;
readonly high: number;
readonly low: number;
readonly open: number;
readonly volume: number;
}
const parseDate = timeParse("%Y-%m-%d");
const parseData = () => {
return (d: any) => {
const date = parseDate(d.date);
if (date === null) {
d.date = new Date(Number(d.date));
} else {
d.date = new Date(date);
}
for (const key in d) {
if (key !== "date" && Object.prototype.hasOwnProperty.call(d, key)) {
d[key] = +d[key];
}
}
return d as IOHLCData;
};
};
interface WithOHLCDataProps {
readonly data: IOHLCData[];
}
interface WithOHLCState {
data?: IOHLCData[];
message: string;
}
export function withOHLCData(interval : string = "5m") {
return <TProps extends WithOHLCDataProps>(OriginalComponent: React.ComponentClass<TProps>) => {
return class WithOHLCData extends React.Component<Omit<TProps, "data">, WithOHLCState> {
public constructor(props: Omit<TProps, "data">) {
super(props);
this.state = {
message: `Loading price data...`,
};
}
public componentDidMount() {
fetch(
`/data/klines/ETHUSDT-5m.csv`,
)
.then((response) => response.text())
.then((data) => tsvParse(data, parseData()))
.then((data) => {
this.setState({
data,
});
})
.catch(() => {
this.setState({
message: `Failed to fetch data.`,
});
});
}
public render() {
const { data, message } = this.state;
if (data === undefined) {
return <div className="center">{message}</div>;
}
return <OriginalComponent {...(this.props as TProps)} data={data} />;
}
};
};
}
interface StockChartProps {
readonly data: IOHLCData[];
readonly height: number;
readonly dateTimeFormat?: string;
readonly width: number;
readonly ratio: number;
}
class StockChart extends React.Component<StockChartProps> {
private readonly margin = { left: 0, right: 48, top: 0, bottom: 24 };
private readonly pricesDisplayFormat = format(".2f");
private readonly xScaleProvider = discontinuousTimeScaleProviderBuilder().inputDateAccessor(
(d: IOHLCData) => d.date,
);
public render() {
const { data: initialData, dateTimeFormat = "%d %b", height, ratio, width } = this.props;
const ema12 = ema()
.id(1)
.options({ windowSize: 12 })
.merge((d: any, c: any) => {
d.ema12 = c;
})
.accessor((d: any) => d.ema12);
const ema26 = ema()
.id(2)
.options({ windowSize: 26 })
.merge((d: any, c: any) => {
d.ema26 = c;
})
.accessor((d: any) => d.ema26);
const elder = elderRay();
const calculatedData = elder(ema26(ema12(initialData)));
const { margin, xScaleProvider } = this;
const { data, xScale, xAccessor, displayXAccessor } = xScaleProvider(calculatedData);
const max = xAccessor(data[data.length - 1]);
const min = xAccessor(data[Math.max(0, data.length - 100)]);
const xExtents = [min, max + 5];
const gridHeight = height - margin.top - margin.bottom;
const elderRayHeight = 100;
const elderRayOrigin = (_: number, h: number) => [0, h - elderRayHeight];
const barChartHeight = gridHeight / 4;
const barChartOrigin = (_: number, h: number) => [0, h - barChartHeight - elderRayHeight];
const chartHeight = gridHeight - elderRayHeight;
const timeDisplayFormat = timeFormat(dateTimeFormat);
return (
<ChartCanvas
height={height}
ratio={ratio}
width={width}
margin={margin}
data={data}
displayXAccessor={displayXAccessor}
seriesName="Data"
xScale={xScale}
xAccessor={xAccessor}
xExtents={xExtents}
zoomAnchor={lastVisibleItemBasedZoomAnchor}
>
<Chart id={2} height={barChartHeight} origin={barChartOrigin} yExtents={this.barChartExtents}>
<BarSeries fillStyle={this.volumeColor} yAccessor={this.volumeSeries} />
</Chart>
<Chart id={3} height={chartHeight} yExtents={this.candleChartExtents}>
<XAxis showGridLines showTicks={false} showTickLabel={false} />
<YAxis showGridLines tickFormat={this.pricesDisplayFormat} />
<CandlestickSeries />
<LineSeries yAccessor={ema26.accessor()} strokeStyle={ema26.stroke()} />
<CurrentCoordinate yAccessor={ema26.accessor()} fillStyle={ema26.stroke()} />
<LineSeries yAccessor={ema12.accessor()} strokeStyle={ema12.stroke()} />
<CurrentCoordinate yAccessor={ema12.accessor()} fillStyle={ema12.stroke()} />
<MouseCoordinateY rectWidth={margin.right} displayFormat={this.pricesDisplayFormat} />
<EdgeIndicator
itemType="last"
rectWidth={margin.right}
fill={this.openCloseColor}
lineStroke={this.openCloseColor}
displayFormat={this.pricesDisplayFormat}
yAccessor={this.yEdgeIndicator}
/>
<MovingAverageTooltip
origin={[8, 24]}
options={[
{
yAccessor: ema26.accessor(),
type: "EMA",
stroke: ema26.stroke(),
windowSize: ema26.options().windowSize,
},
{
yAccessor: ema12.accessor(),
type: "EMA",
stroke: ema12.stroke(),
windowSize: ema12.options().windowSize,
},
]}
/>
<ZoomButtons />
<OHLCTooltip origin={[8, 16]} />
</Chart>
<Chart
id={4}
height={elderRayHeight}
yExtents={[0, elder.accessor()]}
origin={elderRayOrigin}
padding={{ top: 8, bottom: 8 }}
>
<XAxis showGridLines gridLinesStrokeStyle="#e0e3eb" />
<YAxis ticks={4} tickFormat={this.pricesDisplayFormat} />
<MouseCoordinateX displayFormat={timeDisplayFormat} />
<MouseCoordinateY rectWidth={margin.right} displayFormat={this.pricesDisplayFormat} />
<ElderRaySeries yAccessor={elder.accessor()} />
<SingleValueTooltip
yAccessor={elder.accessor()}
yLabel="Elder Ray"
yDisplayFormat={(d: any) =>
`${this.pricesDisplayFormat(d.bullPower)}, ${this.pricesDisplayFormat(d.bearPower)}`
}
origin={[8, 16]}
/>
</Chart>
<CrossHairCursor />
</ChartCanvas>
);
}
private readonly barChartExtents = (data: IOHLCData) => {
return data.volume;
};
private readonly candleChartExtents = (data: IOHLCData) => {
return [data.high, data.low];
};
private readonly yEdgeIndicator = (data: IOHLCData) => {
return data.close;
};
private readonly volumeColor = (data: IOHLCData) => {
return data.close > data.open ? "rgba(38, 166, 154, 0.3)" : "rgba(239, 83, 80, 0.3)";
};
private readonly volumeSeries = (data: IOHLCData) => {
return data.volume;
};
private readonly openCloseColor = (data: IOHLCData) => {
return data.close > data.open ? "#26a69a" : "#ef5350";
};
}
export default withOHLCData()(withSize({ style: { minHeight: 400 } })(withDeviceRatio()(StockChart)));
export const MinutesStockChart = withOHLCData("MINUTES")(
withSize({ style: { minHeight: 400 } })(withDeviceRatio()(StockChart)),
);
export const SecondsStockChart = withOHLCData("SECONDS")(
withSize({ style: { minHeight: 400 } })(withDeviceRatio()(StockChart)),
);

View File

@ -4,12 +4,7 @@
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem 0;
}
.footer {
@ -28,34 +23,6 @@
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;

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: string[];
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;
}

View File

@ -10,7 +10,14 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.16.3":
"@babel/runtime@7.9.6":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.6.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
@ -18,20 +25,59 @@
regenerator-runtime "^0.13.4"
"@eslint/eslintrc@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae"
integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==
version "1.2.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886"
integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.3.1"
espree "^9.3.2"
globals "^13.9.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.0.4"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@formatjs/ecma402-abstract@1.11.4":
version "1.11.4"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz#b962dfc4ae84361f9f08fbce411b4e4340930eda"
integrity sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==
dependencies:
"@formatjs/intl-localematcher" "0.2.25"
tslib "^2.1.0"
"@formatjs/fast-memoize@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21"
integrity sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==
dependencies:
tslib "^2.1.0"
"@formatjs/icu-messageformat-parser@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz#a54293dd7f098d6a6f6a084ab08b6d54a3e8c12d"
integrity sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==
dependencies:
"@formatjs/ecma402-abstract" "1.11.4"
"@formatjs/icu-skeleton-parser" "1.3.6"
tslib "^2.1.0"
"@formatjs/icu-skeleton-parser@1.3.6":
version "1.3.6"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz#4ce8c0737d6f07b735288177049e97acbf2e8964"
integrity sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==
dependencies:
"@formatjs/ecma402-abstract" "1.11.4"
tslib "^2.1.0"
"@formatjs/intl-localematcher@0.2.25":
version "0.2.25"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz#60892fe1b271ec35ba07a2eb018a2dd7bca6ea3a"
integrity sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==
dependencies:
tslib "^2.1.0"
"@humanwhocodes/config-array@^0.9.2":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
@ -46,6 +92,28 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@internationalized/date@3.0.0-rc.0":
version "3.0.0-rc.0"
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.0-rc.0.tgz#7050a46c7abc036e32311b6bda79553dd249a867"
integrity sha512-R8ui3O2G43fZ/z5cBdJuU6nswKtuVrKloDE6utvqKEeGf6igFoiapcjg7jbQ+WvWIDGtdUytOp2fOq/X4efBdQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@internationalized/message@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.0.6.tgz#3265be5c5bc70dc56e9a3e59ea08a3f3905ebb31"
integrity sha512-ECk3toFy87I2z5zipRNwdbouvRlIyMKb/FzKj1upMaNS52AKhpvrLgo3CY/ZXQKm4CRIbeh6p/F/Ztt+enhIEA==
dependencies:
"@babel/runtime" "^7.6.2"
intl-messageformat "^9.12.0"
"@internationalized/number@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.1.0.tgz#441c262e43344371b17765cf691e54df5ab8726b"
integrity sha512-CEts+2rIB4QveKeeF6xIHdn8aLVvUt5aiarkpCZgtMyYqfqo/ZBELf2UyhvLPGpRxcF24ClCISMTP9BTVreSAg==
dependencies:
"@babel/runtime" "^7.6.2"
"@next/env@12.1.6":
version "12.1.6"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.6.tgz#5f44823a78335355f00f1687cfc4f1dafa3eca08"
@ -118,6 +186,36 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz#a350caf42975e7197b24b495b8d764eec7e6a36e"
integrity sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==
"@nextui-org/react@^1.0.0-beta.7":
version "1.0.0-beta.7"
resolved "https://registry.yarnpkg.com/@nextui-org/react/-/react-1.0.0-beta.7.tgz#80e054fd9c886569127b7b76c7467bdbdac21136"
integrity sha512-yy40vZuwbE4JhNfAnrkRsPBf9yAT7d7VjqKhmhnDEixhhNqRRNbjLmeMdAlRAwRA4Zhct8MiECVQi+4Z28OPcQ==
dependencies:
"@babel/runtime" "7.9.6"
"@react-aria/button" "3.4.4"
"@react-aria/checkbox" "3.3.4"
"@react-aria/dialog" "3.1.9"
"@react-aria/focus" "3.5.5"
"@react-aria/i18n" "3.3.9"
"@react-aria/interactions" "3.8.4"
"@react-aria/label" "3.2.5"
"@react-aria/overlays" "3.8.2"
"@react-aria/ssr" "3.1.2"
"@react-aria/table" "3.2.4"
"@react-aria/utils" "3.12.0"
"@react-aria/visually-hidden" "3.2.8"
"@react-stately/checkbox" "3.0.7"
"@react-stately/data" "3.4.7"
"@react-stately/overlays" "3.2.0"
"@react-stately/table" "3.1.3"
"@react-stately/toggle" "3.2.7"
"@react-types/button" "^3.4.5"
"@react-types/checkbox" "3.2.7"
"@react-types/grid" "3.0.4"
"@react-types/overlays" "3.5.5"
"@react-types/shared" "3.12.0"
"@stitches/react" "1.2.8"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -139,11 +237,388 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@react-aria/button@3.4.4":
version "3.4.4"
resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.4.4.tgz#7bd5c8852c51426dd27b3d9d237d2bc34415c4a9"
integrity sha512-Z4jh8WLXNk8BJZ28beXTJWFwnhdjyC6ymby7qD/UDckqbm5OqM18EqYbKmJVF3+MRScunZdGg2aw0jkNpzo+3w==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.5"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
"@react-stately/toggle" "^3.2.7"
"@react-types/button" "^3.4.5"
"@react-aria/checkbox@3.3.4":
version "3.3.4"
resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.3.4.tgz#f59a65bdc41894d47717ede7b31f49a29f539ba7"
integrity sha512-5IJff+hzNR0LJgNyNJPgu8ElTN8Df1GDHDySdD7gP2Sv5916x1eTx+hZlYq4FUyTsOlW6QuynQ0jrQUK4xAnRA==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/label" "^3.2.5"
"@react-aria/toggle" "^3.2.4"
"@react-aria/utils" "^3.12.0"
"@react-stately/checkbox" "^3.0.7"
"@react-stately/toggle" "^3.2.7"
"@react-types/checkbox" "^3.2.7"
"@react-aria/dialog@3.1.9":
version "3.1.9"
resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.1.9.tgz#01f3256f7fb83936361ed38a27aeaeed23ffb87d"
integrity sha512-S/HE6XxBU9AiL4TGBjmz4CAEXjCD9nwvV5ofBKlbfPzgimmmSJj3SVNtsawKIt3KyP9AEioyJydM/vbGfccJlw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.5"
"@react-aria/utils" "^3.12.0"
"@react-stately/overlays" "^3.2.0"
"@react-types/dialog" "^3.3.5"
"@react-aria/focus@3.5.5", "@react-aria/focus@^3.5.3", "@react-aria/focus@^3.5.5":
version "3.5.5"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.5.5.tgz#d5e3eb7af8612e8dafda214746084766e55c4b01"
integrity sha512-scv+jhbQ25JCh36gu8a++edvdEFdlRScdQdnkJOB4NbHbYYfY36APtI70hgQHdfq9dDl5fJ9LMsH9hoF7X3gLw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
"@react-types/shared" "^3.12.0"
clsx "^1.1.1"
"@react-aria/grid@^3.2.4":
version "3.2.6"
resolved "https://registry.yarnpkg.com/@react-aria/grid/-/grid-3.2.6.tgz#ae4fe7e48e4de021640039922b875bc97edfbe57"
integrity sha512-hNQHJkedMMAj+XmqbFW97Nybe5nEh+mRWB5SD7yuIvBLOFxnWid2BUF6zRA6nkZpfsPPTY1YHefgCGzQTFxbNQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.5"
"@react-aria/i18n" "^3.3.9"
"@react-aria/interactions" "^3.8.4"
"@react-aria/live-announcer" "^3.0.6"
"@react-aria/selection" "^3.8.2"
"@react-aria/utils" "^3.12.0"
"@react-stately/grid" "^3.1.4"
"@react-stately/selection" "^3.9.4"
"@react-stately/virtualizer" "^3.1.9"
"@react-types/checkbox" "^3.2.7"
"@react-types/grid" "^3.0.4"
"@react-types/shared" "^3.12.0"
"@react-aria/i18n@3.3.9", "@react-aria/i18n@^3.3.7", "@react-aria/i18n@^3.3.9":
version "3.3.9"
resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.3.9.tgz#58c69e650bd00e94270e2dc31ad85ffd2ee10f04"
integrity sha512-EOqiOu84NYH/CW0s/tt3yDqDsjHlrHdi5qzrOGpGN/BvxtA/4UkMBdi8TTKXdRk8oHUIdNW1z5mZxzxkLDy1sA==
dependencies:
"@babel/runtime" "^7.6.2"
"@internationalized/date" "3.0.0-rc.0"
"@internationalized/message" "^3.0.6"
"@internationalized/number" "^3.1.0"
"@react-aria/ssr" "^3.1.2"
"@react-aria/utils" "^3.12.0"
"@react-types/shared" "^3.12.0"
"@react-aria/interactions@3.8.4", "@react-aria/interactions@^3.8.2", "@react-aria/interactions@^3.8.4":
version "3.8.4"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.8.4.tgz#f3704470b70f150b753bba01a8714f97d6299906"
integrity sha512-6EHFKK8pmjSJSKcBbduijPETKqE669XZ1VaEY8ubr6VnlVhCszvKHoxpU384CkNiDNLJOVkK6HDzPXsn3lxhng==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.12.0"
"@react-types/shared" "^3.12.0"
"@react-aria/label@3.2.5", "@react-aria/label@^3.2.5":
version "3.2.5"
resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.2.5.tgz#f1aafc2531540e56df1221233bab343fcfb84dc2"
integrity sha512-MkcPa7Ps/BsWTctH7IgVWtYENwrByfYMPmYdZCgotI0MiI6wK4LWwRaUQmfc7mWwJ7ns2NPyBRwrzJT4+RRbew==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.12.0"
"@react-types/label" "^3.5.4"
"@react-types/shared" "^3.12.0"
"@react-aria/live-announcer@^3.0.4", "@react-aria/live-announcer@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@react-aria/live-announcer/-/live-announcer-3.0.6.tgz#cf57ed51b5f693af28f41a19e91086109154fec7"
integrity sha512-dXd5knYAFQPNr4ApxGwHXIDBuO50d9koat1ViFI22yS1QJF3y1dcIkBHfiAWIUtGr8AbRbWDZZnHtKrfPl25Zg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.12.0"
"@react-aria/visually-hidden" "^3.2.8"
"@react-aria/overlays@3.8.2":
version "3.8.2"
resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.8.2.tgz#630177cdd20bd6aa20e3516485d90003390d896e"
integrity sha512-HeOkYUILH4CrMVv3HkTrcK1jeZ2NJ7tS39tTciEGZ9JO1d8nUJ5jzDGGiQ587T9YWc1EYxiA+fbnn5Krxyq8IQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/i18n" "^3.3.9"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
"@react-aria/visually-hidden" "^3.2.8"
"@react-stately/overlays" "^3.2.0"
"@react-types/button" "^3.4.5"
"@react-types/overlays" "^3.5.5"
"@react-types/shared" "^3.12.0"
dom-helpers "^3.3.1"
"@react-aria/selection@^3.8.0", "@react-aria/selection@^3.8.2":
version "3.8.2"
resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.8.2.tgz#0e64c412ee8e73268994e72f4cb28f5b1100aff9"
integrity sha512-sBkSza8kT06tUKzIX68H2k+svYNCBOwhHmU0gbx164CmitCLk/akDGIds3LeoA9FhFFXw6/5CuLp6SNhmqlLWw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.5"
"@react-aria/i18n" "^3.3.9"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
"@react-stately/collections" "^3.3.8"
"@react-stately/selection" "^3.9.4"
"@react-types/shared" "^3.12.0"
"@react-aria/ssr@3.1.2", "@react-aria/ssr@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.1.2.tgz#665a6fd56385068c7417922af2d0d71b0618e52d"
integrity sha512-amXY11ImpokvkTMeKRHjsSsG7v1yzzs6yeqArCyBIk60J3Yhgxwx9Cah+Uu/804ATFwqzN22AXIo7SdtIaMP+g==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/table@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@react-aria/table/-/table-3.2.4.tgz#6d11265983f5a553b0c0b1a849bf6c26161d19d8"
integrity sha512-2t/EGyNAYsU841ZPLYUkwN+tdXztccgllUJ9qL5a0RJm7DYKKFHHsAYWmnV5ONj/tXqNZQfoqJSzsL6wpTXOZw==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.3"
"@react-aria/grid" "^3.2.4"
"@react-aria/i18n" "^3.3.7"
"@react-aria/interactions" "^3.8.2"
"@react-aria/live-announcer" "^3.0.4"
"@react-aria/selection" "^3.8.0"
"@react-aria/utils" "^3.11.3"
"@react-stately/table" "^3.1.3"
"@react-stately/virtualizer" "^3.1.8"
"@react-types/checkbox" "^3.2.6"
"@react-types/grid" "^3.0.3"
"@react-types/shared" "^3.11.2"
"@react-types/table" "^3.1.3"
"@react-aria/toggle@^3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.2.4.tgz#b4cb71782c4f4bfbfa858a37c1fd71d6470e490f"
integrity sha512-q1NiUKkWt9trgVj/VvKrTpe/tvNcsM9ie5JJVxikF4moPCyIqxRWaDzi2/g/63c1I6LZDMVT4v6V5tk9xgfuiQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/focus" "^3.5.5"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
"@react-stately/toggle" "^3.2.7"
"@react-types/checkbox" "^3.2.7"
"@react-types/shared" "^3.12.0"
"@react-types/switch" "^3.1.6"
"@react-aria/utils@3.12.0", "@react-aria/utils@^3.11.3", "@react-aria/utils@^3.12.0":
version "3.12.0"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.12.0.tgz#39d0f37525e050356c4de725c85d9b10e7a5c0d9"
integrity sha512-1TMrE7UpgTgQHgW3z0r6Zo4CTUDwNsZEwzg+mQVub8ZalonhuNs5OrulUn+lRIsGELNktGNkeh/29WsS1Od8eg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/ssr" "^3.1.2"
"@react-stately/utils" "^3.4.1"
"@react-types/shared" "^3.12.0"
clsx "^1.1.1"
"@react-aria/visually-hidden@3.2.8", "@react-aria/visually-hidden@^3.2.8":
version "3.2.8"
resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.2.8.tgz#1f3531f065752f642089a584a402de0fa1daa288"
integrity sha512-SLBID66sUZrCdxaxLhgxypF/UyGuFVFGc+VhqmFi9QacDfAQcoT3DQyXEaKUIDMVtwAtSuWl7BpuooEctKBe6Q==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/interactions" "^3.8.4"
"@react-aria/utils" "^3.12.0"
clsx "^1.1.1"
"@react-stately/checkbox@3.0.7", "@react-stately/checkbox@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.0.7.tgz#c3c77832e90087d20a4facc66aae8a22ce59b447"
integrity sha512-dBY4x3qWoCO2IFeTVovnq6xkWa9ycqdNNws+gbYt79EwFdKSIfH1iTuFrv9DIwTU2N72quFCC/xQ23+a1/ZqSg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/toggle" "^3.2.7"
"@react-stately/utils" "^3.4.1"
"@react-types/checkbox" "^3.2.7"
"@react-stately/collections@^3.3.7", "@react-stately/collections@^3.3.8":
version "3.3.8"
resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.3.8.tgz#f0f9def181fab8b2551dfe0c0fc59760fa6eaf05"
integrity sha512-R4RXLc0aaCZaCTh3NT/lmpMtVqP3HIdi2d1kyq4/uIC8APUFzEoUMEV+P0k3nQ5v6mO/UCkP3ePdOywnJBm/Gg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-types/shared" "^3.12.0"
"@react-stately/data@3.4.7":
version "3.4.7"
resolved "https://registry.yarnpkg.com/@react-stately/data/-/data-3.4.7.tgz#81319f7dcccd33a4a7f66a726d8ac92c919b451d"
integrity sha512-ShxXFEjrtktH9e8u/7Z/lifPjE5T11Tgx5+DBKjZ/kpH3xbHZIpd1riaowpLxeCWt2bYwgoQYxmrL6PA5NclIg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-types/shared" "^3.12.0"
"@react-stately/grid@^3.1.3", "@react-stately/grid@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.1.4.tgz#9ef4e6f8ea2dd9446fe6d9212704b0af2addc113"
integrity sha512-f0BjDSGcPFHI7x6PmLwfMMhFj1ttKD3QKZgTrSIhmPnZqY/LEk1XFq8RFdnk5bNmt4JwiEDbytS7W4HbSTIe3g==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/selection" "^3.9.4"
"@react-types/grid" "^3.0.4"
"@react-types/shared" "^3.12.0"
"@react-stately/overlays@3.2.0", "@react-stately/overlays@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.2.0.tgz#4bd2b42e28caa527b7400a7c7d135a44f286ac57"
integrity sha512-Ys+dfhFVyRGFRvvE35+Ychvgk868BDry9Td5rfvjVEwx6x8jaNShbonoo8CYYUkkJhaEnRaiJNG+0OGRCpvjTA==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/utils" "^3.4.1"
"@react-types/overlays" "^3.5.5"
"@react-stately/selection@^3.9.3", "@react-stately/selection@^3.9.4":
version "3.9.4"
resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.9.4.tgz#5903a6bb59ef1ae51c014c63c33617c93f6530a2"
integrity sha512-hgJ4raHFQMfQ1aQYgL+nRpQgA7GdPDh9esIeB8Ih+yS783cV4vyyqKxuLd2u9W4cilnEkgXjrI5Z21RU86jzEg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/collections" "^3.3.8"
"@react-stately/utils" "^3.4.1"
"@react-types/shared" "^3.12.0"
"@react-stately/table@3.1.3", "@react-stately/table@^3.1.3":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.1.3.tgz#23583a2bff89f74b84ced3398edeaba1efd70c04"
integrity sha512-HSsamFabtCSHib4A5rxXBtfKPd0InXjXSoaxUNi6RLOyptULMA06q1ShbWDEijTYGQI5Pfevs/5bgLonj0enbg==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/collections" "^3.3.7"
"@react-stately/grid" "^3.1.3"
"@react-stately/selection" "^3.9.3"
"@react-types/grid" "^3.0.3"
"@react-types/shared" "^3.11.2"
"@react-types/table" "^3.1.3"
"@react-stately/toggle@3.2.7", "@react-stately/toggle@^3.2.7":
version "3.2.7"
resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.2.7.tgz#4d15261b438f89ea78bf3c08c812e0f854e0373f"
integrity sha512-McKc2wIp1z7Dw6EqQgOgjr2QnKR+LWXppZjdx30K4hnCiP6cXZp66DmR2ngekPrtOYDN6Xdqbty/Ez7kiJxmnQ==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/utils" "^3.4.1"
"@react-types/checkbox" "^3.2.7"
"@react-types/shared" "^3.12.0"
"@react-stately/utils@^3.4.1":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.4.1.tgz#56f049aa1704d338968b5973c796ee606e9c0c62"
integrity sha512-mjFbKklj/W8KRw1CQSpUJxHd7lhUge4i00NwJTwGxbzmiJgsTWlKKS/1rBf48ey9hUBopXT5x5vG/AxQfWTQug==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-stately/virtualizer@^3.1.8", "@react-stately/virtualizer@^3.1.9":
version "3.1.9"
resolved "https://registry.yarnpkg.com/@react-stately/virtualizer/-/virtualizer-3.1.9.tgz#8f885c966747b36c5ed9df76020aeccebd35034a"
integrity sha512-to0CQU4l08ZI/Ar3h/BeDqFTjK0nJUfhdk8mTpP+bV0RGBQnDwqCnrLFdQCc3Xl8fbYWa+Y6pvSUqJ0rq6Bp7Q==
dependencies:
"@babel/runtime" "^7.6.2"
"@react-aria/utils" "^3.12.0"
"@react-types/shared" "^3.12.0"
"@react-types/button@^3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.4.5.tgz#ba258ad274d9e1ad775662edcde0839c1c24ecdb"
integrity sha512-wqOw3LvqFRJl6lDhije7koTINWBv+LRBKAlGOri2ddw3VDqvm0/zu2ENDIP/XX0FtUzuffoc1U5YgxmBlXd7gQ==
dependencies:
"@react-types/shared" "^3.12.0"
"@react-types/checkbox@3.2.7", "@react-types/checkbox@^3.2.6", "@react-types/checkbox@^3.2.7":
version "3.2.7"
resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.2.7.tgz#fa65452931942bfccf804114b6ef4cd086b67226"
integrity sha512-c/hJwVRr7JoakyU39hUQstCc/0uPPvE+Eie8SspV2u9umSs7dYiUBc7F2wpboWIdNkQUEHG/Uq/Vs6/hk+yrkg==
dependencies:
"@react-types/shared" "^3.12.0"
"@react-types/dialog@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.3.5.tgz#8b2fe99d7c535d8b09ef439f936462e0cd1231cf"
integrity sha512-K77big4JVDy6nhIv5V8PbnfHOeTK8JeGBLAEhl1DGCgCnPhq2eJ/R342rSjQobSyyaVUAYu1Z4AW4jjBSM17ug==
dependencies:
"@react-types/overlays" "^3.5.5"
"@react-types/shared" "^3.12.0"
"@react-types/grid@3.0.4", "@react-types/grid@^3.0.3", "@react-types/grid@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.0.4.tgz#c372456b49281577aec24a6480442f8c448a1f68"
integrity sha512-Ot6V/2PajcqBq2GH/YrsuiA8EqEmTcuvICfPd5RpjbLDFhhHbxOFsgOrXX2Qr33huu96dAhhEEEvOuVKbLcBdQ==
dependencies:
"@react-types/shared" "^3.12.0"
"@react-types/label@^3.5.4":
version "3.5.4"
resolved "https://registry.yarnpkg.com/@react-types/label/-/label-3.5.4.tgz#56bf50332845a161761902876cc5e6034521b48b"
integrity sha512-LuShOdEYokzn58SKUIo7kQdN3CV5Rs+HCxmvix4+Uw6BAYG9/aqqoKKolTA9klbM8rvvEzDqFzNZZHeMTBoN6w==
dependencies:
"@react-types/shared" "^3.12.0"
"@react-types/overlays@3.5.5", "@react-types/overlays@^3.5.5":
version "3.5.5"
resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.5.5.tgz#64c20eae8bce39618431205a2a695a8cd7b57473"
integrity sha512-TEfn+hv3E6iX1gEjJ6+Gl3r0+WCIPPMhPjTidU6AKqhS0phtcITQ8gPovr0PYEP4Ub8QuT0ttZWu0nWZP3IxIg==
dependencies:
"@react-types/shared" "^3.12.0"
"@react-types/shared@3.12.0", "@react-types/shared@^3.11.2", "@react-types/shared@^3.12.0":
version "3.12.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.12.0.tgz#31a53fcec5c3159fd0d5c67f7e3f3876c7d19908"
integrity sha512-faGr9xOjtMlkQPfA1i36iUmWS/hpPPtxIwdAtBi6p7rCejmShMLFZ2YN4DxzbJUCVubF2S1+rMMIKuXG17DkEw==
"@react-types/switch@^3.1.6":
version "3.1.6"
resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.1.6.tgz#75c59ae46f7289bc3b1a6a97e111f346d6ac3f4c"
integrity sha512-H9ECjBeEK82tGGiCNx2gQfrx5nJEviICAvUCfemLCS4zdUxs9NUYxIfI12v1Bl5NJ1dD0Cyc0hb4haiB+mX1ig==
dependencies:
"@react-types/checkbox" "^3.2.7"
"@react-types/shared" "^3.12.0"
"@react-types/table@^3.1.3":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.1.3.tgz#45e3b0c0a1d6a14db32c2dbeb9e5d15e5f01940a"
integrity sha512-l5ZmoPEnnMNUOW/mC7x/HDXC0CmHGz5IpWBPbV1aJtQCxRD42yosMaP8pT48EPZjupSeEuUGVFW2sZQlEjHbwg==
dependencies:
"@react-types/grid" "^3.0.3"
"@react-types/shared" "^3.11.2"
"@rushstack/eslint-patch@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0"
integrity sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==
"@stitches/react@1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==
"@types/d3-dsv@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.0.tgz#f3c61fb117bd493ec0e814856feb804a14cfc311"
integrity sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==
"@types/d3-format@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d"
integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==
"@types/d3-time-format@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946"
integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -166,7 +641,16 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.0.8":
"@types/react@*":
version "18.0.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@18.0.8":
version "18.0.8"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.8.tgz#a051eb380a9fbcaa404550543c58e1cf5ce4ab87"
integrity sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==
@ -181,55 +665,55 @@
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@typescript-eslint/parser@^5.21.0":
version "5.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.22.0.tgz#7bedf8784ef0d5d60567c5ba4ce162460e70c178"
integrity sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1"
integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw==
dependencies:
"@typescript-eslint/scope-manager" "5.22.0"
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/typescript-estree" "5.22.0"
"@typescript-eslint/scope-manager" "5.23.0"
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/typescript-estree" "5.23.0"
debug "^4.3.2"
"@typescript-eslint/scope-manager@5.22.0":
version "5.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz#590865f244ebe6e46dc3e9cab7976fc2afa8af24"
integrity sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==
"@typescript-eslint/scope-manager@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b"
integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==
dependencies:
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/visitor-keys" "5.22.0"
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/visitor-keys" "5.23.0"
"@typescript-eslint/types@5.22.0":
version "5.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.22.0.tgz#50a4266e457a5d4c4b87ac31903b28b06b2c3ed0"
integrity sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==
"@typescript-eslint/types@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09"
integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==
"@typescript-eslint/typescript-estree@5.22.0":
version "5.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz#e2116fd644c3e2fda7f4395158cddd38c0c6df97"
integrity sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==
"@typescript-eslint/typescript-estree@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065"
integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==
dependencies:
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/visitor-keys" "5.22.0"
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/visitor-keys" "5.23.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@5.22.0":
version "5.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz#f49c0ce406944ffa331a1cfabeed451ea4d0909c"
integrity sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==
"@typescript-eslint/visitor-keys@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b"
integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==
dependencies:
"@typescript-eslint/types" "5.22.0"
"@typescript-eslint/types" "5.23.0"
eslint-visitor-keys "^3.0.0"
acorn-jsx@^5.3.1:
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.7.0:
acorn@^8.7.1:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
@ -311,9 +795,9 @@ ast-types-flow@^0.0.7:
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
axe-core@^4.3.5:
version "4.4.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
version "4.4.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c"
integrity sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA==
axobject-query@^2.2.0:
version "2.2.0"
@ -354,9 +838,9 @@ callsites@^3.0.0:
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001332:
version "1.0.30001338"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz#b5dd7a7941a51a16480bdf6ff82bded1628eec0d"
integrity sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ==
version "1.0.30001340"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz#029a2f8bfc025d4820fafbfaa6259fd7778340c7"
integrity sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==
chalk@^4.0.0:
version "4.1.2"
@ -366,6 +850,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@ -389,9 +878,9 @@ concat-map@0.0.1:
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
core-js-pure@^3.20.2:
version "3.22.4"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.4.tgz#a992210f4cad8b32786b8654563776c56b0e0d0a"
integrity sha512-4iF+QZkpzIz0prAFuepmxwJ2h5t4agvE8WPYqs2mjLJMNNwJOnpch76w2Q7bUfCPEv/V7wpvOfog0w273M+ZSw==
version "3.22.5"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.5.tgz#bdee0ed2f9b78f2862cda4338a07b13a49b6c9a9"
integrity sha512-8xo9R00iYD7TcV7OrC98GwxiUEAabVWO3dix+uyWjnYrx9fyASLlIX+f/3p5dW5qByaP2bcZ8X/T47s55et/tA==
cross-spawn@^7.0.2:
version "7.0.3"
@ -502,11 +991,26 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^3.3.1:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
enhanced-resolve@^5.7.0:
version "5.9.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
version "1.20.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.0.tgz#b2d526489cceca004588296334726329e0a6bfb6"
@ -552,6 +1056,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@ -727,13 +1236,13 @@ eslint@8.14.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^9.3.1:
version "9.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd"
integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==
espree@^9.3.1, espree@^9.3.2:
version "9.3.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596"
integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==
dependencies:
acorn "^8.7.0"
acorn-jsx "^5.3.1"
acorn "^8.7.1"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.3.0"
esquery@^1.4.0:
@ -760,6 +1269,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
fancy-canvas@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-0.2.2.tgz#33fd4976724169a1eda5015f515a2a1302d1ec91"
integrity sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -913,9 +1427,9 @@ glob@^7.1.3, glob@^7.2.0:
path-is-absolute "^1.0.0"
globals@^13.6.0, globals@^13.9.0:
version "13.13.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b"
integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==
version "13.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac"
integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==
dependencies:
type-fest "^0.20.2"
@ -931,6 +1445,11 @@ globby@^11.0.4:
merge2 "^1.4.1"
slash "^3.0.0"
graceful-fs@^4.2.4:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -1019,6 +1538,16 @@ internal-slot@^1.0.3:
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
intl-messageformat@^9.12.0:
version "9.13.0"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.13.0.tgz#97360b73bd82212e4f6005c712a4a16053165468"
integrity sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==
dependencies:
"@formatjs/ecma402-abstract" "1.11.4"
"@formatjs/fast-memoize" "1.2.1"
"@formatjs/icu-messageformat-parser" "2.1.0"
tslib "^2.1.0"
is-bigint@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
@ -1160,11 +1689,6 @@ json5@^1.0.1:
array-includes "^3.1.4"
object.assign "^4.1.2"
klinecharts@^8.3.6:
version "8.3.6"
resolved "https://registry.yarnpkg.com/klinecharts/-/klinecharts-8.3.6.tgz#42f4e08b2627caf5855be133a537b1135b675221"
integrity sha512-HH8ToVEwP9n+S6BtTI9cjVa/JWsjsbN5x9P7ts0l+BVFeK3OOcfL+R9oym9SQJ1fjG1LJ5E+sMNL6xj8dl4RPA==
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@ -1185,6 +1709,13 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lightweight-charts@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/lightweight-charts/-/lightweight-charts-3.8.0.tgz#8c41ad7c1c083f18621f11ece7fc1096e131a0d3"
integrity sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==
dependencies:
fancy-canvas "0.2.2"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -1262,6 +1793,14 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-transpile-modules@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90"
integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ==
dependencies:
enhanced-resolve "^5.7.0"
escalade "^3.1.1"
next@12.1.6:
version "12.1.6"
resolved "https://registry.yarnpkg.com/next/-/next-12.1.6.tgz#eb205e64af1998651f96f9df44556d47d8bbc533"
@ -1664,6 +2203,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -1691,6 +2235,11 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"

3
go.mod
View File

@ -64,6 +64,7 @@ require (
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/go-test/deep v1.0.6 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect
github.com/golang/protobuf v1.5.2 // indirect
@ -104,7 +105,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.9 // indirect

4
go.sum
View File

@ -160,6 +160,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -683,6 +685,8 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@ -1,15 +1,14 @@
package backtest
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strconv"
"time"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/types"
)
@ -23,16 +22,14 @@ type symbolInterval struct {
// KLineDumper dumps the received kline data into a folder for the backtest report to load the charts.
type KLineDumper struct {
OutputDirectory string
files map[symbolInterval]*os.File
writers map[symbolInterval]*csv.Writer
writers map[symbolInterval]*tsv.Writer
filenames map[symbolInterval]string
}
func NewKLineDumper(outputDirectory string) *KLineDumper {
return &KLineDumper{
OutputDirectory: outputDirectory,
files: make(map[symbolInterval]*os.File),
writers: make(map[symbolInterval]*csv.Writer),
writers: make(map[symbolInterval]*tsv.Writer),
filenames: make(map[symbolInterval]string),
}
}
@ -42,7 +39,7 @@ func (d *KLineDumper) Filenames() map[symbolInterval]string {
}
func (d *KLineDumper) formatFileName(symbol string, interval types.Interval) string {
return filepath.Join(d.OutputDirectory, fmt.Sprintf("%s-%s.csv",
return filepath.Join(d.OutputDirectory, fmt.Sprintf("%s-%s.tsv",
symbol,
interval))
}
@ -69,36 +66,28 @@ func (d *KLineDumper) Record(k types.KLine) error {
w, ok := d.writers[si]
if !ok {
filename := d.formatFileName(k.Symbol, k.Interval)
f2, err := os.Create(filename)
w2, err := tsv.NewWriterFile(filename)
if err != nil {
return err
}
w = w2
w = csv.NewWriter(f2)
d.files[si] = f2
d.writers[si] = w
d.writers[si] = w2
d.filenames[si] = filename
if err := w.Write(csvHeader); err != nil {
return err
if err2 := w2.Write(csvHeader); err2 != nil {
return err2
}
}
if err := w.Write(d.encode(k)); err != nil {
return err
}
return nil
return w.Write(d.encode(k))
}
func (d *KLineDumper) Close() error {
var err error = nil
for _, w := range d.writers {
w.Flush()
}
var err error = nil
for _, f := range d.files {
err2 := f.Close()
err2 := w.Close()
if err2 != nil {
err = multierr.Append(err, err2)
}

View File

@ -1,15 +1,14 @@
package backtest
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/types"
)
@ -27,16 +26,14 @@ type InstancePropertyIndex struct {
type StateRecorder struct {
outputDirectory string
strategies []Instance
files map[interface{}]*os.File
writers map[types.CsvFormatter]*csv.Writer
writers map[types.CsvFormatter]*tsv.Writer
manifests Manifests
}
func NewStateRecorder(outputDir string) *StateRecorder {
return &StateRecorder{
outputDirectory: outputDir,
files: make(map[interface{}]*os.File),
writers: make(map[types.CsvFormatter]*csv.Writer),
writers: make(map[types.CsvFormatter]*tsv.Writer),
manifests: make(Manifests),
}
}
@ -96,7 +93,7 @@ func (r *StateRecorder) Scan(instance Instance) error {
}
func (r *StateRecorder) formatCsvFilename(instance Instance, objType string) string {
return filepath.Join(r.outputDirectory, fmt.Sprintf("%s-%s.csv", instance.InstanceID(), objType))
return filepath.Join(r.outputDirectory, fmt.Sprintf("%s-%s.tsv", instance.InstanceID(), objType))
}
func (r *StateRecorder) Manifests() Manifests {
@ -105,34 +102,26 @@ func (r *StateRecorder) Manifests() Manifests {
func (r *StateRecorder) newCsvWriter(o types.CsvFormatter, instance Instance, typeName string) error {
fn := r.formatCsvFilename(instance, typeName)
f, err := os.Create(fn)
w, err := tsv.NewWriterFile(fn)
if err != nil {
return err
}
if _, exists := r.files[o]; exists {
return fmt.Errorf("file of object %v already exists", o)
}
r.manifests[InstancePropertyIndex{
ID: instance.ID(),
InstanceID: instance.InstanceID(),
Property: typeName,
}] = fn
r.files[o] = f
w := csv.NewWriter(f)
r.writers[o] = w
return w.Write(o.CsvHeader())
}
func (r *StateRecorder) Close() error {
var err error
for _, f := range r.files {
err2 := f.Close()
for _, w := range r.writers {
err2 := w.Close()
if err2 != nil {
err = multierr.Append(err, err2)
}

View File

@ -1,38 +1,109 @@
package backtest
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/fatih/color"
"github.com/gofrs/flock"
"github.com/c9s/bbgo/pkg/accounting/pnl"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
type Run struct {
ID string `json:"id"`
Config *bbgo.Config `json:"config"`
Time time.Time `json:"time"`
}
type ReportIndex struct {
Runs []Run `json:"runs,omitempty"`
}
// SummaryReport is the summary of the back-test session
type SummaryReport struct {
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Sessions []string `json:"sessions"`
Symbols []string `json:"symbols"`
InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"`
FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"`
SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"`
Manifests Manifests `json:"manifests,omitempty"`
}
// SessionSymbolReport is the report per exchange session
// trades are merged, collected and re-calculated
type SessionSymbolReport struct {
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Exchange types.ExchangeName `json:"exchange"`
Symbol string `json:"symbol,omitempty"`
Market types.Market `json:"market"`
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
PnLReport *pnl.AverageCostPnlReport `json:"pnlReport,omitempty"`
PnL *pnl.AverageCostPnlReport `json:"pnl,omitempty"`
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
Manifests Manifests `json:"manifests,omitempty"`
}
func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) {
color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol)
color.Green("===============================================")
r.PnL.Print()
initQuoteAsset := inQuoteAsset(r.InitialBalances, r.Market, r.StartPrice)
finalQuoteAsset := inQuoteAsset(r.FinalBalances, r.Market, r.LastPrice)
color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(initQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.StartPrice)
color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(finalQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.LastPrice)
if r.PnL.Profit.Sign() > 0 {
color.Green("REALIZED PROFIT: +%v %s", r.PnL.Profit, r.Market.QuoteCurrency)
} else {
color.Red("REALIZED PROFIT: %v %s", r.PnL.Profit, r.Market.QuoteCurrency)
}
if r.PnL.UnrealizedProfit.Sign() > 0 {
color.Green("UNREALIZED PROFIT: +%v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency)
} else {
color.Red("UNREALIZED PROFIT: %v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency)
}
if finalQuoteAsset.Compare(initQuoteAsset) > 0 {
color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
} else {
color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
}
if wantBaseAssetBaseline {
if r.LastPrice.Compare(r.StartPrice) > 0 {
color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)",
r.Market.BaseCurrency,
r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2),
r.LastPrice.FormatString(2),
r.StartPrice.FormatString(2),
r.StartPrice.FormatString(2))
} else {
color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)",
r.Market.BaseCurrency,
r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2),
r.LastPrice.FormatString(2),
r.StartPrice.FormatString(2),
r.StartPrice.FormatString(2))
}
}
}
const SessionTimeFormat = "2006-01-02T15_04"
// FormatSessionName returns the back-test session name
@ -44,3 +115,62 @@ func FormatSessionName(sessions []string, symbols []string, startTime, endTime t
endTime.Format(SessionTimeFormat),
)
}
func WriteReportIndex(outputDirectory string, reportIndex *ReportIndex) error {
indexFile := filepath.Join(outputDirectory, "index.json")
if err := util.WriteJsonFile(indexFile, reportIndex); err != nil {
return err
}
return nil
}
func LoadReportIndex(outputDirectory string) (*ReportIndex, error) {
var reportIndex ReportIndex
indexFile := filepath.Join(outputDirectory, "index.json")
if _, err := os.Stat(indexFile); err == nil {
o, err := ioutil.ReadFile(indexFile)
if err != nil {
return nil, err
}
if err := json.Unmarshal(o, &reportIndex); err != nil {
return nil, err
}
}
return &reportIndex, nil
}
func AddReportIndexRun(outputDirectory string, run Run) error {
// append report index
lockFile := filepath.Join(outputDirectory, ".report.lock")
fileLock := flock.New(lockFile)
err := fileLock.Lock()
if err != nil {
return err
}
defer func() {
if err := fileLock.Unlock(); err != nil {
log.WithError(err).Errorf("report index file lock error: %s", lockFile)
}
if err := os.Remove(lockFile); err != nil {
log.WithError(err).Errorf("can not remove lock file: %s", lockFile)
}
}()
reportIndex, err := LoadReportIndex(outputDirectory)
if err != nil {
return err
}
reportIndex.Runs = append(reportIndex.Runs, run)
return WriteReportIndex(outputDirectory, reportIndex)
}
// inQuoteAsset converts all balances in quote asset
func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
quote := balances[market.QuoteCurrency]
base := balances[market.BaseCurrency]
return base.Total().Mul(price).Add(quote.Total())
}

View File

@ -7,6 +7,7 @@ import (
"io/ioutil"
"reflect"
"runtime"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
@ -106,8 +107,10 @@ type Backtest struct {
// RecordTrades is an option, if set to true, back-testing should record the trades into database
RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"`
// Deprecated:
// Account is deprecated, use Accounts instead
Account map[string]BacktestAccount `json:"account" yaml:"account"`
Account map[string]BacktestAccount `json:"account" yaml:"account"`
Accounts map[string]BacktestAccount `json:"accounts" yaml:"accounts"`
Symbols []string `json:"symbols" yaml:"symbols"`
Sessions []string `json:"sessions" yaml:"sessions"`
@ -119,7 +122,12 @@ func (b *Backtest) GetAccount(n string) BacktestAccount {
return accountConfig
}
return b.Account[n]
accountConfig, ok = b.Account[n]
if ok {
return accountConfig
}
return DefaultBacktestAccount
}
type BacktestAccount struct {
@ -130,6 +138,14 @@ type BacktestAccount struct {
Balances BacktestAccountBalanceMap `json:"balances" yaml:"balances"`
}
var DefaultBacktestAccount = BacktestAccount{
MakerFeeRate: fixedpoint.MustNewFromString("0.050%"),
TakerFeeRate: fixedpoint.MustNewFromString("0.075%"),
Balances: BacktestAccountBalanceMap{
"USDT": fixedpoint.NewFromFloat(10000),
},
}
type BA BacktestAccount
func (b *BacktestAccount) UnmarshalYAML(value *yaml.Node) error {
@ -310,6 +326,38 @@ func (c *Config) YAML() ([]byte, error) {
return buf.Bytes(), err
}
func (c *Config) GetSignature() string {
var s string
var ps []string
// for single exchange strategy
if len(c.ExchangeStrategies) == 1 && len(c.CrossExchangeStrategies) == 0 {
mount := c.ExchangeStrategies[0].Mounts[0]
ps = append(ps, mount)
strategy := c.ExchangeStrategies[0].Strategy
id := strategy.ID()
ps = append(ps, id)
if symbol, ok := isSymbolBasedStrategy(reflect.ValueOf(strategy)); ok {
ps = append(ps, symbol)
}
}
startTime := c.Backtest.StartTime.Time()
ps = append(ps, startTime.Format("2006-01-02"))
if c.Backtest.EndTime != nil {
endTime := c.Backtest.EndTime.Time()
ps = append(ps, endTime.Format("2006-01-02"))
}
s = strings.Join(ps, "_")
return s
}
type Stash map[string]interface{}
func loadStash(config []byte) (Stash, error) {

View File

@ -4,11 +4,10 @@ import (
"reflect"
)
type InstanceIDProvider interface{
type InstanceIDProvider interface {
InstanceID() string
}
func callID(obj interface{}) string {
sv := reflect.ValueOf(obj)
st := reflect.TypeOf(obj)
@ -21,6 +20,10 @@ func callID(obj interface{}) string {
}
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
if rs.Kind() == reflect.Ptr {
rs = rs.Elem()
}
field := rs.FieldByName("Symbol")
if !field.IsValid() {
return "", false

View File

@ -3,10 +3,7 @@ package cmd
import (
"bufio"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -24,8 +21,10 @@ import (
"github.com/c9s/bbgo/pkg/backtest"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
func init() {
@ -279,25 +278,19 @@ var BacktestCmd = &cobra.Command{
return err
}
// back-test session report name
var backtestSessionName = backtest.FormatSessionName(
userConfig.Backtest.Sessions,
userConfig.Backtest.Symbols,
userConfig.Backtest.StartTime.Time(),
userConfig.Backtest.EndTime.Time(),
)
var kLineHandlers []func(k types.KLine, exSource *backtest.ExchangeDataSource)
var manifests backtest.Manifests
var runID = userConfig.GetSignature() + "_" + uuid.NewString()
var reportDir = outputDirectory
if generatingReport {
reportDir := outputDirectory
if reportFileInSubDir {
reportDir = filepath.Join(reportDir, backtestSessionName)
reportDir = filepath.Join(reportDir, uuid.NewString())
// reportDir = filepath.Join(reportDir, backtestSessionName)
reportDir = filepath.Join(reportDir, runID)
}
kLineDataDir := filepath.Join(reportDir, "klines")
if err := safeMkdirAll(kLineDataDir); err != nil {
if err := util.SafeMkdirAll(kLineDataDir); err != nil {
return err
}
@ -305,8 +298,12 @@ var BacktestCmd = &cobra.Command{
err = trader.IterateStrategies(func(st bbgo.StrategyID) error {
return stateRecorder.Scan(st.(backtest.Instance))
})
manifests = stateRecorder.Manifests()
if err != nil {
return err
}
manifests = stateRecorder.Manifests()
manifests, err = rewriteManifestPaths(manifests, reportDir)
if err != nil {
return err
}
@ -338,18 +335,17 @@ var BacktestCmd = &cobra.Command{
})
// equity curve recording -- record per 1h kline
equityCurveFile, err := os.Create(filepath.Join(reportDir, "equity_curve.csv"))
equityCurveTsv, err := tsv.NewWriterFile(filepath.Join(reportDir, "equity_curve.tsv"))
if err != nil {
return err
}
defer func() { _ = equityCurveFile.Close() }()
defer func() { _ = equityCurveTsv.Close() }()
equityCurveCsv := csv.NewWriter(equityCurveFile)
_ = equityCurveCsv.Write([]string{
_ = equityCurveTsv.Write([]string{
"time",
"in_usd",
})
defer equityCurveCsv.Flush()
defer equityCurveTsv.Flush()
kLineHandlers = append(kLineHandlers, func(k types.KLine, exSource *backtest.ExchangeDataSource) {
if k.Interval != types.Interval1h {
@ -361,29 +357,26 @@ var BacktestCmd = &cobra.Command{
log.WithError(err).Errorf("query back-test account balance error")
} else {
assets := balances.Assets(exSource.Session.AllLastPrices(), k.EndTime.Time())
_ = equityCurveCsv.Write([]string{
_ = equityCurveTsv.Write([]string{
k.EndTime.Time().Format(time.RFC1123),
assets.InUSD().String(),
})
}
})
// equity curve recording -- record per 1h kline
ordersFile, err := os.Create(filepath.Join(reportDir, "orders.csv"))
ordersTsv, err := tsv.NewWriterFile(filepath.Join(reportDir, "orders.tsv"))
if err != nil {
return err
}
defer func() { _ = ordersFile.Close() }()
defer func() { _ = ordersTsv.Close() }()
_ = ordersTsv.Write(types.Order{}.CsvHeader())
ordersCsv := csv.NewWriter(ordersFile)
_ = ordersCsv.Write(types.Order{}.CsvHeader())
defer ordersCsv.Flush()
defer ordersTsv.Flush()
for _, exSource := range exchangeSources {
exSource.Session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if order.Status == types.OrderStatusFilled {
for _, record := range order.CsvRecords() {
_ = ordersCsv.Write(record)
_ = ordersTsv.Write(record)
}
}
})
@ -444,15 +437,10 @@ var BacktestCmd = &cobra.Command{
// put the logger back to print the pnl
log.SetLevel(log.InfoLevel)
color.Green("BACK-TEST REPORT")
color.Green("===============================================\n")
color.Green("START TIME: %s\n", startTime.Format(time.RFC1123))
color.Green("END TIME: %s\n", endTime.Format(time.RFC1123))
// aggregate total balances
initTotalBalances := types.BalanceMap{}
finalTotalBalances := types.BalanceMap{}
sessionNames := []string{}
var sessionNames []string
for _, session := range environ.Sessions() {
sessionNames = append(sessionNames, session.Name)
accountConfig := userConfig.Backtest.GetAccount(session.Name)
@ -462,6 +450,11 @@ var BacktestCmd = &cobra.Command{
finalBalances := session.GetAccount().Balances()
finalTotalBalances = finalTotalBalances.Add(finalBalances)
}
color.Green("BACK-TEST REPORT")
color.Green("===============================================\n")
color.Green("START TIME: %s\n", startTime.Format(time.RFC1123))
color.Green("END TIME: %s\n", endTime.Format(time.RFC1123))
color.Green("INITIAL TOTAL BALANCE: %v\n", initTotalBalances)
color.Green("FINAL TOTAL BALANCE: %v\n", finalTotalBalances)
@ -471,111 +464,45 @@ var BacktestCmd = &cobra.Command{
Sessions: sessionNames,
InitialTotalBalances: initTotalBalances,
FinalTotalBalances: finalTotalBalances,
Manifests: manifests,
Symbols: nil,
}
_ = summaryReport
for _, session := range environ.Sessions() {
backtestExchange, ok := session.Exchange.(*backtest.Exchange)
if !ok {
return fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
}
// per symbol report
exchangeName := session.Exchange.Name().String()
for symbol, trades := range session.Trades {
market, ok := session.Market(symbol)
if !ok {
return fmt.Errorf("market not found: %s, %s", symbol, exchangeName)
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades)
if err != nil {
return err
}
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
Market: market,
}
startPrice, ok := session.StartPrice(symbol)
if !ok {
return fmt.Errorf("start price not found: %s, %s. run --sync first", symbol, exchangeName)
}
lastPrice, ok := session.LastPrice(symbol)
if !ok {
return fmt.Errorf("last price not found: %s, %s", symbol, exchangeName)
}
color.Green("%s %s PROFIT AND LOSS REPORT", strings.ToUpper(exchangeName), symbol)
color.Green("===============================================")
report := calculator.Calculate(symbol, trades.Trades, lastPrice)
report.Print()
accountConfig := userConfig.Backtest.GetAccount(exchangeName)
initBalances := accountConfig.Balances.BalanceMap()
finalBalances := session.GetAccount().Balances()
summaryReport.Symbols = append(summaryReport.Symbols, symbol)
summaryReport.SymbolReports = append(summaryReport.SymbolReports, *symbolReport)
// write report to a file
if generatingReport {
result := backtest.SessionSymbolReport{
StartTime: startTime,
EndTime: endTime,
Symbol: symbol,
LastPrice: lastPrice,
StartPrice: startPrice,
PnLReport: report,
InitialBalances: initBalances,
FinalBalances: finalBalances,
Manifests: manifests,
}
if err := writeJsonFile(filepath.Join(outputDirectory, symbol+".json"), &result); err != nil {
reportFileName := fmt.Sprintf("symbol_report_%s.json", symbol)
if err := util.WriteJsonFile(filepath.Join(reportDir, reportFileName), &symbolReport); err != nil {
return err
}
}
initQuoteAsset := inQuoteAsset(initBalances, market, startPrice)
finalQuoteAsset := inQuoteAsset(finalBalances, market, lastPrice)
color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(initQuoteAsset), market.QuoteCurrency, market.BaseCurrency, startPrice)
color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(finalQuoteAsset), market.QuoteCurrency, market.BaseCurrency, lastPrice)
symbolReport.Print(wantBaseAssetBaseline)
}
}
if report.Profit.Sign() > 0 {
color.Green("REALIZED PROFIT: +%v %s", report.Profit, market.QuoteCurrency)
} else {
color.Red("REALIZED PROFIT: %v %s", report.Profit, market.QuoteCurrency)
}
if generatingReport && reportFileInSubDir {
summaryReportFile := filepath.Join(reportDir, "summary.json")
if err := util.WriteJsonFile(summaryReportFile, summaryReport); err != nil {
return err
}
if report.UnrealizedProfit.Sign() > 0 {
color.Green("UNREALIZED PROFIT: +%v %s", report.UnrealizedProfit, market.QuoteCurrency)
} else {
color.Red("UNREALIZED PROFIT: %v %s", report.UnrealizedProfit, market.QuoteCurrency)
}
if finalQuoteAsset.Compare(initQuoteAsset) > 0 {
color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
} else {
color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2))
}
if wantBaseAssetBaseline {
// initBaseAsset := inBaseAsset(initBalances, market, startPrice)
// finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice)
// log.Infof("INITIAL ASSET IN %s ~= %s %s (1 %s = %f)", market.BaseCurrency, market.FormatQuantity(initBaseAsset), market.BaseCurrency, market.BaseCurrency, startPrice)
// log.Infof("FINAL ASSET IN %s ~= %s %s (1 %s = %f)", market.BaseCurrency, market.FormatQuantity(finalBaseAsset), market.BaseCurrency, market.BaseCurrency, lastPrice)
if lastPrice.Compare(startPrice) > 0 {
color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)",
market.BaseCurrency,
lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2),
lastPrice.FormatString(2),
startPrice.FormatString(2),
startPrice.FormatString(2))
} else {
color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)",
market.BaseCurrency,
lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2),
lastPrice.FormatString(2),
startPrice.FormatString(2),
startPrice.FormatString(2))
}
}
// append report index
if err := backtest.AddReportIndexRun(outputDirectory, backtest.Run{
ID: runID,
Config: userConfig,
Time: time.Now(),
}); err != nil {
return err
}
}
@ -583,6 +510,50 @@ var BacktestCmd = &cobra.Command{
},
}
func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade) (*backtest.SessionSymbolReport, error) {
backtestExchange, ok := session.Exchange.(*backtest.Exchange)
if !ok {
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
}
market, ok := session.Market(symbol)
if !ok {
return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
}
startPrice, ok := session.StartPrice(symbol)
if !ok {
return nil, fmt.Errorf("start price not found: %s, %s. run --sync first", symbol, session.Exchange.Name())
}
lastPrice, ok := session.LastPrice(symbol)
if !ok {
return nil, fmt.Errorf("last price not found: %s, %s", symbol, session.Exchange.Name())
}
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
Market: market,
}
report := calculator.Calculate(symbol, trades, lastPrice)
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
initBalances := accountConfig.Balances.BalanceMap()
finalBalances := session.GetAccount().Balances()
symbolReport := backtest.SessionSymbolReport{
Exchange: session.Exchange.Name(),
Symbol: symbol,
Market: market,
LastPrice: lastPrice,
StartPrice: startPrice,
PnL: report,
InitialBalances: initBalances,
FinalBalances: finalBalances,
// Manifests: manifests,
}
return &symbolReport, nil
}
func verify(userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime time.Time, verboseCnt int) error {
for _, sourceExchange := range sourceExchanges {
err := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt)
@ -615,32 +586,6 @@ func confirmation(s string) bool {
}
}
func writeJsonFile(p string, obj interface{}) error {
out, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(p, out, 0644)
}
func safeMkdirAll(p string) error {
st, err := os.Stat(p)
if err == nil {
if !st.IsDir() {
return fmt.Errorf("path %s is not a directory", p)
}
return nil
}
if os.IsNotExist(err) {
return os.MkdirAll(p, 0755)
}
return nil
}
func toExchangeSources(sessions map[string]*bbgo.ExchangeSession) (exchangeSources []backtest.ExchangeDataSource, err error) {
for _, session := range sessions {
exchange := session.Exchange.(*backtest.Exchange)
@ -699,3 +644,15 @@ func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service
}
return nil
}
func rewriteManifestPaths(manifests backtest.Manifests, basePath string) (backtest.Manifests, error) {
var filterManifests = backtest.Manifests{}
for k, m := range manifests {
p, err := filepath.Rel(basePath, m)
if err != nil {
return nil, err
}
filterManifests[k] = p
}
return filterManifests, nil
}

View File

@ -5,11 +5,12 @@ import (
"github.com/spf13/viper"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/c9s/bbgo/pkg/exchange/ftx"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func cobraInitRequired(required []string) func(cmd *cobra.Command, args []string) error {
@ -23,6 +24,7 @@ func cobraInitRequired(required []string) func(cmd *cobra.Command, args []string
}
}
// inQuoteAsset converts all balances in quote asset
func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
quote := balances[market.QuoteCurrency]
base := balances[market.BaseCurrency]

36
pkg/data/tsv/writer.go Normal file
View File

@ -0,0 +1,36 @@
package tsv
import (
"encoding/csv"
"io"
"os"
)
type Writer struct {
file io.WriteCloser
*csv.Writer
}
func NewWriterFile(filename string) (*Writer, error) {
f, err := os.Create(filename)
if err != nil {
return nil, err
}
return NewWriter(f), nil
}
func NewWriter(file io.WriteCloser) *Writer {
tsv := csv.NewWriter(file)
tsv.Comma = '\t'
return &Writer{
Writer: tsv,
file: file,
}
}
func (w *Writer) Close() error {
w.Writer.Flush()
return w.file.Close()
}

View File

@ -266,6 +266,11 @@ func (t *LooseFormatTime) UnmarshalJSON(data []byte) error {
return nil
}
func (t LooseFormatTime) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(time.Time(t).Format(time.RFC3339))), nil
}
func (t LooseFormatTime) Time() time.Time {
return time.Time(t)
}

23
pkg/util/dir.go Normal file
View File

@ -0,0 +1,23 @@
package util
import (
"fmt"
"os"
)
func SafeMkdirAll(p string) error {
st, err := os.Stat(p)
if err == nil {
if !st.IsDir() {
return fmt.Errorf("path %s is not a directory", p)
}
return nil
}
if os.IsNotExist(err) {
return os.MkdirAll(p, 0755)
}
return nil
}

15
pkg/util/json.go Normal file
View File

@ -0,0 +1,15 @@
package util
import (
"encoding/json"
"io/ioutil"
)
func WriteJsonFile(p string, obj interface{}) error {
out, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(p, out, 0644)
}