@tastytrade/api 5.0.0 → 6.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/CHANGELOG.md +29 -0
- package/README.md +87 -43
- package/dist/account-streamer.d.ts +6 -3
- package/dist/account-streamer.d.ts.map +1 -1
- package/dist/account-streamer.js +21 -15
- package/dist/account-streamer.js.map +1 -1
- package/dist/logger.d.ts +5 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/market-data-streamer.d.ts.map +1 -1
- package/dist/market-data-streamer.js +1 -0
- package/dist/market-data-streamer.js.map +1 -1
- package/dist/models/access-token.d.ts +12 -0
- package/dist/models/access-token.d.ts.map +1 -0
- package/dist/models/access-token.js +35 -0
- package/dist/models/access-token.js.map +1 -0
- package/dist/models/tastytrade-session.d.ts.map +1 -1
- package/dist/models/tastytrade-session.js.map +1 -1
- package/dist/quote-streamer.d.ts +58 -0
- package/dist/quote-streamer.d.ts.map +1 -0
- package/dist/quote-streamer.js +132 -0
- package/dist/quote-streamer.js.map +1 -0
- package/dist/services/account-status-service.js +0 -1
- package/dist/services/account-status-service.js.map +1 -1
- package/dist/services/accounts-and-customers-service.js +0 -1
- package/dist/services/accounts-and-customers-service.js.map +1 -1
- package/dist/services/balances-and-positions-service.js +0 -1
- package/dist/services/balances-and-positions-service.js.map +1 -1
- package/dist/services/instruments-service.js +1 -2
- package/dist/services/instruments-service.js.map +1 -1
- package/dist/services/margin-requirements-service.js +0 -1
- package/dist/services/margin-requirements-service.js.map +1 -1
- package/dist/services/market-metrics-service.js +0 -1
- package/dist/services/market-metrics-service.js.map +1 -1
- package/dist/services/net-liquidating-value-history-service.js +0 -1
- package/dist/services/net-liquidating-value-history-service.js.map +1 -1
- package/dist/services/orders-service.js +0 -1
- package/dist/services/orders-service.js.map +1 -1
- package/dist/services/risk-parameters-service.js +0 -1
- package/dist/services/risk-parameters-service.js.map +1 -1
- package/dist/services/session-service.d.ts +6 -1
- package/dist/services/session-service.d.ts.map +1 -1
- package/dist/services/session-service.js +10 -2
- package/dist/services/session-service.js.map +1 -1
- package/dist/services/symbol-search-service.js +0 -1
- package/dist/services/symbol-search-service.js.map +1 -1
- package/dist/services/tastytrade-http-client.d.ts +16 -2
- package/dist/services/tastytrade-http-client.d.ts.map +1 -1
- package/dist/services/tastytrade-http-client.js +71 -14
- package/dist/services/tastytrade-http-client.js.map +1 -1
- package/dist/services/transactions-service.js +0 -1
- package/dist/services/transactions-service.js.map +1 -1
- package/dist/services/watchlists-service.js +0 -1
- package/dist/services/watchlists-service.js.map +1 -1
- package/dist/tastytrade-api.d.ts +12 -2
- package/dist/tastytrade-api.d.ts.map +1 -1
- package/dist/tastytrade-api.js +22 -6
- package/dist/tastytrade-api.js.map +1 -1
- package/dist/utils/json-util.d.ts.map +1 -1
- package/dist/utils/json-util.js +0 -2
- package/dist/utils/json-util.js.map +1 -1
- package/dist/utils/response-util.d.ts.map +1 -1
- package/dist/utils/response-util.js +0 -1
- package/dist/utils/response-util.js.map +1 -1
- package/eslint.config.cjs +46 -0
- package/lib/account-streamer.ts +31 -23
- package/lib/logger.ts +6 -1
- package/lib/models/access-token.ts +40 -0
- package/lib/models/tastytrade-session.ts +1 -1
- package/lib/quote-streamer.ts +151 -0
- package/lib/services/instruments-service.ts +1 -1
- package/lib/services/session-service.ts +13 -1
- package/lib/services/tastytrade-http-client.ts +93 -15
- package/lib/tastytrade-api.ts +31 -4
- package/lib/utils/json-util.ts +0 -2
- package/lib/utils/response-util.ts +3 -4
- package/package.json +26 -18
- package/tsconfig.json +2 -2
- package/.eslintrc.cjs +0 -18
- package/lib/market-data-streamer.ts +0 -376
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { DXLinkWebSocketClient, DXLinkFeed, FeedContract, FeedDataFormat, type DXLinkFeedEventListener } from '@dxfeed/dxlink-api'
|
|
2
|
+
import type AccountsAndCustomersService from './services/accounts-and-customers-service.js'
|
|
3
|
+
import type Logger from './logger.js'
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
|
|
6
|
+
// TODO: Make sure this works in node and we don't have to override the global Websocket class
|
|
7
|
+
|
|
8
|
+
export enum MarketDataSubscriptionType {
|
|
9
|
+
Candle = 'Candle',
|
|
10
|
+
Quote = 'Quote',
|
|
11
|
+
Trade = 'Trade',
|
|
12
|
+
Summary = 'Summary',
|
|
13
|
+
Profile = 'Profile',
|
|
14
|
+
Greeks = 'Greeks',
|
|
15
|
+
Underlying = 'Underlying'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ALL_EVENT_TYPES = [
|
|
19
|
+
MarketDataSubscriptionType.Quote,
|
|
20
|
+
MarketDataSubscriptionType.Trade,
|
|
21
|
+
MarketDataSubscriptionType.Summary,
|
|
22
|
+
MarketDataSubscriptionType.Profile,
|
|
23
|
+
MarketDataSubscriptionType.Greeks,
|
|
24
|
+
MarketDataSubscriptionType.Underlying
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export enum CandleType {
|
|
28
|
+
Tick = 't',
|
|
29
|
+
Second = 's',
|
|
30
|
+
Minute = 'm',
|
|
31
|
+
Hour = 'h',
|
|
32
|
+
Day = 'd',
|
|
33
|
+
Week = 'w',
|
|
34
|
+
Month = 'mo',
|
|
35
|
+
ThirdFriday = 'o',
|
|
36
|
+
Year = 'y',
|
|
37
|
+
Volume = 'v',
|
|
38
|
+
Price = 'p'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default class QuoteStreamer {
|
|
42
|
+
public dxLinkFeed: DXLinkFeed<any> | null = null
|
|
43
|
+
public dxLinkUrl: string | null = null
|
|
44
|
+
public dxLinkAuthToken: string | null = null
|
|
45
|
+
public eventListeners: DXLinkFeedEventListener[] = []
|
|
46
|
+
|
|
47
|
+
constructor(private readonly accountsAndCustomersService: AccountsAndCustomersService, private readonly logger: Logger) {
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Connects to the DxLink WebSocket and sets up the feed
|
|
52
|
+
* Make sure to call disconnect() when done
|
|
53
|
+
* Calls `getApiQuoteToken` to get the connection URL and auth token
|
|
54
|
+
* Make sure you have a valid session or access token before calling this
|
|
55
|
+
*/
|
|
56
|
+
async connect() {
|
|
57
|
+
const tokenResponse = await this.accountsAndCustomersService.getApiQuoteToken()
|
|
58
|
+
this.dxLinkUrl = _.get(tokenResponse, 'dxlink-url')
|
|
59
|
+
this.dxLinkAuthToken = _.get(tokenResponse, 'token')
|
|
60
|
+
|
|
61
|
+
const client = new DXLinkWebSocketClient()
|
|
62
|
+
client.connect(this.dxLinkUrl!)
|
|
63
|
+
client.setAuthToken(this.dxLinkAuthToken!)
|
|
64
|
+
|
|
65
|
+
this.dxLinkFeed = new DXLinkFeed(client, FeedContract.AUTO)
|
|
66
|
+
|
|
67
|
+
this.dxLinkFeed.configure({
|
|
68
|
+
acceptAggregationPeriod: 10,
|
|
69
|
+
acceptDataFormat: FeedDataFormat.COMPACT
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
this.eventListeners.forEach(listener => this.dxLinkFeed!.addEventListener(listener))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
disconnect() {
|
|
76
|
+
if (_.isNil(this.dxLinkFeed)) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.eventListeners.forEach(listener => this.removeEventListener(listener))
|
|
81
|
+
this.dxLinkFeed = null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Returns a function that can be called to remove the listener
|
|
85
|
+
addEventListener(listener: DXLinkFeedEventListener): () => void {
|
|
86
|
+
this.eventListeners.push(listener)
|
|
87
|
+
if (this.dxLinkFeed) {
|
|
88
|
+
this.dxLinkFeed.addEventListener(listener)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
this.removeEventListener(listener)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeEventListener(listenerToRemove: DXLinkFeedEventListener) {
|
|
97
|
+
_.remove(this.eventListeners, listener => listener === listenerToRemove)
|
|
98
|
+
if (this.dxLinkFeed) {
|
|
99
|
+
this.dxLinkFeed.removeEventListener(listenerToRemove)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
subscribe(streamerSymbols: string[], types: MarketDataSubscriptionType[] | null = null) {
|
|
104
|
+
if (_.isNil(this.dxLinkFeed)) {
|
|
105
|
+
throw new Error('DxLink feed is not connected')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
types = types ?? ALL_EVENT_TYPES
|
|
109
|
+
streamerSymbols.forEach(symbol => {
|
|
110
|
+
types.forEach(type => {
|
|
111
|
+
this.dxLinkFeed!.addSubscriptions({ type, symbol })
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
unsubscribe(streamerSymbols: string[]) {
|
|
117
|
+
if (_.isNil(this.dxLinkFeed)) {
|
|
118
|
+
throw new Error('DxLink feed is not connected')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
streamerSymbols.forEach(symbol => {
|
|
122
|
+
ALL_EVENT_TYPES.forEach(type => {
|
|
123
|
+
this.dxLinkFeed!.removeSubscriptions({ type, symbol })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Adds a candle subscription
|
|
130
|
+
* @param streamerSymbol Get this from an instrument's streamer-symbol json response field
|
|
131
|
+
* @param fromTime Epoch timestamp from where you want to start
|
|
132
|
+
* @param period The duration of each candle
|
|
133
|
+
* @param type The duration type of the period
|
|
134
|
+
* For example, a period/type of 5/m means you want each candle to represent 5 minutes of data
|
|
135
|
+
* From there, setting fromTime to 24 hours ago would give you 24 hours of data grouped in 5 minute intervals
|
|
136
|
+
* @returns
|
|
137
|
+
*/
|
|
138
|
+
subscribeCandles(streamerSymbol: string, fromTime: number, period: number, type: CandleType) {
|
|
139
|
+
// Example: AAPL{=5m} where each candle represents 5 minutes of data
|
|
140
|
+
const candleSymbol = `${streamerSymbol}{=${period}${type}}`
|
|
141
|
+
if (_.isNil(this.dxLinkFeed)) {
|
|
142
|
+
throw new Error('DxLink feed is not connected')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.dxLinkFeed!.addSubscriptions({
|
|
146
|
+
type: MarketDataSubscriptionType.Candle,
|
|
147
|
+
symbol: candleSymbol,
|
|
148
|
+
fromTime
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -118,7 +118,7 @@ export default class InstrumentsService {
|
|
|
118
118
|
return extractResponseData(futureOptionChain)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//Option-chains: Allows an API client to fetch
|
|
121
|
+
//Option-chains: Allows an API client to fetch equity option chains.
|
|
122
122
|
async getNestedOptionChain(symbol: string){
|
|
123
123
|
//Returns an option chain given an underlying symbol,
|
|
124
124
|
//i.e. AAPL in a nested form to minimize redundant processing
|
|
@@ -5,6 +5,18 @@ export default class SessionService {
|
|
|
5
5
|
constructor(public httpClient: TastytradeHttpClient) {
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
private get clientId(): string {
|
|
9
|
+
return '9953f07a-5de4-408c-a8ab-688a6320f00f'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private get clientSecret(): string {
|
|
13
|
+
return 'baa245033420a05d013541c0c6ef4f98bb16a1ec'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private get refreshToken(): string {
|
|
17
|
+
return 'eyJhbGciOiJFZERTQSIsInR5cCI6InJ0K2p3dCIsImtpZCI6IkZqVTdUT25qVEQ2WnVySlg2cVlwWmVPbzBDQzQ5TnIzR1pUN1E4MTc0cUkiLCJqa3UiOiJodHRwczovL2ludGVyaW9yLWFwaS5hcjIudGFzdHl0cmFkZS5zeXN0ZW1zL29hdXRoL2p3a3MifQ.eyJpc3MiOiJodHRwczovL2FwaS50YXN0eXRyYWRlLmNvbSIsInN1YiI6IlUwMDAwMDM3MTg0IiwiaWF0IjoxNzU4MjQzNjY0LCJhdWQiOiI5OTUzZjA3YS01ZGU0LTQwOGMtYThhYi02ODhhNjMyMGYwMGYiLCJncmFudF9pZCI6Ikc0ZTc4MjFkYy03NTQyLTQ0NTQtODBkMy1iYjU3NGEwMGRkYWMiLCJzY29wZSI6InJlYWQgdHJhZGUifQ.ZsP51rUGQIXsP-cU0OCa-45AwwMp18YxOrT_mrrocClhRL7bfWctX8GOJ35Nn_E48WOuQPxUF3KMhSn1tkqLBQ'
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
// Sessions: Allows an API client to interact with their session, or create a new one.
|
|
9
21
|
async login(usernameOrEmail: string, password: string, rememberMe = false) {
|
|
10
22
|
// Create a new user session.
|
|
@@ -30,6 +42,6 @@ export default class SessionService {
|
|
|
30
42
|
async logout(){
|
|
31
43
|
const response = await this.httpClient.deleteData('/sessions', {});// added this for the integration tests?
|
|
32
44
|
this.httpClient.session.clear()
|
|
33
|
-
return
|
|
45
|
+
return { status: response.status };
|
|
34
46
|
}
|
|
35
47
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import TastytradeSession from "../models/tastytrade-session.js"
|
|
2
|
+
import AccessToken from "../models/access-token.js"
|
|
2
3
|
import axios from "axios"
|
|
3
4
|
import qs from 'qs'
|
|
4
5
|
import { recursiveDasherizeKeys } from "../utils/json-util.js"
|
|
5
6
|
import _ from 'lodash'
|
|
6
7
|
import type Logger from "../logger.js"
|
|
8
|
+
import type { ClientConfig } from "../tastytrade-api.js"
|
|
7
9
|
|
|
8
10
|
const ParamsSerializer = {
|
|
9
11
|
serialize: function (queryParams: object) {
|
|
@@ -11,22 +13,65 @@ const ParamsSerializer = {
|
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
const ApiVersionRegex = /^\d{8}$/
|
|
17
|
+
|
|
14
18
|
export default class TastytradeHttpClient {
|
|
15
19
|
private readonly logger?: Logger
|
|
20
|
+
public baseUrl: string
|
|
21
|
+
public clientSecret?: string
|
|
22
|
+
public refreshToken?: string
|
|
23
|
+
public oauthScopes?: string[]
|
|
24
|
+
public readonly accessToken: AccessToken
|
|
16
25
|
public readonly session: TastytradeSession
|
|
26
|
+
private _targetApiVersion?: string
|
|
17
27
|
|
|
18
|
-
constructor(
|
|
28
|
+
constructor(clientConfig: Partial<ClientConfig>, logger?: Logger) {
|
|
19
29
|
this.logger = logger
|
|
30
|
+
this.baseUrl = clientConfig.baseUrl!
|
|
31
|
+
this.accessToken = new AccessToken()
|
|
20
32
|
this.session = new TastytradeSession()
|
|
33
|
+
this.updateConfig(clientConfig)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public updateConfig(config: Partial<ClientConfig>) {
|
|
37
|
+
const httpClientConfig = _.pick(config, ['clientSecret', 'refreshToken', 'oauthScopes', 'targetApiVersion'])
|
|
38
|
+
if (!_.isEmpty(httpClientConfig)) {
|
|
39
|
+
Object.assign(this, httpClientConfig)
|
|
40
|
+
this.accessToken.clear()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get needsTokenRefresh(): boolean {
|
|
45
|
+
if (this.session.isValid) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
if (_.isNil(this.refreshToken) || _.isNil(this.clientSecret)) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
return this.accessToken.isExpired
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get authHeader(): string | null {
|
|
55
|
+
if (this.session.isValid) {
|
|
56
|
+
return this.session.authToken
|
|
57
|
+
}
|
|
58
|
+
if (this.accessToken.isValid) {
|
|
59
|
+
return this.accessToken.authorizationHeader
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
21
62
|
}
|
|
22
63
|
|
|
23
64
|
private getDefaultHeaders(): any {
|
|
24
65
|
const headers: { [key: string]: any } = {
|
|
25
66
|
"Content-Type": "application/json",
|
|
26
67
|
"Accept": "application/json",
|
|
27
|
-
"Authorization": this.
|
|
68
|
+
"Authorization": this.authHeader
|
|
28
69
|
};
|
|
29
70
|
|
|
71
|
+
if (!_.isNil(this.targetApiVersion)) {
|
|
72
|
+
headers["Accept-Version"] = this.targetApiVersion
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
// Only set user agent if running in node
|
|
31
76
|
if (typeof window === 'undefined') {
|
|
32
77
|
headers["User-Agent"] = 'tastytrade-sdk-js'
|
|
@@ -35,21 +80,43 @@ export default class TastytradeHttpClient {
|
|
|
35
80
|
return headers
|
|
36
81
|
}
|
|
37
82
|
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
83
|
+
private axiosConfig(method: string, url: string, data: object = {}, headers: object = {}, params: object = {}): any {
|
|
84
|
+
return _.omitBy(
|
|
85
|
+
{ method, url, baseURL: this.baseUrl, data, headers, params, paramsSerializer: ParamsSerializer},
|
|
86
|
+
_.isEmpty
|
|
87
|
+
)
|
|
88
|
+
}
|
|
42
89
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
public async generateAccessToken(): Promise<any> {
|
|
91
|
+
if (_.isNil(this.refreshToken) || _.isNil(this.clientSecret) || _.isNil(this.oauthScopes)) {
|
|
92
|
+
throw new Error('Missing required parameters to generate access token (refreshToken, clientSecret, oauthScopes)')
|
|
93
|
+
}
|
|
94
|
+
const params = {
|
|
95
|
+
refresh_token: this.refreshToken,
|
|
96
|
+
client_secret: this.clientSecret,
|
|
97
|
+
scope: this.oauthScopes!.join(' '),
|
|
98
|
+
grant_type: 'refresh_token'
|
|
99
|
+
}
|
|
52
100
|
|
|
101
|
+
const config = this.axiosConfig('post', '/oauth/token', params)
|
|
102
|
+
this.logger?.info('Making request', config)
|
|
103
|
+
const tokenResponse = await axios.request(config)
|
|
104
|
+
this.accessToken.updateFromTokenResponse(tokenResponse)
|
|
105
|
+
return this.accessToken
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async executeRequest(method: string, url: string, data: object = {}, headers: object = {}, params: object = {}): Promise<any> {
|
|
109
|
+
if (this.needsTokenRefresh) {
|
|
110
|
+
await this.generateAccessToken()
|
|
111
|
+
}
|
|
112
|
+
let dasherizedParams = params
|
|
113
|
+
let dasherizedData = data
|
|
114
|
+
dasherizedParams = recursiveDasherizeKeys(params)
|
|
115
|
+
dasherizedData = recursiveDasherizeKeys(data)
|
|
116
|
+
|
|
117
|
+
const mergedHeaders = { ...headers, ...this.getDefaultHeaders() }
|
|
118
|
+
|
|
119
|
+
const config = this.axiosConfig(method, url, dasherizedData, mergedHeaders, dasherizedParams)
|
|
53
120
|
this.logger?.info('Making request', config)
|
|
54
121
|
return axios.request(config)
|
|
55
122
|
}
|
|
@@ -73,4 +140,15 @@ export default class TastytradeHttpClient {
|
|
|
73
140
|
async deleteData(url: string, headers: object): Promise<any> {
|
|
74
141
|
return this.executeRequest('delete', url, headers);
|
|
75
142
|
}
|
|
143
|
+
|
|
144
|
+
public get targetApiVersion(): string | undefined {
|
|
145
|
+
return this._targetApiVersion
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public set targetApiVersion(version: string | undefined) {
|
|
149
|
+
if (!_.isNil(version) && !ApiVersionRegex.test(version)) {
|
|
150
|
+
throw new Error('Invalid API version format. Expected YYYYMMDD.')
|
|
151
|
+
}
|
|
152
|
+
this._targetApiVersion = version
|
|
153
|
+
}
|
|
76
154
|
}
|
package/lib/tastytrade-api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import TastytradeHttpClient from "./services/tastytrade-http-client.js"
|
|
2
2
|
import { AccountStreamer, STREAMER_STATE, type Disposer, type StreamerStateObserver } from './account-streamer.js'
|
|
3
|
-
import
|
|
3
|
+
import _ from 'lodash'
|
|
4
4
|
|
|
5
5
|
//Services:
|
|
6
6
|
import SessionService from "./services/session-service.js"
|
|
@@ -19,19 +19,34 @@ import WatchlistsService from "./services/watchlists-service.js"
|
|
|
19
19
|
import TastytradeSession from "./models/tastytrade-session.js"
|
|
20
20
|
import type Logger from "./logger.js"
|
|
21
21
|
import { TastytradeLogger, LogLevel } from "./logger.js"
|
|
22
|
+
import QuoteStreamer, { MarketDataSubscriptionType, CandleType } from "./quote-streamer.js"
|
|
23
|
+
import type AccessToken from "./models/access-token.js"
|
|
22
24
|
|
|
23
25
|
export type ClientConfig = {
|
|
24
26
|
baseUrl: string,
|
|
25
27
|
accountStreamerUrl: string,
|
|
28
|
+
clientSecret?: string,
|
|
29
|
+
refreshToken?: string,
|
|
30
|
+
oauthScopes?: string[],
|
|
26
31
|
logger?: Logger,
|
|
27
32
|
logLevel?: LogLevel
|
|
33
|
+
targetApiVersion?: string
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export default class TastytradeClient {
|
|
37
|
+
public static readonly ProdConfig: ClientConfig = {
|
|
38
|
+
baseUrl: 'https://api.tastyworks.com',
|
|
39
|
+
accountStreamerUrl: 'wss://streamer.tastyworks.com',
|
|
40
|
+
}
|
|
41
|
+
public static readonly SandboxConfig: ClientConfig = {
|
|
42
|
+
baseUrl: 'https://api.cert.tastyworks.com',
|
|
43
|
+
accountStreamerUrl: 'wss://streamer.cert.tastyworks.com',
|
|
44
|
+
}
|
|
31
45
|
public readonly logger: TastytradeLogger
|
|
32
46
|
public readonly httpClient: TastytradeHttpClient
|
|
33
47
|
|
|
34
48
|
public readonly accountStreamer: AccountStreamer
|
|
49
|
+
public readonly quoteStreamer: QuoteStreamer
|
|
35
50
|
|
|
36
51
|
public readonly sessionService: SessionService
|
|
37
52
|
public readonly accountStatusService: AccountStatusService
|
|
@@ -49,8 +64,7 @@ export default class TastytradeClient {
|
|
|
49
64
|
|
|
50
65
|
constructor(config: ClientConfig) {
|
|
51
66
|
this.logger = new TastytradeLogger(config.logger, config.logLevel)
|
|
52
|
-
this.httpClient = new TastytradeHttpClient(config
|
|
53
|
-
this.accountStreamer = new AccountStreamer(config.accountStreamerUrl, this.session, this.logger)
|
|
67
|
+
this.httpClient = new TastytradeHttpClient(config, this.logger)
|
|
54
68
|
|
|
55
69
|
this.sessionService = new SessionService(this.httpClient)
|
|
56
70
|
this.accountStatusService = new AccountStatusService(this.httpClient)
|
|
@@ -65,14 +79,27 @@ export default class TastytradeClient {
|
|
|
65
79
|
this.symbolSearchService = new SymbolSearchService(this.httpClient)
|
|
66
80
|
this.transactionsService = new TransactionsService(this.httpClient)
|
|
67
81
|
this.watchlistsService = new WatchlistsService(this.httpClient)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
this.accountStreamer = new AccountStreamer(config.accountStreamerUrl, this.session, this.accessToken, this.logger)
|
|
85
|
+
this.quoteStreamer = new QuoteStreamer(this.accountsAndCustomersService, this.logger)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public updateConfig(config: Partial<ClientConfig>) {
|
|
89
|
+
this.httpClient.updateConfig(config)
|
|
90
|
+
this.logger.updateConfig(config)
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
get session(): TastytradeSession {
|
|
71
94
|
return this.httpClient.session
|
|
72
95
|
}
|
|
96
|
+
|
|
97
|
+
get accessToken(): AccessToken {
|
|
98
|
+
return this.httpClient.accessToken
|
|
99
|
+
}
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
export {
|
|
102
|
+
export { MarketDataSubscriptionType, CandleType }
|
|
76
103
|
export { AccountStreamer, STREAMER_STATE, type Disposer, type StreamerStateObserver }
|
|
77
104
|
export { TastytradeLogger, LogLevel }
|
|
78
105
|
export type { Logger }
|
package/lib/utils/json-util.ts
CHANGED
|
@@ -20,11 +20,9 @@ export class JsonBuilder {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
23
|
export function recursiveDasherizeKeys(body: any) {
|
|
25
24
|
let dasherized = _.mapKeys(body, (_value, key) => dasherize(key))
|
|
26
25
|
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
26
|
dasherized = _.mapValues(dasherized, (value: any) => {
|
|
29
27
|
if (_.isPlainObject(value)) {
|
|
30
28
|
return recursiveDasherizeKeys(value)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export default function extractResponseData(httpResponse: any){
|
|
3
|
+
export default function extractResponseData(httpResponse: any) {
|
|
5
4
|
if (_.has(httpResponse, 'data.data.items')) {
|
|
6
5
|
return _.get(httpResponse, 'data.data.items')
|
|
7
|
-
} else if (_.has(httpResponse, 'data.data')){
|
|
6
|
+
} else if (_.has(httpResponse, 'data.data')) {
|
|
8
7
|
return _.get(httpResponse, 'data.data')
|
|
9
|
-
}else{
|
|
8
|
+
} else {
|
|
10
9
|
return httpResponse
|
|
11
10
|
}
|
|
12
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastytrade/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "dist/tastytrade-api.js",
|
|
6
6
|
"types": "dist/tastytrade-api.d.ts",
|
|
@@ -15,37 +15,45 @@
|
|
|
15
15
|
"npm": ">=9.0.0",
|
|
16
16
|
"node": ">=20.0.0"
|
|
17
17
|
},
|
|
18
|
+
"prettier": {
|
|
19
|
+
"semi": false,
|
|
20
|
+
"singleQuote": true,
|
|
21
|
+
"trailingComma": "none"
|
|
22
|
+
},
|
|
18
23
|
"scripts": {
|
|
19
24
|
"build": "tsc -p tsconfig.json",
|
|
20
25
|
"test": "jest -i --restoreMocks",
|
|
21
26
|
"unit-test": "node --experimental-vm-modules ./node_modules/.bin/jest tests/unit",
|
|
22
27
|
"integration-test": "node --experimental-vm-modules ./node_modules/.bin/jest tests/integration",
|
|
23
28
|
"lint": "eslint lib/** tests/**",
|
|
29
|
+
"format": "prettier --write .",
|
|
24
30
|
"prepublishOnly": "npm run unit-test && npm run build",
|
|
25
31
|
"postpack": "git tag -a $npm_package_version -m $npm_package_version && git push origin $npm_package_version"
|
|
26
32
|
},
|
|
27
33
|
"dependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@types/
|
|
30
|
-
"
|
|
34
|
+
"@dxfeed/dxlink-api": "^0.3.0",
|
|
35
|
+
"@types/lodash": "^4.17.16",
|
|
36
|
+
"@types/qs": "^6.9.18",
|
|
37
|
+
"axios": "^1.9.0",
|
|
31
38
|
"isomorphic-ws": "^5.0.0",
|
|
32
39
|
"lodash": "^4.17.21",
|
|
33
|
-
"qs": "^6.
|
|
34
|
-
"uuid": "^
|
|
35
|
-
"ws": "^8.
|
|
40
|
+
"qs": "^6.14.0",
|
|
41
|
+
"uuid": "^11.1.0",
|
|
42
|
+
"ws": "^8.18.2"
|
|
36
43
|
},
|
|
37
44
|
"devDependencies": {
|
|
38
|
-
"@types/jest": "^29.5.
|
|
39
|
-
"@types/node": "
|
|
40
|
-
"@types/uuid": "^
|
|
41
|
-
"@types/ws": "^8.
|
|
42
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
43
|
-
"@typescript-eslint/parser": "^
|
|
44
|
-
"dotenv": "^16.0
|
|
45
|
-
"eslint": "^
|
|
45
|
+
"@types/jest": "^29.5.14",
|
|
46
|
+
"@types/node": "22.15.14",
|
|
47
|
+
"@types/uuid": "^10.0.0",
|
|
48
|
+
"@types/ws": "^8.18.1",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
|
50
|
+
"@typescript-eslint/parser": "^8.32.0",
|
|
51
|
+
"dotenv": "^16.5.0",
|
|
52
|
+
"eslint": "^9.26.0",
|
|
46
53
|
"jest": "^29.7.0",
|
|
47
|
-
"nock": "^
|
|
48
|
-
"
|
|
49
|
-
"
|
|
54
|
+
"nock": "^14.0.4",
|
|
55
|
+
"prettier": "^3.5.3",
|
|
56
|
+
"ts-jest": "^29.3.2",
|
|
57
|
+
"typescript": "^5.8.3"
|
|
50
58
|
}
|
|
51
59
|
}
|
package/tsconfig.json
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
49
49
|
// "types": [], /* Type declaration files to be included in compilation. */
|
|
50
50
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
51
|
-
|
|
51
|
+
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
|
52
52
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
53
53
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
54
54
|
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
/* Advanced Options */
|
|
66
66
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
|
67
67
|
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
|
68
|
-
"verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
|
68
|
+
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
|
69
69
|
},
|
|
70
70
|
"include": ["lib/**/*"],
|
|
71
71
|
"exclude": ["node_modules", "dist", "tests/**/*", "examples", ".env", ".env.sample"]
|
package/.eslintrc.cjs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
|
3
|
-
parser: '@typescript-eslint/parser',
|
|
4
|
-
plugins: ['@typescript-eslint'],
|
|
5
|
-
root: true,
|
|
6
|
-
rules: {
|
|
7
|
-
"@typescript-eslint/no-inferrable-types": "warn",
|
|
8
|
-
"@typescript-eslint/no-unused-vars": [
|
|
9
|
-
"error",
|
|
10
|
-
{
|
|
11
|
-
"argsIgnorePattern": "^_",
|
|
12
|
-
"varsIgnorePattern": "^_",
|
|
13
|
-
"caughtErrorsIgnorePattern": "^_"
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"@typescript-eslint/no-non-null-assertion": "off"
|
|
17
|
-
}
|
|
18
|
-
};
|