Add charts (#98)
This commit is contained in:
parent
7878768f46
commit
7442669214
|
@ -25,3 +25,9 @@ yarn-error.log*
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
.env
|
.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 `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`.
|
- 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.
|
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"
|
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" />
|
<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/polyfills.js"></script>
|
||||||
<script src="%PUBLIC_URL%/datafeeds/udf/dist/bundle.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
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
|
|
@ -4,18 +4,19 @@ import {
|
||||||
widget,
|
widget,
|
||||||
ChartingLibraryWidgetOptions,
|
ChartingLibraryWidgetOptions,
|
||||||
IChartingLibraryWidget,
|
IChartingLibraryWidget,
|
||||||
ResolutionString,
|
} from '../../charting_library';
|
||||||
} from '../../charting_library'; // Make sure to follow step 1 of the README
|
import { useMarket, USE_MARKETS } from '../../utils/markets';
|
||||||
import { useMarket } from '../../utils/markets';
|
import * as saveLoadAdapter from './saveLoadAdapter';
|
||||||
|
import { flatten } from '../../utils/utils';
|
||||||
import { BONFIDA_DATA_FEED } from '../../utils/bonfidaConnector';
|
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 {
|
export interface ChartContainerProps {
|
||||||
symbol: ChartingLibraryWidgetOptions['symbol'];
|
symbol: ChartingLibraryWidgetOptions['symbol'];
|
||||||
interval: ChartingLibraryWidgetOptions['interval'];
|
interval: ChartingLibraryWidgetOptions['interval'];
|
||||||
|
auto_save_delay: ChartingLibraryWidgetOptions['auto_save_delay'];
|
||||||
|
|
||||||
|
// BEWARE: no trailing slash is expected in feed URL
|
||||||
|
// datafeed: any;
|
||||||
datafeedUrl: string;
|
datafeedUrl: string;
|
||||||
libraryPath: ChartingLibraryWidgetOptions['library_path'];
|
libraryPath: ChartingLibraryWidgetOptions['library_path'];
|
||||||
chartsStorageUrl: ChartingLibraryWidgetOptions['charts_storage_url'];
|
chartsStorageUrl: ChartingLibraryWidgetOptions['charts_storage_url'];
|
||||||
|
@ -32,35 +33,55 @@ export interface ChartContainerProps {
|
||||||
export interface ChartContainerState {}
|
export interface ChartContainerState {}
|
||||||
|
|
||||||
export const TVChartContainer = () => {
|
export const TVChartContainer = () => {
|
||||||
// @ts-ignore
|
// let datafeed = useTvDataFeed();
|
||||||
const defaultProps: ChartContainerProps = {
|
const defaultProps: ChartContainerProps = {
|
||||||
symbol: 'BTC/USDC',
|
symbol: 'BTC/USDC',
|
||||||
interval: '60' as ResolutionString,
|
// @ts-ignore
|
||||||
|
interval: '60',
|
||||||
|
auto_save_delay: 5,
|
||||||
theme: 'Dark',
|
theme: 'Dark',
|
||||||
containerId: 'tv_chart_container',
|
containerId: 'tv_chart_container',
|
||||||
datafeedUrl: BONFIDA_DATA_FEED,
|
// datafeed: datafeed,
|
||||||
libraryPath: '/charting_library/',
|
libraryPath: '/charting_library/',
|
||||||
|
chartsStorageApiVersion: '1.1',
|
||||||
|
clientId: 'tradingview.com',
|
||||||
|
userId: 'public_user_id',
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
autosize: true,
|
autosize: true,
|
||||||
|
datafeedUrl: BONFIDA_DATA_FEED,
|
||||||
studiesOverrides: {},
|
studiesOverrides: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tvWidgetRef = React.useRef<IChartingLibraryWidget | null>(null);
|
const tvWidgetRef = React.useRef<IChartingLibraryWidget | null>(null);
|
||||||
const { market } = useMarket();
|
const { market } = useMarket();
|
||||||
|
|
||||||
|
const chartProperties = JSON.parse(
|
||||||
|
localStorage.getItem('chartproperties') || '{}',
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
const savedProperties = flatten(chartProperties, {
|
||||||
|
restrictTo: ['scalesProperties', 'paneProperties', 'tradingProperties'],
|
||||||
|
});
|
||||||
|
|
||||||
const widgetOptions: ChartingLibraryWidgetOptions = {
|
const widgetOptions: ChartingLibraryWidgetOptions = {
|
||||||
symbol: findTVMarketFromAddress(
|
symbol:
|
||||||
market?.address.toBase58() || '',
|
USE_MARKETS.find(
|
||||||
) as string,
|
(m) => m.address.toBase58() === market?.publicKey.toBase58(),
|
||||||
|
)?.name || 'SRM/USDC',
|
||||||
// BEWARE: no trailing slash is expected in feed URL
|
// BEWARE: no trailing slash is expected in feed URL
|
||||||
// tslint:disable-next-line:no-any
|
// tslint:disable-next-line:no-any
|
||||||
|
// @ts-ignore
|
||||||
|
// datafeed: datafeed,
|
||||||
|
// @ts-ignore
|
||||||
datafeed: new (window as any).Datafeeds.UDFCompatibleDatafeed(
|
datafeed: new (window as any).Datafeeds.UDFCompatibleDatafeed(
|
||||||
defaultProps.datafeedUrl,
|
defaultProps.datafeedUrl,
|
||||||
),
|
),
|
||||||
interval: defaultProps.interval as ChartingLibraryWidgetOptions['interval'],
|
interval: defaultProps.interval as ChartingLibraryWidgetOptions['interval'],
|
||||||
container_id: defaultProps.containerId as ChartingLibraryWidgetOptions['container_id'],
|
container_id: defaultProps.containerId as ChartingLibraryWidgetOptions['container_id'],
|
||||||
library_path: defaultProps.libraryPath as string,
|
library_path: defaultProps.libraryPath as string,
|
||||||
|
auto_save_delay: 5,
|
||||||
|
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
disabled_features: ['use_localstorage_for_settings'],
|
disabled_features: ['use_localstorage_for_settings'],
|
||||||
enabled_features: ['study_templates'],
|
enabled_features: ['study_templates'],
|
||||||
|
@ -70,30 +91,59 @@ export const TVChartContainer = () => {
|
||||||
fullscreen: defaultProps.fullscreen,
|
fullscreen: defaultProps.fullscreen,
|
||||||
autosize: defaultProps.autosize,
|
autosize: defaultProps.autosize,
|
||||||
studies_overrides: defaultProps.studiesOverrides,
|
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);
|
const tvWidget = new widget(widgetOptions);
|
||||||
tvWidgetRef.current = tvWidget;
|
|
||||||
|
|
||||||
tvWidget.onChartReady(() => {
|
tvWidget.onChartReady(() => {
|
||||||
tvWidget.headerReady().then(() => {
|
tvWidgetRef.current = tvWidget;
|
||||||
const button = tvWidget.createButton();
|
tvWidget
|
||||||
button.setAttribute('title', 'Click to show a notification popup');
|
// @ts-ignore
|
||||||
button.classList.add('apply-common-tooltip');
|
.subscribe('onAutoSaveNeeded', () => tvWidget.saveChartToServer());
|
||||||
button.addEventListener('click', () =>
|
|
||||||
tvWidget.showNoticeDialog({
|
|
||||||
title: 'Notification',
|
|
||||||
body: 'TradingView Charting Library API works correctly',
|
|
||||||
callback: () => {
|
|
||||||
console.log('It works!!');
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
button.innerHTML = 'Check API';
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}, [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 { notify } from '../utils/notifications';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { TVChartContainer } from '../components/TradingView';
|
||||||
|
|
||||||
const { Option, OptGroup } = Select;
|
const { Option, OptGroup } = Select;
|
||||||
|
|
||||||
|
@ -335,8 +336,13 @@ const RenderNormal = ({ onChangeOrderRef, onPrice, onSize }) => {
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Col flex="auto" style={{ height: '100%', display: 'flex' }}>
|
<Col flex="auto" style={{ height: '50vh' }}>
|
||||||
<UserInfoTable />
|
<Row style={{ height: '100%' }}>
|
||||||
|
<TVChartContainer />
|
||||||
|
</Row>
|
||||||
|
<Row style={{ height: '70%' }}>
|
||||||
|
<UserInfoTable />
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col flex={'360px'} style={{ height: '100%' }}>
|
<Col flex={'360px'} style={{ height: '100%' }}>
|
||||||
<Orderbook smallScreen={false} onPrice={onPrice} onSize={onSize} />
|
<Orderbook smallScreen={false} onPrice={onPrice} onSize={onSize} />
|
||||||
|
@ -356,6 +362,9 @@ const RenderNormal = ({ onChangeOrderRef, onPrice, onSize }) => {
|
||||||
const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
|
const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Row style={{ height: '30vh' }}>
|
||||||
|
<TVChartContainer />
|
||||||
|
</Row>
|
||||||
<Row
|
<Row
|
||||||
style={{
|
style={{
|
||||||
height: '900px',
|
height: '900px',
|
||||||
|
@ -392,6 +401,9 @@ const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
|
||||||
const RenderSmaller = ({ onChangeOrderRef, onPrice, onSize }) => {
|
const RenderSmaller = ({ onChangeOrderRef, onPrice, onSize }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Row style={{ height: '50vh' }}>
|
||||||
|
<TVChartContainer />
|
||||||
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24} sm={12} style={{ height: '100%', display: 'flex' }}>
|
<Col xs={24} sm={12} style={{ height: '100%', display: 'flex' }}>
|
||||||
<TradeForm style={{ flex: 1 }} setChangeOrderRef={onChangeOrderRef} />
|
<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;
|
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