├── 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;
--------------------------------------------------------------------------------