├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── exchange-integration-request.md └── workflows │ └── node.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── FAQ.md ├── LICENSE ├── README.md ├── __tests__ ├── BasicClient.spec.ts ├── Jwt.spec.ts ├── TestRunner.ts ├── Trade.spec.ts ├── Watcher.spec.ts ├── Zlib.spec.ts ├── exchanges │ ├── BiboxClient.spec.ts │ ├── BinanceClient.spec.ts │ ├── BinanceFuturesCoinmClient.spec.ts │ ├── BinanceFuturesUsdtmClient.spec.ts │ ├── BinanceJeClient.spec.ts │ ├── BinanceUsClient.spec.ts │ ├── BitfinexClient.spec.ts │ ├── BitflyerClient.spec.ts │ ├── BithumbClient.spec.ts │ ├── BitmexClient.spec.ts │ ├── BitstampClient.spec.ts │ ├── BittrexClient.spec.ts │ ├── CexClient.spec.ts │ ├── CoinbaseProClient.spec.ts │ ├── CoinexClient.spec.ts │ ├── DeribitClient.spec.ts │ ├── DigifinexClient.spec.ts │ ├── FtxClient.spec.ts │ ├── FtxUsClient.spec.ts │ ├── GateioClient.spec.ts │ ├── GeminiClient.spec.ts │ ├── HitBtcClient.spec.ts │ ├── HuobiClient.spec.ts │ ├── HuobiFuturesClient.spec.ts │ ├── HuobiJapanClient.spec.ts │ ├── HuobiKoreaClient.spec.ts │ ├── HuobiSwapsClient.spec.ts │ ├── KrakenClient.spec.ts │ ├── KucoinClient.spec.ts │ ├── LiquidClient.spec.ts │ ├── OkexClient.spec.ts │ ├── PoloniexClient.spec.ts │ ├── UpbitClient.spec.ts │ └── ZbClient.spec.ts └── flowcontrol │ ├── Batch.spec.ts │ ├── CircularBuffer.spec.ts │ ├── Debounce.spec.ts │ ├── Queue.spec.ts │ └── Throttle.spec.ts ├── package-lock.json ├── package.json ├── src ├── Auction.ts ├── BasicClient.ts ├── BasicMultiClient.ts ├── BlockTrade.ts ├── Candle.ts ├── CandlePeriod.ts ├── ClientOptions.ts ├── Https.ts ├── IClient.ts ├── Jwt.ts ├── Level2Point.ts ├── Level2Snapshots.ts ├── Level2Update.ts ├── Level3Point.ts ├── Level3Snapshot.ts ├── Level3Update.ts ├── Market.ts ├── NotImplementedFn.ts ├── SmartWss.ts ├── SubscriptionType.ts ├── Ticker.ts ├── Trade.ts ├── Util.ts ├── Watcher.ts ├── ZlibUtils.ts ├── exchanges │ ├── BiboxClient.ts │ ├── BinanceBase.ts │ ├── BinanceClient.ts │ ├── BinanceFuturesCoinmClient.ts │ ├── BinanceFuturesUsdtmClient.ts │ ├── BinanceJeClient.ts │ ├── BinanceUsClient.ts │ ├── BitfinexClient.ts │ ├── BitflyerClient.ts │ ├── BithumbClient.ts │ ├── BitmexClient.ts │ ├── BitstampClient.ts │ ├── BittrexClient.ts │ ├── CexClient.ts │ ├── CoinbaseProClient.ts │ ├── CoinexClient.ts │ ├── DeribitClient.ts │ ├── DigifinexClient.ts │ ├── ErisxClient.ts │ ├── FtxBase.ts │ ├── FtxClient.ts │ ├── FtxUsClient.ts │ ├── GateioClient.ts │ ├── Geminiclient.ts │ ├── HitBtcClient.ts │ ├── HuobiBase.ts │ ├── HuobiClient.ts │ ├── HuobiFuturesClient.ts │ ├── HuobiJapanClient.ts │ ├── HuobiKoreaClient.ts │ ├── HuobiSwapsClient.ts │ ├── KrakenClient.ts │ ├── KucoinClient.ts │ ├── LedgerXClient.ts │ ├── LiquidClient.ts │ ├── OkexClient.ts │ ├── PoloniexClient.ts │ ├── UpbitClient.ts │ └── ZbClient.ts ├── flowcontrol │ ├── Batch.ts │ ├── CircularBuffer.ts │ ├── Debounce.ts │ ├── Fn.ts │ ├── Queue.ts │ └── Throttle.ts ├── index.ts └── orderbooks │ ├── DeribitOrderBook.ts │ ├── ErisXOrderBook.ts │ ├── KrakenOrderBook.ts │ ├── KucoinOrderBook.ts │ ├── L2Point.ts │ ├── L3Point.ts │ ├── L3PointStore.ts │ ├── LedgerXOrderBook.ts │ └── LiquidOrderBook.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.ts] 13 | indent_style = space 14 | indent_size = 4 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": false, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 12 | "prettier" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "plugins": ["@typescript-eslint"], 16 | "rules": { 17 | "no-console": "warn", 18 | "quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }], 19 | "semi": ["error", "always"], 20 | "@typescript-eslint/no-inferrable-types": "off", 21 | "@typescript-eslint/restrict-template-expressions": "off", 22 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/explicit-module-boundary-types": "off", // eventually enable this 25 | "@typescript-eslint/explicit-member-accessibility": [ 26 | "error", 27 | { 28 | "overrides": { 29 | "constructors": "off", 30 | "parameterProperties": "off" 31 | } 32 | } 33 | ], 34 | "@typescript-eslint/member-ordering": [ 35 | "error", 36 | { 37 | "default": [ 38 | "static-field", 39 | "static-method", 40 | "instance-field", 41 | "constructor", 42 | "instance-method" 43 | ] 44 | } 45 | ] 46 | }, 47 | "ignorePatterns": [ 48 | "**/node_modules", 49 | "**/dist", 50 | "**/coverage" 51 | ], 52 | "overrides": [ 53 | { 54 | "files": "__tests__/**/*", 55 | "rules":{ 56 | "no-sparse-arrays": "off", 57 | "@typescript-eslint/no-explicit-any": "off", 58 | "@typescript-eslint/unbound-method": "off", 59 | "@typescript-eslint/no-unsafe-assignment": "off", 60 | "@typescript-eslint/no-unsafe-call": "off", 61 | "@typescript-eslint/no-unsafe-member-access": "off", 62 | "@typescript-eslint/no-unsafe-return": "off", 63 | "@typescript-eslint/restrict-plus-operands": "off" 64 | } 65 | } 66 | ], 67 | "parserOptions": { 68 | "project": ["./tsconfig.json"] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | ---**Exchange** 5 | If this is related to a specific exchange, please list it. IE: Binance 6 | 7 | **Subscription type** 8 | If this is related to a specific subscription, please list it. IE: Trades, Level2 orderbook updates 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Additional context** 14 | Add any other context about the problem here. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/exchange-integration-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Exchange integration request 3 | about: Suggest addition of an exchange 4 | ---**Name of exchange** 5 | Provide the name of the exchange 6 | 7 | **Exchange URL** 8 | Provide the URL to the exchange 9 | 10 | **Exchange API URL** 11 | Provide the URL for the documentation of the exchange's realtime API 12 | 13 | --- 14 | 15 | Refer to [Contributing Guide](https://github.com/altangent/ccxws/blob/master/CONTRIBUTING.md) for additional information. 16 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | validate: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [12.x] 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - run: npm ci 24 | 25 | - name: Verify formatting 26 | run: npm run format 27 | 28 | - name: Verify linting 29 | run: npm run lint 30 | 31 | - name: Test non-exchanges 32 | run: $(npm bin)/nyc --reporter=lcov --extension=.ts --reporter=text $(npm bin)/mocha --exit --require ts-node/register --exclude "__tests__/exchanges/**" --recursive "__tests__/**/*.spec.ts" 33 | env: 34 | CEX_API_KEY: ${{ secrets.CEX_API_KEY }} 35 | CEX_API_SECRET: ${{ secrets.CEX_API_SECRET }} 36 | 37 | - name: Coveralls Parallel 38 | uses: coverallsapp/github-action@master 39 | with: 40 | github-token: ${{ secrets.github_token }} 41 | flag-name: run-general 42 | parallel: true 43 | 44 | exchange: 45 | needs: [validate] 46 | runs-on: ubuntu-latest 47 | strategy: 48 | matrix: 49 | node-version: [12.x] 50 | exchange: 51 | - Bibox 52 | - Binance 53 | - BinanceFuturesCoinm 54 | - BinanceFuturesUsdtm 55 | - BinanceUs 56 | - Bitfinex 57 | - Bitflyer 58 | - Bithumb 59 | - Bitmex 60 | - Bitstamp 61 | - Bittrex 62 | - Cex 63 | - CoinbasePro 64 | - Coinex 65 | - Deribit 66 | - Digifinex 67 | - Ftx 68 | - FtxUs 69 | - Gateio 70 | - Gemini 71 | - HitBtc 72 | - Huobi 73 | - HuobiFutures 74 | - HuobiJapan 75 | - HuobiKorea 76 | - HuobiSwaps 77 | - Kucoin 78 | - Liquid 79 | - Okex 80 | - Poloniex 81 | - Upbit 82 | - Zb 83 | 84 | steps: 85 | - name: Checkout code 86 | uses: actions/checkout@v2 87 | 88 | - name: Use Node.js ${{ matrix.node-version }} 89 | uses: actions/setup-node@v1 90 | with: 91 | node-version: ${{ matrix.node-version }} 92 | 93 | - name: Install dependencies 94 | run: npm install 95 | 96 | - name: Run tests 97 | run: $(npm bin)/nyc --reporter=lcov --extension=.ts --reporter=text $(npm bin)/mocha --exit --require ts-node/register __tests__/exchanges/${{ matrix.exchange }}Client.spec.ts 98 | env: 99 | CEX_API_KEY: ${{ secrets.CEX_API_KEY }} 100 | CEX_API_SECRET: ${{ secrets.CEX_API_SECRET }} 101 | 102 | - name: Coveralls Parallel 103 | uses: coverallsapp/github-action@master 104 | with: 105 | github-token: ${{ secrets.github_token }} 106 | flag-name: run-${{ matrix.exchange }} 107 | parallel: true 108 | 109 | finish: 110 | needs: [validate, exchange] 111 | runs-on: ubuntu-latest 112 | steps: 113 | - name: Coveralls Finished 114 | uses: coverallsapp/github-action@master 115 | with: 116 | github-token: ${{ secrets.github_token }} 117 | parallel-finished: true 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | test.ts 61 | test*.ts 62 | 63 | .vscode 64 | **/.DS_Store 65 | 66 | dist 67 | coverage 68 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 100, 4 | "singleQuote": false, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQs 2 | 3 | #### What is the format for markets? 4 | 5 | The format for markets requires: 6 | 7 | - `id`:`string` remote identifier used by the exchange 8 | - `base`:`string` - base symbol for the market 9 | - `quote`:`string` - quote symbol fro the market 10 | - `type`:`string` - type of market (spot/futures/option/swap) 11 | 12 | ```javascript 13 | { 14 | id: "BTCUSDT" 15 | base: "BTC" 16 | quote: "USDT", 17 | type: "spot" 18 | } 19 | ``` 20 | 21 | #### How can I obtain markets for an exchange? 22 | 23 | You can load markets in several ways: 24 | 25 | 1. Load the markets from the exchanges REST API and parse them into the format required by CCXWS 26 | 2. Load markets from your own database into the format required by CCXWS 27 | 3. Use `CCXT` to load markets 28 | 29 | #### When I connect to an exchange I receive no data, what is wrong? 30 | 31 | Ensure you are using the correct market format. 32 | 33 | #### How do I maintain an Level 2 order book? 34 | 35 | This is a complex question and varies by each exchange. The two basic methods are `snapshots` 36 | and `updates`. A `snapshot` provides an order book at a particular point in time and includes bids 37 | and asks up to a certain depth (for example 10, 50, or 100 asks and bids). 38 | 39 | An `update` usually starts with a `snapshot` and then provides incremental updates to the order book. 40 | These updates include insertions, updates, and deletions. Usually with update streams, the point provided 41 | includes the absolute volume not the delta. This means you can replace a price point with the new size 42 | provided in the stream. Typically deletions have zero size for the price point, indicating you can 43 | remove the price point. 44 | 45 | Some exchanges also include sequence identifiers to help you identify when you may have missed a message. 46 | In the event that you miss a message, you should reconnect the stream. 47 | 48 | #### Will CCXWS include order books? 49 | 50 | Yes! We are working on prototype order books and have implemented them for several exchanges. 51 | These can be found in `src/orderbooks` folder. Once we have finalized some of the data structures 52 | and patterns, we will be implementing full support for other exchanges. 53 | 54 | #### Are ticker streams updated with each trade or more frequently? 55 | 56 | This depends on the exchange. Some exchanges will provide periodic ticker updates including top 57 | of book updates others will only update the ticker with each tick of the market. 58 | 59 | #### Are you going to add private feeds? 60 | 61 | Yes, we would like to add common functionality for for private feeds. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Altangent Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/Jwt.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as jwt from "../src/Jwt"; 3 | 4 | describe("JWT", () => { 5 | describe("hs256", () => { 6 | it("valid token", () => { 7 | const payload = { 8 | loggedInAs: "admin", 9 | iat: 1422779638, 10 | }; 11 | const secret = "secretkey"; 12 | const result = jwt.hs256(payload, secret); 13 | expect(result).to.equal( 14 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI", 15 | ); 16 | }); 17 | 18 | it("valid token", () => { 19 | const payload = { 20 | sub: "1234567890", 21 | name: "John Doe", 22 | iat: 1516239022, 23 | }; 24 | const secret = "secret"; 25 | const result = jwt.hs256(payload, secret); 26 | expect(result).to.equal( 27 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o", 28 | ); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/Trade.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Trade } from "../src/Trade"; 3 | 4 | describe("Trade", () => { 5 | it("marketId should be base + quote", () => { 6 | const t = new Trade({ base: "BTC", quote: "USD" }); 7 | expect(t.marketId).to.equal("BTC/USD"); 8 | }); 9 | 10 | it("fullId should be exchange + base + quote", () => { 11 | const t = new Trade({ exchange: "GDAX", base: "BTC", quote: "USD" }); 12 | expect(t.fullId).to.equal("GDAX:BTC/USD"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/Watcher.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import sinon from "sinon"; 3 | import { EventEmitter } from "events"; 4 | import { Watcher } from "../src/Watcher"; 5 | 6 | class MockClient extends EventEmitter { 7 | public reconnect: sinon.SinonStub; 8 | constructor() { 9 | super(); 10 | this.reconnect = sinon.stub(); 11 | } 12 | } 13 | 14 | function wait(ms: number) { 15 | return new Promise(resolve => setTimeout(resolve, ms)); 16 | } 17 | 18 | describe("Watcher", () => { 19 | let sut; 20 | let client; 21 | 22 | before(() => { 23 | client = new MockClient(); 24 | sut = new Watcher(client, 100); 25 | sinon.spy(sut, "stop"); 26 | }); 27 | 28 | describe("start", () => { 29 | before(() => { 30 | sut.start(); 31 | }); 32 | it("should trigger a stop", () => { 33 | expect(sut.stop.callCount).to.equal(1); 34 | }); 35 | it("should start the interval", () => { 36 | expect(sut._intervalHandle).to.not.be.undefined; 37 | }); 38 | }); 39 | 40 | describe("stop", () => { 41 | before(() => { 42 | sut.stop(); 43 | }); 44 | it("should clear the interval", () => { 45 | expect(sut._intervalHandle).to.be.undefined; 46 | }); 47 | }); 48 | 49 | describe("on messages", () => { 50 | beforeEach(() => { 51 | sut._lastMessage = undefined; 52 | }); 53 | it("other should not mark", () => { 54 | client.emit("other"); 55 | expect(sut._lastMessage).to.be.undefined; 56 | }); 57 | it("ticker should mark", () => { 58 | client.emit("ticker"); 59 | expect(sut._lastMessage).to.not.be.undefined; 60 | }); 61 | it("trade should mark", () => { 62 | client.emit("trade"); 63 | expect(sut._lastMessage).to.not.be.undefined; 64 | }); 65 | it("l2snapshot should mark", () => { 66 | client.emit("l2snapshot"); 67 | expect(sut._lastMessage).to.not.be.undefined; 68 | }); 69 | it("l2update should mark", () => { 70 | client.emit("l2update"); 71 | expect(sut._lastMessage).to.not.be.undefined; 72 | }); 73 | it("l3snapshot should mark", () => { 74 | client.emit("l3snapshot"); 75 | expect(sut._lastMessage).to.not.be.undefined; 76 | }); 77 | it("l3update should mark", () => { 78 | client.emit("l3update"); 79 | expect(sut._lastMessage).to.not.be.undefined; 80 | }); 81 | }); 82 | 83 | describe("on expire", () => { 84 | before(() => { 85 | sut.start(); 86 | }); 87 | it("it should call reconnect on the client", async () => { 88 | await wait(500); 89 | expect(client.reconnect.callCount).to.be.gt(0); 90 | }); 91 | }); 92 | }).retries(3); 93 | -------------------------------------------------------------------------------- /__tests__/Zlib.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as zlib from "../src/ZlibUtils"; 3 | 4 | describe("unzip", () => { 5 | it("should process valid unzip operations in order", done => { 6 | const vals = []; 7 | const cb = (err, val) => { 8 | vals.push(val); 9 | if (vals.length === 5) { 10 | expect(vals).to.deep.equal([ 11 | Buffer.from("1"), 12 | Buffer.from("2"), 13 | Buffer.from("3"), 14 | Buffer.from("4"), 15 | Buffer.from("5"), 16 | ]); 17 | done(); 18 | } 19 | }; 20 | zlib.unzip(Buffer.from("1f8b0800000000000013330400b7efdc8301000000", "hex"), cb); 21 | zlib.unzip(Buffer.from("1f8b08000000000000133302000dbed51a01000000", "hex"), cb); 22 | zlib.unzip(Buffer.from("1f8b08000000000000133306009b8ed26d01000000", "hex"), cb); 23 | zlib.unzip(Buffer.from("1f8b0800000000000013330100381bb6f301000000", "hex"), cb); 24 | zlib.unzip(Buffer.from("1f8b0800000000000013330500ae2bb18401000000", "hex"), cb); 25 | }); 26 | 27 | it("should process invalid unzip operations in order", done => { 28 | const errs = []; 29 | const cb = err => { 30 | errs.push(err); 31 | if (errs.length === 3) done(); 32 | }; 33 | zlib.unzip(Buffer.from("1", "hex"), cb); 34 | zlib.unzip(Buffer.from("2", "hex"), cb); 35 | zlib.unzip(Buffer.from("3", "hex"), cb); 36 | }); 37 | 38 | it("should process invalid and valid unzip operations in order", done => { 39 | const vals = []; 40 | const cb = (err, val) => { 41 | vals.push(err || val); 42 | if (vals.length === 3) { 43 | expect(vals[0]).to.deep.equal(Buffer.from("1")); 44 | expect(vals[1]).to.be.instanceOf(Error); 45 | expect(vals[2]).to.deep.equal(Buffer.from("2")); 46 | done(); 47 | } 48 | }; 49 | zlib.unzip(Buffer.from("1f8b0800000000000013330400b7efdc8301000000", "hex"), cb); 50 | zlib.unzip(Buffer.from("2", "hex"), cb); 51 | zlib.unzip(Buffer.from("1f8b08000000000000133302000dbed51a01000000", "hex"), cb); 52 | }); 53 | }); 54 | 55 | describe("inflateRaw", () => { 56 | it("should process operations in order", done => { 57 | const vals = []; 58 | const cb = (err, val) => { 59 | vals.push(val); 60 | if (vals.length === 5) { 61 | expect(vals).to.deep.equal([ 62 | Buffer.from("1"), 63 | Buffer.from("2"), 64 | Buffer.from("3"), 65 | Buffer.from("4"), 66 | Buffer.from("5"), 67 | ]); 68 | done(); 69 | } 70 | }; 71 | zlib.inflateRaw(Buffer.from("330400", "hex"), cb); 72 | zlib.inflateRaw(Buffer.from("330200", "hex"), cb); 73 | zlib.inflateRaw(Buffer.from("330600", "hex"), cb); 74 | zlib.inflateRaw(Buffer.from("330100", "hex"), cb); 75 | zlib.inflateRaw(Buffer.from("330500", "hex"), cb); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/exchanges/BiboxClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BiboxClient } from "../../src/exchanges/BiboxClient"; 3 | import * as https from "../../src/Https"; 4 | 5 | testClient({ 6 | clientFactory: () => new BiboxClient(), 7 | clientName: "BiboxClient", 8 | exchangeName: "Bibox", 9 | markets: [ 10 | { 11 | id: "BTC_USDT", 12 | base: "BTC", 13 | quote: "USDT", 14 | }, 15 | { 16 | id: "ETH_USDT", 17 | base: "ETH", 18 | quote: "USDT", 19 | }, 20 | { 21 | id: "ETH_BTC", 22 | base: "ETH", 23 | quote: "BTC", 24 | }, 25 | ], 26 | 27 | async fetchAllMarkets() { 28 | const res = (await https.get("https://api.bibox.com/v1/mdata?cmd=pairList")) as any; 29 | return res.result.map(p => ({ 30 | id: p.pair, 31 | base: p.pair.split("_")[0], 32 | quote: p.pair.split("_")[1], 33 | })); 34 | }, 35 | 36 | getEventingSocket(client) { 37 | return (client as any)._clients[0]._wss; 38 | }, 39 | 40 | testAllMarketsTrades: true, 41 | testAllMarketsTradesSuccess: 50, 42 | 43 | testConnectEvents: true, 44 | testDisconnectEvents: true, 45 | testReconnectionEvents: true, 46 | testCloseEvents: true, 47 | 48 | hasTickers: true, 49 | hasTrades: true, 50 | hasCandles: true, 51 | hasLevel2Snapshots: true, 52 | hasLevel2Updates: false, 53 | hasLevel3Snapshots: false, 54 | hasLevel3Updates: false, 55 | 56 | ticker: { 57 | hasTimestamp: true, 58 | hasLast: true, 59 | hasOpen: true, 60 | hasHigh: true, 61 | hasLow: true, 62 | hasVolume: true, 63 | hasQuoteVolume: false, 64 | hasChange: true, 65 | hasChangePercent: true, 66 | hasBid: true, 67 | hasBidVolume: false, 68 | hasAsk: true, 69 | hasAskVolume: false, 70 | }, 71 | 72 | trade: { 73 | hasTradeId: false, 74 | }, 75 | 76 | candle: {}, 77 | 78 | l2snapshot: { 79 | hasTimestampMs: true, 80 | hasSequenceId: false, 81 | hasCount: false, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /__tests__/exchanges/BinanceClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BinanceClient } from "../../src/exchanges/BinanceClient"; 3 | import { get } from "../../src/Https"; 4 | import { Market } from "../../src/Market"; 5 | 6 | async function fetchAllMarkets(): Promise { 7 | const results: any = await get("https://api.binance.com/api/v1/exchangeInfo"); 8 | return results.symbols 9 | .filter(p => p.status === "TRADING") 10 | .map(p => ({ id: p.symbol, base: p.baseAsset, quote: p.quoteAsset })); 11 | } 12 | 13 | testClient({ 14 | clientFactory: () => new BinanceClient(), 15 | clientName: "BinanceClient", 16 | exchangeName: "Binance", 17 | markets: [ 18 | { 19 | id: "BTCUSDT", 20 | base: "BTC", 21 | quote: "USDT", 22 | }, 23 | { 24 | id: "BTCUSDC", 25 | base: "BTC", 26 | quote: "USDC", 27 | }, 28 | ], 29 | 30 | fetchAllMarkets, 31 | 32 | unsubWaitMs: 1500, 33 | 34 | testConnectEvents: true, 35 | testDisconnectEvents: true, 36 | testReconnectionEvents: true, 37 | testCloseEvents: true, 38 | 39 | testAllMarketsTrades: true, 40 | testAllMarketsTradesSuccess: 50, 41 | 42 | hasTickers: true, 43 | hasTrades: true, 44 | hasCandles: true, 45 | hasLevel2Snapshots: true, 46 | hasLevel2Updates: true, 47 | hasLevel3Snapshots: false, 48 | hasLevel3Updates: false, 49 | 50 | ticker: { 51 | hasTimestamp: true, 52 | hasLast: true, 53 | hasOpen: true, 54 | hasHigh: true, 55 | hasLow: true, 56 | hasVolume: true, 57 | hasQuoteVolume: true, 58 | hasChange: true, 59 | hasChangePercent: true, 60 | hasBid: true, 61 | hasBidVolume: true, 62 | hasAsk: true, 63 | hasAskVolume: true, 64 | }, 65 | 66 | trade: { 67 | hasTradeId: true, 68 | }, 69 | 70 | candle: {}, 71 | 72 | l2snapshot: { 73 | hasTimestampMs: false, 74 | hasSequenceId: true, 75 | hasCount: false, 76 | }, 77 | 78 | l2update: { 79 | hasSnapshot: true, 80 | hasTimestampMs: false, 81 | hasSequenceId: true, 82 | hasLastSequenceId: true, 83 | hasEventMs: true, 84 | hasCount: false, 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /__tests__/exchanges/BinanceFuturesCoinmClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BinanceFuturesCoinmClient } from "../../src/exchanges/BinanceFuturesCoinmClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | async function fetchAllMarkets() { 6 | const results = (await get("https://dapi.binance.com/dapi/v1/exchangeInfo")) as any; 7 | return results.symbols 8 | .filter(p => p.contractStatus === "TRADING") 9 | .map(p => ({ id: p.symbol, base: p.baseAsset, quote: p.quoteAsset })); 10 | } 11 | 12 | testClient({ 13 | clientFactory: () => new BinanceFuturesCoinmClient(), 14 | clientName: "BinanceFuturesCoinmClient", 15 | exchangeName: "Binance Futures COIN-M", 16 | 17 | markets: [], 18 | allMarkets: [], 19 | 20 | fetchMarkets: fetchAllMarkets, 21 | fetchAllMarkets: fetchAllMarkets, 22 | 23 | unsubWaitMs: 1500, 24 | 25 | testConnectEvents: true, 26 | testDisconnectEvents: true, 27 | testReconnectionEvents: true, 28 | testCloseEvents: true, 29 | 30 | testAllMarketsTrades: true, 31 | testAllMarketsTradesSuccess: 5, 32 | 33 | hasTickers: true, 34 | hasTrades: true, 35 | hasCandles: true, 36 | hasLevel2Snapshots: true, 37 | hasLevel2Updates: true, 38 | hasLevel3Snapshots: false, 39 | hasLevel3Updates: false, 40 | 41 | ticker: { 42 | hasTimestamp: true, 43 | hasLast: true, 44 | hasOpen: true, 45 | hasHigh: true, 46 | hasLow: true, 47 | hasVolume: true, 48 | hasQuoteVolume: true, 49 | hasChange: true, 50 | hasChangePercent: true, 51 | hasBid: false, // deviation from spot 52 | hasBidVolume: false, // deviation from spot 53 | hasAsk: false, // deviation from spot 54 | hasAskVolume: false, // deviation from spot 55 | }, 56 | 57 | trade: { 58 | hasTradeId: true, 59 | }, 60 | 61 | candle: {}, 62 | 63 | l2snapshot: { 64 | hasTimestampMs: true, 65 | hasSequenceId: true, 66 | hasCount: false, 67 | }, 68 | 69 | l2update: { 70 | hasSnapshot: true, 71 | hasTimestampMs: true, 72 | hasEventMs: true, 73 | hasSequenceId: true, 74 | hasLastSequenceId: true, 75 | hasCount: false, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/exchanges/BinanceFuturesUsdtmClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BinanceFuturesUsdtmClient } from "../../src/exchanges/BinanceFuturesUsdtmClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | async function fetchAllMarkets() { 6 | const results = (await get("https://fapi.binance.com/fapi/v1/exchangeInfo")) as any; 7 | return results.symbols 8 | .filter(p => p.status === "TRADING") 9 | .map(p => ({ id: p.symbol, base: p.baseAsset, quote: p.quoteAsset })); 10 | } 11 | 12 | testClient({ 13 | clientFactory: () => new BinanceFuturesUsdtmClient(), 14 | clientName: "BinanceFuturesUsdtMClient", 15 | exchangeName: "Binance Futures USDT-M", 16 | markets: [ 17 | { 18 | id: "BTCUSDT", 19 | base: "BTC", 20 | quote: "USDT", 21 | }, 22 | { 23 | id: "ETHUSDT", 24 | base: "ETH", 25 | quote: "USDT", 26 | }, 27 | ], 28 | 29 | fetchAllMarkets, 30 | 31 | unsubWaitMs: 1500, 32 | 33 | testConnectEvents: true, 34 | testDisconnectEvents: true, 35 | testReconnectionEvents: true, 36 | testCloseEvents: true, 37 | 38 | testAllMarketsTrades: true, 39 | testAllMarketsTradesSuccess: 20, 40 | 41 | hasTickers: true, 42 | hasTrades: true, 43 | hasCandles: true, 44 | hasLevel2Snapshots: true, 45 | hasLevel2Updates: true, 46 | hasLevel3Snapshots: false, 47 | hasLevel3Updates: false, 48 | 49 | ticker: { 50 | hasTimestamp: true, 51 | hasLast: true, 52 | hasOpen: true, 53 | hasHigh: true, 54 | hasLow: true, 55 | hasVolume: true, 56 | hasQuoteVolume: true, 57 | hasChange: true, 58 | hasChangePercent: true, 59 | hasBid: false, // deviation from spot 60 | hasBidVolume: false, // deviation from spot 61 | hasAsk: false, // deviation from spot 62 | hasAskVolume: false, // deviation from spot 63 | }, 64 | 65 | trade: { 66 | hasTradeId: true, 67 | }, 68 | 69 | candle: {}, 70 | 71 | l2snapshot: { 72 | hasTimestampMs: true, 73 | hasSequenceId: true, 74 | hasCount: false, 75 | }, 76 | 77 | l2update: { 78 | hasSnapshot: true, 79 | hasTimestampMs: true, 80 | hasEventMs: true, 81 | hasSequenceId: true, 82 | hasLastSequenceId: true, 83 | hasCount: false, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /__tests__/exchanges/BinanceJeClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BinanceJeClient } from "../../src/exchanges/BinanceJeClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | async function fetchAllMarkets() { 6 | const results = (await get("https://api.binance.je/api/v1/exchangeInfo")) as any; 7 | return results.symbols 8 | .filter(p => p.status === "TRADING") 9 | .map(p => ({ id: p.symbol, base: p.baseAsset, quote: p.quoteAsset })); 10 | } 11 | 12 | testClient({ 13 | clientFactory: () => new BinanceJeClient(), 14 | clientName: "BinanceJeClient", 15 | exchangeName: "BinanceJe", 16 | 17 | fetchMarkets: fetchAllMarkets, 18 | 19 | skip: true, 20 | unsubWaitMs: 1500, 21 | 22 | testConnectEvents: true, 23 | testDisconnectEvents: true, 24 | testReconnectionEvents: true, 25 | testCloseEvents: true, 26 | 27 | hasTickers: true, 28 | hasTrades: true, 29 | hasCandles: true, 30 | hasLevel2Snapshots: true, 31 | hasLevel2Updates: true, 32 | hasLevel3Snapshots: false, 33 | hasLevel3Updates: false, 34 | 35 | ticker: { 36 | hasTimestamp: true, 37 | hasLast: true, 38 | hasOpen: true, 39 | hasHigh: true, 40 | hasLow: true, 41 | hasVolume: true, 42 | hasQuoteVolume: true, 43 | hasChange: true, 44 | hasChangePercent: true, 45 | hasBid: true, 46 | hasBidVolume: true, 47 | hasAsk: true, 48 | hasAskVolume: true, 49 | }, 50 | 51 | trade: { 52 | hasTradeId: true, 53 | }, 54 | 55 | candle: {}, 56 | 57 | l2snapshot: { 58 | hasTimestampMs: false, 59 | hasSequenceId: true, 60 | hasCount: false, 61 | }, 62 | 63 | l2update: { 64 | hasSnapshot: true, 65 | hasTimestampMs: false, 66 | hasSequenceId: true, 67 | hasLastSequenceId: true, 68 | hasEventMs: true, 69 | hasCount: false, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/exchanges/BinanceUsClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BinanceUsClient } from "../../src/exchanges/BinanceUsClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | async function fetchAllMarkets() { 6 | const results = (await get("https://api.binance.us/api/v1/exchangeInfo")) as any; 7 | return results.symbols 8 | .filter(p => p.status === "TRADING") 9 | .map(p => ({ id: p.symbol, base: p.baseAsset, quote: p.quoteAsset })); 10 | } 11 | 12 | testClient({ 13 | clientFactory: () => new BinanceUsClient(), 14 | clientName: "BinanceUSClient", 15 | exchangeName: "BinanceUS", 16 | 17 | fetchMarkets: fetchAllMarkets, 18 | 19 | skip: false, 20 | unsubWaitMs: 1500, 21 | 22 | testConnectEvents: true, 23 | testDisconnectEvents: true, 24 | testReconnectionEvents: true, 25 | testCloseEvents: true, 26 | 27 | hasTickers: true, 28 | hasTrades: true, 29 | hasCandles: true, 30 | hasLevel2Snapshots: true, 31 | hasLevel2Updates: true, 32 | hasLevel3Snapshots: false, 33 | hasLevel3Updates: false, 34 | 35 | ticker: { 36 | hasTimestamp: true, 37 | hasLast: true, 38 | hasOpen: true, 39 | hasHigh: true, 40 | hasLow: true, 41 | hasVolume: true, 42 | hasQuoteVolume: true, 43 | hasChange: true, 44 | hasChangePercent: true, 45 | hasBid: true, 46 | hasBidVolume: true, 47 | hasAsk: true, 48 | hasAskVolume: true, 49 | }, 50 | 51 | trade: { 52 | hasTradeId: true, 53 | }, 54 | 55 | candle: {}, 56 | 57 | l2snapshot: { 58 | hasTimestampMs: false, 59 | hasSequenceId: true, 60 | hasCount: false, 61 | }, 62 | 63 | l2update: { 64 | hasSnapshot: true, 65 | hasTimestampMs: false, 66 | hasSequenceId: true, 67 | hasLastSequenceId: true, 68 | hasEventMs: true, 69 | hasCount: false, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/exchanges/BitfinexClient.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | import { testClient } from "../TestRunner"; 4 | import { BitfinexClient, BitfinexTradeMessageType } from "../../src/exchanges/BitfinexClient"; 5 | 6 | const regularSpec = { 7 | exchangeName: "Bitfinex", 8 | markets: [ 9 | { 10 | id: "BTCUSD", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "ETHUSD", 16 | base: "ETH", 17 | quote: "USD", 18 | }, 19 | { 20 | id: "ETHBTC", 21 | base: "ETH", 22 | quote: "BTC", 23 | }, 24 | { 25 | // test a very low volume market 26 | id: "ENJUSD", 27 | base: "ENJ", 28 | quote: "USD", 29 | }, 30 | ], 31 | 32 | testConnectEvents: true, 33 | testDisconnectEvents: true, 34 | testReconnectionEvents: true, 35 | testCloseEvents: true, 36 | 37 | hasTickers: true, 38 | hasTrades: true, 39 | hasCandles: false, 40 | hasLevel2Snapshots: false, 41 | hasLevel2Updates: true, 42 | hasLevel3Snapshots: false, 43 | hasLevel3Updates: true, 44 | 45 | ticker: { 46 | hasTimestamp: true, 47 | hasLast: true, 48 | hasOpen: true, 49 | hasHigh: true, 50 | hasLow: true, 51 | hasVolume: true, 52 | hasQuoteVolume: false, 53 | hasChange: true, 54 | hasChangePercent: true, 55 | hasBid: true, 56 | hasBidVolume: true, 57 | hasAsk: true, 58 | hasSequenceId: true, 59 | hasAskVolume: true, 60 | }, 61 | 62 | trade: { 63 | hasTradeId: true, 64 | hasSequenceId: true, 65 | }, 66 | 67 | l2snapshot: { 68 | hasTimestampMs: true, 69 | hasSequenceId: true, 70 | hasCount: true, 71 | }, 72 | 73 | l2update: { 74 | hasSnapshot: true, 75 | hasTimestampMs: true, 76 | hasSequenceId: true, 77 | hasCount: true, 78 | done: function (spec, result, update) { 79 | const hasAsks = update.asks && update.asks.length > 0; 80 | const hasBids = update.bids && update.bids.length > 0; 81 | return hasAsks || hasBids; 82 | }, 83 | }, 84 | 85 | l3snapshot: { 86 | hasTimestampMs: true, 87 | hasSequenceId: true, 88 | }, 89 | 90 | l3update: { 91 | hasSnapshot: true, 92 | hasTimestampMs: true, 93 | hasSequenceId: true, 94 | hasCount: true, 95 | done: function (spec, result, update) { 96 | const hasAsks = update.asks && update.asks.length > 0; 97 | const hasBids = update.bids && update.bids.length > 0; 98 | return hasAsks || hasBids; 99 | }, 100 | }, 101 | }; 102 | 103 | const sequenceIdValidateWithEmptyHeartbeatsSpec = { 104 | ...JSON.parse(JSON.stringify(regularSpec)), 105 | markets: [ 106 | { 107 | // test a very low volume market 108 | id: "ENJUSD", 109 | base: "ENJ", 110 | quote: "USD", 111 | }, 112 | { 113 | id: "BTCUSD", 114 | base: "BTC", 115 | quote: "USDT", 116 | }, 117 | ], 118 | trade: { 119 | // note: the empty trade event for heartbeat won't have tradeId. but that won't be the first message so TestRunner won't encounter it 120 | hasTradeId: true, 121 | hasSequenceId: true, 122 | }, 123 | }; 124 | 125 | testClient({ 126 | clientName: "BitfinexClient - default options", 127 | clientFactory: () => new BitfinexClient(), 128 | ...regularSpec, 129 | }); 130 | 131 | testClient({ 132 | clientName: "BitfinexClient - custom options", 133 | clientFactory: () => 134 | new BitfinexClient({ 135 | enableEmptyHeartbeatEvents: true, 136 | tradeMessageType: BitfinexTradeMessageType.All, 137 | }), 138 | ...sequenceIdValidateWithEmptyHeartbeatsSpec, 139 | }); 140 | -------------------------------------------------------------------------------- /__tests__/exchanges/BitflyerClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BitflyerClient } from "../../src/exchanges/BitflyerClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new BitflyerClient(), 6 | clientName: "BitflyerClient", 7 | exchangeName: "bitFlyer", 8 | markets: [ 9 | { 10 | id: "BTC_JPY", 11 | base: "BTC", 12 | quote: "JPY", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: false, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: false, 33 | hasHigh: false, 34 | hasLow: false, 35 | hasVolume: true, 36 | hasQuoteVolume: true, 37 | hasChange: false, 38 | hasChangePercent: false, 39 | hasBid: true, 40 | hasBidVolume: true, 41 | hasAsk: true, 42 | hasAskVolume: true, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | }, 48 | 49 | l2snapshot: { 50 | hasTimestampMs: false, 51 | hasSequenceId: false, 52 | hasCount: false, 53 | }, 54 | 55 | l2update: { 56 | hasSnapshot: true, 57 | hasTimestampMs: false, 58 | hasSequenceId: false, 59 | hasCount: false, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /__tests__/exchanges/BithumbClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BithumbClient } from "../../src/exchanges/BithumbClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new BithumbClient(), 6 | clientName: "BithumbClient", 7 | exchangeName: "Bithumb", 8 | markets: [ 9 | { 10 | id: "BTC_KRW", 11 | base: "BTC", 12 | quote: "KRW", 13 | }, 14 | { 15 | id: "ETH_KRW", 16 | base: "ETH", 17 | quote: "KRW", 18 | }, 19 | ], 20 | 21 | testConnectEvents: true, 22 | testDisconnectEvents: true, 23 | testReconnectionEvents: true, 24 | testCloseEvents: true, 25 | 26 | hasTickers: true, 27 | hasTrades: true, 28 | hasCandles: false, 29 | hasLevel2Snapshots: false, 30 | hasLevel2Updates: true, 31 | hasLevel3Snapshots: false, 32 | hasLevel3Updates: false, 33 | 34 | ticker: { 35 | hasTimestamp: true, 36 | hasLast: true, 37 | hasOpen: true, 38 | hasHigh: true, 39 | hasLow: true, 40 | hasVolume: true, 41 | hasQuoteVolume: true, 42 | hasChange: true, 43 | hasChangePercent: true, 44 | hasAsk: false, 45 | hasBid: false, 46 | hasAskVolume: false, 47 | hasBidVolume: false, 48 | }, 49 | 50 | trade: { 51 | hasTradeId: false, 52 | }, 53 | 54 | l2update: { 55 | hasSnapshot: true, 56 | hasTimestampMs: true, 57 | hasSequenceId: false, 58 | hasCount: true, 59 | }, 60 | 61 | l2snapshot: { 62 | hasTimestampMs: true, 63 | hasSequenceId: false, 64 | hasCount: false, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/exchanges/BitmexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { testClient } from "../TestRunner"; 3 | import { BitmexClient } from "../../src/exchanges/BitmexClient"; 4 | 5 | testClient({ 6 | clientFactory: () => new BitmexClient(), 7 | clientName: "BitMEXClient", 8 | exchangeName: "BitMEX", 9 | markets: [ 10 | { 11 | id: "XBTUSD", 12 | base: "BTC", 13 | quote: "USD", 14 | }, 15 | ], 16 | 17 | testConnectEvents: true, 18 | testDisconnectEvents: true, 19 | testReconnectionEvents: true, 20 | testCloseEvents: true, 21 | 22 | hasTickers: true, 23 | hasTrades: true, 24 | hasCandles: true, 25 | hasLevel2Snapshots: false, 26 | hasLevel2Updates: true, 27 | hasLevel3Snapshots: false, 28 | hasLevel3Updates: false, 29 | 30 | ticker: { 31 | hasTimestamp: true, 32 | hasLast: true, 33 | hasOpen: false, 34 | hasHigh: false, 35 | hasLow: false, 36 | hasVolume: false, 37 | hasQuoteVolume: false, 38 | hasChange: false, 39 | hasChangePercent: false, 40 | hasAsk: true, 41 | hasBid: true, 42 | hasAskVolume: true, 43 | hasBidVolume: true, 44 | }, 45 | 46 | trade: { 47 | hasTradeId: true, 48 | tests: (spec, result) => { 49 | it("trade.tradeId should be 32 hex characters", () => { 50 | expect(result.trade.tradeId).to.match(/^[a-f0-9]{32,32}$/); 51 | }); 52 | }, 53 | }, 54 | 55 | candle: {}, 56 | 57 | l2snapshot: { 58 | hasTimestampMs: false, 59 | hasSequenceId: false, 60 | hasCount: false, 61 | }, 62 | 63 | l2update: { 64 | hasSnapshot: true, 65 | hasTimestampMs: false, 66 | hasSequenceId: false, 67 | hasCount: false, 68 | done: (spec, result, update) => { 69 | const point = update.bids[0] || update.asks[0]; 70 | if (point.meta.type === "update") result.hasUpdate = true; 71 | if (point.meta.type === "insert") result.hasInsert = true; 72 | if (point.meta.type === "delete") result.hasDelete = true; 73 | return result.hasUpdate && result.hasInsert && result.hasDelete; 74 | }, 75 | tests: (spec, result) => { 76 | it("update.bid/ask should have meta.id", () => { 77 | const point = result.update.bids[0] || result.update.asks[0]; 78 | expect(point.meta.id).to.be.greaterThan(0); 79 | }); 80 | 81 | it("update.bid/ask should have meta.type", () => { 82 | const point = result.update.bids[0] || result.update.asks[0]; 83 | expect(point.meta.type).to.be.match(/update|delete|insert/); 84 | }); 85 | }, 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /__tests__/exchanges/BitstampClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { BitstampClient } from "../../src/exchanges/BitstampClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new BitstampClient(), 6 | clientName: "BitstampClient", 7 | exchangeName: "Bitstamp", 8 | markets: [ 9 | { 10 | id: "btcusd", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: false, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: true, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | trade: { 30 | hasTradeId: true, 31 | }, 32 | 33 | l2snapshot: { 34 | hasTimestampMs: true, 35 | hasSequenceId: false, 36 | hasCount: false, 37 | }, 38 | 39 | l2update: { 40 | hasSnapshot: true, 41 | hasTimestampMs: true, 42 | hasSequenceId: false, 43 | hasCount: false, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/exchanges/BittrexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import * as https from "../../src/Https"; 3 | import { BittrexClient } from "../../src/exchanges/BittrexClient"; 4 | 5 | testClient({ 6 | clientFactory: () => new BittrexClient(), 7 | clientName: "BittrexClient", 8 | exchangeName: "Bittrex", 9 | markets: [ 10 | { 11 | id: "BTC-USDT", 12 | base: "BTC", 13 | quote: "USDT", 14 | }, 15 | { 16 | id: "ETH-BTC", 17 | base: "ETH", 18 | quote: "BTC", 19 | }, 20 | { 21 | id: "LTC-BTC", 22 | base: "LTC", 23 | quote: "BTC", 24 | }, 25 | { 26 | id: "XRP-BTC", 27 | base: "XRP", 28 | quote: "BTC", 29 | }, 30 | ], 31 | 32 | async fetchAllMarkets() { 33 | const res: any = await https.get("https://api.bittrex.com/v3/markets"); 34 | return res.map(p => ({ 35 | id: p.symbol, 36 | base: p.baseCurrencySymbol, 37 | quote: p.quoteCurrencySymbol, 38 | })); 39 | }, 40 | 41 | testConnectEvents: false, 42 | testDisconnectEvents: false, 43 | testReconnectionEvents: false, 44 | testCloseEvents: false, 45 | 46 | testAllMarketsTrades: true, 47 | testAllMarketsTradesSuccess: 30, 48 | 49 | testAllMarketsL2Updates: true, 50 | testAllMarketsL2UpdatesSuccess: 400, 51 | 52 | hasTickers: true, 53 | hasTrades: true, 54 | hasCandles: true, 55 | hasLevel2Snapshots: false, 56 | hasLevel2Updates: true, 57 | hasLevel3Snapshots: false, 58 | hasLevel3Updates: false, 59 | 60 | ticker: { 61 | hasTimestamp: true, 62 | hasLast: false, 63 | hasOpen: false, 64 | hasHigh: true, 65 | hasLow: true, 66 | hasVolume: true, 67 | hasQuoteVolume: true, 68 | hasChange: false, 69 | hasChangePercent: true, 70 | hasBid: false, 71 | hasBidVolume: false, 72 | hasAsk: false, 73 | hasAskVolume: false, 74 | }, 75 | 76 | trade: { 77 | hasTradeId: true, 78 | }, 79 | 80 | l2snapshot: { 81 | hasTimestampMs: false, 82 | hasSequenceId: true, 83 | hasCount: false, 84 | }, 85 | 86 | l2update: { 87 | hasSnapshot: true, 88 | hasTimestampMs: false, 89 | hasSequenceId: true, 90 | hasCount: false, 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /__tests__/exchanges/CexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { CexClient } from "../../src/exchanges/CexClient"; 3 | 4 | testClient({ 5 | clientFactory: () => 6 | new CexClient({ 7 | apiKey: process.env.CEX_API_KEY, 8 | apiSecret: process.env.CEX_API_SECRET, 9 | }), 10 | clientName: "CexClient", 11 | exchangeName: "CEX", 12 | markets: [ 13 | { 14 | id: "BTC/USD", 15 | base: "BTC", 16 | quote: "USD", 17 | }, 18 | { 19 | id: "BTC/EUR", 20 | base: "BTC", 21 | quote: "USD", 22 | }, 23 | { 24 | id: "BTT/EUR", 25 | base: "BTT", 26 | quote: "EUR", 27 | }, 28 | ], 29 | 30 | getEventingSocket(client, market) { 31 | return (client as any)._clients.get(market.id).then(c => c._wss); 32 | }, 33 | 34 | testConnectEvents: true, 35 | testDisconnectEvents: true, 36 | testReconnectionEvents: true, 37 | testCloseEvents: true, 38 | 39 | hasTickers: true, 40 | hasTrades: true, 41 | hasCandles: true, 42 | hasLevel2Snapshots: true, 43 | hasLevel2Updates: false, 44 | hasLevel3Snapshots: false, 45 | hasLevel3Updates: false, 46 | 47 | ticker: { 48 | hasTimestamp: true, 49 | hasLast: true, 50 | hasOpen: true, 51 | hasHigh: false, 52 | hasLow: false, 53 | hasVolume: true, 54 | hasQuoteVolume: false, 55 | hasChange: true, 56 | hasChangePercent: true, 57 | hasBid: false, 58 | hasBidVolume: false, 59 | hasAsk: false, 60 | hasAskVolume: false, 61 | }, 62 | 63 | trade: { 64 | hasTradeId: true, 65 | }, 66 | 67 | candle: {}, 68 | 69 | l2snapshot: { 70 | hasTimestampMs: false, 71 | hasSequenceId: true, 72 | hasCount: false, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /__tests__/exchanges/CoinbaseProClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { CoinbaseProClient } from "../../src/exchanges/CoinbaseProClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new CoinbaseProClient(), 6 | clientName: "CoinbasePro", 7 | exchangeName: "CoinbasePro", 8 | markets: [ 9 | { 10 | id: "BTC-USD", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: false, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: true, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: true, 33 | hasHigh: true, 34 | hasLow: true, 35 | hasVolume: true, 36 | hasQuoteVolume: false, 37 | hasChange: true, 38 | hasChangePercent: true, 39 | hasBid: true, 40 | hasBidVolume: false, 41 | hasAsk: true, 42 | hasAskVolume: false, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | }, 48 | 49 | l2snapshot: { 50 | hasTimestampMs: false, 51 | hasSequenceId: false, 52 | hasCount: false, 53 | }, 54 | 55 | l2update: { 56 | hasSnapshot: true, 57 | hasTimestampMs: true, 58 | hasSequenceId: false, 59 | hasCount: false, 60 | }, 61 | 62 | l3update: { 63 | hasSnapshot: false, 64 | hasTimestampMs: true, 65 | hasSequenceId: true, 66 | orderIdPattern: /^[-a-f0-9]{36,36}$/, 67 | done: (spec, result, update) => { 68 | const point = update.asks[0] || update.bids[0]; 69 | switch (point.meta.type) { 70 | case "received": 71 | result.hasReceived = true; 72 | // if (point.meta.order_type === "limit") { 73 | // expect(parseFloat(point.price)).toBeGreaterThan(0); 74 | // expect(parseFloat(point.size)).toBeGreaterThan(0); 75 | // } 76 | break; 77 | case "open": 78 | result.hasOpen = true; 79 | // expect(parseFloat(point.price)).toBeGreaterThan(0); 80 | // expect(parseFloat(point.size)).toBeGreaterThan(0); 81 | // expect(parseFloat(point.meta.remaining_size)).toBeGreaterThanOrEqual(0); 82 | break; 83 | case "done": 84 | result.hasDone = true; 85 | // expect(point.meta.reason).toMatch(/filled|canceled/); 86 | break; 87 | case "match": 88 | result.hasMatch = true; 89 | // expect(parseFloat(point.price)).toBeGreaterThan(0); 90 | // expect(parseFloat(point.size)).toBeGreaterThan(0); 91 | // expect(point.meta.trade_id).toBeGreaterThan(0); 92 | // expect(point.meta.maker_order_id).toMatch(/^[a-f0-9]{32,32}$/); 93 | // expect(point.meta.taker_order_id).toMatch(/^[a-f0-9]{32,32}$/); 94 | break; 95 | } 96 | return result.hasReceived && result.hasOpen && result.hasDone && result.hasMatch; 97 | }, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /__tests__/exchanges/CoinexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { CoinexClient } from "../../src/exchanges/CoinexClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new CoinexClient(), 6 | clientName: "CoinexClient", 7 | exchangeName: "Coinex", 8 | markets: [ 9 | { 10 | id: "BTCUSDT", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "ETHBTC", 16 | base: "ETH", 17 | quote: "BTC", 18 | }, 19 | { 20 | id: "ETHUSDT", 21 | base: "ETH", 22 | quote: "USDT", 23 | }, 24 | ], 25 | 26 | getEventingSocket(client: any, market) { 27 | return client._clients.get(market.id).then(c => c._wss); 28 | }, 29 | 30 | testConnectEvents: true, 31 | testDisconnectEvents: true, 32 | testReconnectionEvents: true, 33 | testCloseEvents: true, 34 | 35 | hasTickers: true, 36 | hasTrades: true, 37 | hasCandles: false, 38 | hasLevel2Snapshots: false, 39 | hasLevel2Updates: true, 40 | hasLevel3Snapshots: false, 41 | hasLevel3Updates: false, 42 | 43 | ticker: { 44 | hasTimestamp: true, 45 | hasLast: true, 46 | hasOpen: true, 47 | hasHigh: true, 48 | hasLow: true, 49 | hasVolume: true, 50 | hasQuoteVolume: true, 51 | hasChange: true, 52 | hasChangePercent: true, 53 | hasBid: false, 54 | hasBidVolume: false, 55 | hasAsk: false, 56 | hasAskVolume: false, 57 | }, 58 | 59 | trade: { 60 | hasTradeId: true, 61 | }, 62 | 63 | l2snapshot: { 64 | hasTimestampMs: false, 65 | hasSequenceId: false, 66 | hasCount: false, 67 | }, 68 | 69 | l2update: { 70 | hasSnapshot: true, 71 | hasTimestampMs: false, 72 | hasSequenceId: false, 73 | hasCount: false, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/exchanges/DeribitClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { DeribitClient } from "../../src/exchanges/DeribitClient"; 3 | import * as https from "../../src/Https"; 4 | 5 | const assertions = { 6 | hasTickers: true, 7 | hasTrades: true, 8 | hasCandles: true, 9 | hasLevel2Snapshots: false, 10 | hasLevel2Updates: true, 11 | hasLevel3Snapshots: false, 12 | hasLevel3Updates: false, 13 | 14 | ticker: { 15 | hasTimestamp: true, 16 | hasLast: true, 17 | hasOpen: false, 18 | hasHigh: true, 19 | hasLow: true, 20 | hasVolume: true, 21 | hasQuoteVolume: false, 22 | hasChange: false, 23 | hasChangePercent: true, 24 | hasAsk: true, 25 | hasBid: true, 26 | hasAskVolume: true, 27 | hasBidVolume: true, 28 | }, 29 | 30 | trade: { 31 | hasTradeId: true, 32 | }, 33 | 34 | candle: {}, 35 | 36 | l2snapshot: { 37 | hasTimestampMs: true, 38 | hasSequenceId: true, 39 | hasCount: false, 40 | }, 41 | 42 | l2update: { 43 | hasSnapshot: true, 44 | hasSequenceId: true, 45 | hasTimestampMs: true, 46 | }, 47 | }; 48 | 49 | testClient({ 50 | clientFactory: () => new DeribitClient(), 51 | clientName: "DeribitClient - Swaps", 52 | exchangeName: "Deribit", 53 | markets: [ 54 | { 55 | id: "BTC-PERPETUAL", 56 | base: "BTC", 57 | quote: "USD", 58 | }, 59 | ], 60 | 61 | testConnectEvents: true, 62 | testDisconnectEvents: true, 63 | testReconnectionEvents: true, 64 | testCloseEvents: true, 65 | 66 | ...assertions, 67 | }); 68 | 69 | testClient({ 70 | clientFactory: () => new DeribitClient(), 71 | clientName: "DeribitClient - Futures", 72 | exchangeName: "Deribit", 73 | 74 | async fetchMarkets() { 75 | const res: any = await https.get( 76 | "https://www.deribit.com/api/v2/public/get_instruments?currency=BTC&expired=false&kind=future", 77 | ); 78 | return res.result.map(p => ({ 79 | id: p.instrument_name, 80 | base: p.base_currency, 81 | quote: "USD", 82 | type: "futures", 83 | })); 84 | }, 85 | 86 | ...assertions, 87 | }); 88 | 89 | testClient({ 90 | clientFactory: () => new DeribitClient(), 91 | clientName: "DeribitClient - Options", 92 | exchangeName: "Deribit", 93 | 94 | async fetchMarkets() { 95 | const res: any = await https.get( 96 | "https://www.deribit.com/api/v2/public/get_instruments?currency=BTC&expired=false&kind=option", 97 | ); 98 | return res.result 99 | .map(p => ({ 100 | id: p.instrument_name, 101 | base: p.base_currency, 102 | quote: "USD", 103 | type: "option", 104 | })) 105 | .slice(0, 10); 106 | }, 107 | 108 | async fetchTradeMarkets() { 109 | const res: any = await https.get( 110 | "https://www.deribit.com/api/v2/public/get_instruments?currency=BTC&expired=false&kind=option", 111 | ); 112 | return res.result.map(p => ({ 113 | id: p.instrument_name, 114 | base: p.base_currency, 115 | quote: "USD", 116 | type: "option", 117 | })); 118 | }, 119 | 120 | ...assertions, 121 | }); 122 | -------------------------------------------------------------------------------- /__tests__/exchanges/DigifinexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { DigifinexClient } from "../../src/exchanges/DigifinexClient"; 3 | import * as https from "../../src/Https"; 4 | 5 | testClient({ 6 | clientFactory: () => new DigifinexClient(), 7 | clientName: "DigifinexClient", 8 | exchangeName: "Digifinex", 9 | markets: [ 10 | { 11 | id: "btc_usdt", 12 | base: "BTC", 13 | quote: "USDT", 14 | }, 15 | { 16 | id: "eth_usdt", 17 | base: "ETH", 18 | quote: "USDT", 19 | }, 20 | ], 21 | 22 | async fetchAllMarkets() { 23 | const res: any = await https.get("https://openapi.digifinex.com/v3/markets"); 24 | return res.data.map(p => ({ 25 | id: p.market, 26 | base: p.market.split("_")[0], 27 | quote: p.market.split("_")[1], 28 | })); 29 | }, 30 | 31 | testConnectEvents: true, 32 | testDisconnectEvents: true, 33 | testReconnectionEvents: true, 34 | testCloseEvents: true, 35 | 36 | testAllMarketsTrades: true, 37 | testAllMarketsTradesSuccess: 20, 38 | 39 | hasTickers: true, 40 | hasTrades: true, 41 | hasCandles: false, 42 | hasLevel2Snapshots: false, 43 | hasLevel2Updates: true, 44 | hasLevel3Snapshots: false, 45 | hasLevel3Updates: false, 46 | 47 | ticker: { 48 | hasTimestamp: true, 49 | hasLast: true, 50 | hasOpen: true, 51 | hasHigh: true, 52 | hasLow: true, 53 | hasVolume: true, 54 | hasQuoteVolume: true, 55 | hasChange: true, 56 | hasChangePercent: true, 57 | hasAsk: true, 58 | hasBid: true, 59 | hasAskVolume: true, 60 | hasBidVolume: true, 61 | }, 62 | 63 | trade: { 64 | hasTradeId: true, 65 | tradeIdPattern: /[0-9]+/, 66 | }, 67 | 68 | l2update: { 69 | hasSnapshot: true, 70 | hasTimestampMs: false, 71 | hasSequenceId: false, 72 | hasCount: false, 73 | }, 74 | 75 | l2snapshot: { 76 | hasTimestampMs: false, 77 | hasSequenceId: false, 78 | hasCount: false, 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /__tests__/exchanges/FtxClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { FtxClient } from "../../src/exchanges/FtxClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new FtxClient(), 6 | clientName: "FtxClient", 7 | exchangeName: "FTX", 8 | markets: [ 9 | { 10 | id: "BTC/USD", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: false, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: false, 33 | hasHigh: false, 34 | hasLow: false, 35 | hasVolume: false, 36 | hasQuoteVolume: false, 37 | hasChange: false, 38 | hasChangePercent: false, 39 | hasAsk: true, 40 | hasBid: true, 41 | hasAskVolume: true, 42 | hasBidVolume: true, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | tradeIdPattern: /[0-9]+/, 48 | }, 49 | 50 | l2snapshot: { 51 | hasTimestampMs: true, 52 | hasSequenceId: false, 53 | hasCount: false, 54 | }, 55 | 56 | l2update: { 57 | hasSnapshot: true, 58 | hasTimestampMs: true, 59 | hasSequenceId: false, 60 | hasCount: false, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/exchanges/FtxUsClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { FtxUsClient } from "../../src/exchanges/FtxUsClient"; 3 | import * as https from "../../src/Https"; 4 | 5 | testClient({ 6 | clientFactory: () => new FtxUsClient(), 7 | clientName: "FtxUsClient", 8 | exchangeName: "FTX US", 9 | 10 | async fetchMarkets() { 11 | const res: any = await https.get("https://ftx.us/api/markets"); 12 | return res.result.map(p => ({ 13 | id: p.name, 14 | type: p.type, 15 | base: p.baseCurrency, 16 | quote: p.quoteCurrency, 17 | })); 18 | }, 19 | 20 | testConnectEvents: true, 21 | testDisconnectEvents: true, 22 | testReconnectionEvents: true, 23 | testCloseEvents: true, 24 | 25 | hasTickers: true, 26 | hasTrades: true, 27 | hasCandles: false, 28 | hasLevel2Snapshots: false, 29 | hasLevel2Updates: true, 30 | hasLevel3Snapshots: false, 31 | hasLevel3Updates: false, 32 | 33 | ticker: { 34 | hasTimestamp: true, 35 | hasLast: true, 36 | hasOpen: false, 37 | hasHigh: false, 38 | hasLow: false, 39 | hasVolume: false, 40 | hasQuoteVolume: false, 41 | hasChange: false, 42 | hasChangePercent: false, 43 | hasAsk: true, 44 | hasBid: true, 45 | hasAskVolume: true, 46 | hasBidVolume: true, 47 | }, 48 | 49 | trade: { 50 | hasTradeId: true, 51 | tradeIdPattern: /[0-9]+/, 52 | }, 53 | 54 | l2snapshot: { 55 | hasTimestampMs: true, 56 | hasSequenceId: false, 57 | hasCount: false, 58 | }, 59 | 60 | l2update: { 61 | hasSnapshot: true, 62 | hasTimestampMs: true, 63 | hasSequenceId: false, 64 | hasCount: false, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/exchanges/GateioClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { GateioClient } from "../../src/exchanges/GateioClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new GateioClient(), 6 | clientName: "GateioClient", 7 | exchangeName: "Gateio", 8 | markets: [ 9 | { 10 | id: "btc_usdt", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "eth_btc", 16 | base: "ETH", 17 | quote: "BTC", 18 | }, 19 | ], 20 | 21 | testConnectEvents: true, 22 | testDisconnectEvents: true, 23 | testReconnectionEvents: true, 24 | testCloseEvents: true, 25 | 26 | hasTickers: true, 27 | hasTrades: true, 28 | hasCandles: false, 29 | hasLevel2Snapshots: false, 30 | hasLevel2Updates: true, 31 | hasLevel3Snapshots: false, 32 | hasLevel3Updates: false, 33 | 34 | ticker: { 35 | hasTimestamp: true, 36 | hasLast: true, 37 | hasOpen: true, 38 | hasHigh: true, 39 | hasLow: true, 40 | hasVolume: true, 41 | hasQuoteVolume: true, 42 | hasChange: true, 43 | hasChangePercent: true, 44 | hasBid: false, 45 | hasBidVolume: false, 46 | hasAsk: false, 47 | hasAskVolume: false, 48 | }, 49 | 50 | trade: { 51 | hasTradeId: true, 52 | }, 53 | 54 | l2snapshot: { 55 | hasTimestampMs: false, 56 | hasSequenceId: false, 57 | hasCount: false, 58 | }, 59 | 60 | l2update: { 61 | hasSnapshot: true, 62 | hasTimestampMs: false, 63 | hasSequenceId: false, 64 | hasCount: false, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/exchanges/GeminiClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { GeminiClient } from "../../src/exchanges/Geminiclient"; 3 | 4 | testClient({ 5 | clientFactory: () => new GeminiClient(), 6 | clientName: "GeminiClient", 7 | exchangeName: "Gemini", 8 | markets: [ 9 | { 10 | id: "btcusd", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | { 15 | id: "ethusd", 16 | base: "ETH", 17 | quote: "USD", 18 | }, 19 | { 20 | id: "ltcusd", 21 | base: "LTC", 22 | quote: "USD", 23 | }, 24 | ], 25 | 26 | getEventingSocket(client, market) { 27 | return (client as any)._subscriptions.get(market.id).wss; 28 | }, 29 | 30 | testConnectEvents: true, 31 | testDisconnectEvents: true, 32 | testReconnectionEvents: true, 33 | testCloseEvents: true, 34 | 35 | hasTickers: true, 36 | hasTrades: true, 37 | hasCandles: false, 38 | hasLevel2Snapshots: false, 39 | hasLevel2Updates: true, 40 | hasLevel3Snapshots: false, 41 | hasLevel3Updates: false, 42 | 43 | trade: { 44 | hasTradeId: true, 45 | }, 46 | 47 | ticker: { 48 | hasTimestamp: true, 49 | hasLast: true, 50 | hasOpen: false, 51 | hasHigh: false, 52 | hasLow: false, 53 | hasVolume: false, 54 | hasQuoteVolume: false, 55 | hasChange: false, 56 | hasChangePercent: false, 57 | hasBid: true, 58 | hasBidVolume: false, 59 | hasAsk: true, 60 | hasAskVolume: false, 61 | }, 62 | 63 | l2snapshot: { 64 | hasTimestampMs: false, 65 | hasSequenceId: true, 66 | hasEventId: true, 67 | hasCount: false, 68 | }, 69 | 70 | l2update: { 71 | done: function (spec, result, update) { 72 | const hasAsks = update.asks && update.asks.length > 0; 73 | const hasBids = update.bids && update.bids.length > 0; 74 | return hasAsks || hasBids; 75 | }, 76 | hasSnapshot: true, 77 | hasTimestampMs: true, 78 | hasSequenceId: true, 79 | hasEventId: true, 80 | hasCount: false, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /__tests__/exchanges/HitBtcClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { HitBtcClient } from "../../src/exchanges/HitBtcClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | testClient({ 6 | clientFactory: () => new HitBtcClient(), 7 | clientName: "HitBTCClient", 8 | exchangeName: "HitBTC", 9 | markets: [ 10 | { 11 | id: "ETHBTC", 12 | base: "ETH", 13 | quote: "BTC", 14 | }, 15 | { 16 | id: "BTCUSDT", 17 | base: "BTC", 18 | quote: "USDT", 19 | }, 20 | ], 21 | 22 | fetchAllMarkets: async () => { 23 | const results: any = await get("https://api.hitbtc.com/api/2/public/symbol"); 24 | return results.map(p => ({ id: p.id, base: p.baseCurrency, quote: p.quoteCurrency })); 25 | }, 26 | 27 | testAllMarketsTrades: true, 28 | testAllMarketsTradesSuccess: 50, 29 | 30 | testConnectEvents: true, 31 | testDisconnectEvents: true, 32 | testReconnectionEvents: true, 33 | testCloseEvents: true, 34 | 35 | hasTickers: true, 36 | hasTrades: true, 37 | hasCandles: true, 38 | hasLevel2Snapshots: false, 39 | hasLevel2Updates: true, 40 | hasLevel3Snapshots: false, 41 | hasLevel3Updates: false, 42 | 43 | ticker: { 44 | hasTimestamp: true, 45 | hasLast: true, 46 | hasOpen: true, 47 | hasHigh: true, 48 | hasLow: true, 49 | hasVolume: true, 50 | hasQuoteVolume: true, 51 | hasChange: true, 52 | hasChangePercent: true, 53 | hasAsk: true, 54 | hasBid: true, 55 | hasAskVolume: false, 56 | hasBidVolume: false, 57 | }, 58 | 59 | trade: { 60 | hasTradeId: true, 61 | }, 62 | 63 | candle: {}, 64 | 65 | l2snapshot: { 66 | hasTimestampMs: false, 67 | hasSequenceId: true, 68 | hasCount: false, 69 | }, 70 | 71 | l2update: { 72 | hasSnapshot: true, 73 | hasTimestampMs: false, 74 | hasSequenceId: true, 75 | hasCount: false, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/exchanges/HuobiClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { HuobiClient } from "../../src/exchanges/HuobiClient"; 2 | import { testClient } from "../TestRunner"; 3 | 4 | testClient({ 5 | clientFactory: () => new HuobiClient(), 6 | clientName: "HuobiClient", 7 | exchangeName: "Huobi", 8 | markets: [ 9 | { 10 | id: "btcusdt", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: true, 24 | hasLevel2Snapshots: true, 25 | hasLevel2Updates: false, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: true, 33 | hasHigh: true, 34 | hasLow: true, 35 | hasVolume: true, 36 | hasQuoteVolume: true, 37 | hasChange: true, 38 | hasChangePercent: true, 39 | hasAsk: false, 40 | hasBid: false, 41 | hasAskVolume: false, 42 | hasBidVolume: false, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | tradeIdPattern: /[0-9]+/, 48 | }, 49 | 50 | candle: {}, 51 | 52 | l2snapshot: { 53 | hasTimestampMs: true, 54 | hasSequenceId: true, 55 | hasCount: false, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /__tests__/exchanges/HuobiFuturesClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { HuobiFuturesClient } from "../../src/exchanges/HuobiFuturesClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new HuobiFuturesClient(), 6 | clientName: "HuobiFuturesClient", 7 | exchangeName: "Huobi Futures", 8 | markets: [ 9 | { 10 | id: "BTC_CW", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | { 15 | id: "BTC_NW", 16 | base: "BTC", 17 | quote: "USD", 18 | }, 19 | { 20 | id: "BTC_CQ", 21 | base: "BTC", 22 | quote: "USD", 23 | }, 24 | { 25 | id: "BTC_NQ", 26 | base: "BTC", 27 | quote: "USD", 28 | }, 29 | ], 30 | 31 | testConnectEvents: true, 32 | testDisconnectEvents: true, 33 | testReconnectionEvents: true, 34 | testCloseEvents: true, 35 | 36 | hasTickers: true, 37 | hasTrades: true, 38 | hasCandles: true, 39 | hasLevel2Snapshots: true, 40 | hasLevel2Updates: true, 41 | hasLevel3Snapshots: false, 42 | hasLevel3Updates: false, 43 | 44 | ticker: { 45 | hasTimestamp: true, 46 | hasLast: true, 47 | hasOpen: true, 48 | hasHigh: true, 49 | hasLow: true, 50 | hasVolume: true, 51 | hasQuoteVolume: true, 52 | hasChange: true, 53 | hasChangePercent: true, 54 | hasAsk: false, 55 | hasBid: false, 56 | hasAskVolume: false, 57 | hasBidVolume: false, 58 | }, 59 | 60 | trade: { 61 | hasTradeId: true, 62 | tradeIdPattern: /[0-9]+/, 63 | }, 64 | 65 | candle: {}, 66 | 67 | l2update: { 68 | hasSnapshot: true, 69 | hasSequenceId: true, 70 | hasTimestampMs: true, 71 | }, 72 | 73 | l2snapshot: { 74 | hasTimestampMs: true, 75 | hasSequenceId: true, 76 | hasCount: false, 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/exchanges/HuobiJapanClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { HuobiJapanClient } from "../../src/exchanges/HuobiJapanClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new HuobiJapanClient(), 6 | clientName: "HuobiJapanClient", 7 | exchangeName: "Huobi Japan", 8 | markets: [ 9 | { 10 | id: "btcusdt", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "ethusdt", 16 | base: "ETH", 17 | quote: "USDT", 18 | }, 19 | { 20 | id: "ethbtc", 21 | base: "ETH", 22 | quote: "BTC", 23 | }, 24 | ], 25 | 26 | testConnectEvents: true, 27 | testDisconnectEvents: true, 28 | testReconnectionEvents: true, 29 | testCloseEvents: true, 30 | 31 | hasTickers: true, 32 | hasTrades: true, 33 | hasCandles: true, 34 | hasLevel2Snapshots: true, 35 | hasLevel2Updates: false, 36 | hasLevel3Snapshots: false, 37 | hasLevel3Updates: false, 38 | 39 | ticker: { 40 | hasTimestamp: true, 41 | hasLast: true, 42 | hasOpen: true, 43 | hasHigh: true, 44 | hasLow: true, 45 | hasVolume: true, 46 | hasQuoteVolume: true, 47 | hasChange: true, 48 | hasChangePercent: true, 49 | hasAsk: false, 50 | hasBid: false, 51 | hasAskVolume: false, 52 | hasBidVolume: false, 53 | }, 54 | 55 | trade: { 56 | hasTradeId: true, 57 | tradeIdPattern: /[0-9]+/, 58 | }, 59 | 60 | candle: {}, 61 | 62 | l2snapshot: { 63 | hasTimestampMs: true, 64 | hasSequenceId: true, 65 | hasCount: false, 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /__tests__/exchanges/HuobiKoreaClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { HuobiKoreaClient } from "../../src/exchanges/HuobiKoreaClient"; 2 | import { testClient } from "../TestRunner"; 3 | 4 | testClient({ 5 | clientFactory: () => new HuobiKoreaClient(), 6 | clientName: "HuobiKoreaClient", 7 | exchangeName: "Huobi Korea", 8 | markets: [ 9 | { 10 | id: "btcusdt", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "ethusdt", 16 | base: "ETH", 17 | quote: "USDT", 18 | }, 19 | { 20 | id: "ethbtc", 21 | base: "ETH", 22 | quote: "BTC", 23 | }, 24 | ], 25 | 26 | testConnectEvents: true, 27 | testDisconnectEvents: true, 28 | testReconnectionEvents: true, 29 | testCloseEvents: true, 30 | 31 | hasTickers: true, 32 | hasTrades: true, 33 | hasCandles: true, 34 | hasLevel2Snapshots: true, 35 | hasLevel2Updates: false, 36 | hasLevel3Snapshots: false, 37 | hasLevel3Updates: false, 38 | 39 | ticker: { 40 | hasTimestamp: true, 41 | hasLast: true, 42 | hasOpen: true, 43 | hasHigh: true, 44 | hasLow: true, 45 | hasVolume: true, 46 | hasQuoteVolume: true, 47 | hasChange: true, 48 | hasChangePercent: true, 49 | hasAsk: false, 50 | hasBid: false, 51 | hasAskVolume: false, 52 | hasBidVolume: false, 53 | }, 54 | 55 | trade: { 56 | hasTradeId: true, 57 | tradeIdPattern: /[0-9]+/, 58 | }, 59 | 60 | candle: {}, 61 | 62 | l2snapshot: { 63 | hasTimestampMs: true, 64 | hasSequenceId: true, 65 | hasCount: false, 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /__tests__/exchanges/HuobiSwapsClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { HuobiSwapsClient } from "../../src/exchanges/HuobiSwapsClient"; 2 | import { testClient } from "../TestRunner"; 3 | 4 | testClient({ 5 | clientFactory: () => new HuobiSwapsClient(), 6 | clientName: "HuobiSwapsClient", 7 | exchangeName: "Huobi Swaps", 8 | markets: [ 9 | { 10 | id: "BTC-USD", 11 | base: "BTC", 12 | quote: "USD", 13 | }, 14 | { 15 | id: "ETH-USD", 16 | base: "ETH", 17 | quote: "USD", 18 | }, 19 | ], 20 | 21 | testConnectEvents: true, 22 | testDisconnectEvents: true, 23 | testReconnectionEvents: true, 24 | testCloseEvents: true, 25 | 26 | hasTickers: true, 27 | hasTrades: true, 28 | hasCandles: true, 29 | hasLevel2Snapshots: true, 30 | hasLevel2Updates: true, 31 | hasLevel3Snapshots: false, 32 | hasLevel3Updates: false, 33 | 34 | ticker: { 35 | hasTimestamp: true, 36 | hasLast: true, 37 | hasOpen: true, 38 | hasHigh: true, 39 | hasLow: true, 40 | hasVolume: true, 41 | hasQuoteVolume: true, 42 | hasChange: true, 43 | hasChangePercent: true, 44 | hasAsk: false, 45 | hasBid: false, 46 | hasAskVolume: false, 47 | hasBidVolume: false, 48 | }, 49 | 50 | trade: { 51 | hasTradeId: true, 52 | tradeIdPattern: /[0-9]+/, 53 | }, 54 | 55 | candle: {}, 56 | 57 | l2update: { 58 | hasSnapshot: true, 59 | hasSequenceId: true, 60 | hasTimestampMs: true, 61 | }, 62 | 63 | l2snapshot: { 64 | hasTimestampMs: true, 65 | hasSequenceId: true, 66 | hasCount: false, 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/exchanges/KrakenClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { KrakenClient } from "../../src/exchanges/KrakenClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new KrakenClient(), 6 | clientName: "KrakenClient", 7 | exchangeName: "Kraken", 8 | markets: [ 9 | { 10 | id: "XXBTZEUR", 11 | base: "BTC", 12 | quote: "EUR", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: true, 24 | hasLevel2Snapshots: false, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: true, 33 | hasHigh: true, 34 | hasLow: true, 35 | hasVolume: true, 36 | hasQuoteVolume: true, 37 | hasChange: true, 38 | hasChangePercent: true, 39 | hasAsk: true, 40 | hasBid: true, 41 | hasAskVolume: true, 42 | hasBidVolume: true, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | tradeIdPattern: /\d{19,}/, 48 | }, 49 | 50 | candle: {}, 51 | 52 | l2snapshot: { 53 | hasTimestampMs: true, 54 | hasSequenceId: false, 55 | hasCount: false, 56 | }, 57 | 58 | l2update: { 59 | hasSnapshot: true, 60 | hasTimestampMs: true, 61 | hasSequenceId: false, 62 | hasCount: false, 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/exchanges/KucoinClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { KucoinClient } from "../../src/exchanges/KucoinClient"; 3 | import { testClient } from "../TestRunner"; 4 | 5 | testClient({ 6 | clientFactory: () => new KucoinClient(), 7 | clientName: "KucoinClient", 8 | exchangeName: "KuCoin", 9 | markets: [ 10 | { 11 | id: "BTC-USDT", 12 | base: "BTC", 13 | quote: "USDT", 14 | }, 15 | ], 16 | 17 | testConnectEvents: true, 18 | testDisconnectEvents: true, 19 | testReconnectionEvents: true, 20 | testCloseEvents: true, 21 | 22 | hasTickers: true, 23 | hasTrades: true, 24 | hasCandles: true, 25 | hasLevel2Snapshots: false, 26 | hasLevel2Updates: true, 27 | hasLevel3Snapshots: false, 28 | hasLevel3Updates: false, 29 | 30 | ticker: { 31 | hasTimestamp: true, 32 | hasLast: true, 33 | hasOpen: true, 34 | hasHigh: true, 35 | hasLow: true, 36 | hasVolume: true, 37 | hasQuoteVolume: false, 38 | hasChange: true, 39 | hasChangePercent: true, 40 | hasAsk: true, 41 | hasBid: true, 42 | hasAskVolume: false, 43 | hasBidVolume: false, 44 | }, 45 | 46 | trade: { 47 | hasTradeId: true, 48 | tradeIdPattern: /\w{24,}/, 49 | }, 50 | 51 | candle: {}, 52 | 53 | l2snapshot: { 54 | hasTimestampMs: false, 55 | hasSequenceId: true, 56 | hasCount: false, 57 | }, 58 | 59 | l2update: { 60 | hasSnapshot: true, 61 | hasTimestampMs: false, 62 | hasSequenceId: true, 63 | hasLastSequenceId: true, 64 | hasCount: false, 65 | }, 66 | 67 | l3update: { 68 | hasSnapshot: true, 69 | hasTimestampMs: true, 70 | hasSequenceId: true, 71 | orderIdPattern: /^[a-f0-9]{24,24}$/, 72 | done: (spec, result, update) => { 73 | const point = update.asks[0] || update.bids[0]; 74 | 75 | switch (point.meta.type) { 76 | case "received": 77 | if (!result.hasReceived) { 78 | result.hasReceived = true; 79 | expect(update.sequenceId).to.be.greaterThan(0); 80 | expect(update.timestampMs).to.be.greaterThan(1597679523725); 81 | expect(point.orderId).to.match(/^[a-f0-9]{24,24}/); 82 | expect(point.price).to.equal("0"); 83 | expect(point.size).to.equal("0"); 84 | expect(point.meta.ts).to.match(/[0-9]{19,}/); 85 | } 86 | break; 87 | case "open": 88 | if (!result.hasOpen) { 89 | result.hasOpen = true; 90 | expect(update.sequenceId).to.be.greaterThan(0); 91 | expect(update.timestampMs).to.be.greaterThan(1597679523725); 92 | expect(point.orderId).to.match(/^[a-f0-9]{24,24}/); 93 | expect(Number(point.price)).to.be.greaterThan(0); 94 | expect(Number(point.size)).to.be.greaterThan(0); 95 | expect(point.meta.ts).to.match(/[0-9]{19,}/); 96 | expect(point.meta.orderTime).to.match(/[0-9]{19,}/); 97 | } 98 | break; 99 | case "done": 100 | if (!result.hasDone) { 101 | result.hasDone = true; 102 | expect(update.sequenceId).to.be.greaterThan(0); 103 | expect(update.timestampMs).to.be.greaterThan(1597679523725); 104 | expect(point.orderId).to.match(/^[a-f0-9]{24,24}/); 105 | expect(point.price).to.equal("0"); 106 | expect(point.size).to.equal("0"); 107 | expect(point.meta.ts).to.match(/[0-9]{19,}/); 108 | expect(point.meta.reason).to.match(/filled|canceled/); 109 | } 110 | break; 111 | case "match": 112 | if (!result.hasMatch) { 113 | result.hasMatch = true; 114 | expect(update.sequenceId).to.be.greaterThan(0); 115 | expect(update.timestampMs).to.be.greaterThan(1597679523725); 116 | expect(point.orderId).to.match(/^[a-f0-9]{24,24}/); 117 | expect(point.price).to.equal("0"); 118 | expect(Number(point.size)).to.be.gte(0); 119 | expect(point.meta.ts).to.match(/[0-9]{19,}/); 120 | expect(point.meta.remainSize).to.not.be.undefined; 121 | expect(point.meta.takerOrderId).to.not.be.undefined; 122 | expect(point.meta.makerOrderId).to.not.be.undefined; 123 | expect(point.meta.tradeId).to.not.be.undefined; 124 | expect(Number((point as any).tradePrice)).to.be.gte(0); 125 | expect(Number((point as any).tradeSize)).to.be.gte(0); 126 | } 127 | 128 | break; 129 | case "update": 130 | if (!result.hasUpdate) { 131 | result.hasUpdate = true; 132 | expect(update.sequenceId).to.be.gt(0); 133 | expect(update.timestampMs).to.be.gt(1597679523725); 134 | expect(point.orderId).to.match(/^[a-f0-9]{24,24}/); 135 | expect(point.price).to.equal("0"); 136 | expect(Number(point.size)).to.be.gte(0); 137 | expect(point.meta.ts).to.match(/[0-9]{19,}/); 138 | } 139 | 140 | break; 141 | } 142 | return result.hasReceived && result.hasOpen && result.hasDone && result.hasMatch; 143 | }, 144 | }, 145 | 146 | l3snapshot: { 147 | hasTimestampMs: true, 148 | hasSequenceId: true, 149 | }, 150 | }); 151 | -------------------------------------------------------------------------------- /__tests__/exchanges/LiquidClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { LiquidClient } from "../../src/exchanges/LiquidClient"; 2 | import { testClient } from "../TestRunner"; 3 | 4 | testClient({ 5 | clientFactory: () => new LiquidClient(), 6 | clientName: "LiquidClient", 7 | exchangeName: "Liquid", 8 | markets: [ 9 | { 10 | id: "btcjpy", 11 | base: "BTC", 12 | quote: "JPY", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: false, 25 | hasLevel2Updates: true, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: true, 33 | hasHigh: false, 34 | hasLow: false, 35 | hasVolume: true, 36 | hasQuoteVolume: false, 37 | hasChange: true, 38 | hasChangePercent: true, 39 | hasAsk: true, 40 | hasBid: true, 41 | hasAskVolume: false, 42 | hasBidVolume: false, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | }, 48 | 49 | // l2snapshot: { 50 | // hasTimestampMs: true, 51 | // hasSequenceId: false, 52 | // hasCount: true, 53 | // }, 54 | 55 | l2update: { 56 | hasSnapshot: false, 57 | hasTimestampMs: false, 58 | hasSequenceId: false, 59 | hasCount: false, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /__tests__/exchanges/OkexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { OkexClient } from "../../src/exchanges/OkexClient"; 3 | import { get } from "../../src/Https"; 4 | 5 | const assertions = { 6 | hasTickers: true, 7 | hasTrades: true, 8 | hasCandles: true, 9 | hasLevel2Snapshots: true, 10 | hasLevel2Updates: true, 11 | hasLevel3Snapshots: false, 12 | hasLevel3Updates: false, 13 | 14 | ticker: { 15 | hasTimestamp: true, 16 | hasLast: true, 17 | hasOpen: true, 18 | hasHigh: true, 19 | hasLow: true, 20 | hasVolume: true, 21 | hasQuoteVolume: false, 22 | hasChange: true, 23 | hasChangePercent: true, 24 | hasAsk: true, 25 | hasBid: true, 26 | hasAskVolume: true, 27 | hasBidVolume: true, 28 | }, 29 | 30 | trade: { 31 | hasTradeId: true, 32 | }, 33 | 34 | candle: {}, 35 | 36 | l2snapshot: { 37 | hasTimestampMs: true, 38 | hasSequenceId: false, 39 | hasCount: true, 40 | hasChecksum: true, 41 | }, 42 | 43 | l2update: { 44 | hasSnapshot: true, 45 | hasTimestampMs: true, 46 | hasSequenceId: false, 47 | hasCount: true, 48 | hasChecksum: true, 49 | }, 50 | }; 51 | 52 | testClient({ 53 | clientFactory: () => new OkexClient(), 54 | exchangeName: "OKEx", 55 | clientName: "OKExClient - Spot", 56 | markets: [ 57 | { 58 | id: "BTC-USDT", 59 | base: "BTC", 60 | quote: "USDT", 61 | }, 62 | { 63 | id: "ETH-BTC", 64 | base: "ETH", 65 | quote: "BTC", 66 | }, 67 | ], 68 | 69 | testConnectEvents: true, 70 | testDisconnectEvents: true, 71 | testReconnectionEvents: true, 72 | testCloseEvents: true, 73 | 74 | ...assertions, 75 | }); 76 | 77 | testClient({ 78 | clientFactory: () => new OkexClient(), 79 | exchangeName: "OKEx", 80 | clientName: "OKExClient - Futures", 81 | fetchMarkets: async () => { 82 | const results: any = await get("https://www.okex.com/api/futures/v3/instruments"); 83 | return results 84 | .filter(p => p.base_currency === "BTC") 85 | .map(p => ({ 86 | id: p.instrument_id, 87 | base: p.base_currency, 88 | quote: p.quote_currency, 89 | type: "futures", 90 | })); 91 | }, 92 | ...assertions, 93 | }); 94 | 95 | testClient({ 96 | clientFactory: () => new OkexClient(), 97 | exchangeName: "OKEx", 98 | clientName: "OKExClient - Swap", 99 | fetchMarkets: async () => { 100 | const results: any = await get("https://www.okex.com/api/swap/v3/instruments"); 101 | return results 102 | .filter(p => ["BTC", "ETH", "LTC"].includes(p.base_currency)) 103 | .map(p => ({ 104 | id: p.instrument_id, 105 | base: p.base_currency, 106 | quote: p.quote_currency, 107 | type: "swap", 108 | })); 109 | }, 110 | ...assertions, 111 | }); 112 | 113 | testClient({ 114 | clientFactory: () => new OkexClient(), 115 | exchangeName: "OKEx", 116 | clientName: "OKExClient - Options", 117 | fetchMarkets: async () => { 118 | const results: any = await get("https://www.okex.com/api/option/v3/instruments/BTC-USD"); 119 | return results 120 | .map(p => ({ 121 | id: p.instrument_id, 122 | base: p.base_currency, 123 | quote: p.quote_currency, 124 | type: "option", 125 | })) 126 | .filter(p => p.id.endsWith("-C")) 127 | .slice(0, 20); 128 | }, 129 | ...assertions, 130 | }); 131 | -------------------------------------------------------------------------------- /__tests__/exchanges/PoloniexClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { PoloniexClient } from "../../src/exchanges/PoloniexClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new PoloniexClient(), 6 | clientName: "PoloniexClient", 7 | exchangeName: "Poloniex", 8 | markets: [ 9 | { 10 | id: "USDT_BTC", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | { 15 | id: "BTC_ETH", 16 | base: "ETH", 17 | quote: "BTC", 18 | }, 19 | { 20 | id: "USDT_ETH", 21 | base: "ETH", 22 | quote: "USDT", 23 | }, 24 | ], 25 | 26 | testConnectEvents: false, 27 | testDisconnectEvents: false, 28 | testReconnectionEvents: false, 29 | testCloseEvents: false, 30 | 31 | hasTickers: true, 32 | hasTrades: true, 33 | hasCandles: false, 34 | hasLevel2Snapshots: false, 35 | hasLevel2Updates: true, 36 | hasLevel3Snapshots: false, 37 | hasLevel3Updates: false, 38 | 39 | ticker: { 40 | hasTimestamp: true, 41 | hasLast: true, 42 | hasOpen: true, 43 | hasHigh: true, 44 | hasLow: true, 45 | hasVolume: true, 46 | hasQuoteVolume: true, 47 | hasChange: true, 48 | hasChangePercent: true, 49 | hasAsk: true, 50 | hasBid: true, 51 | hasAskVolume: false, 52 | hasBidVolume: false, 53 | }, 54 | 55 | trade: { 56 | hasTradeId: true, 57 | }, 58 | 59 | l2snapshot: { 60 | hasTimestampMs: false, 61 | hasSequenceId: true, 62 | hasCount: false, 63 | }, 64 | 65 | l2update: { 66 | hasSnapshot: true, 67 | hasTimestampMs: false, 68 | hasSequenceId: true, 69 | hasCount: false, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/exchanges/UpbitClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { UpbitClient } from "../../src/exchanges/UpbitClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new UpbitClient(), 6 | clientName: "UpbitClient", 7 | exchangeName: "Upbit", 8 | markets: [ 9 | { 10 | id: "KRW-BTC", 11 | base: "KRW", 12 | quote: "BTC", 13 | }, 14 | { 15 | id: "KRW-BTT", 16 | base: "KRW", 17 | quote: "BTT", 18 | }, 19 | ], 20 | 21 | testConnectEvents: true, 22 | testDisconnectEvents: true, 23 | testReconnectionEvents: true, 24 | testCloseEvents: true, 25 | 26 | hasTickers: true, 27 | hasTrades: true, 28 | hasCandles: false, 29 | hasLevel2Snapshots: true, 30 | hasLevel2Updates: false, 31 | hasLevel3Snapshots: false, 32 | hasLevel3Updates: false, 33 | 34 | ticker: { 35 | hasTimestamp: true, 36 | hasLast: true, 37 | hasOpen: true, 38 | hasHigh: true, 39 | hasLow: true, 40 | hasVolume: true, 41 | hasQuoteVolume: true, 42 | hasChange: true, 43 | hasChangePercent: true, 44 | hasAsk: false, 45 | hasBid: false, 46 | hasAskVolume: false, 47 | hasBidVolume: false, 48 | }, 49 | 50 | trade: { 51 | hasTradeId: true, 52 | }, 53 | 54 | l2snapshot: { 55 | hasTimestampMs: true, 56 | hasSequenceId: false, 57 | hasCount: false, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /__tests__/exchanges/ZbClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { testClient } from "../TestRunner"; 2 | import { ZbClient } from "../../src/exchanges/ZbClient"; 3 | 4 | testClient({ 5 | clientFactory: () => new ZbClient(), 6 | clientName: "ZbClient", 7 | exchangeName: "ZB", 8 | markets: [ 9 | { 10 | id: "btc_usdt", 11 | base: "BTC", 12 | quote: "USDT", 13 | }, 14 | ], 15 | 16 | testConnectEvents: true, 17 | testDisconnectEvents: true, 18 | testReconnectionEvents: true, 19 | testCloseEvents: true, 20 | 21 | hasTickers: true, 22 | hasTrades: true, 23 | hasCandles: false, 24 | hasLevel2Snapshots: true, 25 | hasLevel2Updates: false, 26 | hasLevel3Snapshots: false, 27 | hasLevel3Updates: false, 28 | 29 | ticker: { 30 | hasTimestamp: true, 31 | hasLast: true, 32 | hasOpen: false, 33 | hasHigh: true, 34 | hasLow: true, 35 | hasVolume: true, 36 | hasQuoteVolume: false, 37 | hasChange: false, 38 | hasChangePercent: false, 39 | hasAsk: true, 40 | hasBid: true, 41 | hasAskVolume: false, 42 | hasBidVolume: false, 43 | }, 44 | 45 | trade: { 46 | hasTradeId: true, 47 | tradeIdPattern: /[0-9]+/, 48 | }, 49 | 50 | l2snapshot: { 51 | hasTimestampMs: true, 52 | hasSequenceId: false, 53 | hasCount: false, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /__tests__/flowcontrol/Batch.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import sinon from "sinon"; 3 | import { batch } from "../../src/flowcontrol/Batch"; 4 | 5 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | describe("batch", () => { 8 | describe("small batch size", () => { 9 | let fn; 10 | let sut; 11 | 12 | beforeEach(() => { 13 | const batchSize = 2; 14 | const delayMs = 50; 15 | fn = sinon.stub(); 16 | sut = batch(fn, batchSize, delayMs); 17 | }); 18 | 19 | it("groups calls within timeout period", async () => { 20 | sut(1); 21 | await wait(10); 22 | 23 | sut(2); 24 | await wait(10); 25 | 26 | sut(3); 27 | await wait(100); 28 | 29 | expect(fn.callCount).to.equal(2); 30 | expect(fn.args[0][0]).to.deep.equal([[1], [2]]); 31 | expect(fn.args[1][0]).to.deep.equal([[3]]); 32 | }); 33 | 34 | it("groups calls within debounce periods", async () => { 35 | sut(1); 36 | await wait(100); 37 | 38 | sut(2); 39 | await wait(100); 40 | 41 | sut(3); 42 | await wait(100); 43 | 44 | expect(fn.callCount).to.equal(3); 45 | expect(fn.args[0][0]).to.deep.equal([[1]]); 46 | expect(fn.args[1][0]).to.deep.equal([[2]]); 47 | expect(fn.args[2][0]).to.deep.equal([[3]]); 48 | }); 49 | 50 | it("can reset pending executions", async () => { 51 | sut(1); 52 | sut.cancel(); 53 | 54 | await wait(100); 55 | expect(fn.callCount).to.equal(0); 56 | }); 57 | }); 58 | 59 | describe("large batch size", () => { 60 | let fn; 61 | let sut; 62 | 63 | beforeEach(() => { 64 | const batchSize = 100; 65 | const delayMs = 50; 66 | fn = sinon.stub(); 67 | sut = batch(fn, batchSize, delayMs); 68 | }); 69 | 70 | it("groups calls within timeout period", async () => { 71 | sut(1); 72 | await wait(10); 73 | 74 | sut(2); 75 | await wait(10); 76 | 77 | sut(3); 78 | await wait(100); 79 | 80 | expect(fn.callCount).to.equal(1); 81 | expect(fn.args[0][0]).to.deep.equal([[1], [2], [3]]); 82 | }); 83 | 84 | it("groups calls within debounce periods", async () => { 85 | sut(1); 86 | await wait(100); 87 | 88 | sut(2); 89 | await wait(100); 90 | 91 | sut(3); 92 | await wait(100); 93 | 94 | expect(fn.callCount).to.equal(3); 95 | expect(fn.args[0][0]).to.deep.equal([[1]]); 96 | expect(fn.args[1][0]).to.deep.equal([[2]]); 97 | expect(fn.args[2][0]).to.deep.equal([[3]]); 98 | }); 99 | 100 | it("can reset pending executions", async () => { 101 | sut(1); 102 | sut.cancel(); 103 | 104 | await wait(100); 105 | expect(fn.callCount).to.equal(0); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /__tests__/flowcontrol/CircularBuffer.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-sparse-arrays */ 2 | import { CircularBuffer } from "../../src/flowcontrol/CircularBuffer"; 3 | import { expect } from "chai"; 4 | 5 | describe("CircularBuffer", () => { 6 | it("empty read returns undefined", () => { 7 | const sut = new CircularBuffer(4); 8 | expect(sut.read()).to.be.undefined; 9 | }); 10 | 11 | it("empty read after values", () => { 12 | const sut = new CircularBuffer(4); 13 | sut.write(1); 14 | expect(sut.read()).to.equal(1); 15 | expect(sut.read()).to.be.undefined; 16 | }); 17 | 18 | it("multi enq/deq", () => { 19 | const sut = new CircularBuffer(4); 20 | sut.write(0); 21 | sut.write(1); 22 | expect(sut.read()).to.equal(0); 23 | expect(sut.read()).to.equal(1); 24 | }); 25 | 26 | it("multi enq/deq max", () => { 27 | const sut = new CircularBuffer(4); 28 | sut.write(0); 29 | sut.write(1); 30 | sut.write(2); 31 | expect(sut.read()).to.equal(0); 32 | expect(sut.read()).to.equal(1); 33 | expect(sut.read()).to.equal(2); 34 | }); 35 | 36 | it("multi enq/deq repeatedly", () => { 37 | const sut = new CircularBuffer(4); 38 | for (let i = 0; i < 1024; i++) { 39 | sut.write(0); 40 | sut.write(1); 41 | expect(sut.read()).to.equal(0); 42 | expect(sut.read()).to.equal(1); 43 | } 44 | }); 45 | 46 | it("cycle 1", () => { 47 | const sut = new CircularBuffer(4); 48 | expect(sut.write(0)).to.be.true; 49 | expect(sut.write(1)).to.be.true; 50 | expect(sut.write(2)).to.be.true; 51 | expect(sut.write(3)).to.be.false; 52 | expect(sut.buffer).to.deep.equal([undefined, 0, 1, 2]); 53 | }); 54 | 55 | it("cycle 2", () => { 56 | const sut = new CircularBuffer(4); 57 | expect(sut.write(0)).to.be.true; 58 | expect(sut.write(1)).to.be.true; 59 | expect(sut.write(2)).to.be.true; 60 | 61 | sut.read(); 62 | expect(sut.write(3)).to.be.true; 63 | expect(sut.buffer).to.deep.equal([3, undefined, 1, 2]); 64 | 65 | expect(sut.write(4)).to.be.false; 66 | }); 67 | 68 | it("cycle 3", () => { 69 | const sut = new CircularBuffer(4); 70 | expect(sut.write(0)).to.be.true; 71 | expect(sut.write(1)).to.be.true; 72 | expect(sut.write(2)).to.be.true; 73 | 74 | sut.read(); 75 | expect(sut.write(3)).to.be.true; 76 | expect(sut.buffer).to.deep.equal([3, undefined, 1, 2]); 77 | 78 | sut.read(); 79 | expect(sut.write(4)).to.be.true; 80 | expect(sut.buffer).to.deep.equal([3, 4, undefined, 2]); 81 | 82 | expect(sut.write(5)).to.be.false; 83 | }); 84 | 85 | it("cycle 4", () => { 86 | const sut = new CircularBuffer(4); 87 | expect(sut.write(0)).to.be.true; 88 | expect(sut.write(1)).to.be.true; 89 | expect(sut.write(2)).to.be.true; 90 | 91 | sut.read(); 92 | expect(sut.write(3)).to.be.true; 93 | expect(sut.buffer).to.deep.equal([3, undefined, 1, 2]); 94 | 95 | sut.read(); 96 | expect(sut.write(4)).to.be.true; 97 | expect(sut.buffer).to.deep.equal([3, 4, undefined, 2]); 98 | 99 | sut.read(); 100 | expect(sut.write(5)).to.be.true; 101 | expect(sut.buffer).to.deep.equal([3, 4, 5, undefined]); 102 | 103 | expect(sut.write(6)).to.be.false; 104 | }); 105 | 106 | it("cycle 5", () => { 107 | const sut = new CircularBuffer(4); 108 | expect(sut.write(0)).to.be.true; 109 | expect(sut.write(1)).to.be.true; 110 | expect(sut.write(2)).to.be.true; 111 | 112 | sut.read(); 113 | expect(sut.write(3)).to.be.true; 114 | expect(sut.buffer).to.deep.equal([3, undefined, 1, 2]); 115 | 116 | sut.read(); 117 | expect(sut.write(4)).to.be.true; 118 | expect(sut.buffer).to.deep.equal([3, 4, undefined, 2]); 119 | 120 | sut.read(); 121 | expect(sut.write(5)).to.be.true; 122 | expect(sut.buffer).to.deep.equal([3, 4, 5, undefined]); 123 | 124 | sut.read(); 125 | expect(sut.write(6)).to.be.true; 126 | expect(sut.buffer).to.deep.equal([undefined, 4, 5, 6]); 127 | 128 | expect(sut.write(7)).to.be.false; 129 | }); 130 | 131 | it("fills and empties", () => { 132 | const sut = new CircularBuffer(4); 133 | expect(sut.write(0)).to.be.true; 134 | expect(sut.write(1)).to.be.true; 135 | expect(sut.write(2)).to.be.true; 136 | expect(sut.read()).to.equal(0); 137 | expect(sut.read()).to.equal(1); 138 | expect(sut.read()).to.equal(2); 139 | 140 | expect(sut.write(3)).to.be.true; 141 | expect(sut.write(4)).to.be.true; 142 | expect(sut.read()).to.equal(3); 143 | expect(sut.read()).to.equal(4); 144 | 145 | expect(sut.write(5)).to.be.true; 146 | expect(sut.read()).to.equal(5); 147 | 148 | expect(sut.write(6)).to.be.true; 149 | expect(sut.read()).to.equal(6); 150 | 151 | expect(sut.write(7)).to.be.true; 152 | expect(sut.read()).to.equal(7); 153 | 154 | expect(sut.write(8)).to.be.true; 155 | expect(sut.read()).to.equal(8); 156 | }); 157 | 158 | it("full cycles", () => { 159 | const sut = new CircularBuffer(4); 160 | sut.write("a"); 161 | sut.write("b"); 162 | sut.write("c"); 163 | 164 | for (let i = 0; i < 1000; i++) { 165 | const a = sut.read(); 166 | const b = sut.read(); 167 | const c = sut.read(); 168 | expect(a).to.equal("a"); 169 | expect(b).to.equal("b"); 170 | expect(c).to.equal("c"); 171 | sut.write(a); 172 | sut.write(b); 173 | sut.write(c); 174 | expect(sut.write("nope")).to.be.false; 175 | } 176 | }); 177 | 178 | it("partial cycles", () => { 179 | const sut = new CircularBuffer(8); 180 | sut.write("a"); 181 | sut.write("b"); 182 | sut.write("c"); 183 | 184 | for (let i = 0; i < 10000; i++) { 185 | const a = sut.read(); 186 | const b = sut.read(); 187 | const c = sut.read(); 188 | expect(a).to.equal("a"); 189 | expect(b).to.equal("b"); 190 | expect(c).to.equal("c"); 191 | sut.write(a); 192 | sut.write(b); 193 | sut.write(c); 194 | } 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /__tests__/flowcontrol/Debounce.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import sinon from "sinon"; 3 | import { debounce } from "../../src/flowcontrol/Debounce"; 4 | 5 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | describe("debounce", () => { 8 | let fn; 9 | let sut; 10 | 11 | beforeEach(() => { 12 | const debounceMs = 50; 13 | fn = sinon.stub(); 14 | sut = debounce(fn, debounceMs); 15 | }); 16 | 17 | it("groups calls within timeout period", async () => { 18 | sut(1); 19 | await wait(10); 20 | 21 | sut(2); 22 | await wait(10); 23 | 24 | sut(3); 25 | await wait(100); 26 | 27 | expect(fn.callCount).to.equal(1); 28 | expect(fn.args[0][0]).to.deep.equal(3); 29 | }); 30 | 31 | it("groups calls within debounce periods", async () => { 32 | sut(1); 33 | await wait(100); 34 | 35 | sut(2); 36 | await wait(100); 37 | 38 | sut(3); 39 | await wait(100); 40 | 41 | expect(fn.callCount).to.equal(3); 42 | expect(fn.args[0][0]).to.deep.equal(1); 43 | expect(fn.args[1][0]).to.deep.equal(2); 44 | expect(fn.args[2][0]).to.deep.equal(3); 45 | }); 46 | 47 | it("can cancel pending executions", async () => { 48 | sut(1); 49 | sut.cancel(); 50 | 51 | await wait(100); 52 | expect(fn.callCount).to.equal(0); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/flowcontrol/Queue.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-sparse-arrays */ 2 | import { Queue } from "../../src/flowcontrol/Queue"; 3 | import { expect } from "chai"; 4 | 5 | describe("Queue", () => { 6 | it("empty returns undefined", () => { 7 | const sut = new Queue(4); 8 | expect(sut.shift()).to.be.undefined; 9 | }); 10 | 11 | it("pushes and shifts", () => { 12 | const sut = new Queue(4); 13 | sut.push(0); 14 | sut.push(1); 15 | expect(sut.shift()).to.equal(0); 16 | expect(sut.shift()).to.equal(1); 17 | }); 18 | 19 | it("pushes and shifts without resize", () => { 20 | const sut = new Queue(4); 21 | sut.push(0); 22 | sut.push(1); 23 | sut.push(2); 24 | expect(sut.buffer.buffer).to.deep.equal([undefined, 0, 1, 2]); 25 | expect(sut.shift()).to.equal(0); 26 | expect(sut.shift()).to.equal(1); 27 | expect(sut.shift()).to.equal(2); 28 | }); 29 | 30 | for (let iter = 0; iter < 3; iter++) { 31 | it(`resize iteration ${iter}`, () => { 32 | const sut = new Queue(4); 33 | for (let i = 0; i < iter; i++) { 34 | sut.push(0); 35 | sut.shift(); 36 | } 37 | 38 | sut.push(1); 39 | sut.push(2); 40 | sut.push(3); 41 | sut.push(4); // causes resize 42 | 43 | expect(sut.buffer.buffer).to.deep.equal([ 44 | undefined, 45 | 1, 46 | 2, 47 | 3, 48 | 4, 49 | undefined, 50 | undefined, 51 | undefined, 52 | ]); 53 | }); 54 | } 55 | 56 | it("resize many", () => { 57 | const sut = new Queue(2); 58 | for (let i = 0; i < 1024; i++) { 59 | sut.push(i); 60 | } 61 | for (let i = 0; i < 1024; i++) { 62 | expect(sut.shift()).to.equal(i); 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/flowcontrol/Throttle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import sinon from "sinon"; 3 | import { throttle } from "../../src/flowcontrol/Throttle"; 4 | 5 | const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | describe("throttle", () => { 8 | it("all at once", async () => { 9 | const fn = sinon.stub(); 10 | const sut = throttle(fn, 10); 11 | 12 | sut(1); 13 | sut(2); 14 | sut(3); 15 | 16 | expect(fn.callCount).to.equal(1); 17 | 18 | await wait(200); 19 | 20 | expect(fn.callCount).to.equal(3); 21 | expect(fn.args[0][0]).to.equal(1); 22 | expect(fn.args[1][0]).to.equal(2); 23 | expect(fn.args[2][0]).to.equal(3); 24 | }); 25 | 26 | it("delayed", async () => { 27 | const fn = sinon.stub(); 28 | const sut = throttle(fn, 100); 29 | 30 | sut(1); 31 | expect(fn.callCount).to.equal(1); 32 | await wait(10); 33 | 34 | sut(2); 35 | expect(fn.callCount).to.equal(1); 36 | await wait(100); 37 | 38 | sut(3); 39 | await wait(300); 40 | 41 | expect(fn.callCount).to.equal(3); 42 | expect(fn.args[0][0]).to.equal(1); 43 | expect(fn.args[1][0]).to.equal(2); 44 | expect(fn.args[2][0]).to.equal(3); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccxws", 3 | "version": "0.47.0", 4 | "description": "Websocket client for 37 cryptocurrency exchanges", 5 | "keywords": [ 6 | "bitmex", 7 | "binance", 8 | "coinbase", 9 | "cryptocurrency", 10 | "exchange", 11 | "websocket", 12 | "realtime" 13 | ], 14 | "author": "Brian Mancini ", 15 | "license": "MIT", 16 | "main": "dist/src/index.js", 17 | "scripts": { 18 | "prepublish": "npm run build", 19 | "build": "tsc", 20 | "format:fix": "prettier --write \"src/**\" \"__tests__/**\"", 21 | "format": "prettier --check \"src/**\" \"__tests__/**\"", 22 | "lint": "eslint src __tests__", 23 | "test": "nyc --reporter=lcov --reporter=text --extension=.ts mocha --require ts-node/register --recursive \"__tests__/**/*.spec.ts\"" 24 | }, 25 | "repository": "altangent/ccxws", 26 | "dependencies": { 27 | "crc": "^3.8.0", 28 | "decimal.js": "^10.2.0", 29 | "moment": "^2.26.0", 30 | "pusher-js": "^4.4.0", 31 | "semaphore": "^1.1.0", 32 | "ws": "^7.3.0" 33 | }, 34 | "devDependencies": { 35 | "@types/chai": "^4.2.18", 36 | "@types/mocha": "^8.2.2", 37 | "@types/node": "^15.6.0", 38 | "@types/semaphore": "^1.1.1", 39 | "@types/sinon": "^10.0.0", 40 | "@types/ws": "^7.4.4", 41 | "@typescript-eslint/eslint-plugin": "^4.24.0", 42 | "@typescript-eslint/parser": "^4.24.0", 43 | "chai": "^4.3.4", 44 | "eslint": "^7.27.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-import": "^2.23.3", 47 | "eslint-plugin-prefer-arrow": "^1.2.3", 48 | "mocha": "^8.4.0", 49 | "nyc": "^15.1.0", 50 | "prettier": "^2.3.0", 51 | "sinon": "^10.0.0", 52 | "ts-node": "^10.0.0", 53 | "typescript": "^4.2.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Auction.ts: -------------------------------------------------------------------------------- 1 | export class Auction { 2 | public exchange: string; 3 | public quote: string; 4 | public base: string; 5 | public tradeId: string; 6 | public unix: number; 7 | public price: string; 8 | public high: string; 9 | public low: string; 10 | public amount: string; 11 | 12 | constructor({ 13 | exchange, 14 | base, 15 | quote, 16 | tradeId, 17 | unix, 18 | price, 19 | amount, 20 | high, 21 | low, 22 | }: Partial) { 23 | this.exchange = exchange; 24 | this.quote = quote; 25 | this.base = base; 26 | this.tradeId = tradeId; 27 | this.unix = unix; 28 | this.price = price; 29 | this.high = high; 30 | this.low = low; 31 | this.amount = amount; 32 | } 33 | 34 | public get marketId() { 35 | return `${this.base}/${this.quote}`; 36 | } 37 | 38 | /** 39 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 40 | */ 41 | public get fullId() { 42 | return `${this.exchange}:${this.base}/${this.quote}`; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/BasicMultiClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/member-ordering */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-misused-promises */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 5 | /* eslint-disable @typescript-eslint/no-floating-promises */ 6 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 7 | 8 | import { EventEmitter } from "events"; 9 | import semaphore from "semaphore"; 10 | import { Market } from "./Market"; 11 | import { IClient } from "./IClient"; 12 | import { SubscriptionType } from "./SubscriptionType"; 13 | import { wait } from "./Util"; 14 | import { NotImplementedFn } from "./NotImplementedFn"; 15 | 16 | export abstract class BasicMultiClient extends EventEmitter { 17 | public name: string; 18 | public hasTickers: boolean; 19 | public hasTrades: boolean; 20 | public hasCandles: boolean; 21 | public hasLevel2Snapshots: boolean; 22 | public hasLevel2Updates: boolean; 23 | public hasLevel3Snapshots: boolean; 24 | public hasLevel3Updates: boolean; 25 | public throttleMs: number; 26 | public sem: semaphore.Semaphore; 27 | public auth: any; 28 | 29 | protected _clients: Map>; 30 | 31 | constructor() { 32 | super(); 33 | this._clients = new Map(); 34 | 35 | this.hasTickers = false; 36 | this.hasTrades = false; 37 | this.hasCandles = false; 38 | this.hasLevel2Snapshots = false; 39 | this.hasLevel2Updates = false; 40 | this.hasLevel3Snapshots = false; 41 | this.hasLevel3Updates = false; 42 | this.throttleMs = 250; 43 | this.sem = semaphore(3); // this can be overriden to allow more or less 44 | } 45 | 46 | public async reconnect() { 47 | for (const client of Array.from(this._clients.values())) { 48 | (await client).reconnect(); 49 | await wait(this.throttleMs); // delay the reconnection throttling 50 | } 51 | } 52 | 53 | public async close(): Promise { 54 | for (const client of Array.from(this._clients.values())) { 55 | (await client).close(); 56 | } 57 | } 58 | 59 | ////// ABSTRACT 60 | protected abstract _createBasicClient(clientArgs: any): IClient; 61 | 62 | ////// PUBLIC 63 | 64 | public subscribeTicker(market: Market) { 65 | if (!this.hasTickers) return; 66 | this._subscribe(market, this._clients, SubscriptionType.ticker); 67 | } 68 | 69 | public async unsubscribeTicker(market: Market) { 70 | if (!this.hasTickers) return; 71 | if (this._clients.has(market.id)) { 72 | const client = await this._clients.get(market.id); 73 | client.unsubscribeTicker(market); 74 | } 75 | } 76 | 77 | public subscribeCandles(market: Market) { 78 | if (!this.hasCandles) return; 79 | this._subscribe(market, this._clients, SubscriptionType.candle); 80 | } 81 | 82 | public async unsubscribeCandles(market: Market) { 83 | if (!this.hasCandles) return; 84 | if (this._clients.has(market.id)) { 85 | const client = await this._clients.get(market.id); 86 | client.unsubscribeCandles(market); 87 | } 88 | } 89 | 90 | public subscribeTrades(market) { 91 | if (!this.hasTrades) return; 92 | this._subscribe(market, this._clients, SubscriptionType.trade); 93 | } 94 | 95 | public async unsubscribeTrades(market: Market) { 96 | if (!this.hasTrades) return; 97 | if (this._clients.has(market.id)) { 98 | const client = await this._clients.get(market.id); 99 | client.unsubscribeTrades(market); 100 | } 101 | } 102 | 103 | public subscribeLevel2Updates(market: Market) { 104 | if (!this.hasLevel2Updates) return; 105 | this._subscribe(market, this._clients, SubscriptionType.level2update); 106 | } 107 | 108 | public async unsubscribeLevel2Updates(market: Market) { 109 | if (!this.hasLevel2Updates) return; 110 | if (this._clients.has(market.id)) { 111 | const client = await this._clients.get(market.id); 112 | client.unsubscribeLevel2Updates(market); 113 | } 114 | } 115 | 116 | public subscribeLevel2Snapshots(market: Market) { 117 | if (!this.hasLevel2Snapshots) return; 118 | this._subscribe(market, this._clients, SubscriptionType.level2snapshot); 119 | } 120 | 121 | public async unsubscribeLevel2Snapshots(market: Market) { 122 | if (!this.hasLevel2Snapshots) return; 123 | if (this._clients.has(market.id)) { 124 | const client = await this._clients.get(market.id); 125 | client.unsubscribeLevel2Snapshots(market); 126 | } 127 | } 128 | 129 | public subscribeLevel3Snapshots = NotImplementedFn; 130 | public unsubscribeLevel3Snapshots = NotImplementedFn; 131 | public subscribeLevel3Updates = NotImplementedFn; 132 | public unsubscribeLevel3Updates = NotImplementedFn; 133 | 134 | ////// PROTECTED 135 | 136 | protected _createBasicClientThrottled(clientArgs: any) { 137 | return new Promise(resolve => { 138 | this.sem.take(() => { 139 | const client: any = this._createBasicClient(clientArgs); 140 | client.on("connecting", () => this.emit("connecting", clientArgs.market)); 141 | client.on("connected", () => this.emit("connected", clientArgs.market)); 142 | client.on("disconnected", () => this.emit("disconnected", clientArgs.market)); 143 | client.on("reconnecting", () => this.emit("reconnecting", clientArgs.market)); 144 | client.on("closing", () => this.emit("closing", clientArgs.market)); 145 | client.on("closed", () => this.emit("closed", clientArgs.market)); 146 | client.on("error", err => this.emit("error", err, clientArgs.market)); 147 | const clearSem = async () => { 148 | await wait(this.throttleMs); 149 | this.sem.leave(); 150 | resolve(client); 151 | }; 152 | client.once("connected", clearSem); 153 | (client as any)._connect(); 154 | }); 155 | }); 156 | } 157 | 158 | protected async _subscribe( 159 | market: Market, 160 | map: Map>, 161 | subscriptionType: SubscriptionType, 162 | ) { 163 | try { 164 | const remote_id = market.id; 165 | let client = null; 166 | 167 | // construct a client 168 | if (!map.has(remote_id)) { 169 | const clientArgs = { auth: this.auth, market: market }; 170 | client = this._createBasicClientThrottled(clientArgs); 171 | // we MUST store the promise in here otherwise we will stack up duplicates 172 | map.set(remote_id, client); 173 | } 174 | 175 | // wait for client to be made! 176 | client = await map.get(remote_id); 177 | 178 | if (subscriptionType === SubscriptionType.ticker) { 179 | const subscribed = client.subscribeTicker(market); 180 | if (subscribed) { 181 | client.on("ticker", (ticker, market) => { 182 | this.emit("ticker", ticker, market); 183 | }); 184 | } 185 | } 186 | 187 | if (subscriptionType === SubscriptionType.candle) { 188 | const subscribed = client.subscribeCandles(market); 189 | if (subscribed) { 190 | client.on("candle", (candle, market) => { 191 | this.emit("candle", candle, market); 192 | }); 193 | } 194 | } 195 | 196 | if (subscriptionType === SubscriptionType.trade) { 197 | const subscribed = client.subscribeTrades(market); 198 | if (subscribed) { 199 | client.on("trade", (trade, market) => { 200 | this.emit("trade", trade, market); 201 | }); 202 | } 203 | } 204 | 205 | if (subscriptionType === SubscriptionType.level2update) { 206 | const subscribed = client.subscribeLevel2Updates(market); 207 | if (subscribed) { 208 | client.on("l2update", (l2update, market) => { 209 | this.emit("l2update", l2update, market); 210 | }); 211 | client.on("l2snapshot", (l2snapshot, market) => { 212 | this.emit("l2snapshot", l2snapshot, market); 213 | }); 214 | } 215 | } 216 | 217 | if (subscriptionType === SubscriptionType.level2snapshot) { 218 | const subscribed = client.subscribeLevel2Snapshots(market); 219 | if (subscribed) { 220 | client.on("l2snapshot", (l2snapshot, market) => { 221 | this.emit("l2snapshot", l2snapshot, market); 222 | }); 223 | } 224 | } 225 | } catch (ex) { 226 | this.emit("error", ex, market); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/BlockTrade.ts: -------------------------------------------------------------------------------- 1 | export class BlockTrade { 2 | public exchange: string; 3 | public quote: string; 4 | public base: string; 5 | public tradeId: string; 6 | public unix: string; 7 | public price: string; 8 | public amount: string; 9 | 10 | constructor({ exchange, base, quote, tradeId, unix, price, amount }: Partial) { 11 | this.exchange = exchange; 12 | this.quote = quote; 13 | this.base = base; 14 | this.tradeId = tradeId; 15 | this.unix = unix; 16 | this.price = price; 17 | this.amount = amount; 18 | } 19 | 20 | public get marketId() { 21 | return `${this.base}/${this.quote}`; 22 | } 23 | 24 | /** 25 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 26 | */ 27 | public get fullId() { 28 | return `${this.exchange}:${this.base}/${this.quote}`; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Candle.ts: -------------------------------------------------------------------------------- 1 | export class Candle { 2 | constructor( 3 | readonly timestampMs: number, 4 | readonly open: string, 5 | readonly high: string, 6 | readonly low: string, 7 | readonly close: string, 8 | readonly volume: string, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/CandlePeriod.ts: -------------------------------------------------------------------------------- 1 | export enum CandlePeriod { 2 | _1m = "_1m", 3 | _2m = "_2m", 4 | _3m = "_3m", 5 | _5m = "_5m", 6 | _10m = "_10m", 7 | _15m = "_15m", 8 | _30m = "_30m", 9 | _1h = "_1h", 10 | _2h = "_2h", 11 | _3h = "_3h", 12 | _4h = "_4h", 13 | _6h = "_6h", 14 | _8h = "_8h", 15 | _12h = "_12h", 16 | _1d = "_1d", 17 | _3d = "_3d", 18 | _1w = "_1w", 19 | _2w = "_2w", 20 | _1M = "_1M", 21 | } 22 | -------------------------------------------------------------------------------- /src/ClientOptions.ts: -------------------------------------------------------------------------------- 1 | export type ClientOptions = { 2 | wssPath?: string; 3 | watcherMs?: number; 4 | throttleMs?: number; 5 | l2UpdateDepth?: number; 6 | throttleL2Snapshot?: number; 7 | }; 8 | -------------------------------------------------------------------------------- /src/Https.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "http"; 2 | import https from "https"; 3 | import url from "url"; 4 | 5 | /** 6 | * Maks an HTTPS GET request to the specified URI and returns the parsed JSON 7 | * body data. 8 | */ 9 | export async function get(uri: string): Promise { 10 | const result = await getResponse(uri); 11 | return result.data; 12 | } 13 | 14 | /** 15 | * Make an HTTPS GET request to the specified URI and returns the parsed JSON 16 | * body data as well as the full response. 17 | */ 18 | export async function getResponse(uri: string): Promise<{ data: T; response: IncomingMessage }> { 19 | return new Promise((resolve, reject) => { 20 | const req = https.get(url.parse(uri), res => { 21 | const results: Buffer[] = []; 22 | res.on("error", reject); 23 | res.on("data", (data: Buffer) => results.push(data)); 24 | res.on("end", () => { 25 | const finalResults = Buffer.concat(results).toString(); 26 | if (res.statusCode !== 200) { 27 | return reject(new Error(results.toString())); 28 | } else { 29 | const resultsParsed = JSON.parse(finalResults) as T; 30 | return resolve({ 31 | data: resultsParsed, 32 | response: res, 33 | }); 34 | } 35 | }); 36 | }); 37 | req.on("error", reject); 38 | req.end(); 39 | }); 40 | } 41 | 42 | export async function post(uri: string, postData: string = ""): Promise { 43 | return new Promise((resolve, reject) => { 44 | const { hostname, port, pathname } = url.parse(uri); 45 | 46 | const req = https.request( 47 | { 48 | host: hostname, 49 | port, 50 | path: pathname, 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json", 54 | "Content-Length": postData.length, 55 | }, 56 | }, 57 | res => { 58 | const results: Buffer[] = []; 59 | res.on("error", reject); 60 | res.on("data", data => results.push(data)); 61 | res.on("end", () => { 62 | const finalResults = Buffer.concat(results).toString(); 63 | if (res.statusCode !== 200) { 64 | return reject(results.toString()); 65 | } else { 66 | return resolve(JSON.parse(finalResults)); 67 | } 68 | }); 69 | }, 70 | ); 71 | req.on("error", reject); 72 | req.write(postData); 73 | req.end(); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/IClient.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { Market } from "./Market"; 3 | 4 | export interface IClient extends EventEmitter { 5 | hasTickers: boolean; 6 | hasTrades: boolean; 7 | hasCandles: boolean; 8 | hasLevel2Snapshots: boolean; 9 | hasLevel2Updates: boolean; 10 | hasLevel3Snapshots: boolean; 11 | hasLevel3Updates: boolean; 12 | 13 | reconnect(): void; 14 | close(): void; 15 | 16 | subscribeTicker(market: Market): void; 17 | unsubscribeTicker(market: Market): Promise; 18 | subscribeCandles(market: Market): void; 19 | unsubscribeCandles(market: Market): Promise; 20 | subscribeTrades(market: Market): void; 21 | unsubscribeTrades(market: Market): void; 22 | subscribeLevel2Snapshots(market: Market): void; 23 | unsubscribeLevel2Snapshots(market: Market): Promise; 24 | subscribeLevel2Updates(market: Market): void; 25 | unsubscribeLevel2Updates(market: Market): Promise; 26 | subscribeLevel3Snapshots(market: Market): void; 27 | unsubscribeLevel3Snapshots(market: Market): Promise; 28 | subscribeLevel3Updates(market: Market): void; 29 | unsubscribeLevel3Updates(market: Market): Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/Jwt.ts: -------------------------------------------------------------------------------- 1 | import { createHmac } from "crypto"; 2 | 3 | function base64Encode(value: Buffer | string | any): string { 4 | let buffer: Buffer; 5 | if (Buffer.isBuffer(value)) { 6 | buffer = value; 7 | } else if (typeof value === "object") { 8 | buffer = Buffer.from(JSON.stringify(value)); 9 | } else if (typeof value === "string") { 10 | buffer = Buffer.from(value); 11 | } 12 | return buffer.toString("base64"); 13 | } 14 | 15 | function base64UrlEncode(value: Buffer | string | any): string { 16 | return base64Encode(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); 17 | } 18 | 19 | function hmacSign(algorithm: string, secret: string, data: string): Buffer { 20 | const hmac = createHmac(algorithm, secret); 21 | hmac.update(data); 22 | return hmac.digest(); 23 | } 24 | 25 | export function hs256(payload: any, secret: string): string { 26 | const encHeader = base64UrlEncode({ alg: "HS256", typ: "JWT" }); 27 | const encPayload = base64UrlEncode(payload); 28 | const sig = hmacSign("sha256", secret, encHeader + "." + encPayload); 29 | const encSig = base64UrlEncode(sig); 30 | return encHeader + "." + encPayload + "." + encSig; 31 | } 32 | -------------------------------------------------------------------------------- /src/Level2Point.ts: -------------------------------------------------------------------------------- 1 | export class Level2Point { 2 | constructor( 3 | readonly price: string, 4 | readonly size: string, 5 | readonly count?: number, 6 | readonly meta?: any, 7 | readonly timestamp?: number, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/Level2Snapshots.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | import { Level2Point } from "./Level2Point"; 5 | 6 | export class Level2Snapshot { 7 | public base: string; 8 | public quote: string; 9 | public exchange: string; 10 | public sequenceId: number; 11 | public timestampMs: number; 12 | public asks: Level2Point[]; 13 | public bids: Level2Point[]; 14 | 15 | constructor(props) { 16 | for (const key in props) { 17 | this[key] = props[key]; 18 | } 19 | } 20 | 21 | public get marketId() { 22 | return `${this.base}/${this.quote}`; 23 | } 24 | 25 | /** 26 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 27 | */ 28 | public get fullId() { 29 | return `${this.exchange}:${this.base}/${this.quote}`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Level2Update.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | import { Level2Point } from "./Level2Point"; 5 | 6 | export class Level2Update { 7 | public base: string; 8 | public quote: string; 9 | public exchange: string; 10 | public sequenceId: number; 11 | public timestampMs: number; 12 | public asks: Level2Point[]; 13 | public bids: Level2Point[]; 14 | 15 | constructor(props: any) { 16 | for (const key in props) { 17 | this[key] = props[key]; 18 | } 19 | } 20 | 21 | public get marketId() { 22 | return `${this.base}/${this.quote}`; 23 | } 24 | 25 | /** 26 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 27 | */ 28 | public get fullId() { 29 | return `${this.exchange}:${this.base}/${this.quote}`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Level3Point.ts: -------------------------------------------------------------------------------- 1 | export class Level3Point { 2 | constructor( 3 | readonly orderId: string, 4 | readonly price: string, 5 | readonly size: string, 6 | readonly meta?: any, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/Level3Snapshot.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | import { Level3Point } from "./Level3Point"; 5 | 6 | export class Level3Snapshot { 7 | public exchange: string; 8 | public base: string; 9 | public quote: string; 10 | public sequenceId: number; 11 | public timestampMs: number; 12 | public asks: Level3Point[]; 13 | public bids: Level3Point[]; 14 | 15 | constructor(props: any) { 16 | for (const key in props) { 17 | this[key] = props[key]; 18 | } 19 | } 20 | 21 | /** 22 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 23 | */ 24 | public get fullId() { 25 | return `${this.exchange}:${this.base}/${this.quote}`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Level3Update.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | import { Level3Point } from "./Level3Point"; 5 | 6 | export class Level3Update { 7 | public exchange: string; 8 | public base: string; 9 | public quote: string; 10 | public sequenceId: number; 11 | public timestampMs: number; 12 | public asks: Level3Point[]; 13 | public bids: Level3Point[]; 14 | 15 | constructor(props: any) { 16 | for (const key in props) { 17 | this[key] = props[key]; 18 | } 19 | } 20 | 21 | /** 22 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 23 | */ 24 | public get fullId() { 25 | return `${this.exchange}:${this.base}/${this.quote}`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Market.ts: -------------------------------------------------------------------------------- 1 | export type Market = { 2 | id: string; 3 | base: string; 4 | quote: string; 5 | type?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/NotImplementedFn.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/require-await */ 2 | 3 | export const NotImplementedFn: (...args: any[]) => any = () => new Error("Not implemented"); 4 | export const NotImplementedAsyncFn: (...args: any[]) => Promise = async () => 5 | new Error("Not implemented"); 6 | -------------------------------------------------------------------------------- /src/SmartWss.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | 5 | import { EventEmitter } from "events"; 6 | import WebSocket from "ws"; 7 | import { wait } from "./Util"; 8 | 9 | export class SmartWss extends EventEmitter { 10 | private _retryTimeoutMs: number; 11 | private _connected: boolean; 12 | private _wss: any; 13 | 14 | constructor(readonly wssPath: string) { 15 | super(); 16 | this._retryTimeoutMs = 15000; 17 | this._connected = false; 18 | } 19 | 20 | /** 21 | * Gets if the socket is currently connected 22 | */ 23 | public get isConnected() { 24 | return this._connected; 25 | } 26 | 27 | /** 28 | * Attempts to connect 29 | */ 30 | public async connect(): Promise { 31 | await this._attemptConnect(); 32 | } 33 | 34 | /** 35 | * Closes the connection 36 | */ 37 | public close(): void { 38 | this.emit("closing"); 39 | if (this._wss) { 40 | this._wss.removeAllListeners(); 41 | this._wss.on("close", () => this.emit("closed")); 42 | this._wss.on("error", err => { 43 | if (err.message !== "WebSocket was closed before the connection was established") 44 | return; 45 | this.emit("error", err); 46 | }); 47 | this._wss.close(); 48 | } 49 | } 50 | 51 | /** 52 | * Sends the data if the socket is currently connected. 53 | * Otherwise the consumer needs to retry to send the information 54 | * when the socket is connected. 55 | */ 56 | public send(data: string) { 57 | if (this._connected) { 58 | try { 59 | this._wss.send(data); 60 | } catch (e) { 61 | this.emit("error", e); 62 | } 63 | } 64 | } 65 | 66 | ///////////////////////// 67 | 68 | /** 69 | * Attempts a connection and will either fail or timeout otherwise. 70 | */ 71 | private _attemptConnect(): Promise { 72 | return new Promise(resolve => { 73 | const wssPath = this.wssPath; 74 | this.emit("connecting"); 75 | this._wss = new WebSocket(wssPath, { 76 | perMessageDeflate: false, 77 | handshakeTimeout: 15000, 78 | }); 79 | this._wss.on("open", () => { 80 | this._connected = true; 81 | this.emit("open"); // deprecated 82 | this.emit("connected"); 83 | resolve(); 84 | }); 85 | this._wss.on("close", () => this._closeCallback()); 86 | this._wss.on("error", err => this.emit("error", err)); 87 | this._wss.on("message", msg => this.emit("message", msg)); 88 | }); 89 | } 90 | 91 | /** 92 | * Handles the closing event by reconnecting 93 | */ 94 | private _closeCallback(): void { 95 | this._connected = false; 96 | this._wss = null; 97 | this.emit("disconnected"); 98 | void this._retryConnect(); 99 | } 100 | 101 | /** 102 | * Perform reconnection after the timeout period 103 | * and will loop on hard failures 104 | */ 105 | private async _retryConnect(): Promise { 106 | // eslint-disable-next-line no-constant-condition 107 | while (true) { 108 | try { 109 | await wait(this._retryTimeoutMs); 110 | await this._attemptConnect(); 111 | return; 112 | } catch (ex) { 113 | this.emit("error", ex); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/SubscriptionType.ts: -------------------------------------------------------------------------------- 1 | export enum SubscriptionType { 2 | ticker = 1, 3 | trade = 2, 4 | level2snapshot = 3, 5 | level2update = 4, 6 | level3snapshot = 5, 7 | level3update = 6, 8 | candle = 7, 9 | } 10 | -------------------------------------------------------------------------------- /src/Ticker.ts: -------------------------------------------------------------------------------- 1 | export class Ticker { 2 | public exchange: string; 3 | public base: string; 4 | public quote: string; 5 | public timestamp: number; 6 | public sequenceId: number; 7 | public last: string; 8 | public open: string; 9 | public high: string; 10 | public low: string; 11 | public volume: string; 12 | public quoteVolume: string; 13 | public change: string; 14 | public changePercent: string; 15 | public bid: string; 16 | public bidVolume: string; 17 | public ask: string; 18 | public askVolume: string; 19 | 20 | constructor({ 21 | exchange, 22 | base, 23 | quote, 24 | timestamp, 25 | sequenceId, 26 | last, 27 | open, 28 | high, 29 | low, 30 | volume, 31 | quoteVolume, 32 | change, 33 | changePercent, 34 | bid, 35 | bidVolume, 36 | ask, 37 | askVolume, 38 | }: Partial) { 39 | this.exchange = exchange; 40 | this.base = base; 41 | this.quote = quote; 42 | this.timestamp = timestamp; 43 | this.sequenceId = sequenceId; 44 | this.last = last; 45 | this.open = open; 46 | this.high = high; 47 | this.low = low; 48 | this.volume = volume; 49 | this.quoteVolume = quoteVolume; 50 | this.change = change; 51 | this.changePercent = changePercent; 52 | this.bid = bid; 53 | this.bidVolume = bidVolume; 54 | this.ask = ask; 55 | this.askVolume = askVolume; 56 | } 57 | 58 | /** 59 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 60 | */ 61 | public get fullId() { 62 | return `${this.exchange}:${this.base}/${this.quote}`; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Trade.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | 4 | export class Trade { 5 | public exchange: string; 6 | public quote: string; 7 | public base: string; 8 | public tradeId: string; 9 | public sequenceId: number; 10 | public unix: number; 11 | public side: string; 12 | public price: string; 13 | public amount: string; 14 | public buyOrderId: string; 15 | public sellOrderId: string; 16 | 17 | constructor(props: Partial | any) { 18 | this.exchange = props.exchange; 19 | this.quote = props.quote; 20 | this.base = props.base; 21 | this.tradeId = props.tradeId; 22 | this.sequenceId = props.sequenceId; 23 | this.unix = props.unix; 24 | this.side = props.side; 25 | this.price = props.price; 26 | this.amount = props.amount; 27 | this.buyOrderId = props.buyOrderId; 28 | this.sellOrderId = props.sellOrderId; 29 | 30 | // attach any extra props 31 | for (const key in props) { 32 | if (!this[key]) this[key] = props[key]; 33 | } 34 | } 35 | 36 | public get marketId() { 37 | return `${this.base}/${this.quote}`; 38 | } 39 | 40 | /** 41 | * @deprecated use Market object (second argument to each event) to determine exchange and trade pair 42 | */ 43 | public get fullId() { 44 | return `${this.exchange}:${this.base}/${this.quote}`; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Util.ts: -------------------------------------------------------------------------------- 1 | export function wait(ms) { 2 | return new Promise(resolve => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/Watcher.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-implied-eval */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 5 | 6 | /** 7 | * Watcher subscribes to a client's messages and 8 | * will trigger a restart of the client if no 9 | * information has been transmitted in the checking interval 10 | */ 11 | export class Watcher { 12 | private _intervalHandle: NodeJS.Timeout; 13 | private _lastMessage: number; 14 | 15 | constructor(readonly client: any, readonly intervalMs = 90000) { 16 | this._intervalHandle = undefined; 17 | this._lastMessage = undefined; 18 | 19 | client.on("ticker", this.markAlive.bind(this)); 20 | client.on("candle", this.markAlive.bind(this)); 21 | client.on("trade", this.markAlive.bind(this)); 22 | client.on("l2snapshot", this.markAlive.bind(this)); 23 | client.on("l2update", this.markAlive.bind(this)); 24 | client.on("l3snapshot", this.markAlive.bind(this)); 25 | client.on("l3update", this.markAlive.bind(this)); 26 | } 27 | 28 | /** 29 | * Starts an interval to check if a reconnction is required 30 | */ 31 | public start() { 32 | this.stop(); // always clear the prior interval 33 | this._intervalHandle = setInterval(this._onCheck.bind(this), this.intervalMs); 34 | } 35 | 36 | /** 37 | * Stops an interval to check if a reconnection is required 38 | */ 39 | public stop() { 40 | clearInterval(this._intervalHandle); 41 | this._intervalHandle = undefined; 42 | } 43 | 44 | /** 45 | * Marks that a message was received 46 | */ 47 | public markAlive() { 48 | this._lastMessage = Date.now(); 49 | } 50 | 51 | /** 52 | * Checks if a reconnecton is required by comparing the current 53 | * date to the last receieved message date 54 | */ 55 | private _onCheck() { 56 | if (!this._lastMessage || this._lastMessage < Date.now() - this.intervalMs) { 57 | this._reconnect(); 58 | } 59 | } 60 | 61 | /** 62 | * Logic to perform a reconnection event of the client 63 | */ 64 | private _reconnect() { 65 | this.client.reconnect(); 66 | this.stop(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ZlibUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import zlib from "zlib"; 3 | import { Queue } from "./flowcontrol/Queue"; 4 | const queue = new Queue(); 5 | 6 | let current: [string, Buffer, ZlibCallback]; 7 | 8 | export type ZlibCallback = (err: Error, result: Buffer) => void; 9 | 10 | /** 11 | * Serialized unzip using async zlib.unzip method. This function is a helper to 12 | * address issues with memory fragmentation issues as documented here: 13 | * https://nodejs.org/api/zlib.html#zlib_threadpool_usage_and_performance_considerations 14 | */ 15 | export function unzip(data: Buffer, cb: ZlibCallback): void { 16 | queue.push(["unzip", data, cb]); 17 | serialExecute(); 18 | } 19 | 20 | /** 21 | * Serialized inflate using async zlib.inflate method. This function is a helper to 22 | * address issues with memory fragmentation issues as documented here: 23 | * https://nodejs.org/api/zlib.html#zlib_threadpool_usage_and_performance_considerations 24 | */ 25 | export function inflate(data: Buffer, cb: ZlibCallback): void { 26 | queue.push(["inflate", data, cb]); 27 | serialExecute(); 28 | } 29 | 30 | /** 31 | * Serialized inflateRaw using async zlib.inflateRaw method. This function is a helper to 32 | * address issues with memory fragmentation issues as documented here: 33 | * https://nodejs.org/api/zlib.html#zlib_threadpool_usage_and_performance_considerations 34 | * 35 | */ 36 | export function inflateRaw(data: Buffer, cb: ZlibCallback) { 37 | queue.push(["inflateRaw", data, cb]); 38 | serialExecute(); 39 | } 40 | 41 | function serialExecute() { 42 | // abort if already executng 43 | if (current) return; 44 | 45 | // remove first item and abort if nothing else to do 46 | current = queue.shift(); 47 | if (!current) return; 48 | 49 | // perform unzip 50 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 51 | zlib[current[0]](current[1], (err: Error, res: Buffer) => { 52 | // call supplied callback 53 | current[2](err, res); 54 | 55 | // reset the current status 56 | current = undefined; 57 | 58 | // immediate try next item 59 | serialExecute(); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/exchanges/BinanceClient.ts: -------------------------------------------------------------------------------- 1 | import { BinanceBase, BinanceClientOptions } from "./BinanceBase"; 2 | 3 | export class BinanceClient extends BinanceBase { 4 | constructor({ 5 | useAggTrades = true, 6 | requestSnapshot = true, 7 | socketBatchSize = 200, 8 | socketThrottleMs = 1000, 9 | restThrottleMs = 1000, 10 | testNet = false, 11 | wssPath = "wss://stream.binance.com:9443/stream", 12 | restL2SnapshotPath = "https://api.binance.com/api/v1/depth", 13 | watcherMs, 14 | l2updateSpeed, 15 | l2snapshotSpeed, 16 | batchTickers, 17 | }: BinanceClientOptions = {}) { 18 | if (testNet) { 19 | wssPath = "wss://testnet.binance.vision/stream"; 20 | restL2SnapshotPath = "https://testnet.binance.vision/api/v1/depth"; 21 | } 22 | super({ 23 | name: "Binance", 24 | restL2SnapshotPath, 25 | wssPath, 26 | useAggTrades, 27 | requestSnapshot, 28 | socketBatchSize, 29 | socketThrottleMs, 30 | restThrottleMs, 31 | watcherMs, 32 | l2updateSpeed, 33 | l2snapshotSpeed, 34 | batchTickers, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/exchanges/BinanceFuturesCoinmClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | import { Level2Point } from "../Level2Point"; 5 | import { Level2Snapshot } from "../Level2Snapshots"; 6 | import { Level2Update } from "../Level2Update"; 7 | import { Market } from "../Market"; 8 | import { BinanceBase } from "./BinanceBase"; 9 | import { BinanceClientOptions } from "./BinanceBase"; 10 | 11 | export class BinanceFuturesCoinmClient extends BinanceBase { 12 | constructor({ 13 | useAggTrades = true, 14 | requestSnapshot = true, 15 | socketBatchSize = 200, 16 | socketThrottleMs = 1000, 17 | restThrottleMs = 1000, 18 | l2snapshotSpeed = "100ms", 19 | l2updateSpeed = "100ms", 20 | watcherMs, 21 | }: BinanceClientOptions = {}) { 22 | super({ 23 | name: "Binance Futures COIN-M", 24 | wssPath: "wss://dstream.binance.com/stream", 25 | restL2SnapshotPath: "https://dapi.binance.com/dapi/v1/depth", 26 | useAggTrades, 27 | requestSnapshot, 28 | socketBatchSize, 29 | socketThrottleMs, 30 | restThrottleMs, 31 | l2snapshotSpeed, 32 | l2updateSpeed, 33 | watcherMs, 34 | }); 35 | } 36 | 37 | /** 38 | * Custom construction for a partial depth update. This deviates from 39 | * the spot market by including the `pu` property where updates may 40 | * not be sequential. The update message looks like: 41 | { 42 | "e": "depthUpdate", // Event type 43 | "E": 1591270260907, // Event time 44 | "T": 1591270260891, // Transction time 45 | "s": "BTCUSD_200626", // Symbol 46 | "ps": "BTCUSD", // Pair 47 | "U": 17285681, // First update ID in event 48 | "u": 17285702, // Final update ID in event 49 | "pu": 17285675, // Final update Id in last stream(ie `u` in last stream) 50 | "b": [ // Bids to be updated 51 | [ 52 | "9517.6", // Price level to be updated 53 | "10" // Quantity 54 | ] 55 | ], 56 | "a": [ // Asks to be updated 57 | [ 58 | "9518.5", // Price level to be updated 59 | "45" // Quantity 60 | ] 61 | ] 62 | } 63 | */ 64 | protected _constructLevel2Update(msg, market: Market) { 65 | const eventMs = msg.data.E; 66 | const timestampMs = msg.data.T; 67 | const sequenceId = msg.data.U; 68 | const lastSequenceId = msg.data.u; 69 | const previousLastSequenceId = msg.data.pu; 70 | const asks = msg.data.a.map(p => new Level2Point(p[0], p[1])); 71 | const bids = msg.data.b.map(p => new Level2Point(p[0], p[1])); 72 | return new Level2Update({ 73 | exchange: this.name, 74 | base: market.base, 75 | quote: market.quote, 76 | sequenceId, 77 | lastSequenceId, 78 | previousLastSequenceId, 79 | timestampMs, 80 | eventMs, 81 | asks, 82 | bids, 83 | }); 84 | } 85 | 86 | /** 87 | * Partial book snapshot that. This deviates from the spot market by 88 | * including a previous last update id, `pu`. 89 | { 90 | "e":"depthUpdate", // Event type 91 | "E":1591269996801, // Event time 92 | "T":1591269996646, // Transaction time 93 | "s":"BTCUSD_200626", // Symbol 94 | "ps":"BTCUSD", // Pair 95 | "U":17276694, 96 | "u":17276701, 97 | "pu":17276678, 98 | "b":[ // Bids to be updated 99 | [ 100 | "9523.0", // Price Level 101 | "5" // Quantity 102 | ], 103 | [ 104 | "9522.8", 105 | "8" 106 | ] 107 | ], 108 | "a":[ // Asks to be updated 109 | [ 110 | "9524.6", // Price level to be 111 | "2" // Quantity 112 | ], 113 | [ 114 | "9524.7", 115 | "3" 116 | ] 117 | ] 118 | } 119 | */ 120 | protected _constructLevel2Snapshot(msg, market: Market) { 121 | const timestampMs = msg.data.E; 122 | const sequenceId = msg.data.U; 123 | const lastSequenceId = msg.data.u; 124 | const previousLastSequenceId = msg.data.pu; 125 | const asks = msg.data.a.map(p => new Level2Point(p[0], p[1])); 126 | const bids = msg.data.b.map(p => new Level2Point(p[0], p[1])); 127 | return new Level2Snapshot({ 128 | exchange: this.name, 129 | base: market.base, 130 | quote: market.quote, 131 | sequenceId, 132 | lastSequenceId, 133 | previousLastSequenceId, 134 | timestampMs, 135 | asks, 136 | bids, 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/exchanges/BinanceFuturesUsdtmClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | 5 | import { Level2Point } from "../Level2Point"; 6 | import { Level2Snapshot } from "../Level2Snapshots"; 7 | import { Level2Update } from "../Level2Update"; 8 | import { Market } from "../Market"; 9 | import { BinanceBase, BinanceClientOptions } from "./BinanceBase"; 10 | 11 | export class BinanceFuturesUsdtmClient extends BinanceBase { 12 | constructor({ 13 | useAggTrades = true, 14 | requestSnapshot = true, 15 | socketBatchSize = 200, 16 | socketThrottleMs = 1000, 17 | restThrottleMs = 1000, 18 | l2snapshotSpeed = "100ms", 19 | l2updateSpeed = "0ms", 20 | watcherMs, 21 | }: BinanceClientOptions = {}) { 22 | super({ 23 | name: "Binance Futures USDT-M", 24 | wssPath: "wss://fstream.binance.com/stream", 25 | restL2SnapshotPath: "https://fapi.binance.com/fapi/v1/depth", 26 | useAggTrades, 27 | requestSnapshot, 28 | socketBatchSize, 29 | socketThrottleMs, 30 | restThrottleMs, 31 | l2snapshotSpeed, 32 | l2updateSpeed, 33 | watcherMs, 34 | }); 35 | } 36 | 37 | /** 38 | * Custom construction for a partial depth update. This deviates from 39 | * the spot market by including the `pu` property where updates may 40 | * not be sequential. The update message looks like: 41 | * { 42 | "e": "depthUpdate", // Event type 43 | "E": 123456789, // Event time 44 | "T": 123456788, // transaction time 45 | "s": "BTCUSDT", // Symbol 46 | "U": 157, // First update ID in event 47 | "u": 160, // Final update ID in event 48 | "pu": 149, // Final update Id in last stream(ie `u` in last stream) 49 | "b": [ // Bids to be updated 50 | [ 51 | "0.0024", // Price level to be updated 52 | "10" // Quantity 53 | ] 54 | ], 55 | "a": [ // Asks to be updated 56 | [ 57 | "0.0026", // Price level to be updated 58 | "100" // Quantity 59 | ] 60 | ] 61 | } 62 | */ 63 | protected _constructLevel2Update(msg, market) { 64 | const eventMs = msg.data.E; 65 | const timestampMs = msg.data.T; 66 | const sequenceId = msg.data.U; 67 | const lastSequenceId = msg.data.u; 68 | const previousLastSequenceId = msg.data.pu; 69 | const asks = msg.data.a.map(p => new Level2Point(p[0], p[1])); 70 | const bids = msg.data.b.map(p => new Level2Point(p[0], p[1])); 71 | return new Level2Update({ 72 | exchange: this.name, 73 | base: market.base, 74 | quote: market.quote, 75 | sequenceId, 76 | lastSequenceId, 77 | previousLastSequenceId, 78 | timestampMs, 79 | eventMs, 80 | asks, 81 | bids, 82 | }); 83 | } 84 | 85 | /** 86 | * Partial book snapshot that. This deviates from the spot market by 87 | * including a previous last update id, `pu`. 88 | { 89 | "e": "depthUpdate", // Event type 90 | "E": 1571889248277, // Event time 91 | "T": 1571889248276, // transaction time 92 | "s": "BTCUSDT", 93 | "U": 390497796, 94 | "u": 390497878, 95 | "pu": 390497794, 96 | "b": [ // Bids to be updated 97 | [ 98 | "7403.89", // Price Level to be 99 | "0.002" // Quantity 100 | ], 101 | [ 102 | "7403.90", 103 | "3.906" 104 | ] 105 | ], 106 | "a": [ // Asks to be updated 107 | [ 108 | "7405.96", // Price level to be 109 | "3.340" // Quantity 110 | ], 111 | [ 112 | "7406.63", 113 | "4.525" 114 | ] 115 | ] 116 | } 117 | */ 118 | protected _constructLevel2Snapshot(msg, market: Market) { 119 | const timestampMs = msg.data.E; 120 | const sequenceId = msg.data.U; 121 | const lastSequenceId = msg.data.u; 122 | const previousLastSequenceId = msg.data.pu; 123 | const asks = msg.data.a.map(p => new Level2Point(p[0], p[1])); 124 | const bids = msg.data.b.map(p => new Level2Point(p[0], p[1])); 125 | return new Level2Snapshot({ 126 | exchange: this.name, 127 | base: market.base, 128 | quote: market.quote, 129 | sequenceId, 130 | lastSequenceId, 131 | previousLastSequenceId, 132 | timestampMs, 133 | asks, 134 | bids, 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/exchanges/BinanceJeClient.ts: -------------------------------------------------------------------------------- 1 | import { BinanceBase, BinanceClientOptions } from "./BinanceBase"; 2 | 3 | export class BinanceJeClient extends BinanceBase { 4 | constructor({ 5 | useAggTrades = true, 6 | requestSnapshot = true, 7 | socketBatchSize = 200, 8 | socketThrottleMs = 1000, 9 | restThrottleMs = 1000, 10 | watcherMs, 11 | l2updateSpeed, 12 | l2snapshotSpeed, 13 | }: BinanceClientOptions = {}) { 14 | super({ 15 | name: "BinanceJe", 16 | wssPath: "wss://stream.binance.je:9443/stream", 17 | restL2SnapshotPath: "https://api.binance.je/api/v1/depth", 18 | useAggTrades, 19 | requestSnapshot, 20 | socketBatchSize, 21 | socketThrottleMs, 22 | restThrottleMs, 23 | watcherMs, 24 | l2updateSpeed, 25 | l2snapshotSpeed, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/exchanges/BinanceUsClient.ts: -------------------------------------------------------------------------------- 1 | import { BinanceBase, BinanceClientOptions } from "./BinanceBase"; 2 | 3 | export class BinanceUsClient extends BinanceBase { 4 | constructor({ 5 | useAggTrades = true, 6 | requestSnapshot = true, 7 | socketBatchSize = 200, 8 | socketThrottleMs = 1000, 9 | restThrottleMs = 1000, 10 | watcherMs, 11 | l2updateSpeed, 12 | l2snapshotSpeed, 13 | }: BinanceClientOptions = {}) { 14 | super({ 15 | name: "BinanceUS", 16 | wssPath: "wss://stream.binance.us:9443/stream", 17 | restL2SnapshotPath: "https://api.binance.us/api/v1/depth", 18 | useAggTrades, 19 | requestSnapshot, 20 | socketBatchSize, 21 | socketThrottleMs, 22 | restThrottleMs, 23 | watcherMs, 24 | l2updateSpeed, 25 | l2snapshotSpeed, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/exchanges/FtxBase.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/member-ordering */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 5 | import Decimal from "decimal.js"; 6 | import moment = require("moment"); 7 | import { BasicClient } from "../BasicClient"; 8 | import { Level2Point } from "../Level2Point"; 9 | import { Level2Snapshot } from "../Level2Snapshots"; 10 | import { Level2Update } from "../Level2Update"; 11 | import { NotImplementedFn } from "../NotImplementedFn"; 12 | import { Ticker } from "../Ticker"; 13 | import { Trade } from "../Trade"; 14 | 15 | export class FtxBaseClient extends BasicClient { 16 | constructor({ name, wssPath, watcherMs }) { 17 | super(wssPath, name, undefined, watcherMs); 18 | this.hasTickers = true; 19 | this.hasTrades = true; 20 | this.hasLevel2Updates = true; 21 | } 22 | 23 | protected _sendSubTicker(market) { 24 | this._wss.send( 25 | JSON.stringify({ 26 | op: "subscribe", 27 | channel: "ticker", 28 | market, 29 | }), 30 | ); 31 | } 32 | 33 | protected _sendUnsubTicker(market) { 34 | this._wss.send( 35 | JSON.stringify({ 36 | op: "unsubscribe", 37 | channel: "ticker", 38 | market, 39 | }), 40 | ); 41 | } 42 | 43 | protected _sendSubTrades(market) { 44 | this._wss.send( 45 | JSON.stringify({ 46 | op: "subscribe", 47 | channel: "trades", 48 | market, 49 | }), 50 | ); 51 | } 52 | 53 | protected _sendUnsubTrades(market) { 54 | this._wss.send( 55 | JSON.stringify({ 56 | op: "unsubscribe", 57 | channel: "trades", 58 | market, 59 | }), 60 | ); 61 | } 62 | 63 | protected _sendSubLevel2Updates(market) { 64 | this._wss.send( 65 | JSON.stringify({ 66 | op: "subscribe", 67 | channel: "orderbook", 68 | market, 69 | }), 70 | ); 71 | } 72 | 73 | protected _sendUnsubLevel2Updates(market) { 74 | this._wss.send( 75 | JSON.stringify({ 76 | op: "subscribe", 77 | channel: "orderbook", 78 | market, 79 | }), 80 | ); 81 | } 82 | 83 | protected _sendSubCandles = NotImplementedFn; 84 | protected _sendUnsubCandles = NotImplementedFn; 85 | protected _sendSubLevel2Snapshots = NotImplementedFn; 86 | protected _sendUnsubLevel2Snapshots = NotImplementedFn; 87 | protected _sendSubLevel3Snapshots = NotImplementedFn; 88 | protected _sendUnsubLevel3Snapshots = NotImplementedFn; 89 | protected _sendSubLevel3Updates = NotImplementedFn; 90 | protected _sendUnsubLevel3Updates = NotImplementedFn; 91 | 92 | protected _onMessage(raw) { 93 | const { type, channel, market: symbol, data } = JSON.parse(raw); 94 | if (!data || !type || !channel || !symbol) { 95 | return; 96 | } 97 | 98 | switch (channel) { 99 | case "ticker": 100 | this._tickerMessageHandler(data, symbol); 101 | break; 102 | case "trades": 103 | this._tradesMessageHandler(data, symbol); 104 | break; 105 | case "orderbook": 106 | this._orderbookMessageHandler(data, symbol, type); 107 | break; 108 | } 109 | } 110 | 111 | protected _tickerMessageHandler(data, symbol) { 112 | const market = this._tickerSubs.get(symbol); 113 | if (!market || !market.base || !market.quote) { 114 | return; 115 | } 116 | 117 | const timestamp = this._timeToTimestampMs(data.time); 118 | const { last, bid, ask, bidSize: bidVolume, askSize: askVolume } = data; 119 | const ticker = new Ticker({ 120 | exchange: this.name, 121 | base: market.base, 122 | quote: market.quote, 123 | timestamp, 124 | last: last !== undefined && last !== null ? last.toFixed(8) : undefined, 125 | bid: bid !== undefined && bid !== null ? bid.toFixed(8) : undefined, 126 | ask: ask !== undefined && ask !== null ? ask.toFixed(8) : undefined, 127 | bidVolume: 128 | bidVolume !== undefined && bidVolume !== null ? bidVolume.toFixed(8) : undefined, 129 | askVolume: 130 | askVolume !== undefined && askVolume !== null ? askVolume.toFixed(8) : undefined, 131 | }); 132 | 133 | this.emit("ticker", ticker, market); 134 | } 135 | 136 | protected _tradesMessageHandler(data, symbol) { 137 | const market = this._tradeSubs.get(symbol); 138 | if (!market || !market.base || !market.quote) { 139 | return; 140 | } 141 | 142 | for (const entry of data) { 143 | const { id, price, size, side, time, liquidation } = entry; 144 | const unix = moment.utc(time).valueOf(); 145 | 146 | const trade = new Trade({ 147 | exchange: this.name, 148 | base: market.base, 149 | quote: market.quote, 150 | tradeId: id.toString(), 151 | side, 152 | unix, 153 | price: price.toFixed(8), 154 | amount: size.toFixed(8), 155 | liquidation, 156 | }); 157 | 158 | this.emit("trade", trade, market); 159 | } 160 | } 161 | 162 | protected _orderbookMessageHandler(data, symbol, type) { 163 | const market = this._level2UpdateSubs.get(symbol); 164 | if (!market || !market.base || !market.quote || (!data.asks.length && !data.bids.length)) { 165 | return; 166 | } 167 | 168 | switch (type) { 169 | case "partial": 170 | this._orderbookSnapshotEvent(data, market); 171 | break; 172 | case "update": 173 | this._orderbookUpdateEvent(data, market); 174 | break; 175 | } 176 | } 177 | 178 | protected _orderbookUpdateEvent(data, market) { 179 | const content = this._orderbookEventContent(data, market); 180 | const eventData = new Level2Update(content); 181 | this.emit("l2update", eventData, market); 182 | } 183 | 184 | protected _orderbookSnapshotEvent(data, market) { 185 | const content = this._orderbookEventContent(data, market); 186 | const eventData = new Level2Snapshot(content); 187 | this.emit("l2snapshot", eventData, market); 188 | } 189 | 190 | protected _orderbookEventContent(data, market) { 191 | const { time, asks, bids, checksum } = data; 192 | const level2PointAsks = asks.map(p => new Level2Point(p[0].toFixed(8), p[1].toFixed(8))); 193 | const level2PointBids = bids.map(p => new Level2Point(p[0].toFixed(8), p[1].toFixed(8))); 194 | const timestampMs = this._timeToTimestampMs(time); 195 | 196 | return { 197 | exchange: this.name, 198 | base: market.base, 199 | quote: market.quote, 200 | timestampMs, 201 | asks: level2PointAsks, 202 | bids: level2PointBids, 203 | checksum, 204 | }; 205 | } 206 | 207 | protected _timeToTimestampMs(time) { 208 | return new Decimal(time).mul(1000).toDecimalPlaces(0).toNumber(); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/exchanges/FtxClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { FtxBaseClient } from "./FtxBase"; 3 | 4 | export class FtxClient extends FtxBaseClient { 5 | constructor({ wssPath = "wss://ftx.com/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "FTX", wssPath, watcherMs }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/FtxUsClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { FtxBaseClient } from "./FtxBase"; 3 | 4 | export class FtxUsClient extends FtxBaseClient { 5 | constructor({ wssPath = "wss://ftx.us/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "FTX US", wssPath, watcherMs }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/HuobiClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { HuobiBase } from "./HuobiBase"; 3 | 4 | export class HuobiClient extends HuobiBase { 5 | constructor({ wssPath = "wss://api.huobi.pro/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "Huobi", wssPath, watcherMs }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/HuobiFuturesClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { HuobiBase } from "./HuobiBase"; 3 | 4 | export class HuobiFuturesClient extends HuobiBase { 5 | constructor({ wssPath = "wss://api.hbdm.com/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "Huobi Futures", wssPath, watcherMs }); 7 | this.hasLevel2Updates = true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/exchanges/HuobiJapanClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { HuobiBase } from "./HuobiBase"; 3 | 4 | export class HuobiJapanClient extends HuobiBase { 5 | constructor({ wssPath = "wss://api-cloud.huobi.co.jp/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "Huobi Japan", wssPath, watcherMs }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/HuobiKoreaClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { HuobiBase } from "./HuobiBase"; 3 | 4 | export class HuobiKoreaClient extends HuobiBase { 5 | constructor({ wssPath = "wss://api-cloud.huobi.co.kr/ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "Huobi Korea", wssPath, watcherMs }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/HuobiSwapsClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOptions } from "../ClientOptions"; 2 | import { HuobiBase } from "./HuobiBase"; 3 | 4 | export class HuobiSwapsClient extends HuobiBase { 5 | constructor({ wssPath = "wss://api.hbdm.com/swap-ws", watcherMs }: ClientOptions = {}) { 6 | super({ name: "Huobi Swaps", wssPath, watcherMs }); 7 | this.hasLevel2Updates = true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/exchanges/ZbClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/member-ordering */ 2 | /* eslint-disable prefer-const */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 5 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 6 | import { BasicClient } from "../BasicClient"; 7 | import { ClientOptions } from "../ClientOptions"; 8 | import { Level2Point } from "../Level2Point"; 9 | import { Level2Snapshot } from "../Level2Snapshots"; 10 | import { NotImplementedFn } from "../NotImplementedFn"; 11 | import { Ticker } from "../Ticker"; 12 | import { Trade } from "../Trade"; 13 | 14 | export class ZbClient extends BasicClient { 15 | public remoteIdMap: Map; 16 | 17 | constructor({ wssPath = "wss://api.zb.work/websocket", watcherMs }: ClientOptions = {}) { 18 | super(wssPath, "ZB", undefined, watcherMs); 19 | this.hasTickers = true; 20 | this.hasTrades = true; 21 | this.hasLevel2Snapshots = true; 22 | this.remoteIdMap = new Map(); 23 | } 24 | 25 | protected _sendSubTicker(remote_id: string) { 26 | const wss_remote_id = remote_id.replace(/_/, ""); 27 | this.remoteIdMap.set(wss_remote_id, remote_id); 28 | this._wss.send( 29 | JSON.stringify({ 30 | event: "addChannel", 31 | channel: `${wss_remote_id}_ticker`, 32 | }), 33 | ); 34 | } 35 | 36 | protected _sendUnsubTicker(remote_id: string) { 37 | const wss_remote_id = remote_id.replace(/_/, ""); 38 | this.remoteIdMap.set(wss_remote_id, remote_id); 39 | this._wss.send( 40 | JSON.stringify({ 41 | event: "removeChannel", 42 | channel: `${wss_remote_id}_ticker`, 43 | }), 44 | ); 45 | } 46 | 47 | protected _sendSubTrades(remote_id: string) { 48 | const wss_remote_id = remote_id.replace(/_/, ""); 49 | this.remoteIdMap.set(wss_remote_id, remote_id); 50 | this._wss.send( 51 | JSON.stringify({ 52 | event: "addChannel", 53 | channel: `${wss_remote_id}_trades`, 54 | }), 55 | ); 56 | } 57 | 58 | protected _sendUnsubTrades(remote_id: string) { 59 | const wss_remote_id = remote_id.replace(/_/, ""); 60 | this.remoteIdMap.set(wss_remote_id, remote_id); 61 | this._wss.send( 62 | JSON.stringify({ 63 | event: "removeChannel", 64 | channel: `${wss_remote_id}_trades`, 65 | }), 66 | ); 67 | } 68 | 69 | protected _sendSubLevel2Snapshots(remote_id: string) { 70 | const wss_remote_id = remote_id.replace(/_/, ""); 71 | this.remoteIdMap.set(wss_remote_id, remote_id); 72 | this._wss.send( 73 | JSON.stringify({ 74 | event: "addChannel", 75 | channel: `${wss_remote_id}_depth`, 76 | }), 77 | ); 78 | } 79 | 80 | protected _sendUnsubLevel2Snapshots(remote_id: string) { 81 | const wss_remote_id = remote_id.replace(/_/, ""); 82 | this.remoteIdMap.set(wss_remote_id, remote_id); 83 | this._wss.send( 84 | JSON.stringify({ 85 | event: "removeChannel", 86 | channel: `${wss_remote_id}_depth`, 87 | }), 88 | ); 89 | } 90 | 91 | protected _sendSubCandles = NotImplementedFn; 92 | protected _sendUnsubCandles = NotImplementedFn; 93 | protected _sendSubLevel2Updates = NotImplementedFn; 94 | protected _sendUnsubLevel2Updates = NotImplementedFn; 95 | protected _sendSubLevel3Snapshots = NotImplementedFn; 96 | protected _sendUnsubLevel3Snapshots = NotImplementedFn; 97 | protected _sendSubLevel3Updates = NotImplementedFn; 98 | protected _sendUnsubLevel3Updates = NotImplementedFn; 99 | 100 | protected _onMessage(raw: any) { 101 | const msg = JSON.parse(raw); 102 | const [wssRemoteId, type] = msg.channel.split("_"); 103 | const remoteId = this.remoteIdMap.get(wssRemoteId); 104 | 105 | // prevent errors from crashing the party 106 | if (msg.success === false) { 107 | return; 108 | } 109 | 110 | // tickers 111 | if (type === "ticker") { 112 | const market = this._tickerSubs.get(remoteId); 113 | if (!market) return; 114 | 115 | const ticker = this._constructTicker(msg, market); 116 | this.emit("ticker", ticker, market); 117 | return; 118 | } 119 | 120 | // trades 121 | if (type === "trades") { 122 | for (const datum of msg.data) { 123 | const market = this._tradeSubs.get(remoteId); 124 | if (!market) return; 125 | 126 | const trade = this._constructTradesFromMessage(datum, market); 127 | this.emit("trade", trade, market); 128 | } 129 | return; 130 | } 131 | 132 | // level2snapshots 133 | if (type === "depth") { 134 | const market = this._level2SnapshotSubs.get(remoteId); 135 | if (!market) return; 136 | 137 | const snapshot = this._constructLevel2Snapshot(msg, market); 138 | this.emit("l2snapshot", snapshot, market); 139 | return; 140 | } 141 | } 142 | 143 | protected _constructTicker(data, market) { 144 | const timestamp = parseInt(data.date); 145 | const ticker = data.ticker; 146 | return new Ticker({ 147 | exchange: "ZB", 148 | base: market.base, 149 | quote: market.quote, 150 | timestamp, 151 | last: ticker.last, 152 | open: undefined, 153 | high: ticker.high, 154 | low: ticker.low, 155 | volume: ticker.vol, 156 | quoteVolume: undefined, 157 | change: undefined, 158 | changePercent: undefined, 159 | bid: ticker.buy, 160 | ask: ticker.sell, 161 | }); 162 | } 163 | 164 | protected _constructTradesFromMessage(datum, market) { 165 | const { date, price, amount, tid, type } = datum; 166 | return new Trade({ 167 | exchange: "ZB", 168 | base: market.base, 169 | quote: market.quote, 170 | tradeId: tid.toString(), 171 | side: type, 172 | unix: parseInt(date) * 1000, 173 | price, 174 | amount, 175 | }); 176 | } 177 | 178 | protected _constructLevel2Snapshot(msg, market) { 179 | let { timestamp, asks, bids } = msg; 180 | asks = asks.map(p => new Level2Point(p[0].toFixed(8), p[1].toFixed(8))).reverse(); 181 | bids = bids.map(p => new Level2Point(p[0].toFixed(8), p[1].toFixed(8))); 182 | return new Level2Snapshot({ 183 | exchange: "ZB", 184 | base: market.base, 185 | quote: market.quote, 186 | timestampMs: timestamp * 1000, 187 | asks, 188 | bids, 189 | }); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/flowcontrol/Batch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-implied-eval */ 4 | 5 | import { CancelableFn, Fn } from "./Fn"; 6 | 7 | export class Batch { 8 | protected _handle: NodeJS.Timeout; 9 | protected _args: any[]; 10 | 11 | constructor(readonly fn: Fn, readonly batchSize: number, readonly collectMs: number = 0) { 12 | this._handle; 13 | this._args = []; 14 | } 15 | 16 | public add(...args) { 17 | this._args.push(args); 18 | this._unschedule(); 19 | this._schedule(); 20 | } 21 | 22 | public cancel() { 23 | this._unschedule(); 24 | this._args = []; 25 | } 26 | 27 | protected _unschedule() { 28 | clearTimeout(this._handle); 29 | } 30 | 31 | protected _schedule() { 32 | this._handle = setTimeout(this._process.bind(this), this.collectMs); 33 | if (this._handle.unref) { 34 | this._handle.unref(); 35 | } 36 | } 37 | 38 | protected _process() { 39 | if (!this._args.length) return; 40 | while (this._args.length) { 41 | this.fn(this._args.splice(0, this.batchSize)); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Batcher allows repeated calls to a function but will delay execution of the 48 | * until the next tick or a timeout expires. Upon expiration, the function is 49 | * called with the arguments of the calls batched by the batch size 50 | * 51 | * @example 52 | * const fn = n => console.log(n); 53 | * const batchFn = batch(fn, debounceMs); 54 | * batchFn(1); 55 | * batchFn(2); 56 | * batchFn(3); 57 | * // [[1],[2],[3]] 58 | */ 59 | export function batch( 60 | fn: Fn, 61 | batchSize: number = Number.MAX_SAFE_INTEGER, 62 | collectMs: number = 0, 63 | ): CancelableFn { 64 | const inst = new Batch(fn, batchSize, collectMs); 65 | const add = inst.add.bind(inst); 66 | add.cancel = inst.cancel.bind(inst); 67 | return add as CancelableFn; 68 | } 69 | -------------------------------------------------------------------------------- /src/flowcontrol/CircularBuffer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | 3 | /** 4 | * Implements a fast fixed size circular buffer. This buffer has O(1) 5 | * reads and write. The fixed size is limited to n-1 values in the 6 | * buffer. The final value is used as a marker to indicate that the 7 | * buffer is full. This trades a small amount of space for performance 8 | * by not requiring maintenance of a counter. 9 | * 10 | * In benchmarks this performs ~50,000 ops/sec which is twice as fast 11 | * as the `double-ended-queue` library. 12 | */ 13 | export class CircularBuffer { 14 | public buffer: T[]; 15 | public writePos: number; 16 | public readPos: number; 17 | 18 | constructor(readonly size: number) { 19 | this.buffer = new Array(size).fill(undefined); 20 | this.writePos = 0; 21 | this.readPos = 0; 22 | } 23 | 24 | /** 25 | * Writes a value into the buffer. Returns `false` if the buffer is 26 | * full. Otherwise returns `true`. 27 | * 28 | * @remarks 29 | * 30 | * The `writePos` is incremented prior to writing. This allows the 31 | * `readPos` to chase the `writePos` and allows us to not require a 32 | * counter that needs to be maintained. 33 | */ 34 | public write(val: T) { 35 | const newPos = (this.writePos + 1) % this.size; 36 | if (newPos === this.readPos) return false; 37 | this.writePos = newPos; 38 | this.buffer[this.writePos] = val; 39 | return true; 40 | } 41 | 42 | /** 43 | * Reads the next value from the circular buffer. Returns `undefined` 44 | * when there is no data in the buffer. 45 | * 46 | * @remarks 47 | * 48 | * The `readPos` will chase the `writePos` and we increment the 49 | * `readPos` prior to reading in the same way that we increment teh 50 | * `writePos` prior to writing. 51 | */ 52 | public read() { 53 | if (this.readPos === this.writePos) return; // empty 54 | this.readPos = (this.readPos + 1) % this.size; 55 | const val = this.buffer[this.readPos]; 56 | this.buffer[this.readPos] = undefined; 57 | return val; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/flowcontrol/Debounce.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-implied-eval */ 4 | 5 | import { CancelableFn, Fn } from "./Fn"; 6 | 7 | export class Debounce { 8 | protected _handle: NodeJS.Timeout; 9 | protected _last: any; 10 | 11 | constructor(readonly fn: Fn, readonly waitMs: number = 100) { 12 | this._handle; 13 | this._last; 14 | } 15 | 16 | public add(...args: any[]) { 17 | this._last = args; 18 | this._unschedule(); 19 | this._schedule(); 20 | } 21 | 22 | public cancel() { 23 | this._unschedule(); 24 | this._last = undefined; 25 | } 26 | 27 | protected _unschedule() { 28 | clearTimeout(this._handle); 29 | } 30 | 31 | protected _schedule() { 32 | this._handle = setTimeout(this._process.bind(this), this.waitMs); 33 | if (this._handle.unref) { 34 | this._handle.unref(); 35 | } 36 | } 37 | 38 | protected _process() { 39 | if (!this._last) return; 40 | this.fn(...this._last); 41 | } 42 | } 43 | 44 | /** 45 | * Debounce allows repeated calls to a function but will delay execution of the 46 | * function until a a timeout period expires. Upon expiration, the function is 47 | * called with the last value that was provided 48 | * 49 | * @example 50 | * const debounceMs = 100; 51 | * const fn = n => console.log(n, new Date()); 52 | * const debouncedFn = debounce(fn, debounceMs); 53 | * debouncedFn('h'); 54 | * debouncedFn('he'); 55 | * debouncedFn('hel'); 56 | * debouncedFn('hell'); 57 | * debouncedFn('hello'); 58 | */ 59 | export function debounce(fn: Fn, debounceMs: number = 100): CancelableFn { 60 | const i = new Debounce(fn, debounceMs); 61 | const add = i.add.bind(i); 62 | add.cancel = i.cancel.bind(i); 63 | return add as CancelableFn; 64 | } 65 | -------------------------------------------------------------------------------- /src/flowcontrol/Fn.ts: -------------------------------------------------------------------------------- 1 | export type Fn = (...args: any[]) => void; 2 | export type CancelableFn = { (...args: any[]): void; cancel: () => void }; 3 | -------------------------------------------------------------------------------- /src/flowcontrol/Queue.ts: -------------------------------------------------------------------------------- 1 | import { CircularBuffer } from "./CircularBuffer"; 2 | 3 | /** 4 | * Implements a fast FIFO Queue using a circular buffer. 5 | */ 6 | export class Queue { 7 | public buffer: CircularBuffer; 8 | 9 | constructor(readonly bufferSize = 1 << 12) { 10 | this.buffer = new CircularBuffer(bufferSize); 11 | } 12 | 13 | public shift(): T { 14 | return this.buffer.read(); 15 | } 16 | 17 | public push(val: T) { 18 | if (!this.buffer.write(val)) { 19 | this._resize(); 20 | this.buffer.write(val); 21 | } 22 | } 23 | 24 | protected _resize() { 25 | // construct a new buffer 26 | const newBuf = new CircularBuffer(this.buffer.size * 2); 27 | 28 | // eslint-disable-next-line no-constant-condition 29 | while (true) { 30 | const val = this.buffer.read(); 31 | if (val === undefined) break; 32 | newBuf.write(val); 33 | } 34 | 35 | this.buffer = newBuf; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/flowcontrol/Throttle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-implied-eval */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | 5 | import { CancelableFn, Fn } from "./Fn"; 6 | 7 | export class Throttle { 8 | private _calls: any[][]; 9 | private _handle: NodeJS.Timeout; 10 | 11 | constructor(readonly fn: Fn, readonly delayMs: number) { 12 | this._calls = []; 13 | this._handle; 14 | this.add = this.add.bind(this); 15 | } 16 | 17 | public add(...args: any[]) { 18 | this._calls.push(args); 19 | if (!this._handle) this._process(); 20 | } 21 | 22 | public cancel() { 23 | this._unschedule(); 24 | this._calls = []; 25 | } 26 | 27 | private _unschedule() { 28 | clearTimeout(this._handle); 29 | this._handle = undefined; 30 | } 31 | 32 | private _schedule() { 33 | this._handle = setTimeout(this._process.bind(this), this.delayMs); 34 | if (this._handle.unref) { 35 | this._handle.unref(); 36 | } 37 | } 38 | 39 | private _process() { 40 | this._handle = undefined; 41 | const args = this._calls.shift(); 42 | if (args) { 43 | this.fn(...args); 44 | this._schedule(); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Throttles the function execution to the rate limit specified. This can be 51 | * used "enqueue" a bunch of function executes and limit the rate at which they 52 | * will be called. 53 | * 54 | * @example 55 | * ```javascript 56 | * const fn = n => console.log(n, new Date()); 57 | * const delayMs = 1000; 58 | * const throttledFn = throttle(fn, delayMs); 59 | * throttledFn(1); 60 | * throttledFn(2); 61 | * throttledFn(3); 62 | * ``` 63 | */ 64 | export function throttle(fn: Fn, delayMs: number): CancelableFn { 65 | const inst = new Throttle(fn, delayMs); 66 | const add = inst.add.bind(inst); 67 | add.cancel = inst.cancel.bind(inst); 68 | return add as CancelableFn; 69 | } 70 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BasicClient } from "./BasicClient"; 2 | import { BasicMultiClient } from "./BasicMultiClient"; 3 | import { SmartWss } from "./SmartWss"; 4 | import { Watcher } from "./Watcher"; 5 | 6 | import { Auction } from "./Auction"; 7 | import { BlockTrade } from "./BlockTrade"; 8 | import { Candle } from "./Candle"; 9 | import { CandlePeriod } from "./CandlePeriod"; 10 | import { Level2Point } from "./Level2Point"; 11 | import { Level2Snapshot } from "./Level2Snapshots"; 12 | import { Level2Update } from "./Level2Update"; 13 | import { Level3Point } from "./Level3Point"; 14 | import { Level3Snapshot } from "./Level3Snapshot"; 15 | import { Level3Update } from "./Level3Update"; 16 | import { Ticker } from "./Ticker"; 17 | import { Trade } from "./Trade"; 18 | 19 | import { BiboxClient } from "./exchanges/BiboxClient"; 20 | import { BinanceClient } from "./exchanges/BinanceClient"; 21 | import { BinanceFuturesCoinmClient } from "./exchanges/BinanceFuturesCoinmClient"; 22 | import { BinanceFuturesUsdtmClient } from "./exchanges/BinanceFuturesUsdtmClient"; 23 | import { BinanceJeClient } from "./exchanges/BinanceJeClient"; 24 | import { BinanceUsClient } from "./exchanges/BinanceUsClient"; 25 | import { BitfinexClient } from "./exchanges/BitfinexClient"; 26 | import { BitflyerClient } from "./exchanges/BitflyerClient"; 27 | import { BithumbClient } from "./exchanges/BithumbClient"; 28 | import { BitmexClient } from "./exchanges/BitmexClient"; 29 | import { BitstampClient } from "./exchanges/BitstampClient"; 30 | import { BittrexClient } from "./exchanges/BittrexClient"; 31 | import { CexClient } from "./exchanges/CexClient"; 32 | import { CoinbaseProClient } from "./exchanges/CoinbaseProClient"; 33 | import { CoinexClient } from "./exchanges/CoinexClient"; 34 | import { DeribitClient } from "./exchanges/DeribitClient"; 35 | import { DigifinexClient } from "./exchanges/DigifinexClient"; 36 | import { ErisXClient } from "./exchanges/ErisxClient"; 37 | import { FtxClient } from "./exchanges/FtxClient"; 38 | import { FtxUsClient } from "./exchanges/FtxUsClient"; 39 | import { GateioClient } from "./exchanges/GateioClient"; 40 | import { GeminiClient } from "./exchanges/Geminiclient"; 41 | import { HitBtcClient } from "./exchanges/HitBtcClient"; 42 | import { HuobiClient } from "./exchanges/HuobiClient"; 43 | import { HuobiFuturesClient } from "./exchanges/HuobiFuturesClient"; 44 | import { HuobiJapanClient } from "./exchanges/HuobiJapanClient"; 45 | import { HuobiKoreaClient } from "./exchanges/HuobiKoreaClient"; 46 | import { HuobiSwapsClient } from "./exchanges/HuobiSwapsClient"; 47 | import { KrakenClient } from "./exchanges/KrakenClient"; 48 | import { KucoinClient } from "./exchanges/KucoinClient"; 49 | import { LedgerXClient } from "./exchanges/LedgerXClient"; 50 | import { LiquidClient } from "./exchanges/LiquidClient"; 51 | import { OkexClient } from "./exchanges/OkexClient"; 52 | import { PoloniexClient } from "./exchanges/PoloniexClient"; 53 | import { UpbitClient } from "./exchanges/UpbitClient"; 54 | import { ZbClient } from "./exchanges/ZbClient"; 55 | 56 | export { 57 | // 58 | // Base clients 59 | BasicClient, 60 | BasicMultiClient, 61 | SmartWss, 62 | Watcher, 63 | // 64 | // Event types 65 | Auction, 66 | BlockTrade, 67 | Candle, 68 | CandlePeriod, 69 | Level2Point, 70 | Level2Snapshot, 71 | Level2Update, 72 | Level3Point, 73 | Level3Snapshot, 74 | Level3Update, 75 | Ticker, 76 | Trade, 77 | // 78 | // Clients 79 | BiboxClient, 80 | BinanceClient, 81 | BinanceFuturesCoinmClient, 82 | BinanceFuturesUsdtmClient, 83 | BinanceJeClient, 84 | BinanceUsClient, 85 | BitfinexClient, 86 | BitflyerClient, 87 | BithumbClient, 88 | BitmexClient, 89 | BitstampClient, 90 | BittrexClient, 91 | CexClient, 92 | CoinbaseProClient, 93 | CoinexClient, 94 | DeribitClient, 95 | DigifinexClient, 96 | ErisXClient, 97 | FtxClient, 98 | FtxUsClient, 99 | GateioClient, 100 | GeminiClient, 101 | HitBtcClient, 102 | HuobiClient, 103 | HuobiFuturesClient, 104 | HuobiSwapsClient, 105 | HuobiJapanClient, 106 | HuobiKoreaClient, 107 | KucoinClient, 108 | KrakenClient, 109 | LedgerXClient, 110 | LiquidClient, 111 | OkexClient, 112 | PoloniexClient, 113 | UpbitClient, 114 | ZbClient, 115 | }; 116 | 117 | /** 118 | * @deprecated Use named imports instead of default import. Client 119 | * names have also changed and are now suffixed with `Client`. Deprecation 120 | * warning added in v0.46.0 and will be removed in a future version. 121 | */ 122 | export default { 123 | Bibox: BiboxClient, 124 | Binance: BinanceClient, 125 | BinanceFuturesCoinM: BinanceFuturesCoinmClient, 126 | BinanceFuturesUsdtM: BinanceFuturesUsdtmClient, 127 | BinanceJe: BinanceJeClient, 128 | BinanceUs: BinanceUsClient, 129 | Bitfinex: BitfinexClient, 130 | Bitflyer: BitflyerClient, 131 | Bithumb: BithumbClient, 132 | BitMEX: BitmexClient, 133 | Bitstamp: BitstampClient, 134 | Bittrex: BittrexClient, 135 | Cex: CexClient, 136 | CoinbasePro: CoinbaseProClient, 137 | Coinex: CoinexClient, 138 | Deribit: DeribitClient, 139 | Digifinex: DigifinexClient, 140 | ErisX: ErisXClient, 141 | Ftx: FtxClient, 142 | FtxUs: FtxUsClient, 143 | Gateio: GateioClient, 144 | Gemini: GeminiClient, 145 | HitBTC: HitBtcClient, 146 | Huobi: HuobiClient, 147 | HuobiFutures: HuobiFuturesClient, 148 | HuobiSwaps: HuobiSwapsClient, 149 | HuobiJapan: HuobiJapanClient, 150 | HuobiKorea: HuobiKoreaClient, 151 | Kucoin: KucoinClient, 152 | Kraken: KrakenClient, 153 | LedgerX: LedgerXClient, 154 | Liquid: LiquidClient, 155 | OKEx: OkexClient, 156 | Poloniex: PoloniexClient, 157 | Upbit: UpbitClient, 158 | Zb: ZbClient, 159 | }; 160 | -------------------------------------------------------------------------------- /src/orderbooks/DeribitOrderBook.ts: -------------------------------------------------------------------------------- 1 | import { Level2Snapshot } from "../Level2Snapshots"; 2 | import { Level2Update } from "../Level2Update"; 3 | import { L2Point } from "./L2Point"; 4 | 5 | export class DeribitOrderBook { 6 | public sequenceId: number; 7 | public asks: L2Point[]; 8 | public bids: L2Point[]; 9 | constructor(snapshot: Level2Snapshot) { 10 | this.sequenceId = snapshot.sequenceId; 11 | this.asks = snapshot.asks 12 | .map(p => new L2Point(Number(p.price), Number(p.size), snapshot.timestampMs)) 13 | .sort(sortDesc); 14 | 15 | this.bids = snapshot.bids 16 | .map(p => new L2Point(Number(p.price), Number(p.size), snapshot.timestampMs)) 17 | .sort(sortAsc); 18 | } 19 | 20 | public update(update: Level2Update) { 21 | this.sequenceId = update.sequenceId; 22 | 23 | for (const ask of update.asks) { 24 | this._updatePoint(false, Number(ask.price), Number(ask.size), update.timestampMs); 25 | } 26 | 27 | for (const bid of update.bids) { 28 | this._updatePoint(true, Number(bid.price), Number(bid.size), update.timestampMs); 29 | } 30 | } 31 | 32 | protected _updatePoint(bid: boolean, price: number, size: number, timestamp: number) { 33 | let arr: L2Point[]; 34 | let index: number; 35 | 36 | // The best bids are the highest priced, meaning the tail of array 37 | // using the best bids would be sorted ascending 38 | if (bid) { 39 | arr = this.bids; 40 | index = findIndexAsc(arr, price); 41 | } 42 | // The best asks are the lowest priced, meaning the tail of array 43 | // with the best asks would be sorted descending 44 | else { 45 | arr = this.asks; 46 | index = findIndexDesc(arr, price); 47 | } 48 | 49 | // We perform an update when the index of hte current value has 50 | // the same price as the update we are now processing. 51 | if (arr[index] && arr[index].price === price) { 52 | // Remove the value when the size is 0 53 | if (Number(size) === 0) { 54 | arr.splice(index, 1); 55 | return; 56 | } 57 | 58 | // Otherwise we perform an update by changing the size 59 | arr[index].size = size; 60 | arr[index].timestamp = timestamp; 61 | } 62 | 63 | // Otherwise we are performing an insert, which we will construct 64 | // a new point. Because we are using splice, which should have a 65 | // worst case runtime of O(N), we 66 | // O() 67 | else if (Number(size) > 0) { 68 | const point = new L2Point(price, size, timestamp); 69 | arr.splice(index, 0, point); 70 | } 71 | } 72 | 73 | /** 74 | * Captures a simple snapshot of best asks and bids up to the 75 | * requested depth. 76 | */ 77 | public snapshot(depth: number): { sequenceId: number; asks: L2Point[]; bids: L2Point[] } { 78 | const asks = []; 79 | for (let i = this.asks.length - 1; i >= this.asks.length - depth; i--) { 80 | const val = this.asks[i]; 81 | if (val) asks.push(val); 82 | } 83 | const bids = []; 84 | for (let i = this.bids.length - 1; i >= this.bids.length - depth; i--) { 85 | const val = this.bids[i]; 86 | if (val) bids.push(val); 87 | } 88 | return { 89 | sequenceId: this.sequenceId, 90 | asks, 91 | bids, 92 | }; 93 | } 94 | } 95 | 96 | /** 97 | * Performs a binary search of a sorted array for the insert or update 98 | * position of the value and operates on a KrakenOrderBookPoint value 99 | */ 100 | function findIndexAsc(arr: L2Point[], key: number, l: number = 0, r: number = arr.length): number { 101 | const mid = Math.floor((l + r) / 2); 102 | if (l === r) return mid; 103 | if (arr[mid] && arr[mid].price === key) return mid; 104 | if (arr[mid] && arr[mid].price > key) return findIndexAsc(arr, key, l, mid); 105 | if (arr[mid] && arr[mid].price < key) return findIndexAsc(arr, key, mid + 1, r); 106 | } 107 | 108 | /** 109 | * Performs a binary search of a sorted array for the insert or update 110 | * position of the value and operates on a KrakenOrderBookPoint value 111 | */ 112 | function findIndexDesc(arr: L2Point[], key: number, l: number = 0, r: number = arr.length): number { 113 | const mid = Math.floor((l + r) / 2); 114 | if (l === r) return mid; 115 | if (arr[mid] && arr[mid].price === key) return mid; 116 | if (arr[mid] && arr[mid].price < key) return findIndexDesc(arr, key, l, mid); 117 | if (arr[mid] && arr[mid].price > key) return findIndexDesc(arr, key, mid + 1, r); 118 | } 119 | 120 | /** 121 | * Sorts points from high to low 122 | */ 123 | function sortDesc(a: L2Point, b: L2Point): number { 124 | if (a.price > b.price) return -1; 125 | if (a.price < b.price) return 1; 126 | return 0; 127 | } 128 | 129 | /** 130 | * Sorts points from low to high 131 | */ 132 | function sortAsc(a: L2Point, b: L2Point) { 133 | if (a.price < b.price) return -1; 134 | if (a.price > b.price) return 1; 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /src/orderbooks/ErisXOrderBook.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | import { Level3Point } from "../Level3Point"; 4 | import { Level3Snapshot } from "../Level3Snapshot"; 5 | import { Level3Update } from "../Level3Update"; 6 | import { L3Point } from "./L3Point"; 7 | import { L3PointStore } from "./L3PointStore"; 8 | 9 | /** 10 | * Maintains a Level 3 order book for ErisX 11 | */ 12 | export class ErisXOrderBook { 13 | public asks: L3PointStore; 14 | public bids: L3PointStore; 15 | public timestampMs: number; 16 | public runId: number; 17 | public sequenceId: number; 18 | 19 | constructor(snap: Level3Snapshot) { 20 | this.asks = new L3PointStore(); 21 | this.bids = new L3PointStore(); 22 | this.timestampMs = snap.timestampMs; 23 | this.runId = 0; 24 | 25 | for (const ask of snap.asks) { 26 | this.asks.set(new L3Point(ask.orderId, Number(ask.price), Number(ask.size))); 27 | } 28 | 29 | for (const bid of snap.bids) { 30 | this.bids.set(new L3Point(bid.orderId, Number(bid.price), Number(bid.size))); 31 | } 32 | } 33 | 34 | public update(update: Level3Update) { 35 | this.timestampMs = update.timestampMs; 36 | 37 | for (const point of update.asks) { 38 | this.updatePoint(point, false); 39 | } 40 | 41 | for (const point of update.bids) { 42 | this.updatePoint(point, false); 43 | } 44 | } 45 | 46 | public updatePoint(point: Level3Point, isAsk: boolean) { 47 | const map = isAsk ? this.asks : this.bids; 48 | 49 | const orderId = point.orderId; 50 | const price = Number(point.price); 51 | const size = Number(point.size); 52 | const type = point.meta.type; 53 | 54 | if (type === "DELETE") { 55 | map.delete(orderId); 56 | return; 57 | } else if (type === "NEW") { 58 | map.set(new L3Point(orderId, price, size)); 59 | } else { 60 | throw new Error("Unknown type"); 61 | } 62 | } 63 | 64 | public snapshot(depth = 10) { 65 | return { 66 | sequenceId: this.sequenceId, 67 | runId: this.runId, 68 | asks: this.asks.snapshot(depth, "asc"), 69 | bids: this.bids.snapshot(depth, "desc"), 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/orderbooks/KucoinOrderBook.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import { Level3Snapshot } from "../Level3Snapshot"; 3 | import { Level3Update } from "../Level3Update"; 4 | import { L2Point } from "./L2Point"; 5 | import { L3Point } from "./L3Point"; 6 | 7 | /** 8 | * Prototype for maintaining a Level 3 order book for Kucoin according 9 | * to the instructions defined here: 10 | * https://docs.kucoin.com/#full-matchengine-data-level-3 11 | * 12 | * This technique uses a Map to store orders. It has efficient updates 13 | * but will be slow for performing tip of book or snapshot operations. 14 | * 15 | * # Example 16 | * ```javascript 17 | * const ccxws = require("ccxws"); 18 | * const KucoinOrderBook = require("ccxws/src/orderbooks/KucoinOrderBook"); 19 | * 20 | * let market = { id: "BTC-USDT", base: "BTC", quote: "USDT" }; 21 | * let updates = []; 22 | * let ob; 23 | * 24 | * const client = new ccxws.Kucoin(); 25 | * client.subscribeLevel3Updates(market); 26 | * client.on("l3snapshot", snapshot => { 27 | * ob = new KucoinOrderBook(snapshot, updates); 28 | * }); 29 | * 30 | * client.on("l3update", update => { 31 | * // enqueue updates until snapshot arrives 32 | * if (!ob) { 33 | * updates.push(update); 34 | * return; 35 | * } 36 | * 37 | * // validate the sequence and exit if we are out of sync 38 | * if (ob.sequenceId + 1 !== update.sequenceId) { 39 | * console.log(`out of sync, expected ${ob.sequenceId + 1}, got ${update.sequenceId}`); 40 | * process.exit(1); 41 | * } 42 | * 43 | * // apply update 44 | * ob.update(update); 45 | * }); 46 | * ``` 47 | */ 48 | export class KucoinOrderBook { 49 | public asks: Map; 50 | public bids: Map; 51 | public sequenceId: number; 52 | 53 | /** 54 | * Constructs a new order book by starting with a snapshop and replaying 55 | * any updates that have been queued. 56 | */ 57 | constructor(snap: Level3Snapshot, updates: Level3Update[]) { 58 | this.asks = new Map(); 59 | this.bids = new Map(); 60 | this.sequenceId = snap.sequenceId; 61 | 62 | // Verify that we have queued updates 63 | if (!updates.length || snap.sequenceId >= updates[updates.length - 1].sequenceId) { 64 | throw new Error("Must queue updates prior to snapshot"); 65 | } 66 | 67 | // apply asks from snapshot 68 | for (const ask of snap.asks) { 69 | this.asks.set( 70 | ask.orderId, 71 | new L3Point( 72 | ask.orderId, 73 | Number(ask.price), 74 | Number(ask.size), 75 | Number(ask.meta.timestampMs), 76 | ), 77 | ); 78 | } 79 | 80 | // apply bids from snapshot 81 | for (const bid of snap.bids) { 82 | this.bids.set( 83 | bid.orderId, 84 | new L3Point( 85 | bid.orderId, 86 | Number(bid.price), 87 | Number(bid.size), 88 | Number(bid.meta.timestampMs), 89 | ), 90 | ); 91 | } 92 | 93 | // Replay pending updates 94 | for (const update of updates) { 95 | // Ignore updates that are prior to the snapshot 96 | if (update.sequenceId <= this.sequenceId) continue; 97 | 98 | // Ensure that we are in sync 99 | if (update.sequenceId > this.sequenceId + 1) { 100 | throw new Error("Missing update"); 101 | } 102 | 103 | this.update(update); 104 | } 105 | } 106 | 107 | public update(update: Level3Update) { 108 | // Always update the sequence 109 | this.sequenceId = update.sequenceId; 110 | 111 | // find the point in the update 112 | const updatePoint = update.asks[0] || update.bids[0]; 113 | 114 | // Skip received orders 115 | if (updatePoint.meta.type === "received") return; 116 | 117 | // Open - insert a new point in the appropriate side (ask, bid). 118 | // When receiving a message with price="", size="0", 119 | // it means this is a hidden order and we can ignore it. 120 | if (updatePoint.meta.type === "open") { 121 | const map = update.asks[0] ? this.asks : this.bids; 122 | 123 | // Ignore private orders 124 | if (!Number(updatePoint.price) && !Number(updatePoint.size)) { 125 | return; 126 | } 127 | 128 | const obPoint = new L3Point( 129 | updatePoint.orderId, 130 | Number(updatePoint.price), 131 | Number(updatePoint.size), 132 | update.timestampMs, 133 | ); 134 | 135 | map.set(obPoint.orderId, obPoint); 136 | return; 137 | } 138 | 139 | // Done - remove the order, this won't include the side, so we 140 | // remove it from both side. 141 | if (updatePoint.meta.type === "done") { 142 | this.asks.delete(updatePoint.orderId); 143 | this.bids.delete(updatePoint.orderId); 144 | return; 145 | } 146 | 147 | // Change - modify the amount for the order. Update will be in both 148 | // the asks and bids since the update message doesn't include a 149 | // side. Change messages are sent when an order changes in size. 150 | // This includes resting orders (open) as well as recieved but not 151 | // yet open. In the latter case, no point will exist on the book 152 | // yet. 153 | if (updatePoint.meta.type === "update") { 154 | const obPoint = this.asks.get(updatePoint.orderId) || this.bids.get(updatePoint.orderId); // prettier-ignore 155 | if (obPoint) obPoint.size = Number(updatePoint.size); 156 | return; 157 | } 158 | 159 | // Trade - reduce the size of the maker to the remain size. We ignore 160 | // any updates if the remainSize is zero, since the done event may 161 | // have already removed the trae 162 | if (updatePoint.meta.type === "match") { 163 | const obPoint = 164 | this.asks.get(updatePoint.orderId) || this.bids.get(updatePoint.orderId); 165 | if (obPoint) obPoint.size = Number(updatePoint.size); 166 | return; 167 | } 168 | } 169 | 170 | /** 171 | * Captures a price aggregated snapshot 172 | * @param {number} depth 173 | */ 174 | public snapshot(depth = 10) { 175 | return { 176 | sequenceId: this.sequenceId, 177 | asks: snapSide(this.asks, sortAsc, depth), 178 | bids: snapSide(this.bids, sortDesc, depth), 179 | }; 180 | } 181 | } 182 | 183 | function snapSide( 184 | map: Map, 185 | sorter: (a: L2Point, b: L2Point) => number, 186 | depth: number, 187 | ) { 188 | const aggMap = aggByPrice(map); 189 | return Array.from(aggMap.values()).sort(sorter).slice(0, depth); 190 | } 191 | 192 | function aggByPrice(map: Map) { 193 | // Aggregate the values into price points 194 | const aggMap: Map = new Map(); 195 | for (const point of map.values()) { 196 | const price = point.price; 197 | const size = point.size; 198 | const timestamp = point.timestamp; 199 | 200 | // If we don't have this price point in the aggregate then we create 201 | // a new price point with empty values. 202 | if (!aggMap.has(price)) { 203 | aggMap.set(price, new L2Point(price, 0, 0)); 204 | } 205 | 206 | // Obtain the price point from the aggregation 207 | const aggPoint = aggMap.get(price); 208 | 209 | // Update the size 210 | aggPoint.size += size; 211 | 212 | // Update the timestamp 213 | if (aggPoint.timestamp < timestamp) aggPoint.timestamp = timestamp; 214 | } 215 | 216 | return aggMap; 217 | } 218 | 219 | function sortAsc(a: L2Point, b: L2Point): number { 220 | return a.price - b.price; 221 | } 222 | 223 | function sortDesc(a: L2Point, b: L2Point) { 224 | return b.price - a.price; 225 | } 226 | -------------------------------------------------------------------------------- /src/orderbooks/L2Point.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | export class L2Point { 3 | public price: number; 4 | public size: number; 5 | public timestamp: number; 6 | public meta: any; 7 | 8 | constructor(price: number, size: number, timestamp?: number, meta?: any) { 9 | this.price = price; 10 | this.size = size; 11 | this.timestamp = timestamp; 12 | this.meta = meta; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/orderbooks/L3Point.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Level 3 order book point 3 | */ 4 | export class L3Point { 5 | public orderId: string; 6 | public price: number; 7 | public size: number; 8 | public timestamp: number; 9 | 10 | constructor(orderId: string, price: number, size: number, timestamp?: number) { 11 | this.orderId = orderId; 12 | this.price = price; 13 | this.size = size; 14 | this.timestamp = timestamp; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/orderbooks/L3PointStore.ts: -------------------------------------------------------------------------------- 1 | import { L2Point } from "./L2Point"; 2 | import { L3Point } from "./L3Point"; 3 | 4 | export class L3PointStore { 5 | public points: Map; 6 | 7 | constructor() { 8 | this.points = new Map(); 9 | } 10 | 11 | public get(orderId: string): L3Point { 12 | return this.points.get(orderId); 13 | } 14 | 15 | public set(point: L3Point) { 16 | this.points.set(point.orderId, point); 17 | } 18 | 19 | public delete(orderId: string) { 20 | this.points.delete(orderId); 21 | } 22 | 23 | public has(orderId: string): boolean { 24 | return this.points.has(orderId); 25 | } 26 | 27 | public clear() { 28 | this.points.clear(); 29 | } 30 | 31 | public snapshot(depth: number, dir: "asc" | "desc"): L2Point[] { 32 | let sorter; 33 | switch (dir) { 34 | case "asc": 35 | sorter = sortAsc; 36 | break; 37 | case "desc": 38 | sorter = sortDesc; 39 | break; 40 | default: 41 | throw new Error("Unknown sorter"); 42 | } 43 | 44 | return Array.from(aggByPrice(this.points).values()).sort(sorter).slice(0, depth); 45 | } 46 | } 47 | 48 | function aggByPrice(map: Map): Map { 49 | // Aggregate the values into price points 50 | const aggMap: Map = new Map(); 51 | for (const point of map.values()) { 52 | const price = Number(point.price); 53 | const size = Number(point.size); 54 | 55 | // If we don't have this price point in the aggregate then we create 56 | // a new price point with empty values. 57 | if (!aggMap.has(price)) { 58 | aggMap.set(price, new L2Point(price, 0, 0)); 59 | } 60 | 61 | // Obtain the price point from the aggregation 62 | const aggPoint = aggMap.get(price); 63 | 64 | // Update the size 65 | aggPoint.size += size; 66 | } 67 | 68 | return aggMap; 69 | } 70 | 71 | function sortAsc(a: L3Point, b: L3Point) { 72 | return a.price - b.price; 73 | } 74 | 75 | function sortDesc(a: L3Point, b: L3Point) { 76 | return b.price - a.price; 77 | } 78 | -------------------------------------------------------------------------------- /src/orderbooks/LedgerXOrderBook.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | import { Level3Snapshot } from "../Level3Snapshot"; 4 | import { Level3Update } from "../Level3Update"; 5 | import { L3Point } from "./L3Point"; 6 | import { L3PointStore } from "./L3PointStore"; 7 | 8 | /** 9 | * Maintains a Level 3 order book for LedgerX 10 | */ 11 | export class LedgerXOrderBook { 12 | public asks: L3PointStore; 13 | public bids: L3PointStore; 14 | public sequenceId: number; 15 | public runId: number; 16 | 17 | constructor(snap: Level3Snapshot) { 18 | this.asks = new L3PointStore(); 19 | this.bids = new L3PointStore(); 20 | this.sequenceId = snap.sequenceId; 21 | this.runId = 0; 22 | 23 | for (const ask of snap.asks) { 24 | this.asks.set(new L3Point(ask.orderId, Number(ask.price), Number(ask.size))); 25 | } 26 | 27 | for (const bid of snap.bids) { 28 | this.bids.set(new L3Point(bid.orderId, Number(bid.price), Number(bid.size))); 29 | } 30 | } 31 | 32 | public reset() { 33 | this.sequenceId = 0; 34 | this.asks.clear(); 35 | this.bids.clear(); 36 | } 37 | 38 | public update(update: Level3Update & { runId: number; timestamp: number }) { 39 | this.sequenceId += 1; 40 | 41 | // Capture the runId of the first update 42 | if (this.runId === 0) { 43 | this.runId = update.runId; 44 | } 45 | // Handle when the run_id changes and we need to reset things 46 | else if (update.runId > this.runId) { 47 | this.reset(); 48 | } 49 | 50 | // Handle if we have odd data for some reason 51 | if (update.asks.length > 1 || update.bids.length > 1) { 52 | throw new Error("Malformed update"); 53 | } 54 | 55 | // Extract the update 56 | const isAsk = update.asks.length > 0; 57 | 58 | const value = isAsk ? update.asks[0] : update.bids[0]; 59 | const map = isAsk ? this.asks : this.bids; 60 | 61 | const orderId = value.orderId; 62 | const price = Number(value.price); 63 | const size = Number(value.size); 64 | const timestamp = (value as any).timestamp; 65 | 66 | // Handle deleting the point 67 | if (size === 0) { 68 | map.delete(orderId); 69 | return; 70 | } 71 | 72 | // Next try to obtain the point 73 | 74 | // Update existing point 75 | if (map.has(orderId)) { 76 | const point = map.get(orderId); 77 | point.price = price; 78 | point.size = size; 79 | point.timestamp = timestamp; 80 | } 81 | 82 | // Insert the new point 83 | else { 84 | map.set(new L3Point(orderId, price, size, timestamp)); 85 | } 86 | } 87 | 88 | /** 89 | * Captures a price aggregated snapshot 90 | * @param {number} depth 91 | */ 92 | public snapshot(depth = 10) { 93 | return { 94 | sequenceId: this.sequenceId, 95 | runId: this.runId, 96 | asks: this.asks.snapshot(depth, "asc"), 97 | bids: this.bids.snapshot(depth, "desc"), 98 | }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/orderbooks/LiquidOrderBook.ts: -------------------------------------------------------------------------------- 1 | import { Level2Update } from "../Level2Update"; 2 | import { L2Point } from "./L2Point"; 3 | 4 | /** 5 | * Implementation of the Liquid Order Book that pulls information 6 | * from the Liquid Web Socket feed defined here: 7 | * https://developers.liquid.com/#public-channels 8 | * 9 | * Liquid does not provide timestamps or sequence identifiers with their 10 | * order book stream. The stream acts as a psuedo snapshot stream in 11 | * that each "update" message is actually the top 40 items in one side 12 | * of the book (ask or bid). Therefore, the update processing simply 13 | * replaces one side of the book. 14 | * 15 | * # Example 16 | * 17 | * ```javascript 18 | * const client = new ccxws.liquid(); 19 | * const market = { id: "btceur", base: "BTC", quote: "EUR" }; 20 | * const ob = new LiquidOrderBook(); 21 | * 22 | * client.subscribeLevel2Updates(market); 23 | * client.on("l2update", update => { 24 | * ob.update(update); 25 | * }); 26 | * ``` 27 | */ 28 | export class LiquidOrderBook { 29 | public asks: L2Point[]; 30 | public bids: L2Point[]; 31 | 32 | constructor(asks: L2Point[] = [], bids: L2Point[] = []) { 33 | this.asks = asks; 34 | this.bids = bids; 35 | } 36 | 37 | /** 38 | * The update will contain 40 new points for either the ask or bid 39 | * side. The update replaces the appropriate side of the book with 40 | * the new values. 41 | */ 42 | public update(update: Level2Update) { 43 | const now = Date.now(); 44 | if (update.asks.length) { 45 | this.asks = update.asks.map(p => new L2Point(Number(p.price), Number(p.size), now)); 46 | } 47 | 48 | if (update.bids.length) { 49 | this.bids = update.bids.map(p => new L2Point(Number(p.price), Number(p.size), now)); 50 | } 51 | } 52 | 53 | /** 54 | * Obtains a snapshot of the best asks and bids according to requested 55 | * depth. 56 | */ 57 | public snapshot(depth: number = 10) { 58 | return { 59 | asks: this.asks.slice(0, depth), 60 | bids: this.bids.slice(0, depth), 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "removeComments": false, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "outDir": "./dist" 11 | }, 12 | "include": [ "src/**/*", "__tests__" ], 13 | "exclude": [] 14 | } 15 | --------------------------------------------------------------------------------