binbot-charts 0.3.0 → 0.3.2
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.
|
@@ -4,26 +4,17 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = TVChartContainer;
|
|
7
|
-
|
|
8
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
9
|
-
|
|
10
8
|
var _charting_library = require("../charting_library");
|
|
11
|
-
|
|
12
9
|
var _datafeed = _interopRequireDefault(require("./datafeed"));
|
|
13
|
-
|
|
14
10
|
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
15
|
-
|
|
16
11
|
var _useImmer = require("use-immer");
|
|
17
|
-
|
|
18
12
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
19
|
-
|
|
20
13
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
21
|
-
|
|
22
14
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
23
|
-
|
|
24
15
|
function TVChartContainer(_ref) {
|
|
25
16
|
let {
|
|
26
|
-
symbol = "
|
|
17
|
+
symbol = "BTCUSDT",
|
|
27
18
|
interval = "1h",
|
|
28
19
|
libraryPath = "/charting_library/",
|
|
29
20
|
timescaleMarks = [],
|
|
@@ -39,27 +30,23 @@ function TVChartContainer(_ref) {
|
|
|
39
30
|
const prevTimescaleMarks = (0, _react.useRef)(timescaleMarks);
|
|
40
31
|
(0, _react.useEffect)(() => {
|
|
41
32
|
if (!widgetState) {
|
|
42
|
-
initializeChart();
|
|
33
|
+
initializeChart("1h");
|
|
43
34
|
}
|
|
44
|
-
|
|
45
35
|
if (orderLines && orderLines.length > 0) {
|
|
46
36
|
updateOrderLines(orderLines);
|
|
47
37
|
}
|
|
48
|
-
|
|
49
38
|
if (widgetState && symbol !== symbolState) {
|
|
50
39
|
widgetState.setSymbol(symbol, interval);
|
|
51
40
|
}
|
|
52
|
-
|
|
53
41
|
if (widgetState && prevTimescaleMarks.current && timescaleMarks !== prevTimescaleMarks.current) {
|
|
54
42
|
widgetState._options.datafeed.timescaleMarks = timescaleMarks;
|
|
55
43
|
prevTimescaleMarks.current = timescaleMarks;
|
|
56
44
|
}
|
|
57
45
|
}, [orderLines, timescaleMarks]);
|
|
58
|
-
|
|
59
|
-
const initializeChart = () => {
|
|
46
|
+
const initializeChart = interval => {
|
|
60
47
|
const widgetOptions = {
|
|
61
48
|
symbol: symbol,
|
|
62
|
-
datafeed: new _datafeed.default(timescaleMarks),
|
|
49
|
+
datafeed: new _datafeed.default(timescaleMarks, interval),
|
|
63
50
|
interval: interval,
|
|
64
51
|
container: containerRef.current,
|
|
65
52
|
library_path: libraryPath,
|
|
@@ -76,8 +63,9 @@ function TVChartContainer(_ref) {
|
|
|
76
63
|
const tvWidget = new _charting_library.widget(widgetOptions);
|
|
77
64
|
tvWidget.onChartReady(() => {
|
|
78
65
|
tvWidget.subscribe("onTick", event => onTick(event));
|
|
79
|
-
setWidgetState(tvWidget);
|
|
66
|
+
setWidgetState(tvWidget);
|
|
80
67
|
|
|
68
|
+
// get latest bar for last price
|
|
81
69
|
const prices = async () => {
|
|
82
70
|
const data = await tvWidget.activeChart().exportData({
|
|
83
71
|
includeTime: false,
|
|
@@ -86,11 +74,9 @@ function TVChartContainer(_ref) {
|
|
|
86
74
|
});
|
|
87
75
|
getLatestBar(data.data[data.data.length - 1]);
|
|
88
76
|
};
|
|
89
|
-
|
|
90
77
|
prices();
|
|
91
78
|
});
|
|
92
79
|
};
|
|
93
|
-
|
|
94
80
|
const updateOrderLines = orderLines => {
|
|
95
81
|
if (chartOrderLines && chartOrderLines.length > 0) {
|
|
96
82
|
chartOrderLines.forEach(item => {
|
|
@@ -104,8 +90,9 @@ function TVChartContainer(_ref) {
|
|
|
104
90
|
if (widgetState && orderLines && orderLines.length > 0) {
|
|
105
91
|
orderLines.forEach(order => {
|
|
106
92
|
const lineStyle = order.lineStyle || 0;
|
|
107
|
-
let chartOrderLine = widgetState.chart().createOrderLine().setText(order.text).setTooltip(order.tooltip).setQuantity(order.quantity).setQuantityFont("inherit 14px Arial").setQuantityBackgroundColor(order.color).setQuantityBorderColor(order.color).setLineStyle(lineStyle).setLineLength(25).setLineColor(order.color).setBodyFont("inherit 14px Arial").setBodyBorderColor(order.color).setBodyTextColor(order.color).setPrice(order.price);
|
|
93
|
+
let chartOrderLine = widgetState.chart().createOrderLine().setText(order.text).setTooltip(order.tooltip).setQuantity(order.quantity).setQuantityFont("inherit 14px Arial").setQuantityBackgroundColor(order.color).setQuantityBorderColor(order.color).setLineStyle(lineStyle).setLineLength(25).setLineColor(order.color).setBodyFont("inherit 14px Arial").setBodyBorderColor(order.color).setBodyTextColor(order.color).setPrice(order.price);
|
|
108
94
|
|
|
95
|
+
// set custom id easier search
|
|
109
96
|
chartOrderLine.id = order.id;
|
|
110
97
|
setChartOrderLines(draft => {
|
|
111
98
|
draft.push(chartOrderLine);
|
|
@@ -115,7 +102,6 @@ function TVChartContainer(_ref) {
|
|
|
115
102
|
}
|
|
116
103
|
}
|
|
117
104
|
};
|
|
118
|
-
|
|
119
105
|
return /*#__PURE__*/_react.default.createElement("div", {
|
|
120
106
|
ref: containerRef,
|
|
121
107
|
style: {
|
|
@@ -123,7 +109,6 @@ function TVChartContainer(_ref) {
|
|
|
123
109
|
}
|
|
124
110
|
});
|
|
125
111
|
}
|
|
126
|
-
|
|
127
112
|
TVChartContainer.propTypes = {
|
|
128
113
|
apiKey: _propTypes.default.string,
|
|
129
114
|
symbol: _propTypes.default.string,
|
|
@@ -4,15 +4,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("./helpers.js");
|
|
9
|
-
|
|
10
8
|
var _streaming = require("./streaming.js");
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
|
|
9
|
+
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
10
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
11
|
+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
14
12
|
const binanceResolutions = ["1s", "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"];
|
|
15
|
-
|
|
16
13
|
const getConfigurationData = async () => {
|
|
17
14
|
return {
|
|
18
15
|
supports_marks: true,
|
|
@@ -32,26 +29,27 @@ const getConfigurationData = async () => {
|
|
|
32
29
|
};
|
|
33
30
|
};
|
|
34
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @param timescale { Array }. timescaleMark objects
|
|
34
|
+
* @param interval { string }. Klines timescale from the list of Binance Enums
|
|
35
|
+
*/
|
|
35
36
|
class Datafeed {
|
|
36
37
|
constructor() {
|
|
37
38
|
let timescaleMarks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
38
|
-
|
|
39
|
+
let _interval = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "1h";
|
|
39
40
|
_defineProperty(this, "onReady", async callback => {
|
|
40
41
|
this.configurationData = await getConfigurationData();
|
|
41
42
|
callback(this.configurationData);
|
|
42
43
|
});
|
|
43
|
-
|
|
44
44
|
_defineProperty(this, "searchSymbols", async (userInput, exchange, symbolType, onResultReadyCallback) => {
|
|
45
45
|
const symbols = await (0, _helpers.getAllSymbols)(userInput);
|
|
46
46
|
onResultReadyCallback(symbols);
|
|
47
47
|
});
|
|
48
|
-
|
|
49
48
|
_defineProperty(this, "resolveSymbol", async (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => {
|
|
50
49
|
if (!symbolName) {
|
|
51
50
|
await onResolveErrorCallback("cannot resolve symbol");
|
|
52
51
|
return;
|
|
53
52
|
}
|
|
54
|
-
|
|
55
53
|
const symbolInfo = () => {
|
|
56
54
|
return {
|
|
57
55
|
ticker: symbolName,
|
|
@@ -75,11 +73,9 @@ class Datafeed {
|
|
|
75
73
|
resolution: "1h"
|
|
76
74
|
};
|
|
77
75
|
};
|
|
78
|
-
|
|
79
76
|
const symbol = await symbolInfo();
|
|
80
77
|
onSymbolResolvedCallback(symbol);
|
|
81
78
|
});
|
|
82
|
-
|
|
83
79
|
_defineProperty(this, "getBars", async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
|
|
84
80
|
const {
|
|
85
81
|
from,
|
|
@@ -88,7 +84,6 @@ class Datafeed {
|
|
|
88
84
|
} = periodParams;
|
|
89
85
|
let interval = "60"; // 1 hour
|
|
90
86
|
// Calculate interval using resolution data
|
|
91
|
-
|
|
92
87
|
if (!/[a-zA-Z]$/.test(resolution)) {
|
|
93
88
|
if (parseInt(resolution) >= 60) {
|
|
94
89
|
interval = parseInt(resolution) / 60 + "h";
|
|
@@ -98,7 +93,6 @@ class Datafeed {
|
|
|
98
93
|
} else {
|
|
99
94
|
interval = resolution.toLowerCase().replace(/[a-z]\b/g, c => c.toLowerCase());
|
|
100
95
|
}
|
|
101
|
-
|
|
102
96
|
let urlParameters = {
|
|
103
97
|
symbol: symbolInfo.name,
|
|
104
98
|
interval: interval,
|
|
@@ -107,10 +101,8 @@ class Datafeed {
|
|
|
107
101
|
limit: 600
|
|
108
102
|
};
|
|
109
103
|
const query = Object.keys(urlParameters).map(name => `${name}=${encodeURIComponent(urlParameters[name])}`).join("&");
|
|
110
|
-
|
|
111
104
|
try {
|
|
112
105
|
const data = await (0, _helpers.makeApiRequest)(`api/v3/uiKlines?${query}`);
|
|
113
|
-
|
|
114
106
|
if (data.Response && data.Response === "Error" || data.length === 0) {
|
|
115
107
|
// "noData" should be set if there is no data in the requested period.
|
|
116
108
|
onHistoryCallback([], {
|
|
@@ -118,7 +110,6 @@ class Datafeed {
|
|
|
118
110
|
});
|
|
119
111
|
return;
|
|
120
112
|
}
|
|
121
|
-
|
|
122
113
|
let bars = [];
|
|
123
114
|
data.forEach(bar => {
|
|
124
115
|
if (bar[0] >= from * 1000 && bar[0] < to * 1000) {
|
|
@@ -140,32 +131,26 @@ class Datafeed {
|
|
|
140
131
|
onErrorCallback(error);
|
|
141
132
|
}
|
|
142
133
|
});
|
|
143
|
-
|
|
144
134
|
_defineProperty(this, "subscribeBars", (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {
|
|
145
135
|
(0, _streaming.subscribeOnStream)(symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback, this.interval);
|
|
146
136
|
});
|
|
147
|
-
|
|
148
137
|
_defineProperty(this, "unsubscribeBars", subscriberUID => {
|
|
149
138
|
(0, _streaming.unsubscribeFromStream)(subscriberUID);
|
|
150
139
|
});
|
|
151
|
-
|
|
152
140
|
this.streaming = null;
|
|
153
141
|
this.timescaleMarks = timescaleMarks;
|
|
142
|
+
this.interval = _interval;
|
|
154
143
|
}
|
|
155
|
-
|
|
156
144
|
getTimescaleMarks(symbolInfo, from, to, onDataCallback, resolution) {
|
|
157
145
|
if (this.timescaleMarks.length > 0) {
|
|
158
146
|
let timescaleMarks = Object.assign([], this.timescaleMarks);
|
|
159
147
|
onDataCallback(timescaleMarks);
|
|
160
148
|
}
|
|
161
149
|
}
|
|
162
|
-
|
|
163
150
|
async getServerTime(onServertimeCallback) {
|
|
164
151
|
const data = await (0, _helpers.makeApiRequest)(`api/v3/time`);
|
|
165
152
|
const serverTime = data.serverTime / 1000;
|
|
166
153
|
onServertimeCallback(serverTime);
|
|
167
154
|
}
|
|
168
|
-
|
|
169
155
|
}
|
|
170
|
-
|
|
171
156
|
exports.default = Datafeed;
|
|
@@ -6,9 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.getAllSymbols = getAllSymbols;
|
|
7
7
|
exports.makeApiRequest = makeApiRequest;
|
|
8
8
|
exports.usePrevious = usePrevious;
|
|
9
|
-
|
|
10
9
|
var _react = require("react");
|
|
11
|
-
|
|
12
10
|
async function makeApiRequest(path) {
|
|
13
11
|
try {
|
|
14
12
|
const response = await fetch(`https://api.binance.com/${path}`);
|
|
@@ -17,10 +15,8 @@ async function makeApiRequest(path) {
|
|
|
17
15
|
throw new Error(`Binance request error: ${error.status}`);
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
|
-
|
|
21
18
|
async function getAllSymbols(symbol) {
|
|
22
19
|
let newSymbols = [];
|
|
23
|
-
|
|
24
20
|
try {
|
|
25
21
|
const data = await makeApiRequest(`api/v3/exchangeInfo?symbol=${symbol.toUpperCase()}`);
|
|
26
22
|
data.symbols.forEach(item => {
|
|
@@ -38,10 +34,8 @@ async function getAllSymbols(symbol) {
|
|
|
38
34
|
} catch (e) {
|
|
39
35
|
return newSymbols;
|
|
40
36
|
}
|
|
41
|
-
|
|
42
37
|
return newSymbols;
|
|
43
38
|
}
|
|
44
|
-
|
|
45
39
|
function usePrevious(value) {
|
|
46
40
|
const ref = (0, _react.useRef)();
|
|
47
41
|
(0, _react.useEffect)(() => {
|
package/dist/components/index.js
CHANGED
|
@@ -9,7 +9,5 @@ Object.defineProperty(exports, "TVChartContainer", {
|
|
|
9
9
|
return _TVChartContainer.default;
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
|
-
|
|
13
12
|
var _TVChartContainer = _interopRequireDefault(require("./TVChartContainer"));
|
|
14
|
-
|
|
15
13
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
@@ -6,32 +6,25 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.subscribeOnStream = subscribeOnStream;
|
|
7
7
|
exports.unsubscribeFromStream = unsubscribeFromStream;
|
|
8
8
|
const channelToSubscription = new Map();
|
|
9
|
-
|
|
10
9
|
function setupSockets(subRequest) {
|
|
11
10
|
const socket = new WebSocket("wss://stream.binance.com:9443/ws");
|
|
12
11
|
window.socket = socket;
|
|
13
|
-
|
|
14
12
|
socket.onopen = event => {
|
|
15
13
|
console.log("[socket] Connected");
|
|
16
14
|
socket.send(JSON.stringify(subRequest));
|
|
17
15
|
};
|
|
18
|
-
|
|
19
16
|
socket.onclose = reason => {
|
|
20
17
|
console.log("[socket] Disconnected:", reason);
|
|
21
18
|
};
|
|
22
|
-
|
|
23
19
|
socket.onerror = error => {
|
|
24
20
|
console.log("[socket] Error:", error);
|
|
25
21
|
};
|
|
26
|
-
|
|
27
22
|
socket.onmessage = e => {
|
|
28
23
|
const data = JSON.parse(e.data);
|
|
29
|
-
|
|
30
24
|
if (data.e == undefined) {
|
|
31
25
|
// skip all non-TRADE events
|
|
32
26
|
return;
|
|
33
27
|
}
|
|
34
|
-
|
|
35
28
|
const {
|
|
36
29
|
s: symbol,
|
|
37
30
|
t: startTime,
|
|
@@ -47,11 +40,9 @@ function setupSockets(subRequest) {
|
|
|
47
40
|
} = data.k;
|
|
48
41
|
const channelString = `${symbol.toLowerCase()}@kline_${interval}`;
|
|
49
42
|
const subscriptionItem = channelToSubscription.get(channelString);
|
|
50
|
-
|
|
51
43
|
if (subscriptionItem === undefined) {
|
|
52
44
|
return;
|
|
53
45
|
}
|
|
54
|
-
|
|
55
46
|
const bar = {
|
|
56
47
|
time: startTime,
|
|
57
48
|
open: open,
|
|
@@ -59,12 +50,11 @@ function setupSockets(subRequest) {
|
|
|
59
50
|
low: low,
|
|
60
51
|
close: close,
|
|
61
52
|
volume: volume
|
|
62
|
-
};
|
|
63
|
-
|
|
53
|
+
};
|
|
54
|
+
// send data to every subscriber of that symbol
|
|
64
55
|
subscriptionItem.handlers.forEach(handler => handler.callback(bar));
|
|
65
56
|
};
|
|
66
57
|
}
|
|
67
|
-
|
|
68
58
|
function subscribeOnStream(symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback, interval) {
|
|
69
59
|
const channelString = `${symbolInfo.name.toLowerCase()}@kline_${interval}`;
|
|
70
60
|
const handler = {
|
|
@@ -72,13 +62,11 @@ function subscribeOnStream(symbolInfo, resolution, onRealtimeCallback, subscribe
|
|
|
72
62
|
callback: onRealtimeCallback
|
|
73
63
|
};
|
|
74
64
|
let subscriptionItem = channelToSubscription.get(channelString);
|
|
75
|
-
|
|
76
65
|
if (subscriptionItem) {
|
|
77
66
|
// already subscribed to the channel, use the existing subscription
|
|
78
67
|
subscriptionItem.handlers.push(handler);
|
|
79
68
|
return;
|
|
80
69
|
}
|
|
81
|
-
|
|
82
70
|
subscriptionItem = {
|
|
83
71
|
subscribeUID,
|
|
84
72
|
resolution,
|
|
@@ -92,17 +80,14 @@ function subscribeOnStream(symbolInfo, resolution, onRealtimeCallback, subscribe
|
|
|
92
80
|
channelToSubscription.set(channelString, subscriptionItem);
|
|
93
81
|
setupSockets(subRequest);
|
|
94
82
|
}
|
|
95
|
-
|
|
96
83
|
function unsubscribeFromStream(subscriberUID) {
|
|
97
84
|
// find a subscription with id === subscriberUID
|
|
98
85
|
for (const channelString of channelToSubscription.keys()) {
|
|
99
86
|
const subscriptionItem = channelToSubscription.get(channelString);
|
|
100
87
|
const handlerIndex = subscriptionItem.handlers.findIndex(handler => handler.id === subscriberUID);
|
|
101
|
-
|
|
102
88
|
if (handlerIndex !== -1) {
|
|
103
89
|
// remove from handlers
|
|
104
90
|
subscriptionItem.handlers.splice(handlerIndex, 1);
|
|
105
|
-
|
|
106
91
|
if (subscriptionItem.handlers.length === 0) {
|
|
107
92
|
// unsubscribe from the channel, if it was the last handler
|
|
108
93
|
console.log("[unsubscribeBars]: Unsubscribe from streaming. Channel:", channelString);
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.3.
|
|
2
|
+
"version": "0.3.2",
|
|
3
3
|
"name": "binbot-charts",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"react": "^17.0.1",
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
"@babel/core": "^7.18.13",
|
|
12
12
|
"@babel/polyfill": "^7.12.1",
|
|
13
13
|
"@babel/preset-env": "^7.19.0",
|
|
14
|
-
"react
|
|
14
|
+
"@types/react": "^18.0.26",
|
|
15
|
+
"@types/react-dom": "^18.0.10",
|
|
16
|
+
"immer": "^9.0.17",
|
|
17
|
+
"react-scripts": "^5.0.1",
|
|
18
|
+
"typescript": "^4.9.4"
|
|
15
19
|
},
|
|
16
20
|
"scripts": {
|
|
17
21
|
"start": "react-scripts start",
|
|
18
22
|
"build": "rm -rf dist && NODE_ENV=production babel -d dist/components src/components --extensions \".js,.jsx\" && cp -r src/charting_library dist/charting_library",
|
|
19
23
|
"release": "yarn build && yarn publish"
|
|
20
24
|
},
|
|
21
|
-
"description": "Binbot charts is the default candlestick bars chart used in terminal.binbot.
|
|
25
|
+
"description": "Binbot charts is the default candlestick bars chart used in terminal.binbot.in to render bots graphically.",
|
|
22
26
|
"repository": {
|
|
23
27
|
"type": "git",
|
|
24
28
|
"url": "git+https://github.com/carkod/binbot-charts.git"
|