├── runtime ├── sample.js ├── index.js └── context.js ├── lib ├── model │ ├── currency.js │ ├── positions.js │ ├── subscription.js │ ├── trades.js │ ├── account.js │ ├── accounts.js │ ├── displaygroups.js │ ├── system.js │ ├── orders.js │ ├── depth.js │ ├── candlesticks.js │ ├── quote.js │ ├── contract.js │ ├── market.js │ └── order.js ├── service │ ├── relay.js │ ├── replay.js │ ├── dispatch.js │ ├── mock.js │ ├── request.js │ ├── proxy.js │ └── service.js ├── session.js ├── constants.js └── symbol.js ├── README.md ├── doc ├── remoting.md ├── orders.md ├── symbols.md └── service.md ├── html ├── repl.html └── debug.html ├── package.json ├── example └── startup.js ├── run.js └── index.js /runtime/sample.js: -------------------------------------------------------------------------------- 1 | /* rules file */ 2 | 3 | var x, y, z; 4 | 5 | when: if (x > 5) { 6 | console.log("Too much!") 7 | x = 1 8 | } 9 | 10 | set: { 11 | x = y + z 12 | } 13 | 14 | set: x = y + z 15 | 16 | 17 | 18 | var series = $CL.contract.history() -------------------------------------------------------------------------------- /lib/model/currency.js: -------------------------------------------------------------------------------- 1 | const getSymbolFromCurrency = require('currency-symbol-map') 2 | 3 | class Currency { 4 | 5 | constructor(currency, amount) { 6 | this.abbreviation = currency; 7 | this.amount = amount; 8 | } 9 | 10 | get formatted() { 11 | return getSymbolFromCurrency(this.abbreviation) + this.amount.format(2); 12 | } 13 | 14 | toString() { 15 | return this.formatted; 16 | } 17 | 18 | } 19 | 20 | module.exports = Currency; -------------------------------------------------------------------------------- /lib/model/positions.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"); 2 | 3 | class Positions extends Subscription { 4 | 5 | constructor(service) { 6 | super(service); 7 | } 8 | 9 | async stream() { 10 | return new Promise((yes, no) => { 11 | this.subscriptions.push(this.service.positions().on("data", data => { 12 | if (!this[data.contract.conId]) this[data.contract.conId] = { }; 13 | this[data.contract.conId][data.accountName] = data; 14 | }).on("end", cancel => { 15 | this.loaded = true; 16 | yes(this); 17 | }).on("error", err => { 18 | this.error = err; 19 | no(err); 20 | }).send()); 21 | }); 22 | } 23 | 24 | } 25 | 26 | module.exports = Positions; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IB Javascript Container 2 | 3 | Container to host Interactive Brokers trading system logic written in Javascript. 4 | 5 | ## Installation 6 | 7 | npm i ibjs 8 | node ./node_modules/ibjs/run init 9 | 10 | ## License 11 | 12 | Copyright 2018 Jonathan Hollinger 13 | 14 | This is an open source project unrelated to Interactive Brokers. 15 | 16 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /doc/remoting.md: -------------------------------------------------------------------------------- 1 | # Remoting 2 | 3 | `Service` instances also supports a mechanism to relay streaming responses to proxy instances of the SDK, enabling a distributed/networked system architecture. The `relay` method takes a `EventEmitter` compatible (i.e. implements `emit` and `on`) object and relays `data`, `error`, and `end` events. A `Proxy` is a `Service`-compatible object that can be instantiated remotely and use a similar `EventEmitter` compatible transport (e.g. [socket.io](http://socket.io/)) to communicate with a `Relay` server. 4 | 5 | __Server__ 6 | ```javascript 7 | let app = require('http').createServer(handler), 8 | session = sdk.session({ host: "localhost", port: 4001 }), 9 | io = require('socket.io')(app); 10 | 11 | session.service.socket.on("connected", () => { 12 | session.service.relay(io); 13 | app.listen(8080); 14 | }).connect(); 15 | ``` 16 | 17 | __Client__ 18 | ```javascript 19 | var io = require('socket.io-client')('http://localhost:8080'), 20 | session = sdk.proxy(io); 21 | 22 | session.service.relay(socket); 23 | ``` -------------------------------------------------------------------------------- /html/repl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibjs", 3 | "description": "Interactive Brokers javascript container.", 4 | "author": "Jonathan Hollinger", 5 | "license": "MIT", 6 | "version": "0.17.0", 7 | "main": "index.js", 8 | "scripts": { 9 | "start": "node run.js" 10 | }, 11 | "homepage": "https://github.com/zgsrc/ibjs#readme", 12 | "dependencies": { 13 | "body-parser": "^1.18.2", 14 | "commander": "^2.14.1", 15 | "currency-symbol-map": "^4.0.3", 16 | "escodegen": "^1.9.1", 17 | "esprima": "^4.0.0", 18 | "express": "^4.16.2", 19 | "hyperactiv": "git+https://github.com/zgsrc/hyperactiv.git", 20 | "ib": "^0.2.4", 21 | "line-by-line": "^0.1.6", 22 | "luxon": "^0.3.1", 23 | "memoize-fs": "^1.4.0", 24 | "node-schedule": "^1.3.0", 25 | "numbers": "^0.6.0", 26 | "numeric": "^1.2.6", 27 | "p-memoize": "^1.0.0", 28 | "simple-statistics": "^5.2.1", 29 | "sugar": "^2.0.4", 30 | "ws": "^4.1.0", 31 | "xml2js": "^0.4.19" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/zgsrc/ibjs.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/zgsrc/ibjs/issues" 39 | }, 40 | "keywords": [ 41 | "interactive brokers", 42 | "ib", 43 | "ib api" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /lib/model/subscription.js: -------------------------------------------------------------------------------- 1 | const { Observable, Computable } = require("hyperactiv/mixins"); 2 | 3 | class Subscription extends Computable(Observable(Object)) { 4 | 5 | constructor(base, data) { 6 | super({ }); 7 | 8 | if (base) { 9 | if (base.service) { 10 | Object.defineProperty(this, "contract", { value: base, enumerable: false }); 11 | Object.defineProperty(this, "service", { value: base.service, enumerable: false }); 12 | } 13 | else if (base.socket) { 14 | Object.defineProperty(this, "service", { value: base, enumerable: false }); 15 | } 16 | } 17 | 18 | Object.defineProperty(this, "subscriptions", { value: [ ], enumerable: false }); 19 | } 20 | 21 | dispose() { 22 | super.dispose(); 23 | 24 | this.streaming = false; 25 | 26 | while (this.subscriptions.length) { 27 | this.subscriptions.pop().cancel(); 28 | } 29 | 30 | Object.values(this).forEach(value => { 31 | if (value.dispose && typeof value == "function") { 32 | value.dispose(); 33 | } 34 | }); 35 | } 36 | 37 | } 38 | 39 | module.exports = Subscription; -------------------------------------------------------------------------------- /lib/service/relay.js: -------------------------------------------------------------------------------- 1 | function relay(service, socket) { 2 | let map = { }; 3 | 4 | socket.on("command", command => { 5 | service[command.fn](...command.args); 6 | }); 7 | 8 | socket.on("request", request => { 9 | request.args = request.args || [ ]; 10 | let req = service[request.fn](...request.args); 11 | map[request.ref] = req.id; 12 | req.proxy(socket, request.ref).send(); 13 | }).on("cancel", request => { 14 | service.dispatch.cancel(map[request.ref]); 15 | delete map[request.ref]; 16 | }); 17 | 18 | let onConnected = () => socket.emit("connected", { time: Date.create() }), 19 | onDisconnected = () => socket.emit("disconnected", { time: Date.create() }); 20 | 21 | service.socket 22 | .on("connected", onConnected) 23 | .on("disconnected", onDisconnected); 24 | 25 | socket.on("disconnect", () => { 26 | Object.values(map).forEach(id => service.dispatch.cancel(id)); 27 | map = null; 28 | 29 | service.socket.removeListener("connected", onConnected); 30 | service.socket.removeListener("disconnected", onDisconnected); 31 | }); 32 | 33 | socket.on("error", err => { 34 | console.log(err); 35 | }); 36 | 37 | socket.emit("connected", { time: Date.create() }); 38 | } 39 | 40 | module.exports = relay; -------------------------------------------------------------------------------- /lib/model/trades.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"); 2 | 3 | class Trades extends Subscription { 4 | 5 | constructor(service, options) { 6 | super(service); 7 | 8 | options = options || { }; 9 | 10 | let filter = { }; 11 | if (options.account) filter.acctCode = options.account; 12 | if (options.client) filter.clientId = options.client; 13 | if (options.exchange) filter.exchange = options.exchange; 14 | if (options.secType) filter.secType = options.secType; 15 | if (options.side) filter.side = options.side; 16 | if (options.symbol) filter.symbol = options.symbol; 17 | if (options.time) filter.time = options.time; 18 | 19 | this.subscriptions.push(this.service.executions(filter)); 20 | } 21 | 22 | async stream() { 23 | return new Promise((yes, no) => { 24 | this.subscriptions[0].on("data", data => { 25 | if (!this[data.exec.permId]) this[data.exec.permId] = { }; 26 | this[data.exec.permId][data.exec.execId] = data; 27 | }).on("error", err => { 28 | this.error = err; 29 | no(err); 30 | }).on("end", () => { 31 | this.loaded = true; 32 | yes(this); 33 | }).send(); 34 | }); 35 | } 36 | 37 | } 38 | 39 | module.exports = Trades; -------------------------------------------------------------------------------- /doc/orders.md: -------------------------------------------------------------------------------- 1 | # Orders 2 | 3 | An `Order` can be initiated from a `Security` and has chainable methods to build and transmit an order. 4 | 5 | ```javascript 6 | AAPL.order() 7 | .sell(100) 8 | .show(10) 9 | .limit(100.50) 10 | .goodUntilCancelled() 11 | .transmit(); 12 | ``` 13 | 14 | Quantity and market side can be set with an appropriate method. Display size can be set an extra parameter or with the separate `show` method. 15 | 16 | ```javascript 17 | order.buy(100).show(10); 18 | order.buy(100, 10); 19 | order.trade(100, 10); 20 | 21 | order.sell(100).show(10); 22 | order.sell(100, 10); 23 | order.trade(-100, -10); 24 | ``` 25 | 26 | Order type is set with an appropriate method or manually. 27 | 28 | ```javascript 29 | order.market(); 30 | order.marketWithProtection(); 31 | order.marketThenLimit(); 32 | order.limit(100.50); 33 | 34 | order.stop(100.50); 35 | order.stopLimit(100.50, 100.48); 36 | order.stopWithProtection(100.50); 37 | ``` 38 | 39 | Time in force is presumed to be "DAY" unless otherwise specified. Timeframe can be set with appropriate methods. 40 | 41 | ```javascript 42 | order.goodToday() 43 | order.immediateOrCancel(); 44 | order.goodUntilCancelled().outsideRegularTradingHours(); 45 | ``` 46 | 47 | Order transmission can be performed in a single transaction or in parts. 48 | 49 | ```javascript 50 | // Will suppress certain IB warnings for large trades 51 | order.overridePercentageConstraints(); 52 | 53 | // Will open the order without transmitting it. 54 | order.save(); 55 | 56 | // Will open the order and trasmit it. 57 | order.transmit(); 58 | 59 | // Will update existing order. 60 | order.save(); 61 | 62 | // Will cancel order. 63 | order.cancel(); 64 | ``` -------------------------------------------------------------------------------- /lib/service/replay.js: -------------------------------------------------------------------------------- 1 | const LineByLineReader = require('line-by-line'); 2 | 3 | function replay(file, emitter, speed, done) { 4 | let reader = LineByLineReader(file); 5 | reader.pause(); 6 | 7 | let buffer = [ ]; 8 | reader.on("error", err => { 9 | emitter.emit("error", err); 10 | }).on("line", line => { 11 | line = line.split("|"); 12 | 13 | let time = parseInt(line[0]), 14 | name = line[2], 15 | data = JSON.parse(line.slice(3).join('')); 16 | 17 | buffer.push({ time: time, name: name, data: data }); 18 | 19 | if (buffer.length > 100) { 20 | reader.pause(); 21 | } 22 | }).on("end", () => { 23 | resume = false; 24 | 25 | let clear = setInterval(() => { 26 | if (buffer.length == 0) { 27 | clearInterval(loop); 28 | clearInterval(clear); 29 | if (done) done(); 30 | } 31 | }, 100); 32 | }); 33 | 34 | let delta = -1, 35 | resume = true; 36 | 37 | let loop = setInterval(() => { 38 | if (buffer.length < 100) { 39 | if (resume) reader.resume(); 40 | } 41 | 42 | if (delta == -1 && buffer.length) { 43 | delta = (new Date()).getTime() - (buffer[0].time / (speed || 1)); 44 | } 45 | 46 | let now = (new Date()).getTime(); 47 | while (buffer.length && now + 10 > (buffer[0].time / (speed || 1)) + delta) { 48 | let event = buffer.shift(); 49 | emitter.emit(event.name, ...event.data); 50 | } 51 | }, 50); 52 | } 53 | 54 | module.exports = replay; -------------------------------------------------------------------------------- /doc/symbols.md: -------------------------------------------------------------------------------- 1 | # Symbols 2 | 3 | The SDK lets you specify financial instruments in a readable symbol format. 4 | 5 | [date]? [symbol] [side/type]? (in [currency])? (on [exchange])? (at [strike])? 6 | 7 | So for example, a stock might look like: 8 | 9 | * IBM 10 | * IBM stock 11 | * IBM stock in MXN on BMV 12 | 13 | Futures: 14 | 15 | * Jan16 CL futures 16 | * Jan16 CL futures in USD on NYMEX 17 | 18 | Options: 19 | 20 | * Sep'17 AAPL puts at 110 21 | 22 | Indices: 23 | 24 | * INDU index 25 | 26 | Currencies: 27 | 28 | * EUR currency in USD 29 | * EUR.USD currency 30 | * EUR,USD currency 31 | * EUR/USD currency 32 | * EUR*USD currency 33 | 34 | ## Date 35 | 36 | Dates must be in the format of a three letter month abbreviation, and then either a 2 or 4 digit year component. The year component may be separated by a dash (-), slash (/), or apostrophe ('). If the date component is omitted, the current year is used. 37 | 38 | For example: 39 | 40 | Jan 41 | Jun18 42 | Sep'2018 43 | 44 | Alternatively, the "Front" syntax can be used to reference the front contract. This format is composed of the word "Front" (or "First"), followed by a cutoff day of month (i.e. the last safe day to trade before rolling over to the next contract), and an optional month offset. If the cutoff day is omitted, it is assumed to be 15. 45 | 46 | For example: 47 | 48 | Front 49 | Front+1 50 | Front20 51 | Front15+1 52 | Front20+5 53 | 54 | ## Symbol 55 | 56 | Symbols should be the common symbol for the contract. You can use the [IB contract search](https://pennies.interactivebrokers.com/cstools/contract_info) to find this. 57 | 58 | ## Type 59 | 60 | Type must be either a security type, or in the case of options, a side. See options at `sdk.flags.SECURITY_TYPE`. 61 | 62 | ## Currency 63 | 64 | Valid currencies are in `sdk.flags.CURRENCIES`. -------------------------------------------------------------------------------- /lib/service/dispatch.js: -------------------------------------------------------------------------------- 1 | const Request = require("./request"); 2 | 3 | class Dispatch { 4 | 5 | constructor(id) { 6 | this.id = 1 || id; 7 | this.requests = { }; 8 | } 9 | 10 | singleton(call, send, cancel, timeout, event) { 11 | if (this.requests[event]) { 12 | return this.requests[event]; 13 | } 14 | else { 15 | let request = new Request(this, event, call, send, cancel, timeout); 16 | this.requests[event] = request; 17 | return request; 18 | } 19 | } 20 | 21 | instance(call, send, cancel, timeout) { 22 | let request = new Request(this, this.id, call, send, cancel, timeout); 23 | this.requests[request.id] = request; 24 | this.id++; 25 | return request; 26 | } 27 | 28 | data(id, data) { 29 | if (this.requests[id]) { 30 | this.requests[id].emit("data", data, () => this.cancel(id)); 31 | } 32 | } 33 | 34 | end(id) { 35 | if (this.requests[id]) { 36 | this.requests[id].emit("end", () => this.cancel(id)); 37 | } 38 | } 39 | 40 | error(id, err) { 41 | if (this.requests[id]) { 42 | this.requests[id].emit("error", err, () => this.cancel(id)); 43 | } 44 | } 45 | 46 | cancel(id) { 47 | if (this.requests[id]) { 48 | this.requests[id].cancel(); 49 | } 50 | } 51 | 52 | connected() { 53 | for (let p in this.requests) { 54 | if (this.requests[p] && this.requests[p].emit) { 55 | this.requests[p].emit("connected"); 56 | } 57 | } 58 | } 59 | 60 | disconnected() { 61 | for (let p in this.requests) { 62 | if (this.requests[p] && this.requests[p].emit) { 63 | this.requests[p].emit("disconnected"); 64 | } 65 | } 66 | } 67 | 68 | } 69 | 70 | module.exports = Dispatch; -------------------------------------------------------------------------------- /lib/model/account.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | Currency = require("./currency"); 3 | 4 | class Account extends Subscription { 5 | 6 | constructor(service, options) { 7 | super(service); 8 | 9 | if (typeof options == "string") options = { id: options }; 10 | if (typeof options.id != "string") throw new Error("Account id is required."); 11 | 12 | this.id = options.id; 13 | this.balances = { }; 14 | this.positions = { }; 15 | } 16 | 17 | async stream() { 18 | return new Promise((yes, no) => { 19 | this.subscriptions.push(this.service.accountUpdates(this.id).on("data", data => { 20 | if (data.key) { 21 | let value = data.value; 22 | if (/^\-?[0-9]+(\.[0-9]+)?$/.test(value)) value = parseFloat(value); 23 | else if (value == "true") value = true; 24 | else if (value == "false") value = false; 25 | 26 | if (data.currency && data.currency != "") { 27 | if (data.currency != value) { 28 | value = new Currency(data.currency, value); 29 | } 30 | } 31 | 32 | let key = data.key.camelize(false); 33 | this.balances[key] = value; 34 | } 35 | else if (data.timestamp) { 36 | this.timestamp = Date.create(data.timestamp); 37 | } 38 | else if (data.contract) { 39 | this.positions[data.contract.conId] = data; 40 | } 41 | }).once("end", () => { 42 | this.loaded = true; 43 | yes(this); 44 | }).on("error", err => { 45 | this.error = err; 46 | no(err); 47 | }).send()); 48 | }) 49 | } 50 | 51 | } 52 | 53 | module.exports = Account; -------------------------------------------------------------------------------- /lib/model/accounts.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | constants = require("../constants"), 3 | Currency = require("./currency"); 4 | 5 | class Accounts extends Subscription { 6 | 7 | /* string group, array tags, boolean positions */ 8 | constructor(service, options) { 9 | super(service); 10 | 11 | options = options || { }; 12 | 13 | this.subscriptions.push(this.service.accountSummary( 14 | options.group || "All", 15 | options.tags || Object.values(constants.ACCOUNT_TAGS).join(',') 16 | )); 17 | } 18 | 19 | async stream() { 20 | return new Promise((yes, no) => { 21 | this.subscriptions[0].on("data", datum => { 22 | if (datum.account && datum.tag) { 23 | let id = datum.account; 24 | if (this[id] == null) this[id] = { }; 25 | if (datum.tag) { 26 | var value = datum.value; 27 | if (/^\-?[0-9]+(\.[0-9]+)?$/.test(value)) value = parseFloat(value); 28 | else if (value == "true") value = true; 29 | else if (value == "false") value = false; 30 | 31 | 32 | if (datum.currency && datum.currency != "") { 33 | if (datum.currency != value) { 34 | value = new Currency(datum.currency, value); 35 | } 36 | } 37 | 38 | var key = datum.tag.camelize(false); 39 | this[id][key] = value; 40 | } 41 | } 42 | }).on("end", cancel => { 43 | this.loaded = true; 44 | yes(); 45 | }).on("error", err => { 46 | this.error = err; 47 | no(err); 48 | }).send(); 49 | }); 50 | } 51 | 52 | } 53 | 54 | module.exports = Accounts; -------------------------------------------------------------------------------- /lib/model/displaygroups.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | Contract = require("./contract"); 3 | 4 | class DisplayGroups extends Subscription { 5 | 6 | constructor(service) { 7 | super(service); 8 | } 9 | 10 | async stream() { 11 | return new Promise((yes, no) => { 12 | this.service.queryDisplayGroups().on("data", groups => { 13 | groups.forEach((group, index) => { 14 | let displayGroup = this.service.subscribeToGroupEvents(group); 15 | this.subscriptions.push(displayGroup); 16 | this[index] = new DisplayGroup(this, group, displayGroup.id); 17 | displayGroup.on("data", async contract => { 18 | if (contract && contract != "none") { 19 | try { 20 | this[index].contract = await contract.first(this.service, contract); 21 | delete this[index].error; 22 | } 23 | catch (ex) { 24 | this[index].error = ex; 25 | } 26 | } 27 | else { 28 | this[index].contract = null; 29 | } 30 | }).send(); 31 | 32 | this.loaded = true; 33 | yes(); 34 | }); 35 | }).once("error", err => no(err)).send(); 36 | }) 37 | } 38 | 39 | } 40 | 41 | class DisplayGroup { 42 | constructor(displayGroups, group, id) { 43 | this.group = group; 44 | this.contract = null; 45 | Object.defineProperty(this, "update", { 46 | value: contract => { 47 | if (contract.summary) displayGroups.service.updateDisplayGroup(id, this.contract.summary.conId.toString() + "@" + this.contract.summary.exchange); 48 | else if (contract) displayGroups.service.updateDisplayGroup(id, this.contract.toString()); 49 | else throw new Error("No contract specified."); 50 | }, 51 | enumerable: false 52 | }); 53 | } 54 | } 55 | 56 | module.exports = DisplayGroups; -------------------------------------------------------------------------------- /doc/service.md: -------------------------------------------------------------------------------- 1 | # Service 2 | 3 | The `Service` class makes the request/reponse paradigm more reliable and cogent. 4 | 5 | ```javscript 6 | let IB = require("ib"), 7 | sdk = require("ib-sdk"); 8 | 9 | let socket = new IB({ 10 | host: "localhost", 11 | port: 4001 12 | }); 13 | 14 | let service = new sdk.Service(socket); 15 | 16 | service.socket.on("connected", () => { 17 | // service ready for use 18 | }).connect(); 19 | ``` 20 | 21 | A `Service` uses a `Dispatch` to deconflict requests routed through the same socket. In most cases, there is one `Socket`, one `Dispatch`, and one `Service` in use. So by default, the `Service` class instantiates its own `Dispatch`. However, in cases where multiple `Service` instances utilize the same `Socket`, they should share a `Dispatch`. 22 | 23 | ```javascript 24 | let optionalRequestSeed = 1, // default is 1 25 | dispatch = new sdk.Dispatch(optionalRequestSeed), 26 | service = new sdk.Service(socket, dispatch); 27 | 28 | service.dispath === dispatch; 29 | ``` 30 | 31 | The `Service` class provides method analogs to the native API calls that synchronously return promise-esque `Request` objects. 32 | 33 | ```javascript 34 | service.positions() 35 | .on("error", (err, cancel) => { 36 | if (err.timeout) console.log("timeout!"); 37 | else console.log(err); 38 | cancel(); 39 | }).on("data", (data, cancel) => { 40 | console.log(data); 41 | }).on("end", cancel => { 42 | console.log("done"); 43 | }).on("close", () => { 44 | console.log("cancel was called."); 45 | }).send(); 46 | 47 | // service requests 48 | service.system(); 49 | service.currentTime(); 50 | service.contractDetails(contract); 51 | service.fundamentalData(contract, reportType); 52 | service.historicalData(contract, endDateTime, durationStr, barSizeSetting, whatToShow, useRTH, formatDate); 53 | service.realTimeBars(contract, barSize, whatToShow, useRTH); 54 | service.mktData(contract, genericTickList, snapshot); 55 | service.mktDepth(contract, numRows); 56 | service.scannerParameters(); 57 | service.scannerSubscription(subscription); 58 | service.accountSummary(group, tags); 59 | service.accountUpdates(subscribe, acctCode); 60 | service.executions(filter); 61 | service.commissions(); 62 | service.openOrders(); 63 | service.allOpenOrders(); 64 | service.positions(); 65 | service.orderIds(numIds); 66 | service.placeOrder(contract, order); 67 | service.exerciseOptions(contract, exerciseAction, exerciseQuantity, account, override); 68 | service.newsBulletins(allMsgs); 69 | serivce.queryDisplayGroups(); 70 | service.subscribeToGroupEvents(); 71 | serivce.updateDisplayGroup(); 72 | ``` -------------------------------------------------------------------------------- /lib/service/mock.js: -------------------------------------------------------------------------------- 1 | const Events = require("events"), 2 | IB = require("ib"); 3 | 4 | class Mock extends Events { 5 | 6 | constructor() { 7 | super(); 8 | } 9 | 10 | get contract() { 11 | return IB.contract; 12 | } 13 | 14 | get order() { 15 | return IB.order; 16 | } 17 | 18 | get util() { 19 | return IB.util; 20 | } 21 | 22 | replay(file) { 23 | require("./replay")(file, this); 24 | } 25 | 26 | connect() { } 27 | disconnect() { } 28 | 29 | calculateImpliedVolatility(reqId, contract, optionPrice, underPrice) { } 30 | calculateOptionPrice(reqId, contract, volatility, underPrice) { } 31 | cancelAccountSummary(reqId) { } 32 | cancelCalculateImpliedVolatility(reqId) { } 33 | cancelCalculateOptionPrice(reqId) { } 34 | cancelFundamentalData(reqId) { } 35 | cancelHistoricalData(tickerId) { } 36 | cancelMktData(tickerId) { } 37 | cancelMktDepth(tickerId) { } 38 | cancelNewsBulletins() { } 39 | cancelOrder(id) { } 40 | cancelPositions() { } 41 | cancelRealTimeBars(tickerId) { } 42 | cancelScannerSubscription(tickerId) { } 43 | exerciseOptions(tickerId, contract, exerciseAction, exerciseQuantity, account, override) { } 44 | placeOrder(id, contract, order) { } 45 | replaceFA(faDataType, xml) { } 46 | reqAccountSummary(reqId, group, tags) { } 47 | reqAccountUpdates(subscribe, acctCode) { } 48 | reqAllOpenOrders() { } 49 | reqAutoOpenOrders(bAutoBind) { } 50 | reqContractDetails(reqId, contract) { } 51 | reqCurrentTime() { } 52 | reqExecutions(reqId, filter) { } 53 | reqFundamentalData(reqId, contract, reportType) { } 54 | reqGlobalCancel() { } 55 | reqHistoricalData(tickerId, contract, endDateTime, durationStr, barSizeSetting, whatToShow, useRTH, formatDate, keepUpToDate) { } 56 | reqIds(numIds) { } 57 | reqManagedAccts() { } 58 | reqMarketDataType(marketDataType) { } 59 | reqMktData(tickerId, contract, genericTickList, snapshot, regulatorySnapshot) { } 60 | reqMktDepth(tickerId, contract, numRows) { } 61 | reqNewsBulletins(allMsgs) { } 62 | reqOpenOrders() { } 63 | reqPositions() { } 64 | reqRealTimeBars(tickerId, contract, barSize, whatToShow, useRTH) { } 65 | reqScannerParameters() { } 66 | reqScannerSubscription(tickerId, subscription) { } 67 | requestFA(faDataType) { } 68 | queryDisplayGroups(reqId) { } 69 | subscribeToGroupEvents(reqId, group) { } 70 | unsubscribeToGroupEvents(reqId) { } 71 | updateDisplayGroup(reqId, contract) { } 72 | setServerLogLevel(logLevel) { } 73 | } 74 | 75 | module.exports = Mock; -------------------------------------------------------------------------------- /example/startup.js: -------------------------------------------------------------------------------- 1 | require("ibjs").environment({ 2 | 3 | /* Auto set client id */ 4 | id: -1, 5 | /* Think out loud */ 6 | verbose: true, 7 | /* Host of IB API */ 8 | host: "localhost", 9 | /* Port of IB API */ 10 | port: 4001, 11 | /* Connection timeout */ 12 | timeout: 2500, 13 | 14 | /* Session order processing ("all", "local", "passive") */ 15 | orders: "passive", 16 | 17 | /* REPL interface */ 18 | repl: true, 19 | /* HTTP interface/port */ 20 | http: 8080, 21 | /* Static HTML path to serve */ 22 | html: "./html", 23 | 24 | /* Well known symbols */ 25 | symbols: { }, 26 | 27 | /* Initial subscriptions */ 28 | subscriptions: { 29 | /* System errors, bulletins, and fyi's */ 30 | system: true, 31 | /* Account details (bool for default or specify id) */ 32 | account: true, 33 | /* Summaries of all accounts */ 34 | accounts: false, 35 | /* Summaries of all positions */ 36 | positions: false, 37 | /* Trade history (default today or specify options object) */ 38 | trades: true, 39 | /* Pending and saved orders */ 40 | orders: true, 41 | /* Display groups used in TWS */ 42 | displayGroups: false, 43 | /* Contract descriptions of quotes to load */ 44 | quotes: [], 45 | /* Automatically stream quotes (bool or "all" for depth and candlesticks) */ 46 | autoStreamQuotes: false 47 | }, 48 | 49 | /* Global code */ 50 | global: "./global", 51 | /* Modularized code */ 52 | module: "./module", 53 | /* Save raw events to files */ 54 | log: "./log", 55 | /* Cache folder */ 56 | cache: "./cache", 57 | /* Lifecycle hooks */ 58 | hooks: { 59 | async init(config) { 60 | /* Modify config after it has been loaded */ 61 | }, 62 | async setup(session, context) { 63 | /* Modify context after it has been setup */ 64 | }, 65 | async ready(session, context) { 66 | /* Called after all initialization has completed */ 67 | }, 68 | async load(session, context) { 69 | /* Called after all globals and modules are loaded */ 70 | }, 71 | afterReplay(session, context) { 72 | /* Called after all events have been replayed from a file */ 73 | }, 74 | sigint(session, context) { 75 | /* Called if the process receives as SIGINT message */ 76 | session.close() 77 | }, 78 | exit(session, context) { 79 | /* Called if the process receives an 'exit' events from the terminal */ 80 | session.close() 81 | }, 82 | warning(msg) { 83 | /* Handles warnings */ 84 | console.warn(msg) 85 | }, 86 | error(err) { 87 | /* Handles errors */ 88 | console.error(err) 89 | } 90 | } 91 | 92 | }) -------------------------------------------------------------------------------- /lib/model/system.js: -------------------------------------------------------------------------------- 1 | //const Observable = require("hyperactiv/object/observable"); 2 | const Subscription = require('./subscription'), 3 | constants = require('../constants'); 4 | 5 | class System extends Subscription { 6 | 7 | constructor(service) { 8 | super(service); 9 | 10 | this.connectivity = { }; 11 | this.bulletins = [ ]; 12 | this.state = "disconnected"; 13 | 14 | this.service.system().on("data", data => { 15 | if (data.code == 321) { 16 | if (!this.readOnly && data.message.indexOf("Read-Only") > 0) { 17 | this.readOnly = true; 18 | this.emit("connectivity", "API is in read-only mode. Orders cannot be placed."); 19 | } 20 | } 21 | else if (data.code == 1100 || data.code == 2110) { 22 | this.state = "disconnected"; 23 | } 24 | else if (data.code == 1101 || data.code == 1102) { 25 | this.state = "connected"; 26 | } 27 | else if (data.code == 1300) { 28 | this.state = "disconnected"; 29 | } 30 | else if (data.code >= 2103 && data.code <= 2106 || data.code == 2119) { 31 | let name = data.message.from(data.message.indexOf(" is ") + 4).trim(); 32 | name = name.split(":"); 33 | 34 | let status = name[0]; 35 | name = name.from(1).join(":"); 36 | 37 | this.connectivity[name] = { name: name, status: status, time: Date.create() }; 38 | } 39 | else if (data.code >= 2107 && data.code <= 2108) { 40 | let name = data.message.trim(); 41 | name = name.split("."); 42 | 43 | let status = name[0]; 44 | name = name.from(1).join("."); 45 | 46 | this.connectivity[name] = { name: name, status: status, time: Date.create() }; 47 | } 48 | else if (data.code == 2148) { 49 | this.bulletins.push(data); 50 | } 51 | else if (data.code >= 2000 && data.code < 3000) { 52 | this.warning = data; 53 | } 54 | else { 55 | this.error = data; 56 | } 57 | }); 58 | 59 | this.subscriptions.push(this.service.newsBulletins(true).on("data", data => { 60 | this.bulletins.push(data); 61 | }).on("error", err => { 62 | this.error = err; 63 | }).send()); 64 | 65 | this.service.socket.on("connected", () => { 66 | this.state = "connected"; 67 | }).on("disconnected", () => { 68 | this.state = "disconnected"; 69 | }); 70 | } 71 | 72 | get useFrozenMarketData() { 73 | return this.service.lastMktDataType == constants.MARKET_DATA_TYPE.frozen; 74 | } 75 | 76 | set useFrozenMarketData(value) { 77 | this.service.mktDataType(value ? constants.MARKET_DATA_TYPE.frozen : constants.MARKET_DATA_TYPE.live); 78 | } 79 | 80 | } 81 | 82 | module.exports = System; -------------------------------------------------------------------------------- /lib/model/orders.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | Order = require("./order"), 3 | contract = require("./contract"); 4 | 5 | class Orders extends Subscription { 6 | 7 | constructor(service, local) { 8 | super(service); 9 | 10 | this.nextOrderId = Number.MAX_SAFE_INTEGER; 11 | 12 | this.subscriptions.push((local ? this.service.openOrders() : this.service.allOpenOrders()).on("data", async data => { 13 | if (data.nextOrderId) { 14 | this.nextOrderId = data.nextOrderId; 15 | } 16 | else { 17 | let id = data.orderId; 18 | if (id == 0) { 19 | if (data.state) id = data.state.permId; 20 | if (data.ticket) id = data.ticket.permId; 21 | id = id + "_readonly"; 22 | } 23 | 24 | if (this[id] == null) { 25 | this[id] = new Order(await contract.first(this.service, data.contract), data); 26 | } 27 | else { 28 | if (data.ticket) this[id].ticket = data.ticket; 29 | if (data.state) this[id].state = data.state; 30 | } 31 | 32 | if (data.orderId == 0) { 33 | this[id].readOnly = true; 34 | } 35 | } 36 | }).on("end", () => { 37 | this.loaded = true; 38 | }).on("error", err => { 39 | this.streaming = false; 40 | this.error = err; 41 | })); 42 | } 43 | 44 | async stream() { 45 | this.streaming = true; 46 | return new Promise((yes, no) => this.subscriptions[0].send().once("end", yes).once("error", no)); 47 | } 48 | 49 | newOrderIds(count) { 50 | this.service.orderIds(count > 0 ? count : 1); 51 | } 52 | 53 | placeOrder(order) { 54 | if (order.readOnly) { 55 | throw new Error("Cannot modify read-only trade."); 56 | } 57 | 58 | if (order.orderId == null) { 59 | order.orderId = this.nextOrderId; 60 | this[order.orderId] = order; 61 | } 62 | 63 | if (order.children.length) { 64 | order.children.forEach(child => { 65 | child.parentId = order.orderId; 66 | delete child.parent; 67 | }); 68 | } 69 | 70 | this.service.placeOrder(order.orderId, order.contract.summary, order.ticket).send(); 71 | 72 | return order; 73 | } 74 | 75 | cancelOrder(order) { 76 | if (order && order.orderId) { 77 | if (!order.readOnly) this.service.cancelOrder(order.orderId); 78 | else throw new Error("Cannot cancel read-only trade."); 79 | } 80 | else throw new Error("Order has not been placed."); 81 | } 82 | 83 | cancelAllOrders() { 84 | this.service.globalCancel(); 85 | } 86 | 87 | } 88 | 89 | const serviceLookup = { }; 90 | module.exports = (service, local) => { 91 | return serviceLookup[service] || (serviceLookup[service] = new Orders(service, local)); 92 | }; -------------------------------------------------------------------------------- /runtime/index.js: -------------------------------------------------------------------------------- 1 | require("sugar").extend() 2 | 3 | const { observe, computed, dispose } = require("hyperactiv"), 4 | { Observable, Computable } = require("hyperactiv/mixins"), 5 | ObservableObject = Computable(Observable(Object)); 6 | 7 | const utility = { 8 | at: require('node-schedule').scheduleJob, 9 | get time() { return Date.create() }, 10 | require, 11 | process, 12 | observe, 13 | computed, 14 | dispose, 15 | Observable, 16 | Computable, 17 | ObservableObject 18 | } 19 | 20 | let math = require("simple-statistics") 21 | Object.assign(math, require("numeric")) 22 | Object.assign(math, require("numbers")) 23 | 24 | const esprima = require("esprima"), 25 | escodegen = require("escodegen"); 26 | 27 | function computedStatement(...args) { 28 | return { 29 | "type": "ExpressionStatement", 30 | "expression": { 31 | "type": "CallExpression", 32 | "callee": { 33 | "type": "Identifier", 34 | "name": "computed" 35 | }, 36 | "arguments": [ 37 | { 38 | "type": "ArrowFunctionExpression", 39 | "id": null, 40 | "params": [], 41 | "body": { 42 | "type": "BlockStatement", 43 | "body": [ ...args ] 44 | }, 45 | "generator": false, 46 | "expression": false, 47 | "async": true 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | 54 | function translateRules(src) { 55 | let tree = esprima.parseScript(src); 56 | tree.body = tree.body.map(line => { 57 | if (line.type == "LabeledStatement") { 58 | if (line.label.name == "when") { 59 | if (line.body.type == "IfStatement") { 60 | console.log("Valid when statement") 61 | return computed(line.body) 62 | } 63 | else throw new Error("When statement must take an if condition") 64 | } 65 | else if (line.label.name == "set") { 66 | return computedStatement(line.body) 67 | } 68 | else return line; 69 | } 70 | else return line; 71 | }); 72 | 73 | return escodegen.generate(tree); 74 | } 75 | 76 | const Context = require("./context"); 77 | function createContext(session) { 78 | let context = new Context(require("../lib/constants"), math, utility, global); 79 | 80 | context.global("require('sugar').extend()"); 81 | context.resolvers.push(name => session.quote(name)); 82 | 83 | Object.defineProperty(context, "rules", { value: translateRules }) 84 | 85 | Object.defineProperty(context, "read", { 86 | value: path => { 87 | let src = fs.readFileSync(path).toString().trim(); 88 | src = path.endsWith(".r.js") || src.startsWith("/*rules*/") ? translateRules(src) : src; 89 | } 90 | }) 91 | 92 | Object.defineProperty(context, "import", { value: path => context.module(context.read(path)) }) 93 | 94 | Object.defineProperty(context, "include", { value: path => context.global(context.read(path)) }) 95 | 96 | context.scopes.push({ 97 | import: context.import, 98 | include: context.include 99 | }) 100 | 101 | return context 102 | } 103 | 104 | module.exports = createContext; -------------------------------------------------------------------------------- /html/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 |
14 |

