Add charts (#98)
This commit is contained in:
parent
7878768f46
commit
7442669214
|
@ -25,3 +25,9 @@ yarn-error.log*
|
|||
.idea
|
||||
|
||||
.env
|
||||
|
||||
|
||||
# TV
|
||||
public/charting_library
|
||||
src/charting_library
|
||||
public/datafeeds
|
19
README.md
19
README.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'} />;
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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' }}>
|
||||
<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} />
|
||||
|
|
|
@ -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';
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue