@tastytrade/api 1.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/.env +1 -2
- package/README.md +25 -9
- package/dist/market-data-streamer.d.ts +95 -0
- package/dist/market-data-streamer.js +345 -0
- package/dist/services/accounts-and-customers-service.d.ts +1 -1
- package/dist/services/accounts-and-customers-service.js +6 -6
- package/dist/tastytrade-api.d.ts +2 -2
- package/dist/tastytrade-api.js +28 -3
- package/lib/market-data-streamer.ts +369 -0
- package/lib/services/accounts-and-customers-service.ts +4 -5
- package/lib/tastytrade-api.ts +2 -2
- package/package.json +9 -4
- package/lib/quote-streamer.ts +0 -40
package/.env
CHANGED
package/README.md
CHANGED
|
@@ -18,30 +18,36 @@ const accountPositions = await tastytradeClient.balancesAndPositionsService.getP
|
|
|
18
18
|
|
|
19
19
|
### Market Data
|
|
20
20
|
```js
|
|
21
|
-
import TastytradeClient, {
|
|
21
|
+
import TastytradeClient, { MarketDataStreamer, MarketDataSubscriptionType } from "@tastytrade-api"
|
|
22
22
|
const tastytradeClient = new TastytradeClient(baseUrl, accountStreamerUrl)
|
|
23
23
|
await tastytradeClient.sessionService.login(usernameOrEmail, pasword)
|
|
24
|
-
const tokenResponse = await tastytradeClient.AccountsAndCustomersService.
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const tokenResponse = await tastytradeClient.AccountsAndCustomersService.getApiQuoteToken()
|
|
25
|
+
const streamer = new MarketDataStreamer()
|
|
26
|
+
streamer.connect(tokenResponse['dxlink-url'], tokenResponse.token)
|
|
27
27
|
|
|
28
|
-
function handleMarketDataReceived(
|
|
28
|
+
function handleMarketDataReceived(data) {
|
|
29
29
|
// Triggers every time market data event occurs
|
|
30
|
-
console.log(
|
|
30
|
+
console.log(data)
|
|
31
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
|
+
|
|
32
36
|
// Subscribe to a single equity quote
|
|
33
|
-
|
|
37
|
+
streamer.addSubscription('AAPL')
|
|
38
|
+
// Optionally specify which market data events you want to subscribe to
|
|
39
|
+
streamer.addSubscription('SPY', { subscriptionTypes: [MarketDataSubscriptionType.Quote] })
|
|
34
40
|
|
|
35
41
|
// Subscribe to a single equity option quote
|
|
36
42
|
const optionChain = await tastytradeClient.instrumentsService.getOptionChain('AAPL')
|
|
37
|
-
|
|
43
|
+
streamer.addSubscription(optionChain[0]['streamer-symbol'])
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
### Account Streamer
|
|
41
47
|
```js
|
|
42
48
|
const TastytradeApi = require("@tastytrade/api")
|
|
43
49
|
const TastytradeClient = TastytradeApi.default
|
|
44
|
-
const { AccountStreamer
|
|
50
|
+
const { AccountStreamer } = TastytradeApi
|
|
45
51
|
const _ = require('lodash')
|
|
46
52
|
|
|
47
53
|
function handleStreamerMessage(json) {
|
|
@@ -79,6 +85,16 @@ global.window = { WebSocket, setTimeout, clearTimeout }
|
|
|
79
85
|
`npm run build`
|
|
80
86
|
Outputs everything to `dist/`
|
|
81
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
|
+
|
|
82
98
|
## Running example app
|
|
83
99
|
```sh
|
|
84
100
|
npm run build
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export declare enum MarketDataSubscriptionType {
|
|
2
|
+
Candle = "Candle",
|
|
3
|
+
Quote = "Quote",
|
|
4
|
+
Trade = "Trade",
|
|
5
|
+
Summary = "Summary",
|
|
6
|
+
Profile = "Profile",
|
|
7
|
+
Greeks = "Greeks",
|
|
8
|
+
Underlying = "Underlying"
|
|
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
|
+
}
|
|
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;
|
|
32
|
+
export default class MarketDataStreamer {
|
|
33
|
+
private webSocket;
|
|
34
|
+
private token;
|
|
35
|
+
private keepaliveIntervalId;
|
|
36
|
+
private dataListeners;
|
|
37
|
+
private openChannels;
|
|
38
|
+
private subscriptionsQueue;
|
|
39
|
+
private authState;
|
|
40
|
+
private errorListeners;
|
|
41
|
+
private authStateListeners;
|
|
42
|
+
addDataListener(dataListener: MarketDataListener, channelId?: number | null): Remover;
|
|
43
|
+
addErrorListener(errorListener: ErrorListener): Remover;
|
|
44
|
+
addAuthStateChangeListener(authStateListener: AuthStateListener): Remover;
|
|
45
|
+
connect(url: string, token: string): void;
|
|
46
|
+
disconnect(): void;
|
|
47
|
+
addSubscription(symbol: string, options?: {
|
|
48
|
+
subscriptionTypes: MarketDataSubscriptionType[];
|
|
49
|
+
channelId: number;
|
|
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;
|
|
61
|
+
removeSubscription(symbol: string, options?: {
|
|
62
|
+
subscriptionTypes: MarketDataSubscriptionType[];
|
|
63
|
+
channelId: number;
|
|
64
|
+
}): void;
|
|
65
|
+
removeAllSubscriptions(channelId?: number): void;
|
|
66
|
+
openFeedChannel(channelId: number): void;
|
|
67
|
+
isChannelOpened(channelId: number): boolean;
|
|
68
|
+
get isReadyToOpenChannels(): boolean;
|
|
69
|
+
get isConnected(): boolean;
|
|
70
|
+
private scheduleKeepalive;
|
|
71
|
+
private sendKeepalive;
|
|
72
|
+
private queueSubscription;
|
|
73
|
+
private dequeueSubscription;
|
|
74
|
+
private sendQueuedSubscriptions;
|
|
75
|
+
/**
|
|
76
|
+
*
|
|
77
|
+
* @param {*} symbol
|
|
78
|
+
* @param {*} subscriptionTypes
|
|
79
|
+
* @param {*} channelId
|
|
80
|
+
* @param {*} direction add or remove
|
|
81
|
+
*/
|
|
82
|
+
private sendSubscriptionMessage;
|
|
83
|
+
private onError;
|
|
84
|
+
private onOpen;
|
|
85
|
+
private onClose;
|
|
86
|
+
private clearKeepalive;
|
|
87
|
+
get isDxLinkAuthorized(): boolean;
|
|
88
|
+
private handleAuthStateMessage;
|
|
89
|
+
private handleChannelOpened;
|
|
90
|
+
private notifyListeners;
|
|
91
|
+
private notifyErrorListeners;
|
|
92
|
+
private handleMessageReceived;
|
|
93
|
+
private sendMessage;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
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.CandleType = 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["Candle"] = "Candle";
|
|
13
|
+
MarketDataSubscriptionType["Quote"] = "Quote";
|
|
14
|
+
MarketDataSubscriptionType["Trade"] = "Trade";
|
|
15
|
+
MarketDataSubscriptionType["Summary"] = "Summary";
|
|
16
|
+
MarketDataSubscriptionType["Profile"] = "Profile";
|
|
17
|
+
MarketDataSubscriptionType["Greeks"] = "Greeks";
|
|
18
|
+
MarketDataSubscriptionType["Underlying"] = "Underlying";
|
|
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
|
|
35
|
+
var AllSubscriptionTypes = Object.values(MarketDataSubscriptionType);
|
|
36
|
+
var KeepaliveInterval = 30000; // 30 seconds
|
|
37
|
+
var DefaultChannelId = 1;
|
|
38
|
+
var MarketDataStreamer = /** @class */ (function () {
|
|
39
|
+
function MarketDataStreamer() {
|
|
40
|
+
this.webSocket = null;
|
|
41
|
+
this.token = '';
|
|
42
|
+
this.keepaliveIntervalId = null;
|
|
43
|
+
this.dataListeners = new Map();
|
|
44
|
+
this.openChannels = new Set();
|
|
45
|
+
this.subscriptionsQueue = new Map();
|
|
46
|
+
this.authState = '';
|
|
47
|
+
this.errorListeners = new Map();
|
|
48
|
+
this.authStateListeners = new Map();
|
|
49
|
+
}
|
|
50
|
+
MarketDataStreamer.prototype.addDataListener = function (dataListener, channelId) {
|
|
51
|
+
var _this = this;
|
|
52
|
+
if (channelId === void 0) { channelId = null; }
|
|
53
|
+
if (lodash_1.default.isNil(dataListener)) {
|
|
54
|
+
return lodash_1.default.noop;
|
|
55
|
+
}
|
|
56
|
+
var guid = (0, uuid_1.v4)();
|
|
57
|
+
this.dataListeners.set(guid, { listener: dataListener, channelId: channelId });
|
|
58
|
+
return function () { return _this.dataListeners.delete(guid); };
|
|
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
|
+
};
|
|
78
|
+
MarketDataStreamer.prototype.connect = function (url, token) {
|
|
79
|
+
if (this.isConnected) {
|
|
80
|
+
throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected');
|
|
81
|
+
}
|
|
82
|
+
this.token = token;
|
|
83
|
+
this.webSocket = new isomorphic_ws_1.default(url);
|
|
84
|
+
this.webSocket.onopen = this.onOpen.bind(this);
|
|
85
|
+
this.webSocket.onerror = this.onError.bind(this);
|
|
86
|
+
this.webSocket.onmessage = this.handleMessageReceived.bind(this);
|
|
87
|
+
this.webSocket.onclose = this.onClose.bind(this);
|
|
88
|
+
};
|
|
89
|
+
MarketDataStreamer.prototype.disconnect = function () {
|
|
90
|
+
if (lodash_1.default.isNil(this.webSocket)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.clearKeepalive();
|
|
94
|
+
this.webSocket.onopen = null;
|
|
95
|
+
this.webSocket.onerror = null;
|
|
96
|
+
this.webSocket.onmessage = null;
|
|
97
|
+
this.webSocket.onclose = null;
|
|
98
|
+
this.webSocket.close();
|
|
99
|
+
this.webSocket = null;
|
|
100
|
+
this.openChannels.clear();
|
|
101
|
+
this.subscriptionsQueue.clear();
|
|
102
|
+
this.authState = '';
|
|
103
|
+
};
|
|
104
|
+
MarketDataStreamer.prototype.addSubscription = function (symbol, options) {
|
|
105
|
+
var _this = this;
|
|
106
|
+
if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
|
|
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, "}");
|
|
137
|
+
var isOpen = this.isChannelOpened(channelId);
|
|
138
|
+
var subscriptionArgs = { fromTime: fromTime };
|
|
139
|
+
if (isOpen) {
|
|
140
|
+
this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.queueSubscription(candleSymbol, { subscriptionTypes: subscriptionTypes, channelId: channelId, subscriptionArgs: subscriptionArgs });
|
|
144
|
+
}
|
|
145
|
+
return function () {
|
|
146
|
+
_this.removeSubscription(candleSymbol, { subscriptionTypes: subscriptionTypes, channelId: channelId });
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
MarketDataStreamer.prototype.removeSubscription = function (symbol, options) {
|
|
150
|
+
if (options === void 0) { options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }; }
|
|
151
|
+
var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId;
|
|
152
|
+
var isOpen = this.isChannelOpened(channelId);
|
|
153
|
+
if (isOpen) {
|
|
154
|
+
this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove');
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.dequeueSubscription(symbol, options);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
MarketDataStreamer.prototype.removeAllSubscriptions = function (channelId) {
|
|
161
|
+
if (channelId === void 0) { channelId = DefaultChannelId; }
|
|
162
|
+
var isOpen = this.isChannelOpened(channelId);
|
|
163
|
+
if (isOpen) {
|
|
164
|
+
this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.subscriptionsQueue.set(channelId, []);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
MarketDataStreamer.prototype.openFeedChannel = function (channelId) {
|
|
171
|
+
if (!this.isReadyToOpenChannels) {
|
|
172
|
+
throw new Error("Unable to open channel ".concat(channelId, " due to DxLink authorization state: ").concat(this.authState));
|
|
173
|
+
}
|
|
174
|
+
if (this.isChannelOpened(channelId)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.sendMessage({
|
|
178
|
+
"type": "CHANNEL_REQUEST",
|
|
179
|
+
"channel": channelId,
|
|
180
|
+
"service": "FEED",
|
|
181
|
+
"parameters": {
|
|
182
|
+
"contract": "AUTO"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
MarketDataStreamer.prototype.isChannelOpened = function (channelId) {
|
|
187
|
+
return this.isConnected && this.openChannels.has(channelId);
|
|
188
|
+
};
|
|
189
|
+
Object.defineProperty(MarketDataStreamer.prototype, "isReadyToOpenChannels", {
|
|
190
|
+
get: function () {
|
|
191
|
+
return this.isConnected && this.isDxLinkAuthorized;
|
|
192
|
+
},
|
|
193
|
+
enumerable: false,
|
|
194
|
+
configurable: true
|
|
195
|
+
});
|
|
196
|
+
Object.defineProperty(MarketDataStreamer.prototype, "isConnected", {
|
|
197
|
+
get: function () {
|
|
198
|
+
return !lodash_1.default.isNil(this.webSocket);
|
|
199
|
+
},
|
|
200
|
+
enumerable: false,
|
|
201
|
+
configurable: true
|
|
202
|
+
});
|
|
203
|
+
MarketDataStreamer.prototype.scheduleKeepalive = function () {
|
|
204
|
+
this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval);
|
|
205
|
+
};
|
|
206
|
+
MarketDataStreamer.prototype.sendKeepalive = function () {
|
|
207
|
+
if (lodash_1.default.isNil(this.keepaliveIntervalId)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.sendMessage({
|
|
211
|
+
"type": "KEEPALIVE",
|
|
212
|
+
"channel": 0
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
MarketDataStreamer.prototype.queueSubscription = function (symbol, options) {
|
|
216
|
+
var subscriptionTypes = options.subscriptionTypes, channelId = options.channelId, subscriptionArgs = options.subscriptionArgs;
|
|
217
|
+
var queue = this.subscriptionsQueue.get(options.channelId);
|
|
218
|
+
if (lodash_1.default.isNil(queue)) {
|
|
219
|
+
queue = [];
|
|
220
|
+
this.subscriptionsQueue.set(channelId, queue);
|
|
221
|
+
}
|
|
222
|
+
queue.push({ symbol: symbol, subscriptionTypes: subscriptionTypes, subscriptionArgs: subscriptionArgs });
|
|
223
|
+
};
|
|
224
|
+
MarketDataStreamer.prototype.dequeueSubscription = function (symbol, options) {
|
|
225
|
+
var queue = this.subscriptionsQueue.get(options.channelId);
|
|
226
|
+
if (lodash_1.default.isNil(queue) || lodash_1.default.isEmpty(queue)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
lodash_1.default.remove(queue, function (queueItem) { return queueItem.symbol === symbol; });
|
|
230
|
+
};
|
|
231
|
+
MarketDataStreamer.prototype.sendQueuedSubscriptions = function (channelId) {
|
|
232
|
+
var _this = this;
|
|
233
|
+
var queuedSubscriptions = this.subscriptionsQueue.get(channelId);
|
|
234
|
+
if (lodash_1.default.isNil(queuedSubscriptions)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Clear out queue immediately
|
|
238
|
+
this.subscriptionsQueue.set(channelId, []);
|
|
239
|
+
queuedSubscriptions.forEach(function (subscription) {
|
|
240
|
+
_this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add', subscription.subscriptionArgs);
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
*
|
|
245
|
+
* @param {*} symbol
|
|
246
|
+
* @param {*} subscriptionTypes
|
|
247
|
+
* @param {*} channelId
|
|
248
|
+
* @param {*} direction add or remove
|
|
249
|
+
*/
|
|
250
|
+
MarketDataStreamer.prototype.sendSubscriptionMessage = function (symbol, subscriptionTypes, channelId, direction, subscriptionArgs) {
|
|
251
|
+
var _a;
|
|
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 : {})); });
|
|
254
|
+
this.sendMessage((_a = {
|
|
255
|
+
"type": "FEED_SUBSCRIPTION",
|
|
256
|
+
"channel": channelId
|
|
257
|
+
},
|
|
258
|
+
_a[direction] = subscriptions,
|
|
259
|
+
_a));
|
|
260
|
+
};
|
|
261
|
+
MarketDataStreamer.prototype.onError = function (error) {
|
|
262
|
+
console.error('Error received: ', error);
|
|
263
|
+
this.notifyErrorListeners(error);
|
|
264
|
+
};
|
|
265
|
+
MarketDataStreamer.prototype.onOpen = function () {
|
|
266
|
+
this.openChannels.clear();
|
|
267
|
+
this.sendMessage({
|
|
268
|
+
"type": "SETUP",
|
|
269
|
+
"channel": 0,
|
|
270
|
+
"keepaliveTimeout": KeepaliveInterval,
|
|
271
|
+
"acceptKeepaliveTimeout": KeepaliveInterval,
|
|
272
|
+
"version": "0.1-js/1.0.0"
|
|
273
|
+
});
|
|
274
|
+
this.scheduleKeepalive();
|
|
275
|
+
};
|
|
276
|
+
MarketDataStreamer.prototype.onClose = function () {
|
|
277
|
+
this.webSocket = null;
|
|
278
|
+
this.clearKeepalive();
|
|
279
|
+
};
|
|
280
|
+
MarketDataStreamer.prototype.clearKeepalive = function () {
|
|
281
|
+
if (!lodash_1.default.isNil(this.keepaliveIntervalId)) {
|
|
282
|
+
clearInterval(this.keepaliveIntervalId);
|
|
283
|
+
}
|
|
284
|
+
this.keepaliveIntervalId = null;
|
|
285
|
+
};
|
|
286
|
+
Object.defineProperty(MarketDataStreamer.prototype, "isDxLinkAuthorized", {
|
|
287
|
+
get: function () {
|
|
288
|
+
return this.authState === 'AUTHORIZED';
|
|
289
|
+
},
|
|
290
|
+
enumerable: false,
|
|
291
|
+
configurable: true
|
|
292
|
+
});
|
|
293
|
+
MarketDataStreamer.prototype.handleAuthStateMessage = function (data) {
|
|
294
|
+
var _this = this;
|
|
295
|
+
this.authState = data.state;
|
|
296
|
+
this.authStateListeners.forEach(function (listener) { return listener(_this.isDxLinkAuthorized); });
|
|
297
|
+
if (this.isDxLinkAuthorized) {
|
|
298
|
+
this.openFeedChannel(DefaultChannelId);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.sendMessage({
|
|
302
|
+
"type": "AUTH",
|
|
303
|
+
"channel": 0,
|
|
304
|
+
"token": this.token
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
MarketDataStreamer.prototype.handleChannelOpened = function (jsonData) {
|
|
309
|
+
this.openChannels.add(jsonData.channel);
|
|
310
|
+
this.sendQueuedSubscriptions(jsonData.channel);
|
|
311
|
+
};
|
|
312
|
+
MarketDataStreamer.prototype.notifyListeners = function (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); });
|
|
321
|
+
};
|
|
322
|
+
MarketDataStreamer.prototype.handleMessageReceived = function (data) {
|
|
323
|
+
var messageData = lodash_1.default.get(data, 'data', data);
|
|
324
|
+
var jsonData = JSON.parse(messageData);
|
|
325
|
+
switch (jsonData.type) {
|
|
326
|
+
case 'AUTH_STATE':
|
|
327
|
+
this.handleAuthStateMessage(jsonData);
|
|
328
|
+
break;
|
|
329
|
+
case 'CHANNEL_OPENED':
|
|
330
|
+
this.handleChannelOpened(jsonData);
|
|
331
|
+
break;
|
|
332
|
+
case 'FEED_DATA':
|
|
333
|
+
this.notifyListeners(jsonData);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
MarketDataStreamer.prototype.sendMessage = function (json) {
|
|
338
|
+
if (lodash_1.default.isNil(this.webSocket)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.webSocket.send(JSON.stringify(json));
|
|
342
|
+
};
|
|
343
|
+
return MarketDataStreamer;
|
|
344
|
+
}());
|
|
345
|
+
exports.default = MarketDataStreamer;
|
|
@@ -6,5 +6,5 @@ export default class AccountsAndCustomersService {
|
|
|
6
6
|
getCustomerResource(): Promise<any>;
|
|
7
7
|
getCustomerAccountResources(): Promise<any>;
|
|
8
8
|
getFullCustomerAccountResource(accountNumber: string): Promise<any>;
|
|
9
|
-
|
|
9
|
+
getApiQuoteToken(): Promise<any>;
|
|
10
10
|
}
|
|
@@ -97,16 +97,16 @@ var AccountsAndCustomersService = /** @class */ (function () {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
};
|
|
100
|
-
//
|
|
101
|
-
AccountsAndCustomersService.prototype.
|
|
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
|
|
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-
|
|
106
|
+
case 0: return [4 /*yield*/, this.httpClient.getData('/api-quote-tokens', {}, {})];
|
|
107
107
|
case 1:
|
|
108
|
-
|
|
109
|
-
return [2 /*return*/, (0, response_util_1.default)(
|
|
108
|
+
apiQuoteToken = (_a.sent());
|
|
109
|
+
return [2 /*return*/, (0, response_util_1.default)(apiQuoteToken)];
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
});
|
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
|
|
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 {
|
|
39
|
+
export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener, CandleSubscriptionOptions, CandleType };
|
|
40
40
|
export { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver };
|
package/dist/tastytrade-api.js
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
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.
|
|
29
|
+
exports.STREAMER_STATE = exports.AccountStreamer = exports.CandleType = 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
|
|
12
|
-
exports.
|
|
34
|
+
var market_data_streamer_1 = __importStar(require("./market-data-streamer"));
|
|
35
|
+
exports.MarketDataStreamer = market_data_streamer_1.default;
|
|
36
|
+
Object.defineProperty(exports, "CandleType", { enumerable: true, get: function () { return market_data_streamer_1.CandleType; } });
|
|
37
|
+
Object.defineProperty(exports, "MarketDataSubscriptionType", { enumerable: true, get: function () { return market_data_streamer_1.MarketDataSubscriptionType; } });
|
|
13
38
|
//Services:
|
|
14
39
|
var session_service_1 = __importDefault(require("./services/session-service"));
|
|
15
40
|
var account_status_service_1 = __importDefault(require("./services/account-status-service"));
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import WebSocket from 'isomorphic-ws'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
4
|
+
|
|
5
|
+
export enum MarketDataSubscriptionType {
|
|
6
|
+
Candle = 'Candle',
|
|
7
|
+
Quote = 'Quote',
|
|
8
|
+
Trade = 'Trade',
|
|
9
|
+
Summary = 'Summary',
|
|
10
|
+
Profile = 'Profile',
|
|
11
|
+
Greeks = 'Greeks',
|
|
12
|
+
Underlying = 'Underlying'
|
|
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
|
+
|
|
29
|
+
export type MarketDataListener = (data: any) => void
|
|
30
|
+
export type ErrorListener = (error: any) => void
|
|
31
|
+
export type AuthStateListener = (isAuthorized: boolean) => void
|
|
32
|
+
|
|
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
|
|
37
|
+
|
|
38
|
+
// List of all subscription types except for Candle
|
|
39
|
+
const AllSubscriptionTypes = Object.values(MarketDataSubscriptionType)
|
|
40
|
+
|
|
41
|
+
const KeepaliveInterval = 30000 // 30 seconds
|
|
42
|
+
|
|
43
|
+
const DefaultChannelId = 1
|
|
44
|
+
|
|
45
|
+
export default class MarketDataStreamer {
|
|
46
|
+
private webSocket: WebSocket | null = null
|
|
47
|
+
private token = ''
|
|
48
|
+
private keepaliveIntervalId: any | null = null
|
|
49
|
+
private dataListeners = new Map()
|
|
50
|
+
private openChannels = new Set()
|
|
51
|
+
private subscriptionsQueue: Map<number, QueuedSubscription[]> = new Map()
|
|
52
|
+
private authState = ''
|
|
53
|
+
private errorListeners = new Map()
|
|
54
|
+
private authStateListeners = new Map()
|
|
55
|
+
|
|
56
|
+
addDataListener(dataListener: MarketDataListener, channelId: number | null = null): Remover {
|
|
57
|
+
if (_.isNil(dataListener)) {
|
|
58
|
+
return _.noop
|
|
59
|
+
}
|
|
60
|
+
const guid = uuidv4()
|
|
61
|
+
this.dataListeners.set(guid, { listener: dataListener, channelId })
|
|
62
|
+
|
|
63
|
+
return () => this.dataListeners.delete(guid)
|
|
64
|
+
}
|
|
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
|
+
|
|
86
|
+
connect(url: string, token: string) {
|
|
87
|
+
if (this.isConnected) {
|
|
88
|
+
throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.token = token
|
|
92
|
+
this.webSocket = new WebSocket(url)
|
|
93
|
+
this.webSocket.onopen = this.onOpen.bind(this)
|
|
94
|
+
this.webSocket.onerror = this.onError.bind(this)
|
|
95
|
+
this.webSocket.onmessage = this.handleMessageReceived.bind(this)
|
|
96
|
+
this.webSocket.onclose = this.onClose.bind(this)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
disconnect() {
|
|
100
|
+
if (_.isNil(this.webSocket)) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.clearKeepalive()
|
|
105
|
+
|
|
106
|
+
this.webSocket.onopen = null
|
|
107
|
+
this.webSocket.onerror = null
|
|
108
|
+
this.webSocket.onmessage = null
|
|
109
|
+
this.webSocket.onclose = null
|
|
110
|
+
|
|
111
|
+
this.webSocket.close()
|
|
112
|
+
this.webSocket = null
|
|
113
|
+
|
|
114
|
+
this.openChannels.clear()
|
|
115
|
+
this.subscriptionsQueue.clear()
|
|
116
|
+
this.authState = ''
|
|
117
|
+
}
|
|
118
|
+
|
|
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}}`
|
|
151
|
+
const isOpen = this.isChannelOpened(channelId)
|
|
152
|
+
const subscriptionArgs = { fromTime }
|
|
153
|
+
if (isOpen) {
|
|
154
|
+
this.sendSubscriptionMessage(candleSymbol, subscriptionTypes, channelId, 'add', subscriptionArgs)
|
|
155
|
+
} else {
|
|
156
|
+
this.queueSubscription(candleSymbol, { subscriptionTypes, channelId, subscriptionArgs })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
this.removeSubscription(candleSymbol, { subscriptionTypes, channelId })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
removeSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) {
|
|
165
|
+
const { subscriptionTypes, channelId } = options
|
|
166
|
+
const isOpen = this.isChannelOpened(channelId)
|
|
167
|
+
if (isOpen) {
|
|
168
|
+
this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove')
|
|
169
|
+
} else {
|
|
170
|
+
this.dequeueSubscription(symbol, options)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
removeAllSubscriptions(channelId = DefaultChannelId) {
|
|
175
|
+
const isOpen = this.isChannelOpened(channelId)
|
|
176
|
+
if (isOpen) {
|
|
177
|
+
this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true })
|
|
178
|
+
} else {
|
|
179
|
+
this.subscriptionsQueue.set(channelId, [])
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
openFeedChannel(channelId: number) {
|
|
184
|
+
if (!this.isReadyToOpenChannels) {
|
|
185
|
+
throw new Error(`Unable to open channel ${channelId} due to DxLink authorization state: ${this.authState}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.isChannelOpened(channelId)) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.sendMessage({
|
|
193
|
+
"type": "CHANNEL_REQUEST",
|
|
194
|
+
"channel": channelId,
|
|
195
|
+
"service": "FEED",
|
|
196
|
+
"parameters": {
|
|
197
|
+
"contract": "AUTO"
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
isChannelOpened(channelId: number) {
|
|
203
|
+
return this.isConnected && this.openChannels.has(channelId)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get isReadyToOpenChannels() {
|
|
207
|
+
return this.isConnected && this.isDxLinkAuthorized
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get isConnected() {
|
|
211
|
+
return !_.isNil(this.webSocket)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private scheduleKeepalive() {
|
|
215
|
+
this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private sendKeepalive() {
|
|
219
|
+
if (_.isNil(this.keepaliveIntervalId)) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.sendMessage({
|
|
224
|
+
"type": "KEEPALIVE",
|
|
225
|
+
"channel": 0
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private queueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
230
|
+
const { subscriptionTypes, channelId, subscriptionArgs } = options
|
|
231
|
+
let queue = this.subscriptionsQueue.get(options.channelId)
|
|
232
|
+
if (_.isNil(queue)) {
|
|
233
|
+
queue = []
|
|
234
|
+
this.subscriptionsQueue.set(channelId, queue)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
queue.push({ symbol, subscriptionTypes, subscriptionArgs })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private dequeueSubscription(symbol: string, options: SubscriptionOptions) {
|
|
241
|
+
const queue = this.subscriptionsQueue.get(options.channelId)
|
|
242
|
+
if (_.isNil(queue) || _.isEmpty(queue)) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_.remove(queue, (queueItem: any) => queueItem.symbol === symbol)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private sendQueuedSubscriptions(channelId: number) {
|
|
250
|
+
const queuedSubscriptions = this.subscriptionsQueue.get(channelId)
|
|
251
|
+
if (_.isNil(queuedSubscriptions)) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Clear out queue immediately
|
|
256
|
+
this.subscriptionsQueue.set(channelId, [])
|
|
257
|
+
queuedSubscriptions.forEach(subscription => {
|
|
258
|
+
this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add', subscription.subscriptionArgs)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
*
|
|
264
|
+
* @param {*} symbol
|
|
265
|
+
* @param {*} subscriptionTypes
|
|
266
|
+
* @param {*} channelId
|
|
267
|
+
* @param {*} direction add or remove
|
|
268
|
+
*/
|
|
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 ?? {})))
|
|
271
|
+
this.sendMessage({
|
|
272
|
+
"type": "FEED_SUBSCRIPTION",
|
|
273
|
+
"channel": channelId,
|
|
274
|
+
[direction]: subscriptions
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private onError(error: any) {
|
|
279
|
+
console.error('Error received: ', error)
|
|
280
|
+
this.notifyErrorListeners(error)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private onOpen() {
|
|
284
|
+
this.openChannels.clear()
|
|
285
|
+
|
|
286
|
+
this.sendMessage({
|
|
287
|
+
"type": "SETUP",
|
|
288
|
+
"channel": 0,
|
|
289
|
+
"keepaliveTimeout": KeepaliveInterval,
|
|
290
|
+
"acceptKeepaliveTimeout": KeepaliveInterval,
|
|
291
|
+
"version": "0.1-js/1.0.0"
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
this.scheduleKeepalive()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private onClose() {
|
|
298
|
+
this.webSocket = null
|
|
299
|
+
this.clearKeepalive()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private clearKeepalive() {
|
|
303
|
+
if (!_.isNil(this.keepaliveIntervalId)) {
|
|
304
|
+
clearInterval(this.keepaliveIntervalId)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.keepaliveIntervalId = null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
get isDxLinkAuthorized() {
|
|
311
|
+
return this.authState === 'AUTHORIZED'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private handleAuthStateMessage(data: any) {
|
|
315
|
+
this.authState = data.state
|
|
316
|
+
this.authStateListeners.forEach(listener => listener(this.isDxLinkAuthorized))
|
|
317
|
+
|
|
318
|
+
if (this.isDxLinkAuthorized) {
|
|
319
|
+
this.openFeedChannel(DefaultChannelId)
|
|
320
|
+
} else {
|
|
321
|
+
this.sendMessage({
|
|
322
|
+
"type": "AUTH",
|
|
323
|
+
"channel": 0,
|
|
324
|
+
"token": this.token
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private handleChannelOpened(jsonData: any) {
|
|
330
|
+
this.openChannels.add(jsonData.channel)
|
|
331
|
+
this.sendQueuedSubscriptions(jsonData.channel)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private notifyListeners(jsonData: any) {
|
|
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))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private handleMessageReceived(data: string) {
|
|
347
|
+
const messageData = _.get(data, 'data', data)
|
|
348
|
+
const jsonData = JSON.parse(messageData)
|
|
349
|
+
switch (jsonData.type) {
|
|
350
|
+
case 'AUTH_STATE':
|
|
351
|
+
this.handleAuthStateMessage(jsonData)
|
|
352
|
+
break
|
|
353
|
+
case 'CHANNEL_OPENED':
|
|
354
|
+
this.handleChannelOpened(jsonData)
|
|
355
|
+
break
|
|
356
|
+
case 'FEED_DATA':
|
|
357
|
+
this.notifyListeners(jsonData)
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private sendMessage(json: object) {
|
|
363
|
+
if (_.isNil(this.webSocket)) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.webSocket.send(JSON.stringify(json))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -26,10 +26,9 @@ export default class AccountsAndCustomersService {
|
|
|
26
26
|
return extractResponseData(fullCustomerAccountResource)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
async
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
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
|
|
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 {
|
|
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": "1.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",
|
|
@@ -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",
|
package/lib/quote-streamer.ts
DELETED
|
@@ -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
|
-
}
|