mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +00:00
add react-financial-charts example
This commit is contained in:
parent
704d121fb1
commit
b855d2e30b
|
@ -18,6 +18,9 @@
|
|||
"react-financial-charts": "^1.3.2"
|
||||
},
|
||||
"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",
|
||||
|
|
|
@ -3,6 +3,8 @@ import Head from 'next/head'
|
|||
import Image from 'next/image'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
import StockChart from '../src/StockChart';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
@ -17,54 +19,8 @@ const Home: NextPage = () => {
|
|||
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>
|
||||
<StockChart> </StockChart>
|
||||
</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(dataSet = "DAILY") {
|
||||
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 ${dataSet} data...`,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
fetch(
|
||||
`https://raw.githubusercontent.com/reactivemarkets/react-financial-charts/master/packages/stories/src/data/${dataSet}.tsv`,
|
||||
)
|
||||
.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: 600 } })(withDeviceRatio()(StockChart)));
|
||||
|
||||
export const MinutesStockChart = withOHLCData("MINUTES")(
|
||||
withSize({ style: { minHeight: 600 } })(withDeviceRatio()(StockChart)),
|
||||
);
|
||||
|
||||
export const SecondsStockChart = withOHLCData("SECONDS")(
|
||||
withSize({ style: { minHeight: 600 } })(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 {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user