@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.
@@ -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
- addDataListener(dataListener: MarketDataListener): (...args: any[]) => void;
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
- }): void;
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, channelId = options.channelId;
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(symbol, subscriptionTypes, channelId, 'add');
140
+ this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs);
70
141
  }
71
142
  else {
72
- this.queueSubscription(symbol, options);
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.isDxLinkAuthorized) {
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
- var subscriptions = subscriptionTypes.map(function (type) { return ({ "symbol": symbol, "type": type }); });
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 (listener) { return listener(jsonData); });
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);
@@ -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 };
@@ -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
- // TODO: add listener to options, return unsubscriber
73
- addSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) {
74
- const { subscriptionTypes, channelId } = options
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(symbol, subscriptionTypes, channelId, 'add')
154
+ this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs)
78
155
  } else {
79
- this.queueSubscription(symbol, options)
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.isDxLinkAuthorized) {
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(listener => listener(jsonData))
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) {
@@ -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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastytrade/api",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "dist/tastytrade-api.js",
5
5
  "typings": "dist/tastytrade-api.d.ts",
6
6
  "repository": "https://github.com/tastytrade/tastytrade-api-js",