@tastytrade/api 2.0.0 → 2.1.0
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/dist/market-data-streamer.d.ts +41 -2
- package/dist/market-data-streamer.js +106 -14
- package/dist/tastytrade-api.d.ts +2 -2
- package/dist/tastytrade-api.js +2 -1
- package/lib/market-data-streamer.ts +112 -16
- package/lib/tastytrade-api.ts +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare enum MarketDataSubscriptionType {
|
|
2
|
+
Candle = "Candle",
|
|
2
3
|
Quote = "Quote",
|
|
3
4
|
Trade = "Trade",
|
|
4
5
|
Summary = "Summary",
|
|
@@ -6,7 +7,28 @@ export declare enum MarketDataSubscriptionType {
|
|
|
6
7
|
Greeks = "Greeks",
|
|
7
8
|
Underlying = "Underlying"
|
|
8
9
|
}
|
|
10
|
+
export declare enum CandleType {
|
|
11
|
+
Tick = "t",
|
|
12
|
+
Second = "s",
|
|
13
|
+
Minute = "m",
|
|
14
|
+
Hour = "h",
|
|
15
|
+
Day = "d",
|
|
16
|
+
Week = "w",
|
|
17
|
+
Month = "mo",
|
|
18
|
+
ThirdFriday = "o",
|
|
19
|
+
Year = "y",
|
|
20
|
+
Volume = "v",
|
|
21
|
+
Price = "p"
|
|
22
|
+
}
|
|
9
23
|
export declare type MarketDataListener = (data: any) => void;
|
|
24
|
+
export declare type ErrorListener = (error: any) => void;
|
|
25
|
+
export declare type AuthStateListener = (isAuthorized: boolean) => void;
|
|
26
|
+
export declare type CandleSubscriptionOptions = {
|
|
27
|
+
period: number;
|
|
28
|
+
type: CandleType;
|
|
29
|
+
channelId: number;
|
|
30
|
+
};
|
|
31
|
+
declare type Remover = () => void;
|
|
10
32
|
export default class MarketDataStreamer {
|
|
11
33
|
private webSocket;
|
|
12
34
|
private token;
|
|
@@ -15,13 +37,27 @@ export default class MarketDataStreamer {
|
|
|
15
37
|
private openChannels;
|
|
16
38
|
private subscriptionsQueue;
|
|
17
39
|
private authState;
|
|
18
|
-
|
|
40
|
+
private errorListeners;
|
|
41
|
+
private authStateListeners;
|
|
42
|
+
addDataListener(dataListener: MarketDataListener, channelId?: number | null): Remover;
|
|
43
|
+
addErrorListener(errorListener: ErrorListener): Remover;
|
|
44
|
+
addAuthStateChangeListener(authStateListener: AuthStateListener): Remover;
|
|
19
45
|
connect(url: string, token: string): void;
|
|
20
46
|
disconnect(): void;
|
|
21
47
|
addSubscription(symbol: string, options?: {
|
|
22
48
|
subscriptionTypes: MarketDataSubscriptionType[];
|
|
23
49
|
channelId: number;
|
|
24
|
-
}):
|
|
50
|
+
}): Remover;
|
|
51
|
+
/**
|
|
52
|
+
* Adds a candle subscription (historical data)
|
|
53
|
+
* @param streamerSymbol Get this from an instrument's streamer-symbol json response field
|
|
54
|
+
* @param fromTime Epoch timestamp from where you want to start
|
|
55
|
+
* @param options Period and Type are the grouping you want to apply to the candle data
|
|
56
|
+
* For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
|
|
57
|
+
* From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
|
|
58
|
+
* @returns
|
|
59
|
+
*/
|
|
60
|
+
addCandleSubscription(streamerSymbol: string, fromTime: number, options: CandleSubscriptionOptions): () => void;
|
|
25
61
|
removeSubscription(symbol: string, options?: {
|
|
26
62
|
subscriptionTypes: MarketDataSubscriptionType[];
|
|
27
63
|
channelId: number;
|
|
@@ -29,6 +65,7 @@ export default class MarketDataStreamer {
|
|
|
29
65
|
removeAllSubscriptions(channelId?: number): void;
|
|
30
66
|
openFeedChannel(channelId: number): void;
|
|
31
67
|
isChannelOpened(channelId: number): boolean;
|
|
68
|
+
get isReadyToOpenChannels(): boolean;
|
|
32
69
|
get isConnected(): boolean;
|
|
33
70
|
private scheduleKeepalive;
|
|
34
71
|
private sendKeepalive;
|
|
@@ -51,6 +88,8 @@ export default class MarketDataStreamer {
|
|
|
51
88
|
private handleAuthStateMessage;
|
|
52
89
|
private handleChannelOpened;
|
|
53
90
|
private notifyListeners;
|
|
91
|
+
private notifyErrorListeners;
|
|
54
92
|
private handleMessageReceived;
|
|
55
93
|
private sendMessage;
|
|
56
94
|
}
|
|
95
|
+
export {};
|
|
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MarketDataSubscriptionType = void 0;
|
|
6
|
+
exports.CandleType = exports.MarketDataSubscriptionType = void 0;
|
|
7
7
|
var isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
|
|
8
8
|
var lodash_1 = __importDefault(require("lodash"));
|
|
9
9
|
var uuid_1 = require("uuid");
|
|
10
10
|
var MarketDataSubscriptionType;
|
|
11
11
|
(function (MarketDataSubscriptionType) {
|
|
12
|
+
MarketDataSubscriptionType["Candle"] = "Candle";
|
|
12
13
|
MarketDataSubscriptionType["Quote"] = "Quote";
|
|
13
14
|
MarketDataSubscriptionType["Trade"] = "Trade";
|
|
14
15
|
MarketDataSubscriptionType["Summary"] = "Summary";
|
|
@@ -16,6 +17,21 @@ var MarketDataSubscriptionType;
|
|
|
16
17
|
MarketDataSubscriptionType["Greeks"] = "Greeks";
|
|
17
18
|
MarketDataSubscriptionType["Underlying"] = "Underlying";
|
|
18
19
|
})(MarketDataSubscriptionType = exports.MarketDataSubscriptionType || (exports.MarketDataSubscriptionType = {}));
|
|
20
|
+
var CandleType;
|
|
21
|
+
(function (CandleType) {
|
|
22
|
+
CandleType["Tick"] = "t";
|
|
23
|
+
CandleType["Second"] = "s";
|
|
24
|
+
CandleType["Minute"] = "m";
|
|
25
|
+
CandleType["Hour"] = "h";
|
|
26
|
+
CandleType["Day"] = "d";
|
|
27
|
+
CandleType["Week"] = "w";
|
|
28
|
+
CandleType["Month"] = "mo";
|
|
29
|
+
CandleType["ThirdFriday"] = "o";
|
|
30
|
+
CandleType["Year"] = "y";
|
|
31
|
+
CandleType["Volume"] = "v";
|
|
32
|
+
CandleType["Price"] = "p";
|
|
33
|
+
})(CandleType = exports.CandleType || (exports.CandleType = {}));
|
|
34
|
+
// List of all subscription types except for Candle
|
|
19
35
|
var AllSubscriptionTypes = Object.values(MarketDataSubscriptionType);
|
|
20
36
|
var KeepaliveInterval = 30000; // 30 seconds
|
|
21
37
|
var DefaultChannelId = 1;
|
|
@@ -28,16 +44,37 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
28
44
|
this.openChannels = new Set();
|
|
29
45
|
this.subscriptionsQueue = new Map();
|
|
30
46
|
this.authState = '';
|
|
47
|
+
this.errorListeners = new Map();
|
|
48
|
+
this.authStateListeners = new Map();
|
|
31
49
|
}
|
|
32
|
-
MarketDataStreamer.prototype.addDataListener = function (dataListener) {
|
|
50
|
+
MarketDataStreamer.prototype.addDataListener = function (dataListener, channelId) {
|
|
33
51
|
var _this = this;
|
|
52
|
+
if (channelId === void 0) { channelId = null; }
|
|
34
53
|
if (lodash_1.default.isNil(dataListener)) {
|
|
35
54
|
return lodash_1.default.noop;
|
|
36
55
|
}
|
|
37
56
|
var guid = (0, uuid_1.v4)();
|
|
38
|
-
this.dataListeners.set(guid, dataListener);
|
|
57
|
+
this.dataListeners.set(guid, { listener: dataListener, channelId: channelId });
|
|
39
58
|
return function () { return _this.dataListeners.delete(guid); };
|
|
40
59
|
};
|
|
60
|
+
MarketDataStreamer.prototype.addErrorListener = function (errorListener) {
|
|
61
|
+
var _this = this;
|
|
62
|
+
if (lodash_1.default.isNil(errorListener)) {
|
|
63
|
+
return lodash_1.default.noop;
|
|
64
|
+
}
|
|
65
|
+
var guid = (0, uuid_1.v4)();
|
|
66
|
+
this.errorListeners.set(guid, errorListener);
|
|
67
|
+
return function () { return _this.errorListeners.delete(guid); };
|
|
68
|
+
};
|
|
69
|
+
MarketDataStreamer.prototype.addAuthStateChangeListener = function (authStateListener) {
|
|
70
|
+
var _this = this;
|
|
71
|
+
if (lodash_1.default.isNil(authStateListener)) {
|
|
72
|
+
return lodash_1.default.noop;
|
|
73
|
+
}
|
|
74
|
+
var guid = (0, uuid_1.v4)();
|
|
75
|
+
this.authStateListeners.set(guid, authStateListener);
|
|
76
|
+
return function () { return _this.authStateListeners.delete(guid); };
|
|
77
|
+
};
|
|
41
78
|
MarketDataStreamer.prototype.connect = function (url, token) {
|
|
42
79
|
if (this.isConnected) {
|
|
43
80
|
throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected');
|
|
@@ -54,23 +91,60 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
54
91
|
return;
|
|
55
92
|
}
|
|
56
93
|
this.clearKeepalive();
|
|
94
|
+
this.webSocket.onopen = null;
|
|
95
|
+
this.webSocket.onerror = null;
|
|
96
|
+
this.webSocket.onmessage = null;
|
|
97
|
+
this.webSocket.onclose = null;
|
|
57
98
|
this.webSocket.close();
|
|
58
99
|
this.webSocket = null;
|
|
59
100
|
this.openChannels.clear();
|
|
60
101
|
this.subscriptionsQueue.clear();
|
|
61
102
|
this.authState = '';
|
|
62
103
|
};
|
|
63
|
-
// TODO: add listener to options, return unsubscriber
|
|
64
104
|
MarketDataStreamer.prototype.addSubscription = function (symbol, options) {
|
|
105
|
+
var _this = this;
|
|
65
106
|
if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
|
|
66
|
-
var subscriptionTypes = options.subscriptionTypes
|
|
107
|
+
var subscriptionTypes = options.subscriptionTypes;
|
|
108
|
+
// Don't allow candle subscriptions in this method. Use addCandleSubscription instead
|
|
109
|
+
subscriptionTypes = lodash_1.default.without(subscriptionTypes, MarketDataSubscriptionType.Candle);
|
|
110
|
+
var isOpen = this.isChannelOpened(options.channelId);
|
|
111
|
+
if (isOpen) {
|
|
112
|
+
this.sendSubscriptionMessage(symbol, subscriptionTypes, options.channelId, 'add');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.queueSubscription(symbol, { subscriptionTypes: subscriptionTypes, channelId: options.channelId });
|
|
116
|
+
}
|
|
117
|
+
return function () {
|
|
118
|
+
_this.removeSubscription(symbol, options);
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Adds a candle subscription (historical data)
|
|
123
|
+
* @param streamerSymbol Get this from an instrument's streamer-symbol json response field
|
|
124
|
+
* @param fromTime Epoch timestamp from where you want to start
|
|
125
|
+
* @param options Period and Type are the grouping you want to apply to the candle data
|
|
126
|
+
* For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
|
|
127
|
+
* From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
|
|
128
|
+
* @returns
|
|
129
|
+
*/
|
|
130
|
+
MarketDataStreamer.prototype.addCandleSubscription = function (streamerSymbol, fromTime, options) {
|
|
131
|
+
var _this = this;
|
|
132
|
+
var _a;
|
|
133
|
+
var subscriptionTypes = [MarketDataSubscriptionType.Candle];
|
|
134
|
+
var channelId = (_a = options.channelId) !== null && _a !== void 0 ? _a : DefaultChannelId;
|
|
135
|
+
// Example: AAPL{=5m} where each candle represents 5 minutes of data
|
|
136
|
+
var candleSymbol = "".concat(streamerSymbol, "{=").concat(options.period).concat(options.type, "}");
|
|
67
137
|
var isOpen = this.isChannelOpened(channelId);
|
|
138
|
+
var subscriptionArgs = { fromTime: fromTime };
|
|
68
139
|
if (isOpen) {
|
|
69
|
-
this.sendSubscriptionMessage(
|
|
140
|
+
this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs);
|
|
70
141
|
}
|
|
71
142
|
else {
|
|
72
|
-
this.queueSubscription(
|
|
143
|
+
this.queueSubscription(candleSymbol, { subscriptionTypes: subscriptionTypes, channelId: channelId, subscriptionArgs: subscriptionArgs });
|
|
73
144
|
}
|
|
145
|
+
return function () {
|
|
146
|
+
_this.removeSubscription(candleSymbol, { subscriptionTypes: subscriptionTypes, channelId: channelId });
|
|
147
|
+
};
|
|
74
148
|
};
|
|
75
149
|
MarketDataStreamer.prototype.removeSubscription = function (symbol, options) {
|
|
76
150
|
if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
|
|
@@ -94,7 +168,7 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
94
168
|
}
|
|
95
169
|
};
|
|
96
170
|
MarketDataStreamer.prototype.openFeedChannel = function (channelId) {
|
|
97
|
-
if (!this.
|
|
171
|
+
if (!this.isReadyToOpenChannels) {
|
|
98
172
|
throw new Error("Unable to open channel ".concat(channelId, " due to DxLink authorization state: ").concat(this.authState));
|
|
99
173
|
}
|
|
100
174
|
if (this.isChannelOpened(channelId)) {
|
|
@@ -112,6 +186,13 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
112
186
|
MarketDataStreamer.prototype.isChannelOpened = function (channelId) {
|
|
113
187
|
return this.isConnected && this.openChannels.has(channelId);
|
|
114
188
|
};
|
|
189
|
+
Object.defineProperty(MarketDataStreamer.prototype, "isReadyToOpenChannels", {
|
|
190
|
+
get: function () {
|
|
191
|
+
return this.isConnected && this.isDxLinkAuthorized;
|
|
192
|
+
},
|
|
193
|
+
enumerable: false,
|
|
194
|
+
configurable: true
|
|
195
|
+
});
|
|
115
196
|
Object.defineProperty(MarketDataStreamer.prototype, "isConnected", {
|
|
116
197
|
get: function () {
|
|
117
198
|
return !lodash_1.default.isNil(this.webSocket);
|
|
@@ -132,13 +213,13 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
132
213
|
});
|
|
133
214
|
};
|
|
134
215
|
MarketDataStreamer.prototype.queueSubscription = function (symbol, options) {
|
|
135
|
-
var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId;
|
|
216
|
+
var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId, subscriptionArgs = options.subscriptionArgs;
|
|
136
217
|
var queue = this.subscriptionsQueue.get(options.channelId);
|
|
137
218
|
if (lodash_1.default.isNil(queue)) {
|
|
138
219
|
queue = [];
|
|
139
220
|
this.subscriptionsQueue.set(channelId, queue);
|
|
140
221
|
}
|
|
141
|
-
queue.push({ symbol: symbol, subscriptionTypes: subscriptionTypes });
|
|
222
|
+
queue.push({ symbol: symbol, subscriptionTypes: subscriptionTypes, subscriptionArgs: subscriptionArgs });
|
|
142
223
|
};
|
|
143
224
|
MarketDataStreamer.prototype.dequeueSubscription = function (symbol, options) {
|
|
144
225
|
var queue = this.subscriptionsQueue.get(options.channelId);
|
|
@@ -156,7 +237,7 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
156
237
|
// Clear out queue immediately
|
|
157
238
|
this.subscriptionsQueue.set(channelId, []);
|
|
158
239
|
queuedSubscriptions.forEach(function (subscription) {
|
|
159
|
-
_this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add');
|
|
240
|
+
_this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add', subscription.subscriptionArgs);
|
|
160
241
|
});
|
|
161
242
|
};
|
|
162
243
|
/**
|
|
@@ -166,9 +247,10 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
166
247
|
* @param {*} channelId
|
|
167
248
|
* @param {*} direction add or remove
|
|
168
249
|
*/
|
|
169
|
-
MarketDataStreamer.prototype.sendSubscriptionMessage = function (symbol, subscriptionTypes, channelId, direction) {
|
|
250
|
+
MarketDataStreamer.prototype.sendSubscriptionMessage = function (symbol, subscriptionTypes, channelId, direction, subscriptionArgs) {
|
|
170
251
|
var _a;
|
|
171
|
-
|
|
252
|
+
if (subscriptionArgs === void 0) { subscriptionArgs = {}; }
|
|
253
|
+
var subscriptions = subscriptionTypes.map(function (type) { return (Object.assign({}, { "symbol": symbol, "type": type }, subscriptionArgs !== null && subscriptionArgs !== void 0 ? subscriptionArgs : {})); });
|
|
172
254
|
this.sendMessage((_a = {
|
|
173
255
|
"type": "FEED_SUBSCRIPTION",
|
|
174
256
|
"channel": channelId
|
|
@@ -178,6 +260,7 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
178
260
|
};
|
|
179
261
|
MarketDataStreamer.prototype.onError = function (error) {
|
|
180
262
|
console.error('Error received: ', error);
|
|
263
|
+
this.notifyErrorListeners(error);
|
|
181
264
|
};
|
|
182
265
|
MarketDataStreamer.prototype.onOpen = function () {
|
|
183
266
|
this.openChannels.clear();
|
|
@@ -208,7 +291,9 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
208
291
|
configurable: true
|
|
209
292
|
});
|
|
210
293
|
MarketDataStreamer.prototype.handleAuthStateMessage = function (data) {
|
|
294
|
+
var _this = this;
|
|
211
295
|
this.authState = data.state;
|
|
296
|
+
this.authStateListeners.forEach(function (listener) { return listener(_this.isDxLinkAuthorized); });
|
|
212
297
|
if (this.isDxLinkAuthorized) {
|
|
213
298
|
this.openFeedChannel(DefaultChannelId);
|
|
214
299
|
}
|
|
@@ -225,7 +310,14 @@ var MarketDataStreamer = /** @class */ (function () {
|
|
|
225
310
|
this.sendQueuedSubscriptions(jsonData.channel);
|
|
226
311
|
};
|
|
227
312
|
MarketDataStreamer.prototype.notifyListeners = function (jsonData) {
|
|
228
|
-
this.dataListeners.forEach(function (
|
|
313
|
+
this.dataListeners.forEach(function (listenerData) {
|
|
314
|
+
if (listenerData.channelId === jsonData.channel || lodash_1.default.isNil(listenerData.channelId)) {
|
|
315
|
+
listenerData.listener(jsonData);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
MarketDataStreamer.prototype.notifyErrorListeners = function (error) {
|
|
320
|
+
this.errorListeners.forEach(function (listener) { return listener(error); });
|
|
229
321
|
};
|
|
230
322
|
MarketDataStreamer.prototype.handleMessageReceived = function (data) {
|
|
231
323
|
var messageData = lodash_1.default.get(data, 'data', data);
|
package/dist/tastytrade-api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import TastytradeHttpClient from "./services/tastytrade-http-client";
|
|
2
2
|
import { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver } from './account-streamer';
|
|
3
|
-
import MarketDataStreamer, { MarketDataSubscriptionType, MarketDataListener } from "./market-data-streamer";
|
|
3
|
+
import MarketDataStreamer, { CandleSubscriptionOptions, CandleType, MarketDataSubscriptionType, MarketDataListener } from "./market-data-streamer";
|
|
4
4
|
import SessionService from "./services/session-service";
|
|
5
5
|
import AccountStatusService from "./services/account-status-service";
|
|
6
6
|
import AccountsAndCustomersService from "./services/accounts-and-customers-service";
|
|
@@ -36,5 +36,5 @@ export default class TastytradeClient {
|
|
|
36
36
|
constructor(baseUrl: string, accountStreamerUrl: string);
|
|
37
37
|
get session(): TastytradeSession;
|
|
38
38
|
}
|
|
39
|
-
export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener };
|
|
39
|
+
export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener, CandleSubscriptionOptions, CandleType };
|
|
40
40
|
export { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver };
|
package/dist/tastytrade-api.js
CHANGED
|
@@ -26,13 +26,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.STREAMER_STATE = exports.AccountStreamer = exports.MarketDataSubscriptionType = exports.MarketDataStreamer = void 0;
|
|
29
|
+
exports.STREAMER_STATE = exports.AccountStreamer = exports.CandleType = exports.MarketDataSubscriptionType = exports.MarketDataStreamer = void 0;
|
|
30
30
|
var tastytrade_http_client_1 = __importDefault(require("./services/tastytrade-http-client"));
|
|
31
31
|
var account_streamer_1 = require("./account-streamer");
|
|
32
32
|
Object.defineProperty(exports, "AccountStreamer", { enumerable: true, get: function () { return account_streamer_1.AccountStreamer; } });
|
|
33
33
|
Object.defineProperty(exports, "STREAMER_STATE", { enumerable: true, get: function () { return account_streamer_1.STREAMER_STATE; } });
|
|
34
34
|
var market_data_streamer_1 = __importStar(require("./market-data-streamer"));
|
|
35
35
|
exports.MarketDataStreamer = market_data_streamer_1.default;
|
|
36
|
+
Object.defineProperty(exports, "CandleType", { enumerable: true, get: function () { return market_data_streamer_1.CandleType; } });
|
|
36
37
|
Object.defineProperty(exports, "MarketDataSubscriptionType", { enumerable: true, get: function () { return market_data_streamer_1.MarketDataSubscriptionType; } });
|
|
37
38
|
//Services:
|
|
38
39
|
var session_service_1 = __importDefault(require("./services/session-service"));
|
|
@@ -3,6 +3,7 @@ import _ from 'lodash'
|
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid'
|
|
4
4
|
|
|
5
5
|
export enum MarketDataSubscriptionType {
|
|
6
|
+
Candle = 'Candle',
|
|
6
7
|
Quote = 'Quote',
|
|
7
8
|
Trade = 'Trade',
|
|
8
9
|
Summary = 'Summary',
|
|
@@ -11,11 +12,30 @@ export enum MarketDataSubscriptionType {
|
|
|
11
12
|
Underlying = 'Underlying'
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export enum CandleType {
|
|
16
|
+
Tick = 't',
|
|
17
|
+
Second = 's',
|
|
18
|
+
Minute = 'm',
|
|
19
|
+
Hour = 'h',
|
|
20
|
+
Day = 'd',
|
|
21
|
+
Week = 'w',
|
|
22
|
+
Month = 'mo',
|
|
23
|
+
ThirdFriday = 'o',
|
|
24
|
+
Year = 'y',
|
|
25
|
+
Volume = 'v',
|
|
26
|
+
Price = 'p'
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export type MarketDataListener = (data: any) => void
|
|
30
|
+
export type ErrorListener = (error: any) => void
|
|
31
|
+
export type AuthStateListener = (isAuthorized: boolean) => void
|
|
15
32
|
|
|
16
|
-
type QueuedSubscription = { symbol: string, subscriptionTypes: MarketDataSubscriptionType[] }
|
|
17
|
-
type SubscriptionOptions = { subscriptionTypes: MarketDataSubscriptionType[], channelId: number }
|
|
33
|
+
type QueuedSubscription = { symbol: string, subscriptionTypes: MarketDataSubscriptionType[], subscriptionArgs?: any }
|
|
34
|
+
type SubscriptionOptions = { subscriptionTypes: MarketDataSubscriptionType[], channelId: number, subscriptionArgs?: any }
|
|
35
|
+
export type CandleSubscriptionOptions = { period: number, type: CandleType, channelId: number }
|
|
36
|
+
type Remover = () => void
|
|
18
37
|
|
|
38
|
+
// List of all subscription types except for Candle
|
|
19
39
|
const AllSubscriptionTypes = Object.values(MarketDataSubscriptionType)
|
|
20
40
|
|
|
21
41
|
const KeepaliveInterval = 30000 // 30 seconds
|
|
@@ -30,17 +50,39 @@ export default class MarketDataStreamer {
|
|
|
30
50
|
private openChannels = new Set()
|
|
31
51
|
private subscriptionsQueue: Map<number, QueuedSubscription[]> = new Map()
|
|
32
52
|
private authState = ''
|
|
53
|
+
private errorListeners = new Map()
|
|
54
|
+
private authStateListeners = new Map()
|
|
33
55
|
|
|
34
|
-
addDataListener(dataListener: MarketDataListener) {
|
|
56
|
+
addDataListener(dataListener: MarketDataListener, channelId: number | null = null): Remover {
|
|
35
57
|
if (_.isNil(dataListener)) {
|
|
36
58
|
return _.noop
|
|
37
59
|
}
|
|
38
60
|
const guid = uuidv4()
|
|
39
|
-
this.dataListeners.set(guid, dataListener)
|
|
61
|
+
this.dataListeners.set(guid, { listener: dataListener, channelId })
|
|
40
62
|
|
|
41
63
|
return () => this.dataListeners.delete(guid)
|
|
42
64
|
}
|
|
43
65
|
|
|
66
|
+
addErrorListener(errorListener: ErrorListener): Remover {
|
|
67
|
+
if (_.isNil(errorListener)) {
|
|
68
|
+
return _.noop
|
|
69
|
+
}
|
|
70
|
+
const guid = uuidv4()
|
|
71
|
+
this.errorListeners.set(guid, errorListener)
|
|
72
|
+
|
|
73
|
+
return () => this.errorListeners.delete(guid)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addAuthStateChangeListener(authStateListener: AuthStateListener): Remover {
|
|
77
|
+
if (_.isNil(authStateListener)) {
|
|
78
|
+
return _.noop
|
|
79
|
+
}
|
|
80
|
+
const guid = uuidv4()
|
|
81
|
+
this.authStateListeners.set(guid, authStateListener)
|
|
82
|
+
|
|
83
|
+
return () => this.authStateListeners.delete(guid)
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
connect(url: string, token: string) {
|
|
45
87
|
if (this.isConnected) {
|
|
46
88
|
throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected')
|
|
@@ -61,6 +103,11 @@ export default class MarketDataStreamer {
|
|
|
61
103
|
|
|
62
104
|
this.clearKeepalive()
|
|
63
105
|
|
|
106
|
+
this.webSocket.onopen = null
|
|
107
|
+
this.webSocket.onerror = null
|
|
108
|
+
this.webSocket.onmessage = null
|
|
109
|
+
this.webSocket.onclose = null
|
|
110
|
+
|
|
64
111
|
this.webSocket.close()
|
|
65
112
|
this.webSocket = null
|
|
66
113
|
|
|
@@ -69,14 +116,48 @@ export default class MarketDataStreamer {
|
|
|
69
116
|
this.authState = ''
|
|
70
117
|
}
|
|
71
118
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
addSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }): Remover {
|
|
120
|
+
let { subscriptionTypes } = options
|
|
121
|
+
// Don't allow candle subscriptions in this method. Use addCandleSubscription instead
|
|
122
|
+
subscriptionTypes = _.without(subscriptionTypes, MarketDataSubscriptionType.Candle)
|
|
123
|
+
|
|
124
|
+
const isOpen = this.isChannelOpened(options.channelId)
|
|
125
|
+
if (isOpen) {
|
|
126
|
+
this.sendSubscriptionMessage(symbol, subscriptionTypes, options.channelId, 'add')
|
|
127
|
+
} else {
|
|
128
|
+
this.queueSubscription(symbol, { subscriptionTypes, channelId: options.channelId })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
this.removeSubscription(symbol, options)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Adds a candle subscription (historical data)
|
|
138
|
+
* @param streamerSymbol Get this from an instrument's streamer-symbol json response field
|
|
139
|
+
* @param fromTime Epoch timestamp from where you want to start
|
|
140
|
+
* @param options Period and Type are the grouping you want to apply to the candle data
|
|
141
|
+
* For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
|
|
142
|
+
* From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
|
|
143
|
+
* @returns
|
|
144
|
+
*/
|
|
145
|
+
addCandleSubscription(streamerSymbol: string, fromTime: number, options: CandleSubscriptionOptions) {
|
|
146
|
+
const subscriptionTypes = [MarketDataSubscriptionType.Candle]
|
|
147
|
+
const channelId = options.channelId ?? DefaultChannelId
|
|
148
|
+
|
|
149
|
+
// Example: AAPL{=5m} where each candle represents 5 minutes of data
|
|
150
|
+
const candleSymbol = `${streamerSymbol}{=${options.period}${options.type}}`
|
|
75
151
|
const isOpen = this.isChannelOpened(channelId)
|
|
152
|
+
const subscriptionArgs = { fromTime }
|
|
76
153
|
if (isOpen) {
|
|
77
|
-
this.sendSubscriptionMessage(
|
|
154
|
+
this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs)
|
|
78
155
|
} else {
|
|
79
|
-
this.queueSubscription(
|
|
156
|
+
this.queueSubscription(candleSymbol, { subscriptionTypes, channelId, subscriptionArgs })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
this.removeSubscription(candleSymbol, { subscriptionTypes, channelId })
|
|
80
161
|
}
|
|
81
162
|
}
|
|
82
163
|
|
|
@@ -100,7 +181,7 @@ export default class MarketDataStreamer {
|
|
|
100
181
|
}
|
|
101
182
|
|
|
102
183
|
openFeedChannel(channelId: number) {
|
|
103
|
-
if (!this.
|
|
184
|
+
if (!this.isReadyToOpenChannels) {
|
|
104
185
|
throw new Error(`Unable to open channel ${channelId} due to DxLink authorization state: ${this.authState}`)
|
|
105
186
|
}
|
|
106
187
|
|
|
@@ -122,6 +203,10 @@ export default class MarketDataStreamer {
|
|
|
122
203
|
return this.isConnected && this.openChannels.has(channelId)
|
|
123
204
|
}
|
|
124
205
|
|
|
206
|
+
get isReadyToOpenChannels() {
|
|
207
|
+
return this.isConnected && this.isDxLinkAuthorized
|
|
208
|
+
}
|
|
209
|
+
|
|
125
210
|
get isConnected() {
|
|
126
211
|
return !_.isNil(this.webSocket)
|
|
127
212
|
}
|
|
@@ -142,14 +227,14 @@ export default class MarketDataStreamer {
|
|
|
142
227
|
}
|
|
143
228
|
|
|
144
229
|
private queueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
145
|
-
const { subscriptionTypes, channelId } = options
|
|
230
|
+
const { subscriptionTypes, channelId, subscriptionArgs } = options
|
|
146
231
|
let queue = this.subscriptionsQueue.get(options.channelId)
|
|
147
232
|
if (_.isNil(queue)) {
|
|
148
233
|
queue = []
|
|
149
234
|
this.subscriptionsQueue.set(channelId, queue)
|
|
150
235
|
}
|
|
151
236
|
|
|
152
|
-
queue.push({ symbol, subscriptionTypes })
|
|
237
|
+
queue.push({ symbol, subscriptionTypes, subscriptionArgs })
|
|
153
238
|
}
|
|
154
239
|
|
|
155
240
|
private dequeueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
@@ -170,7 +255,7 @@ export default class MarketDataStreamer {
|
|
|
170
255
|
// Clear out queue immediately
|
|
171
256
|
this.subscriptionsQueue.set(channelId, [])
|
|
172
257
|
queuedSubscriptions.forEach(subscription => {
|
|
173
|
-
this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add')
|
|
258
|
+
this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add', subscription.subscriptionArgs)
|
|
174
259
|
})
|
|
175
260
|
}
|
|
176
261
|
|
|
@@ -181,8 +266,8 @@ export default class MarketDataStreamer {
|
|
|
181
266
|
* @param {*} channelId
|
|
182
267
|
* @param {*} direction add or remove
|
|
183
268
|
*/
|
|
184
|
-
private sendSubscriptionMessage(symbol: string, subscriptionTypes: MarketDataSubscriptionType[], channelId: number, direction: string) {
|
|
185
|
-
const subscriptions = subscriptionTypes.map(type => ({ "symbol": symbol, "type": type }))
|
|
269
|
+
private sendSubscriptionMessage(symbol: string, subscriptionTypes: MarketDataSubscriptionType[], channelId: number, direction: string, subscriptionArgs: any = {}) {
|
|
270
|
+
const subscriptions = subscriptionTypes.map(type => (Object.assign({}, { "symbol": symbol, "type": type }, subscriptionArgs ?? {})))
|
|
186
271
|
this.sendMessage({
|
|
187
272
|
"type": "FEED_SUBSCRIPTION",
|
|
188
273
|
"channel": channelId,
|
|
@@ -192,6 +277,7 @@ export default class MarketDataStreamer {
|
|
|
192
277
|
|
|
193
278
|
private onError(error: any) {
|
|
194
279
|
console.error('Error received: ', error)
|
|
280
|
+
this.notifyErrorListeners(error)
|
|
195
281
|
}
|
|
196
282
|
|
|
197
283
|
private onOpen() {
|
|
@@ -227,6 +313,8 @@ export default class MarketDataStreamer {
|
|
|
227
313
|
|
|
228
314
|
private handleAuthStateMessage(data: any) {
|
|
229
315
|
this.authState = data.state
|
|
316
|
+
this.authStateListeners.forEach(listener => listener(this.isDxLinkAuthorized))
|
|
317
|
+
|
|
230
318
|
if (this.isDxLinkAuthorized) {
|
|
231
319
|
this.openFeedChannel(DefaultChannelId)
|
|
232
320
|
} else {
|
|
@@ -244,7 +332,15 @@ export default class MarketDataStreamer {
|
|
|
244
332
|
}
|
|
245
333
|
|
|
246
334
|
private notifyListeners(jsonData: any) {
|
|
247
|
-
this.dataListeners.forEach(
|
|
335
|
+
this.dataListeners.forEach(listenerData => {
|
|
336
|
+
if (listenerData.channelId === jsonData.channel || _.isNil(listenerData.channelId)) {
|
|
337
|
+
listenerData.listener(jsonData)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private notifyErrorListeners(error: any) {
|
|
343
|
+
this.errorListeners.forEach(listener => listener(error))
|
|
248
344
|
}
|
|
249
345
|
|
|
250
346
|
private handleMessageReceived(data: string) {
|
package/lib/tastytrade-api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import TastytradeHttpClient from "./services/tastytrade-http-client"
|
|
2
2
|
import { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver } from './account-streamer'
|
|
3
|
-
import MarketDataStreamer, { MarketDataSubscriptionType, MarketDataListener } from "./market-data-streamer"
|
|
3
|
+
import MarketDataStreamer, { CandleSubscriptionOptions, CandleType, MarketDataSubscriptionType, MarketDataListener } from "./market-data-streamer"
|
|
4
4
|
|
|
5
5
|
//Services:
|
|
6
6
|
import SessionService from "./services/session-service"
|
|
@@ -61,5 +61,5 @@ export default class TastytradeClient {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener }
|
|
64
|
+
export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener, CandleSubscriptionOptions, CandleType }
|
|
65
65
|
export { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver }
|