Loading...

15 |
16 | 85 | 86 | -------------------------------------------------------------------------------- /runtime/context.js: -------------------------------------------------------------------------------- 1 | const esprima = require('esprima'), 2 | vm = require('vm'), 3 | repl = require('repl'); 4 | 5 | module.exports = class Context { 6 | 7 | constructor() { 8 | Object.defineProperty(this, "scopes", { 9 | value: Array.create(arguments), 10 | enerumable: false 11 | }); 12 | 13 | Object.defineProperty(this, "scope", { 14 | value: new Proxy({ }, { 15 | has: (scope, name) => { 16 | return this.scopes.some(scope => name in scope); 17 | }, 18 | get: (scope, name) => { 19 | return (this.scopes.find(scope => name in scope) || { })[name]; 20 | }, 21 | set: (scope, name, value) => { 22 | let match = this.scopes.find(scope => name in scope); 23 | if (match) match[name] = value; 24 | else this.scopes[0][name] = value; 25 | return true; 26 | }, 27 | deleteProperty: (scope, name) => { 28 | let match = this.scopes.find(scope => name in scope); 29 | if (match) { 30 | delete match[name]; 31 | return true; 32 | } 33 | else return false; 34 | } 35 | }), 36 | enumerable: false 37 | }); 38 | 39 | Object.defineProperty(this, "resolvers", { 40 | value: [ ], 41 | enumerable: false 42 | }); 43 | 44 | Object.defineProperty(this, "vm", { 45 | value: vm.createContext(this.scope), 46 | enumerable: false 47 | }); 48 | } 49 | 50 | async resolve(name, property) { 51 | for (let i = 0; i < this.resolvers.length; i++) { 52 | let resolver = this.resolvers[i]; 53 | if (Object.isFunction(resolver)) { 54 | let result = await resolver(name); 55 | if (result) { 56 | if (property) this.scope[property] = result; 57 | return result; 58 | } 59 | } 60 | else throw new Error("Resolver " + resolver.toString() + " is not a function."); 61 | } 62 | } 63 | 64 | async reifyImplicitIdentifiers(ids) { 65 | if (Array.isArray(ids)) return Promise.all(ids.map(async id => this.scope[id] = await this.resolve(id.substr(1)))); 66 | else this.scope[ids] = await this.resolve(ids.substr(1)); 67 | } 68 | 69 | async reifyImplicitIdentifiersInSrc(src) { 70 | let ids = esprima.tokenize(src.toString()).filter( 71 | token => token.type == "Identifier" && token.value[0] == "$" && token.value.length > 1 72 | ).map("value").unique().filter(id => this.scope[id] == null); 73 | 74 | await this.reifyImplicitIdentifiers(ids); 75 | } 76 | 77 | async runInContext(src, file) { 78 | if (this.resolvers.length) await this.reifyImplicitIdentifiersInSrc(src); 79 | return await vm.runInContext(src.toString(), this.vm, { filename: file }); 80 | } 81 | 82 | get replEval() { 83 | return (cmd, cxt, filename, cb) => { 84 | this.runInContext(cmd).then(val => cb(null, val)).catch(e => { 85 | if (e.name === "SyntaxError" && /^(Unexpected end of input|Unexpected token)/.test(e.message)) cb(new repl.Recoverable(e)); 86 | else cb(e); 87 | }); 88 | }; 89 | } 90 | 91 | async call(fn) { 92 | if (this.resolvers.length) await this.reifyImplicitIdentifiersInSrc(fn); 93 | fn = vm.runInContext(`(${fn.toString()})`, this.vm, { columnOffset: 2 }); 94 | return await fn(); 95 | } 96 | 97 | async module(src, file) { 98 | if (this.resolvers.length) await this.reifyImplicitIdentifiersInSrc(src); 99 | let fn = vm.runInContext(`(async exports => {\n${src.toString()}\nreturn exports\n})`, this.vm, { filename: file, lineOffset: -1 }); 100 | Object.assign(this.scopes[0], await fn({ })) 101 | } 102 | 103 | async global(src, file) { 104 | this.runInContext(src, file); 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /lib/model/depth.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"); 2 | 3 | class Depth extends Subscription { 4 | 5 | constructor(contract) { 6 | super(contract); 7 | this.exchanges = [ ]; 8 | this.bids = { }; 9 | this.offers = { }; 10 | } 11 | 12 | get validExchanges() { 13 | return this.contract.validExchanges; 14 | } 15 | 16 | async subscribe(exchange, rows) { 17 | return new Promise((yes, no) => { 18 | if (this.exchanges.indexOf(exchange) < 0) { 19 | this.exchanges.push(exchange); 20 | 21 | let copy = Object.clone(this.contract.summary); 22 | copy.exchange = exchange; 23 | 24 | this.bids[exchange] = { }; 25 | this.offers[exchange] = { }; 26 | 27 | let fail = (err, cancel) => { 28 | if (!Depth.failures[exchange]) failures[exchange] = { }; 29 | 30 | if (!Depth.failures[exchange][this.contract.summary.secType]) Depth.failures[exchange][this.contract.summary.secType] = 1; 31 | else Depth.failures[exchange][this.contract.summary.secType]++ 32 | 33 | if (!Depth.failures[exchange][this.contract.summary.conId]) Depth.failures[exchange][this.contract.summary.conId] = 1; 34 | else Depth.failures[exchange][this.contract.summary.conId]++; 35 | 36 | this.unsubscribe(exchange); 37 | no(err); 38 | }; 39 | 40 | let req = this.service.mktDepth(copy, rows || 5).on("data", datum => { 41 | if (datum.side == 1) this.bids[exchange][datum.position] = datum; 42 | else this.offers[exchange][datum.position] = datum; 43 | this.lastUpdate = Date.create(); 44 | this.emit("update", { contract: this.contract.summary.conId, type: "depth", field: exchange, value: datum }); 45 | this.streaming = true; 46 | }) 47 | 48 | this.subscriptions.push(req); 49 | 50 | req.once("data", () => { 51 | req.removeListener("error", fail); 52 | req.on("error", (err, cancel) => { 53 | this.emit("error", this.contract.summary.localSymbol + " level 2 quotes on " + exchange + " failed."); 54 | this.unsubscribe(exchange); 55 | }); 56 | 57 | yes(this); 58 | }).once("error", fail).send(); 59 | } 60 | }); 61 | } 62 | 63 | unsubscribe(exchange) { 64 | let idx = this.exchanges.indexOf(exchange), 65 | req = this.subscriptions[idx]; 66 | 67 | req.cancel(); 68 | 69 | this.subscriptions.remove(req); 70 | this.exchanges.remove(exchange); 71 | delete this.bids[exchange]; 72 | delete this.offers[exchange]; 73 | 74 | if (this.exchanges.length == 0) { 75 | this.streaming = false; 76 | setTimeout(() => this.streaming = false, 100); 77 | } 78 | 79 | return this; 80 | } 81 | 82 | async stream(exchanges, rows, swallow) { 83 | if (typeof exchanges == "number") { 84 | rows = exchanges; 85 | exchanges = null; 86 | } 87 | 88 | if (exchanges == null) { 89 | swallow = true; 90 | if (this.exchanges.length) { 91 | exchanges = this.exchanges; 92 | this.exchanges = [ ]; 93 | } 94 | else exchanges = this.validExchanges; 95 | } 96 | 97 | exchanges = exchanges.filter(exchange => { 98 | return ( 99 | Depth.failures[exchange][this.contract.summary.conId] < 3 || 100 | Depth.failures[exchange][this.contract.summary.secType] < 6 101 | ); 102 | }); 103 | 104 | for (let i = 0; i < exchanges.length; i++) { 105 | try { 106 | await (this.subscribe(exchanges[i], rows)); 107 | } 108 | catch (ex) { 109 | if (!swallow) throw ex; 110 | } 111 | } 112 | 113 | return this; 114 | } 115 | 116 | } 117 | 118 | const failures = Depth.failures = { }; 119 | 120 | module.exports = Depth; -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"), 2 | json = file => JSON.parse(fs.readFileSync(file).toString()), 3 | program = require("commander"), 4 | ibjs = require("./index"); 5 | 6 | let config = null; 7 | 8 | function filter(config) { 9 | let command = config._name, keys = Object.keys(config).filter(k => k[0] != '_' && typeof config[k] !== 'object'); 10 | config = Object.select(config, keys); 11 | if (command && !config.command) { 12 | config.command = command; 13 | } 14 | 15 | return config; 16 | } 17 | 18 | function preprocess(config) { 19 | if (config.paper) { 20 | config.port = config.paper; 21 | delete config.paper; 22 | } 23 | else if (config.tws) { 24 | config.port = config.tws; 25 | delete config.tws; 26 | } 27 | 28 | if (config.symbols && typeof config.symbols === 'string') { 29 | if (config.symbols.endsWith(".json")) config.symbols = json(process.cwd() + "/" + config.symbols); 30 | else config.symbols = require(process.cwd() + "/" + config.symbols); 31 | } 32 | 33 | if (config.subscriptions && typeof config.subscriptions === 'string') { 34 | if (config.subscriptions.endsWith(".json")) config.subscriptions = json(process.cwd() + "/" + config.subscriptions); 35 | else config.subscriptions = require(process.cwd() + "/" + config.subscriptions); 36 | } 37 | 38 | if (config.hooks && typeof config.hooks === 'string') { 39 | config.hooks = require(process.cwd() + "/" + config.hooks); 40 | } 41 | 42 | return config; 43 | } 44 | 45 | program.version("0.15.0") 46 | .usage("command [options]"); 47 | 48 | program 49 | .command("connect") 50 | .description('Connect to TWS or IB Gateway software and setup container') 51 | .option("--verbose", "Think out loud") 52 | .option("--id ", "Specifies the client id", parseInt, -1) 53 | .option("--host ", "Specifies the host", "localhost") 54 | .option("--port ", "Specifies the port (otherwise IB gateway default port)", parseInt, 4001) 55 | .option("--paper", "Uses the IB gateway default paper trading port", 4002) 56 | .option("--tws", "Uses the TWS default port", 7496) 57 | .option("--timeout ", "Specifies the connection timeout", parseInt, 2500) 58 | .option("--orders [type]", "Specify session order processing", "passive") 59 | .option("--repl", "Terminal interface") 60 | .option("--http [port]", "Launch http subscription interface using port", parseInt) 61 | .option("--html ", "Configure static HTML path", parseInt) 62 | .option("--symbols ", "Configure well known symbols") 63 | .option("--subscriptions ", "Configure initial subscriptions") 64 | .option("--global ", "Configure global path", "./global") 65 | .option("--module ", "Configure module path", "./module") 66 | .option("--log ", "Configure module path", "./log") 67 | .option("--cache ", "Configure module path", "./cache") 68 | .option("--hooks ", "Configure hooks") 69 | .action(options => ibjs.environment(filter(preprocess(options)))); 70 | 71 | program 72 | .command("init") 73 | .description('Initialize a new environment') 74 | .action(() => { 75 | console.log("Setting up environment in " + process.cwd()) 76 | try { 77 | fs.writeFileSync(process.cwd() + "/startup.js", fs.readFileSync(__dirname + "/example/startup.js").toString()) 78 | fs.mkdirSync(process.cwd() + "/global") 79 | fs.mkdirSync(process.cwd() + "/module") 80 | fs.mkdirSync(process.cwd() + "/log") 81 | fs.mkdirSync(process.cwd() + "/cache") 82 | console.log("Success! Configure startup.js to customize environment"); 83 | console.log(); 84 | process.exit(0); 85 | } 86 | catch(ex) { 87 | console.log("Failure!"); 88 | console.log(ex); 89 | process.exit(1); 90 | } 91 | }); 92 | 93 | program 94 | .command("subscribe") 95 | .description('Connect to an IBJS node') 96 | .option("--host ", "Specifies the host", "localhost") 97 | .option("--port ", "Specifies the port", parseInt, 8080) 98 | .option("--timeout ", "Specifies the connection timeout", parseInt, 2500) 99 | .option("--repl", "Terminal interface") 100 | .option("--http [port]", "Launch http subscription interface using port", parseInt) 101 | .action(options => config = filter(options)); 102 | 103 | program.parse(process.argv); -------------------------------------------------------------------------------- /lib/service/request.js: -------------------------------------------------------------------------------- 1 | const Events = require("events"); 2 | 3 | class Request extends Events { 4 | 5 | constructor(dispatch, id, call, send, cancel, timeout, oneOff) { 6 | super(); 7 | 8 | Object.defineProperty(this, "dispatch", { value: dispatch, enumerable: false }); 9 | 10 | this.id = id; 11 | this.call = call; 12 | 13 | if (!typeof send == "function") { 14 | throw new Error("Send must be a function."); 15 | } 16 | else { 17 | Object.defineProperty(this, "send", { 18 | value: () => { 19 | if (timeout) { 20 | if (typeof timeout != "number" || timeout <= 0) { 21 | throw new Error("Timeout must be a positive number."); 22 | } 23 | 24 | this.timeout = setTimeout(() => { 25 | this.cancel(); 26 | 27 | let timeoutError = new Error("Request " + (this.call || this.id) + " timed out."); 28 | timeoutError.timeout = timeout; 29 | this.emit("error", timeoutError, () => this.cancel()); 30 | }, timeout); 31 | 32 | this.once("data", () => { 33 | if (oneOff) { 34 | this.cancel(); 35 | } 36 | else if (this.timeout) { 37 | clearTimeout(this.timeout); 38 | delete this.timeout; 39 | } 40 | }); 41 | 42 | this.once("end", () => { 43 | if (oneOff) { 44 | this.cancel(); 45 | } 46 | else if (this.timeout) { 47 | clearTimeout(this.timeout); 48 | delete this.timeout; 49 | } 50 | }); 51 | 52 | this.once("error", () => { 53 | if (oneOff) { 54 | this.cancel(); 55 | } 56 | else if (this.timeout) { 57 | clearTimeout(this.timeout); 58 | delete this.timeout; 59 | } 60 | }); 61 | } 62 | 63 | try { 64 | send(this); 65 | } 66 | catch (ex) { 67 | this.emit("error", ex); 68 | } 69 | 70 | return this; 71 | }, 72 | enumerable: false 73 | }); 74 | } 75 | 76 | if (cancel) { 77 | if (!typeof cancel == "function") { 78 | throw new Error("Cancel must be a function."); 79 | } 80 | 81 | this.cancel = () => { 82 | if (this.timeout) { 83 | clearTimeout(this.timeout); 84 | delete this.timeout; 85 | } 86 | 87 | cancel(this); 88 | delete this.dispatch.requests[this.id]; 89 | this.emit("close"); 90 | 91 | this.cancel = () => { }; 92 | }; 93 | } 94 | else { 95 | this.cancel = () => { 96 | if (this.timeout) { 97 | clearTimeout(this.timeout); 98 | delete this.timeout; 99 | } 100 | 101 | delete this.dispatch.requests[this.id]; 102 | this.emit("close"); 103 | 104 | this.cancel = () => { }; 105 | }; 106 | } 107 | } 108 | 109 | proxy(destination, ref) { 110 | let id = this.id; 111 | this.on("data", data => { 112 | destination.emit("data", { id: id, data: data, ref: ref }); 113 | }); 114 | 115 | this.on("end", () => { 116 | destination.emit("end", { id: id, ref: ref }); 117 | }); 118 | 119 | this.on("error", error => { 120 | destination.emit("error", { 121 | id: id, 122 | error: { message: error.message, stack: error.stack, timeout: error.timeout }, 123 | ref: ref 124 | }); 125 | }); 126 | 127 | return this; 128 | } 129 | 130 | } 131 | 132 | module.exports = Request; -------------------------------------------------------------------------------- /lib/model/candlesticks.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | constants = require("../constants"); 3 | 4 | Date.getLocale('en').addFormat('{yyyy}{MM}{dd} {hh}:{mm}:{ss}'); 5 | 6 | function barDate(size, date) { 7 | let now = Date.create(date), 8 | count = parseInt(size.split(' ').first()); 9 | 10 | if (size.endsWith("day")) now = now.beginningOfDay(); 11 | else if (size.endsWith("week")) now = now.beginningOfWeek(); 12 | else if (size.endsWith("month")) now = now.beginningOfMonth(); 13 | else if (size.endsWith("hour")) { 14 | let hours = now.getHours(); 15 | let whole = Math.floor(hours / count); 16 | let current= whole * count; 17 | 18 | now.set({ hours: current }, true); 19 | } 20 | else if (size.endsWith("mins")) { 21 | let minutes = now.getMinutes(); 22 | let whole = Math.floor(minutes / count); 23 | let current= whole * count; 24 | 25 | now.set({ minutes: current }, true); 26 | } 27 | else if (size.endsWith("secs")) { 28 | let seconds = now.getSeconds(); 29 | let whole = Math.floor(seconds / count); 30 | let current= whole * count; 31 | 32 | now.set({ seconds: current }, true); 33 | } 34 | 35 | return now; 36 | } 37 | 38 | function merge(oldBar, newBar) { 39 | oldBar.high = Math.max(oldBar.high, newBar.high); 40 | oldBar.low = Math.min(oldBar.low, newBar.low); 41 | oldBar.close = newBar.close; 42 | oldBar.volume += newBar.volume; 43 | } 44 | 45 | class Candlesticks extends Subscription { 46 | 47 | constructor(contract, field) { 48 | super(contract); 49 | Object.defineProperty(this, 'field', { value: field || constants.HISTORICAL.trades, enumerable: false }); 50 | 51 | return new Proxy(this, { 52 | get: function(obj, prop) { 53 | if (obj[prop]) { 54 | return obj[prop]; 55 | } 56 | else { 57 | if (constants.BAR_SIZES[prop]) return obj[prop] = { last: { }, count: 0 }; 58 | else return undefined; 59 | } 60 | } 61 | }); 62 | } 63 | 64 | get periods() { 65 | return Object.keys(this).filter(key => constants.BAR_SIZES[key]); 66 | } 67 | 68 | async stream(retry) { 69 | this.service.headTimestamp(this.contract.summary, this.field, 0, 1).once("data", data => { 70 | this.earliestDataTimestamp = Date.create(data); 71 | }).send(); 72 | 73 | return new Promise((yes, no) => { 74 | let req = this.service.realTimeBars(this.contract.summary, 5, this.field, false); 75 | this.subscriptions.push(req); 76 | this.count = 0; 77 | this.last = { }; 78 | 79 | let errHandler = err => { 80 | if (!retry && err.timeout) { 81 | this.stream(true).then(yes).catch(no); 82 | } 83 | else { 84 | this.streaming = false; 85 | no(err); 86 | } 87 | } 88 | 89 | req.once("error", errHandler).once("data", () => { 90 | req.removeListener("error", errHandler); 91 | req.on("error", err => { 92 | this.streaming = false; 93 | this.emit("error", `Real time streaming bars request for ${this.contract.summary.localSymbol} timed out.`); 94 | }); 95 | 96 | this.streaming = true; 97 | yes(this); 98 | }).on("data", data => { 99 | data.date = Date.create(data.date * 1000); 100 | data.timestamp = data.date.getTime(); 101 | 102 | if (this["5 secs"]) { 103 | Object.assign(this["5 secs"].last, data); 104 | this["5 secs"].count++; 105 | } 106 | 107 | this.periods.forEach(period => { 108 | if (period == "5 secs") { 109 | return; 110 | } 111 | 112 | let bd = barDate(constants.BAR_SIZE[period].text, data.date); 113 | if (this.series.length && this.series.last().date == bd) { 114 | merge(this[period].last, data); 115 | } 116 | else { 117 | data.synthetic = true; 118 | data.date = bd; 119 | data.timestamp = bd.getTime(); 120 | Object.assign(this[period].last, data); 121 | this[period].count++; 122 | } 123 | }); 124 | 125 | this.emit("update", { contract: this.contract.summary.conId, type: "chart", field: "realtime", value: data }); 126 | }).send(); 127 | }); 128 | } 129 | 130 | } 131 | 132 | module.exports = Candlesticks; -------------------------------------------------------------------------------- /lib/service/proxy.js: -------------------------------------------------------------------------------- 1 | const Dispatch = require("./dispatch"), 2 | relay = require("./relay"); 3 | 4 | class Proxy { 5 | 6 | constructor(socket, dispatch) { 7 | 8 | dispatch = dispatch || new Dispatch(); 9 | 10 | socket.on("connected", msg => { 11 | dispatch.connected(); 12 | }).on("disconnected", msg => { 13 | dispatch.disconnected(); 14 | }).on("data", msg => { 15 | dispatch.data(msg.ref, msg.data); 16 | }).on("end", msg => { 17 | dispatch.end(msg.ref); 18 | }).on("error", msg => { 19 | dispatch.error(msg.ref, msg.error); 20 | }); 21 | 22 | this.isProxy = true; 23 | 24 | this.socket = socket; 25 | 26 | this.dispatch = dispatch; 27 | 28 | this.relay = socket => relay(this, socket); 29 | 30 | this.mktDataType = type => { 31 | socket.emit("command", { 32 | fn: "mktDataType", 33 | args: [ type ] 34 | }); 35 | }; 36 | 37 | this.autoOpenOrders = autoBind => { 38 | socket.emit("command", { 39 | fn: "autoOpenOrders", 40 | args: [ autoBind ] 41 | }); 42 | }; 43 | 44 | this.orderIds = () => { 45 | socket.emit("command", { 46 | fn: "orderIds", 47 | args: [ ] 48 | }); 49 | } 50 | 51 | this.globalCancel = () => { 52 | socket.emit("command", { 53 | fn: "globalCancel", 54 | args: [ ] 55 | }); 56 | }; 57 | 58 | this.system = request("system", null, socket, dispatch); 59 | 60 | this.newsBulletins = request("newsBulletins", null, socket, dispatch); 61 | 62 | this.queryDisplayGroups = request("queryDisplayGroups", 10000, socket, dispatch); 63 | 64 | this.subscribeToGroupEvents = request("subscribeToGroupEvents", 10000, socket, dispatch); 65 | 66 | this.updateDisplayGroup = function(ref, contract) { 67 | socket.emit("request", { 68 | fn: "updateDisplayGroup", 69 | args: [ contract ], 70 | ref: ref 71 | }); 72 | }; 73 | 74 | this.currentTime = request("currentTime", 2000, socket, dispatch); 75 | 76 | this.contractDetails = request("contractDetails", 10000, socket, dispatch); 77 | 78 | this.fundamentalData = request("fundamentalData", 20000, socket, dispatch); 79 | 80 | this.historicalData = request("historicalData", 20000, socket, dispatch); 81 | 82 | this.headTimestamp = request("headTimestamp", 20000, socket, dispatch); 83 | 84 | this.realTimeBars = request("realTimeBars", 10000, socket, dispatch); 85 | 86 | this.mktData = request("mktData", 10000, socket, dispatch); 87 | 88 | this.mktDepth = request("mktDepth", 10000, socket, dispatch); 89 | 90 | this.scannerParameters = request("scannerParameters", 10000, socket, dispatch); 91 | 92 | this.scannerSubscription = request("scannerSubscription", 10000, socket, dispatch); 93 | 94 | this.managedAccounts = request("managedAccounts", 10000, socket, dispatch); 95 | 96 | this.accountSummary = request("accountSummary", 10000, socket, dispatch); 97 | 98 | this.accountUpdates = request("accountUpdates", 10000, socket, dispatch); 99 | 100 | this.positions = request("positions", 10000, socket, dispatch); 101 | 102 | this.executions = request("executions", 10000, socket, dispatch); 103 | 104 | this.openOrders = request("openOrders", 10000, socket, dispatch); 105 | 106 | this.allOpenOrders = request("allOpenOrders", 10000, socket, dispatch); 107 | 108 | this.placeOrder = (orderId, contract, ticket) => { 109 | socket.emit("command", { 110 | fn: "placeOrder", 111 | args: [ orderId, contract, ticket ] 112 | }); 113 | }; 114 | 115 | this.cancelOrder = orderId => { 116 | socket.emit("command", { 117 | fn: "cancelOrder", 118 | args: [ orderId ] 119 | }); 120 | }; 121 | 122 | this.exerciseOptions = request("exerciseOptions", 10000, socket, dispatch); 123 | 124 | } 125 | 126 | } 127 | 128 | function request(fn, timeout, socket, dispatch) { 129 | return function() { 130 | let args = Array.create(arguments); 131 | return dispatch.instance( 132 | fn, 133 | req => { 134 | socket.emit("request", { 135 | fn: fn, 136 | args: args, 137 | ref: req.id 138 | }); 139 | }, 140 | req => { 141 | socket.emit("cancel", { 142 | ref: req.id 143 | }); 144 | }, 145 | timeout 146 | ); 147 | }; 148 | } 149 | 150 | module.exports = Proxy; -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | const Events = require("events"), 2 | constants = require("./constants"), 3 | symbol = require("./symbol"), 4 | Subscription = require("./model/subscription"), 5 | contract = require("./model/contract"), 6 | orders = require("./model/orders"), 7 | DisplayGroups = require("./model/displaygroups"), 8 | Account = require("./model/account"), 9 | Accounts = require("./model/accounts"), 10 | Positions = require("./model/positions"), 11 | Trades = require("./model/trades"), 12 | System = require("./model/system"); 13 | 14 | class Session extends Events { 15 | 16 | constructor(service, options) { 17 | super(); 18 | 19 | Object.defineProperty(this, "domain", { value: this.domain, enumerable: false }); 20 | Object.defineProperty(this, "_events", { value: this._events, enumerable: false }); 21 | Object.defineProperty(this, "_eventsCount", { value: this._eventsCount, enumerable: false }); 22 | Object.defineProperty(this, "_maxListeners", { value: this._maxListeners, enumerable: false }); 23 | 24 | Object.defineProperty(this, "service", { value: service, enumerable: false }); 25 | 26 | if (options.orders) { 27 | this.orders = orders(this.service, options.orders != "all"); 28 | } 29 | 30 | this.service.socket.once("managedAccounts", async data => { 31 | this.managedAccounts = Array.isArray(data) ? data : [ data ]; 32 | this.emit("ready", this); 33 | 34 | if (this.clientId === 0) { 35 | this.service.autoOpenOrders(true); 36 | if (options.orders != "passive") await this.orders.stream(); 37 | } 38 | else if (options.orders && options.orders != "passive") await this.orders.stream(); 39 | this.emit("load", this); 40 | }); 41 | 42 | this.service.socket.on("connected", () => { 43 | this.connected = true; 44 | this.emit("connected"); 45 | }).on("disconnected", () => { 46 | this.connected = false; 47 | this.emit("disconnected"); 48 | }); 49 | } 50 | 51 | close(exit) { 52 | if (this.connected) { 53 | this.connected = false; 54 | this.service.socket.disconnect(); 55 | } 56 | 57 | if (exit) process.exit(); 58 | } 59 | 60 | get clientId() { 61 | return this.service.socket._controller.options.clientId; 62 | } 63 | 64 | system() { 65 | return new System(this.service); 66 | } 67 | 68 | async account(options) { 69 | return new Account(this.service, options || this.managedAccounts.first()).stream(); 70 | } 71 | 72 | async accounts(options) { 73 | return new Accounts(this.service, options).stream(); 74 | } 75 | 76 | async positions() { 77 | return new Positions(this.service).stream(); 78 | } 79 | 80 | async trades(options) { 81 | return new Trades(this.service, options).stream(); 82 | } 83 | 84 | async contract(description) { 85 | if (description.indexOf(",") >= 0) { 86 | let legs = await Promise.all(description.split(",").map("trim").map(async leg => { 87 | let ratio = parseInt(leg.to(leg.indexOf(" "))); 88 | leg = leg.from(leg.indexOf(" ")).trim(); 89 | 90 | let summary = await contract.first(this.service, symbol.contract(leg)); 91 | if (summary) { 92 | summary = summary.summary; 93 | return { 94 | symbol: summary.symbol, 95 | conId: summary.conId, 96 | exchange: summary.exchange, 97 | ratio: Math.abs(ratio), 98 | action: Math.sign(ratio) == -1 ? "SELL" : "BUY", 99 | currency: summary.currency 100 | }; 101 | } 102 | else { 103 | throw new Error("No contract for " + leg); 104 | } 105 | })); 106 | 107 | let name = legs.map("symbol").unique().join(','); 108 | legs.forEach(leg => delete leg.symbol); 109 | 110 | return new contract.Contract(this.service, { 111 | summary: { 112 | symbol: name, 113 | secType: "BAG", 114 | currency: legs.first().currency, 115 | exchange: legs.first().exchange, 116 | comboLegs: legs 117 | } 118 | }); 119 | } 120 | else { 121 | let summary = symbol.contract(description); 122 | return await contract.first(this.service, summary); 123 | } 124 | } 125 | 126 | async contracts(description) { 127 | let summary = symbol.contract(description); 128 | return await contract.all(this.service, summary); 129 | } 130 | 131 | async quote(description) { 132 | return (await this.contract(description)).quote(); 133 | } 134 | 135 | async quotes(description) { 136 | return (await this.contracts(description)).map(c => c.quote()); 137 | } 138 | 139 | async displayGroups() { 140 | return new Promise((yes, no) => (new DisplayGroups(this.service)).once("load", yes).once("error", no)); 141 | } 142 | 143 | async order(description) { 144 | symbol.order(this.service, description); 145 | } 146 | 147 | } 148 | 149 | module.exports = Session; -------------------------------------------------------------------------------- /lib/model/quote.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | constants = require("../constants"), 3 | TICKS = constants.QUOTE_TICK_TYPES, 4 | Depth = require("./depth"), 5 | Candlesticks = require("./candlesticks"), 6 | Order = require("./order"); 7 | 8 | Date.getLocale('en').addFormat('{yyyy}{MM}{dd}-{hh}:{mm}:{ss}'); 9 | 10 | class Quote extends Subscription { 11 | 12 | constructor(contract) { 13 | super(contract); 14 | 15 | this.contractId = contract.summary.conId; 16 | this.localSymbol = contract.summary.localSymbol; 17 | 18 | this.loaded = false; 19 | this.streaming = false; 20 | 21 | this.depth = new Depth(this.contract); 22 | this.candlesticks = new Candlesticks(this.contract); 23 | 24 | Object.defineProperty(this, "_fieldTypes", { value: [ ], enumerable: false }); 25 | } 26 | 27 | addFieldTypes(fieldTypes) { 28 | if (fieldTypes) { 29 | this._fieldTypes.append(fieldTypes); 30 | this._fieldTypes = this._fieldTypes.unique().compact(true); 31 | } 32 | 33 | return this; 34 | } 35 | 36 | ticks() { 37 | this._fieldTypes.append(TICKS.realTimeVolume); 38 | this._fieldTypes = this._fieldTypes.unique().compact(true); 39 | return this; 40 | } 41 | 42 | stats() { 43 | this._fieldTypes.append([ TICKS.tradeCount, TICKS.tradeRate, TICKS.volumeRate, TICKS.priceRange ]); 44 | this._fieldTypes = this._fieldTypes.unique().compact(true); 45 | return this; 46 | } 47 | 48 | fundamentals() { 49 | this._fieldTypes.append(TICKS.fundamentalRatios); 50 | this._fieldTypes = this._fieldTypes.unique().compact(true); 51 | return this; 52 | } 53 | 54 | volatility() { 55 | this._fieldTypes.append([ TICKS.historicalVolatility, TICKS.optionImpliedVolatility ]); 56 | this._fieldTypes = this._fieldTypes.unique().compact(true); 57 | return this; 58 | } 59 | 60 | options() { 61 | this._fieldTypes.append([ TICKS.optionVolume, TICKS.optionOpenInterest ]); 62 | this._fieldTypes = this._fieldTypes.unique().compact(true); 63 | return this; 64 | } 65 | 66 | futures() { 67 | this._fieldTypes.append(TICKS.futuresOpenInterest); 68 | this._fieldTypes = this._fieldTypes.unique().compact(true); 69 | return this; 70 | } 71 | 72 | short() { 73 | this._fieldTypes.append(TICKS.shortable); 74 | this._fieldTypes = this._fieldTypes.unique().compact(true); 75 | return this; 76 | } 77 | 78 | news() { 79 | this._fieldTypes.append(TICKS.news); 80 | this._fieldTypes = this._fieldTypes.unique().compact(true); 81 | return this; 82 | } 83 | 84 | all() { 85 | return this.ticks().stats().fundamentals().volatility().options().futures().short().news(); 86 | } 87 | 88 | async refresh() { 89 | let state = { }; 90 | return new Promise((yes, no) => { 91 | this.service.mktData(this.contract.summary, this._fieldTypes.join(","), true, false) 92 | .on("data", datum => { 93 | datum = parseQuotePart(datum); 94 | if (datum && datum.key && datum.value) { 95 | this[datum.key] = state[datum.key] = datum.value; 96 | } 97 | }) 98 | .once("error", err => no(err)) 99 | .once("end", () => yes(state)) 100 | .send(); 101 | }); 102 | } 103 | 104 | async stream() { 105 | let req = this.service.mktData(this.contract.summary, this._fieldTypes.join(","), false, false); 106 | this.subscriptions.push(req); 107 | 108 | return new Promise((yes, no) => { 109 | let fail = err => { 110 | this.streaming = false; 111 | no(err); 112 | }; 113 | 114 | req.once("data", () => { 115 | this.streaming = true; 116 | req.removeListener("error", fail); 117 | req.on("error", err => { 118 | this.streaming = false; 119 | this.error = err; 120 | }); 121 | 122 | yes(this); 123 | }).on("data", datum => { 124 | datum = parseQuotePart(datum); 125 | if (datum && datum.key && datum.value) { 126 | this[datum.key] = datum.value; 127 | } 128 | }).once("error", fail).send(); 129 | }); 130 | } 131 | 132 | async streamAll() { 133 | return Promise.all([ 134 | this.stream(), 135 | this.depth.stream(), 136 | this.candlesticks.stream() 137 | ]); 138 | } 139 | 140 | order(data) { 141 | return new Order(this.contract, data); 142 | } 143 | 144 | } 145 | 146 | function parseQuotePart(datum) { 147 | let key = String(datum.name), value = datum.value; 148 | 149 | if (!key || key == "") throw new Error("Tick key not found."); 150 | if (value === null || value === "") throw new Error("No tick data value found."); 151 | 152 | if (key == "LAST_TIMESTAMP") { 153 | value = Date.create(parseInt(value) * 1000); 154 | } 155 | else if (key == "RT_VOLUME") { 156 | value = value.split(";"); 157 | value = { 158 | price: parseFloat(value[0]), 159 | size: parseInt(value[1]), 160 | time: Date.create(parseInt(value[2])), 161 | volume: parseInt(value[3]), 162 | vwap: parseFloat(value[4]), 163 | marketMaker: value[5] == "true" ? true : false 164 | }; 165 | } 166 | else if (key == "FUNDAMENTAL_RATIOS") { 167 | let ratios = { }; 168 | value.split(";").forEach(r => { 169 | let parts = r.split("="); 170 | if (parts[0].trim().length > 0) { 171 | ratios[parts[0]] = parseFloat(parts[1]); 172 | } 173 | }); 174 | 175 | value = ratios; 176 | } 177 | else if (key == "NEWS_TICK") { 178 | value = String(value).split(" "); 179 | value = { 180 | id: value[0], 181 | time: value[1], 182 | source: value[2], 183 | text: value.from(3).join(' ') 184 | }; 185 | } 186 | 187 | return { key: key.camelize(false), value: value }; 188 | } 189 | 190 | module.exports = Quote; -------------------------------------------------------------------------------- /lib/model/contract.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require('luxon'), 2 | constants = require("../constants"), 3 | market = require("./market"), 4 | Order = require("./order"), 5 | Quote = require("./quote"), 6 | Depth = require("./depth"), 7 | Candlesticks = require("./candlesticks"), 8 | memoize = require('p-memoize'); 9 | 10 | exports.cache = null; 11 | 12 | class Contract { 13 | 14 | constructor(service, data) { 15 | Object.defineProperty(this, "service", { value: service, enumerable: false }); 16 | Object.merge(this, data); 17 | 18 | this.symbol = this.summary.localSymbol; 19 | if (this.symbol) { 20 | this.symbol = this.symbol.compact().parameterize().underscore().toUpperCase(); 21 | } 22 | 23 | if (this.orderTypes) { 24 | Object.defineProperty(this, "orderTypes", { value: this.orderTypes.split(",").compact(), enumerable: false }); 25 | } 26 | 27 | if (this.validExchanges) { 28 | Object.defineProperty(this, "validExchanges", { value: this.validExchanges.split(",").compact(), enumerable: false }); 29 | } 30 | 31 | Object.defineProperty(this, "market", { 32 | value: market.getMarket( 33 | this.summary.primaryExch, 34 | this.summary.secType, 35 | this.timeZoneId, 36 | this.tradingHours, 37 | this.liquidHours 38 | ), 39 | enumerable: false 40 | }); 41 | 42 | delete this.timeZoneId; 43 | delete this.tradingHours; 44 | delete this.liquidHours; 45 | 46 | if (this.summary.expiry) { 47 | this.expiry = Date.create(DateTime.fromISO(this.summary.expiry, { zone: this.market.timeZoneId }).toJSDate()); 48 | } 49 | 50 | const summary = this.summary; 51 | if (this.summary.secType == constants.SECURITY_TYPE.stock) { 52 | let fn = async function(type) { 53 | return new Promise((yes, no) => { 54 | service.fundamentalData(summary, constants.FUNDAMENTALS_REPORTS[type] || type) 55 | .once("data", data => { 56 | let keys = Object.keys(data), 57 | report = keys.length == 1 ? data[keys.first()] : data; 58 | 59 | yes(report); 60 | }) 61 | .once("end", () => no(new Error("Could not load " + type + " fundamental data for " + symbol + ". " + err.message))) 62 | .once("error", err => no(new Error("Could not load " + type + " fundamental data for " + symbol + ". " + err.message))) 63 | .send(); 64 | }); 65 | }; 66 | 67 | Object.defineProperty(this, "getFundamentalsReport", { 68 | value: memoize(fn, { maxAge: 1000 * 60 }), /* 1 minute */ 69 | configurable: true 70 | }); 71 | 72 | if (exports.cache) { 73 | exports.cache.fn(this.getFundamentalsReport, { 74 | maxAge: 1000 * 60 * 60 * 24 * 7, /* 1 week */ 75 | salt: summary.conId.toString() 76 | }).then(fn => Object.defineProperty(this, "getFundamentalsReport", { value: fn })); 77 | } 78 | } 79 | 80 | Object.defineProperty(this, "getRecentHistory", { 81 | /* 5 second cache */ 82 | value: memoize( 83 | (barSize, field, rth) => history(service, summary, undefined, barSize, field, rth), 84 | { maxAge: 1000 * 5 } 85 | ) 86 | }); 87 | 88 | let historyBefore = (date, barSize, field, rth) => history(service, summary, date, barSize, field, rth); 89 | historyBefore = memoize(historyBefore, { maxAge: 1000 * 60 }); 90 | 91 | let quantHistoryBefore = (date, barSize, field, rth) => historyBefore(date.reset('day'), barSize, field, rth); 92 | 93 | Object.defineProperty(this, "getHistoryBefore", { 94 | value: quantHistoryBefore, 95 | configurable: true 96 | }); 97 | 98 | if (exports.cache) { 99 | exports.cache.fn(quantHistoryBefore, { 100 | maxAge: 1000 * 60 * 60 * 24 * 7, /* 1 week */ 101 | salt: summary.conId.toString() 102 | }).then(fn => Object.defineProperty(this, "getHistoryBefore", { value: fn })) 103 | } 104 | } 105 | 106 | quote() { 107 | return new Quote(this); 108 | } 109 | 110 | order(data) { 111 | return new Order(this, data); 112 | } 113 | 114 | toString() { 115 | return `${this.summary.localSymbol}@${this.summary.primaryExchange} ${this.summary.secType}`; 116 | } 117 | 118 | } 119 | 120 | exports.Contract = Contract; 121 | 122 | async function history(service, summary, lastDate, barSize, field, rth, dateFormat, retry) { 123 | return new Promise((yes, no) => { 124 | let series = [ ]; 125 | service.historicalData( 126 | summary, 127 | lastDate ? lastDate.format("{yyyy}{MM}{dd} {HH}:{mm}:{ss}") : "", 128 | barSize.duration, barSize.text, 129 | field, 130 | rth ? 1 : 0, dateFormat || 1, 131 | false 132 | ).on("data", record => { 133 | record.date = Date.create(record.date); 134 | record.timestamp = record.date.getTime(); 135 | series.push(record); 136 | }).once("error", err => { 137 | if (!retry && err.timeout) history(service, summary, lastDate, barSize, field, rth, dateFormat, true).then(yes).catch(no); 138 | else no(err); 139 | }).once("end", () => { 140 | yes(series.sortBy("timestamp")); 141 | }).send(); 142 | }); 143 | } 144 | 145 | exports.history = history; 146 | 147 | async function all(service, summary) { 148 | let list = [ ]; 149 | return new Promise((yes, no) => { 150 | service.contractDetails(summary) 151 | .on("data", contract => list.push(new Contract(service, contract))) 152 | .once("error", err => no(err)) 153 | .once("end", () => yes(list)) 154 | .send(); 155 | }); 156 | } 157 | 158 | exports.all = memoize(all, { 159 | maxAge: 1000 * 60, /* 1 minute */ 160 | cacheKey: (service, summary) => JSON.stringify(summary) 161 | }); 162 | 163 | async function first(service, summary) { 164 | let list = [ ]; 165 | return new Promise((yes, no) => { 166 | service.contractDetails(summary) 167 | .on("data", contract => yes(new Contract(service, contract))) 168 | .once("error", err => no(err)) 169 | .once("end", () => null) 170 | .send(); 171 | }); 172 | } 173 | 174 | exports.first = memoize(first, { 175 | maxAge: 1000 * 60, /* 1 minute */ 176 | cacheKey: (service, summary) => JSON.stringify(summary) 177 | }); -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const HISTORICAL = exports.HISTORICAL = { 2 | trades: "TRADES", 3 | midpoint: "MIDPOINT", 4 | bid: "BID", 5 | ask: "ASK", 6 | bidAsk: "BID_ASK", 7 | historicalVol: "HISTORICAL_VOLATILITY", 8 | optionVol: "OPTION_IMPLIED_VOLATILITY", 9 | rebate: "REBATE_RATE", 10 | fee: "FEE_RATE", 11 | yieldBid: "YIELD_BID", 12 | yieldAsk: "YIELD_ASK", 13 | yieldBidAsk: "YIELD_BID_ASK", 14 | yieldLast: "YIELD_LAST" 15 | }; 16 | 17 | const BAR_SIZES = exports.BAR_SIZES = { 18 | "5 secs": { 19 | text: "5 secs", 20 | integer: 5, 21 | duration: "3600 S" 22 | }, 23 | "10 secs": { 24 | text: "10 secs", 25 | integer: 10, 26 | duration: "7200 S" 27 | }, 28 | "15 secs": { 29 | text: "15 secs", 30 | integer: 15, 31 | duration: "10800 S" 32 | }, 33 | "30 secs": { 34 | text: "30 secs", 35 | integer: 30, 36 | duration: "1 D" 37 | }, 38 | "1 min": { 39 | text: "1 min", 40 | integer: 60, 41 | duration: "2 D" 42 | }, 43 | "2 mins": { 44 | text: "2 mins", 45 | integer: 120, 46 | duration: "3 D" 47 | }, 48 | "3 mins": { 49 | text: "3 mins", 50 | integer: 180, 51 | duration: "4 D" 52 | }, 53 | "5 mins": { 54 | text: "5 mins", 55 | integer: 300, 56 | duration: "1 W" 57 | }, 58 | "10 mins": { 59 | text: "10 mins", 60 | integer: 600, 61 | duration: "2 W" 62 | }, 63 | "15 mins": { 64 | text: "15 mins", 65 | integer: 900, 66 | duration: "2 W" 67 | }, 68 | "20 mins": { 69 | text: "20 mins", 70 | integer: 1200, 71 | duration: "3 W" 72 | }, 73 | "30 mins": { 74 | text: "30 mins", 75 | integer: 1800, 76 | duration: "1 M" 77 | }, 78 | "1 hour": { 79 | text: "1 hour", 80 | integer: 3600, 81 | duration: "2 M" 82 | }, 83 | "2 hours": { 84 | text: "2 hours", 85 | integer: 7200, 86 | duration: "2 M" 87 | }, 88 | "3 hours": { 89 | text: "3 hours", 90 | integer: 10800, 91 | duration: "3 M" 92 | }, 93 | "4 hours": { 94 | text: "4 hours", 95 | integer: 14400, 96 | duration: "4 M" 97 | }, 98 | "8 hours": { 99 | text: "8 hours", 100 | integer: 28800, 101 | duration: "8 M" 102 | }, 103 | "1 day": { 104 | text: "1 day", 105 | integer: 3600 * 24, 106 | duration: "1 Y" 107 | }, 108 | "1 week": { 109 | text: "1W", 110 | integer: 3600 * 24 * 7, 111 | duration: "2 Y" 112 | }, 113 | "1 month": { 114 | text: "1M", 115 | integer: 3600 * 24 * 7 * 30, 116 | duration: "5 Y" 117 | } 118 | }; 119 | 120 | const ACCOUNT_TAGS = exports.ACCOUNT_TAGS = { 121 | accountType: "AccountType", 122 | netLiquidation: "NetLiquidation", 123 | totalCashValue: "TotalCashValue", 124 | settledCash: "SettledCash", 125 | accruedCash: "AccruedCash", 126 | buyingPower: "BuyingPower", 127 | equityWithLoanValue: "EquityWithLoanValue", 128 | previousDayEquityWithLoanValue: "PreviousDayEquityWithLoanValue", 129 | grossPositionValue: "GrossPositionValue", 130 | regTEquity: "RegTEquity", 131 | regTMargin: "RegTMargin", 132 | sma: "SMA", 133 | initMarginReq: "InitMarginReq", 134 | maintMarginReq: "MaintMarginReq", 135 | availableFunds: "AvailableFunds", 136 | excessLiquidity: "ExcessLiquidity", 137 | cushion: "Cushion", 138 | fullInitMarginReq: "FullInitMarginReq", 139 | fullMaintMarginReq: "FullMaintMarginReq", 140 | fullAvailableFunds: "FullAvailableFunds", 141 | fullExcessLiquidity: "FullExcessLiquidity", 142 | lookAheadNextChange: "LookAheadNextChange", 143 | lookAheadInitMarginReq: "LookAheadInitMarginReq", 144 | lookAheadMaintMarginReq: "LookAheadMaintMarginReq", 145 | lookAheadAvailableFunds: "LookAheadAvailableFunds", 146 | lookAheadExcessLiquidity: "LookAheadExcessLiquidity", 147 | highestSeverity: "HighestSeverity", 148 | dayTradesRemaining: "DayTradesRemaining", 149 | leverage: "Leverage" 150 | }; 151 | 152 | const QUOTE_TICK_TYPES = exports.QUOTE_TICK_TYPES = { 153 | optionVolume: 100, 154 | optionOpenInterest: 101, 155 | historicalVolatility: 104, 156 | averageOptionValue: 105, 157 | optionImpliedVolatility: 106, 158 | indexFuturePremium: 162, 159 | priceRange: 165, 160 | markPrice: 221, 161 | auctionValues: 225, 162 | realTimeVolume: 233, 163 | shortable: 236, 164 | fundamentalRatios: 258, 165 | news: 292, 166 | tradeCount: 293, 167 | tradeRate: 294, 168 | volumeRate: 295, 169 | lastPrice: 318, 170 | realtimeTradeVolume: 375, 171 | realtimeHistoricalVolatility: 411, 172 | dividends: 456, 173 | futuresOpenInterest: 588, 174 | shortTermVolume: 595 175 | }; 176 | 177 | const FUNDAMENTALS_REPORTS = exports.FUNDAMENTALS_REPORTS = { 178 | financials: "ReportsFinSummary", 179 | ownership: "ReportsOwnership", 180 | snapshot: "ReportSnapshot", 181 | statements: "ReportsFinStatements", 182 | consensus: "RESC", 183 | calendar: "CalendarReport" 184 | }; 185 | 186 | const CURRENCIES = exports.CURRENCIES = [ 187 | 'USD', 'AUD', 'CAD', 'CHF', 'CNH', "CNY", 188 | 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 189 | 'HUF', 'ILS', 'JPY', 'MXN', 'NOK', 190 | 'NZD', 'PLN', 'RUB', 'SEK', 'SGD', 191 | 'ZAR', 'KRW' 192 | ]; 193 | 194 | const SECURITY_TYPE = exports.SECURITY_TYPE = { 195 | stock: "STK", 196 | equity: "STK", 197 | option: "OPT", 198 | options: "OPT", 199 | put: "OPT", 200 | puts: "OPT", 201 | call: "OPT", 202 | calls: "OPT", 203 | future: "FUT", 204 | futures: "FUT", 205 | index: "IND", 206 | forward: "FOP", 207 | forwards: "FOP", 208 | cash: "CASH", 209 | currency: "CASH", 210 | spread: "BAG", 211 | spreads: "BAG", 212 | combo: "BAG", 213 | news: "NEWS" 214 | }; 215 | 216 | const SIDE = exports.SIDE = { 217 | buy: "BUY", 218 | sell: "SELL", 219 | short: "SSHORT" 220 | }; 221 | 222 | const ORDER_TYPE = exports.ORDER_TYPE = { 223 | market: "MKT", 224 | marketProtect: "MKT PRT", 225 | marketToLimit: "MTL", 226 | marketIfTouched: "MIT", 227 | marketOnClose: "MOC", 228 | 229 | limit: "LMT", 230 | limitIfTouched: "LIT", 231 | limitOnClose: "LOC", 232 | 233 | stop: "STP", 234 | stopProtect: "STP PRT", 235 | stopLimit: "STP LMT", 236 | 237 | trailingStop: "TRAIL", 238 | trailingStopLimit: "TRAIL LIMIT", 239 | trailingLimitIfTouched: "TRAIL LIT", 240 | trailingMarketIfTouched: "TRAIL MIT" 241 | }; 242 | 243 | const RULE80A = exports.RULE80A = { 244 | individual: "I", 245 | agency: "A", 246 | agentOtherMember: "W", 247 | individualPTIA: "J", 248 | agencyPTIA: "U", 249 | agentOtherMemberPTIA: "M", 250 | individualPT: "K", 251 | agencyPT: "Y", 252 | agentOtherMemberPT: "N" 253 | }; 254 | 255 | const TIME_IN_FORCE = exports.TIME_IN_FORCE = { 256 | day: "DAY", 257 | goodUntilCancelled: "GTC", 258 | goodTilCancelled: "GTC", 259 | immediateOrCancel: "IOC", 260 | fillOrKill: "FOK", 261 | goodUntil: "GTD", 262 | auction: "AUC", 263 | open: "OPG" 264 | }; 265 | 266 | const OCA_TYPE = exports.OCA_TYPE = { 267 | cancel: 1, 268 | reduce: 2, 269 | reduceWithoutOverfillProtection: 3 270 | }; 271 | 272 | const MARKET_DATA_TYPE = exports.MARKET_DATA_TYPE = { 273 | live: 1, 274 | frozen: 2, 275 | delayed: 3 276 | }; -------------------------------------------------------------------------------- /lib/model/market.js: -------------------------------------------------------------------------------- 1 | const Events = require("events"), 2 | { DateTime } = require('luxon'), 3 | timer = require('node-schedule'); 4 | 5 | const tz = { 6 | // USA 7 | EST5EDT: "America/New_York", 8 | EST: "America/New_York", 9 | EDT: "America/New_York", 10 | CST6CDT: "America/Chicago", 11 | CST: "America/Chicago", 12 | CDT: "America/Chicago", 13 | MST7MDT: "America/Denver", 14 | MST: "America/Denver", 15 | MDT: "America/Denver", 16 | PST8PDT: "America/Los_Angeles", 17 | PST: "America/Los_Angeles", 18 | PDT: "America/Los_Angeles", 19 | 20 | // SOUTH AMERICA 21 | ART: "America/Buenos_Aires", 22 | BRST: "America/Sao_Paolo", 23 | VET: "America/Caracas", 24 | 25 | // EUROPE 26 | WET: "Europe/Lisbon", 27 | GMT: "Europe/London", 28 | CET: "Europe/Paris", 29 | MET: "Europe/Paris", 30 | EET: "Europe/Helsinki", 31 | MSK: "Europe/Moscow", 32 | 33 | // MIDDLE EAST 34 | IST: "Asia/Tel_Aviv", 35 | AST: "Asia/Dubai", 36 | 37 | // AFRICA 38 | SAST: "Africa/Johannesburg", 39 | 40 | // ASIA 41 | IST: "Asia/Kolkata", 42 | HKT: "Asia/Hong_Kong", 43 | CST: "Asia/Shanghai", 44 | KST: "Asia/Seoul", 45 | JST: "Asia/Tokyo", 46 | AEDT: "Australia/Sydney" 47 | }; 48 | 49 | const markets = exports.markets = { }; 50 | 51 | class Market extends Events { 52 | 53 | constructor(primaryExch, secType, timeZoneId, tradingHours, liquidHours) { 54 | 55 | super(); 56 | 57 | Object.defineProperty(this, "domain", { value: this.domain, enumerable: false }); 58 | Object.defineProperty(this, "_events", { value: this._events, enumerable: false }); 59 | Object.defineProperty(this, "_eventsCount", { value: this._eventsCount, enumerable: false }); 60 | Object.defineProperty(this, "_maxListeners", { value: this._maxListeners, enumerable: false }); 61 | 62 | Object.defineProperty(this, 'name', { value: primaryExch }); 63 | Object.defineProperty(this, 'type', { value: secType }); 64 | Object.defineProperty(this, 'timeZoneId', { value: timeZoneId }); 65 | Object.defineProperty(this, 'schedule', { value: { } }); 66 | 67 | tradingHours = (tradingHours || "").split(';').compact(true).map(d => d.split(':')); 68 | 69 | tradingHours.forEach(arr => { 70 | if (arr[1] == "CLOSED") return; 71 | 72 | let date = Date.create(arr[0], { future: true }); 73 | 74 | let label = date.format("{Mon}{dd}"); 75 | if (!this.schedule[label]) this.schedule[label] = { }; 76 | 77 | let times = arr[1].split(',').map(d => d.split('-').map(t => t.to(2) + ":" + t.from(2))); 78 | 79 | this.schedule[label].start = [ ]; 80 | this.schedule[label].end = [ ]; 81 | 82 | times.forEach(time => { 83 | let start = Date.create(DateTime.fromISO(date.format(`{yyyy}-{MM}-{dd}T${time[0]}:00`), { zone: this.timeZoneId }).toJSDate()), 84 | end = Date.create(DateTime.fromISO(date.format(`{yyyy}-{MM}-{dd}T${time[1]}:00`), { zone: this.timeZoneId }).toJSDate()); 85 | 86 | if (end.isBefore(start)) start.addDays(-1); 87 | 88 | this.schedule[label].start.push(start); 89 | this.schedule[label].end.push(end); 90 | }); 91 | 92 | if (this.schedule[label].start.length != this.schedule[label].end.length) { 93 | throw new Error("Bad trading hours."); 94 | } 95 | }); 96 | 97 | liquidHours = (liquidHours || "").split(';').compact(true).map(d => d.split(':')); 98 | 99 | liquidHours.forEach(arr => { 100 | if (arr[1] == "CLOSED") return; 101 | 102 | let date = Date.create(arr[0], { future: true }); 103 | 104 | let label = date.format("{Mon}{dd}"); 105 | if (!this.schedule[label]) this.schedule[label] = { }; 106 | 107 | let times = arr[1].split(',').map(d => d.split('-').map(t => t.to(2) + ":" + t.from(2))); 108 | 109 | this.schedule[label].open = [ ]; 110 | this.schedule[label].close = [ ]; 111 | 112 | times.forEach(time => { 113 | let start = Date.create(DateTime.fromISO(date.format(`{yyyy}-{MM}-{dd}T${time[0]}:00`), { zone: this.timeZoneId }).toJSDate()), 114 | end = Date.create(DateTime.fromISO(date.format(`{yyyy}-{MM}-{dd}T${time[1]}:00`), { zone: this.timeZoneId }).toJSDate()); 115 | 116 | if (end.isBefore(start)) start.addDays(-1); 117 | 118 | this.schedule[label].open.push(start); 119 | this.schedule[label].close.push(end); 120 | }); 121 | 122 | if (this.schedule[label].open.length != this.schedule[label].close.length) { 123 | throw new Error("Bad liquid hours."); 124 | } 125 | }); 126 | 127 | let sod = this.nextStartOfDay, 128 | eod = this.nextEndOfDay, 129 | open = this.nextOpen, 130 | close = this.nextClose; 131 | 132 | if (sod) { 133 | timer.scheduleJob(sod, () => this.emit("startOfDay")); 134 | timer.scheduleJob(sod.clone().addSeconds(-15), () => this.emit("beforeStartOfDay", sod)); 135 | } 136 | 137 | if (eod) { 138 | timer.scheduleJob(eod, () => this.emit("endOfDay")); 139 | timer.scheduleJob(eod.clone().addSeconds(-15), () => this.emit("beforeEndOfDay", eod)); 140 | } 141 | 142 | if (open) { 143 | timer.scheduleJob(open, () => this.emit("open")); 144 | timer.scheduleJob(open.clone().addSeconds(-15), () => this.emit("beforeOpen", open)); 145 | } 146 | 147 | if (close) { 148 | timer.scheduleJob(close, () => this.emit("close")); 149 | timer.scheduleJob(close.addSeconds(-15), () => this.emit("beforeClose", close)); 150 | } 151 | } 152 | 153 | get today() { 154 | let now = Date.create(), 155 | today = this.schedule[now.format("{Mon}{dd}")]; 156 | 157 | if (today && today.end.every(end => end.isBefore(now))) { 158 | now.addDays(1); 159 | today = this.schedule[now.format("{Mon}{dd}")]; 160 | } 161 | 162 | return today; 163 | } 164 | 165 | get tomorrow() { 166 | if (this.today) { 167 | let now = this.today.addDays(1); 168 | return this.schedule[now.format("{Mon}{dd}")]; 169 | } 170 | else return null; 171 | } 172 | 173 | get next() { 174 | let now = Date.create(), 175 | today = this.schedule[now.format("{Mon}{dd}")], 176 | advances = 0; 177 | 178 | while (today == null && advances < 7) { 179 | advances++; 180 | now.addDays(1); 181 | today = this.schedule[now.format("{Mon}{dd}")]; 182 | if (today && today.end.every(end => end.isPast())) { 183 | today = null; 184 | } 185 | } 186 | 187 | return today; 188 | } 189 | 190 | get marketsOpen() { 191 | let now = Date.create(), hours = this.today; 192 | if (hours && hours.start && hours.end) { 193 | for (let i = 0; i < hours.start.length; i++) { 194 | if (now.isBetween(hours.start[i], hours.end[i])) return true; 195 | } 196 | } 197 | 198 | return false; 199 | } 200 | 201 | get marketsLiquid() { 202 | let now = Date.create(), hours = this.today; 203 | if (hours && hours.open && hours.close) { 204 | for (let i = 0; i < hours.open.length; i++) { 205 | if (now.isBetween(hours.open[i], hours.close[i])) return true; 206 | } 207 | } 208 | 209 | return false; 210 | } 211 | 212 | get nextStartOfDay() { 213 | return this.next ? this.next.start.find(start => start.isFuture()) : null; 214 | } 215 | 216 | get nextOpen() { 217 | return this.next ? this.next.open.find(open => open.isFuture()) : null; 218 | } 219 | 220 | get nextClose() { 221 | return this.next ? this.next.close.find(close => close.isFuture()) : null; 222 | } 223 | 224 | get nextEndOfDay() { 225 | return this.next ? this.next.end.find(end => end.isFuture()) : null; 226 | } 227 | 228 | } 229 | 230 | exports.getMarket = function(primaryExch, secType, timeZoneId, tradingHours, liquidHours) { 231 | let hash = Array.create(arguments).join("|"); 232 | if (markets[hash]) return markets[hash]; 233 | else return markets[hash] = new Market(primaryExch, secType, tz[timeZoneId] || timeZoneId, tradingHours, liquidHours); 234 | }; -------------------------------------------------------------------------------- /lib/symbol.js: -------------------------------------------------------------------------------- 1 | const constants = require("./constants"), 2 | contract = require("./model/contract"); 3 | 4 | const wellKnownSymbols = exports.wellKnownSymbols = { }; 5 | 6 | function frontMonth(cutOffDay, offset) { 7 | let date = Date.create(); 8 | 9 | if (date.getDate() >= cutOffDay) { 10 | date.addMonths(1); 11 | } 12 | 13 | if (offset) { 14 | date.addMonths(offset); 15 | } 16 | 17 | return date; 18 | } 19 | 20 | exports.frontMonth = frontMonth; 21 | 22 | function summary(definition) { 23 | if (typeof definition == "number") { 24 | definition = { conId: definition }; 25 | } 26 | else if (typeof definition == "string") { 27 | if (/[0-9]+@[A-Z]+/.test(definition)) { 28 | definition = definition.split("@"); 29 | definition = { 30 | conId: parseInt(definition[0]), 31 | exchange: definition[1] 32 | }; 33 | } 34 | else { 35 | if (wellKnownSymbols[definition.trim().toUpperCase()]) { 36 | definition = wellKnownSymbols[definition.trim().toUpperCase()]; 37 | } 38 | 39 | let tokens = definition.split(' ').map("trim").compact(true); 40 | definition = { }; 41 | 42 | let date = tokens[0], 43 | symbol = tokens[1], 44 | side = tokens[2] ? tokens[2].toLowerCase() : null, 45 | type = constants.SECURITY_TYPE[side]; 46 | 47 | if (type) { 48 | definition.secType = type; 49 | definition.symbol = symbol; 50 | 51 | if (type == "OPT") { 52 | if (side.startsWith("put") || side.startsWith("call")) definition.right = side.toUpperCase(); 53 | else throw new Error("Must specify 'put' or 'call' for option contracts."); 54 | } 55 | 56 | if (date) { 57 | if (date.toLowerCase().startsWith("front") || date.toLowerCase().startsWith("first")) { 58 | date = date.from(5); 59 | date = date.split('+'); 60 | 61 | if (date[0] == "") date[0] = "15"; 62 | 63 | let cutOff = parseInt(date[0]), 64 | offset = date[1] ? parseInt(date[1]) : 0; 65 | 66 | date = frontMonth(cutOff, offset); 67 | if (type == "FUT") date.addMonths(1); 68 | } 69 | else { 70 | let month = date.to(3), 71 | year = date.from(3).trim(); 72 | 73 | if (year.startsWith("'") || year.startsWith("`") || year.startsWith("-") || year.startsWith("/")) year = year.from(1); 74 | 75 | if (year.length == 2) year = "20" + year; 76 | if (year == "") year = Date.create().fullYear(); 77 | 78 | try { 79 | date = Date.create(month + " " + year); 80 | } 81 | catch (ex) { 82 | throw new Error("Invalid date " + month + " " + year + " in " + definition); 83 | } 84 | } 85 | 86 | date = date.format("{yyyy}{MM}"); 87 | definition.expiry = date; 88 | } 89 | 90 | tokens = tokens.from(3); 91 | } 92 | else { 93 | definition.symbol = tokens[0].toUpperCase(); 94 | 95 | if (tokens[1] && constants.SECURITY_TYPE[tokens[1].toLowerCase()]) { 96 | definition.secType = constants.SECURITY_TYPE[tokens[1].toLowerCase()]; 97 | tokens = tokens.from(2); 98 | } 99 | else tokens = tokens.from(1); 100 | } 101 | 102 | tokens.inGroupsOf(2).forEach(field => { 103 | if (field.length == 2 && field.every(a => a != null)) { 104 | if (field[0].toLowerCase() == "in") { 105 | definition.currency = field[1].toUpperCase(); 106 | if (constants.CURRENCIES.indexOf(definition.currency) < 0) throw new Error("Invalid currency " + definition.currency); 107 | } 108 | else if (field[0].toLowerCase() == "on") definition.exchange = field[1].toUpperCase(); 109 | else if (field[0].toLowerCase() == "at") definition.strike = parseFloat(field[1]); 110 | else throw new Error("Unrecognized field " + field.join(' ')); 111 | } 112 | else { 113 | throw new Error("Unrecognized field " + field.join(' ')); 114 | } 115 | }); 116 | } 117 | } 118 | 119 | if (typeof definition == "object") { 120 | if (definition.symbol == null && definition.conId == null) { 121 | throw new Error("Definition must have symbol or conId."); 122 | } 123 | 124 | if (definition.conId == null) { 125 | if (!definition.secType && constants.CURRENCIES.indexOf(definition.symbol) >= 0) definition.secType = "CASH"; 126 | else definition.secType = definition.secType || "STK"; 127 | 128 | if (definition.secType == "CASH") { 129 | definition.exchange = "IDEALPRO"; 130 | definition.currency = definition.symbol.from(4); 131 | definition.symbol = definition.symbol.to(3); 132 | } 133 | else { 134 | if (definition.secType == "STK" || definition.secType == "OPT") definition.exchange = definition.exchange || "SMART"; 135 | definition.currency = definition.currency || "USD"; 136 | } 137 | } 138 | 139 | return definition; 140 | } 141 | else { 142 | throw new Error("Unrecognized security definition '" + definition + "'"); 143 | } 144 | } 145 | 146 | exports.contract = summary; 147 | 148 | async function order(service, script) { 149 | if (script && Object.isString(script) && script.length) { 150 | let tokens = script.toUpperCase().split(" ").map("trim").compact(true); 151 | 152 | let action = tokens.shift().toLowerCase(), 153 | qty = parseInt(tokens.shift()), 154 | unit = tokens.shift(); 155 | 156 | if (unit != "SHARE" && unit != "SHARES" && unit != "CONTRACT" && unit != "CONTRACTS") { 157 | tokens.unshift(unit); 158 | } 159 | 160 | let symbol = tokens.to(tokens.lastIndexOf("AT")), 161 | order = await contract.first(service, summary(symbol.join(" "))).order(); 162 | 163 | tokens = tokens.from(symbol.length); 164 | 165 | if (tokens.length == 0) return order; 166 | else { 167 | let price = tokens.shift(); 168 | 169 | if (price == "THE") price = tokens.shift(); 170 | 171 | if (price == "MARKET") { 172 | price = tokens.shift(); 173 | 174 | if (price == "ON") { 175 | price = tokens.shift(); 176 | if (price == "THE") price = tokens.shift(); 177 | if (price == "OPEN") order.marketOnOpen(); 178 | else if (price == "CLOSE") order.marketOnClose(); 179 | } 180 | else if (price == "WHEN") { 181 | price = tokens.shift(); 182 | if (!price[0].test(/[0-9]/)) price = price.from(1); 183 | price = parseFloat(price); 184 | 185 | // stop of if-touched order depending on position and price 186 | } 187 | else { 188 | order.market(); 189 | tokens.unshift(price); 190 | } 191 | } 192 | else if (price == "MARKET-PROTECT") order.marketProtect(); 193 | else if (price == "MARKET-TO-LIMIT") order.marketToLimit(); 194 | else { 195 | if (!price[0].test(/[0-9]/)) price = price.from(1); 196 | let limit = parseFloat(price); 197 | 198 | price = tokens.shift(); 199 | 200 | if (price == "ON") { 201 | price = tokens.shift(); 202 | if (price == "THE") price = tokens.shift(); 203 | if (price == "OPEN") order.limitOnOpen(limit); 204 | else if (price == "CLOSE") order.limitOnClose(limit); 205 | } 206 | else if (price == "WHEN") { 207 | price = tokens.shift(); 208 | if (!price[0].test(/[0-9]/)) price = price.from(1); 209 | price = parseFloat(price); 210 | 211 | // stop of if-touched order depending on position and price 212 | } 213 | else { 214 | order.limit(price); 215 | tokens.unshift(price); 216 | } 217 | } 218 | } 219 | 220 | return order; 221 | } 222 | } 223 | 224 | exports.order = order; -------------------------------------------------------------------------------- /lib/model/order.js: -------------------------------------------------------------------------------- 1 | const Subscription = require("./subscription"), 2 | constants = require("../constants"), 3 | orders = require("./orders"); 4 | 5 | class Order extends Subscription { 6 | 7 | constructor(contract, data) { 8 | super(contract); 9 | 10 | this.ticket = (data ? data.ticket : null) || { 11 | tif: constants.TIME_IN_FORCE.day, 12 | outsideRth: true, 13 | totalQuantity: 1, 14 | action: constants.SIDE.buy, 15 | orderType: constants.ORDER_TYPE.market, 16 | transmit: false 17 | }; 18 | 19 | this.state = data ? data.state : { }; 20 | 21 | this.orderId = data ? data.orderId : null; 22 | } 23 | 24 | toString() { 25 | let str = `${this.ticket.action} ${this.ticket.totalQuantity} ${this.contract.summary.localSymbol} ${this.ticket.orderType} ${this.ticket.tif}`; 26 | if (this.ticket.lmtPrice) str += " " + this.ticket.lmtPrice; 27 | if (this.ticket.auxPrice) str += " " + this.ticket.auxPrice; 28 | if (this.ticket.trailStopPrice) str += " " + this.ticket.trailStopPrice; 29 | } 30 | 31 | //////////////////////////////////////// 32 | // QUANTITY 33 | //////////////////////////////////////// 34 | trade(qty, show) { 35 | if (qty != null) { 36 | this.ticket.totalQuantity = Math.abs(qty); 37 | this.ticket.action = qty > 0 ? constants.SIDE.buy : constants.SIDE.sell; 38 | } 39 | 40 | if (show != null) { 41 | if (show == 0) this.hidden = true; 42 | this.displaySize = Math.abs(show); 43 | } 44 | 45 | return this; 46 | } 47 | 48 | buy(qty, show) { 49 | if (qty != null) { 50 | this.ticket.totalQuantity = qty; 51 | this.ticket.action = constants.SIDE.buy; 52 | } 53 | 54 | if (show != null) { 55 | if (show == 0) this.hidden = true; 56 | this.displaySize = Math.abs(show); 57 | } 58 | 59 | return this; 60 | } 61 | 62 | sell(qty, show) { 63 | if (qty != null) { 64 | this.ticket.totalQuantity = qty; 65 | this.ticket.action = constants.SIDE.sell; 66 | } 67 | 68 | if (show != null) { 69 | if (show == 0) this.hidden = true; 70 | this.displaySize = Math.abs(show); 71 | } 72 | 73 | return this; 74 | } 75 | 76 | show(qty) { 77 | if (qty != null) { 78 | if (qty == 0) this.hidden = true; 79 | this.displaySize = Math.abs(qty); 80 | } 81 | 82 | return this; 83 | } 84 | 85 | //////////////////////////////////////// 86 | // PRICE 87 | //////////////////////////////////////// 88 | type(orderType) { 89 | this.ticket.orderType = orderType; 90 | } 91 | 92 | market() { 93 | this.ticket.orderType = constants.ORDER_TYPE.market; 94 | return this; 95 | } 96 | 97 | marketProtect() { 98 | this.ticket.orderType = constants.ORDER_TYPE.marketProtect; 99 | return this; 100 | } 101 | 102 | marketToLimit() { 103 | this.ticket.orderType = constants.ORDER_TYPE.marketToLimit; 104 | return this; 105 | } 106 | 107 | auction() { 108 | this.ticket.orderType = constants.ORDER_TYPE.marketToLimit; 109 | this.ticket.tif = constants.TIME_IN_FORCE.auction; 110 | } 111 | 112 | marketIfTouched(price) { 113 | this.ticket.orderType = constants.ORDER_TYPE.marketIfTouched; 114 | this.ticket.auxPrice = price; 115 | return this; 116 | } 117 | 118 | marketOnClose() { 119 | this.ticket.orderType = constants.ORDER_TYPE.marketOnClose; 120 | return this; 121 | } 122 | 123 | marketOnOpen() { 124 | this.ticket.orderType = constants.ORDER_TYPE.market; 125 | this.ticket.tif = constants.TIME_IN_FORCE.open; 126 | return this; 127 | } 128 | 129 | limit(price, discretionaryAmount) { 130 | this.ticket.orderType = constants.ORDER_TYPE.limit; 131 | this.ticket.lmtPrice = price; 132 | if (discretionaryAmount) { 133 | this.ticket.discretionaryAmt = discretionaryAmount; 134 | } 135 | 136 | return this; 137 | } 138 | 139 | limitIfTouched(trigger, limit) { 140 | this.ticket.orderType = constants.ORDER_TYPE.limitIfTouched; 141 | this.ticket.auxPrice = trigger; 142 | this.ticket.lmtPrice = limit; 143 | return this; 144 | } 145 | 146 | limitOnClose(price) { 147 | this.ticket.orderType = constants.ORDER_TYPE.limitOnClose; 148 | this.ticket.lmtPrice = price; 149 | return this; 150 | } 151 | 152 | limitOnOpen(price) { 153 | this.ticket.orderType = constants.ORDER_TYPE.limit; 154 | this.ticket.tif = constants.TIME_IN_FORCE.open; 155 | this.ticket.lmtPrice = price; 156 | return this; 157 | } 158 | 159 | stop(trigger) { 160 | this.ticket.orderType = constants.ORDER_TYPE.stop; 161 | this.ticket.auxPrice = trigger; 162 | return this; 163 | } 164 | 165 | stopProtect(trigger) { 166 | this.ticket.orderType = constants.ORDER_TYPE.stopProtect; 167 | this.ticket.auxPrice = trigger; 168 | return this; 169 | } 170 | 171 | stopLimit(trigger, limit) { 172 | this.ticket.orderType = constants.ORDER_TYPE.stopLimit; 173 | this.ticket.auxPrice = trigger; 174 | this.ticket.lmtPrice = limit; 175 | return this; 176 | } 177 | 178 | trail(trigger, offset) { 179 | this.ticket.orderType = "TRAIL"; 180 | this.ticket.trailStopPrice = trigger; 181 | this.ticket.auxPrice = offset; 182 | return this; 183 | } 184 | 185 | trailPercent(trigger, pct) { 186 | this.ticket.orderType = "TRAIL"; 187 | this.ticket.trailStopPrice = trigger; 188 | this.ticket.trailingPercent = pct; 189 | return this; 190 | } 191 | 192 | trailLimit(trigger, offset, limit) { 193 | this.ticket.orderType = "TRAIL LIMIT"; 194 | this.ticket.trailStopPrice = trigger; 195 | this.ticket.auxPrice = offset; 196 | this.ticket.lmtPriceOffset = limit; 197 | return this; 198 | } 199 | 200 | trailLimitPercent(trigger, pct, limit) { 201 | this.ticket.orderType = "TRAIL LIMIT"; 202 | this.ticket.trailStopPrice = trigger; 203 | this.ticket.trailingPercent = pct; 204 | this.ticket.lmtPriceOffset = limit; 205 | return this; 206 | } 207 | 208 | //////////////////////////////////////// 209 | // TIMEFRAME 210 | //////////////////////////////////////// 211 | goodToday() { 212 | this.ticket.tif = constants.TIME_IN_FORCE.day; 213 | return this; 214 | } 215 | 216 | goodUntilCancelled() { 217 | this.ticket.tif = constants.TIME_IN_FORCE.goodUntilCancelled; 218 | return this; 219 | } 220 | 221 | immediateOrCancel() { 222 | this.ticket.tif = constants.TIME_IN_FORCE.immediateOrCancel; 223 | return this; 224 | } 225 | 226 | fillOrKill() { 227 | this.ticket.tif = constants.TIME_IN_FORCE.fillOrKill; 228 | return this; 229 | } 230 | 231 | atTheOpen() { 232 | this.ticket.tif = constants.TIME_IN_FORCE.open; 233 | } 234 | 235 | auction() { 236 | this.ticket.tif = constants.TIME_IN_FORCE.auction; 237 | } 238 | 239 | regularTradingHours() { 240 | this.ticket.outsideRth = false; 241 | return this; 242 | } 243 | 244 | //////////////////////////////////////// 245 | // CONDITIONS 246 | //////////////////////////////////////// 247 | overridePercentageConstraints() { 248 | this.ticket.overridePercentageConstraints = true; 249 | return this; 250 | } 251 | 252 | whatIf() { 253 | this.ticket.whatIf = true; 254 | return this; 255 | } 256 | 257 | or(cb) { 258 | if (this.ocaGroup == null) { 259 | let group = Math.floor(Math.random * 1000000).toString(); 260 | this.ocaGroup = group; 261 | this.oraType = constants.OCA_TYPE.cancel; 262 | } 263 | 264 | let siblingOrder = new Order(this.orders, this.contract); 265 | siblingOrder.ocaGroup = this.ocaGroup; 266 | siblingOrder.ocaType = constants.OCA_TYPE.cancel; 267 | 268 | if (cb && typeof cb == "function") { 269 | cb(siblingOrder); 270 | return this; 271 | } 272 | else { 273 | return siblingOrder; 274 | } 275 | } 276 | 277 | then(cb) { 278 | if (!this.children) { 279 | Object.defineProperty(this, "children", { value: [ ] }); 280 | } 281 | 282 | let childOrder = new Order(this.orders, this.contract); 283 | childOrder.parent = this; 284 | this.children.push(childOrder); 285 | 286 | if (cb && typeof cb == "function") { 287 | cb(childOrder); 288 | return this; 289 | } 290 | else { 291 | return childOrder; 292 | } 293 | } 294 | 295 | //////////////////////////////////////// 296 | // EXECUTION 297 | //////////////////////////////////////// 298 | save() { 299 | return orders(contract.service).placeOrder(this); 300 | } 301 | 302 | transmit() { 303 | this.ticket.transmit = true; 304 | this.save(); 305 | } 306 | 307 | cancel() { 308 | orders(contract.service).cancelOrder(this); 309 | } 310 | 311 | } 312 | 313 | module.exports = Order; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("sugar").extend(); 2 | 3 | //////////////////////////////////////////////////////////////////////////////////////////////// 4 | // Open Session 5 | //////////////////////////////////////////////////////////////////////////////////////////////// 6 | const IB = require("ib"), 7 | Service = require("./lib/service/service"), 8 | Dispatch = require("./lib/service/dispatch"), 9 | Proxy = require("./lib/service/proxy"), 10 | Session = require("./lib/session"), 11 | constants = require("./lib/constants"); 12 | 13 | const connectErrorHelp = "Make sure TWS or IB Gateway is running and you are logged in.\n" + 14 | "Then check IB software is configured to accept API connections over the correct port.\n" + 15 | "If all else fails, try restarting TWS or IB Gateway."; 16 | 17 | let id = 0; 18 | 19 | async function session(config) { 20 | if (Object.isNumber(config)) config = { port: config }; 21 | config = config || { }; 22 | config.id = config.id >= 0 ? config.id : id++; 23 | if (!Number.isInteger(config.id)) throw new Error("Client id must be an integer: " + config.id); 24 | if (config.host && typeof config.host !== 'string') throw new Error("Host must be a string: " + config.host); 25 | if (config.port && !Number.isInteger(config.port)) throw new Error("Port must be a number: " + config.port); 26 | 27 | return new Promise((yes, no) => { 28 | let timeout = setTimeout(() => { 29 | no(new Error("Connection timeout. " + connectErrorHelp)); 30 | }, config.timeout || 2500); 31 | 32 | let ib = config.ib || new IB({ 33 | clientId: config.id, 34 | host: config.host || "127.0.0.1", 35 | port: config.port || 4001 36 | }); 37 | 38 | if (config.trace && typeof config.trace == "function") { 39 | ib.on("all", config.trace); 40 | } 41 | 42 | if (typeof config.orders == "undefined") { 43 | config.orders = "passive"; // "all", "local", "passive" 44 | } 45 | 46 | new Session( 47 | new Service(ib, config.dispatch || new Dispatch()), 48 | config 49 | ).once("load", sess => { 50 | clearTimeout(timeout); 51 | Object.defineProperty(sess, "subscribe", { value: options => subscribe(sess, options), enumerable: false }); 52 | yes(sess); 53 | }).once("error", err => { 54 | clearTimeout(timeout); 55 | no(err); 56 | }).service.socket.once("error", err => { 57 | clearTimeout(timeout); 58 | if (err.code == "ECONNREFUSED") no(new Error("Connection refused. " + connectErrorHelp)); 59 | else no(err); 60 | }).connect(); 61 | }); 62 | } 63 | 64 | async function subscribe(session, options) { 65 | let scope = { }; 66 | if (options.system) { 67 | scope.system = session.system(); 68 | if (options.system == "frozen") scope.system.useFrozenMarketData = true; 69 | } 70 | 71 | if (options.displayGroups) scope.displayGroups = await session.displayGroups(); 72 | if (options.account) scope.account = await session.account(Object.isObject(options.account) ? options.account : null); 73 | if (options.accounts) scope.accounts = await session.accounts(); 74 | if (options.positions) scope.positions = await session.positions(); 75 | if (options.trades) scope.trades = await session.trades(Object.isObject(options.trades) ? options.trades : null); 76 | 77 | if (options.orders) { 78 | scope.orders = session.orders; 79 | scope.order = description => session.order(description); 80 | } 81 | 82 | if (options.quotes) { 83 | if (Array.isArray(options.quotes)) { 84 | await Promise.all(options.quotes.map(async description => { 85 | let quote = await session.quote(description.description || description); 86 | scope[quote.contract.toString()] = quote; 87 | if (description.fields) session.query.addFieldTypes(description.fields); 88 | if (options.autoStreamQuotes) { 89 | if (options.autoStreamQuotes == "all") return quote.streamAll(); 90 | else return quote.stream(); 91 | } 92 | else quote.refresh(); 93 | })); 94 | } 95 | else { 96 | await Promise.all(Object.keys(options.quotes).map(async key => { 97 | let description = options.quotes[key]; 98 | let quote = scope[key] = await session.quote(description.description || description); 99 | if (description.fields) session.query.addFieldTypes(description.fields); 100 | if (options.autoStreamQuotes) { 101 | if (options.autoStreamQuotes == "all") return quote.streamAll(); 102 | else return quote.stream(); 103 | } 104 | else quote.refresh(); 105 | })); 106 | } 107 | } 108 | 109 | return scope; 110 | } 111 | 112 | //////////////////////////////////////////////////////////////////////////////////////////////// 113 | // Create Express App Interface 114 | //////////////////////////////////////////////////////////////////////////////////////////////// 115 | const http = require('http'), 116 | WebSocket = require('ws'), 117 | express = require('express'), 118 | bodyParser = require('body-parser'), 119 | util = require('util'), 120 | fs = require('fs'); 121 | 122 | function createApp(context, app) { 123 | app = app || express(); 124 | 125 | app.use("/hyperactiv", express.static("node_modules/hyperactiv")); 126 | app.use(express.static(__dirname + '/html')); 127 | app.use(bodyParser.urlencoded({ extended: false })); 128 | app.use(bodyParser.json()); 129 | 130 | app.get('/cmd/:cmd', async (req, res) => { 131 | let cmd = req.params.cmd; 132 | res.send(cmd); 133 | }); 134 | 135 | app.post('/eval', async (req, res) => { 136 | let src = req.body.src.trim(); 137 | if (src.length) { 138 | try { 139 | let result = await context.evalInContext(req.body.src); 140 | res.send(util.inspect(result)); 141 | } 142 | catch (ex) { 143 | res.send(util.inspect(ex)); 144 | } 145 | } 146 | else res.end(); 147 | }); 148 | 149 | return app; 150 | } 151 | 152 | //////////////////////////////////////////////////////////////////////////////////////////////// 153 | // Startup Environment 154 | //////////////////////////////////////////////////////////////////////////////////////////////// 155 | const createContext = require("./runtime"), 156 | wellKnownSymbols = require("./lib/symbol").wellKnownSymbols, 157 | repl = require("repl"), 158 | { observe, computed, dispose } = require("hyperactiv"), 159 | { Observable, Computable } = require("hyperactiv/mixins"), 160 | ObservableObject = Computable(Observable(Object)), 161 | wss = require('hyperactiv/websocket/server').server; 162 | 163 | 164 | 165 | async function environment(config) { 166 | config.hooks = config.hooks || { }; 167 | if (config.hooks.init) await config.hooks.init(config); 168 | 169 | if (config.symbols) { 170 | Object.assign(wellKnownSymbols, config.symbols) 171 | } 172 | 173 | if (config.cache) { 174 | require("./lib/model/contract").cache = require('memoize-fs')({ cachePath: config.cache }); 175 | } 176 | 177 | if (config.log) { 178 | config.log += "/" + Date.create().format("{dow}-{H}:{mm}:{ss}"); 179 | 180 | let file = config.log + ".api.log"; 181 | config.trace = (name, data) => { 182 | let msg = (new Date()).getTime() + "|" + name + "|" + JSON.stringify(data) + "\n"; 183 | fs.appendFile(file, msg, err => err ? config.hooks.traceError(err) || console.error(err) : null); 184 | }; 185 | 186 | config.log += ".change.log" 187 | } 188 | 189 | let connection; 190 | if (config.verbose) console.log("Connecting..."); 191 | session(config).then(async session => { 192 | if (config.verbose) console.log("Session established"); 193 | session.on("error", config.hooks.error || console.error); 194 | 195 | let context = createContext(session); 196 | context.scopes.unshift(require("./lib/model/market").markets); 197 | if (config.hooks.setup) await config.hooks.setup(session, context); 198 | 199 | if (config.verbose) console.log("Opening subscriptions..."); 200 | let subscriptions = await session.subscribe(config.subscriptions || { }); 201 | if (config.log) { 202 | fs.appendFile(config.log, JSON.stringify({ type: "sync", state: subscriptions }), err => err ? config.hooks.traceError(err) || console.error(err) : null) 203 | } 204 | 205 | if (config.http) { 206 | if (config.verbose) console.log(`Starting HTTP server on port ${Number.isInteger(config.http) ? config.http : 8080}...`); 207 | const app = createApp(context); 208 | if (config.html) app.use(express.static(config.html)); 209 | 210 | const server = http.createServer(app), endpoint = wss(new WebSocket.Server({ server })); 211 | context.scopes.unshift(endpoint.host(subscriptions)); 212 | server.listen(Number.isInteger(config.http) ? config.http : 8080); 213 | } 214 | else { 215 | subscriptions = Object.assign(new ObservableObject({ }, { bubble: true, batch: true }), subscriptions); 216 | context.scopes.unshift(subscriptions); 217 | } 218 | 219 | if (config.log) { 220 | subscriptions.__handler = (keys, value, old, proxy) => { 221 | fs.appendFile(config.log, JSON.stringify({ type: "update", keys: keys, value: value }), err => err ? config.hooks.traceError(err) || console.error(err) : null) 222 | }; 223 | } 224 | 225 | if (config.repl) { 226 | if (config.verbose) console.log("Starting REPL...\n"); 227 | let terminal = repl.start({ prompt: "> ", eval: context.replEval }); 228 | terminal.on("exit", () => session.close(true)); 229 | } 230 | else if (config.verbose) console.log("Ready."); 231 | 232 | process.on("exit", config.hooks.exit || (() => session.close())); 233 | process.on("SIGINT", config.hooks.sigint || (() => session.close())); 234 | process.on("message", msg => msg == "shutdown" ? session.close() : null); 235 | if (Object.isFunction(process.send)) process.send("ready"); 236 | if (config.hooks.ready) config.hooks.ready(session, context); 237 | 238 | if (config.globals) { 239 | if (typeof config.globals === 'string') { 240 | config.globals = fs.readdirSync(config.globals).filter(file => file[0] !== '.' && file.endsWith(".js")); 241 | } 242 | 243 | if (Array.isArray(config.globals)) { 244 | for (let i = 0; i < config.globals.length; i++) { 245 | if (config.verbose) console.log("Including " + config.globals[i]) 246 | await context.include(config.globals[i]); 247 | } 248 | } 249 | } 250 | 251 | if (config.modules) { 252 | if (typeof config.modules === 'string') { 253 | config.modules = fs.readdirSync(config.modules).filter(file => file[0] !== '.' && file.endsWith(".js")); 254 | } 255 | 256 | if (Array.isArray(config.modules)) { 257 | for (let i = 0; i < config.modules.length; i++) { 258 | if (config.verbose) console.log("Importing " + config.modules[i]) 259 | await context.import(config.modules[i]); 260 | } 261 | } 262 | } 263 | 264 | if (config.hooks.load) config.hooks.load(session, context); 265 | }).catch(config.hooks.error || (err => { 266 | console.error(err); 267 | process.exit(1); 268 | })); 269 | 270 | if (config.input) { 271 | config.ib.replay(config.input, config.inputSpeed || 1, config.hooks.afterReplay); 272 | } 273 | 274 | process.on("warning", config.hooks.warning || config.hooks.error || console.warn); 275 | process.on("uncaughtException", config.hooks.error || console.error); 276 | } 277 | 278 | //////////////////////////////////////////////////////////////////////////////////////////////// 279 | // Exports 280 | //////////////////////////////////////////////////////////////////////////////////////////////// 281 | module.exports = { IB, Service, Dispatch, Proxy, Session, constants, session, environment } -------------------------------------------------------------------------------- /lib/service/service.js: -------------------------------------------------------------------------------- 1 | const Dispatch = require("./dispatch"), 2 | relay = require("./relay"), 3 | parseString = require('xml2js').parseString; 4 | 5 | function camelize(str) { 6 | return str.camelize(false); 7 | } 8 | 9 | function parseXML(txt, cb) { 10 | parseString(txt, { 11 | trim: true, 12 | mergeAttrs: true, 13 | charkey: "text", 14 | tagNameProcessors: [ camelize ], 15 | attrNameProcessors: [ camelize ] 16 | }, cb); 17 | } 18 | 19 | class Service { 20 | 21 | constructor(ib, dispatch) { 22 | 23 | dispatch = dispatch || new Dispatch(); 24 | 25 | attach(ib, dispatch); 26 | 27 | this.isProxy = false; 28 | 29 | this.socket = ib; 30 | 31 | this.dispatch = dispatch; 32 | 33 | this.relay = socket => relay(this, socket); 34 | 35 | this.mktDataType = type => { 36 | ib.reqMarketDataType(type); 37 | this.lastMktDataType = type; 38 | }; 39 | 40 | this.autoOpenOrders = autoBind => { 41 | this.socket.reqAutoOpenOrders(autoBind || false); 42 | this.lastAutoOpenOrders = (autoBind || false); 43 | }; 44 | 45 | this.orderIds = count => { 46 | this.socket.reqIds(count); 47 | }; 48 | 49 | this.globalCancel = () => { 50 | this.socket.reqGlobalCancel(); 51 | }; 52 | 53 | this.system = () => { 54 | return dispatch.singleton( 55 | `system()`, 56 | req => null, 57 | req => null, 58 | null, 59 | "system" 60 | ); 61 | }; 62 | 63 | this.newsBulletins = singleton("news", "reqNewsBulletins", "cancelNewsBulletins", null, ib, dispatch); 64 | 65 | this.queryDisplayGroups = instance("queryDisplayGroups", null, 5000, ib, dispatch); 66 | 67 | this.subscribeToGroupEvents = instance("subscribeToGroupEvents", "unsubscribeFromGroupEvents", 5000, ib, dispatch); 68 | 69 | this.updateDisplayGroup = (reqId, contract) => { 70 | ib.updateDisplayGroup(reqId, contract); 71 | }; 72 | 73 | this.currentTime = singleton("currentTime", "reqCurrentTime", null, 1000, ib, dispatch); 74 | 75 | this.contractDetails = instance("reqContractDetails", null, 10000, ib, dispatch); 76 | 77 | this.fundamentalData = instance("reqFundamentalData", null, 10000, ib, dispatch); 78 | 79 | this.historicalData = instance("reqHistoricalData", null, 10000, ib, dispatch); 80 | 81 | this.headTimestamp = instance("reqHeadTimestamp", null, 10000, ib, dispatch); 82 | 83 | this.realTimeBars = instance("reqRealTimeBars", "cancelRealTimeBars", 10000, ib, dispatch); 84 | 85 | this.mktData = instance("reqMktData", "cancelMktData", 10000, ib, dispatch); 86 | 87 | this.mktDepth = instance("reqMktDepth", "cancelMktDepth", 10000, ib, dispatch); 88 | 89 | this.scannerParameters = singleton("scannerParameters", "reqScannerParameters", null, 10000, ib, dispatch); 90 | 91 | this.scannerSubscription = instance("reqScannerSubscription", "cancelScannerSubscription", 10000, ib, dispatch); 92 | 93 | this.managedAccounts = singleton("managedAccounts", "reqManagedAccts", null, 10000, ib, dispatch); 94 | 95 | this.accountSummary = instance("reqAccountSummary", "cancelAccountSummary", 10000, ib, dispatch); 96 | 97 | this.accountUpdates = accountCode => { 98 | return dispatch.singleton( 99 | `accountUpdates(${ accountCode })`, 100 | req => ib.reqAccountUpdates(true, accountCode), 101 | req => ib.reqAccountUpdates(false, accountCode), 102 | 10000, 103 | "accountUpdates" 104 | ); 105 | }; 106 | 107 | this.positions = singleton("positions", "reqPositions", "cancelPositions", 10000, ib, dispatch); 108 | 109 | this.executions = instance("reqExecutions", null, 10000, ib, dispatch); 110 | 111 | this.commissions = () => { 112 | return dispatch.singleton( 113 | `commissions()`, 114 | req => null, 115 | req => null, 116 | null, 117 | "commissions" 118 | ); 119 | }; 120 | 121 | this.openOrders = singleton("orders", "reqOpenOrders", null, 10000, ib, dispatch); 122 | 123 | this.allOpenOrders = singleton("orders", "reqAllOpenOrders", null, 10000, ib, dispatch); 124 | 125 | this.placeOrder = (orderId, contract, ticket) => { 126 | this.socket.placeOrder(orderId, contract, ticket); 127 | } 128 | 129 | this.cancelOrder = orderId => { 130 | this.socket.cancelOrder(orderId); 131 | }; 132 | 133 | this.exerciseOptions = instance("exerciseOptions", "cancelOrder", 5000, ib, dispatch); 134 | 135 | } 136 | 137 | } 138 | 139 | function singleton(event, send, cancel, timeout, ib, dispatch) { 140 | return function() { 141 | return dispatch.singleton( 142 | `${ send }(${ Array.create(arguments).map(JSON.stringify).join(', ') })`, 143 | req => ib[send](...arguments), 144 | cancel ? req => ib[cancel]() : null, 145 | timeout, 146 | event 147 | ); 148 | }; 149 | } 150 | 151 | function instance(send, cancel, timeout, ib, dispatch) { 152 | return function() { 153 | return dispatch.instance( 154 | `${ send }(${ Array.create(arguments).map(JSON.stringify).join(', ') })`, 155 | req => ib[send](req.id, ...arguments), 156 | cancel ? req => ib[cancel](req.id) : null, 157 | timeout 158 | ); 159 | }; 160 | } 161 | 162 | function attach(ib, dispatch) { 163 | 164 | ib.on("connected", function() { 165 | dispatch.connected(); 166 | }).on("disconnected", function() { 167 | dispatch.disconnected(); 168 | }).on("error", function(err, args) { 169 | if (args) { 170 | if (args.id > 0) { 171 | dispatch.error(args.id, err); 172 | } 173 | else if (args.id < -1) { 174 | args.orderId = args.id; 175 | delete args.id; 176 | dispatch.error("order", args); 177 | } 178 | else { 179 | args.message = err.message; 180 | dispatch.data("system", args); 181 | } 182 | } 183 | else if (err.syscall == "connect" || err.code == "ECONNRESET") { 184 | dispatch.disconnected(); 185 | } 186 | else if (err && err.id != -1) { 187 | dispatch.error("system", err); 188 | } 189 | }); 190 | 191 | ib.once("currentTime", function(time) { 192 | dispatch.data("currentTime", time); 193 | }); 194 | 195 | ib.on('contractDetails', function(reqId, contract) { 196 | dispatch.data(reqId, contract); 197 | }).on('bondContractDetails', function(reqId, contract) { 198 | dispatch.data(reqId, contract); 199 | }).on('contractDetailsEnd', function(reqId) { 200 | dispatch.end(reqId); 201 | }); 202 | 203 | ib.on('fundamentalData', function(reqId, data) { 204 | if (data) { 205 | parseXML(data.toString(), function(err, result) { 206 | if (err) dispatch.error(reqId, err); 207 | if (result) dispatch.data(reqId, result); 208 | }); 209 | } 210 | else { 211 | dispatch.end(reqId); 212 | } 213 | }); 214 | 215 | ib.on('historicalData', function(reqId, date, open, high, low, close, volume, count, wap, hasGaps) { 216 | if (date && date.startsWith("finished")) { 217 | dispatch.end(reqId); 218 | } 219 | else { 220 | dispatch.data(reqId, { 221 | date: date, 222 | open: open, 223 | high: high, 224 | low: low, 225 | close: close, 226 | volume: volume, 227 | count: count, 228 | wap: wap, 229 | hasGaps: hasGaps 230 | }); 231 | } 232 | }); 233 | 234 | ib.on('headTimestamp', function(reqId, timestamp) { 235 | dispatch.data(reqId, timestamp); 236 | }); 237 | 238 | ib.on('realtimeBar', function(reqId, date, open, high, low, close, volume, wap, count) { 239 | dispatch.data(reqId, { 240 | date: date, 241 | open: open, 242 | high: high, 243 | low: low, 244 | close: close, 245 | volume: volume, 246 | count: count, 247 | wap: wap 248 | }); 249 | }); 250 | 251 | ib.on('tickEFP', function(tickerId, tickType, basisPoints, formattedBasisPoints, impliedFuturesPrice, holdDays, futureExpiry, dividendImpact, dividendsToExpiry) { 252 | dispatch.data(tickerId, { 253 | type: 'EFP', 254 | tickType: tickType, 255 | name: ib.util.tickTypeToString(tickType), 256 | basisPoints: basisPoints, 257 | formattedBasisPoints: formattedBasisPoints, 258 | impliedFuturesPrice: impliedFuturesPrice, 259 | holdDays: holdDays, 260 | futureExpiry: futureExpiry, 261 | dividendImpact: dividendImpact, 262 | dividendsToExpiry: dividendsToExpiry 263 | }); 264 | }).on('tickGeneric', function(tickerId, tickType, value) { 265 | dispatch.data(tickerId, { 266 | type: 'Generic', 267 | tickType: tickType, 268 | name: ib.util.tickTypeToString(tickType), 269 | value: value 270 | }); 271 | }).on('tickPrice', function(tickerId, tickType, price, canAutoExecute) { 272 | dispatch.data(tickerId, { 273 | type: 'Price', 274 | tickType: tickType, 275 | name: ib.util.tickTypeToString(tickType), 276 | value: price, 277 | canAutoExecute: canAutoExecute 278 | }); 279 | }).on('tickSize', function(tickerId, sizeTickType, size) { 280 | dispatch.data(tickerId, { 281 | type: 'Size', 282 | tickType: sizeTickType, 283 | name: ib.util.tickTypeToString(sizeTickType), 284 | value: size 285 | }); 286 | }).on('tickString', function(tickerId, tickType, value) { 287 | dispatch.data(tickerId, { 288 | type: 'String', 289 | tickType: tickType, 290 | name: ib.util.tickTypeToString(tickType), 291 | value: value 292 | }); 293 | }).on('tickSnapshotEnd', function(reqId) { 294 | dispatch.end(reqId); 295 | }).on('tickOptionComputation', function(tickerId, tickType, impliedVol, delta, optPrice, pvDividend, gamma, vega, theta, undPrice) { 296 | dispatch.data(tickerId, { 297 | type: 'OptionComputation', 298 | tickType: tickType, 299 | name: ib.util.tickTypeToString(tickType), 300 | value: { 301 | impliedVol: impliedVol, 302 | delta: delta, 303 | optPrice: optPrice, 304 | pvDividend: pvDividend, 305 | gamma: gamma, 306 | vega: vega, 307 | theta: theta, 308 | undPrice: undPrice 309 | } 310 | }); 311 | }); 312 | 313 | ib.on("marketDataType", function(id, type) { 314 | dispatch.data(id, type); 315 | }); 316 | 317 | ib.on('updateMktDepth', function(id, position, operation, side, price, size) { 318 | dispatch.data(id, { 319 | position: position, 320 | marketMaker: "N/A", 321 | operation: operation, 322 | side: side, 323 | price: price, 324 | size: size 325 | }); 326 | }).on('updateMktDepthL2', function(id, position, marketMaker, operation, side, price, size) { 327 | dispatch.data(id, { 328 | position: position, 329 | marketMaker: marketMaker, 330 | operation: operation, 331 | side: side, 332 | price: price, 333 | size: size 334 | }); 335 | }); 336 | 337 | ib.on("scannerParameters", function(xml) { 338 | if (xml) { 339 | parseXML(xml.toString(), function(err, result) { 340 | if (err) dispatch.error("scannerParameters", err); 341 | if (result) dispatch.data("scannerParameters", result); 342 | }); 343 | } 344 | else dispatch.end("scannerParameters"); 345 | }); 346 | 347 | ib.on("scannerData", function(tickerId, rank, contract, distance, benchmark, projection, legsStr) { 348 | dispatch.data(tickerId, { 349 | rank: rank, 350 | contract: contract, 351 | distance: distance, 352 | benchmark: benchmark, 353 | projection: projection, 354 | legsStr: legsStr 355 | }); 356 | }).on("scannerDataEnd", function(tickerId) { 357 | dispatch.end(tickerId); 358 | }); 359 | 360 | ib.on('managedAccounts', function(accountsList) { 361 | dispatch.data("managedAccounts", accountsList); 362 | }); 363 | 364 | ib.on('receiveFA', function(faDataType, xml) { 365 | if (xml) { 366 | parseXML(xml.toString(), function(err, result) { 367 | if (err) dispatch.error("receiveFA", err); 368 | if (result) dispatch.data("receiveFA", { type: faDataType, xml: result }); 369 | }); 370 | } 371 | else { 372 | dispatch.end("receiveFA"); 373 | } 374 | }); 375 | 376 | ib.on('accountSummary', function(reqId, account, tag, value, currency) { 377 | dispatch.data(reqId, { 378 | account: account, 379 | tag: tag, 380 | value: value, 381 | currency: currency 382 | }); 383 | }).on('accountSummaryEnd', function(reqId) { 384 | dispatch.end(reqId); 385 | }); 386 | 387 | ib.on('updateAccountTime', function(timeStamp) { 388 | dispatch.data("accountUpdates", { 389 | timestamp: timeStamp 390 | }); 391 | }).on('updateAccountValue', function(key, value, currency, accountName) { 392 | dispatch.data("accountUpdates", { 393 | key: key, 394 | value: value, 395 | currency: currency, 396 | accountName: accountName 397 | }); 398 | }).on('updatePortfolio', function(contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName) { 399 | dispatch.data("accountUpdates", { 400 | contract: contract, 401 | position: position, 402 | marketPrice: marketPrice, 403 | marketValue: marketValue, 404 | averageCost: averageCost, 405 | unrealizedPNL: unrealizedPNL, 406 | realizedPNL: realizedPNL, 407 | accountName: accountName 408 | }); 409 | }).on('accountDownloadEnd', function(accountId) { 410 | dispatch.end("accountUpdates"); 411 | }); 412 | 413 | ib.on('position', function(account, contract, pos, avgCost) { 414 | dispatch.data("positions", { 415 | accountName: account, 416 | contract: contract, 417 | position: pos, 418 | averageCost: avgCost 419 | }); 420 | }).on('positionEnd', function() { 421 | dispatch.end("positions"); 422 | }); 423 | 424 | ib.on('execDetails', function(reqId, contract, exec) { 425 | dispatch.data(reqId, { 426 | contract: contract, 427 | exec: exec 428 | }); 429 | }).on('execDetailsEnd', function(reqId) { 430 | dispatch.end(reqId); 431 | }); 432 | 433 | ib.on("nextValidId", function(orderId) { 434 | dispatch.data("orders", { nextOrderId: orderId }); 435 | }); 436 | 437 | ib.on('openOrder', function(orderId, contract, order, orderState) { 438 | dispatch.data("orders", { 439 | orderId: orderId, 440 | contract: { summary: contract }, 441 | ticket: order, 442 | state: orderState 443 | }); 444 | }).on('openOrderEnd', function() { 445 | dispatch.end("orders"); 446 | }); 447 | 448 | ib.on('orderStatus', function(orderId, status, filled, remaining, avgFillPrice, permId, parentId, lastFillPrice, clientId, whyHeld) { 449 | dispatch.data("orders", { 450 | orderId: orderId, 451 | state: { 452 | status: status, 453 | filled: filled, 454 | remaining: remaining, 455 | avgFillPrice: avgFillPrice, 456 | permId: permId, 457 | parentId: parentId, 458 | lastFillPrice: lastFillPrice, 459 | clientId: clientId, 460 | whyHeld: whyHeld 461 | } 462 | }); 463 | }); 464 | 465 | ib.on('commissionReport', function(commissionReport) { 466 | dispatch.data("commissions", commissionReport); 467 | }); 468 | 469 | ib.on('updateNewsBulletin', function(newsMsgId, newsMsgType, newsMessage, originatingExch) { 470 | dispatch.data("news", { 471 | newsMsgId: newsMsgId, 472 | newsMsgType: newsMsgType, 473 | newsMessage: newsMessage, 474 | originatingExch: originatingExch 475 | }); 476 | }); 477 | 478 | ib.on('displayGroupList', function(reqId, groups) { 479 | dispatch.data(reqId, groups ? groups.split("|") : [ ]); 480 | }); 481 | 482 | ib.on('displayGroupUpdated', function(reqId, contractInfo) { 483 | dispatch.data(reqId, contractInfo); 484 | }); 485 | 486 | } 487 | 488 | module.exports = Service; --------------------------------------------------------------------------------