├── .editorconfig ├── .travis.yml ├── .eslintrc.json ├── .gitignore ├── package.json ├── yarn.lock ├── README.md ├── index.js └── README-v1.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,json}] 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | - "11" 7 | - "12" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb-base" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 11, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | .DS_Store 29 | 30 | # Test file 31 | test.js 32 | *.test.js 33 | 34 | # Key files 35 | keys -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "questrade", 3 | "version": "2.0.5", 4 | "description": "Questrade API", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:leanderlee/questrade.git" 10 | }, 11 | "keywords": [ 12 | "Questrade", 13 | "stocks", 14 | "trading" 15 | ], 16 | "author": "Leander Lee ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/leanderlee/questrade/issues" 20 | }, 21 | "homepage": "https://github.com/leanderlee/questrade", 22 | "devDependencies": {}, 23 | "dependencies": { 24 | "isomorphic-unfetch": "^3.1.0", 25 | "lodash": "^4.17.11", 26 | "moment": "^2.29.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | isomorphic-unfetch@^3.1.0: 6 | version "3.1.0" 7 | resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" 8 | integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== 9 | dependencies: 10 | node-fetch "^2.6.1" 11 | unfetch "^4.2.0" 12 | 13 | lodash@^4.17.11: 14 | version "4.17.21" 15 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 16 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 17 | 18 | moment@^2.29.4: 19 | version "2.29.4" 20 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" 21 | integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== 22 | 23 | node-fetch@^2.6.1: 24 | version "2.6.7" 25 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 26 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 27 | dependencies: 28 | whatwg-url "^5.0.0" 29 | 30 | tr46@~0.0.3: 31 | version "0.0.3" 32 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 33 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 34 | 35 | unfetch@^4.2.0: 36 | version "4.2.0" 37 | resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" 38 | integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== 39 | 40 | webidl-conversions@^3.0.0: 41 | version "3.0.1" 42 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 43 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 44 | 45 | whatwg-url@^5.0.0: 46 | version "5.0.0" 47 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 48 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 49 | dependencies: 50 | tr46 "~0.0.3" 51 | webidl-conversions "^3.0.0" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Questrade API 2 | ============= 3 | 4 | [![npm package](https://nodei.co/npm/questrade.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/questrade/) 5 | 6 | [![Build status](https://img.shields.io/travis/leanderlee/questrade.svg?style=flat-square)](https://travis-ci.org/leanderlee/questrade) 7 | [![Dependency Status](https://img.shields.io/david/leanderlee/questrade.svg?style=flat-square)](https://david-dm.org/leanderlee/questrade) 8 | [![Known Vulnerabilities](https://snyk.io/test/npm/questrade/badge.svg?style=flat-square)](https://snyk.io/test/npm/questrade) 9 | [![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat-square)](https://gitter.im/leanderlee/questrade?utm_source=badge) 10 | 11 | 12 | This API is an easy way to use the [Questrade API](www.questrade.com/api/documentation/getting-started) immediately. 13 | 14 | If you're looking for the old API docs, [click here](README-v1.md). 15 | 16 | ### Features 17 | 18 | - Easy to use API calls 19 | - Supports options chain 20 | - Supports account fetching 21 | - Auto-fetch primary account 22 | 23 | ## Getting Started 24 | 25 | Simply start by installing the questrade library: 26 | 27 | ```bash 28 | npm install --save questrade 29 | ``` 30 | 31 | You will then need to get an [API key](https://login.questrade.com/APIAccess/userapps.aspx). 32 | 33 | **Important note about key management:** 34 | After that's it's really simple to use, but you **WILL** need to save the new API key and use it every time you try to reconnect. The API key Questrade initially gives you will no longer be valid after you call `connect`. 35 | 36 | ```js 37 | const Questrade = require('questrade'); 38 | 39 | const qt = new Questrade(''); 40 | 41 | // Connect to Questrade 42 | const newKey = await qt.connect() 43 | // Save newKey for next time 44 | 45 | // Access your account here 46 | const account = await qt.getPrimaryAccount() 47 | const accounts = await qt.getAccounts() 48 | const balances = await account.getBalances() 49 | 50 | // Get Market quotes 51 | const msft = await qt.getSymbol('MSFT') 52 | const quote = await msft.getQuote() 53 | const options = await msft.getOptionChain() 54 | 55 | // ... etc. See the full documentation for all the calls you can make! 56 | ``` 57 | 58 | ## Some examples 59 | 60 | #### Root API 61 | ```js 62 | const account = await qt.getPrimaryAccount() // Account 63 | const markets = await qt.getMarkets() 64 | const accounts = await qt.getAccounts() // => [Account] 65 | ``` 66 | 67 | #### Account 68 | ```js 69 | const balances = await account.getBalances() 70 | const activities = await account.getActivities() 71 | const orders = await qt.getOpenOrders() // [Order] 72 | const orders = await qt.getOrders() // [Order] 73 | const orders = await qt.getClosedOrders() // [Order] 74 | const order = await qt.getOrder(orderId) // => Order 75 | await qt.createOrder(newOrder) 76 | await qt.updateOrder(orderId, newOrder) 77 | await qt.removeOrder(orderId) 78 | await qt.testOrder(order) 79 | ``` 80 | 81 | #### Symbols 82 | ```js 83 | const symbol = await qt.getSymbol('MSFT') // => Symbol 84 | const symbols = await qt.getSymbols(['MSFT', 'AAPL', 'BMO']) // => [Symbol] 85 | const symbols = qt.searchSymbols('MS') // => [Symbol] 86 | ``` 87 | 88 | #### Option Chain 89 | ```js 90 | // Example fetching TSLA options 91 | const tsla = await qt.getSymbol('tsla') 92 | const chain = await tsla.getOptionChain() 93 | const jan25 = chain['2025-01-17'] 94 | const jan25600 = jan25['600'] 95 | const quote = await jan25600.getQuote() 96 | ``` 97 | 98 | 99 | ## Streaming 100 | 101 | For those accounts that have L1 data access (either practice account or Advanced market data packages) you can stream live market data. 102 | 103 | ```js 104 | 105 | // ... connect to Questrade first! 106 | 107 | // Websocket port changes by API and by symbol. So you have to get the port every time you need different data stream 108 | var getWebSocketURL = function (symbolId, cb) { 109 | var webSocketURL; 110 | request({ 111 | method: 'GET', 112 | url: qt.apiUrl + '/markets/quotes?ids=' + symbolId + '&stream=true&mode=WebSocket', 113 | auth: { 114 | bearer: qt.accessToken 115 | } 116 | }, function (err, http, body) { 117 | if (err) { 118 | cb(err, null); 119 | } else { 120 | response = JSON.parse(body); 121 | webSocketURL = qt.api_server.slice(0, -1) + ':' + response.streamPort + '/' + qt.apiVersion + '/markets/quotes?ids=' + symbolId + 'stream=true&mode=WebSocket'; 122 | cb(null, webSocketURL); 123 | } 124 | }); 125 | } 126 | getWebSocketURL('9291,8049', function (err, webSocketURL) { // BMO.TO & AAPL 127 | console.log(webSocketURL); 128 | const WebSocket = require('ws'); 129 | const ws = new WebSocket(webSocketURL); 130 | 131 | ws.on('open', function open() { 132 | ws.send(qt.accessToken); 133 | }); 134 | 135 | ws.on('message', function incoming(data) { 136 | console.log(data); 137 | // Do what you want with the data 138 | }); 139 | 140 | // CLOSING WebSocket Connections otherwise will remain open 141 | process.on('exit', function () { 142 | if (ws) { 143 | console.log('CLOSE WebSocket'); 144 | ws.close(); 145 | } 146 | }); 147 | 148 | //catches ctrl+c event 149 | process.on('SIGINT', function () { 150 | if (ws) { 151 | console.log('CLOSE WebSocket SIGINT'); 152 | ws.close(); 153 | } 154 | }); 155 | 156 | //catches uncaught exceptions 157 | process.on('uncaughtException', function () { 158 | if (ws) { 159 | console.log('CLOSE WebSocket'); 160 | ws.close(); 161 | } 162 | }); 163 | }); 164 | ``` 165 | 166 | 167 | ### Full Options 168 | 169 | - **clientId** - The API key Questrade provided 170 | - **isDev** - Whether or not to use real or fake login server (and API server). Defaults to `false` 171 | - **apiVersion** - Defaults to `v1` 172 | - **accessToken** - Optionally, instead of calling `connect`, you can use an access token directly, perhaps through an implicit OAuth flow. 173 | 174 | 175 | ### Full Documentation 176 | 177 | - **getPrimaryAccount** () => [Account](#account-object) 178 | - **getAccounts** () => [[Account](#account-object)] 179 | - **getMarkets** (cb) 180 | - **getQuotesById** (symbolIds) 181 | - **getSymbol** (ticker) => [Symbol](#symbol-object) 182 | - **getSymbols** (tickers) => [[Symbol](#symbol-object)] 183 | - **searchSymbols** (prefix, offset = 0) => [[Symbol](#symbol-object)] 184 | 185 | #### Account Object 186 | - **getPositions** () 187 | - **getBalances** () 188 | - **getExecutions** () 189 | - **createOrder** (params) 190 | - **getOrders** (params) 191 | - **getOpenOrders** () 192 | - **getClosedOrders** () 193 | - **getAllOrders** () 194 | - **getOrder** (orderId, params) 195 | - **updateOrder** (orderId, params) 196 | - **removeOrder** (orderId) 197 | - **testOrder** (params) 198 | - **createStrategy** (params) 199 | - **testStrategy** (params) 200 | - **getActivities** ({ [startTime], [endTime] }) 201 | 202 | #### Symbol Object 203 | - **getOptionChain** () 204 | - **getQuote** () 205 | - **getCandles** ({ [startTime], [endTime], [interval] }) 206 | 207 | ### Contributions 208 | Are welcome! 209 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const fetch = require('isomorphic-unfetch') 3 | const moment = require('moment') 4 | 5 | class QuestradeOptions { 6 | constructor(api, data) { 7 | Object.assign(this, data) 8 | this.api = api 9 | } 10 | async getQuote() { 11 | const { callSymbolId, putSymbolId } = this 12 | const quotes = await this.api.getQuotesById([callSymbolId, putSymbolId]) 13 | return { call: quotes[callSymbolId], put: quotes[putSymbolId] } 14 | } 15 | } 16 | 17 | class QuestradeSymbol { 18 | constructor(api, data) { 19 | Object.assign(this, data) 20 | this.api = api 21 | } 22 | async getOptionChain() { 23 | const { api, symbolId } = this 24 | const { optionChain } = await api.request('GET', `/symbols/${symbolId}/options`) 25 | optionChain.forEach((chain) => { 26 | chain.expiryDate = moment(chain.expiryDate).format('YYYY-MM-DD') 27 | }) 28 | return _.chain(optionChain) 29 | .keyBy('expiryDate') 30 | .mapValues((option) => { 31 | const { chainPerStrikePrice } = option.chainPerRoot[0] 32 | const chainItems = chainPerStrikePrice.map(data => new QuestradeOptions(api, data)) 33 | return _.keyBy(chainItems, 'strikePrice') 34 | }) 35 | .value() 36 | } 37 | async getQuote() { 38 | const { quotes } = await this.api.request('GET', `/markets/quotes/${this.symbolId}`) 39 | return quotes[0] 40 | } 41 | async getCandles(params = {}) { 42 | const { 43 | interval = 'OneDay', 44 | startTime: inputStartTime, 45 | endTime: inputEndTime, 46 | } = params 47 | if (inputStartTime && !moment(inputStartTime).isValid()) { 48 | throw new Error(`Invalid start time "${inputStartTime}".`) 49 | } 50 | if (inputEndTime && !moment(inputEndTime).isValid()) { 51 | throw new Error(`Invalid start time "${inputEndTime}".`) 52 | } 53 | const startTime = inputStartTime ? moment(inputStartTime).toISOString() : moment().startOf('day').subtract(30, 'days').toISOString() 54 | const endTime = inputEndTime ? moment(inputEndTime).toISOString() : moment().toISOString() 55 | const { candles } = await this.api.request('GET', `/markets/candles/${this.symbolId}`, { startTime, endTime, interval }) 56 | return candles 57 | } 58 | } 59 | 60 | class QuestradeAccount { 61 | constructor(api, { number: accountId, ...data }) { 62 | Object.assign(this, data) 63 | this.api = api 64 | this.accountId = accountId 65 | } 66 | 67 | async request(method, endpoint, params) { 68 | return this.api.request(method, `/accounts/${this.accountId}${endpoint}`, params); 69 | } 70 | 71 | async getPositions() { 72 | return this.request('GET', '/positions') 73 | } 74 | async getBalances() { 75 | return this.request('GET', '/balances') 76 | } 77 | async getExecutions() { 78 | return this.request('GET', '/executions') 79 | } 80 | async createOrder(params) { 81 | return this.request('POST', '/orders', params) 82 | } 83 | async getOrders(params) { 84 | return this.request('GET', '/orders', params) 85 | } 86 | async getOpenOrders() { 87 | return this.getOrders({ stateFilter: 'Open' }) 88 | } 89 | async getClosedOrders() { 90 | return this.getOrders({ stateFilter: 'Closed' }) 91 | } 92 | async getAllOrders() { 93 | return this.getOrders({ stateFilter: 'All' }) 94 | } 95 | async getOrder(orderId, params) { 96 | return this.request('GET', `/orders/${orderId}`, params) 97 | } 98 | async updateOrder(orderId, params) { 99 | return this.request('POST', `/orders/${orderId}`, params) 100 | } 101 | async removeOrder(orderId) { 102 | return this.request('DELETE', `/orders/${orderId}`) 103 | } 104 | async testOrder(params) { 105 | return this.request('POST', '/orders/impact', params) 106 | } 107 | async createStrategy(params) { 108 | return this.request('POST', '/orders/strategy', params) 109 | } 110 | async testStrategy(params) { 111 | return this.request('POST', '/orders/strategy/impact', params) 112 | } 113 | async getActivities(params = {}) { 114 | const { startTime: inputStartTime, endTime: inputEndTime } = params 115 | if (inputStartTime && !moment(inputStartTime).isValid()) { 116 | throw new Error(`Invalid start time "${inputStartTime}".`) 117 | } 118 | if (inputEndTime && !moment(inputEndTime).isValid()) { 119 | throw new Error(`Invalid start time "${inputEndTime}".`) 120 | } 121 | const startTime = inputStartTime ? moment(inputStartTime).toISOString() : moment().startOf('day').subtract(30, 'days').toISOString(); 122 | const endTime = inputEndTime ? moment(inputEndTime).toISOString() : moment().toISOString(); 123 | return this.request('GET', '/activities', { startTime, endTime }) 124 | } 125 | } 126 | 127 | /** 128 | * Questrade Class to interact with Questrade API 129 | */ 130 | class QuestradeApi { 131 | constructor(opts = {}) { 132 | if (typeof opts === 'string') { 133 | opts = { clientId: opts } 134 | } 135 | const { 136 | isDev = false, // Set to true if using a practice account (http://www.questrade.com/api/free-practice-account) 137 | apiVersion = 'v1', // Used as part of the API URL 138 | clientId, 139 | accessToken, 140 | } = opts 141 | this.isDev = isDev 142 | this.apiVersion = apiVersion 143 | this.clientId = clientId // Stores The unique token that is used to call each API call, Changes everytime you Refresh Tokens (aka Login) 144 | this.apiUrl = '' // Stores the URL (without the endpoint) to use for regular GET/POST Apis 145 | this.accessToken = accessToken // The default Account agains wich the API are made. GetAccounts() will return the possible values 146 | if (this.isDev) { 147 | this.authUrl = 'https://practicelogin.questrade.com' 148 | } else { 149 | this.authUrl = 'https://login.questrade.com' 150 | } 151 | } 152 | get isConnected() { 153 | return !!this.accessToken 154 | } 155 | 156 | // Refreshed the tokem (aka Logs in) using the latest RefreshToken (or the SeedToken if no previous saved file) 157 | async connect() { 158 | const authUrl = `${this.authUrl}/oauth2/token?${new URLSearchParams({ 159 | grant_type: 'refresh_token', 160 | refresh_token: this.clientId, 161 | })}` 162 | const response = await fetch(authUrl, { 163 | method: 'POST', 164 | headers: { 165 | 'Content-Type': 'application/json', 166 | }, 167 | }) 168 | const credsJson = await response.text() 169 | try { 170 | const creds = JSON.parse(credsJson) 171 | const { api_server, access_token, refresh_token } = creds 172 | this.apiUrl = `${api_server}${this.apiVersion}` 173 | this.accessToken = access_token 174 | return refresh_token 175 | } catch (e) { 176 | e.receivedText = credsJson 177 | throw e 178 | } 179 | } 180 | 181 | // Method that actually mades the GET/POST request to Questrade 182 | async request(method, endpoint, params = {}) { 183 | if (!this.accessToken) { 184 | throw new Error('Not connected') 185 | } 186 | let url = `${this.apiUrl}${endpoint}` 187 | let body 188 | if (method === 'GET') { 189 | url = `${url}?${new URLSearchParams(params)}` 190 | } else { 191 | body = JSON.stringify(params) 192 | } 193 | const response = await fetch(url, { 194 | method, 195 | headers: { 196 | 'Authorization': `Bearer ${this.accessToken}`, 197 | 'Content-Type': 'application/json', 198 | }, 199 | body, 200 | }) 201 | return response.json() 202 | } 203 | 204 | async getPrimaryAccount() { 205 | const accounts = await this.getAccounts() 206 | for (const accountId in accounts) { 207 | const account = accounts[accountId] 208 | const { isPrimary } = account 209 | if (isPrimary) { 210 | return account 211 | } 212 | } 213 | throw new Error('No primary account') 214 | } 215 | async getAccounts() { 216 | const { accounts: accountData } = await this.request('GET', '/accounts') 217 | const accounts = accountData.map((data) => new QuestradeAccount(this, data)) 218 | return _.keyBy(accounts, 'accountId') 219 | } 220 | 221 | getSymbol(id, cb) { 222 | let params = false; 223 | if (typeof id === 'number') { 224 | params = { 225 | id, 226 | }; 227 | } else if (typeof id === 'string') { 228 | params = { 229 | names: `${id}`, 230 | }; 231 | } 232 | if (params === false) { 233 | return cb({ 234 | message: 'missing_id', 235 | }); 236 | } 237 | this._api('GET', '/symbols', params, (err, response) => { 238 | if (err) return cb(err); 239 | if (!response.symbols.length) { 240 | return cb({ 241 | message: 'symbol_not_found', 242 | }); 243 | } 244 | cb(null, response.symbols[0]); 245 | }); 246 | } 247 | async getSymbols(tickers) { 248 | const { symbols } = await this.request('GET', '/symbols', { names: tickers.join(',') }) 249 | return symbols.map(data => new QuestradeSymbol(this, data)) 250 | } 251 | async getSymbol(ticker) { 252 | const [symbol] = await this.getSymbols([ticker]) 253 | return symbol 254 | } 255 | async searchSymbols(prefix, offset = 0) { 256 | if (!prefix) { 257 | throw new Error('Missing prefix') 258 | } 259 | const { symbols } = await this.request('GET', '/symbols/search', { prefix, offset }) 260 | return symbols.map(data => new QuestradeSymbol(this, data)) 261 | } 262 | async getMarkets() { 263 | const { markets } = await this._api('GET', '/markets') 264 | return markets 265 | } 266 | async getQuotesById(symbolIds) { 267 | const { quotes } = await this.request('GET', '/markets/quotes', { ids: symbolIds.join(',') }) 268 | return _.keyBy(quotes, 'symbolId') 269 | } 270 | } 271 | 272 | 273 | module.exports = QuestradeApi 274 | -------------------------------------------------------------------------------- /README-v1.md: -------------------------------------------------------------------------------- 1 | Questrade API (v1) 2 | ============= 3 | 4 | ## Deprecation notice 5 | ``` 6 | This API is no longer used, please refer to the v2 README for the latest version API. 7 | ``` 8 | 9 | [![npm package](https://nodei.co/npm/questrade.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/questrade/) 10 | 11 | [![Build status](https://img.shields.io/travis/leanderlee/questrade.svg?style=flat-square)](https://travis-ci.org/leanderlee/questrade) 12 | [![Dependency Status](https://img.shields.io/david/leanderlee/questrade.svg?style=flat-square)](https://david-dm.org/leanderlee/questrade) 13 | [![Known Vulnerabilities](https://snyk.io/test/npm/questrade/badge.svg?style=flat-square)](https://snyk.io/test/npm/questrade) 14 | [![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat-square)](https://gitter.im/leanderlee/questrade?utm_source=badge) 15 | 16 | 17 | This API is an easy way to use the [Questrade API](www.questrade.com/api/documentation/getting-started) immediately. 18 | 19 | ### Features 20 | 21 | - Token management 22 | - Easy to use API calls 23 | - Auto-select primary account 24 | 25 | ## Getting Started 26 | 27 | Simply start by installing the questrade library: 28 | 29 | ```bash 30 | npm install --save questrade 31 | ``` 32 | 33 | You will then need to get an [API key](https://login.questrade.com/APIAccess/userapps.aspx). 34 | 35 | After that's it's really simple to use: 36 | 37 | ```js 38 | var Questrade = require('questrade'); 39 | 40 | var qt = new Questrade(''); 41 | // - OR - 42 | var qt = new Questrade('./path/to/file'); // Location of a text file with the API key 43 | 44 | // Wait to login 45 | qt.on('ready', function () { 46 | 47 | // Access your account here 48 | qt.getAccounts() 49 | qt.getBalances() 50 | 51 | // Get Market quotes 52 | qt.getQuote('MSFT') 53 | 54 | // ... etc. See the full documentation for all the calls you can make! 55 | }) 56 | ``` 57 | 58 | Apart from the `ready` event, we emit include `error` on fatal errors, and `refresh` when the login token is refreshed. 59 | 60 | I would not recommend calling other calls before the `ready` event fires. 61 | 62 | ### Security and Token management 63 | 64 | Questrade's security token system requires that you save the latest refresh token that it vends you. After you create one in the user apps page, our library needs to save a key somewhere onto disk. By default, we create a folder for these keys in `./keys` at your working directory, but you can change the directory location or to load from a text file (with the key as its contents). 65 | 66 | In order to do that, you should set either the `keyDir` option (defaults to `./keys`) or `keyFile` to point to a file (defaults to using a directory.) -- See full options below. 67 | 68 | ## Switching Accounts 69 | 70 | By default, if you instantiate the `Questrade` class without passing in an account ID to options, we will try to find and select the primary account (by fetching a list of all the accounts). If you want to change the account, simply do: 71 | 72 | ```js 73 | qt.account = '123456'; // Switch to account 123456 -- All future calls will use this account. 74 | ``` 75 | 76 | ## Streaming 77 | 78 | For those accounts that have L1 data access (either practice account or Advanced market data packages) you can stream live market data. 79 | 80 | ```js 81 | var request = require('request'); 82 | var Questrade = require('questrade'); 83 | 84 | var options = { 85 | test: true, // For practice accounts 86 | seedToken: 'YOURTOKENHERE', 87 | } 88 | 89 | var qt = new Questrade(options); 90 | 91 | // Wait to login 92 | qt.on('ready', function (err) { 93 | // Websocket port changes by API and by symbol. So you have to get the port every time you need different data stream 94 | var getWebSocketURL = function (symbolId, cb) { 95 | var webSocketURL; 96 | request({ 97 | method: 'GET', 98 | url: qt.apiUrl + '/markets/quotes?ids=' + symbolId + '&stream=true&mode=WebSocket', 99 | auth: { 100 | bearer: qt.accessToken 101 | } 102 | }, function (err, http, body) { 103 | if (err) { 104 | cb(err, null); 105 | } else { 106 | response = JSON.parse(body); 107 | webSocketURL = qt.api_server.slice(0, -1) + ':' + response.streamPort + '/' + qt.apiVersion + '/markets/quotes?ids=' + symbolId + 'stream=true&mode=WebSocket'; 108 | cb(null, webSocketURL); 109 | } 110 | }); 111 | } 112 | getWebSocketURL('9291,8049', function (err, webSocketURL) { // BMO.TO & AAPL 113 | console.log(webSocketURL); 114 | const WebSocket = require('ws'); 115 | const ws = new WebSocket(webSocketURL); 116 | 117 | ws.on('open', function open() { 118 | ws.send(qt.accessToken); 119 | }); 120 | 121 | ws.on('message', function incoming(data) { 122 | console.log(data); 123 | // Do what you want with the data 124 | }); 125 | 126 | // CLOSING WebSocket Connections otherwise will remain open 127 | process.on('exit', function () { 128 | if (ws) { 129 | console.log('CLOSE WebSocket'); 130 | ws.close(); 131 | } 132 | }); 133 | 134 | //catches ctrl+c event 135 | process.on('SIGINT', function () { 136 | if (ws) { 137 | console.log('CLOSE WebSocket SIGINT'); 138 | ws.close(); 139 | } 140 | }); 141 | 142 | //catches uncaught exceptions 143 | process.on('uncaughtException', function () { 144 | if (ws) { 145 | console.log('CLOSE WebSocket'); 146 | ws.close(); 147 | } 148 | }); 149 | }); 150 | 151 | }) 152 | 153 | ``` 154 | 155 | ## Some examples 156 | 157 | #### Account Info 158 | ```js 159 | qt.getBalances(function (err, balances) {}) 160 | qt.getAccounts(function (err, accounts) {}) 161 | qt.getActivities(function (err, activities) {}) 162 | qt.getMarkets(function (err, markets) {}) 163 | ``` 164 | 165 | #### Orders 166 | ```js 167 | qt.getOrder(orderId, function (err, order) {}) 168 | qt.getOpenOrders(function (err, orders) {}) 169 | qt.getAllOrders(function (err, orders) {}) 170 | qt.getClosedOrders(function (err, orders) {}) 171 | qt.createOrder(newOrder, function (err, response) {}) 172 | qt.updateOrder(orderId, newOrder, function (err, response) {}) 173 | qt.removeOrder(orderId, function (err, response) {}) 174 | qt.testOrder(order, function (err, impact) {}) 175 | ``` 176 | 177 | #### Quotes 178 | ```js 179 | qt.getSymbol(symbolId, function (err, symbol) {}) 180 | qt.getSymbol('MSFT', function (err, symbol) {}) 181 | qt.search('B', function (err, symbols) {}) 182 | qt.getQuote('MSFT', function (err, quote) {}) 183 | qt.getQuotes(['MSFT', 'AAPL', 'BMO'], function (err, quotes) {}) 184 | qt.getCandles(symbolId, options, function (err, candles) {}) 185 | ``` 186 | 187 | #### Strategy 188 | ```js 189 | qt.createStrategy(newStrategy, function (err, response) {}) 190 | qt.testStrategy(strategy, function (err, impact) {}) 191 | ``` 192 | 193 | #### Option Chain 194 | ```js 195 | qt.getSymbol('MSFT', function (err, symbol) { 196 | qt.getOptionChain(symbol.symbolId, function (err, options) { 197 | var filters = []; 198 | Object.keys(options).forEach(function (expiryDate) { 199 | filters.push({ 200 | optionType: 'Call', 201 | underlyingId: symbol.symbolId, 202 | expiryDate: expiryDate 203 | }) 204 | filters.push({ 205 | optionType: 'Put', 206 | underlyingId: symbol.symbolId, 207 | expiryDate: expiryDate 208 | }) 209 | }) 210 | qt.getOptionQuoteSimplified(filters, function (err, options) { 211 | /* 212 | 213 | options = { 214 | MSFT: { 215 | Call: { 216 | '2016-06-24T00:00:00.000000-04:00': [Option Chain], 217 | '2016-07-01T00:00:00.000000-04:00': [Option Chain], 218 | '2016-07-08T00:00:00.000000-04:00': [Option Chain], 219 | '2016-07-15T00:00:00.000000-04:00': [Option Chain], 220 | '2016-07-22T00:00:00.000000-04:00': [Option Chain], 221 | '2016-07-29T00:00:00.000000-04:00': [Option Chain], 222 | '2016-08-05T00:00:00.000000-04:00': [Option Chain], 223 | '2016-08-19T00:00:00.000000-04:00': [Option Chain], 224 | '2016-09-16T00:00:00.000000-04:00': [Option Chain], 225 | '2016-10-21T00:00:00.000000-04:00': [Option Chain], 226 | '2017-01-20T00:00:00.000000-04:00': [Option Chain], 227 | '2017-04-21T00:00:00.000000-04:00': [Option Chain], 228 | '2017-06-16T00:00:00.000000-04:00': [Option Chain], 229 | '2018-01-19T00:00:00.000000-04:00': [Option Chain] 230 | }, 231 | Put: { 232 | '2016-06-24T00:00:00.000000-04:00': [Option Chain], 233 | '2016-07-01T00:00:00.000000-04:00': [Option Chain], 234 | '2016-07-08T00:00:00.000000-04:00': [Option Chain], 235 | '2016-07-15T00:00:00.000000-04:00': [Option Chain], 236 | '2016-07-22T00:00:00.000000-04:00': [Option Chain], 237 | '2016-07-29T00:00:00.000000-04:00': [Option Chain], 238 | '2016-08-05T00:00:00.000000-04:00': [Option Chain], 239 | '2016-08-19T00:00:00.000000-04:00': [Option Chain], 240 | '2016-09-16T00:00:00.000000-04:00': [Option Chain], 241 | '2016-10-21T00:00:00.000000-04:00': [Option Chain], 242 | '2017-01-20T00:00:00.000000-04:00': [Option Chain], 243 | '2017-04-21T00:00:00.000000-04:00': [Option Chain], 244 | '2017-06-16T00:00:00.000000-04:00': [Option Chain], 245 | '2018-01-19T00:00:00.000000-04:00': [Option Chain] 246 | } 247 | } 248 | } 249 | 250 | [Option Chain] = 251 | { 252 | '30.00': { 253 | symbol: 'MSFT16Jun17C30.00', 254 | symbolId: 14053313, 255 | lastTradePrice: 20 256 | }, 257 | '35.00': { 258 | symbol: 'MSFT16Jun17C35.00', 259 | symbolId: 14053314, 260 | lastTradePrice: 15.3 261 | }, 262 | 263 | ... etc 264 | 265 | '75.00': { 266 | symbol: 'MSFT16Jun17C75.00', 267 | symbolId: 14053324, 268 | lastTradePrice: null 269 | } 270 | } 271 | */ 272 | }) 273 | }) 274 | }) 275 | ``` 276 | ### Full Options 277 | 278 | - **test** - Whether or not to use real or fake login server (and API server) 279 | - **keyDir** - Directory location of tokens to be saved. Defaults to `./keys`. 280 | - **keyFile** - Instead of picking a directory location, specify the exact key file to use instead. 281 | - **account** - Specify which account ID to use 282 | - **apiVersion** - Defaults to `v1` 283 | 284 | 285 | ### Full Documentation 286 | 287 | - **setPrimaryAccount** (cb) 288 | - **getAccounts** (cb) 289 | - **getPositions** (cb) 290 | - **getBalances** (cb) 291 | - **getExecutions** (cb) 292 | - **getOrder** (id, cb) 293 | - **getOrders** (ids, cb) 294 | - **getOpenOrders** (cb) 295 | - **getAllOrders** (cb) 296 | - **getClosedOrders** (cb) 297 | - **getActivities** (cb) 298 | - **getSymbol** (id, cb) 299 | - **getSymbols** (ids, cb) 300 | - **search** (query, offset, cb) 301 | - **getOptionChain** (symbolId, cb) 302 | - **getMarkets** (cb) 303 | - **getQuote** (id, cb) 304 | - **getQuotes** (ids, cb) 305 | - **getOptionQuote** (filters, cb) 306 | - **getOptionQuoteSimplified** (filters, cb) 307 | - **getCandles** (id, opts, cb) 308 | - **createOrder** (opts, cb) 309 | - **updateOrder** (id, opts, cb) 310 | - **testOrder** (opts, cb) 311 | - **removeOrder** (id, cb) 312 | - **createStrategy** (opts, cb) 313 | - **testStrategy** (opts, cb) 314 | 315 | ### Contributions 316 | Are welcome! 317 | --------------------------------------------------------------------------------