Add charts (#98)

This commit is contained in:
DR497 2021-07-07 01:33:36 +08:00 committed by GitHub
parent 7878768f46
commit 7442669214
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 218 additions and 66 deletions

6
.gitignore vendored
View File

@ -25,3 +25,9 @@ yarn-error.log*
.idea
.env
# TV
public/charting_library
src/charting_library
public/datafeeds

View File

@ -27,25 +27,6 @@ It is possible to add OHLCV candles built from on chain data using [Bonfida's AP
- Copy `charting_library` folder from https://github.com/tradingview/charting_library/ to `/public` and to `/src` folders.
- Copy `datafeeds` folder from https://github.com/tradingview/charting_library/ to `/public`.
3. Import `TVChartContainer` from `/src/components/TradingView` and add it to your `TradePage.tsx`. The TradingView widget will work out of the box using [Bonfida's](https://bonfida.com) datafeed.
4. Remove the following from the `tsconfig.json`
```json
"./src/components/TradingView/index.tsx"
```
5. Uncomment the following in `public/index.html`
```
<script src="%PUBLIC_URL%/datafeeds/udf/dist/polyfills.js"></script>
<script src="%PUBLIC_URL%/datafeeds/udf/dist/bundle.js">
```
<p align="center">
<img height="300" src="https://i.imgur.com/UyFKmTv.png">
</p>
---
See the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started) for other commands and options.

View File

@ -39,11 +39,11 @@
content="Serum DEX - The world's first completely decentralized derivatives exchange with trustless cross-chain trading"
/>
<meta name="twitter:image" content="https://i.imgur.com/YS5Csfy.png" />
<!--
uncomment the script tags below to enable the TradingView display
<script src="%PUBLIC_URL%/datafeeds/udf/dist/polyfills.js"></script>
<script src="%PUBLIC_URL%/datafeeds/udf/dist/bundle.js"></script>
-->
<!--
manifest.json provides metadata used when your web app is installed on a

114
src/components/TradingView/index.tsx Executable file → Normal file
View File

@ -4,18 +4,19 @@ import {
widget,
ChartingLibraryWidgetOptions,
IChartingLibraryWidget,
ResolutionString,
} from '../../charting_library'; // Make sure to follow step 1 of the README
import { useMarket } from '../../utils/markets';
} from '../../charting_library';
import { useMarket, USE_MARKETS } from '../../utils/markets';
import * as saveLoadAdapter from './saveLoadAdapter';
import { flatten } from '../../utils/utils';
import { BONFIDA_DATA_FEED } from '../../utils/bonfidaConnector';
import { findTVMarketFromAddress } from '../../utils/tradingview';
// This is a basic example of how to create a TV widget
// You can add more feature such as storing charts in localStorage
export interface ChartContainerProps {
symbol: ChartingLibraryWidgetOptions['symbol'];
interval: ChartingLibraryWidgetOptions['interval'];
auto_save_delay: ChartingLibraryWidgetOptions['auto_save_delay'];
// BEWARE: no trailing slash is expected in feed URL
// datafeed: any;
datafeedUrl: string;
libraryPath: ChartingLibraryWidgetOptions['library_path'];
chartsStorageUrl: ChartingLibraryWidgetOptions['charts_storage_url'];
@ -32,35 +33,55 @@ export interface ChartContainerProps {
export interface ChartContainerState {}
export const TVChartContainer = () => {
// @ts-ignore
// let datafeed = useTvDataFeed();
const defaultProps: ChartContainerProps = {
symbol: 'BTC/USDC',
interval: '60' as ResolutionString,
// @ts-ignore
interval: '60',
auto_save_delay: 5,
theme: 'Dark',
containerId: 'tv_chart_container',
datafeedUrl: BONFIDA_DATA_FEED,
// datafeed: datafeed,
libraryPath: '/charting_library/',
chartsStorageApiVersion: '1.1',
clientId: 'tradingview.com',
userId: 'public_user_id',
fullscreen: false,
autosize: true,
datafeedUrl: BONFIDA_DATA_FEED,
studiesOverrides: {},
};
const tvWidgetRef = React.useRef<IChartingLibraryWidget | null>(null);
const { market } = useMarket();
const chartProperties = JSON.parse(
localStorage.getItem('chartproperties') || '{}',
);
React.useEffect(() => {
const savedProperties = flatten(chartProperties, {
restrictTo: ['scalesProperties', 'paneProperties', 'tradingProperties'],
});
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: findTVMarketFromAddress(
market?.address.toBase58() || '',
) as string,
symbol:
USE_MARKETS.find(
(m) => m.address.toBase58() === market?.publicKey.toBase58(),
)?.name || 'SRM/USDC',
// BEWARE: no trailing slash is expected in feed URL
// tslint:disable-next-line:no-any
// @ts-ignore
// datafeed: datafeed,
// @ts-ignore
datafeed: new (window as any).Datafeeds.UDFCompatibleDatafeed(
defaultProps.datafeedUrl,
),
interval: defaultProps.interval as ChartingLibraryWidgetOptions['interval'],
container_id: defaultProps.containerId as ChartingLibraryWidgetOptions['container_id'],
library_path: defaultProps.libraryPath as string,
auto_save_delay: 5,
locale: 'en',
disabled_features: ['use_localstorage_for_settings'],
enabled_features: ['study_templates'],
@ -70,30 +91,59 @@ export const TVChartContainer = () => {
fullscreen: defaultProps.fullscreen,
autosize: defaultProps.autosize,
studies_overrides: defaultProps.studiesOverrides,
theme: 'Dark',
theme: defaultProps.theme === 'Dark' ? 'Dark' : 'Light',
overrides: {
...savedProperties,
'mainSeriesProperties.candleStyle.upColor': '#41C77A',
'mainSeriesProperties.candleStyle.downColor': '#F23B69',
'mainSeriesProperties.candleStyle.borderUpColor': '#41C77A',
'mainSeriesProperties.candleStyle.borderDownColor': '#F23B69',
'mainSeriesProperties.candleStyle.wickUpColor': '#41C77A',
'mainSeriesProperties.candleStyle.wickDownColor': '#F23B69',
},
// @ts-ignore
save_load_adapter: saveLoadAdapter,
settings_adapter: {
initialSettings: {
'trading.orderPanelSettingsBroker': JSON.stringify({
showRelativePriceControl: false,
showCurrencyRiskInQty: false,
showPercentRiskInQty: false,
showBracketsInCurrency: false,
showBracketsInPercent: false,
}),
// "proterty"
'trading.chart.proterty':
localStorage.getItem('trading.chart.proterty') ||
JSON.stringify({
hideFloatingPanel: 1,
}),
'chart.favoriteDrawings':
localStorage.getItem('chart.favoriteDrawings') ||
JSON.stringify([]),
'chart.favoriteDrawingsPosition':
localStorage.getItem('chart.favoriteDrawingsPosition') ||
JSON.stringify({}),
},
setValue: (key, value) => {
localStorage.setItem(key, value);
},
removeValue: (key) => {
localStorage.removeItem(key);
},
},
};
const tvWidget = new widget(widgetOptions);
tvWidgetRef.current = tvWidget;
tvWidget.onChartReady(() => {
tvWidget.headerReady().then(() => {
const button = tvWidget.createButton();
button.setAttribute('title', 'Click to show a notification popup');
button.classList.add('apply-common-tooltip');
button.addEventListener('click', () =>
tvWidget.showNoticeDialog({
title: 'Notification',
body: 'TradingView Charting Library API works correctly',
callback: () => {
console.log('It works!!');
},
}),
);
button.innerHTML = 'Check API';
});
tvWidgetRef.current = tvWidget;
tvWidget
// @ts-ignore
.subscribe('onAutoSaveNeeded', () => tvWidget.saveChartToServer());
});
}, [market]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [market, tvWidgetRef.current]);
return <div id={defaultProps.containerId} className="tradingview-chart" />;
return <div id={defaultProps.containerId} className={'TVChartContainer'} />;
};

View File

@ -0,0 +1,92 @@
const CHARTS_KEY = 'tradingviewCharts';
const STUDIES_KEY = 'tradingviewStudies';
// See https://github.com/tradingview/charting_library/wiki/Widget-Constructor#save_load_adapter
export function getAllCharts() {
// @ts-ignore
let charts = JSON.parse(localStorage.getItem(CHARTS_KEY)) || [];
return new Promise((resolve) => resolve(charts));
}
export function removeChart(chartId) {
// @ts-ignore
let charts = JSON.parse(localStorage.getItem(CHARTS_KEY)) || [];
charts = charts.filter((chart) => chart.id !== chartId);
localStorage.setItem(CHARTS_KEY, JSON.stringify(charts));
localStorage.removeItem(CHARTS_KEY + '.' + chartId);
return new Promise<void>((resolve) => resolve());
}
export function saveChart(chartData) {
let { content, ...info } = chartData;
if (!info.id) {
info.id = 'chart' + Math.floor(Math.random() * 1e8);
}
// @ts-ignore
info.timestamp = new Date() - 0;
content = JSON.parse(content);
content['content'] = JSON.parse(content['content']);
// Remove "study_Overlay" i.e the indexes
try {
for (
var i = 0;
i < content['content']['charts'][0]['panes'][0]['sources'].length;
i++
) {
if (
content['content']['charts'][0]['panes'][0]['sources'][i]['type'] ===
'study_Overlay'
) {
content['content']['charts'][0]['panes'][0]['sources'].splice(i, 1);
}
}
} catch (err) {
console.log(err);
}
content['content'] = JSON.stringify(content['content']);
content = JSON.stringify(content);
// @ts-ignore
let charts = JSON.parse(localStorage.getItem(CHARTS_KEY)) || [];
charts = charts.filter((chart) => chart.id !== info.id);
charts.push(info);
localStorage.setItem(CHARTS_KEY, JSON.stringify(charts));
localStorage.setItem(CHARTS_KEY + '.' + info.id, content);
return new Promise((resolve) => resolve(info.id));
}
export function getChartContent(chartId) {
let content = localStorage.getItem(CHARTS_KEY + '.' + chartId);
return new Promise((resolve) => resolve(content));
}
export function getAllStudyTemplates() {
// @ts-ignore
let studies = JSON.parse(localStorage.getItem(STUDIES_KEY)) || [];
return new Promise((resolve) => resolve(studies));
}
export function removeStudyTemplate({ name }) {
// @ts-ignore
let studies = JSON.parse(localStorage.getItem(STUDIES_KEY)) || [];
studies = studies.filter((study) => study.name !== name);
localStorage.setItem(STUDIES_KEY, JSON.stringify(studies));
localStorage.removeItem(STUDIES_KEY + '.' + name);
return new Promise((resolve) => resolve());
}
export function saveStudyTemplate({ content, ...info }) {
// @ts-ignore
let studies = JSON.parse(localStorage.getItem(STUDIES_KEY)) || [];
studies = studies.filter((study) => study.name !== info.name);
studies.push(info);
localStorage.setItem(STUDIES_KEY, JSON.stringify(studies));
localStorage.setItem(STUDIES_KEY + '.' + info.name, content);
return new Promise((resolve) => resolve());
}
export function getStudyTemplateContent({ name }) {
let content = localStorage.getItem(STUDIES_KEY + '.' + name);
return new Promise((resolve) => resolve(content));
}

View File

@ -25,6 +25,7 @@ import CustomMarketDialog from '../components/CustomMarketDialog';
import { notify } from '../utils/notifications';
import { useHistory, useParams } from 'react-router-dom';
import { nanoid } from 'nanoid';
import { TVChartContainer } from '../components/TradingView';
const { Option, OptGroup } = Select;
@ -335,8 +336,13 @@ const RenderNormal = ({ onChangeOrderRef, onPrice, onSize }) => {
flexWrap: 'nowrap',
}}
>
<Col flex="auto" style={{ height: '100%', display: 'flex' }}>
<UserInfoTable />
<Col flex="auto" style={{ height: '50vh' }}>
<Row style={{ height: '100%' }}>
<TVChartContainer />
</Row>
<Row style={{ height: '70%' }}>
<UserInfoTable />
</Row>
</Col>
<Col flex={'360px'} style={{ height: '100%' }}>
<Orderbook smallScreen={false} onPrice={onPrice} onSize={onSize} />
@ -356,6 +362,9 @@ const RenderNormal = ({ onChangeOrderRef, onPrice, onSize }) => {
const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
return (
<>
<Row style={{ height: '30vh' }}>
<TVChartContainer />
</Row>
<Row
style={{
height: '900px',
@ -392,6 +401,9 @@ const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
const RenderSmaller = ({ onChangeOrderRef, onPrice, onSize }) => {
return (
<>
<Row style={{ height: '50vh' }}>
<TVChartContainer />
</Row>
<Row>
<Col xs={24} sm={12} style={{ height: '100%', display: 'flex' }}>
<TradeForm style={{ flex: 1 }} setChangeOrderRef={onChangeOrderRef} />

View File

@ -1,10 +0,0 @@
import { USE_MARKETS } from './markets';
export const findTVMarketFromAddress = (marketAddressString: string) => {
USE_MARKETS.forEach((market) => {
if (market.address.toBase58() === marketAddressString) {
return market.name;
}
});
return 'SRM/USDC';
};

View File

@ -162,3 +162,24 @@ export function isEqual(obj1, obj2, keys) {
}
return true;
}
export function flatten(obj, { prefix = '', restrictTo }) {
let restrict = restrictTo;
if (restrict) {
restrict = restrict.filter((k) => obj.hasOwnProperty(k));
}
const result = {};
(function recurse(obj, current, keys) {
(keys || Object.keys(obj)).forEach((key) => {
const value = obj[key];
const newKey = current ? current + '.' + key : key; // joined key with dot
if (value && typeof value === 'object') {
// @ts-ignore
recurse(value, newKey); // nested object
} else {
result[newKey] = value;
}
});
})(obj, prefix, restrict);
return result;
}