@tastytrade/api 0.0.1 → 2.0.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/.env CHANGED
@@ -1,5 +1,4 @@
1
- BASE_URL=https://api.tastyworks.com
2
- API_USERNAME=dmoss18
1
+ BASE_URL=https://api.cert.tastyworks.com
2
+ API_USERNAME=devin.moss@tastytrade.com
3
3
  API_PASSWORD=rebuke-padding-vizor
4
- API_CUSTOMER_ID=me
5
- API_ACCOUNT_NUMBER=5WT05020
4
+ API_ACCOUNT_NUMBER=5WT05179
@@ -7,18 +7,14 @@
7
7
  {
8
8
  "type": "node",
9
9
  "request": "launch",
10
- "name": "Integration Tests",
11
- "skipFiles": ["<node_internals>/**"],
10
+ "name": "Launch Program",
11
+ "skipFiles": [
12
+ "<node_internals>/**"
13
+ ],
12
14
  "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
13
- "args": ["-i", "--", "tests/integration/service/instruments-service.test.ts"]
14
- },
15
- {
16
- "type": "node",
17
- "request": "launch",
18
- "name": "Unit Tests",
19
- "skipFiles": ["<node_internals>/**"],
20
- "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
21
- "args": ["-i", "--", "tests/unit/service/session-service.test.ts"]
15
+ "args": [
16
+ "-c", "jest.config.js", "tests/integration/service/accounts-and-customers-service.test.ts"
17
+ ]
22
18
  }
23
19
  ]
24
20
  }
package/README.md CHANGED
@@ -1,8 +1,100 @@
1
1
  # Tastytrade Api Javascript SDK
2
+
3
+ ## Installation
4
+ npm:
5
+ `npm -i @tastytrade/api`
6
+
7
+ yarn:
8
+ `yarn add @tastytrade/api`
9
+
10
+ ## Quickstart
11
+ ```js
12
+ import TastytradeClient from "@tastytrade/api"
13
+ const tastytradeClient = new TastytradeClient(baseUrl, accountStreamerUrl)
14
+ const loginResponse = await tastytradeClient.sessionService.login(usernameOrEmail, pasword)
15
+ const accounts = await tastytradeClient.accountsAndCustomersService.getCustomerAccounts()
16
+ const accountPositions = await tastytradeClient.balancesAndPositionsService.getPositionsList(accounts[0].accounts['account-number'])
17
+ ```
18
+
19
+ ### Market Data
20
+ ```js
21
+ import TastytradeClient, { MarketDataStreamer, MarketDataSubscriptionType } from "@tastytrade-api"
22
+ const tastytradeClient = new TastytradeClient(baseUrl, accountStreamerUrl)
23
+ await tastytradeClient.sessionService.login(usernameOrEmail, pasword)
24
+ const tokenResponse = await tastytradeClient.AccountsAndCustomersService.getApiQuoteToken()
25
+ const streamer = new MarketDataStreamer()
26
+ streamer.connect(tokenResponse['dxlink-url'], tokenResponse.token)
27
+
28
+ function handleMarketDataReceived(data) {
29
+ // Triggers every time market data event occurs
30
+ console.log(data)
31
+ }
32
+
33
+ // Add a listener for incoming market data. Returns a remove() function that removes the listener from the quote streamer
34
+ const removeDataListener = streamer.addDataListener(handleMarketDataReceived)
35
+
36
+ // Subscribe to a single equity quote
37
+ streamer.addSubscription('AAPL')
38
+ // Optionally specify which market data events you want to subscribe to
39
+ streamer.addSubscription('SPY', { subscriptionTypes: [MarketDataSubscriptionType.Quote] })
40
+
41
+ // Subscribe to a single equity option quote
42
+ const optionChain = await tastytradeClient.instrumentsService.getOptionChain('AAPL')
43
+ streamer.addSubscription(optionChain[0]['streamer-symbol'])
44
+ ```
45
+
46
+ ### Account Streamer
47
+ ```js
48
+ const TastytradeApi = require("@tastytrade/api")
49
+ const TastytradeClient = TastytradeApi.default
50
+ const { AccountStreamer } = TastytradeApi
51
+ const _ = require('lodash')
52
+
53
+ function handleStreamerMessage(json) {
54
+ console.log('streamer message received: ', json)
55
+ }
56
+
57
+ function handleStreamerStateChange(streamerState) {
58
+ console.log('streamer state changed: ', streamerState)
59
+ }
60
+
61
+ const tastytradeClient = new TastytradeClient(baseUrl, accountStreamerUrl)
62
+ const accountStreamer = tastytradeClient.accountStreamer
63
+ const loginResponse = await tastytradeClient.sessionService.login(usernameOrEmail, password)
64
+ const accounts = await tastytradeClient.accountsAndCustomersService.getCustomerAccounts()
65
+ const accountNumbers = _.map(accounts, account => _.get(account, 'account.account-number'))
66
+ await accountStreamer.start()
67
+ await accountStreamer.subscribeToAccounts(accountNumbers)
68
+ accountStreamer.addMessageObserver(handleStreamerMessage)
69
+ accountStreamer.addStreamerStateObserver(handleStreamerStateChange)
70
+ ```
71
+
72
+ You should then be able to place a trade and see live status updates for the order come through via `handleStreamerMessage`.
73
+
74
+ ## Running in Node
75
+ The `cometd` package has an explicit reference to `window`, so there's not a perfect way to run this code in a NodeJs. You could fake the `window` object to get it running. You'll have to `npm install ws` and do this:
76
+
77
+ ```js
78
+ const WebSocket = require('ws')
79
+
80
+ global.WebSocket = WebSocket
81
+ global.window = { WebSocket, setTimeout, clearTimeout }
82
+ ```
83
+
2
84
  ## Building Locally
