binbot-charts 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/App.jsx +2 -2
- package/src/components/TVChartContainer.jsx +16 -26
- package/src/components/datafeed.js +201 -0
- package/src/components/helpers.js +32 -0
- package/src/components/streaming.js +136 -0
- package/dist/yarn.lock +0 -4
- package/public/charting_library/yarn.lock +0 -4
- package/src/charting_library/yarn.lock +0 -4
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.4",
|
|
3
3
|
"name": "binbot-charts",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@babel/cli": "^7.18.10",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "react-scripts start",
|
|
15
|
-
"build": "rm -rf dist && NODE_ENV=production babel src/components src/charting_library --out-dir dist --copy-files"
|
|
15
|
+
"build": "rm -rf dist && NODE_ENV=production babel src/components src/charting_library --out-dir dist --copy-files",
|
|
16
|
+
"publish": "yarn build && npm publish"
|
|
16
17
|
},
|
|
17
18
|
"description": "Binbot charts is the default candlestick bars chart used in terminal.binbot.com to render bots graphically.",
|
|
18
19
|
"repository": {
|
package/src/App.jsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import './App.css';
|
|
3
3
|
import { TVChartContainer } from './components/TVChartContainer';
|
|
4
|
-
import { version } from './charting_library';
|
|
5
4
|
|
|
6
5
|
class App extends React.Component {
|
|
6
|
+
|
|
7
7
|
render() {
|
|
8
8
|
return (
|
|
9
9
|
<div className={ 'App' }>
|
|
10
|
-
<TVChartContainer />
|
|
10
|
+
<TVChartContainer symbol="symbol" interval="D"/>
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
);
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React, { Component } from "react";
|
|
2
2
|
import { widget } from "../charting_library";
|
|
3
|
+
import Datafeed from "./datafeed";
|
|
3
4
|
|
|
4
|
-
export class TVChartContainer extends
|
|
5
|
-
static defaultProps = {
|
|
6
|
-
symbol: "AAPL",
|
|
7
|
-
interval: "D",
|
|
8
|
-
datafeedUrl: "https://demo_feed.tradingview.com",
|
|
9
|
-
libraryPath: "/charting_library/",
|
|
10
|
-
chartsStorageApiVersion: "1.1",
|
|
11
|
-
fullscreen: false,
|
|
12
|
-
autosize: true,
|
|
13
|
-
studiesOverrides: {},
|
|
14
|
-
};
|
|
5
|
+
export default class TVChartContainer extends Component {
|
|
15
6
|
|
|
16
7
|
tvWidget = null;
|
|
17
8
|
|
|
@@ -22,26 +13,25 @@ export class TVChartContainer extends React.PureComponent {
|
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
componentDidMount() {
|
|
16
|
+
|
|
17
|
+
|
|
25
18
|
const widgetOptions = {
|
|
26
19
|
symbol: "AAPL",
|
|
27
20
|
// BEWARE: no trailing slash is expected in feed URL
|
|
28
|
-
datafeed:
|
|
29
|
-
|
|
30
|
-
),
|
|
31
|
-
interval: this.props.interval,
|
|
21
|
+
datafeed: Datafeed,
|
|
22
|
+
// interval: this.props.interval,
|
|
32
23
|
container: this.ref.current,
|
|
33
24
|
library_path: this.props.libraryPath,
|
|
34
|
-
|
|
35
25
|
locale: "en",
|
|
36
|
-
disabled_features: ["use_localstorage_for_settings"],
|
|
37
|
-
enabled_features: ["study_templates"],
|
|
38
|
-
charts_storage_url: this.props.chartsStorageUrl,
|
|
39
|
-
charts_storage_api_version: this.props.chartsStorageApiVersion,
|
|
40
|
-
client_id: this.props.clientId,
|
|
41
|
-
user_id: this.props.userId,
|
|
42
|
-
fullscreen:
|
|
43
|
-
autosize:
|
|
44
|
-
studies_overrides:
|
|
26
|
+
// disabled_features: ["use_localstorage_for_settings"],
|
|
27
|
+
// enabled_features: ["study_templates"],
|
|
28
|
+
// charts_storage_url: this.props.chartsStorageUrl,
|
|
29
|
+
// charts_storage_api_version: this.props.chartsStorageApiVersion,
|
|
30
|
+
// client_id: this.props.clientId,
|
|
31
|
+
// user_id: this.props.userId,
|
|
32
|
+
fullscreen: false,
|
|
33
|
+
autosize: false,
|
|
34
|
+
studies_overrides: {},
|
|
45
35
|
};
|
|
46
36
|
|
|
47
37
|
const tvWidget = new widget(widgetOptions);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { makeApiRequest, generateSymbol, parseFullSymbol } from "./helpers.js";
|
|
2
|
+
import { subscribeOnStream, unsubscribeFromStream } from "./streaming.js";
|
|
3
|
+
|
|
4
|
+
const lastBarsCache = new Map();
|
|
5
|
+
const exchange = "Binance";
|
|
6
|
+
const configurationData = {
|
|
7
|
+
supported_resolutions: ["1D", "1W", "1M"],
|
|
8
|
+
exchanges: [
|
|
9
|
+
{
|
|
10
|
+
value: "Binance",
|
|
11
|
+
name: "Binance",
|
|
12
|
+
desc: "Binance",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
symbols_types: [
|
|
16
|
+
{
|
|
17
|
+
name: "crypto",
|
|
18
|
+
|
|
19
|
+
// `symbolType` argument for the `searchSymbols` method, if a user selects this symbol type
|
|
20
|
+
value: "crypto",
|
|
21
|
+
},
|
|
22
|
+
// ...
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function getAllSymbols() {
|
|
27
|
+
const data = await makeApiRequest("data/v3/all/exchanges");
|
|
28
|
+
let allSymbols = [];
|
|
29
|
+
|
|
30
|
+
const pairs = data.Data[exchange].pairs;
|
|
31
|
+
for (const leftPairPart of Object.keys(pairs)) {
|
|
32
|
+
const symbols = pairs[leftPairPart].map((rightPairPart) => {
|
|
33
|
+
const symbol = generateSymbol(
|
|
34
|
+
exchange,
|
|
35
|
+
leftPairPart,
|
|
36
|
+
rightPairPart
|
|
37
|
+
);
|
|
38
|
+
return {
|
|
39
|
+
symbol: symbol.short,
|
|
40
|
+
full_name: symbol.full,
|
|
41
|
+
description: symbol.short,
|
|
42
|
+
exchange: exchange,
|
|
43
|
+
type: "crypto",
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
allSymbols = [...allSymbols, ...symbols];
|
|
47
|
+
}
|
|
48
|
+
return allSymbols;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default class Datafeed {
|
|
52
|
+
static onReady = (callback) => {
|
|
53
|
+
console.log("[onReady]: Method call");
|
|
54
|
+
setTimeout(() => callback(configurationData));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
static searchSymbols = async (
|
|
58
|
+
userInput,
|
|
59
|
+
exchange,
|
|
60
|
+
symbolType,
|
|
61
|
+
onResultReadyCallback
|
|
62
|
+
) => {
|
|
63
|
+
console.log("[searchSymbols]: Method call");
|
|
64
|
+
const symbols = await getAllSymbols();
|
|
65
|
+
const newSymbols = symbols.filter((symbol) => {
|
|
66
|
+
const isExchangeValid = exchange === "" || symbol.exchange === exchange;
|
|
67
|
+
const isFullSymbolContainsInput =
|
|
68
|
+
symbol.full_name.toLowerCase().indexOf(userInput.toLowerCase()) !== -1;
|
|
69
|
+
return isExchangeValid && isFullSymbolContainsInput;
|
|
70
|
+
});
|
|
71
|
+
onResultReadyCallback(newSymbols);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
static resolveSymbol = async (
|
|
75
|
+
symbolName,
|
|
76
|
+
onSymbolResolvedCallback,
|
|
77
|
+
onResolveErrorCallback
|
|
78
|
+
) => {
|
|
79
|
+
console.log("[resolveSymbol]: Method call", symbolName);
|
|
80
|
+
const symbols = await getAllSymbols();
|
|
81
|
+
const symbolItem = symbols.find(
|
|
82
|
+
({ full_name }) => full_name === symbolName
|
|
83
|
+
);
|
|
84
|
+
if (!symbolItem) {
|
|
85
|
+
console.log("[resolveSymbol]: Cannot resolve symbol", symbolName);
|
|
86
|
+
onResolveErrorCallback("cannot resolve symbol");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const symbolInfo = {
|
|
90
|
+
ticker: symbolItem.full_name,
|
|
91
|
+
name: symbolItem.symbol,
|
|
92
|
+
description: symbolItem.description,
|
|
93
|
+
type: symbolItem.type,
|
|
94
|
+
session: "24x7",
|
|
95
|
+
timezone: "Etc/UTC",
|
|
96
|
+
exchange: symbolItem.exchange,
|
|
97
|
+
minmov: 1,
|
|
98
|
+
pricescale: 100,
|
|
99
|
+
has_intraday: false,
|
|
100
|
+
has_no_volume: true,
|
|
101
|
+
has_weekly_and_monthly: false,
|
|
102
|
+
supported_resolutions: ["1D", "1W", "1M"],
|
|
103
|
+
volume_precision: 2,
|
|
104
|
+
data_status: "streaming",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
console.log("[resolveSymbol]: Symbol resolved", symbolName);
|
|
108
|
+
onSymbolResolvedCallback(symbolInfo);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
static getBars = async (
|
|
112
|
+
symbolInfo,
|
|
113
|
+
resolution,
|
|
114
|
+
periodParams,
|
|
115
|
+
onHistoryCallback,
|
|
116
|
+
onErrorCallback
|
|
117
|
+
) => {
|
|
118
|
+
const { from, to, firstDataRequest } = periodParams;
|
|
119
|
+
console.log("[getBars]: Method call", symbolInfo, resolution, from, to);
|
|
120
|
+
const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
|
|
121
|
+
const urlParameters = {
|
|
122
|
+
e: parsedSymbol.exchange,
|
|
123
|
+
fsym: parsedSymbol.fromSymbol,
|
|
124
|
+
tsym: parsedSymbol.toSymbol,
|
|
125
|
+
toTs: to,
|
|
126
|
+
limit: 2000,
|
|
127
|
+
};
|
|
128
|
+
const query = Object.keys(urlParameters)
|
|
129
|
+
.map((name) => `${name}=${encodeURIComponent(urlParameters[name])}`)
|
|
130
|
+
.join("&");
|
|
131
|
+
try {
|
|
132
|
+
const data = await makeApiRequest(`data/histoday?${query}`);
|
|
133
|
+
if (
|
|
134
|
+
(data.Response && data.Response === "Error") ||
|
|
135
|
+
data.Data.length === 0
|
|
136
|
+
) {
|
|
137
|
+
// "noData" should be set if there is no data in the requested period.
|
|
138
|
+
onHistoryCallback([], {
|
|
139
|
+
noData: true,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let bars = [];
|
|
144
|
+
data.Data.forEach((bar) => {
|
|
145
|
+
if (bar.time >= from && bar.time < to) {
|
|
146
|
+
bars = [
|
|
147
|
+
...bars,
|
|
148
|
+
{
|
|
149
|
+
time: bar.time * 1000,
|
|
150
|
+
low: bar.low,
|
|
151
|
+
high: bar.high,
|
|
152
|
+
open: bar.open,
|
|
153
|
+
close: bar.close,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
if (firstDataRequest) {
|
|
159
|
+
lastBarsCache.set(symbolInfo.full_name, {
|
|
160
|
+
...bars[bars.length - 1],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
console.log(`[getBars]: returned ${bars.length} bar(s)`);
|
|
164
|
+
onHistoryCallback(bars, {
|
|
165
|
+
noData: false,
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.log("[getBars]: Get error", error);
|
|
169
|
+
onErrorCallback(error);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
static subscribeBars = (
|
|
174
|
+
symbolInfo,
|
|
175
|
+
resolution,
|
|
176
|
+
onRealtimeCallback,
|
|
177
|
+
subscribeUID,
|
|
178
|
+
onResetCacheNeededCallback
|
|
179
|
+
) => {
|
|
180
|
+
console.log(
|
|
181
|
+
"[subscribeBars]: Method call with subscribeUID:",
|
|
182
|
+
subscribeUID
|
|
183
|
+
);
|
|
184
|
+
subscribeOnStream(
|
|
185
|
+
symbolInfo,
|
|
186
|
+
resolution,
|
|
187
|
+
onRealtimeCallback,
|
|
188
|
+
subscribeUID,
|
|
189
|
+
onResetCacheNeededCallback,
|
|
190
|
+
lastBarsCache.get(symbolInfo.full_name)
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
static unsubscribeBars = (subscriberUID) => {
|
|
195
|
+
console.log(
|
|
196
|
+
"[unsubscribeBars]: Method call with subscriberUID:",
|
|
197
|
+
subscriberUID
|
|
198
|
+
);
|
|
199
|
+
unsubscribeFromStream(subscriberUID);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Make requests to CryptoCompare API
|
|
2
|
+
export async function makeApiRequest(path) {
|
|
3
|
+
try {
|
|
4
|
+
const response = await fetch(`https://min-api.cryptocompare.com/${path}`);
|
|
5
|
+
return response.json();
|
|
6
|
+
} catch (error) {
|
|
7
|
+
throw new Error(`CryptoCompare request error: ${error.status}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Generate a symbol ID from a pair of the coins
|
|
12
|
+
export function generateSymbol(exchange, fromSymbol, toSymbol) {
|
|
13
|
+
const short = `${fromSymbol}/${toSymbol}`;
|
|
14
|
+
return {
|
|
15
|
+
short,
|
|
16
|
+
full: `${exchange}:${short}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseFullSymbol(fullSymbol) {
|
|
21
|
+
const match = fullSymbol.match(/^(\w+):(\w+)\/(\w+)$/);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
exchange: match[1],
|
|
28
|
+
fromSymbol: match[2],
|
|
29
|
+
toSymbol: match[3],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { parseFullSymbol } from "./helpers.js";
|
|
2
|
+
import io from 'socket.io-client';
|
|
3
|
+
|
|
4
|
+
const socket = io("wss://streamer.cryptocompare.com");
|
|
5
|
+
const channelToSubscription = new Map();
|
|
6
|
+
|
|
7
|
+
socket.on("connect", () => {
|
|
8
|
+
console.log("[socket] Connected");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
socket.on("disconnect", (reason) => {
|
|
12
|
+
console.log("[socket] Disconnected:", reason);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
socket.on("error", (error) => {
|
|
16
|
+
console.log("[socket] Error:", error);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
socket.on("m", (data) => {
|
|
20
|
+
console.log("[socket] Message:", data);
|
|
21
|
+
const [
|
|
22
|
+
eventTypeStr,
|
|
23
|
+
exchange,
|
|
24
|
+
fromSymbol,
|
|
25
|
+
toSymbol,
|
|
26
|
+
,
|
|
27
|
+
,
|
|
28
|
+
tradeTimeStr,
|
|
29
|
+
,
|
|
30
|
+
tradePriceStr,
|
|
31
|
+
] = data.split("~");
|
|
32
|
+
|
|
33
|
+
if (parseInt(eventTypeStr) !== 0) {
|
|
34
|
+
// skip all non-TRADE events
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const tradePrice = parseFloat(tradePriceStr);
|
|
38
|
+
const tradeTime = parseInt(tradeTimeStr);
|
|
39
|
+
const channelString = `0~${exchange}~${fromSymbol}~${toSymbol}`;
|
|
40
|
+
const subscriptionItem = channelToSubscription.get(channelString);
|
|
41
|
+
if (subscriptionItem === undefined) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const lastDailyBar = subscriptionItem.lastDailyBar;
|
|
45
|
+
const nextDailyBarTime = getNextDailyBarTime(lastDailyBar.time);
|
|
46
|
+
|
|
47
|
+
let bar;
|
|
48
|
+
if (tradeTime >= nextDailyBarTime) {
|
|
49
|
+
bar = {
|
|
50
|
+
time: nextDailyBarTime,
|
|
51
|
+
open: tradePrice,
|
|
52
|
+
high: tradePrice,
|
|
53
|
+
low: tradePrice,
|
|
54
|
+
close: tradePrice,
|
|
55
|
+
};
|
|
56
|
+
console.log("[socket] Generate new bar", bar);
|
|
57
|
+
} else {
|
|
58
|
+
bar = {
|
|
59
|
+
...lastDailyBar,
|
|
60
|
+
high: Math.max(lastDailyBar.high, tradePrice),
|
|
61
|
+
low: Math.min(lastDailyBar.low, tradePrice),
|
|
62
|
+
close: tradePrice,
|
|
63
|
+
};
|
|
64
|
+
console.log("[socket] Update the latest bar by price", tradePrice);
|
|
65
|
+
}
|
|
66
|
+
subscriptionItem.lastDailyBar = bar;
|
|
67
|
+
|
|
68
|
+
// send data to every subscriber of that symbol
|
|
69
|
+
subscriptionItem.handlers.forEach((handler) => handler.callback(bar));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function getNextDailyBarTime(barTime) {
|
|
73
|
+
const date = new Date(barTime * 1000);
|
|
74
|
+
date.setDate(date.getDate() + 1);
|
|
75
|
+
return date.getTime() / 1000;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function subscribeOnStream(
|
|
79
|
+
symbolInfo,
|
|
80
|
+
resolution,
|
|
81
|
+
onRealtimeCallback,
|
|
82
|
+
subscribeUID,
|
|
83
|
+
onResetCacheNeededCallback,
|
|
84
|
+
lastDailyBar
|
|
85
|
+
) {
|
|
86
|
+
const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
|
|
87
|
+
const channelString = `0~${parsedSymbol.exchange}~${parsedSymbol.fromSymbol}~${parsedSymbol.toSymbol}`;
|
|
88
|
+
const handler = {
|
|
89
|
+
id: subscribeUID,
|
|
90
|
+
callback: onRealtimeCallback,
|
|
91
|
+
};
|
|
92
|
+
let subscriptionItem = channelToSubscription.get(channelString);
|
|
93
|
+
if (subscriptionItem) {
|
|
94
|
+
// already subscribed to the channel, use the existing subscription
|
|
95
|
+
subscriptionItem.handlers.push(handler);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
subscriptionItem = {
|
|
99
|
+
subscribeUID,
|
|
100
|
+
resolution,
|
|
101
|
+
lastDailyBar,
|
|
102
|
+
handlers: [handler],
|
|
103
|
+
};
|
|
104
|
+
channelToSubscription.set(channelString, subscriptionItem);
|
|
105
|
+
console.log(
|
|
106
|
+
"[subscribeBars]: Subscribe to streaming. Channel:",
|
|
107
|
+
channelString
|
|
108
|
+
);
|
|
109
|
+
socket.emit("SubAdd", { subs: [channelString] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function unsubscribeFromStream(subscriberUID) {
|
|
113
|
+
// find a subscription with id === subscriberUID
|
|
114
|
+
for (const channelString of channelToSubscription.keys()) {
|
|
115
|
+
const subscriptionItem = channelToSubscription.get(channelString);
|
|
116
|
+
const handlerIndex = subscriptionItem.handlers.findIndex(
|
|
117
|
+
(handler) => handler.id === subscriberUID
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (handlerIndex !== -1) {
|
|
121
|
+
// remove from handlers
|
|
122
|
+
subscriptionItem.handlers.splice(handlerIndex, 1);
|
|
123
|
+
|
|
124
|
+
if (subscriptionItem.handlers.length === 0) {
|
|
125
|
+
// unsubscribe from the channel, if it was the last handler
|
|
126
|
+
console.log(
|
|
127
|
+
"[unsubscribeBars]: Unsubscribe from streaming. Channel:",
|
|
128
|
+
channelString
|
|
129
|
+
);
|
|
130
|
+
socket.emit("SubRemove", { subs: [channelString] });
|
|
131
|
+
channelToSubscription.delete(channelString);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/yarn.lock
DELETED