mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
Merge pull request #605 from c9s/feature/backtest-report
feature: add web-based back-test report
This commit is contained in:
commit
e57c39e665
|
@ -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
|
||||
|
|
51
apps/bbgo-backtest-report/components/ReportDetails.tsx
Normal file
51
apps/bbgo-backtest-report/components/ReportDetails.tsx
Normal 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;
|
57
apps/bbgo-backtest-report/components/ReportNavigator.tsx
Normal file
57
apps/bbgo-backtest-report/components/ReportNavigator.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from '@nextui-org/react';
|
||||
import {ReportEntry, ReportIndex} from '../types';
|
||||
|
||||
function fetchIndex(basePath: string, setter: (data: any) => void) {
|
||||
return fetch(
|
||||
`${basePath}/index.json`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("reportIndex", data);
|
||||
setter(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("failed to fetch index", e)
|
||||
});
|
||||
}
|
||||
|
||||
interface ReportNavigatorProps {
|
||||
onSelect: (reportEntry: ReportEntry) => void;
|
||||
}
|
||||
|
||||
const ReportNavigator = (props: ReportNavigatorProps) => {
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [reportIndex, setReportIndex] = useState<ReportIndex>({runs: []});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchIndex('/output', setReportIndex).then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (reportIndex.runs.length == 0) {
|
||||
return <div>No back-test report data</div>
|
||||
}
|
||||
|
||||
return <div>
|
||||
{
|
||||
reportIndex.runs.map((entry) => {
|
||||
return <Link key={entry.id} onClick={() => {
|
||||
if (props.onSelect) {
|
||||
props.onSelect(entry);
|
||||
}
|
||||
}}>{entry.id}</Link>
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default ReportNavigator;
|
436
apps/bbgo-backtest-report/components/TradingViewChart.js
Normal file
436
apps/bbgo-backtest-report/components/TradingViewChart.js
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
28
apps/bbgo-backtest-report/pages/_document.tsx
Normal file
28
apps/bbgo-backtest-report/pages/_document.tsx
Normal 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;
|
|
@ -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 →</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 →</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 →</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 →</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>
|
||||
)
|
||||
}
|
||||
|
|
287
apps/bbgo-backtest-report/src/StockChart.tsx
Normal file
287
apps/bbgo-backtest-report/src/StockChart.tsx
Normal 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)),
|
||||
);
|
|
@ -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;
|
||||
|
|
1
apps/bbgo-backtest-report/types/index.ts
Normal file
1
apps/bbgo-backtest-report/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './report';
|
16
apps/bbgo-backtest-report/types/market.ts
Normal file
16
apps/bbgo-backtest-report/types/market.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export interface Market {
|
||||
symbol: string;
|
||||
localSymbol: string;
|
||||
pricePrecision: number;
|
||||
volumePrecision: number;
|
||||
quoteCurrency: string;
|
||||
baseCurrency: string;
|
||||
minNotional: number;
|
||||
minAmount: number;
|
||||
minQuantity: number;
|
||||
maxQuantity: number;
|
||||
stepSize: number;
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
tickSize: number;
|
||||
}
|
75
apps/bbgo-backtest-report/types/report.ts
Normal file
75
apps/bbgo-backtest-report/types/report.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {Market} from './market';
|
||||
|
||||
export interface ReportEntry {
|
||||
id: string;
|
||||
|
||||
config: object;
|
||||
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface ReportIndex {
|
||||
runs: Array<ReportEntry>;
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
currency: string;
|
||||
available: number;
|
||||
locked: number;
|
||||
borrowed: number;
|
||||
}
|
||||
|
||||
export interface BalanceMap {
|
||||
[currency: string]: Balance;
|
||||
}
|
||||
|
||||
export interface ReportSummary {
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
sessions: string[];
|
||||
symbols: 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;
|
||||
}
|
|
@ -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
3
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
36
pkg/data/tsv/writer.go
Normal 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()
|
||||
}
|
|
@ -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
23
pkg/util/dir.go
Normal 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
15
pkg/util/json.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user