3
85
  `npm run build`
4
86
  Outputs everything to `dist/`
5
87
 
88
+ ## Running tests locally
89
+ Add a `.env` file with the following keys (you'll have to fill in the values yourself):
90
+
91
+ ```
92
+ BASE_URL=https://api.cert.tastyworks.com
93
+ API_USERNAME=<your cert username>
94
+ API_PASSWORD=<your cert password>
95
+ API_ACCOUNT_NUMBER=<your cert account number>
96
+ ```
97
+
6
98
  ## Running example app
7
99
  ```sh
8
100
  npm run build
@@ -8,7 +8,7 @@ export declare enum STREAMER_STATE {
8
8
  }
9
9
  export declare type Disposer = () => void;
10
10
  export declare type StreamerStateObserver = (streamerState: STREAMER_STATE) => void;
11
- export declare type StreamerMessageObserver = (messageType: string, action: string, status: string) => void;
11
+ export declare type StreamerMessageObserver = (json: object) => void;
12
12
  export declare class AccountStreamer {
13
13
  private readonly url;
14
14
  private readonly session;
@@ -143,7 +143,7 @@ var AccountStreamer = /** @class */ (function () {
143
143
  this.handleOneMessage = function (json) {
144
144
  _this.logger.info(json);
145
145
  var action = json.action;
146
- _this.streamerMessageObservers.forEach(function (observer) { return observer(json.type, action, json.status); });
146
+ _this.streamerMessageObservers.forEach(function (observer) { return observer(json); });
147
147
  if (action) {
148
148
  if (action === MessageAction.HEARTBEAT) {
149
149
  // schedule next heartbeat
@@ -0,0 +1,56 @@
1
+ export declare enum MarketDataSubscriptionType {
2
+ Quote = "Quote",
3
+ Trade = "Trade",
4
+ Summary = "Summary",
5
+ Profile = "Profile",
6
+ Greeks = "Greeks",
7
+ Underlying = "Underlying"
8
+ }
9
+ export declare type MarketDataListener = (data: any) => void;
10
+ export default class MarketDataStreamer {
11
+ private webSocket;
12
+ private token;
13
+ private keepaliveIntervalId;
14
+ private dataListeners;
15
+ private openChannels;
16
+ private subscriptionsQueue;
17
+ private authState;
18
+ addDataListener(dataListener: MarketDataListener): (...args: any[]) => void;
19
+ connect(url: string, token: string): void;
20
+ disconnect(): void;
21
+ addSubscription(symbol: string, options?: {
22
+ subscriptionTypes: MarketDataSubscriptionType[];
23
+ channelId: number;
24
+ }): void;
25
+ removeSubscription(symbol: string, options?: {
26
+ subscriptionTypes: MarketDataSubscriptionType[];
27
+ channelId: number;
28
+ }): void;
29
+ removeAllSubscriptions(channelId?: number): void;
30
+ openFeedChannel(channelId: number): void;
31
+ isChannelOpened(channelId: number): boolean;
32
+ get isConnected(): boolean;
33
+ private scheduleKeepalive;
34
+ private sendKeepalive;
35
+ private queueSubscription;
36
+ private dequeueSubscription;
37
+ private sendQueuedSubscriptions;
38
+ /**
39
+ *
40
+ * @param {*} symbol
41
+ * @param {*} subscriptionTypes
42
+ * @param {*} channelId
43
+ * @param {*} direction add or remove
44
+ */
45
+ private sendSubscriptionMessage;
46
+ private onError;
47
+ private onOpen;
48
+ private onClose;
49
+ private clearKeepalive;
50
+ get isDxLinkAuthorized(): boolean;
51
+ private handleAuthStateMessage;
52
+ private handleChannelOpened;
53
+ private notifyListeners;
54
+ private handleMessageReceived;
55
+ private sendMessage;
56
+ }
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MarketDataSubscriptionType = void 0;
7
+ var isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
8
+ var lodash_1 = __importDefault(require("lodash"));
9
+ var uuid_1 = require("uuid");
10
+ var MarketDataSubscriptionType;
11
+ (function (MarketDataSubscriptionType) {
12
+ MarketDataSubscriptionType["Quote"] = "Quote";
13
+ MarketDataSubscriptionType["Trade"] = "Trade";
14
+ MarketDataSubscriptionType["Summary"] = "Summary";
15
+ MarketDataSubscriptionType["Profile"] = "Profile";
16
+ MarketDataSubscriptionType["Greeks"] = "Greeks";
17
+ MarketDataSubscriptionType["Underlying"] = "Underlying";
18
+ })(MarketDataSubscriptionType = exports.MarketDataSubscriptionType || (exports.MarketDataSubscriptionType = {}));
19
+ var AllSubscriptionTypes = Object.values(MarketDataSubscriptionType);
20
+ var KeepaliveInterval = 30000; // 30 seconds
21
+ var DefaultChannelId = 1;
22
+ var MarketDataStreamer = /** @class */ (function () {
23
+ function MarketDataStreamer() {
24
+ this.webSocket = null;
25
+ this.token = '';
26
+ this.keepaliveIntervalId = null;
27
+ this.dataListeners = new Map();
28
+ this.openChannels = new Set();
29
+ this.subscriptionsQueue = new Map();
30
+ this.authState = '';
31
+ }
32
+ MarketDataStreamer.prototype.addDataListener = function (dataListener) {
33
+ var _this = this;
34
+ if (lodash_1.default.isNil(dataListener)) {
35
+ return lodash_1.default.noop;
36
+ }
37
+ var guid = (0, uuid_1.v4)();
38
+ this.dataListeners.set(guid, dataListener);
39
+ return function () { return _this.dataListeners.delete(guid); };
40
+ };
41
+ MarketDataStreamer.prototype.connect = function (url, token) {
42
+ if (this.isConnected) {
43
+ throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected');
44
+ }
45
+ this.token = token;
46
+ this.webSocket = new isomorphic_ws_1.default(url);
47
+ this.webSocket.onopen = this.onOpen.bind(this);
48
+ this.webSocket.onerror = this.onError.bind(this);
49
+ this.webSocket.onmessage = this.handleMessageReceived.bind(this);
50
+ this.webSocket.onclose = this.onClose.bind(this);
51
+ };
52
+ MarketDataStreamer.prototype.disconnect = function () {
53
+ if (lodash_1.default.isNil(this.webSocket)) {
54
+ return;
55
+ }
56
+ this.clearKeepalive();
57
+ this.webSocket.close();
58
+ this.webSocket = null;
59
+ this.openChannels.clear();
60
+ this.subscriptionsQueue.clear();
61
+ this.authState = '';
62
+ };
63
+ // TODO: add listener to options, return unsubscriber
64
+ MarketDataStreamer.prototype.addSubscription = function (symbol, options) {
65
+ if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
66
+ var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId;
67
+ var isOpen = this.isChannelOpened(channelId);
68
+ if (isOpen) {
69
+ this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'add');
70
+ }
71
+ else {
72
+ this.queueSubscription(symbol, options);
73
+ }
74
+ };
75
+ MarketDataStreamer.prototype.removeSubscription = function (symbol, options) {
76
+ if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
77
+ var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId;
78
+ var isOpen = this.isChannelOpened(channelId);
79
+ if (isOpen) {
80
+ this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove');
81
+ }
82
+ else {
83
+ this.dequeueSubscription(symbol, options);
84
+ }
85
+ };
86
+ MarketDataStreamer.prototype.removeAllSubscriptions = function (channelId) {
87
+ if (channelId === void 0) { channelId = DefaultChannelId; }
88
+ var isOpen = this.isChannelOpened(channelId);
89
+ if (isOpen) {
90
+ this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true });
91
+ }
92
+ else {
93
+ this.subscriptionsQueue.set(channelId, []);
94
+ }
95
+ };
96
+ MarketDataStreamer.prototype.openFeedChannel = function (channelId) {
97
+ if (!this.isDxLinkAuthorized) {
98
+ throw new Error("Unable to open channel ".concat(channelId, " due to DxLink authorization state: ").concat(this.authState));
99
+ }
100
+ if (this.isChannelOpened(channelId)) {
101
+ return;
102
+ }
103
+ this.sendMessage({
104
+ "type": "CHANNEL_REQUEST",
105
+ "channel": channelId,
106
+ "service": "FEED",
107
+ "parameters": {
108
+ "contract": "AUTO"
109
+ }
110
+ });
111
+ };
112
+ MarketDataStreamer.prototype.isChannelOpened = function (channelId) {
113
+ return this.isConnected && this.openChannels.has(channelId);
114
+ };
115
+ Object.defineProperty(MarketDataStreamer.prototype, "isConnected", {
116
+ get: function () {
117
+ return !lodash_1.default.isNil(this.webSocket);
118
+ },
119
+ enumerable: false,
120
+ configurable: true
121
+ });
122
+ MarketDataStreamer.prototype.scheduleKeepalive = function () {
123
+ this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval);
124
+ };
125
+ MarketDataStreamer.prototype.sendKeepalive = function () {
126
+ if (lodash_1.default.isNil(this.keepaliveIntervalId)) {
127
+ return;
128
+ }
129
+ this.sendMessage({
130
+ "type": "KEEPALIVE",
131
+ "channel": 0
132
+ });
133
+ };
134
+ MarketDataStreamer.prototype.queueSubscription = function (symbol, options) {
135
+ var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId;
136
+ var queue = this.subscriptionsQueue.get(options.channelId);
137
+ if (lodash_1.default.isNil(queue)) {
138
+ queue = [];
139
+ this.subscriptionsQueue.set(channelId, queue);
140
+ }
141
+ queue.push({ symbol: symbol, subscriptionTypes: subscriptionTypes });
142
+ };
143
+ MarketDataStreamer.prototype.dequeueSubscription = function (symbol, options) {
144
+ var queue = this.subscriptionsQueue.get(options.channelId);
145
+ if (lodash_1.default.isNil(queue) || lodash_1.default.isEmpty(queue)) {
146
+ return;
147
+ }
148
+ lodash_1.default.remove(queue, function (queueItem) { return queueItem.symbol === symbol; });
149
+ };
150
+ MarketDataStreamer.prototype.sendQueuedSubscriptions = function (channelId) {
151
+ var _this = this;
152
+ var queuedSubscriptions = this.subscriptionsQueue.get(channelId);
153
+ if (lodash_1.default.isNil(queuedSubscriptions)) {
154
+ return;
155
+ }
156
+ // Clear out queue immediately
157
+ this.subscriptionsQueue.set(channelId, []);
158
+ queuedSubscriptions.forEach(function (subscription) {
159
+ _this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add');
160
+ });
161
+ };
162
+ /**
163
+ *
164
+ * @param {*} symbol
165
+ * @param {*} subscriptionTypes
166
+ * @param {*} channelId
167
+ * @param {*} direction add or remove
168
+ */
169
+ MarketDataStreamer.prototype.sendSubscriptionMessage = function (symbol, subscriptionTypes, channelId, direction) {
170
+ var _a;
171
+ var subscriptions = subscriptionTypes.map(function (type) { return ({ "symbol": symbol, "type": type }); });
172
+ this.sendMessage((_a = {
173
+ "type": "FEED_SUBSCRIPTION",
174
+ "channel": channelId
175
+ },
176
+ _a[direction] = subscriptions,
177
+ _a));
178
+ };
179
+ MarketDataStreamer.prototype.onError = function (error) {
180
+ console.error('Error received: ', error);
181
+ };
182
+ MarketDataStreamer.prototype.onOpen = function () {
183
+ this.openChannels.clear();
184
+ this.sendMessage({
185
+ "type": "SETUP",
186
+ "channel": 0,
187
+ "keepaliveTimeout": KeepaliveInterval,
188
+ "acceptKeepaliveTimeout": KeepaliveInterval,
189
+ "version": "0.1-js/1.0.0"
190
+ });
191
+ this.scheduleKeepalive();
192
+ };
193
+ MarketDataStreamer.prototype.onClose = function () {
194
+ this.webSocket = null;
195
+ this.clearKeepalive();
196
+ };
197
+ MarketDataStreamer.prototype.clearKeepalive = function () {
198
+ if (!lodash_1.default.isNil(this.keepaliveIntervalId)) {
199
+ clearInterval(this.keepaliveIntervalId);
200
+ }
201
+ this.keepaliveIntervalId = null;
202
+ };
203
+ Object.defineProperty(MarketDataStreamer.prototype, "isDxLinkAuthorized", {
204
+ get: function () {
205
+ return this.authState === 'AUTHORIZED';
206
+ },
207
+ enumerable: false,
208
+ configurable: true
209
+ });
210
+ MarketDataStreamer.prototype.handleAuthStateMessage = function (data) {
211
+ this.authState = data.state;
212
+ if (this.isDxLinkAuthorized) {
213
+ this.openFeedChannel(DefaultChannelId);
214
+ }
215
+ else {
216
+ this.sendMessage({
217
+ "type": "AUTH",
218
+ "channel": 0,
219
+ "token": this.token
220
+ });
221
+ }
222
+ };
223
+ MarketDataStreamer.prototype.handleChannelOpened = function (jsonData) {
224
+ this.openChannels.add(jsonData.channel);
225
+ this.sendQueuedSubscriptions(jsonData.channel);
226
+ };
227
+ MarketDataStreamer.prototype.notifyListeners = function (jsonData) {
228
+ this.dataListeners.forEach(function (listener) { return listener(jsonData); });
229
+ };
230
+ MarketDataStreamer.prototype.handleMessageReceived = function (data) {
231
+ var messageData = lodash_1.default.get(data, 'data', data);
232
+ var jsonData = JSON.parse(messageData);
233
+ switch (jsonData.type) {
234
+ case 'AUTH_STATE':
235
+ this.handleAuthStateMessage(jsonData);
236
+ break;
237
+ case 'CHANNEL_OPENED':
238
+ this.handleChannelOpened(jsonData);
239
+ break;
240
+ case 'FEED_DATA':
241
+ this.notifyListeners(jsonData);
242
+ break;
243
+ }
244
+ };
245
+ MarketDataStreamer.prototype.sendMessage = function (json) {
246
+ if (lodash_1.default.isNil(this.webSocket)) {
247
+ return;
248
+ }
249
+ this.webSocket.send(JSON.stringify(json));
250
+ };
251
+ return MarketDataStreamer;
252
+ }());
253
+ exports.default = MarketDataStreamer;
@@ -3,8 +3,8 @@ export default class AccountsAndCustomersService {
3
3
  private httpClient;
4
4
  constructor(httpClient: TastytradeHttpClient);
5
5
  getCustomerAccounts(): Promise<any>;
6
- getCustomerResource(customerId: string): Promise<any>;
7
- getCustomerAccountResources(customerId: string): Promise<any>;
8
- getFullCustomerAccountResource(customerId: string, accountNumber: string): Promise<any>;
9
- getQuoteStreamerTokens(): Promise<any>;
6
+ getCustomerResource(): Promise<any>;
7
+ getCustomerAccountResources(): Promise<any>;
8
+ getFullCustomerAccountResource(accountNumber: string): Promise<any>;
9
+ getApiQuoteToken(): Promise<any>;
10
10
  }
@@ -58,12 +58,12 @@ var AccountsAndCustomersService = /** @class */ (function () {
58
58
  });
59
59
  };
60
60
  //Customers: Operations about customers
61
- AccountsAndCustomersService.prototype.getCustomerResource = function (customerId) {
61
+ AccountsAndCustomersService.prototype.getCustomerResource = function () {
62
62
  return __awaiter(this, void 0, void 0, function () {
63
63
  var customerResource;
64
64
  return __generator(this, function (_a) {
65
65
  switch (_a.label) {
66
- case 0: return [4 /*yield*/, this.httpClient.getData("/customers/".concat(customerId), {}, {})];
66
+ case 0: return [4 /*yield*/, this.httpClient.getData("/customers/me", {}, {})];
67
67
  case 1:
68
68
  customerResource = (_a.sent());
69
69
  return [2 /*return*/, (0, response_util_1.default)(customerResource)];
@@ -71,12 +71,12 @@ var AccountsAndCustomersService = /** @class */ (function () {
71
71
  });
72
72
  });
73
73
  };
74
- AccountsAndCustomersService.prototype.getCustomerAccountResources = function (customerId) {
74
+ AccountsAndCustomersService.prototype.getCustomerAccountResources = function () {
75
75
  return __awaiter(this, void 0, void 0, function () {
76
76
  var customerAccountResources;
77
77
  return __generator(this, function (_a) {
78
78
  switch (_a.label) {
79
- case 0: return [4 /*yield*/, this.httpClient.getData("/customers/".concat(customerId, "/accounts"), {}, {})];
79
+ case 0: return [4 /*yield*/, this.httpClient.getData("/customers/me/accounts", {}, {})];
80
80
  case 1:
81
81
  customerAccountResources = (_a.sent());
82
82
  return [2 /*return*/, (0, response_util_1.default)(customerAccountResources)];
@@ -84,12 +84,12 @@ var AccountsAndCustomersService = /** @class */ (function () {
84
84
  });
85
85
  });
86
86
  };
87
- AccountsAndCustomersService.prototype.getFullCustomerAccountResource = function (customerId, accountNumber) {
87
+ AccountsAndCustomersService.prototype.getFullCustomerAccountResource = function (accountNumber) {
88
88
  return __awaiter(this, void 0, void 0, function () {
89
89
  var fullCustomerAccountResource;
90
90
  return __generator(this, function (_a) {
91
91
  switch (_a.label) {
92
- case 0: return [4 /*yield*/, this.httpClient.getData("/customers/".concat(customerId, "/accounts/").concat(accountNumber), {}, {})];
92
+ case 0: return [4 /*yield*/, this.httpClient.getData("/customers/me/accounts/".concat(accountNumber), {}, {})];
93
93
  case 1:
94
94
  fullCustomerAccountResource = (_a.sent());
95
95
  return [2 /*return*/, (0, response_util_1.default)(fullCustomerAccountResource)];
@@ -97,16 +97,16 @@ var AccountsAndCustomersService = /** @class */ (function () {
97
97
  });
98
98
  });
99
99
  };
100
- //Quote-streamer-tokens: Operations about quote-streamer-tokens
101
- AccountsAndCustomersService.prototype.getQuoteStreamerTokens = function () {
100
+ //Returns the appropriate quote streamer endpoint, level and identification token for the current customer to receive market data.
101
+ AccountsAndCustomersService.prototype.getApiQuoteToken = function () {
102
102
  return __awaiter(this, void 0, void 0, function () {
103
- var quoteStreamerTokens;
103
+ var apiQuoteToken;
104
104
  return __generator(this, function (_a) {
105
105
  switch (_a.label) {
106
- case 0: return [4 /*yield*/, this.httpClient.getData('/quote-streamer-tokens', {}, {})];
106
+ case 0: return [4 /*yield*/, this.httpClient.getData('/api-quote-tokens', {}, {})];
107
107
  case 1:
108
- quoteStreamerTokens = (_a.sent());
109
- return [2 /*return*/, (0, response_util_1.default)(quoteStreamerTokens)];
108
+ apiQuoteToken = (_a.sent());
109
+ return [2 /*return*/, (0, response_util_1.default)(apiQuoteToken)];
110
110
  }
111
111
  });
112
112
  });
@@ -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 QuoteStreamer from "./quote-streamer";
3
+ import MarketDataStreamer, { 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 { QuoteStreamer };
39
+ export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener };
40
40
  export { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver };
@@ -1,15 +1,39 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.STREAMER_STATE = exports.AccountStreamer = exports.QuoteStreamer = void 0;
29
+ exports.STREAMER_STATE = exports.AccountStreamer = exports.MarketDataSubscriptionType = exports.MarketDataStreamer = void 0;
7
30
  var tastytrade_http_client_1 = __importDefault(require("./services/tastytrade-http-client"));
8
31
  var account_streamer_1 = require("./account-streamer");
9
32
  Object.defineProperty(exports, "AccountStreamer", { enumerable: true, get: function () { return account_streamer_1.AccountStreamer; } });
10
33
  Object.defineProperty(exports, "STREAMER_STATE", { enumerable: true, get: function () { return account_streamer_1.STREAMER_STATE; } });
11
- var quote_streamer_1 = __importDefault(require("./quote-streamer"));
12
- exports.QuoteStreamer = quote_streamer_1.default;
34
+ var market_data_streamer_1 = __importStar(require("./market-data-streamer"));
35
+ exports.MarketDataStreamer = market_data_streamer_1.default;
36
+ Object.defineProperty(exports, "MarketDataSubscriptionType", { enumerable: true, get: function () { return market_data_streamer_1.MarketDataSubscriptionType; } });
13
37
  //Services:
14
38
  var session_service_1 = __importDefault(require("./services/session-service"));
15
39
  var account_status_service_1 = __importDefault(require("./services/account-status-service"));
@@ -26,7 +26,7 @@ export type Disposer = () => void
26
26
 
27
27
  export type StreamerStateObserver = (streamerState: STREAMER_STATE) => void
28
28
 
29
- export type StreamerMessageObserver = (messageType: string, action: string, status: string) => void
29
+ export type StreamerMessageObserver = (json: object) => void
30
30
 
31
31
  const REQUEST_ID = 'request-id'
32
32
 
@@ -368,7 +368,7 @@ export class AccountStreamer {
368
368
  this.logger.info(json)
369
369
 
370
370
  const action = json.action as string
371
- this.streamerMessageObservers.forEach(observer => observer(json.type as string, action, json.status as string))
371
+ this.streamerMessageObservers.forEach(observer => observer(json))
372
372
  if (action) {
373
373
  if (action === MessageAction.HEARTBEAT) {
374
374
  // schedule next heartbeat
@@ -0,0 +1,273 @@
1
+ import WebSocket from 'isomorphic-ws'
2
+ import _ from 'lodash'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+
5
+ export enum MarketDataSubscriptionType {
6
+ Quote = 'Quote',
7
+ Trade = 'Trade',
8
+ Summary = 'Summary',
9
+ Profile = 'Profile',
10
+ Greeks = 'Greeks',
11
+ Underlying = 'Underlying'
12
+ }
13
+
14
+ export type MarketDataListener = (data: any) => void
15
+
16
+ type QueuedSubscription = { symbol: string, subscriptionTypes: MarketDataSubscriptionType[] }
17
+ type SubscriptionOptions = { subscriptionTypes: MarketDataSubscriptionType[], channelId: number }
18
+
19
+ const AllSubscriptionTypes = Object.values(MarketDataSubscriptionType)
20
+
21
+ const KeepaliveInterval = 30000 // 30 seconds
22
+
23
+ const DefaultChannelId = 1
24
+
25
+ export default class MarketDataStreamer {
26
+ private webSocket: WebSocket | null = null
27
+ private token = ''
28
+ private keepaliveIntervalId: any | null = null
29
+ private dataListeners = new Map()
30
+ private openChannels = new Set()
31
+ private subscriptionsQueue: Map<number, QueuedSubscription[]> = new Map()
32
+ private authState = ''
33
+
34
+ addDataListener(dataListener: MarketDataListener) {
35
+ if (_.isNil(dataListener)) {
36
+ return _.noop
37
+ }
38
+ const guid = uuidv4()
39
+ this.dataListeners.set(guid, dataListener)
40
+
41
+ return () => this.dataListeners.delete(guid)
42
+ }
43
+
44
+ connect(url: string, token: string) {
45
+ if (this.isConnected) {
46
+ throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected')
47
+ }
48
+
49
+ this.token = token
50
+ this.webSocket = new WebSocket(url)
51
+ this.webSocket.onopen = this.onOpen.bind(this)
52
+ this.webSocket.onerror = this.onError.bind(this)
53
+ this.webSocket.onmessage = this.handleMessageReceived.bind(this)
54
+ this.webSocket.onclose = this.onClose.bind(this)
55
+ }
56
+
57
+ disconnect() {
58
+ if (_.isNil(this.webSocket)) {
59
+ return
60
+ }
61
+
62
+ this.clearKeepalive()
63
+
64
+ this.webSocket.close()
65
+ this.webSocket = null
66
+
67
+ this.openChannels.clear()
68
+ this.subscriptionsQueue.clear()
69
+ this.authState = ''
70
+ }
71
+
72
+ // TODO: add listener to options, return unsubscriber
73
+ addSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) {
74
+ const { subscriptionTypes, channelId } = options
75
+ const isOpen = this.isChannelOpened(channelId)
76
+ if (isOpen) {
77
+ this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'add')
78
+ } else {
79
+ this.queueSubscription(symbol, options)
80
+ }
81
+ }
82
+
83
+ removeSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) {
84
+ const { subscriptionTypes, channelId } = options
85
+ const isOpen = this.isChannelOpened(channelId)
86
+ if (isOpen) {
87
+ this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove')
88
+ } else {
89
+ this.dequeueSubscription(symbol, options)
90
+ }
91
+ }
92
+
93
+ removeAllSubscriptions(channelId = DefaultChannelId) {
94
+ const isOpen = this.isChannelOpened(channelId)
95
+ if (isOpen) {
96
+ this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true })
97
+ } else {
98
+ this.subscriptionsQueue.set(channelId, [])
99
+ }
100
+ }
101
+
102
+ openFeedChannel(channelId: number) {
103
+ if (!this.isDxLinkAuthorized) {
104
+ throw new Error(`Unable to open channel ${channelId} due to DxLink authorization state: ${this.authState}`)
105
+ }
106
+
107
+ if (this.isChannelOpened(channelId)) {
108
+ return
109
+ }
110
+
111
+ this.sendMessage({
112
+ "type": "CHANNEL_REQUEST",
113
+ "channel": channelId,
114
+ "service": "FEED",
115
+ "parameters": {
116
+ "contract": "AUTO"
117
+ }
118
+ })
119
+ }
120
+
121
+ isChannelOpened(channelId: number) {
122
+ return this.isConnected && this.openChannels.has(channelId)
123
+ }
124
+
125
+ get isConnected() {
126
+ return !_.isNil(this.webSocket)
127
+ }
128
+
129
+ private scheduleKeepalive() {
130
+ this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval)
131
+ }
132
+
133
+ private sendKeepalive() {
134
+ if (_.isNil(this.keepaliveIntervalId)) {
135
+ return
136
+ }
137
+
138
+ this.sendMessage({
139
+ "type": "KEEPALIVE",
140
+ "channel": 0
141
+ })
142
+ }
143
+
144
+ private queueSubscription(symbol: string, options: SubscriptionOptions) {
145
+ const { subscriptionTypes, channelId } = options
146
+ let queue = this.subscriptionsQueue.get(options.channelId)
147
+ if (_.isNil(queue)) {
148
+ queue = []
149
+ this.subscriptionsQueue.set(channelId, queue)
150
+ }
151
+
152
+ queue.push({ symbol, subscriptionTypes })
153
+ }
154
+
155
+ private dequeueSubscription(symbol: string, options: SubscriptionOptions) {
156
+ const queue = this.subscriptionsQueue.get(options.channelId)
157
+ if (_.isNil(queue) || _.isEmpty(queue)) {
158
+ return
159
+ }
160
+
161
+ _.remove(queue, (queueItem: any) => queueItem.symbol === symbol)
162
+ }
163
+
164
+ private sendQueuedSubscriptions(channelId: number) {
165
+ const queuedSubscriptions = this.subscriptionsQueue.get(channelId)
166
+ if (_.isNil(queuedSubscriptions)) {
167
+ return
168
+ }
169
+
170
+ // Clear out queue immediately
171
+ this.subscriptionsQueue.set(channelId, [])
172
+ queuedSubscriptions.forEach(subscription => {
173
+ this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add')
174
+ })
175
+ }
176
+
177
+ /**
178
+ *
179
+ * @param {*} symbol
180
+ * @param {*} subscriptionTypes
181
+ * @param {*} channelId
182
+ * @param {*} direction add or remove
183
+ */
184
+ private sendSubscriptionMessage(symbol: string, subscriptionTypes: MarketDataSubscriptionType[], channelId: number, direction: string) {
185
+ const subscriptions = subscriptionTypes.map(type => ({ "symbol": symbol, "type": type }))
186
+ this.sendMessage({
187
+ "type": "FEED_SUBSCRIPTION",
188
+ "channel": channelId,
189
+ [direction]: subscriptions
190
+ })
191
+ }
192
+
193
+ private onError(error: any) {
194
+ console.error('Error received: ', error)
195
+ }
196
+
197
+ private onOpen() {
198
+ this.openChannels.clear()
199
+
200
+ this.sendMessage({
201
+ "type": "SETUP",
202
+ "channel": 0,
203
+ "keepaliveTimeout": KeepaliveInterval,
204
+ "acceptKeepaliveTimeout": KeepaliveInterval,
205
+ "version": "0.1-js/1.0.0"
206
+ })
207
+
208
+ this.scheduleKeepalive()
209
+ }
210
+
211
+ private onClose() {
212
+ this.webSocket = null
213
+ this.clearKeepalive()
214
+ }
215
+
216
+ private clearKeepalive() {
217
+ if (!_.isNil(this.keepaliveIntervalId)) {
218
+ clearInterval(this.keepaliveIntervalId)
219
+ }
220
+
221
+ this.keepaliveIntervalId = null
222
+ }
223
+
224
+ get isDxLinkAuthorized() {
225
+ return this.authState === 'AUTHORIZED'
226
+ }
227
+
228
+ private handleAuthStateMessage(data: any) {
229
+ this.authState = data.state
230
+ if (this.isDxLinkAuthorized) {
231
+ this.openFeedChannel(DefaultChannelId)
232
+ } else {
233
+ this.sendMessage({
234
+ "type": "AUTH",
235
+ "channel": 0,
236
+ "token": this.token
237
+ })
238
+ }
239
+ }
240
+
241
+ private handleChannelOpened(jsonData: any) {
242
+ this.openChannels.add(jsonData.channel)
243
+ this.sendQueuedSubscriptions(jsonData.channel)
244
+ }
245
+
246
+ private notifyListeners(jsonData: any) {
247
+ this.dataListeners.forEach(listener => listener(jsonData))
248
+ }
249
+
250
+ private handleMessageReceived(data: string) {
251
+ const messageData = _.get(data, 'data', data)
252
+ const jsonData = JSON.parse(messageData)
253
+ switch (jsonData.type) {
254
+ case 'AUTH_STATE':
255
+ this.handleAuthStateMessage(jsonData)
256
+ break
257
+ case 'CHANNEL_OPENED':
258
+ this.handleChannelOpened(jsonData)
259
+ break
260
+ case 'FEED_DATA':
261
+ this.notifyListeners(jsonData)
262
+ break
263
+ }
264
+ }
265
+
266
+ private sendMessage(json: object) {
267
+ if (_.isNil(this.webSocket)) {
268
+ return
269
+ }
270
+
271
+ this.webSocket.send(JSON.stringify(json))
272
+ }
273
+ }
@@ -10,26 +10,25 @@ export default class AccountsAndCustomersService {
10
10
  }
11
11
 
12
12
  //Customers: Operations about customers
13
- async getCustomerResource(customerId: string){
13
+ async getCustomerResource(){
14
14
  //Get a full customer resource.
15
- const customerResource = (await this.httpClient.getData(`/customers/${customerId}`, {}, {}))
15
+ const customerResource = (await this.httpClient.getData(`/customers/me`, {}, {}))
16
16
  return extractResponseData(customerResource)
17
17
  }
18
- async getCustomerAccountResources(customerId: string){
18
+ async getCustomerAccountResources(){
19
19
  //Get a list of all the customer account resources attached to the current customer.
20
- const customerAccountResources = (await this.httpClient.getData(`/customers/${customerId}/accounts`, {}, {}))
20
+ const customerAccountResources = (await this.httpClient.getData(`/customers/me/accounts`, {}, {}))
21
21
  return extractResponseData(customerAccountResources)
22
22
  }
23
- async getFullCustomerAccountResource(customerId: string, accountNumber: string){
23
+ async getFullCustomerAccountResource(accountNumber: string){
24
24
  //Get a full customer account resource.
25
- const fullCustomerAccountResource = (await this.httpClient.getData(`/customers/${customerId}/accounts/${accountNumber}`, {}, {}))
25
+ const fullCustomerAccountResource = (await this.httpClient.getData(`/customers/me/accounts/${accountNumber}`, {}, {}))
26
26
  return extractResponseData(fullCustomerAccountResource)
27
27
  }
28
28
 
29
- //Quote-streamer-tokens: Operations about quote-streamer-tokens
30
- async getQuoteStreamerTokens(){
31
- //Returns the appropriate quote streamer endpoint, level and identification token for the current customer to receive market data.
32
- const quoteStreamerTokens = (await this.httpClient.getData('/quote-streamer-tokens', {}, {}))
33
- return extractResponseData(quoteStreamerTokens)
29
+ //Returns the appropriate quote streamer endpoint, level and identification token for the current customer to receive market data.
30
+ async getApiQuoteToken() {
31
+ const apiQuoteToken = (await this.httpClient.getData('/api-quote-tokens', {}, {}))
32
+ return extractResponseData(apiQuoteToken)
34
33
  }
35
34
  }
@@ -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 QuoteStreamer from "./quote-streamer"
3
+ import MarketDataStreamer, { 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 { QuoteStreamer }
64
+ export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener }
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": "0.0.1",
3
+ "version": "2.0.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",
@@ -11,19 +11,24 @@
11
11
  "test": "jest -i --restoreMocks",
12
12
  "unit-test": "jest --testPathPattern=tests/unit",
13
13
  "integration-test": "jest --testPathPattern=tests/integration",
14
- "lint": "eslint lib/** tests/**"
14
+ "lint": "eslint lib/** tests/**",
15
+ "prepublishOnly": "npm run test && npm run build",
16
+ "postpack": "git tag -a $npm_package_version -m $npm_package_version && git push origin $npm_package_version"
15
17
  },
16
18
  "dependencies": {
17
- "@dxfeed/api": "^1.1.0",
18
19
  "@types/lodash": "^4.14.182",
19
20
  "@types/qs": "^6.9.7",
20
21
  "axios": "^1.3.4",
22
+ "isomorphic-ws": "^5.0.0",
21
23
  "lodash": "^4.17.21",
22
- "qs": "^6.11.1"
24
+ "qs": "^6.11.1",
25
+ "uuid": "^9.0.0",
26
+ "ws": "^8.13.0"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@types/jest": "^29.5.0",
26
30
  "@types/node": "17.0.27",
31
+ "@types/uuid": "^9.0.2",
27
32
  "@typescript-eslint/eslint-plugin": "^5.57.1",
28
33
  "@typescript-eslint/parser": "^5.57.1",
29
34
  "dotenv": "^16.0.3",
@@ -1,40 +0,0 @@
1
- import Feed, { EventType, IEvent } from '@dxfeed/api'
2
- import _ from 'lodash'
3
-
4
- export const SupportedEventTypes = [
5
- EventType.Quote,
6
- EventType.Trade,
7
- EventType.Summary,
8
- EventType.Greeks,
9
- EventType.Profile
10
- ]
11
-
12
- export default class QuoteStreamer {
13
- private feed: Feed | null = null
14
-
15
- constructor(private readonly token: string, private readonly url: string) {}
16
-
17
- connect() {
18
- this.feed = new Feed()
19
- this.feed.setAuthToken(this.token)
20
- this.feed.connect(this.url)
21
- }
22
-
23
- disconnect() {
24
- if (!_.isNil(this.feed)) {
25
- this.feed.disconnect()
26
- }
27
- }
28
-
29
- subscribe(dxfeedSymbol: string, eventHandler: (event: IEvent) => void): () => void {
30
- if (_.isNil(this.feed)) {
31
- return _.noop
32
- }
33
-
34
- return this.feed.subscribe(
35
- SupportedEventTypes,
36
- [dxfeedSymbol],
37
- eventHandler
38
- )
39
- }
40
- }